Skip to content

Agent 对话、工具循环、持久化与 MCP 可靠性增强 - #1549

Open
cyfung1031 wants to merge 79 commits into
scriptscat:mainfrom
cyfung1031:feat/ai-001
Open

Agent 对话、工具循环、持久化与 MCP 可靠性增强#1549
cyfung1031 wants to merge 79 commits into
scriptscat:mainfrom
cyfung1031:feat/ai-001

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Checklist / 检查清单

背景

本 PR 关联 Issue #1545,解决内置 Agent 在长时间、高频 Tool Calling、后台任务和会话续接场景中的可靠性与 Token 成本问题。

原始需求包括:

  1. 最大工具调用次数固定,复杂任务容易被强制中断;
  2. 历史工具结果在后续请求中重复携带,Token 消耗持续增长;
  3. 检测到重复或循环调用后,用户无法及时介入;
  4. Agent 的取消、压缩、持久化、附件所有权和后台任务生命周期存在竞态边界。

在后续实现和审查中,又确认并处理了跨上下文写入、OPFS 提交确认、会话 ID 复用、任务快照代际隔离、MCP 连接生命周期以及结构化错误传播等问题。分支已同步最新 main,并完成了与官方 MCP SDK 迁移相关的冲突语义合并。

本次改动 / 实现内容

1. 可配置最大工具调用次数,并支持原会话继续执行

  • 新增 Agent 配置持久化 chatMaxIterations
    • 默认值为 50;
    • 有效范围为 1–1000;
    • 设置页输入、直接传参和损坏存储值统一取整、截断并回退。
  • 达到迭代上限时:
    • 持久化带 errorCode: "max_iterations" 的错误消息;
    • UI 仅对最后一个有效的 max-iterations 错误组显示“继续对话”;
    • 继续操作在原会话中追加指令,并复用持久化历史。
  • 重建发送给 Provider 的上下文时跳过错误占位消息,保留 UI 和 OPFS 中的完整历史。
  • 定时任务遇到终态错误时正确记录失败状态、usage 和 duration。

2. 降低长 Tool Loop 的上下文和 Token 成本

  • Anthropic 请求在稳定消息前缀和最后一条消息上使用 Prompt Cache 断点。
  • execute_script 按最终 JSON envelope 限制结果大小,保留首尾内容并标记截断;截断不会拆分 UTF-16 surrogate pair,也覆盖引号、反斜杠等转义内容。
  • Tool Loop 按消息缓存序列化结果估算上下文,避免重复计算造成 O(n²) 开销。
  • 图片视觉 Token 按像素尺寸估算,并按 Provider 实际内联行为计算附件预算。
  • 请求前执行上下文预算检查;长历史在达到阈值时按滑动窗口裁剪旧 tool result,同时保留可恢复标识。
  • 历史裁剪只作用于发送给 LLM 的副本,不改写 OPFS/UI 中的完整消息;普通 ephemeral/direct tool-loop 保持原有内存消息语义。
  • 小上下文模型使用实际可用预算,避免预算计算为 0 后所有对话必然失败。
  • Provider 的上下文长度拒绝统一分类为 context_too_large,并保留可信的结构化错误码。

3. Loop Guard 与用户介入

  • 重复或循环工具调用达到阈值后暂停并询问用户继续或停止。
  • 使用稳定的 continue / stop action,拒绝任意自定义输入。
  • Continue 会重置本轮累计警告计数,避免后续每次命中都立即暂停。
  • 超时、取消、attach、disconnect 和多监听器场景会同步清理 pending 状态并广播终态。
  • 定时任务和子代理继续保持无人值守语义:只告警,不等待用户输入。
  • Continue 双击、AskUserBlock 选项 key 和实时错误码传播均已修正。

4. 取消、终态和跨上下文生命周期

  • 统一处理 UI、后台任务、子代理、用户脚本工具和 compact 的 abort 竞态。
  • 取消后不再因连接关闭时序误报成功,也不会遗漏 done / error / tool_call_complete 等终态事件。
  • SSE 截断、端口断开、后台任务取消和 reentrant chat 都按实际终态处理。
  • 工具后处理、附件保存、任务保存和通知阶段持续观察 AbortSignal。
  • 会话队列在排队前注册连接感知回调、入锁后复查;重入 clear 会显式拒绝。

