The whenIdle() JSDoc cited "a closing ACP connection" as a non-owner that awaits whenIdle(). That is false against the code: ACP OWNS its agent handles and tears them down via rec.dispose()/handle.dispose() (quiesce() at packages/ui/acp/src/index.ts:666-686), never whenIdle(). The only whenIdle() consumers are tests (acp dispose/turns/edges specs, agent specs) — which is genuinely why the primitive stays (a test harness programs against the seam), but the contract doc must not claim a production ACP path uses it. Replace the parenthetical with truthful non-owning observers (a test awaiting a turn to settle, a monitor) and state explicitly that an OWNER does not need whenIdle() because AgentHandle.dispose() already awaits the loop-exit promise. - packages/core/agent/src/types.ts: the Agent.whenIdle() contract JSDoc. - docs/core-data-structures/core.md: the type-equiv mirror (re-copied verbatim). - Regenerate the cordis catalog (whenIdle source line shifted).
core/ — product API spine
The packages every harness build is assembled from: the session log, the system-prompt assembly, the tool registry, the agent vocabulary, and the one concrete loop that drives them. These are product packages — the stable surface plugins and consumers build against.
| Package | Role | ctx key |
|---|---|---|
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 loop plugin: ReactLoopAgent + the loop driver |
ctx.agentLoop |
agent-loop is the one concrete implementation of the agent seam and lives here because it is the harness's default product loop; everything else in core/ is interface/vocabulary. Plugins depend on the agent vocabulary, never on agent-loop directly, so the loop stays swappable.