Skip to content

Latest commit

 

History

History
42 lines (23 loc) · 5.14 KB

File metadata and controls

42 lines (23 loc) · 5.14 KB

Agent Note: Follow-up enqueue and owned run boundaries

Status: implemented

English | 中文

Problem

Agent.followup() identifies and queues a user message, but one follow-up does not own the activity that follows it. Steering, injected context, tool continuations, recovery, and later queued messages can all contribute before the agent next becomes idle. A MessageId can therefore prove inbox admission, but it cannot identify which assistant message or turn/end is the result of that input.

The one-send-one-turn decision already rejects a per-send completion handle in the core API. Protocol and SDK layers that pair one prompt request with a turn result manufacture that missing relationship downstream. The pairing becomes ambiguous as soon as activity admits more input, and it exposes turn mechanics as if they were a prompt-level outcome.

Decision

Keep Agent.followup(message): void as an enqueue-only operation. Agent.whenIdle() and agent/status remain whole-agent lifecycle observations; neither settles an individual message. Inbox durability records the identified message and its admission or cancellation, without assigning later output to it.

The low-level SDK protocol answers session/prompt as soon as enqueue succeeds with { messageId }. It streams durable facts through session.event, publishes whole-agent transitions through session.status, and has no session.finished. A low-level client may observe that receipt and later idleness, but receives no prompt result.

High-level automation APIs return a RunResult only when they explicitly own an activity interval. The TypeScript and Python SDK run() methods collect from the submitted message's durable inbox receipt through the next whole-agent idle; their final response is the last committed assistant message in that interval, not a response causally attributed to the submitted prompt. The Python SDK also reports the last root turn's reason kind as the run-level finish_reason, without attributing it to the submitted prompt. The one-shot CLI owns the analogous idle-to-idle interval. An isolated child-agent run may report a result because its caller owns the complete child lifecycle and any steering belongs to that run.

ACP must return a protocol stopReason. Its bridge serializes one in-flight prompt per ACP session and owns the interval from admission through whole-Agent idle and ordered update delivery. It correlates the turn that admits the identified ACP message without claiming that every activity in the interval was caused only by that message. A correlated token-limit ending maps to standard max_tokens; a correlated model error rejects at the same quiescence boundary; a turnless slot settles as cancelled alongside explicit ACP cancellation or disposal. Other normal quiescence reports end_turn.

Goal continuation retains MessageId only to recognize its durable queued and admitted goal message. It advances from durable goal state at whole-agent idle, without mapping the message to a turn result.

Alternatives considered

Map MessageId to the turn that admits it. A turn may consume steering and injected context and may continue through multiple model/tool steps. The mapping identifies admission, not causal ownership of the resulting output or stop reason.

Return a per-follow-up completion handle. A handle would imply a result boundary that the shared agent lifecycle does not have. It would either omit work that influenced the activity or silently absorb unrelated later input.

Use the last turn/end observed before idle. This is a useful run-level observation for an explicitly owned interval, but naming it as the submitted message's outcome recreates the false causal claim.

Verification

  • Agent and inbox tests pin enqueue-only follow-up, durable admission or cancellation, and whole-agent idle observation.
  • SDK protocol, TypeScript SDK, and Python SDK tests pin the { messageId } receipt, session.status, the absence of session.finished, and receipt-to-idle RunResult collection without prompt-level status or reason; Python SDK tests separately pin its run-level finish_reason observation.
  • ACP, one-shot CLI, goal continuation, and subagent tests pin the distinct activity ownership each integration possesses.
  • Consumer tests pin that no production integration derives a follow-up result by correlating MessageId with turn/end.

Consequences

An owned activity interval can include steering, injected context, or other work submitted before idleness, so its final response, finish reason, and events are deliberately broader than the initiating message. Prompt-level model error and token-limit classifications remain absent from the low-level DSH SDK result. ACP projects the correlated turn into its required standard error or max_tokens stop reason at interval quiescence, without adding a DSH-specific result or claiming exclusive causality. Concurrent automation on one session requires an explicit serialization or ownership policy rather than an implicit per-follow-up result.