5. OPFS 持久化、会话代际与任务 CAS

  • 消息、工具轮和 compact 写入使用跨上下文锁及事务式快照,避免读改写覆盖。
  • 对可能已经提交但 close 返回错误的写入执行读回确认;无法确认时不把数据当作已丢失。
  • conversation 使用 generation/revision 保护 chat、attach、clear、delete、compact 和后台续接边界。
  • 任务快照保存 generation/revision,并使用 CAS 防止 Options、Service Worker 和后台任务互相覆盖。
  • 会话 ID 复用时不会读取上一代残留的任务或消息;旧格式裸 Task 数组仅在 legacy 会话中兼容读取。
  • 主元数据提交与消息、任务、附件 GC 解耦;清理失败不会伪装成主操作失败。
  • 生成附件按本轮租约和消息所有权管理:
    • 失败、取消或未被持久化消息接管时回收;
    • legacy 会话可从 content block 推断旧所有权;
    • GC 会检查工具附件、子代理嵌套附件以及跨会话和同会话借用引用;
    • 无法完整确认引用关系时安全保留附件,避免误删。
  • 生成图片保存失败会作为持久化 warning 传播,而不是静默丢失。
  • 定时任务的 claim、取消、删除和会话续接边界已统一处理,避免并发执行与元数据生命周期错位。

6. MCP 连接与结果处理

  • 分支已同步最新 main 的官方 @modelcontextprotocol/sdk 客户端实现,并保留 request AbortSignal。
  • 同一 MCP server ID 的并发连接请求复用同一个连接操作。
  • initialize 或 listTools 失败时关闭临时 client,不注册半成品工具。
  • 已连接服务器配置更新后会断开并按新 URL、headers、API key 重连。
  • 工具名保留可读 server name,同时以原始 server ID 的码点编码避免 a-b / a_b 等碰撞。
  • MCP 的 structuredContent 在普通结果、image 附件、ToolRegistry 归一化和结构化错误路径中均得到保留。
  • 异步 disconnect 会等待 client.close 完成后再清理注册状态。

7. 用户脚本 API、类型和 UI

  • 补齐 Agent Chat、Task、Model provider、warning 和 errorCode 的公开类型声明。
  • userscript tool batch 使用 requestId 隔离超时批次,避免迟到结果污染新请求。
  • Tool result、子代理详情、warning、usage 和附件 ownership 在流式与非流式路径保持一致。
  • ChatInput 和消息编辑附件预览 URL 在取消、保存和卸载时正确释放。
  • 设置页、任务页、MCP 页面和 ChatArea 的状态、双击、代际透传和错误展示已补齐回归覆盖。
  • Agent 的错误、warning、Loop Guard 和任务相关文案已同步到现有 locale,并通过 i18n key-set 检查。

实现考虑

  • 持久化不确定态优先安全保留。 close 报错或确认读失败不能证明写入失败,因此不会删除可能已经被持久化消息引用的附件。
  • generation + revision 是会话边界和并发边界。 generation 防止 ID 复用后的旧数据复活,revision/CAS 防止不同上下文用旧快照覆盖新快照。
  • LLM 上下文副本与展示历史分离。 裁剪只改变发送前副本,保证继续对话、刷新页面和 UI 展示仍可访问完整工具结果。
  • 取消终态只有一个提交责任方。 AbortSignal 会贯穿工具、附件、任务和连接生命周期,避免多个主体重复发布或误报成功。
  • 附件 GC 采用 fail-safe 策略。 引用扫描不完整时保留附件;同一附件既可能由消息拥有,也可能被其他消息或会话借用。
  • MCP 注册名必须由原始身份决定。 可读名称允许清洗,但唯一性不能依赖清洗后的 server name 或 ID。

兼容性

  • 未修改 Agent 配置的用户仍使用默认最大工具调用次数 50。
  • execute_script、上下文裁剪和 Anthropic cache 不改变 OPFS 中的原始消息。
  • 旧 conversation message、legacy attachment ownership 和 legacy 裸 Task 数组继续兼容;新 generation 不会读取旧会话 ID 残留数据。
  • 现有 userscript tool、子代理、后台/定时任务和 Provider 接口保持兼容;新增字段均为可选或向后兼容的结构化字段。
  • MCP 服务器配置仍使用原有 URL、headers 和 API key 语义;工具名的 server ID 编码只影响注册到 Agent 的内部工具名。

