From 205f7cd04d6c53aee2846e113e90e2dbf6ec1c98 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:38:03 +0800 Subject: [PATCH] refactor(ui): fold the stdio UI helper into the stdio app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The readline UI lives inside @deepseek-ai/dsh-stdio-agent as the in-package stdio-chat module; the packages/support/ui-stdio package is gone. The app's front-door cluster always includes this UI and nothing else composes it, so the boundary bought manifest/tsconfig/module-graph/ README/publint surface for a helper that is not independently swappable — and a product app no longer depends on a support package documented as not-product-surface. createStdioChat, the StdioRuntime test seam, and both unit suites moved verbatim (imports rewired to the module path); the named name/inject/Config/apply export shape stays, being the contract the app's ctx.plugin mount consumes. Coverage stays per-file 100%; the built-bin smoke under plain node and both keyless Loader-path smokes prove the published artifact and the demos end-to-end. Implements docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md (moved from proposed/ and amended to the shipped shape). --- AGENTS.md | 2 - docs/module-graph.md | 7 +-- docs/rfc/README.md | 2 +- .../2026-07-04-fold-stdio-ui-helper.md | 24 ++++++++++ .../2026-07-04-fold-stdio-ui-helper.md | 27 ------------ .../coding-agent/tests/keyless-smoke.e2e.ts | 4 +- examples/echo-agent/tests/echo.e2e.ts | 2 +- packages/README.md | 4 +- packages/support/README.md | 3 +- packages/support/ui-stdio/README.md | 44 ------------------- packages/support/ui-stdio/package.json | 39 ---------------- packages/support/ui-stdio/tsconfig.json | 30 ------------- packages/todo/README.md | 2 +- packages/todo/tool-todo/README.md | 2 +- packages/ui/README.md | 2 +- packages/ui/stdio-agent/README.md | 2 +- packages/ui/stdio-agent/package.json | 2 - packages/ui/stdio-agent/src/index.ts | 9 ++-- .../stdio-agent/src/stdio-chat.ts} | 29 +++++------- .../ui/stdio-agent/tests/built-bin.e2e.ts | 2 +- .../stdio-agent}/tests/readline.spec.ts | 4 +- .../stdio-agent/tests/stdio-chat.spec.ts} | 2 +- packages/ui/stdio-agent/tsconfig.json | 3 -- pnpm-lock.yaml | 22 ---------- tsconfig.build.json | 1 - tsconfig.json | 1 - 26 files changed, 56 insertions(+), 215 deletions(-) create mode 100644 docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md delete mode 100644 docs/rfc/proposed/simplification/2026-07-04-fold-stdio-ui-helper.md delete mode 100644 packages/support/ui-stdio/README.md delete mode 100644 packages/support/ui-stdio/package.json delete mode 100644 packages/support/ui-stdio/tsconfig.json rename packages/{support/ui-stdio/src/index.ts => ui/stdio-agent/src/stdio-chat.ts} (88%) rename packages/{support/ui-stdio => ui/stdio-agent}/tests/readline.spec.ts (92%) rename packages/{support/ui-stdio/tests/ui-stdio.spec.ts => ui/stdio-agent/tests/stdio-chat.spec.ts} (99%) diff --git a/AGENTS.md b/AGENTS.md index c5e7f5c701..004fef4611 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,8 +114,6 @@ packages/ Harness packages, grouped by role at packages///. acp bridge, NO stdout logger + a bin (the demo:acp front door) support/ dev/test/example infrastructure (lower compat expectations) invariants/ dev-mode event-contract invariants + session-log freeze - ui-stdio/ minimal stdio (readline) UI plugin: renders agent/* events, - feeds stdin lines to the agent (shared by the demos) llm-replay/ record/replay adapter: short-circuits llm/stream from a recorded session JSONL (keyless snapshot tests) subagent-mock/ scripted SubagentProvider for deterministic seam/tool tests diff --git a/docs/module-graph.md b/docs/module-graph.md index 452639a90f..c2c2c97bf7 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -48,9 +48,6 @@ graph TD tools --> agent tools --> llm tools --> system-prompt - ui-stdio --> agent - ui-stdio --> llm - ui-stdio --> session acp --> agent acp --> llm acp --> session @@ -121,7 +118,6 @@ graph TD stdio-agent --> agent-core stdio-agent --> session stdio-agent --> session-persistence-jsonl - stdio-agent --> ui-stdio subagent-fork --> agent subagent-fork --> session subagent-fork --> subagent @@ -158,7 +154,6 @@ graph TD | `session-persistence-jsonl` | `session`, `session-persistence` | | `session-persistence-sqlite` | `session`, `session-persistence` | | `tools` | `agent`, `llm`, `system-prompt` | -| `ui-stdio` | `agent`, `llm`, `session` | | `acp` | `agent`, `llm`, `session`, `session-persistence`, `tools` | | `agent-loop` | `agent`, `llm`, `session`, `session-persistence`, `system-prompt`, `tools` | | `hooks-codex` | `agent`, `hook-protocol`, `llm`, `session`, `tools` | @@ -174,6 +169,6 @@ graph TD | `subagent-mock` | `agent`, `llm`, `subagent` | | `tool-subagent` | `agent`, `llm`, `subagent`, `tools` | | `acp-agent` | `acp`, `agent-core`, `session-persistence-jsonl` | -| `stdio-agent` | `agent`, `agent-core`, `session`, `session-persistence-jsonl`, `ui-stdio` | +| `stdio-agent` | `agent`, `agent-core`, `session`, `session-persistence-jsonl` | | `subagent-fork` | `agent`, `session`, `subagent`, `subagent-inprocess` | | `subagent-spawn` | `subagent`, `subagent-inprocess` | diff --git a/docs/rfc/README.md b/docs/rfc/README.md index d53684bf6b..9a4aff345e 100644 --- a/docs/rfc/README.md +++ b/docs/rfc/README.md @@ -55,7 +55,6 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | [Drop the `image` content block until a path can honor it](proposed/simplification/2026-07-04-drop-image-content-block.md) | 2026-07-04 | | [Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path](proposed/simplification/2026-07-04-drop-inert-request-knobs.md) | 2026-07-04 | | [Drop the unconsumed web observation surface — the `providers-change` event and the status methods](proposed/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) | 2026-07-04 | -| [Fold the stdio UI helper into the stdio app](proposed/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 | | [Prune dead core-spine surface — `SurfaceManager.invalidate()`, the loop-internal exports, `ToolExecutionResult.callId`](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 | | [Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)](proposed/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) | 2026-07-04 | | [Prune write-only fields and a dead routing knob from the fs seam](proposed/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 | @@ -120,6 +119,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | [Stop mirroring durable boundaries as agent events](implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md) | 2026-06-20 | | [Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin](implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) | 2026-06-26 | | [Stop mirroring the token stream as an agent event](implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md) | 2026-07-02 | +| [Fold the stdio UI helper into the stdio app](implemented/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 | ### Architecture diff --git a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md b/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md new file mode 100644 index 0000000000..292e2dc541 --- /dev/null +++ b/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md @@ -0,0 +1,24 @@ +# RFC: Fold the stdio UI helper into the stdio app + +Status: implemented + +## Problem + +The readline UI was a whole package (`@deepseek-ai/dsh-ui-stdio` under `packages/support/`) whose only runtime importer was the app package `@deepseek-ai/dsh-stdio-agent`. The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference was mechanical or descriptive surface that existed BECAUSE the package boundary existed — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. The ui group README recorded the support placement rationale ("exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product"), which left a standing tension: a shipped product app depending on a support package documented as NOT product surface. + +The boundary bought package metadata, workspace and tsconfig references, module-graph rows, README entries, and publint surface for a helper that is not independently swappable: the stdio app's front-door cluster always includes the readline UI, and nothing else can meaningfully consume it. + +## Decision + +The helper lives inside `@deepseek-ai/dsh-stdio-agent` as the in-package `stdio-chat` module (`packages/ui/stdio-agent/src/stdio-chat.ts`): `createStdioChat`, its `StdioRuntime` test seam, and its unit tests (`packages/ui/stdio-agent/tests/stdio-chat.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 guarding the app's export shape end-to-end. + +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. + +## Why not promote it to `ui/` instead? + +Promotion would have resolved the support-vs-product mismatch while keeping the boundary — the right call only if the readline UI were an independently swappable integration or had a second composer, and the consumer census said neither. The structured ACP bridge stays its own package because it is the product protocol surface with its own contract and snapshot tiers; the readline helper is scaffolding for one app's front door. Re-extraction stays cheap pre-release: if a second product app wants the readline UI, split it back out then, with that consumer shaping the package contract. + +## Consequences + +- The stdio app owns its whole front door; a leaf `cordis.yml` still loads one app package and nothing changed shape for the demos. +- A future standalone terminal UI that wants the helper as a package reintroduces it with that second consumer, rather than the repo keeping a boundary for hypothetical reuse. diff --git a/docs/rfc/proposed/simplification/2026-07-04-fold-stdio-ui-helper.md b/docs/rfc/proposed/simplification/2026-07-04-fold-stdio-ui-helper.md deleted file mode 100644 index aba2faf4ee..0000000000 --- a/docs/rfc/proposed/simplification/2026-07-04-fold-stdio-ui-helper.md +++ /dev/null @@ -1,27 +0,0 @@ -# RFC: Fold the stdio UI helper into the stdio app - -Status: proposed - -## Problem - -`@deepseek-ai/dsh-ui-stdio` is a whole package whose only runtime importer is the app package `@deepseek-ai/dsh-stdio-agent` (`packages/ui/stdio-agent/src/index.ts`). The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference is mechanical or descriptive surface that exists BECAUSE the package boundary exists — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. [The ui group README](../../../../packages/ui/README.md) records the placement rationale — the helper "exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product" — which leaves a standing tension: a shipped product app depends on a support package documented as NOT product surface. - -The boundary buys package metadata, workspace and tsconfig references, module-graph rows, README entries, and publint surface for a helper that is not independently swappable: the stdio app's front-door cluster always includes the readline UI, and nothing else can meaningfully consume it. - -## Proposal - -Fold the helper into `@deepseek-ai/dsh-stdio-agent`: move `createStdioChat`, its `StdioRuntime` test seam, and its unit tests into `packages/ui/stdio-agent`; delete the `packages/support/ui-stdio` package with its manifest, references, module-graph rows, and README rows; update every reference that names the package (the example e2e module docs, `packages/README.md`, the support and todo README rows, the stdio-agent README, the ui group README, tsconfig references, the generated module graph). Keep the runtime seam so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered without hijacking process globals; the keyless Loader-path smokes keep guarding the export shape end-to-end. - -## Why not promote it to `ui/` instead? - -Promotion would resolve the support-vs-product mismatch while keeping the boundary — the right call only if the readline UI were an independently swappable integration or had a second composer, and the consumer census says neither. The structured ACP bridge stays its own package because it is the product protocol surface with its own contract and snapshot tiers; the readline helper is scaffolding for one app's front door. Re-extraction stays cheap pre-release: if a second product app wants the readline UI, split it back out then, with that consumer shaping the package contract. - -## Acceptance criteria - -- `packages/support/ui-stdio` no longer exists; the helper and its tests live in `packages/ui/stdio-agent`; no reference to the deleted package remains outside RFC history. -- The stdio app still renders transcript events, handles stdin lines and EOF, renders todo checklists, and disposes readline listeners under HMR; the echo/coding keyless smokes still boot through the real Loader path and guard the export shape. -- Manifests, tsconfig references, the generated module graph, and docs are updated; `pnpm run test:coverage`, `pnpm run test:snapshot`, `pnpm run doc-sync`, `pnpm run build`, and `pnpm run hygiene` pass. - -## Risks - -A future standalone terminal UI may want the helper as a package again — reintroduce it with that second consumer rather than keeping the boundary for hypothetical reuse. Moving tests risks blurring app-composition tests with UI-rendering tests; keeping the runtime seam and the colocated unit tests avoids that. diff --git a/examples/coding-agent/tests/keyless-smoke.e2e.ts b/examples/coding-agent/tests/keyless-smoke.e2e.ts index 8448d09dda..e079aa014b 100644 --- a/examples/coding-agent/tests/keyless-smoke.e2e.ts +++ b/examples/coding-agent/tests/keyless-smoke.e2e.ts @@ -9,8 +9,8 @@ import { afterEach, describe, expect, it } from 'vitest' * Keyless Loader-path smoke for examples/coding-agent: boot the REAL example * through the `@deepseek-ai/dsh-stdio-agent` bin against its `cordis.yml` (the * cordis Loader, `unwrapExports`, the full plugin tree incl. the - * `@deepseek-ai/dsh-agent-core` bundle and the extracted - * `@deepseek-ai/dsh-ui-stdio`), then close stdin with no prompt and assert the + * `@deepseek-ai/dsh-agent-core` bundle and the app's in-package readline UI + * module), then close stdin with no prompt and assert the * ready banner + a clean exit. * * No prompt is ever sent, so the model is NEVER called — this is why it runs diff --git a/examples/echo-agent/tests/echo.e2e.ts b/examples/echo-agent/tests/echo.e2e.ts index 944a36e429..ec02e58f5b 100644 --- a/examples/echo-agent/tests/echo.e2e.ts +++ b/examples/echo-agent/tests/echo.e2e.ts @@ -13,7 +13,7 @@ import { afterEach, describe, expect, it } from 'vitest' * * This is the guard the per-file unit suite structurally cannot be: it drives * the `@deepseek-ai/dsh-stdio-agent` app plugin, the `@deepseek-ai/dsh-agent-core` - * bundle it loads, the extracted `@deepseek-ai/dsh-ui-stdio` plugin, AND the + * bundle it loads, the app's in-package readline UI module, AND the * example-local `mock-llm.ts` / `echo-tool.ts` through their REAL load path, so * a broken plugin export shape (a stray `export default` that `unwrapExports` * would collapse, dropping `inject`/`Config`) fails here even though hand-mounted diff --git a/packages/README.md b/packages/README.md index 2233123772..da17075d36 100644 --- a/packages/README.md +++ b/packages/README.md @@ -53,7 +53,6 @@ dsh-llm-pi-ai ← dsh-llm (pi-ai-backed adapter) dsh-agent-loop ← dsh-llm, dsh-session, dsh-session-persistence, dsh-system-prompt, dsh-tools, dsh-agent dsh-invariants ← dsh-llm, dsh-session, dsh-agent (dev-mode contract checks) dsh-acp ← dsh-agent, dsh-llm, dsh-session, dsh-session-persistence, dsh-tools (ACP JSON-RPC bridge) -dsh-ui-stdio ← dsh-agent, dsh-llm, dsh-session (stdio readline UI plugin) dsh-llm-replay ← dsh-llm, dsh-session (record/replay adapter for keyless snapshot tests) dsh-subagent ← dsh-agent, dsh-llm, dsh-tools (abstract subagent provider-registry seam) dsh-subagent-inprocess ← dsh-subagent, dsh-agent, dsh-session, dsh-llm (shared in-process run driver) @@ -64,7 +63,7 @@ dsh-subagent-acp ← dsh-subagent, dsh-agent, dsh-llm, @agentclientprotocol/sdk dsh-tool-subagent ← dsh-subagent, dsh-tools, dsh-agent, dsh-llm (model-facing delegation tool) dsh-tool-todo ← dsh-tools, dsh-agent, dsh-session (model-facing todo_write tool; whole list on the session log) dsh-agent-core ← timer, dsh-llm, dsh-session, dsh-system-prompt, dsh-tools, dsh-agent, dsh-invariants, dsh-tool-bash, dsh-agent-loop (the providerless spine, as one bundle plugin) -dsh-stdio-agent ← dsh-agent-core, dsh-ui-stdio, dsh-session-persistence-jsonl, dsh-agent, dsh-session (stdio chat APP + bin) +dsh-stdio-agent ← dsh-agent-core, dsh-session-persistence-jsonl, dsh-agent, dsh-session (stdio chat APP + readline UI + bin) dsh-acp-agent ← dsh-agent-core, dsh-acp, dsh-session-persistence-jsonl (ACP server APP + bin) ``` @@ -105,7 +104,6 @@ The rule: **extension** plugins depend on interfaces, never on the concrete loop | `acp/` | `ui` | Agent Client Protocol bridge: serves the agent to an ACP editor over JSON-RPC stdio | (drives `ctx.agents`/`ctx.sessions`) | | `stdio-agent/` | `ui` | Terminal stdio chat APP: agent-core spine + console logger + readline UI + a pre-created `main` agent, with a `bin` | (composition + `bin`) | | `acp-agent/` | `ui` | ACP server APP: agent-core spine + JSONL persistence + the `acp` bridge (no stdout logger), with a `bin` | (composition + `bin`) | -| `ui-stdio/` | `support` | Minimal stdio (readline) UI plugin: renders `agent/*` events, feeds stdin lines to the agent | (drives `ctx.agents`) | | `llm-replay/` | `support` | Record/replay adapter: short-circuits `llm/stream` with chunks from a recorded session JSONL (keyless snapshot tests) | (listens on `llm/stream`) | | `subagent/` | `subagent` | Abstract subagent seam: named-provider registry for delegating to child agents | `ctx.subagents` | | `subagent-inprocess/` | `subagent` | Shared in-process subagent run driver used by spawn/fork; pure library, registers nothing | (none) | diff --git a/packages/support/README.md b/packages/support/README.md index 942734fc1e..233a32d77f 100644 --- a/packages/support/README.md +++ b/packages/support/README.md @@ -5,8 +5,7 @@ Packages that exist to serve development, testing, and the examples rather than | Package | Role | ctx key | |---|---|---| | `invariants/` | Dev-mode event-contract invariants + session-log freeze | (listens on `session/*`, `agent/*`) | -| `ui-stdio/` | Minimal stdio (readline) UI plugin: renders `agent/*` events, feeds stdin lines to the agent | (drives `ctx.agents`) | | `llm-replay/` | Record/replay adapter: short-circuits `llm/stream` from a recorded session JSONL (keyless snapshot tests) | (listens on `llm/stream`) | | `subagent-mock/` | Scripted `SubagentProvider` for deterministic seam/tool tests | (registers on `ctx.subagents`) | -`invariants` runs only in dev mode (contract checks, not runtime behavior). `ui-stdio` and `llm-replay` were extracted from the examples for reuse and to bring them under the per-file coverage gate; they back the demos and the snapshot test tier. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers. +`invariants` runs only in dev mode (contract checks, not runtime behavior). `llm-replay` backs the demos and the snapshot test tier under the per-file coverage gate. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers. diff --git a/packages/support/ui-stdio/README.md b/packages/support/ui-stdio/README.md deleted file mode 100644 index 922202695d..0000000000 --- a/packages/support/ui-stdio/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# @deepseek-ai/dsh-ui-stdio - -A minimal stdio (readline) UI, as a plugin. It reads lines from stdin and feeds them to an agent (`send` when idle, `steer` while a turn is running), and renders that agent's streamed output and tool activity to stdout. A UI is "just a plugin" here — it consumes the `session/event` transcript feed plus a few `agent/*` control events (`agent/status`, `agent/created`/`agent/disposed`) and the `agents` service (`inject: ['agents']`), so the same plugin drives any example or product surface. - -This is a **convenience REPL for local testing and the demos, not a product surface** — its observable behavior is free to change. It is deliberately NOT treated as a load-bearing consumer when weighing whether a live event/API must exist: the boundary mirror events were removed precisely because "ui-stdio renders from them" is not a product constraint (it was migrated to `session/event`). The real product surfaces are the ACP bridge (`dsh-acp`) and the app packages. - -This package consolidates what were two near-identical copies under `examples/echo-agent` and `examples/coding-agent`. The coding copy was a superset; this package IS that superset — dimmed chain-of-thought rendering plus robust piped-stdin EOF handling — with the per-consumer differences moved into `Config`. - -## Config - -| Key | Type | Default | Notes | -|---|---|---|---| -| `welcome` | string | `'ready.'` | Banner printed once on start, before the first `> ` prompt. | -| `agent` | string | `'main'` | Id of the agent that stdin **drives** (`send`/`steer`) and whose `agent/status` gates the EOF exit. Rendering is **not** scoped by it — see below. | - -```yaml -- id: ui-stdio - name: '@deepseek-ai/dsh-ui-stdio' - config: - welcome: 'agent REPL ready. Give it a coding task.' -``` - -## Rendering - -Rendering is **global** — every agent's events are written to stdout, not just `config.agent`'s. `config.agent` scopes only *input* (which agent stdin drives) and the EOF-exit gate; the single-agent demos this serves have just one agent, so the distinction is moot for them. (A multi-agent UI that needs per-agent panes would filter these handlers by the agent argument — deliberately out of scope here.) - -- `session/event` — the durable transcript feed drives ALL rendering, from a single listener so `inReasoning` transitions stay deterministic in append order: `assistant/chunk` writes the model's `text-delta` verbatim and wraps `reasoning-delta` in the dim SGR (`\x1B[2m … \x1B[0m`) so the chain-of-thought is visually subordinate to the answer (inert when no `reasoning-delta` chunks arrive, e.g. a mock model); `turn/start` prints a `[ turn N]` header (the short agent label comes from a session-id→agent-id map seeded from `ctx.agents.list()` at install and kept live via `agent/created`/`agent/disposed`, since the turn event carries only the turn number); `turn/end` prints the trailing `> ` prompt; `tool/call` renders `[tool call] name(args)`; `tool/result` renders the joined text blocks as `[tool result] …`; and `todo/write` renders a glyphed checklist. - -## The I/O seam - -The production entry point `apply(ctx, config)` binds the real `process` streams. The testable core is `createStdioChat(ctx, config, runtime)`, where `runtime: StdioRuntime` supplies `input` / `output` / `exit`. This seam is deliberately **not** part of the serializable `Config` (streams and functions do not belong in YAML config); it exists so the render, EOF, and disposal branches can be exercised with fakes instead of hijacking globals. - -## Piped-stdin exit - -On stdin EOF the plugin exits the process, but carefully: - -- **No work submitted** (empty stdin, blank-only lines): exit immediately — no turn will ever start, so there is nothing to wait for. Gating on an observed `running` here would hang forever. -- **Work submitted**: exit the next time the agent settles to `idle` *after* having been observed `running`. `agent.send()` does not synchronously flip status to `running`, so requiring an observed `running` first (`sawRunning`) avoids exiting in the gap before the turn starts and dropping work; and the loop batches several queued messages into one turn, so the exit keys off the idle transition rather than counting sends. - -Disposal (HMR or fiber teardown) closes the readline interface, which also fires `close` — a `disposed` guard ensures teardown never calls `process.exit`. - -## Plugin export shape - -Named `name` / `inject` / `Config` / `apply`, with **no default export**: the cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray default would collapse the module to the bare function and drop the `inject` namespace (see [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)). The keyless Loader-path e2e smokes in `examples/{echo,coding}-agent` guard this end-to-end. diff --git a/packages/support/ui-stdio/package.json b/packages/support/ui-stdio/package.json deleted file mode 100644 index 5d65356e79..0000000000 --- a/packages/support/ui-stdio/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@deepseek-ai/dsh-ui-stdio", - "description": "Minimal stdio (readline) UI plugin: renders agent/* events to stdout and feeds stdin lines to the agent", - "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", - "cordis": "^4.0.0-rc.6" - }, - "dependencies": { - "schemastery": "^3.18.0" - }, - "devDependencies": { - "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-llm": "workspace:^", - "@deepseek-ai/dsh-session": "workspace:^", - "cordis": "^4.0.0-rc.6" - } -} diff --git a/packages/support/ui-stdio/tsconfig.json b/packages/support/ui-stdio/tsconfig.json deleted file mode 100644 index b333de0302..0000000000 --- a/packages/support/ui-stdio/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types" - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../../../vendor/cosmokit" - }, - { - "path": "../../../vendor/cordis" - }, - { - "path": "../../../vendor/schemastery" - }, - { - "path": "../../core/agent" - }, - { - "path": "../../llm/llm" - }, - { - "path": "../../core/session" - } - ] -} diff --git a/packages/todo/README.md b/packages/todo/README.md index df258fab0c..3f427295c1 100644 --- a/packages/todo/README.md +++ b/packages/todo/README.md @@ -6,4 +6,4 @@ The model-facing todo tool. A single **product** package — there is no interfa |---|---|---| | `tool-todo/` | Model-facing `todo_write` tool; writes the whole list to the session log (`todo/write`) | (registers on `ctx.tools`) | -The list lives on the event-sourced session log (`SessionEventMap['todo/write']`, owned by [`dsh-session`](../core/session)); this package is the thin consumer that appends the snapshot. UIs render off `session/event`: the [stdio UI](../support/ui-stdio) prints the list, the [ACP bridge](../ui/acp) maps it to a `plan` sessionUpdate. +The list lives on the event-sourced session log (`SessionEventMap['todo/write']`, owned by [`dsh-session`](../core/session)); this package is the thin consumer that appends the snapshot. UIs render off `session/event`: the [stdio app's readline UI](../ui/stdio-agent) prints the list, the [ACP bridge](../ui/acp) maps it to a `plan` sessionUpdate. diff --git a/packages/todo/tool-todo/README.md b/packages/todo/tool-todo/README.md index fc6dc94860..b27cc65227 100644 --- a/packages/todo/tool-todo/README.md +++ b/packages/todo/tool-todo/README.md @@ -18,7 +18,7 @@ Beyond the schema's type/required/enum checks, `execute` rejects an empty or dup ## Rendering -The tool writes only the session event; it does not render. UIs subscribe to `session/event` and render the `todo/write` data themselves: the [stdio UI](../../support/ui-stdio) prints a glyphed checklist, and the [ACP bridge](../../ui/acp) maps the list to a `plan` sessionUpdate (synthesizing the `priority` ACP requires). +The tool writes only the session event; it does not render. UIs subscribe to `session/event` and render the `todo/write` data themselves: the [stdio app's readline UI](../../ui/stdio-agent) prints a glyphed checklist, and the [ACP bridge](../../ui/acp) maps the list to a `plan` sessionUpdate (synthesizing the `priority` ACP requires). ## Export shape diff --git a/packages/ui/README.md b/packages/ui/README.md index 659519407c..1a3c45727d 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -8,6 +8,6 @@ Integrations that expose the agent to an external editor or client. These are ** | `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`) | -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 readline `ui-stdio` plugin is the unstructured analogue but lives in `support/` because it exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product. +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 readline UI is the unstructured analogue of the `acp` bridge and lives INSIDE the stdio app (the `stdio-chat` module of [`stdio-agent/`](stdio-agent/README.md)): it is scaffolding for that one front door, not an independently swappable integration, so it carries no package boundary of its own. `stdio-agent` and `acp-agent` are the two **app packages**: each composes the [`core/agent-core`](../core/agent-core/README.md) spine with its coupled front-door cluster (and owns the boot `bin`), so a leaf `cordis.yml` is the swappable backends plus one app entry plus any optional product tools. They live in `ui/` because each IS a user-facing front door; the stdout-purity coupling (logger vs. no logger) becomes a property of the artifact rather than a leaf convention. diff --git a/packages/ui/stdio-agent/README.md b/packages/ui/stdio-agent/README.md index 550b52c1ea..c75fee966a 100644 --- a/packages/ui/stdio-agent/README.md +++ b/packages/ui/stdio-agent/README.md @@ -13,7 +13,7 @@ A terminal chat always wants the same cluster, so the package owns it rather tha | `@cordisjs/plugin-logger-console` | the console logger — stdout is just the terminal here, so logging to it is correct (the ACP app must NOT have this) | | `@deepseek-ai/dsh-agent-core` | the spine, pre-creating a `main` agent from this app's `model`/`systemPrompt` | | `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` | -| `@deepseek-ai/dsh-ui-stdio` | the readline UI, bound to the `main` agent | +| `stdio-chat` (in-package module) | 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`. diff --git a/packages/ui/stdio-agent/package.json b/packages/ui/stdio-agent/package.json index bc9c98a411..f6db23a232 100644 --- a/packages/ui/stdio-agent/package.json +++ b/packages/ui/stdio-agent/package.json @@ -37,7 +37,6 @@ "@deepseek-ai/dsh-agent-core": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1", - "@deepseek-ai/dsh-ui-stdio": "^0.0.1", "cordis": "^4.0.0-rc.6", "schemastery": "^3.17.0" }, @@ -49,7 +48,6 @@ "@deepseek-ai/dsh-agent-core": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", - "@deepseek-ai/dsh-ui-stdio": "workspace:^", "cordis": "^4.0.0-rc.6", "schemastery": "^3.17.0" } diff --git a/packages/ui/stdio-agent/src/index.ts b/packages/ui/stdio-agent/src/index.ts index df42b3115b..6a854dfe29 100644 --- a/packages/ui/stdio-agent/src/index.ts +++ b/packages/ui/stdio-agent/src/index.ts @@ -1,12 +1,13 @@ /** * The stdio chat app: the providerless agent spine ({@link * @deepseek-ai/dsh-agent-core}) plus the coupled front-door cluster a terminal - * chat needs — a console logger, the readline `ui-stdio` UI, JSONL session + * 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. * * The cluster is BAKED IN, not left to the leaf: a stdio app always logs to the * console (stdout is just the terminal) and always pre-creates the `main` agent - * `ui-stdio` sends to. The leaf supplies the swappable backends (the LLM + * the readline UI sends to. The leaf supplies the swappable backends (the LLM * adapter, the bash executor), optional product tools, the optional `hmr` * dev-reload plugin, and this app's {@link Config} (model, prompt, persistence * root, welcome banner). @@ -42,7 +43,7 @@ import { AgentId } from '@deepseek-ai/dsh-agent' import { SessionId } from '@deepseek-ai/dsh-session' import * as agentCore from '@deepseek-ai/dsh-agent-core' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' -import * as uiStdio from '@deepseek-ai/dsh-ui-stdio' +import * as uiStdio from './stdio-chat.ts' export const name = 'stdio-agent' @@ -81,7 +82,7 @@ export const Config: z = z.object({ * Compose the spine with the stdio front door. The console logger comes first * (infra), then the agent-core bundle pre-creating the `main` agent from this * app's `model`/`systemPrompt`/`resumeSessionId`, then the JSONL backend, then - * the `ui-stdio` UI bound to `main`. The `hmr` dev-reload plugin is a leaf + * the readline UI bound to `main`. The `hmr` dev-reload plugin is a leaf * concern (see the module doc), so it is not mounted here. */ export function apply(ctx: Context, config: Config): void { diff --git a/packages/support/ui-stdio/src/index.ts b/packages/ui/stdio-agent/src/stdio-chat.ts similarity index 88% rename from packages/support/ui-stdio/src/index.ts rename to packages/ui/stdio-agent/src/stdio-chat.ts index 8bee2e4baa..2996e11e89 100644 --- a/packages/support/ui-stdio/src/index.ts +++ b/packages/ui/stdio-agent/src/stdio-chat.ts @@ -1,23 +1,18 @@ /** - * Minimal stdio UI plugin: reads lines from stdin → `agent.send()`/`steer()`, - * and renders the durable transcript to stdout. A UI is "just a plugin" — it - * consumes the `session/event` feed (the assistant token stream, turn/step - * boundaries, tool activity, todos) plus a few `agent/*` control events - * (`agent/status`, `agent/created`/`agent/disposed`) and the `agents` service, - * so the same plugin drives any example or product surface. + * The stdio app's readline UI: reads lines from stdin → `agent.send()`/ + * `steer()`, and renders the durable transcript to stdout. A UI is "just a + * plugin" — it consumes the `session/event` feed (the assistant token stream, + * turn/step boundaries, tool activity, todos) plus a few `agent/*` control + * events (`agent/status`, `agent/created`/`agent/disposed`) and the `agents` + * service. Dimmed chain-of-thought rendering plus robust piped-stdin EOF→idle + * exit handling, configured via {@link Config}. * - * Consolidates what were two near-identical copies under `examples/echo-agent` - * and `examples/coding-agent` (the latter a superset). This package IS that - * superset: dimmed chain-of-thought rendering plus the robust piped-stdin - * EOF→idle exit handling, configured per consumer via {@link Config}. + * An internal module of the stdio app, not a package of its own: the app's + * front-door cluster always includes this UI, and nothing else composes it. + * The export shape stays named `name`/`inject`/`Config`/`apply` — the plugin + * contract the app's `ctx.plugin(uiStdio, …)` mount consumes. * - * Plugin export shape: named `name`/`inject`/`Config`/`apply`, NO default - * export — the cordis Loader's `unwrapExports` does `exports.default ?? exports`, - * so a stray default would collapse the module to the bare function and drop - * the `inject` namespace (see docs/postmortem/0001). The keyless Loader-path - * e2e smokes in `examples/{echo,coding}-agent` guard this end-to-end. - * - * @module @deepseek-ai/dsh-ui-stdio + * @module @deepseek-ai/dsh-stdio-agent/stdio-chat */ import { createInterface } from 'node:readline' diff --git a/packages/ui/stdio-agent/tests/built-bin.e2e.ts b/packages/ui/stdio-agent/tests/built-bin.e2e.ts index 7605b35bb7..30bc952673 100644 --- a/packages/ui/stdio-agent/tests/built-bin.e2e.ts +++ b/packages/ui/stdio-agent/tests/built-bin.e2e.ts @@ -35,7 +35,7 @@ const stdioBin = join(repoRoot, 'packages/ui/stdio-agent/lib/bin.js') const dshPackages = [ 'core/agent-core', 'core/agent', 'core/session', 'core/system-prompt', 'core/tools', 'core/agent-loop', 'llm/llm', 'bash/bash', 'bash/bash-local', - 'bash/tool-bash', 'support/invariants', 'support/ui-stdio', + 'bash/tool-bash', 'support/invariants', 'session-persistence/session-persistence', 'session-persistence/session-persistence-jsonl', 'ui/stdio-agent', ] diff --git a/packages/support/ui-stdio/tests/readline.spec.ts b/packages/ui/stdio-agent/tests/readline.spec.ts similarity index 92% rename from packages/support/ui-stdio/tests/readline.spec.ts rename to packages/ui/stdio-agent/tests/readline.spec.ts index 5e092fb913..ae3c480756 100644 --- a/packages/support/ui-stdio/tests/readline.spec.ts +++ b/packages/ui/stdio-agent/tests/readline.spec.ts @@ -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/index.ts' +import type { StdioRuntime } from '../src/stdio-chat.ts' const createInterface = vi.hoisted(() => vi.fn(() => { const reader = new EventEmitter() as EventEmitter & { close(): void } @@ -32,7 +32,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/index.ts') + const { createStdioChat } = await import('../src/stdio-chat.ts') const tty = fakeRuntime(true, true) createStdioChat(fakeContext(), {}, tty) diff --git a/packages/support/ui-stdio/tests/ui-stdio.spec.ts b/packages/ui/stdio-agent/tests/stdio-chat.spec.ts similarity index 99% rename from packages/support/ui-stdio/tests/ui-stdio.spec.ts rename to packages/ui/stdio-agent/tests/stdio-chat.spec.ts index e991370c39..090b77ac64 100644 --- a/packages/support/ui-stdio/tests/ui-stdio.spec.ts +++ b/packages/ui/stdio-agent/tests/stdio-chat.spec.ts @@ -5,7 +5,7 @@ import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' 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 { createStdioChat, type Config, type StdioRuntime } from '../src/index.ts' +import { createStdioChat, type Config, type StdioRuntime } from '../src/stdio-chat.ts' /** * Unit tests for the stdio UI plugin. They drive the REAL plugin body diff --git a/packages/ui/stdio-agent/tsconfig.json b/packages/ui/stdio-agent/tsconfig.json index 58b492a549..948813c370 100644 --- a/packages/ui/stdio-agent/tsconfig.json +++ b/packages/ui/stdio-agent/tsconfig.json @@ -31,9 +31,6 @@ }, { "path": "../../session-persistence/session-persistence-jsonl" - }, - { - "path": "../../support/ui-stdio" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8045aa9cc3..8950475e0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -774,25 +774,6 @@ importers: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) - packages/support/ui-stdio: - dependencies: - schemastery: - specifier: ^3.18.0 - version: 3.18.0 - devDependencies: - '@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 - cordis: - specifier: ^4.0.0-rc.6 - version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) - packages/todo/tool-todo: devDependencies: '@deepseek-ai/dsh-agent': @@ -922,9 +903,6 @@ importers: '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ version: link:../../session-persistence/session-persistence-jsonl - '@deepseek-ai/dsh-ui-stdio': - specifier: workspace:^ - version: link:../../support/ui-stdio cordis: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) diff --git a/tsconfig.build.json b/tsconfig.build.json index d1a369036e..477c68d31e 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -42,7 +42,6 @@ { "path": "./packages/ui/acp" }, { "path": "./packages/ui/acp-agent" }, { "path": "./packages/ui/stdio-agent" }, - { "path": "./packages/support/ui-stdio" }, { "path": "./packages/support/llm-replay" }, { "path": "./packages/subagent/subagent" }, { "path": "./packages/support/subagent-mock" }, diff --git a/tsconfig.json b/tsconfig.json index c43e6690c6..7d1209b42e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -53,7 +53,6 @@ { "path": "./packages/ui/acp" }, { "path": "./packages/ui/acp-agent" }, { "path": "./packages/ui/stdio-agent" }, - { "path": "./packages/support/ui-stdio" }, { "path": "./packages/support/llm-replay" }, { "path": "./packages/subagent/subagent" }, { "path": "./packages/support/subagent-mock" },