fix review findings: whenIdle() is observation, not the teardown await
Codex's confirmation pass found the teardown-framing error went deeper than the three prose spots already fixed: the whenIdle() JSDoc itself (and its mirrors) claimed "the quiescence signal a teardown awaits ... a lifecycle owner disposes the agent through its AgentHandle which ... awaits THIS". The disposer does not call whenIdle() — it does `stop(); await agent.done` directly (packages/core/agent-loop/src/index.ts:271). whenIdle() is the NON-OWNER observation hook; owner teardown awaits the loop-exit promise (done) through AgentHandle.dispose(). Reframe every copy accordingly: - packages/core/agent/src/types.ts: the Agent.whenIdle() contract JSDoc. - packages/core/agent-loop/src/agent.ts: the impl JSDoc. - packages/core/agent/README.md and docs/core-data-structures/core.md (the type-equiv mirror of the types.ts JSDoc — re-copied verbatim). - docs/rfc/proposed/feature/2026-06-14-acp-agent-client-protocol.md:40 and :70: owner teardown via AgentHandle.dispose(); a non-owner observing quiescence uses the interface-level agent.whenIdle(), not hand-rolled agent/status. - Regenerate the cordis catalog (whenIdle source line moved).
This commit is contained in:
@@ -25,7 +25,7 @@ An agent was registered in the AgentRegistry and is ready to receive messages.
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:137`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:135`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/disposed` — emit
|
||||
|
||||
@@ -37,7 +37,7 @@ An agent was disposed and removed from the registry; its fiber and any in-flight
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:143`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:141`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/error` — emit
|
||||
|
||||
@@ -49,7 +49,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:220`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:218`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/queued` — emit
|
||||
|
||||
@@ -61,7 +61,7 @@ A message entered the agent's inbox (queued or steering). `source` is the resolv
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:156`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:154`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/request` — waterfall
|
||||
|
||||
@@ -73,7 +73,7 @@ Waterfall: mutate the fully-assembled GenerateOptions before the model call (hoo
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [GenerateOptions](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:189`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:187`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/status` — emit
|
||||
|
||||
@@ -85,7 +85,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). Drive lifecycle
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:150`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:148`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/steering` — emit
|
||||
|
||||
@@ -97,7 +97,7 @@ Steering content was injected into a running turn.
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:214`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:212`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/step-end` — emit
|
||||
|
||||
@@ -109,7 +109,7 @@ A step ended.
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:180`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:178`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/step-result` — waterfall
|
||||
|
||||
@@ -121,7 +121,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:195`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:193`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/step-start` — emit
|
||||
|
||||
@@ -133,7 +133,7 @@ A step (one model call plus its tool dispatch) began. `step` is 1-based within t
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:175`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:173`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/stream-chunk` — emit
|
||||
|
||||
@@ -145,7 +145,7 @@ A raw StreamChunk arrived from the model (token-level UI/log feed).
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:209`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:207`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/turn-continuation` — waterfall
|
||||
|
||||
@@ -157,7 +157,7 @@ Waterfall: override the turn-continuation decision. The default (computed by the
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:202`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:200`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/turn-end` — emit
|
||||
|
||||
@@ -169,7 +169,7 @@ A turn ended. `reason` distinguishes a clean stop from a truncated or aborted on
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [TurnEndReason](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:169`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
#### `agent/turn-start` — emit
|
||||
|
||||
@@ -181,7 +181,7 @@ A turn began. `turn` is the 1-based turn number within the session.
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:163`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:161`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `llm/*`
|
||||
|
||||
|
||||
@@ -253,12 +253,14 @@ interface Agent {
|
||||
|
||||
/**
|
||||
* Resolve once the agent has reached quiescence after settling out of
|
||||
* `running`, or immediately if it is already idle with no queued work. The
|
||||
* quiescence signal a teardown awaits: a lifecycle owner disposes the agent
|
||||
* through its `AgentHandle` (which aborts in-flight work then awaits this), so
|
||||
* the caller proceeds only after queued/running work has fully stopped (a
|
||||
* closing ACP connection, a disposing UI plugin) rather than returning while
|
||||
* the driver is still streaming or about to start a queued turn.
|
||||
* `running`, or immediately if it is already idle with no queued work. A
|
||||
* non-owner's quiescence-observation hook: a consumer that does NOT own the
|
||||
* agent's lifecycle (a closing ACP connection, a UI plugin) awaits this to
|
||||
* proceed only after queued/running work has fully stopped, rather than
|
||||
* returning while the driver is still streaming or about to start a queued
|
||||
* turn. It does NOT tear the agent down — a lifecycle owner stops and
|
||||
* unregisters the agent through its `AgentHandle.dispose()` (which awaits the
|
||||
* loop-exit promise directly), separate from this.
|
||||
*
|
||||
* "Quiescence", not merely "status changed": a disposed agent emits
|
||||
* `agent/status('disposed')` from inside its disposer, BEFORE the driver loop
|
||||
@@ -266,10 +268,6 @@ interface Agent {
|
||||
* to actually exit (the implementation chains the loop-exit promise), not just
|
||||
* observe the status flip. A mid-step disposal that never reaches `idle` still
|
||||
* unblocks the await this way.
|
||||
*
|
||||
* Distinct from disposal: `whenIdle()` observes the transition WITHOUT tearing
|
||||
* the agent down. A consumer that owns the agent's lifecycle disposes it
|
||||
* separately.
|
||||
*/
|
||||
whenIdle(): Promise<void>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ The mapping between ACP and existing harness seams — each row names the seam a
|
||||
|
||||
The permission gate is the first real consumer of the `tools/execute` veto seam (the documented "single veto/sandbox/permission seam" plus the deferred "Permission system" TODO in [docs/architecture.md](../../../architecture.md)). It is a single global listener registered with `prepend: true` so it runs before any other tool wrapper. `ToolExecution.agent` is optional and the `Agent` interface carries no origin marker, so the bridge tracks ownership itself: it records each agent it creates in a `WeakMap<Agent, sessionId>` and the gate no-ops (calls `next()` immediately) for any `exec.agent` it does not own — non-ACP agents and the no-agent case pass straight through. For an owned agent it resolves the session, issues `session/request_permission`, and stores the pending resolver on that session's record so the outcome — or a `session/cancel`/connection-close — settles it exactly once.
|
||||
|
||||
Lifecycle and disposal: the connection, listeners, and in-flight permission promises register via `ctx.effect`/`ctx.on`; teardown is async and must *reach* quiescence, not just request it — close the connection, settle/reject pending permissions, and dispose each owned agent through its `AgentHandle.dispose()` (which stops the loop, `await`s its exit, and unregisters). Disposal must come through the `dsh-agent` handle seam, not the loop: `agent.done` exists only on the concrete `ReactLoopAgent`, so a bridge that wanted to wait on quiescence directly would instead observe `agent/status` reaching `idle`/`disposed` — but routing teardown through the handle's `dispose()` makes that unnecessary. Every listener contains its `send()` exceptions (log, never reject the turn) because stream chunks are emitted inside the model step, so a throwing listener would corrupt the turn.
|
||||
Lifecycle and disposal: the connection, listeners, and in-flight permission promises register via `ctx.effect`/`ctx.on`; teardown is async and must *reach* quiescence, not just request it — close the connection, settle/reject pending permissions, and dispose each owned agent through its `AgentHandle.dispose()` (which stops the loop, `await`s its exit, and unregisters). Owner teardown goes through that handle seam, not the loop's concrete `agent.done` (which exists only on `ReactLoopAgent`); a non-owner that merely wants to *observe* the current work settling without tearing the agent down awaits the interface-level `agent.whenIdle()`. Every listener contains its `send()` exceptions (log, never reject the turn) because stream chunks are emitted inside the model step, so a throwing listener would corrupt the turn.
|
||||
|
||||
**Dependency note (architecture rule).** [docs/architecture.md](../../../architecture.md) states "plugins depend on interface packages, never on `dsh-agent-loop`." Creating and resuming agents is currently only on the concrete `AgentLoop` (`ctx.agentLoop`), so this RFC proposes adding an **abstract create/resume factory** to the `dsh-agent` interface (registry-level `create({ sessionId, meta })` / `resume(...)`), implemented by the loop, so `dsh-acp` injects only `agents` (the interface) and the dependency rule holds. The alternative — injecting the concrete `agentLoop` and recording a documented exception in the architecture doc — is explicitly the non-preferred fallback.
|
||||
|
||||
@@ -67,7 +67,7 @@ New third-party runtime dependency plus protocol drift: `@agentclientprotocol/sd
|
||||
|
||||
Turn-settle and prompt-correlation hazards: honor "queued messages batch into one turn" and "`send()` does not synchronously flip to running" (see `stdio-chat.ts` and the defensive-patterns section of [docs/architecture.md](../../../architecture.md)); gate resolution on an observed running→idle transition and handle the empty-prompt / no-work branch so an RPC can't hang.
|
||||
|
||||
Permission-await and disposal hangs: a pending `request_permission` whose connection closes or whose turn aborts must settle exactly once; disposal must reach quiescence (observe the interface-level settle signal — `agent/status` reaching `idle`/`disposed`, since `agent.done` is `ReactLoopAgent`-only), not orphan awaits on a closed pipe.
|
||||
Permission-await and disposal hangs: a pending `request_permission` whose connection closes or whose turn aborts must settle exactly once; disposal must reach quiescence — tear each owned agent down through `AgentHandle.dispose()` (which stops the loop and awaits its exit), rather than orphaning awaits on a closed pipe.
|
||||
|
||||
The 100% per-file coverage gate (repo policy) makes a branch-heavy protocol bridge real work. Accepted deliberately, surfaced so it isn't a surprise at PR time.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user