Skip to content

feat(context): Tier1 迷你卡 + 收紧 handle 与 host metadata 契约 - #30

Merged
zhanghanduo merged 2 commits into
mainfrom
feat/tier1-mini-card-and-recovery-contract
Sep 4, 2026
Merged

zhanghanduo merged 2 commits into
mainfrom
feat/tier1-mini-card-and-recovery-contract

Conversation

@zhanghanduo

@zhanghanduo zhanghanduo commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

四件事,同一条线:Tier 1 丢掉的东西要么别丢,要么留得住,而「留得住」依赖 handle 语义统一。第 4 件和末尾的 CI 检查,是这次讨论里发现的一类静默缺口的修复与防复发。

1. Tier1 占位符换成带 args + 来源 URL 的迷你卡

KeepLastNToolResultsCompactor 原先把老 tool body 整条换成占位符,丢掉调用 args 和 body 里所有 URL —— 恰好是后续轮次判断「这条 query 我是不是已经跑过」最需要的两样。Tier 2 的摘要保留这两样,但 Tier 2 只在 Tier 1 没减够时才触发,所以 Tier1-only 的轮次里模型看到的严格更少

两个字段都是免费的:args 在请求方 assistant message 上,URL 在即将被丢弃的 body 里。没有 LLM 调用,没有额外存储,也没有第二套模型可见的恢复路径 —— 卡片只说「调了什么」,不提供取回手段(那仍归下面的 recovery 脚注)。

Tool result dropped to save tokens. You already read it; ...
[Called: web_search({"query": "NVIDIA H100 market share 2025"})]
[Source URLs] https://nvidianews.nvidia.com/q3 | https://tomshardware.com/h100
[Full text] /spill/ab/cd1234

实测 +218 字符/条,硬上限 400 字符、URL 上限 3 条。50 条 placeholder 的长跑约 +2.7K token。

placeholder 仍是第一行,所以既有的 startswith 幂等判据一字不改继续成立。

「卡片不比原文短就保留原文」仅当没配 spill。 有 handle 一律替换,即使卡片更长:handle 只能经 spill_refs → Tier 2 recovery index 到达模型,留着 body 反而让上游 spill 掉的全文永久不可恢复,而那种 body 本身已经是截断预览。

这条判据在本仓库的作用域比同源实现窄一档:配了 spill 而被拒收时,既有分支已经无条件保留 body(任意大小),所以这里不需要额外的最小尺寸门槛。新增 test_declined_spill_keeps_the_body_at_any_size 锁住它依赖的那条既有分支——否则以后有人对照同源实现会以为漏了。

顺带:KeepLastNToolResultsCompactor 此前没有任何直接测试,只被 tiered 用例借道覆盖过两次。

2. handle 解析改为 spill_refs 优先于 result_store_ref

spill_refs 描述的是消息上当前还在的内容,result_store_ref 描述的是上游截断前的 body。先读后者会把已存过的 body 再存一遍,更糟的是把错的 handle 钉进 recovery index。第 1 条的正确性判据依赖这个语义。

3. manifest 上限可配置

manifest_max_paths / manifest_max_chars,任一可为 None 解除。默认值不变(20 / 3,000)。

那是按「handle 渲染成文件路径」(60+ 字符/条)定的尺寸。handle 是短内容寻址 id 的产品每条只花约 17 字符,应当自己放开:上限一旦生效丢的是最旧的 handle,而实测过决定性的早期证据正因为它的 handle 老化掉,在一段无关的长弯路之后变得不可恢复。

上限按渲染后字符数计,因为那是两种 handle 形态唯一共享的量 —— 也正因如此,上限和 handle 形态不是可独立选择的两件事。这让两种 handle 形态可以共存于同一个核:数据层统一存 id,呈现层按 visible_root 投影成路径或 id,上限按投影后长度算。

4. ToolResult.host_metadata:产品自述 metadata 原样透传

