Skip to content

feat: announce autonomous turn ends via _session/turn_ended - #997

Open
wingleeio wants to merge 1 commit into
agentclientprotocol:mainfrom
wingleeio:autonomous-turn-ended
Open

feat: announce autonomous turn ends via _session/turn_ended#997
wingleeio wants to merge 1 commit into
agentclientprotocol:mainfrom
wingleeio:autonomous-turn-ended

Conversation

@wingleeio

Copy link
Copy Markdown

Problem

An autonomous cycle — a task-notification followup, or a peer/coordinator/observer message the model handled on its own — streams its output as out-of-turn session/update frames, but its turn-end has nowhere to go: there is no session/prompt response for it to ride, so the SDK's result message is consumed silently by the autonomous-result lane.

A client can then only guess at the turn boundary from stream silence. In practice it shows the session as "working" for however long its own silence watchdog waits after the agent already finished — in comet's case, two minutes of phantom "working" after every background-task notification (that client's report and root-cause trace: the adapter's autonomous lane breaks without emitting; journal shows output with no terminal event until the next user prompt).

Change

Emit a _session/turn_ended extension notification (_-prefixed per the ACP extension conventions, like _session/steering) from the autonomous-result lane:

  • Payload: sessionId, the result's own stop_reason (the accumulated stopReason belongs to the user-turn lifecycle and may still hold the previous turn's value at this point), and the origin under _meta["_claude/origin"].
  • Only between turns: sent only when no user turn is active or queued — a live turn's end rides its own prompt response, and a deferred-settle hold consumes its followup's result instead. This reuses the exact gate the branch already had for the emittedAssistantText reset.
  • Best-effort: wrapped so a client that can't take the extension never kills the consumer loop; per spec, clients ignore unknown methods.

Tests

  • An idle background wake announces exactly once, with the right session, stop reason, and origin.
  • A background result consumed during a live prompt does not announce.
  • Full suite passes (704 tests).

Verified end-to-end against a patched comet client: background sleep 8 task → prompt turn settles → wake streams "finished" → _session/turn_ended → client status settles immediately (previously: a 120s watchdog was the only settle path).

An autonomous cycle — a task-notification followup, or a peer/
coordinator/observer message the model handled on its own — streams its
output as out-of-turn session/update frames, but its turn-end has no
session/prompt response to ride: the SDK's result message was consumed
silently. A client can then only guess at the boundary from stream
silence, and shows the session as "working" for however long its own
watchdog waits after the agent already finished (in comet's case: two
minutes of phantom working after every background-task notification).

Emit a `_session/turn_ended` extension notification (per the ACP `_`
extension conventions, like `_session/steering`) from the
autonomous-result lane, carrying the sessionId, the result's own stop
reason (the accumulated stopReason belongs to the user-turn lifecycle
and may still hold the previous turn's value), and the origin under
`_meta`. Only sent when no user turn is active or queued — a live
turn's end rides its own prompt response, and a deferred-settle hold
consumes the followup's result. Best-effort: a client that cannot take
the extension never kills the consumer loop; unknown methods are
ignored by spec.

Tested: an idle background wake announces exactly once with the right
session and origin; a background result consumed during a live prompt
does not announce.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants