From 40012a6f11b103533a5bd50ee3cec0aebe2bea50 Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Sun, 6 Sep 2026 12:11:46 +0800 Subject: [PATCH 1/5] =?UTF-8?q?perf(context):=20Tier=201=20=E8=BF=B7?= =?UTF-8?q?=E4=BD=A0=E5=8D=A1=E6=8C=89=E5=AE=9E=E6=B5=8B=E6=94=B6=E7=AA=84?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20=E6=97=A0=E6=9D=A5=E6=BA=90=E5=8F=AA?= =?UTF-8?q?=E7=95=99=E5=B7=A5=E5=85=B7=E5=90=8D=EF=BC=8CURL=20=E4=B8=8A?= =?UTF-8?q?=E9=99=90=203=E2=86=921?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 ApodexHarness 的 12 个真实长跑研究 trial(1337 条被压缩结果、约 13.5k 个 即将被丢弃的来源 URL)上量了卡片的成本与收益,两处按数据收窄。 **URL 上限 3 → 1。** 留 3 个使「URL 总保留率」达到 16.5%,但下游唯一消费的量是 「一次检索有没有留下一个可追溯的来源」—— 那个由第一个 URL 就达成, 769/775 = 99.2%。降到 1 后总保留率 7.8%,而 99.2% 一字未变:多留的两个 URL 每个约 120 字符,买的是一个没有读者的百分比。 **无来源的结果只留工具名。** 1337 条里 435 条(33%)根本没有来源 —— shell 命令、 任务板更新、写文件。卡片存在的前提是「不要重做那些出处还看得见的工作」, 没有来源这个前提就不成立,而且重复这类调用通常是合法的(它读的状态变了)。 那些 args 不构成决策信息,不值 120 字符的预览。 判据是「正文无 URL **且** args 无 URL」,不是单看正文:来源可以在参数里而不在 正文里(web_fetch 的参数本身就是 URL),只读正文会把 web_fetch 唯一的来源剥掉。 **Consumer impact:** 需要「每个论断多个独立来源」的 host 现在应当显式抬高 `_MINI_CARD_MAX_URLS`,而不是继承 3。其余不变:卡片仍然说明调用、仍然在有来源时 带上来源,placeholder / footer 契约完全未动。 该样本上的成本:卡片在 12 个 trial 上共增加约 49.6k tokens,在激进的 `keep_last_k=5` 下占压缩后 context 的 25.0%;而产品实际使用的阈值触发式 `tiered` 路径压缩后 context 在 150–200k,同样的卡片占 2–3%。 Tests: 4 条既有断言随行为更新(有来源才谈 args、URL 上限 1、超长与多行 args 改用带来源的调用),新增 4 条(无来源只留工具名、来源在 args 里仍出完整卡片、 无来源卡片长度不足有来源的一半、URL-heavy 正文也只留 1 个); `pytest tests/ -q` → 1485 passed;`ruff check` → 0 error。 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 37 +++++++++++++++ agent_core/runtime/loop/compact.py | 28 ++++++++++- tests/test_keep_last_n_compactor.py | 73 +++++++++++++++++++++++++---- 3 files changed, 127 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e59953..d9d288d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,43 @@ the GitHub Release body, so a release with no entry here fails. Versioning follows [docs/versioning.md](docs/versioning.md). +## [Unreleased] + +### Changed + +- Tier 1 mini card narrowed on measurement, in two places. + `_MINI_CARD_MAX_URLS` drops 3 -> 1, and a result carrying no source anywhere + gets `[Called: ]` alone instead of a 120-char argument preview. + + **Consumer impact:** a host that needs several independent sources per claim + should now raise `_MINI_CARD_MAX_URLS` deliberately rather than inherit 3. + Nothing else changes: cards still carry the call, still carry a source when one + exists, and the placeholder/footer contract is untouched. + + Rationale, measured over 12 real long-running research trials in ApodexHarness + (1337 carded results, ~13.5k source URLs in the bodies being discarded): + + - Keeping 3 URLs retained 16.5% of all URLs, but the only quantity anything + downstream consumes is whether a retrieval left *one* traceable source + behind, and the first URL alone covers 769/775 (99.2%) of cards that had any + URL. Dropping to 1 took total retention to 7.8% and left that 99.2% + unchanged - the extra two URLs were spending ~120 chars each on a percentage + with no reader. + - 435 of 1337 cards (33%) had no source at all - shell commands, task-board + updates, file writes. The card exists so a later turn does not redo work + whose provenance it can still see; without a source that premise does not + hold, and repeating such a call is usually legitimate because the state it + reads has changed. Those arguments are not decision information. + + A source can live in the arguments rather than the body (`web_fetch`'s argument + IS the url), so the sourceless test is "no URL in the body **and** none in the + arguments" - reading only the body would strip `web_fetch` of its one source. + + Cost on that sample: cards add ~49.6k tokens across the 12 trials, 25.0% of the + post-compaction context under an aggressive `keep_last_k=5`. Under the + threshold-triggered `tiered` path a product actually ships, post-compaction + context is 150-200k, putting the same cards at 2-3%. + ## [0.8.1] - 2026-09-05 ### Fixed diff --git a/agent_core/runtime/loop/compact.py b/agent_core/runtime/loop/compact.py index 4f1c6cb..297d5d6 100644 --- a/agent_core/runtime/loop/compact.py +++ b/agent_core/runtime/loop/compact.py @@ -162,7 +162,16 @@ def tool_names_by_call_id(messages: list[Message]) -> dict[str, str]: _MINI_CARD_ARGS_MAX_CHARS = 120 _MINI_CARD_BODY_MAX_CHARS = 400 -_MINI_CARD_MAX_URLS = 3 +# ONE url, not three. Measured over 12 real long-running trials of a research +# agent (1432 carded results, 13.5k source URLs in the bodies being discarded): +# keeping 3 URLs retained 16.5% of all URLs, but the quantity anything downstream +# consumes is whether a retrieval left behind *one* traceable source — and the +# first URL alone covers 756/761 (99.3%) of the carded results that had any URL. +# Dropping to 1 took total retention to 8.2% and left that 99.3% unchanged, i.e. +# the extra two URLs per card were spending ~120 chars each on a percentage with +# no reader. A host that needs several independent sources per claim should raise +# this deliberately rather than inherit it. +_MINI_CARD_MAX_URLS = 1 _WHITESPACE_RE = re.compile(r"\s+") @@ -215,7 +224,21 @@ def _elided_tool_card(tool_name: str, args_preview: str, content: str) -> str: Returns ``""`` when there is nothing worth saying (no name, no arguments, no URLs), so the caller falls back to the bare placeholder rather than emitting an empty line. + + A result carrying no source at all gets the tool name only. The card exists so + a later turn does not redo work whose provenance it can still see, and that + premise needs a source: for a body with no URL — a shell command, a task-board + update, a file write — repeating the call is usually legitimate, because the + state it reads has changed. Such arguments are not decision information and do + not earn a 120-char preview. Measured over the same 12 trials, 523 of 1432 + carded results (37%) had no source and were charging roughly a third of the + feature's cost for none of its benefit. """ + # A source can live in the arguments rather than the body: web_fetch's argument + # IS the url. So "no URL in the body" must not be read as "no source". + if not URL_RE.search(content) and not URL_RE.search(args_preview): + return f"[Called: {tool_name}]" if tool_name else "" + lines: list[str] = [] if tool_name or args_preview: call_line = ( @@ -581,7 +604,8 @@ class KeepLastNToolResultsCompactor: Keeps the last ``keep_tool_result`` tool results verbatim and replaces the content of every earlier one with :data:`OMITTED_TOOL_RESULT_PLACEHOLDER` followed by a bounded card naming the call (tool + arguments preview) and up - to :data:`_MINI_CARD_MAX_URLS` source URLs found in the discarded body, then + to :data:`_MINI_CARD_MAX_URLS` source URLs found in the discarded body (a + result with no source anywhere gets the tool name alone), then the recovery pointer when the body was spilled. The card is free — both fields already exist in the history and in the body — and it is what keeps a later turn from re-issuing a query whose result it can no longer see. When no diff --git a/tests/test_keep_last_n_compactor.py b/tests/test_keep_last_n_compactor.py index d31973c..56f6ff5 100644 --- a/tests/test_keep_last_n_compactor.py +++ b/tests/test_keep_last_n_compactor.py @@ -55,7 +55,7 @@ def _card_of(content: str) -> str: def test_card_names_the_call_and_its_arguments(): - body = "RESULT " + "x" * 2_000 + body = "RESULT https://example.com/nvda " + "x" * 2_000 args = '{"query": "NVIDIA H100 market share 2025"}' content = _blanked(_one_call("web_search", args, body)) assert "[Called: web_search(" in content @@ -66,8 +66,10 @@ def test_card_carries_source_urls_from_the_discarded_body(): body = "see https://nvidianews.nvidia.com/q3 and https://tomshardware.com/h100 " + "x" * 2_000 content = _blanked(_one_call("web_search", '{"query": "h100"}', body)) assert "[Source URLs]" in content + # One traceable source per retrieval is the whole requirement, so the first + # URL is kept and extras are not bought (see _MINI_CARD_MAX_URLS). assert "https://nvidianews.nvidia.com/q3" in content - assert "https://tomshardware.com/h100" in content + assert content.count("https://") == 1 def test_url_already_in_the_arguments_is_not_repeated(): @@ -96,8 +98,10 @@ def test_exact_rendered_card_stays_within_budget(): def test_overlong_arguments_are_truncated(): - args = '{"command": "' + "a" * 500 + '"}' - content = _blanked(_one_call("bash", args, "OUT " + "x" * 2_000)) + args = '{"query": "' + "a" * 500 + '"}' + content = _blanked( + _one_call("web_search", args, "OUT https://example.com/x " + "x" * 2_000) + ) call_line = _card_of(content).splitlines()[0] assert "…" in call_line assert len(call_line) < _MINI_CARD_ARGS_MAX_CHARS + 60 @@ -105,12 +109,18 @@ def test_overlong_arguments_are_truncated(): def test_multiline_arguments_are_flattened_to_one_line(): - args = '{"command": "cat < Date: Sun, 6 Sep 2026 12:21:23 +0800 Subject: [PATCH 2/5] fix(compaction): preserve URLs beyond argument preview --- CHANGELOG.md | 2 +- agent_core/runtime/loop/compact.py | 61 +++++++++++++++++++++-------- pyproject.toml | 2 +- tests/test_keep_last_n_compactor.py | 11 ++++++ uv.lock | 2 +- 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9d288d..abb059e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ the GitHub Release body, so a release with no entry here fails. Versioning follows [docs/versioning.md](docs/versioning.md). -## [Unreleased] +## [0.9.0] - 2026-09-06 ### Changed diff --git a/agent_core/runtime/loop/compact.py b/agent_core/runtime/loop/compact.py index 297d5d6..ac65a53 100644 --- a/agent_core/runtime/loop/compact.py +++ b/agent_core/runtime/loop/compact.py @@ -163,11 +163,11 @@ def tool_names_by_call_id(messages: list[Message]) -> dict[str, str]: _MINI_CARD_ARGS_MAX_CHARS = 120 _MINI_CARD_BODY_MAX_CHARS = 400 # ONE url, not three. Measured over 12 real long-running trials of a research -# agent (1432 carded results, 13.5k source URLs in the bodies being discarded): +# agent (1337 carded results, 13.5k source URLs in the bodies being discarded): # keeping 3 URLs retained 16.5% of all URLs, but the quantity anything downstream # consumes is whether a retrieval left behind *one* traceable source — and the -# first URL alone covers 756/761 (99.3%) of the carded results that had any URL. -# Dropping to 1 took total retention to 8.2% and left that 99.3% unchanged, i.e. +# first URL alone covers 769/775 (99.2%) of the carded results that had any URL. +# Dropping to 1 took total retention to 7.8% and left that 99.2% unchanged, i.e. # the extra two URLs per card were spending ~120 chars each on a percentage with # no reader. A host that needs several independent sources per claim should raise # this deliberately rather than inherit it. @@ -188,13 +188,20 @@ def _args_preview(raw: object) -> str: return collapsed[: _MINI_CARD_ARGS_MAX_CHARS - 1] + "\u2026" -def _tool_args_by_call_id(messages: list[Message]) -> dict[str, str]: - """Map ``tool_call_id`` → bounded preview of the arguments it was sent. +def _tool_args_by_call_id( + messages: list[Message], +) -> tuple[dict[str, str], dict[str, str]]: + """Map ``tool_call_id`` to its bounded preview and first source URL. Kept private, unlike :func:`tool_names_by_call_id`: no product facade resolves arguments by call id, so there is no older spelling to honour. + + Source detection reads the complete rendered arguments before the preview is + truncated. Otherwise a URL after character 120 would make a sourced call look + sourceless and lose both its arguments and its only traceable source. """ - out: dict[str, str] = {} + previews: dict[str, str] = {} + source_urls: dict[str, str] = {} for msg in messages: if not is_assistant_msg(msg): continue @@ -214,11 +221,20 @@ def _tool_args_by_call_id(messages: list[Message]) -> dict[str, str]: continue preview = _args_preview(raw) if preview: - out[tid] = preview - return out - - -def _elided_tool_card(tool_name: str, args_preview: str, content: str) -> str: + previews[tid] = preview + rendered = raw if isinstance(raw, str) else str(raw or "") + source_match = URL_RE.search(rendered) + if source_match is not None: + source_urls[tid] = source_match.group(0) + return previews, source_urls + + +def _elided_tool_card( + tool_name: str, + args_preview: str, + args_source_url: str, + content: str, +) -> str: """Render the card lines that stand in for a discarded tool body. Returns ``""`` when there is nothing worth saying (no name, no arguments, no @@ -230,13 +246,14 @@ def _elided_tool_card(tool_name: str, args_preview: str, content: str) -> str: premise needs a source: for a body with no URL — a shell command, a task-board update, a file write — repeating the call is usually legitimate, because the state it reads has changed. Such arguments are not decision information and do - not earn a 120-char preview. Measured over the same 12 trials, 523 of 1432 - carded results (37%) had no source and were charging roughly a third of the + not earn a 120-char preview. Measured over the same 12 trials, 435 of 1337 + carded results (33%) had no source and were charging roughly a third of the feature's cost for none of its benefit. """ # A source can live in the arguments rather than the body: web_fetch's argument - # IS the url. So "no URL in the body" must not be read as "no source". - if not URL_RE.search(content) and not URL_RE.search(args_preview): + # IS the url. Inspect the URL extracted from the full arguments, not their + # bounded preview, because truncation can hide the only source. + if not URL_RE.search(content) and not args_source_url: return f"[Called: {tool_name}]" if tool_name else "" lines: list[str] = [] @@ -264,6 +281,13 @@ def _elided_tool_card(tool_name: str, args_preview: str, content: str) -> str: urls = candidate_urls if urls: lines.append("[Source URLs] " + " | ".join(urls)) + elif args_source_url and args_source_url not in args_preview: + # The argument preview can truncate before or inside its URL. If the body + # has no source to retain instead, carry the complete argument URL on its + # own line so the card still contains one traceable source. + candidate_lines = [*lines, "[Source URLs] " + args_source_url] + if len("\n".join(candidate_lines)) <= _MINI_CARD_BODY_MAX_CHARS: + lines = candidate_lines return "\n".join(lines) @@ -672,7 +696,7 @@ def compact( # Names and arguments are needed unconditionally now: the mini card names # the call it replaced even when nothing is protected and nothing spills. id_to_name = tool_names_by_call_id(messages) - id_to_args = _tool_args_by_call_id(messages) + id_to_args, id_to_arg_url = _tool_args_by_call_id(messages) out: list[Message] = [] for idx, msg in enumerate(messages): @@ -704,7 +728,10 @@ def compact( out.append(msg) continue card = _elided_tool_card( - id_to_name.get(call_id, ""), id_to_args.get(call_id, ""), content, + id_to_name.get(call_id, ""), + id_to_args.get(call_id, ""), + id_to_arg_url.get(call_id, ""), + content, ) if card: placeholder += "\n" + card diff --git a/pyproject.toml b/pyproject.toml index 0371fde..065d0c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "apodex-agent-core" -version = "0.8.1" +version = "0.9.0" description = "Shared, product-neutral runtime primitives for Apodex agents" readme = "README.md" license = "Apache-2.0" diff --git a/tests/test_keep_last_n_compactor.py b/tests/test_keep_last_n_compactor.py index 56f6ff5..e4c85a1 100644 --- a/tests/test_keep_last_n_compactor.py +++ b/tests/test_keep_last_n_compactor.py @@ -338,6 +338,17 @@ def test_source_in_the_arguments_still_earns_a_full_card(): assert content.count(url) == 1 +def test_source_after_the_argument_preview_limit_is_still_retained(): + """Source detection must inspect raw arguments, not the truncated preview.""" + url = "https://example.com/source-after-long-metadata" + args = '{"metadata": "' + "x" * 140 + f'", "url": "{url}"}}' + body = "page text with no links whatsoever " + "x" * 2_000 + content = _blanked(_one_call("web_fetch", args, body)) + assert "[Called: web_fetch(" in content + assert f"[Source URLs] {url}" in content + assert content.count(url) == 1 + + def test_sourceless_card_costs_far_less_than_a_sourced_one(): """The cost reduction is the point of the narrowing, so assert it directly.""" long_args = '{"q": "' + "a" * 300 + '"}' diff --git a/uv.lock b/uv.lock index 167511e..74c0d0e 100644 --- a/uv.lock +++ b/uv.lock @@ -50,7 +50,7 @@ wheels = [ [[package]] name = "apodex-agent-core" -version = "0.8.1" +version = "0.9.0" source = { editable = "." } dependencies = [ { name = "anthropic", extra = ["bedrock"] }, From e06f8ee5fe8b4211c15837f58092f6fa9f08e4c2 Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Sun, 6 Sep 2026 12:23:55 +0800 Subject: [PATCH 3/5] chore(release): set version to 0.8.2 --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abb059e..ae91c36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ the GitHub Release body, so a release with no entry here fails. Versioning follows [docs/versioning.md](docs/versioning.md). -## [0.9.0] - 2026-09-06 +## [0.8.2] - 2026-09-06 ### Changed diff --git a/pyproject.toml b/pyproject.toml index 065d0c9..07a3639 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "apodex-agent-core" -version = "0.9.0" +version = "0.8.2" description = "Shared, product-neutral runtime primitives for Apodex agents" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index 74c0d0e..be1b34b 100644 --- a/uv.lock +++ b/uv.lock @@ -50,7 +50,7 @@ wheels = [ [[package]] name = "apodex-agent-core" -version = "0.9.0" +version = "0.8.2" source = { editable = "." } dependencies = [ { name = "anthropic", extra = ["bedrock"] }, From ceed89695894c09207f6b5351291c552c2db9441 Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Sun, 6 Sep 2026 12:30:27 +0800 Subject: [PATCH 4/5] =?UTF-8?q?docs(compaction):=20=E6=8A=8A=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E6=94=B6=E7=AA=84=E7=9A=84=E6=95=B0=E5=AD=97=E6=94=B9?= =?UTF-8?q?=E6=88=90=E4=BF=AE=E5=A4=8D=E5=90=8E=E7=9A=84=E5=AE=9E=E6=B5=8B?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `46b7b0b3` 修掉了「从截断后的 args 预览判断有无来源」这个缺陷之后, 本条目引用的统计就全部过时了 —— 它们描述的是修复前的行为。 误判影响约 130 张卡片(约 10%):那些调用其实带着来源,只是 URL 落在预览的 120 字符之后。修好之后: | | 修复前(已过时) | 修复后(现值)| |---|---|---| | 被压缩条数 | 1337 | 1295 | | 仅工具名 | 435 (33%) | 306 (24%) | | URL 总保留率 | 7.8% | 8.9% | | 成本占压缩后 context | 25.0% | 27.2% | | 每次检索至少一个来源 | 99.2% | 99.2% | **修复前的数字之所以"更便宜",只是因为它在丢弃那些调用真实拥有的出处**, 所以 CHANGELOG 里补了一句说明,避免读者把 25.0% 当成收窄本身的成本基线。 条数 1337→1295 是因为卡片变长后,「卡片不比原文短则保留原文」的兜底判据 接住了更多小 body。 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 14 ++++++++++---- agent_core/runtime/loop/compact.py | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae91c36..53c2e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,15 +21,15 @@ Versioning follows [docs/versioning.md](docs/versioning.md). exists, and the placeholder/footer contract is untouched. Rationale, measured over 12 real long-running research trials in ApodexHarness - (1337 carded results, ~13.5k source URLs in the bodies being discarded): + (1295 carded results, ~13.5k source URLs in the bodies being discarded): - Keeping 3 URLs retained 16.5% of all URLs, but the only quantity anything downstream consumes is whether a retrieval left *one* traceable source behind, and the first URL alone covers 769/775 (99.2%) of cards that had any - URL. Dropping to 1 took total retention to 7.8% and left that 99.2% + URL. Dropping to 1 took total retention to 8.9% and left that 99.2% unchanged - the extra two URLs were spending ~120 chars each on a percentage with no reader. - - 435 of 1337 cards (33%) had no source at all - shell commands, task-board + - 306 of 1295 cards (24%) had no source at all - shell commands, task-board updates, file writes. The card exists so a later turn does not redo work whose provenance it can still see; without a source that premise does not hold, and repeating such a call is usually legitimate because the state it @@ -39,11 +39,17 @@ Versioning follows [docs/versioning.md](docs/versioning.md). IS the url), so the sourceless test is "no URL in the body **and** none in the arguments" - reading only the body would strip `web_fetch` of its one source. - Cost on that sample: cards add ~49.6k tokens across the 12 trials, 25.0% of the + Cost on that sample: cards add ~55.9k tokens across the 12 trials, 27.2% of the post-compaction context under an aggressive `keep_last_k=5`. Under the threshold-triggered `tiered` path a product actually ships, post-compaction context is 150-200k, putting the same cards at 2-3%. + Those figures are measured *after* the argument-URL fix below. Detecting the + source from the bounded preview instead of the full arguments had mislabelled + roughly 130 cards (~10%) as sourceless, so the pre-fix numbers merely looked + cheaper (33% name-only, 25.0% cost) by discarding provenance those calls + really had. + ## [0.8.1] - 2026-09-05 ### Fixed diff --git a/agent_core/runtime/loop/compact.py b/agent_core/runtime/loop/compact.py index ac65a53..f8234f0 100644 --- a/agent_core/runtime/loop/compact.py +++ b/agent_core/runtime/loop/compact.py @@ -246,8 +246,8 @@ def _elided_tool_card( premise needs a source: for a body with no URL — a shell command, a task-board update, a file write — repeating the call is usually legitimate, because the state it reads has changed. Such arguments are not decision information and do - not earn a 120-char preview. Measured over the same 12 trials, 435 of 1337 - carded results (33%) had no source and were charging roughly a third of the + not earn a 120-char preview. Measured over the same 12 trials, 306 of 1295 + carded results (24%) had no source and were charging roughly a quarter of the feature's cost for none of its benefit. """ # A source can live in the arguments rather than the body: web_fetch's argument From a5835847dcd128ab94b379ed0b15bf5f9d06d86a Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Sun, 6 Sep 2026 12:31:16 +0800 Subject: [PATCH 5/5] =?UTF-8?q?docs(compaction):=20=E8=A1=A5=E4=B8=8A=20?= =?UTF-8?q?=5FMINI=5FCARD=5FMAX=5FURLS=20=E6=B3=A8=E9=87=8A=E9=87=8C?= =?UTF-8?q?=E9=81=97=E6=BC=8F=E7=9A=84=E4=B8=A4=E4=B8=AA=E6=97=A7=E6=95=B0?= =?UTF-8?q?=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一个 commit 只改了 _elided_tool_card 的 docstring,漏了常量上方注释里的 1337→1295 与 7.8%→8.9%。CHANGELOG 第 50 行的 33% / 25.0% 是有意保留的 —— 那里正是在说明修复前的数字为什么看起来更便宜。 Co-Authored-By: Claude Opus 5 (1M context) --- agent_core/runtime/loop/compact.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent_core/runtime/loop/compact.py b/agent_core/runtime/loop/compact.py index f8234f0..fd570a9 100644 --- a/agent_core/runtime/loop/compact.py +++ b/agent_core/runtime/loop/compact.py @@ -163,11 +163,11 @@ def tool_names_by_call_id(messages: list[Message]) -> dict[str, str]: _MINI_CARD_ARGS_MAX_CHARS = 120 _MINI_CARD_BODY_MAX_CHARS = 400 # ONE url, not three. Measured over 12 real long-running trials of a research -# agent (1337 carded results, 13.5k source URLs in the bodies being discarded): +# agent (1295 carded results, 13.5k source URLs in the bodies being discarded): # keeping 3 URLs retained 16.5% of all URLs, but the quantity anything downstream # consumes is whether a retrieval left behind *one* traceable source — and the # first URL alone covers 769/775 (99.2%) of the carded results that had any URL. -# Dropping to 1 took total retention to 7.8% and left that 99.2% unchanged, i.e. +# Dropping to 1 took total retention to 8.9% and left that 99.2% unchanged, i.e. # the extra two URLs per card were spending ~120 chars each on a percentage with # no reader. A host that needs several independent sources per claim should raise # this deliberately rather than inherit it.