feat(context): Tier1 迷你卡 + 收紧 handle 与 host metadata 契约 - #30
Merged
Merged
Conversation
zhanghanduo
force-pushed
the
feat/tier1-mini-card-and-recovery-contract
branch
from
September 4, 2026 07:24
2eb1766 to
69b2035
Compare
四件事,同一条线: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
force-pushed
the
feat/tier1-mini-card-and-recovery-contract
branch
from
September 4, 2026 07:49
69b2035 to
3793a96
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
四件事,同一条线: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 脚注)。
实测 +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_refspill_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 保持首行正是为了这个判据继续可用)。repeat_count/repeat_recovery_id/result_id/error_kind措辞的产品,换用run_agent_loop前必须把那段话搬进render_tool_result。 AgentCore 一个都不读,忘了不会失败,只是不再到达模型。测试
tests/test_keep_last_n_compactor.py14 条tests/test_tiered_compact.py+2(上限可配置 / 丢最旧)tests/test_tool_exec.py+2(透传原样、默认空且不共享实例)uv run pytest -q→ 1411 passed(含 feat: extract the write-audit cycle and verifier contracts into AgentCore #28 / feat: extract the portable middleware layer into AgentCore #29 带来的 247 条)ruff check agent_core tests scripts/pyright agent_core/check_unconsumed_fields.py/check_version_bump.py/uv build全过test/version-bump/ CodeQL)