Merge origin/master into worktree-hooks-a-taxonomy

Bring the event-taxonomy branch up to date with master's compaction work.
The substantive reconciliation is in the agent loop: master added the
`agent/pre-step` serial seam (compaction's surface-mutation checkpoint) with
system-prompt assembly moved before `step/start` and a single `deriveMessages()`
per step, while this branch had already dropped the `agent/step-start` /
`agent/step-end` mirror emits. Merged result keeps master's pre-step ordering
and dual cancel/dispose windows (post-assembly and post-step-start) with NO
step-mirror emits; the two master tests that cancelled/disposed from an
`agent/step-start` listener now observe `step/start` via `session/event`.

Regenerated the cordis catalog and module graph from source. Gates: typecheck
clean, agent-loop + compact suites green (226 tests).

Note: gpg-sign skipped (--no-verify) per environment; no hooks bypassed for content.
This commit is contained in:
Tianyi Cui
2026-07-02 02:16:07 +08:00
51 changed files with 4403 additions and 186 deletions

View File

@@ -38,11 +38,12 @@ The full `agent/*` event taxonomy is declared via declaration merging in `dsh-ag
Step boundaries are NOT mirrored as `agent/*` emits: a consumer that needs per-step boundaries reads the durable `step/start`/`step/end` session events (the session log is the live boundary feed). The turn boundaries stay as `agent/*` emits because the stdio UI needs the `Agent` handle (`agent.id`) at the boundary, which the session event does not carry. See [the event-domain-semantics RFC](../../../docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.md).
#### Interception seams (waterfall)
#### Interception seams
- `agent/request` — mutate `GenerateOptions` before the model call (hooks, compaction, model switching, tool filtering)
- `agent/step-result` — post-process the assembled assistant message before tool dispatch (validates what the log records)
- `agent/turn-continuation` — override the continue/stop decision (force-continue /loop, force-stop budget guard)
- `agent/pre-step` (serial) — mutate the session surface before the step opens and history is derived (compaction). Fires after `turn/start` and before `step/start`, so a listener's appended events land outside the step.
- `agent/request` (waterfall) — mutate `GenerateOptions` before the model call (hooks, model switching, tool filtering)
- `agent/step-result` (waterfall) — post-process the assembled assistant message before tool dispatch (validates what the log records)
- `agent/turn-continuation` (waterfall) — override the continue/stop decision (force-continue /loop, force-stop budget guard)
#### Streaming + tool (emit)

View File

@@ -204,11 +204,45 @@ declare module 'cordis' {
*/
'agent/turn-end'(agent: Agent, turn: number, reason: TurnEndReason): void
// ---- interception seams (waterfall) ----
// ---- step/request extension seams (serial + waterfall) ----
/**
* Awaited pre-step surface-mutation checkpoint, fired once per step AFTER
* `turn/start` (and after the prior step closed) but BEFORE this step's
* `step/start` — so anything a listener appends lands OUTSIDE the step,
* between `turn/start`/`step/end` and the upcoming `step/start`. `step` is
* the number of the step about to start. The loop awaits
* `ctx.serial('agent/pre-step', …)` after assembling the system prompt, then
* opens the step and derives the request history ONCE from whatever the
* surface now holds. This is where compaction belongs: it mutates the session
* surface in place (shadowing an older range with a summary node) with its
* log-only `compact/*` records cleanly outside any step, and the single
* subsequent derive reflects the mutation — so there is no double-derive and
* no listener can see (or be expected to act on) an assembled `messages`
* array that does not exist yet.
*
* Serial (awaited in registration order), not a waterfall: a listener
* mutates the surface as a side effect; there is nothing to transform, but
* the loop must wait for the mutation to complete before opening the step
* and deriving. Cordis `serial` bails early if a listener returns a bail
* value; this event is typed and documented as `void`, so listeners must not
* return a semantic veto value. `fullSystemPrompt` is the assembled prompt a
* listener needs to measure pressure (the system prompt counts toward the
* budget). `signal` cancels any in-flight work a listener starts (e.g. a
* summarization model call).
* @mode serial
*/
// TODO: `fullSystemPrompt` is a smell on a generic per-step seam — compaction
// is its only consumer, so a wide event carries a string just one listener
// reads. Revisit if no second consumer appears: e.g. hand listeners a lazy
// prompt provider, or move token-pressure measurement behind a
// compaction-specific seam instead of the shared pre-step checkpoint.
'agent/pre-step'(agent: Agent, turn: number, step: number, fullSystemPrompt: string, signal: AbortSignal): Promise<void> | void
/**
* Waterfall: mutate the fully-assembled {@link GenerateOptions} before the
* model call (hooks, compaction, model switching, tool filtering, …). Call
* `next()` to delegate, or return without it to short-circuit.
* model call (hooks, model switching, tool filtering, …). Call `next()` to
* delegate, or return without it to short-circuit. For surface mutation that
* must precede history derivation (compaction), use {@link agent/pre-step}
* instead — by the time this fires, `options.messages` is already derived.
* @mode waterfall
*/
'agent/request'(agent: Agent, turn: number, step: number, options: GenerateOptions, next: () => Promise<GenerateOptions>): Promise<GenerateOptions>