已知限制

  • 未执行真实远程 MCP server 的端到端连接;MCP transport、失败清理、并发连接和结果转换由协议 mock 回归测试覆盖。
  • 附件 GC 仍是 best-effort 清理,不引入持久化 GC 重试队列;扫描或删除失败时保留附件,可能留下可回收的存储。
  • 未执行真实浏览器安装包手工验证;本次范围没有新增页面视觉设计,验证以完整测试、类型检查、lint 和生产构建为准。
  • 构建仍报告仓库已有的 bundle size 和 Monaco 动态 require 警告,不是本 PR 新增的失败。

建议审查重点

  • 最大工具调用次数的归一化、继续对话的历史重建和错误占位消息过滤。
  • 40%/60% 上下文裁剪阈值、Provider 实际 Token/附件预算和持久化历史不可变性。
  • Loop Guard 的 Continue/Stop、超时、取消、attach 和后台任务终态。
  • OPFS close 不确定态、跨上下文锁、generation/revision/CAS、附件所有权和 GC fail-safe 行为。
  • MCP 并发 connect/disconnect、配置重连、工具名唯一性和 structuredContent 保留。
  • 公开 userscript 类型与流式/非流式终态事件的一致性。

关联

验证 / Test plan

  • pnpm run test:ci — 完整测试套件通过。
  • pnpm run lint:ci — Prettier、TypeScript、i18n、issue-template 检查和 ESLint 全部通过。
  • pnpm run build — 生产构建成功;仅有已知 bundle size 与 Monaco 动态 require 警告。
  • 相关 Agent、MCP、OPFS、任务和 Chat UI 回归测试均已覆盖本次新增边界。
  • 3 个 subagents 完成 fresh review;未声称其等同于 human review。

Screenshots / 截图

N/A — 本 PR 没有新增页面视觉设计或需要截图的 UI 变更。

cyfung1031 and others added 6 commits July 7, 2026 02:24
针对 scriptscat#1545:UI 对话此前硬编码 50 次工具调用上限且无法调整,达到上限
后用户误以为必须新建对话、丢失已探索的上下文。

- 新增 AgentConfigRepo(chatMaxIterations,默认 50,1-1000),Settings
  页新增"对话"分类可视化配置
- 达到 max_iterations 时持久化 errorCode,聊天界面据此渲染"继续对话"
  按钮,一键复用已持久化的完整历史继续执行
- 全部 8 个 locale 补齐相应文案

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
针对 scriptscat#1545 的 Token 消耗问题:长 tool loop 每轮都全量计费输入 token,
且 execute_script 返回值(如 DOM dump、模块映射)不设上限,被完整保留
并在后续每轮重复发送。

- Anthropic provider 为最后一条消息追加 cache_control 断点,使已产生的
  历史前缀被缓存,下一轮仅新增部分计费(system/tools 断点已存在,此为
  第三个断点,未超过 4 个断点上限)
- execute_script 返回值超过 30000 字符时截断为首尾各 15000 字符并标注
  truncated / original_length,避免超大返回值反复占用上下文

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
针对 scriptscat#1545:在触及 80% 的 autoCompact 阈值之前,长对话的旧 tool 结果
(如 DOM dump)会在后续每一轮都被完整重复发送,进一步放大 Token 消耗。

- 新增 elideOldToolResults:保留最近 5 轮 assistant/tool 消息原文,
  更早的 tool 结果替换为占位文本;只裁剪内存中传给 LLM 的消息,不影响
  chatRepo 持久化与 UI 历史
- 在 40% / 60% 两个上下文占用阈值各触发一次(而非逐轮触发),避免
  频繁重写消息前缀导致 Anthropic 的 prompt cache 断点失效

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
针对 scriptscat#1545 的 Loop Guard 诉求:现有 tool_call_guard 检测到重复/死循环
模式时只向 LLM 注入提醒,LLM 不理会则持续烧 Token,用户无法介入。

- ToolLoopOrchestrator 新增 askUserForGuard 可选回调:循环检测连续命中
  达到 2 次时暂停循环,询问用户"继续"或"停止";回答停止则以 done(非
  error)优雅收尾并持久化提示
- 仅 UI 对话(含后台会话,复用既有 ask_user 事件/resolver 机制,5 分钟
  无人应答默认继续)传入该回调;定时任务与子代理不传,保持原有的
  仅告警不暂停行为,避免无人值守场景被阻塞

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
针对 PR scriptscat#1549 的审查反馈:

1. 【阻断性】继续对话时,历史中 max_iterations 错误占位消息(content
   为空字符串)此前会被完整重放进 LLM 请求,部分 provider(如
   Anthropic)会因空 content 拒绝请求。buildAndPersistUserMessage 现在
   跳过带 error 字段的历史消息,仅用于 UI 展示,不进入 LLM 上下文。

