fix(core): close turn cancellation contract gaps

This commit is contained in:
Tianyi Cui
2026-07-21 12:14:53 +08:00
parent 81cdebc531
commit c6e1d35a99
23 changed files with 185 additions and 168 deletions

View File

@@ -117,7 +117,7 @@ Pruning precedes summaries; overflow retries require durable progress. Bounded t
The turn contains failures. Adapter failures close the step before `agent/request-error`, which receives exact `Error`, `LlmFailure`, and history. Retry opens another step; success clears history; exhaustion stores failure on `turn/end`. Failed chunks commit no message/tool.
Others use `agent/error`. Cancellation/disposal beat recovery; undispatched calls get synthetic `tool/call` plus `ABORTED_BEFORE_DISPATCH` results before `turn/end`. One turn-wide `AbortSignal` covers stages. `cancel()` validates `user | parent`, clears queues, and aborts it; durability records `aborted`. Disposal quiesces before unregistering ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)).
Others use `agent/error`. Cancellation/disposal beat recovery; undispatched calls get synthetic `tool/call`/`ABORTED_BEFORE_DISPATCH` pairs. The signal spans stages until retirement before `turn/end`; typed `cancel()` clears queues and aborts it with `user | parent`. Durability records `aborted`; disposal quiesces before unregistering ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)).
Every session event is turn-enclosed. Reloading preserves an interrupted tail and closes it with a synthetic `interrupted` turn end. Failures after durable turn close report only through `agent/error` because no safe in-turn position remains. Each turn has one `TurnEndReason`; [TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap) owns the variants.

View File

@@ -832,7 +832,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId):
Types: [CreateSessionOptions](../core-data-structures/persistence.md) · [Session](../core-data-structures/session.md) · [SessionId](../core-data-structures/core.md)
Source: [`packages/core/session/src/index.ts:553`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:570`](../../packages/core/session/src/index.ts)
## `ctx.skills` — `SkillService`

View File

@@ -397,8 +397,8 @@ interface Agent {
* Clear all queued and steering work, including items waiting to start, and
* abort the active turn. The first cause wins for that turn, and `whenIdle()`
* resolves after cancellation reaches quiescence. Omission means
* `{ kind: 'user' }`; invalid causes throw synchronously even while idle.
* Idle cancellation is a no-op after validation and does not arm a later cancel.
* `{ kind: 'user' }`. Idle cancellation is a no-op and does not arm a later
* cancel. The active turn snapshots and freezes the typed cause.
* @param cause - the stable caller intent carried by the current turn signal.
*/
cancel(cause?: AgentCancelCause): void
@@ -411,7 +411,7 @@ interface Agent {
`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `running` describes the driver-wide drain interval, which can span turn close, its durability checkpoint, and consecutive queued turns; it does not prove a turn is still open. `AgentOptions` is merge-extensible and currently includes `provider?` and `model?`; dispatch requires both after `agent/request`. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
The cause is runtime-only and becomes `AbortSignal.reason` on the turn's explicit signal. `agentInterruptReasonOf(signal)` recognizes `user`, `parent`, and lifecycle-only `disposed` without consulting ambient initiator state. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result.
The cause is a TypeScript-enforced same-process input. An active holder copies its discriminant into the runtime-only `AbortSignal.reason`; it is retired before `turn/end` publication. `agentInterruptReasonOf(signal)` recognizes `user`, `parent`, and lifecycle-only `disposed` without consulting ambient initiator state. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result.
The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits.