core.md doubled as the folder index: its intro claimed "this folder catalogs the data structures" and carried the 38-row page table, wording that predates the one-page-per-subsystem shape where every page also carries its generated Cordis surface. The folder index now lives in docs/subsystems/README.md (page table plus the type-equiv note), and core.md is one subsystem page among siblings: the spine vocabulary. Structural referents move with it: the docs/AGENTS.md tier table and update rule, development.md's type-equiv pointer, the dsh-code-review skill, the two owning catalog Agent Notes, and website/docs.ts (README projects as reference/subsystems/index.md and takes the docs/subsystems folder alias; sidebar orders shift by one). Remaining "data-structure catalog" / "sub-page" phrasing in active notes and READMEs is reworded to subsystem-page terms in both languages; touched pairs re-recorded; translation-prompt snapshot re-recorded (its example embeds development.md).
61 KiB
核心数据结构
English | 中文
本页编目 DeepSeek Harness 的核心数据结构:每个主干类型代表什么、它的字面形状,以及完整细节在哪里。它与 architecture.md 互补——后者描述行为(服务映射、会话/轮次/步骤生命周期、事件分类体系);本页描述行为所操作的词汇。同级的各子系统页面见目录 README。
什么算"核心"
harness 是一个微内核:一个极小的核心加上众多插件。大多数类型属于某一个插件或某一项能力。但有少数类型构成主干——agent loop(智能体循环)及其事件在每一个轮次中使用的语言,无论加载了哪些可选插件。这些就是"核心"。
精确地说,一个数据结构是核心的,当且仅当满足以下条件之一:
- 它流经 agent loop 主干——循环在每个轮次中持有、派生、流式输出或记录它(
Message、StreamChunk、SessionEvent、Agent句柄本身),与当前加载了哪些插件无关;或者 - 它是插件作者面向某条流水线编写的代表性类型——
ToolDefinition(每个工具是什么)。
其他一切都记录在各自的子系统页面上,而非本页。划线的规则是:你编写、持有或接收的类型是核心;为它提供类型推导、渲染或持久化的机制是其他页面的细节。因此 ToolDefinition 是核心,但为它提供类型推导的 ValueSchemaSpec/ParameterSchemaSpec 机制、为它提供渲染意图的 ToolCallView/ToolResultView 词汇,以及存储事件日志的 SessionPersistence seam 都不是——它们各有自己的页面(目录)。
…Map → derived-union 模式
harness 中几乎所有可扩展的和类型都遵循同一形状:一个以判别标签为键的接口(…Map),联合类型由 keyof 派生。插件通过声明合并添加变体——无需修改拥有该类型的包(package)。
// The pattern, schematically:
interface ThingMap {
'a': { kind: 'a'; /* … */ }
'b': { kind: 'b'; /* … */ }
}
type ThingKind = keyof ThingMap // 'a' | 'b'
type Thing = ThingMap[keyof ThingMap] // the discriminated union
// A plugin extends it without touching the source package:
declare module '@deepseek-ai/dsh-llm' {
interface ThingMap {
'c': { kind: 'c'; /* … */ }
}
}
五个规范 map 使用此模式;插件作者扩展它们:
| Map | 包 | 派生 | 目录 |
|---|---|---|---|
ContentBlockMap |
dsh-llm | ContentBlock |
下文 |
MessageSourceMap |
dsh-llm | MessageSource |
下文 |
FinishReasonMap |
dsh-llm | FinishReason |
下文 |
TurnEndReasonMap |
dsh-session | TurnEndReason |
session.md |
SessionEventMap |
dsh-session | SessionEvent |
session.md |
消费方最常 switch 的两个大型判别联合类型是:StreamChunk(流式协议)和 SessionEvent(日志条目)。按仓库约定,对标签做 switch——不要链式 if——这样每个分支都能窄化类型,拼错的标签会编译失败。
品牌化 ID
跨越包边界的 ID 都经过品牌化——结构上是字符串,但在类型层面不可互换(不能把 SessionId 传给需要 CallId 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。
Branded<B> 原语位于独立的纯类型包 dsh-brand 中(没有运行时代码,也不依赖 Harness 包),因此任何包都能品牌化其拥有的 id,而无需依赖无关的能力包。
源码:packages/util/brand/src/index.ts
/** A string carrying a compile-time-only brand `B`. */
type Branded<B extends string> = string & { readonly [BRAND]: B }
两个核心 ID 是 CallId(关联工具调用及其结果;dsh-llm)和 SessionId(活跃 agent 与持久会话共享的标识;dsh-session)。能力包也会品牌化各自的 id,例如 tasks.md 中的 TaskId。
内容块与消息
一段对话由 Message 组成;一条消息是一个类型化内容块的数组。块的联合类型从 ContentBlockMap 派生。
源码:packages/llm/llm/src/types.ts
/**
* Merge-extensible content blocks keyed by `type`. New core blocks must land
* with adapter, UI, and compaction support.
*/
interface ContentBlockMap {
'text': TextBlock
'reasoning': ReasoningBlock
'tool-call': ToolCallBlock
'tool-result': ToolResultBlock
}
各块接口(完整字段见源码):TextBlock(text)、ReasoningBlock(thinking,区别于可见文本)、ToolCallBlock(id: CallId、name、原始 JSON arguments)、ToolResultBlock(toolCallId、嵌套 content: ContentBlock[]、isError?)。ContentBlock = ContentBlockMap[ContentBlockType]。核心集仅限于每条交付路径都尊重的块——多模态内容(图像、音频等)没有核心块类型;需要的功能通过可合并扩展的 map 添加,同时提供适配器/UI/压缩支持。
源码:packages/llm/llm/src/message.ts
Message 是一个带标识且不可变的角色/来源/内容值。模型产生的 assistant 消息会在其来源中携带提供方/模型所有权与可选的适配器私有回放元数据:
/** Provider ownership and adapter-private replay data for an assistant message. */
interface AssistantProvenance {
/** Provider route that produced the message. */
provider: string
/** Provider model id that produced the message. */
model: string
/**
* Lossless-JSON adapter state needed to replay the provider response.
* `LlmService` exposes it to a target adapter only when that adapter instance
* currently owns both this historical provider and the target provider.
*/
replayState?: unknown
}
/** One immutable message representation shared by delivery, durable history, and model requests. */
interface Message {
/** Stable identity preserved across every representation boundary. */
readonly id: MessageId
/** Provider-neutral conversation role. */
readonly role: 'system' | 'user' | 'assistant'
/** Exact model-facing blocks. */
readonly content: ContentBlock[]
/** Required producer provenance. */
readonly source: MessageSource
}
消息来源本身也是一个可合并扩展的和类型:
/**
* Where a message (or injected content) came from.
* Merge-extensible sum type — plugins add their own `kind`s.
*/
interface MessageSourceMap {
user: { kind: 'user' }
plugin: { kind: 'plugin'; plugin: string } & ContextFormed
model: ModelMessageSource
tool: ToolMessageSource
}
溯源与形态是相互独立的两根轴。kind 回答「由谁产生」;生产方可选混入的 form 回答「这是何种形态的信息」,因此多个生产方可以共用一种呈现,一个生产方在一次会话中也可以发出多种形态。该词汇表是语义的,逐个取值增长;未声明或无法识别的取值是有文档的默认,按不透明内容呈现:
/**
* What SHAPE of information a producer-supplied context carries, declared by
* the producer beside its provenance.
*
* `MessageSource.kind` answers *who produced this*; `form` answers *what kind
* of thing it is*, and the two axes are deliberately independent — several
* producers share one form (three snapshot producers today), and one producer
* may emit more than one form over a session.
*
* The vocabulary is SEMANTIC, never visual: a value states that the content is
* a file's instructions or a catalog of available items, and a consumer decides
* what that looks like. Colors, icons, ordering, and collapse defaults are the
* consumer's business and must not enter this union. It grows one value at a
* time as producers gain the structured fields their form needs; an absent or
* unknown value is the documented default, presented as opaque content.
*/
type ContextForm =
/** Instructions read out of workspace files the model is expected to follow. */
| 'instructions'
/** A catalog of items available in this session, republished as it changes. */
| 'catalog'
/** Current state, where a later snapshot from the same producer supersedes an earlier one. */
| 'snapshot'
/** A one-off account of something that just happened; it supersedes nothing. */
| 'notice'
/** A message another agent addressed to this one. */
| 'relay'
/** Material lifted out of another session's log, possibly reduced on the way in. */
| 'recall'
/** One named contribution to a `snapshot`-form context, in assembly order. */
interface ContextSnapshotSection {
/** The contributing subsystem's name. */
readonly name: string
/** That contribution's model-facing text, exactly as assembled. */
readonly text: string
}
/**
* Producer-declared {@link ContextForm} and the fields that form requires,
* mixed into the source shapes that carry one.
*
* Discriminated by `form` so a producer cannot declare a shape without the
* facts that shape is presented from: a `notice` must record its one-line
* account, a `snapshot` its sections. Omitting `form` stays valid — an
* undeclared context is the documented default.
*/
type ContextFormed =
| { readonly form?: never }
| { readonly form: 'instructions' }
| { readonly form: 'catalog' }
| {
readonly form: 'snapshot'
/** The named contributions this snapshot assembled, in order. */
readonly sections: readonly ContextSnapshotSection[]
}
| {
readonly form: 'notice'
/** One-line account of what happened, shown without expanding the row. */
readonly summary: string
}
| { readonly form: 'relay' }
| { readonly form: 'recall' }
流式输出
适配器发出原始分片协议;循环记录分片(回放保真度),同时将同一批分片送入 BlockAssembler 以重建块和消息。StreamChunk 是基于 type 的封闭判别联合——block-start、text-delta、reasoning-delta、tool-call-delta、block-end、usage、finish。
完整联合类型、适配器契约(usage-before-finish、原始 JSON 工具参数、两条认可的错误路径)和 BlockAssembler 在 llm-streaming.md 中。
模型请求
一次模型调用是一个完全组装好的 GenerateOptions。适配器以原始 StreamChunk 流作答;消费方用 BlockAssembler 组装它(见 llm-streaming.md)。
源码:packages/llm/llm/src/types.ts
提供方与模型发现使用小型、提供方无关的描述符。模型目录仅供参考:路由仍以已注册提供方为键,适配器也可以接受未列出的模型 id。
注册适配器会返回一个句柄:既是释放器,也带有原子的路由替换——路由集合由用户配置决定的插件正需要它。
/**
* What {@link LlmService.registerAdapter} returns: the disposer, plus an
* atomic route replacement for the same adapter instance.
*/
interface AdapterRegistrationHandle {
/** Release every route this registration currently holds. */
(): void
/**
* Replace this registration's routes with `providers`, keeping the same
* adapter instance. The candidate set is validated in full first — a
* conflict with another adapter, an invalid name, or bad provider metadata
* throws and leaves the current routes untouched — and the swap itself is
* one synchronous section, so no request can observe a gap. An empty array
* is legal here (a settings section that emptied holds zero routes while
* staying registered), unlike an empty initial registration.
*
* Throws `LlmError` with code `REGISTRATION_DISPOSED` once the registration
* has been released: its routes are gone and its disposer has already run,
* so anything registered afterwards would have no owner left to release it.
* @param providers - the complete next route set for this registration.
*/
replace(providers: string[]): void
}
/** Display metadata for one registered provider route. */
interface LlmProviderInfo {
/** Provider route key used by {@link GenerateOptions.provider}. */
id: string
/** Human-readable provider name for selectors and diagnostics. */
name: string
}
适配器插件还会通过 registerConfigurableProviders() 声明哪些路由可以运行,并指明每条路由的用户设置分节,使配置界面能在任何路由注册之前就呈现休眠的提供方。
/**
* One provider route an adapter plugin can activate through configuration,
* whether or not the route is currently registered. Configuration surfaces
* merge this directory with `listProviders()` to offer every configurable
* provider alongside its live/dormant state.
*/
interface LlmConfigurableProvider {
/** Provider route key this entry activates when configured. */
provider: string
/** Human-readable provider name for configuration surfaces. */
displayName: string
/** User-settings namespace whose section configures this provider. */
settingsNs: string
/**
* Path from that namespace's section root to this provider's profile
* object; empty when the whole section is the profile.
*/
settingsPath: readonly string[]
}
/** One adapter-discovered model; catalog membership is advisory, not request validation. */
interface LlmModelInfo {
/** Provider route that owns this model entry. */
provider: string
/** Model id passed to {@link GenerateOptions.model}. */
id: string
/** Human-readable model name for selectors. */
name: string
/** Optional user-facing distinction from otherwise similar models. */
description?: string
}
界面正在起草的提供方既没有路由也没有 catalog,因此询问被单独描述:请求携带用户正在编辑的草稿,回复是界面可以采纳的候选,而不是它必须服务的 catalog。
/**
* One interrogation of a provider endpoint that configuration has not stored
* yet. Configuration surfaces send the draft a user is still editing, so the
* request carries the endpoint and credential directly instead of naming a
* route: a provider being added has no route to name.
*/
interface LlmModelDiscoveryRequest {
/**
* Route the draft is editing, when it edits an existing one. A route whose
* adapter already knows its models answers from that knowledge instead of
* asking the endpoint — the adapter's own registry is the better answer, and
* it costs no network call.
*/
provider?: string
/**
* Endpoint to interrogate. Optional because a route the adapter already
* describes needs none; a route it does not must supply one.
*/
baseURL?: string
/** Wire protocol the endpoint speaks, when the draft names one. */
api?: string
/** Credential for this interrogation alone; the harness never stores it. */
apiKey?: string
/** Caller cancellation; implementations must settle promptly after it aborts. */
signal?: AbortSignal
}
/**
* One model an endpoint reports about itself. Every field but the id is
* optional because most provider listings disclose an id and nothing else;
* a surface adopting one of these still owes the capacities its adapter needs.
*/
interface LlmDiscoveredModel {
/** Model id the endpoint accepts. */
id: string
/** Human-readable name when the endpoint supplies one. */
name?: string
/** Maximum combined request and response context, when disclosed. */
contextWindow?: number
/** Maximum output tokens, when disclosed. */
maxTokens?: number
}
对正确性敏感的元数据与参考目录分开解析,并归服务该确切路由的适配器所有。上下文容量、适配器调用默认值和推理选项共用同一个确切模型结果,消费方因而无需重复执行权威模型解析。
/** Provider-owned context capacity for one exact provider/model route. */
interface LlmModelContext {
/** Maximum combined request and response context in tokens. */
contextWindow: number
}
推理强度是另一项针对确切路由的能力。核心为标识符添加品牌类型,但不枚举其值;有序集合、展示名称和可选的部署默认值均由各适配器持有。
/** Adapter-owned identifier for one model's selectable reasoning effort. */
type ReasoningEffortId = Branded<'ReasoningEffortId'>
/** Display metadata for one adapter-owned reasoning effort. */
interface LlmReasoningEffortInfo {
/** Opaque stable value accepted by {@link GenerateOptions.reasoningEffort}. */
id: ReasoningEffortId
/** Human-readable effort name for selectors and diagnostics. */
name: string
/** Optional user-facing distinction from otherwise similar efforts. */
description?: string
}
/** Selectable reasoning efforts for one exact provider/model route. */
interface LlmModelReasoningInfo {
/** Supported efforts in adapter-preferred display order. */
efforts: readonly LlmReasoningEffortInfo[]
/**
* Adapter-configured default materialized into requests when callers omit
* an effort. Absence preserves the provider's own default.
*/
defaultEffort?: ReasoningEffortId
}
/** Exact-route model metadata resolved by its owning adapter. */
interface LlmResolvedModelInfo extends LlmModelInfo {
/** Provider-owned context capacity when known. */
context?: LlmModelContext
/** Adapter-configured per-request output cap materialized when callers omit one. */
defaultMaxTokens?: number
/** Adapter-owned selectable reasoning levels when exposed. */
reasoning?: LlmModelReasoningInfo
}
/** A single model request, fully assembled. */
interface GenerateOptions {
/** Registered provider route selecting the adapter instance. */
provider: string
model: string
/** Adapter-owned reasoning effort selected for this exact model. */
reasoningEffort?: ReasoningEffortId
/**
* Ordered conversation messages, exactly as the provider sees them (after
* the `system` slot). A loop-built request assembles them as
* the derived history (dsh-agent-loop); a hand-built one-shot passes any list.
*/
messages: Message[]
/** System prompt text (adapters map to the provider's system slot). */
system?: string
/** Tool schemas (adapters map to the provider's `tools` field). */
tools?: ToolSchema[]
temperature?: number
maxTokens?: number
/**
* Stop sequences: generation halts as soon as the model produces any one of
* these strings (adapters map to the provider's stop field, e.g. OpenAI
* `stop`). The stop string itself is not included in the output.
*/
stop?: string[]
signal?: AbortSignal
/**
* Session identity stamped by the loop for listener routing. Adapters ignore
* it; replay uses it to keep concurrent parent and child cursors independent.
*/
sessionId?: Branded<'SessionId'>
/**
* Provider-neutral classification for an auxiliary model call. Adapters may
* map the purpose to model-hidden transport metadata or purpose-specific
* generation policy. Ordinary conversation requests leave it unset.
*/
purpose?: 'compaction' | 'session-title'
}
模型响应为何停止由可合并扩展的原因表示。提供方终态失败携带流式契约的 LlmFailure:
/**
* Why a model response stopped.
* Merge-extensible so adapters can surface provider-specific reasons.
*/
interface FinishReasonMap {
'stop': { kind: 'stop' }
'tool-calls': { kind: 'tool-calls' }
'max-tokens': { kind: 'max-tokens' }
'aborted': { kind: 'aborted'; failure: LlmFailure }
'error': { kind: 'error'; failure: LlmFailure }
}
FinishReason = FinishReasonMap[keyof FinishReasonMap]。TokenUsage(逐调用计量,含不相交的缓存字段)详见 llm-streaming.md。
GenerateOptions.tools 携带 ToolSchema——工具的 JSON Schema 描述,发送给模型。它声明在 dsh-llm(而非 dsh-tools)中,正是因为它是循环每一步组装请求的一部分:
/**
* JSON-schema description of a tool, as sent to the model.
*
* Declared here (not in dsh-tools) because it is part of {@link GenerateOptions};
* dsh-tools' ToolDefinition and dsh-system-prompt's PromptAssembly both import
* it from this package.
*/
interface ToolSchema {
name: string
description: string
/** JSON Schema object for the arguments. */
parameters: Record<string, unknown>
}
面向模型的 ToolSchema 是协议格式;产出它的已注册 ToolDefinition(schema + execute)在 tools.md 中。
请求信封:LlmCallConfig 与记录的 header
循环从已记录状态构建每个请求。EpochHeader 通过完整的 request/header 快照记录调用配置、适配器默认值来源、渲染后的提示词以及权威返回工具顺序(由 toolOrder 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 session.md 与可重建性 Agent Note(agent 决策记录)。
agent/request 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置及其来源。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 llm/stream 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
在协议格式上,循环构建的请求先读取 system 槽位(渲染后的提示词组装),再读取派生历史——边界快照,其尾部在轮次首步是最新的 user/message,在后续步骤是上一步的工具结果。开发不变式针对每个循环构建的请求精确重算此等式。
/**
* Provider, model, reasoning effort, and sampling scalars of one conversation's
* requests. Every field maps 1:1 onto the same-named `GenerateOptions` field;
* the loop builds requests from the logged header rather than accepting these
* per call.
*/
interface LlmCallConfig {
provider: string
model: string
reasoningEffort?: ReasoningEffortId
temperature?: number
maxTokens?: number
stop?: string[]
}
/**
* Effective config fields supplied by exact-model adapter resolution rather
* than by the caller's request proposal.
*/
interface LlmCallConfigAdapterDefaults {
reasoningEffort?: true
maxTokens?: true
}
会话
Session 是一份类型化 SessionEvent 的仅追加日志——唯一的真源。LLM(大语言模型)消息历史从日志派生(deriveMessages()),而非单独存储。事件词汇从 SessionEventMap 派生:
源码:packages/core/session/src/types.ts
/**
* One immutable entry in the session log.
*
* A proper discriminated union over `type` (not independent `type`/`data`
* unions), so `switch (event.type)` narrows `event.data` without casts.
*
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
* they only exist on {@link SurfaceEventType} variants (`user/message`,
* `assistant/message`, `tool/result`).
* Non-surface events (boundary markers, chunks, usage, errors) never carry
* surface metadata — the compiler enforces this at `Session.append()`
* call sites.
*/
type SessionEvent<T extends SessionEventType = SessionEventType> = {
[K in SessionEventType]: {
type: K
/** Monotonic sequence number within the session. */
seq: number
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of events that are provenance sources of this event
* (e.g. the `assistant/chunk` seqs that built an `assistant/message`,
* or the surface nodes shadowed by a compaction replace node). An
* `assistant/message` may carry a present empty array for a known empty
* provider stream; omission means unrecorded provenance.
*/
sourceEventSeqs?: number[]
/** How this event entered the surface; absent for non-surface events. */
surfaceOp?: SurfaceOp
} : object)
}[T]
会话事件变体、deriveMessages() 投影规则、TurnEndReason 词汇以及执行封闭和独立事件规则都在 session.md 中。日志如何持久化——SessionPersistence seam、JSONL/SQLite 后端、session/flush 检查点、崩溃恢复与 SessionHeader——则在 persistence.md 中。
Agent 句柄
Agent 是每个插件(UI、钩子、orchestrator)面向编程的 surface。具体实现为 dsh-agent-loop 包内部细节;循环外没有任何组件依赖它。
源码:packages/core/agent/src/types.ts
/** One of the two ordered pending-message lists owned by an agent. */
type InboxTarget = 'next-turn' | 'next-step'
每个待处理入队项就是其 UserMessage;MessageId 是唯一标识。Inbox.append、prepend、replace、remove、clear、splice 与 claim 会记录规范化的持久 agent/inbox/spliced 变更,并拒绝重复的待处理 id。replace(messageId, newMessage) 与 remove(messageId) 通过 MessageId 跨两份列表定位待处理消息;替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。普通删除和 clear() 都表示取消。claim(target) 通过无 outcome 的纯删除 splice 移除拟进入步骤的批次——全部 next-step 输入,外加轮次边界上的一条 next-turn 消息——且不发出 discarded 通知;循环另行逐条发出 claimed 通知。UI 投影等整体队列消费方通过持久 splice 重建 nextTurn 与 nextStep,而跟踪单条消息的消费方使用精确的 agent/inbox/inserted、claimed 与 discarded 通知。
/** Options for {@link Agent.cancel}. */
interface CancelOptions {
/**
* Preserve queued and steering inbox items instead of discarding them. The
* active turn is still aborted, but un-started and pending work survives for a
* later turn and no canceled inbox splice is logged.
*/
keepInbox?: boolean | undefined
}
/** Why an active agent driver was cancelled. */
type AgentCancelCause =
| { readonly kind: 'user' }
| { readonly kind: 'parent' }
| { readonly kind: 'hook'; readonly reason: string }
| { readonly kind: 'disposed' }
Agent 是覆盖公开活跃 agent 契约的接口。它的统一 send 方法直接公开目标与唤醒路由;followup、steer 和 inject 是固定预设别名。
/** Public live-agent handle. */
interface Agent {
/** The single identity shared with {@link session}. */
readonly id: SessionId
/** The provider route and model this agent's requests use. */
readonly options: AgentOptions
/** The live session this agent drives; its log is the durable source of truth. */
readonly session: Session
/** The agent-owned projection of durable pending work. */
readonly inbox: Inbox
/** The current lifecycle state, mirrored on every `agent/status` transition. */
readonly status: AgentStatus
/** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
readonly ctx: Context
/**
* Clear queued and steering work — unless `keepInbox` — and abort the active
* turn or between-turn task. The first cause wins for that activity. With no
* active activity, cancellation is a no-op and does not arm later work.
* @param cause - the stable caller intent carried by the active operation signal.
* @param options - cancellation options; `keepInbox` preserves pending work.
*/
cancel(cause: AgentCancelCause, options?: CancelOptions): void
/**
* Resolve after the current whole-agent activity reaches quiescence. This
* follows replacement work started before the observed driver retires,
* but does not identify the settlement of any particular message.
* @returns fulfillment after no active driver or maintenance task remains.
*/
whenIdle(): Promise<void>
/**
* Run one non-turn maintenance task from the true idle phase. The task starts
* synchronously after claiming that phase; later waking input remains in the
* inbox until the task settles, while public status stays `idle`.
* `whenIdle()` follows both the task and any waking work released behind it.
* @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}.
* @throws synchronously when turn-driving or another maintenance task already owns the agent.
* @returns the task promise.
*/
runMaintenance<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>
/**
* Route identified input to an inbox boundary and optionally wake the driver.
* Waking input submitted after active cancellation is queued for the next
* turn and runs when the aborted activity converges to idle; a `disposed`
* cancel leaves it parked. A wake submitted while already idle always opens
* its turn boundary, even when its message is cleared before the driver
* claims ([cancel-convergence wake latch](../../../../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)).
* @param message - identified content and its producer provenance.
* @param target - the preferred next-turn or next-step inbox boundary.
* @param wakeup - whether delivery may wake the driver.
*/
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void
/**
* Queue an ordinary follow-up turn and wake the driver. The item becomes the
* sole ordinary message of its own turn.
* @param message - identified prompt content and its producer provenance.
*/
followup(message: UserMessage): void
/**
* Submit steering for the nearest step. An idle driver starts a turn;
* a running driver consumes it at its next step boundary.
* A rejected step leaves steering parked in the inbox until the next
* wake; cancellation or disposal may discard pending steering.
* @param message - identified steering content and its producer provenance.
*/
steer(message: UserMessage): void
/**
* Queue model-facing context for the next pre-step without waking the
* driver. A running driver claims it at the nearest later step boundary;
* idle drivers leave it pending until follow-up or steering
* wakes them. It may miss a request whose pre-step already claimed its
* batch. Cancellation or disposal may discard pending context.
* @param message - identified injected context and its producer provenance.
*/
inject(message: UserMessage): void
}
AgentStatus 为 'idle' | 'running',SessionId 是品牌类型。dispose(资源释放)会把 agent 从注册表移除并发出 agent/disposed;它不是一个终态 status 值。running 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。followup() 不返回 handle:其 MessageId 标识持久 inbox 的插入、领取与丢弃事实,而不标识之后的助手输出或轮次结束。whenIdle() 观察整个 agent,因此只有显式拥有从回执到 idle 这一完整区间的调用方才能将其称为一次运行(决策)。AgentOptions 可合并扩展:core 声明 provider?、model? 与 maxTokens?(在 agent/request 后,分发要求 provider 与 model 都存在)。提供 maxTokens 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。Persona 归 dsh-system-prompt 所有:agent 作用域的 deployment:persona 可以遮蔽全局默认值。
cause 是由 TypeScript 强制约束的同进程输入。活跃的取消持有者会将它复制到仅运行时的 AbortSignal.reason;signal 不授予协作监听器任何分类权限。持久 turn/end 保留粗粒度 { kind: 'aborted' } 结果;若需记录请求 provenance,应使用单独的持久事件,而不是让终态结果承担额外含义。
事件分类拥有 agent/* 生命周期、检查点与 waterfall(瀑布式事件)契约。轮次和步骤边界是持久会话事件,而不是 agent emit。
发起 Agent
ctx.agents 携带的进程本地 initiator 就是上面的确切 Agent,不是单独的 frame 或复制的标识。环境中存在该值既不能证明存活,也不代表授权;其生命周期与边界规则由 initiator 作用域决策规定。
拦截决策
pre-step 决策使用与持久 user-role 输入相同、带标识的 UserMessage 形状。进入步骤的批次具有权威性,并保留每条消息的标识与 provenance。钩子桥接层把其原生决策字段映射到这一类型化结果上。
源码:packages/core/agent/src/types.ts
agent/pre-step 接收一个 payload,携带独占的已领取批次(messages)、拟进入步骤的坐标(turn、step)与当前轮次的取消 signal。首次提案在已打开的轮次内、任何步骤开始前运行;工具 continuation 可以在步骤之间提交空的已领取批次:
它返回 PreStepDecision。reject 不会打开步骤。enter 提供在 step/start 后追加的完整消息批次;最终决策省略的已领取消息保持已删除,而领取后插入的输入仍留待后续处理:
/** Whether and with which messages the loop enters a proposed step. */
type PreStepDecision =
| { kind: 'reject' }
| { kind: 'enter'; messages: UserMessage[] }
agent/request-error 在失败的模型步骤关闭之后、其轮次关闭之前运行。listener 可以在失败轮次的 signal 仍然存活时修复持久状态或 await 策略工作。处理该错误的 listener 返回 { kind: 'retry' } 且不调用 next();默认的 undefined 会让失败保持终态。
/** Action returned by a listener that owns model-request recovery. */
type RequestErrorAction = { kind: 'retry' } | undefined
agent/pre-step 是请求推导前唯一的串行边界。agent/turn-stopping 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。
agent/session-start 携带 SessionStartSource(会话生命周期为何开始;桥接层据此匹配其 SessionStart):
/** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */
type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
ToolDefinition
唯一属于核心的流水线编写类型:每个已注册工具是什么——一个面向模型的 ToolSchema 加上一个 execute 函数,以及可选的最终内容回调与 UI 回调。工具作者很少手动构造它(defineTool DSL 会用类型化参数构建),但它是注册表持有、循环分发所经过的契约。
其完整字段、defineTool/ValueSchemaSpec/ParameterSchemaSpec 类型化 schema DSL、ToolExecution/ToolExecutionResult waterfall 形状,以及工具展示 UI 词汇在 tools.md 中。
Cordis surface
Generated from source by scripts/gen-cordis-catalog.ts (verified fresh by pnpm run verify-cordis-catalog in doc-sync; regenerate with pnpm run gen-cordis-catalog) — this section is byte-identical in both language sides of the page. Signature blocks use a ts cordis-catalog fence and keep the original source JSDoc; dispatch modes are defined in the primer, and the framework-inherited ctx surface lives in cordis-api/inherited.md.
ctx.agentLoop — AgentLoop
Concrete agent factory and driver service.
/**
* Create an agent and session under one caller-supplied identity, owned by
* the accessing fiber. Constructor-driven config calls mint a fresh combined
* id before entering this boundary.
* @param id - shared agent/session identity.
* @param options - concrete loop options.
* @param meta - optional fresh-session workspace metadata.
* @returns the published running agent.
*/
create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd'> = {}): Agent
/**
* Create an owned agent on a caller-supplied session id.
* @param ownerCtx - caller context that structurally owns the lifecycle.
* @param options - identities, session seed/metadata, loop options, setup, and cancellation.
* @returns the published handle.
*/
async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>
/**
* Resume an owned agent from the configured persistence service.
* @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
* @param options - persisted identity, loop options, setup, and cancellation.
* @returns the published handle.
*/
async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
Types: SessionHeader
Source: packages/core/agent-loop/src/index.ts:277
ctx.agents — AgentRegistry
Agent service (ctx.agents): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent creation is provided by whichever plugin implements the AgentFactory (@deepseek-ai/dsh-agent-loop), registered via setFactory.
Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain.
/**
* Read the Agent that initiated the inherited asynchronous driver chain.
* Use this optional form for logging, tracing, metrics, or host attribution
* that also supports agentless calls. When a parent creates a child, setup
* reports the causal parent while `agentCtx.agent` identifies the child.
* @returns the inherited Agent, or `undefined` outside an initiator boundary
* and inside an explicit clearing boundary.
* @throws when this service instance has been disposed.
*/
currentInitiator(): Agent | undefined
/**
* Read the initiating Agent and fail when no initiator boundary is active.
* Use this for private helpers contractually below a driver, or for a
* deployment-owned outbound request whose contract forbids agentless calls.
* Generic or direct-call seams use optional lookup or explicit request fields.
* @returns the inherited Agent.
* @throws when no initiator is active or this service instance has been disposed.
*/
requireInitiator(): Agent
/**
* Run an operation with one exact Agent as its process-local initiator. The
* exact synchronous value or Promise returned by the operation is preserved.
* Custom drivers and test harnesses wrap their complete returned foreground
* lifetime.
* A queue or wire receiver may establish this boundary only after validating
* explicit identity and resolving the exact live Agent; this method does neither.
* Detached work remains owned by the subsystem that starts it.
* @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
* @param operation - synchronous or asynchronous operation to invoke.
* @returns the exact value returned by `operation`.
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
*/
withInitiator<T>(agent: Agent, operation: () => T): T
/**
* Run an operation inside a boundary that hides any inherited initiating
* Agent. The exact synchronous value or Promise is preserved.
* Use this while creating lazy shared timers, queue pumps, pool maintenance,
* watchers, or exporters so they do not inherit the first Agent that happens
* to initialize them. It clears only initiator attribution, not explicit
* fields, and does not own or drain detached resources.
* @param operation - synchronous or asynchronous operation to invoke without an initiator.
* @returns the exact value returned by `operation`.
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
*/
withoutInitiator<T>(operation: () => T): T
/**
* Register the agent-creation factory (the loop calls this on construction,
* effect-scoped). A traced Cordis service is canonicalized to its concrete
* target; each create/resume call is then traced through that caller's
* context so ownership follows the caller without stacking proxy layers.
* Throws if a factory is already registered. Returns the disposer; on
* dispose the factory slot is cleared.
* @param factory - the loop-owned factory {@link create}/{@link resume} delegate to.
* @returns the disposer that clears the factory slot. The exact
* Cordis effect disposer (single-shot): composite (generator) effects may
* yield it directly — exact identity nests the teardown in order.
*/
setFactory(factory: AgentFactory): () => void
/**
* Create and publish a new agent through the registered factory.
* Distinct from {@link register} (which records an already-constructed
* agent): this constructs the agent and its session. Rejects if no factory is
* registered or creation/setup fails. The resolved {@link AgentHandle} lets
* the owner tear down exactly this agent.
* @param options - shared identity, session seed/metadata, and agent options.
* @returns the handle after setup, rollback-covered publication, and loop start complete.
*/
async create(options: CreateAgentOptions): Promise<AgentHandle>
/**
* Load a persisted session and resume an agent on it through the registered
* factory. Rejects if no factory is registered; the factory rejects if
* session persistence is not configured or persistence/setup fails.
* @param options - persisted identity, configuration, and optional setup.
* @returns the handle after setup, rollback-covered publication, and loop start complete.
*/
async resume(options: ResumeAgentOptions): Promise<AgentHandle>
/**
* Register a live agent. Throws if an agent with the same id is already
* registered. Emits `agent/created` on registration and `agent/disposed`
* when the calling fiber is disposed — both with the agent's scope carrier
* (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
* emits are scope-filtered regardless of which context invoked `register`
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
* requires passing the carrier). Returns the disposer.
* @param agent - the already-constructed agent to record in the store.
* @returns the EXACT Cordis effect disposer (single-shot; a repeat call
* returns undefined without awaiting an in-flight teardown). Exact
* identity is load-bearing: a composite (generator) effect that owns a
* teardown ORDER — the agent factory's lifecycle chain — must yield THIS
* function so Cordis nests the unregistration at that yield position;
* yielding a wrapper would leave it disposing as a concurrent sibling on
* owner unload, unregistering the agent (and emitting `agent/disposed`)
* while its final turn is still draining.
*/
register(agent: Agent): () => void
/**
* Insert an already-constructed agent without announcing it. This is the
* advanced ordered-lifecycle primitive used by the async agent factory: it
* first completes setup while the agent is unpublished, then assigns the
* returned detach closure into its pre-installed composite teardown before
* calling {@link announce}. Ordinary callers use {@link register}.
* @param agent - the prepared, unpublished agent.
* @param owner - live agent whose scoped context created this agent, or
* undefined for a top-level runtime root. This is runtime ownership, not
* the resumed session's durable parent lineage.
* @returns an idempotent closure that removes this exact entry and emits
* `agent/disposed` with listener failures contained. When called from a
* synchronous `agent/created` listener, removal and disposal wait until
* that creation dispatch unwinds.
*/
enter(agent: Agent, owner: Agent | undefined): () => void
/**
* Announce an agent previously inserted with {@link enter}.
* @param agent - the live inserted agent to announce.
* @throws if `agent` is not the exact live registry entry for its id, or its
* creation announcement already began (including a reentrant call from a
* creation listener).
*/
announce(agent: Agent): void
/**
* Look up a live agent.
* @param id - the shared agent/session id to look up.
* @returns the agent, or undefined when no live agent has that id.
*/
get(id: SessionId): Agent | undefined
/**
* Test whether a live agent was created through one exact parent agent's
* scoped context. Runtime ownership is independent of durable session
* lineage and remains unambiguous when unrelated providers reuse an id.
* @param id - the candidate child agent's shared agent/session id.
* @param owner - the expected runtime creator agent.
* @returns true only while the exact child entry is live under that owner.
*/
isOwnedBy(id: SessionId, owner: Agent): boolean
/**
* All live agents, in registration order.
* @returns a fresh array; mutating it does not affect the registry.
*/
list(): Agent[]
/**
* All live top-level agents in registration order. A top-level agent was
* created without an owning agent context; durable session lineage does not
* affect this runtime relation, so a resumed fork may still be a root.
* @returns a fresh array; mutating it does not affect the registry.
*/
roots(): Agent[]
Source: packages/core/agent/src/index.ts:242
agent/* events
agent/created — emit
A fully configured agent and live session were published. Setup is composition-only; agent/session-start is the first startup-driving seam. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.
/**
* A fully configured agent and live session were published. Setup is
* composition-only; `agent/session-start` is the first startup-driving seam.
* Synchronous listener failure vetoes publication, while returned-promise
* rejection is reported. Detach requested during dispatch waits until every
* creation listener has observed the stable entry.
* @param payload.agent - the newly registered agent with its live session and completed setup.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/created'(this: Scoped<Agent>, payload: { agent: Agent }): void
Types: Scoped
Source: packages/core/agent/src/types.ts:158
agent/disposed — emit
An agent left the registry; AgentLoop emits this after driver quiescence and scoped-registration unwind, but before session detachment. Custom registry users own their driver-ordering contract.
/**
* An agent left the registry; AgentLoop emits this after driver quiescence
* and scoped-registration unwind, but before session detachment. Custom
* registry users own their driver-ordering contract.
* @param payload.agent - the exact agent removed from the registry.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/disposed'(this: Scoped<Agent>, payload: { agent: Agent }): void
Types: Scoped
Source: packages/core/agent/src/types.ts:167
agent/error — emit
A step or turn errored. The machine reports a failure here even when the error has no in-turn position for a durable record.
/**
* A step or turn errored. The machine reports a failure here even when
* the error has no in-turn position for a durable record.
* @param payload.agent - the agent whose turn errored.
* @param payload.turn - the turn in which the failure surfaced.
* @param payload.step - the step at which the failure surfaced.
* @param payload.error - the failure, verbatim.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; error: unknown }): void
Types: Scoped
Source: packages/core/agent/src/types.ts:289
agent/inbox/claimed — emit
One message left the inbox inside its open turn. If the proposed step is rejected, the claimed message ends here: it is neither discarded nor re-emitted as a user/message, and the turn closes without a step.
/**
* One message left the inbox inside its open turn. If the proposed step
* is rejected, the claimed message ends here: it is neither discarded nor
* re-emitted as a user/message, and the turn closes without a step.
* @param payload.agent - the agent whose inbox changed.
* @param payload.message - the claimed message.
* @param payload.turn - the owning turn.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/claimed'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage; turn: number }): void
Types: Scoped · UserMessage
Source: packages/core/agent/src/types.ts:196
agent/inbox/discarded — emit
One message was discarded from the live inbox.
/**
* One message was discarded from the live inbox.
* @param payload.agent - the agent whose inbox changed.
* @param payload.message - the discarded message.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/discarded'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
Types: Scoped · UserMessage
Source: packages/core/agent/src/types.ts:204
agent/inbox/inserted — emit
One message entered the live inbox.
/**
* One message entered the live inbox.
* @param payload.agent - the agent whose inbox changed.
* @param payload.message - the inserted message.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/inserted'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
Types: Scoped · UserMessage
Source: packages/core/agent/src/types.ts:185
agent/pre-step — waterfall
Reject a proposed step or replace the messages that enter it. Calling next() preserves the current messages.
/**
* Reject a proposed step or replace the messages that enter it. Calling
* `next()` preserves the current messages.
* @param payload.agent - the agent proposing the step.
* @param payload.messages - messages removed from the inbox for this step.
* @param payload.turn - the turn that will own the step.
* @param payload.step - the step proposed by the loop.
* @param payload.signal - the current turn's cancellation signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/pre-step'(this: Scoped<Agent>, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
Types: Scoped · UserMessage
Source: packages/core/agent/src/types.ts:230
agent/request — waterfall
Replace the frozen call configuration. await next() yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. Model-visible content must use logged channels; this seam cannot mutate messages.
/**
* Replace the frozen call configuration. `await next()` yields the config
* the machine would use (agent options on the first request, the logged
* header afterwards); return a replacement to switch. Model-visible
* content must use logged channels; this seam cannot mutate messages.
* @param payload.agent - the agent making the model call.
* @param payload.turn - the open turn number.
* @param payload.step - the step whose request this is.
* @param payload.signal - the current turn's explicit abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/request'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
Types: Scoped
Source: packages/core/agent/src/types.ts:243
agent/request-error — waterfall
Handle one failed model-request attempt before the loop retries or closes its step. A listener returns { kind: 'retry' } without calling next() when it owns recovery, or calls next() to delegate. The default undefined leaves the failure terminal.
/**
* Handle one failed model-request attempt before the loop retries or closes
* its step. A listener returns `{ kind: 'retry' }` without calling `next()`
* when it owns recovery, or calls `next()` to delegate. The default
* `undefined` leaves the failure terminal.
* @param payload.agent - the agent whose request failed.
* @param payload.turn - the turn containing the failed request.
* @param payload.step - the step containing the failed request attempt.
* @param payload.provider - the provider selected for the failed request.
* @param payload.failure - serializable facts normalized at the final adapter boundary.
* @param payload.retryPolicy - the policy of the adapter registration that served the failed request.
* @param payload.signal - the turn abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/request-error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; provider: string; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined; signal: AbortSignal }, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
Types: LlmFailure · ResolvedRetryPolicy · Scoped
Source: packages/core/agent/src/types.ts:259
agent/session-start — emit
The session lifecycle began, once before the first turn. Use agent.inject() to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.
/**
* The session lifecycle began, once before the first turn. Use
* `agent.inject()` to seed model-facing context. This is a notification, not
* a veto; disposal requested by a lifecycle owner is rechecked before the
* driver starts.
* @param payload.agent - the agent whose session lifecycle began.
* @param payload.source - why the session started (fresh startup, resume, …).
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/session-start'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource }): void
Types: Scoped
Source: packages/core/agent/src/types.ts:216
agent/status — emit
Agent status changed (idle ⇄ running). A waking delivery enters running synchronously after reserving cancellation; idle means no driver remains scheduled or active.
/**
* Agent status changed (`idle` ⇄ `running`). A waking delivery enters
* `running` synchronously after reserving cancellation; `idle` means no
* driver remains scheduled or active.
* @param payload.agent - the agent whose status flipped.
* @param payload.status - the status just entered (the transition's destination).
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/status'(this: Scoped<Agent>, payload: { agent: Agent; status: AgentStatus }): void
Types: Scoped
Source: packages/core/agent/src/types.ts:177
agent/turn-stopping — serial
The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (agent.steer(...)) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying concludesTurn ends the turn at its step. The conclusion never short-circuits already-submitted next-step work: same-step additionalContexts or racing steering still runs, and the turn closes only when that inbox drains.
/**
* The turn is about to close: the model owes no response (no live tool
* calls, no fresh steering). Awaited before the boundary commits — a
* listener that objects steers (`agent.steer(...)`) and the machine
* re-reads its inbox: fresh steering runs another step, none closes the
* turn. Data decides, so listener order cannot change the outcome. The
* inverse control (stop a tool loop early) is data too: a tool result
* carrying `concludesTurn` ends the turn at its step. The conclusion
* never short-circuits already-submitted next-step work: same-step
* `additionalContexts` or racing steering still runs, and the turn
* closes only when that inbox drains.
* @param payload.agent - the agent whose turn is at its stop boundary.
* @param payload.turn - the turn about to close.
* @param payload.signal - the current turn's explicit abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode serial
*/
'agent/turn-stopping'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; signal: AbortSignal }): Promise<void> | void
Types: Scoped
Source: packages/core/agent/src/types.ts:277
agent-loop/* events
agent-loop/config-start-failed — emit
A declarative agent entry failed before it could publish a live agent. Consumers that buffer work for the configured identity use this transient signal to reject that work instead of waiting forever. Normal factory teardown suppresses failures from the cancelled startup attempt.
/**
* A declarative agent entry failed before it could publish a live agent.
* Consumers that buffer work for the configured identity use this
* transient signal to reject that work instead of waiting forever. Normal
* factory teardown suppresses failures from the cancelled startup attempt.
* @param payload.sessionId - exact shared agent/session identity that failed startup.
* @param payload.error - persistence, setup, or publication failure.
* @mode emit
*/
'agent-loop/config-start-failed'(payload: { sessionId: SessionId; error: unknown }): void