2. 循环检测升级提问 5 分钟无人应答超时后,此前只删除 resolver,未清除
   后台会话的 rc.pendingAskUser,导致后续 attach 的 UI 仍会看到已失效
   的提问,回答会静默无效。超时回调现在同步清除 pendingAskUser。

3. chatMaxIterations 的合法范围此前只在 Settings UI 校验,
   AgentConfigRepo 直接读写未归一化的原始值;损坏的 storage、旧版本
   遗留值或绕过 UI 的写入都可能导致 0/负数/超大值,进而导致循环立即
   报错或失控运行。新增 normalizeChatMaxIterations,在 getConfig /
   saveConfig 中统一归一化到 [1, 1000];ChatService 解析最终值时改用
   ?? 并对结果做同样的兜底截断。

4. 循环检测升级的连续命中计数此前永不重置,用户选择"继续"后,此后
   每一次告警都会重新暂停询问,比"连续命中 2 次暂停"更激进。现在回答
   继续后重置计数,需再次连续命中 2 次才会重新暂停。

均已按 TDD 补充回归测试(先复现失败用例,再修复)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031

This comment was marked as outdated.

This comment was marked as outdated.

This comment has been minimized.

This comment was marked as outdated.

This comment was marked as outdated.

This comment was marked as outdated.

This comment was marked as outdated.

@cyfung1031
cyfung1031 marked this pull request as draft July 12, 2026 00:45
cyfung1031 and others added 5 commits July 12, 2026 10:17
每轮 LLM 调用前用当前 messages + 工具定义重新估算体积,超出安全阈值时立即裁剪;
不再仅依赖上一轮响应的 usage 反馈,避免巨大 tool 结果在下一次请求发出前就把上下文撑爆。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
file/audio 从不被 provider 内联,image 只在 vision 模型上才会解析为 data URL;
按类型和模型能力估算体积,避免非 vision 场景或无法读取的附件把预算估算撑到 Infinity。
裁剪后的占位文本保留 type/attachmentId(OPFS 路径),而非完全丢弃可恢复信息。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
非流式与流式适配器都只处理了 tool_call_start/delta,从未处理 tool_call_complete 和
new_message,导致 ephemeral 历史中的 toolCalls 没有结果/终态,且多轮对话被压平成一条
消息、最终回复重复追加。按轮次边界重建 assistant + 对应 tool 消息,并在 StreamChunk
新增 tool_call_complete/new_message 类型透传给用户脚本。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
orchestrator 的 callLLM/autoCompact 原生失败只 throw、不 sendEvent,导致实时 UI 收不到
子代理的终态事件、卡在 running 直到刷新;在捕获异常时补发一次 error 事件(若尚未上报过
终态)。同时 tool_call_complete 记录状态改为 event.status ?? "completed",避免持久化后
把失败的嵌套工具显示为成功。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
后台会话 attach 收到 askUserResponse 后自己广播一次 ask_user_resolved,resolver
(ask_user.ts / 循环检测 loop-guard)自己又广播一次,同一次回答产生两条终态事件;
loop-guard 超时路径也是先发 ask_user_expired 又紧接着触发 ask_user_resolved。
改为终态事件只由 resolver 在恰好一处发出,attach 不再重复广播,超时/abort 只发
expired,只有真实回答才发 resolved。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031

This comment has been minimized.

This comment has been minimized.

@cyfung1031

This comment has been minimized.

…tinue double-click, fix AskUserBlock option key

- tool_registry.ts: script-tool结构化结果的附件保存失败此前只在 signal.aborted 时才落为
  error结果,OPFS配额超限等其他失败会被误判为"非JSON/非结构化结果"回退到按原始字符串处理,
  从而把附件其实未写入的失败当作成功上报。拆分 JSON.parse 与 saveAttachments 的错误处理,
  saveAttachments失败一律落为error结果。
- ChatArea.tsx: "继续对话"按钮点击后依赖异步 isStreaming state 去重,同一渲染帧内的两次
  点击可能都读到旧值 false 从而各自发起一次 handleSend,产生重复会话/端口泄漏。新增同步
  ref 在点击时立即锁定,避免重复提交。
- AskUserBlock.tsx: 选项列表 key 使用 label(opt) 而非 index,当 optionValues 使
  两个选项标签相同但取值不同时会导致 React key 冲突,可能把选中状态错误应用到另一个选项。
@cyfung1031

