From be6ed7f0aa8428dcf06ea591cbbd6bfe46f30670 Mon Sep 17 00:00:00 2001 From: Yixiang Tu Date: Thu, 22 Oct 2026 17:37:52 +0800 Subject: [PATCH] fix(llm): rebuild addition-only history on every removal --- .../2026-09-20-dynamic-tool-updates.i18n.yaml | 12 +++---- .../2026-09-20-dynamic-tool-updates.md | 6 ++-- .../2026-09-20-dynamic-tool-updates.zh.md | 6 ++-- packages/llm/llm-deepseek/README.i18n.yaml | 4 +-- packages/llm/llm-deepseek/README.md | 2 +- packages/llm/llm-deepseek/README.zh.md | 2 +- .../tests/fixtures/deferred-tools.ts | 18 ++++++++-- packages/llm/llm/README.i18n.yaml | 4 +-- packages/llm/llm/README.md | 2 +- packages/llm/llm/README.zh.md | 2 +- packages/llm/llm/src/content.ts | 33 ++++--------------- packages/llm/llm/tests/content.spec.ts | 25 +++++++++----- .../deepseek-deferred-tools/session.v4.jsonl | 17 +++++++--- .../deepseek-deferred-tools/snapshot.yml | 2 +- .../tool-schemas.expected.json | 18 ++++++++++ 15 files changed, 90 insertions(+), 63 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.i18n.yaml index 357c16c769..4215b56930 100644 --- a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.i18n.yaml @@ -9,11 +9,11 @@ en: 97fe58a13135b4a1 zh: 7490373a8d941da5 /agent-note-active-tool-headers-and-stateful-request-projection/decision: - en: b6770e6bd8ff17ee - zh: cc409f4e9f9be495 + en: 2bbb6ab17f2cbb1a + zh: bb925627b6798548 /agent-note-active-tool-headers-and-stateful-request-projection/alternatives-considered: - en: 69370c9cb0014985 - zh: ccba65967ab1c34d + en: f1271717cbe0d4a0 + zh: 367ad512ad08b221 /agent-note-active-tool-headers-and-stateful-request-projection/consequences: - en: cabc2b9d378d186e - zh: 7aa21854f43bf8e8 + en: aa3e68c78ac3f666 + zh: 5ec40c83183fc7e0 diff --git a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.md b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.md index 6421cb7e43..2f75132d51 100644 --- a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.md +++ b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.md @@ -14,7 +14,7 @@ Plugin and MCP tools change during conversations. Providers differ in whether th `Session.toolHistory()` caches a capability-independent fold of committed headers and developer messages. First access restores inherited events; later reads consume only unseen events. Immutable snapshots carry initial declarations and ordered update identities with additions resolved through their historical headers. Explicit series starts and changed retained definitions rebuild declarations. Restoring a retained name with an unchanged definition continues the series: the projection keeps the declaration unchanged and lets the recorded addition re-offer the tool after its earlier removal, so a removal never disables a declared tool and a loading mode never rewrites an earlier declaration. -Loop and compaction requests carry this snapshot as `GenerateOptions.toolHistory`. At adapter dispatch, `projectToolUpdates` uses the prepared route's `toolUpdate`. Unsupported routes receive active definitions without `deferLoading` or developer messages. Supported routes defer historical additions; `in-history` retains removed definitions and removal blocks, while `addition-only` omits both. On addition-only routes, removing an immediate baseline tool rebuilds the route's baseline after that update: the initial declarations already change, so retaining superseded activation blocks cannot preserve the full conversation prefix. Previously activated tools become immediate; explicitly deferred declarations with no activation remain deferred. Subsequent additions remain deferred until their recorded activation. Superseded tool blocks are omitted without discarding other content in those messages. The Session fold and durable events remain capability-independent. Missing history or a request prefix omitting recorded updates uses current declarations without developer updates. +Loop and compaction requests carry this snapshot as `GenerateOptions.toolHistory`. At adapter dispatch, `projectToolUpdates` uses the prepared route's `toolUpdate`. Unsupported routes receive active definitions without `deferLoading` or developer messages. Supported routes defer historical additions; `in-history` retains removed definitions and removal blocks, while `addition-only` omits both. On addition-only routes, removing any tool rebuilds the route's baseline after that update: the request declarations change even when the removed tool was activated later, so retaining superseded activation blocks cannot preserve the full conversation prefix. Previously activated tools become immediate; explicitly deferred declarations with no activation remain deferred. Subsequent additions remain deferred until their recorded activation. Superseded tool blocks are omitted without discarding other content in those messages. The Session fold and durable events remain capability-independent. Missing history or a request prefix omitting recorded updates uses current declarations without developer updates. DeepSeek translates projected updates into system-role `tool_addition` and `tool_removal` blocks and `defer_loading` declarations, sending the tool-changes beta header when update blocks occur. The default `deepseek-flash` entry declares `addition-only`; pi-ai declares no mode. Chat and Trajectory display developer messages as context nodes, using localized tool-change notices: single changes show the tool name; multiple changes show counts and expandable name lists. @@ -26,7 +26,7 @@ DeepSeek translates projected updates into system-role `tool_addition` and `tool **Scan the complete log on every request.** The cached fold consumes new events and supplies stable snapshots to ordinary and auxiliary calls without adding arbitrary synchronous history readers. -**Rebuild whenever additions and removals occur together.** Replacing a later-added tool can preserve the prefix before its activation while the immediate baseline stays unchanged. Conversely, a removal-only update can remove the last immediate baseline tool. The reset therefore follows baseline removals, not the presence of both update kinds. A past reset cannot force every future addition to become immediate: each reset establishes a new baseline from which deferred additions resume. +**Rebuild only for immediate baseline removals or mixed additions and removals.** Deferred tools also have request declarations; deleting a tool changes those declarations regardless of its activation position. Addition-only routes cannot express that deletion in history. Every removal therefore rebuilds the baseline, including removal-only updates and removals of later-activated tools. A past reset cannot force every future addition to become immediate: each reset establishes a new baseline from which deferred additions resume. **Reject same-name schema changes during tool registration.** This restriction is not enforced. Immediate failure for a plugin registering an incompatible tool requires comparing the new registration with historical definitions of tools called on each affected current surface, while excluding calls removed by compaction. That registration-time historical and scope-aware check is deferred because of its implementation complexity. Same-name schema changes remain allowed; they rebuild declarations and invalidate prefix reuse even on `in-history` routes. @@ -36,6 +36,6 @@ Tool lists and logical update events are capability-independent; only outgoing d The authored [headless prompt/tool scenario](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml) pins an appended prompt beside a deferred tool addition and verifies that the first request's messages stay unchanged. It shares the stable-prompt scenario's tool lifecycle and schema expectations. -The [DeepSeek replacement scenario](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml) checks the rebuilt baseline and a subsequent deferred addition through Messages serialization. An adapter-local placeholder covers nonempty all-deferred requests that remain, including explicit deferred declarations and direct calls. It does not activate real tools. These checks establish request construction, not measured cache savings from rebuilding. +The [DeepSeek replacement scenario](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml) checks a rebuilt baseline, a subsequent deferred addition, and removal of that later-activated tool through Messages serialization. An adapter-local placeholder covers nonempty all-deferred requests that remain, including explicit deferred declarations and direct calls. It does not activate real tools. These checks establish request construction, not measured cache savings from rebuilding. The [real DeepSeek SDK test](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) measures the first request after a native tool addition with both unchanged and simultaneously updated instructions. Unique user history prevents a shared system-prompt cache hit from satisfying the preceding-input assertion. Removal checks tool availability without requiring cache retention; each case disables retries. diff --git a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.zh.md b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.zh.md index 86350a0318..36df584a80 100644 --- a/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-20-dynamic-tool-updates.zh.md @@ -14,7 +14,7 @@ Status: implemented `Session.toolHistory()` 缓存对已提交请求头和 developer 消息的归约,不依赖模型能力。首次访问恢复继承事件;后续读取仅处理新增事件。不可变快照携带初始声明和有序更新标识,添加定义通过历史请求头解析。显式新序列和保留定义变化会重建声明。以未变化的定义恢复保留名称时序列继续:投影保持声明不变,由记录的添加在先前移除之后重新提供该工具,因此移除不会禁用已声明的工具,加载模式也不会改写先前声明。 -循环和压缩请求通过 `GenerateOptions.toolHistory` 携带此快照。适配器分发时,`projectToolUpdates` 使用已准备路由的 `toolUpdate`。不支持的路由接收有效定义,不携带 `deferLoading` 或 developer 消息。支持的路由延迟历史添加;`in-history` 保留已移除定义和移除块,`addition-only` 则省略两者。在 addition-only 路由移除基线中的立即加载工具时,使用该次更新后的状态重建路由基线:初始声明已发生变化,保留已被取代的激活块无法保留完整对话前缀。此前已激活工具变为立即加载;没有激活记录的显式延迟声明仍保持延迟。后续添加在记录的激活位置前保持延迟加载。已被取代的工具块会被省略,但不会丢弃这些消息中的其他内容。Session 归约和持久事件仍不依赖模型能力。缺少历史或请求前缀遗漏已记录更新时,使用当前声明且不发送 developer 更新。 +循环和压缩请求通过 `GenerateOptions.toolHistory` 携带此快照。适配器分发时,`projectToolUpdates` 使用已准备路由的 `toolUpdate`。不支持的路由接收有效定义,不携带 `deferLoading` 或 developer 消息。支持的路由延迟历史添加;`in-history` 保留已移除定义和移除块,`addition-only` 则省略两者。在 addition-only 路由移除任何工具时,使用该次更新后的状态重建路由基线:即使被移除工具是后来激活的,请求声明也已发生变化,保留已被取代的激活块无法保留完整对话前缀。此前已激活工具变为立即加载;没有激活记录的显式延迟声明仍保持延迟。后续添加在记录的激活位置前保持延迟加载。已被取代的工具块会被省略,但不会丢弃这些消息中的其他内容。Session 归约和持久事件仍不依赖模型能力。缺少历史或请求前缀遗漏已记录更新时,使用当前声明且不发送 developer 更新。 DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `tool_removal` 块以及 `defer_loading` 声明,在存在更新块时发送工具变更 beta 请求头。默认 `deepseek-flash` 条目声明 `addition-only`;pi-ai 不声明模式。Chat 和 Trajectory 将 developer 消息显示为上下文节点,工具变更使用本地化通知:单项变更显示工具名,多项变更显示数量及可展开的名称列表。 @@ -26,7 +26,7 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to **每次请求扫描完整日志。** 缓存归约处理新事件,为普通和辅助调用提供稳定快照,不增加任意同步历史读取接口。 -**同时出现添加和移除就重建。** 替换后来添加的工具时,只要立即加载的基线未变,就仍可保留该工具激活之前的前缀。反之,仅包含移除的更新也可能移除最后一个立即加载的基线工具。因此,重置取决于基线工具移除,而不是两类更新是否同时出现。一次历史重置不能让所有后续添加都变为立即加载:每次重置确立新基线,后续添加继续采用延迟加载。 +**仅在移除立即加载的基线工具或同时添加和移除时重建。** 延迟工具也具有请求声明;删除工具会改变这些声明,与其激活位置无关。addition-only 路由无法通过历史表达删除。因此,每次移除都重建基线,包括仅移除的更新和移除后来激活的工具。一次历史重置不能让所有后续添加都变为立即加载:每次重置确立新基线,后续添加继续采用延迟加载。 **在工具注册时拒绝同名 schema 变化。** 当前不执行此限制。要让注册不兼容工具的插件立即收到异常,需要将新注册与各受影响当前 surface 中已调用工具的历史定义比较,同时排除已被压缩移除的调用。由于实现复杂,注册时结合历史和作用域的检查留待后续实现。同名 schema 变化仍被允许;它们会重建声明,即使在 `in-history` 路由上也会使前缀复用失效。 @@ -36,6 +36,6 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to 手写的[无界面提示词/工具场景](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml)钉住追加提示词与延迟工具添加同时发生的行为,并验证首次请求的消息保持不变。它复用稳定提示词场景的工具生命周期和 schema 期望值。 -[DeepSeek 替换场景](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml)通过 Messages 序列化验证重建后的基线和后续延迟添加。适配器内的占位工具覆盖仍然存在的非空全延迟请求,包括显式延迟声明和直接调用,不会激活真实工具。这些检查验证请求构造,不代表已经测量重建带来的缓存收益。 +[DeepSeek 替换场景](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml)通过 Messages 序列化验证重建后的基线、后续延迟添加,以及移除该后来激活的工具。适配器内的占位工具覆盖仍然存在的非空全延迟请求,包括显式延迟声明和直接调用,不会激活真实工具。这些检查验证请求构造,不代表已经测量重建带来的缓存收益。 [真实 DeepSeek SDK 测试](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) 分别在指令不变和指令同时更新时测量原生工具新增后的首次请求。独有的用户历史确保仅命中共享系统提示词缓存无法满足前序输入断言。移除场景检查工具可用性,不要求保留缓存;每个用例均禁用重试。 diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index eb6b7092a7..dae2900fa7 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -63,8 +63,8 @@ en: 1dc50052999253c6 zh: 7abf968fc18bffcd /deepseek-ai-dsh-llm-deepseek/model-experience/deepseek-request/what-the-model-sees: - en: fda9111504c5af32 - zh: 6306a70fdafbdb1b + en: 1b8d726045c386f9 + zh: 8254d9f748decef3 /deepseek-ai-dsh-llm-deepseek/model-experience/deepseek-request/token-effect: en: 67f4bfa42cbee911 zh: 5e9dc9bc07253686 diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 6c3717c4d7..ecce4d514b 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -170,7 +170,7 @@ Read these pages when the package-level contract is not enough. They move from t #### What the model sees -The selected DeepSeek model receives the harness system prompt, message history, tool schemas, stop sequences, and call config (`maxTokens`, `reasoningEffort`, `temperature`) without adapter-authored prompt prose. Provider-specific request-extension fields remain outside model input. The vision model normally receives retained user and tool-result images as Files API references beside attachment handles and request-preview dimensions. It also receives a normalized-object path when the current execution filesystem maps the attachment provider's host object; the descriptor marks this copy read-only and warns that normalization may have resized or re-encoded the upload. A Files resolution failure sends all retained images as inline base64 instead, and an over-budget older image keeps the access resolved for that request in its placeholder. Reasoning content from a prior assistant turn is passed back verbatim, whether or not that turn called a tool. Messages sends `{}` for historical tool arguments that are malformed JSON or are not objects. This argument fallback preserves call ids, tool names, and tool results; the original arguments stay in the Session log. Newly generated Messages tool arguments still require valid JSON objects. Messages omits `reasoning` and `tool-call` blocks inside user messages and tool results. This also permits replay of saved subagent notices containing assistant output; the original Session content remains intact. Empty user messages are skipped after conversion, while empty tool results retain their call ids and error flags. Other unsupported input blocks still fail with `UNSUPPORTED_CONTENT`. When every declared tool is deferred, the adapter prepends a non-deferred `DeferredToolPlaceholder` with the description `Reserved placeholder that keeps deferred tool loading active; never call this tool.` A numeric suffix avoids collisions with declared tools. The placeholder has no executor; calls receive the ordinary unavailable-tool result. The placeholder leaves real declarations unchanged. The shared [tool-update projection](../llm/README.md#known-limitations-and-deferred-work) rebuilds an addition-only baseline when an immediate tool is removed, while keeping later additions deferred. Empty tool lists receive no placeholder. +The selected DeepSeek model receives the harness system prompt, message history, tool schemas, stop sequences, and call config (`maxTokens`, `reasoningEffort`, `temperature`) without adapter-authored prompt prose. Provider-specific request-extension fields remain outside model input. The vision model normally receives retained user and tool-result images as Files API references beside attachment handles and request-preview dimensions. It also receives a normalized-object path when the current execution filesystem maps the attachment provider's host object; the descriptor marks this copy read-only and warns that normalization may have resized or re-encoded the upload. A Files resolution failure sends all retained images as inline base64 instead, and an over-budget older image keeps the access resolved for that request in its placeholder. Reasoning content from a prior assistant turn is passed back verbatim, whether or not that turn called a tool. Messages sends `{}` for historical tool arguments that are malformed JSON or are not objects. This argument fallback preserves call ids, tool names, and tool results; the original arguments stay in the Session log. Newly generated Messages tool arguments still require valid JSON objects. Messages omits `reasoning` and `tool-call` blocks inside user messages and tool results. This also permits replay of saved subagent notices containing assistant output; the original Session content remains intact. Empty user messages are skipped after conversion, while empty tool results retain their call ids and error flags. Other unsupported input blocks still fail with `UNSUPPORTED_CONTENT`. When every declared tool is deferred, the adapter prepends a non-deferred `DeferredToolPlaceholder` with the description `Reserved placeholder that keeps deferred tool loading active; never call this tool.` A numeric suffix avoids collisions with declared tools. The placeholder has no executor; calls receive the ordinary unavailable-tool result. The placeholder leaves real declarations unchanged. The shared [tool-update projection](../llm/README.md#known-limitations-and-deferred-work) rebuilds an addition-only baseline whenever a tool is removed, while keeping later additions deferred. Empty tool lists receive no placeholder. #### Token effect diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index 4350fe39e9..2f70619773 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -170,7 +170,7 @@ Files 模式通过 `maxRequestFilesBytes` 与 `maxImagesPerRequest` 限制保留 #### 模型看到什么 -所选 DeepSeek 模型会收到 harness 系统提示词、消息历史、工具 schema、停止序列与调用配置(`maxTokens`、`reasoningEffort`、`temperature`),不包含适配器撰写的提示词散文。提供方专用请求扩展字段留在模型输入之外。视觉模型通常接收 Files API 引用形式的用户与工具结果图片,其旁带附件句柄和请求预览尺寸。当前执行文件系统可以映射附件提供方的宿主对象时,它还会收到规范化对象路径;描述符会把该副本标记为只读,并警告规范化可能缩放或重新编码上传内容。Files 解析失败时,全部保留图片改用内联 base64;超出预算的较旧图片则在占位文本中保留当前请求已解析的访问方式。此前 assistant 轮次的推理内容会原样传回,无论该轮次是否调用了工具。 对于非法 JSON 或非对象的历史工具参数,Messages 发送 `{}`。此参数兜底保留调用 ID、工具名和工具结果;原始参数仍保留在 Session 日志中。新生成的 Messages 工具参数仍须是有效 JSON 对象。 Messages 会省略用户消息和工具结果中的 `reasoning` 与 `tool-call` 块。这也允许回放包含助手输出的已保存子 Agent 通知;原始 Session 内容保持完整。转换后的空用户消息会被跳过,空工具结果则保留调用 id 和错误标记。其他不支持的输入块仍会以 `UNSUPPORTED_CONTENT` 失败。 当所有已声明工具都为延迟加载时,适配器会在列表开头添加非延迟的 `DeferredToolPlaceholder`,描述为 `Reserved placeholder that keeps deferred tool loading active; never call this tool.`。如与已声明工具重名,则添加数字后缀。占位工具没有执行器;调用会收到常规的工具不可用结果。占位工具不会改变真实工具声明。共享的[工具更新投影](../llm/README.zh.md#known-limitations-and-deferred-work)在移除立即加载工具时重建 addition-only 基线,后续添加仍保持延迟加载。空工具列表不会添加占位工具。 +所选 DeepSeek 模型会收到 harness 系统提示词、消息历史、工具 schema、停止序列与调用配置(`maxTokens`、`reasoningEffort`、`temperature`),不包含适配器撰写的提示词散文。提供方专用请求扩展字段留在模型输入之外。视觉模型通常接收 Files API 引用形式的用户与工具结果图片,其旁带附件句柄和请求预览尺寸。当前执行文件系统可以映射附件提供方的宿主对象时,它还会收到规范化对象路径;描述符会把该副本标记为只读,并警告规范化可能缩放或重新编码上传内容。Files 解析失败时,全部保留图片改用内联 base64;超出预算的较旧图片则在占位文本中保留当前请求已解析的访问方式。此前 assistant 轮次的推理内容会原样传回,无论该轮次是否调用了工具。 对于非法 JSON 或非对象的历史工具参数,Messages 发送 `{}`。此参数兜底保留调用 ID、工具名和工具结果;原始参数仍保留在 Session 日志中。新生成的 Messages 工具参数仍须是有效 JSON 对象。 Messages 会省略用户消息和工具结果中的 `reasoning` 与 `tool-call` 块。这也允许回放包含助手输出的已保存子 Agent 通知;原始 Session 内容保持完整。转换后的空用户消息会被跳过,空工具结果则保留调用 id 和错误标记。其他不支持的输入块仍会以 `UNSUPPORTED_CONTENT` 失败。 当所有已声明工具都为延迟加载时,适配器会在列表开头添加非延迟的 `DeferredToolPlaceholder`,描述为 `Reserved placeholder that keeps deferred tool loading active; never call this tool.`。如与已声明工具重名,则添加数字后缀。占位工具没有执行器;调用会收到常规的工具不可用结果。占位工具不会改变真实工具声明。共享的[工具更新投影](../llm/README.zh.md#known-limitations-and-deferred-work)在移除任何工具时重建 addition-only 基线,后续添加仍保持延迟加载。空工具列表不会添加占位工具。 #### Token 影响 diff --git a/packages/llm/llm-deepseek/tests/fixtures/deferred-tools.ts b/packages/llm/llm-deepseek/tests/fixtures/deferred-tools.ts index b1eb397763..ea3e240223 100644 --- a/packages/llm/llm-deepseek/tests/fixtures/deferred-tools.ts +++ b/packages/llm/llm-deepseek/tests/fixtures/deferred-tools.ts @@ -24,9 +24,16 @@ export function apply(ctx: Context): void { agent.ctx.tools.register(defineContentToolFixture({ name: 'snapshot_ping', description: 'Return pong once.', parameters: {}, async execute() { - agent.ctx.tools.register(defineContentToolFixture({ + const removeLater = agent.ctx.tools.register(defineContentToolFixture({ name: 'snapshot_later', description: 'Return later.', parameters: {}, - async execute() { return [{ type: 'text', text: 'later' }] }, + async execute() { + removeLater() + agent.ctx.tools.register(defineContentToolFixture({ + name: 'snapshot_final', description: 'Return final.', parameters: {}, + async execute() { return [{ type: 'text', text: 'final' }] }, + })) + return [{ type: 'text', text: 'later' }] + }, })) return [{ type: 'text', text: 'pong' }] }, @@ -60,6 +67,13 @@ export function apply(ctx: Context): void { { role: 'system', content: [{ type: 'tool_addition', tool: { type: 'tool_reference', name: 'snapshot_later' } }] }, ]) break + case 4: + assert.deepEqual(request.tools, [ + { name: 'snapshot_ping', description: 'Return pong once.', input_schema: { type: 'object', properties: {} } }, + { name: 'snapshot_final', description: 'Return final.', input_schema: { type: 'object', properties: {} } }, + ]) + assert.deepEqual(request.messages.filter(message => message.role === 'system'), []) + break default: assert.fail('Unexpected request after the replacement tool call completed') } diff --git a/packages/llm/llm/README.i18n.yaml b/packages/llm/llm/README.i18n.yaml index 53db29f075..78c5071896 100644 --- a/packages/llm/llm/README.i18n.yaml +++ b/packages/llm/llm/README.i18n.yaml @@ -54,8 +54,8 @@ en: a81634136baef72f zh: b92dda883986b0e1 /deepseek-ai-dsh-llm/known-limitations-and-deferred-work: - en: 94e36b92eb0dcc78 - zh: c8b99bf993411f11 + en: 9b2c811157cb5148 + zh: 94577c68b22f0abb /deepseek-ai-dsh-llm/known-limitations-and-deferred-work/dev-note: en: ecbc333b88e14d88 zh: 77c725d92efd8646 diff --git a/packages/llm/llm/README.md b/packages/llm/llm/README.md index 203dde885d..a9caca0951 100644 --- a/packages/llm/llm/README.md +++ b/packages/llm/llm/README.md @@ -180,7 +180,7 @@ These limits define where this service stops and other packages or future work b - **Variants normally require a producer** — `prefill`, per-tool `strict`, block `cache` hints, and the `agent` message-source variant have no producer ([Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)). - **`BlockAssembler` handles core block kinds only** — a plugin-added block type whose stream is never closed by `block-end` makes `blocks()` throw. - **`GenerateOptions.sessionId` is a locally-declared brand** — importing dsh-session's `SessionId` would create a dependency cycle. -- **Tool updates require session history** — `GenerateOptions.tools` contains active definitions. `toolHistory` supplies the initial declarations and historically resolved additions from `Session.toolHistory()`. At adapter dispatch, `projectToolUpdates` constructs deferred declarations and retains removed definitions for `in-history`; `addition-only` omits removed definitions and removal messages. Removing an immediate baseline tool on an addition-only route rebuilds that route's baseline from the declarations available after that update. Superseded tool blocks are omitted while unrelated content survives; subsequent additions remain deferred. Removing only later-added tools preserves the earlier baseline. Unsupported routes receive active tools without developer messages or `deferLoading`. Missing history or a request prefix omitting recorded updates falls back to current declarations without developer messages. Explicitly deferred tools become immediate during a rebuild only if an addition has already activated them; unactivated declarations remain deferred. +- **Tool updates require session history** — `GenerateOptions.tools` contains active definitions. `toolHistory` supplies the initial declarations and historically resolved additions from `Session.toolHistory()`. At adapter dispatch, `projectToolUpdates` constructs deferred declarations and retains removed definitions for `in-history`; `addition-only` omits removed definitions and removal messages. Removing any tool on an addition-only route rebuilds that route's baseline from the declarations available after that update. Superseded tool blocks are omitted while unrelated content survives; subsequent additions remain deferred. Unsupported routes receive active tools without developer messages or `deferLoading`. Missing history or a request prefix omitting recorded updates falls back to current declarations without developer messages. Explicitly deferred tools become immediate during a rebuild only if an addition has already activated them; unactivated declarations remain deferred. ### Dev Note diff --git a/packages/llm/llm/README.zh.md b/packages/llm/llm/README.zh.md index 38a7f235be..6b917948c1 100644 --- a/packages/llm/llm/README.zh.md +++ b/packages/llm/llm/README.zh.md @@ -180,7 +180,7 @@ const configure = (maxTokens: number): ConfigureCall => - **变体通常要求实际产生方**——`prefill`、逐工具 `strict`、内容块 `cache` 提示和 `agent` 消息来源变体都没有产生方(见 [Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md))。 - **`BlockAssembler` 只处理核心块类型**——插件添加块类型的流若从未由 `block-end` 关闭,`blocks()` 会抛出异常。 - **`GenerateOptions.sessionId` 是本地声明的品牌类型**——导入 dsh-session 的 `SessionId` 会产生依赖循环。 -- **工具更新需要会话历史** — `GenerateOptions.tools` 包含当前有效定义。`toolHistory` 提供 `Session.toolHistory()` 派生的初始声明及已解析历史定义的添加记录。适配器分发时,`projectToolUpdates` 构造延迟声明,并在 `in-history` 模式保留已移除定义;`addition-only` 省略已移除定义和移除消息。在 addition-only 路由移除基线中的立即加载工具时,使用该次更新后的可用声明重建此路由的基线。已被取代的工具块会被省略,其他内容保留;后续添加仍保持延迟加载。仅移除后来添加的工具时保留早期基线。不支持更新的路由接收有效工具,不携带 developer 消息或 `deferLoading`。缺少历史或请求前缀遗漏已记录更新时,回退为当前声明且不发送 developer 消息。显式延迟工具仅在已有添加记录将其激活时,才会在重建时变为立即加载;尚未激活的声明仍保持延迟加载。 +- **工具更新需要会话历史** — `GenerateOptions.tools` 包含当前有效定义。`toolHistory` 提供 `Session.toolHistory()` 派生的初始声明及已解析历史定义的添加记录。适配器分发时,`projectToolUpdates` 构造延迟声明,并在 `in-history` 模式保留已移除定义;`addition-only` 省略已移除定义和移除消息。在 addition-only 路由移除任何工具时,使用该次更新后的可用声明重建此路由的基线。已被取代的工具块会被省略,其他内容保留;后续添加仍保持延迟加载。不支持更新的路由接收有效工具,不携带 developer 消息或 `deferLoading`。缺少历史或请求前缀遗漏已记录更新时,回退为当前声明且不发送 developer 消息。显式延迟工具仅在已有添加记录将其激活时,才会在重建时变为立即加载;尚未激活的声明仍保持延迟加载。 ### 开发备注 diff --git a/packages/llm/llm/src/content.ts b/packages/llm/llm/src/content.ts index bec3190d1a..8dd5430169 100644 --- a/packages/llm/llm/src/content.ts +++ b/packages/llm/llm/src/content.ts @@ -379,11 +379,7 @@ function withoutDeveloperMessages(messages: readonly RequestMessage[]): readonly return retained.length === messages.length ? messages : retained } -function toolDeclarations( - tools: readonly ToolSchema[] | undefined, - mode: ToolUpdate, - history: ToolHistory, -): Map { +function toolDeclarations(history: ToolHistory): Map { const declarations = new Map(history.tools.map(tool => [tool.name, tool])) for (const update of history.updates) { for (const tool of update.additions) { @@ -394,32 +390,16 @@ function toolDeclarations( } } - switch (mode) { - case 'in-history': - // Removal blocks disable tools without discarding their historical definitions. - return declarations - case 'addition-only': { - // Without removal support, the declaration list must omit inactive tools. - const activeNames = new Set(tools?.map(tool => tool.name)) - for (const name of declarations.keys()) { - if (!activeNames.has(name)) declarations.delete(name) - } - return declarations - } - /* v8 ignore next 2 -- closed-union exhaustiveness guard */ - default: - return assertNever(mode) - } + return declarations } -/** Reset only when a removal changes the initially loaded definitions; later removals retain the earlier prefix. */ +/** Rebuild declarations after removals that addition-only routes cannot represent in history. */ function rebaseAdditionOnlyHistory( history: ToolHistory, messages: ReadonlyMap, ): ToolHistory { const available = new Map(history.tools.map(tool => [tool.name, tool])) let baseline = history.tools - let immediate = new Set(baseline.filter(tool => !tool.deferLoading).map(tool => tool.name)) let retainedFrom = 0 for (const [index, update] of history.updates.entries()) { const additions = new Map(update.additions.map(tool => [tool.name, tool])) @@ -430,13 +410,12 @@ function rebaseAdditionOnlyHistory( const { deferLoading: _loading, ...tool } = additions.get(block.toolName) as ToolSchema available.set(tool.name, tool) } else if (block.type === 'tool-removal') { - reset ||= immediate.has(block.toolName) + reset = true available.delete(block.toolName) } } if (reset) { baseline = [...available.values()] - immediate = new Set(baseline.filter(tool => !tool.deferLoading).map(tool => tool.name)) retainedFrom = index + 1 } } @@ -447,7 +426,7 @@ function rebaseAdditionOnlyHistory( * Construct provider declarations from session-folded history without changing logged active tools. * Unsupported routes and incomplete history use current declarations without developer updates. * Explicitly deferred baseline tools become available only after their first retained addition. - * Addition-only routes rebuild their baseline when an initially loaded tool is removed; + * Addition-only routes rebuild their baseline whenever a tool is removed; * later additions remain deferred and superseded tool blocks are omitted without dropping other content. * @param messages - complete request inputs, or the prefix selected for an auxiliary call. * @param tools - currently active tool schemas. @@ -481,7 +460,7 @@ export function projectToolUpdates( } const projectedHistory = toolUpdate === 'addition-only' ? rebaseAdditionOnlyHistory(history, updates) : history - const declarations = toolDeclarations(tools, toolUpdate, projectedHistory) + const declarations = toolDeclarations(projectedHistory) const updateIds = new Set(history.updates.map(update => update.messageId)) const retainedUpdateIds = new Set(projectedHistory.updates.map(update => update.messageId)) // Deferred baseline tools still need their first addition to become available. diff --git a/packages/llm/llm/tests/content.spec.ts b/packages/llm/llm/tests/content.spec.ts index cf9d13594f..c3376c1f70 100644 --- a/packages/llm/llm/tests/content.spec.ts +++ b/packages/llm/llm/tests/content.spec.ts @@ -448,7 +448,7 @@ describe('projectToolUpdates', () => { expect(projected.tools).toEqual([deferred]) expect(projected.messages).toEqual(mode === 'in-history' ? [prompt, added, removed, restored] - : [prompt, added]) + : [prompt, restored]) }) it('uses current declarations without updates when history is missing or the prefix omits an update', () => { @@ -562,17 +562,26 @@ describe('projectToolUpdates', () => { expect(JSON.stringify({ messages, history })).toBe(saved) }) - it('keeps the original baseline when only later-added tools are replaced', () => { + it.each([false, true])('rebuilds after removing a later-activated tool (replacement=%s)', (replace) => { const replacement = { name: 'replacement', description: 'Replacement', parameters: {} } - const added = developer([{ type: 'tool-addition', toolName: 'fetch' }]) - const swapped = developer([{ type: 'tool-addition', toolName: 'replacement' }, { type: 'tool-removal', toolName: 'fetch' }]) + const retained = { name: 'retained', description: 'Retained', parameters: {} } + const later = { name: 'later', description: 'Later', parameters: {} } + const added = developer([{ type: 'tool-addition', toolName: 'fetch' }, { type: 'tool-addition', toolName: 'retained' }]) + const swapped = developer([ + ...replace ? [{ type: 'tool-addition' as const, toolName: 'replacement' }] : [], + { type: 'tool-removal', toolName: 'fetch' }, + ]) + const next = developer([{ type: 'tool-addition', toolName: 'later' }]) const history: ToolHistory = { tools: [search], updates: [ - { messageId: added.id, additions: [fetch] }, { messageId: swapped.id, additions: [replacement] }, + { messageId: added.id, additions: [fetch, retained] }, + { messageId: swapped.id, additions: replace ? [replacement] : [] }, + { messageId: next.id, additions: [later] }, ] } - const projected = projectToolUpdates([prompt, added, prompt, swapped], [search, replacement], 'addition-only', history) - expect(projected.tools).toEqual([search, { ...replacement, deferLoading: true }]) + const active = [search, retained, ...replace ? [replacement] : [], later] + const projected = projectToolUpdates([prompt, added, prompt, swapped, next], active, 'addition-only', history) + expect(projected.tools).toEqual([search, retained, ...replace ? [replacement] : [], { ...later, deferLoading: true }]) expect(projected.messages[0]).toBe(prompt) - expect(roles(projected.messages)).toEqual([[{ type: 'tool-addition', toolName: 'replacement' }]]) + expect(roles(projected.messages)).toEqual([[{ type: 'tool-addition', toolName: 'later' }]]) }) it.each([false, true])('preserves explicitly deferred baseline activation across a reset (activated=%s)', (activated) => { diff --git a/snapshots/session/deepseek-deferred-tools/session.v4.jsonl b/snapshots/session/deepseek-deferred-tools/session.v4.jsonl index 85d8585724..ceb1cb5325 100644 --- a/snapshots/session/deepseek-deferred-tools/session.v4.jsonl +++ b/snapshots/session/deepseek-deferred-tools/session.v4.jsonl @@ -2,30 +2,37 @@ {"type":"permission/preset","data":{"preset":"danger-full-access"}} {"type":"sandbox/mode","data":{"mode":"danger-full-access"}} {"type":"approval/policy","data":{"policy":"never"}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call snapshot_initial, then snapshot_ping, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call snapshot_initial, then snapshot_ping, then snapshot_later, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} {"type":"turn/start","data":{"turn":1}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":1}} {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Call snapshot_initial, then snapshot_ping, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Call snapshot_initial, then snapshot_ping, then snapshot_later, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["snapshot_initial"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Call snapshot_initial, then snapshot_pin","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_initial","name":"snapshot_initial","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792658934151,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792658934151,"index":0,"dt":[],"id":"call_initial","name":"snapshot_initial","args":["{}"]},{"type":"chunk","time":1792658934151,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_initial","name":"snapshot_initial","arguments":"{}"}}},{"type":"chunk","time":1792658934151,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792658934151,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_initial","name":"snapshot_initial","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1792661643282,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792661643282,"index":0,"dt":[],"id":"call_initial","name":"snapshot_initial","args":["{}"]},{"type":"chunk","time":1792661643282,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_initial","name":"snapshot_initial","arguments":"{}"}}},{"type":"chunk","time":1792661643282,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1792661643282,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_initial","name":"snapshot_initial","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_initial"},"toolCallId":"call_initial","content":[{"type":"text","text":"snapshot_ping is available."}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["snapshot_ping"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"},{"type":"tool-removal","toolName":"snapshot_initial"}],"role":"developer","id":"{{message:6}}"},"headerSeq":18},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1792658934170,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792658934170,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1792658934170,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1792658934170,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1792661643298,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792661643298,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1792661643298,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1792661643298,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:8}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["snapshot_later","snapshot_ping"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_later"}],"role":"developer","id":"{{message:9}}"},"headerSeq":25},"surfaceOp":"append"} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1792658934186,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792658934186,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1792658934186,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1792658934186,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1792658934186,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_later","name":"snapshot_later","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"stream":[{"type":"chunk","time":1792661643311,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792661643311,"index":0,"dt":[],"id":"call_later","name":"snapshot_later","args":["{}"]},{"type":"chunk","time":1792661643311,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_later","name":"snapshot_later","arguments":"{}"}}},{"type":"chunk","time":1792661643311,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":3,"callId":"call_later","name":"snapshot_later","arguments":"{}"}} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"call_later"},"toolCallId":"call_later","content":[{"type":"text","text":"later"}],"isError":false,"id":"{{message:11}}"}},"sourceEventSeqs":[28],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} +{"type":"step/start","data":{"turn":1,"step":4}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["snapshot_final","snapshot_ping"]},"reason":"change"}} +{"type":"developer/message","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_final"},{"type":"tool-removal","toolName":"snapshot_later"}],"role":"developer","id":"{{message:12}}"},"headerSeq":32},"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1792661643324,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792661643324,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1792661643324,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1792661643324,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1792661643324,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":4}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/deepseek-deferred-tools/snapshot.yml b/snapshots/session/deepseek-deferred-tools/snapshot.yml index 6a4e4e6626..ceedc9db93 100644 --- a/snapshots/session/deepseek-deferred-tools/snapshot.yml +++ b/snapshots/session/deepseek-deferred-tools/snapshot.yml @@ -6,4 +6,4 @@ recording: authored header: class: deepseek-deferred-tools pin: true - changes: 2 + changes: 3 diff --git a/snapshots/session/deepseek-deferred-tools/tool-schemas.expected.json b/snapshots/session/deepseek-deferred-tools/tool-schemas.expected.json index 6103ea1762..255e5a96d0 100644 --- a/snapshots/session/deepseek-deferred-tools/tool-schemas.expected.json +++ b/snapshots/session/deepseek-deferred-tools/tool-schemas.expected.json @@ -37,6 +37,24 @@ "properties": {} } } + ], + [ + { + "name": "snapshot_final", + "description": "Return final.", + "parameters": { + "type": "object", + "properties": {} + } + }, + { + "name": "snapshot_ping", + "description": "Return pong once.", + "parameters": { + "type": "object", + "properties": {} + } + } ] ] }