# Cordis Events Catalog Every cordis event a plugin can listen to: exact signature, dispatch mode, and the declaration's JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence (skipped by doc-typecheck, since a bare signature is not standalone-compilable). Type names in a signature link to the page that documents them. The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`). ## `agent/*` ### `agent/created` — emit An agent was registered in the AgentRegistry and is ready to receive messages. ```ts cordis-catalog 'agent/created'(agent: Agent): void ``` Types: [Agent](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:264`](../../packages/core/agent/src/types.ts) ### `agent/disposed` — emit An agent was disposed and removed from the registry; its fiber and any in-flight turn have been torn down. ```ts cordis-catalog 'agent/disposed'(agent: Agent): void ``` Types: [Agent](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:271`](../../packages/core/agent/src/types.ts) ### `agent/error` — emit A step or turn errored. The loop reports a failure here (plus the logger) even when the error has no in-turn position for a session `error` event. ```ts cordis-catalog 'agent/error'(agent: Agent, turn: number, step: number, error: Error): void ``` Types: [Agent](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:420`](../../packages/core/agent/src/types.ts) ### `agent/pre-step` — serial 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). ```ts cordis-catalog 'agent/pre-step'(agent: Agent, turn: number, step: number, fullSystemPrompt: string, signal: AbortSignal): Promise | void ``` Types: [Agent](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:349`](../../packages/core/agent/src/types.ts) ### `agent/prompt-submit` — waterfall Waterfall: decide what happens to ONE drained queued message before it becomes a `user/message` — allow (optionally rewriting the prompt bytes or attaching `additionalContext`) or block it. Fires inside the already-open turn, per drained message. Maps onto Claude Code's `UserPromptSubmit` hook. Call `next()` to delegate to the default (allow unchanged), or return a PromptDecision without calling `next()` to short-circuit. ```ts cordis-catalog 'agent/prompt-submit'(agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise): Promise ``` Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:362`](../../packages/core/agent/src/types.ts) ### `agent/queued` — emit A message entered the agent's inbox (queued or steering). `source` is the resolved source (defaults applied), not the caller's raw options. ```ts cordis-catalog 'agent/queued'(agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void ``` Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:289`](../../packages/core/agent/src/types.ts) ### `agent/request` — waterfall Waterfall: shape the step's call configuration — model switching, sampling overrides — by returning a replacement LlmCallConfig (the frozen seed is the config the loop would otherwise use). Config is ALL a listener shapes here: every request is a pure function of the session log (the reconstructability RFC), so model-visible content flows through the log channels — `inject()`, steering, prompt-submit `additionalContext`, prompt sections via `system-prompt/assemble` — never through request mutation, and the loop records whatever config the request actually uses as a `request/header*` event before dispatch. The step's messages are already snapshotted when this fires (the `step/start` boundary): an `inject()` from a listener here lands in the log but joins the NEXT request. For surface mutation that must precede the snapshot (compaction), use agent/pre-step. Call `next()` to delegate, or return an LlmCallConfig without it to short-circuit. ```ts cordis-catalog 'agent/request'(agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise): Promise ``` Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:385`](../../packages/core/agent/src/types.ts) ### `agent/session-start` — emit The agent's session lifecycle began, fired once before its first turn. `source` says why (SessionStartSource: fresh startup, a resumed persisted session, …). A pure NOTIFICATION (emit, not waterfall): it carries no veto — a session-start listener that wants to seed context does so via `agent.inject()` (a `context/message` the first request sees), not by returning a decision. Cannot block the session from starting; that gap is deliberate (a bridge logs/injects, it does not gate startup). ```ts cordis-catalog 'agent/session-start'(agent: Agent, source: SessionStartSource): void ``` Types: [Agent](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:304`](../../packages/core/agent/src/types.ts) ### `agent/status` — emit Agent status changed (`idle` ⇄ `running`, or → `disposed`). Drive lifecycle off this transition, never off a status you just requested — `send()` does not flip status to `running` before it returns. ```ts cordis-catalog 'agent/status'(agent: Agent, status: AgentStatus): void ``` Types: [Agent](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:280`](../../packages/core/agent/src/types.ts) ### `agent/step-result` — waterfall Waterfall: post-process the assembled assistant Message before tool dispatch (validation, content rewriting, …). ```ts cordis-catalog 'agent/step-result'(agent: Agent, turn: number, step: number, message: Message, next: () => Promise): Promise ``` Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:395`](../../packages/core/agent/src/types.ts) ### `agent/turn-continuation` — waterfall Waterfall: override the turn-continuation decision via a typed ContinuationDecision. The loop's `defaultDecision` is `continue` when the step had tool calls or steering was injected, else `stop`. Listeners force-continue (`/goal`, `/loop` — optionally attaching a `reason` recorded as next-step steering) or force-stop (budget guards). Call `next()` to delegate to the default, or return a decision to override. ```ts cordis-catalog 'agent/turn-continuation'(agent: Agent, turn: number, defaultDecision: ContinuationDecision, next: () => Promise): Promise ``` Types: [Agent](../core-data-structures/core.md) Source: [`packages/core/agent/src/types.ts:408`](../../packages/core/agent/src/types.ts) ## `fs/*` ### `fs/edit-intent` — waterfall Single-slot decision: produce the optional version guard for the next FileSystem.editText. The tool dispatches this as an unbound waterfall and supplies a default thunk returning `undefined` (unconditional edit of the current content — the bare provider; no `stat`). The `@deepseek-ai/dsh-fs-policy` policy listener returns `{ version: vObserved }`, or throws `FS_NOT_OBSERVED` if the actor is unset or has not observed the target. Does NOT call `next()`: one decision, first-wins (see Events.'fs/write-intent'). ```ts cordis-catalog 'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined> ``` Types: [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md) Source: [`packages/fs/fs/src/index.ts:123`](../../packages/fs/fs/src/index.ts) ### `fs/observed` — emit Record that an actor observed a target at a version, after a successful read/write/edit. Fire-and-forget (plain `emit`). A listener MUST be a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s is a `WeakMap.set`): the tool does not guard the emit, so a listener that throws surfaces as the tool's `isError` result, and cordis `emit` does not await listener promises — async or fallible audit/telemetry does not belong here. No listener ⇒ nothing recorded. `actor` is the opaque tool-execution context. ```ts cordis-catalog 'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void ``` Types: [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md) Source: [`packages/fs/fs/src/index.ts:138`](../../packages/fs/fs/src/index.ts) ### `fs/write-intent` — waterfall Single-slot decision: produce the write intent for the next FileSystem.writeText. The tool dispatches this as an unbound waterfall (no `this`) and supplies a default thunk returning `undefined` (unconditional create-or-overwrite — the bare provider). The `@deepseek-ai/dsh-fs-policy` policy listener returns `createIfAbsent` (unobserved actor) or `{ kind: 'replaceIfVersion', version: vObserved }` (observed) and does NOT call `next()` — one decision, not a composable chain. The slot is first-wins: the first non-`next()` decider (registration order, or `prepend`) occupies it; a second decider is a misconfiguration, not layering. `actor` is the opaque tool-execution context, never read here. ```ts cordis-catalog 'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise): Promise ``` Types: [FsTarget](../core-data-structures/filesystem.md) · [FsWriteIntent](../core-data-structures/filesystem.md) Source: [`packages/fs/fs/src/index.ts:109`](../../packages/fs/fs/src/index.ts) ## `llm/*` ### `llm/stream` — waterfall Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit. ```ts cordis-catalog 'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable): AsyncIterable ``` Types: [GenerateOptions](../core-data-structures/core.md) · [StreamChunk](../core-data-structures/llm-streaming.md) Source: [`packages/llm/llm/src/index.ts:39`](../../packages/llm/llm/src/index.ts) ## `session/*` ### `session/created` — emit A session was created in the store. ```ts cordis-catalog 'session/created'(session: Session): void ``` Source: [`packages/core/session/src/index.ts:39`](../../packages/core/session/src/index.ts) ### `session/event` — emit An event was appended to a session log (sync, fire-and-forget). This is the per-append feed a UI or invariant plugin tails. ```ts cordis-catalog 'session/event'(session: Session, event: SessionEvent): void ``` Types: [SessionEvent](../core-data-structures/core.md) Source: [`packages/core/session/src/index.ts:47`](../../packages/core/session/src/index.ts) ### `session/flush` — parallel Awaited durability checkpoint. The agent loop awaits `ctx.parallel('session/flush', session)` at every turn end; persistence plugins (JSONL, SQLite) drain their write-behind buffers here and on fiber dispose. Awaited (parallel), not a waterfall: every listener runs and the loop waits for all of them, but none can veto. ```ts cordis-catalog 'session/flush'(session: Session): Promise | void ``` Source: [`packages/core/session/src/index.ts:57`](../../packages/core/session/src/index.ts) ## `subagent/*` ### `subagent/end` — emit A subagent run settled — emitted when SubagentRun.result resolves (any stop reason). Paired with Events['subagent/start']. ```ts cordis-catalog 'subagent/end'(info: SubagentRunEndInfo): void ``` Source: [`packages/subagent/subagent/src/index.ts:98`](../../packages/subagent/subagent/src/index.ts) ### `subagent/provider-added` — emit A provider became resolvable in the SubagentService registry. Consumers that derive state from a named provider (e.g. the model-facing tool wording in `dsh-tool-subagent`) react HERE instead of assuming load order — the cordis Loader starts sibling plugins concurrently, so "listed earlier in cordis.yml" does not mean "registered earlier". ```ts cordis-catalog 'subagent/provider-added'(provider: SubagentProvider): void ``` Source: [`packages/subagent/subagent/src/index.ts:72`](../../packages/subagent/subagent/src/index.ts) ### `subagent/provider-removed` — emit A provider left the registry (its plugin's fiber was disposed — an unload or an HMR reload). Consumers holding provider-derived state drop it here; a reload re-fires `subagent/provider-added` with the fresh provider. Delivered with per-listener containment: a throwing subscriber is logged, never starves later subscribers, and never disrupts the provider's teardown. ```ts cordis-catalog 'subagent/provider-removed'(name: string): void ``` Source: [`packages/subagent/subagent/src/index.ts:83`](../../packages/subagent/subagent/src/index.ts) ### `subagent/start` — emit A subagent run started — emitted after the provider is resolved and its capabilities validated, as the child run begins. Paired with Events['subagent/end']. ```ts cordis-catalog 'subagent/start'(info: SubagentRunInfo): void ``` Source: [`packages/subagent/subagent/src/index.ts:91`](../../packages/subagent/subagent/src/index.ts) ## `system-prompt/*` ### `system-prompt/assemble` — waterfall Waterfall around prompt assembly — mutate or extend the PromptAssembly (sections + tools + variables) before it is rendered. Bound to the SystemPrompt service; call `next()` to delegate. ```ts cordis-catalog 'system-prompt/assemble'(this: SystemPrompt, assembly: PromptAssembly, context: AssembleContext, next: () => Promise): Promise ``` Source: [`packages/core/system-prompt/src/index.ts:38`](../../packages/core/system-prompt/src/index.ts) ### `system-prompt/change` — emit A section, tool provider, or variable provider was registered or unregistered (the assembly inputs changed). ```ts cordis-catalog 'system-prompt/change'(): void ``` Source: [`packages/core/system-prompt/src/index.ts:44`](../../packages/core/system-prompt/src/index.ts) ## `tools/*` ### `tools/change` — emit A tool was registered or unregistered (the available tool set changed). ```ts cordis-catalog 'tools/change'(): void ``` Source: [`packages/core/tools/src/index.ts:118`](../../packages/core/tools/src/index.ts) ### `tools/execute` — waterfall Around-dispatch waterfall wrapping the registry's core tool dispatch, between the `tools/pre-execute` gate and the `tools/post-execute` seam. A listener receives `(exec, next)`: call `next()` to delegate to dispatch (returning its ToolExecutionResult, optionally wrapped), or return a replacement result without calling `next()` to short-circuit dispatch. The base `next()` IS the dispatch-with-normalization thunk — a thrown tool (or unknown tool) is already normalized to an `isError` result by the time a listener's `await next()` returns, so a wrapper never sees a raw throw from the tool body. This is the seam a timeout/retry/metrics plugin wraps: it can mutate `exec` (e.g. replace `exec.signal` with a per-call deadline) BEFORE `next()` and inspect the result AFTER. (Cordis `next()` ignores any passed arguments and re-invokes downstream with the shared payload, so a wrapper mutates `exec` in place rather than passing a new object to `next()`.) Multiple listeners compose by registration order — an outer one wraps the inner ones plus dispatch. ```ts cordis-catalog 'tools/execute'(this: ToolRegistry, exec: ToolExecution, next: () => Promise): Promise ``` Types: [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) Source: [`packages/core/tools/src/index.ts:97`](../../packages/core/tools/src/index.ts) ### `tools/post-execute` — waterfall Waterfall AFTER a tool runs — where hook plugins inspect the result and accept it (optionally REPLACING the model-facing content, and/or attaching `additionalContext` for the next request) or block it with corrective `feedback` (Claude Code's `PostToolUse`). Listeners receive `(exec, result, next)`: call `next()` to delegate to the default (accept unchanged), or return a PostToolDecision to override. Core tool dispatch runs earlier as the base `next()` of the `tools/execute` waterfall, all inside `execute`'s outer try/catch (and the tool body keeps its own inner try/catch, so a thrown tool still reaches `post-execute` as an `isError` result). ```ts cordis-catalog 'tools/post-execute'(this: ToolRegistry, exec: ToolExecution, result: ToolExecutionResult, next: () => Promise): Promise ``` Types: [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) Source: [`packages/core/tools/src/index.ts:113`](../../packages/core/tools/src/index.ts) ### `tools/pre-execute` — waterfall Waterfall BEFORE a tool runs — the gate where sandbox, permission, and hook plugins allow or deny a call (Claude Code's `PreToolUse`). Listeners receive `(exec, next)`: call `next()` to delegate to the default (allow), or return a PreToolDecision without calling `next()` to short-circuit. A `deny` skips dispatch and yields an `isError` result; the tool body never runs. Input rewrite is deliberately NOT offered here (see PreToolDecision); `ask` degrades to deny until the permission system lands (`FIXME(permissions)`). ```ts cordis-catalog 'tools/pre-execute'(this: ToolRegistry, exec: ToolExecution, next: () => Promise): Promise ``` Types: [ToolExecution](../core-data-structures/tools.md) Source: [`packages/core/tools/src/index.ts:77`](../../packages/core/tools/src/index.ts) ## Inherited events (cordis core + loader/hmr/timer) The framework events every plugin also sees, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of the event bus, without elevating framework internals to the harness tier's prominence. - `internal/plugin` — A plugin fiber was created. ([`vendor/cordis/src/events.ts:197`](../../vendor/cordis/src/events.ts)) - `internal/status` — A fiber changed lifecycle state. ([`vendor/cordis/src/events.ts:198`](../../vendor/cordis/src/events.ts)) - `internal/service` — Interception hook for a service binding (no core producer). ([`vendor/cordis/src/events.ts:199`](../../vendor/cordis/src/events.ts)) - `internal/update` — Waterfall: a fiber config update is being applied. ([`vendor/cordis/src/events.ts:200`](../../vendor/cordis/src/events.ts)) - `internal/get` — Waterfall: a service is being read from the store. ([`vendor/cordis/src/events.ts:201`](../../vendor/cordis/src/events.ts)) - `internal/set` — Waterfall: a service is being written to the store. ([`vendor/cordis/src/events.ts:202`](../../vendor/cordis/src/events.ts)) - `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:203`](../../vendor/cordis/src/events.ts)) - `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:204`](../../vendor/cordis/src/events.ts)) - `hmr/change` — A watched source file changed on disk. ([`vendor/hmr/src/index.ts:20`](../../vendor/hmr/src/index.ts)) - `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:21`](../../vendor/hmr/src/index.ts)) - `exit` — The process is exiting on a signal. ([`vendor/loader/src/index.ts:23`](../../vendor/loader/src/index.ts)) - `loader/config-update` — The loader config tree changed. ([`vendor/loader/src/index.ts:24`](../../vendor/loader/src/index.ts)) - `loader/entry-init` — A config entry is being initialized. ([`vendor/loader/src/index.ts:25`](../../vendor/loader/src/index.ts)) - `loader/partial-dispose` — An entry is being partially disposed on reload. ([`vendor/loader/src/index.ts:26`](../../vendor/loader/src/index.ts)) - `loader/patch-context` — A context is being patched during a reload. ([`vendor/loader/src/index.ts:27`](../../vendor/loader/src/index.ts))