This comment has been minimized.

…t truncation boundary

大结果截断时按原始字符偏移量二分定位首尾保留区间,未考虑 UTF-16 代理对:截断点落在
emoji 等非 BMP 字符中间会切出孤立高/低代理项,JSON.stringify 虽仍能转义输出但字符本身
已损坏。截断边界落在代理对中间时向内收缩一位,改为完整排除该字符。
@cyfung1031

This comment has been minimized.

@cyfung1031

This comment has been minimized.

@cyfung1031

This comment has been minimized.

cyfung1031 and others added 2 commits July 20, 2026 20:10
三个 High、五个 Medium、一个 Low:

- tool-round 提交确认读失败(indeterminate)时不再直接当作成功发布:先幂等重试一次
  commitToolRound,仍无法确认落盘状态才以 persist_indeterminate 终止,不删除可能仍被
  引用的附件。
- CAT.agent.task.update()/remove() 现在强制要求调用方携带 get()/list() 返回的
  generation/revision;服务端对 update/delete 也不再把版本号当可选项,缺失时直接拒绝,
  避免 update 被 CAS 误判失败、也避免过期引用 stale-delete 掉被重建的新任务。
- Options 的 chat/attach/compact/clearMessages/deleteMessages 现在都携带当前会话的
  generation;服务端 compact 分支补上了与其它分支一致的 generation 校验,堵住了此前
  "过期标签页可以操作被删除重建的同 ID 会话" 的漏洞。
- Options 新建任务不再允许选择 event 模式(没有可注入的目标脚本 UUID);service 端对
  create/update 统一校验 sourceScriptUuid 必须指向已安装脚本。
- 会话任务列表持久化为 { generation, revision, tasks } 快照,会话 ID 复用且旧任务文件
  清理失败时不会再读到上一代的任务。
- 生成数据丢失的 warning 现在覆盖所有路径:带工具调用的 assistant 消息、工具轮开始前的
  即时提示、子代理详情持久化、ChatArea 的子代理分支、以及非流式/流式 userscript API。
- 上下文预算的字节数启发式估算不再是终审:裁剪到底后仍超预算但未达到 2 倍安全阈值时,
  放行给 provider 自行判定,只有极端超限时才本地拒绝,避免估算偏差误伤本可放下的请求。
- 并行 agent 工具调用现在通过显式 toolCallId → agentId 映射定位子代理状态,不再靠"猜
  第一个正在运行的子代理"。
- 编辑用户消息时新增的附件预览 URL 保存成功后会被释放,组件卸载时也会清理,不再泄漏。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

Copy link
Copy Markdown
Collaborator Author

本轮已完成同步与收尾,当前 PR head 为 de6c4560,并已合入最新 main(解决了 mcp_client.ts / 测试冲突)。

经复现、回归测试和 3 个 subagent 的 fresh review,按 PickInvariant 逐项检查后处理了以下确认有效的问题:

  • 修复 compact 产生的结构化 context_too_large 在 ChatService 边界被错误降级为 api_error
  • MCP 连接并发去重;连接初始化或 listTools 失败时关闭临时 client;更新已连接配置时重连。
  • MCP 工具名改为基于原始 server ID 的无碰撞编码,避免不同 ID 被清洗成同名工具。
  • 保留 MCP 的 structuredContent,包括 image 附件结果和仅由结构化内容携带的错误详情。
  • 任务快照加入 generation/revision 的读取与 CAS 写入保护;会话 ID 复用时不会复活旧任务,取消的定时任务不会误记成功。
  • 附件 GC 同时检查跨会话借用和同一会话新快照中的引用,避免所有权转移时误删附件。
  • ChatArea / task hook 透传正确的 conversation generation;补齐 compact duration 和公开类型声明。

验证结果:

  • pnpm run test:ci:完整测试通过
  • pnpm run lint:ci:Prettier、TypeScript、i18n、issue-template、ESLint 全部通过
  • pnpm run build:构建成功;仅有仓库原有的 bundle 体积和 Monaco 动态 require 警告
  • 每个 commit 创建前均完成 PickInvariant Review;每个确认问题都有对应回归测试

本轮 fresh review 中未能以可重复证据确认的风险(例如 GC 失败后的持久化重试债务、跨上下文全局扫描锁竞争、极窄的取消时序窗口)没有扩大改动范围;现有实现对这些情况采取安全保留/失败关闭策略。

