Add @deepseek-ai/dsh-tool-todo (a new packages/todo/ group): a model-facing
todo_write(todos: [{content, status}]) tool with whole-list-replace semantics.
Each call appends the full list as a todo/write event to the calling agent's
session log; the current list is the most recent such event (last-write-wins).
Single-owner — a non-agent caller is rejected. Beyond the schema's
type/required/enum checks, execute rejects empty/duplicate content and more than
one in_progress task, narrowing the loosely-typed args into a real TodoItem[].
Both UIs render off the existing session/event: the stdio UI prints a glyphed
checklist; the ACP bridge maps the list to a `plan` sessionUpdate (todosToPlan
synthesizes the priority ACP requires; status maps 1:1). Wired into the
coding-agent, acp-agent, and snapshot example configs with a system-prompt nudge.
Tests: unit (schema, validation, append/replace, no-agent rejection, presentCall,
HMR-safety, Loader export-shape guard), full-loop integration through the agent
loop, the ACP todosToPlan mapping + stream-update arm, the stdio render arm, and
a session/load replay that re-emits the plan. New-group TS wiring added to
tsconfig.base/json/build. RFC + a doc-inventory sweep (architecture, packages
README, AGENTS layout, cookbook group list, example READMEs) ship with it.
The todo-plan ACP snapshot scenario is recorded separately (needs an API key).
@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 only consumes the agent/* event taxonomy plus the agents service (inject: ['agents']), so the same plugin drives any example or product surface.
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. |
- id: ui-stdio
name: '@deepseek-ai/dsh-ui-stdio'
config:
welcome: 'coding-agent 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.)
agent/stream-chunk—text-deltais written verbatim;reasoning-deltais wrapped in the dim SGR (\x1B[2m … \x1B[0m) so the chain-of-thought is visually subordinate to the answer. Reasoning rendering is inert when noreasoning-deltachunks arrive (e.g. a mock model), so it is always on.agent/turn-start/agent/turn-end— a[<agent> turn N]header and a trailing>prompt.session/event—tool/callrenders[tool call] name(args);tool/resultrenders the joined text blocks as[tool result] ….
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
runninghere would hang forever. - Work submitted: exit the next time the agent settles to
idleafter having been observedrunning.agent.send()does not synchronously flip status torunning, so requiring an observedrunningfirst (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). The keyless Loader-path e2e smokes in examples/{echo,coding}-agent guard this end-to-end.