Skip to content

[Bug] ToolSearch 激活恢复字段不一致,引发跨回合工具集变化与潜在缓存前缀失效 #913

Description

@whitzard0421

What happened? / 问题描述

PI-Desktop 0.15.3 存在一个可复现的按需工具恢复缺陷:ToolSearch 写入 details.activated,但新用户回合的恢复函数读取 details.addedToolNames。因此,已激活但尚未实际调用过的工具,会在下一用户回合从激活集合中丢失,即使成功的 ToolSearch 结果仍在有效上下文中。

这也是 #225 的后续问题:恢复函数已经存在,但生产端与消费端的数据字段不一致。实际调用过的工具可以通过自己的 toolResult.toolName 恢复,因此“激活后立即调用”的测试可能掩盖该缺陷。

排查连续对话中 prompt-cache 命中突降时,还发现子代理完成会重新组合系统提示。下文将已验证的工具恢复 bug与需要进一步量化的缓存前缀影响分开说明,不将所有 cache miss 归因于客户端。

App version / 应用版本

  • 实际观察及本地隔离复现:0.15.3。
  • 另检查了 v0.15.4 tag 的公开源码,相同的字段不一致仍存在;未在 0.15.4 上运行端到端复现。

Operating system / 操作系统

Windows,已安装的桌面应用。

Extra environment / 其他环境信息

Agent 模式,按需工具及子代理;缓存现象来自 OpenAI-compatible Responses API 工作流。字段恢复缺陷本身可在不调用模型、不联网的隔离环境中验证,不依赖特定供应商或模型。

Steps to reproduce / 复现步骤

  1. 在 Agent 模式中启用一个按需工具目录。
  2. 使用 ToolSearch 成功激活一个或多个工具,但先不要实际调用其中至少一个工具。
  3. 在同一会话发送新的用户消息。保留成功的 ToolSearch 结果,不触发压缩或切换模式。
  4. 检查 resetDeferredToolsForPrompt() 之后的 activeDeferredToolNames 和工具定义。
  5. 已激活但没有自己的成功调用记录的工具未被恢复;有独立成功调用记录的工具则可以恢复。

Synthetic fixture / 合成复现数据

下面的名称仅为测试占位符,不是用户的实际工具或配置:

const deferredToolNames = new Set(["ToolA", "ToolB"]);
const messages = [
  {
    role: "toolResult",
    toolName: "ToolSearch",
    isError: false,
    content: [{ type: "text", text: "Activated on-demand tools: ToolA, ToolB." }],
    details: {
      query: "synthetic fixture",
      matches: ["ToolA", "ToolB"],
      activated: ["ToolA", "ToolB"],
    },
  },
  {
    role: "toolResult",
    toolName: "ToolA",
    isError: false,
    content: [{ type: "text", text: "ok" }],
  },
];
// Clear activeDeferredToolNames, then run restoreDeferredToolsFromContext()
// with the above effective context and deferred catalog.
// Expected: ToolA, ToolB
// Actual:   ToolA

验证方式:在隔离 harness 中执行安装包内实际的 restoreDeferredToolsFromContext() 方法,仅提供有效工具结果及必要辅助函数,不执行整个应用或网络请求。观察到“用过的工具恢复、仅激活的工具丢失”。以上 fixture 是同一字段问题的脱敏最小表达,不是完整端到端测试。

Expected behavior / 预期行为

  • 成功的 ToolSearch 激活记录仍处于有效上下文、且工具仍被当前目录和模式允许时,应在新回合恢复相同工具集,无须先调用工具或重新搜索。
  • 不应因为激活元数据字段不一致而无故增删工具定义。
  • 尽可能保持固定系统提示和工具声明前缀稳定;运行中的动态状态不应无必要地改写历史前面的稳定内容。

Actual behavior / 实际行为及源码证据

以下链接均为公开的 v0.15.4 tag 源码;本地 0.15.3 安装包也包含相同字段不一致:

因此,仅激活的工具丢失,而用过的工具可通过另一条恢复路径保留。工具集变化可能影响后续请求的可缓存前缀;具体影响范围取决于适配器序列化和上游缓存实现。

Related cache-prefix behavior / 相关缓存前缀行为

另一个值得独立评估的实现路径:

  • composeSystemPrompt 将可复用子代理列表拼进系统提示,并根据运行中的代理进行过滤。
  • settleDelegation 在子代理完成后调用 refreshResumablePrompt(),更新该系统提示。

在所观察的 0.15.3 连续会话中,子代理完成附近出现过缓存命中从“数万 token”降到“数千 token”,随后恢复的情况。系统提示的更新机制已从代码确认,时间顺序与现象相符;但没有保存当时完整出站请求的前缀指纹,不能将每次 cache miss 与该路径建立排他的因果关系。上游缓存路由、驱逐或保留策略仍可能有影响。

会话分支导致会话 ID、缓存 key 或提示内容变化,不作为本 Issue 的缺陷证据。也不要求缓存命中量在所有请求间严格单调增长。

Suggested fix / 建议修复及验证

  1. 统一 ToolSearch 激活结果的写入、序列化、持久化和恢复字段;兼容历史中的 activated / addedToolNames 表示,避免只修复新写入而无法恢复旧历史。
  2. 保留当前安全边界:只信任成功、非占位的工具结果,并限制为当前目录及模式允许的工具。
  3. 增加回归测试:激活但未调用;激活后只调用一部分;新用户回合与历史恢复;失败或占位结果不恢复;已不在有效上下文或当前目录中的工具不恢复。
  4. 对子代理动态信息评估追加消息等方案,尽量避免重写固定系统提示;具体行为需要结合适配器兼容性验证。
  5. 可增加默认不包含正文的缓存诊断:工具集合/定义、系统提示和消息前缀的哈希,cache-key 哈希,以及上游返回的缓存用量。借此区分客户端前缀变化与上游缓存失效。

Logs / 日志与隐私处理

未附原始日志、对话、配置或截图。已省略用户/设备名称、本地绝对路径、项目与业务信息、会话/请求/子代理 ID、供应商和模型标识、端点、IP、凭据、精确时间及账单数据。报告仅保留应用版本、操作系统、公开源码链接、函数/字段名称和合成复现数据。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions