refactor(events): document event-domain semantics, drop step-boundary mirror emits

Pin the three-domain rule (session = durable fact log, agent = live runtime
surface, tools = registry/exec): a durable replayable fact is a SessionEvent; a
live interception or transient/live-object signal is an agent/tools Cordis
event. A boundary that is both is mirrored as an agent/* emit ONLY where a live
consumer needs the Agent handle.

Apply it to the boundary twins: drop agent/step-start and agent/step-end (no
production consumer needs the live Agent at a step boundary — consumers read the
durable step/start/step/end session events). Keep agent/turn-start/turn-end (the
stdio UI labels output by agent.id). Tests that observed step boundaries via the
removed emits now observe the durable session events; the pinned behavior is
unchanged.

Conservative subset of the proposed "remove boundary mirror events"
simplification; foundation for the Hooks subsystem's canonical event surface.
This commit is contained in:
Tianyi Cui
2026-06-30 10:32:55 +08:00
parent 3f85f522ea
commit 05b75abbca
9 changed files with 176 additions and 151 deletions

View File

@@ -6,6 +6,34 @@
* Merge-extensible: `AgentOptions` supports declaration merging for
* plugin-specific creation options.
*
* ## Event-domain semantics (the boundary rule)
*
* The harness has three event domains, each with one job:
*
* - **`session/*`** (`@deepseek-ai/dsh-session`) — the DURABLE, replayable FACT
* log. Owns `SessionEventMap`; every entry is JSON-only (no live objects).
* One `session/event` emit per append, plus the `session/flush` parallel
* durability checkpoint. Answers "what happened, durably/replayably." A
* consumer that wants the live transcript subscribes here.
* - **`agent/*`** (this module) — the LIVE runtime surface. Always carries the
* live `Agent`. Two shapes: INTERCEPTION waterfalls (`agent/request`,
* `agent/step-result`, `agent/turn-continuation`) that mutate/veto, and
* TRANSIENT emits (`agent/status`, `agent/stream-chunk`, `agent/error`,
* `agent/created`/`agent/disposed`, `agent/queued`, `agent/steering`, and the
* turn boundaries) that notify with the `Agent` in hand. Answers "right now,
* with the agent object — intercept or observe."
* - **`tools/*`** (`@deepseek-ai/dsh-tools`) — the tool registry + execution.
*
* **The rule:** a durable, replayable fact is a SessionEvent; a live
* interception or a transient/live-object signal is an `agent`/`tools` Cordis
* event. A datum that is BOTH (a turn/step boundary) lives in the session log,
* and is mirrored as an `agent/*` emit ONLY where a live consumer provably
* needs the `Agent` handle at that instant. Turn boundaries are so mirrored
* (the stdio UI labels output by `agent.id`); step boundaries are NOT (no live
* consumer needs them — read `step/start`/`step/end` from the session log).
* See `docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md`
* and the related `docs/rfc/proposed/simplification/2026-06-20-remove-agent-boundary-mirror-events.md`.
*
* @module @deepseek-ai/dsh-agent/types
*/
@@ -155,29 +183,25 @@ declare module 'cordis' {
*/
'agent/queued'(agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void
// ---- turn/step boundaries (emit) ----
// ---- turn boundaries (emit) — the live boundary surface ----
// Step boundaries are NOT mirrored here: a consumer that needs per-step
// boundaries reads the durable `step/start`/`step/end` session events (the
// session log is the live transcript feed). The TURN boundaries stay as
// agent/* emits because the only live consumer (the stdio UI) needs the
// `Agent` handle at the boundary to label output, which the session event
// does not carry. See the module doc's three-domain rule.
/**
* A turn began. `turn` is the 1-based turn number within the session.
* @mode emit
*/
'agent/turn-start'(agent: Agent, turn: number): void
/**
* A turn ended. `reason` distinguishes a clean stop from a truncated or
* aborted one (`completed` | `aborted` | `error` | `disposed` | `max-tokens`).
* A turn ended. `reason` distinguishes a clean stop from a truncated,
* aborted, or hook-rejected one (`completed` | `aborted` | `error` |
* `disposed` | `max-tokens` | `rejected` | `interrupted`).
* @mode emit
*/
'agent/turn-end'(agent: Agent, turn: number, reason: TurnEndReason): void
/**
* A step (one model call plus its tool dispatch) began. `step` is 1-based
* within the turn; a turn runs one or more steps.
* @mode emit
*/
'agent/step-start'(agent: Agent, turn: number, step: number): void
/**
* A step ended.
* @mode emit
*/
'agent/step-end'(agent: Agent, turn: number, step: number): void
// ---- interception seams (waterfall) ----
/**