Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
4.0 KiB
dsh-agent
Agent interface, registry, and agent/* event vocabulary. Every plugin (UI, hooks, orchestrators) programs against the Agent handle defined here — it has zero loop dependency, so the loop is swappable.
Service: AgentRegistry (ctx key: agents)
Tracks live agents so UI, hook, and orchestrator plugins can find them without importing the concrete loop package.
Public API
ctx.agents.register(agent: Agent): () => void— record an already-constructed agent. Disposed with the calling fiber.ctx.agents.get(id: string): Agent | undefinedctx.agents.list(): Agent[]
Factory seam (creation)
Agent creation is provided by whichever plugin implements AgentFactory (phase 1: dsh-agent-loop), registered via setFactory. This keeps creation on the dsh-agent interface so consumers (UI, the ACP bridge) program against ctx.agents without depending on the concrete loop package.
ctx.agents.setFactory(factory: AgentFactory): () => void— register the creation factory (the loop calls this on construction). Throws on a second factory; the slot clears on dispose.ctx.agents.create(options: CreateAgentOptions): Agent— construct, start, AND register a new agent on a caller-suppliedsessionId(with optionalmeta.cwd). Distinct fromregister(which only records). Throws if no factory is registered.ctx.agents.resume(options: ResumeAgentOptions): Promise<Agent>— load a persisted session (RFC 009) and resume an agent on it. Async; rejects if no factory is registered, or if the factory finds session persistence unconfigured.
Events
The full agent/* event taxonomy is declared via declaration merging in dsh-agent (not dsh-agent-loop), so plugins depend only on this package.
Lifecycle (emit)
agent/created,agent/disposed— registration/deregistrationagent/status— idle / running / disposed transitionagent/queued— message entered inbox (source-resolved, steering flag)
Turn/step boundaries (emit)
agent/turn-start,agent/turn-end(carriesTurnEndReason)agent/step-start,agent/step-end
Interception seams (waterfall)
agent/request— mutateGenerateOptionsbefore the model call (hooks, compaction, model switching, tool filtering)agent/step-result— post-process the assembled assistant message before tool dispatch (validates what the log records)agent/turn-continuation— override the continue/stop decision (force-continue /loop, force-stop budget guard)
Streaming + tool (emit)
agent/stream-chunk— raw chunk from the model (token-level UI/log feed)agent/steering— steering content injected mid-turnagent/error— step/turn error
Agent interface (types.ts)
The handle every plugin programs against:
agent.send(content, options?)— queue a message; starts a turn when idleagent.steer(content, options?)— steer a running turn (inject between steps); behaves likesendwhen idleagent.inject(content, options?)— inject in-session context (context/message event); the next request sees it. Does not run the model. While a turn is open it joins that turn; while idle it is wrapped in a one-shotinjectionturn so every event stays turn-enclosed (ADR 0017)agent.abort(reason?)— abort the in-flight stepagent.whenIdle()— resolve once the agent reaches quiescence after settling out ofrunning(idle → immediately; disposed → awaits the loop exit), the signal a teardown awaits (abort()thenawait whenIdle()). Observes the transition without disposing the agent.agent.session,agent.status,agent.options,agent.id
Extension points
- Agent creation:
AgentLoop.create()is the concrete implementation (indsh-agent-loop). Replace the loop by implementingAgentand registering viactx.agents.register(). - Event listeners: all
agent/*events are declared here — no dependency on the loop package needed.
What is NOT here (TODO)
- Sub-agent spawn/fork — seam on
AgentLoop.create(), semantics deferred.