diff --git a/README.md b/README.md
index 9abe45d3..c7b31cd0 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ users retain control of the durable files.
- [2026.10] - **[Decision-model context compaction](docs/en/context_compaction.md)**: score old tool calls and results
against the next action, preserve protected history, and return a recoverable compaction proposal. Includes an SVG
- walkthrough, two live-model integration scenarios, and a fixed-summary Top-N benchmark.
+ walkthrough, two live-model integration scenarios, and a 14-query fixed-summary Top-N benchmark with a forced Hermes summary baseline.
- [2026.10] - **[ReMe Multimodal Memory](https://reme.agentscope.io/en/blog_20261009) update**: Auto Memory reads images
with their conversation context and preserves image sources; Auto Resource turns image files into searchable daily
cards linked to the originals.
diff --git a/README_ZH.md b/README_ZH.md
index 7b3a99a4..27049c3f 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -50,7 +50,7 @@
- [2026.10] - **[基于决策模型的上下文压缩](docs/zh/context_compaction.md)**:根据下一步任务判断旧工具证据的保留价值,
- 保护关键历史,并返回可回溯的压缩建议;提供 SVG 流程图、两个真实模型集成场景和固定摘要 Top-N 实验。
+ 保护关键历史,并返回可回溯的压缩建议;提供 SVG 流程图、两个真实模型集成场景和 14-query 固定摘要实验,包含强制 Hermes 摘要基线。
- [2026.10] - **[ReMe 多模态记忆更新](https://reme.agentscope.io/zh/blog_20261009)**:Auto Memory 将会话图片和上下文一起
写入记忆并保留图片来源;Auto Resource 将图片资料整理为可检索、可回溯原图的 daily 卡片。
- [2026.10] - **[ReMe Studio Playground](https://reme.agentscope.io/studio/?lang=zh) 上线**:无需安装或启动后端,
diff --git a/benchmark/compaction/README.md b/benchmark/compaction/README.md
index f2d99c75..02883cf8 100644
--- a/benchmark/compaction/README.md
+++ b/benchmark/compaction/README.md
@@ -1,56 +1,57 @@
-# Frozen summary + Jev Top N
+# Context compaction: fixed summary + selected evidence
-One payment trajectory, one fixed-summary experiment, one [results table](./RESULTS.md).
-The question is whether adding selected original evidence repairs information lost by a summary.
-This benchmark is separate from production `compact_context`.
+A **14-query fixture panel** with predefined answers and rule scoring: four primary tasks and ten short/query-only
+controls. See [queries, answers and frozen settings](./STUDY.md) and [measured results](./RESULTS.md).
-## Protocol
-
-Hermes collected the synthetic payment investigation: 11 messages with the actual system prompt and six paired tool
-calls/results. Exclude the final assistant report, then losslessly group tool results into **10 evidence units**.
-Short files remain whole; long logs group correlated events and routine text. No noise or identifier variants are added.
-The fixture supplies ten exact facts and six executable repair checks; no LLM creates or judges the gold answers.
-
-For each of five generations, create one summary **S** of all evidence and one Jev ranking using only the initial task
-and original evidence. Neither compressor sees the follow-up question or gold. Freeze both before answering:
-
-- **Full context:** original history, allowed to exceed the compressed-input ceiling.
-- **S:** the frozen summary.
-- **S + Jev Top 1–5:** exactly the same S, plus the first N original units ranked by usefulness.
-
-Select a strict descending score prefix, break ties by unit ID, and restore source order when rendering. There is no
-probability threshold, skipping, truncation or summary rewriting. **There are 10 units, so Top 10 is the maximum; the
-reported sweep tests Top 1–5.** Larger N requires more input space.
-
-S is generated with a **3,200-token complete-input allowance**. The sweep uses a **3,800-token complete-input ceiling**,
-including the same original system/task, evidence, follow-up and framing. This is incremental evidence at extra input
-cost, not an equal-length S-versus-fusion comparison. A local Qwen tokenizer and 64-token reserve budget requests;
-actual API usage is checked independently. Overruns fail instead of shrinking S or silently dropping original units.
-
-Each context receives three answers: 35 contexts and 105 answers, temperature zero, thinking disabled. Average ten
-strict fact checks within each answer, then answers and generations equally. Also report the proportion passing all
-six restricted fake-client repair tests. Invalid conditions stay in the denominator with zero credit.
-**Hermes collected the source; evaluation calls Qwen directly**, without tool continuation or recovery.
+Hermes collects the source through real `read_file` calls. ReMe's experiment then compares Full context, a frozen Qwen
+summary S, S + Jev Top 1–5, and a **Hermes small-window summary adapter**. Scored answering calls Qwen directly;
+there is no resumed Hermes tool loop or archive recovery. Production `compact_context` is separate.
## Run
-Use the development environment plus optional `tokenizers`; no Hermes runtime is needed for replay.
-The runner accepts only the pinned original payment trajectory whose fixture oracle is in [cases.py](./cases.py).
-Credentials come from `.env`: `LLM_API_KEY`, `LLM_BASE_URL`, `DECISION_MODEL_API_KEY`, and optional
-`DECISION_MODEL_NAME` / `DECISION_MODEL_BASE_URL`. Provider calls require `--live`.
+Use ReMe's development environment plus optional `tokenizers`. Collection and native generation need a separately
+installed Hermes environment, also able to import `tokenizers`. Credentials come from ReMe's `.env`:
+`LLM_API_KEY`, `LLM_BASE_URL`, `DECISION_MODEL_API_KEY`, and optional `DECISION_MODEL_NAME` / `DECISION_MODEL_BASE_URL`.
+All commands require `--live`; outputs must be new directories outside this repository.
```bash
-python -m benchmark.compaction.run --live \
- --trajectory /path/outside/repository/trajectory.full.json \
+# Freeze all task/answer definitions, then collect 13 new read-only trajectories.
+# Reuse the pinned original payment trajectory as the fourteenth source.
+python -m benchmark.compaction.suite_collect --live \
+ --hermes /path/to/hermes-agent \
+ --hermes-python /path/to/hermes-environment/bin/python \
+ --payment-trajectory /path/to/original/trajectory.full.json \
+ --output /path/outside/repository/corpus
+
+# Freeze S and Jev rankings, then run the seven-method Top-N sweep.
+python -m benchmark.compaction.suite_run --live \
+ --corpus /path/outside/repository/corpus/corpus.json \
--tokenizer /path/to/cached/qwen/tokenizer.json \
- --output /path/outside/repository/new-evaluation
+ --output /path/outside/repository/evaluation
+
+# Generate the native-adapter summaries and add the eighth method.
+# An existing complete --native directory reuses all frozen native generations.
+python -m benchmark.compaction.suite_native --live \
+ --corpus /path/outside/repository/corpus/corpus.json \
+ --evaluation /path/outside/repository/evaluation \
+ --native /path/outside/repository/native-generations \
+ --hermes /path/to/hermes-agent \
+ --hermes-python /path/to/hermes-environment/bin/python \
+ --tokenizer /path/to/cached/qwen/tokenizer.json \
+ --output /path/outside/repository/combined-results
```
-Defaults: `qwen3.8-max`, five generations, three answers, Top 1–5 and four concurrent requests. To use every previously
-frozen S/ranking pair without new compression calls, add `--frozen-run /path/to/previous/results`. This generates fresh
-answers for every method; `--compressions` must match all saved summary generations. For an offline input reconstruction,
-replace `--live` with `--check-only --frozen-run /path/to/previous/results`.
+The task definitions use local Hermes commit `30463589eb54fa0f48724594cc0e98e6b995729b` (MIT). The recorded study uses
+`qwen3.8-max`, three summary/ranking generations and two answers per context: **336 contexts, 672 answers**. Provider
+requests are bounded to four at a time. Raw files, sessions, wire bodies, code snapshots and per-field grades stay
+outside git; existing runs are never overwritten.
-`--top-n` can extend the sweep to at most 10. Increase `--input-cap` explicitly if needed; the runner records over-budget
-contexts rather than changing their content. Outputs must be new directories outside the repository. Protocol, source
-hashes, frozen selections, code snapshots, requests/responses, usage and per-answer checks remain there.
+S remains byte-identical across its five fusion variants. Top N means a strict descending-score prefix, without a
+probability threshold, skipping or truncation; selected units are rendered in source order. When a task has fewer
+than N units, all available units are retained and `actual_n` records the count. The original payment source has ten
+units (maximum Top 10); this study sweeps Top 1–5 with task-specific unit counts.
+
+The native baseline invokes Hermes' actual summary generator with an explicit small-window adapter: legacy template,
+S-matched body budget, final word constraint, native provenance validation, and body-only downstream framing. It bypasses
+normal triggers/head-tail selection and **must actually shorten input**; it is not the unchanged automatic Hermes policy.
+See [the exact simulation settings and preflight disclosures](./STUDY.md#added-baseline-hermes-small-window-summary-adapter).
diff --git a/benchmark/compaction/RESULTS.md b/benchmark/compaction/RESULTS.md
index 11ca5d20..ab1a404b 100644
--- a/benchmark/compaction/RESULTS.md
+++ b/benchmark/compaction/RESULTS.md
@@ -1,63 +1,97 @@
-# Fixed summary + selected original evidence
+# Fixed-summary compaction on fourteen fixture tasks
-2026-10-16 · Qwen `qwen3.8-max` · Jev `jev-1.13.0` · one original Hermes-collected synthetic payment trajectory.
+2026-10-16 · Qwen `qwen3.8-max` · Jev `jev-1.13.0` · **14 distinct queries with predefined answers**.
+Four source-length-qualified tasks form the primary cohort; ten short/query-only controls are reported separately.
+Cohorts were fixed before collection, with no accuracy-based task filtering.
-| Method | Input tokens | Fact accuracy |
-| --- | ---: | ---: |
-| Full context | 4,715 | 100.0% |
-| S | 2,937 | 73.3% |
-| S + Jev Top 1 | 3,134 | 72.7% |
-| S + Jev Top 2 | 3,247 | 86.0% |
-| S + Jev Top 3 | 3,360 | 94.0% |
-| S + Jev Top 4 | 3,529 | 100.0% |
-| S + Jev Top 5 | 3,585 | 100.0% |
+## Primary cohort: four tasks
-Input tokens are mean actual downstream API usage, including the original system/task and follow-up.
-Every method has five valid contexts and 15 answers. All answers pass all six executable repair checks; the table
-therefore measures differences in fact retention, not a demonstrated difference in repair success.
+| Method | Mean input tokens | Fact score | Valid contexts |
+| --- | ---: | ---: | --- |
+| Full context | 4,014 | 100.0% | 12/12 |
+| S | 2,480 | 82.0% | 12/12 |
+| Hermes small-window adapter | 2,432 | 79.4% | 12/12 |
+| S + Jev Top 1 | 2,753 | 83.0% | 12/12 |
+| S + Jev Top 2 | 2,974 | 94.5% | 12/12 |
+| S + Jev Top 3 | 3,241 | 99.5% | 12/12 |
+| S + Jev Top 4 | 3,496 | 98.1% | 12/12 |
+| S + Jev Top 5 | 3,660 | 97.4% | 12/12 |
-**Top 4 matches Full context on the ten checked facts while reducing downstream input by 25.2%.** Adding Top 5
-uses more tokens without improving this score. Top 1 does not improve S; Top 2 and Top 3 improve it by 12.7 and
-20.7 percentage points. These observations are specific to this task, not statistical equivalence or general superiority.
+Input tokens are actual downstream API usage, including the original system/task and follow-up. Fact score averages
+strict JSON field checks within each answer, then generations and tasks equally; invalid conditions score zero.
+Full context reaches 100%; S + Top 3 reaches **99.5% with 19.3% less downstream input** (3,241 versus 4,014 tokens).
+Top 4 and Top 5 do not improve this point estimate. No equivalence test or general optimal-N claim is made.
-
+
-## What is fixed
+## What was tested
-The [protocol and runnable commands](./README.md) keep one byte-identical summary S and one Jev ranking per generation.
-S summarizes all original evidence under a 3,200-token complete-input allowance; fusion appends original blocks without
-rewriting S. The extended sweep raises the final complete-input ceiling from 3,600 to **3,800 tokens** before new answer
-calls, solely to accommodate Top 5 with its full evidence and budgeting reserve. All compressed API inputs are within
-this ceiling (2,924–3,602 tokens). More originals intentionally cost more tokens; lengths are not forced equal.
+The [full query/answer table and protocol](./STUDY.md) define the oracle before collection. The four primary tasks are
+payment retry investigation, checkout 504 analysis, server-error localization and a build-output token lookup.
+The ten controls cover shorter configuration, code, dependency, notes, version and file-generation questions.
+All fourteen sources are synthetic fixtures: one existing payment trajectory plus thirteen genuine Hermes executions
+over explicitly read-only adaptations of upstream tasks. Long-file views are deterministic original excerpts, not
+added noise. These are not the maintainers' private production transcripts.
-The source has **10 evidence units, so the maximum is Top 10**. This sweep tests Top 1–5. In the first ranking, Top 3
-contains the incident log, retry implementation and payment policy; Top 4 adds `expected_cases.json`, restoring the exact
-regression attempt counts that S + Top 3 sometimes loses. Top 5 also retains the README.
+Each task has three frozen summary/ranking pairs and two answers per method: **336 contexts and 672 answers** across
+eight methods. S summarizes every source unit once. Each S + Top N preserves that S byte-for-byte and adds complete
+original units, ranked without a threshold and restored to source order. Actual N saturates at the available unit count.
+The original payment source has ten units, so its maximum is Top 10; the sweep tests Top 1–5.
-There are five frozen summary/ranking pairs and three answers per method: **35 contexts, 105 answers**. Full context,
-S and Top 1–2 reuse all 60 answers from the immediately preceding paired run. Top 3–5 add 45 new answers. All five pairs
-are retained; no prediction-based filtering, new summaries or new Jev scores enter the extension.
+S's evidence allowance is 20% of rendered-evidence tokens, clamped to 64–256 local Qwen tokens. A per-task complete-input
+ceiling is calculated from protected prompts, the summary allowance, the five largest whole units and reserve, before
+scored answers. This permits the incremental sweep; **it is not an equal-length S-versus-fusion comparison or a claim
+that every Full input exceeds its ceiling**. All actual inputs are checked. Short cases may gain no token savings.
-## Gold, audit and scope
+The added **Hermes small-window adapter** calls the real native generator, with its legacy template, an overridden
+64–256-token body budget and an appended total word limit. It retains native redaction/provenance checks and strips
+only the native control prefix before the common summary carrier. It bypasses automatic triggering and full-compressor
+head/tail selection: **this is a forced simulation, not the unchanged default Hermes compressor**. All **42/42** native
+contexts fit their body/input budgets and use fewer actual API input tokens than the corresponding Full context.
+No original-history fallback is accepted.
-The original fixture predates these evaluations. Its oracle contains ten facts: incident, invoice and charge IDs,
-amount, ordered idempotency keys, first-attempt commit state, retry limit, non-retryable HTTP codes, original key-generation
-line and named regression-test attempt counts. Matching is type-strict and nested; HTTP-code order is ignored. The six
-restricted executable checks cover success, timeout retry with a stable key, HTTP 402/422 propagation, three-attempt
-exhaustion and independent keys for separate payments. No LLM judge or generated answer key is used.
+Hermes collects the trajectories; answering calls Qwen directly. Gold and follow-up questions never enter collection,
+summary or decision requests. The final collection report is excluded from every history. There is no tool continuation,
+retrieval, LLM judge or generated answer key. Exact string/type compliance is part of this metric; it does not by itself
+prove semantic information loss or task-completion differences. The native and S methods have the same maximum body
+allowance, but their actual lengths differ. Four primary templates are too few to establish general superiority.
-The source has 11 messages, six tool calls and six complete results, including the actual system prompt. Its final
-assistant report is excluded from compression. No synthetic noise or identifier substitutions are added. Source SHA-256:
-`0f37e6510ffafd920d088452d8719026b00fd4e74f87f7dd1a42a85c8287e6f1`.
+
+Ten short/query-only controls
-The audit replayed all 105 grades, verified exact retained originals and nested Top-N sets, byte-identical summaries,
-protected prompts, original source hash, actual input ceilings and unchanged reused request/response records. No configured
-API keys were found. The 45 new Qwen calls use 157,092 input and 9,316 output tokens; existing answers and compression
-have additional usage. Downstream input reduction is not a claim about total pipeline cost.
+| Method | Mean input tokens | Fact score | Valid contexts |
+| --- | ---: | ---: | --- |
+| Full context | 2,727 | 100.0% | 30/30 |
+| S | 2,315 | 67.1% | 30/30 |
+| Hermes small-window adapter | 2,308 | 73.2% | 30/30 |
+| S + Jev Top 1 | 2,493 | 84.7% | 30/30 |
+| S + Jev Top 2 | 2,573 | 87.5% | 30/30 |
+| S + Jev Top 3 | 2,687 | 92.9% | 30/30 |
+| S + Jev Top 4 | 2,722 | 92.9% | 30/30 |
+| S + Jev Top 5 | 2,722 | 92.5% | 30/30 |
-Artifacts stay outside the repository under `trajectory-runs/payment-fixed-summary-topn1to5-20261016/results/`:
-`protocol.json`, frozen contexts/selections, exact requests/responses, per-answer results and `audit.json`.
-One synthetic task with repeated measurements is not 35 independent tasks. This is direct Qwen evaluation, not Hermes
-continuing its agent loop. The earlier length-matched diagnostic also found pure Jev reaching 100% with about 3,251
-input tokens on this task, so **this sweep does not establish fusion superiority over pure Jev**. Production
-`compact_context` retains its conservative preview/recovery contract; this hybrid remains benchmark-only.
+All controls remain in their separate denominator. They are not presented as ten additional long-context tasks.
+
+
+
+## Audit and reproducibility
+
+See [commands](./README.md). The audit replayed all **672** grades, reconstructed original source spans, verified source
+hashes and actual initial system prompts, checked frozen S and strict nested Top-N sets, protected prompts, question/gold
+isolation, actual token ceilings, native strict shrinkage and configured-key exclusion. All 336 contexts are valid.
+
+The format preflight exposed two underspecified answer shapes: payment counts as arrays and note text with bullet
+markers. Those two follow-ups were clarified without changing gold or graders, and every seven-arm condition on both
+tasks was re-answered using the same S/rankings. Their 84 initial answers remain archived and excluded; all 504 answers
+from the other twelve tasks remain included. The native adapter adds 84 fresh answers. Unadapted native/length-only
+calibration attempts are also retained; they are not silently counted as successful compression.
+
+Formal recorded usage: **757 Qwen calls** (672 answers, 42 S calls, 43 native calls including one length retry),
+2,027,184 input / 40,099 output tokens; **54 Jev calls**, 77,799 input / 5,634 output tokens. This excludes Hermes
+collection and preflights, including interrupted calibration attempts. No complete monetary-cost claim is made.
+Downstream input reduction is not total pipeline savings or a prompt-cache measurement.
+
+Artifacts remain outside the repository: `hermes-fixture14-20261016/` holds the frozen corpus and actual Hermes sessions;
+`compaction-fixture14-20261016-v2/` holds the seven-arm records; `compaction-fixture14-hermes-adapted-20261016/` holds native
+generation; `compaction-fixture14-final-20261016/` holds the combined results and `audit.json`. This is a development panel,
+including a previously used payment task, not independent held-out confirmation. Production `compact_context` is unchanged.
diff --git a/benchmark/compaction/STUDY.md b/benchmark/compaction/STUDY.md
new file mode 100644
index 00000000..2b9290ea
--- /dev/null
+++ b/benchmark/compaction/STUDY.md
@@ -0,0 +1,97 @@
+# Fourteen-task study: frozen queries, answers and rules
+
+The initial task panel was frozen before collection. Explicit format corrections and native additions are recorded below.
+It evaluates **14 distinct task/answer templates**:
+one existing payment trajectory and thirteen newly collected read-only adaptations of Hermes task fixtures.
+The original action tasks are converted to evidence investigation and proposed actions; no files are modified and no cron jobs are created.
+Hermes performs real read_file calls over immutable source snapshots. Evaluation subsequently calls Qwen directly, not a resumed Hermes loop.
+
+## Queries and expected answers
+
+Each initial query below supplies the collection goal. The follow-up asks for the named JSON fields in the answer column, with field descriptions from `suite.py`.
+The follow-up and expected values never enter collection or compression requests. This is one task with a follow-up, not two independent samples.
+
+| ID | Initial query | Expected facts | Cohort |
+| --- | --- | --- | --- |
+| payment | Investigate the original payment retry incident. | `{"incident_id": "PAY-4827", "invoice_id": "INV-731", "successful_charge_id": "CH-991", "amount_minor": 12900, "attempt_keys": ["pay-a", "pay-b"], "first_attempt_committed": true, "max_attempts": 3, "no_retry_http_statuses": [402, 422], "key_generation_line": 5, "regression_attempt_counts": {"timeout_then_success": 2, "validation_422": 1, "three_timeouts": 3}}` | Primary candidate |
+| backup | A while back I told you one of my repos needed nightly backups — find which repo that was in our past conversations (do not guess). Then: write backup.sh in the workspace that archives that repo directory under repos/, schedule it to run nightly at 2am, and track the whole job with your todo checklist. Report back which repo it was and what you set up. | `{"repo": "sparks-data", "ignored_repo": "toybox", "data_text": "important"}` | Control: short_fixture |
+| refactor | In src/, every requests call uses timeout=30. Bump them all to timeout=45, add a CHANGELOG.md entry describing the change, and tell me exactly how many call sites you changed. | `{"old_timeout": 30, "call_sites": 3, "functions": ["fetch_a", "fetch_b", "fetch_c"], "methods": ["get", "post", "head"], "old_version": "1.0"}` | Control: short_fixture |
+| three_files | This is a multi-step job — track it with your todo checklist tool and keep it updated as you go, marking each step completed when verified. Steps: (1) create greet.py containing a greet(name) function that returns 'hello '; (2) create data/rows.csv with header 'item,qty' and exactly two data rows for widgets; (3) create notes/summary.md stating how many files you created. Finish only when all three are done and checked off. | `{"file_count": 3, "header": "item,qty", "data_rows": 2}` | Control: query_only |
+| active_config | Search this workspace for wherever the retry_limit setting is configured and tell me its current value. | `{"retry_limit": 7, "backoff": 2, "active_file": "conf/app.ini", "old_file": "conf/old.ini"}` | Control: short_fixture |
+| checkout_504 | Investigate the checkout 504 incident: distinguish the failed rollback, root cause and final database fix. | `{"setting": "statement_timeout", "value_seconds": 45, "index_mode": "CONCURRENTLY", "root_cause": "CREATE INDEX without CONCURRENTLY", "rolled_back_version": "v2.30.2", "rollback_fixed": false, "deployed_version": "v2.31.0", "initial_error_percent": 8}` | Primary candidate |
+| dependency_version | What exact left-pad version is pinned, and which file declares the test script? Record the package scripts for a handoff. | `{"version": "1.3.0", "declaration": "package.json", "test": "vitest run", "build": "tsc -p .", "package_version": "4.2.1"}` | Control: short_fixture |
+| retry_backoff | Find the retry-delay function in src/ and explain its base, growth, cap and jitter. | `{"base_ms": 250, "cap_ms": 30000, "growth_factor": 2, "jitter": "uniform_integer_below_bound", "function": "retryDelay"}` | Control: short_fixture |
+| server_error | Locate the unique ERROR in the server log and retain its exact request, timestamp, upstream and connection failure. | `{"request_id": "req-7f3d9", "timestamp": "2026-08-08T23:41:17Z", "status": 502, "upstream": "payments", "error": "ECONNREFUSED", "address": "10.0.4.17:8443"}` | Primary candidate |
+| meeting_notes | Read the meeting notes and report the third bullet exactly, retaining the other agreed actions. | `{"third": "rotate the API keys quarterly", "first": "ship the payments retry fix", "second": "audit the staging TLS certs", "fourth": "close out the Q3 incident review"}` | Control: short_fixture |
+| targeted_edit | In ./handlers.py exactly one of the three identical `timeout = 10` lines must change: the one inside `slow_handler`. Change it to `timeout = 60` using the patch tool. Then run `python3 check_handlers.py` from . and reply with its output. | `{"target": "slow_handler", "original_timeouts": {"fast_handler": 10, "slow_handler": 10, "medium_handler": 10}, "required_timeouts": {"fast_handler": 10, "slow_handler": 60, "medium_handler": 10}, "success_marker": "HANDLERS_OK_552"}` | Control: short_fixture |
+| build_token | Run `python3 ./noisy_build.py` (it prints a lot). Somewhere in the middle of its output is a single line starting with 'UNIQUE_TOKEN='. Reply with the full token value. | `{"token": "tok_9f31c_middle", "step": 2000, "total_steps": 4000, "prefix": "UNIQUE_TOKEN="}` | Primary candidate |
+| package_versions | The project . has three package dirs: pkg_a, pkg_b, pkg_c, each containing version.txt. Working through the directories, collect the three versions and create ./versions.txt containing them comma-separated in order (a,b,c). Reply DONE plus the joined string. | `{"versions": {"pkg_a": "1.4.2", "pkg_b": "0.9.7", "pkg_c": "3.2.1"}, "joined": "1.4.2,0.9.7,3.2.1"}` | Control: short_fixture |
+| checksum_anomaly | The file ./records.log contains exactly one line starting with 'ANOMALY:'. Find it using read_file (not terminal) and reply with the full anomaly line. | `{"line": "ANOMALY: checksum drift detected in shard 7 (code X99Q)", "code": "X99Q", "shard": 7, "line_number": 4201}` | Control: short_fixture |
+
+## Scoring
+
+**No LLM judge.** The model must return `{"facts": {...}}`. Each expected field is checked with recursive type-strict JSON equality.
+Integers differ from booleans and floats. Strings and ordered arrays match exactly; missing/malformed fields score zero.
+Only payment `no_retry_http_statuses` ignores list order. No answer aliases, rewrites or retries are added after scoring.
+Primary fact accuracy averages fields within an answer, answers within a frozen generation, generations within a task, then tasks equally.
+Also report all-fields-correct rate and actual downstream API input tokens. Failed planned conditions remain zero in the denominator.
+These are evidence-retention metrics; the new sweep does not execute proposed patches or measure task completion.
+
+## Cohorts fixed before collection
+
+Primary candidates require at least 2,000 source-snapshot characters and evidence-dependent facts. The original payment source is pinned separately.
+This structural rule selects **4 primary candidates**: payment, checkout_504, server_error, build_token.
+
+The remaining 10 cases are short-fixture or query-only controls and are reported separately. All fourteen are collected/evaluated;
+no task is selected because a method wins. Coverage, pairing and collection failures are logged for every planned task.
+
+## Paired experiment
+
+- Model: `qwen3.8-max`; same actual system prompt, task and follow-up within each case. Final collection reports are excluded.
+- Three independently generated summary/ranking pairs per task; two answers per context, temperature zero, thinking disabled.
+- Methods: Full context, S, S + Jev Top 1, 2, 3, 4, 5. Total planned: **294 contexts and 588 answers**.
+- S summarizes every observed source unit once; it remains byte-identical across all five fusion methods.
+- Jev sees only the initial task and source evidence. Scores are shared across N, with no probability threshold.
+- Select the strict score prefix, then render in source order. If fewer than N units exist, retain all of them and report the actual count.
+- Summary allowance: 64–256 local Qwen tokens, equal to 20% of rendered evidence tokens clamped to those limits. The allowance is frozen from source length.
+- Before answering, calculate a per-task complete-input ceiling that fits the protected prompts, the frozen summary allowance, and the five largest whole units, plus a 64-token reserve.
+ This size-only ceiling permits the entire N sweep without changing S or truncating evidence; actual lengths are reported rather than forced equal.
+- Report Full-to-fusion input reduction. A short control may cost more with fusion; it is not presented as a successful compression.
+- Save protocol, source hashes, code snapshots, requests/responses, nested selections and per-field grades outside the repository. No archive recovery.
+- The original payment-only table remains a development result; this new task panel is not an independent held-out confirmation because the payment task was already used.
+
+Fixture source: local Hermes commit `30463589eb54fa0f48724594cc0e98e6b995729b`, MIT licensed. Long logs/build output use deterministic source views (head and matching-line context),
+not every byte of the upstream workspace. No artificial noise or identifier replacements are added. This adapts existing tasks; it is not the maintainers’ private trajectory corpus.
+
+## Format validation before the final comparison
+
+The first answer pass exposed two under-specified follow-up formats: payment test counts were returned as arrays,
+and meeting-note bullets retained their list markers. Expected answers and the strict grader remain unchanged.
+The payment follow-up now explicitly requests the named JSON mapping; meeting notes request bullet text without its list marker.
+All seven methods on both affected tasks are re-answered with the same frozen S/rankings; their 84 initial answers are
+retained as a format preflight and excluded from final metrics. The other twelve tasks retain all 504 original answers,
+with identical questions. This correction changes no cohort, evidence, answer values, ranking or summary.
+
+## Added baseline: Hermes small-window summary adapter
+
+This is a **simulation adapter**, not Hermes' unchanged automatic policy. It calls the real
+`agent.context_compressor.ContextCompressor._generate_summary` with native serialization, checkpoint template,
+redaction and provenance validation. Use the built-in `tail_mode=legacy` to omit lean's additional 4,000-token session
+log and deterministic appendices. A benchmark subclass sets the summary budget to the same per-task 64–256 tokens as S,
+and appends a total word limit (35% of the token target, at least 18 words) allowing nonessential sections to be omitted.
+The Historical Task Snapshot/no-user sentinel and Critical Context sections remain. Native validation is retained.
+
+Strip the native control prefix with Hermes' own helper before using the same downstream `Summary:` carrier as S.
+This compares generated summary bodies without charging Hermes twice for handoff framing. No model response is edited
+for factual content. The native call sees only the pre-final evidence history and the original task as its focus;
+no exam or gold. Allow at most three length-only retries, reducing the requested target by 0.8, without answer feedback.
+
+Three summaries × two answers × fourteen tasks add **42 contexts and 84 planned answers**: **336 contexts and 672 planned
+answers** across eight methods. Keep the same original system/task and exact scoring follow-up as other methods.
+A valid native condition must fit the summary-body allowance, fit the per-task complete-input ceiling, and use fewer
+actual API input tokens than Full context. Failure receives zero credit; no original-history fallback is accepted.
+
+Unadapted native generation and length-calibration records remain outside the repo. The first forced native baseline
+included its long control prefix and exceeded Full input on 40/42 contexts; it is retained as an integration preflight,
+not silently treated as successful compression. The explicit body carrier and word-budget adapter above are fixed
+before their new scored answers. No expected answer, cohort, Jev ranking or existing S is changed.
diff --git a/benchmark/compaction/cases.py b/benchmark/compaction/cases.py
index 94a70a06..4d72e563 100644
--- a/benchmark/compaction/cases.py
+++ b/benchmark/compaction/cases.py
@@ -1,11 +1,8 @@
"""The original payment fixture, lossless source grouping and deterministic checks."""
-import hashlib
import json
import re
-from benchmark.compaction.repair_check import repair_checks
-
SOURCE_SHA256 = "0f37e6510ffafd920d088452d8719026b00fd4e74f87f7dd1a42a85c8287e6f1"
GOLD = {
@@ -20,25 +17,6 @@ GOLD = {
"key_generation_line": 5,
"regression_attempt_counts": {"timeout_then_success": 2, "validation_422": 1, "three_timeouts": 3},
}
-EXAM = """Using only the recorded evidence above, answer this follow-up in JSON. Do not read files or use tools.
-An absent fact must be null; do not invent identifiers. The conversation is a completed read-only investigation.
-Return {"facts": {...}, "repair_plan": {...}}.
-The facts keys are:
-incident_id: the incident reference;
-invoice_id: the affected invoice;
-successful_charge_id: the returned charge ID;
-amount_minor: the recorded integer amount in minor units;
-attempt_keys: the two recorded idempotency keys in attempt order;
-first_attempt_committed: whether the first, timed-out attempt had committed;
-max_attempts: the policy limit, including the initial attempt;
-no_retry_http_statuses: the policy's non-retryable HTTP codes;
-key_generation_line: the original retry.py line generating the key;
-regression_attempt_counts: a mapping from each named fixture test case to its expected attempt count.
-For repair_plan return:
-key_scope: choose before_retry_loop or inside_retry_loop;
-retry_exceptions: list the exception names that should be retried;
-exhaustion_action: choose raise_last_timeout, return_none, or retry_forever.
-These plan fields describe a proposed fix; do not execute or modify anything."""
SUMMARY = (
"Compress the supplied recorded investigation for an agent continuing the same task.\n"
"Return a concise structured handoff: Goal, Constraints, Findings, Decisions, Relevant file"
@@ -68,36 +46,6 @@ def exact_match(observed, expected):
return observed == expected
-FOLLOW_UP = (
- EXAM.replace(
- 'Return {"facts": {...}, "repair_plan": {...}}.',
- 'Return {"facts": {...}, "repair_plan": {...}, "repair_code": "Python source"}.',
- )
- + """
-Also propose a complete Python function charge_with_retry(client, invoice, attempts=3).
-Return source in repair_code, with no markdown fences. Use uuid.uuid4() to create idempotency keys;
-only import uuid if needed. The harness supplies uuid, range, str and TimeoutError.
-Use a bounded for loop with range(attempts), client.charge(invoice, idempotency_key=key),
-and ordinary try/except. No other imports, decorators, annotations, helpers or APIs.
-The function must satisfy the recorded payment contract. Propose code only; do not run tools."""
-)
-
-
-def load_case(path):
- """Accept only the recorded task whose independent fixture oracle is defined above."""
- original = path.read_bytes()
- if hashlib.sha256(original).hexdigest() != SOURCE_SHA256:
- raise ValueError("Expected the unchanged original payment trajectory (see RESULTS.md).")
- history = json.loads(original)["messages"]
- if history[-1]["role"] != "assistant" or history[-1].get("tool_calls"):
- raise ValueError("Expected a final assistant report to exclude.")
- allowed = {"role", "content", "tool_calls", "tool_call_id", "name"}
- history = [{k: v for k, v in m.items() if k in allowed} for m in history[:-1]]
- if [m["role"] for m in history[:2]] != ["system", "user"]:
- raise ValueError("Expected the actual system prompt and initial task.")
- return {"messages": history, "exam": FOLLOW_UP, "gold": GOLD}
-
-
def add_group(groups, spans):
"""Add one ordinary source block to this observation's event groups."""
if spans:
@@ -114,7 +62,9 @@ def event_units(messages):
call = calls[message["tool_call_id"]]
args = json.loads(call["function"]["arguments"])
path = args.get("path") or args.get("file_path") or str(args)
- text = json.loads(message["content"])["content"]
+ payload = json.loads(message["content"])
+ text = payload.get("content") if isinstance(payload, dict) else None
+ text = text if isinstance(text, str) else message["content"]
groups, offset, routine, routine_chars = {}, 0, [], 0
if len(text) <= 1200:
@@ -151,17 +101,3 @@ def render(units):
f"[{u['id']} {u['file']} chars " + ",".join(f"{s['start']}:{s['end']}" for s in u["spans"]) + "]\n" + u["text"]
for u in units
)
-
-
-def evaluate(case, answer):
- """Average ten exact, type-strict facts separately from six executable repair checks."""
- answer = answer if isinstance(answer, dict) else {}
- observed = answer.get("facts", {})
- observed = observed if isinstance(observed, dict) else {}
- facts = {}
- for name, expected in case["gold"].items():
- value = observed.get(name)
- if name == "no_retry_http_statuses" and isinstance(value, list):
- value, expected = sorted(value, key=str), sorted(expected, key=str)
- facts[name] = exact_match(value, expected)
- return {"facts": facts, "task": repair_checks(answer.get("repair_code", ""))}
diff --git a/benchmark/compaction/hermes_summary.py b/benchmark/compaction/hermes_summary.py
new file mode 100644
index 00000000..3c50e709
--- /dev/null
+++ b/benchmark/compaction/hermes_summary.py
@@ -0,0 +1,179 @@
+"""Run the real Hermes summary generator with a small-window adapter and pinned Qwen transport."""
+
+# Private generator hooks are intentionally pinned to the Hermes revision recorded in the corpus.
+# pylint: disable=protected-access
+
+import argparse
+from copy import deepcopy
+import hashlib
+import json
+import os
+from pathlib import Path
+import sys
+
+from dotenv import dotenv_values
+from openai import OpenAI
+
+from benchmark.compaction.cases import write
+
+
+def run(args):
+ """Isolate state, apply host budgets to the native template and retain wire bodies."""
+ root = args.output.resolve()
+ root.mkdir(parents=True)
+ home = root / "hermes_home"
+ home.mkdir()
+ os.environ["HERMES_HOME"] = str(home)
+ (home / "config.yaml").write_text("model:\n provider: custom\n context_length: 131072\n")
+ values = dotenv_values(args.env_file)
+ sys.path.insert(0, str(args.hermes.resolve()))
+ import agent.context_compressor as cc
+
+ source = json.loads(args.source.read_text())
+ history = source["messages"]
+ # The source is the already validated public history, with the final report excluded.
+ from tokenizers import Tokenizer
+
+ tokenizer = Tokenizer.from_file(str(args.tokenizer))
+ allowance = json.loads((args.source.parent / "budget.json").read_text())["allowance"]
+ records = []
+ index = 0
+ attempt = 0
+
+ class ForcedSummary(cc.ContextCompressor):
+ """Model the small-window budget without altering the installed Hermes checkout."""
+
+ target = allowance
+
+ def _compute_summary_budget(self, turns_to_summarize): # pylint: disable=unused-argument
+ return self.target
+
+ def _build_summary_prompt(
+ self,
+ content_to_summarize,
+ summary_budget,
+ focus_topic,
+ memory_context,
+ has_user_turn,
+ ):
+ prompt = super()._build_summary_prompt(
+ content_to_summarize,
+ summary_budget,
+ focus_topic,
+ memory_context,
+ has_user_turn,
+ )
+ words = max(18, int(self.target * 0.35))
+ return prompt + (
+ f"\n\nSMALL-WINDOW SIMULATION: Write at most {words} English words TOTAL. "
+ "This total budget takes precedence over the detailed template above. "
+ "Keep the Historical Task Snapshot section and its required no-user sentinel. "
+ "Keep a Critical Context section with the most useful exact facts. "
+ "Omit every other section, empty entry, repeated task and prose when necessary. "
+ "No preamble. Do not include the context-compaction control prefix."
+ )
+
+ def transport(**kwargs):
+ request = {
+ "model": args.model,
+ "messages": kwargs["messages"],
+ "temperature": 0,
+ "reasoning_effort": "none",
+ "extra_body": {"enable_thinking": False},
+ }
+ request_file = root / f"c{index}.a{attempt}.summary.request.json"
+ response_file = root / f"c{index}.a{attempt}.summary.response.json"
+ write(request_file, request)
+ response = client.chat.completions.create(**request)
+ write(response_file, response.model_dump(mode="json"))
+ kwargs.get("route_info", {}).update(provider="custom", model=args.model)
+ records.append(
+ {
+ "generation": index,
+ "usage": response.usage.model_dump(),
+ "finish_reason": response.choices[0].finish_reason,
+ },
+ )
+ return response
+
+ client = OpenAI(api_key=values["LLM_API_KEY"], base_url=values["LLM_BASE_URL"], timeout=90, max_retries=1)
+ original = cc.call_llm
+ cc.call_llm = transport
+ summaries = []
+ try:
+ for index in range(args.compressions):
+ accepted = None
+ for attempt in range(3):
+ compressor = ForcedSummary(
+ model=args.model,
+ base_url=values["LLM_BASE_URL"],
+ api_key=values["LLM_API_KEY"],
+ provider="custom",
+ api_mode="chat_completions",
+ config_context_length=131072,
+ quiet_mode=True,
+ tail_mode="legacy",
+ )
+ compressor.target = max(12, int(allowance * 0.8**attempt))
+ summary = compressor._generate_summary(
+ deepcopy(history[2:]),
+ focus_topic=history[1]["content"],
+ ) # pylint: disable=protected-access
+ body = compressor._strip_summary_prefix(summary) if summary else "" # pylint: disable=protected-access
+ if body and len(tokenizer.encode(body, add_special_tokens=False).ids) <= allowance:
+ accepted = body
+ break
+ summaries.append(
+ {
+ "repeat": index,
+ "summary": accepted,
+ "native_budget": allowance,
+ "summary_tokens": len(tokenizer.encode(accepted or "", add_special_tokens=False).ids),
+ "valid": accepted is not None,
+ "error": None if accepted else "summary_budget_exceeded",
+ },
+ )
+ write(root / "summaries.json", summaries)
+ print(
+ args.source.parent.name,
+ "native small-window summary",
+ index,
+ "budget",
+ allowance,
+ "OK" if accepted else "FAILED",
+ flush=True,
+ )
+ write(root / "usage.json", records)
+ for file in root.rglob("*"):
+ if file.is_file():
+ assert values["LLM_API_KEY"].encode() not in file.read_bytes()
+ write(
+ root / "validation.json",
+ {
+ "no_api_keys": True,
+ "source_sha256": hashlib.sha256(args.source.read_bytes()).hexdigest(),
+ "generator": (
+ "Hermes legacy generator with explicit small-window prompt/budget adapter; body-only carrier"
+ ),
+ },
+ )
+ finally:
+ cc.call_llm = original
+ client.close()
+
+
+def main():
+ """Internal subprocess entry point; the orchestrator owns the live-call opt-in."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--source", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--hermes", type=Path, required=True)
+ parser.add_argument("--env-file", type=Path, required=True)
+ parser.add_argument("--tokenizer", type=Path, required=True)
+ parser.add_argument("--compressions", type=int, default=3)
+ parser.add_argument("--model", default="qwen3.8-max")
+ run(parser.parse_args())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmark/compaction/hermes_worker.py b/benchmark/compaction/hermes_worker.py
new file mode 100644
index 00000000..66c8a5fa
--- /dev/null
+++ b/benchmark/compaction/hermes_worker.py
@@ -0,0 +1,197 @@
+"""Collect one genuine Hermes trajectory in an isolated process and read-only fixture workspace."""
+
+import argparse
+import hashlib
+import importlib
+import json
+import os
+from pathlib import Path
+import re
+import sys
+import time
+
+from dotenv import dotenv_values
+
+from benchmark.compaction.cases import write
+
+
+def collect(root, hermes, env_file):
+ """Configure isolated Hermes state before importing its runtime; never save credentials."""
+ values = dotenv_values(env_file)
+ secrets = [values[k] for k in ("LLM_API_KEY", "DECISION_MODEL_API_KEY") if values.get(k)]
+ task = json.loads((root / "collection-task.json").read_text())
+ work, home = root / "workspace", root / "hermes_home"
+ home.mkdir()
+ os.environ.update(
+ {
+ "HERMES_HOME": str(home),
+ "TERMINAL_ENV": "local",
+ "TERMINAL_CWD": str(work),
+ "TOKENIZERS_PARALLELISM": "false",
+ },
+ )
+ (home / "config.yaml").write_text(
+ "compression:\n enabled: false\n micro_compact_enabled: false\n"
+ f"terminal:\n backend: local\n cwd: {work}\n"
+ "model:\n provider: custom\n context_length: 131072\n",
+ )
+ sys.path.insert(0, str(hermes))
+ from run_agent import AIAgent
+ from hermes_state import SessionDB
+
+ requests = []
+
+ def append(name, data):
+ text = json.dumps(data, ensure_ascii=False)
+ if any(secret in text for secret in secrets):
+ raise ValueError("Credential in captured body")
+ with (root / name).open("a", encoding="utf-8") as target:
+ target.write(text + "\n")
+
+ seen = set()
+ for name in ("httpx", "httpx2"):
+ try:
+ module = importlib.import_module(name)
+ except ImportError:
+ continue
+ if id(module.Client) in seen:
+ continue
+ seen.add(id(module.Client))
+ original = module.Client.send
+
+ def wrap_send(send):
+ def capture(client, request, *args, **kwargs):
+ captured = "/chat/completions" in request.url.path
+ if captured:
+ body = json.loads(request.content)
+ requests.append(body)
+ append("api_requests.jsonl", body)
+ response = send(client, request, *args, **kwargs)
+ if captured:
+ response.read()
+ try:
+ body = response.json()
+ except ValueError:
+ body = response.text
+ append("api_responses.jsonl", {"status": response.status_code, "body": body})
+ return response
+
+ return capture
+
+ module.Client.send = wrap_send(original)
+ agent = None
+ database = SessionDB(home / "state.db")
+ started = time.monotonic()
+ try:
+ agent = AIAgent(
+ base_url=values["LLM_BASE_URL"],
+ api_key=values["LLM_API_KEY"],
+ provider="custom",
+ api_mode="chat_completions",
+ model=task["model"],
+ max_iterations=12,
+ run_budget_seconds=240,
+ max_tokens=1024,
+ request_overrides={"reasoning_effort": "none", "extra_body": {"enable_thinking": False}},
+ enabled_toolsets=["file"],
+ save_trajectories=False,
+ quiet_mode=True,
+ skip_context_files=True,
+ skip_memory=True,
+ skip_background_review=True,
+ session_db=database,
+ cwd=str(work),
+ platform="batch",
+ )
+ from tools.registry import registry
+
+ entry = registry.get_entry("read_file")
+ original_read = entry.handler
+
+ def confined_read(args, **kwargs):
+ path = Path(args.get("path", ""))
+ resolved = (path if path.is_absolute() else work / path).resolve()
+ if not resolved.is_relative_to(work) or str(resolved.relative_to(work)) not in task["source_hashes"]:
+ return json.dumps({"error": "Only the listed source snapshots may be read"})
+ return original_read(args, **kwargs)
+
+ entry.handler = confined_read
+ agent.tools = [t for t in agent.tools if t["function"]["name"] == "read_file"]
+ agent.valid_tool_names = {"read_file"}
+ agent.context_compressor.enabled = False
+ result = agent.run_conversation(
+ task["prompt"],
+ task_id=root.name,
+ system_message=f"Investigate the supplied fixture evidence and propose actions only. "
+ f"Use only read_file within {work}. "
+ "Do not read environment files, credentials or any path outside this workspace.",
+ )
+ if not result.get("completed") or not requests:
+ raise ValueError("Hermes collection did not complete")
+ systems = [m for m in requests[0]["messages"] if m["role"] == "system"]
+ history = [*systems, *result["messages"]]
+ if len(systems) != 1 or history[-1]["role"] != "assistant" or history[-1].get("tool_calls"):
+ raise ValueError("Incomplete system prompt or final assistant report")
+ calls = {c["id"]: c for m in history for c in m.get("tool_calls", []) or []}
+ results = [m for m in history if m["role"] == "tool"]
+ if set(calls) != {m["tool_call_id"] for m in results}:
+ raise ValueError("Unpaired tool calls")
+ observed = set()
+ for message in results:
+ call = calls[message["tool_call_id"]]
+ args = json.loads(call["function"]["arguments"])
+ path = Path(args.get("path") or args.get("file_path"))
+ path = (path if path.is_absolute() else work / path).resolve()
+ output = json.loads(message["content"])
+ if not path.is_relative_to(work) or str(path.relative_to(work)) not in task["source_hashes"]:
+ if "error" not in output:
+ raise ValueError("Unexpected content from outside the source allowlist")
+ continue # Denied reads remain in the trajectory; they never reached the filesystem.
+ text = "\n".join(re.sub(r"^\d+\|", "", line) for line in output.get("content", "").splitlines())
+ if text.rstrip() == path.read_text(encoding="utf-8").rstrip():
+ observed.add(str(path.relative_to(work)))
+ if observed != set(task["source_hashes"]):
+ raise ValueError("Not every source snapshot was read without truncation")
+ if any(
+ hashlib.sha256((work / name).read_bytes()).hexdigest() != sha for name, sha in task["source_hashes"].items()
+ ):
+ raise ValueError("Source fixture changed")
+ write(root / "trajectory.full.json", {"messages": history})
+ write(root / "result.json", result)
+ write(root / "session_export.json", database.export_session(agent.session_id, include_compacted=True))
+ write(
+ root / "collection.json",
+ {
+ "completed": True,
+ "session_id": agent.session_id,
+ "messages": len(history),
+ "tool_calls": len(calls),
+ "articles_read": len(observed),
+ "source_unchanged": True,
+ "seconds": time.monotonic() - started,
+ "trajectory_sha256": hashlib.sha256((root / "trajectory.full.json").read_bytes()).hexdigest(),
+ },
+ )
+ if any(
+ secret.encode() in file.read_bytes() for file in root.rglob("*") if file.is_file() for secret in secrets
+ ):
+ raise ValueError("Credential in artifacts")
+ print(root.name, "collected", len(calls), "tool calls", flush=True)
+ finally:
+ if agent is not None:
+ agent.close()
+ database.close()
+
+
+def main():
+ """Internal subprocess entry point; provider authorization belongs to the parent collector."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--hermes", type=Path, required=True)
+ parser.add_argument("--env-file", type=Path, required=True)
+ args = parser.parse_args()
+ collect(args.root.resolve(), args.hermes.resolve(), args.env_file.resolve())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmark/compaction/repair_check.py b/benchmark/compaction/repair_check.py
deleted file mode 100644
index c4ed36a7..00000000
--- a/benchmark/compaction/repair_check.py
+++ /dev/null
@@ -1,139 +0,0 @@
-"""Run generated payment fixes with a deliberately restricted Python grammar."""
-
-import ast
-from types import SimpleNamespace
-
-
-class PaymentError(Exception):
- """A non-timeout provider error carrying an HTTP status."""
-
- status_code = 422
-
-
-def repair_checks(source): # pylint: disable=too-many-return-statements
- """Reject general Python programs; allow only a bounded retry function and inert clients."""
- names = ("success", "timeout_then_success", "http_402", "http_422", "exhaustion", "distinct_payments")
- failed = dict.fromkeys(names, False)
- try:
- tree = ast.parse(source)
- functions = [node for node in tree.body if isinstance(node, ast.FunctionDef)]
- if len(functions) != 1 or functions[0].name != "charge_with_retry":
- return failed
- for node in tree.body:
- if node is functions[0]:
- continue
- if not isinstance(node, ast.Import) or any(alias.name != "uuid" for alias in node.names):
- return failed
- allowed_calls = {"range", "str", "TimeoutError", "uuid.uuid4", "client.charge"}
- allowed_nodes = (
- ast.FunctionDef,
- ast.arguments,
- ast.arg,
- ast.Constant,
- ast.Name,
- ast.Load,
- ast.Store,
- ast.Assign,
- ast.For,
- ast.Call,
- ast.Attribute,
- ast.keyword,
- ast.Try,
- ast.ExceptHandler,
- ast.Return,
- ast.Raise,
- ast.If,
- ast.Compare,
- ast.Eq,
- ast.NotEq,
- ast.Lt,
- ast.LtE,
- ast.Gt,
- ast.GtE,
- ast.BinOp,
- ast.Sub,
- ast.Pass,
- ast.Expr,
- )
- for node in ast.walk(functions[0]):
- if not isinstance(node, allowed_nodes):
- return failed
- if isinstance(node, ast.FunctionDef) and node is not functions[0]:
- return failed
- if isinstance(node, ast.Constant) and (
- isinstance(node.value, str)
- and len(node.value) > 100
- or isinstance(node.value, int)
- and abs(node.value) > 10
- ):
- return failed
- if isinstance(node, ast.Attribute) and ast.unparse(node) not in {"uuid.uuid4", "client.charge"}:
- return failed
- if isinstance(node, ast.Call) and ast.unparse(node.func) not in allowed_calls:
- return failed
- if isinstance(node, ast.Name) and node.id.startswith("__"):
- return failed
- if functions[0].decorator_list or functions[0].returns or any(a.annotation for a in functions[0].args.args):
- return failed
- # No executable defaults, annotations or decorators in the accepted grammar.
- if any(not isinstance(node, ast.Constant) for node in functions[0].args.defaults):
- return failed
- if functions[0].args.kwonlyargs or functions[0].args.vararg or functions[0].args.kwarg:
- return failed
- if any(arg.annotation for arg in ast.walk(functions[0]) if isinstance(arg, ast.arg)):
- return failed
- if sum(isinstance(node, ast.For) for node in ast.walk(functions[0])) > 1:
- return failed
- serial = iter(range(1000))
-
- def bounded_range(*args):
- result = range(*args)
- if len(result) > 10:
- raise ValueError("Retry range exceeds test limit")
- return result
-
- namespace = {
- "__builtins__": {},
- "range": bounded_range,
- "str": str,
- "TimeoutError": TimeoutError,
- "uuid": SimpleNamespace(uuid4=lambda: f"test-key-{next(serial)}"),
- }
- module = ast.Module(body=functions, type_ignores=[])
- exec(compile(module, "", "exec"), namespace) # pylint: disable=exec-used
- fix = namespace["charge_with_retry"]
-
- def attempt(events, expected_calls, error=None):
- keys = []
- remaining = iter(events)
-
- def charge(invoice, idempotency_key):
- if invoice != "test-invoice":
- raise AssertionError("Invoice was changed")
- keys.append(idempotency_key)
- event = next(remaining)
- if isinstance(event, Exception):
- raise event
- return event
-
- try:
- result = fix(SimpleNamespace(charge=charge), "test-invoice")
- correct = error is None and result == "ok"
- except Exception as exc:
- correct = error is not None and exc is error
- return correct and len(keys) == expected_calls and len(set(keys)) == 1, keys
-
- result = {}
- result["success"], first = attempt(["ok"], 1)
- result["timeout_then_success"], _ = attempt([TimeoutError(), "ok"], 2)
- for status in (402, 422):
- error = PaymentError()
- error.status_code = status
- result[f"http_{status}"], _ = attempt([error, "ok"], 1, error)
- errors = [TimeoutError() for _ in range(3)]
- result["exhaustion"], _ = attempt(errors, 3, errors[-1])
- success, second = attempt(["ok"], 1)
- result["distinct_payments"] = success and result["success"] and first != second
- return result
- except Exception:
- return failed
diff --git a/benchmark/compaction/run.py b/benchmark/compaction/run.py
index c0d15c3f..cd88fcbb 100644
--- a/benchmark/compaction/run.py
+++ b/benchmark/compaction/run.py
@@ -1,17 +1,13 @@
-"""Frozen-summary experiment: Full context, S, and S + Jev Top 1–N."""
+"""Shared request budgeting, model calls and strict Top-N selection for the study."""
-import argparse
import asyncio
from dataclasses import asdict, dataclass, is_dataclass
-import hashlib
import json
from pathlib import Path
-import statistics
-from dotenv import dotenv_values
from openai import AsyncOpenAI
-from benchmark.compaction.cases import SUMMARY, SOURCE_SHA256, evaluate, event_units, load_case, render, write
+from benchmark.compaction.cases import SUMMARY, write
from reme.components.as_decision_model import AgentScopeAsDecisionModel
from reme.steps.compaction.compact_context import probability
@@ -151,264 +147,3 @@ def top_n(units, scores, n):
raise ValueError("Scores must be finite probabilities")
chosen = {u["id"] for u in sorted(units, key=lambda u: (-scores[u["id"]], u["id"]))[:n]}
return [u for u in units if u["id"] in chosen]
-
-
-def prepare(experiment, case, units, summaries, scores, maximum):
- """Freeze all inputs before answers; fusion never rewrites or shrinks its summary."""
- rows = []
- for index, summary in enumerate(summaries):
- for arm, n in [("full", 0), ("summary", 0), *[(f"fusion_top{n}", n) for n in range(1, maximum + 1)]]:
- chosen = top_n(units, scores[index], n)
- text = "" if arm == "full" else summary
- payload = None if arm == "full" else compose(text, render(chosen))
- context = messages(case, payload)
- rows.append(
- {
- "arm": arm,
- "repeat": index,
- "summary": text,
- "selected": [u["id"] for u in chosen],
- "estimated_input_tokens": experiment.input_tokens(context),
- "within_budget": arm == "full" or experiment.fits(case, payload),
- "valid": False,
- "trials": [],
- },
- )
- write(experiment.output / f"c{index}.{arm}.context.json", {"messages": context})
- return rows
-
-
-def aggregate(rows):
- """Keep every planned condition in the denominator, including failed generations."""
- result = []
- for arm in dict.fromkeys(r["arm"] for r in rows):
- group = [r for r in rows if r["arm"] == arm]
- valid = [r for r in group if r["valid"]]
- trials = [t for r in valid for t in r["trials"]]
- metrics = {}
- for section in ("facts", "task"):
- metrics[section] = statistics.mean(
- (
- statistics.mean(sum(t["checks"][section].values()) / len(t["checks"][section]) for t in r["trials"])
- if r["valid"] and r["trials"]
- else 0
- )
- for r in group
- )
- result.append(
- {
- "arm": arm,
- "valid": len(valid),
- "planned": len(group),
- "input_tokens": statistics.mean(t["usage"]["prompt_tokens"] for t in trials) if trials else None,
- **metrics,
- },
- )
- return result
-
-
-def report(output, rows):
- """Write one compact table; retain detailed provenance outside the repository."""
- table = aggregate(rows)
- write(output / "aggregate.json", table)
- lines = ["| Method | Input tokens | Fact accuracy | Repair pass |", "| --- | ---: | ---: | ---: |"]
- for row in table:
- label = {"full": "Full context", "summary": "S"}.get(
- row["arm"],
- row["arm"].replace("fusion_top", "S + Jev Top "),
- )
- tokens = f"{row['input_tokens']:.0f}" if row["input_tokens"] is not None else "—"
- lines.append(f"| {label} | {tokens} | {row['facts']:.1%} | {row['task']:.1%} |")
- lines += ["", "Valid/planned: " + "; ".join(f"{r['arm']} {r['valid']}/{r['planned']}" for r in table)]
- (output / "REPORT.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
-
-
-def frozen_inputs(path, case, units, count):
- """Load every previously frozen summary/ranking pair, never choose by answer accuracy."""
- source = json.loads((path / "source.json").read_text())
- if any(source[k] != case[k] for k in ("messages", "exam", "gold")) or source["units"] != units:
- raise ValueError("Frozen evidence does not match the original trajectory")
- rows = json.loads((path / "results.json").read_text())
- baseline = [r for r in rows if r["arm"] == "summary"]
- if len(baseline) != count or {r["repeat"] for r in baseline} != set(range(count)):
- raise ValueError("Use all frozen summary generations with the matching --compressions count")
- summaries = [next(r["summary"] for r in baseline if r["repeat"] == i) for i in range(count)]
- scores = [
- json.loads((path / "provenance" / f"paired.c{i}.decision0.response.json").read_text())["scores"]
- for i in range(count)
- ]
- if any(not isinstance(s, str) or not s for s in summaries):
- raise ValueError("Every frozen summary must be nonempty text")
- return summaries, scores
-
-
-async def answer(experiment, row, case, repeats):
- """Only complete answers within the actual downstream input ceiling receive credit."""
- label = f"c{row['repeat']}.{row['arm']}"
- try:
- if not row["within_budget"]:
- raise ValueError("Frozen context exceeds input ceiling; increase the ceiling without truncating evidence")
- context = json.loads((experiment.output / f"{label}.context.json").read_text())["messages"]
- trials = []
- for index in range(repeats):
- text, usage = await experiment.ask(f"{label}.answer{index}", context)
- if row["arm"] != "full" and usage["prompt_tokens"] > experiment.cap:
- raise ValueError("Actual API input exceeds ceiling")
- parsed = json.loads(text)
- trials.append({"answer": parsed, "usage": usage, "checks": evaluate(case, parsed)})
- row.update(valid=True, trials=trials)
- except Exception as exc:
- row.update(valid=False, error=type(exc).__name__, trials=[])
- print(label, "valid" if row["valid"] else row["error"], flush=True)
-
-
-async def run(args):
- """Fresh runs create one S/ranking per generation; replay keeps them byte-identical."""
- from tokenizers import Tokenizer
-
- repo = Path(__file__).resolve().parents[2]
- output = args.output.resolve()
- if output.exists() or output == repo or repo in output.parents:
- raise ValueError("Use a new output directory outside the repository")
- case = load_case(args.trajectory)
- units = event_units(case["messages"])
- if args.top_n > len(units):
- raise ValueError(f"Only {len(units)} evidence units: maximum Top {len(units)}")
- if args.summary_cap > args.input_cap:
- raise ValueError("Summary generation cap cannot exceed the final input cap")
- frozen = frozen_inputs(args.frozen_run, case, units, args.compressions) if args.frozen_run else None
- values = dotenv_values(repo / ".env") if not args.check_only else {}
- if not args.check_only and any(not values.get(k) for k in ("LLM_API_KEY", "LLM_BASE_URL")):
- raise ValueError("Missing authorized LLM configuration")
- if not frozen and not values.get("DECISION_MODEL_API_KEY"):
- raise ValueError("Fresh compression requires DECISION_MODEL_API_KEY")
- output.mkdir(parents=True)
- write(output / "source.json", {**case, "units": units})
- for name in ("run.py", "cases.py", "repair_check.py"):
- (output / name).write_bytes((Path(__file__).parent / name).read_bytes())
- write(
- output / "protocol.json",
- {
- "source_sha256": SOURCE_SHA256,
- "tokenizer_sha256": hashlib.sha256(args.tokenizer.read_bytes()).hexdigest(),
- "model": args.model,
- "summary_generation_input_cap": args.summary_cap,
- "compressed_input_cap": args.input_cap,
- "top_n": args.top_n,
- "evidence_count": len(units),
- "compressions": args.compressions,
- "answer_repeats": args.repeats,
- "summary_frozen": True,
- "selection": "Strict score prefix; no threshold; restore source order",
- "frozen_run": str(args.frozen_run.resolve()) if args.frozen_run else None,
- "check_only": args.check_only,
- "evaluation": "Direct Qwen; not Hermes continuation",
- },
- )
- client = None
- try:
- if not args.check_only:
- client = AsyncOpenAI(
- api_key=values["LLM_API_KEY"],
- base_url=values["LLM_BASE_URL"],
- timeout=90,
- max_retries=1,
- )
- experiment = Experiment(
- client,
- None,
- output,
- Tokenizer.from_file(str(args.tokenizer)),
- asyncio.Semaphore(args.concurrency),
- args.model,
- args.input_cap,
- )
- if frozen:
- summaries, scores = frozen
- else:
- credential = {
- "api_key": values["DECISION_MODEL_API_KEY"],
- "base_url": values.get("DECISION_MODEL_BASE_URL") or None,
- }
- model = values.get("DECISION_MODEL_NAME")
- model = None if model in (None, "", "null") else model
- summaries, scores = [], []
- async with AgentScopeAsDecisionModel(credential=credential, model=model, timeout=45) as decision:
- experiment.decision = decision
- allowance = args.summary_cap - RESERVE - experiment.input_tokens(messages(case, compose("", ""))) - 8
- if allowance < 40:
- raise ValueError("Insufficient summary capacity")
- for index in range(args.compressions):
- label = f"paired.c{index}"
- ranking, _ = await experiment.scores(label, case["messages"][1]["content"], units)
- summary, _ = await experiment.summarize(label, case["messages"][1]["content"], units, allowance)
- scores.append(ranking)
- summaries.append(summary)
- # Persist compressor outputs so a later replay can use them without new provider calls.
- (output / "provenance").mkdir()
- for index, ranking in enumerate(scores):
- write(output / "provenance" / f"paired.c{index}.decision0.response.json", {"scores": ranking})
- rows = prepare(experiment, case, units, summaries, scores, args.top_n)
- write(output / "frozen-selections.json", rows)
- write(output / "results.json", rows)
- if not args.check_only:
- work = asyncio.Semaphore(args.concurrency)
-
- async def condition(row):
- async with work:
- await answer(experiment, row, case, args.repeats)
- write(output / "results.json", rows)
- report(output, rows)
-
- async with asyncio.TaskGroup() as tasks:
- for row in rows:
- tasks.create_task(condition(row))
- load_case(args.trajectory)
- secrets = [values[k] for k in ("LLM_API_KEY", "DECISION_MODEL_API_KEY") if values.get(k)]
- if any(
- secret.encode() in file.read_bytes() for file in output.rglob("*") if file.is_file() for secret in secrets
- ):
- raise RuntimeError("Credential in artifacts")
- write(
- output / "validation.json",
- {
- "source_unchanged": True,
- "no_api_keys": True,
- "prepared": len(rows),
- "within_budget": sum(r["within_budget"] for r in rows),
- "valid": sum(r["valid"] for r in rows),
- "check_only": args.check_only,
- },
- )
- finally:
- if client is not None:
- await client.close()
-
-
-def main():
- """Require live opt-in or an offline frozen-input check; never overwrite existing runs."""
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--trajectory", type=Path, required=True)
- parser.add_argument("--tokenizer", type=Path, required=True)
- parser.add_argument("--output", type=Path, required=True)
- parser.add_argument("--frozen-run", type=Path)
- parser.add_argument("--summary-cap", type=int, default=3200)
- parser.add_argument("--input-cap", type=int, default=3800)
- parser.add_argument("--top-n", type=int, default=5)
- parser.add_argument("--compressions", type=int, default=5)
- parser.add_argument("--repeats", type=int, default=3)
- parser.add_argument("--concurrency", type=int, default=4)
- parser.add_argument("--model", default="qwen3.8-max")
- mode = parser.add_mutually_exclusive_group(required=True)
- mode.add_argument("--live", action="store_true")
- mode.add_argument("--check-only", action="store_true")
- args = parser.parse_args()
- if min(args.summary_cap, args.input_cap, args.top_n, args.compressions, args.repeats, args.concurrency) < 1:
- parser.error("All numerical settings must be positive")
- if args.check_only and not args.frozen_run:
- parser.error("--check-only requires --frozen-run")
- asyncio.run(run(args))
-
-
-if __name__ == "__main__":
- main()
diff --git a/benchmark/compaction/suite.py b/benchmark/compaction/suite.py
new file mode 100644
index 00000000..086788ba
--- /dev/null
+++ b/benchmark/compaction/suite.py
@@ -0,0 +1,455 @@
+"""Freeze rule-scored, read-only adaptations of fourteen distinct fixture tasks."""
+
+import ast
+import hashlib
+import importlib.util
+import json
+import re
+import random
+import subprocess
+import sys
+
+from benchmark.compaction.cases import GOLD, exact_match, write
+
+
+def module(path, name):
+ """Load fixture definitions only; never run their CLI or agent workers."""
+ spec = importlib.util.spec_from_file_location(name, path)
+ value = importlib.util.module_from_spec(spec)
+ sys.modules[name] = value
+ spec.loader.exec_module(value)
+ return value
+
+
+def context(text, pattern, radius=10):
+ """Freeze original matching lines with context, with no generated filler or answer lookup."""
+ lines = text.splitlines(keepends=True)
+ indices = {
+ j
+ for i, line in enumerate(lines)
+ if re.search(pattern, line)
+ for j in range(max(0, i - radius), min(len(lines), i + radius + 1))
+ }
+ return "".join(f"{i+1}|{lines[i]}" for i in sorted(indices))
+
+
+def planted_session(path, function, session_id, position):
+ """Extract a literal upstream seeded history without running DB setup or worker code."""
+ tree = ast.parse(path.read_text())
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == function:
+ if any(isinstance(a, ast.Constant) and a.value == session_id for a in node.args):
+ return ast.literal_eval(node.args[position])
+ raise ValueError("Expected upstream seeded session")
+
+
+def define(task_id, query, files, gold, fields, origin, *, query_only=False, unordered=()):
+ """Keep fixture answers outside the read-only tool workspace and compressor inputs."""
+ return {
+ "id": task_id,
+ "query": query,
+ "files": files,
+ "gold": gold,
+ "fields": fields,
+ "origin": origin,
+ "query_only": query_only,
+ "unordered": list(unordered),
+ }
+
+
+def definitions(hermes, scratch):
+ """Use pinned upstream fixture bytes; adapt actions to read-only investigation explicitly."""
+ core = module(hermes / "evals/core_tool_deferral/tasks.py", "compaction_core_tasks")
+ perf = module(hermes / "evals/toolperf_abeval/ab_eval.py", "compaction_perf_tasks")
+ read = module(hermes / "evals/readtool/fixtures.py", "compaction_read_fixtures")
+ core_root, perf_root, read_root = (scratch / name for name in ("core", "perf", "read"))
+ for root in (core_root, perf_root, read_root):
+ root.mkdir(parents=True)
+ for name in ("longrange_backup_pipeline", "eager_refactor_control", "todo_three_files", "config_grep_distractor"):
+ root = core_root / name
+ root.mkdir()
+ core.TASKS_BY_ID[name]["fixtures"](str(root))
+ perf.make_sandbox(perf_root)
+ read._write_package_json(read_root) # pylint: disable=protected-access
+ read._write_app_js(read_root) # pylint: disable=protected-access
+ read._write_unicode_notes(read_root) # pylint: disable=protected-access
+ read._write_server_log(read_root, random.Random(20260809)) # pylint: disable=protected-access
+ read._write_lockfile(read_root, random.Random(20260809)) # pylint: disable=protected-access
+ backup = planted_session(hermes / "evals/core_tool_deferral/worker.py", "sess", "seed_backup_talk", 1)
+ incident = planted_session(hermes / "evals/session_search_schema/fixtures.py", "mk", "20260818_140200_cc22dd", 4)
+
+ def core_files(name):
+ root = core_root / name
+ return {str(p.relative_to(root)): p.read_text() for p in root.rglob("*") if p.is_file()}
+
+ def core_query(name):
+ return core.TASKS_BY_ID[name]["prompt"]
+
+ def perf_files(*names):
+ return {name: (perf_root / "proj" / name).read_text() for name in names}
+
+ def perf_query(name):
+ return perf.TASKS[name].replace("{WORK}/proj", ".")
+
+ rows = []
+ rows.append(
+ define(
+ "payment",
+ "Investigate the original payment retry incident.",
+ {},
+ GOLD,
+ {k: k.replace("_", " ") for k in GOLD},
+ "Original Hermes payment fixture",
+ unordered=("no_retry_http_statuses",),
+ ),
+ )
+ files = core_files("longrange_backup_pipeline")
+ files["session_history.json"] = json.dumps(backup, indent=2)
+ rows.append(
+ define(
+ "backup",
+ core_query("longrange_backup_pipeline"),
+ files,
+ {"repo": "sparks-data", "ignored_repo": "toybox", "data_text": "important"},
+ {
+ "repo": "repository requiring backup",
+ "ignored_repo": "repository explicitly excluded",
+ "data_text": "contents of the target repository data file",
+ },
+ "core_tool_deferral/longrange_backup_pipeline",
+ ),
+ )
+ rows.append(
+ define(
+ "refactor",
+ core_query("eager_refactor_control"),
+ core_files("eager_refactor_control"),
+ {
+ "old_timeout": 30,
+ "call_sites": 3,
+ "functions": ["fetch_a", "fetch_b", "fetch_c"],
+ "methods": ["get", "post", "head"],
+ "old_version": "1.0",
+ },
+ {
+ "old_timeout": "original timeout integer",
+ "call_sites": "number of requests call sites",
+ "functions": "function names in source order",
+ "methods": "HTTP methods in source order",
+ "old_version": "existing changelog version",
+ },
+ "core_tool_deferral/eager_refactor_control",
+ ),
+ )
+ rows.append(
+ define(
+ "three_files",
+ core_query("todo_three_files"),
+ core_files("todo_three_files"),
+ {"file_count": 3, "header": "item,qty", "data_rows": 2},
+ {"file_count": "required output file count", "header": "CSV header", "data_rows": "CSV data row count"},
+ "core_tool_deferral/todo_three_files",
+ query_only=True,
+ ),
+ )
+ rows.append(
+ define(
+ "active_config",
+ core_query("config_grep_distractor"),
+ core_files("config_grep_distractor"),
+ {"retry_limit": 7, "backoff": 2, "active_file": "conf/app.ini", "old_file": "conf/old.ini"},
+ {
+ "retry_limit": "active integer value",
+ "backoff": "active integer backoff",
+ "active_file": "workspace-relative active config",
+ "old_file": "workspace-relative retired config",
+ },
+ "core_tool_deferral/config_grep_distractor",
+ ),
+ )
+ rows.append(
+ define(
+ "checkout_504",
+ (
+ "Investigate the checkout 504 incident: distinguish the failed rollback, "
+ "root cause and final database fix."
+ ),
+ {"session_history.json": json.dumps(incident, indent=2)},
+ {
+ "setting": "statement_timeout",
+ "value_seconds": 45,
+ "index_mode": "CONCURRENTLY",
+ "root_cause": "CREATE INDEX without CONCURRENTLY",
+ "rolled_back_version": "v2.30.2",
+ "rollback_fixed": False,
+ "deployed_version": "v2.31.0",
+ "initial_error_percent": 8,
+ },
+ {
+ "setting": "final database timeout setting name",
+ "value_seconds": "final timeout in seconds",
+ "index_mode": "mode used to recreate the index",
+ "root_cause": "SQL operation that competed with checkout writes",
+ "rolled_back_version": "rollback target version",
+ "rollback_fixed": "did rollback resolve the incident, boolean",
+ "deployed_version": "version deployed before the incident",
+ "initial_error_percent": "initial request error percentage as integer",
+ },
+ "session_search_schema/t2_scroll",
+ ),
+ )
+ rows.append(
+ define(
+ "dependency_version",
+ (
+ "What exact left-pad version is pinned, and which file declares the test script? "
+ "Record the package scripts for a handoff."
+ ),
+ {
+ "package.json": (read_root / "package.json").read_text(),
+ "package-lock.head.txt": "".join(
+ (read_root / "package-lock.json").read_text().splitlines(keepends=True)[:40],
+ ),
+ },
+ {
+ "version": "1.3.0",
+ "declaration": "package.json",
+ "test": "vitest run",
+ "build": "tsc -p .",
+ "package_version": "4.2.1",
+ },
+ {
+ "version": "pinned left-pad version",
+ "declaration": "file declaring the test script",
+ "test": "exact test command",
+ "build": "exact build command",
+ "package_version": "project package version",
+ },
+ "readtool/lockfile_version",
+ ),
+ )
+ tree = ast.parse((hermes / "evals/readtool/fixtures.py").read_text())
+ function = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == "_write_minified_bundle")
+ parts = next(
+ n
+ for n in ast.walk(function)
+ if isinstance(n, ast.Assign) and any(isinstance(t, ast.Name) and t.id == "parts" for t in n.targets)
+ )
+ fragment = ast.literal_eval(parts.value)[0]
+ rows.append(
+ define(
+ "retry_backoff",
+ "Find the retry-delay function in src/ and explain its base, growth, cap and jitter.",
+ {"src/app.js": (read_root / "src/app.js").read_text(), "src/app.min.head.txt": fragment},
+ {
+ "base_ms": 250,
+ "cap_ms": 30000,
+ "growth_factor": 2,
+ "jitter": "uniform_integer_below_bound",
+ "function": "retryDelay",
+ },
+ {
+ "base_ms": "base delay in milliseconds",
+ "cap_ms": "maximum delay in milliseconds",
+ "growth_factor": "exponential multiplier per attempt",
+ "jitter": "choose none or uniform_integer_below_bound",
+ "function": "function name",
+ },
+ "readtool/minified_backoff",
+ ),
+ )
+ log = (read_root / "logs/server.log").read_text()
+ rows.append(
+ define(
+ "server_error",
+ (
+ "Locate the unique ERROR in the server log and retain its exact request, timestamp, "
+ "upstream and connection failure."
+ ),
+ {
+ "server.head.txt": "".join(log.splitlines(keepends=True)[:20]),
+ "server.error-context.txt": context(log, r"\bERROR\b"),
+ },
+ {
+ "request_id": "req-7f3d9",
+ "timestamp": "2026-08-08T23:41:17Z",
+ "status": 502,
+ "upstream": "payments",
+ "error": "ECONNREFUSED",
+ "address": "10.0.4.17:8443",
+ },
+ {
+ "request_id": "error request ID",
+ "timestamp": "full UTC error timestamp",
+ "status": "HTTP integer status",
+ "upstream": "failed upstream service",
+ "error": "connection error code",
+ "address": "upstream host and port",
+ },
+ "readtool/log_error_hunt",
+ ),
+ )
+ notes = next((read_root / "notes").iterdir())
+ rows.append(
+ define(
+ "meeting_notes",
+ "Read the meeting notes and report the third bullet exactly, retaining the other agreed actions.",
+ {"notes.txt": notes.read_text()},
+ {
+ "third": "rotate the API keys quarterly",
+ "first": "ship the payments retry fix",
+ "second": "audit the staging TLS certs",
+ "fourth": "close out the Q3 incident review",
+ },
+ {k: f"exact {k} bullet" for k in ("third", "first", "second", "fourth")},
+ "readtool/unicode_filename",
+ ),
+ )
+ rows.append(
+ define(
+ "targeted_edit",
+ perf_query("err_ambiguous_edit"),
+ perf_files("handlers.py", "check_handlers.py"),
+ {
+ "target": "slow_handler",
+ "original_timeouts": {"fast_handler": 10, "slow_handler": 10, "medium_handler": 10},
+ "required_timeouts": {"fast_handler": 10, "slow_handler": 60, "medium_handler": 10},
+ "success_marker": "HANDLERS_OK_552",
+ },
+ {
+ "target": "function requiring modification",
+ "original_timeouts": "mapping of all original function timeouts",
+ "required_timeouts": "mapping after the requested fix",
+ "success_marker": "checker success marker",
+ },
+ "toolperf_abeval/err_ambiguous_edit",
+ ),
+ )
+ output = subprocess.check_output([sys.executable, str(perf_root / "proj/noisy_build.py")], text=True)
+ rows.append(
+ define(
+ "build_token",
+ perf_query("err_big_output"),
+ {
+ **perf_files("noisy_build.py"),
+ "build.head.txt": "".join(output.splitlines(keepends=True)[:20]),
+ "build.token-context.txt": context(output, r"^UNIQUE_TOKEN="),
+ },
+ {"token": "tok_9f31c_middle", "step": 2000, "total_steps": 4000, "prefix": "UNIQUE_TOKEN="},
+ {
+ "token": "full unique token value without its prefix",
+ "step": "build loop step that prints the token",
+ "total_steps": "build loop iteration count",
+ "prefix": "exact token line prefix",
+ },
+ "toolperf_abeval/err_big_output",
+ ),
+ )
+ rows.append(
+ define(
+ "package_versions",
+ perf_query("err_multi_dir"),
+ perf_files("pkg_a/version.txt", "pkg_b/version.txt", "pkg_c/version.txt"),
+ {"versions": {"pkg_a": "1.4.2", "pkg_b": "0.9.7", "pkg_c": "3.2.1"}, "joined": "1.4.2,0.9.7,3.2.1"},
+ {
+ "versions": "mapping from package directory to exact version",
+ "joined": "comma-separated versions in a,b,c order",
+ },
+ "toolperf_abeval/err_multi_dir",
+ ),
+ )
+ anomaly = (perf_root / "proj/records.log").read_text()
+ rows.append(
+ define(
+ "checksum_anomaly",
+ perf_query("err_big_file_read"),
+ {
+ "records.head.txt": "".join(anomaly.splitlines(keepends=True)[:20]),
+ "records.anomaly-context.txt": context(anomaly, r"^ANOMALY:"),
+ },
+ {
+ "line": "ANOMALY: checksum drift detected in shard 7 (code X99Q)",
+ "code": "X99Q",
+ "shard": 7,
+ "line_number": 4201,
+ },
+ {
+ "line": "full anomaly line without display line number",
+ "code": "anomaly code",
+ "shard": "integer shard number",
+ "line_number": "one-based original line number",
+ },
+ "toolperf_abeval/err_big_file_read",
+ ),
+ )
+ return rows
+
+
+def exam(case):
+ """Supply field names/descriptions, never their expected values, to the answering model."""
+ prompt = (
+ 'Using only recorded evidence, return JSON {"facts": {...}}. No tools. Missing facts must be null. '
+ "Use exact strings and JSON types; numbers are integers. Answer these fields: "
+ + json.dumps(case["fields"], ensure_ascii=False)
+ )
+ if case["id"] == "payment":
+ prompt += (
+ " regression_attempt_counts must be a JSON object with keys "
+ "timeout_then_success, validation_422 and three_timeouts, each mapping to an integer count; "
+ "do not return an array."
+ )
+ if case["id"] == "meeting_notes":
+ prompt += " Return the bullet text without its leading list marker (-), indentation or surrounding whitespace."
+ return prompt
+
+
+def grade(case, prediction):
+ """Type-strict field checks; declared unordered lists ignore order, with no LLM judge."""
+ observed = prediction.get("facts", {}) if isinstance(prediction, dict) else {}
+ observed = observed if isinstance(observed, dict) else {}
+ checks = {}
+ for key, expected in case["gold"].items():
+ value = observed.get(key)
+ if key in case["unordered"] and isinstance(value, list):
+ value, expected = sorted(value, key=str), sorted(expected, key=str)
+ checks[key] = exact_match(value, expected)
+ return checks
+
+
+def freeze(hermes, output):
+ """Write all queries, answers and rules before model calls; runtime artifacts stay outside git."""
+ output.mkdir(parents=True)
+ rows = definitions(hermes, output / "upstream-fixtures")
+ for case in rows:
+ root = output / case["id"]
+ work = root / "workspace"
+ work.mkdir(parents=True)
+ for name, text in case["files"].items():
+ path = work / name
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(text)
+ case["source_chars"] = sum(map(len, case["files"].values()))
+ case["control_reason"] = (
+ "query_only" if case["query_only"] else "short_fixture" if case["source_chars"] < 2000 else None
+ )
+ if case["id"] == "payment":
+ case["control_reason"] = None
+ files = case.pop("files")
+ case["source_hashes"] = {name: hashlib.sha256(text.encode()).hexdigest() for name, text in files.items()}
+ prompt = (
+ "Read-only evidence collection for this task: " + case["query"] + "\n"
+ "Read ALL of these snapshot files completely using read_file: " + ", ".join(files) + ". "
+ "Then report findings and proposed actions only. Do not modify files or run commands. "
+ "Snapshots preserve selected original fixture regions; display line numbers refer to the original file."
+ )
+ write(
+ root / "collection-task.json",
+ {"prompt": prompt, "model": "qwen3.8-max", "source_hashes": case["source_hashes"]},
+ )
+ manifest = {
+ "hermes_commit": subprocess.check_output(["git", "-C", str(hermes), "rev-parse", "HEAD"], text=True).strip(),
+ "source": "Local Hermes MIT-licensed task fixtures; explicitly adapted read-only",
+ "cases": rows,
+ }
+ write(output / "corpus.json", manifest)
+ return manifest
diff --git a/benchmark/compaction/suite_collect.py b/benchmark/compaction/suite_collect.py
new file mode 100644
index 00000000..2fdd0f7e
--- /dev/null
+++ b/benchmark/compaction/suite_collect.py
@@ -0,0 +1,88 @@
+"""Freeze the fourteen-task oracle panel, then collect real read-only Hermes trajectories."""
+
+import argparse
+import asyncio
+import hashlib
+from pathlib import Path
+import shutil
+
+from benchmark.compaction.cases import SOURCE_SHA256, write
+from benchmark.compaction.suite import freeze
+
+
+async def run(args):
+ """Run isolated finite subprocesses; preserve every planned task and failure record."""
+ repo = Path(__file__).resolve().parents[2]
+ output = args.output.resolve()
+ if output.exists() or output == repo or repo in output.parents:
+ raise ValueError("Use a new directory outside repository")
+ payment = args.payment_trajectory.read_bytes()
+ if hashlib.sha256(payment).hexdigest() != SOURCE_SHA256:
+ raise ValueError("Expected the pinned original payment source")
+ manifest = freeze(args.hermes.resolve(), output)
+ shutil.copy2(args.payment_trajectory, output / "payment/trajectory.full.json")
+ write(output / "payment/collection.json", {"completed": True, "reused": True, "trajectory_sha256": SOURCE_SHA256})
+ for name in ("suite.py", "suite_collect.py", "hermes_worker.py", "STUDY.md"):
+ shutil.copy2(Path(__file__).parent / name, output / name)
+ semaphore = asyncio.Semaphore(args.concurrency)
+ statuses = []
+
+ async def collect(case):
+ if case["id"] == "payment":
+ return
+ root = output / case["id"]
+ async with semaphore:
+ with (root / "worker.log").open("w") as log:
+ process = await asyncio.create_subprocess_exec(
+ str(args.hermes_python),
+ "-m",
+ "benchmark.compaction.hermes_worker",
+ "--root",
+ str(root),
+ "--hermes",
+ str(args.hermes.resolve()),
+ "--env-file",
+ str(repo / ".env"),
+ cwd=repo,
+ stdout=log,
+ stderr=log,
+ )
+ try:
+ code = await asyncio.wait_for(process.wait(), 360)
+ except BaseException:
+ if process.returncode is None:
+ process.terminate()
+ try:
+ await asyncio.wait_for(process.wait(), 10)
+ except asyncio.TimeoutError:
+ process.kill()
+ await process.wait()
+ raise
+ statuses.append({"id": case["id"], "completed": code == 0, "returncode": code})
+ write(output / "collection-status.json", statuses)
+ print(case["id"], "collected" if code == 0 else "FAILED", flush=True)
+
+ async with asyncio.TaskGroup() as tasks:
+ for case in manifest["cases"]:
+ tasks.create_task(collect(case))
+ if any(not s["completed"] for s in statuses):
+ raise RuntimeError("Frozen collection failures remain; inspect retained logs")
+
+
+def main():
+ """Only explicit live collection can contact the configured provider."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--hermes", type=Path, required=True)
+ parser.add_argument("--hermes-python", type=Path, required=True)
+ parser.add_argument("--payment-trajectory", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--concurrency", type=int, default=4)
+ parser.add_argument("--live", action="store_true")
+ args = parser.parse_args()
+ if not args.live or args.concurrency < 1:
+ parser.error("Use --live and positive concurrency")
+ asyncio.run(run(args))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmark/compaction/suite_native.py b/benchmark/compaction/suite_native.py
new file mode 100644
index 00000000..b57febb9
--- /dev/null
+++ b/benchmark/compaction/suite_native.py
@@ -0,0 +1,240 @@
+"""Score forced native summaries alongside the fixed seven-method task panel."""
+
+import argparse
+import asyncio
+from copy import deepcopy
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+
+from dotenv import dotenv_values
+from openai import AsyncOpenAI
+
+from benchmark.compaction.cases import write
+from benchmark.compaction.run import Experiment, compose, messages
+from benchmark.compaction.suite import grade
+from benchmark.compaction.suite_run import ARMS, load_cases, report
+
+
+def validate_base(evaluation, corpus, cases):
+ """Reject incompatible base panels before launching any paid native generation."""
+ protocol = json.loads((evaluation / "protocol.json").read_text())
+ expected = {
+ "model": "qwen3.8-max",
+ "compressions": 3,
+ "answer_repeats": 2,
+ "corpus_sha256": hashlib.sha256(corpus.read_bytes()).hexdigest(),
+ }
+ rows = json.loads((evaluation / "results.json").read_text())
+ planned = {(key, arm, i) for key in cases for arm in ARMS for i in range(3)}
+ actual = {(r["case"], r["arm"], r["repeat"]) for r in rows}
+ if len(cases) != 14 or any(protocol.get(k) != v for k, v in expected.items()):
+ raise ValueError("Native adapter requires the matching fourteen-task Qwen 3x2 panel")
+ if actual != planned or len(rows) != len(planned):
+ raise ValueError("Base evaluation must contain each planned context exactly once")
+ return rows
+
+
+async def prepare_native(args, cases, repo):
+ """Prepare a new isolated native generation set or validate an existing frozen set."""
+ if args.native.exists():
+ if not all((args.native / key / "generation/summaries.json").is_file() for key in cases):
+ raise ValueError("Native generation set is incomplete; preserve it and use a new path")
+ return
+ if not args.hermes or not args.hermes_python:
+ raise ValueError("New native generations require --hermes and --hermes-python")
+ native = args.native.resolve()
+ if native == repo or repo in native.parents:
+ raise ValueError("Native artifacts must stay outside repository")
+ native.mkdir(parents=True)
+ for name in ("hermes_summary.py", "STUDY.md"):
+ shutil.copy2(Path(__file__).parent / name, native / name)
+ limit = asyncio.Semaphore(4)
+
+ async def worker(key):
+ async with limit:
+ root = native / key
+ root.mkdir()
+ env = dict(os.environ)
+ env["PYTHONPATH"] = str(repo) + os.pathsep + env.get("PYTHONPATH", "")
+ with (root / "worker.log").open("w", encoding="utf-8") as log:
+ process = await asyncio.create_subprocess_exec(
+ str(args.hermes_python),
+ "-m",
+ "benchmark.compaction.hermes_summary",
+ "--source",
+ str((args.evaluation / key / "source.json").resolve()),
+ "--output",
+ str(root / "generation"),
+ "--hermes",
+ str(args.hermes.resolve()),
+ "--env-file",
+ str(repo / ".env"),
+ "--tokenizer",
+ str(args.tokenizer.resolve()),
+ cwd=repo,
+ env=env,
+ stdout=log,
+ stderr=log,
+ )
+ try:
+ code = await asyncio.wait_for(process.wait(), 600)
+ except BaseException:
+ if process.returncode is None:
+ process.terminate()
+ try:
+ await asyncio.wait_for(process.wait(), 10)
+ except asyncio.TimeoutError:
+ process.kill()
+ await process.wait()
+ raise
+ if code:
+ raise RuntimeError("Native worker failed: " + key)
+
+ async with asyncio.TaskGroup() as tasks:
+ for key in cases:
+ tasks.create_task(worker(key))
+
+
+async def run(args):
+ """Keep all native generations, including non-shrinking failures, in the denominator."""
+ from tokenizers import Tokenizer
+
+ repo = Path(__file__).resolve().parents[2]
+ output = args.output.resolve()
+ if output.exists() or output == repo or repo in output.parents:
+ raise ValueError("Use a new output directory outside repository")
+ _, cases = load_cases(args.corpus.resolve())
+ original = validate_base(args.evaluation, args.corpus, cases)
+ await prepare_native(args, cases, repo)
+ rows = deepcopy(original)
+ output.mkdir(parents=True)
+ for name in ("suite_native.py", "hermes_summary.py", "suite.py", "STUDY.md"):
+ shutil.copy2(Path(__file__).parent / name, output / name)
+ write(
+ output / "protocol.json",
+ {
+ "base_evaluation": str(args.evaluation.resolve()),
+ "native_generations": str(args.native.resolve()),
+ "methods": 8,
+ "tasks": 14,
+ "generations": 3,
+ "answers_per_context": 2,
+ "total_planned_contexts": 336,
+ "total_planned_answers": 672,
+ "native_strategy": (
+ "Hermes small-window adapter: legacy generator, S-matched 64..256 budget, "
+ "hard word limit, native body-only carrier"
+ ),
+ "acceptance": "native actual API input < corresponding Full and <= the predeclared case ceiling",
+ "no_full_fallback": True,
+ "judge": "same strict JSON field grader; no LLM judge",
+ },
+ )
+ native_rows = []
+ for key, case in cases.items():
+ root = output / key
+ root.mkdir()
+ generated = json.loads((args.native / key / "generation/summaries.json").read_text())
+ assert len(generated) == 3 and {r["repeat"] for r in generated} == set(range(3))
+ for entry in generated:
+ i = entry["repeat"]
+ full = next(r for r in original if r["case"] == key and r["repeat"] == i and r["arm"] == "full")
+ full_tokens = full["trials"][0]["usage"]["prompt_tokens"]
+ row = {
+ "case": key,
+ "arm": "hermes_summary",
+ "repeat": i,
+ "summary": entry["summary"],
+ "selected": [],
+ "valid": False,
+ "trials": [],
+ "generation_valid": entry["valid"],
+ "native_budget": entry["native_budget"],
+ "summary_tokens": entry["summary_tokens"],
+ "input_cap": full["input_cap"],
+ "full_input_tokens": full_tokens,
+ }
+ write(
+ root / f"c{i}.hermes_summary.context.json",
+ {"messages": messages(case, compose(entry["summary"] or "", ""))},
+ )
+ native_rows.append(row)
+ rows.extend(native_rows)
+ write(output / "frozen-selections.json", rows)
+ write(output / "results.json", rows)
+ values = dotenv_values(repo / ".env")
+ client = AsyncOpenAI(api_key=values["LLM_API_KEY"], base_url=values["LLM_BASE_URL"], timeout=90, max_retries=1)
+ tokenizer = Tokenizer.from_file(str(args.tokenizer))
+ limit = asyncio.Semaphore(4)
+ work = asyncio.Semaphore(4)
+
+ async def answer(row):
+ async with work:
+ root = output / row["case"]
+ label = f"c{row['repeat']}.hermes_summary"
+ exp = Experiment(client, None, root, tokenizer, limit, "qwen3.8-max", row["input_cap"])
+ try:
+ if not row["generation_valid"]:
+ raise ValueError("Native summary generation failed its body budget")
+ context = json.loads((root / f"{label}.context.json").read_text())["messages"]
+ trials = []
+ for j in range(2):
+ text, usage = await exp.ask(f"{label}.answer{j}", context)
+ prediction = json.loads(text)
+ trials.append(
+ {"answer": prediction, "usage": usage, "checks": grade(cases[row["case"]], prediction)},
+ )
+ row["trials"] = trials
+ shorter = all(t["usage"]["prompt_tokens"] < row["full_input_tokens"] for t in trials)
+ fits = all(t["usage"]["prompt_tokens"] <= row["input_cap"] for t in trials)
+ row.update(
+ valid=shorter and fits,
+ error=(
+ None
+ if shorter and fits
+ else "not_shorter_than_full" if not shorter else "input_ceiling_exceeded"
+ ),
+ )
+ except Exception as exc:
+ row.update(valid=False, error=type(exc).__name__, trials=[])
+ write(output / "results.json", rows)
+ report(output, rows, cases, 2, include_native=True)
+ print(row["case"], label, "OK" if row["valid"] else row["error"], flush=True)
+
+ try:
+ async with asyncio.TaskGroup() as tasks:
+ for row in native_rows:
+ tasks.create_task(answer(row))
+ write(
+ output / "validation.json",
+ {
+ "planned_contexts": len(rows),
+ "valid_contexts": sum(r["valid"] for r in rows),
+ "actual_answers": sum(len(r["trials"]) for r in rows),
+ "native_valid": sum(r["valid"] for r in native_rows),
+ "native_failed": sum(not r["valid"] for r in native_rows),
+ },
+ )
+ finally:
+ await client.close()
+
+
+def main():
+ """A prepared, frozen native baseline must exist before scored calls are allowed."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ for name in ("corpus", "evaluation", "native", "tokenizer", "output"):
+ parser.add_argument("--" + name, type=Path, required=True)
+ parser.add_argument("--hermes", type=Path)
+ parser.add_argument("--hermes-python", type=Path)
+ parser.add_argument("--live", action="store_true")
+ args = parser.parse_args()
+ if not args.live:
+ parser.error("Use --live to authorize provider calls")
+ asyncio.run(run(args))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmark/compaction/suite_run.py b/benchmark/compaction/suite_run.py
new file mode 100644
index 00000000..ceb8267c
--- /dev/null
+++ b/benchmark/compaction/suite_run.py
@@ -0,0 +1,314 @@
+"""Evaluate frozen-summary Top-N curves on the predeclared fixture task panel."""
+
+import argparse
+import asyncio
+import hashlib
+import json
+from pathlib import Path
+import shutil
+import statistics
+
+from dotenv import dotenv_values
+from openai import AsyncOpenAI
+
+from benchmark.compaction.cases import event_units, render, write
+from benchmark.compaction.run import Experiment, RESERVE, compose, messages, top_n
+from benchmark.compaction.suite import exam, grade
+from reme.components.as_decision_model import AgentScopeAsDecisionModel
+
+ARMS = ["full", "summary"] + [f"fusion_top{n}" for n in range(1, 6)]
+LABELS = ["Full context", "S"] + [f"S + Jev Top {n}" for n in range(1, 6)]
+
+
+def load_cases(corpus_path):
+ """Project public chat fields, verify source hashes and exclude final collection reports."""
+ manifest = json.loads(corpus_path.read_text())
+ root = corpus_path.parent
+ result = {}
+ for definition in manifest["cases"]:
+ task = root / definition["id"]
+ source = task / "trajectory.full.json"
+ meta = json.loads((task / "collection.json").read_text())
+ if not meta["completed"] or hashlib.sha256(source.read_bytes()).hexdigest() != meta["trajectory_sha256"]:
+ raise ValueError("Incomplete or changed collection: " + definition["id"])
+ for name, sha in definition["source_hashes"].items():
+ if hashlib.sha256((task / "workspace" / name).read_bytes()).hexdigest() != sha:
+ raise ValueError("Changed source fixture")
+ history = json.loads(source.read_text())["messages"]
+ if history[-1]["role"] != "assistant" or history[-1].get("tool_calls"):
+ raise ValueError("Expected the final collection report to exclude")
+ allowed = {"role", "content", "tool_calls", "tool_call_id", "name"}
+ history = [{k: v for k, v in m.items() if k in allowed} for m in history[:-1]]
+ for m in history:
+ if m.get("tool_calls"):
+ m["tool_calls"] = [
+ {
+ "id": c["id"],
+ "type": "function",
+ "function": {k: c["function"][k] for k in ("name", "arguments")},
+ }
+ for c in m["tool_calls"]
+ ]
+ if [m["role"] for m in history[:2]] != ["system", "user"]:
+ raise ValueError("Missing real system/task")
+ calls = [c["id"] for m in history for c in m.get("tool_calls", []) or []]
+ outputs = [m["tool_call_id"] for m in history if m["role"] == "tool"]
+ if len(calls) != len(set(calls)) or sorted(calls) != sorted(outputs):
+ raise ValueError("Unpaired or duplicate calls")
+ case = {
+ **definition,
+ "messages": history,
+ "exam": exam(definition),
+ "trajectory_sha256": meta["trajectory_sha256"],
+ }
+ result[definition["id"]] = case
+ return manifest, result
+
+
+def metrics(rows, ids, repeats, include_native=False):
+ """Equal task weights; invalid planned contexts remain zero instead of disappearing."""
+ table = []
+ arms = ARMS[:2] + ["hermes_summary"] + ARMS[2:] if include_native else ARMS
+ labels = LABELS[:2] + ["Hermes small-window adapter"] + LABELS[2:] if include_native else LABELS
+ for arm, label in zip(arms, labels):
+ task_facts = []
+ task_exact = []
+ tokens = []
+ valid = 0
+ for task_id in ids:
+ group = [r for r in rows if r["case"] == task_id and r["arm"] == arm]
+ task_facts.append(
+ statistics.mean(
+ (
+ sum(sum(t["checks"].values()) / len(t["checks"]) for t in r["trials"]) / repeats
+ if r["valid"]
+ else 0
+ )
+ for r in group
+ ),
+ )
+ task_exact.append(
+ statistics.mean(
+ sum(all(t["checks"].values()) for t in r["trials"]) / repeats if r["valid"] else 0 for r in group
+ ),
+ )
+ valid += sum(r["valid"] for r in group)
+ tokens.extend(t["usage"]["prompt_tokens"] for r in group for t in r["trials"])
+ table.append(
+ {
+ "arm": arm,
+ "label": label,
+ "facts": statistics.mean(task_facts),
+ "exact": statistics.mean(task_exact),
+ "input_tokens": statistics.mean(tokens) if tokens else None,
+ "valid_contexts": valid,
+ "planned_contexts": sum(r["case"] in ids and r["arm"] == arm for r in rows),
+ },
+ )
+ return table
+
+
+def report(output, rows, cases, repeats, include_native=False):
+ """Keep primary and short/query-only controls separate in compact seven-row tables."""
+ result = {}
+ lines = ["# Fourteen-task fixed-summary study", ""]
+ for cohort in ("primary", "controls"):
+ ids = [k for k, c in cases.items() if (c["control_reason"] is None) == (cohort == "primary")]
+ result[cohort] = metrics(rows, ids, repeats, include_native)
+ lines += [
+ f"## {cohort.title()} ({len(ids)} tasks)",
+ "",
+ "| Method | Mean input tokens | Fact score | All facts correct | Valid contexts |",
+ "| --- | ---: | ---: | ---: | --- |",
+ ]
+ for r in result[cohort]:
+ tokens = f"{r['input_tokens']:.0f}" if r["input_tokens"] is not None else "—"
+ lines.append(
+ f"| {r['label']} | {tokens} | {r['facts']:.1%} | {r['exact']:.1%} | "
+ f"{r['valid_contexts']}/{r['planned_contexts']} |",
+ )
+ lines += [""]
+ result["per_task"] = {k: metrics(rows, [k], repeats, include_native) for k in cases}
+ write(output / "aggregate.json", result)
+ (output / "REPORT.md").write_text("\n".join(lines) + "\n")
+
+
+async def run(args):
+ """Freeze all inputs across tasks before any scored answers; no outcome-based tuning."""
+ from tokenizers import Tokenizer
+
+ repo = Path(__file__).resolve().parents[2]
+ output = args.output.resolve()
+ if output.exists() or output == repo or repo in output.parents:
+ raise ValueError("Use a new directory outside repository")
+ manifest, cases = load_cases(args.corpus.resolve())
+ original = args.corpus.read_bytes()
+ values = dotenv_values(repo / ".env")
+ for key in ("LLM_API_KEY", "LLM_BASE_URL", "DECISION_MODEL_API_KEY"):
+ if not values.get(key):
+ raise ValueError("Missing " + key)
+ output.mkdir(parents=True)
+ write(output / "corpus.json", manifest)
+ for name in ("suite.py", "suite_run.py", "run.py", "cases.py", "STUDY.md"):
+ shutil.copy2(Path(__file__).parent / name, output / name)
+ write(
+ output / "protocol.json",
+ {
+ "corpus_sha256": hashlib.sha256(original).hexdigest(),
+ "model": args.model,
+ "compressions": args.compressions,
+ "answer_repeats": args.repeats,
+ "top_n": [1, 2, 3, 4, 5],
+ "concurrency": args.concurrency,
+ "summary_allowance": "20% of rendered evidence tokens, clamped to 64..256",
+ "input_ceiling": (
+ "protected context + five largest whole units + summary allowance + 96 reserve; "
+ "adjusted before answers if serialized S needs more space"
+ ),
+ "cohorts": {k: c["control_reason"] for k, c in cases.items()},
+ "judge": "recursive type-strict JSON field rules",
+ "tokenizer_sha256": hashlib.sha256(args.tokenizer.read_bytes()).hexdigest(),
+ "evaluation": "direct Qwen; no Hermes continuation",
+ },
+ )
+ client = AsyncOpenAI(api_key=values["LLM_API_KEY"], base_url=values["LLM_BASE_URL"], timeout=90, max_retries=1)
+ limit = asyncio.Semaphore(args.concurrency)
+ tokenizer = Tokenizer.from_file(str(args.tokenizer))
+ experiments = {}
+ rows = []
+ credential = {
+ "api_key": values["DECISION_MODEL_API_KEY"],
+ "base_url": values.get("DECISION_MODEL_BASE_URL") or None,
+ }
+ model = values.get("DECISION_MODEL_NAME")
+ model = None if model in (None, "", "null") else model
+ try:
+ async with AgentScopeAsDecisionModel(credential=credential, model=model, timeout=45) as decision:
+ work = asyncio.Semaphore(args.concurrency)
+
+ async def prepare_case(task_id, case):
+ async with work:
+ root = output / task_id
+ root.mkdir()
+ exp = Experiment(client, decision, root, tokenizer, limit, args.model, 100000)
+ units = event_units(case["messages"])
+ allowance = max(64, min(256, int(exp.count(render(units)) * 0.2)))
+ write(root / "source.json", {**case, "units": units})
+ base = exp.input_tokens(messages(case, compose("", "")))
+ largest = sorted(
+ units,
+ key=lambda u: exp.input_tokens(messages(case, compose("", render([u])))) - base,
+ reverse=True,
+ )[:5]
+ cap = exp.input_tokens(messages(case, compose("", render(largest)))) + allowance + RESERVE + 32
+ prepared = []
+ for i in range(args.compressions):
+ scores, _ = await exp.scores(f"paired.c{i}", case["messages"][1]["content"], units)
+ summary, _ = await exp.summarize(
+ f"paired.c{i}",
+ case["messages"][1]["content"],
+ units,
+ allowance,
+ )
+ for arm in ARMS:
+ n = int(arm[-1]) if arm.startswith("fusion") else 0
+ chosen = top_n(units, scores, min(n, len(units)))
+ s = "" if arm == "full" else summary
+ payload = None if arm == "full" else compose(s, render(chosen))
+ context = messages(case, payload)
+ label = f"c{i}.{arm}"
+ write(root / f"{label}.context.json", {"messages": context})
+ prepared.append(
+ {
+ "case": task_id,
+ "arm": arm,
+ "repeat": i,
+ "summary": s,
+ "selected": [u["id"] for u in chosen],
+ "actual_n": len(chosen),
+ "estimated_input_tokens": exp.input_tokens(context),
+ "valid": False,
+ "trials": [],
+ },
+ )
+ cap = max(cap, max(r["estimated_input_tokens"] + RESERVE for r in prepared if r["arm"] != "full"))
+ exp.cap = cap
+ experiments[task_id] = exp
+ for r in prepared:
+ r["input_cap"] = cap
+ rows.extend(prepared)
+ write(root / "budget.json", {"allowance": allowance, "input_cap": cap, "units": len(units)})
+ print("PREPARED", task_id, len(units), "units", "summary", allowance, "cap", cap, flush=True)
+
+ async with asyncio.TaskGroup() as tasks:
+ for key, case in cases.items():
+ tasks.create_task(prepare_case(key, case))
+ rows.sort(key=lambda r: (list(cases).index(r["case"]), r["repeat"], ARMS.index(r["arm"])))
+ write(output / "frozen-selections.json", rows)
+ write(output / "results.json", rows)
+ answer_limit = asyncio.Semaphore(args.concurrency)
+
+ async def answer(row):
+ async with answer_limit:
+ exp = experiments[row["case"]]
+ label = f"c{row['repeat']}.{row['arm']}"
+ try:
+ context = json.loads((exp.output / f"{label}.context.json").read_text())["messages"]
+ trials = []
+ for j in range(args.repeats):
+ text, usage = await exp.ask(f"{label}.answer{j}", context)
+ if row["arm"] != "full" and usage["prompt_tokens"] > exp.cap:
+ raise ValueError("Actual input ceiling exceeded")
+ prediction = json.loads(text)
+ checks = grade(cases[row["case"]], prediction)
+ trials.append({"answer": prediction, "usage": usage, "checks": checks})
+ row.update(valid=True, trials=trials)
+ except Exception as exc:
+ row.update(valid=False, error=type(exc).__name__, trials=[])
+ write(output / "results.json", rows)
+ report(output, rows, cases, args.repeats)
+ print("ANSWERED", row["case"], label, "OK" if row["valid"] else row["error"], flush=True)
+
+ async with asyncio.TaskGroup() as tasks:
+ for row in rows:
+ tasks.create_task(answer(row))
+ assert args.corpus.read_bytes() == original
+ load_cases(args.corpus.resolve())
+ for file in output.rglob("*"):
+ if file.is_file():
+ data = file.read_bytes()
+ assert all(values[k].encode() not in data for k in ("LLM_API_KEY", "DECISION_MODEL_API_KEY"))
+ write(
+ output / "validation.json",
+ {
+ "source_unchanged": True,
+ "no_api_keys": True,
+ "tasks": len(cases),
+ "contexts": len(rows),
+ "valid": sum(r["valid"] for r in rows),
+ "answers": sum(len(r["trials"]) for r in rows),
+ },
+ )
+ finally:
+ await client.close()
+
+
+def main():
+ """Explicit live opt-in and bounded request concurrency."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--corpus", type=Path, required=True)
+ parser.add_argument("--tokenizer", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--model", default="qwen3.8-max")
+ parser.add_argument("--compressions", type=int, default=3)
+ parser.add_argument("--repeats", type=int, default=2)
+ parser.add_argument("--concurrency", type=int, default=4)
+ parser.add_argument("--live", action="store_true")
+ args = parser.parse_args()
+ if not args.live or min(args.compressions, args.repeats, args.concurrency) < 1:
+ parser.error("Use --live and positive settings")
+ asyncio.run(run(args))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/en/context_compaction.md b/docs/en/context_compaction.md
index d4ad921a..3223cc98 100644
--- a/docs/en/context_compaction.md
+++ b/docs/en/context_compaction.md
@@ -119,20 +119,25 @@ latency and total cost alongside token reduction. ReMe does not automatically su
## Fixed summary + selected original evidence
-The payment experiment freezes one summary **S**, then appends Jev-ranked original blocks without rewriting it.
-Five summary/ranking pairs and three answers per method compare Full context, S and S + Top 1–5 (105 answers).
+The experiment uses **14 fixture queries with predefined answers**, collected through Hermes: four source-length-qualified
+primary tasks and ten short/query-only controls. Rule scoring checks exact JSON fields; no LLM judge creates or grades gold.
-
+
-S + Top 4 reaches **100% on ten exact facts**, matching Full context with **25.2% fewer downstream input tokens**
-(3,529 versus 4,715). S alone reaches 73.3%; Top 2 reaches 86.0% and Top 3 reaches 94.0%. Top 5 adds tokens without
-improving the score. All methods pass the six executable repair tests.
+Across the four primary tasks, Full context reaches 100%, S reaches 82.0%, and S + Top 3 reaches **99.5% with 19.3% fewer
+input tokens** (3,241 versus 4,014). Top 4 and Top 5 score 98.1% and 97.4%, so more retained text is not automatically better.
+Each generation freezes S and the Jev ranking before answers; all five fusion methods keep byte-identical S.
-
-There are **10 evidence units, so Top 10 is the maximum**; this sweep tests Top 1–5. S uses a 3,200-token generation
-allowance; the extended answering sweep has a 3,800-token total-input ceiling. S remains byte-identical in every fusion;
-adding originals intentionally increases input length. Actual compressed inputs stay within the ceiling.
+The **Hermes small-window adapter** scores 79.4% at 2,432 input tokens. It calls the real native summary generator with
+explicit small-window budget/word constraints and a body-only carrier; this is not the unchanged automatic compressor.
+All 42 native contexts fit their budgets and use strictly fewer actual input tokens than Full, with no original fallback.
-This is one synthetic task collected by Hermes, evaluated by calling Qwen directly. Repeated measurements do not
-establish general task performance or superiority over pure Jev. The hybrid is benchmark-only; the production contract
-above is unchanged. See the [protocol, scoring and complete results](https://github.com/jinliyl/ReMe/blob/26a83c1904d5c8312ed8e8cb5ffa21500b6e50e6/benchmark/compaction/RESULTS.md).
+
+Top N counts complete source units and saturates at each task's available count. The original payment has ten units,
+so its maximum is Top 10; the sweep tests Top 1–5. Summary/body allowances and complete-input ceilings are task-specific;
+this is an incremental-evidence curve, not an equal-length comparison or proof that every Full exceeds its ceiling.
+
+The panel has 336 contexts and 672 answers, with the controls reported separately. Hermes collects source trajectories;
+answering calls Qwen directly without tool continuation or recovery. These few synthetic tasks do not establish general
+superiority or statistical equivalence. The hybrid and native adapter remain benchmark-only.
+See the [exact queries, answers and settings](https://github.com/jinliyl/ReMe/blob/feat/decision-model-compaction/benchmark/compaction/STUDY.md) and [results, controls and audit](https://github.com/jinliyl/ReMe/blob/feat/decision-model-compaction/benchmark/compaction/RESULTS.md).
diff --git a/docs/figure/compaction-topn-results.svg b/docs/figure/compaction-topn-results.svg
index 24d8ab6a..9a93cf1c 100644
--- a/docs/figure/compaction-topn-results.svg
+++ b/docs/figure/compaction-topn-results.svg
@@ -1,7 +1,7 @@
-