Files
deepseek-harness/docs/cordis-catalog/events.md
Tianyi Cui 78276c52a7 Merge branch 'codex/simp-session-dead-surface' into codex/simp-session-log-representation
# Conflicts:
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/persistence-catalog.md
#	docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md
#	docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md
#	docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md
#	docs/rfc/implemented/feature/2026-07-06-sandbox.md
#	docs/rfc/implemented/feature/2026-07-07-session-prefix.md
#	packages/compact/compact-basic/src/index.ts
#	packages/compact/compact-basic/tests/compact-basic.spec.ts
#	packages/core/agent-loop/src/loop.ts
#	packages/core/agent-loop/src/request-log.ts
#	packages/core/agent-loop/tests/request-reconstruction.spec.ts
#	packages/core/agent/src/types.ts
#	packages/core/session/README.md
#	packages/core/session/src/request-header.ts
#	packages/core/session/src/surface.ts
#	packages/core/session/src/tool-pairing.ts
#	packages/core/session/src/types.ts
#	packages/core/session/tests/surface.spec.ts
#	packages/core/session/tests/tool-pairing.spec.ts
#	packages/llm/llm/src/call-config.ts
#	packages/support/acp-snapshot/src/suite.ts
#	packages/support/invariants/README.md
#	packages/ui/user-approval/README.md
2026-07-14 18:34:34 +08:00

31 KiB

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.<key> surface is the sibling services catalog, and core-data-structures/ 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), 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's fully composed scoped world was published in the AgentRegistry. Its session is already live in the session store. Setup is composition-only by contract; the subsequent agent/session-start boundary is the first supported place to inject or queue startup work. A synchronous listener throw vetoes publication and rollback emits the matching disposal edges; returned-promise rejection is observed and logged but cannot retroactively veto this synchronous boundary. A synchronous listener that requests the advanced registry detach does not remove the entry immediately: removal and the paired agent/disposed edge wait until the creation dispatch unwinds, so no later creation listener observes a disposal that preceded its own creation callback.

'agent/created'(this: Scoped<Agent>, agent: Agent): void

Types: Agent

Source: packages/core/agent/src/types.ts:316

agent/disposed — emit

An agent was removed from the registry. The concrete AgentLoop lifecycle emits this only after its driver and any in-flight turn reach quiescence; a custom agent registered through the public registry owns its own driver contract, which the registry cannot infer. Ordered teardown may still be detaching the session and unwinding scoped registrations when this runs.

'agent/disposed'(this: Scoped<Agent>, agent: Agent): void

Types: Agent

Source: packages/core/agent/src/types.ts:331

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.

'agent/error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: Error): void

Types: Agent

Source: packages/core/agent/src/types.ts:605

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), and sessionPrefix is the instance's composed agent/session-prefix product for the same reason — every request carries it in front of the derived history, and it is composed BEFORE this seam fires precisely so a pressure gate counts the prefix the request will actually send (never a stale logged one). signal cancels any in-flight work a listener starts (e.g. a summarization model call). Scope-filtered dispatch (@deepseek-ai/dsh-scope): a listener registered through agent.ctx fires only for that agent's dispatches; a listener on a plain plugin context fires for every agent. The dispatch this is the scope carrier (Scoped<Agent>), built by the emitting side via scopeTarget/agentEvents.

'agent/pre-step'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, fullSystemPrompt: string, sessionPrefix: readonly Message[], signal: AbortSignal): Promise<void> | void

Types: Agent · Message

Source: packages/core/agent/src/types.ts:438

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.

'agent/prompt-submit'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise<PromptDecision>): Promise<PromptDecision>

Types: Agent · ContentBlock · MessageSource

Source: packages/core/agent/src/types.ts:456

agent/queued — emit

A message entered the agent's inbox (queued or steering). Content and the resolved source are the detached, deeply-frozen values retained by the inbox. source has defaults applied and is not the caller's raw options.

'agent/queued'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void

