docs/design/tool-stub-design.md
范围: OpenViking 当前 tool stub 能力的实现说明,覆盖类型识别、规则化摘要、原文外置、回溯读取与测试边界。
状态: 已实现,本文档描述当前代码行为,不额外引入新需求。
OpenViking 原生已经支持 tool result preview。原有链路能够在 session 写入阶段把过大的 tool output externalize,并在 ToolPart 中留下一个 preview stub,同时保留 ref 供后续回溯。
这次工作的重点不是新建 externalize 机制,而是在现有能力上优化 preview 的生成方式:从偏 head + tail 的直接截断,升级为按内容类型输出更稳定、更可读的规则化摘要。
换句话说,本次改动保持下面这些基础能力不变:
ToolResultStore。ToolPart 仍保留 stub 和 tool_output_ref。read/search/list。这次变化主要集中在 preview 生成层:
ToolPart.tool_output 替换成 stub 文本,并保留 tool_output_ref。read/search/list 工具按 ref 回溯原文。text 类型只做规则化摘要,不接 LLM。
head + tail 的截断 preview,升级为按类型输出的规则化摘要。入口在 session.py 的 _externalize_large_tool_output_group()。
当前按两类条件触发:
threshold_chars。assistant_turn_inline_budget_chars。命中后会进入 externalization 流程。触发阈值仍沿用 OV 原有配置;规则化摘要本身不改变“什么时候 externalize”。
入口在 session.py 的 _externalize_tool_part()。
这一步会:
tool_output 写入 ToolResultStore。ToolPart.tool_output。ToolPart.tool_output_ref。因此被 stub 后,消息里保留的是 preview,不再是完整原始 tool result;原始结果仍在外置存储里。
入口在 tool_result_store.py 的 make_preview():
generate_tool_result_synopsis() 负责类型识别和摘要生成。render_tool_result_stub() 负责把摘要渲染成最终 stub 文本。核心实现位于 tool_result_synopsis.py。
当前原则:
preview_chars 控制摘要长度。preview_chars 只作为无法规则化时的 fallback head/tail 采样预算,并作为兼容字段保留在 stub header / metadata 中。当前回溯能力由 session 暴露三类工具:
read_tool_result():按 offset/limit 读取原始内容片段。search_tool_result():在原始内容中做关键字搜索。list_tool_results():列出当前 session 已 externalize 的结果。对应存储层实现位于 tool_result_store.py。
当前支持的 synopsis kind 定义在 tool_result_synopsis.py:
json / csv / tsv / yaml / xml / code / text / unknown
| 类型 | 识别方式 | 处理办法 | stub 中保留内容 |
|---|---|---|---|
json | MIME 含 json,或内容以 { / [ 开头且能被 JSON decoder 解析 | 解析 top-level shape,提取 keys、array length、标量示例 | summary + structure + notable_items |
csv | 含逗号,且能按 CSV 读成规则表格 | 统计行列数、列名,保留首条数据样例 | summary + structure |
tsv | 含制表符,且能按 TSV 读成规则表格 | 统计行列数、列名,保留首条数据样例 | summary + structure |
yaml | 满足 YAML 启发式并能 yaml.safe_load() 成 dict/list | 提取 top-level keys 和 child type | summary + structure |
xml | MIME 含 xml,或内容以 < 开头且可解析 | 提取 root tag、属性数、子标签计数 | summary + structure |
code | 命中代码模式正则 | 提取 imports、symbols、line_count | summary + structure + notable_items |
text | 作为最终 fallback | 规则化文本摘要,不保留全文 sample | summary |
unknown | 空内容、binary-like 内容,或带明确 MIME 但解析失败的结构化内容 | 无法规则化时使用 fallback head/tail sample | summary + sample |
识别顺序定义在 tool_result_synopsis.py。
当前顺序如下:
unknown。unknown。jsonxmltsvcsvyamlcodetext这个顺序的目的是优先识别结构化格式,再识别代码,最后才把剩余内容视作普通文本。日志样式输出不再作为独立类型处理,会走 text,与 lossless-claw 的 large-file exploration 行为保持一致。
实现位于 tool_result_synopsis.py。
输出重点:
trailing_chars_after_first_json_value。实现位于 tool_result_synopsis.py。
输出重点:
只接受“列数基本一致”的表格;不规则分隔文本不会被误判成表格。
实现位于 tool_result_synopsis.py 与 tool_result_synopsis.py。
输出重点:
实现位于 tool_result_synopsis.py。
输出重点:
实现位于 tool_result_synopsis.py。
输出重点:
class Foo、def bar、fn baz,最多 24 条,单条最多 200 字符。当前是轻量规则识别,不做 AST 级代码摘要。
实现位于 tool_result_synopsis.py。
text 类型明确不接 LLM,只做 deterministic fallback。摘录采用固定上限,不受 preview_chars 影响。
日志样式输出也归入 text。如果需要定位错误/警告行,优先通过 stub 中的 ref 使用 openviking_tool_result_search 搜索原始 payload,避免仅靠关键字把普通文档误判成日志。
输出重点:
CharactersWordsLinesDetected section headersOpening excerptClosing excerpt标题提取规则:
# HeadingSYSTEM STATUS摘录规则:
sample 字段,避免把完整原文重新带回上下文。unknown 是当前实现里的保守分类,不是富类型支持。
当前会落到 unknown 的场景包括:
对这些内容,stub 保留基础说明;非空内容会使用 preview_chars 生成 head/tail fallback sample。原始 payload 仍通过 ref 回溯。
渲染逻辑位于 tool_result_synopsis.py。
当前 stub 由两部分组成:
包含:
tool_namekindoriginal_charspreview_charsrefsha256reason按 synopsis 内容选择性渲染:
SynopsisStructureNotable itemsSampleExplore其中 Explore 会提示模型使用:
openviking_tool_result_searchopenviking_tool_result_readopenviking_tool_result_list实现位于 tool_result_store.py。
每个 externalized tool result 会写入两类文件:
output.txt:原始输出内容。metadata.json:元数据和 synopsis。元数据包括:
tool_result_idsession_idmessage_idtool_idtool_nameagent_idcreated_atoriginal_charspreview_charssha256mime_typesynopsis_kindsynopsisstorage_urioutput_urioffset_unit=unicode_code_pointread():按 offset/limit 读取原始文本片段,适合长文本逐段展开。search():在原始文本中查关键词,并返回带 offset 的 snippet。list():按 session 列出已有外置结果,便于发现 ref。当前读取模型是“面向长文本”的;它适合回看日志、代码、表格文本、普通文本。
text 不接 LLM,原因是我们当前只需要稳定、低成本、可测试的规则化 stub。read/search/list,更适合大文本原文回看。offset/limit 对“顺序文本展开”很合适,但对图片、二进制、复杂多模态结果并不理想。当前相关测试包括:
read/list/search 回溯,以及 synopsis_kind / synopsis.kind 元数据透出。当前相关测试共 29 个用例通过,可作为后续继续补齐真实输出回归用例的基础。
OpenViking 当前的 tool stub 已经具备一版完整闭环:
head + tail 的截断,升级为按类型的规则化摘要。text 类型采用 deterministic 规则摘要,不接 LLM。read/search/list 对外置原文进行回溯。后续优先级应放在更深的回归测试、更多真实输出样本,以及是否需要继续优化 read/search/list 的原文回溯体验,而不是先扩展 LLM 摘要或媒体解析。