feat(tools): resolve conditional tool admission per request
This commit is contained in:
1 parent
039fe73c55
commit
9c3a6a34d4
192 files changed
+1322
-1804
No files matched your search
@@ -27,6 +27,7 @@ try {
|
||||
const agentId = SessionId('dsh-badge-snapshot')
|
||||
const session = ctx.sessions.create(agentId, { meta: { cwd: process.cwd() } })
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
ctx,
|
||||
id: agentId,
|
||||
options: {},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
en: 41d772192075e133
|
||||
zh: 305a6826a598b96d
|
||||
/agent-turn-and-step-lifecycle:
|
||||
en: a1389c8ed7e37324
|
||||
zh: c2944b9192af4ec9
|
||||
en: 8b6b2eff5a5c93b9
|
||||
zh: f63a8b82e1888d12
|
||||
+12
-7
@@ -25,16 +25,18 @@ sequenceDiagram
|
||||
Note over Agent,Driver: claim pending next-step input plus one queued prompt
|
||||
Driver-->>SDK: <code>agent/inbox/spliced</code> pure deletion
|
||||
Driver-->>SDK: <code>agent/inbox/claimed</code> { message, turn } per message
|
||||
Driver->>Prompt: <code>system-prompt/assemble</code> waterfall
|
||||
Driver->>Hooks: <code>agent/request</code> waterfall
|
||||
Driver->>LLM: prepareCall(config, signal), bind Agent.currentRoute()
|
||||
Driver->>Prompt: <code>system-prompt/prepare</code> waterfall
|
||||
Driver->>Hooks: <code>agent/pre-step</code> waterfall
|
||||
Hooks-->>Driver: authoritative reject or enter(messages)
|
||||
alt proposed step rejected, first batch empty, or pre-step failed
|
||||
Driver-->>Driver: claimed batch stays removed, the open turn spends no step
|
||||
else enter proposed step
|
||||
Driver->>Session: <code>step/start</code>
|
||||
Driver->>Hooks: <code>agent/request</code> waterfall
|
||||
Driver->>LLM: prepareCall(config, signal)
|
||||
Note over Driver,LLM: cancellation during either async phase commits neither system nor users
|
||||
Driver->>Tools: resolve({ agent, signal })
|
||||
Driver->>Prompt: <code>system-prompt/assemble</code> with resolved tool set
|
||||
Note over Driver,LLM: cancellation during preparation commits neither system nor users
|
||||
Note over Driver,Session: synchronous admission using the prepared call capability
|
||||
Driver->>Session: <code>system/message</code> ordered per-node reconciliation
|
||||
Driver->>Session: <code>user/message</code> per entered message
|
||||
@@ -48,7 +50,7 @@ sequenceDiagram
|
||||
Driver-->>SDK: <code>agent/assistant-stream</code> committed end
|
||||
Driver->>Hooks: <code>agent/request-error</code> waterfall
|
||||
Hooks-->>Driver: return retry action or preserve the original error
|
||||
Note over Driver,LLM: retry in the open step: prepare and reconcile the same rendered assembly without repeating pre-step or users
|
||||
Note over Driver,LLM: retry resolves route and tools. Unchanged inputs reuse the prompt without repeating pre-step or users
|
||||
else model request succeeded
|
||||
Driver->>Session: <code>assistant/message</code>
|
||||
Driver-->>SDK: <code>agent/assistant-stream</code> committed end
|
||||
@@ -56,7 +58,7 @@ sequenceDiagram
|
||||
loop barriers and bounded rolling pool, reclassify before start
|
||||
opt call starts
|
||||
Driver->>Session: <code>tool/call</code>
|
||||
Driver->>Tools: ordered pre, concurrent execute
|
||||
Driver->>Tools: ordered pre, concurrent execute with request tool set
|
||||
Tools-->>Session: tool-owned events when applicable
|
||||
end
|
||||
opt next model-order result ready
|
||||
@@ -64,13 +66,16 @@ sequenceDiagram
|
||||
Driver->>Session: <code>tool/result</code>
|
||||
end
|
||||
end
|
||||
Driver->>Tools: dispose request tool set
|
||||
Driver->>Session: <code>step/end</code>
|
||||
Driver->>Driver: clear bound route
|
||||
opt natural stop and next-step inbox empty
|
||||
Driver->>Hooks: <code>agent/turn-stopping</code> serial terminal checkpoint
|
||||
end
|
||||
opt next-step input is pending
|
||||
Driver-->>Driver: claim pending next-step input
|
||||
Driver-->>SDK: <code>agent/inbox/claimed</code> { message, turn } per message
|
||||
Driver->>Driver: prepare route and runtime context for the next step
|
||||
Driver->>Hooks: <code>agent/pre-step</code> waterfall
|
||||
Hooks-->>Driver: authoritative reject or enter(messages)
|
||||
end
|
||||
@@ -82,7 +87,7 @@ sequenceDiagram
|
||||
|
||||
The `assistant/message` event records every successful provider call, including content-less and `max-tokens` finishes, and embeds the exact compact timed stream. Empty content stays out of derived history. A failed, retried, cancelled, or stream-error attempt that reaches settlement without a surface message records its stream as `assistant/attempt`. Live `agent/assistant-stream` chunk frames are transient; replay reads either durable settlement, and a hard process loss before settlement leaves no durable attempt stream.
|
||||
|
||||
`dsh-compaction-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery runs within the open step and retries only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative. Each retry prepares its call and reconciles the retained rendered assembly before request derivation, without repeating assembly, pre-step, or user admission.
|
||||
`dsh-compaction-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery runs within the open step and retries only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative. Each retry prepares its call and resolves tool conditions before request derivation, without repeating pre-step or user admission. Prompt assembly repeats only when the bound route or tool schemas change.
|
||||
|
||||
The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages and `startsRequestSeries` unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.
|
||||
|
||||
|
||||
@@ -27,16 +27,18 @@ sequenceDiagram
|
||||
Note over Agent,Driver: claim pending next-step input plus one queued prompt
|
||||
Driver-->>SDK: <code>agent/inbox/spliced</code> pure deletion
|
||||
Driver-->>SDK: <code>agent/inbox/claimed</code> { message, turn } per message
|
||||
Driver->>Prompt: <code>system-prompt/assemble</code> waterfall
|
||||
Driver->>Hooks: <code>agent/request</code> waterfall
|
||||
Driver->>LLM: prepareCall(config, signal), bind Agent.currentRoute()
|
||||
Driver->>Prompt: <code>system-prompt/prepare</code> waterfall
|
||||
Driver->>Hooks: <code>agent/pre-step</code> waterfall
|
||||
Hooks-->>Driver: authoritative reject or enter(messages)
|
||||
alt proposed step rejected, first batch empty, or pre-step failed
|
||||
Driver-->>Driver: claimed batch stays removed, the open turn spends no step
|
||||
else enter proposed step
|
||||
Driver->>Session: <code>step/start</code>
|
||||
Driver->>Hooks: <code>agent/request</code> waterfall
|
||||
Driver->>LLM: prepareCall(config, signal)
|
||||
Note over Driver,LLM: cancellation during either async phase commits neither system nor users
|
||||
Driver->>Tools: resolve({ agent, signal })
|
||||
Driver->>Prompt: <code>system-prompt/assemble</code> with resolved tool set
|
||||
Note over Driver,LLM: cancellation during preparation commits neither system nor users
|
||||
Note over Driver,Session: synchronous admission using the prepared call capability
|
||||
Driver->>Session: <code>system/message</code> ordered per-node reconciliation
|
||||
Driver->>Session: <code>user/message</code> per entered message
|
||||
@@ -50,7 +52,7 @@ sequenceDiagram
|
||||
Driver-->>SDK: <code>agent/assistant-stream</code> committed end
|
||||
Driver->>Hooks: <code>agent/request-error</code> waterfall
|
||||
Hooks-->>Driver: return retry action or preserve the original error
|
||||
Note over Driver,LLM: retry in the open step: prepare and reconcile the same rendered assembly without repeating pre-step or users
|
||||
Note over Driver,LLM: retry resolves route and tools. Unchanged inputs reuse the prompt without repeating pre-step or users
|
||||
else model request succeeded
|
||||
Driver->>Session: <code>assistant/message</code>
|
||||
Driver-->>SDK: <code>agent/assistant-stream</code> committed end
|
||||
@@ -58,7 +60,7 @@ sequenceDiagram
|
||||
loop barriers and bounded rolling pool, reclassify before start
|
||||
opt call starts
|
||||
Driver->>Session: <code>tool/call</code>
|
||||
Driver->>Tools: ordered pre, concurrent execute
|
||||
Driver->>Tools: ordered pre, concurrent execute with request tool set
|
||||
Tools-->>Session: tool-owned events when applicable
|
||||
end
|
||||
opt next model-order result ready
|
||||
@@ -66,13 +68,16 @@ sequenceDiagram
|
||||
Driver->>Session: <code>tool/result</code>
|
||||
end
|
||||
end
|
||||
Driver->>Tools: dispose request tool set
|
||||
Driver->>Session: <code>step/end</code>
|
||||
Driver->>Driver: clear bound route
|
||||
opt natural stop and next-step inbox empty
|
||||
Driver->>Hooks: <code>agent/turn-stopping</code> serial terminal checkpoint
|
||||
end
|
||||
opt next-step input is pending
|
||||
Driver-->>Driver: claim pending next-step input
|
||||
Driver-->>SDK: <code>agent/inbox/claimed</code> { message, turn } per message
|
||||
Driver->>Driver: prepare route and runtime context for the next step
|
||||
Driver->>Hooks: <code>agent/pre-step</code> waterfall
|
||||
Hooks-->>Driver: authoritative reject or enter(messages)
|
||||
end
|
||||
@@ -84,7 +89,7 @@ sequenceDiagram
|
||||
|
||||
`assistant/message` 事件会记录每次成功的提供方调用,包括返回空内容或以 `max-tokens` 结束的调用,并嵌入精确的紧凑带时间 stream。空内容不会进入派生历史。失败、重试、取消或 stream error attempt 到达 settlement 时,如果没有 surface message,就会把 stream 记录为 `assistant/attempt`。实时 `agent/assistant-stream` chunk frame 是瞬态数据;回放读取任一种持久 settlement,如果进程在 settlement 前硬中断,则不会留下持久 attempt stream。
|
||||
|
||||
`dsh-compaction-basic` 在派生请求之前通过 `agent/pre-step` 处理压力,而 `agent/request-error` 仅用于规范的上下文溢出。任一触发条件满足后,系统都会先执行可选的工具结果剪枝,再选择摘要。恢复发生在仍打开的步骤内,只有剪枝或摘要生成推进 surface replacement generation 时才重试,否则仍以原始请求错误为准。每次重试都会准备调用,并在派生请求之前协调保留的已渲染组装结果,不重复组装、pre-step 或用户消息准入。
|
||||
`dsh-compaction-basic` 在派生请求之前通过 `agent/pre-step` 处理压力,而 `agent/request-error` 仅用于规范的上下文溢出。任一触发条件满足后,系统都会先执行可选的工具结果剪枝,再选择摘要。恢复发生在仍打开的步骤内,只有剪枝或摘要生成推进 surface replacement generation 时才重试,否则仍以原始请求错误为准。每次重试都会在派生请求之前准备调用并解析工具条件,不重复 pre-step 或用户消息准入。只有已绑定路由或工具 schema 变化时才重新组装提示词。
|
||||
|
||||
以返回的 `agent/pre-step` 决策为准;通过包装 `next()` 的监听器会保留下游消息与 `startsRequestSeries`,除非有意替换。steering(中途引导)和注入的上下文在后续的认领操作取得其下一步骤批次后,会经过同一 waterfall(瀑布式事件)。
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: e89802f066e4955d
|
||||
zh: ea2fb4bef94e086d
|
||||
/deepseek-harness-architecture/turn-flow:
|
||||
en: e6402b28c0ed15fa
|
||||
zh: 6c07be9047b42056
|
||||
en: e7f529503233d805
|
||||
zh: f1d1e4a58e44a2ec
|
||||
/deepseek-harness-architecture/session-log:
|
||||
en: a2e35c43438fec6d
|
||||
zh: e27bdac8e9feff32
|
||||
|
||||
@@ -83,12 +83,13 @@ A **step** is one model request plus the tools it calls. A **turn** is zero or m
|
||||
```text
|
||||
turn/start
|
||||
claim next-step input plus one queued message
|
||||
assemble prompt sections + tool schemas
|
||||
agent/request -> prepareCall -> bind Agent.currentRoute()
|
||||
prepare variables + runtime context
|
||||
-> agent/pre-step; project runtime context at fallback
|
||||
reject | enter(messages, startsRequestSeries?)
|
||||
reject, or a first enter rewritten empty -> close the turn with no step
|
||||
step/start
|
||||
agent/request -> prepareCall (cancellation commits neither system nor users)
|
||||
resolve conditional tools + assemble prompt using that tool set
|
||||
reconcile system/message using the prepared call capability
|
||||
append entered messages as user/message; reconcile retained runtime context
|
||||
log request/header and request/context as needed
|
||||
@@ -107,7 +108,7 @@ turn/end
|
||||
|
||||
One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents.
|
||||
|
||||
`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes append non-empty updates after cached history, including supported tool updates; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)).
|
||||
`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. `agent/request` and `prepareCall()` bind the route before context preparation and `agent/pre-step`; cancellation commits no step or messages. Accepted steps resolve tool conditions before final assembly. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes runtime facts, reconciles the prompt, admits users once, restores removed context, logs headers, and freezes the request before streaming. Retries do not repeat `agent/pre-step`; prompt assembly repeats only when the resolved route or tool schemas change. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes append non-empty updates after cached history, including supported tool updates; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)).
|
||||
|
||||
The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes.
|
||||
|
||||
|
||||
@@ -87,12 +87,13 @@ AgentLoop 在启动已排队工作前等待串行 `agent/created`,初始化失
|
||||
```text
|
||||
turn/start
|
||||
claim next-step input plus one queued message
|
||||
assemble prompt sections + tool schemas
|
||||
agent/request -> prepareCall -> bind Agent.currentRoute()
|
||||
prepare variables + runtime context
|
||||
-> agent/pre-step; project runtime context at fallback
|
||||
reject | enter(messages, startsRequestSeries?)
|
||||
reject, or a first enter rewritten empty -> close the turn with no step
|
||||
step/start
|
||||
agent/request -> prepareCall (cancellation commits neither system nor users)
|
||||
resolve conditional tools + assemble prompt using that tool set
|
||||
reconcile system/message using the prepared call capability
|
||||
append entered messages as user/message; reconcile retained runtime context
|
||||
log request/header and request/context as needed
|
||||
@@ -111,7 +112,7 @@ turn/end
|
||||
|
||||
输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。
|
||||
|
||||
`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新,包括同时发生的受支持工具更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。
|
||||
`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复 `agent/pre-step`;仅在已解析路由或工具 schema 改变时重新组装提示词。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新,包括同时发生的受支持工具更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。
|
||||
|
||||
循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。
|
||||
|
||||
|
||||
@@ -2372,7 +2372,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-plan-mode`
|
||||
|
||||
- `inject`: `tools` · `systemPrompt` · `sessionProjections`
|
||||
- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts)
|
||||
- `source`: [`packages/plan/plan-mode/src/index.ts:67`](../packages/plan/plan-mode/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment-owned plan guidance. */
|
||||
@@ -3475,7 +3475,7 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-system-prompt`
|
||||
|
||||
- `source`: [`packages/core/system-prompt/src/index.ts:254`](../packages/core/system-prompt/src/index.ts)
|
||||
- `source`: [`packages/core/system-prompt/src/index.ts:266`](../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */
|
||||
@@ -4154,7 +4154,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tools`
|
||||
|
||||
- `inject`: `systemPrompt`
|
||||
- `source`: [`packages/core/tools/src/index.ts:675`](../packages/core/tools/src/index.ts)
|
||||
- `source`: [`packages/core/tools/src/index.ts:733`](../packages/core/tools/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config: how the registered tools are presented to the model. */
|
||||
|
||||
@@ -2374,7 +2374,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-plan-mode`
|
||||
|
||||
- `inject`: `tools` · `systemPrompt` · `sessionProjections`
|
||||
- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts)
|
||||
- `source`: [`packages/plan/plan-mode/src/index.ts:67`](../packages/plan/plan-mode/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment-owned plan guidance. */
|
||||
@@ -3477,7 +3477,7 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-system-prompt`
|
||||
|
||||
- `source`: [`packages/core/system-prompt/src/index.ts:254`](../packages/core/system-prompt/src/index.ts)
|
||||
- `source`: [`packages/core/system-prompt/src/index.ts:266`](../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */
|
||||
@@ -4156,7 +4156,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-tools`
|
||||
|
||||
- `inject`: `systemPrompt`
|
||||
- `source`: [`packages/core/tools/src/index.ts:675`](../packages/core/tools/src/index.ts)
|
||||
- `source`: [`packages/core/tools/src/index.ts:733`](../packages/core/tools/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config: how the registered tools are presented to the model. */
|
||||
|
||||
@@ -10,18 +10,18 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:369`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:267`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:276`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:305`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:313`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:294`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:326`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:343`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:359`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:286`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:387`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:619`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:599`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:626`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
@@ -71,14 +71,15 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime` |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:44`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `system-prompt/prepare` | `waterfall` | [`packages/core/system-prompt/src/index.ts:38`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`working-directory`](../packages/session/working-directory) |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:216`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:172`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:184`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:161`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:206`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:369`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:267`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:276`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:305`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:313`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:294`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:326`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:343`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:359`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:286`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:387`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:619`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:599`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:626`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
@@ -73,14 +73,15 @@
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`working-directory`](../packages/session/working-directory) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime` |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:44`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `system-prompt/prepare` | `waterfall` | [`packages/core/system-prompt/src/index.ts:38`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`working-directory`](../packages/session/working-directory) |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:216`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:172`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:184`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:161`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:206`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
|
||||
@@ -144,8 +144,8 @@
|
||||
en: 4294f78d26ed985b
|
||||
zh: 4294f78d26ed985b
|
||||
/session-persistence-event-catalog/events/plan/plan-mode-log-only:
|
||||
en: 862175bfe8692ef7
|
||||
zh: a8ab123bf674cf77
|
||||
en: f23bf0e5e6408adf
|
||||
zh: a9442b4c5cdeef0b
|
||||
/session-persistence-event-catalog/events/request:
|
||||
en: a8df82440ebdf5f8
|
||||
zh: a8df82440ebdf5f8
|
||||
@@ -1239,8 +1239,8 @@
|
||||
en: f43a635a14568780
|
||||
zh: 8d22226b045122eb
|
||||
/session-persistence-event-catalog/resolved-persistence-types/toolrestriction:
|
||||
en: d7d3c0238d9b373e
|
||||
zh: f49d93ee54e7a780
|
||||
en: 739d19df365ed5df
|
||||
zh: b1e41e56995fe238
|
||||
/session-persistence-event-catalog/resolved-persistence-types/toolresultmessage:
|
||||
en: 2258683c3cfa4ce3
|
||||
zh: 5663aa76c911761b
|
||||
@@ -1347,8 +1347,8 @@
|
||||
en: a74e3dfa51c987af
|
||||
zh: 6c3c87974d7a7260
|
||||
/session-persistence-event-catalog/resolved-persistence-types/active~2:
|
||||
en: a3de6a6fe9672c7a
|
||||
zh: a357481e27b3b81f
|
||||
en: 1bc41f689438a051
|
||||
zh: babdc92cc0ad089a
|
||||
/session-persistence-event-catalog/resolved-persistence-types/agentpreset:
|
||||
en: b8a0f8aac9708056
|
||||
zh: f5f004bd205b41e1
|
||||
@@ -1779,8 +1779,8 @@
|
||||
en: 08be4349548406a9
|
||||
zh: 8e7cf9966198ef78
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-tool-registry:
|
||||
en: 79db83dd2c430faf
|
||||
zh: 173f568241f5ce86
|
||||
en: bbcefadaa064365a
|
||||
zh: 0285b8b877d6193d
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-user:
|
||||
en: cb5d3fbab4debac6
|
||||
zh: 72add55254356149
|
||||
|
||||
@@ -700,7 +700,7 @@ Source: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/
|
||||
'plan/mode': { active: boolean }
|
||||
```
|
||||
|
||||
Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
|
||||
Source: [`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts)
|
||||
|
||||
### `request/*`
|
||||
|
||||
@@ -4996,7 +4996,7 @@ Sources: [`packages/llm/llm/src/types.ts:127`](../packages/llm/llm/src/types.ts)
|
||||
|
||||
SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f`
|
||||
|
||||
Sources: [`packages/core/tools/src/index.ts:701`](../packages/core/tools/src/index.ts)
|
||||
Sources: [`packages/core/tools/src/index.ts:759`](../packages/core/tools/src/index.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -5556,7 +5556,7 @@ Array of `unknown` (opaque).
|
||||
|
||||
SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5`
|
||||
|
||||
Sources: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
|
||||
Sources: [`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
@@ -7329,7 +7329,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49`
|
||||
|
||||
SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
|
||||
|
||||
Sources: [`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/index.ts)
|
||||
Sources: [`packages/core/tools/src/index.ts:42`](../packages/core/tools/src/index.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
|
||||
@@ -702,7 +702,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
'plan/mode': { active: boolean }
|
||||
```
|
||||
|
||||
来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
|
||||
来源:[`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts)
|
||||
|
||||
### `request/*`
|
||||
|
||||
@@ -4998,7 +4998,7 @@ SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8`
|
||||
|
||||
SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f`
|
||||
|
||||
来源:[`packages/core/tools/src/index.ts:701`](../packages/core/tools/src/index.ts)
|
||||
来源:[`packages/core/tools/src/index.ts:759`](../packages/core/tools/src/index.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -5558,7 +5558,7 @@ SHA-256: `b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7`
|
||||
|
||||
SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5`
|
||||
|
||||
来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
|
||||
来源:[`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/src/index.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
@@ -7331,7 +7331,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49`
|
||||
|
||||
SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
|
||||
|
||||
来源:[`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/index.ts)
|
||||
来源:[`packages/core/tools/src/index.ts:42`](../packages/core/tools/src/index.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
|
||||
Generated
+3
-3
@@ -22518,7 +22518,7 @@
|
||||
"digest": "20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5",
|
||||
"names": [],
|
||||
"sources": [
|
||||
"packages/plan/plan-mode/src/index.ts:52"
|
||||
"packages/plan/plan-mode/src/index.ts:51"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -22908,7 +22908,7 @@
|
||||
"packages/core/tools/src/index.ts#ToolRestriction"
|
||||
],
|
||||
"sources": [
|
||||
"packages/core/tools/src/index.ts:701"
|
||||
"packages/core/tools/src/index.ts:759"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -24142,7 +24142,7 @@
|
||||
"digest": "8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543",
|
||||
"names": [],
|
||||
"sources": [
|
||||
"packages/core/tools/src/index.ts:35"
|
||||
"packages/core/tools/src/index.ts:42"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+11
-5
@@ -61,8 +61,14 @@ Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types
|
||||
interface Agent {
|
||||
/** Session-backed Agent identity. */
|
||||
readonly id: SessionId
|
||||
/** The provider route and model this agent's requests use. */
|
||||
/** Initial request options; currentRoute() reports the bound request route. */
|
||||
readonly options: AgentOptions
|
||||
/**
|
||||
* Read the immutable route bound to this request and its tool batch.
|
||||
* @returns the actual provider and model selected by prepareCall().
|
||||
* @throws when no main request is prepared; never falls back to prior requests.
|
||||
*/
|
||||
currentRoute(): Readonly<Pick<LlmCallConfig, 'provider' | 'model'>>
|
||||
/** The live session this agent drives; its log is the durable source of truth. */
|
||||
readonly session: Session
|
||||
/** Agent-owned access to durable pending work. */
|
||||
@@ -952,16 +958,16 @@ Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/s
|
||||
|
||||
#### `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. On step admission, this runs after assembly and `step/start`, before the system prompt and accepted user batch are committed. Cancellation here or during subsequent `prepareCall()` resolution commits neither. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall 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. On step admission, this runs before context preparation and `agent/pre-step`. Cancellation here or during subsequent `prepareCall()` resolution opens no step and commits no system prompt or user batch. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* 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. On step admission,
|
||||
* this runs after assembly and `step/start`, before the system prompt and
|
||||
* accepted user batch are committed. Cancellation here or during subsequent
|
||||
* `prepareCall()` resolution commits neither. The prepared call capability
|
||||
* this runs before context preparation and `agent/pre-step`. Cancellation
|
||||
* here or during subsequent `prepareCall()` resolution opens no step and
|
||||
* commits no system prompt or user batch. The prepared call capability
|
||||
* governs prompt admission. Model-visible content must use logged channels;
|
||||
* this waterfall cannot mutate messages.
|
||||
* @param payload.agent - the agent making the model call.
|
||||
|
||||
@@ -65,8 +65,14 @@ interface AgentHandle {
|
||||
interface Agent {
|
||||
/** Session-backed Agent identity. */
|
||||
readonly id: SessionId
|
||||
/** The provider route and model this agent's requests use. */
|
||||
/** Initial request options; currentRoute() reports the bound request route. */
|
||||
readonly options: AgentOptions
|
||||
/**
|
||||
* Read the immutable route bound to this request and its tool batch.
|
||||
* @returns the actual provider and model selected by prepareCall().
|
||||
* @throws when no main request is prepared; never falls back to prior requests.
|
||||
*/
|
||||
currentRoute(): Readonly<Pick<LlmCallConfig, 'provider' | 'model'>>
|
||||
/** The live session this agent drives; its log is the durable source of truth. */
|
||||
readonly session: Session
|
||||
/** Agent-owned access to durable pending work. */
|
||||
@@ -962,16 +968,16 @@ Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/s
|
||||
|
||||
#### `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. On step admission, this runs after assembly and `step/start`, before the system prompt and accepted user batch are committed. Cancellation here or during subsequent `prepareCall()` resolution commits neither. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall 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. On step admission, this runs before context preparation and `agent/pre-step`. Cancellation here or during subsequent `prepareCall()` resolution opens no step and commits no system prompt or user batch. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* 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. On step admission,
|
||||
* this runs after assembly and `step/start`, before the system prompt and
|
||||
* accepted user batch are committed. Cancellation here or during subsequent
|
||||
* `prepareCall()` resolution commits neither. The prepared call capability
|
||||
* this runs before context preparation and `agent/pre-step`. Cancellation
|
||||
* here or during subsequent `prepareCall()` resolution opens no step and
|
||||
* commits no system prompt or user batch. The prepared call capability
|
||||
* governs prompt admission. Model-visible content must use logged channels;
|
||||
* this waterfall cannot mutate messages.
|
||||
* @param payload.agent - the agent making the model call.
|
||||
|
||||
@@ -50,7 +50,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
|
||||
### `ctx.planMode` — `PlanModeController`
|
||||
|
||||
`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool. Client carriers expose the projection's cropped `{ active, pending }` view.
|
||||
`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the conditional exit tool. Client carriers expose the projection's cropped `{ active, pending }` view.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
|
||||
@@ -50,7 +50,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
|
||||
### `ctx.planMode` — `PlanModeController`
|
||||
|
||||
`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool. Client carriers expose the projection's cropped `{ active, pending }` view.
|
||||
`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the conditional exit tool. Client carriers expose the projection's cropped `{ active, pending }` view.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
|
||||
@@ -184,23 +184,29 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine
|
||||
* added only by the assembly waterfall retain their accepted values. Current
|
||||
* suppression removes optional contexts, and missing required registrations
|
||||
* are restored in registry order. Sections, tools, and interpolation variables
|
||||
* retain the accepted assembly; their providers and waterfall do not rerun.
|
||||
* retain their accepted values during this refresh.
|
||||
* @param assembly - accepted assembly for this scope and step.
|
||||
* @param context - the same scope and current plugin-defined assembly fields.
|
||||
* @returns the accepted assembly with current runtime-context provider text.
|
||||
*/
|
||||
refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly
|
||||
refreshContext<T extends PromptPreparation>(assembly: T, context: AssembleContext = {}): T
|
||||
|
||||
/**
|
||||
* Assemble global and scoped providers, detach tool parameters, apply
|
||||
* canonical ordering, then run the assembly waterfall. Scoped sections and
|
||||
* variables shadow globals. The returned waterfall value is authoritative
|
||||
* except that an effective complete section is restored afterwards as the
|
||||
* sole prompt section.
|
||||
* @param context - the optional scope and plugin-defined assembly fields.
|
||||
* @returns the post-waterfall assembly with any complete prompt enforced.
|
||||
* Prepare variables and runtime facts before accepting a step. No tool
|
||||
* conditions or prompt sections are evaluated in this phase.
|
||||
* @param context - calling scope and request cancellation.
|
||||
* @returns prepared inputs after the preparation waterfall.
|
||||
*/
|
||||
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
|
||||
async prepare(context: AssembleContext = {}): Promise<PromptPreparation>
|
||||
|
||||
/**
|
||||
* Assemble prompt sections and tool declarations after request preparation.
|
||||
* The presentation waterfall cannot replace provider-owned tools.
|
||||
* @param context - calling scope, route-bound agent, and resolved tools.
|
||||
* @param preparation - accepted variables and runtime facts; prepared here when omitted.
|
||||
* @returns the complete assembly with ordered tool declarations.
|
||||
*/
|
||||
async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise<PromptAssembly>
|
||||
```
|
||||
|
||||
Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)
|
||||
@@ -213,13 +219,13 @@ Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-
|
||||
|
||||
#### `system-prompt/assemble` — waterfall
|
||||
|
||||
Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt.
|
||||
Presentation waterfall over assembled sections, contexts, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Expert waterfall over the assembled sections, contexts, tools, and variables.
|
||||
* Presentation waterfall over assembled sections, contexts, and variables.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
|
||||
* receive only that scope's assemblies. The returned value is authoritative.
|
||||
* receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned.
|
||||
* A supplied signal controls only this explicit assembly request and must not
|
||||
* be retained to control later turns. A registered complete section is
|
||||
* restored after this waterfall, so listeners cannot add to or replace
|
||||
@@ -228,7 +234,7 @@ Expert waterfall over the assembled sections, contexts, tools, and variables. Sc
|
||||
* @param context - the caller's per-assembly context.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
|
||||
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptPresentation, context: AssembleContext, next: () => Promise<PromptPresentation>): Promise<PromptPresentation>
|
||||
```
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
@@ -250,5 +256,25 @@ Emitted when any prompt provider changes. This registry notification is unfilter
|
||||
'system-prompt/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
<a id="system-promptprepare--waterfall"></a>
|
||||
|
||||
#### `system-prompt/prepare` — waterfall
|
||||
|
||||
Prepare variables and runtime context before step admission.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Prepare variables and runtime context before step admission.
|
||||
* @param preparation - registered variables and context contributions.
|
||||
* @param context - calling scope and cancellation.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'system-prompt/prepare'(this: Scoped<SystemPrompt>, preparation: PromptPreparation, context: AssembleContext, next: () => Promise<PromptPreparation>): Promise<PromptPreparation>
|
||||
```
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
@@ -184,23 +184,29 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine
|
||||
* added only by the assembly waterfall retain their accepted values. Current
|
||||
* suppression removes optional contexts, and missing required registrations
|
||||
* are restored in registry order. Sections, tools, and interpolation variables
|
||||
* retain the accepted assembly; their providers and waterfall do not rerun.
|
||||
* retain their accepted values during this refresh.
|
||||
* @param assembly - accepted assembly for this scope and step.
|
||||
* @param context - the same scope and current plugin-defined assembly fields.
|
||||
* @returns the accepted assembly with current runtime-context provider text.
|
||||
*/
|
||||
refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly
|
||||
refreshContext<T extends PromptPreparation>(assembly: T, context: AssembleContext = {}): T
|
||||
|
||||
/**
|
||||
* Assemble global and scoped providers, detach tool parameters, apply
|
||||
* canonical ordering, then run the assembly waterfall. Scoped sections and
|
||||
* variables shadow globals. The returned waterfall value is authoritative
|
||||
* except that an effective complete section is restored afterwards as the
|
||||
* sole prompt section.
|
||||
* @param context - the optional scope and plugin-defined assembly fields.
|
||||
* @returns the post-waterfall assembly with any complete prompt enforced.
|
||||
* Prepare variables and runtime facts before accepting a step. No tool
|
||||
* conditions or prompt sections are evaluated in this phase.
|
||||
* @param context - calling scope and request cancellation.
|
||||
* @returns prepared inputs after the preparation waterfall.
|
||||
*/
|
||||
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
|
||||
async prepare(context: AssembleContext = {}): Promise<PromptPreparation>
|
||||
|
||||
/**
|
||||
* Assemble prompt sections and tool declarations after request preparation.
|
||||
* The presentation waterfall cannot replace provider-owned tools.
|
||||
* @param context - calling scope, route-bound agent, and resolved tools.
|
||||
* @param preparation - accepted variables and runtime facts; prepared here when omitted.
|
||||
* @returns the complete assembly with ordered tool declarations.
|
||||
*/
|
||||
async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise<PromptAssembly>
|
||||
```
|
||||
|
||||
Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)
|
||||
@@ -213,13 +219,13 @@ Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-
|
||||
|
||||
#### `system-prompt/assemble` — waterfall
|
||||
|
||||
Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt.
|
||||
Presentation waterfall over assembled sections, contexts, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope's system prompt.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Expert waterfall over the assembled sections, contexts, tools, and variables.
|
||||
* Presentation waterfall over assembled sections, contexts, and variables.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
|
||||
* receive only that scope's assemblies. The returned value is authoritative.
|
||||
* receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned.
|
||||
* A supplied signal controls only this explicit assembly request and must not
|
||||
* be retained to control later turns. A registered complete section is
|
||||
* restored after this waterfall, so listeners cannot add to or replace
|
||||
@@ -228,7 +234,7 @@ Expert waterfall over the assembled sections, contexts, tools, and variables. Sc
|
||||
* @param context - the caller's per-assembly context.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
|
||||
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptPresentation, context: AssembleContext, next: () => Promise<PromptPresentation>): Promise<PromptPresentation>
|
||||
```
|
||||
|
||||
Types: [Scoped](scope.zh.md)
|
||||
@@ -250,5 +256,25 @@ Emitted when any prompt provider changes. This registry notification is unfilter
|
||||
'system-prompt/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
<a id="system-promptprepare--waterfall"></a>
|
||||
|
||||
#### `system-prompt/prepare` — waterfall
|
||||
|
||||
Prepare variables and runtime context before step admission.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Prepare variables and runtime context before step admission.
|
||||
* @param preparation - registered variables and context contributions.
|
||||
* @param context - calling scope and cancellation.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'system-prompt/prepare'(this: Scoped<SystemPrompt>, preparation: PromptPreparation, context: AssembleContext, next: () => Promise<PromptPreparation>): Promise<PromptPreparation>
|
||||
```
|
||||
|
||||
Types: [Scoped](scope.zh.md)
|
||||
|
||||
Source: [`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
@@ -206,6 +206,8 @@ interface ToolExecutionInput {
|
||||
readonly arguments: unknown
|
||||
/** The agent on whose behalf the call runs (set by the agent loop). */
|
||||
readonly agent?: Agent
|
||||
/** Request admission shared by Native and PTC dispatch. Conditional tools require it. */
|
||||
readonly toolSet?: ResolvedToolSet
|
||||
/**
|
||||
* Opaque token of the enclosing transport execution, when one exists. PTC
|
||||
* mode sets this on SDK sub-dispatches so commit-style observers can wait for
|
||||
@@ -525,9 +527,10 @@ presentAs(mode: ToolPresentationMode): () => void
|
||||
* Register globally or in the calling agent scope. Scoped tools shadow
|
||||
* globals; duplicates within one layer and the reserved `run_code` name fail.
|
||||
* @param definition - tool schema, execution, and optional finalization/presentation callbacks.
|
||||
* @param options - optional request-time activation condition.
|
||||
* @returns the exact disposer that unregisters the tool.
|
||||
*/
|
||||
register(definition: ToolDefinition): () => void
|
||||
register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void
|
||||
|
||||
/**
|
||||
* Restrict global tools for the calling agent scope. Empty filters, unknown
|
||||
@@ -557,17 +560,28 @@ guard(guard: ToolGuard): () => void
|
||||
* actually executed.
|
||||
* @param name - the tool name as registered.
|
||||
* @param scope - the viewing scope (the agent); omitted = the global view.
|
||||
* @param toolSet - request admission; omit only to inspect registered definitions.
|
||||
* @returns the definition the scope resolves, or undefined when none is visible.
|
||||
*/
|
||||
get(name: string, scope?: ScopeKey): ToolDefinition | undefined
|
||||
get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined
|
||||
|
||||
/**
|
||||
* Resolve conditions once for this request without changing shared registrations.
|
||||
* Cancellation or a rejected condition publishes no partial selection. Removed
|
||||
* registrations cannot be restored by a late condition result.
|
||||
* @param context - agent with a bound route and request cancellation.
|
||||
* @returns the complete selection; the caller disposes it after its tool batch settles.
|
||||
*/
|
||||
async resolve(context: ToolActivationContext): Promise<ResolvedToolSet>
|
||||
|
||||
/**
|
||||
* Project visible definitions onto the allowlisted model-facing schema fields,
|
||||
* excluding execution and presentation callbacks.
|
||||
* @param scope - the viewing scope (the agent); omitted = the global view.
|
||||
* @param toolSet - request admission, or omitted for registered-definition inspection.
|
||||
* @returns one deep-cloned schema per visible tool.
|
||||
*/
|
||||
schemas(scope?: ScopeKey): ToolSchema[]
|
||||
schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[]
|
||||
|
||||
/**
|
||||
* Classify a pending call through the caller's visible tool definition. Only
|
||||
|
||||
@@ -206,6 +206,8 @@ interface ToolExecutionInput {
|
||||
readonly arguments: unknown
|
||||
/** The agent on whose behalf the call runs (set by the agent loop). */
|
||||
readonly agent?: Agent
|
||||
/** Request admission shared by Native and PTC dispatch. Conditional tools require it. */
|
||||
readonly toolSet?: ResolvedToolSet
|
||||
/**
|
||||
* Opaque token of the enclosing transport execution, when one exists. PTC
|
||||
* mode sets this on SDK sub-dispatches so commit-style observers can wait for
|
||||
@@ -525,9 +527,10 @@ presentAs(mode: ToolPresentationMode): () => void
|
||||
* Register globally or in the calling agent scope. Scoped tools shadow
|
||||
* globals; duplicates within one layer and the reserved `run_code` name fail.
|
||||
* @param definition - tool schema, execution, and optional finalization/presentation callbacks.
|
||||
* @param options - optional request-time activation condition.
|
||||
* @returns the exact disposer that unregisters the tool.
|
||||
*/
|
||||
register(definition: ToolDefinition): () => void
|
||||
register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void
|
||||
|
||||
/**
|
||||
* Restrict global tools for the calling agent scope. Empty filters, unknown
|
||||
@@ -557,17 +560,28 @@ guard(guard: ToolGuard): () => void
|
||||
* actually executed.
|
||||
* @param name - the tool name as registered.
|
||||
* @param scope - the viewing scope (the agent); omitted = the global view.
|
||||
* @param toolSet - request admission; omit only to inspect registered definitions.
|
||||
* @returns the definition the scope resolves, or undefined when none is visible.
|
||||
*/
|
||||
get(name: string, scope?: ScopeKey): ToolDefinition | undefined
|
||||
get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined
|
||||
|
||||
/**
|
||||
* Resolve conditions once for this request without changing shared registrations.
|
||||
* Cancellation or a rejected condition publishes no partial selection. Removed
|
||||
* registrations cannot be restored by a late condition result.
|
||||
* @param context - agent with a bound route and request cancellation.
|
||||
* @returns the complete selection; the caller disposes it after its tool batch settles.
|
||||
*/
|
||||
async resolve(context: ToolActivationContext): Promise<ResolvedToolSet>
|
||||
|
||||
/**
|
||||
* Project visible definitions onto the allowlisted model-facing schema fields,
|
||||
* excluding execution and presentation callbacks.
|
||||
* @param scope - the viewing scope (the agent); omitted = the global view.
|
||||
* @param toolSet - request admission, or omitted for registered-definition inspection.
|
||||
* @returns one deep-cloned schema per visible tool.
|
||||
*/
|
||||
schemas(scope?: ScopeKey): ToolSchema[]
|
||||
schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[]
|
||||
|
||||
/**
|
||||
* Classify a pending call through the caller's visible tool definition. Only
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/conditional-tools/guide.md
|
||||
/:
|
||||
en: 0a9569f33806d6bf
|
||||
zh: 3bd885a0d518aad8
|
||||
/update-conditional-tools-and-prompt-hooks:
|
||||
en: a292521a564f5f0b
|
||||
zh: 2e15d7b96d966b29
|
||||
/update-conditional-tools-and-prompt-hooks/change:
|
||||
en: 018c4ade3568fbc5
|
||||
zh: e2f0f9c1467250be
|
||||
/update-conditional-tools-and-prompt-hooks/migration:
|
||||
en: abe994f27c41f65e
|
||||
zh: 106d04a591a0faa2
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Custom tool and prompt plugins must use request-owned tool admission and the earlier route preparation stage."
|
||||
---
|
||||
|
||||
# Update conditional tools and prompt hooks
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`agent/request` and `prepareCall()` bind the route before `agent/pre-step`. Runtime context preparation uses `system-prompt/prepare`; `system-prompt/assemble` receives presentation fields without a tool array. Tool declarations remain owned by registered providers. Conditional tools require a resolved request set for execution, including nested PTC calls. Replacing a registration does not authorize an outstanding call to run the replacement.
|
||||
|
||||
`exit_plan_mode` is offered only while committed plan mode is active. A failed plan-state write stops step admission and keeps the pending selection. Existing Sessions require no format migration.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Declare tool conditions with `ctx.tools.register(definition, { when })`. Read the bound route through `agent.currentRoute()`; never infer it from initial options or the preceding Session header.
|
||||
2. Move runtime-context preparation listeners to `system-prompt/prepare`. Keep presentation changes in `system-prompt/assemble` and stop replacing its tool list.
|
||||
3. For programmatic conditional calls, resolve `{ agent, signal }` through `ctx.tools.resolve()`, pass the resulting `toolSet` to assembly and execution, and dispose it after the batch settles. AgentLoop does this for model calls. Pass `context.toolSet` when tool guidance queries `ctx.tools.get()` or `schemas()`.
|
||||
4. Update custom Agent implementations with `currentRoute()`, valid only while a main request is bound. Verify that inactive tools are absent from declarations and that a historical name returns `UNKNOWN_TOOL` without reaching the implementation.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "自定义工具和提示词插件必须使用请求拥有的工具准入集合以及提前的路由准备阶段。"
|
||||
---
|
||||
|
||||
# 更新条件工具与提示词钩子
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
`agent/request` 与 `prepareCall()` 在 `agent/pre-step` 之前绑定路由。运行时上下文准备使用 `system-prompt/prepare`;`system-prompt/assemble` 接收不含工具数组的展示字段。工具声明仍由已注册提供方持有。条件工具执行需要已解析的请求集合,包括嵌套 PTC 调用。替换注册不会授权尚未执行的旧调用使用新实现。
|
||||
|
||||
`exit_plan_mode` 只在已提交的计划模式启用时提供。计划状态写入失败会停止步骤接纳并保留待提交选择。现有 Session 无需格式迁移。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 使用 `ctx.tools.register(definition, { when })` 声明工具条件。通过 `agent.currentRoute()` 读取已绑定路由,不从初始选项或先前 Session 请求头推断。
|
||||
2. 将运行时上下文准备监听器迁移到 `system-prompt/prepare`。展示变更留在 `system-prompt/assemble`,停止替换它的工具列表。
|
||||
3. 程序化调用条件工具时,通过 `ctx.tools.resolve()` 解析 `{ agent, signal }`,将返回的 `toolSet` 传给组装与执行,并在批次结束后释放。AgentLoop 会为模型调用完成这些操作。工具指导查询 `ctx.tools.get()` 或 `schemas()` 时传入 `context.toolSet`。
|
||||
4. 为自定义 Agent 实现补充 `currentRoute()`,仅在主请求已绑定期间可用。验证未启用工具不出现在声明中,且使用历史名称调用会返回 `UNKNOWN_TOOL`,不会进入实现。
|
||||
@@ -21,6 +21,7 @@ export async function registerAgent(ctx: Context, rawId: string): Promise<Agent
|
||||
const id = SessionId(rawId)
|
||||
const session = Session.create(id)
|
||||
const agent: Agent & { disposeScope: () => Promise<void> } = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -226,6 +226,7 @@ describe('JobController', () => {
|
||||
async function liveOwner(ctx: Context, rawId: string): Promise<Agent> {
|
||||
const session = ctx.sessions.create(SessionId(rawId))
|
||||
const owner: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -26,6 +26,7 @@ async function harness(): Promise<Harness> {
|
||||
/** A registered idle Agent double: the gate reads only its Session header. */
|
||||
async function liveAgent(ctx: Context, session: Session): Promise<Agent> {
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -149,6 +149,7 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{
|
||||
let idle = Promise.resolve()
|
||||
let running = false
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: session.id,
|
||||
options: createOptions.agentOptions ?? {},
|
||||
session,
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
en: 169f100ba62f2120
|
||||
zh: 3fd4fc0bd73b2370
|
||||
/deepseek-ai-dsh-session-reference/use-this-package/configuration:
|
||||
en: d9abc1b28c3feb40
|
||||
zh: ac85539027b37130
|
||||
en: 5dfb1f0c52d300bf
|
||||
zh: ae731dd6fcd93027
|
||||
/deepseek-ai-dsh-session-reference/understand-the-implementation:
|
||||
en: db349483e453caf7
|
||||
zh: 70035c291e1efe84
|
||||
/deepseek-ai-dsh-session-reference/understand-the-implementation/design-concept:
|
||||
en: 5c7712ca91eaae1c
|
||||
zh: 2d923b605afa7a88
|
||||
en: d7ff10eae41712ad
|
||||
zh: 2b91a16dee11808e
|
||||
/deepseek-ai-dsh-session-reference/understand-the-implementation/source-map:
|
||||
en: 7c7b413082507150
|
||||
zh: 0229eb73d441b305
|
||||
|
||||
@@ -50,7 +50,7 @@ For a truncated reference, an optional spill backend saves the full captured tex
|
||||
| `maxReferenceBytes` | automatic | Explicit maximum serialized JSON bytes per source; overrides the automatic budget exactly |
|
||||
| `referenceContextFraction` | `0.2` | Context-window fraction per source, from `0` to `1` |
|
||||
|
||||
The automatic budget is `max(65536, floor(contextWindow × 4 × referenceContextFraction))` bytes per source. Model context capacity is measured in tokens; four bytes per token is a sizing heuristic, not an exact token conversion. A missing route, LLM service, adapter, or capacity uses 64 KiB; other model metadata lookup errors and cancellation fail preparation.
|
||||
The automatic budget is `max(65536, floor(contextWindow × 4 × referenceContextFraction))` bytes per source. Model context capacity is measured in tokens; four bytes per token is a sizing heuristic, not an exact token conversion. A missing LLM service, adapter, or capacity uses 64 KiB; other model metadata lookup errors and cancellation fail preparation.
|
||||
|
||||
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-reference) is the exhaustive source for every accepted field and its JSDoc.
|
||||
|
||||
@@ -70,7 +70,7 @@ Preparation reads each referenced session's current surface exactly once, when t
|
||||
|
||||
The resolver discovers optional storage through `ctx.get("spillStore")` and saves only truncated references. Storage ownership is the target session; the source descriptor identifies the referenced session and label, without a fabricated tool call. Cancellation is checked after the asynchronous save and prevents publication even if an artifact was written. Artifact expiry remains the backend's existing policy.
|
||||
|
||||
The budget uses the provider and model captured after `system-prompt/assemble` completes for the target agent. Direct `prepare` calls before any assembly use agent options; session headers do not select the budget model. Diagnostic assemblies without an agent do not affect captured routes.
|
||||
The budget uses `agent.currentRoute()`, bound before step input preparation. The agent must have a prepared main request when a model-relative budget is needed; initial options and stored headers do not select the budget model.
|
||||
|
||||
### Source map
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ kind: "package-reference"
|
||||
| `maxReferenceBytes` | 自动 | 每个来源的最大序列化 JSON 字节数;显式设置时精确覆盖自动预算 |
|
||||
| `referenceContextFraction` | `0.2` | 每个来源的上下文窗口比例,范围为 `0` 到 `1` |
|
||||
|
||||
自动预算为每个来源 `max(65536, floor(contextWindow × 4 × referenceContextFraction))` 字节。模型上下文容量以 token 计量;每个 token 四字节是容量估算,不是精确的 token 换算。缺少路由、LLM(大语言模型)服务、适配器或容量时使用 64 KiB;其他模型元数据查询错误与取消会使准备失败。
|
||||
自动预算为每个来源 `max(65536, floor(contextWindow × 4 × referenceContextFraction))` 字节。模型上下文容量以 token 计量;每个 token 四字节是容量估算,不是精确的 token 换算。缺少 LLM(大语言模型)服务、适配器或容量时使用 64 KiB;其他模型元数据查询错误与取消会使准备失败。
|
||||
|
||||
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-reference)是每个受支持字段及其 JSDoc 的穷尽式真源。
|
||||
|
||||
@@ -70,7 +70,7 @@ kind: "package-reference"
|
||||
|
||||
解析器通过 `ctx.get("spillStore")` 获取可选存储,只保存被截断的引用。存储归目标会话所有;来源信息标识被引用的源会话与标签,不伪造工具调用。异步保存后会检查取消,即使产物已写入,也会阻止发布。产物过期仍遵循后端既有策略。
|
||||
|
||||
预算使用目标 agent 的 `system-prompt/assemble` 完成后捕获的提供方与模型。首次组装前直接调用 `prepare` 时使用 agent 选项;会话头不决定预算模型。不带 agent 的诊断组装不会影响已捕获路由。
|
||||
预算使用步骤输入准备前绑定的 `agent.currentRoute()`。需要模型相对预算时,agent 必须已有准备完成的主请求;初始选项与已存储请求头不决定预算模型。
|
||||
|
||||
### 源码地图
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ export class SessionReferenceResolver extends TypertRemoteService {
|
||||
})
|
||||
|
||||
private readonly config: Required<Omit<Config, 'maxReferenceBytes'>> & { maxReferenceBytes: number | undefined }
|
||||
private readonly assembledRoutes = new WeakMap<Agent, { provider: string | undefined; model: string | undefined }>()
|
||||
|
||||
constructor(ctx: Context, config: Config = {}) {
|
||||
super(ctx, 'sessionReferenceResolver')
|
||||
@@ -123,15 +122,6 @@ export class SessionReferenceResolver extends TypertRemoteService {
|
||||
'SESSION_REFERENCE_INVALID_CONFIG',
|
||||
)
|
||||
}
|
||||
// Prepend observes model-selection overrides after downstream assembly completes.
|
||||
ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
|
||||
const assembly = await next()
|
||||
if (context.agent !== undefined) {
|
||||
const { provider, model } = assembly.variables
|
||||
this.assembledRoutes.set(context.agent, { provider, model })
|
||||
}
|
||||
return assembly
|
||||
}, { prepend: true })
|
||||
ctx.on('agent/pre-step', async ({ agent, signal }, next): Promise<PreStepDecision> => {
|
||||
const decision = await next()
|
||||
if (decision.kind === 'reject') return decision
|
||||
@@ -359,10 +349,9 @@ export class SessionReferenceResolver extends TypertRemoteService {
|
||||
|
||||
private async referenceBudget(agent: Agent, signal: AbortSignal | undefined): Promise<number> {
|
||||
if (this.config.maxReferenceBytes !== undefined) return this.config.maxReferenceBytes
|
||||
// Options seed direct preparation; an assembled route owns model-step preparation.
|
||||
const { provider, model } = this.assembledRoutes.get(agent) ?? agent.options
|
||||
const { provider, model } = agent.currentRoute()
|
||||
const llm = this.ctx.get('llm')
|
||||
if (provider === undefined || model === undefined || llm === undefined) return DEFAULT_MAX_REFERENCE_BYTES
|
||||
if (llm === undefined) return DEFAULT_MAX_REFERENCE_BYTES
|
||||
let info: LlmResolvedModelInfo
|
||||
try {
|
||||
info = await settleWithCancellation(llm.resolveModelInfo(provider, model, signal), signal)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { agentEvents, installModelSelection, type Agent, type ModelSelectionRef } from '@deepseek-ai/dsh-agent'
|
||||
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compaction'
|
||||
import LlmRuntime, { createDeveloperMessage, createMessage, createSystemMessage, createToolResultMessage, createUserMessage, LlmError, ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
|
||||
@@ -86,7 +86,8 @@ function withProjectionCache(
|
||||
}
|
||||
|
||||
function fakeAgent(session: Session): Agent {
|
||||
return { id: session.id, session, options: {} } as Agent
|
||||
const options: Agent['options'] = {}
|
||||
return { id: session.id, session, options, currentRoute: () => ({ provider: options.provider ?? 'seed', model: options.model ?? 'seed' }) } as Agent
|
||||
}
|
||||
|
||||
function expectCode(code: SessionReferenceErrorCode): Error {
|
||||
@@ -533,12 +534,10 @@ describe('model-relative reference budgets', () => {
|
||||
else expect(resolve).toHaveBeenCalledWith('seed', 'seed', undefined)
|
||||
})
|
||||
|
||||
it('uses the assembled selection, not the header, seed, or next selected model', async () => {
|
||||
it('uses the bound request route independently of stored and initial routes', async () => {
|
||||
const { ctx, agent, source, resolve } = await setup()
|
||||
const selection: ModelSelectionRef = { current: { provider: 'selected', model: 'large' }, assembled: undefined }
|
||||
installModelSelection(ctx, selection)
|
||||
await ctx.systemPrompt.assemble({ agent, scope: agent })
|
||||
selection.current = { provider: 'selected', model: 'small' }
|
||||
let route = { provider: 'selected', model: 'large' }
|
||||
vi.spyOn(agent, 'currentRoute').mockImplementation(() => route)
|
||||
const message = createUserMessage({ source: { kind: 'user' }, content: [{ type: 'text', text: formatSessionReferenceMention({ sessionId: source.id }) }] })
|
||||
const signal = new AbortController().signal
|
||||
const enter = () => agentEvents(ctx, agent).waterfall('agent/pre-step', { messages: [message], turn: 1, step: 1, signal },
|
||||
@@ -551,6 +550,7 @@ describe('model-relative reference budgets', () => {
|
||||
expect(bytes({ content: [], additionalContext: firstContext })).toBe(160_000)
|
||||
expect(resolve).toHaveBeenLastCalledWith('selected', 'large', signal)
|
||||
await ctx.systemPrompt.assemble({ agent, scope: agent })
|
||||
route = { provider: 'selected', model: 'small' }
|
||||
resolve.mockResolvedValue({ provider: 'selected', id: 'small', name: 'small', context: { contextWindow: 8_000 } })
|
||||
const second = await enter()
|
||||
if (second.kind !== 'enter' || second.messages[1] === undefined) throw new Error('expected reference context')
|
||||
@@ -558,23 +558,13 @@ describe('model-relative reference budgets', () => {
|
||||
expect(resolve).toHaveBeenLastCalledWith('selected', 'small', signal)
|
||||
})
|
||||
|
||||
it('uses the floor for absent metadata, service, or assembled route and ignores diagnostic assemblies', async () => {
|
||||
it('uses the floor for absent metadata or service', async () => {
|
||||
const { ctx, agent, resolve, prepare, llmFiber } = await setup()
|
||||
await ctx.systemPrompt.assemble()
|
||||
resolve.mockResolvedValue({ provider: 'seed', id: 'seed', name: 'seed' })
|
||||
expect(bytes(await prepare())).toBe(65_536)
|
||||
expect(resolve).toHaveBeenCalledOnce()
|
||||
await ctx.systemPrompt.assemble({ agent, scope: agent })
|
||||
expect(bytes(await prepare())).toBe(65_536)
|
||||
expect(resolve).toHaveBeenCalledOnce()
|
||||
delete agent.options.model
|
||||
const other = fakeAgent(agent.session)
|
||||
other.options.provider = 'seed'
|
||||
await ctx.sessionReferenceResolver.prepare(other, [], [{ sessionId: SessionId('source') }])
|
||||
expect(resolve).toHaveBeenCalledOnce()
|
||||
await llmFiber.dispose()
|
||||
other.options.model = 'seed'
|
||||
expect(bytes(await ctx.sessionReferenceResolver.prepare(other, [], [{ sessionId: SessionId('source') }]))).toBe(65_536)
|
||||
expect(bytes(await ctx.sessionReferenceResolver.prepare(agent, [], [{ sessionId: SessionId('source') }]))).toBe(65_536)
|
||||
})
|
||||
|
||||
it('uses the floor when the real LLM runtime has no adapter for the route', async () => {
|
||||
@@ -619,7 +609,7 @@ describe('model-relative reference budgets', () => {
|
||||
expect(read).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('removes both listeners when the resolver fiber is disposed', async () => {
|
||||
it('removes the preparation listener when the resolver fiber is disposed', async () => {
|
||||
const { ctx, agent, source, resolve, resolverFiber } = await setup()
|
||||
const resolver = ctx.sessionReferenceResolver
|
||||
await resolverFiber.dispose()
|
||||
|
||||
@@ -47,6 +47,7 @@ async function mount(config: Config = {}) {
|
||||
|
||||
function sessionAgent(session: Session, id = 'agent'): Agent {
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: SessionId(id),
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -106,6 +106,7 @@ async function mount(
|
||||
|
||||
function sessionAgent(session: Session, id = 'agent'): Agent {
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: SessionId(id),
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
en: 3c5721bc00a0bbf4
|
||||
zh: 711f832343ffbb75
|
||||
/deepseek-ai-dsh-agent-loop/understand-the-implementation/turn-and-step-flow:
|
||||
en: 28939261ccc7defe
|
||||
zh: 9d157a321307ebd4
|
||||
en: 29c33bc92b4a8db6
|
||||
zh: d1152a6656bb772d
|
||||
/deepseek-ai-dsh-agent-loop/understand-the-implementation/failure-and-cancellation:
|
||||
en: eea56903c4ff82f5
|
||||
zh: c0d5abb5767780ed
|
||||
@@ -63,8 +63,8 @@
|
||||
en: ed0e841bed26a836
|
||||
zh: f30d91bc5a64fa20
|
||||
/deepseek-ai-dsh-agent-loop/model-experience/complete-conversation-request/token-effect:
|
||||
en: ad93d8d61968a25d
|
||||
zh: 268ff858b0fa96e8
|
||||
en: 3862d03b3b4d6ec4
|
||||
zh: 164c6ecc8a21fdb1
|
||||
/deepseek-ai-dsh-agent-loop/model-experience/complete-conversation-request/kv-cache-effect:
|
||||
en: c002d27c825d04e8
|
||||
zh: b0dbdd95d3b3b6b5
|
||||
|
||||
@@ -117,7 +117,7 @@ The loop is the production acquisition point for session write handles. When `ct
|
||||
|
||||
### Turn and step flow
|
||||
|
||||
The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`. `AgentLoop` registers the standard `inbox` projection for its service lifetime, so cold reads work before any Agent exists and after all Agents unload. Its package-internal `ReactLoopInbox` uses that shared projection for structural commands and loop-only claims. At a turn boundary it opens the durable turn, then atomically claims pending next-step input plus one queued prompt; between steps it claims only next-step input. The driver assembles the prompt and tools and runs `agent/pre-step`; the fallback projects runtime context after listeners run. A rejected decision or empty first batch opens no step. On the first attempt after acceptance, `step/start` precedes the `agent/request` waterfall and `prepareCall()`; neither async phase sees the pending system prompt or accepted users committed to history, and cancellation during either commits neither. For every attempt, the loop then synchronously reconciles the rendered prompt against the surviving `system/message` nodes using the prepared call capability, refreshes registered runtime facts without reassembling the bound prompt or tools, appends the accepted `user/message` batch only on the first attempt with its owned context candidate replaced in place, and restores removed runtime context, logs the header and context as needed, and derives and freezes the request before streaming through that bound prepared call. Retries reuse the same rendered assembly without repeating assembly, `agent/pre-step`, or user admission. Reconciliation sees pre-step and retry compaction; a broken series consolidates the prompt at the head rather than appending an update after already-committed users. The request is `header.config`, `deriveMessages()`, and `header.tools`; it carries no `system` field. Each model attempt emits one process-local `start`, emits every `chunk` only after the matching durable assistant-frame settlement, and emits exactly one terminal `end`; final assembly or message-append failure settles it as `aborted`, while `committed` follows the durable `assistant/message`. Each successful model call appends one message anchor, and a cancelled stream appends an `interrupted: true` anchor with the delivered prefix so the next request contains what the user saw. Within a step, exclusive calls form barriers and parallel-safe calls use the bounded rolling pool; policy, durable results... (line truncated to 2000 chars)
|
||||
The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`. `AgentLoop` registers the standard `inbox` projection for its service lifetime, so cold reads work before any Agent exists and after all Agents unload. Its package-internal `ReactLoopInbox` uses that shared projection for structural commands and loop-only claims. At a turn boundary it opens the durable turn, then atomically claims pending next-step input plus one queued prompt; between steps it claims only next-step input. The driver binds the route through `agent/request` and `prepareCall()`, prepares variables and runtime context, then runs `agent/pre-step`; its fallback projects that context. A rejected decision or empty first batch opens no step. Cancellation during route preparation commits neither a system prompt nor users. After acceptance, `step/start` precedes asynchronous tool activation and final prompt assembly. For every attempt, the loop then synchronously reconciles the rendered prompt against the surviving `system/message` nodes using the prepared call capability, refreshes registered runtime facts, appends the accepted `user/message` batch only on the first attempt with its owned context candidate replaced in place, and restores removed runtime context, logs the header and context as needed, and derives and freezes the request before streaming through that bound prepared call. Retries resolve tool conditions against their bound route without repeating `agent/pre-step` or user admission. An unchanged route and tool schema set reuse the accepted prompt; a changed route or tool set assembles the corresponding prompt again. The request-owned tool set is closed after dispatch settles, and the bound Agent route is cleared when the step ends. Reconciliation sees pre-step and retry compaction; a broken series consolidates the prompt at the head rather than appending an update after already-committed users. The request is `header.config`, `deriveMessages()`, and `header.tools`; it carries no `system` field. Each model attempt emits one process-local `start`, emits every `chunk` only after the matching durable assistant-frame settlement, and emits exactly one terminal `end`; final assembly or message-append failure settles it as `aborted`, while `committed` follows the durable `assistant/message`. Each successful model call appends one message anchor, and a cancelled stream appends an `interrupted: true` anchor with the delivered prefix so the next request contains what the user saw. Within a step, exclusive calls form barriers and parallel-safe calls use the bounded rolling pool; policy, durable results... (line truncated to 2000 chars)
|
||||
|
||||
Prompt admission uses the actual `prepareCall()` result, not the preceding `request/context`. With no system node, even an empty prompt is appended (reserving node 0 without a wire message). On an incapable route or at a new request series, a non-empty rendering is consolidated at the first system node: each non-empty later system node receives a logged per-node empty replacement, then the head is rewritten if needed. Dormant empty tails need no replacement and do not determine the effective text. Consolidation applies even when the latest effective text is unchanged. On a continuing `in-history` series, an unchanged effective prompt produces no event and a non-empty change appends. An empty rendering clears every non-empty later system node through a logged per-node empty replacement, then empties the head if needed, regardless of route or series state. No older instructions remain model-visible. An empty head with no active later system node means no prompt; repeated clears and resume leave it empty. A restored non-empty prompt follows the same route/series rule: a continuing capable route may append it, while an incapable route or new series refills the head. A step starts a series when the pre-step decision declares `startsRequestSeries` or `session.surface.contentGeneration` changed since attachment or the last request (a replacement or image-offload decision). Visible tool-schema changes force prompt consolidation only when the prepared route lacks `toolUpdate`. Routes supporting both tool and system-prompt updates can append a changed prompt and activate added tools without rewriting earlier messages. If another admission condition replaces a system node, the request starts a new series with current tool declarations. Resume and a provider or model swap alone continue the series; the prepared route still governs admission. Per-node empty replacements preserve intervening history without a surface delete operation.
|
||||
|
||||
@@ -157,7 +157,7 @@ For each step, the loop sends the session's derived messages and visible tool sc
|
||||
|
||||
#### Token effect
|
||||
|
||||
System text and schemas are paid again on every step, and on an `in-history` route every retained prompt version is paid until compaction shadows it or prompt reconciliation empties it. Per-agent scoping chooses the contributions, while the authoritative assembly waterfall can alter the final request and makes its listener responsible for protocol coherence.
|
||||
System text and schemas are paid again on every step, and on an `in-history` route every retained prompt version is paid until compaction shadows it or prompt reconciliation empties it. Per-agent scoping chooses the contributions, while the presentation waterfall can alter prompt text but cannot replace provider-owned tool declarations.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ const handle = await ctx.agents.create({
|
||||
|
||||
### 轮次与步骤流程
|
||||
|
||||
驱动器在其整个生命周期内拥有一个 agent,并在 `ctx.agents.withInitiator(agent, ...)` 内运行。`AgentLoop` 在服务生命周期内注册标准 `inbox` 投影,因此冷读取在没有 Agent 和所有 Agent 卸载后都可用。其包内部 `ReactLoopInbox` 使用该共享投影执行结构化命令与仅供 loop 使用的领取操作。在轮次边界,它先打开持久轮次,再原子领取待处理的 next-step 输入与一条排队提示词;在步骤之间则只领取 next-step 输入。驱动器组装提示词与工具,并运行 `agent/pre-step`;fallback 在监听器执行后投影运行时上下文。被拒绝的决定或空的首批输入不打开步骤。接纳后的首次尝试先记录 `step/start`,再运行 `agent/request` waterfall 与 `prepareCall()`;这两个异步阶段都看不到待提交的系统提示词与已接纳用户消息进入历史,在任一阶段取消都不会提交这两者。每次尝试时,循环随后依据已准备调用的能力,同步将渲染后的提示词与存活的 `system/message` 节点协调一致、刷新已注册运行时事实而不重新组装绑定的提示词或工具、仅在首次尝试追加已接纳的 `user/message` 批次并原位替换循环自有上下文候选、恢复被移除的运行时上下文、按需记录 header 与 context,再派生并冻结请求,通过该绑定的已准备调用发起流式请求。重试复用同一份已渲染组装结果,不重复组装、`agent/pre-step` 或用户消息准入。协调过程可见 pre-step 与重试中的压缩;序列中断时将提示词归并到头部,而非在已提交用户消息之后追加更新。请求由 `header.config`、`deriveMessages()` 与 `header.tools` 构成,不携带 `system` 字段。每次模型尝试会发出一个进程本地 `start`,仅在匹配的持久 assistant-frame 结算之后发出各个 `chunk`,并恰好发出一个终态 `end`;最终组装或消息追加失败时以 `aborted` 结算,`committed` 则出现在持久 `assistant/message` 之后。每次成功的模型调用都恰好追加一个 message 锚点,被取消的流则追加带 `interrupted: true` 的锚点并携带已交付前缀,使下一次请求包含用户看到的内容。在步骤内,独占调用形成屏障,并行安全调用使用有界滚动池;策略、持久结果与结果上下文保持模型顺序。
|
||||
驱动器在其整个生命周期内拥有一个 agent,并在 `ctx.agents.withInitiator(agent, ...)` 内运行。`AgentLoop` 在服务生命周期内注册标准 `inbox` 投影,因此冷读取在没有 Agent 和所有 Agent 卸载后都可用。其包内部 `ReactLoopInbox` 使用该共享投影执行结构化命令与仅供 loop 使用的领取操作。在轮次边界,它先打开持久轮次,再原子领取待处理的 next-step 输入与一条排队提示词;在步骤之间则只领取 next-step 输入。驱动器通过 `agent/request` 与 `prepareCall()` 绑定路由,准备变量和运行时上下文,然后运行 `agent/pre-step`;fallback 投影该上下文。被拒绝的决定或空的首批输入不打开步骤。路由准备期间取消不会提交系统提示词或用户消息。接纳后,`step/start` 先于异步工具激活与最终提示词组装。每次尝试时,循环随后依据已准备调用的能力,同步将渲染后的提示词与存活的 `system/message` 节点协调一致、刷新已注册运行时事实、仅在首次尝试追加已接纳的 `user/message` 批次并原位替换循环自有上下文候选、恢复被移除的运行时上下文、按需记录 header 与 context,再派生并冻结请求,通过该绑定的已准备调用发起流式请求。重试按其绑定路由重新解析工具条件,不重复 `agent/pre-step` 或用户消息接纳。路由与工具 schema 集合未变时复用已接纳提示词;任一变化时重新组装对应提示词。请求拥有的工具集合在分派结束后关闭,Agent 的绑定路由在步骤结束时清除。协调过程可见 pre-step 与重试中的压缩;序列中断时将提示词归并到头部,而非在已提交用户消息之后追加更新。请求由 `header.config`、`deriveMessages()` 与 `header.tools` 构成,不携带 `system` 字段。每次模型尝试会发出一个进程本地 `start`,仅在匹配的持久 assistant-frame 结算之后发出各个 `chunk`,并恰好发出一个终态 `end`;最终组装或消息追加失败时以 `aborted` 结算,`committed` 则出现在持久 `assistant/message` 之后。每次成功的模型调用都恰好追加一个 message 锚点,被取消的流则追加带 `interrupted: true` 的锚点并携带已交付前缀,使下一次请求包含用户看到的内容。在步骤内,独占调用形成屏障,并行安全调用使用有界滚动池;策略、持久结果与结果上下文保持模型顺序。
|
||||
|
||||
提示词准入依据实际的 `prepareCall()` 结果,而非先前的 `request/context`。没有系统节点时,即使提示词为空也追加(预留第 0 号节点,但不产生协议消息)。在不具备能力的路由上或新请求序列开始时,非空渲染文本归并到首个系统节点:每个非空的后续系统节点分别收到有日志记录的空内容替换,随后按需重写头节点。未生效的空尾节点无需替换,也不决定有效文本。即使最新有效文本未变,也执行归并。延续中的 `in-history` 序列在有效提示词不变时不产生事件,非空变更则追加。无论路由或序列状态如何,空渲染文本都会通过有日志记录的逐节点空内容替换清除每个非空的后续系统节点,再按需清空头节点。模型不会继续看到旧指令。空头节点且没有生效的后续系统节点表示没有提示词;重复清除与恢复会话都保持为空。重新提供的非空提示词遵循同一路由/序列规则:延续中的具备能力路由可以追加它,不具备能力的路由或新序列则重新填充头节点。以下情况开启序列:pre-step 决定声明 `startsRequestSeries`,或 `session.surface.contentGeneration` 自附接或上次请求以来发生变化(替换或图片省略决定)。仅当已准备路由未声明 `toolUpdate` 时,可见工具 schema 变化才强制归并提示词。同时支持工具和系统提示词更新的路由可以追加变更后的提示词并激活新增工具,无需改写此前消息。如果其他准入条件替换系统节点,请求会以当前工具声明开启新序列。恢复与单纯的提供方或模型切换都延续序列;准入仍由已准备的路由决定。逐节点的空内容替换保留其间历史,无需 surface 删除操作。
|
||||
|
||||
@@ -157,7 +157,7 @@ const handle = await ctx.agents.create({
|
||||
|
||||
#### Token 影响
|
||||
|
||||
系统文本与 schema 在每个步骤都会再次计入,在 `in-history` 路由上,每个保留的提示词版本都会持续计入,直到压缩将其遮蔽或提示词协调将其清空。逐 agent 作用域决定贡献,而权威组装 waterfall 可以改变最终请求,并使其监听器负责保持协议连贯。
|
||||
系统文本与 schema 在每个步骤都会再次计入,在 `in-history` 路由上,每个保留的提示词版本都会持续计入,直到压缩将其遮蔽或提示词协调将其清空。逐 agent 作用域决定贡献;展示 waterfall 可以修改提示词文本,但不能替换提供方拥有的工具声明。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import { createScope } from '@deepseek-ai/dsh-scope'
|
||||
import type { EpochHeader, RequestContext, Session, SessionId, SessionSeq, TurnEndReason, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { canonicalHeader, headerEquals, ToolCallRecovery } from '@deepseek-ai/dsh-session'
|
||||
import { joinContextSections, renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { PromptAssembly, PromptPreparation } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-session-projection'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { ReactLoopInbox } from './inbox.ts'
|
||||
@@ -38,6 +38,7 @@ import { RuntimeContextProjection } from './runtime-context.ts'
|
||||
import { AssistantStreamAttempt } from './assistant-stream.ts'
|
||||
import { SystemPromptProjection } from './runtime-context.ts'
|
||||
import { executeToolCalls } from './tool-calls.ts'
|
||||
import { ToolActivationError } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
type Phase =
|
||||
| { kind: 'idle'; lastTurn: number }
|
||||
@@ -51,13 +52,19 @@ type Phase =
|
||||
|
||||
type StepEndReason = Extract<TurnEndReason, { kind: 'completed' | 'max-tokens' }>
|
||||
|
||||
interface PreparedRequest {
|
||||
config: LlmCallConfig
|
||||
preparedCall?: PreparedLlmCall
|
||||
}
|
||||
|
||||
type PreparedStep =
|
||||
| { kind: 'reject' }
|
||||
| {
|
||||
kind: 'enter'
|
||||
messages: UserMessage[]
|
||||
startsRequestSeries?: true
|
||||
assembly: PromptAssembly
|
||||
preparation: PromptPreparation
|
||||
request: PreparedRequest
|
||||
}
|
||||
|
||||
/** Remove adapter-derived values before plugins propose the next request config. */
|
||||
@@ -97,6 +104,7 @@ function abortedCancelCause(signal: AbortSignal): AgentCancelCause | undefined {
|
||||
/** Drives one session through turn and step boundaries. */
|
||||
export class ReactLoopAgent implements Agent {
|
||||
readonly inbox: ReactLoopInbox
|
||||
private requestRoute: Readonly<Pick<LlmCallConfig, 'provider' | 'model'>> | undefined
|
||||
private phase: Phase
|
||||
private activityDone: Promise<void> = Promise.resolve()
|
||||
|
||||
@@ -137,6 +145,11 @@ export class ReactLoopAgent implements Agent {
|
||||
this.systemPrompt = new SystemPromptProjection(session)
|
||||
}
|
||||
|
||||
currentRoute(): Readonly<Pick<LlmCallConfig, 'provider' | 'model'>> {
|
||||
if (this.requestRoute === undefined) throw new Error('agent has no prepared model request')
|
||||
return this.requestRoute
|
||||
}
|
||||
|
||||
get status(): AgentStatus {
|
||||
return this.phase.kind === 'idle' || this.phase.kind === 'maintenance' ? 'idle' : 'running'
|
||||
}
|
||||
@@ -269,12 +282,14 @@ export class ReactLoopAgent implements Agent {
|
||||
if (this.phase.kind !== 'running') throw new Error(`agent "${this.id}": pre-step outside running phase`)
|
||||
const signal = this.phase.abort.signal
|
||||
const claimed = this.inbox.claim(target, position.turn)
|
||||
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
|
||||
const request = await this.prepareRequest(position.turn, position.step, signal)
|
||||
const preparation = await this.loopCtx.systemPrompt.prepare(assembleContextFor(this, signal))
|
||||
preparation.variables = { ...preparation.variables, ...this.currentRoute() }
|
||||
signal.throwIfAborted()
|
||||
const decision = await this.dispatch.waterfall(
|
||||
'agent/pre-step', { messages: claimed, ...position, signal },
|
||||
(): Promise<PreStepDecision> => {
|
||||
const sections = renderContextSections(assembly)
|
||||
const sections = renderContextSections(preparation)
|
||||
const context = this.runtimeContext.project(joinContextSections(sections), sections)
|
||||
return Promise.resolve<PreStepDecision>({
|
||||
kind: 'enter',
|
||||
@@ -284,7 +299,7 @@ export class ReactLoopAgent implements Agent {
|
||||
)
|
||||
signal.throwIfAborted()
|
||||
if (decision.kind === 'reject') return decision
|
||||
return { ...decision, assembly }
|
||||
return { ...decision, preparation, request }
|
||||
}
|
||||
|
||||
/** Whether the assembled tool schemas differ from the logged request header's. */
|
||||
@@ -313,6 +328,7 @@ export class ReactLoopAgent implements Agent {
|
||||
let target: InboxTarget = 'next-turn'
|
||||
try {
|
||||
while (true) {
|
||||
this.requestRoute = undefined
|
||||
signal.throwIfAborted()
|
||||
const step = phase.step + 1
|
||||
const decision = await this.preStep(target, { turn, step })
|
||||
@@ -356,6 +372,7 @@ export class ReactLoopAgent implements Agent {
|
||||
} finally {
|
||||
stopRecovery()
|
||||
this.session.append('step/end', { turn, step })
|
||||
this.requestRoute = undefined
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
if (turnEnds && this.inbox.nextStep.length === 0) {
|
||||
@@ -372,16 +389,15 @@ export class ReactLoopAgent implements Agent {
|
||||
turnEnds = { kind: 'aborted', reason: cause }
|
||||
throw error
|
||||
}
|
||||
// Every failure is structured: an `LlmError` keeps its facts, anything
|
||||
// else flattens to `errorChain` text under the `UNKNOWN` code.
|
||||
turnEnds = {
|
||||
kind: 'error',
|
||||
error: error instanceof LlmError
|
||||
? error.failure
|
||||
: { message: errorChain(error), code: 'UNKNOWN' },
|
||||
: { message: errorChain(error), code: error instanceof ToolActivationError ? error.code : 'UNKNOWN' },
|
||||
}
|
||||
this.throwError(error)
|
||||
} finally {
|
||||
this.requestRoute = undefined
|
||||
try {
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- every exit assigns a turn ending
|
||||
this.session.append('turn/end', { turn, reason: turnEnds! })
|
||||
@@ -403,11 +419,23 @@ export class ReactLoopAgent implements Agent {
|
||||
const { turn, step, abort: { signal } } = this.phase
|
||||
signal.throwIfAborted()
|
||||
|
||||
const { assembly } = decision
|
||||
const renderedPrompt = renderPrompt(assembly)
|
||||
let firstAttempt = true
|
||||
let acceptedAssembly: { provider: string; model: string; schemas: string; assembly: PromptAssembly } | undefined
|
||||
while (true) {
|
||||
const { config, preparedCall } = await this.prepareRequest(turn, step, signal)
|
||||
const { config, preparedCall } = firstAttempt ? decision.request : await this.prepareRequest(turn, step, signal)
|
||||
const toolSet = await this.loopCtx.tools.resolve({ agent: this, signal })
|
||||
try {
|
||||
const schemas = JSON.stringify(this.loopCtx.tools.schemas(this, toolSet))
|
||||
if (acceptedAssembly === undefined || acceptedAssembly.provider !== config.provider
|
||||
|| acceptedAssembly.model !== config.model || acceptedAssembly.schemas !== schemas) {
|
||||
const assembly = await this.loopCtx.systemPrompt.assemble(
|
||||
{ ...assembleContextFor(this, signal), toolSet },
|
||||
{ ...decision.preparation, variables: { ...decision.preparation.variables, ...this.currentRoute() } },
|
||||
)
|
||||
acceptedAssembly = { provider: config.provider, model: config.model, schemas, assembly }
|
||||
}
|
||||
const { assembly } = acceptedAssembly
|
||||
const renderedPrompt = renderPrompt(assembly)
|
||||
const currentContext = this.loopCtx.systemPrompt.refreshContext(assembly, assembleContextFor(this, signal))
|
||||
const sections = renderContextSections(currentContext)
|
||||
const context = this.runtimeContext.project(joinContextSections(sections), sections)
|
||||
@@ -551,12 +579,16 @@ export class ReactLoopAgent implements Agent {
|
||||
const { concluded } = await executeToolCalls(
|
||||
this.loopCtx, turn, step, toolCalls, signal,
|
||||
context => this.inbox.splice('next-step', this.inbox.nextStep.length, 0, [context]),
|
||||
toolSet,
|
||||
)
|
||||
return concluded ? { kind: 'completed' } : null
|
||||
} catch (error: unknown) {
|
||||
if (!live.ended) live.abandon()
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
toolSet.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +597,8 @@ export class ReactLoopAgent implements Agent {
|
||||
turn: number,
|
||||
step: number,
|
||||
signal: AbortSignal,
|
||||
): Promise<{ config: LlmCallConfig; preparedCall?: PreparedLlmCall }> {
|
||||
): Promise<PreparedRequest> {
|
||||
this.requestRoute = undefined
|
||||
const { session } = this
|
||||
|
||||
// A loop instance starts from its declared route, restoring only an explicit
|
||||
@@ -609,6 +642,7 @@ export class ReactLoopAgent implements Agent {
|
||||
config = proposedConfig
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
this.requestRoute = Object.freeze({ provider: config.provider, model: config.model })
|
||||
return { config, ...preparedCall === undefined ? {} : { preparedCall } }
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* @module dsh-agent-loop/tool-calls
|
||||
*/
|
||||
|
||||
import type { ResolvedToolSet } from '@deepseek-ai/dsh-tools'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { createToolResultMessage, type ToolCallBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { Session, SessionSeq, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
@@ -56,6 +57,7 @@ interface GroupOutcome {
|
||||
* @param toolCalls - assistant calls in model order.
|
||||
* @param signal - abort signal shared by the step.
|
||||
* @param acceptContext - accepts committed result context for the next step boundary.
|
||||
* @param toolSet - request-owned admission propagated into every Native dispatch.
|
||||
*/
|
||||
export async function executeToolCalls(
|
||||
ctx: Context,
|
||||
@@ -64,6 +66,7 @@ export async function executeToolCalls(
|
||||
toolCalls: ToolCallBlock[],
|
||||
signal: AbortSignal,
|
||||
acceptContext: (context: UserMessage) => void,
|
||||
toolSet?: ResolvedToolSet,
|
||||
): Promise<{ concluded: boolean }> {
|
||||
const agent = ctx.agents.requireInitiator()
|
||||
const { session } = agent
|
||||
@@ -77,6 +80,7 @@ export async function executeToolCalls(
|
||||
arguments: parseArguments(block.arguments),
|
||||
agent,
|
||||
signal,
|
||||
...toolSet === undefined ? {} : { toolSet },
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
@@ -1280,8 +1280,8 @@ describe('tool result call identity', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('disposal and cancellation during pre-step assembly', () => {
|
||||
it('disposal during system-prompt assembly closes a no-step turn', { timeout: 30000 }, async () => {
|
||||
describe('disposal and cancellation during prompt assembly', () => {
|
||||
it('disposal during system-prompt assembly closes an admitted step without model output', { timeout: 30000 }, async () => {
|
||||
// Start disposal, then release assembly. Do not await disposal first: it
|
||||
// waits for the blocked driver to exit.
|
||||
const adapter = new MockAdapter(['hang'])
|
||||
@@ -1327,13 +1327,13 @@ describe('disposal and cancellation during pre-step assembly', () => {
|
||||
const e = agent.session.snapshotEvents()
|
||||
expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type))
|
||||
.toEqual(['turn/start', 'turn/end'])
|
||||
expect(e.some(x => x.type === 'step/start')).toBe(false)
|
||||
expect(e.some(x => x.type === 'step/end')).toBe(false)
|
||||
expect(e.some(x => x.type === 'step/start')).toBe(true)
|
||||
expect(e.some(x => x.type === 'step/end')).toBe(true)
|
||||
expect(e.some(x => x.type === 'assistant/attempt')).toBe(false)
|
||||
expect(reasons).toEqual([{ kind: 'aborted', reason: { kind: 'disposed' } }])
|
||||
})
|
||||
|
||||
it('cancel during system-prompt assembly closes a no-step turn', { timeout: 30000 }, async () => {
|
||||
it('cancel during system-prompt assembly closes an admitted step without model output', { timeout: 30000 }, async () => {
|
||||
const adapter = new MockAdapter([textResponse('should not appear')])
|
||||
let releaseAssemble!: () => void
|
||||
const blocker = new Promise<void>(r => void (releaseAssemble = r))
|
||||
@@ -1374,8 +1374,8 @@ describe('disposal and cancellation during pre-step assembly', () => {
|
||||
const e = agent.session.snapshotEvents()
|
||||
expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type))
|
||||
.toEqual(['turn/start', 'turn/end'])
|
||||
expect(e.some(x => x.type === 'step/start')).toBe(false)
|
||||
expect(e.some(x => x.type === 'step/end')).toBe(false)
|
||||
expect(e.some(x => x.type === 'step/start')).toBe(true)
|
||||
expect(e.some(x => x.type === 'step/end')).toBe(true)
|
||||
expect(e.some(x => x.type === 'assistant/attempt')).toBe(false)
|
||||
expect(e.some(x => x.type === 'assistant/message')).toBe(false)
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
|
||||
@@ -22,6 +22,7 @@ function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
|
||||
const session = overrides.session ?? Session.create(id)
|
||||
const ctx = overrides.ctx ?? new Context()
|
||||
return {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -164,13 +164,12 @@ describe('loop-owned request freezing', () => {
|
||||
const { ctx } = await harness(adapter)
|
||||
const schemas: ToolSchema[][] = []
|
||||
const stops: string[][] = []
|
||||
ctx.on('system-prompt/assemble', async (_assembly, _context, next) => {
|
||||
const assembly = await next()
|
||||
ctx.systemPrompt.tools(() => {
|
||||
const tools: ToolSchema[] = [{ name: 'nested', description: 'test', parameters: {
|
||||
type: 'object', properties: { value: { type: 'array', items: { type: 'string', enum: ['a', 'b'] } } },
|
||||
} }]
|
||||
schemas.push(tools)
|
||||
return { ...assembly, tools }
|
||||
return { schemas: tools }
|
||||
})
|
||||
ctx.on('agent/request', async (_payload, next) => {
|
||||
const config = await next()
|
||||
@@ -193,8 +192,8 @@ describe('loop-owned request freezing', () => {
|
||||
expect(errors).toEqual([])
|
||||
expect(requests).toHaveLength(2)
|
||||
for (const [index, request] of requests.entries()) {
|
||||
expect(request.tools).toBe(schemas[index])
|
||||
expectFrozen(schemas[index])
|
||||
expect(request.tools).toEqual(schemas[index])
|
||||
expectFrozen(request.tools)
|
||||
expect(() => request.stop!.push('mutate')).toThrow(TypeError)
|
||||
if (!registered) expect(request.stop).toBe(stops[index])
|
||||
}
|
||||
|
||||
@@ -369,13 +369,13 @@ describe('prepared-route prompt admission', () => {
|
||||
expect(resumed.session.snapshotEvents().filter(event => event.type === 'request/header').at(-1)?.data.reason).toBe('resume')
|
||||
})
|
||||
|
||||
it.each(['request', 'prepare'] as const)('cancels during %s with a balanced empty step before input admission', async (stage) => {
|
||||
it.each(['request', 'prepare'] as const)('cancels during %s before step and input admission', async (stage) => {
|
||||
const h = await harness()
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const observe = async () => {
|
||||
const types = h.agent.session.snapshotEvents().map(event => event.type)
|
||||
expect(types).toContain('step/start')
|
||||
expect(types).not.toContain('step/start')
|
||||
expect(types).not.toContain('system/message')
|
||||
expect(types).not.toContain('user/message')
|
||||
expect(types).not.toContain('request/header')
|
||||
@@ -394,7 +394,7 @@ describe('prepared-route prompt admission', () => {
|
||||
await h.agent.whenIdle()
|
||||
expect(h.capable.requests).toHaveLength(0)
|
||||
const types = h.agent.session.snapshotEvents().map(event => event.type)
|
||||
expect(types.filter(type => type === 'step/start' || type === 'step/end')).toEqual(['step/start', 'step/end'])
|
||||
expect(types.filter(type => type === 'step/start' || type === 'step/end')).toEqual([])
|
||||
expect(types.filter(type => ['system/message', 'user/message', 'request/header'].includes(type))).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('tool-call scheduler: grouping and barriers', () => {
|
||||
expect(order).toEqual(['r-start-A1', 'r-end-A1', 'w-A2', 'r-start-A3', 'r-end-A3'])
|
||||
})
|
||||
|
||||
it('reclassifies pending calls after an exclusive barrier replaces their tool', async () => {
|
||||
it('rejects pending calls whose original registration was replaced', async () => {
|
||||
const adapter = new MockAdapter([
|
||||
multiCall([
|
||||
{ id: 'c1', name: 'replace', args: { id: '0' } },
|
||||
@@ -181,17 +181,13 @@ describe('tool-call scheduler: grouping and barriers', () => {
|
||||
const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
|
||||
await until(() => replacement.started.length === 1)
|
||||
await new Promise(r => setTimeout(r, 5))
|
||||
expect(replacement.started).toEqual(['1'])
|
||||
replacement.release('1')
|
||||
await until(() => replacement.started.length === 2)
|
||||
expect(replacement.started).toEqual(['1', '2'])
|
||||
replacement.release('2')
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(replacement.started).toEqual([])
|
||||
expect(events(agent).filter(event => event.type === 'tool/result').slice(1).map(event => event.data.error?.code))
|
||||
.toEqual(['UNKNOWN_TOOL', 'UNKNOWN_TOOL'])
|
||||
})
|
||||
|
||||
it('stops replenishing when a result observer makes the next call exclusive', async () => {
|
||||
it('drains started calls and rejects unstarted calls after replacement', async () => {
|
||||
const adapter = new MockAdapter([
|
||||
multiCall([
|
||||
{ id: 'c1', name: 'x', args: { id: '1' } },
|
||||
@@ -219,10 +215,9 @@ describe('tool-call scheduler: grouping and barriers', () => {
|
||||
await new Promise(r => setTimeout(r, 5))
|
||||
expect(replacement.started).toEqual([])
|
||||
initial.release('2')
|
||||
await until(() => replacement.started.length === 1)
|
||||
expect(replacement.started).toEqual(['3'])
|
||||
replacement.release('3')
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(replacement.started).toEqual([])
|
||||
expect(events(agent).filter(event => event.type === 'tool/result').at(-1)?.data.error?.code).toBe('UNKNOWN_TOOL')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('loop-level canonical tool order', () => {
|
||||
expect(Object.isFrozen(adapter.requests[0])).toBe(true)
|
||||
})
|
||||
|
||||
it('closes a no-step turn when toolOrder names an unregistered tool', async () => {
|
||||
it('closes the admitted step without a request when toolOrder names an unregistered tool', async () => {
|
||||
const adapter = new MockAdapter([textResponse('never sent')])
|
||||
const ctx = await harness(adapter, ['ghost', TOOL_ORDER_REST])
|
||||
registerNamed(ctx, 'alpha')
|
||||
@@ -106,7 +106,7 @@ describe('loop-level canonical tool order', () => {
|
||||
expect(foldRequestHeader(agent.session.snapshotEvents())).toBeUndefined()
|
||||
expect(agent.session.snapshotEvents().some(e => e.type === 'turn/start')).toBe(true)
|
||||
expect(agent.session.snapshotEvents().some(e => e.type === 'turn/end')).toBe(true)
|
||||
expect(agent.session.snapshotEvents().some(e => e.type === 'step/start')).toBe(false)
|
||||
expect(agent.session.snapshotEvents().some(e => e.type === 'step/end')).toBe(false)
|
||||
expect(agent.session.snapshotEvents().some(e => e.type === 'step/start')).toBe(true)
|
||||
expect(agent.session.snapshotEvents().some(e => e.type === 'step/end')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,7 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import { MockAdapter, textResponse } from './mock-adapter.ts'
|
||||
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
|
||||
|
||||
const contexts: Context[] = []
|
||||
afterEach(async () => {
|
||||
@@ -112,3 +112,127 @@ describe('tool update emission', () => {
|
||||
expect(agent.session.requestHeader()?.tools?.map(schema => schema.name)).toEqual(['search'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('request-time tool activation', () => {
|
||||
it('uses the bound route for asynchronous conditions and rejects a historical name after removal', async () => {
|
||||
const adapter = new MockAdapter([
|
||||
toolCallResponse('allowed', 'conditional', {}), textResponse('done'),
|
||||
toolCallResponse('removed', 'conditional', {}), textResponse('done'),
|
||||
])
|
||||
const { ctx, agent } = await harness(adapter)
|
||||
const routed: string[] = []
|
||||
let calls = 0
|
||||
let model = 'enabled'
|
||||
ctx.on('agent/request', async (_payload, next) => ({ ...await next(), model }))
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'conditional', description: 'Available on one route.', parameters: {},
|
||||
async execute(_args, exec) {
|
||||
expect(exec.agent?.currentRoute()).toEqual({ provider: 'mock', model: 'enabled' })
|
||||
calls++
|
||||
return [{ type: 'text', text: 'done' }]
|
||||
},
|
||||
}), { when: async ({ agent: subject }) => {
|
||||
const route = subject.currentRoute()
|
||||
expect(Object.isFrozen(route)).toBe(true)
|
||||
routed.push(route.model)
|
||||
await Promise.resolve()
|
||||
return route.model === 'enabled'
|
||||
} })
|
||||
expect(() => agent.currentRoute()).toThrow('no prepared model request')
|
||||
await send(agent, 'enabled')
|
||||
model = 'disabled'
|
||||
await send(agent, 'try the historical name')
|
||||
expect(calls).toBe(1)
|
||||
expect(routed).toEqual(['enabled', 'enabled', 'disabled', 'disabled'])
|
||||
expect(developerEvents(agent).at(-1)?.data.message.content)
|
||||
.toEqual([{ type: 'tool-removal', toolName: 'conditional' }])
|
||||
expect(agent.session.snapshotEvents().find(event => event.type === 'tool/result' && event.data.message.toolCallId === 'removed')?.data)
|
||||
.toMatchObject({ error: { code: 'UNKNOWN_TOOL' } })
|
||||
expect(() => agent.currentRoute()).toThrow('no prepared model request')
|
||||
})
|
||||
|
||||
it('cancels an asynchronous condition without committing the proposed request', async () => {
|
||||
const adapter = new MockAdapter([textResponse('unused')])
|
||||
const { ctx, agent } = await harness(adapter)
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const settle = Promise.withResolvers<boolean>()
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'conditional', description: 'A pending condition.', parameters: {},
|
||||
execute: async () => [{ type: 'text', text: 'unused' }],
|
||||
}), { when: () => { entered.resolve(undefined); return settle.promise } })
|
||||
const running = send(agent, 'cancel during activation')
|
||||
await entered.promise
|
||||
agent.cancel({ kind: 'user' })
|
||||
settle.resolve(true)
|
||||
await running
|
||||
expect(adapter.requests).toEqual([])
|
||||
expect(agent.session.snapshotEvents().filter(event => ['request/header', 'system/message', 'user/message'].includes(event.type))).toEqual([])
|
||||
expect(() => agent.currentRoute()).toThrow('no prepared model request')
|
||||
})
|
||||
|
||||
it.each(['condition', 'route'])('reassembles on a retry after the %s changes without admitting users again', async (change) => {
|
||||
const adapter = new MockAdapter([
|
||||
[{ type: 'finish', reason: { kind: 'error', failure: { code: 'SERVER', message: 'retry', status: 503 } } }],
|
||||
textResponse('done'),
|
||||
])
|
||||
const { ctx, agent } = await harness(adapter)
|
||||
let retried = false
|
||||
let admissions = 0
|
||||
ctx.on('agent/pre-step', (_input, next) => { admissions++; return next() })
|
||||
ctx.on('agent/request', async (_input, next) => ({
|
||||
...await next(), model: change === 'route' && retried ? 'second' : 'first',
|
||||
}))
|
||||
ctx.on('agent/request-error', async (_input, next) => {
|
||||
await next()
|
||||
retried = true
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'conditional', description: 'Retry-dependent availability.', parameters: {},
|
||||
execute: async () => [{ type: 'text', text: 'unused' }],
|
||||
}), { when: () => change === 'route' || retried })
|
||||
ctx.systemPrompt.section({
|
||||
name: 'retry:guidance', order: 0, complete: true,
|
||||
text: context => `${agent.currentRoute().model}:${ctx.tools.schemas(agent, context.toolSet).map(tool => tool.name).join(',')}`,
|
||||
})
|
||||
await send(agent, 'retry once')
|
||||
const prompts = adapter.requests.map(request => request.messages.find(message => message.role === 'system')?.content)
|
||||
expect(prompts[0]).toEqual([{ type: 'text', text: change === 'route' ? 'first:conditional' : 'first:' }])
|
||||
expect(prompts[1]).toEqual([{ type: 'text', text: change === 'route' ? 'second:conditional' : 'first:conditional' }])
|
||||
expect(admissions).toBe(1)
|
||||
expect(agent.session.snapshotEvents().filter(event => event.type === 'user/message' && event.data.source.kind === 'user')).toHaveLength(1)
|
||||
expect(() => agent.currentRoute()).toThrow('no prepared model request')
|
||||
})
|
||||
|
||||
it('reports condition failures without admitting a partial request', async () => {
|
||||
const adapter = new MockAdapter([textResponse('unused')])
|
||||
const { ctx, agent } = await harness(adapter)
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'conditional', description: 'An unavailable activation dependency.', parameters: {},
|
||||
execute: async () => [{ type: 'text', text: 'unused' }],
|
||||
}), { when: async () => { throw new Error('credential storage unavailable') } })
|
||||
await send(agent, 'inspect')
|
||||
expect(adapter.requests).toEqual([])
|
||||
expect(agent.session.snapshotEvents().filter(event => ['request/header', 'system/message', 'user/message'].includes(event.type))).toEqual([])
|
||||
expect(agent.session.snapshotEvents().find(event => event.type === 'turn/end')?.data)
|
||||
.toMatchObject({ reason: { kind: 'error', error: { code: 'TOOL_ACTIVATION_FAILED' } } })
|
||||
expect(() => agent.currentRoute()).toThrow('no prepared model request')
|
||||
})
|
||||
|
||||
it('shares the selected set with prompt guidance', async () => {
|
||||
const adapter = new MockAdapter([textResponse('done')])
|
||||
const { ctx, agent } = await harness(adapter)
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'conditional', description: 'Disabled.', parameters: {},
|
||||
execute: async () => [{ type: 'text', text: 'unused' }],
|
||||
}), { when: () => false })
|
||||
ctx.systemPrompt.section({
|
||||
name: 'conditional:guidance', order: 0,
|
||||
text: context => ctx.tools.get('conditional', context.scope, context.toolSet) === undefined ? '' : 'Conditional guidance',
|
||||
})
|
||||
await send(agent, 'inspect')
|
||||
expect(adapter.requests[0]?.tools ?? []).toEqual([])
|
||||
expect(JSON.stringify(adapter.requests[0]?.messages)).not.toContain('Conditional guidance')
|
||||
})
|
||||
})
|
||||
@@ -36,14 +36,14 @@
|
||||
en: 7c850051d2857bfa
|
||||
zh: 4cab817553114e15
|
||||
/deepseek-ai-dsh-agent/understand-the-implementation/step-admission:
|
||||
en: 348b9ecd0890c585
|
||||
zh: d0f73d28d0d2c6f1
|
||||
en: 5bba803d6c23370a
|
||||
zh: 0e5e3c037a111141
|
||||
/deepseek-ai-dsh-agent/understand-the-implementation/durable-inbox:
|
||||
en: f6802c7f94f623b3
|
||||
zh: cc3885db80765970
|
||||
/deepseek-ai-dsh-agent/understand-the-implementation/source-map:
|
||||
en: 7e09b94c79dc97ca
|
||||
zh: 3f9c5455c4dc563a
|
||||
en: 8efa6e74d4dbe28e
|
||||
zh: 6126170ac464703f
|
||||
/deepseek-ai-dsh-agent/understand-the-implementation/registry-and-lifecycle:
|
||||
en: df92e97cedd0d02e
|
||||
zh: f6040c145bffd933
|
||||
|
||||
@@ -82,7 +82,9 @@ The package is built on one separation: the public `Agent` surface and registry
|
||||
|
||||
### Step admission
|
||||
|
||||
`PreStepDecision` is either `{ kind: 'reject' }` or `{ kind: 'enter', messages, startsRequestSeries? }`. The enter branch contains the complete identified, frozen message batch. Acceptance does not commit it: after assembly and `step/start`, `agent/request` and `prepareCall()` resolve the route before the loop commits the system prompt and user batch. Cancellation during either async phase commits neither. `startsRequestSeries: true` declares a distinct model-message series; a wrapping listener preserves that declaration and the batch unless it intentionally replaces either one. Claiming removes offered messages from the inbox, while messages inserted after the claim remain pending for a later boundary.
|
||||
`PreStepDecision` is either `{ kind: 'reject' }` or `{ kind: 'enter', messages, startsRequestSeries? }`. The enter branch contains the complete identified, frozen message batch. Route preparation runs before `agent/pre-step`. Acceptance does not commit the batch: tool conditions and prompt assembly must also succeed before system and user messages are logged. Cancellation during route preparation opens no step and commits neither message kind. `startsRequestSeries: true` declares a distinct model-message series; a wrapping listener preserves that declaration and the batch unless it intentionally replaces either one. Claiming removes offered messages from the inbox, while messages inserted after the claim remain pending for a later boundary.
|
||||
|
||||
`agent.currentRoute()` returns the immutable provider/model bound by request preparation, through the corresponding tool batch. It throws before preparation and after the batch ends; initial options and previous Session headers are not fallbacks. Auxiliary model calls do not replace the main request route.
|
||||
|
||||
### Durable inbox
|
||||
|
||||
@@ -99,7 +101,7 @@ The package is built on one separation: the public `Agent` surface and registry
|
||||
| [`src/types.ts`](src/types.ts) | `AgentOptions`, cancellation causes, and inbox projection vocabulary |
|
||||
| [`src/dispatch.ts`](src/dispatch.ts) | `agentEvents` fused dispatcher and `assembleContextFor(agent)` |
|
||||
| [`src/consumed-work.ts`](src/consumed-work.ts) | `foldConsumedWork(events)`: what the log's consumed work became |
|
||||
| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`: coupling one selection to assembly and routing |
|
||||
| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`: capturing a selection for step routing |
|
||||
| [`src/archive-admission.ts`](src/archive-admission.ts) | The `turn` family of the Workspace registry's archive admission: a running turn and its user-cause cancel |
|
||||
|
||||
### Registry and lifecycle
|
||||
|
||||
@@ -82,7 +82,9 @@ await handle.agent.whenIdle()
|
||||
|
||||
### 步骤准入
|
||||
|
||||
`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages, startsRequestSeries? }`。enter 分支包含完整、带标识且冻结的消息批次。接纳不等于提交:组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析路由,循环随后才提交系统提示词与用户批次。在任一异步阶段取消都不会提交这两者。`startsRequestSeries: true` 声明一个独立的模型消息序列;包装下游 enter 的监听器会保留该声明与批次,除非有意替换其中一项。领取会从 inbox 移除候选消息,领取后插入的消息则等待后续边界。
|
||||
`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages, startsRequestSeries? }`。enter 分支包含完整、带标识且冻结的消息批次。路由准备在 `agent/pre-step` 之前执行。接纳不等于提交:工具条件与提示词组装也必须成功,随后才记录系统提示词与用户批次。在路由准备期间取消不会打开步骤,也不会提交这两种消息。`startsRequestSeries: true` 声明一个独立的模型消息序列;包装下游 enter 的监听器会保留该声明与批次,除非有意替换其中一项。领取会从 inbox 移除候选消息,领取后插入的消息则等待后续边界。
|
||||
|
||||
`agent.currentRoute()` 返回请求准备绑定的不可变提供方与模型,持续到对应工具批次结束。准备前和批次结束后调用会抛错,不回退到初始选项或先前 Session 请求头。辅助模型调用不会替换主请求路由。
|
||||
|
||||
### 持久 inbox
|
||||
|
||||
@@ -99,7 +101,7 @@ await handle.agent.whenIdle()
|
||||
| [`src/types.ts`](src/types.ts) | `AgentOptions`、取消原因与收件箱投影词汇 |
|
||||
| [`src/dispatch.ts`](src/dispatch.ts) | `agentEvents` 融合分发器与 `assembleContextFor(agent)` |
|
||||
| [`src/consumed-work.ts`](src/consumed-work.ts) | `foldConsumedWork(events)`:日志消费掉的工作最终怎样了 |
|
||||
| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`:把一个选择耦合到组装与路由 |
|
||||
| [`src/model-selection.ts`](src/model-selection.ts) | `installModelSelection`:为步骤路由捕获模型选择 |
|
||||
| [`src/archive-admission.ts`](src/archive-admission.ts) | Workspace 注册表归档准入中的 `turn` 族:运行中的回合及其用户原因取消 |
|
||||
|
||||
### 注册表与生命周期
|
||||
|
||||
@@ -30,9 +30,9 @@ export interface ModelSelection {
|
||||
|
||||
/** Mutable model selection plus the value captured for the current step. */
|
||||
export interface ModelSelectionRef {
|
||||
/** Model selected for the next step that enters prompt assembly. */
|
||||
/** Model selected for the next step entering route preparation. */
|
||||
current: ModelSelection | undefined
|
||||
/** Selection captured when the current step entered prompt assembly. */
|
||||
/** Selection captured by the first route preparation of the current step. */
|
||||
assembled: ModelSelection | undefined
|
||||
}
|
||||
|
||||
@@ -61,15 +61,13 @@ function modelSwitchNotice(previous: ModelSelection, selected: ModelSelection) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Couple one mutable selection to Agent-scoped prompt assembly and request routing.
|
||||
* Prompt assembly snapshots the selected model before delegating, then applies
|
||||
* its provider/model pair and effort to request config so a
|
||||
* concurrent switch takes effect on a later step instead of splitting the two
|
||||
* surfaces. An absent selected effort clears any inherited effort, restoring
|
||||
* the selected model's provider/default behavior.
|
||||
* Couple one mutable selection to Agent-scoped request routing. The first
|
||||
* request preparation in a step captures the selection before delegating;
|
||||
* retries retain it, and concurrent selections apply to later steps. An absent
|
||||
* selected effort clears inherited effort, restoring the model's provider default.
|
||||
*
|
||||
* A provider/model change appends a durable user-role notice to the next
|
||||
* admitted request. It compares the assembled selection with the latest
|
||||
* admitted request. It compares the bound request route with the latest
|
||||
* request header; effort-only changes and empty no-request decisions add no
|
||||
* notice. Failure before header persistence repeats the notice on the next
|
||||
* request.
|
||||
@@ -79,23 +77,14 @@ function modelSwitchNotice(previous: ModelSelection, selected: ModelSelection) {
|
||||
* @returns Disposer for all scoped waterfall listeners.
|
||||
*/
|
||||
export function installModelSelection(agentCtx: Context, selection: ModelSelectionRef): () => void {
|
||||
const disposeAssembly = agentCtx.on('system-prompt/assemble', async (_assembly, _context, next) => {
|
||||
const selected = selection.current
|
||||
const assembled = await next()
|
||||
selection.assembled = selected
|
||||
if (selected === undefined) return assembled
|
||||
return {
|
||||
...assembled,
|
||||
variables: {
|
||||
...assembled.variables,
|
||||
provider: selected.provider,
|
||||
model: selected.model,
|
||||
},
|
||||
}
|
||||
})
|
||||
let capturedStep: { turn: number; step: number } | undefined
|
||||
const disposeRequest = agentCtx.on(
|
||||
'agent/request',
|
||||
async (_payload, next): Promise<LlmCallConfig> => {
|
||||
async ({ turn, step }, next): Promise<LlmCallConfig> => {
|
||||
if (capturedStep?.turn !== turn || capturedStep.step !== step) {
|
||||
capturedStep = { turn, step }
|
||||
selection.assembled = selection.current === undefined ? undefined : { ...selection.current }
|
||||
}
|
||||
const resolved = await next()
|
||||
const selected = selection.assembled
|
||||
if (selected === undefined) return resolved
|
||||
@@ -117,15 +106,14 @@ export function installModelSelection(agentCtx: Context, selection: ModelSelecti
|
||||
if (decision.kind === 'reject' || signal.aborted) return decision
|
||||
// The loop skips an empty first step and an emptied offered continuation.
|
||||
if (decision.messages.length === 0 && (step === 1 || messages.length > 0)) return decision
|
||||
const selected = selection.assembled
|
||||
const selected = agent.currentRoute()
|
||||
const previous = agent.session.requestHeader()?.config
|
||||
if (selected === undefined || previous === undefined || sameRoute(selected, previous)) return decision
|
||||
if (previous === undefined || sameRoute(selected, previous)) return decision
|
||||
return { ...decision, messages: [...decision.messages, modelSwitchNotice(previous, selected)] }
|
||||
},
|
||||
{ prepend: true },
|
||||
)
|
||||
return () => {
|
||||
disposeAssembly()
|
||||
disposeRequest()
|
||||
disposeNotice()
|
||||
}
|
||||
|
||||
@@ -162,8 +162,14 @@ export type AssistantStreamFrame =
|
||||
|
||||
declare module './types.ts' {
|
||||
interface Agent {
|
||||
/** The provider route and model this agent's requests use. */
|
||||
/** Initial request options; currentRoute() reports the bound request route. */
|
||||
readonly options: AgentOptions
|
||||
/**
|
||||
* Read the immutable route bound to this request and its tool batch.
|
||||
* @returns the actual provider and model selected by prepareCall().
|
||||
* @throws when no main request is prepared; never falls back to prior requests.
|
||||
*/
|
||||
currentRoute(): Readonly<Pick<LlmCallConfig, 'provider' | 'model'>>
|
||||
/** The live session this agent drives; its log is the durable source of truth. */
|
||||
readonly session: Session
|
||||
/** Agent-owned access to durable pending work. */
|
||||
@@ -322,9 +328,9 @@ declare module '@deepseek-ai/cordis' {
|
||||
* 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. On step admission,
|
||||
* this runs after assembly and `step/start`, before the system prompt and
|
||||
* accepted user batch are committed. Cancellation here or during subsequent
|
||||
* `prepareCall()` resolution commits neither. The prepared call capability
|
||||
* this runs before context preparation and `agent/pre-step`. Cancellation
|
||||
* here or during subsequent `prepareCall()` resolution opens no step and
|
||||
* commits no system prompt or user batch. The prepared call capability
|
||||
* governs prompt admission. Model-visible content must use logged channels;
|
||||
* this waterfall cannot mutate messages.
|
||||
* @param payload.agent - the agent making the model call.
|
||||
|
||||
@@ -18,6 +18,7 @@ function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
|
||||
const session = overrides.session ?? Session.create(id)
|
||||
const ctx = overrides.ctx ?? new Context()
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -15,6 +15,7 @@ async function liveAgent(ctx: Context, rawId: string, initial: AgentStatus): Pro
|
||||
const cancels: CancelCall[] = []
|
||||
let status = initial
|
||||
const agent: LiveAgent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session: Session.create(id),
|
||||
|
||||
@@ -1,191 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
/** Model selection is captured before route preparation and remains stable within a step. */
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import {
|
||||
agentEvents,
|
||||
installModelSelection,
|
||||
type Agent,
|
||||
type ModelSelection,
|
||||
type ModelSelectionRef,
|
||||
} from '../src/index.ts'
|
||||
import {
|
||||
createUserMessage,
|
||||
ReasoningEffortId,
|
||||
type LlmCallConfig,
|
||||
type UserMessage,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { agentEvents, installModelSelection, type Agent, type ModelSelection, type ModelSelectionRef } from '../src/index.ts'
|
||||
import { createUserMessage, ReasoningEffortId, type LlmCallConfig } from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
const SIGNAL = new AbortController().signal
|
||||
const INPUT = createUserMessage({
|
||||
content: [{ type: 'text', text: 'continue' }],
|
||||
source: { kind: 'user' },
|
||||
const contexts: Context[] = []
|
||||
afterEach(async () => {
|
||||
for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
|
||||
})
|
||||
const signal = new AbortController().signal
|
||||
const input = createUserMessage({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } })
|
||||
|
||||
function createAgent(): Agent {
|
||||
return { session: Session.create(SessionId('model-selection')) } as Agent
|
||||
}
|
||||
|
||||
function expectedNotice(from: string, to: string) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `[model changed: assistant turns above this point were generated by ${from}; the session continues with ${to}]`,
|
||||
}],
|
||||
source: { kind: 'model-selection', form: 'notice', summary: `${from} → ${to}` },
|
||||
}
|
||||
}
|
||||
|
||||
async function switchHarness(current: ModelSelection, previous?: ModelSelection) {
|
||||
function harness(current?: ModelSelection, previous?: ModelSelection) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
contexts.push(ctx)
|
||||
const selection: ModelSelectionRef = { current, assembled: undefined }
|
||||
const dispose = installModelSelection(ctx, selection)
|
||||
const agent = createAgent()
|
||||
if (previous !== undefined) {
|
||||
agent.session.append('request/header', { header: { config: previous }, reason: 'initial' })
|
||||
let route: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 }
|
||||
const agent = { session: Session.create(SessionId('model-selection')), currentRoute: () => route } as Agent
|
||||
if (previous) agent.session.append('request/header', { header: { config: previous }, reason: 'initial' })
|
||||
const request = async (step: number, seed: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 }) => {
|
||||
route = await agentEvents(ctx, agent).waterfall('agent/request', { turn: 1, step, signal }, () => Promise.resolve(seed))
|
||||
return route
|
||||
}
|
||||
await ctx.systemPrompt.assemble()
|
||||
return { agent, ctx, dispose, selection }
|
||||
const preStep = (messages = [input], offered = [input], step = 1, requestSignal = signal) =>
|
||||
agentEvents(ctx, agent).waterfall('agent/pre-step', { turn: 1, step, messages: offered, signal: requestSignal },
|
||||
() => Promise.resolve({ kind: 'enter' as const, messages }))
|
||||
return { ctx, agent, selection, dispose, request, preStep }
|
||||
}
|
||||
|
||||
async function preStep(
|
||||
ctx: Context,
|
||||
agent: Agent,
|
||||
{
|
||||
messages = [INPUT],
|
||||
offered = [INPUT],
|
||||
step = 1,
|
||||
signal = SIGNAL,
|
||||
}: {
|
||||
messages?: UserMessage[]
|
||||
offered?: UserMessage[]
|
||||
step?: number
|
||||
signal?: AbortSignal
|
||||
} = {},
|
||||
) {
|
||||
return agentEvents(ctx, agent).waterfall(
|
||||
'agent/pre-step',
|
||||
{ turn: 1, step, messages: offered, signal },
|
||||
() => Promise.resolve({ kind: 'enter' as const, messages }),
|
||||
)
|
||||
function notice(from: string, to: string) {
|
||||
return { content: [{ type: 'text', text: `[model changed: assistant turns above this point were generated by ${from}; the session continues with ${to}]` }] }
|
||||
}
|
||||
|
||||
describe('installModelSelection()', () => {
|
||||
it('snapshots prompt variables and request routing together, then disposes its listeners', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
const selection: ModelSelectionRef = { current: undefined, assembled: undefined }
|
||||
const dispose = installModelSelection(ctx, selection)
|
||||
const agent = createAgent()
|
||||
it('captures selection per step, preserves retries, and disposes routing', async () => {
|
||||
const h = harness()
|
||||
const seed: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 }
|
||||
const signal = new AbortController().signal
|
||||
|
||||
expect((await ctx.systemPrompt.assemble()).variables).toEqual({})
|
||||
await expect(agentEvents(ctx, agent).waterfall(
|
||||
'agent/request', { turn: 1, step: 0, signal }, () => Promise.resolve(seed),
|
||||
)).resolves.toBe(seed)
|
||||
|
||||
selection.current = {
|
||||
provider: 'alpha',
|
||||
model: 'a1',
|
||||
reasoningEffort: ReasoningEffortId('high'),
|
||||
}
|
||||
expect((await ctx.systemPrompt.assemble()).variables).toMatchObject({ provider: 'alpha', model: 'a1' })
|
||||
selection.current = { provider: 'beta', model: 'b1' }
|
||||
await expect(agentEvents(ctx, agent).waterfall(
|
||||
'agent/request', { turn: 1, step: 0, signal }, () => Promise.resolve(seed),
|
||||
)).resolves.toEqual({
|
||||
provider: 'alpha',
|
||||
model: 'a1',
|
||||
reasoningEffort: ReasoningEffortId('high'),
|
||||
temperature: 0.2,
|
||||
expect(await h.request(0, seed)).toBe(seed)
|
||||
h.selection.current = { provider: 'alpha', model: 'a1', reasoningEffort: ReasoningEffortId('high') }
|
||||
expect(await h.request(1)).toEqual({ ...seed, ...h.selection.current })
|
||||
h.selection.current = { provider: 'beta', model: 'b1' }
|
||||
expect(await h.request(1)).toMatchObject({ provider: 'alpha', model: 'a1' })
|
||||
expect(await h.request(2, { ...seed, reasoningEffort: ReasoningEffortId('max') })).toEqual({ ...seed, ...h.selection.current })
|
||||
h.dispose()
|
||||
expect(await h.request(3, seed)).toBe(seed)
|
||||
})
|
||||
|
||||
expect((await ctx.systemPrompt.assemble()).variables).toMatchObject({ provider: 'beta', model: 'b1' })
|
||||
const inherited: LlmCallConfig = {
|
||||
provider: 'alpha',
|
||||
model: 'a1',
|
||||
reasoningEffort: ReasoningEffortId('max'),
|
||||
temperature: 0.2,
|
||||
}
|
||||
await expect(agentEvents(ctx, agent).waterfall(
|
||||
'agent/request', { turn: 1, step: 1, signal }, () => Promise.resolve(inherited),
|
||||
)).resolves.toEqual({ provider: 'beta', model: 'b1', temperature: 0.2 })
|
||||
|
||||
dispose()
|
||||
expect((await ctx.systemPrompt.assemble()).variables).toEqual({})
|
||||
await expect(agentEvents(ctx, agent).waterfall(
|
||||
'agent/request', { turn: 2, step: 0, signal }, () => Promise.resolve(seed),
|
||||
)).resolves.toBe(seed)
|
||||
await ctx.fiber.dispose()
|
||||
it('announces the bound same-provider and cross-provider routes', async () => {
|
||||
const h = harness({ provider: 'alpha', model: 'a1' }, { provider: 'alpha', model: 'a0' })
|
||||
await h.request(1)
|
||||
await expect(h.preStep()).resolves.toMatchObject({ messages: [input, notice('a0', 'a1')] })
|
||||
h.selection.current = { provider: 'beta', model: 'b1' }
|
||||
await h.request(2)
|
||||
h.selection.current = { provider: 'alpha', model: 'a2' }
|
||||
await expect(h.preStep()).resolves.toMatchObject({ messages: [input, notice('alpha/a0', 'beta/b1')] })
|
||||
await h.request(3)
|
||||
await expect(h.preStep()).resolves.toMatchObject({ messages: [input, notice('a0', 'a2')] })
|
||||
})
|
||||
|
||||
it('announces same-provider and cross-provider route changes from the assembled selection', async () => {
|
||||
const { agent, ctx, dispose, selection } = await switchHarness(
|
||||
{ provider: 'alpha', model: 'a1' },
|
||||
{ provider: 'alpha', model: 'a0' },
|
||||
)
|
||||
await expect(preStep(ctx, agent)).resolves.toMatchObject({
|
||||
messages: [INPUT, expectedNotice('a0', 'a1')],
|
||||
it('preserves initial, unchanged, rejected, cancelled, and disposed decisions', async () => {
|
||||
const h = harness({ provider: 'alpha', model: 'a0' })
|
||||
await h.request(1)
|
||||
await expect(h.preStep()).resolves.toEqual({ kind: 'enter', messages: [input] })
|
||||
h.agent.session.append('request/header', { header: { config: h.agent.currentRoute() }, reason: 'initial' })
|
||||
h.selection.current = { provider: 'alpha', model: 'a0', reasoningEffort: ReasoningEffortId('high') }
|
||||
await h.request(2)
|
||||
await expect(h.preStep()).resolves.toEqual({ kind: 'enter', messages: [input] })
|
||||
h.selection.current = { provider: 'alpha', model: 'a1' }
|
||||
await h.request(3)
|
||||
await expect(agentEvents(h.ctx, h.agent).waterfall('agent/pre-step', { turn: 1, step: 3, messages: [], signal },
|
||||
() => Promise.resolve({ kind: 'reject' as const }))).resolves.toEqual({ kind: 'reject' })
|
||||
const aborted = AbortSignal.abort()
|
||||
await expect(h.preStep([input], [input], 3, aborted)).resolves.toEqual({ kind: 'enter', messages: [input] })
|
||||
h.dispose()
|
||||
await expect(h.preStep()).resolves.toEqual({ kind: 'enter', messages: [input] })
|
||||
})
|
||||
|
||||
selection.current = { provider: 'beta', model: 'b1' }
|
||||
await ctx.systemPrompt.assemble()
|
||||
selection.current = { provider: 'alpha', model: 'a2' }
|
||||
await expect(preStep(ctx, agent)).resolves.toMatchObject({
|
||||
messages: [INPUT, expectedNotice('alpha/a0', 'beta/b1')],
|
||||
})
|
||||
await ctx.systemPrompt.assemble()
|
||||
await expect(preStep(ctx, agent)).resolves.toMatchObject({
|
||||
messages: [INPUT, expectedNotice('a0', 'a2')],
|
||||
})
|
||||
|
||||
dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('does not announce initial, same-route, effort-only, rejected, aborted, or disposed steps', async () => {
|
||||
const { agent, ctx, dispose, selection } = await switchHarness({ provider: 'alpha', model: 'a0' })
|
||||
await expect(preStep(ctx, agent)).resolves.toMatchObject({ kind: 'enter', messages: [INPUT] })
|
||||
agent.session.append('request/header', {
|
||||
header: { config: { provider: 'alpha', model: 'a0' } }, reason: 'initial',
|
||||
})
|
||||
selection.current = {
|
||||
provider: 'alpha',
|
||||
model: 'a0',
|
||||
reasoningEffort: ReasoningEffortId('high'),
|
||||
}
|
||||
await ctx.systemPrompt.assemble()
|
||||
await expect(preStep(ctx, agent)).resolves.toMatchObject({ kind: 'enter', messages: [INPUT] })
|
||||
|
||||
selection.current = { provider: 'alpha', model: 'a1' }
|
||||
await ctx.systemPrompt.assemble()
|
||||
const rejected = await agentEvents(ctx, agent).waterfall(
|
||||
'agent/pre-step',
|
||||
{ turn: 1, step: 1, messages: [], signal: SIGNAL },
|
||||
() => Promise.resolve({ kind: 'reject' as const }),
|
||||
)
|
||||
expect(rejected).toEqual({ kind: 'reject' })
|
||||
const aborted = new AbortController()
|
||||
aborted.abort()
|
||||
await expect(preStep(ctx, agent, { signal: aborted.signal })).resolves.toMatchObject({ messages: [INPUT] })
|
||||
|
||||
dispose()
|
||||
await expect(preStep(ctx, agent)).resolves.toMatchObject({ kind: 'enter', messages: [INPUT] })
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('preserves empty no-call decisions and announces an empty tool continuation', async () => {
|
||||
const { agent, ctx } = await switchHarness(
|
||||
{ provider: 'alpha', model: 'a1' },
|
||||
{ provider: 'alpha', model: 'a0' },
|
||||
)
|
||||
await expect(preStep(ctx, agent, { messages: [] })).resolves.toEqual({ kind: 'enter', messages: [] })
|
||||
await expect(preStep(ctx, agent, { messages: [], step: 2 })).resolves.toEqual({ kind: 'enter', messages: [] })
|
||||
await expect(preStep(ctx, agent, { messages: [], offered: [], step: 2 })).resolves.toMatchObject({
|
||||
messages: [{ source: { summary: 'a0 → a1' } }],
|
||||
})
|
||||
await ctx.fiber.dispose()
|
||||
it('preserves empty claims and announces an empty tool continuation', async () => {
|
||||
const h = harness({ provider: 'alpha', model: 'a1' }, { provider: 'alpha', model: 'a0' })
|
||||
await h.request(1)
|
||||
await expect(h.preStep([])).resolves.toEqual({ kind: 'enter', messages: [] })
|
||||
await expect(h.preStep([], [input], 2)).resolves.toEqual({ kind: 'enter', messages: [] })
|
||||
await expect(h.preStep([], [], 2)).resolves.toMatchObject({ messages: [notice('a0', 'a1')] })
|
||||
})
|
||||
})
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 87e28893e6cf58d7
|
||||
zh: 06f476b38c4d2038
|
||||
/deepseek-ai-dsh-system-prompt/use-this-package/contribute-a-prompt-section:
|
||||
en: 165fecb5329b00b6
|
||||
zh: a7c595d1787916c9
|
||||
en: db364cc533949015
|
||||
zh: a6e8a12d9a661b60
|
||||
/deepseek-ai-dsh-system-prompt/use-this-package/contribute-a-prompt-variable:
|
||||
en: b3e4c9b4ced59488
|
||||
zh: d80c791e72bd5821
|
||||
@@ -42,8 +42,8 @@
|
||||
en: d9575bcb07a1145b
|
||||
zh: 0f1a388780d055b1
|
||||
/deepseek-ai-dsh-system-prompt/understand-the-implementation/assembly-and-rendering:
|
||||
en: 54977e6c431f851e
|
||||
zh: ef60007054b97ff0
|
||||
en: d51940213af9b87f
|
||||
zh: f3a2540010624a79
|
||||
/deepseek-ai-dsh-system-prompt/understand-the-implementation/scoping:
|
||||
en: 1100ab1dfe961631
|
||||
zh: c510275c893951b4
|
||||
@@ -57,8 +57,8 @@
|
||||
en: cf78b9dd5596495f
|
||||
zh: bd44560f8c2435e8
|
||||
/deepseek-ai-dsh-system-prompt/model-experience/system-prompt/what-the-model-sees:
|
||||
en: 0aaabee3a9883eea
|
||||
zh: 13e6ead5b455fa30
|
||||
en: 622e83de7772dc1f
|
||||
zh: 7b3c54f08a317e93
|
||||
/deepseek-ai-dsh-system-prompt/model-experience/system-prompt/what-the-model-sees/harness-identity:
|
||||
en: 40effd71cb503319
|
||||
zh: 9969356237a4f398
|
||||
@@ -72,8 +72,8 @@
|
||||
en: 3039bc5e7a796b21
|
||||
zh: 6b7a9eaf69c9ea53
|
||||
/deepseek-ai-dsh-system-prompt/model-experience/tool-schemas/what-the-model-sees:
|
||||
en: a983ef220593cf73
|
||||
zh: 28770612ffc1be60
|
||||
en: b3873ff844d91e34
|
||||
zh: 5cccd2afda37af9a
|
||||
/deepseek-ai-dsh-system-prompt/model-experience/tool-schemas/token-effect:
|
||||
en: f3a7d374c55e1720
|
||||
zh: 0ee48af678cb875f
|
||||
|
||||
@@ -67,6 +67,8 @@ Set `interpolate: false` on a section or dynamic context to preserve its text li
|
||||
|
||||
`refreshContext(assembly, context)` refreshes registered runtime facts after pre-step and route preparation. It preserves accepted sections, tools, variables, and contexts contributed only by the assembly waterfall, applies current optional-context suppression, and restores missing required entries. Put changing facts in context providers; registered context text is read again before admission.
|
||||
|
||||
`prepare(context)` resolves variables and runtime facts through `system-prompt/prepare` before step admission. After the loop binds the route and commits step selections, `assemble(context, preparation)` resolves sections and tool declarations. Presentation listeners receive sections, contexts, and variables; tools remain provider-owned. Tool-aware section callbacks use `context.toolSet` to inspect the same request selection as execution.
|
||||
|
||||
### Contribute a prompt variable
|
||||
|
||||
Variables are referenced from section or context text as `{{name}}` and resolved at each assembly; scoped variables shadow a same-named global for that agent. The loop supplies `provider` and `model`; plugins can register the facts they own. The working-directory service supplies directory text as literal user context.
|
||||
@@ -107,7 +109,7 @@ The package is a registry plus a cooperative assembly pipeline. One `assemble()`
|
||||
|
||||
### Assembly and rendering
|
||||
|
||||
Assembly resolves and renders in two stages: `assemble()` returns sections with resolved-but-uninterpolated text, the ordered tool schemas, and every registered variable resolved against the context, while `renderPrompt()` interpolates `{{variable}}` references unless a section sets `interpolate: false`, drops empty sections, and joins with blank lines — strictly, an unknown reference, a registered-but-valueless reference, or a malformed complete group throws, because a malformed prompt is worse than a loud failure. `toolOrder` canonicalizes the collected tools before the waterfall (registration order is a plugin-load artifact); a waterfall listener that mutates the list owns the determinism of what it emits.
|
||||
Assembly resolves and renders in two stages: `assemble()` returns sections with resolved-but-uninterpolated text, the ordered tool schemas, and every registered variable resolved against the context, while `renderPrompt()` interpolates `{{variable}}` references unless a section sets `interpolate: false`, drops empty sections, and joins with blank lines — strictly, an unknown reference, a registered-but-valueless reference, or a malformed complete group throws, because a malformed prompt is worse than a loud failure. `toolOrder` canonicalizes the collected tools before the waterfall (registration order is a plugin-load artifact); the presentation waterfall receives no tool array and cannot replace provider-owned declarations.
|
||||
|
||||
### Scoping
|
||||
|
||||
@@ -137,7 +139,7 @@ The package-level contract is enough for most consumers; read these when you nee
|
||||
|
||||
#### What the model sees
|
||||
|
||||
First-party sections render the harness identity, deployment persona prefix (including the model-name introduction), reusable instructions (including the generated tools SDK and structured-output guidance), then the environment-bearing suffix: harness source (`10000`), Web surface (`10100`), and deployment persona suffix (`10200`). External section orders and assembly listeners remain authoritative. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete — that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain. The rendered prompt reaches the model as a system-role message of derived history — surface node 0, or the latest system node after an in-history update — neither the loop request nor `request/header` carries a separate `system` field. If the complete rendering is empty, the loop clears every active system node through logged empty replacements, so no older prompt remains in model history. Ordered dynamic contexts are separate from sections and become sourced user-role snapshots only when present; `includeRuntimeContext: false` or a scoped suppressor removes optional contributions while required operational context remains.
|
||||
First-party sections render the harness identity, deployment persona prefix (including the model-name introduction), reusable instructions (including the generated tools SDK and structured-output guidance), then the environment-bearing suffix: harness source (`10000`), Web surface (`10100`), and deployment persona suffix (`10200`). External section orders and assembly listeners remain authoritative. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt unless one effective section declares itself complete — that exact section then becomes the whole system prompt while the waterfall's contexts and variables remain. Tool declarations stay provider-owned. The rendered prompt reaches the model as a system-role message of derived history — surface node 0, or the latest system node after an in-history update — neither the loop request nor `request/header` carries a separate `system` field. If the complete rendering is empty, the loop clears every active system node through logged empty replacements, so no older prompt remains in model history. Ordered dynamic contexts are separate from sections and become sourced user-role snapshots only when present; `includeRuntimeContext: false` or a scoped suppressor removes optional contributions while required operational context remains.
|
||||
|
||||
##### Harness identity
|
||||
|
||||
@@ -157,7 +159,7 @@ Prefix-stable while identity, persona, variables, section text, and order render
|
||||
|
||||
#### What the model sees
|
||||
|
||||
For shipped tools, the model receives the per-agent-visible subset of the [generated tool schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tools), ordered by configuration or lexicographically after restrictions and assembly interception. Extensions can contribute additional definitions through the same registry. Sections and schema providers are separate assembly inputs. A restriction does not remove a section registration: tool-guidance plugins use `text({ scope })` and `ctx.tools.get(name, scope)` to return empty text or select applicable fragments. Arbitrary static sections are not automatically rewritten.
|
||||
For shipped tools, the model receives the per-agent-visible subset of the [generated tool schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tools), ordered by configuration or lexicographically after restrictions and request-time activation. Extensions can contribute additional definitions through the same registry. Sections and schema providers are separate assembly inputs. A restriction does not remove a section registration: tool-guidance plugins use `text({ scope, toolSet })` and `ctx.tools.get(name, scope, toolSet)` to return empty text or select applicable fragments. Arbitrary static sections are not automatically rewritten.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ ctx.systemPrompt.section({
|
||||
|
||||
`refreshContext(assembly, context)` 在 pre-step 与路由准备之后刷新已注册的运行时事实。它保留已接纳的段、工具、变量及仅由组装 waterfall 添加的上下文,应用当前的可选上下文抑制,并恢复缺失的必需条目。将变化的事实放在上下文 provider 中;已注册上下文的文本会在准入前再次读取。
|
||||
|
||||
`prepare(context)` 在步骤接纳前通过 `system-prompt/prepare` 解析变量与运行时事实。循环绑定路由并提交步骤选择后,`assemble(context, preparation)` 解析段落与工具声明。展示监听器接收段落、上下文与变量;工具声明仍由提供方持有。依赖工具的段落回调通过 `context.toolSet` 读取与执行相同的请求集合。
|
||||
|
||||
### 贡献提示词变量
|
||||
|
||||
变量从段或上下文文本中以 `{{name}}` 引用,每次组装时解析;带作用域的变量会为该 agent 遮蔽同名全局变量。循环提供 `provider` 和 `model`;插件可注册自己拥有的事实。工作目录服务通过不执行插值的用户上下文提供目录文本。
|
||||
@@ -107,7 +109,7 @@ ctx.systemPrompt.variable('response_language', () => 'English')
|
||||
|
||||
### 组装与渲染
|
||||
|
||||
组装分两阶段完成求值与渲染:`assemble()` 返回文本已求值但尚未插值的段、有序工具 schema,以及每个已注册变量按当前上下文求得的值;`renderPrompt()` 对未设置 `interpolate: false` 的段插值 `{{variable}}` 引用、删除空段并用空行连接——严格规则:未知引用、已注册但无值的引用或格式错误的完整组都会抛出,因为格式错误的提示词比明确失败更糟。`toolOrder` 在 waterfall 分发前规范化收集到的工具(注册顺序只是插件加载产物);修改列表的 waterfall 监听器对其输出的确定性负责。
|
||||
组装分两阶段完成求值与渲染:`assemble()` 返回文本已求值但尚未插值的段、有序工具 schema,以及每个已注册变量按当前上下文求得的值;`renderPrompt()` 对未设置 `interpolate: false` 的段插值 `{{variable}}` 引用、删除空段并用空行连接——严格规则:未知引用、已注册但无值的引用或格式错误的完整组都会抛出,因为格式错误的提示词比明确失败更糟。`toolOrder` 在 waterfall 分发前规范化收集到的工具(注册顺序只是插件加载产物);展示 waterfall 不接收工具数组,也不能替换提供方持有的声明。
|
||||
|
||||
### 作用域
|
||||
|
||||
@@ -137,7 +139,7 @@ ctx.systemPrompt.variable('response_language', () => 'English')
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
第一方段落依次渲染 harness 身份、部署 persona 前缀(含模型名称介绍)、可复用指令(包括生成的工具 SDK 和结构化输出指导),最后是携带环境信息的后缀:harness 源码(`10000`)、Web 表层(`10100`)和部署 persona 后缀(`10200`)。外部段落的顺序与组装监听器仍决定其最终结果。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段与变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema,除非一个有效段声明自身为 complete——此时该确切段会成为完整的系统提示词,而 waterfall 得到的上下文、工具与变量保持不变。渲染后的提示词作为派生历史中的 system 角色消息——surface 第 0 号节点,或历史内更新之后最新的系统节点——到达模型;循环请求与 `request/header` 均不含单独的 `system` 字段。完整渲染结果为空时,循环通过有日志记录的空内容替换清除所有生效的系统节点,模型历史不再保留任何旧提示词。有序动态上下文与段分离,只在存在时才会成为带来源的 user 角色快照;`includeRuntimeContext: false` 或带作用域的抑制器会移除可选贡献;必需的操作上下文仍然保留。
|
||||
第一方段落依次渲染 harness 身份、部署 persona 前缀(含模型名称介绍)、可复用指令(包括生成的工具 SDK 和结构化输出指导),最后是携带环境信息的后缀:harness 源码(`10000`)、Web 表层(`10100`)和部署 persona 后缀(`10200`)。外部段落的顺序与组装监听器仍决定其最终结果。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段与变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词,除非一个有效段声明自身为 complete——此时该确切段会成为完整的系统提示词,而 waterfall 得到的上下文与变量保持不变。工具声明仍由提供方持有。渲染后的提示词作为派生历史中的 system 角色消息——surface 第 0 号节点,或历史内更新之后最新的系统节点——到达模型;循环请求与 `request/header` 均不含单独的 `system` 字段。完整渲染结果为空时,循环通过有日志记录的空内容替换清除所有生效的系统节点,模型历史不再保留任何旧提示词。有序动态上下文与段分离,只在存在时才会成为带来源的 user 角色快照;`includeRuntimeContext: false` 或带作用域的抑制器会移除可选贡献;必需的操作上下文仍然保留。
|
||||
|
||||
##### harness 身份
|
||||
|
||||
@@ -157,7 +159,7 @@ You are an AI agent powered by DeepSeek Harness.
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
对于已交付工具,模型会收到[生成工具 schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tools)中对每个 agent 可见的子集;限制与组装拦截完成后,按配置或字典序排列。扩展可以通过同一注册表贡献其他定义。段与 schema 提供方是独立的组装输入。限制不会移除段落注册:工具指导插件通过 `text({ scope })` 与 `ctx.tools.get(name, scope)` 返回空文本或选择适用片段。任意静态段落不会被自动改写。
|
||||
对于已交付工具,模型会收到[生成工具 schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tools)中对每个 agent 可见的子集;限制与请求时激活完成后,按配置或字典序排列。扩展可以通过同一注册表贡献其他定义。段与 schema 提供方是独立的组装输入。限制不会移除段落注册:工具指导插件通过 `text({ scope, toolSet })` 与 `ctx.tools.get(name, scope, toolSet)` 返回空文本或选择适用片段。任意静态段落不会被自动改写。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ declare module '@deepseek-ai/cordis' {
|
||||
|
||||
interface Events {
|
||||
/**
|
||||
* Expert waterfall over the assembled sections, contexts, tools, and variables.
|
||||
* Presentation waterfall over assembled sections, contexts, and variables.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
|
||||
* receive only that scope's assemblies. The returned value is authoritative.
|
||||
* receive only that scope's assemblies. The returned text is authoritative; tool declarations remain provider-owned.
|
||||
* A supplied signal controls only this explicit assembly request and must not
|
||||
* be retained to control later turns. A registered complete section is
|
||||
* restored after this waterfall, so listeners cannot add to or replace
|
||||
@@ -28,7 +28,14 @@ declare module '@deepseek-ai/cordis' {
|
||||
* @param context - the caller's per-assembly context.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
|
||||
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptPresentation, context: AssembleContext, next: () => Promise<PromptPresentation>): Promise<PromptPresentation>
|
||||
/**
|
||||
* Prepare variables and runtime context before step admission.
|
||||
* @param preparation - registered variables and context contributions.
|
||||
* @param context - calling scope and cancellation.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'system-prompt/prepare'(this: Scoped<SystemPrompt>, preparation: PromptPreparation, context: AssembleContext, next: () => Promise<PromptPreparation>): Promise<PromptPreparation>
|
||||
/**
|
||||
* Emitted when any prompt provider changes. This registry notification is
|
||||
* unfiltered because a global change affects every scope.
|
||||
@@ -117,17 +124,22 @@ export interface ToolProviderResult {
|
||||
readonly knownNames?: readonly string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge-extensible assembled model input. Sections and contexts remain
|
||||
* uninterpolated until rendered; tools are already in canonical order.
|
||||
*/
|
||||
export interface PromptAssembly {
|
||||
sections: AssembledSection[]
|
||||
/** Prepared runtime facts and variables; context text remains uninterpolated. */
|
||||
export interface PromptPreparation {
|
||||
contexts: AssembledContext[]
|
||||
tools: ToolSchema[]
|
||||
variables: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
/** Prompt text contributions; tool declarations are owned by their providers. */
|
||||
export interface PromptPresentation extends PromptPreparation {
|
||||
sections: AssembledSection[]
|
||||
}
|
||||
|
||||
/** Completed model inputs, including provider-owned tool declarations. */
|
||||
export interface PromptAssembly extends PromptPresentation {
|
||||
tools: ToolSchema[]
|
||||
}
|
||||
|
||||
const SECTION_ORDERS = {
|
||||
HARNESS_IDENTITY: -1000,
|
||||
DEPLOYMENT_PERSONA_PREFIX: 0,
|
||||
@@ -323,7 +335,7 @@ export function joinContextSections(sections: readonly ContextSnapshotSection[])
|
||||
* @param assembly - the assembly whose contexts and variables to render.
|
||||
* @returns one entry per contributing context that rendered to non-empty text.
|
||||
*/
|
||||
export function renderContextSections(assembly: PromptAssembly): ContextSnapshotSection[] {
|
||||
export function renderContextSections(assembly: PromptPreparation): ContextSnapshotSection[] {
|
||||
return assembly.contexts
|
||||
.map(context => ({
|
||||
name: context.name,
|
||||
@@ -570,12 +582,12 @@ export class SystemPrompt extends Service {
|
||||
* added only by the assembly waterfall retain their accepted values. Current
|
||||
* suppression removes optional contexts, and missing required registrations
|
||||
* are restored in registry order. Sections, tools, and interpolation variables
|
||||
* retain the accepted assembly; their providers and waterfall do not rerun.
|
||||
* retain their accepted values during this refresh.
|
||||
* @param assembly - accepted assembly for this scope and step.
|
||||
* @param context - the same scope and current plugin-defined assembly fields.
|
||||
* @returns the accepted assembly with current runtime-context provider text.
|
||||
*/
|
||||
refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly {
|
||||
refreshContext<T extends PromptPreparation>(assembly: T, context: AssembleContext = {}): T {
|
||||
const providers = this.layers.merge(context.scope, layer => layer.contexts)
|
||||
const suppressed = !this.layers.global.runtimeContextSuppressors.isEmpty()
|
||||
|| this.layers.chainLayers(context.scope).some(layer => !layer.runtimeContextSuppressors.isEmpty())
|
||||
@@ -598,16 +610,12 @@ export class SystemPrompt extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble global and scoped providers, detach tool parameters, apply
|
||||
* canonical ordering, then run the assembly waterfall. Scoped sections and
|
||||
* variables shadow globals. The returned waterfall value is authoritative
|
||||
* except that an effective complete section is restored afterwards as the
|
||||
* sole prompt section.
|
||||
* @param context - the optional scope and plugin-defined assembly fields.
|
||||
* @returns the post-waterfall assembly with any complete prompt enforced.
|
||||
* Prepare variables and runtime facts before accepting a step. No tool
|
||||
* conditions or prompt sections are evaluated in this phase.
|
||||
* @param context - calling scope and request cancellation.
|
||||
* @returns prepared inputs after the preparation waterfall.
|
||||
*/
|
||||
// Keep configuration failures on the declared asynchronous error path.
|
||||
async assemble(context: AssembleContext = {}): Promise<PromptAssembly> {
|
||||
async prepare(context: AssembleContext = {}): Promise<PromptPreparation> {
|
||||
const scope = context.scope
|
||||
const scopeLayers = this.layers.chainLayers(scope)
|
||||
const runtimeContextSuppressed = !this.layers.global.runtimeContextSuppressors.isEmpty()
|
||||
@@ -623,6 +631,28 @@ export class SystemPrompt extends Service {
|
||||
variables[name] = provider(context)
|
||||
}
|
||||
}
|
||||
const contexts = [...this.layers.merge(scope, layer => layer.contexts).values()]
|
||||
.filter(entry => !runtimeContextSuppressed || entry.required === true)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(entry => resolveContext(entry, context))
|
||||
const preparation = { contexts, variables }
|
||||
return this.ctx.waterfall(scopeTarget(this, scope), 'system-prompt/prepare', preparation, context,
|
||||
() => Promise.resolve(preparation))
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble prompt sections and tool declarations after request preparation.
|
||||
* The presentation waterfall cannot replace provider-owned tools.
|
||||
* @param context - calling scope, route-bound agent, and resolved tools.
|
||||
* @param preparation - accepted variables and runtime facts; prepared here when omitted.
|
||||
* @returns the complete assembly with ordered tool declarations.
|
||||
*/
|
||||
async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise<PromptAssembly> {
|
||||
const prepared = preparation ?? await this.prepare(context)
|
||||
const scope = context.scope
|
||||
const scopeLayers = this.layers.chainLayers(scope)
|
||||
const runtimeContextSuppressed = !this.layers.global.runtimeContextSuppressors.isEmpty()
|
||||
|| scopeLayers.some(layer => !layer.runtimeContextSuppressors.isEmpty())
|
||||
// Scoped sections shadow globals before the deterministic order sort.
|
||||
const sectionByName = this.layers.merge(scope, layer => layer.sections)
|
||||
const contextByName = this.layers.merge(scope, layer => layer.contexts)
|
||||
@@ -661,25 +691,28 @@ export class SystemPrompt extends Service {
|
||||
if (section.complete === true) completeSection = { ...assembled }
|
||||
return assembled
|
||||
})
|
||||
const assembly: PromptAssembly = {
|
||||
sections,
|
||||
contexts: [...contextByName.values()]
|
||||
.filter(entry => !runtimeContextSuppressed || entry.required === true)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(entry => resolveContext(entry, context)),
|
||||
tools: orderTools(collected, this.toolOrder, knownNames),
|
||||
variables,
|
||||
}
|
||||
const tools = orderTools(collected, this.toolOrder, knownNames)
|
||||
const assembly: PromptPresentation = { ...prepared, sections }
|
||||
const transformed = await this.ctx.waterfall(
|
||||
scopeTarget(this, scope), 'system-prompt/assemble', assembly, context,
|
||||
() => Promise.resolve(assembly),
|
||||
)
|
||||
if (completeSection === undefined && !runtimeContextSuppressed) return transformed
|
||||
const requiredContexts = new Set([...contextByName.values()].filter(entry => entry.required === true).map(entry => entry.name))
|
||||
const contexts = runtimeContextSuppressed
|
||||
? transformed.contexts.filter(entry => requiredContexts.has(entry.name)) : [...transformed.contexts]
|
||||
for (const provider of contextByName.values()) {
|
||||
if (provider.required !== true) continue
|
||||
const name = provider.name
|
||||
const value = resolveContext(provider, context)
|
||||
const existing = contexts.findIndex(entry => entry.name === name)
|
||||
if (existing >= 0) contexts[existing] = value
|
||||
else contexts.push(value)
|
||||
}
|
||||
return {
|
||||
...transformed,
|
||||
tools,
|
||||
sections: completeSection === undefined ? transformed.sections : [completeSection],
|
||||
contexts: runtimeContextSuppressed ? transformed.contexts.filter(entry => requiredContexts.has(entry.name)) : transformed.contexts,
|
||||
contexts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,21 +78,14 @@ describe('SystemPrompt tool order', () => {
|
||||
expect(assembly.tools.map(t => t.description)).toEqual(['anchor', 'first', 'second'])
|
||||
})
|
||||
|
||||
it('canonicalizes BEFORE the assemble waterfall: listeners see the ordered list and own their own edits', async () => {
|
||||
it('keeps ordered provider declarations outside presentation transforms', async () => {
|
||||
const ctx = await mount()
|
||||
ctx.systemPrompt.tools(() => ({ schemas: [tool('zulu'), tool('alpha')] }))
|
||||
let seen: string[] | undefined
|
||||
ctx.on('system-prompt/assemble', function (assembly, _context, next) {
|
||||
seen = assembly.tools.map(t => t.name)
|
||||
// A listener-appended tool is NOT re-sorted — same contract as sections:
|
||||
// canonicalization applies to what the registry contributed, and a
|
||||
// listener owns the determinism of what it emits.
|
||||
assembly.tools.push(tool('aardvark'))
|
||||
return next()
|
||||
ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
|
||||
expect(assembly).not.toHaveProperty('tools')
|
||||
return { ...await next(), tools: [tool('aardvark')] }
|
||||
})
|
||||
const assembly = await ctx.systemPrompt.assemble()
|
||||
expect(seen).toEqual(['alpha', 'zulu'])
|
||||
expect(names(assembly)).toEqual(['alpha', 'zulu', 'aardvark'])
|
||||
expect(names(await ctx.systemPrompt.assemble())).toEqual(['alpha', 'zulu'])
|
||||
})
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
/deepseek-ai-dsh-tools/use-this-package/register-a-tool:
|
||||
en: 9d2e6caa39c53d62
|
||||
zh: 096a777f828c7565
|
||||
/deepseek-ai-dsh-tools/use-this-package/conditional-tools:
|
||||
en: 93008deced28d5fe
|
||||
zh: 09fe2f8693c5f147
|
||||
/deepseek-ai-dsh-tools/use-this-package/configure-the-presentation-mode:
|
||||
en: 19a752864651fc41
|
||||
zh: 48e20c3cdc867428
|
||||
|
||||
@@ -63,6 +63,14 @@ ctx.tools.register(defineTool({
|
||||
|
||||
The unified schema DSL supports `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`, author-only `json`, and exact-one `oneOf`; `InferValue` preserves exact types through 16 container levels before widening to `JsonValue`. A raw JSON Schema (`JsonSchemaNode`) is the wire-level counterpart shared with subagents, workflows, and MCP.
|
||||
|
||||
### Conditional tools
|
||||
|
||||
`register(definition, { when })` evaluates a synchronous or asynchronous condition for each model request. The condition receives `{ agent, signal }`; `agent.currentRoute()` reports the resolved provider/model. False omits the tool from Native declarations, PTC bindings, and execution. A thrown condition fails request preparation as `TOOL_ACTIVATION_FAILED`; it is not treated as ordinary unavailability. Conditions must observe cancellation and must not change registrations or perform tool work.
|
||||
|
||||
`resolve({ agent, signal })` returns a request-owned `ResolvedToolSet`. Pass it as `toolSet` to prompt assembly and every Native or PTC execution, then dispose it after the batch settles. Registrations stay effect-owned; each selection admits only the original registration in its exact agent scope. Disposal, new restrictions, cancellation, or same-name replacement prevent unstarted calls from using it. Started operations retain their cooperative cancellation obligations. Conditional calls without a resolved set return `UNKNOWN_TOOL`.
|
||||
|
||||
`get(name, scope, toolSet)` and `schemas(scope, toolSet)` inspect an admitted set; omitting `toolSet` inspects registered definitions without evaluating conditions. Tool guidance receives the same selection as `AssembleContext.toolSet`. A presentation transform cannot add or remove callable tools.
|
||||
|
||||
### Configure the presentation mode
|
||||
|
||||
The `mode` config decides what the model sees: `native` (every visible schema) or `ptc` (only `run_code` plus a generated SDK).
|
||||
|
||||
@@ -63,6 +63,14 @@ ctx.tools.register(defineTool({
|
||||
|
||||
统一 schema DSL 支持 `string`、`number`、`integer`、`boolean`、`null`、`array`、`object`、仅供作者使用的 `json` 与恰好匹配一个分支的 `oneOf`;`InferValue` 在 16 层容器内保留精确类型,之后加宽为 `JsonValue`。原始 JSON Schema(`JsonSchemaNode`)是与 subagent、工作流和 MCP 共享的协议级对应类型。
|
||||
|
||||
### 条件工具
|
||||
|
||||
`register(definition, { when })` 为每次模型请求计算同步或异步条件。条件接收 `{ agent, signal }`;`agent.currentRoute()` 返回已解析的提供方与模型。返回 false 会从原生声明、PTC 绑定和执行集合中排除工具。条件抛错会以 `TOOL_ACTIVATION_FAILED` 中止请求准备,不会被当成正常不可用。条件必须响应取消,不得改变注册或执行工具操作。
|
||||
|
||||
`resolve({ agent, signal })` 返回请求拥有的 `ResolvedToolSet`。将它作为 `toolSet` 传给提示词组装以及每次原生或 PTC 执行,批次结束后释放。注册仍由 effect 持有;每份集合只准入原 agent scope 中的原注册。注销、新的限制、取消或同名替换会阻止尚未开始的调用继续使用它。已经开始的操作保留协作取消义务。没有已解析集合的条件工具调用返回 `UNKNOWN_TOOL`。
|
||||
|
||||
`get(name, scope, toolSet)` 和 `schemas(scope, toolSet)` 读取已准入集合;省略 `toolSet` 时只查看已注册定义,不计算条件。工具指导文本通过 `AssembleContext.toolSet` 接收同一集合。展示变换不能增删可调用工具。
|
||||
|
||||
### 配置呈现模式
|
||||
|
||||
`mode` 配置决定模型看到什么:`native`(每个可见 schema)或 `ptc`(只有 `run_code` 加一份生成 SDK)。
|
||||
|
||||
@@ -29,6 +29,13 @@ import { renderToolsSdk } from './ts-types.ts'
|
||||
import type { ToolSdkSchema } from './ts-types.ts'
|
||||
import { renderToolsSdkPy } from './py-types.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-system-prompt' {
|
||||
interface AssembleContext {
|
||||
/** Request-owned tool selection; absent when inspecting registered definitions. */
|
||||
toolSet?: ResolvedToolSet
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-llm' {
|
||||
interface MessageSourceMap {
|
||||
/** Tool availability changes supplied by the tool registry. */
|
||||
@@ -220,6 +227,55 @@ export interface ToolOutputDefinition {
|
||||
presentationMeta?(args: unknown, value: JsonValue): JsonValue
|
||||
}
|
||||
|
||||
/** Inputs for a tool's request-time activation condition. */
|
||||
export interface ToolActivationContext {
|
||||
/** Agent whose currentRoute() is bound to the request being prepared. */
|
||||
readonly agent: Agent
|
||||
/** Cancellation of request preparation; asynchronous conditions must observe it. */
|
||||
readonly signal: AbortSignal
|
||||
}
|
||||
|
||||
/** Lifecycle-owned conditions for a registered tool. */
|
||||
export interface ToolRegistrationOptions {
|
||||
/**
|
||||
* Decide whether this request may call the tool. Omission enables it. False
|
||||
* excludes it from both declarations and dispatch; rejection fails request
|
||||
* preparation. Conditions must not mutate registrations or start tool work.
|
||||
* @param context - requesting agent and cancellation signal.
|
||||
* @returns whether the tool participates in this request.
|
||||
*/
|
||||
readonly when?: (context: ToolActivationContext) => boolean | Promise<boolean>
|
||||
}
|
||||
|
||||
/** One request's admitted registrations, also used by nested PTC calls. */
|
||||
export interface ResolvedToolSet {
|
||||
/** Close request admission after its tool batch settles. */
|
||||
dispose(): void
|
||||
/**
|
||||
* Read admitted registrations that remain visible in their original scope.
|
||||
* @param scope - exact requesting agent; another scope is never admitted.
|
||||
* @returns original definitions excluding removed or restricted registrations.
|
||||
*/
|
||||
view(scope: ScopeKey | undefined): ReadonlyMap<string, ToolDefinition>
|
||||
}
|
||||
|
||||
/** Failure to determine a tool's request-time availability. */
|
||||
export class ToolActivationError extends HarnessError {
|
||||
/**
|
||||
* Preserve a condition failure without treating it as normal unavailability.
|
||||
* @param name - tool whose condition rejected.
|
||||
* @param cause - underlying condition failure.
|
||||
*/
|
||||
constructor(name: string, cause: unknown) {
|
||||
super(`cannot resolve availability of tool "${name}": ${errorMessage(cause)}`, 'TOOL_ACTIVATION_FAILED', { cause })
|
||||
}
|
||||
}
|
||||
|
||||
interface ToolRegistration {
|
||||
readonly definition: ToolDefinition
|
||||
readonly when: ToolRegistrationOptions['when']
|
||||
}
|
||||
|
||||
/** A registered tool: its schema plus the execution function. */
|
||||
export interface ToolDefinition extends ToolSchema {
|
||||
/** Mandatory canonical output declaration. */
|
||||
@@ -338,6 +394,8 @@ export interface ToolExecutionInput {
|
||||
readonly arguments: unknown
|
||||
/** The agent on whose behalf the call runs (set by the agent loop). */
|
||||
readonly agent?: Agent
|
||||
/** Request admission shared by Native and PTC dispatch. Conditional tools require it. */
|
||||
readonly toolSet?: ResolvedToolSet
|
||||
/**
|
||||
* Opaque token of the enclosing transport execution, when one exists. PTC
|
||||
* mode sets this on SDK sub-dispatches so commit-style observers can wait for
|
||||
@@ -715,6 +773,7 @@ interface CompiledToolRestriction {
|
||||
interface ToolView {
|
||||
/** Visible definitions after restrictions, scoped shadowing, and transport insertion. */
|
||||
readonly visible: ReadonlyMap<string, ToolDefinition>
|
||||
readonly registrations: ReadonlyMap<string, ToolRegistration>
|
||||
/** Pre-restriction capability names used by prompt-order validation. */
|
||||
readonly knownNames: ReadonlySet<string>
|
||||
/** Current global names that a scoped restriction may name. */
|
||||
@@ -733,7 +792,7 @@ export type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefin
|
||||
|
||||
/** One scope's complete tool-registry contribution. */
|
||||
class ToolLayer implements ScopeLayer {
|
||||
readonly tools: NamedEntries<ToolDefinition>
|
||||
readonly tools: NamedEntries<ToolRegistration>
|
||||
readonly restrictions = new AnonymousEntries<CompiledToolRestriction>()
|
||||
readonly guards = new AnonymousEntries<ToolGuard>()
|
||||
/**
|
||||
@@ -852,7 +911,7 @@ export class ToolRuntime extends Service {
|
||||
// optional-input type for direct (non-Loader) construction in tests.
|
||||
this.defaultMode = config.mode ?? 'native'
|
||||
this.maxParallelSubCalls = resolveMaxParallelSubCalls(config.maxParallelSubCalls)
|
||||
ctx.systemPrompt.tools(context => this.wireSchemas(context.scope))
|
||||
ctx.systemPrompt.tools(context => this.wireSchemas(context.scope, context.toolSet))
|
||||
if (this.defaultMode !== 'native') {
|
||||
ctx.systemPrompt.section(this.collapseSection())
|
||||
ctx.systemPrompt.section(this.sdkSection())
|
||||
@@ -909,7 +968,7 @@ export class ToolRuntime extends Service {
|
||||
const render = SDK_RENDERERS[runtime.language]
|
||||
/* v8 ignore next -- requirePtcRuntime rejects an unknown language before this runs. */
|
||||
if (render === undefined) throw new Error(`dsh-tools: no SDK renderer for ${runtime.language}`)
|
||||
return render(this.sdkSchemas(context.scope))
|
||||
return render(this.sdkSchemas(context.scope, context.toolSet))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1014,11 +1073,11 @@ export class ToolRuntime extends Service {
|
||||
* Build one scope's wire schemas and names for prompt-order validation.
|
||||
* Restrictions do not make known tools invalid, but a mode collapse does.
|
||||
*/
|
||||
private wireSchemas(scope?: ScopeKey): ToolProviderResult {
|
||||
private wireSchemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolProviderResult {
|
||||
const view = this.view(scope)
|
||||
const mode = this.modeFor(scope)
|
||||
if (mode === 'native') {
|
||||
const schemas = [...view.visible.values()].map(definition => this.schemaOf(definition, false))
|
||||
const schemas = this.definitions(scope, toolSet).map(definition => this.schemaOf(definition, false))
|
||||
return { schemas, knownNames: [...view.knownNames] }
|
||||
}
|
||||
// Validate the runtime language BEFORE projecting schemas: schemaOf reads
|
||||
@@ -1027,7 +1086,7 @@ export class ToolRuntime extends Service {
|
||||
// renderer-table rejection the canonical assembly-time error for a
|
||||
// language with no SDK renderer.
|
||||
this.requirePtcRuntime(mode)
|
||||
const schemas = [...view.visible.values()].map(definition => this.schemaOf(definition, false))
|
||||
const schemas = this.definitions(scope, toolSet).map(definition => this.schemaOf(definition, false))
|
||||
return {
|
||||
schemas: schemas.filter(schema => schema.name === RUN_CODE_NAME),
|
||||
knownNames: [RUN_CODE_NAME],
|
||||
@@ -1064,9 +1123,10 @@ export class ToolRuntime extends Service {
|
||||
* Register globally or in the calling agent scope. Scoped tools shadow
|
||||
* globals; duplicates within one layer and the reserved `run_code` name fail.
|
||||
* @param definition - tool schema, execution, and optional finalization/presentation callbacks.
|
||||
* @param options - optional request-time activation condition.
|
||||
* @returns the exact disposer that unregisters the tool.
|
||||
*/
|
||||
register(definition: ToolDefinition): () => void {
|
||||
register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void {
|
||||
const name = definition.name
|
||||
const output = (definition as Partial<ToolDefinition>).output
|
||||
if (output === undefined || typeof output !== 'object'
|
||||
@@ -1088,7 +1148,7 @@ export class ToolRuntime extends Service {
|
||||
}
|
||||
return this.layers.effect(
|
||||
this.ctx,
|
||||
layer => layer.tools.insert(name, definition),
|
||||
layer => layer.tools.insert(name, { definition, when: options.when }),
|
||||
{ label: 'tools.register()' },
|
||||
)
|
||||
}
|
||||
@@ -1190,27 +1250,32 @@ export class ToolRuntime extends Service {
|
||||
const own = this.layers.peek(scope)
|
||||
// Inherited surface, nearest ancestor last: a nearer scope's same-name
|
||||
// entry shadows a farther one, and the global layer is the farthest.
|
||||
const inherited = new Map<string, ToolDefinition>(this.layers.global.tools.entries())
|
||||
const inherited = new Map(this.layers.global.tools.entries())
|
||||
for (const layer of layers) {
|
||||
if (layer === own) continue
|
||||
for (const [name, definition] of layer.tools.entries()) inherited.set(name, definition)
|
||||
}
|
||||
const visible = new Map<string, ToolDefinition>()
|
||||
const registrations = new Map<string, ToolRegistration>()
|
||||
const knownNames = new Set<string>()
|
||||
const restrictableNames = new Set<string>()
|
||||
for (const [name, definition] of inherited) {
|
||||
for (const [name, registration] of inherited) {
|
||||
knownNames.add(name)
|
||||
restrictableNames.add(name)
|
||||
// Restrictions intersect across the whole chain: any scope on it may
|
||||
// mask an inherited name for everything nested inside it.
|
||||
if (layers.every(layer => layer.admits(name))) visible.set(name, definition)
|
||||
if (layers.every(layer => layer.admits(name))) {
|
||||
visible.set(name, registration.definition)
|
||||
registrations.set(name, registration)
|
||||
}
|
||||
}
|
||||
// The scope's own registrations last, shadowing an inherited name and
|
||||
// outside the filter above.
|
||||
if (own !== undefined) {
|
||||
for (const [name, definition] of own.tools.entries()) {
|
||||
for (const [name, registration] of own.tools.entries()) {
|
||||
knownNames.add(name)
|
||||
visible.set(name, definition)
|
||||
visible.set(name, registration.definition)
|
||||
registrations.set(name, registration)
|
||||
}
|
||||
}
|
||||
// Presentation infrastructure is resolved last and outside capability
|
||||
@@ -1221,7 +1286,7 @@ export class ToolRuntime extends Service {
|
||||
if (this.modeFor(scope) !== 'native') {
|
||||
visible.set(RUN_CODE_NAME, this.requirePtcTransport())
|
||||
}
|
||||
return { visible, knownNames, restrictableNames }
|
||||
return { visible, registrations, knownNames, restrictableNames }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1231,10 +1296,58 @@ export class ToolRuntime extends Service {
|
||||
* actually executed.
|
||||
* @param name - the tool name as registered.
|
||||
* @param scope - the viewing scope (the agent); omitted = the global view.
|
||||
* @param toolSet - request admission; omit only to inspect registered definitions.
|
||||
* @returns the definition the scope resolves, or undefined when none is visible.
|
||||
*/
|
||||
get(name: string, scope?: ScopeKey): ToolDefinition | undefined {
|
||||
return this.view(scope).visible.get(name)
|
||||
get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined {
|
||||
return toolSet === undefined ? this.view(scope).visible.get(name) : toolSet.view(scope).get(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve conditions once for this request without changing shared registrations.
|
||||
* Cancellation or a rejected condition publishes no partial selection. Removed
|
||||
* registrations cannot be restored by a late condition result.
|
||||
* @param context - agent with a bound route and request cancellation.
|
||||
* @returns the complete selection; the caller disposes it after its tool batch settles.
|
||||
*/
|
||||
async resolve(context: ToolActivationContext): Promise<ResolvedToolSet> {
|
||||
const { agent, signal } = context
|
||||
signal.throwIfAborted()
|
||||
const view = this.view(agent)
|
||||
const admitted = new Map<string, ToolDefinition>()
|
||||
for (const [name, definition] of view.visible) {
|
||||
const registration = view.registrations.get(name)
|
||||
let enabled: boolean
|
||||
try {
|
||||
enabled = registration?.when === undefined || await registration.when(context)
|
||||
} catch (cause: unknown) {
|
||||
signal.throwIfAborted()
|
||||
throw new ToolActivationError(name, cause)
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
if (enabled) admitted.set(name, definition)
|
||||
}
|
||||
let closed = false
|
||||
return Object.freeze({
|
||||
dispose: () => { closed = true },
|
||||
view: (scope: ScopeKey | undefined): ReadonlyMap<string, ToolDefinition> => {
|
||||
const visible = new Map<string, ToolDefinition>()
|
||||
if (closed || scope !== agent || signal.aborted) return visible
|
||||
const current = this.view(agent)
|
||||
for (const [name, definition] of admitted) {
|
||||
if (current.visible.get(name) === definition
|
||||
&& current.registrations.get(name) === view.registrations.get(name)) {
|
||||
visible.set(name, definition)
|
||||
}
|
||||
}
|
||||
return visible
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private definitions(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition[] {
|
||||
if (toolSet === undefined) return [...this.view(scope).visible.values()]
|
||||
return [...toolSet.view(scope).values()]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1250,8 +1363,11 @@ export class ToolRuntime extends Service {
|
||||
* @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
|
||||
* @returns the definition that may run, or undefined when the call must be rejected.
|
||||
*/
|
||||
private resolveExecution(name: string, scope: ScopeKey | undefined, nested: boolean): ToolDefinition | undefined {
|
||||
const tool = this.get(name, scope)
|
||||
private resolveExecution(
|
||||
name: string, scope: ScopeKey | undefined, nested: boolean, toolSet?: ResolvedToolSet,
|
||||
): ToolDefinition | undefined {
|
||||
if (toolSet === undefined && this.view(scope).registrations.get(name)?.when !== undefined) return undefined
|
||||
const tool = this.get(name, scope, toolSet)
|
||||
if (tool === undefined) return undefined
|
||||
if (this.collapses(name, scope, nested)) return undefined
|
||||
return tool
|
||||
@@ -1261,15 +1377,16 @@ export class ToolRuntime extends Service {
|
||||
* Project visible definitions onto the allowlisted model-facing schema fields,
|
||||
* excluding execution and presentation callbacks.
|
||||
* @param scope - the viewing scope (the agent); omitted = the global view.
|
||||
* @param toolSet - request admission, or omitted for registered-definition inspection.
|
||||
* @returns one deep-cloned schema per visible tool.
|
||||
*/
|
||||
schemas(scope?: ScopeKey): ToolSchema[] {
|
||||
return [...this.view(scope).visible.values()].map(definition => this.schemaOf(definition, true))
|
||||
schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[] {
|
||||
return this.definitions(scope, toolSet).map(definition => this.schemaOf(definition, true))
|
||||
}
|
||||
|
||||
/** Project visible callable tools onto the generated PTC mode SDK contract. */
|
||||
private sdkSchemas(scope?: ScopeKey): ToolSdkSchema[] {
|
||||
return [...this.view(scope).visible.values()]
|
||||
private sdkSchemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSdkSchema[] {
|
||||
return this.definitions(scope, toolSet)
|
||||
.filter(definition => definition.name !== RUN_CODE_NAME)
|
||||
.map((definition): ToolSdkSchema => {
|
||||
const output = snapshotJsonValue(definition.output.schema)
|
||||
@@ -1307,7 +1424,7 @@ export class ToolRuntime extends Service {
|
||||
* @returns the fail-closed scheduling mode.
|
||||
*/
|
||||
executionMode(exec: ToolExecutionInput): ToolExecutionMode {
|
||||
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined)
|
||||
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined, exec.toolSet)
|
||||
if (!tool?.isConcurrencySafe) return { kind: 'exclusive' }
|
||||
try {
|
||||
const concurrencySafe: unknown = tool.isConcurrencySafe(exec.arguments)
|
||||
@@ -1410,7 +1527,7 @@ export class ToolRuntime extends Service {
|
||||
// observe — or worse, approve — a call that can only fail. An unknown tool
|
||||
// keeps the historical dispatch-stage `UNKNOWN_TOOL` path so policy
|
||||
// listeners still see every name that reaches the registry.
|
||||
const visible = this.get(name, agent)
|
||||
const visible = this.get(name, agent, exec.toolSet)
|
||||
const collapsed = visible !== undefined && this.collapses(name, agent, parent !== undefined)
|
||||
const concludingExecutions = this.concludingExecutions
|
||||
const base = {
|
||||
@@ -1420,6 +1537,7 @@ export class ToolRuntime extends Service {
|
||||
name,
|
||||
signal,
|
||||
...agent !== undefined ? { agent } : {},
|
||||
...exec.toolSet !== undefined ? { toolSet: exec.toolSet } : {},
|
||||
...parent !== undefined ? { parent } : {},
|
||||
...exec.schema !== undefined ? { schema: exec.schema } : {},
|
||||
deferContext(context: UserMessage): void {
|
||||
@@ -1581,7 +1699,7 @@ export class ToolRuntime extends Service {
|
||||
}
|
||||
exec.signal = signal
|
||||
try {
|
||||
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined)
|
||||
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined, exec.toolSet)
|
||||
if (!tool) throw new ToolNotFoundError(exec.name)
|
||||
state.bodyInvoked = true
|
||||
const returned = await tool.execute(exec.arguments, exec)
|
||||
@@ -1810,7 +1928,7 @@ export class ToolRuntime extends Service {
|
||||
if (result.isError) {
|
||||
throw new TypeError('tools/post-execute cannot replace the value of a failed result')
|
||||
}
|
||||
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined)
|
||||
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined, exec.toolSet)
|
||||
if (tool === undefined) throw new ToolNotFoundError(exec.name)
|
||||
const replaced = this.createSuccessResult(exec, tool, decision.value)
|
||||
return this.markCanonical(exec, {
|
||||
@@ -1879,7 +1997,7 @@ export class ToolRuntime extends Service {
|
||||
...result.additionalContexts !== undefined ? { additionalContexts: result.additionalContexts } : {},
|
||||
})
|
||||
}
|
||||
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined)
|
||||
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined, exec.toolSet)
|
||||
if (tool === undefined) throw new ToolNotFoundError(exec.name)
|
||||
const normalized = this.createSuccessResult(exec, tool, result.value)
|
||||
return this.markCanonical(exec, {
|
||||
|
||||
@@ -554,6 +554,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO
|
||||
schema,
|
||||
arguments: normalized.dispatched,
|
||||
...exec.agent ? { agent: exec.agent } : {},
|
||||
...exec.toolSet !== undefined ? { toolSet: exec.toolSet } : {},
|
||||
parent: exec.token,
|
||||
signal: runController.signal,
|
||||
}
|
||||
@@ -691,7 +692,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO
|
||||
// restricted globals vanish) — the same view the SDK section declared,
|
||||
// so a program can bind exactly what its prompt promised; sub-dispatch
|
||||
// re-resolves per call through the same view (exec.agent threads down).
|
||||
for (const schema of registry.schemas(exec.agent)) {
|
||||
for (const schema of registry.schemas(exec.agent, exec.toolSet)) {
|
||||
if (schema.name === RUN_CODE_NAME) continue
|
||||
Object.defineProperty(functions, schema.name, { enumerable: true, value: binding(deepFreeze(schema)) })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/** Conditional registrations share request admission with direct and nested execution. */
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import { createScope, type Scope } from '@deepseek-ai/dsh-scope'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRuntime, { defineTool, ToolActivationError } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const contexts: Context[] = []
|
||||
afterEach(async () => {
|
||||
for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
async function setup() {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
const agent = { id: SessionId('activation'), currentRoute: () => ({ provider: 'test', model: 'test' }) } as Agent
|
||||
let scope!: Scope
|
||||
await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, agent) }, { inject: ['tools'] }))
|
||||
const controller = new AbortController()
|
||||
let calls = 0
|
||||
const tool = defineTool({
|
||||
name: 'conditional', description: 'A conditional operation.', parameters: {},
|
||||
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
|
||||
async execute() { calls++; return 'done' },
|
||||
})
|
||||
const input = { agent, name: tool.name, arguments: {}, callId: ToolCallId('call'), signal: controller.signal }
|
||||
const resolve = () => ctx.tools.resolve({ agent, signal: controller.signal })
|
||||
return { ctx, agent, scope, controller, tool, input, resolve, calls: () => calls }
|
||||
}
|
||||
|
||||
it('activates and deactivates a registered tool without allowing historical calls', async () => {
|
||||
const h = await setup()
|
||||
let enabled = false
|
||||
h.ctx.tools.register(h.tool, { when: () => enabled })
|
||||
const unavailable = await h.resolve()
|
||||
expect(h.ctx.tools.schemas(h.agent, unavailable)).toEqual([])
|
||||
enabled = true
|
||||
expect((await h.ctx.tools.execute({ ...h.input, toolSet: unavailable })).error?.info?.code).toBe('UNKNOWN_TOOL')
|
||||
expect((await h.ctx.tools.execute(h.input)).error?.info?.code).toBe('UNKNOWN_TOOL')
|
||||
const available = await h.resolve()
|
||||
expect(h.ctx.tools.schemas(h.agent, available).map(tool => tool.name)).toEqual(['conditional'])
|
||||
expect((await h.ctx.tools.execute({ ...h.input, toolSet: available })).isError).toBe(false)
|
||||
enabled = false
|
||||
const disabled = await h.resolve()
|
||||
expect((await h.ctx.tools.execute({ ...h.input, toolSet: disabled })).error?.info?.code).toBe('UNKNOWN_TOOL')
|
||||
expect(h.calls()).toBe(1)
|
||||
})
|
||||
|
||||
it('keeps admissions specific to the requesting agent', async () => {
|
||||
const h = await setup()
|
||||
h.ctx.tools.register(h.tool, { when: () => true })
|
||||
const toolSet = await h.resolve()
|
||||
const other = { id: SessionId('other') } as Agent
|
||||
expect((await h.ctx.tools.execute({ ...h.input, agent: other, toolSet })).error?.info?.code).toBe('UNKNOWN_TOOL')
|
||||
expect(h.calls()).toBe(0)
|
||||
})
|
||||
|
||||
it('applies live restrictions and closes request admission permanently', async () => {
|
||||
const h = await setup()
|
||||
h.ctx.tools.register(h.tool, { when: () => true })
|
||||
const toolSet = await h.resolve()
|
||||
const lift = h.scope.ctx.tools.restrict({ deny: ['conditional'] })
|
||||
expect((await h.ctx.tools.execute({ ...h.input, toolSet })).error?.info?.code).toBe('UNKNOWN_TOOL')
|
||||
lift()
|
||||
expect((await h.ctx.tools.execute({ ...h.input, toolSet })).isError).toBe(false)
|
||||
toolSet.dispose()
|
||||
expect(h.ctx.tools.schemas(h.agent, toolSet)).toEqual([])
|
||||
expect((await h.ctx.tools.execute({ ...h.input, toolSet })).error?.info?.code).toBe('UNKNOWN_TOOL')
|
||||
expect(h.calls()).toBe(1)
|
||||
})
|
||||
|
||||
it('does not substitute a same-name registration after admission', async () => {
|
||||
const h = await setup()
|
||||
const unregister = h.ctx.tools.register(h.tool, { when: () => true })
|
||||
const old = await h.resolve()
|
||||
unregister()
|
||||
h.ctx.tools.register(h.tool, { when: () => true })
|
||||
expect((await h.ctx.tools.execute({ ...h.input, toolSet: old })).error?.info?.code).toBe('UNKNOWN_TOOL')
|
||||
expect((await h.ctx.tools.execute({ ...h.input, toolSet: await h.resolve() })).isError).toBe(false)
|
||||
expect(h.calls()).toBe(1)
|
||||
})
|
||||
|
||||
it('does not restore a registration removed during its asynchronous condition', async () => {
|
||||
const h = await setup()
|
||||
const condition = Promise.withResolvers<boolean>()
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const unregister = h.ctx.tools.register(h.tool, { when: () => { entered.resolve(undefined); return condition.promise } })
|
||||
const pending = h.resolve()
|
||||
await entered.promise
|
||||
unregister()
|
||||
condition.resolve(true)
|
||||
const toolSet = await pending
|
||||
expect(h.ctx.tools.schemas(h.agent, toolSet)).toEqual([])
|
||||
expect((await h.ctx.tools.execute({ ...h.input, toolSet })).error?.info?.code).toBe('UNKNOWN_TOOL')
|
||||
expect(h.calls()).toBe(0)
|
||||
})
|
||||
|
||||
it('propagates cancellation after an asynchronous condition settles', async () => {
|
||||
const h = await setup()
|
||||
const condition = Promise.withResolvers<boolean>()
|
||||
h.ctx.tools.register(h.tool, { when: ({ signal }) => { expect(signal).toBe(h.controller.signal); return condition.promise } })
|
||||
const pending = h.resolve()
|
||||
const rejection = expect(pending).rejects.toMatchObject({ name: 'AbortError' })
|
||||
h.controller.abort()
|
||||
condition.resolve(true)
|
||||
await rejection
|
||||
expect(h.calls()).toBe(0)
|
||||
})
|
||||
|
||||
it('reports condition failures without treating them as disabled tools', async () => {
|
||||
const h = await setup()
|
||||
const cause = new Error('credential storage unavailable')
|
||||
h.ctx.tools.register(h.tool, { when: () => { throw cause } })
|
||||
await expect(h.resolve()).rejects.toMatchObject({
|
||||
name: ToolActivationError.name, code: 'TOOL_ACTIVATION_FAILED', cause,
|
||||
})
|
||||
expect(h.calls()).toBe(0)
|
||||
})
|
||||
@@ -169,6 +169,51 @@ it('logs detached frozen nested metadata without changing the binding value or l
|
||||
})
|
||||
|
||||
describe('mode-aware wire contribution', () => {
|
||||
it('shares conditional admission with SDK bindings and rejects a replaced cached binding', async () => {
|
||||
const { ctx, runtime, systemPrompt } = await setup({ mode: 'ptc' })
|
||||
try {
|
||||
const agent = { session: Session.create(SessionId('conditional-ptc')) } as Agent
|
||||
let active = false
|
||||
const execute = vi.fn(async () => 'done')
|
||||
const definition = defineTool({
|
||||
name: 'conditional', description: 'Available on selected requests.', parameters: {},
|
||||
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
|
||||
execute,
|
||||
})
|
||||
const unregister = ctx.tools.register(definition, { when: () => active })
|
||||
for (const enabled of [false, true]) {
|
||||
active = enabled
|
||||
const toolSet = await ctx.tools.resolve({ agent, signal: testToolSignal })
|
||||
try {
|
||||
const assembly = await systemPrompt.assemble({ scope: agent, agent, toolSet })
|
||||
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
||||
expect(sdk.includes('conditional:')).toBe(enabled)
|
||||
runtime.behavior = async (request) => {
|
||||
const functions = request.bindings[0]!.functions
|
||||
expect(Object.keys(functions)).toEqual(enabled ? ['conditional'] : [])
|
||||
if (enabled) {
|
||||
expect(await functions.conditional!({})).toBe('done')
|
||||
unregister()
|
||||
ctx.tools.register(definition, { when: () => true })
|
||||
await expect(functions.conditional!({})).rejects.toThrow('unknown tool "conditional"')
|
||||
}
|
||||
return { logs: [] }
|
||||
}
|
||||
const result = await ctx.tools.execute({
|
||||
agent, toolSet, signal: testToolSignal, callId: ToolCallId('conditional-program'),
|
||||
name: RUN_CODE_NAME, arguments: { code: 'program', description: 'Check conditional bindings' },
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
} finally {
|
||||
toolSet.dispose()
|
||||
}
|
||||
}
|
||||
expect(execute).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects an unsupported presentation mode in configuration', () => {
|
||||
expect(() => ToolRuntime.Config({ mode: 'both' } as never)).toThrow()
|
||||
})
|
||||
@@ -267,7 +312,7 @@ describe('mode-aware wire contribution', () => {
|
||||
expect(sdk).toContain('deep_output: string | null')
|
||||
})
|
||||
|
||||
it('treats expert assembly output as authoritative in mode ptc', async () => {
|
||||
it('keeps PTC declarations owned by the registry during presentation transforms', async () => {
|
||||
const mode = 'ptc'
|
||||
const { ctx, systemPrompt } = await setup({ mode })
|
||||
registerEcho(ctx)
|
||||
@@ -276,13 +321,13 @@ describe('mode-aware wire contribution', () => {
|
||||
return {
|
||||
...assembly,
|
||||
sections: assembly.sections.filter(section => section.name !== 'tools:sdk'),
|
||||
tools: assembly.tools.filter(tool => tool.name !== RUN_CODE_NAME),
|
||||
tools: [],
|
||||
}
|
||||
}, { prepend: true })
|
||||
|
||||
const assembly = await systemPrompt.assemble()
|
||||
expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
|
||||
expect(assembly.tools.some(tool => tool.name === RUN_CODE_NAME)).toBe(false)
|
||||
expect(assembly.tools.some(tool => tool.name === RUN_CODE_NAME)).toBe(true)
|
||||
})
|
||||
|
||||
it('lets one scope shadow the default SDK section in mode ptc', async () => {
|
||||
|
||||
@@ -31,6 +31,7 @@ async function agent(ctx: Context, cwd: string | undefined): Promise<Agent> {
|
||||
version: SESSION_FORMAT_VERSION, id, createdAt: 0, ...cwd === undefined ? {} : { cwd }, isSeeded: false,
|
||||
})
|
||||
const value: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -16,6 +16,7 @@ async function fixture() {
|
||||
const fiber = ctx.plugin(() => {})
|
||||
const session = Session.create(SessionId(id))
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: session.id, session, ctx: fiber.ctx, options: {}, status: 'idle',
|
||||
inbox: unsupportedInbox(), send() {}, followup() {}, inject() {}, cancel() {},
|
||||
steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
|
||||
|
||||
@@ -43,6 +43,7 @@ async function agent(ctx: Context): Promise<Agent> {
|
||||
const id = SessionId('pty-loader-agent')
|
||||
const session = Session.create(id)
|
||||
const value: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id, options: {}, session, inbox: unsupportedInbox(),
|
||||
status: 'idle',
|
||||
ctx: scope.ctx,
|
||||
|
||||
@@ -20,6 +20,7 @@ async function fakeAgent(ctx: Context, rawId: string): Promise<Agent> {
|
||||
const id = SessionId(rawId)
|
||||
const session = Session.create(id)
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id, options: {}, session, inbox: unsupportedInbox(),
|
||||
status: 'idle',
|
||||
ctx: scope.ctx,
|
||||
|
||||
@@ -42,6 +42,7 @@ export function testAgent(ctx: Context, cwd: string, name = 'worktree-test'): Ag
|
||||
const id = SessionId(name)
|
||||
const session = Session.create(id, [], { version: SESSION_FORMAT_VERSION, id, createdAt: 0, isSeeded: false, cwd })
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id, session, options: {}, status: 'idle', ctx: scope.ctx, inbox: unsupportedInbox(),
|
||||
send() {}, followup() {}, steer() {}, inject() {}, cancel() {},
|
||||
whenIdle: async () => {}, runMaintenance: task => task(new AbortController().signal),
|
||||
|
||||
@@ -1655,8 +1655,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
key: 'planMode',
|
||||
summary: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool.',
|
||||
description: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the stable exit tool. Client carriers expose the projection\'s cropped `{ active, pending }` view.',
|
||||
summary: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the conditional exit tool.',
|
||||
description: '`ctx.planMode`: owns logged plan state, applies and narrates selected state at step start, the `plan:policy` section, the `/plan` command, and the conditional exit tool. Client carriers expose the projection\'s cropped `{ active, pending }` view.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'get(agent: Agent): { active: boolean; pending?: boolean }',
|
||||
@@ -3088,16 +3088,22 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
returns: 'the exact Cordis effect disposer.',
|
||||
},
|
||||
{
|
||||
signature: 'refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly',
|
||||
description: 'Refresh accepted registered runtime facts for request admission. Contexts added only by the assembly waterfall retain their accepted values. Current suppression removes optional contexts, and missing required registrations are restored in registry order. Sections, tools, and interpolation variables retain the accepted assembly; their providers and waterfall do not rerun.',
|
||||
signature: 'refreshContext<T extends PromptPreparation>(assembly: T, context: AssembleContext = {}): T',
|
||||
description: 'Refresh accepted registered runtime facts for request admission. Contexts added only by the assembly waterfall retain their accepted values. Current suppression removes optional contexts, and missing required registrations are restored in registry order. Sections, tools, and interpolation variables retain their accepted values during this refresh.',
|
||||
parameters: [{ name: 'assembly', description: 'accepted assembly for this scope and step.' }, { name: 'context', description: 'the same scope and current plugin-defined assembly fields.' }],
|
||||
returns: 'the accepted assembly with current runtime-context provider text.',
|
||||
},
|
||||
{
|
||||
signature: 'async assemble(context: AssembleContext = {}): Promise<PromptAssembly>',
|
||||
description: 'Assemble global and scoped providers, detach tool parameters, apply canonical ordering, then run the assembly waterfall. Scoped sections and variables shadow globals. The returned waterfall value is authoritative except that an effective complete section is restored afterwards as the sole prompt section.',
|
||||
parameters: [{ name: 'context', description: 'the optional scope and plugin-defined assembly fields.' }],
|
||||
returns: 'the post-waterfall assembly with any complete prompt enforced.',
|
||||
signature: 'async prepare(context: AssembleContext = {}): Promise<PromptPreparation>',
|
||||
description: 'Prepare variables and runtime facts before accepting a step. No tool conditions or prompt sections are evaluated in this phase.',
|
||||
parameters: [{ name: 'context', description: 'calling scope and request cancellation.' }],
|
||||
returns: 'prepared inputs after the preparation waterfall.',
|
||||
},
|
||||
{
|
||||
signature: 'async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise<PromptAssembly>',
|
||||
description: 'Assemble prompt sections and tool declarations after request preparation. The presentation waterfall cannot replace provider-owned tools.',
|
||||
parameters: [{ name: 'context', description: 'calling scope, route-bound agent, and resolved tools.' }, { name: 'preparation', description: 'accepted variables and runtime facts; prepared here when omitted.' }],
|
||||
returns: 'the complete assembly with ordered tool declarations.',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -3327,9 +3333,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
returns: 'the exact disposer that restores the deployment default.',
|
||||
},
|
||||
{
|
||||
signature: 'register(definition: ToolDefinition): () => void',
|
||||
signature: 'register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void',
|
||||
description: 'Register globally or in the calling agent scope. Scoped tools shadow globals; duplicates within one layer and the reserved `run_code` name fail.',
|
||||
parameters: [{ name: 'definition', description: 'tool schema, execution, and optional finalization/presentation callbacks.' }],
|
||||
parameters: [{ name: 'definition', description: 'tool schema, execution, and optional finalization/presentation callbacks.' }, { name: 'options', description: 'optional request-time activation condition.' }],
|
||||
returns: 'the exact disposer that unregisters the tool.',
|
||||
},
|
||||
{
|
||||
@@ -3345,15 +3351,21 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
returns: 'the exact disposer that unregisters the guard.',
|
||||
},
|
||||
{
|
||||
signature: 'get(name: string, scope?: ScopeKey): ToolDefinition | undefined',
|
||||
signature: 'get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined',
|
||||
description: 'Look up a tool as one scope sees it (scoped shadows global; a restricted-away global reads as absent). Presenters pass the calling agent so the rendered card matches the definition that actually executed.',
|
||||
parameters: [{ name: 'name', description: 'the tool name as registered.' }, { name: 'scope', description: 'the viewing scope (the agent); omitted = the global view.' }],
|
||||
parameters: [{ name: 'name', description: 'the tool name as registered.' }, { name: 'scope', description: 'the viewing scope (the agent); omitted = the global view.' }, { name: 'toolSet', description: 'request admission; omit only to inspect registered definitions.' }],
|
||||
returns: 'the definition the scope resolves, or undefined when none is visible.',
|
||||
},
|
||||
{
|
||||
signature: 'schemas(scope?: ScopeKey): ToolSchema[]',
|
||||
signature: 'async resolve(context: ToolActivationContext): Promise<ResolvedToolSet>',
|
||||
description: 'Resolve conditions once for this request without changing shared registrations. Cancellation or a rejected condition publishes no partial selection. Removed registrations cannot be restored by a late condition result.',
|
||||
parameters: [{ name: 'context', description: 'agent with a bound route and request cancellation.' }],
|
||||
returns: 'the complete selection; the caller disposes it after its tool batch settles.',
|
||||
},
|
||||
{
|
||||
signature: 'schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[]',
|
||||
description: 'Project visible definitions onto the allowlisted model-facing schema fields, excluding execution and presentation callbacks.',
|
||||
parameters: [{ name: 'scope', description: 'the viewing scope (the agent); omitted = the global view.' }],
|
||||
parameters: [{ name: 'scope', description: 'the viewing scope (the agent); omitted = the global view.' }, { name: 'toolSet', description: 'request admission, or omitted for registered-definition inspection.' }],
|
||||
returns: 'one deep-cloned schema per visible tool.',
|
||||
},
|
||||
{
|
||||
@@ -3966,7 +3978,7 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
mode: 'waterfall',
|
||||
signature: '\'agent/request\'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>',
|
||||
summary: 'Replace the frozen call configuration.',
|
||||
description: '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. On step admission, this runs after assembly and `step/start`, before the system prompt and accepted user batch are committed. Cancellation here or during subsequent `prepareCall()` resolution commits neither. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages.',
|
||||
description: '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. On step admission, this runs before context preparation and `agent/pre-step`. Cancellation here or during subsequent `prepareCall()` resolution opens no step and commits no system prompt or user batch. The prepared call capability governs prompt admission. Model-visible content must use logged channels; this waterfall cannot mutate messages.',
|
||||
parameters: [{ name: 'payload', description: '.signal - the current turn\'s explicit abort signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }],
|
||||
},
|
||||
{
|
||||
@@ -4388,9 +4400,9 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
{
|
||||
name: 'system-prompt/assemble',
|
||||
mode: 'waterfall',
|
||||
signature: '\'system-prompt/assemble\'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>',
|
||||
summary: 'Expert waterfall over the assembled sections, contexts, tools, and variables.',
|
||||
description: 'Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope\'s assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope\'s system prompt.',
|
||||
signature: '\'system-prompt/assemble\'(this: Scoped<SystemPrompt>, assembly: PromptPresentation, context: AssembleContext, next: () => Promise<PromptPresentation>): Promise<PromptPresentation>',
|
||||
summary: 'Presentation waterfall over assembled sections, contexts, and variables.',
|
||||
description: 'Presentation waterfall over assembled sections, contexts, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope\'s assemblies. The returned text is authoritative; tool declarations remain provider-owned. A supplied signal controls only this explicit assembly request and must not be retained to control later turns. A registered complete section is restored after this waterfall, so listeners cannot add to or replace that scope\'s system prompt.',
|
||||
parameters: [{ name: 'assembly', description: 'the mutable assembly built from registered providers.' }, { name: 'context', description: 'the caller\'s per-assembly context.' }],
|
||||
},
|
||||
{
|
||||
@@ -4401,6 +4413,14 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
description: 'Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope.',
|
||||
parameters: [],
|
||||
},
|
||||
{
|
||||
name: 'system-prompt/prepare',
|
||||
mode: 'waterfall',
|
||||
signature: '\'system-prompt/prepare\'(this: Scoped<SystemPrompt>, preparation: PromptPreparation, context: AssembleContext, next: () => Promise<PromptPreparation>): Promise<PromptPreparation>',
|
||||
summary: 'Prepare variables and runtime context before step admission.',
|
||||
description: 'Prepare variables and runtime context before step admission.',
|
||||
parameters: [{ name: 'preparation', description: 'registered variables and context contributions.' }, { name: 'context', description: 'calling scope and cancellation.' }],
|
||||
},
|
||||
{
|
||||
name: 'tools/change',
|
||||
mode: 'emit',
|
||||
@@ -6315,7 +6335,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'PromptAssembly',
|
||||
declaration: 'export interface PromptAssembly {\n sections: AssembledSection[];\n contexts: AssembledContext[];\n tools: ToolSchema[];\n variables: Record<string, string | undefined>;\n}',
|
||||
declaration: 'export interface PromptAssembly extends PromptPresentation {\n tools: ToolSchema[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'PromptContext',
|
||||
@@ -6333,6 +6353,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'PromptOrigin',
|
||||
declaration: 'export type PromptOrigin = {\n kind: \'composer\';\n} | {\n kind: \'plugin\';\n name: string;\n};',
|
||||
},
|
||||
{
|
||||
name: 'PromptPreparation',
|
||||
declaration: 'export interface PromptPreparation {\n contexts: AssembledContext[];\n variables: Record<string, string | undefined>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PromptPresentation',
|
||||
declaration: 'export interface PromptPresentation extends PromptPreparation {\n sections: AssembledSection[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'PromptSection',
|
||||
declaration: 'export interface PromptSection {\n readonly name: string;\n readonly order: number;\n readonly text: string | ((context: AssembleContext) => string);\n readonly interpolate?: boolean;\n readonly complete?: boolean;\n}',
|
||||
@@ -6521,6 +6549,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ResolvedSubagentStartRequest',
|
||||
declaration: 'export interface ResolvedSubagentStartRequest extends SubagentStartRequest {\n readonly cwd: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ResolvedToolSet',
|
||||
declaration: 'export interface ResolvedToolSet {\n dispose(): void;\n view(scope: ScopeKey | undefined): ReadonlyMap<string, ToolDefinition>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'RestoredSessionOptions',
|
||||
declaration: 'export interface RestoredSessionOptions {\n readonly seed: SessionEvent[];\n readonly meta: SessionHeader;\n readonly inheritedEventCount: SessionLogOffset;\n readonly eventState: SessionSeedEventState;\n}',
|
||||
@@ -7707,7 +7739,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'SystemPrompt',
|
||||
declaration: 'export class SystemPrompt extends Service {\n static Config: z<Config>;\n constructor(ctx: Context, config: Config);\n section(section: PromptSection): () => void;\n getSectionOrder(name: PromptSectionOrderName): number;\n getContextOrder(name: PromptContextOrderName): number;\n context(context: PromptContext): () => void;\n suppressRuntimeContext(): () => void;\n tools(provider: (context: AssembleContext) => ToolProviderResult): () => void;\n variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void;\n refreshContext(assembly: PromptAssembly, context: AssembleContext = {}): PromptAssembly;\n async assemble(context: AssembleContext = {}): Promise<PromptAssembly>;\n}',
|
||||
declaration: 'export class SystemPrompt extends Service {\n static Config: z<Config>;\n constructor(ctx: Context, config: Config);\n section(section: PromptSection): () => void;\n getSectionOrder(name: PromptSectionOrderName): number;\n getContextOrder(name: PromptContextOrderName): number;\n context(context: PromptContext): () => void;\n suppressRuntimeContext(): () => void;\n tools(provider: (context: AssembleContext) => ToolProviderResult): () => void;\n variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void;\n refreshContext<T extends PromptPreparation>(assembly: T, context: AssembleContext = {}): T;\n async prepare(context: AssembleContext = {}): Promise<PromptPreparation>;\n async assemble(context: AssembleContext = {}, preparation?: PromptPreparation): Promise<PromptAssembly>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SystemPromptMessageSource',
|
||||
@@ -7893,6 +7925,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'TokenUsage',
|
||||
declaration: 'export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n totalTokens?: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ToolActivationContext',
|
||||
declaration: 'export interface ToolActivationContext {\n readonly agent: Agent;\n readonly signal: AbortSignal;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ToolAdditionBlock',
|
||||
declaration: 'export interface ToolAdditionBlock {\n type: \'tool-addition\';\n toolName: string;\n tool?: never;\n}',
|
||||
@@ -7943,7 +7979,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'ToolExecutionInput',
|
||||
declaration: 'export interface ToolExecutionInput {\n readonly callId: ToolCallId;\n readonly rootCallId?: ToolCallId;\n readonly name: string;\n readonly schema?: ToolSchema;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n}',
|
||||
declaration: 'export interface ToolExecutionInput {\n readonly callId: ToolCallId;\n readonly rootCallId?: ToolCallId;\n readonly name: string;\n readonly schema?: ToolSchema;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly toolSet?: ResolvedToolSet;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ToolExecutionMode',
|
||||
@@ -7993,6 +8029,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ToolProviderResult',
|
||||
declaration: 'export interface ToolProviderResult {\n readonly schemas: readonly ToolSchema[];\n readonly knownNames?: readonly string[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'ToolRegistrationOptions',
|
||||
declaration: 'export interface ToolRegistrationOptions {\n readonly when?: (context: ToolActivationContext) => boolean | Promise<boolean>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ToolRemovalBlock',
|
||||
declaration: 'export interface ToolRemovalBlock {\n type: \'tool-removal\';\n toolName: string;\n}',
|
||||
@@ -8019,7 +8059,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'ToolRuntime',
|
||||
declaration: 'export class ToolRuntime extends Service {\n static inject;\n static Config: z<Config>;\n readonly [TOOL_RUNTIME_SCHEDULER]: ToolRuntimeScheduler;\n constructor(ctx: Context, config: Config = {});\n presentAs(mode: ToolPresentationMode): () => void;\n register(definition: ToolDefinition): () => void;\n restrict(filter: ToolRestriction): () => void;\n guard(guard: ToolGuard): () => void;\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined;\n schemas(scope?: ScopeKey): ToolSchema[];\n executionMode(exec: ToolExecutionInput): ToolExecutionMode;\n async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>;\n}',
|
||||
declaration: 'export class ToolRuntime extends Service {\n static inject;\n static Config: z<Config>;\n readonly [TOOL_RUNTIME_SCHEDULER]: ToolRuntimeScheduler;\n constructor(ctx: Context, config: Config = {});\n presentAs(mode: ToolPresentationMode): () => void;\n register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void;\n restrict(filter: ToolRestriction): () => void;\n guard(guard: ToolGuard): () => void;\n get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined;\n async resolve(context: ToolActivationContext): Promise<ResolvedToolSet>;\n schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[];\n executionMode(exec: ToolExecutionInput): ToolExecutionMode;\n async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ToolRuntimeScheduler',
|
||||
|
||||
@@ -33,6 +33,7 @@ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session }
|
||||
const session = ctx.sessions.create(SessionId(id))
|
||||
let status: AgentStatus = 'idle'
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -32,6 +32,7 @@ async function agent(ctx: Context): Promise<Agent> {
|
||||
const session = ctx.sessions.create(id)
|
||||
let status: AgentStatus = 'idle'
|
||||
const value: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -34,6 +34,7 @@ async function agent(ctx: Context, cwd: string): Promise<Agent> {
|
||||
version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd, isSeeded: false,
|
||||
})
|
||||
const value: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -25,6 +25,7 @@ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session }
|
||||
const inbox = createInboxStub()
|
||||
let status: AgentStatus = 'idle'
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -54,6 +54,7 @@ function stubAgentForSession(session: Session, suppliedCtx?: Context): StubAgent
|
||||
}
|
||||
const inbox = createInboxStub()
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -39,6 +39,7 @@ interface Bench {
|
||||
async function liveAgent(ctx: Context, session: Session): Promise<Agent> {
|
||||
const status: AgentStatus = 'idle'
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -54,6 +54,7 @@ function stubAgent(rawId: string, supplied?: Session, suppliedCtx?: Context): St
|
||||
const inbox = createInboxStub()
|
||||
let status: AgentStatus = 'running'
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -80,6 +80,7 @@ async function setup(config: toolAskUser.Config = { mode: 'timed', timeout: -1 }
|
||||
function stubAgent(id: string, delegationDepth = 0): Agent {
|
||||
const agentId = SessionId(id)
|
||||
const agent: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: agentId,
|
||||
options: {},
|
||||
session: Session.create(agentId, [], {
|
||||
|
||||
@@ -19,6 +19,7 @@ function agentIn(ctx: Context, rawId: string, presetScope?: ScopeKey): Agent {
|
||||
}
|
||||
const session = Session.create(id)
|
||||
return {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -35,6 +35,7 @@ function stubAgent(ctx: Context, rawId: string, presetScope?: ScopeKey): Agent {
|
||||
}
|
||||
const session = Session.create(id)
|
||||
const agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -39,14 +39,14 @@
|
||||
en: 0acc88b042a99380
|
||||
zh: e4cf66b77f2fc7ae
|
||||
/deepseek-ai-dsh-plan-mode/understand-the-implementation/durable-state-and-step-boundary-appends:
|
||||
en: ff946ab59a7b4a0f
|
||||
zh: b1c79937770f57ad
|
||||
en: 6d0adadc354f211e
|
||||
zh: 86fa49a9f4377368
|
||||
/deepseek-ai-dsh-plan-mode/understand-the-implementation/the-plan-command:
|
||||
en: a7bf35c2a9da52bc
|
||||
zh: dea9f4b3a41f1a6d
|
||||
/deepseek-ai-dsh-plan-mode/understand-the-implementation/the-exit-tool:
|
||||
en: 56798d0f45fb5775
|
||||
zh: 282e4a522ce6dae0
|
||||
en: 86b26413acb2e835
|
||||
zh: a84d876efefa5bea
|
||||
/deepseek-ai-dsh-plan-mode/understand-the-implementation/session-projection-unit:
|
||||
en: 74cd6bdf50af51f5
|
||||
zh: 67609cf7202ececb
|
||||
@@ -90,14 +90,14 @@
|
||||
en: 9622e96bdc01276d
|
||||
zh: 53801e68a49f3d1d
|
||||
/deepseek-ai-dsh-plan-mode/model-experience/exit-tool-schema-and-review-exchange/what-the-model-sees:
|
||||
en: 3768d0189f9f13af
|
||||
zh: 2168a9b24551b7c6
|
||||
en: 76ba584f0225cc7f
|
||||
zh: f8b010dbc909e4ac
|
||||
/deepseek-ai-dsh-plan-mode/model-experience/exit-tool-schema-and-review-exchange/token-effect:
|
||||
en: d1b1418f20ee119d
|
||||
zh: c2b89ea3cae57175
|
||||
en: a1a020eb7ba26e7c
|
||||
zh: a99208693d331a31
|
||||
/deepseek-ai-dsh-plan-mode/model-experience/exit-tool-schema-and-review-exchange/kv-cache-effect:
|
||||
en: 6b1404ea11f1e60d
|
||||
zh: 348d0579de218652
|
||||
en: f640fc33aa30602f
|
||||
zh: e0925a15d01141e9
|
||||
/deepseek-ai-dsh-plan-mode/known-limitations-and-deferred-work:
|
||||
en: a87aabfede0087fd
|
||||
zh: 201e9b3766980005
|
||||
|
||||
@@ -82,7 +82,7 @@ Plan mode is a product package, not a capability seam: there is no swappable bac
|
||||
|
||||
### Durable state and step-boundary appends
|
||||
|
||||
The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next accepted in-turn pre-step — the only append point while an agent runs — and an append failure cannot block the turn. The `set`/`get` service methods and their exact return states live in [`src/index.ts`](src/index.ts) and read the registered `plan` projection; the first dependent access fails explicitly when the registry or key is absent.
|
||||
The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next accepted in-turn pre-step — the only append point while an agent runs — and an append failure stops step admission while retaining the pending selection. The `set`/`get` service methods and their exact return states live in [`src/index.ts`](src/index.ts) and read the registered `plan` projection; the first dependent access fails explicitly when the registry or key is absent.
|
||||
|
||||
### The `/plan` command
|
||||
|
||||
@@ -90,7 +90,7 @@ The command child activates only when a commands service is composed. It maps ba
|
||||
|
||||
### The exit tool
|
||||
|
||||
`exit_plan_mode` stays registered while plan mode is inactive, so entering or leaving changes only the prompt section, never the request tool catalog. An approved review records a silent pending exit that the next accepted in-turn pre-step appends, keeping plan guidance for the rest of the current tool batch. Without a user-questions channel, or after a service reload while the review is pending, the call fails closed and `/plan off` remains the manual escape.
|
||||
`exit_plan_mode` is conditionally admitted only while the committed plan state is active. Entering or leaving changes the tool declarations and plan guidance together; Native and PTC execution use the same admitted set. An approved review records a silent pending exit that the next accepted in-turn pre-step appends, keeping plan guidance for the rest of the current tool batch. Without a user-questions channel, or after a service reload while the review is pending, the call fails closed and `/plan off` remains the manual escape.
|
||||
|
||||
### Session projection unit
|
||||
|
||||
@@ -164,15 +164,15 @@ The user block is append-only conversation growth. Entering or leaving plan mode
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The [`exit_plan_mode` schema](../../../docs/tool-catalog.md#deepseek-aidsh-plan-mode) remains available in both states; execution outside plan mode fails, while an approved in-mode review returns the canonical `{ approved: true }` value and renders the existing confirmation text. Rejection remains a failed call carrying review feedback, and a dismissed review a failed call naming the user's takeover.
|
||||
The [`exit_plan_mode` schema](../../../docs/tool-catalog.md#deepseek-aidsh-plan-mode) is offered only in plan mode; calls outside its admitted set return `UNKNOWN_TOOL`, while an approved in-mode review returns the canonical `{ approved: true }` value and renders the existing confirmation text. Rejection remains a failed call carrying review feedback, and a dismissed review a failed call naming the user's takeover.
|
||||
|
||||
#### Token effect
|
||||
|
||||
The stable schema is paid according to ToolRuntime mode, and each plan argument and review result remains in conversation history.
|
||||
The schema is paid only while active, according to ToolRuntime mode, and each plan argument and review result remains in conversation history.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Mode transitions do not change the tool catalog; plan arguments and review results extend the conversation normally.
|
||||
Mode transitions add or remove the exit tool through the route’s tool-update protocol; plan arguments and review results extend the conversation normally.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并
|
||||
|
||||
### 持久状态与步骤边界追加
|
||||
|
||||
本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有轮次开启时,模式变更会立即追加;轮次开启期间,它保持待生效,直到下一个被接受的轮内 pre-step——agent 运行时唯一的追加点——且追加失败不能阻塞轮次。`set`/`get` 服务方法及其确切返回状态见 [`src/index.ts`](src/index.ts),并读取已注册的 `plan` 投影;注册表或 key 缺失时,第一次依赖它们的访问会显式失败。
|
||||
本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有打开的轮次时立即记录模式变化;轮次内则等待下一个被接纳的 pre-step。写入失败会停止步骤接纳并保留待提交选择。`set`/`get` 的精确返回状态见 [`src/index.ts`](src/index.ts);它们读取已注册的 `plan` 投影,缺少注册表或投影键时在首次依赖访问处明确失败。
|
||||
|
||||
### `/plan` 命令
|
||||
|
||||
@@ -90,7 +90,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并
|
||||
|
||||
### 退出工具
|
||||
|
||||
`exit_plan_mode` 在计划模式未激活时仍保持注册,因此进入或离开只改变提示词段落,绝不改变请求的工具目录。经批准的评审会记录一个静默的待生效退出,由下一个被接受的轮内 pre-step 追加,当前这批工具调用剩余部分仍保留计划引导。缺少用户交互通道,或评审等待期间服务重载,调用都会以拒绝方式失败,`/plan off` 仍是手动退路。
|
||||
`exit_plan_mode` 只在已提交的计划状态为 active 时准入。进入或离开会同时改变工具声明与计划指导;原生和 PTC 执行使用同一准入集合。批准评审后记录静默的待退出选择,由下一个被接纳的轮次内 pre-step 提交,当前工具批次的剩余调用保留计划指导。没有用户问答通道,或评审期间服务重载时,调用失败;`/plan off` 仍可手动退出。
|
||||
|
||||
### 会话投影单元
|
||||
|
||||
@@ -164,15 +164,15 @@ You are in plan mode. Explore and design before presenting the complete plan thr
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
[`exit_plan_mode` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-plan-mode) 在两种状态下均可用;在计划模式之外执行会失败,而计划模式内经批准的评审返回规范的 `{ approved: true }` 值,并渲染既有的确认文本。拒绝仍是携带评审反馈的失败调用,放弃评审则是一次指明用户接手的失败调用。
|
||||
[`exit_plan_mode` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-plan-mode) 只在计划模式中提供;不在准入集合中的调用返回 `UNKNOWN_TOOL`。模式内获批的评审返回规范值 `{ approved: true }` 并渲染现有确认文本。拒绝仍是携带评审反馈的失败调用;关闭评审则报告用户接管对话。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
稳定 schema 的成本取决于 ToolRuntime 模式,每次传入的 plan 参数与评审结果都会保留在对话历史中。
|
||||
schema 仅在启用时按 ToolRuntime 模式计入开销;每份计划参数与评审结果保留在对话历史中。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
模式转换不改变工具目录;plan 参数与评审结果按常规方式扩展对话。
|
||||
模式转换通过路由的工具更新协议新增或移除退出工具;计划参数与评审结果正常扩展对话。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
|
||||
@@ -8,13 +8,12 @@
|
||||
*
|
||||
* The `plan` projection folds the session log, so resume and fork restore the
|
||||
* state. User selections remain pending until the next accepted in-turn
|
||||
* pre-step. The service includes the selected state in the proposed step
|
||||
* assembly, then appends `plan/mode` from `agent/pre-step` only when the step
|
||||
* is accepted. Same-step request retries reuse their assembly.
|
||||
* pre-step. The service appends `plan/mode` from `agent/pre-step` before
|
||||
* request-time tool conditions and plan guidance read that committed state.
|
||||
* Same-step retries do not commit pending selections.
|
||||
*
|
||||
* The exit tool remains registered while plan mode is inactive, so entering
|
||||
* or leaving plan mode changes only the prompt section, not the request tool
|
||||
* catalog.
|
||||
* The exit tool is admitted only for requests whose committed plan state is
|
||||
* active. Native declarations, PTC bindings, and execution share that selection.
|
||||
*
|
||||
* See packages/plan/plan-mode/README.md.
|
||||
*
|
||||
@@ -60,8 +59,7 @@ declare module '@deepseek-ai/cordis' {
|
||||
}
|
||||
|
||||
/**
|
||||
* The model-facing exit tool's name. It stays registered while plan mode is
|
||||
* inactive so the request tool catalog is stable across transitions.
|
||||
* The model-facing exit tool's name, admitted only while plan mode is active.
|
||||
*/
|
||||
export const EXIT_PLAN_MODE = 'exit_plan_mode'
|
||||
|
||||
@@ -169,7 +167,7 @@ export const planProjectionDefinition = {
|
||||
|
||||
/**
|
||||
* `ctx.planMode`: owns logged plan state, applies and narrates selected state at step start,
|
||||
* the `plan:policy` section, the `/plan` command, and the stable exit tool.
|
||||
* the `plan:policy` section, the `/plan` command, and the conditional exit tool.
|
||||
* Client carriers expose the projection's cropped `{ active, pending }` view.
|
||||
*/
|
||||
export class PlanModeController extends Service {
|
||||
@@ -191,8 +189,8 @@ export class PlanModeController extends Service {
|
||||
let disposed = false
|
||||
// Pre-step is outside Session.append publication, so it can append the
|
||||
// log-only mode event inside an open turn without re-entering the session.
|
||||
// A failed append remains pending for a later accepted in-turn pre-step,
|
||||
// and policy cannot block the step.
|
||||
// A failed append remains pending and stops admission before guidance or
|
||||
// tool availability could describe an uncommitted state.
|
||||
ctx.on('agent/pre-step', async (
|
||||
{ agent, signal },
|
||||
next,
|
||||
@@ -201,12 +199,7 @@ export class PlanModeController extends Service {
|
||||
const pending = this.pendingIntents.get(agent.session)
|
||||
if (decision.kind === 'reject' || signal.aborted || pending === undefined) return decision
|
||||
const narration = this.narration(agent.session, pending.active)
|
||||
try {
|
||||
this.onBoundary(agent.session)
|
||||
} catch (error) {
|
||||
ctx.logger.warn('dsh-plan-mode: failed to append selected plan mode at step start: %o', error)
|
||||
return decision
|
||||
}
|
||||
return !pending.narrate || narration === undefined
|
||||
? decision
|
||||
: { ...decision, messages: [...decision.messages, narration] }
|
||||
@@ -218,8 +211,7 @@ export class PlanModeController extends Service {
|
||||
order: ctx.systemPrompt.getSectionOrder('PLAN_POLICY'),
|
||||
text: (context) => {
|
||||
if (context.agent === undefined) return ''
|
||||
const pending = this.pendingIntents.get(context.agent.session)
|
||||
return (pending?.active ?? this.loggedActive(context.agent.session)) ? this.section : ''
|
||||
return this.loggedActive(context.agent.session) ? this.section : ''
|
||||
},
|
||||
})
|
||||
|
||||
@@ -347,7 +339,7 @@ export class PlanModeController extends Service {
|
||||
}
|
||||
// Keep plan guidance for the rest of this assistant tool batch. The
|
||||
// silent selection is appended at the next accepted in-turn pre-step,
|
||||
// before its request assembly.
|
||||
// before its tool and prompt assembly.
|
||||
this.pendingIntents.set(agent.session, { active: false, narrate: false })
|
||||
return { approved: true }
|
||||
},
|
||||
@@ -362,7 +354,7 @@ export class PlanModeController extends Service {
|
||||
title: 'Plan review',
|
||||
content: result.content,
|
||||
}),
|
||||
}))
|
||||
}), { when: ({ agent }) => this.loggedActive(agent.session) })
|
||||
}
|
||||
|
||||
private loggedActive(session: Session): boolean {
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('plan mode through the agent loop', () => {
|
||||
expect(log.some(event => event.type === 'user/message' && event.data.source.kind !== 'user')).toBe(false)
|
||||
})
|
||||
|
||||
it('a user flip between turns lands at the boundary: one notice and a replaced system node with stable tool schemas', async () => {
|
||||
it('a user flip between turns lands at the boundary: one notice and an added exit tool', async () => {
|
||||
const adapter = new MockAdapter([
|
||||
textResponse('First turn, default mode.'),
|
||||
textResponse('Second turn, plan mode.'),
|
||||
@@ -139,7 +139,7 @@ describe('plan mode through the agent loop', () => {
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(planActive(ctx, agent)).toBe(false)
|
||||
const first = findEvent(agent.session.snapshotEvents(), 'request/header')
|
||||
expect(first.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])
|
||||
expect(first.data.header.tools?.map(tool => tool.name)).toEqual(['read', 'write'])
|
||||
const firstSystem = findEvent(agent.session.snapshotEvents(), 'system/message')
|
||||
expect(systemText(agent)).not.toContain(PLAN_CONFIG.section)
|
||||
|
||||
@@ -163,10 +163,10 @@ describe('plan mode through the agent loop', () => {
|
||||
expect(second?.sourceEventSeqs).toEqual([firstSystem.seq])
|
||||
expect(agent.session.surface.nodes[0]).toBe(second?.seq)
|
||||
expect(systemText(agent)).toContain('plan mode')
|
||||
expect(log.filter(event => event.type === 'request/header').map(event => event.data.reason)).toEqual(['initial', 'series'])
|
||||
expect(log.filter(event => event.type === 'request/header').map(event => event.data.reason)).toEqual(['initial', 'change'])
|
||||
const secondHeader = findEvent(log, 'request/header', 'last')
|
||||
expect(secondHeader.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])
|
||||
expect(secondHeader.data.header.tools).toEqual(first.data.header.tools)
|
||||
expect(secondHeader.data.header.tools).not.toEqual(first.data.header.tools)
|
||||
})
|
||||
|
||||
it('a mode flip at error settlement waits until the step after a same-step retry', async () => {
|
||||
@@ -204,7 +204,7 @@ describe('plan mode through the agent loop', () => {
|
||||
|
||||
expect(adapter.requests).toHaveLength(3)
|
||||
expect(requestSystem(adapter.requests[2])).toContain(PLAN_CONFIG.section)
|
||||
expect(adapter.requests[2]?.tools).toEqual(adapter.requests[0]?.tools)
|
||||
expect(adapter.requests[2]?.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])
|
||||
const log = agent.session.snapshotEvents()
|
||||
const planMode = findEvent(log, 'plan/mode')
|
||||
const firstEnd = log.find(event => event.type === 'step/end'
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
@@ -72,8 +74,9 @@ async function agentWithSession(
|
||||
return agent
|
||||
}
|
||||
|
||||
function assembleFor(ctx: Context, agent: Agent) {
|
||||
return ctx.systemPrompt.assemble({ agent, scope: agent })
|
||||
async function assembleFor(ctx: Context, agent: Agent) {
|
||||
const toolSet = await ctx.tools.resolve({ agent, signal: testToolSignal })
|
||||
return ctx.systemPrompt.assemble({ agent, scope: agent, toolSet })
|
||||
}
|
||||
|
||||
function foldPlanMode(events: readonly SessionEvent[], end = events.length): boolean {
|
||||
@@ -159,7 +162,7 @@ function registerNamedTools(ctx: Context, names: string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Assert the mapped PTC mode SDK includes the stable plan exit binding and test tools. */
|
||||
/** Assert the mapped PTC mode SDK includes the active plan exit binding and test tools. */
|
||||
function expectPlanPtcSdkBindings(sdk: string): void {
|
||||
expect(sdk).toContain('interface ToolArgsMap {')
|
||||
expect(sdk).toContain('read: Record<string, JsonValue>;')
|
||||
@@ -409,7 +412,7 @@ describe('the boundary flush', () => {
|
||||
})
|
||||
|
||||
|
||||
it('contains an append failure instead of blocking the prompt or the turn', async () => {
|
||||
it('stops admission on append failure and retries the pending selection', async () => {
|
||||
const ctx = await setup()
|
||||
const warn = vi.fn()
|
||||
ctx.logger.warn = warn as never
|
||||
@@ -423,8 +426,8 @@ describe('the boundary flush', () => {
|
||||
if (type === 'plan/mode') throw new Error('backend gone')
|
||||
return (original as (...args: unknown[]) => unknown)(type, ...rest)
|
||||
}) as unknown) as typeof agent.session.append
|
||||
await boundary(ctx, agent, 'step-start')
|
||||
expect(warn).toHaveBeenCalledOnce()
|
||||
await expect(boundary(ctx, agent, 'step-start')).rejects.toThrow('backend gone')
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
// The failed flush re-parks the intent (cleared only after a landed
|
||||
// append), so the next healthy boundary converges the log with the
|
||||
// picker's optimistic state instead of dropping the switch forever.
|
||||
@@ -435,7 +438,7 @@ describe('the boundary flush', () => {
|
||||
expect(ctx.planMode.get(agent).pending).toBeUndefined()
|
||||
})
|
||||
|
||||
it('contains a pre-step append failure and keeps the intent pending', async () => {
|
||||
it('rejects a pre-step append failure and keeps the intent pending', async () => {
|
||||
const ctx = await setup()
|
||||
const warn = vi.fn()
|
||||
ctx.logger.warn = warn as never
|
||||
@@ -447,24 +450,24 @@ describe('the boundary flush', () => {
|
||||
if (type === 'plan/mode') throw new Error('backend gone')
|
||||
return (original as (...args: unknown[]) => unknown)(type, ...rest)
|
||||
}) as unknown) as typeof agent.session.append
|
||||
await boundary(ctx, agent, 'pre-step')
|
||||
expect(warn).toHaveBeenCalledOnce()
|
||||
await expect(boundary(ctx, agent, 'pre-step')).rejects.toThrow('backend gone')
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
expect(ctx.planMode.get(agent)).toEqual({ active: false, pending: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe('the soft layer', () => {
|
||||
it('keeps the tool schemas identical across default and plan mode', async () => {
|
||||
it('adds the exit schema together with plan guidance', async () => {
|
||||
const ctx = await setup()
|
||||
registerNamedTools(ctx, ['read', 'write'])
|
||||
const agent = await agentWithSession(ctx)
|
||||
const defaultAssembly = await assembleFor(ctx, agent)
|
||||
expect(defaultAssembly.tools.map(tool => tool.name)).toEqual([EXIT_PLAN_MODE, 'read', 'write'])
|
||||
expect(defaultAssembly.tools.map(tool => tool.name)).toEqual(['read', 'write'])
|
||||
expect(defaultAssembly.sections.find(section => section.name === 'plan:policy')?.text).toBe('')
|
||||
|
||||
agent.session.append('plan/mode', { active: true })
|
||||
const planAssembly = await assembleFor(ctx, agent)
|
||||
expect(planAssembly.tools).toEqual(defaultAssembly.tools)
|
||||
expect(planAssembly.tools.map(tool => tool.name)).toEqual([EXIT_PLAN_MODE, 'read', 'write'])
|
||||
expect(planAssembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION)
|
||||
})
|
||||
|
||||
@@ -485,25 +488,15 @@ describe('the soft layer', () => {
|
||||
expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION)
|
||||
})
|
||||
|
||||
it('leaves foreign assemble additions alone (no assemble-layer filtering)', async () => {
|
||||
// Plan guidance does not filter the registry or later assembly additions.
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
await mountProjectionSeam(ctx)
|
||||
ctx.on('system-prompt/assemble', async (_assembly, _context, next) => {
|
||||
const final = await next()
|
||||
final.tools = [...final.tools, { name: 'added-later', description: 'added after next()', parameters: {} }]
|
||||
return final
|
||||
})
|
||||
await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
registerNamedTools(ctx, ['read'])
|
||||
it('keeps independently registered tools available across plan changes', async () => {
|
||||
const ctx = await setup()
|
||||
registerNamedTools(ctx, ['read', 'added-later'])
|
||||
const planning = await agentWithSession(ctx, 'planning', { active: true })
|
||||
expect((await assembleFor(ctx, planning)).tools.map(tool => tool.name))
|
||||
.toEqual(['exit_plan_mode', 'read', 'added-later'])
|
||||
.toEqual(['added-later', 'exit_plan_mode', 'read'])
|
||||
const defaulted = await agentWithSession(ctx, 'defaulted')
|
||||
expect((await assembleFor(ctx, defaulted)).tools.map(tool => tool.name))
|
||||
.toEqual(['exit_plan_mode', 'read', 'added-later'])
|
||||
.toEqual(['added-later', 'read'])
|
||||
})
|
||||
|
||||
it('keeps run_code the only wire tool in plan mode under the registry PTC mode; the SDK gains the exit binding', async () => {
|
||||
@@ -532,7 +525,7 @@ describe('the soft layer', () => {
|
||||
expectPlanPtcSdkBindings(sdk)
|
||||
})
|
||||
|
||||
it('keeps the PTC mode SDK byte-identical across mode switches', async () => {
|
||||
it('includes the PTC exit binding only while plan mode is active', async () => {
|
||||
class FakeRuntime extends PtcRuntime {
|
||||
resolve(request: import('@deepseek-ai/dsh-ptc-runtime').PtcRunRequest): import('@deepseek-ai/dsh-ptc-runtime').PtcRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
|
||||
|
||||
@@ -549,13 +542,11 @@ describe('the soft layer', () => {
|
||||
registerNamedTools(withPlanMode, ['read', 'write'])
|
||||
const agent = await agentWithSession(withPlanMode)
|
||||
const defaultSdk = (await assembleFor(withPlanMode, agent)).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
||||
expectPlanPtcSdkBindings(defaultSdk)
|
||||
expect(defaultSdk).not.toContain('exit_plan_mode:')
|
||||
agent.session.append('plan/mode', { active: true })
|
||||
const planSdk = (await assembleFor(withPlanMode, agent)).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
||||
expect(planSdk).toBe(defaultSdk)
|
||||
expectPlanPtcSdkBindings(planSdk)
|
||||
|
||||
// Loading the plan-mode plugin deliberately adds one stable binding compared
|
||||
// with a deployment that does not compose plan mode at all.
|
||||
const bare = new Context()
|
||||
await bare.plugin(SystemPrompt)
|
||||
await bare.plugin(ToolRuntime, { mode: 'ptc' })
|
||||
@@ -563,7 +554,7 @@ describe('the soft layer', () => {
|
||||
registerNamedTools(bare, ['read', 'write'])
|
||||
const bareSdk = (await bare.systemPrompt.assemble({ agent })).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
||||
expect(bareSdk).not.toContain('exit_plan_mode:')
|
||||
expect(defaultSdk).not.toBe(bareSdk)
|
||||
expect(defaultSdk).toBe(bareSdk)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -804,8 +795,11 @@ describe('exit_plan_mode', () => {
|
||||
return { ctx, agent, asked }
|
||||
}
|
||||
|
||||
function callExit(ctx: Context, agent: Agent | undefined, plan = '# The plan\n\ndo things') {
|
||||
async function callExit(ctx: Context, agent: Agent | undefined, plan = '# The plan\n\ndo things') {
|
||||
const signal = new AbortController().signal
|
||||
const toolSet = agent === undefined ? undefined : await ctx.tools.resolve({ agent, signal })
|
||||
return ctx.tools.execute({
|
||||
...toolSet === undefined ? {} : { toolSet },
|
||||
callId: ToolCallId(`call-exit-${++callCounter}`),
|
||||
name: EXIT_PLAN_MODE,
|
||||
arguments: { plan },
|
||||
@@ -827,16 +821,16 @@ describe('exit_plan_mode', () => {
|
||||
const ctx = await setup()
|
||||
const result = await callExit(ctx, undefined)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: exit_plan_mode requires a calling agent (no session to switch)' }])
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: unknown tool "exit_plan_mode"' }])
|
||||
})
|
||||
|
||||
it('rejects a call outside plan mode while remaining advertised', async () => {
|
||||
it('omits and rejects the exit tool outside plan mode', async () => {
|
||||
const ctx = await setup()
|
||||
const agent = await agentWithSession(ctx)
|
||||
expect(ctx.tools.schemas().map(tool => tool.name)).toContain(EXIT_PLAN_MODE)
|
||||
expect((await assembleFor(ctx, agent)).tools.map(tool => tool.name)).not.toContain(EXIT_PLAN_MODE)
|
||||
const result = await callExit(ctx, agent)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: exit_plan_mode is only available in plan mode' }])
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: unknown tool "exit_plan_mode"' }])
|
||||
})
|
||||
|
||||
it('rejects an empty or heading-less plan before asking the reviewer', async () => {
|
||||
@@ -938,6 +932,7 @@ describe('exit_plan_mode', () => {
|
||||
const agent = await agentWithSession(ctx, 'ptc-exit', { active: true })
|
||||
|
||||
const result = await ctx.tools.execute({
|
||||
toolSet: await ctx.tools.resolve({ agent, signal: testToolSignal }),
|
||||
callId: ToolCallId(`call-exit-${++callCounter}`),
|
||||
name: RUN_CODE_NAME,
|
||||
arguments: { code: `return await tools.${EXIT_PLAN_MODE}({ plan: ${JSON.stringify(plan)} })`, description: 'Submit the plan for review' },
|
||||
@@ -960,21 +955,19 @@ describe('exit_plan_mode', () => {
|
||||
expect(ctx.planMode.get(agent)).toEqual({ active: true, pending: false })
|
||||
})
|
||||
|
||||
it('an approved exit projects the next assembly before the boundary and never removes the tool', async () => {
|
||||
it('removes the exit tool and plan guidance together after the accepted boundary', async () => {
|
||||
const { ctx, agent } = await setupWithReview({ selected: ['Approve'] })
|
||||
const approved = await callExit(ctx, agent)
|
||||
expect(approved.isError).toBe(false)
|
||||
// Calls of the SAME assistant response were requested under the existing
|
||||
// plan-shaped header. Pending state shapes only the proposed next
|
||||
// assembly; the accepted boundary then commits the matching durable fold.
|
||||
// Calls from the same assistant response retain the committed plan state.
|
||||
expect(foldPlanMode(agent.session.snapshotEvents())).toBe(true)
|
||||
const assembly = await ctx.systemPrompt.assemble({ agent })
|
||||
const assembly = await assembleFor(ctx, agent)
|
||||
expect(assembly.tools.some(tool => tool.name === EXIT_PLAN_MODE)).toBe(true)
|
||||
expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe('')
|
||||
expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION)
|
||||
await boundary(ctx, agent, 'step-start')
|
||||
expect(foldPlanMode(agent.session.snapshotEvents())).toBe(false)
|
||||
const afterExit = await ctx.systemPrompt.assemble({ agent })
|
||||
expect(afterExit.tools).toEqual(assembly.tools)
|
||||
const afterExit = await assembleFor(ctx, agent)
|
||||
expect(afterExit.tools.some(tool => tool.name === EXIT_PLAN_MODE)).toBe(false)
|
||||
expect(afterExit.sections.find(section => section.name === 'plan:policy')?.text).toBe('')
|
||||
})
|
||||
|
||||
@@ -1088,6 +1081,7 @@ describe('exit_plan_mode', () => {
|
||||
const { ctx, agent, asked } = await setupWithReview({ selected: ['Approve'] })
|
||||
const controller = new AbortController()
|
||||
const result = await ctx.tools.execute({
|
||||
toolSet: await ctx.tools.resolve({ agent, signal: testToolSignal }),
|
||||
callId: ToolCallId(`call-exit-${++callCounter}`),
|
||||
name: EXIT_PLAN_MODE,
|
||||
arguments: { plan: '# P' },
|
||||
@@ -1163,7 +1157,7 @@ describe('exit_plan_mode', () => {
|
||||
})
|
||||
|
||||
describe('HMR disposal', () => {
|
||||
it('unregisters the service, listeners, prompt section, and stable exit tool with the plugin fiber', async () => {
|
||||
it('unregisters the service, listeners, prompt section, and exit tool with the plugin fiber', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
|
||||
@@ -125,6 +125,7 @@ export interface HarnessAgent extends Agent {
|
||||
export function agentFor(ctx: Context, id = 'original', meta?: CreateSessionOptions['meta']): HarnessAgent {
|
||||
const session = ctx.sessions.create(SessionId(id), meta === undefined ? undefined : { meta })
|
||||
return {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: session.id, session, ctx: ctx.extend(), options: {}, status: 'idle', inbox: unsupportedInbox(),
|
||||
send() {}, followup: vi.fn<(message: UserMessage) => void>(), steer() {}, inject() {}, cancel() {},
|
||||
whenIdle: async () => {}, runMaintenance: operation => operation(new AbortController().signal),
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 0cd615b8f987f950
|
||||
zh: ccf4964df789011c
|
||||
/deepseek-ai-dsh-working-directory/understand-the-implementation:
|
||||
en: f0458147d29631b1
|
||||
zh: 6a288670dd084ffb
|
||||
en: 6f7b264aef175243
|
||||
zh: e2798c0aab0eb21b
|
||||
/deepseek-ai-dsh-working-directory/further-exploration:
|
||||
en: eabff683146786c8
|
||||
zh: 7997dec17802ef64
|
||||
@@ -39,8 +39,8 @@
|
||||
en: f5a095acef81df3b
|
||||
zh: 758796edbc4337fc
|
||||
/deepseek-ai-dsh-working-directory/known-limitations-and-deferred-work:
|
||||
en: a0f7bcd972f4a4fb
|
||||
zh: 051ec0eb0fa92bea
|
||||
en: 7f93a8dd08c52ac5
|
||||
zh: 38f35141d2e7eb6d
|
||||
/deepseek-ai-dsh-working-directory/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: a4b126f9a4eaca6f
|
||||
@@ -39,7 +39,7 @@ Mount alongside `fs`, `sessionProjections`, and `systemPrompt`. `defaultDirector
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
The Session projection owns the effective directory and exposes it in Session observations, so cold readers can use it without activating an Agent; the header continues to identify the original project. Changes serialize per Session, validate through the filesystem provider, check cancellation and Agent lifetime, commit an event, and queue a user-context notice. If notice queuing fails after commit, the operation returns the committed directory and logs a warning; the next request or resume still receives the required directory context. Prompt assembly validates the current directory before publishing its required context contribution. Existing processes and sandbox grants keep their independent ownership. The [service implementation](src/index.ts) defines the API.
|
||||
The Session projection owns the effective directory and exposes it in Session observations, so cold readers can use it without activating an Agent; the header continues to identify the original project. Changes serialize per Session, validate through the filesystem provider, check cancellation and Agent lifetime, commit an event, and queue a user-context notice. If notice queuing fails after commit, the operation returns the committed directory and logs a warning; the next request or resume still receives the required directory context. `system-prompt/prepare` validates the current directory before step admission and before publishing its required context contribution. Existing processes and sandbox grants keep their independent ownership. The [service implementation](src/index.ts) defines the API.
|
||||
|
||||
No runtime invariant companion is published because the service reads its only directory state from the Session projection and checks filesystem validity at use.
|
||||
|
||||
@@ -77,7 +77,7 @@ Directory transitions append context without replacing the stable system-prompt
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **Missing original project** — when the current directory and the original project are both unavailable, prompt assembly rejects every model turn, including chat-only turns. The service does not choose or recreate a repository. Restore the original directory, or use SDK `setWorkingDirectory` (`set_working_directory` in Python) or `ctx.workingDirectory.set` to select an existing absolute directory before retrying. Existing shells retain their process-local directories.
|
||||
- **Missing original project** — when the current directory and the original project are both unavailable, request preparation rejects every model turn, including chat-only turns. The service does not choose or recreate a repository. Restore the original directory, or use SDK `setWorkingDirectory` (`set_working_directory` in Python) or `ctx.workingDirectory.set` to select an existing absolute directory before retrying. Existing shells retain their process-local directories.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
@@ -39,7 +39,7 @@ kind: "package-reference"
|
||||
<details>
|
||||
<summary>实现细节 — 点击展开</summary>
|
||||
|
||||
Session 投影拥有当前目录,并通过 Session observation 暴露它,因此冷态读取方无需激活 Agent 即可使用;header 继续标识原始项目。每个 Session 的变更串行执行,通过文件系统 provider 验证,并检查取消状态与 Agent 生命周期,提交事件后排入用户上下文通知。若提交后通知入队失败,操作仍返回已提交的目录并记录警告;下一次请求或恢复仍会收到必需的目录上下文。提示词组装在发布必需的上下文条目前验证当前目录。已有进程与沙箱授权继续由各自的服务拥有。[服务实现](src/index.ts) 定义 API。
|
||||
Session 投影拥有当前目录,并通过 Session observation 暴露它,因此冷态读取方无需激活 Agent 即可使用;header 继续标识原始项目。每个 Session 的变更串行执行,通过文件系统 provider 验证,并检查取消状态与 Agent 生命周期,提交事件后排入用户上下文通知。若提交后通知入队失败,操作仍返回已提交的目录并记录警告;下一次请求或恢复仍会收到必需的目录上下文。`system-prompt/prepare` 在步骤接纳以及发布必需的上下文条目前验证当前目录。已有进程与沙箱授权继续由各自的服务拥有。[服务实现](src/index.ts) 定义 API。
|
||||
|
||||
不发布 `./invariant` 伴生模块,因为服务从 Session 投影读取唯一的目录状态,并在使用时检查文件系统有效性。
|
||||
|
||||
@@ -77,7 +77,7 @@ Session 投影拥有当前目录,并通过 Session observation 暴露它,因
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **原始项目缺失** — 当前目录与原始项目均不可用时,提示词组装会拒绝每个模型轮次,包括纯聊天轮次。服务不会另选或重建仓库。请先恢复原始目录,或通过 SDK 的 `setWorkingDirectory`(Python 中为 `set_working_directory`)或 `ctx.workingDirectory.set` 选择现有的绝对目录,再重试。已有 shell 保留各自进程的目录。
|
||||
- **原始项目缺失** — 当前目录与原始项目均不可用时,请求准备会拒绝每个模型轮次,包括纯聊天轮次。服务不会另选或重建仓库。请先恢复原始目录,或通过 SDK 的 `setWorkingDirectory`(Python 中为 `set_working_directory`)或 `ctx.workingDirectory.set` 选择现有的绝对目录,再重试。已有 shell 保留各自进程的目录。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
@@ -94,7 +94,7 @@ export class WorkingDirectoryService extends Service {
|
||||
interpolate: false,
|
||||
text: context => context.agent === undefined ? '' : this.contextText(context.agent.session),
|
||||
})
|
||||
ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
|
||||
ctx.on('system-prompt/prepare', async (_assembly, context, next) => {
|
||||
if (context.agent === undefined) return next()
|
||||
await this.ensure(context.agent, context.signal)
|
||||
const assembly = await next()
|
||||
|
||||
@@ -45,6 +45,7 @@ async function agent(ctx: Context, cwd: string): Promise<Agent> {
|
||||
version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd, isSeeded: false,
|
||||
})
|
||||
const value: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -39,6 +39,7 @@ async function agent(ctx: Context, cwd: string | undefined): Promise<Agent> {
|
||||
...cwd === undefined ? {} : { cwd },
|
||||
})
|
||||
const value: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -352,6 +352,7 @@ describe('foreground commands as jobs', () => {
|
||||
const ctx = await setup()
|
||||
const ownerId = SessionId('promote-owner')
|
||||
const owner: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: ownerId,
|
||||
options: {},
|
||||
session: Session.create(ownerId, undefined, {
|
||||
@@ -468,6 +469,7 @@ describe('owned background output', () => {
|
||||
const ctx = await setup()
|
||||
const ownerId = SessionId('background-owner')
|
||||
const owner: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id: ownerId,
|
||||
options: {},
|
||||
session: Session.create(ownerId, undefined, {
|
||||
|
||||
@@ -80,6 +80,7 @@ async function agent(ctx: Context, cwd: string): Promise<Agent> {
|
||||
version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd, isSeeded: false,
|
||||
})
|
||||
const value: Agent = {
|
||||
currentRoute() { throw new Error('no prepared model request in this fixture') },
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
Loaded 100 of 192 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user