Types: Agent · ContentBlock · MessageSource

Source: packages/core/agent/src/types.ts:360

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, or the header-logged session prefix via agent/session-prefix — 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.

'agent/request'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>

Types: Agent · LlmCallConfig

Source: packages/core/agent/src/types.ts:485

agent/session-prefix — waterfall

Waterfall: compose the SESSION PREFIX — request-only messages placed in front of the ENTIRE derived history (directly after the provider's system slot) on every request this loop instance sends. Fired ONCE per loop instance, lazily before its first step's agent/pre-step seam — BEFORE the pre-step so a token-pressure gate (compaction) counts the prefix this instance will actually send, never a previous instance's logged one. The composed result is deep-frozen, recorded as EpochHeader.messagePrefix on the instance's anchoring 'initial'/'resume' header snapshot, and reused verbatim for every subsequent request — never recomputed mid-session, so the provider prefix cache holds by construction (a process restart or ctx.agents.resume() is a new instance: it recomposes, and any drift lands attributably on the 'resume' snapshot). Composition runs outside the step, before the boundary snapshot: a composing listener's session append joins the CURRENT request's derived history. A composition interrupted by a cancel/dispose landing inside the waterfall is discarded — never cached, logged, or sent — and the next turn recomposes under a live signal, so an abort-aware listener's degraded fallback cannot leak into later requests.

This is the home for session-stable openers the model must always see but that must NOT become durable history — a skills catalog, an AGENTS.md digest, a workspace baseline: Session.deriveMessages() never returns the prefix, and the header events are its only durable record, so the request stays reconstructable from the log. Content that CHANGES mid-session belongs in the append-only history channels instead — agent.inject(), a tools/post-execute decision's additionalContext, prompt-submit additionalContext — each a durable context/message paid once and prefix-cached thereafter.

The seed is a frozen empty list; a contributing listener returns a NEW array — never an in-place push. The canonical contribution is a PREPEND, [mine, ...await next()]: the waterfall unwinds innermost-first (the LAST-registered listener's next() resolves first), so prepending yields registration order on the wire, and every plugin using it composes deterministically. The append form [...await next(), mine] is legal but places a contribution AFTER every later-registered plugin's — reverse registration order when all contributors append. Call next() to delegate, or return a list without it to short-circuit. Scope-filtered dispatch (@deepseek-ai/dsh-scope): a listener registered through agent.ctx fires only for that agent's dispatches; a listener on a plain plugin context fires for every agent. The dispatch this is the scope carrier (Scoped<Agent>), built by the emitting side via scopeTarget/agentEvents.

'agent/session-prefix'(this: Scoped<Agent>, agent: Agent, prefix: Message[], signal: AbortSignal, next: () => Promise<Message[]>): Promise<Message[]>

Types: Agent · Message

Source: packages/core/agent/src/types.ts:537

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): a listener cannot veto by returning a decision or throwing. A listener that wants to seed context does so via agent.inject() (a context/message the first request sees). A lifecycle owner can still dispose its structural ownership edge during this notification; publication rechecks liveness and then aborts before the driver starts.

'agent/session-start'(this: Scoped<Agent>, agent: Agent, source: SessionStartSource): void

Types: Agent · SessionStartSource

Source: packages/core/agent/src/types.ts:381

agent/status — emit

Agent status changed (idlerunning, or → disposed). Drive lifecycle off this transition, never off a status you just requested — send() does not flip status to running before it returns.

'agent/status'(this: Scoped<Agent>, agent: Agent, status: AgentStatus): void

Types: Agent

Source: packages/core/agent/src/types.ts:345

agent/step-result — waterfall

Waterfall: post-process the assembled assistant Message before tool dispatch (validation, content rewriting, …).

'agent/step-result'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message>

Types: Agent · Message

Source: packages/core/agent/src/types.ts:552

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.

'agent/turn-continuation'(this: Scoped<Agent>, agent: Agent, turn: number, defaultDecision: ContinuationDecision, next: () => Promise<ContinuationDecision>): Promise<ContinuationDecision>

Types: Agent

Source: packages/core/agent/src/types.ts:570

agent/turn-stop — serial

Serial terminal-stop checkpoint after the ordinary agent/turn-continuation waterfall, any continue.reason, and the pending-steering continuation override have been folded. A listener returns { action: 'stop' } to make this turn terminal, or undefined to abstain. Terminal stop is monotonic: listener order and steering cannot resume the turn, and pending steering is discarded rather than becoming another step or turn.

'agent/turn-stop'(this: Scoped<Agent>, agent: Agent, turn: number): ContinuationStop | undefined

Types: Agent

Source: packages/core/agent/src/types.ts:588

approval/*

approval/request — waterfall

Ask composed answerers for one decision. Return an outcome to claim the request or call next(); failure yields the fail-closed default. Scope-filtered dispatch (@deepseek-ai/dsh-scope): agent-scoped listeners receive only that agent.

'approval/request'(this: Scoped<ApprovalService>, req: ApprovalRequest, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>

Types: ApprovalOutcome · ApprovalRequest

Source: packages/ui/user-approval/src/index.ts:31

fs/*

fs/edit-intent — waterfall

Single-slot decision for the next FileSystem.editText. Calling next() yields an unconditional edit; the first returned guard wins.

'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>

Types: FsTarget · FsVersion

Source: packages/fs/fs/src/index.ts:59

fs/observed — emit

Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited.

'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void

Types: FsTarget · FsVersion

Source: packages/fs/fs/src/index.ts:68

fs/write-intent — waterfall

Single-slot decision for the next FileSystem.writeText. Calling next() yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers.

'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>

Types: FsTarget · FsWriteIntent

Source: packages/fs/fs/src/index.ts:51

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.

'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>

Types: GenerateOptions · StreamChunk

Source: packages/llm/llm/src/index.ts:39

session/*

session/created — emit

Creation announcement during session publication. A synchronous throw vetoes and rolls back with a paired disposal; detach requested during dispatch is deferred. A returned-promise rejection is logged but cannot retroactively veto this synchronous boundary. Scope-filtered dispatch (@deepseek-ai/dsh-scope): agent-scoped listeners receive only sessions entered through that agent's context.

'session/created'(this: Scoped<Session>, session: Session): void

Source: packages/core/session/src/index.ts:46

session/disposed — emit

Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin. Listener failures are logged and contained. Scope-filtered dispatch (@deepseek-ai/dsh-scope) reuses the owner scope.

'session/disposed'(this: Scoped<Session>, session: Session): void

Source: packages/core/session/src/index.ts:55

session/event — emit

Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (@deepseek-ai/dsh-scope): agent-scoped listeners receive only events from sessions entered through that agent's context.

'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void

Types: SessionEvent

Source: packages/core/session/src/index.ts:66

session/flush — parallel

Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Dispatch through SessionStore.flush. Scope-filtered dispatch (@deepseek-ai/dsh-scope) reuses the session's owner scope.

'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void

Source: packages/core/session/src/index.ts:75

skill/*

skill/provider-added — emit

A skill provider became resolvable in the ctx.skills registry. Consumers can observe this instead of depending on Cordis plugin load order, which is concurrent for sibling plugins.

'skill/provider-added'(provider: SkillProvider): void

Source: packages/skill/skill/src/index.ts:131

skill/provider-removed — emit

A skill provider left the registry because its plugin fiber was disposed.

'skill/provider-removed'(name: string): void

Source: packages/skill/skill/src/index.ts:137

subagent/*

subagent/end — emit

A ready child settled. Scope-filtered dispatch uses the same delegating parent carrier as subagent/start, so the lifecycle pair reaches the same scoped audience.

'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void

Source: packages/subagent/subagent/src/index.ts:90

subagent/provider-added — emit

A provider became resolvable in the registry.

'subagent/provider-added'(provider: SubagentProvider): void

Source: packages/subagent/subagent/src/index.ts:66

subagent/provider-removed — emit

A provider left the registry. Accepted runs remain holder-owned.

'subagent/provider-removed'(name: string): void

Source: packages/subagent/subagent/src/index.ts:72

subagent/start — emit

A provider established a ready child. For in-process providers, ctx.agents.get(info.id) resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with subagent/end.

'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void

Source: packages/subagent/subagent/src/index.ts:82

system-prompt/*

system-prompt/assemble — waterfall

Expert waterfall over the assembled sections, tools, and variables. Scope-filtered dispatch (@deepseek-ai/dsh-scope): scoped listeners receive only that scope's assemblies. The returned value is authoritative.

'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>

Source: packages/core/system-prompt/src/index.ts:27

system-prompt/change — emit

Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope.

'system-prompt/change'(): void

Source: packages/core/system-prompt/src/index.ts:33

tools/*

tools/change — emit

A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's.

'tools/change'(): void

Source: packages/core/tools/src/index.ts:116

tools/execute — waterfall

Around-dispatch waterfall for timeout, retry, or metrics. next() returns a normalized result; wrappers may change only exec.signal, while call identity remains immutable. Scope-filtered dispatch (@deepseek-ai/dsh-scope): agent-scoped listeners receive only that agent's calls.

'tools/execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>

Types: ToolExecution · ToolExecutionResult

Source: packages/core/tools/src/index.ts:89

tools/post-execute — waterfall

Accept, replace, enrich, or block a normalized dispatch result. next() accepts it unchanged; thrown tools still reach this seam as errors. Scope-filtered dispatch (@deepseek-ai/dsh-scope): agent-scoped listeners receive only that agent's calls.

'tools/post-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>

Types: ToolExecution · ToolExecutionResult

Source: packages/core/tools/src/index.ts:98

tools/pre-execute — waterfall

Allow, deny, or ask before dispatch. next() delegates to allow; missing approval support turns ask into denial. Scope-filtered dispatch (@deepseek-ai/dsh-scope): agent-scoped listeners receive only that agent's calls.

'tools/pre-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>

Types: ToolExecution

Source: packages/core/tools/src/index.ts:80

tools/result — emit

Observe the frozen, lossless-JSON final outcome. Listener failures are contained. Scope-filtered dispatch (@deepseek-ai/dsh-scope): keyed by exec.agent.

'tools/result'(this: Scoped<ToolRegistry>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined

Types: ToolExecution · ToolExecutionResult

Source: packages/core/tools/src/index.ts:106

workflow/*

workflow/agent-end — emit

One agent() call settled (clean result, child failure, or run cancellation). Paired with Events['workflow/agent-start'] by agent.seq, exactly once per started call on every stop path — on an engine termination path (a worker killed past its grace) the end is engine-synthesized with outcome 'cancelled'.

'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void

Source: packages/workflow/workflow/src/index.ts:81

workflow/agent-start — emit

One agent() call established a ready child run. Paired with Events['workflow/agent-end'] by agent.seq. A call that never receives a ready run from the provider emits neither event in this pair.

'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void

Source: packages/workflow/workflow/src/index.ts:70

workflow/end — emit

A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves. Paired with Events['workflow/start'].

'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void

Source: packages/workflow/workflow/src/index.ts:91

workflow/log — emit

The script emitted a narration line (a log(message) call).

'workflow/log'(info: WorkflowRunInfo, message: string): void

Source: packages/workflow/workflow/src/index.ts:60

workflow/phase — emit

The script entered a phase (a phase(title) call) — progress grouping for observers; no execution semantics.

'workflow/phase'(info: WorkflowRunInfo, title: string): void

Source: packages/workflow/workflow/src/index.ts:53

workflow/start — emit

A workflow run started — the script's meta block validated, the body about to execute. Paired with Events['workflow/end'].

'workflow/start'(info: WorkflowRunInfo): void

Source: packages/workflow/workflow/src/index.ts:45

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); 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.