mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge pull request #304 from deepseek-harness/refactor/agent-leaf-dependencies
refactor: add independent stdio interface - not std-agent allinone
This commit is contained in:
@@ -608,6 +608,22 @@ export interface Config {
|
||||
|
||||
Source: [`packages/skill/skill-local/src/index.ts:39`](../packages/skill/skill-local/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-stdio`
|
||||
|
||||
Requires: `agents` · `userInteraction`
|
||||
|
||||
```ts config-catalog
|
||||
/** Serializable plugin configuration (cordis-native, schemastery). */
|
||||
export interface Config {
|
||||
/** Banner printed once on start, before the first `> ` prompt. */
|
||||
welcome?: string
|
||||
/** Id of the agent stdin drives (`send`/`steer`) and whose status gates the EOF exit; rendering is global. Defaults to `'main'`. */
|
||||
agent?: string
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/ui/stdio/src/index.ts:30`](../packages/ui/stdio/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-stdio-agent`
|
||||
|
||||
```ts config-catalog
|
||||
|
||||
@@ -7,8 +7,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
|
||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio-agent`](../packages/ui/stdio-agent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio-agent`](../packages/ui/stdio-agent) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:139`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`jsonrpc`](../packages/ui/jsonrpc), [`stdio`](../packages/ui/stdio) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:283`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
|
||||
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:202`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:212`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
|
||||
@@ -16,7 +16,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:224`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
|
||||
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:239`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:180`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio-agent`](../packages/ui/stdio-agent) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio`](../packages/ui/stdio) |
|
||||
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:250`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
|
||||
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:260`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:270`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
|
||||
@@ -27,7 +27,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:39`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`invariants`](../packages/support/invariants), [`llm-replay`](../packages/support/llm-replay) |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | - |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio-agent`](../packages/ui/stdio-agent) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:92`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:66`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
|
||||
@@ -99,6 +99,7 @@ flowchart TD
|
||||
pkg_jsonrpc["jsonrpc"]
|
||||
pkg_jsonrpc_agent["jsonrpc-agent"]
|
||||
pkg_permission["permission"]
|
||||
pkg_stdio["stdio"]
|
||||
pkg_stdio_agent["stdio-agent"]
|
||||
pkg_tool_ask_user["tool-ask-user"]
|
||||
pkg_user_approval["user-approval"]
|
||||
@@ -206,6 +207,10 @@ flowchart TD
|
||||
pkg_permission --> pkg_sandbox
|
||||
pkg_permission --> pkg_session
|
||||
pkg_permission --> pkg_user_approval
|
||||
pkg_stdio --> pkg_agent
|
||||
pkg_stdio --> pkg_llm
|
||||
pkg_stdio --> pkg_session
|
||||
pkg_stdio --> pkg_user_interaction
|
||||
pkg_agent_loop --> pkg_agent
|
||||
pkg_agent_loop --> pkg_llm
|
||||
pkg_agent_loop --> pkg_scope
|
||||
@@ -334,6 +339,7 @@ flowchart TD
|
||||
pkg_stdio_agent --> pkg_llm
|
||||
pkg_stdio_agent --> pkg_session
|
||||
pkg_stdio_agent --> pkg_session_persistence_jsonl
|
||||
pkg_stdio_agent --> pkg_stdio
|
||||
pkg_stdio_agent --> pkg_tool_ask_user
|
||||
pkg_stdio_agent --> pkg_tools
|
||||
pkg_stdio_agent --> pkg_user_interaction
|
||||
@@ -387,6 +393,7 @@ flowchart TD
|
||||
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) |
|
||||
| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) |
|
||||
| [`stdio`](../packages/ui/stdio) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`user-interaction`](../packages/ui/user-interaction) |
|
||||
| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
|
||||
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
@@ -412,4 +419,4 @@ flowchart TD
|
||||
| [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
|
||||
| [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
|
||||
| [`acp-agent`](../packages/ui/acp-agent) | `ui` | [`acp`](../packages/ui/acp), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
|
||||
| [`stdio-agent`](../packages/ui/stdio-agent) | `ui` | [`agent`](../packages/core/agent), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
|
||||
| [`stdio-agent`](../packages/ui/stdio-agent) | `ui` | [`agent`](../packages/core/agent), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`stdio`](../packages/ui/stdio), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
|
||||
|
||||
@@ -10,7 +10,7 @@ The boundary bought package metadata, workspace and tsconfig references, module-
|
||||
|
||||
## Decision
|
||||
|
||||
The `stdio-chat` module now lives inside `dsh-stdio-agent` with its runtime seam. Per-file tests cover EOF, rendering, disposal, and piped-versus-TTY behavior without replacing process globals. It retains the named Cordis plugin export shape consumed by the app; an `unwrapExports` assertion and keyless Loader smokes guard both the package and composed entry paths.
|
||||
The helper lives in `@deepseek-ai/dsh-stdio` as the terminal-channel plugin (`packages/ui/stdio/src/index.ts`): `createStdioChat`, its `StdioRuntime` test seam, and its unit tests (`packages/ui/stdio/tests/stdio.spec.ts`, `readline.spec.ts`) moved with it, so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered under the per-file coverage gate without hijacking process globals. The module keeps the named `name`/`inject`/`Config`/`apply` export shape — the contract the app's `ctx.plugin(uiStdio, …)` mount consumes — and the keyless Loader-path smokes in `examples/echo-agent` and `examples/coding-agent` keep proving the composed tree boots through the real Loader (the stdio package's plugin-shape unit suite pins the explicit `unwrapExports` assertion, since a bundle without `inject` would boot past a stray default rather than crash).
|
||||
|
||||
The `packages/support/ui-stdio` package is gone: manifest, tsconfig references, module-graph rows, and README rows deleted; the doc comments that named the package (the example e2e module docs, `packages/README.md`, the support and todo READMEs, [the ui group README](../../../../packages/ui/README.md)) describe the in-package module.
|
||||
|
||||
|
||||
@@ -15,6 +15,16 @@
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
disabled: true
|
||||
- id: sandbox
|
||||
name: '@deepseek-ai/dsh-sandbox-local'
|
||||
config:
|
||||
runnerCommand:
|
||||
- bash
|
||||
- -c
|
||||
- while [ "$1" != "--" ]; do shift; done; shift; exec "$@"
|
||||
- passthrough-runner
|
||||
runnerFailureSignatures:
|
||||
- 'passthrough-runner: profile rejected'
|
||||
- insert:
|
||||
- id: llm-replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
|
||||
@@ -90,6 +90,10 @@
|
||||
"entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts"]
|
||||
},
|
||||
"packages/ui/stdio": {
|
||||
"entry": ["tests/**/*.spec.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts"]
|
||||
},
|
||||
"packages/ui/jsonrpc-agent": {
|
||||
"project": ["src/**/*.ts"]
|
||||
},
|
||||
|
||||
@@ -9,13 +9,14 @@ Integrations that expose the agent to an external editor or client. These are **
|
||||
| `permission/` | User-facing permission presets (`workspace-write`/`danger-full-access`): one product-level select bundling the sandbox-mode and approval-policy knobs, written through to their session events | `ctx.permission` |
|
||||
| `user-interaction/` | Abstract human question/answer seam used by UI-backed confirmation tools | `ctx.userInteraction` |
|
||||
| `tool-ask-user/` | Model-facing `ask_user_question` tool over `ctx.userInteraction` | (registers on `ctx.tools`) |
|
||||
| `stdio/` | Terminal readline channel over `ctx.agents`, `session/event`, and `ctx.userInteraction`; agent lifecycle stays with app/developer code | (drives `ctx.agents`) |
|
||||
| `stdio-agent/` | Terminal stdio chat APP: the agent-core spine + console logger + readline UI + a pre-created `main` agent, with a `bin` | (composition + `bin`) |
|
||||
| `acp-agent/` | ACP server APP: the agent-core spine + JSONL persistence + the `acp` bridge (no stdout logger), with a `bin` | (composition + `bin`) |
|
||||
| `jsonrpc/` | Stdio JSON-RPC server for out-of-process SDK clients | (drives `ctx.agents`) |
|
||||
| `jsonrpc-agent/` | Bin-only SDK runtime app that boots an external `cordis.yml` | (`bin` only) |
|
||||
| `app-boot/` | Shared boot glue for the app bins: `.env` loading, fail-loud Loader guards, snapshot-aware config resolution, the settle-the-tree boot sequence | (library for the bins) |
|
||||
|
||||
A UI integration is a client-driver plugin, not a loop change or capability seam: it consumes the existing `agent/*` events and `dsh-agent` factory. `jsonrpc` is the SDK-client sibling of the `acp` editor bridge. The readline UI lives inside [`stdio-agent/`](stdio-agent/README.md) because it is scaffolding for that front door, not an independently swappable integration.
|
||||
A UI integration is a client-driver plugin, not a loop change and not a capability seam: it consumes the existing `agent/*` event taxonomy and the `dsh-agent` factory. The `jsonrpc` plugin is the SDK-client sibling of the `acp` bridge (a JSON-RPC server over `ctx.agents` for out-of-process SDK clients rather than editors). The [`stdio`](stdio/README.md) plugin is the unstructured readline analogue of the `acp` bridge; app bundles and SDK projects compose it explicitly with the services and tools their product profile selects.
|
||||
|
||||
`user-approval`, `user-interaction`, and `tool-ask-user` live here because asking a human is a UI-backed product affordance, not part of the providerless core spine. `user-approval` owns the one-shot `ctx.approval` decision mechanism and its policy tier; answerers remain with their UI channel owners. `user-interaction` remains provider-neutral (`ctx.userInteraction`), while `tool-ask-user` is its model-facing consumer and the app/bridge packages provide concrete providers.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha
|
||||
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
|
||||
| `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by confirmation tools |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | the model-facing `ask_user_question` tool |
|
||||
| `stdio-chat` (in-package module) | the readline UI, bound to the `main` agent |
|
||||
| `@deepseek-ai/dsh-stdio` | the readline UI, bound to the `main` agent |
|
||||
|
||||
`@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, NOT baked in here: it is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader`, and the in-process test tier cannot even import it (so a package whose `apply` statically pulled it in could never carry the per-file coverage gate). Unlike the console logger, a stray `hmr` is not a stdout-purity footgun, so leaving it at the leaf costs no safety. The `demo:echo` / `demo:repl` leaves load it and pass `--expose-internals`.
|
||||
|
||||
|
||||
@@ -38,9 +38,10 @@
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-agent-core": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
|
||||
"@deepseek-ai/dsh-stdio": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/dsh-user-interaction": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"schemastery": "^3.17.0"
|
||||
@@ -55,9 +56,10 @@
|
||||
"@deepseek-ai/dsh-agent-core": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-stdio": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7",
|
||||
"schemastery": "^3.17.0"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* The stdio chat app: the default agent spine ({@link @deepseek-ai/dsh-agent-core}) plus the
|
||||
* coupled front-door cluster a terminal chat needs — a console logger, the readline UI (the
|
||||
* in-package `stdio-chat` module), JSONL session persistence, and a pre-created `main` agent
|
||||
* the UI drives.
|
||||
* coupled front-door cluster a terminal chat needs — a console logger, the independently
|
||||
* packaged readline UI, JSONL session persistence, the user-interaction seam with its
|
||||
* `ask_user_question` tool, and a pre-created `main` agent the UI drives.
|
||||
* Swappable adapters, executors, optional tools, and HMR stay in the leaf. This
|
||||
* Loader plugin intentionally exposes named exports only; a default export
|
||||
* would hide its `Config` schema (see docs/postmortem/0001).
|
||||
@@ -19,7 +19,7 @@ import * as agentCore from '@deepseek-ai/dsh-agent-core'
|
||||
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user'
|
||||
import * as uiStdio from './stdio-chat.ts'
|
||||
import * as uiStdio from '@deepseek-ai/dsh-stdio'
|
||||
|
||||
export const name = 'stdio-agent'
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ const dshPackages = [
|
||||
'bash/tool-bash', 'support/invariants', 'ui/app-boot',
|
||||
'session-persistence/session-persistence',
|
||||
'session-persistence/session-persistence-jsonl', 'ui/stdio-agent',
|
||||
'ui/stdio', 'ui/tool-ask-user', 'ui/user-interaction',
|
||||
]
|
||||
const vendorPackages = [
|
||||
'cordis', 'loader', 'include', 'timer', 'hmr', 'logger-console',
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
{
|
||||
"path": "../user-interaction"
|
||||
},
|
||||
{
|
||||
"path": "../stdio"
|
||||
},
|
||||
{
|
||||
"path": "../tool-ask-user"
|
||||
},
|
||||
|
||||
42
packages/ui/stdio/README.md
Normal file
42
packages/ui/stdio/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# @deepseek-ai/dsh-stdio
|
||||
|
||||
The terminal readline front door for DeepSeek Harness agents. It reads prompts from stdin, sends or steers them through `ctx.agents`, renders the durable `session/event` transcript to stdout, and answers `ctx.userInteraction` requests in the same terminal.
|
||||
|
||||
This package owns the terminal channel only. It injects `agents` and `userInteraction`, then drives an agent created or resumed by app or developer code. The agent spine, agent lifecycle, console logger, and model-facing [`ask_user_question`](../tool-ask-user/README.md) tool remain separate composition entries.
|
||||
|
||||
## Config
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `welcome` | `ready.` | Banner printed before the first prompt |
|
||||
| `agent` | `main` | Agent id driven by stdin and observed for EOF shutdown |
|
||||
|
||||
The plugin seeds display labels from the live agent registry, then tracks `agent/created` and `agent/disposed` so HMR and externally managed agents render consistently. Disposal closes readline and unregisters every listener/provider through Cordis effects.
|
||||
|
||||
```yaml
|
||||
- id: stdio
|
||||
name: '@deepseek-ai/dsh-stdio'
|
||||
config:
|
||||
welcome: 'agent REPL ready. Give it a coding task.'
|
||||
agent: main
|
||||
```
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Readline prompt input
|
||||
|
||||
**What the model sees**: Each non-empty terminal line outside an active question becomes one text block, sent with `agent.send()` while the target agent is idle and `agent.steer()` while it is running.
|
||||
|
||||
**Token effect**: Submitted text is retained under the agent loop's normal session-history and compaction rules. The welcome banner, `> ` prompt, rendered transcript, and `[tool call]` / `[tool result]` terminal lines add no tokens.
|
||||
|
||||
### Terminal user-interaction answers
|
||||
|
||||
**What the model sees**: When a consumer calls `ctx.userInteraction.ask()`, this provider renders the question in the terminal and returns selected option labels or `custom` text. Through `dsh-tool-ask-user`, closed stdin becomes `Error: ask_user_question cannot be answered because stdin is closed`; disposal or abort becomes `Error: ask_user_question was interrupted before the user answered`.
|
||||
|
||||
**Token effect**: Waiting and terminal prompts add no tokens; the resolved answer or error is model-visible only through the calling tool or plugin's result.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **One configured agent receives stdin** — the session/event renderer can print output from any session, but input lines always drive the configured `agent` id rather than routing by the visible label.
|
||||
- **Terminal questions are text-only and sequential** — the provider queues asks, supports option labels plus custom text, and has no richer UI shapes such as file pickers or diff previews.
|
||||
- **Closed stdin ends the terminal channel** — EOF rejects active or queued questions and exits after submitted work reaches idle; there is no reconnect path for a long-lived process.
|
||||
42
packages/ui/stdio/package.json
Normal file
42
packages/ui/stdio/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-stdio",
|
||||
"description": "Terminal readline front door for driving and rendering DeepSeek Harness agents over stdio",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-user-interaction": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"schemastery": "^3.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cordisjs/plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@
|
||||
* The stdio app's readline UI: reads lines from stdin into `agent.send()` or
|
||||
* `steer()`, renders the durable event stream to stdout, and exits piped input
|
||||
* only after submitted work reaches idle.
|
||||
* @module @deepseek-ai/dsh-stdio-agent/stdio-chat
|
||||
*
|
||||
* This package is the independently composable stdio front door. It establishes
|
||||
* the terminal channel and drives an agent created or resumed by app or
|
||||
* developer code.
|
||||
* @module @deepseek-ai/dsh-stdio
|
||||
*/
|
||||
|
||||
import { createInterface } from 'node:readline'
|
||||
@@ -26,8 +30,6 @@ export const inject = ['agents', 'userInteraction']
|
||||
export interface Config {
|
||||
/** Banner printed once on start, before the first `> ` prompt. */
|
||||
welcome?: string
|
||||
// TODO(fixed-stdio-agent): this app-internal plugin is mounted only for the
|
||||
// precreated `main` agent; remove configurability and its config-only test.
|
||||
/** Id of the agent stdin drives (`send`/`steer`) and whose status gates the EOF exit; rendering is global. Defaults to `'main'`. */
|
||||
agent?: string
|
||||
}
|
||||
@@ -350,16 +352,38 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
|
||||
}, 'ui-stdio')
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the terminal channel once its configured agent exists. Generated stdio
|
||||
* projects boot the Cordis tree first and create or resume the agent from
|
||||
* developer code immediately afterward, so stdin must remain untouched until
|
||||
* the matching `agent/created` notification arrives.
|
||||
* @param ctx - the context supplying the agent registry and event stream.
|
||||
* @param config - presentation and target-agent configuration.
|
||||
* @param runtime - process-I/O seam.
|
||||
*/
|
||||
export function mountStdio(ctx: Context, config: Config, runtime: StdioRuntime): void {
|
||||
const agentId = AgentId(config.agent ?? 'main')
|
||||
if (ctx.agents.get(agentId) !== undefined) {
|
||||
createStdioChat(ctx, config, runtime)
|
||||
return
|
||||
}
|
||||
const dispose = ctx.on('agent/created', (agent) => {
|
||||
if (agent.id !== agentId) return
|
||||
dispose()
|
||||
createStdioChat(ctx, config, runtime)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Cordis entry point. Binds the real `process` streams and delegates to
|
||||
* {@link createStdioChat}; the indirection keeps the side-effecting handles out
|
||||
* {@link mountStdio}; the indirection keeps the side-effecting handles out
|
||||
* of the testable core, which is why the unit suite drives `createStdioChat`
|
||||
* directly. This thin wrapper is exercised end-to-end by the keyless
|
||||
* Loader-path e2e smoke in `examples/echo-agent` (the real product entry).
|
||||
*/
|
||||
/* v8 ignore start -- production stdio wiring; testable core is createStdioChat() (covered), exercised e2e by echo-agent keyless smoke */
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
createStdioChat(ctx, config, {
|
||||
mountStdio(ctx, config, {
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
exit: code => process.exit(code),
|
||||
19
packages/ui/stdio/tests/plugin-shape.spec.ts
Normal file
19
packages/ui/stdio/tests/plugin-shape.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import * as stdio from '../src/index.ts'
|
||||
|
||||
/** Real Loader export-path guard for the namespace stdio plugin. */
|
||||
describe('dsh-stdio plugin export shape', () => {
|
||||
it('preserves name, inject, Config, and apply through Loader unwrapping', () => {
|
||||
expect('default' in stdio).toBe(false)
|
||||
expect(typeof stdio.apply).toBe('function')
|
||||
|
||||
const loader = Object.create(Loader.prototype) as Loader
|
||||
const unwrapped = loader.unwrapExports(stdio) as Record<string, unknown>
|
||||
expect(unwrapped).toBe(stdio)
|
||||
expect(unwrapped.name).toBe('ui-stdio')
|
||||
expect(unwrapped.inject).toEqual(['agents', 'userInteraction'])
|
||||
expect(unwrapped.Config).toBeDefined()
|
||||
expect(typeof unwrapped.apply).toBe('function')
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events'
|
||||
import type { Readable, Writable } from 'node:stream'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { Context } from 'cordis'
|
||||
import type { StdioRuntime } from '../src/stdio-chat.ts'
|
||||
import type { StdioRuntime } from '../src/index.ts'
|
||||
|
||||
const createInterface = vi.hoisted(() => vi.fn(() => {
|
||||
const reader = new EventEmitter() as EventEmitter & { close(): void }
|
||||
@@ -33,7 +33,7 @@ function fakeRuntime(inputIsTTY: boolean, outputIsTTY: boolean): StdioRuntime {
|
||||
|
||||
describe('createStdioChat readline mode', () => {
|
||||
it('enables terminal editing only when both stdio streams are TTYs', async () => {
|
||||
const { createStdioChat } = await import('../src/stdio-chat.ts')
|
||||
const { createStdioChat } = await import('../src/index.ts')
|
||||
|
||||
const tty = fakeRuntime(true, true)
|
||||
createStdioChat(fakeContext(), {}, tty)
|
||||
@@ -6,7 +6,7 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import { createStdioChat, type Config, type StdioRuntime } from '../src/stdio-chat.ts'
|
||||
import { createStdioChat, mountStdio, type Config, type StdioRuntime } from '../src/index.ts'
|
||||
|
||||
/**
|
||||
* Unit tests for the stdio UI plugin. They drive the REAL plugin body
|
||||
@@ -93,6 +93,55 @@ function flushExit(): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, 250))
|
||||
}
|
||||
|
||||
describe('mountStdio readiness', () => {
|
||||
it('leaves stdin untouched until the configured agent is created', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
const { runtime, out } = makeRuntime()
|
||||
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
mountStdio(inner, CONFIG, runtime)
|
||||
}, { inject: ['agents', 'userInteraction'] }))
|
||||
|
||||
expect(out.text()).toBe('')
|
||||
ctx.agents.register(makeAgent('other'))
|
||||
expect(out.text()).toBe('')
|
||||
ctx.agents.register(makeAgent('main'))
|
||||
expect(out.text()).toBe('hi there\n> ')
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
it('opens immediately when the configured agent already exists', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
ctx.agents.register(makeAgent('main'))
|
||||
const { runtime, out } = makeRuntime()
|
||||
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
mountStdio(inner, CONFIG, runtime)
|
||||
}, { inject: ['agents', 'userInteraction'] }))
|
||||
|
||||
expect(out.text()).toBe('hi there\n> ')
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
it('waits for main when no target agent is configured', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
const { runtime, out } = makeRuntime()
|
||||
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
mountStdio(inner, { welcome: 'ready' }, runtime)
|
||||
}, { inject: ['agents', 'userInteraction'] }))
|
||||
|
||||
ctx.agents.register(makeAgent('other'))
|
||||
expect(out.text()).toBe('')
|
||||
ctx.agents.register(makeAgent('main'))
|
||||
expect(out.text()).toBe('ready\n> ')
|
||||
await fiber.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createStdioChat rendering', () => {
|
||||
it('writes the welcome banner and prompt on start', async () => {
|
||||
const { out } = await setup()
|
||||
30
packages/ui/stdio/tsconfig.json
Normal file
30
packages/ui/stdio/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/schemastery"
|
||||
},
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
{
|
||||
"path": "../user-interaction"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -1402,6 +1402,31 @@ importers:
|
||||
specifier: ^4.0.0-rc.7
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
||||
|
||||
packages/ui/stdio:
|
||||
dependencies:
|
||||
schemastery:
|
||||
specifier: ^3.18.0
|
||||
version: 3.18.0
|
||||
devDependencies:
|
||||
'@cordisjs/plugin-loader':
|
||||
specifier: workspace:^
|
||||
version: link:../../../vendor/loader
|
||||
'@deepseek-ai/dsh-agent':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/agent
|
||||
'@deepseek-ai/dsh-llm':
|
||||
specifier: workspace:^
|
||||
version: link:../../llm/llm
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/session
|
||||
'@deepseek-ai/dsh-user-interaction':
|
||||
specifier: workspace:^
|
||||
version: link:../user-interaction
|
||||
cordis:
|
||||
specifier: ^4.0.0-rc.6
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader)
|
||||
|
||||
packages/ui/stdio-agent:
|
||||
devDependencies:
|
||||
'@cordisjs/plugin-include':
|
||||
@@ -1431,6 +1456,9 @@ importers:
|
||||
'@deepseek-ai/dsh-session-persistence-jsonl':
|
||||
specifier: workspace:^
|
||||
version: link:../../session-persistence/session-persistence-jsonl
|
||||
'@deepseek-ai/dsh-stdio':
|
||||
specifier: workspace:^
|
||||
version: link:../stdio
|
||||
'@deepseek-ai/dsh-system-prompt':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/system-prompt
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
{ "path": "./packages/ui/app-boot" },
|
||||
{ "path": "./packages/ui/jsonrpc" },
|
||||
{ "path": "./packages/ui/jsonrpc-agent" },
|
||||
{ "path": "./packages/ui/stdio" },
|
||||
{ "path": "./packages/ui/stdio-agent" },
|
||||
{ "path": "./packages/support/llm-replay" },
|
||||
{ "path": "./packages/support/acp-snapshot" },
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
{ "path": "./packages/ui/app-boot" },
|
||||
{ "path": "./packages/ui/jsonrpc" },
|
||||
{ "path": "./packages/ui/jsonrpc-agent" },
|
||||
{ "path": "./packages/ui/stdio" },
|
||||
{ "path": "./packages/ui/stdio-agent" },
|
||||
{ "path": "./packages/support/llm-replay" },
|
||||
{ "path": "./packages/support/acp-snapshot" },
|
||||
|
||||
Reference in New Issue
Block a user