Match claude-agent-acp / codex-acp: the bash tool_call title IS the command (an execute card hides rawInput), the model description rides as a content text block above the card, and the completed card carries an exit-status pill via _meta.terminal_exit. Bridge fixes found in review of the prior terminal-card commit: - tool_call_update.content is OMITTED in terminal mode (an ACP update.content REPLACES the call's content collection in Zed, so the fenced ```console block would clobber the terminal content block). - terminal.output preserves RAW newlines (terminal renderers rely on exact bytes); only the fenced fallback trims trailing blank lines. - a relative workdir is resolved against the session cwd for the card header, matching where the command actually ran. - result-side terminal output is gated on the pending call having registered a terminal (no orphan _meta.terminal_output for a terminal Zed never made). The exit pill is recovered by parsing renderResult's status markers (the pure presentResult seam sees only content blocks); a round-trip test pins the parse to the marker emission. Neutral ToolTerminal gains exitCode/signal; widened ToolCallPresentation with a content block. Docs (RFC + 3 READMEs) updated; with-key e2e verifies the card + exit pill against the real model.
Packages
Harness packages, all under the @deepseek-ai/dsh-* scope. Each package is a Cordis service (microkernel plugin-style): it exports a default Service class that gets registered via ctx.plugin(), declares its ctx key and events through declaration merging, and exposes extension points through ctx.effect(), ctx.on(), and ctx.waterfall().
Dependency graph
dsh-llm (no harness deps — pure vocabulary)
dsh-bash (no harness deps — abstract executor seam)
dsh-session ← dsh-llm
dsh-system-prompt ← dsh-llm
dsh-agent ← dsh-llm, dsh-session
dsh-tools ← dsh-llm, dsh-system-prompt, dsh-agent
dsh-bash-local ← dsh-bash (BashExecutor impl)
dsh-tool-bash ← dsh-bash, dsh-tools (bash tool schemas)
dsh-llm-deepseek ← dsh-llm (DeepSeek adapter)
dsh-llm-pi-ai ← dsh-llm (pi-ai-backed adapter)
dsh-agent-loop ← dsh-llm, dsh-session, 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 (ACP JSON-RPC bridge)
The rule: plugins depend on interfaces, never on the concrete loop. dsh-agent-loop is swappable — UI/hook/tool plugins keep working against the dsh-agent vocabulary if the loop is replaced. A swappable capability splits into interface / implementation / consumer packages (the bash trio is the template — see capability seams).
What goes where
| Package | Role | ctx key |
|---|---|---|
llm/ |
Abstract LLM service + content-block vocabulary + chunk assembler | ctx.llm |
session/ |
Event-sourced session log + in-memory store | ctx.sessions |
system-prompt/ |
Prompt-section + tool-schema assembly registry | ctx.systemPrompt |
tools/ |
Tool registry + tools/execute waterfall |
ctx.tools |
agent/ |
Agent interface, registry, agent/* event vocabulary |
ctx.agents |
agent-loop/ |
THE concrete plugin: LoopAgent + the loop driver |
ctx.agentLoop |
bash/ |
Abstract bash executor seam (interface + vocabulary) | ctx.bash |
bash-local/ |
Local-subprocess BashExecutor implementation |
(registers ctx.bash) |
tool-bash/ |
Model-facing bash/bash_output/bash_kill tool schemas |
(registers on ctx.tools) |
llm-deepseek/ |
DeepSeek API adapter (hand-rolled fetch/SSE) | (registers on ctx.llm) |
llm-pi-ai/ |
DeepSeek adapter via @earendil-works/pi-ai (design twin) |
(registers on ctx.llm) |
invariants/ |
Dev-mode event-contract invariants + session-log freeze | (listens on session/*, agent/*) |
acp/ |
Agent Client Protocol bridge: serves the agent to an ACP editor over JSON-RPC stdio | (drives ctx.agents/ctx.sessions) |
Each package has its own README.md with purpose, service API, events, extension points, and deliberate non-goals (TODOs).
Conventions (applied across all harness packages)
- Registrations are effects: every contribution (adapter, tool, section, agent, event listener) goes through
ctx.effect()/ctx.on(), so disposal and HMR clean up automatically. Everyregister()returns the disposer. - Declaration merging for events and ctx: services declare their events in
declare module 'cordis' { interface Events { ... } }and their ctx key ininterface Context. - Waterfall semantics:
ctx.waterfalllisteners receive(...args, next)and MUST callnext()to delegate; returning without it short-circuits (the veto mechanism). - Extensible unions:
ContentBlockMap,MessageSourceMap,FinishReasonMap,TurnTriggerMap,TurnEndReasonMap, andSessionEventMapuse the merge-extensible-map pattern so plugins can add variants via declaration merging. - ESM everywhere; imports use package names across package boundaries,
.tsextensions within a package. - Tests: vitest, colocated under
packages/<name>/tests/*.spec.ts. Every registry needs an HMR-safety test. Err on the side of more tests.