@cyfung1031
cyfung1031 marked this pull request as ready for review August 11, 2026 07:22
@cyfung1031 cyfung1031 changed the title Agent 对话可配置最大工具调用次数 + Token 优化 + 循环检测升级 Agent 对话、工具循环、持久化与 MCP 可靠性增强 Aug 11, 2026
@cyfung1031

Copy link
Copy Markdown
Collaborator Author

@CodFrm 你人手实测吧

@cyfung1031 cyfung1031 added the P1 🔥 重要但是不紧急的内容 label Aug 11, 2026
@CodFrm

CodFrm commented Aug 18, 2026

Copy link
Copy Markdown
Member

感觉可以去掉 最大工具调用次数 和相关的内容,其它的我这两天再慢慢看下

CodFrm added 2 commits August 19, 2026 11:43
- 删除 agent_config.ts/agent_config.test.ts(chatMaxIterations 配置与 AgentConfigRepo)
- ToolLoopOrchestrator 循环去掉迭代上限,改为无限循环,依赖 Loop Guard 与用户停止
- 移除 maxIterations 参数(ConversationCreateOptions/AgentTask/子代理/cat_agent/定时任务)
- 移除 Settings 页最大工具调用次数输入与 getAgentConfig/saveAgentConfig API
- 移除达到上限后的继续对话UI 流程(MessageItem/ChatArea/chat_utils)
- 清理相关 i18n key、公开类型声明与测试,更新 architecture-agent 文档
@CodFrm

CodFrm commented Aug 19, 2026

Copy link
Copy Markdown
Member

按评审意见移除了「最大工具调用次数」机制(f83b0793 + c6adf27):

  • 删除 chatMaxIterations 配置(agent_config.ts 整文件、getAgentConfig/saveAgentConfig API、设置页输入框)
  • ToolLoopOrchestrator 改为无限循环,移除 maxIterations 全链路参数(会话/定时任务/子代理/cat_agent 公开类型)及 max_iterations 终态与「继续对话」UI 流程
  • 清理 10 个 locale 的相关 i18n key 与测试;通用用例(错误占位过滤、终态 usage 累计)改用其他错误码保留覆盖

验证:tsc --noEmit、改动文件 ESLint、check:i18n 通过;相关 375 个测试全绿;全量 test:ci 仅有与本次无关的 UI 超时抖动(父提交同样出现)。

⚠️ 一个需要确认的语义变化:删除后定时任务既无 Loop Guard 暂停(无人值守只告警)也无超时,runaway 工具循环不再有硬边界(此前由 maxIterations ?? 10 兜底);子代理仍有 10 分钟超时、UI 会话有 Loop Guard + 用户停止。如不接受该风险,建议后续给定时任务加运行时长上限。

@CodFrm

CodFrm commented Aug 20, 2026

Copy link
Copy Markdown
Member

不应该限制 execute_script 的超大返回结果,这个应该由调用者/LLM自己去决定,可以做引导,但是不应该限制,不然破坏的结果反而产生误导问题;

如果是read文件之类(我有点忘记有没有这种操作了),如果超出一定行数,应该直接拒绝,然后引导llm增加limit操作

裁剪较旧的 tool result,这不对吧,会破坏LLM的cache,cache非常便宜,别越用越贵了

@CodFrm

CodFrm commented Aug 20, 2026

Copy link
Copy Markdown
Member

已按审查意见调整 Agent 上下文策略,提交:d14414ae

本次修改:

  • 删除 execute_script 固定 30,000 字符截断,page/sandbox 均完整保留返回值及结构化结果。
  • 删除正常 Tool Loop 的 40%/60% 阈值裁剪、每 5 个工具轮滑动裁剪、续接历史预裁剪和发送前启发式裁剪。
  • 正常 Tool Loop 不再自动裁掉旧附件,历史保持 append-only,避免反复破坏 Prompt Cache 前缀。
  • 保留手动/自动 Compact 内部的上下文裁剪。
  • 自动 Compact 改为按完整模型 contextWindow 的实际输入占用达到 80% 时触发,不再使用扣除输出预留和安全边际后的 input budget 作为分母。
  • 更新回归测试,覆盖大型 execute_script 结果完整透传、跨过旧裁剪阈值仍完整保留所有 tool result,以及自动 Compact 的 80% 边界。

验证:

  • pnpm run lint:ci:通过(Prettier、TypeScript、i18n、issue templates、ESLint)。
  • pnpm run test:ci:333 个测试文件、3,939 项测试全部通过。
  • git diff --check:通过。