result_metadata 返回的东西原样带到 render_tool_result。这是产品自己给模型措辞「这个调用重复了」所需的接缝:算不算重复是 per-tool 的产品策略(repeat_count),正文是否字节相同是另一件始终实测的事实、没有对应字段。两者缺一就会对着一个实际不同的 body 断言 "identical output" —— 一句模型能对着自己 history 验证的谎言。

透传是原样而非「AgentCore 不认识的剩余 key」:剩余语义会在这里新增保留 key 的那天悄悄改变产品能看到的东西,正是下面这条检查存在的理由。

顺带:把「静默无消费者」变成硬失败

scripts/check_unconsumed_fields.py 进 CI —— 被监视模型上的字段,若在 agent_core/ 内没有任何属性读取,必须在某个 docs/*-boundary.md 里被点名。

这条规则不是「给字段写文档」,而是:决定不消费某个字段本身是一个边界决策,没写下来的边界决策和疏漏无法区分。

0.4.0 有四个字段正是这个状态 —— error_kind / result_id / repeat_count / repeat_recovery_id,类型对、测试过、pyright 干净、零消费者,而它们在产品侧的消费者全留在产品自己那份 loop 拷贝里。产品换用 run_agent_loop 会丢掉据此措辞的提示,且静默丢掉:不抛异常,不挂测试,模型原先读到的那句话直接消失。

canary merge 看不见这一类,因为它只能发现硬冲突(import 失败、属性缺失、需要 monkeypatch)。

脚本写完立刻抓到本次新增的 host_metadata,已按规则补进 boundary doc。

版本

0.6.0 → 0.7.0。改了模型读到的文本,按 docs/versioning.md「compaction or trimming decisions」属于 breaking,走 MINOR。

本分支先后编过 0.5.0 和 0.6.0,两次都在 rebase 时发现号已被并行的抽取 PR(#28#29)占用。CHANGELOG 各段并存,别人的段原样保留。

顺带按 #29 立下的惯例,给 0.6.0 段补了 Never published 标注:tag 只到 v0.4.0,0.5.0 与 0.6.0 都只在 main 上、从未发布,没有东西能 pin 它们。

消费者须知

  • 断言 == OMITTED_TOOL_RESULT_PLACEHOLDER 的要改成 startswith(placeholder 保持首行正是为了这个判据继续可用)。
  • 在自己 loop 拷贝里据 repeat_count / repeat_recovery_id / result_id / error_kind 措辞的产品,换用 run_agent_loop 前必须把那段话搬进 render_tool_result AgentCore 一个都不读,忘了不会失败,只是不再到达模型。

测试

@zhanghanduo
zhanghanduo force-pushed the feat/tier1-mini-card-and-recovery-contract branch from 2eb1766 to 69b2035 Compare September 4, 2026 07:24
四件事,同一条线:Tier 1 丢掉的东西要么别丢,要么留得住,而"留得住"
依赖 handle 语义统一。

`KeepLastNToolResultsCompactor` 原先把老 tool body 整条换成占位符,丢掉调用
args 和 body 里所有 URL —— 恰好是后续轮次判断「这条 query 我是不是已经跑过」
最需要的两样。Tier 2 的摘要保留这两样,但 Tier 2 只在 Tier 1 没减够时才触发,
所以 Tier1-only 的轮次里模型看到的严格更少。

两个字段都是免费的:args 在请求方 assistant message 上,URL 在即将被丢弃的
body 里。没有 LLM 调用,没有额外存储,也没有第二套模型可见的恢复路径 ——
卡片只说"调了什么",不提供取回手段(那仍归下面的 recovery 脚注)。
实测 +218 字符/条,硬上限 400,URL 上限 3 条。

placeholder 仍是第一行,所以既有的 `startswith` 幂等判据一字不改继续成立。

卡片不比原文短就保留原文 —— 但**仅当没配 spill**。有 handle 一律替换,即使
卡片更长:handle 只能经 `spill_refs` → Tier 2 recovery index 到达模型,留着
body 反而让上游 spill 掉的全文永久不可恢复,而那种 body 本身已经是截断预览。
本仓库这条判据的作用域比同源实现窄一档:配了 spill 而被拒收时,既有分支已经
无条件保留 body,所以这里不需要额外的最小尺寸门槛。新增测试锁住它依赖的那条
既有分支。

`KeepLastNToolResultsCompactor` 此前没有任何直接测试,只被 tiered 用例借道
覆盖过两次;连同这次改动补 14 条。

`spill_refs` 描述的是消息上**当前还在**的内容,`result_store_ref` 描述的是
上游截断前的 body。先读后者会把已存过的 body 再存一遍,更糟的是把错的 handle
钉进 recovery index。

`manifest_max_paths` / `manifest_max_chars`,任一可为 `None` 解除。默认值不变
(20 / 3000),那是按「handle 渲染成文件路径」(60+ 字符/条) 定的尺寸。handle 是
短内容寻址 id 的产品每条只花约 17 字符,应当自己放开:上限一旦生效丢的是**最旧**
的 handle,而实测过决定性的早期证据正因为它的 handle 老化掉而在一段无关长弯路
之后不可恢复。

上限按**渲染后字符数**计,因为那是两种 handle 形态唯一共享的量 —— 也正因如此,
上限和 handle 形态不是可独立选择的两件事。

`result_metadata` 返回的东西原样带到 `render_tool_result`。这是产品自己给模型
措辞「这个调用重复了」所需的接缝:**算不算重复**是 per-tool 的产品策略
(`repeat_count`),**正文是否字节相同**是另一件始终实测的事实、没有对应字段,
而两者缺一就会对着一个实际不同的 body 断言 "identical output" —— 模型能对着
自己 history 验证的谎言。

透传是**原样**而非"AgentCore 不认识的剩余 key":剩余语义会在这里新增保留 key
的那天悄悄改变产品能看到的东西,正是下面这条检查存在的理由。

`scripts/check_unconsumed_fields.py` 进 CI —— 被监视模型上的字段,若在
`agent_core/` 内没有任何属性读取,必须在某个 `docs/*-boundary.md` 里被点名。

这条规则不是"给字段写文档",而是:**决定不消费某个字段本身是一个边界决策**,
没写下来的边界决策和疏漏无法区分。0.4.0 有四个字段正是这个状态 ——
类型对、测试过、pyright 干净,零消费者,而它们在产品侧的消费者全留在产品自己
那份 loop 拷贝里。产品换用 `run_agent_loop` 会丢掉据此措辞的提示,且**静默**
丢掉:不抛异常,不挂测试,模型原先读到的那句话直接消失。canary merge 看不见
这一类,因为它只能发现硬冲突。

脚本写完立刻抓到本次新增的 `host_metadata`,已按规则补进 boundary doc。

0.4.0 → 0.5.0。改了模型读到的文本,按 `docs/versioning.md` 属于
compaction-decision 变更,即 breaking,走 MINOR。

- 新增 `tests/test_keep_last_n_compactor.py` 14 条
- `tests/test_tiered_compact.py` +2(上限可配置 / 丢最旧)
- `tests/test_tool_exec.py` +2(透传原样、默认空且不共享实例)
- `uv run pytest -q` → 1164 passed
- ruff / pyright / check_unconsumed_fields / uv build 全过

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zhanghanduo
zhanghanduo force-pushed the feat/tier1-mini-card-and-recovery-contract branch from 69b2035 to 3793a96 Compare September 4, 2026 07:49
@zhanghanduo
zhanghanduo merged commit f938c92 into main Sep 4, 2026
5 checks passed
@zhanghanduo
zhanghanduo deleted the feat/tier1-mini-card-and-recovery-contract branch September 4, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant