Merge remote-tracking branch 'origin/master' into fix/subagent-depth-budget

# Conflicts:
#	docs/persistence-catalog.md
This commit is contained in:
Tianyi Cui
2026-07-20 18:11:59 +08:00
34 changed files with 795 additions and 240 deletions

View File

@@ -55,9 +55,9 @@ Waterfall events behave like around-middleware: a listener delegates by calling
## Default Loop Lifecycle
The shipped loop drains work from prompt through checkpoint. Every pause is a service call or event available to plugins.
The shipped loop drains prompt-to-checkpoint work through plugin-visible services and events.
A **session** is an append-only event log. A **turn** drains queued input until the model stops asking for tools and no plugin requests continuation. A **step** is one model request plus the tool executions caused by that response. In the flow below ([sequence companion](agent-lifecycle.md)), quoted names are durable session events and event names are extension points.
A **session** is an append-only log. Each ordinary **turn** claims one queued `send()` item; injection claims none. A claimed `send()` successor awaits the preceding claimed ordinary turn's checkpoint but may share its `running` interval ([decision](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)). A turn ends when model and plugins stop it. A **step** is one model request plus tools. Below ([sequence companion](agent-lifecycle.md)), quotes mark durable events; other names are extension points.
Startup resolves identity. No id mints `<config-id>-session-<uuid>`; `sessionId` resumes or creates; `resumeSessionId` requires history. Resume reconstructs durable session metadata before publication, so lineage, seed boundaries, and delegation depth survive restart. Active failures emit `agent-loop/config-start-failed(sessionId, error)`, so front doors reject work; teardown stays silent.
@@ -69,13 +69,13 @@ choose declarative identity and fresh/resume path
-> enter session + agent -> session/created -> agent/created
-> enable driving -> agent/session-start(source) -> start driver
forever:
wait for queued messages
wait for a queued message
emit agent/status(running)
TURN:
'turn/start'
each queued message -> agent/prompt-submit
claimed message -> agent/prompt-submit
allowed prompt -> 'user/message' plus injected context
every prompt blocked -> 'turn/end'(rejected)
blocked prompt -> 'prompt/blocked' -> 'turn/end'(rejected)
STEP loop:
drain steering
assemble system prompt and tool schemas

View File

@@ -33,7 +33,7 @@ A fully configured agent and live session were published. Setup is composition-o
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:143`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:150`](../../packages/core/agent/src/types.ts)
### `agent/disposed` — emit
@@ -53,7 +53,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence but bef
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:152`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:159`](../../packages/core/agent/src/types.ts)
### `agent/error` — emit
@@ -75,7 +75,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:307`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:314`](../../packages/core/agent/src/types.ts)
### `agent/post-step` — serial
@@ -98,7 +98,7 @@ Awaited serial checkpoint after the response, real or synthetic tool results, in
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:260`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:267`](../../packages/core/agent/src/types.ts)
### `agent/pre-step` — serial
@@ -121,18 +121,18 @@ Awaited serial checkpoint before `step/start`; appends land outside the pending
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:200`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:207`](../../packages/core/agent/src/types.ts)
### `agent/prompt-submit` — waterfall
Allow, rewrite, or block one drained prompt before it becomes a user message. Call `next()` for the unchanged default.
Allow, rewrite, or block one claimed prompt before it becomes a user message. Call `next()` for the unchanged default.
```ts cordis-catalog
/**
* Allow, rewrite, or block one drained prompt before it becomes a user
* Allow, rewrite, or block one claimed prompt before it becomes a user
* message. Call `next()` for the unchanged default.
* @param agent - the agent draining its inbox.
* @param content - the drained message's blocks, as queued.
* @param agent - the agent whose turn claimed the message.
* @param content - the claimed message's blocks, as queued.
* @param source - the message's resolved source.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
@@ -142,7 +142,7 @@ Allow, rewrite, or block one drained prompt before it becomes a user message. Ca
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:210`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:217`](../../packages/core/agent/src/types.ts)
### `agent/queued` — emit
@@ -163,7 +163,7 @@ Detached, frozen content entered the agent's inbox. Source defaults have already
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:171`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:178`](../../packages/core/agent/src/types.ts)
### `agent/request` — waterfall
@@ -186,7 +186,7 @@ Replace the frozen call configuration. Model-visible content must use logged cha
Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:222`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:229`](../../packages/core/agent/src/types.ts)
### `agent/request-error` — waterfall
@@ -211,7 +211,7 @@ Recover a model-request failure after its failed step has closed. `retry` opens
Types: [Agent](../core-data-structures/core.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:274`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:281`](../../packages/core/agent/src/types.ts)
### `agent/session-prefix` — waterfall
@@ -237,7 +237,7 @@ Compose request-only messages placed before derived history. The frozen result i
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:237`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:244`](../../packages/core/agent/src/types.ts)
### `agent/session-start` — emit
@@ -259,7 +259,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:184`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:191`](../../packages/core/agent/src/types.ts)
### `agent/status` — emit
@@ -279,7 +279,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does no
Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:161`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:168`](../../packages/core/agent/src/types.ts)
### `agent/step-result` — waterfall
@@ -301,7 +301,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) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:248`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:255`](../../packages/core/agent/src/types.ts)
### `agent/turn-continuation` — waterfall
@@ -322,7 +322,7 @@ Override whether the turn continues. The default continues after tool calls or s
Types: [Agent](../core-data-structures/core.md) · [ContinuationDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:284`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:291`](../../packages/core/agent/src/types.ts)
### `agent/turn-stop` — serial
@@ -343,7 +343,7 @@ Monotonic terminal-stop checkpoint after continuation and steering are folded; a
Types: [Agent](../core-data-structures/core.md) · [ContinuationStop](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
Source: [`packages/core/agent/src/types.ts:294`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:301`](../../packages/core/agent/src/types.ts)
## `agent-loop/*`