测试日志仍有仓库既有的 Node deprecation、React controlled/uncontrolled 和 i18next 初始化警告,不影响测试通过状态。

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

结论:PR #1549 已解决 Issue #1545 中“50 次硬编码、长 Tool Calling 成本过高、检测到循环后用户无法介入”的主要症状,但没有完整实现 Issue 提议的“语义摘要 + 相似错误检测 + 可修改 Prompt”方案。Issue #1545 当前仍是 Open,并关联 PR #1549Issue #1545

我审计的是本地 pr/1549 最终 ref,HEAD 为 de6c4560

1. 最大工具调用次数:基本已解决

Issue 原问题是:Agent 固定执行 50 次 Tool Calling 后直接失败,用户只能新建对话,之前的上下文会丢失。

PR 的实现:

  • 新增全局 Agent 配置:

    • 默认值:50
    • 最小值:1
    • 最大值:1000
    • storage 读取、保存和运行时参数都会统一归一化

    代码:agent_config.ts

  • Settings 页面新增“最大工具调用次数”输入框,并提示提高数值会增加 Token 成本:

    Agent Settings

  • 达到上限后不再只是普通错误,而是持久化:

    errorCode: "max_iterations"
    
  • UI 根据 max_iterations 显示“继续对话”按钮。

  • 点击继续时发送新的用户消息,并重新加载完整历史继续执行,而不是创建全新对话。

  • 重放历史时会过滤带 error 的占位消息,避免之前的空 assistant 消息再次进入 LLM 请求,尤其避免 Anthropic 因空 content 拒绝请求:

    persisted_messages.ts

这部分已经实质解决了 Issue 的第一个核心痛点。

尚未完全覆盖的地方

  • 没有“无限制运行”选项,只能限制在 1–1000。
  • 配置是全局 Agent 设置,不是每个会话独立保存的 maxIterations。
  • 提高到 1000 后仍可能消耗大量 Token;PR 只有提示,没有真正的费用或 Token budget 上限。

这些属于功能范围上的残留,不影响“50 次硬编码且无法继续”的主要问题已经被修复。

2. Token 消耗:明显改善,但不是完整的语义压缩

Issue 要求:

  1. 只保留最近几轮详细 Tool 结果。
  2. 较早结果用摘要替代,而不是完整保留。
  3. 避免 execute_script、DOM dump 等大结果在每一轮重复发送。

PR 做了几层处理。

2.1 Anthropic Prompt Cache

Anthropic 请求增加 cache_control: { type: "ephemeral" },覆盖 system、tools 和历史末尾消息,使后续请求可以复用已缓存的前缀。

anthropic.ts

这可以降低 Anthropic 的重复输入计费,但它只对支持该机制的 provider 生效,也不会减少实际请求上下文的字节数。

2.2 execute_script 结果截断

execute_script 返回结果超过 30,000 字符时,只保留头尾,并加入:

[truncated ...]

execute_script.ts

这能阻止单次 DOM dump 或模块映射直接撑爆上下文。

2.3 滑动窗口裁剪旧 Tool 结果

PR 新增 context_elision.ts

  • 保留最近 5 个带 tool call 的 assistant turn。

  • 更早的 tool 消息替换成固定占位文本:

    [tool result elided to save context — re-run the tool if you need this data again]
    
  • 发送请求前进行预算估算。

  • 超预算时继续裁剪旧 Tool 结果和旧附件。

  • 视觉模型会按图片像素尺寸估算 Token,而不是简单按文件字节数估算。

context_elision.ts

2.4 自动 Compact

上下文达到约 80% 时,已有的 auto-compact 会调用摘要模型,把整段历史替换成 [Conversation Summary]

最终 PR 又加强了:

  • compact 前的预算检查;
  • attachment 保留;
  • 持久化失败处理;
  • 取消与 compact 的竞态保护。

compact_service.ts

这个部分的核心残留问题

PR 新增的滑动窗口并没有生成“旧 Tool 结果摘要”,而是直接替换成固定 stub。

也就是说:

旧结果 → [tool result elided...]

而不是:

旧结果 → “第 3 轮发现 webpack module 123 使用 AES,key 候选为 xxx”

因此:

  • 早期结果中的关键发现会从上下文中消失;
  • Agent 必须重新执行工具;
  • 重新执行会再次消耗 Token;
  • execute_script 截断后被丢弃的中间内容也不会自动保存到 OPFS;
  • 只有达到 80% 左右触发 auto-compact 后,才会进行整体摘要。

