Repository navigation
refactor(agent): read the charter and the window's shrinking through the roles - #468
Conversation
The dispatch charter had four read points and none of them was on a harness module: the identity segment reached into a dispatch-layer ContextVar for the brief, and the tool registry held a module-level function for the checks. This moves the two reads that belong to a role onto that role and leaves the two that do not where they are. - prompt / stopWhen: DefaultMemory.assemble() fills task_brief and task_done_when on the TurnContext; the identity segment renders ctx.task_brief and no longer imports the dispatch layer at all. The curator's TurnContext rebuild carries the two new fields through. - checks / code: ActionModule gains judge(name, params, prior). DefaultAction answers with charter.judge(); ToolRegistry takes an injected verifier_provider and asks it once per call, ahead of the permission gate, the way it already asks the plugin gates. The in-process lane builds its own DefaultAction since it runs no harness. - tools stays on ToolRegistry.withheld_names(): it is a real gate, and CapabilitySelection is documented as a view, never a permission. - timeoutSeconds stays in the ACP client. The charter imports stay inside the calls on purpose: raven.agent.subagent pulls its manager and every backend on package import, so naming the charter at module scope makes importing the harness import 173 modules instead of 29. import-linter reads direction and cannot see this cycle. Verified with the feature off against the base commit (a full run_turn capture, byte-identical after normalising nonces and paths) and with it on (the in-process worker lane, byte-identical including both refusals; a real-model dispatch; an ACP fork carrying the charter across the process boundary). Each new link was mutation-checked. Contract pins bumped: CONTRACTS_VERSION 19, CONTRACTS_LINE_CEILING 2830. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The loop ran five recoveries inline -- proactive compaction, the standing image window, a context overflow, a picture refused inside a tool result, pictures refused for their size -- as ~200 lines of loop body plus five class methods of ~300 lines, all deciding the same question: how does the window get smaller so the next call fits. That is the Memory role's question, and this moves the policy half onto it while the mechanism half stays put. - Memory gains shrink(messages, pressure, state, model). WindowPressure names the five reasons a window is asked to get smaller; WindowState carries one turn's readings and retry budgets (held by the shell, since the role outlives the turn); ShrinkResult says whether the window got smaller, which on a reactive pressure is the shell's cue to retry the iteration. - The six `continue` statements, `iteration -= 1`, and the capability-table cache verdict do not move: re-entering the iteration at the top is what lets the hooks and the standing passes see a retry exactly as they saw the attempt, so the retry stays the loop's. - New package raven/agent/window (compaction, images, shrink): the pure pieces the loop and the Memory role both read, imported by both and importing neither. compaction.py moves there from the loop; the picture provenance and withdrawal notes move there from _shared.py; the five operations and their bounds move there from turn_path.py and main.py. Added to the "cargo does not import the loop shell" import-linter contract. - REASONING_EFFORT_LADDER moves to contracts/llm_provider.py: the loop's empty-response descent and the window's head summary both read it. - WindowState.image_window has no default: 0 is a real value meaning no picture stays, so a state built without saying so would withdraw them all. turn_path.py: +68 / -537. Contract pins: CONTRACTS_VERSION 28, CONTRACTS_LINE_CEILING 3200, ledger entries for the new names. Verified against the base commit with scripted providers, byte for byte after normalising nonces and paths: six recovery scenarios (each of the five moves, when broken, turns exactly its scenarios red), the whole turn with the playbook off, and the playbook-on worker lane with two check refusals. A real-model dispatch (two workers, three checks each) completes with every tool call succeeding. Full suite green. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the current head after its merge from main. I covered the repository rules and canonical runtime vocabulary, the full diff, the affected charter/window callers, both feature commits and surrounding history, compatibility of the moved internal helpers and expanded harness contracts, test changes for weakening, and the cargo-versus-loop architecture boundary. The refactor preserves the prior retry/state transitions while moving policy behind Memory, and the charter prompt/check paths remain joined end to end.
Verification: the focused changed-area run passed 247 tests; the PPT ledger passed 13 tests under the all-extras environment; uv run lint-imports kept all 10 contracts. The full all-extras run reported 23,127 passed and 120 skipped, with one unrelated test_cli_theme failure caused by this runner's NO_COLOR=1; that exact test passed when rerun with NO_COLOR unset. GitHub's four Python test shards are also green on this head.
LivXue
left a comment
There was a problem hiding this comment.
Review: verified against f69e6cf7f vs merge-base 3b35e7d34
This is a high-quality refactor. turn_path.py loses 537 lines, the contract paper
is written with more care than most changes of this size get, and the verification
method in the description (per-commit byte-compare under a scripted provider, plus a
mutation check to prove the comparison is not vacuous) is the right one.
Two findings are, in my reading, blocking. Both are small to fix.
B1. The charter's checks/code enforcement went from unconditional to opt-in,
and one registry that dispatches inside a live charter scope did not get the opt-in.
This contradicts "no behaviour change intended", and it moves a security boundary in
the permissive direction. Detail inline at registry.py:407.
B2. Four of the five tests this PR adds live in a directory the default run excludes,
so the guard it wrote for its own new failure mode never executes. Detail inline at
test_subagent_charter_e2e.py:215.
Four more I would like resolved in the same change, each one or two lines:
contracts/context.py:80 (positional-slot shift on a contract-tier dataclass),
harness/__init__.py:52 (no bind_action beside bind_memory),
contracts/harness.py:13 (the paper contradicts itself), and CONTEXT.md:93
(AGENTS.md section 6: three new contract-tier terms and a new ruled package are
undefined, and the charter is renamed to "playbook", which CONTEXT.md already
defines as a different feature).
Nine further comments are non-blocking.
On the nine: five of them are pre-existing code this PR relocates verbatim
(images.py:77, images.py:90, memory.py:217, memory.py:262, shrink.py:237).
I verified them byte-identical to the base after normalising the _name -> name
de-privatisation. They are not defects introduced here; they show up as + lines
to anyone scanning added hunks, and creating the file is the one moment the fix is
free. Fixing them is your call, not a condition of merge.
One side note outside this diff: promoting REASONING_EFFORT_LADDER to contracts
did not reach rpc/methods/console.py:843, which still validates
agents.defaults.reasoningEffort against four of the ladder's six rungs, so the
console rejects xhigh/max while config files and the Anthropic transport accept
them. Worth a separate PR.
Nothing here argues against the shape of the refactor. The policy/mechanism split is
the right seam, and the shell keeping the six continue sites and the iteration
rollback is what makes it safe.
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: charter checks are bypassed by the Curator registry, and the new charter-path guards are outside default CI collection.
The new review evidence changes my prior stance. I verified that a bare ToolRegistry() under an active charter whose rule forbids the call still executed it and returned RAN:blocked; the Curator constructs that same bare registry and dispatches through it inside the turn's charter scope. I also verified that default pytest collection finds zero tests from test_subagent_charter_e2e.py, while the explicitly named file collects 30. tests/test_harness_modules.py remains green (25 passed), but does not cover the missing charter join.
The existing blocking threads already state the required fixes, so I am not duplicating inline findings or replying under another reviewer's name. My earlier clean stance is withdrawn until those open blockers are addressed.
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the merge-only revision leaves the Curator charter-enforcement regression and excluded charter-path tests unchanged.
I reviewed the delta from f69e6cf7f0f1: this head only merges three unrelated main commits, so the PR's feature files and affected callers are unchanged. I also checked the full thread record; the blocking threads remain open with no replies, resolution, or maintainer deferral.
Reverified on this head: a charter-forbidden call through a bare ToolRegistry() still returned RAN:blocked; default pytest collection still found zero test_subagent_charter_e2e tests; and uv run --all-extras pytest tests/test_harness_modules.py -q passed 25 tests. I covered the revision delta and history, the relevant caller path, default test collection, the standing review record, and the repository's test/domain constraints. No new inline findings.
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the unchanged charter-enforcement and default test-discovery blockers still have to be resolved.
This head only merges current main; compared with bb64cb073980, it changes the session manager and its tests but leaves the PR's own patch unchanged. I found no new defect in that merge delta, but the existing blockers still reproduce on the merged tree: a bare registry executes the charter-forbidden probe, and default collection still finds zero entries from test_subagent_charter_e2e.py.
Covered: the project rules (AGENTS.md/CLAUDE.md, CONTEXT-MAP.md, and CONTEXT.md), the full PR diff and merge delta, relevant callers and history, compatibility/API concerns, test placement and whether coverage was weakened, and the harness/role architecture. No new inline findings.
Verification: git diff --check github/main...HEAD passed; uv run pytest tests/test_harness_modules.py -q (25 passed); explicit charter integration suite (30 passed); merged session suite (37 passed); affected loop/contracts/window unit selection (192 passed). The default-collection diagnostic returned 0 charter integration entries.
Review of the charter move found two holes and four things to put right. A registry nobody handed a verifier_provider returned no opinion at all, where the module-level helper it replaced ran on every registry. Of the ten ToolRegistry construction sites, two pass a provider; the curator builds a bare one and dispatches through it inside the turn's charter_scope, so a check or code judge that refused a curator call before this series stopped refusing it. The seat now falls back to the default Action role, which is what that helper already was. The tests for the charter move lived under tests/integration, which norecursedirs keeps out of the default run and no workflow names, so four of five never executed. The two that need no external resource move into tests/test_segments.py, and the registry fallback gets its own regression in tests/test_subagent_charter.py. Also: - AssemblyContext's task_brief / task_done_when move to the end of the dataclass, where the same pair already sits on TurnContext. Inserted mid-list they shifted describe_tool two positional slots, silently rebinding the tenth argument of any out-of-tree SegmentBuilder fixture. - The Action role is admitted through a bind() that names a missing member, the way the Memory role already is. Without it a role lacking judge fails inside ToolRegistry.execute, whose except arm answers "no opinion" -- so the shape error would have read as a permissive playbook. - contracts/harness.py said the in-loop recoveries stay with the shell while MemoryModule.shrink said Memory decides them. The paper now draws the line where the code does: Memory answers what to give up, the shell owns the mechanism and the retry bounds. - CONTEXT.md defines WindowPressure, WindowState and ShrinkResult, and its cargo-contract roster names agent/window. - WindowPressure dispatch compares with == rather than is, since it is a str Enum and the documented string is equal but not identical; a zero image budget reads as a budget in the withdrawal note, matching the guard above it; a negative image window can no longer invert the standing pass. - Two comments that described something the tree does not do, and a test whose name promised the retry budgets while its body drove the one pressure that touches none. Verified: full suite 22898 passed, no failure the base branch does not also show; ruff, format and import contracts clean; the four scripted A/Bs against the branch these sit on are byte-identical. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: move the Memory-to-engine charter join test into default collection and finish the required Charter terminology and cargo-documentation fixes.
This revision fixes the bare-registry enforcement bypass, positional compatibility, Action-role admission, recovery ownership prose, and several non-blocking observations. I found no new defect in the fix commit.
Two established issues remain. Default collection still finds zero entries from test_subagent_charter_e2e.py; the new default-suite tests cover _briefed and identity rendering separately, but not the DefaultMemory.assemble -> _briefed -> engine join. Removing self._briefed(turn) therefore still leaves the default tests green. Also, CONTEXT.md now defines the window terms and adds window to the cargo roster, but the roster still omits harness, while changed runtime prose continues to use playbook for the separately defined Charter concept.
Covered: AGENTS.md/CLAUDE.md and the Runtime context documents, the full final-comparison diff and this revision's delta, affected callers and history, positional/API compatibility, test-strength changes, and harness/import architecture. Patch-risk result: revise (impact high, likelihood high, protection partial, recovery easy, confidence high).
Verification at this head: focused default-suite selection, 130 passed; explicit charter integration file, 30 passed; default collection diagnostic, 0 charter-integration entries; import-linter, 10 contracts kept; git diff --check, passed.
|
Thanks -- this found two real holes. Fourteen of the fifteen are addressed in ec24a4e; the three pre-existing ones I propose to send separately, reasoning at the end. Both blocking items, fixed. The registry early return was a genuine regression, and the curator path is what makes it bite. You are right that the The four "fix before merge", fixed.
Six of the nine non-blocking, fixed. Dispatch compares with On the curator: I kept the two lines and rewrote the comment, which now says the rebuild is partial on purpose and that the brief reaches the identity through The misnamed test is split: one asserts what the standing pass does, and a new one drives Three I propose to leave. The filed-note regex truncation, the base64 copy in Verification after the fixes: full suite 22898 passed, with no failure |
|
Blocking: the default suite still needs to exercise the Memory-to-engine charter join, and the remaining Section 6 documentation mismatches need correction. Thanks for the detailed accounting. I agree that the bare-registry bypass and the other implementation/API issues named here are fixed, and I accept the filed-note truncation, base64-copy cost, and double-tokenisation as explicitly deferred non-blocking follow-ups. Two parts do not hold yet:
Verification: |
Three follow-ups from the second review pass. The join the first round left uncovered: DefaultMemory.assemble -> _briefed -> engine. The two tests that landed in the default suite covered each half on its own, so deleting the _briefed call left them green. The new test drives a real assemble under a bound charter and asserts on what the engine was handed; removing the call turns it red and nothing else does. CONTEXT.md is the map, so the prose follows it: what a dispatch carries is a Charter, defined there, and Playbook is the separate library concept. The comments and docstrings this series added now say Charter. The cargo roster also names `harness`, which the import contract has listed since the roles landed. Verified: full suite 22899 passed, no failure main does not also show; ruff, format and import contracts clean; the new test fails on the mutant that drops the brief. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
This revision closes the remaining concerns from my side. The default suite now exercises the real DefaultMemory.assemble -> _briefed -> engine join, the cargo roster includes harness, and the affected prose consistently uses Charter rather than the distinct Playbook term. I found no new defect in the delta.
The three explicitly deferred, pre-existing follow-ups remain named here: filed-note caption truncation, the base64 length copy, and duplicate transcript tokenisation in _compact_ahead. They do not block this change. LivXue owns the existing review threads, so I have not resolved or regraded them.
Covered: repository rules and Runtime context documents, the full final-comparison diff and new delta, affected callers/history, compatibility, test strength, and harness/import architecture. Patch-risk result: merge / human_review_required (impact high, likelihood low, protection strong, recovery easy, confidence high).
Verification at this head: focused default-suite selection, 131 passed; the new join regression is collected exactly once by default; import-linter, 10 contracts kept; git diff --check, passed.
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
This head only merges the latest main; the feature patch is unchanged, there is no manual conflict-resolution delta, and the newly combined trajectory/replay paths remain compatible with the registry changes. All fifteen review threads are now resolved by their author. The already named deferred, pre-existing follow-ups remain outside this change and are not merge conditions.
Covered: repository rules and Runtime context documents, the final-comparison diff and merge delta, affected callers and history, compatibility, test strength, and harness/import architecture. No new findings. Patch-risk result: merge / human_review_required (impact high, likelihood low, protection strong, recovery easy, confidence high).
Verification at this head: focused harness/contract selection, 131 passed; trajectory/replay/regression selection, 150 passed; import-linter, 10 contracts kept; git diff --check, passed.
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
This revision only merges the latest main commit. Its incoming changes are confined to the two READMEs and tests/test_cli_onboard_commands.py, with no overlap in the feature-modified files, so the reviewed feature patch is unchanged from a0db8b641eb2.
I rechecked the repository rules and terminology documentation, the final diff and its caller/history context, compatibility and test integrity, and the architecture boundaries. Verification: git diff --check github/main...HEAD; uv run --all-extras pytest tests/test_cli_onboard_commands.py tests/test_harness_modules.py tests/test_subagent_charter.py tests/test_segments.py tests/test_contracts_two_tier_ledger.py -q (463 passed); uv run --frozen --python 3.12 lint-imports (10 contracts kept). No new findings.
…es (#469) ## Summary Follows #468 (merged as `869ad78a`), which moved the charter reads and the window's mid-turn shrinking onto the harness roles. This branch is rebased onto `main` and carries only its own three commits. A sub-agent's product logic was an `AgentHook` subclass: six phase methods over a fourteen-field context it could write, with the turn's own state kept in the context's free-form `metadata` dict. Surveyed before this change, the five bundled plugins had 71 phase methods, 42 of them doing real work, reading fourteen context fields and using six decision fields. Every one of them fits a small set of verbs. This PR makes those verbs the contract, in three commits. **1. A sub-agent writes a conduct instead of six hook phases.** New factory-loop-tier paper `contracts/agent_conduct.py`: `AgentConduct` with nine optional verbs (`intake`, `select_tools`, `advise`, `system_addendum`, `review`, `salvage`, `outbound`, `archive`, `observe`), answered against a read-only `StepView` of fourteen fields, with `Intake` and `Verdict` (Accept / Resample / End) as the answers and a `ConductFactory` because the host builds one conduct per turn. It sits beside `loop_hooks` rather than replacing it. The six phases remain the loop's *timing* contract: when the loop asks, in what order, what a refusal does. This is the *judgement* contract: given one step, what does this agent say about it. `ConductHook` seats one in the other, so a plugin that still ships a plain `AgentHook` keeps working and the composite's ordering, merging and rollback are untouched. All five bundled plugins are ported: design-engine, ppt-engine, code-flow, oncall-flow and research-flow. Research keeps its six-phase gate chain: the gates now subclass a plugin-private `Gate` and read a `GateCtx` the conduct builds from its `StepView` and its own turn-private facts, so six thousand lines of gate bodies did not move and its wrappers still see the cross-gate state they read. The turn's conduct and the system-message addendum it splices live in the turn's own `metadata` dict, not on the hook: one hook instance serves every turn a process runs, and a system turn can overlap a user one on the same chain. **2. The conducts seat on the roles.** `MemoryModule.intake`, `PlanningModule.advise`, `ActionModule.review` and `ActionModule.salvage` each take this turn's conducts, so a replaced role decides what a plugin's judgement does rather than the seat deciding it. The composition rules live in `raven/agent/harness/conducts.py`: the first non-Accept verdict wins, advice joins in order, the first salvage wins, an intake threads the text through each conduct. `run_turn` binds the harness for the turn the way it binds the model, and the seat falls back to the same rules when nothing is bound, which is how a plugin test drives a hook directly. Contract papers: the conduct paper is factory-loop tier, so the contract-tier digest and `CONTRACTS_VERSION` do not move with it. `CONTRACTS_LINE_CEILING` 3,200 to 3,420, each bump with its own review paragraph in `tests/test_kernel_budget.py`. **3. What the review found.** Eleven threads, all resolved in the third commit. The ones that changed behaviour rather than prose: `CodeFlowHook` kept its `rolls_back_iterations = False` (inheriting `True` from the new base had it hold every reply behind the draft gate instead of streaming); `StepView` now publishes deep-frozen nested values, since freezing only the outer tuple left a conduct able to write through to `ctx.messages`; a turn's conducts are collected at the role boundary, so a replaced role sees one batch of the turn's judgements rather than one call per seat; an addendum is located by its own text rather than a saved offset, which two conducts appending system text used to shift out from under each other; `Planning` and `Action` gained binders, so a replacement role missing a new member is named at assembly rather than swallowed by the composite's `except Exception`. `CONTEXT.md` carries the Agent Conduct term and the Agent Hook entry no longer describes what this PR replaces. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [x] Refactor - [ ] Other ## Verification Gates. CI runs these at the branch tip (17 pass, 1 skipped -- the skipped row is the disabled Claude Mention workflow). Locally, at each of the three commits: ``` uv run ruff check --no-cache raven evolver agents plugins-dist tests scripts uv run ruff format --check --no-cache raven evolver agents plugins-dist tests scripts uv run lint-imports uv run pytest -q npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs uv run python scripts/check_commit_messages.py origin/main..HEAD uv run python scripts/check_source_language.py origin/main..HEAD uv run python scripts/check_large_files.py origin/main..HEAD ``` Import contracts: 10 kept, 0 broken at each of the three commits. The suite is green at each, and no commit adds a failure that `main` does not already show on this machine (this checkout has the heavy extras uninstalled, so the local run carries failures `main` carries too -- CI is the authority, and it is green). The loop's own e2e files, which the default selection leaves out, were run too (charter, playbook, truncation, direct chat, fallback chain, ACP stdio): no new failure. Behaviour preservation. The oracle for this change is the hook seam itself: all five plugins driven through every phase of a multi-iteration turn under the same scripted provider, recording each phase's decision and the observers it files. Outputs normalised for nonces and temp paths, then byte-compared. Re-run after the review fixes at the branch tip `81e6f423` (all three commits), against `main` at `c65c2059`: | scenario | bytes | | --- | --- | | five-plugin hook seam | 23,840 == 23,840 | | a whole turn with no playbook (host turn, spawned sub-agent, tool calls and refusals) | 12,821 == 12,821 | | six window-shrink scenarios | 133,371 == 133,371 | These cover the tip, not each commit separately. The seam comparison is the one that matters most for the third commit: its first fix changed `rolls_back_iterations` for code-flow, which decides whether replies stream or are held, and the seam oracle records the decision at every phase, so an unintended change there shows as a diff rather than as silence. A unit test proves the seat is a real decision point rather than a pass-through: with a lenient Action role bound, a conduct's Resample is not applied, and Planning's advice and Memory's intake are what the loop receives. Real model, Sonnet 4.5 through OpenRouter, at the branch tip: - playbook on, in-process lane: two workers, each receives the charter (3 checks), writes its file, finishes; 26 recorded model calls - playbook on, ACP fork lane: the charter prompt crosses the process boundary (the fork answers ZEBRA where the task text says PONG, so the brief reached the forked model through Memory), and a charter check blocks `write_file` to `notes/hi.txt` against a `pathPrefix: out/` rule - Raven-Research, a fork running the full gate chain: `plain-first: escalated_tool`, then `sufficiency-gate: released on the pages at iteration 4 after 2 searches / 2 pages` - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk No user-visible behaviour change is intended, and the hook-seam comparison is byte-identical on every phase it covers. For plugin authors: the six-phase `AgentHook` is unchanged and a plugin shipping one keeps working. `ConductHook` is an adapter that seats a conduct in that same chain. For anyone replacing a harness role: the role protocols gain `intake`, `advise`, `review` and `salvage`, so a replacement written against the old protocol needs them. Rollback is a revert of the merge commit. Nothing persisted on disk changes shape. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A --------- Co-authored-by: yao pengfei <yaopengfei@shanda.com> Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…write (#495) ## Summary #469 gave a bundled sub-agent nine verbs in place of six hook phases. Reviewing it turned up a list of things the contract said but did not yet do, and this PR closes that list. No behaviour change is intended: the point is that the same seam becomes something a role can replace and something a generated judgement can enter. **1. The roles are named apart from the verbs, and `observe` is gone.** `ActionModule.review` and the participant's `review` were two different things a line apart. The four delegating role methods now say what they do -- they ask -- and the verbs keep their own names, so `ask_review` asks `review`. `observe` is removed: a verb with no return value decides nothing and can only keep state a participant is forbidden to keep. `outbound` says on its own docstring that it is the one verb with no role seat, and `StepView` gains `phase`, so a verb asked at two moments reads which one it is rather than inferring it from whichever other field happens to be set (`tools_ran` becomes a property derived from it, so the two cannot disagree). **2. The three verbs the seat was applying itself get a seat.** `system_addendum`, `archive` and `select_tools` were rendered straight onto the hook decision, so each was the one judgement in the set with nowhere to compose two participants, nowhere to vet what came back, and nothing a replacement role could decide. They join the other four: `MemoryModule.ask_system_addendum` / `ask_archive` and `CapabilityModule.ask_select_tools`. The seat is not the point where narrowing takes effect -- `select` still is, and the registry still adjudicates every call -- so a participant narrows after the product has spoken and never instead of it. **3. A participant answers with plain data.** `review`, `intake` and `system_addendum` returned host classes (`Verdict`, `Intake`) and `salvage` returned `Any`. `Accept`, `Resample`, `End` and `Intake` are now builders returning plain mappings, the host reads one back with `read_verdict` / `read_intake`, and an answer it cannot read is silence rather than a refusal. That is what makes the third invariant real and what lets a judgement compiled from source answer the same verbs a plugin does -- it can build a `dict`; it cannot build a host class. **4. A dispatch's judgements are a participant.** `judge` was the one verb a role answered by reading the dispatch's own `checks` and `code` for itself. A Charter now enters as a participant like any other, `ActionModule.ask_judge` takes the list, and the merge is a veto so the first refusal decides. The participant list becomes the only route an external judgement takes, which is the thing step four needs: a judgement generated for one dispatch joins the same list without a second way in. **5. The rename.** `AgentConduct` is `AgentParticipant`, `contracts/agent_conduct.py` is `contracts/participant.py`, `agent/hook/conduct.py` is `agent/hook/participant.py`, and `agent/harness/conducts.py` is `agent/harness/participants.py`. "Conduct" was already taken in this tree -- raven-code's Coding Conduct prompts are a different thing -- and a reader hitting both had no way to tell them apart. The rename was done symbol by symbol, not by search and replace, for exactly that reason. **7. Review (LivXue, at 3a1bf07).** The `judge` disclosure had reached two of its four homes; the other two are `ActionModule.ask_judge` on the paper and `DefaultAction.ask_judge`, and they survived because they phrase the claim without naming any symbol the earlier fix touched. The test meant to pin that claim could not fail for the reason it was written -- it asserted the refusal rather than the argument, and built a `ParticipantHook` it dropped -- and now asserts what the registry hands the role, mutation-checked by wiring a participant through. `compose_judge` exploded a one-sentence `str` refusal into one refusal per character, because `str` satisfies `Sequence[str]` structurally; `charter_code.run_judge` coerced it where this composition used to live and the handling did not move with it. Two records corrected: the type-only `raven.contracts` -> `raven.agent` edge, the first in the tree and the price of moving `Verdict` and `Intake` off the paper, is now written beside the import contract it bends rather than left to be discovered; and the ledger entry for the 3,200 -> 3,410 bump is restored to the names and count it was written with. Not taken: reading `step.phase` at `research_flow/flow.py:1000` -- the gate chain's phase vocabulary and `StepView.phase` coincide on `after_iteration` only, and that call site wants `before_execute_tools`, which is the key `_phase` dispatches on. **6. What the paper was claiming and not doing.** Read back over the seam afterwards rather than over the diff. ``judge`` read as though a plugin's override were asked ahead of a dispatch's own rules; it is not, and cannot be as written, because the party that asks the verb is ``ToolRegistry.execute``, which is not the hook chain and holds no handle on this turn's participants. The merge order is real; nothing puts a plugin in the list. Wiring one through is a turn-scoped register this change does not add, and it would start refusing calls that run today, so it is named on the paper and pinned by a test instead. ``max_iterations`` had lost its docstring to the property inserted between the field and the string documenting it. No executable line moves in this commit: the module's AST, docstrings stripped, is identical to its parent's. CONTEXT.md's entry is rewritten to the end state, including the two kinds of role method, the two verbs that are exceptions, and the note that the Coding Conduct prompts are unrelated. One item from the same list is deliberately not here: a `planning.*` block on the dispatch parameters. It is not a contract closeout but a new model-facing field, and it needs the generator's schema and prompt to move with it. Contract papers: `participant.py` and `harness.py` are both factory-loop tier, so the contract-tier digest and `CONTRACTS_VERSION` do not move. `CONTRACTS_LINE_CEILING` 3,440 to 3,470 to 3,500 to 3,540 to 3,550 to 3,570, each bump with its own review paragraph in `tests/test_kernel_budget.py`. The chain was recomputed after rebasing onto #410, which added seven lines to `loop_hooks.py` inside the old headroom and left one commit here with one line of it; every "Measured at" figure in that file is the count at that commit. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [x] Refactor - [ ] Other ## Verification Gates, run at each of the seven commits: ``` uv run ruff check --no-cache raven evolver agents plugins-dist tests scripts uv run ruff format --check --no-cache raven evolver agents plugins-dist tests scripts uv run lint-imports uv run pytest -q npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs uv run python scripts/check_commit_messages.py origin/main..HEAD uv run python scripts/check_source_language.py origin/main..HEAD uv run python scripts/check_large_files.py origin/main..HEAD ``` Import contracts: 10 kept, 0 broken. The suite is green: the failures this machine shows are the same ones it shows on an unmodified `main` in the same worktree layout (missing browser and optional packages), and this branch adds none. Behaviour preservation. The oracle is the hook seam itself. `main` and this branch were run under the same scripted provider, outputs normalised for nonces and temp paths, then byte-compared. Four scenarios, all identical: - the five-plugin hook seam, every phase's decision and metadata per iteration (23,840 bytes) - a whole turn with no playbook: host turn, spawned sub-agent, tool calls and refusals - a playbook-on worker lane in process, including the charter refusals - six window-shrink scenarios This is the seam the change is about, and identical output across it is what "no behaviour change" means here -- it is not a claim about paths the four scenarios do not reach. Two claims had no assertion anywhere and now do. ``phase``, and the ``tools_ran`` derived from it, are what three of the five bundled plugins branch on -- ppt on its close, oncall on its counter, research on which gate runs -- and a mistyped phase string at one of the six call sites would have changed what they do with a green suite. The read-only test now also covers ``mode_overlay``, the one frozen field it skipped. All three assertions were mutation-checked: mislabelling a phase, deriving ``tools_ran`` from the wrong one, and dropping the freeze each turn exactly their own test red. New tests, each written against a claim the contract now makes rather than against the code: a participant cannot write through the `StepView` it is shown; an answer the host cannot read is silence, not a refusal; a participant that answers with a mapping it built itself is understood; every verb with a seat goes through its role rather than around it, asserted as a set so a new verb that bypasses its role fails; two seats in one chain each keep one addendum; two turns in flight keep their own participants; a bound role decides what a participant's verdict does; a plugin's own judgement speaks before the dispatch's, and a silent participant lets the next one speak. Real model, Sonnet 4.5 through OpenRouter, on this branch: a query generates a playbook, two sub-agents are instantiated and run, each writes its `summary.md` under its own fence, and both results are announced back to the originating session. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [x] User-facing docs or screenshots are updated when needed ## Risk For plugin authors: the six-phase `AgentHook` is untouched and a plugin shipping one keeps working. A plugin written against #469's `AgentConduct` needs the new names and, if it answered with `Verdict` or `Intake`, the builders instead -- the five bundled plugins are ported here and are the worked examples. One verb narrowed, and silently for a plugin that used the width. `salvage` was annotated `Any | None` and `compose_salvage` returned whatever was not None; it is now `str | None` and only a non-empty string is taken. A third-party plugin that salvages with a reply object rather than text stops being heard rather than raising -- which is the third invariant working as written, and worth knowing before upgrading. The narrowing is deliberate: `salvage` answers the text a turn sends when it produced none, and `Any` was not a contract. For anyone replacing a harness role: the four delegating methods are renamed and four more are added, so a replacement written against #469 needs them. `bind()` names the missing member at assembly rather than failing inside a turn. Rollback is a revert of the merge commit. Nothing persisted on disk changes shape. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues Follows #468 and #469. Generated with Claude Code (https://claude.com/claude-code) --------- Co-authored-by: yao pengfei <yaopengfei@shanda.com> Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…the roles (#468) ## Summary The agent loop already names four strategy roles (Memory, Planning, Capability, Action), but two judgements a turn makes still lived in the shell around them. This moves both onto the roles, in two dependent commits, with no behaviour change intended. **1. The charter reads through Memory and Action.** A dispatched sub-agent's playbook (`_meta["raven.playbook"]`) was read from four places inside the loop and the tool registry. `prompt` and `stopWhen` are now read by `DefaultMemory.assemble()`, which fills `task_brief` and `task_done_when` on the `TurnContext` the identity segment renders. `checks` and `code` are read by `DefaultAction.judge()`, which `ToolRegistry.execute` asks once per proposed call, the way it already asks the permission gate. `tools` stays in `withheld_names()` and the registry still decides what a refusal does, so a replaced role withholds nothing it could not already withhold. **2. Mid-turn window shrinking seats on Memory.** The five recoveries the loop ran inline go through one method, `MemoryModule.shrink(messages, pressure, state, model)`, with `WindowPressure`, `WindowState` and `ShrinkResult` as the carriers: - proactive compaction, once the last billed reading crosses the trigger - the standing image window - a provider's overflow error - a picture refused inside a tool result - pictures refused for size The retry mechanism stays in the shell. The six `continue` sites and the iteration rollback did not move, so a hook sees a retried call exactly as it saw the first. `WindowState` is a per-turn object the shell owns and hands to each call, because the role outlives the turn; `image_window` has no default, since 0 is a real value meaning "no picture stays". The pure pieces both sides need move to a new package, `raven/agent/window/`, which the import contracts treat as cargo that may not import the loop shell. `REASONING_EFFORT_LADDER` moves to `contracts/llm_provider.py` because the loop's empty-response descent and the window's head summary both read it and neither package may import the other. Contract papers: `CONTRACTS_VERSION` 26 to 28, `CONTRACTS_LINE_CEILING` 3,040 to 3,200, each bump with its own review paragraph in `tests/test_kernel_budget.py`. A follow-up PR carries the third step, which gives a sub-agent nine verbs in place of the six hook phases. It is kept separate because it touches every bundled plugin. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [x] Refactor - [ ] Other ## Verification Gates, run at each of the two commits: ``` uv run ruff check --no-cache raven evolver agents plugins-dist tests scripts uv run ruff format --check --no-cache raven evolver agents plugins-dist tests scripts uv run lint-imports uv run pytest -q npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs uv run python scripts/check_commit_messages.py origin/main..HEAD uv run python scripts/check_source_language.py origin/main..HEAD uv run python scripts/check_large_files.py origin/main..HEAD ``` Import contracts: 10 kept, 0 broken at both commits. The suite is green at both; the failures this machine shows are the same ones it shows on an unmodified `main` (missing browser and optional packages), and neither commit adds one. The loop's own e2e files, which the default selection leaves out, were run too (charter, playbook, truncation, direct chat, fallback chain, ACP stdio): no failure that `main` does not also show. Behaviour preservation. Each commit was run against a worktree of its own parent under the same scripted provider, outputs normalised for nonces and temp paths, then byte-compared. Four scenarios, eight comparisons, all identical: - a whole turn with no playbook: host turn, spawned sub-agent, tool calls and refusals - a playbook-on worker lane in process, including two charter refusals - six window-shrink scenarios: proactive compaction, the standing image window, overflow, a refused picture, oversized pictures, the effort ladder - the five-plugin hook seam, every phase's decision and metadata per iteration Mutation check on the second commit: breaking each of the five moves inside `DefaultMemory.shrink` turns exactly its own scenario red, so the comparison is not vacuous. Real model, Sonnet 4.5 through OpenRouter, at both commits: - playbook on, in-process lane: both workers receive the charter, write their files and finish - playbook on, ACP fork lane: the charter prompt crosses the process boundary, and a charter check blocks a `write_file` outside the fence - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk No user-visible behaviour change is intended, and the comparisons above are byte-identical on every path they cover. For anyone replacing a harness role: `MemoryModule` gains `shrink`, so a replacement written against the old protocol needs it. `DefaultMemory.bind()` names the missing member at assembly rather than failing inside a turn. Rollback is a revert of the merge commit. Nothing persisted on disk changes shape. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A --------- Co-authored-by: yao pengfei <yaopengfei@shanda.com> Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com> Co-authored-by: KT <74288668+0xKT@users.noreply.github.com>
…es (#469) ## Summary Follows #468 (merged as `869ad78a`), which moved the charter reads and the window's mid-turn shrinking onto the harness roles. This branch is rebased onto `main` and carries only its own three commits. A sub-agent's product logic was an `AgentHook` subclass: six phase methods over a fourteen-field context it could write, with the turn's own state kept in the context's free-form `metadata` dict. Surveyed before this change, the five bundled plugins had 71 phase methods, 42 of them doing real work, reading fourteen context fields and using six decision fields. Every one of them fits a small set of verbs. This PR makes those verbs the contract, in three commits. **1. A sub-agent writes a conduct instead of six hook phases.** New factory-loop-tier paper `contracts/agent_conduct.py`: `AgentConduct` with nine optional verbs (`intake`, `select_tools`, `advise`, `system_addendum`, `review`, `salvage`, `outbound`, `archive`, `observe`), answered against a read-only `StepView` of fourteen fields, with `Intake` and `Verdict` (Accept / Resample / End) as the answers and a `ConductFactory` because the host builds one conduct per turn. It sits beside `loop_hooks` rather than replacing it. The six phases remain the loop's *timing* contract: when the loop asks, in what order, what a refusal does. This is the *judgement* contract: given one step, what does this agent say about it. `ConductHook` seats one in the other, so a plugin that still ships a plain `AgentHook` keeps working and the composite's ordering, merging and rollback are untouched. All five bundled plugins are ported: design-engine, ppt-engine, code-flow, oncall-flow and research-flow. Research keeps its six-phase gate chain: the gates now subclass a plugin-private `Gate` and read a `GateCtx` the conduct builds from its `StepView` and its own turn-private facts, so six thousand lines of gate bodies did not move and its wrappers still see the cross-gate state they read. The turn's conduct and the system-message addendum it splices live in the turn's own `metadata` dict, not on the hook: one hook instance serves every turn a process runs, and a system turn can overlap a user one on the same chain. **2. The conducts seat on the roles.** `MemoryModule.intake`, `PlanningModule.advise`, `ActionModule.review` and `ActionModule.salvage` each take this turn's conducts, so a replaced role decides what a plugin's judgement does rather than the seat deciding it. The composition rules live in `raven/agent/harness/conducts.py`: the first non-Accept verdict wins, advice joins in order, the first salvage wins, an intake threads the text through each conduct. `run_turn` binds the harness for the turn the way it binds the model, and the seat falls back to the same rules when nothing is bound, which is how a plugin test drives a hook directly. Contract papers: the conduct paper is factory-loop tier, so the contract-tier digest and `CONTRACTS_VERSION` do not move with it. `CONTRACTS_LINE_CEILING` 3,200 to 3,420, each bump with its own review paragraph in `tests/test_kernel_budget.py`. **3. What the review found.** Eleven threads, all resolved in the third commit. The ones that changed behaviour rather than prose: `CodeFlowHook` kept its `rolls_back_iterations = False` (inheriting `True` from the new base had it hold every reply behind the draft gate instead of streaming); `StepView` now publishes deep-frozen nested values, since freezing only the outer tuple left a conduct able to write through to `ctx.messages`; a turn's conducts are collected at the role boundary, so a replaced role sees one batch of the turn's judgements rather than one call per seat; an addendum is located by its own text rather than a saved offset, which two conducts appending system text used to shift out from under each other; `Planning` and `Action` gained binders, so a replacement role missing a new member is named at assembly rather than swallowed by the composite's `except Exception`. `CONTEXT.md` carries the Agent Conduct term and the Agent Hook entry no longer describes what this PR replaces. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [x] Refactor - [ ] Other ## Verification Gates. CI runs these at the branch tip (17 pass, 1 skipped -- the skipped row is the disabled Claude Mention workflow). Locally, at each of the three commits: ``` uv run ruff check --no-cache raven evolver agents plugins-dist tests scripts uv run ruff format --check --no-cache raven evolver agents plugins-dist tests scripts uv run lint-imports uv run pytest -q npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs uv run python scripts/check_commit_messages.py origin/main..HEAD uv run python scripts/check_source_language.py origin/main..HEAD uv run python scripts/check_large_files.py origin/main..HEAD ``` Import contracts: 10 kept, 0 broken at each of the three commits. The suite is green at each, and no commit adds a failure that `main` does not already show on this machine (this checkout has the heavy extras uninstalled, so the local run carries failures `main` carries too -- CI is the authority, and it is green). The loop's own e2e files, which the default selection leaves out, were run too (charter, playbook, truncation, direct chat, fallback chain, ACP stdio): no new failure. Behaviour preservation. The oracle for this change is the hook seam itself: all five plugins driven through every phase of a multi-iteration turn under the same scripted provider, recording each phase's decision and the observers it files. Outputs normalised for nonces and temp paths, then byte-compared. Re-run after the review fixes at the branch tip `81e6f423` (all three commits), against `main` at `c65c2059`: | scenario | bytes | | --- | --- | | five-plugin hook seam | 23,840 == 23,840 | | a whole turn with no playbook (host turn, spawned sub-agent, tool calls and refusals) | 12,821 == 12,821 | | six window-shrink scenarios | 133,371 == 133,371 | These cover the tip, not each commit separately. The seam comparison is the one that matters most for the third commit: its first fix changed `rolls_back_iterations` for code-flow, which decides whether replies stream or are held, and the seam oracle records the decision at every phase, so an unintended change there shows as a diff rather than as silence. A unit test proves the seat is a real decision point rather than a pass-through: with a lenient Action role bound, a conduct's Resample is not applied, and Planning's advice and Memory's intake are what the loop receives. Real model, Sonnet 4.5 through OpenRouter, at the branch tip: - playbook on, in-process lane: two workers, each receives the charter (3 checks), writes its file, finishes; 26 recorded model calls - playbook on, ACP fork lane: the charter prompt crosses the process boundary (the fork answers ZEBRA where the task text says PONG, so the brief reached the forked model through Memory), and a charter check blocks `write_file` to `notes/hi.txt` against a `pathPrefix: out/` rule - Raven-Research, a fork running the full gate chain: `plain-first: escalated_tool`, then `sufficiency-gate: released on the pages at iteration 4 after 2 searches / 2 pages` - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk No user-visible behaviour change is intended, and the hook-seam comparison is byte-identical on every phase it covers. For plugin authors: the six-phase `AgentHook` is unchanged and a plugin shipping one keeps working. `ConductHook` is an adapter that seats a conduct in that same chain. For anyone replacing a harness role: the role protocols gain `intake`, `advise`, `review` and `salvage`, so a replacement written against the old protocol needs them. Rollback is a revert of the merge commit. Nothing persisted on disk changes shape. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A --------- Co-authored-by: yao pengfei <yaopengfei@shanda.com> Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…write (#495) ## Summary #469 gave a bundled sub-agent nine verbs in place of six hook phases. Reviewing it turned up a list of things the contract said but did not yet do, and this PR closes that list. No behaviour change is intended: the point is that the same seam becomes something a role can replace and something a generated judgement can enter. **1. The roles are named apart from the verbs, and `observe` is gone.** `ActionModule.review` and the participant's `review` were two different things a line apart. The four delegating role methods now say what they do -- they ask -- and the verbs keep their own names, so `ask_review` asks `review`. `observe` is removed: a verb with no return value decides nothing and can only keep state a participant is forbidden to keep. `outbound` says on its own docstring that it is the one verb with no role seat, and `StepView` gains `phase`, so a verb asked at two moments reads which one it is rather than inferring it from whichever other field happens to be set (`tools_ran` becomes a property derived from it, so the two cannot disagree). **2. The three verbs the seat was applying itself get a seat.** `system_addendum`, `archive` and `select_tools` were rendered straight onto the hook decision, so each was the one judgement in the set with nowhere to compose two participants, nowhere to vet what came back, and nothing a replacement role could decide. They join the other four: `MemoryModule.ask_system_addendum` / `ask_archive` and `CapabilityModule.ask_select_tools`. The seat is not the point where narrowing takes effect -- `select` still is, and the registry still adjudicates every call -- so a participant narrows after the product has spoken and never instead of it. **3. A participant answers with plain data.** `review`, `intake` and `system_addendum` returned host classes (`Verdict`, `Intake`) and `salvage` returned `Any`. `Accept`, `Resample`, `End` and `Intake` are now builders returning plain mappings, the host reads one back with `read_verdict` / `read_intake`, and an answer it cannot read is silence rather than a refusal. That is what makes the third invariant real and what lets a judgement compiled from source answer the same verbs a plugin does -- it can build a `dict`; it cannot build a host class. **4. A dispatch's judgements are a participant.** `judge` was the one verb a role answered by reading the dispatch's own `checks` and `code` for itself. A Charter now enters as a participant like any other, `ActionModule.ask_judge` takes the list, and the merge is a veto so the first refusal decides. The participant list becomes the only route an external judgement takes, which is the thing step four needs: a judgement generated for one dispatch joins the same list without a second way in. **5. The rename.** `AgentConduct` is `AgentParticipant`, `contracts/agent_conduct.py` is `contracts/participant.py`, `agent/hook/conduct.py` is `agent/hook/participant.py`, and `agent/harness/conducts.py` is `agent/harness/participants.py`. "Conduct" was already taken in this tree -- raven-code's Coding Conduct prompts are a different thing -- and a reader hitting both had no way to tell them apart. The rename was done symbol by symbol, not by search and replace, for exactly that reason. **7. Review (LivXue, at 3a1bf07).** The `judge` disclosure had reached two of its four homes; the other two are `ActionModule.ask_judge` on the paper and `DefaultAction.ask_judge`, and they survived because they phrase the claim without naming any symbol the earlier fix touched. The test meant to pin that claim could not fail for the reason it was written -- it asserted the refusal rather than the argument, and built a `ParticipantHook` it dropped -- and now asserts what the registry hands the role, mutation-checked by wiring a participant through. `compose_judge` exploded a one-sentence `str` refusal into one refusal per character, because `str` satisfies `Sequence[str]` structurally; `charter_code.run_judge` coerced it where this composition used to live and the handling did not move with it. Two records corrected: the type-only `raven.contracts` -> `raven.agent` edge, the first in the tree and the price of moving `Verdict` and `Intake` off the paper, is now written beside the import contract it bends rather than left to be discovered; and the ledger entry for the 3,200 -> 3,410 bump is restored to the names and count it was written with. Not taken: reading `step.phase` at `research_flow/flow.py:1000` -- the gate chain's phase vocabulary and `StepView.phase` coincide on `after_iteration` only, and that call site wants `before_execute_tools`, which is the key `_phase` dispatches on. **6. What the paper was claiming and not doing.** Read back over the seam afterwards rather than over the diff. ``judge`` read as though a plugin's override were asked ahead of a dispatch's own rules; it is not, and cannot be as written, because the party that asks the verb is ``ToolRegistry.execute``, which is not the hook chain and holds no handle on this turn's participants. The merge order is real; nothing puts a plugin in the list. Wiring one through is a turn-scoped register this change does not add, and it would start refusing calls that run today, so it is named on the paper and pinned by a test instead. ``max_iterations`` had lost its docstring to the property inserted between the field and the string documenting it. No executable line moves in this commit: the module's AST, docstrings stripped, is identical to its parent's. CONTEXT.md's entry is rewritten to the end state, including the two kinds of role method, the two verbs that are exceptions, and the note that the Coding Conduct prompts are unrelated. One item from the same list is deliberately not here: a `planning.*` block on the dispatch parameters. It is not a contract closeout but a new model-facing field, and it needs the generator's schema and prompt to move with it. Contract papers: `participant.py` and `harness.py` are both factory-loop tier, so the contract-tier digest and `CONTRACTS_VERSION` do not move. `CONTRACTS_LINE_CEILING` 3,440 to 3,470 to 3,500 to 3,540 to 3,550 to 3,570, each bump with its own review paragraph in `tests/test_kernel_budget.py`. The chain was recomputed after rebasing onto #410, which added seven lines to `loop_hooks.py` inside the old headroom and left one commit here with one line of it; every "Measured at" figure in that file is the count at that commit. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [x] Refactor - [ ] Other ## Verification Gates, run at each of the seven commits: ``` uv run ruff check --no-cache raven evolver agents plugins-dist tests scripts uv run ruff format --check --no-cache raven evolver agents plugins-dist tests scripts uv run lint-imports uv run pytest -q npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs uv run python scripts/check_commit_messages.py origin/main..HEAD uv run python scripts/check_source_language.py origin/main..HEAD uv run python scripts/check_large_files.py origin/main..HEAD ``` Import contracts: 10 kept, 0 broken. The suite is green: the failures this machine shows are the same ones it shows on an unmodified `main` in the same worktree layout (missing browser and optional packages), and this branch adds none. Behaviour preservation. The oracle is the hook seam itself. `main` and this branch were run under the same scripted provider, outputs normalised for nonces and temp paths, then byte-compared. Four scenarios, all identical: - the five-plugin hook seam, every phase's decision and metadata per iteration (23,840 bytes) - a whole turn with no playbook: host turn, spawned sub-agent, tool calls and refusals - a playbook-on worker lane in process, including the charter refusals - six window-shrink scenarios This is the seam the change is about, and identical output across it is what "no behaviour change" means here -- it is not a claim about paths the four scenarios do not reach. Two claims had no assertion anywhere and now do. ``phase``, and the ``tools_ran`` derived from it, are what three of the five bundled plugins branch on -- ppt on its close, oncall on its counter, research on which gate runs -- and a mistyped phase string at one of the six call sites would have changed what they do with a green suite. The read-only test now also covers ``mode_overlay``, the one frozen field it skipped. All three assertions were mutation-checked: mislabelling a phase, deriving ``tools_ran`` from the wrong one, and dropping the freeze each turn exactly their own test red. New tests, each written against a claim the contract now makes rather than against the code: a participant cannot write through the `StepView` it is shown; an answer the host cannot read is silence, not a refusal; a participant that answers with a mapping it built itself is understood; every verb with a seat goes through its role rather than around it, asserted as a set so a new verb that bypasses its role fails; two seats in one chain each keep one addendum; two turns in flight keep their own participants; a bound role decides what a participant's verdict does; a plugin's own judgement speaks before the dispatch's, and a silent participant lets the next one speak. Real model, Sonnet 4.5 through OpenRouter, on this branch: a query generates a playbook, two sub-agents are instantiated and run, each writes its `summary.md` under its own fence, and both results are announced back to the originating session. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [x] User-facing docs or screenshots are updated when needed ## Risk For plugin authors: the six-phase `AgentHook` is untouched and a plugin shipping one keeps working. A plugin written against #469's `AgentConduct` needs the new names and, if it answered with `Verdict` or `Intake`, the builders instead -- the five bundled plugins are ported here and are the worked examples. One verb narrowed, and silently for a plugin that used the width. `salvage` was annotated `Any | None` and `compose_salvage` returned whatever was not None; it is now `str | None` and only a non-empty string is taken. A third-party plugin that salvages with a reply object rather than text stops being heard rather than raising -- which is the third invariant working as written, and worth knowing before upgrading. The narrowing is deliberate: `salvage` answers the text a turn sends when it produced none, and `Any` was not a contract. For anyone replacing a harness role: the four delegating methods are renamed and four more are added, so a replacement written against #469 needs them. `bind()` names the missing member at assembly rather than failing inside a turn. Rollback is a revert of the merge commit. Nothing persisted on disk changes shape. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues Follows #468 and #469. Generated with Claude Code (https://claude.com/claude-code) --------- Co-authored-by: yao pengfei <yaopengfei@shanda.com> Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Summary
The agent loop already names four strategy roles (Memory, Planning, Capability, Action), but two judgements a turn makes still lived in the shell around them. This moves both onto the roles, in two dependent commits, with no behaviour change intended.
1. The charter reads through Memory and Action. A dispatched sub-agent's playbook (
_meta["raven.playbook"]) was read from four places inside the loop and the tool registry.promptandstopWhenare now read byDefaultMemory.assemble(), which fillstask_briefandtask_done_whenon theTurnContextthe identity segment renders.checksandcodeare read byDefaultAction.judge(), whichToolRegistry.executeasks once per proposed call, the way it already asks the permission gate.toolsstays inwithheld_names()and the registry still decides what a refusal does, so a replaced role withholds nothing it could not already withhold.2. Mid-turn window shrinking seats on Memory. The five recoveries the loop ran inline go through one method,
MemoryModule.shrink(messages, pressure, state, model), withWindowPressure,WindowStateandShrinkResultas the carriers:The retry mechanism stays in the shell. The six
continuesites and the iteration rollback did not move, so a hook sees a retried call exactly as it saw the first.WindowStateis a per-turn object the shell owns and hands to each call, because the role outlives the turn;image_windowhas no default, since 0 is a real value meaning "no picture stays".The pure pieces both sides need move to a new package,
raven/agent/window/, which the import contracts treat as cargo that may not import the loop shell.REASONING_EFFORT_LADDERmoves tocontracts/llm_provider.pybecause the loop's empty-response descent and the window's head summary both read it and neither package may import the other.Contract papers:
CONTRACTS_VERSION26 to 28,CONTRACTS_LINE_CEILING3,040 to 3,200, each bump with its own review paragraph intests/test_kernel_budget.py.A follow-up PR carries the third step, which gives a sub-agent nine verbs in place of the six hook phases. It is kept separate because it touches every bundled plugin.
Type
Verification
Gates, run at each of the two commits:
Import contracts: 10 kept, 0 broken at both commits. The suite is green at both; the failures this machine shows are the same ones it shows on an unmodified
main(missing browser and optional packages), and neither commit adds one. The loop's own e2e files, which the default selection leaves out, were run too (charter, playbook, truncation, direct chat, fallback chain, ACP stdio): no failure thatmaindoes not also show.Behaviour preservation. Each commit was run against a worktree of its own parent under the same scripted provider, outputs normalised for nonces and temp paths, then byte-compared. Four scenarios, eight comparisons, all identical:
Mutation check on the second commit: breaking each of the five moves inside
DefaultMemory.shrinkturns exactly its own scenario red, so the comparison is not vacuous.Real model, Sonnet 4.5 through OpenRouter, at both commits:
playbook on, in-process lane: both workers receive the charter, write their files and finish
playbook on, ACP fork lane: the charter prompt crosses the process boundary, and a charter check blocks a
write_fileoutside the fenceRelevant tests pass locally
Relevant lint / type checks pass locally
User-facing docs or screenshots are updated when needed
Risk
No user-visible behaviour change is intended, and the comparisons above are byte-identical on every path they cover.
For anyone replacing a harness role:
MemoryModulegainsshrink, so a replacement written against the old protocol needs it.DefaultMemory.bind()names the missing member at assembly rather than failing inside a turn.Rollback is a revert of the merge commit. Nothing persisted on disk changes shape.
Related Issues
N/A