View File

@@ -360,15 +360,20 @@ interface Agent {
readonly ctx: Context
/**
* Queue detached, frozen lossless-JSON input; starts a turn when idle.
* Queue one detached, frozen lossless-JSON item. If claimed, it is the sole
* ordinary message in its FIFO-ordered turn; the next claimed item waits for
* that turn's checkpoint.
* Invalid input throws synchronously before notification or enqueue.
*/
send(content: ContentBlock[], options?: SendOptions): void
/**
* Steer a running turn: content is injected between steps of the current
* turn. Uses the same owned-value and synchronous-validation boundary as
* {@link send}; when idle, behaves exactly like that method.
* Submit steering while the agent is `running`. An open turn records it at
* the next steering checkpoint before a request or continuation decision;
* policy may stop before another step. After turn close and its checkpoint,
* any remainder is queued for a later turn; terminal `agent/turn-stop`,
* cancellation, or disposal may discard it. Uses the same synchronous
* snapshot-and-validation boundary as {@link send}; when idle, delegates to it.
*/
steer(content: ContentBlock[], options?: SendOptions): void
@@ -382,10 +387,11 @@ interface Agent {
inject(content: ContentBlock[], options?: InjectOptions): void
/**
* Clear queued and steering work, including work waiting to start, and abort
* the active step. The supplied reason is preserved across pre-step and active
* cancellation windows, and `whenIdle()` resolves after cancellation reaches
* quiescence. Idle cancellation is a no-op and does not arm a later cancel.
* Clear all queued and steering work, including items waiting to start, and
* abort the active step. The supplied reason is preserved across pre-step
* and active cancellation windows, and `whenIdle()` resolves after
* cancellation reaches quiescence. Idle cancellation is a no-op and does not
* arm a later cancel.
*/
cancel(reason?: string): void
@@ -395,7 +401,7 @@ interface Agent {
}
```
`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `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.
`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 [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.
@@ -419,13 +425,14 @@ interface HookContext {
}
```
`agent/prompt-submit` returns a `PromptDecision` (allow a drained queued message — optionally rewriting its `content` or attaching `additionalContexts` — or block it; a batch whose every prompt is blocked opens a zero-step turn that ends `rejected`):
`agent/prompt-submit` returns a `PromptDecision` (allow the turn's claimed queued message — optionally rewriting its `content` or attaching `additionalContexts` — or record `prompt/blocked` and end that zero-step turn as `rejected`):
```ts type-equiv
/**
* Prompt interception result. `allow.content` replaces the prompt and each
* `additionalContexts` entry becomes a separate context message. `block` records a
* durable `prompt/blocked`; an all-blocked batch ends a zero-step rejected turn.
* `additionalContexts` entry becomes a separate context message. `block`
* records a durable `prompt/blocked` and ends the claimed prompt's zero-step
* turn as rejected.
*/
type PromptDecision =
| { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: HookContext[] }

View File

@@ -6,7 +6,7 @@ The seam is a textbook [capability seam](../../.agents/notes/implemented/archite
## The flush checkpoint
`session/event` is a *synchronous* notification; persistence plugins buffer it (write-behind) and drain at the awaited `session/flush` checkpoint the loop fires at every turn end. Flush is `ctx.parallel` (awaited): a turn's events are durably committed before the next turn starts, and the turn boundary is the commit boundary. A rejecting flush is reported via `agent/error` and the logger — never as a session event (it would land past the commit boundary), so the backend keeps its buffered events for the next flush.
`session/event` is a *synchronous* notification; persistence plugins buffer it (write-behind) until `session/flush`. The loop awaits an ordinary turn's checkpoint before claiming the next queue item; synchronous idle `inject()` schedules its checkpoint without blocking `send()`, and disposal still drains it. A successful flush durably commits the closed turn as one unit; a rejecting flush is reported through `agent/error` and the logger — never as a session event past the closed turn — while the backend keeps its buffered events for the next flush.
## Crash recovery preserves an interrupted turn

View File

@@ -17,27 +17,28 @@ The append-only event types. Merge-extensible: a plugin declares extra event typ
*/
interface SessionEventMap {
/**
* Opens turn `turn`. `trigger` records what started it — a drained message
* batch or an idle-time injection. The turn is the durability/replay
* Opens turn `turn`. `trigger` records what started it — one claimed queued
* message or an idle-time injection. The turn is the durability/replay
* boundary: every event sits between a `turn/start` and its matching
* `turn/end` (the turn-enclosure invariant).
*/
'turn/start': { turn: number; trigger: TurnTrigger }
/**
* Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop
* fires the awaited `session/flush` checkpoint at every turn end, so the turn
* boundary is also the durable-commit boundary.
* awaits `session/flush` after an ordinary turn ends before claiming the next
* queued item. Success commits the turn; rejection is reported live and does
* not prevent later work.
*/
'turn/end': { turn: number; reason: TurnEndReason }
/** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */
'step/start': { turn: number; step: number }
/** Closes step `step` of turn `turn`. */
'step/end': { turn: number; step: number }
/** A user-visible prompt (queued message drained at turn start). */
/** A user-visible prompt (the queued message claimed for this turn). */
'user/message': { content: ContentBlock[]; source: MessageSource }
/**
* Durable record of a prompt veto and its reason. It is log-only: the blocked
* prompt never enters the model-visible surface, including in a mixed batch.
* prompt never enters the model-visible surface, and its turn runs zero steps.
*/
'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string }
/**
@@ -480,8 +481,8 @@ interface TurnEndReasonMap {
/** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
'max-tokens': { kind: 'max-tokens' }
/**
* Policy blocked every prompt before the first step. The zero-step turn still
* records a balanced durable boundary and the veto reason.
* Policy blocked the turn's claimed prompt before the first step. The
* zero-step turn still records a balanced durable boundary and veto reason.
*/
rejected: { kind: 'rejected'; reason: string }
/**
@@ -492,7 +493,7 @@ interface TurnEndReasonMap {
}
```
`max-tokens` mirrors the model-call `FinishReason` of the same name: any `max-tokens` step in a turn makes the whole turn end `max-tokens` rather than `completed` (the cut-short fact wins over a later continuation), so a consumer can tell a clean stop from a truncated one — but only over `completed`: the `disposed`/`aborted`/`error` outcomes take precedence. `rejected` is a zero-step turn whose whole prompt batch an `agent/prompt-submit` hook blocked (the ACP bridge maps it to `cancelled`). `interrupted` is the one reason no loop emits — it is synthesized by crash recovery (see [persistence.md](persistence.md)). Both maps are merge-extensible.
`max-tokens` mirrors the model-call `FinishReason` of the same name: any `max-tokens` step in a turn makes the whole turn end `max-tokens` rather than `completed` (the cut-short fact wins over a later continuation), so a consumer can tell a clean stop from a truncated one — but only over `completed`: the `disposed`/`aborted`/`error` outcomes take precedence. `rejected` is a zero-step turn whose claimed prompt an `agent/prompt-submit` hook blocked (the ACP bridge maps it to `cancelled`). `interrupted` is the one reason no loop emits — it is synthesized by crash recovery (see [persistence.md](persistence.md)). Both maps are merge-extensible.
## The turn-enclosure invariant

View File

@@ -12,7 +12,7 @@ When an interface documents two valid ways to signal something — an adapter ma
## Async state is not synchronous state
`agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) rather than counting actions you assume map 1:1 to turns (the loop batches queued messages). The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never `running`), the wait hangs — handle the "nothing to wait for" branch explicitly.
`agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-send result: several queued sends run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items. The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never `running`), the wait hangs — handle the "nothing to wait for" branch explicitly.
## Dispose must reach quiescence, not just request it

View File

@@ -8,21 +8,21 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:362`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:143`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:152`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:307`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`tui`](../packages/ui/tui) |
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:260`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic) |
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:200`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:210`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:171`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:222`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:274`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic) |
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:237`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:184`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) |
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:161`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:248`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:284`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:294`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:150`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:159`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:314`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`tui`](../packages/ui/tui) |
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:267`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic) |
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:207`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:217`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:178`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:229`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:281`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic) |
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:244`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:191`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) |
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:168`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:255`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:291`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:301`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:31`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/ui/acp) |
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:62`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:71`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) |

View File

@@ -28,9 +28,9 @@
**dispose资源释放必须等待所有任务完全停稳不能仅下发终止指令就返回**:如果清理过程只发出终止或中断信号,却不等任务停止就返回,就会留下孤儿进程。清理应采用异步方式,等待所有子任务彻底退出(先发出终止信号,再等待退出);发出信号前应先关闭监听器与通知注册表,使延迟到达的完成事件不再触发通知。测试要证明 dispose 的确等到清理完成:执行完 `await fiber.dispose()` 后进程 PID 立即消失,不能只检查进程最终会自行消亡。
> **Async state is not synchronous state** — `agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) rather than counting actions you assume map 1:1 to turns.
> **Async state is not synchronous state** — `agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-send result: several queued sends run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items.
**异步状态不等同于同步瞬时状态**:调用 `agent.send()` 不会在返回前同步更新状态;后台任务的完成时间与轮次边界存在竞态;`reader.close()` 既会在读到文件末尾时触发,也会在资源释放时触发。切勿把刚刚发起的状态变更当成已经生效,据此控制流程;生命周期逻辑应以实际触发的事件和已完成的 promise`agent/status``task.done`)为准,并观察完整的状态变化(先 `running`,再 `idle`),不要根据操作次数推断操作与轮次一一对应
**异步状态不等同于同步瞬时状态**:调用 `agent.send()` 不会在返回前同步更新状态;后台任务的完成时间与轮次边界存在竞态;`reader.close()` 既会在读到文件末尾时触发,也会在资源释放时触发。切勿把刚刚发起的状态变更当成已经生效,据此控制流程;生命周期逻辑应以实际触发的事件和已完成的 promise`agent/status``task.done`)为准,并观察完整的状态变化(先 `running`,再 `idle`),不要把状态当作逐次 `send()` 的结果:多次排队的 `send()` 会作为连续轮次运行,但可能共用一个 `running` 区间;取消或资源释放还可能丢弃尚未启动的队列项
## ③ 测试政策清单

View File

@@ -79,7 +79,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
}[T]
```
Sources: [`packages/core/session/src/types.ts:262`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:269`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:331`](../packages/core/session/src/types.ts)
Sources: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:270`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:300`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts)
## Events
@@ -151,7 +151,7 @@ Source: [`packages/ui/user-approval/src/index.ts:68`](../packages/ui/user-approv
Types: [StreamChunk](core-data-structures/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:226`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:227`](../packages/core/session/src/types.ts)
#### `assistant/message` — surface
@@ -167,7 +167,7 @@ Source: [`packages/core/session/src/types.ts:226`](../packages/core/session/src/
Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:233`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
### `compact/*`
@@ -246,7 +246,7 @@ Source: [`packages/compact/compact/src/types.ts:22`](../packages/compact/compact
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:220`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:221`](../packages/core/session/src/types.ts)
### `hook/*`
@@ -317,14 +317,14 @@ Source: [`packages/ui/permission/src/index.ts:36`](../packages/ui/permission/src
```ts persistence-catalog
/**
* Durable record of a prompt veto and its reason. It is log-only: the blocked
* prompt never enters the model-visible surface, including in a mixed batch.
* prompt never enters the model-visible surface, and its turn runs zero steps.
*/
'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string }
```
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:208`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:209`](../packages/core/session/src/types.ts)
### `request/*`
@@ -338,7 +338,7 @@ Source: [`packages/core/session/src/types.ts:208`](../packages/core/session/src/
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
Source: [`packages/core/session/src/types.ts:258`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -369,7 +369,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:34`](../packages/s
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts)
### `step/*`
@@ -380,7 +380,7 @@ Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/
'step/end': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:202`](../packages/core/session/src/types.ts)
#### `step/start` — log-only
@@ -389,7 +389,7 @@ Source: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/
'step/start': { turn: number; step: number }
```
Source: [`packages/core/session/src/types.ts:199`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:200`](../packages/core/session/src/types.ts)
### `todo/*`
@@ -402,7 +402,7 @@ Source: [`packages/core/session/src/types.ts:199`](../packages/core/session/src/
Types: [TodoItem](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:254`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -419,7 +419,7 @@ Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/
Types: [CallId](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:239`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:240`](../packages/core/session/src/types.ts)
#### `tool/code-dispatch` — log-only
@@ -463,7 +463,7 @@ Source: [`packages/core/tools/src/code-mode.ts:34`](../packages/core/tools/src/c
Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:249`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:250`](../packages/core/session/src/types.ts)
### `turn/*`
@@ -472,22 +472,23 @@ Source: [`packages/core/session/src/types.ts:249`](../packages/core/session/src/
```ts persistence-catalog
/**
* Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop
* fires the awaited `session/flush` checkpoint at every turn end, so the turn
* boundary is also the durable-commit boundary.
* awaits `session/flush` after an ordinary turn ends before claiming the next
* queued item. Success commits the turn; rejection is reported live and does
* not prevent later work.
*/
'turn/end': { turn: number; reason: TurnEndReason }
```
Types: [TurnEndReason](core-data-structures/session.md)
Source: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:198`](../packages/core/session/src/types.ts)
#### `turn/start` — log-only
```ts persistence-catalog
/**
* Opens turn `turn`. `trigger` records what started it — a drained message
* batch or an idle-time injection. The turn is the durability/replay
* Opens turn `turn`. `trigger` records what started it — one claimed queued
* message or an idle-time injection. The turn is the durability/replay
* boundary: every event sits between a `turn/start` and its matching
* `turn/end` (the turn-enclosure invariant).
*/
@@ -503,10 +504,10 @@ Source: [`packages/core/session/src/types.ts:191`](../packages/core/session/src/
#### `user/message` — surface
```ts persistence-catalog
/** A user-visible prompt (queued message drained at turn start). */
/** A user-visible prompt (the queued message claimed for this turn). */
'user/message': { content: ContentBlock[]; source: MessageSource }
```
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
Source: [`packages/core/session/src/types.ts:203`](../packages/core/session/src/types.ts)
Source: [`packages/core/session/src/types.ts:204`](../packages/core/session/src/types.ts)