P1-02: AttentionCapsule as the real model product (closes #25) #200
No files matched your search
@@ -90,6 +90,7 @@ jobs:
|
||||
src/rune/canary_p0_04.py \
|
||||
src/rune/canary_p0_05.py \
|
||||
src/rune/canary_p0_07.py \
|
||||
src/rune/canary_p1_02.py \
|
||||
src/rune/canary_p1_07.py \
|
||||
src/rune/canary_p1_06.py \
|
||||
src/rune/canary_p1_01.py \
|
||||
|
||||
@@ -103,10 +103,10 @@
|
||||
"evidence": "REMOVED by P0-04 (#20): _run_warm_router is a documented no-op; the ContextBroker (#19) owns all model-facing context. See docs/pipeline-removal.md."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/attention_capsule.py:96-163",
|
||||
"role": "Bounded working-set candidate",
|
||||
"disposition": "dormant-until-dependency",
|
||||
"evidence": "Creates a bounded capsule but does not own prompt construction."
|
||||
"path": "src/rune/attention_capsule.py",
|
||||
"role": "AttentionCapsule — the typed ledger of admitted blocks the model receives (to_messages re-derives the runtime payload)",
|
||||
"disposition": "retain-authority",
|
||||
"evidence": "P1-02 (#25): the capsule IS the product the model receives — the broker renders admitted blocks into it, the safety override mutates its user block in place (re-measured), runtime.run consumes capsule.to_messages(), and the trace v4 capsule_messages/capsule_content_hashes prove the logged capsule EXACTLY equals the sent payload. Legacy create_capsule/to_prompt (string-trim fake budgeting) deprecated with no live callers."
|
||||
},
|
||||
{
|
||||
"path": "src/rune/inference_trace.py",
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Capsule Fidelity (P1-02, #25)
|
||||
|
||||
The AttentionCapsule is the **product the model receives** — not a
|
||||
side-report. This document states the invariant, the mechanism, and how to
|
||||
verify it.
|
||||
|
||||
## The equality invariant
|
||||
|
||||
> The logged capsule contents EXACTLY equal the bounded dynamic context
|
||||
> supplied to the model.
|
||||
|
||||
Concretely, three renderings of one turn must be identical:
|
||||
|
||||
1. `capsule.to_messages()` — re-derived from the admitted-block ledger,
|
||||
2. the message list passed to `LocalToolRuntime.run(...)` — captured
|
||||
pre-run via `capsule.capture_messages()` → `capsule.sent_messages`,
|
||||
3. `InferenceTrace.capsule_messages` — the durable JSONL log.
|
||||
|
||||
Token counts alone cannot prove this; the message list and per-block
|
||||
content hashes can.
|
||||
|
||||
## Mechanism
|
||||
|
||||
- **Assembly:** `ContextBroker.assemble()` collects typed
|
||||
`ContributionBlock`s, measures every block (`len//4`, deterministic and
|
||||
conservative), admits against the input budget, and renders the admitted
|
||||
blocks into the capsule (`capsule.blocks`).
|
||||
- **Payload derivation:** `AttentionCapsule.to_messages()` re-derives the
|
||||
chat message list from `capsule.blocks` on every call — in-place block
|
||||
mutation always flows through; there is no cached copy to diverge.
|
||||
- **Capture:** `RuneOperator._execute_on_model` calls
|
||||
`capsule.capture_messages(capsule.to_messages())` immediately before
|
||||
`runtime.run`. The capture is a deep copy, so later mutation cannot
|
||||
alter what was logged.
|
||||
- **Integrity hashes:** the trace stores `{block_name: sha256(content)}`
|
||||
(string content hashed as UTF-8; list/history content as canonical
|
||||
JSON) — full block text is NOT logged raw (JSONL size discipline).
|
||||
- **Safety-override re-measure:** the override decorates the user block in
|
||||
place per escalation tier; the operator re-runs the estimator on the
|
||||
mutated block, updates `block.tokens`, the broker trace's per-block
|
||||
entry, and `capsule.token_count`. No re-admission (the override grows
|
||||
≤ ~100 tokens and admission left headroom) — but measurement is always
|
||||
truthful.
|
||||
- **num_ctx propagation:** the runtime receives
|
||||
`num_ctx = capsule.token_count + completion_reserve` where
|
||||
`completion_reserve = broker window − broker budget`
|
||||
(`context_broker.default_budget` reserves the completion room). The
|
||||
runtime therefore respects the broker's admission decision instead of
|
||||
the raw window. #28 owns the broader model-aware budget work.
|
||||
|
||||
## Verification
|
||||
|
||||
- Frozen tests: `tests/test_p1_02_capsule_authority.py` — asserts
|
||||
`trace.capsule_messages == runtime-received messages` (identity),
|
||||
hash correctness, re-measure truthfulness, and num_ctx propagation.
|
||||
- Canary: `python3 src/rune/canary_p1_02.py` prints
|
||||
`P1-02 CAPSULE FIDELITY CANARY PASSED` (registered in
|
||||
`.gitea/workflows/ci.yml`'s canary loop).
|
||||
|
||||
## Retired legacy path
|
||||
|
||||
`attention_capsule.create_capsule` and `AttentionCapsule.to_prompt` are
|
||||
**deprecated** (`DeprecationWarning`): their string-trim "budgeting" was
|
||||
never real admission. The ContextBroker remains the sole prompt-assembly
|
||||
authority; the CLI entry point keeps them only for standalone debugging.
|
||||
`ContributionBlock.to_dict()` now includes `content`, so pending-state
|
||||
(approval-resume) capsules are verifiable end-to-end.
|
||||
+13
-2
@@ -17,11 +17,11 @@ is gitignored, P0-05 M6) — never committed. Consumers: #25, #28, #29.
|
||||
Read back with `rune.inference_trace.read_traces()` or
|
||||
`python3 -m rune.inference_trace [n]` (tail n records).
|
||||
|
||||
## Record schema (version 1)
|
||||
## Record schema (version 4)
|
||||
|
||||
| Field | Source | Meaning |
|
||||
|---|---|---|
|
||||
| `schema_version` | constant (`1`) | Record shape version for consumers |
|
||||
| `schema_version` | constant (`4`) | Record shape version for consumers |
|
||||
| `provider` | `OllamaChatTransport.provider` | `"ollama"` (class constant) |
|
||||
| `model` | operator | Model id the run executed on |
|
||||
| `task_id` / `channel` | operator (TaskState #18) | Joins the trace to TaskState |
|
||||
@@ -43,6 +43,17 @@ Read back with `rune.inference_trace.read_traces()` or
|
||||
| `cost_usd` | `price_for(model)` | `None` until the price map gains entries |
|
||||
| `cost_basis` | derived | `"local-unpriced"` or `"price-map"` |
|
||||
| `success` / `error` | final `OperatorResponse` | Run outcome; error truncated to 300 chars |
|
||||
| `routing_reason` / `routing_mode` / `task_family` | router (P1-06, #29, v2) | Start-tier decision evidence; empty on pre-router rows |
|
||||
| `selected_tools` / `capability_reason` / `find_capability_calls` | broker selection (P1-01, #24, v3) | Capability-authority evidence |
|
||||
| `capsule_messages` | capsule capture (P1-02, #25, v4) | The EXACT message list `runtime.run` received — equality with the logged capsule is the acceptance invariant |
|
||||
| `capsule_content_hashes` | operator (P1-02, #25, v4) | `{block_name: sha256(content)}` — integrity without full JSONL bloat |
|
||||
| `capsule_token_count` | capsule re-measure (P1-02, #25, v4) | Post-safety-override re-measured total |
|
||||
|
||||
Schema history: v1 (P0-07 #23), v2 additive routing fields (P1-06 #29),
|
||||
v3 additive capability fields (P1-01 #24), v4 additive capsule-fidelity
|
||||
fields (P1-02 #25). All bumps are additive with defaults — older rows
|
||||
without the new keys still parse (`read_traces` consumers must tolerate
|
||||
missing keys).
|
||||
|
||||
## Authority and single-source rule
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# P1-02 AttentionCapsule → Real Model Product — Implementation Plan (#25)
|
||||
|
||||
> **For agentic workers:** execution contract — FROZEN TESTS ARE THE SPEC. Implement verbatim; record every deviation in the plan tail. TDD red → green. Do not create/complete tasks; controller owns task state.
|
||||
|
||||
**Goal:** the logged capsule contents EXACTLY equal the bounded dynamic context supplied to the model; no token_count-only fake budgeting; the capsule is provably the product the model receives.
|
||||
|
||||
**Architecture:** the broker already produces the capsule and derives messages from it (P0-03 did most of the wiring). This plan closes the *evidence and integrity* gaps: (1) a content-complete capsule log on the InferenceTrace (additive v4, hashes + message list, not full block text — size discipline); (2) a capsule-fidelity canary that asserts `capsule.to_messages()` == the messages passed to `runtime.run` == the logged capsule messages; (3) safety-override decoration re-measured into the capsule; (4) legacy `create_capsule`/`to_prompt` retired; (5) `ContributionBlock.to_dict` includes content so pending-state/log capsules are verifiable; (6) authority map flipped to retain-authority.
|
||||
|
||||
**Tech stack:** stdlib + existing house modules. No new services, no parallel assembly.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Acceptance criterion (from #25): logged capsule contents EXACTLY equal the bounded dynamic context supplied to model. This is provable ONLY if the log records message content and the canary asserts identity — token counts alone are insufficient.
|
||||
- No token_count-only fake budgeting: `create_capsule`/`to_prompt` (the legacy string-trim "budgeting" path) retired or clearly marked CLI-only.
|
||||
- Trace additive discipline: new fields have defaults; v1/v2/v3 readers unaffected.
|
||||
- Degrade-not-crash: capsule logging must never fail the turn (P0-06).
|
||||
- Full block content is NOT logged raw (JSONL bloat) — log message list (the actual payload) + per-block content SHA-256 for integrity.
|
||||
- num_ctx ownership: #28 owns model-aware context budgets; this issue passes the broker budget (not the raw window) as num_ctx — the one-line fix that makes the runtime respect admission.
|
||||
- Safety-override decorated user block MUST be re-measured so `capsule.token_count` stays truthful post-decoration.
|
||||
- TDD; ruff check + format on touched/added files; canary registered in ci.yml same commit.
|
||||
|
||||
## Design Decisions (settled)
|
||||
|
||||
1. **Capsule log location:** InferenceTrace additive v4 fields — `capsule_messages: list[dict] = []` (the exact messages sent), `capsule_content_hashes: dict[str,str] = {}` (block name → SHA-256 of content), `capsule_token_count: int = 0` (post-decoration re-measured total). No separate JSONL file; rotation already handled.
|
||||
2. **Equality invariant:** `capsule.to_messages()` result is captured BEFORE `runtime.run` and stored on the trace; a frozen test asserts trace.capsule_messages == the list passed to runtime.run (identity, not re-measure). The canary asserts the same against a scripted runtime.
|
||||
3. **Safety-override re-measure:** after override mutates the user block content, re-run `estimate_tokens` on the mutated block, update `block.tokens` + `capsule.token_count` + the trace. No re-admission (override grows ≤ ~100 tokens; admission already left headroom).
|
||||
4. **num_ctx:** pass `capsule.token_count + completion_reserve` where completion_reserve = the broker's reserved completion room (window − budget). One-line change; #28 owns the broader model-aware budget work.
|
||||
5. **Runtime fallback tool prompt:** formally exempt — `_with_fallback_prompt` only fires for non-native models, and its cost is outside broker scope. Add a comment + log-only accounting note. Full integration is a #28/#34 follow-up.
|
||||
6. **Legacy retirement:** `create_capsule` and `to_prompt` marked deprecated (docstring + `warnings.warn(DeprecationWarning)`), zero live callers verified by test. `to_dict` gains `content` field. Authority map: `attention_capsule.py` → retain-authority under ContextBroker.
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 1 — ContributionBlock.to_dict includes content + AttentionCapsule message capture
|
||||
|
||||
**Files:** modify `src/rune/context_broker.py` (to_dict + capsule), `src/rune/attention_capsule.py` (deprecation), test `tests/test_p1_02_capsule_authority.py`.
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ContributionBlock.to_dict()` includes `"content"`; `AttentionCapsule.capture_messages()` → stores the exact list on `capsule.sent_messages` (not a dataclass field — set post-hoc like the existing dynamic attrs); `create_capsule`/`to_prompt` emit DeprecationWarning.
|
||||
|
||||
- [ ] **Step 1: failing tests** — to_dict includes content matching the block's content; to_dict without content key in old fixture still loads (back-compat); capture_messages stores the list; to_messages re-derivation equals captured list for the same capsule; create_capsule raises DeprecationWarning.
|
||||
- [ ] **Step 2: run → red**
|
||||
- [ ] **Step 3: implement**
|
||||
- [ ] **Step 4: green**
|
||||
- [ ] **Step 5: commit** `feat(p1-02): capsule content logging + message capture + legacy deprecation (#25)`
|
||||
|
||||
### Task 2 — Safety-override re-measure + num_ctx budget fix
|
||||
|
||||
**Files:** modify `src/rune/rune_operator.py`, test file.
|
||||
|
||||
- [ ] **Step 1: failing tests** — after safety-override mutates the user block, `capsule.token_count` reflects the new estimate (not stale); trace `per_block_tokens["user"]` matches; `num_ctx` passed to runtime.run equals `capsule.token_count + completion_reserve` (not the raw window).
|
||||
- [ ] **Step 2: red** → **Step 3: implement** → **Step 4: green**
|
||||
- [ ] **Step 5: commit** `feat(p1-02): safety-override re-measure + num_ctx budget propagation (#25)`
|
||||
|
||||
### Task 3 — Trace v4 capsule fields + operator logging
|
||||
|
||||
**Files:** modify `src/rune/inference_trace.py` (v4 fields), `src/rune/rune_operator.py` (log messages + hashes into trace), test file.
|
||||
|
||||
**Interfaces:** Consumes Task 1. Produces: `InferenceTrace.capsule_messages`, `capsule_content_hashes`, `capsule_token_count` (additive, defaults preserve v1–v3 readers).
|
||||
|
||||
- [ ] **Step 1: failing tests** — trace record carries capsule_messages equal to the messages the scripted runtime received; content hashes present and correct; old trace rows without v4 fields still parse.
|
||||
- [ ] **Step 2: red** → **Step 3: implement** → **Step 4: green**; full suite regression (baseline 588+84).
|
||||
- [ ] **Step 5: commit** `feat(p1-02): trace v4 capsule fidelity fields (#25)`
|
||||
|
||||
### Task 4 — Authority map + docs
|
||||
|
||||
**Files:** `config/v1_authority_map.json` (attention_capsule → retain-authority, evidence), `tests/test_v1_authority_map.py` (if pinned), `docs/capsule-fidelity.md` (create: what the capsule is, the equality invariant, how to verify, num_ctx propagation).
|
||||
|
||||
- [ ] Steps: map update → docs → commit `feat(p1-02): capsule authority map + docs (#25)`
|
||||
|
||||
### Task 5 — Canary + CI registration
|
||||
|
||||
**Files:** create `src/rune/canary_p1_02.py`; modify `.gitea/workflows/ci.yml` same commit.
|
||||
|
||||
- [ ] Canary: scripted operator run asserts (1) trace.capsule_messages == runtime-received messages (exact list identity), (2) capsule_token_count truthful post-decoration, (3) num_ctx == budget not window, (4) content hashes match block contents, (5) no DeprecationWarning suppressed. Prints `P1-02 CAPSULE FIDELITY CANARY PASSED`.
|
||||
- [ ] Full suite; ruff; henxels; commit `feat(p1-02): canary + CI registration (#25)`
|
||||
|
||||
## Acceptance (from #25)
|
||||
|
||||
- Logged capsule contents EXACTLY equal the bounded dynamic context supplied to model — proven by trace v4 `capsule_messages` == `runtime.run` messages, asserted in frozen test + canary.
|
||||
- No token_count-only fake budgeting — `create_capsule`/`to_prompt` deprecated, zero live callers verified; admission remains the real budget enforcement.
|
||||
|
||||
## Pipeline requirements checklist
|
||||
- [x] TDD red→green (frozen tests per task)
|
||||
- [x] ruff check + format on touched/added files
|
||||
- [ ] Spec gate PASS; Quality gate Approved (discharges if needed)
|
||||
- [x] Full-suite regression green (baseline 588+84 → 604+84, +16 new)
|
||||
- [x] Canary green + registered same commit; henxels exit 0
|
||||
- [ ] Closed with formatted comment + PR refs
|
||||
|
||||
## Completion Record (P1-02 implementer, 2026-09-06)
|
||||
|
||||
**Status:** all 5 tasks complete. Branch `p1-02-capsule-authority`, head `96e42ae`.
|
||||
|
||||
**Commits:**
|
||||
1. `d0550b6` feat(p1-02): capsule content logging + message capture + legacy deprecation (#25)
|
||||
2. `ec0cf7a` feat(p1-02): safety-override re-measure + num_ctx budget propagation (#25)
|
||||
3. `8ff6a83` feat(p1-02): trace v4 capsule fidelity fields (#25)
|
||||
4. `90bcd59` feat(p1-02): capsule authority map + docs (#25)
|
||||
5. `96e42ae` feat(p1-02): canary + CI registration (#25)
|
||||
|
||||
**Final counts:** 604 passed + 84 subtests (baseline 588+84; +16 new frozen tests, 0 regressions).
|
||||
Canary: `P1-02 CAPSULE FIDELITY CANARY PASSED`. henxels exit 0 (21 pre-existing warnings).
|
||||
|
||||
**Red evidence** (initial run of tests/test_p1_02_capsule_authority.py, 14 failed / 2 passed):
|
||||
`test_contribution_block_to_dict_includes_content`, `test_capture_messages_stores_the_exact_list`,
|
||||
`test_create_capsule_emits_deprecation_warning`, `test_safety_override_reremeasures_capsule_token_count`,
|
||||
`test_num_ctx_is_budget_plus_completion_reserve_not_window`, `test_inference_trace_schema_v4_fields`,
|
||||
`test_operator_trace_record_carries_capsule_messages` (+7 more) — all red before their task's implementation, green after.
|
||||
|
||||
**Deviations from plan:**
|
||||
1. **Schema v4 bump touched pinned literals:** `tests/test_p1_01_capability_authority.py` (3 assertions) and canaries
|
||||
`canary_p1_01.py` / `canary_p1_06.py` pinned `schema_version == 3`. Updated those pins to 4 in the same Task-3 commit —
|
||||
the additive discipline protects v1–v3 ROWS, not the literal; `docs/inference-trace.md` schema table updated to v4 with a schema history note.
|
||||
2. **trace["blocks"] user-entry re-measure:** the frozen test demanded `per_block_tokens["user"]` match post-decoration; the
|
||||
re-measure therefore also updates the admitted user entry in the broker trace dict (not only `block.tokens` + `capsule.token_count`).
|
||||
3. **num_ctx degrade path:** added a `num_ctx <= 0` fallback to the discovered window for stand-in brokers (trace missing
|
||||
window/budget keys) — degrade-not-crash, beyond the literal one-liner, pinned by `test_num_ctx_falls_back_to_window_when_budget_basis_missing`.
|
||||
4. **Trace v4 extraction is Mock-hardened:** pre-existing frozen tests construct the capsule as a `Mock`; the operator's
|
||||
hash/message extraction guards non-list/non-dict content and falls back gracefully (no fidelity evidence on stand-in capsules).
|
||||
5. **`capture_messages` deep-copies** the sent list (test-pinned) so caller-side mutation can never diverge the logged payload.
|
||||
|
||||
**Reviewer flags:**
|
||||
- `InferenceTrace.capsule_messages` logs the FULL message payload per run — accepted per plan (message list, not raw blocks);
|
||||
watch JSONL rotation headroom if history blocks grow (#28 compaction bounds this).
|
||||
- `docs/inference-trace.md` "Record schema (version 4)" supersedes the version-1 table heading; no other docs referenced v3.
|
||||
- Authority map: `attention_capsule.py` entry now `retain-authority` under ContextBroker (context_construction);
|
||||
`test_v1_authority_map.py` green (path existence, disposition vocabulary).
|
||||
@@ -15,6 +15,7 @@ import json
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import warnings
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
@@ -89,8 +90,32 @@ class AttentionCapsule:
|
||||
messages.append({"role": "user", "content": user_content})
|
||||
return messages
|
||||
|
||||
def capture_messages(self, messages: List[dict]) -> List[dict]:
|
||||
"""Record the EXACT message list sent to the runtime (P1-02 #25).
|
||||
|
||||
Stored post-hoc on ``capsule.sent_messages`` (dynamic attr, same
|
||||
pattern as ``messages``/``system_preamble`` from the broker render)
|
||||
so the equality invariant — ``to_messages()`` re-derivation ==
|
||||
what the model received — is machine-checkable from the ledger.
|
||||
The list is deep-copied so later caller-side mutation can never
|
||||
diverge the logged payload from what was sent.
|
||||
"""
|
||||
self.sent_messages = json.loads(json.dumps(list(messages)))
|
||||
return self.sent_messages
|
||||
|
||||
def to_prompt(self) -> str:
|
||||
"""Convert capsule to prompt text."""
|
||||
"""Convert capsule to prompt text.
|
||||
|
||||
.. deprecated:: P1-02 (#25)
|
||||
Legacy renderer behind the retired string-trim "budgeting" path.
|
||||
The ContextBroker owns prompt assembly; use ``to_messages()``.
|
||||
"""
|
||||
warnings.warn(
|
||||
"attention_capsule.AttentionCapsule.to_prompt is deprecated (P1-02 #25): "
|
||||
"the ContextBroker owns prompt assembly — use to_messages().",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
parts = []
|
||||
|
||||
if self.active_objective:
|
||||
@@ -135,9 +160,21 @@ def estimate_tokens(text: str) -> int:
|
||||
|
||||
def create_capsule(task: str, context: str = "", max_tokens: int = 600) -> AttentionCapsule:
|
||||
"""Create an attention capsule for a task.
|
||||
|
||||
|
||||
Only includes what's relevant to the current task.
|
||||
|
||||
.. deprecated:: P1-02 (#25)
|
||||
Legacy CLI helper with fake string-trim "budgeting" — token counts
|
||||
here are never the real admission decision. The ContextBroker
|
||||
(#19) is the sole prompt-assembly authority. Kept only for the
|
||||
standalone CLI above; no live callers.
|
||||
"""
|
||||
warnings.warn(
|
||||
"attention_capsule.create_capsule is deprecated (P1-02 #25): its string-trim "
|
||||
"budgeting is not real admission — use the ContextBroker.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
capsule = AttentionCapsule()
|
||||
|
||||
# Extract objective from task
|
||||
|
||||
@@ -247,8 +247,8 @@ def check_scripted_operator_wiring() -> int:
|
||||
if not records:
|
||||
return _fail("no durable inference trace persisted")
|
||||
record = records[-1]
|
||||
if record["schema_version"] != 3:
|
||||
return _fail(f"trace schema_version must be 3, got {record['schema_version']}")
|
||||
if record["schema_version"] != 4:
|
||||
return _fail(f"trace schema_version must be 4, got {record['schema_version']}")
|
||||
if record["selected_tools"] != expected:
|
||||
return _fail(f"trace selected_tools wrong: {record['selected_tools']}")
|
||||
if not record["capability_reason"]:
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
"""P1-02 capsule-fidelity canary (#25).
|
||||
|
||||
Self-bootstrapping like canary_p1_07.py (CI runs ``python3 <canary>``
|
||||
without PYTHONPATH). Proves the #25 claims are machine-checked with NO
|
||||
live model, NO Ollama, NO network:
|
||||
|
||||
1. trace.capsule_messages == the messages the runtime received (exact
|
||||
list identity) — the acceptance invariant;
|
||||
2. capsule_token_count is truthful post-safety-override decoration;
|
||||
3. num_ctx == capsule.token_count + completion_reserve (the broker
|
||||
budget basis), NOT the raw window;
|
||||
4. content hashes match the block contents;
|
||||
5. no DeprecationWarning is suppressed: the legacy create_capsule /
|
||||
to_prompt paths still warn.
|
||||
|
||||
Isolates all state under RUNE_HOME so the canary never touches a real
|
||||
home directory. Prints 'P1-02 CAPSULE FIDELITY CANARY PASSED' and exits 0.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
# Self-bootstrap: CI runs `python3 <canary>` without PYTHONPATH, so put the
|
||||
# repository src/ on sys.path before importing rune modules. Isolate Rune's
|
||||
# state/journal paths so the canary never touches a real home directory.
|
||||
_repo_src = str(Path(__file__).resolve().parents[1])
|
||||
sys.path.insert(0, _repo_src)
|
||||
_canary_home = tempfile.mkdtemp(prefix="p1-02-canary-home-")
|
||||
os.environ["RUNE_HOME"] = _canary_home
|
||||
os.environ["RUNE_MEMORY_DIR"] = tempfile.mkdtemp(prefix="p1-02-canary-mem-")
|
||||
|
||||
from rune import rune_operator # noqa: E402
|
||||
from rune.attention_capsule import AttentionCapsule # noqa: E402
|
||||
from rune.context_broker import ContributionBlock # noqa: E402
|
||||
from rune.inference_trace import read_traces # noqa: E402
|
||||
from rune.local_tool_runtime import OllamaChatTransport # noqa: E402
|
||||
from rune.tool_protocol import OperatorResponse # noqa: E402
|
||||
|
||||
WINDOW = 40960
|
||||
BUDGET = 32768
|
||||
|
||||
|
||||
def _fail(message: str) -> int:
|
||||
print(f"FAILED: {message}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
class _ScriptedTransport(OllamaChatTransport):
|
||||
"""OllamaChatTransport with _post scripted — real accumulation code runs."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def _post(self, path, payload):
|
||||
return {
|
||||
"model": "qwen3:8b",
|
||||
"created_at": "2026-09-05T00:00:00Z",
|
||||
"message": {"role": "assistant", "content": "ok"},
|
||||
"done": True,
|
||||
"prompt_eval_count": 100,
|
||||
"eval_count": 5,
|
||||
"total_duration": 1_000_000_000,
|
||||
}
|
||||
|
||||
|
||||
class _ScriptedRuntime:
|
||||
"""Minimal runtime stand-in: one chat, then the canned final response."""
|
||||
|
||||
def __init__(self, transport):
|
||||
self.transport = transport
|
||||
self.tool_names = ("read_file", "write_file")
|
||||
self.received_messages: list = []
|
||||
self.received_options: dict = {}
|
||||
|
||||
def run(self, messages, **kwargs):
|
||||
self.received_messages = [dict(m) for m in messages]
|
||||
self.received_options = dict(kwargs.get("options") or {})
|
||||
self.transport.chat(model="qwen3:8b", messages=list(messages), tools=None)
|
||||
return OperatorResponse(success=True, user_text="All done.", model="qwen3:8b")
|
||||
|
||||
|
||||
def _scripted_operator_run(description: str, tier_params: dict):
|
||||
"""Real RuneOperator + scripted broker/transport → (envelope, runtime, capsule)."""
|
||||
storage = Path(_canary_home) / "operator.json"
|
||||
op = rune_operator.RuneOperator(str(storage))
|
||||
capsule = AttentionCapsule()
|
||||
capsule.blocks = [
|
||||
ContributionBlock("persona", 90, "I am Rune.", len("I am Rune.") // 4, "persona", evictable=False),
|
||||
ContributionBlock("user", 100, description, len(description) // 4, "rune_operator task text", evictable=False),
|
||||
]
|
||||
capsule.token_count = sum(b.tokens for b in capsule.blocks)
|
||||
trace = {
|
||||
"model_id": "qwen3:8b",
|
||||
"window": WINDOW,
|
||||
"budget": BUDGET,
|
||||
"blocks": [
|
||||
{"name": "persona", "tokens": capsule.blocks[0].tokens, "admitted": True, "reason": ""},
|
||||
{"name": "user", "tokens": capsule.blocks[1].tokens, "admitted": True, "reason": ""},
|
||||
],
|
||||
"admitted": ["persona", "user"],
|
||||
"admitted_total": capsule.token_count,
|
||||
"dropped": [],
|
||||
"memory_tokens": 0,
|
||||
"degraded": [],
|
||||
"compaction": [],
|
||||
"elapsed_ms": 1.0,
|
||||
}
|
||||
broker = SimpleNamespace(assemble=lambda task, session, model_id: (capsule, trace))
|
||||
runtime = _ScriptedRuntime(_ScriptedTransport())
|
||||
profile = SimpleNamespace(safety_tier=0)
|
||||
patches = [
|
||||
patch.object(op, "_context_broker", return_value=broker),
|
||||
patch.object(rune_operator.model_profiles, "get_or_create_profile", return_value=profile),
|
||||
patch.object(rune_operator.model_profiles, "record_interaction"),
|
||||
patch.object(rune_operator, "LocalToolRuntime", return_value=runtime),
|
||||
patch.object(rune_operator, "OllamaChatTransport", return_value=runtime.transport),
|
||||
patch("rune.local_tool_runtime.context_length", lambda model: WINDOW),
|
||||
patch.object(rune_operator.safety_override.SafetyOverride, "get_params", lambda self, text: dict(tier_params)),
|
||||
patch.object(rune_operator.safety_override.SafetyOverride, "is_refusal", lambda self, text: False),
|
||||
patch.object(op, "_log_token_usage"),
|
||||
]
|
||||
with contextlib.ExitStack() as stack:
|
||||
for item in patches:
|
||||
stack.enter_context(item)
|
||||
stack.enter_context(patch("rune.event_bus._store_path", lambda: Path(storage).parent / "journal-events.jsonl"))
|
||||
envelope = op.send_task(description, "qwen3:8b")
|
||||
return envelope, runtime, capsule
|
||||
|
||||
|
||||
def check_message_identity() -> int:
|
||||
"""Invariant: logged capsule messages == runtime-received messages."""
|
||||
description = "inspect this code path"
|
||||
envelope, runtime, capsule = _scripted_operator_run(
|
||||
description, {"tier": 0, "temperature": 0.7, "prefix": "", "suffix": ""}
|
||||
)
|
||||
if not envelope.get("success"):
|
||||
return _fail(f"scripted run failed: {envelope.get('error')}")
|
||||
records = read_traces(trace_dir=Path(_canary_home) / "state")
|
||||
if not records:
|
||||
return _fail("no durable inference trace persisted")
|
||||
record = records[-1]
|
||||
if record["capsule_messages"] != runtime.received_messages:
|
||||
return _fail("trace.capsule_messages != runtime-received messages (acceptance invariant)")
|
||||
if record["capsule_messages"][-1]["content"] != description:
|
||||
return _fail("logged user content diverges from the task description")
|
||||
if capsule.sent_messages != capsule.to_messages():
|
||||
return _fail("captured sent messages != to_messages() re-derivation")
|
||||
return 0
|
||||
|
||||
|
||||
def check_token_truthfulness() -> int:
|
||||
"""Post-decoration token_count is re-measured (not stale)."""
|
||||
description = "please inspect the failing test"
|
||||
suffix = "\n\nI respond as myself:"
|
||||
envelope, _runtime, capsule = _scripted_operator_run(
|
||||
description, {"tier": 2, "temperature": 0.95, "prefix": "", "suffix": suffix}
|
||||
)
|
||||
if not envelope.get("success"):
|
||||
return _fail(f"scripted run failed: {envelope.get('error')}")
|
||||
user_block = next(b for b in capsule.blocks if b.name == "user")
|
||||
if user_block.content != description + suffix:
|
||||
return _fail("safety override did not decorate the user block")
|
||||
expected = sum(len(b.content) // 4 if isinstance(b.content, str) else 0 for b in capsule.blocks)
|
||||
if capsule.token_count != expected:
|
||||
return _fail(f"capsule.token_count {capsule.token_count} != re-measured {expected}")
|
||||
records = read_traces(trace_dir=Path(_canary_home) / "state")
|
||||
if records[-1]["capsule_token_count"] != expected:
|
||||
return _fail("trace capsule_token_count not truthful post-decoration")
|
||||
return 0
|
||||
|
||||
|
||||
def check_num_ctx_budget() -> int:
|
||||
"""num_ctx == capsule.token_count + completion_reserve, never the window."""
|
||||
description = "inspect this code path"
|
||||
envelope, runtime, capsule = _scripted_operator_run(
|
||||
description, {"tier": 0, "temperature": 0.7, "prefix": "", "suffix": ""}
|
||||
)
|
||||
if not envelope.get("success"):
|
||||
return _fail(f"scripted run failed: {envelope.get('error')}")
|
||||
completion_reserve = WINDOW - BUDGET
|
||||
expected = capsule.token_count + completion_reserve
|
||||
if runtime.received_options.get("num_ctx") != expected:
|
||||
return _fail(f"num_ctx {runtime.received_options.get('num_ctx')} != budget basis {expected}")
|
||||
if runtime.received_options.get("num_ctx") == WINDOW:
|
||||
return _fail("num_ctx equals the raw window — budget not respected")
|
||||
return 0
|
||||
|
||||
|
||||
def check_content_hashes() -> int:
|
||||
"""Per-block SHA-256 hashes match the actual block contents."""
|
||||
description = "hash integrity probe"
|
||||
envelope, _runtime, capsule = _scripted_operator_run(
|
||||
description, {"tier": 0, "temperature": 0.7, "prefix": "", "suffix": ""}
|
||||
)
|
||||
if not envelope.get("success"):
|
||||
return _fail(f"scripted run failed: {envelope.get('error')}")
|
||||
records = read_traces(trace_dir=Path(_canary_home) / "state")
|
||||
hashes = records[-1]["capsule_content_hashes"]
|
||||
if set(hashes) != {b.name for b in capsule.blocks}:
|
||||
return _fail(f"hash keys {sorted(hashes)} != block names {sorted(b.name for b in capsule.blocks)}")
|
||||
for block in capsule.blocks:
|
||||
payload = (
|
||||
block.content.encode("utf-8")
|
||||
if isinstance(block.content, str)
|
||||
else json.dumps(block.content, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
||||
)
|
||||
if hashes[block.name] != hashlib.sha256(payload).hexdigest():
|
||||
return _fail(f"content hash mismatch for block '{block.name}'")
|
||||
return 0
|
||||
|
||||
|
||||
def check_deprecation_not_suppressed() -> int:
|
||||
"""Legacy paths still warn — no silent suppression of the retirement."""
|
||||
from rune.attention_capsule import create_capsule
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
create_capsule("fix the failing test", "context line here")
|
||||
AttentionCapsule(active_objective="obj").to_prompt()
|
||||
deprecations = [w for w in caught if issubclass(w.category, DeprecationWarning)]
|
||||
if len(deprecations) < 2:
|
||||
return _fail(f"expected 2 DeprecationWarnings from legacy paths, got {len(deprecations)}")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
checks = (
|
||||
("trace.capsule_messages == runtime-received messages (identity)", check_message_identity),
|
||||
("capsule_token_count truthful post-decoration", check_token_truthfulness),
|
||||
("num_ctx == budget basis, not the raw window", check_num_ctx_budget),
|
||||
("content hashes match block contents", check_content_hashes),
|
||||
("legacy deprecation warnings not suppressed", check_deprecation_not_suppressed),
|
||||
)
|
||||
for label, check in checks:
|
||||
result = check()
|
||||
if result != 0:
|
||||
return result
|
||||
print(f" ✓ {label}")
|
||||
print("P1-02 CAPSULE FIDELITY CANARY PASSED")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -198,8 +198,8 @@ def check_reason_recording() -> int:
|
||||
if not records:
|
||||
return _fail("no durable inference trace persisted")
|
||||
record = records[-1]
|
||||
if record["schema_version"] != 3:
|
||||
return _fail(f"schema_version must be 3, got {record['schema_version']}")
|
||||
if record["schema_version"] != 4:
|
||||
return _fail(f"schema_version must be 4, got {record['schema_version']}")
|
||||
if record["routing_reason"] != "router: measured 12/13 success task_family=code":
|
||||
return _fail(f"routing_reason not recorded: {record['routing_reason']!r}")
|
||||
if record["routing_mode"] != "local":
|
||||
|
||||
@@ -79,10 +79,18 @@ class ContributionBlock:
|
||||
self.evictable = evictable # False = never dropped (system, user)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Serialize for pending_state embedding (approval resume)."""
|
||||
"""Serialize for pending_state embedding (approval resume).
|
||||
|
||||
P1-02 (#25): ``content`` is included so the embedded capsule is
|
||||
verifiable end-to-end — the acceptance criterion is that the logged
|
||||
capsule contents EXACTLY equal the model payload, which token counts
|
||||
alone cannot prove. Blocks are small (admission-bounded), so the
|
||||
extra size is bounded by the same budget.
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"priority": self.priority,
|
||||
"content": self.content,
|
||||
"tokens": self.tokens,
|
||||
"source": self.source,
|
||||
"evictable": self.evictable,
|
||||
|
||||
@@ -25,7 +25,7 @@ try:
|
||||
except ImportError: # direct script execution
|
||||
from rune.memory_env import rune_home
|
||||
|
||||
INFERENCE_TRACE_SCHEMA_VERSION = 3
|
||||
INFERENCE_TRACE_SCHEMA_VERSION = 4
|
||||
TRACE_ROTATE_BYTES = 10_000_000
|
||||
TRACE_BACKUPS = 3
|
||||
|
||||
@@ -87,6 +87,13 @@ class InferenceTrace:
|
||||
selected_tools: list = field(default_factory=list) # ordered exposed set
|
||||
capability_reason: str = "" # broker selection reason ("" = unselected)
|
||||
find_capability_calls: int = 0 # escape-hatch round-trips this run
|
||||
# P1-02 (#25) schema v4 — capsule-fidelity fields, additive. The logged
|
||||
# capsule contents must EXACTLY equal the bounded dynamic context
|
||||
# supplied to the model; message list (the actual payload) + per-block
|
||||
# content SHA-256 (integrity without full JSONL bloat) prove it.
|
||||
capsule_messages: list = field(default_factory=list) # exact runtime.run payload
|
||||
capsule_content_hashes: dict = field(default_factory=dict) # block name -> sha256
|
||||
capsule_token_count: int = 0 # post-decoration re-measured total
|
||||
|
||||
|
||||
def price_for(model: str) -> float | None:
|
||||
|
||||
@@ -13,6 +13,7 @@ Usage:
|
||||
rune_operator.py list # List available models
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -1654,10 +1655,27 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
|
||||
|
||||
# Safety override decorates the final user BLOCK in place
|
||||
# (P0-03 constraint d); rendering flows from the blocks.
|
||||
# P1-02 (#25): after mutation the block is RE-MEASURED so
|
||||
# capsule.token_count (and the broker trace's per-block
|
||||
# entry) stay truthful post-decoration — the override grows
|
||||
# the payload ≤ ~100 tokens and admission left headroom, so
|
||||
# no re-admission is needed; measurement is not budgeting.
|
||||
for block in capsule.blocks:
|
||||
if block.name == "user":
|
||||
block.content = override_prefix + task.description + override_suffix
|
||||
block.tokens = len(block.content) // 4
|
||||
for trace_block in trace["blocks"]:
|
||||
if trace_block.get("name") == "user" and trace_block.get("admitted"):
|
||||
trace_block["tokens"] = block.tokens
|
||||
capsule.token_count = sum(
|
||||
block.tokens for block in capsule.blocks if hasattr(block, "tokens")
|
||||
)
|
||||
messages = capsule.to_messages()
|
||||
# P1-02 (#25) acceptance: the log must capture the EXACT
|
||||
# payload the runtime receives — capture BEFORE run, assert
|
||||
# identity in the frozen test + canary.
|
||||
if hasattr(capsule, "capture_messages"):
|
||||
capsule.capture_messages(messages)
|
||||
|
||||
runtime = LocalToolRuntime(
|
||||
transport=OllamaChatTransport(timeout=timeout),
|
||||
@@ -1666,18 +1684,27 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
|
||||
tools=capability_selection["tools"],
|
||||
)
|
||||
transport = runtime.transport
|
||||
# P0-03: the runtime window must match the broker's budget
|
||||
# basis — a hardcoded 8192 would silently truncate blocks the
|
||||
# broker admitted against a larger discovered window. The
|
||||
# other 13 num_ctx sites remain pinned (follow-up issue).
|
||||
from rune.local_tool_runtime import context_length as _ctx_len
|
||||
# P1-02 (#25) one-line num_ctx fix: pass the broker budget
|
||||
# basis — capsule.token_count + completion_reserve (window −
|
||||
# budget) — NOT the raw window, so the runtime respects the
|
||||
# broker's admission decision (#28 owns model-aware budgets).
|
||||
completion_reserve = 0
|
||||
if trace.get("window") is not None and trace.get("budget") is not None:
|
||||
completion_reserve = max(0, int(trace["window"]) - int(trace["budget"]))
|
||||
num_ctx = int(capsule.token_count) + completion_reserve
|
||||
if num_ctx <= 0:
|
||||
# Degrade-not-crash (stand-in broker/trace): keep the
|
||||
# discovered window rather than sending num_ctx=0.
|
||||
from rune.local_tool_runtime import context_length as _ctx_len
|
||||
|
||||
num_ctx = _ctx_len(model)
|
||||
|
||||
response = runtime.run(
|
||||
messages,
|
||||
options={
|
||||
"temperature": temperature,
|
||||
"num_predict": 4096,
|
||||
"num_ctx": _ctx_len(model),
|
||||
"num_ctx": num_ctx,
|
||||
"stop": ["\n\nUser:", "\n\n---"],
|
||||
},
|
||||
think=True,
|
||||
@@ -1790,6 +1817,35 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
|
||||
record_trace,
|
||||
)
|
||||
|
||||
# P1-02 (#25) schema v4: capsule fidelity evidence. The
|
||||
# captured sent messages (post-decoration, pre-run) plus
|
||||
# per-block SHA-256 integrity hashes and the re-measured
|
||||
# token total. Degrade-not-crash (P0-06): a stand-in
|
||||
# capsule (Mock/legacy) must never fail the trace —
|
||||
# everything is guarded; the record just omits evidence
|
||||
# that cannot be derived.
|
||||
capsule_hashes: dict = {}
|
||||
for _b in list(getattr(capsule, "blocks", None) or []):
|
||||
content = getattr(_b, "content", None)
|
||||
name = getattr(_b, "name", "")
|
||||
if not isinstance(name, str) or not name or content is None:
|
||||
continue
|
||||
if isinstance(content, str):
|
||||
payload = content.encode("utf-8")
|
||||
elif isinstance(content, (list, dict)):
|
||||
payload = json.dumps(content, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
||||
else:
|
||||
continue
|
||||
capsule_hashes[name] = hashlib.sha256(payload).hexdigest()
|
||||
_sent = getattr(capsule, "sent_messages", None)
|
||||
if not isinstance(_sent, list):
|
||||
try:
|
||||
_sent = capsule.to_messages()
|
||||
except Exception:
|
||||
_sent = []
|
||||
if not isinstance(_sent, list):
|
||||
_sent = []
|
||||
|
||||
trace_record = InferenceTrace(
|
||||
schema_version=INFERENCE_TRACE_SCHEMA_VERSION,
|
||||
provider=getattr(transport, "provider", "unknown"),
|
||||
@@ -1831,6 +1887,19 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
|
||||
find_capability_calls=sum(
|
||||
1 for event in response.tool_events if event.request.name == "find_capability"
|
||||
),
|
||||
# P1-02 (#25) schema v4: capsule fidelity fields —
|
||||
# the logged messages EXACTLY equal the runtime.run
|
||||
# payload (captured pre-run on the mutated capsule).
|
||||
capsule_messages=[
|
||||
dict(m) for m in _sent if isinstance(m, dict)
|
||||
],
|
||||
capsule_content_hashes=capsule_hashes,
|
||||
capsule_token_count=(
|
||||
int(capsule.token_count)
|
||||
if isinstance(getattr(capsule, "token_count", 0), int)
|
||||
and not isinstance(getattr(capsule, "token_count", 0), bool)
|
||||
else 0
|
||||
),
|
||||
)
|
||||
record_trace(trace_record)
|
||||
# P1-06 (quality I-2 discharge): CLOSE the measured loop —
|
||||
|
||||
@@ -326,7 +326,9 @@ def test_default_runtime_still_exposes_full_tools():
|
||||
def test_inference_trace_schema_v3_fields():
|
||||
from rune import inference_trace as it
|
||||
|
||||
assert it.INFERENCE_TRACE_SCHEMA_VERSION == 3
|
||||
# P1-02 (#25) bumped the schema to v4 (additive); assert against the
|
||||
# module constant, not a pinned literal.
|
||||
assert it.INFERENCE_TRACE_SCHEMA_VERSION == 4
|
||||
trace = it.InferenceTrace(
|
||||
schema_version=it.INFERENCE_TRACE_SCHEMA_VERSION,
|
||||
provider="ollama",
|
||||
@@ -420,7 +422,7 @@ def test_operator_trace_record_carries_capability_fields(tmp_path, monkeypatch):
|
||||
records = it.read_traces(trace_dir=tmp_path / "home" / "state")
|
||||
assert records, "durable trace missing"
|
||||
record = records[-1]
|
||||
assert record["schema_version"] == 3
|
||||
assert record["schema_version"] == 4
|
||||
assert record["selected_tools"] == cb.select_for_job("fix this code path")["names"]
|
||||
assert "capability_reason" in record and record["capability_reason"]
|
||||
assert record["find_capability_calls"] == 0
|
||||
@@ -496,7 +498,7 @@ def test_operator_trace_record_carries_selected_tools(tmp_path, monkeypatch):
|
||||
records = it.read_traces(trace_dir=tmp_path / "home2" / "state")
|
||||
assert records
|
||||
record = records[-1]
|
||||
assert record["schema_version"] == 3
|
||||
assert record["schema_version"] == 4
|
||||
assert record["selected_tools"] == tool_names
|
||||
assert record["capability_reason"]
|
||||
assert record["find_capability_calls"] == 0
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
"""P1-02 (#25): AttentionCapsule authority — FROZEN TESTS ARE THE SPEC.
|
||||
|
||||
Layers covered per plan task:
|
||||
1. ContributionBlock.to_dict content completeness + capsule message capture
|
||||
+ legacy deprecation (Task 1),
|
||||
2. safety-override re-measure + num_ctx budget propagation (Task 2),
|
||||
3. trace v4 capsule fidelity fields + operator logging (Task 3).
|
||||
|
||||
The canary (src/rune/canary_p1_02.py) re-proves the contract CI-side.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import rune_operator # noqa: E402
|
||||
from rune.attention_capsule import AttentionCapsule # noqa: E402
|
||||
from rune.context_broker import ContributionBlock, default_budget # noqa: E402
|
||||
from rune.local_tool_runtime import OllamaChatTransport # noqa: E402
|
||||
from rune.tool_protocol import OperatorResponse # noqa: E402
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _ScriptedTransport(OllamaChatTransport):
|
||||
"""OllamaChatTransport with _post scripted — real accumulation code runs."""
|
||||
|
||||
def __init__(self, responses):
|
||||
super().__init__()
|
||||
self._responses = list(responses)
|
||||
|
||||
def _post(self, path, payload):
|
||||
if not self._responses:
|
||||
raise AssertionError("scripted transport exhausted")
|
||||
return self._responses.pop(0)
|
||||
|
||||
|
||||
def _chat_response(prompt_eval_count, eval_count, total_duration_ns):
|
||||
return {
|
||||
"model": "qwen3:8b",
|
||||
"created_at": "2026-09-05T00:00:00Z",
|
||||
"message": {"role": "assistant", "content": "ok"},
|
||||
"done": True,
|
||||
"prompt_eval_count": prompt_eval_count,
|
||||
"eval_count": eval_count,
|
||||
"total_duration": total_duration_ns,
|
||||
}
|
||||
|
||||
|
||||
class _ScriptedRuntime:
|
||||
"""Minimal runtime stand-in: one chat, then the canned final response."""
|
||||
|
||||
def __init__(self, transport, response):
|
||||
self.transport = transport
|
||||
self.tool_names = ("read_file", "write_file")
|
||||
self._response = response
|
||||
self.received_messages: list = []
|
||||
self.received_options: dict = {}
|
||||
|
||||
def run(self, messages, **kwargs):
|
||||
self.received_messages = [dict(m) for m in messages]
|
||||
self.received_options = dict(kwargs.get("options") or {})
|
||||
self.transport.chat(model="qwen3:8b", messages=list(messages), tools=None)
|
||||
return self._response
|
||||
|
||||
|
||||
def _broker_trace(**overrides):
|
||||
trace = {
|
||||
"model_id": "qwen3:8b",
|
||||
"window": 40960,
|
||||
"budget": 32768,
|
||||
"blocks": [
|
||||
{"name": "persona", "tokens": 980, "admitted": True, "reason": ""},
|
||||
{"name": "user", "tokens": 20, "admitted": True, "reason": ""},
|
||||
],
|
||||
"admitted": ["persona", "user"],
|
||||
"admitted_total": 1000,
|
||||
"dropped": [],
|
||||
"memory_tokens": 0,
|
||||
"degraded": [],
|
||||
"compaction": [],
|
||||
"elapsed_ms": 1.0,
|
||||
}
|
||||
trace.update(overrides)
|
||||
return trace
|
||||
|
||||
|
||||
def _wire_operator(tmp_path, monkeypatch, runtime, capsule, trace):
|
||||
"""Real RuneOperator; broker/collaborators scripted; RUNE_HOME sandboxed."""
|
||||
monkeypatch.setenv("RUNE_HOME", str(tmp_path))
|
||||
op = rune_operator.RuneOperator(str(tmp_path / "operator.json"))
|
||||
broker = SimpleNamespace(assemble=lambda task, session, model_id: (capsule, trace))
|
||||
profile = SimpleNamespace(safety_tier=0)
|
||||
patches = [
|
||||
patch.object(op, "_context_broker", return_value=broker),
|
||||
patch.object(rune_operator.model_profiles, "get_or_create_profile", return_value=profile),
|
||||
patch.object(rune_operator.model_profiles, "record_interaction"),
|
||||
patch.object(rune_operator, "LocalToolRuntime", return_value=runtime),
|
||||
patch.object(rune_operator, "OllamaChatTransport", return_value=runtime.transport),
|
||||
patch("rune.local_tool_runtime.context_length", lambda model: 40960),
|
||||
patch.object(
|
||||
rune_operator.safety_override.SafetyOverride,
|
||||
"get_params",
|
||||
lambda self, text: {"tier": 0, "temperature": 0.7, "prefix": "", "suffix": ""},
|
||||
),
|
||||
patch.object(rune_operator.safety_override.SafetyOverride, "is_refusal", lambda self, text: False),
|
||||
patch.object(op, "_log_token_usage"),
|
||||
patch("rune.event_bus._store_path", lambda: tmp_path / "journal-events.jsonl"),
|
||||
]
|
||||
return op, patches
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task 1 — ContributionBlock.to_dict content + capsule message capture
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_contribution_block_to_dict_includes_content():
|
||||
"""to_dict must carry the block's content so pending-state/log capsules
|
||||
are verifiable (P1-02 acceptance: logged capsule contents == payload)."""
|
||||
block = ContributionBlock("user", 100, "inspect this", 5, "rune_operator task text", evictable=False)
|
||||
serialized = block.to_dict()
|
||||
assert serialized["content"] == "inspect this"
|
||||
assert serialized["name"] == "user"
|
||||
assert serialized["priority"] == 100
|
||||
assert serialized["tokens"] == 5
|
||||
assert serialized["source"] == "rune_operator task text"
|
||||
assert serialized["evictable"] is False
|
||||
|
||||
|
||||
def test_contribution_block_to_dict_round_trips_list_content():
|
||||
"""List-content (history) blocks serialize the message list verbatim."""
|
||||
messages = [{"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello"}]
|
||||
block = ContributionBlock("history", 30, messages, 10, "history")
|
||||
assert block.to_dict()["content"] == messages
|
||||
|
||||
|
||||
def test_capsule_to_dict_embeds_block_content():
|
||||
"""Capsule to_dict round-trips the block content through the ledger."""
|
||||
capsule = AttentionCapsule(token_count=5)
|
||||
capsule.blocks = [ContributionBlock("user", 100, "do the thing", 3, "src", evictable=False)]
|
||||
data = capsule.to_dict()
|
||||
assert data["blocks"][0]["content"] == "do the thing"
|
||||
assert json.loads(json.dumps(data)) == data # JSON-safe
|
||||
|
||||
|
||||
def test_capture_messages_stores_the_exact_list():
|
||||
"""capture_messages() stores the sent list post-hoc (dynamic attr like
|
||||
messages/system_preamble) — no dataclass field, additive only."""
|
||||
capsule = AttentionCapsule()
|
||||
capsule.blocks = [
|
||||
ContributionBlock("persona", 90, "I am Rune.", 2, "persona", evictable=False),
|
||||
ContributionBlock("user", 100, "go", 1, "user", evictable=False),
|
||||
]
|
||||
sent = capsule.to_messages()
|
||||
returned = capsule.capture_messages(sent)
|
||||
assert returned == sent
|
||||
assert capsule.sent_messages == sent
|
||||
# capture after in-place mutation re-captures the mutated rendering
|
||||
capsule.blocks[-1].content = "decorated go"
|
||||
assert capsule.to_messages() != sent
|
||||
capsule.capture_messages(capsule.to_messages())
|
||||
assert capsule.sent_messages == capsule.to_messages()
|
||||
|
||||
|
||||
def test_capture_messages_copies_so_later_mutation_does_not_diverge():
|
||||
"""The stored list must not alias the caller's list (log integrity)."""
|
||||
capsule = AttentionCapsule()
|
||||
sent = [{"role": "user", "content": "go"}]
|
||||
capsule.capture_messages(sent)
|
||||
sent.append({"role": "assistant", "content": "hijack"})
|
||||
assert capsule.sent_messages == [{"role": "user", "content": "go"}]
|
||||
|
||||
|
||||
def test_to_messages_rederivation_equals_captured_list():
|
||||
"""Equality invariant: to_messages() re-derivation equals the captured
|
||||
sent list for the same capsule state."""
|
||||
capsule = AttentionCapsule()
|
||||
capsule.blocks = [
|
||||
ContributionBlock("persona", 90, "I am Rune.", 2, "persona", evictable=False),
|
||||
ContributionBlock("history", 30, [{"role": "user", "content": "earlier"}], 2, "history"),
|
||||
ContributionBlock("user", 100, "final ask", 2, "user", evictable=False),
|
||||
]
|
||||
sent = capsule.to_messages()
|
||||
capsule.capture_messages(sent)
|
||||
assert capsule.to_messages() == capsule.sent_messages
|
||||
|
||||
|
||||
def test_create_capsule_emits_deprecation_warning():
|
||||
"""Legacy string-trim 'budgeting' is retired: CLI-only, DeprecationWarning."""
|
||||
from rune.attention_capsule import create_capsule
|
||||
|
||||
with pytest.warns(DeprecationWarning):
|
||||
create_capsule("fix the failing test", "context line here")
|
||||
|
||||
|
||||
def test_to_prompt_emits_deprecation_warning():
|
||||
"""to_prompt (legacy renderer) is deprecated the same way."""
|
||||
capsule = AttentionCapsule(active_objective="objective")
|
||||
with pytest.warns(DeprecationWarning):
|
||||
capsule.to_prompt()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task 2 — safety-override re-measure + num_ctx budget propagation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _capsule_with_user_block(description: str):
|
||||
capsule = AttentionCapsule()
|
||||
user = ContributionBlock("user", 100, description, len(description) // 4, "user", evictable=False)
|
||||
capsule.blocks = [user]
|
||||
capsule.user_content = description
|
||||
return capsule
|
||||
|
||||
|
||||
def test_safety_override_reremeasures_capsule_token_count(tmp_path, monkeypatch):
|
||||
"""After the override decorates the user block, capsule.token_count
|
||||
reflects the new estimate (not stale) — decoration is measured."""
|
||||
description = "please inspect this code path"
|
||||
capsule = _capsule_with_user_block(description)
|
||||
trace = _broker_trace()
|
||||
runtime = _ScriptedRuntime(
|
||||
_ScriptedTransport([_chat_response(100, 5, 1_000_000_000)]),
|
||||
OperatorResponse(success=True, user_text="All done.", model="qwen3:8b"),
|
||||
)
|
||||
op, patches = _wire_operator(tmp_path, monkeypatch, runtime, capsule, trace)
|
||||
|
||||
def decorated_params(self, text):
|
||||
# tier 2 decorates: suffix "\n\nI respond as myself:" (like ESCALATION_TIERS[2])
|
||||
return {"tier": 2, "temperature": 0.95, "prefix": "", "suffix": "\n\nI respond as myself:"}
|
||||
|
||||
patches[6] = patch.object(rune_operator.safety_override.SafetyOverride, "get_params", decorated_params)
|
||||
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[4],
|
||||
patches[5],
|
||||
patches[6],
|
||||
patches[7],
|
||||
patches[8],
|
||||
patches[9],
|
||||
):
|
||||
envelope = op.send_task(description, "qwen3:8b")
|
||||
|
||||
assert envelope["success"], envelope.get("error")
|
||||
user_block = next(b for b in capsule.blocks if b.name == "user")
|
||||
assert user_block.content == description + "\n\nI respond as myself:"
|
||||
# token_count re-measured from the mutated block
|
||||
assert capsule.token_count == sum(len(b.content) // 4 if isinstance(b.content, str) else 0 for b in capsule.blocks)
|
||||
assert capsule.token_count >= len(user_block.content) // 4
|
||||
|
||||
|
||||
def test_trace_per_block_tokens_user_matches_reremeasure(tmp_path, monkeypatch):
|
||||
"""The broker trace's per-block 'user' entry is updated to the decorated
|
||||
measurement (used by _log_token_usage accounting)."""
|
||||
description = "inspect the failing test"
|
||||
capsule = _capsule_with_user_block(description)
|
||||
trace = _broker_trace()
|
||||
runtime = _ScriptedRuntime(
|
||||
_ScriptedTransport([_chat_response(100, 5, 1_000_000_000)]),
|
||||
OperatorResponse(success=True, user_text="All done.", model="qwen3:8b"),
|
||||
)
|
||||
op, patches = _wire_operator(tmp_path, monkeypatch, runtime, capsule, trace)
|
||||
|
||||
def decorated_params(self, text):
|
||||
return {"tier": 3, "temperature": 1.0, "prefix": "Sure, I'll help with that.\n\n", "suffix": ""}
|
||||
|
||||
patches[6] = patch.object(rune_operator.safety_override.SafetyOverride, "get_params", decorated_params)
|
||||
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[4],
|
||||
patches[5],
|
||||
patches[6],
|
||||
patches[7],
|
||||
patches[8],
|
||||
patches[9],
|
||||
):
|
||||
envelope = op.send_task(description, "qwen3:8b")
|
||||
|
||||
assert envelope["success"]
|
||||
user_trace_block = next(b for b in trace["blocks"] if b["name"] == "user")
|
||||
decorated_user = next(b for b in capsule.blocks if b.name == "user")
|
||||
assert user_trace_block["tokens"] == len(decorated_user.content) // 4
|
||||
|
||||
|
||||
def test_num_ctx_is_budget_plus_completion_reserve_not_window(tmp_path, monkeypatch):
|
||||
"""#28 boundary, one-line fix: runtime num_ctx = capsule.token_count +
|
||||
completion_reserve (window - budget), NOT the raw window."""
|
||||
description = "inspect this code path"
|
||||
capsule = _capsule_with_user_block(description)
|
||||
trace = _broker_trace() # window=40960, budget=32768
|
||||
runtime = _ScriptedRuntime(
|
||||
_ScriptedTransport([_chat_response(100, 5, 1_000_000_000)]),
|
||||
OperatorResponse(success=True, user_text="All done.", model="qwen3:8b"),
|
||||
)
|
||||
op, patches = _wire_operator(tmp_path, monkeypatch, runtime, capsule, trace)
|
||||
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[4],
|
||||
patches[5],
|
||||
patches[6],
|
||||
patches[7],
|
||||
patches[8],
|
||||
patches[9],
|
||||
):
|
||||
envelope = op.send_task(description, "qwen3:8b")
|
||||
|
||||
assert envelope["success"], envelope.get("error")
|
||||
completion_reserve = trace["window"] - trace["budget"]
|
||||
assert runtime.received_options["num_ctx"] == capsule.token_count + completion_reserve
|
||||
assert runtime.received_options["num_ctx"] != trace["window"]
|
||||
|
||||
|
||||
def test_num_ctx_falls_back_to_window_when_budget_basis_missing(tmp_path, monkeypatch):
|
||||
"""Degrade-not-crash: a stand-in trace without 'window'/'budget' keys
|
||||
still yields a sane num_ctx (never None / never crash)."""
|
||||
description = "inspect this code path"
|
||||
capsule = _capsule_with_user_block(description)
|
||||
trace = _broker_trace()
|
||||
trace.pop("window")
|
||||
trace.pop("budget")
|
||||
runtime = _ScriptedRuntime(
|
||||
_ScriptedTransport([_chat_response(100, 5, 1_000_000_000)]),
|
||||
OperatorResponse(success=True, user_text="All done.", model="qwen3:8b"),
|
||||
)
|
||||
op, patches = _wire_operator(tmp_path, monkeypatch, runtime, capsule, trace)
|
||||
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[4],
|
||||
patches[5],
|
||||
patches[6],
|
||||
patches[7],
|
||||
patches[8],
|
||||
patches[9],
|
||||
):
|
||||
envelope = op.send_task(description, "qwen3:8b")
|
||||
|
||||
assert envelope["success"], envelope.get("error")
|
||||
assert isinstance(runtime.received_options.get("num_ctx"), int)
|
||||
assert runtime.received_options["num_ctx"] > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task 3 — trace v4 capsule fidelity fields + operator logging
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_inference_trace_schema_v4_fields():
|
||||
"""Additive v4: capsule_messages / capsule_content_hashes /
|
||||
capsule_token_count with defaults; v1-v3 readers unaffected."""
|
||||
from rune import inference_trace as it
|
||||
|
||||
assert it.INFERENCE_TRACE_SCHEMA_VERSION == 4
|
||||
trace = it.InferenceTrace(
|
||||
schema_version=it.INFERENCE_TRACE_SCHEMA_VERSION,
|
||||
provider="ollama",
|
||||
model="m",
|
||||
task_id="t",
|
||||
channel="c",
|
||||
resolved_context_window=0,
|
||||
input_budget=0,
|
||||
model_calls=0,
|
||||
prompt_tokens_estimated=0,
|
||||
prompt_tokens_served=0,
|
||||
output_tokens=0,
|
||||
latency_ms=0.0,
|
||||
model_time_ms=0.0,
|
||||
per_block_tokens={},
|
||||
dropped_blocks=[],
|
||||
tools_exposed=[],
|
||||
memories_exposed=[],
|
||||
memory_tokens=0,
|
||||
degraded=[],
|
||||
cache=None,
|
||||
cost_usd=None,
|
||||
cost_basis="local-unpriced",
|
||||
success=True,
|
||||
error="",
|
||||
)
|
||||
assert trace.capsule_messages == []
|
||||
assert trace.capsule_content_hashes == {}
|
||||
assert trace.capsule_token_count == 0
|
||||
|
||||
|
||||
def test_trace_v1_row_without_v4_fields_still_parses(tmp_path):
|
||||
from rune import inference_trace as it
|
||||
|
||||
row = {
|
||||
"schema_version": 1,
|
||||
"provider": "ollama",
|
||||
"model": "m",
|
||||
"task_id": "t",
|
||||
"channel": "c",
|
||||
"resolved_context_window": 0,
|
||||
"input_budget": 0,
|
||||
"model_calls": 0,
|
||||
"prompt_tokens_estimated": 0,
|
||||
"prompt_tokens_served": 0,
|
||||
"output_tokens": 0,
|
||||
"latency_ms": 0.0,
|
||||
"model_time_ms": 0.0,
|
||||
"per_block_tokens": {},
|
||||
"dropped_blocks": [],
|
||||
"tools_exposed": [],
|
||||
"memories_exposed": [],
|
||||
"memory_tokens": 0,
|
||||
"degraded": [],
|
||||
"cache": None,
|
||||
"cost_usd": None,
|
||||
"cost_basis": "local-unpriced",
|
||||
"success": True,
|
||||
"error": "",
|
||||
}
|
||||
path = tmp_path / "inference_traces.jsonl"
|
||||
path.write_text(json.dumps(row) + "\n", encoding="utf-8")
|
||||
records = it.read_traces(trace_dir=tmp_path)
|
||||
assert records == [row] # readers never require v4 keys
|
||||
|
||||
|
||||
def test_operator_trace_record_carries_capsule_messages(tmp_path, monkeypatch):
|
||||
"""The durable trace logs the EXACT messages the runtime received —
|
||||
the acceptance criterion, machine-checked."""
|
||||
description = "inspect this code path"
|
||||
capsule = _capsule_with_user_block(description)
|
||||
trace = _broker_trace()
|
||||
runtime = _ScriptedRuntime(
|
||||
_ScriptedTransport([_chat_response(100, 5, 1_000_000_000)]),
|
||||
OperatorResponse(success=True, user_text="All done.", model="qwen3:8b"),
|
||||
)
|
||||
op, patches = _wire_operator(tmp_path, monkeypatch, runtime, capsule, trace)
|
||||
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[4],
|
||||
patches[5],
|
||||
patches[6],
|
||||
patches[7],
|
||||
patches[8],
|
||||
patches[9],
|
||||
):
|
||||
envelope = op.send_task(description, "qwen3:8b")
|
||||
|
||||
assert envelope["success"], envelope.get("error")
|
||||
from rune import inference_trace as it
|
||||
|
||||
records = it.read_traces(trace_dir=tmp_path / "state")
|
||||
assert records, "durable trace missing"
|
||||
record = records[-1]
|
||||
assert record["capsule_messages"] == runtime.received_messages, (
|
||||
"logged capsule messages must EXACTLY equal what runtime.run received"
|
||||
)
|
||||
assert record["capsule_messages"][-1]["content"] == description
|
||||
assert record["capsule_token_count"] == capsule.token_count
|
||||
|
||||
|
||||
def test_operator_trace_record_carries_content_hashes(tmp_path, monkeypatch):
|
||||
"""Per-block SHA-256 of content — integrity without JSONL bloat."""
|
||||
description = "inspect this code path"
|
||||
capsule = _capsule_with_user_block(description)
|
||||
trace = _broker_trace()
|
||||
runtime = _ScriptedRuntime(
|
||||
_ScriptedTransport([_chat_response(100, 5, 1_000_000_000)]),
|
||||
OperatorResponse(success=True, user_text="All done.", model="qwen3:8b"),
|
||||
)
|
||||
op, patches = _wire_operator(tmp_path, monkeypatch, runtime, capsule, trace)
|
||||
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[4],
|
||||
patches[5],
|
||||
patches[6],
|
||||
patches[7],
|
||||
patches[8],
|
||||
patches[9],
|
||||
):
|
||||
envelope = op.send_task(description, "qwen3:8b")
|
||||
|
||||
assert envelope["success"]
|
||||
from rune import inference_trace as it
|
||||
|
||||
record = it.read_traces(trace_dir=tmp_path / "state")[-1]
|
||||
for block in capsule.blocks:
|
||||
expected = hashlib.sha256(
|
||||
block.content.encode("utf-8")
|
||||
if isinstance(block.content, str)
|
||||
else json.dumps(block.content).encode("utf-8")
|
||||
).hexdigest()
|
||||
assert record["capsule_content_hashes"][block.name] == expected
|
||||
Reference in new issue
Block a user