所以它解决的是“上下文和计费增长过快”,但没有完全解决 Issue 要求的“在降低 Token 的同时保留早期分析结论”。

这是目前最重要的未完成部分。

3. Loop Guard:已加入用户干预,但检测能力仍较窄

Issue 希望检测:

  • 基本相同的 execute_script
  • 返回极其相似的错误;
  • 检测后暂停;
  • 询问用户继续、停止或修改 Prompt。

PR 保留并扩展了已有的 tool_call_guard

检测规则包括:

  • 相同工具 + 相同参数重复调用至少 2 次;
  • execute_script 连续返回 null 至少 3 次;
  • 同一个 tab 重复调用 get_tab_content 至少 3 次;
  • 最近 8 次调用中同一工具出现至少 5 次。

tool_call_guard.ts

PR 新增:

  • 连续两次 Loop Guard 告警后暂停;
  • UI 显示 Continue / Stop;
  • 用户选择 Stop 后持久化正常 assistant 消息并以 done 结束;
  • 用户选择 Continue 后重置告警计数;
  • 5 分钟没有回应时默认 Continue;
  • Abort、断线和超时时会清理 pending ask 状态。

tool_loop_orchestrator.ts

chat_service.ts

这个部分尚未解决的核心问题

3.1 没有真正做“相似错误”检测

当前主要是:

  • 参数完全相同;
  • 返回值为 null
  • 工具调用次数过多。

它没有对错误文本、代码结构或结果内容做相似度判断。

例如以下调用可能不会及时触发专门的错误循环检测:

document.querySelector(".a").click()
document.querySelector(".b").click()
document.querySelector(".c").click()

如果三次都返回类似的:

Cannot read properties of null

它们参数不同,因此不会命中“完全相同参数”;只有达到通用调用次数阈值后才可能触发。

这与 Issue 要求的“基本相同的脚本并返回极其相似的错误”仍有差距。

3.2 没有“修改 Prompt”操作

UI 只提供:

  • Continue
  • Stop

没有直接提供:

  • 修改当前 Prompt;
  • 修改脚本;
  • 让用户补充指导后再继续。

用户可以 Stop 后手动发送新消息,但这不是 Loop Guard 弹窗内的 Prompt 修复流程。

3.3 不是所有 Agent 场景都会暂停询问

PR 只对交互式 UI 对话启用询问。

以下场景仍然只发 warning,不暂停:

  • 定时任务;
  • 子代理;
  • scriptUuid 的脚本调用。

这是合理的无人值守保护,但意味着 Issue 的 Loop Guard 方案并未覆盖所有 Agent 执行路径。

4. 最终 PR 还加入了很多可靠性修复

这些不是 Issue #1545 的直接需求,但属于最终 PR HEAD 的重要代码:

  • 取消时正确结束 SSE 和工具调用;
  • 防止 Stop 与 compact 互相覆盖历史;
  • 工具结果持久化采用事务式提交;
  • 处理 storage 写入失败和确认读取失败;
  • 防止不同 conversation generation 之间互相写入;
  • 防止附件被错误删除或泄漏;
  • 处理脚本工具断线、超时和 requestId;
  • 处理生成图片保存失败;
  • 区分 context_too_largemax_iterationscancelled 等结构化错误;
  • 修复继续对话时错误占位消息进入 LLM 上下文的问题;
  • 修复 Loop Guard 超时后残留 pendingAskUser 的问题。

最终 PR HEAD:de6c4560

最终判断

Issue 要求 PR 状态 结论
50 次限制可配置 已完成 默认 50,可调到 1000
达到上限后继续原对话 已完成 复用历史,不需要新建对话
降低长 Tool Loop Token 消耗 基本完成 缓存、截断、滑动窗口、预算检查
旧结果用摘要替代 部分完成 只有整体 auto-compact;滑动窗口本身只是 stub
Loop Guard 部分完成 可检测并暂停,但检测规则不够智能
检测相似错误 未完整完成 没有错误文本或代码相似度分析
允许用户修改 Prompt 未完成 只有 Continue / Stop
无限运行 未完成 最大值为 1000

因此,PR 已经解决了 Issue 的主要可用性和成本问题,但 Issue 中最核心的“保留早期分析语义,同时压缩上下文”和“识别相似失败模式并帮助用户修正 Prompt”仍未完全解决。

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

Labels

P1 🔥 重要但是不紧急的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 建议暴露 AI Agent 最大工具调用限制并优化多轮 Tool Calling 下的 Token 消耗

2 participants