Skip to content

refactor(*): let a sub-agent write a conduct instead of six hook phases - #469

Merged
ypflll merged 6 commits into
mainfrom
refactor/agent_conduct_seam
Sep 18, 2026
Merged

ypflll merged 6 commits into
mainfrom
refactor/agent_conduct_seam

Conversation

@ypflll

@ypflll ypflll commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

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
  • 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

  • Relevant 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 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.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

@ypflll
ypflll requested review from 0xKT and LivXue September 17, 2026 06:30
@ypflll ypflll changed the title Refactor/agent conduct seam refactor(*): let a sub-agent write a conduct instead of six hook phases Sep 17, 2026

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the conduct seam has five concrete contract and behavior regressions to resolve.

I posted five inline blockers: Raven-Code loses incremental reply streaming; StepView aliases mutable loop data; harness roles receive singleton conducts rather than the promised turn set; multiple system addenda accumulate; and the new canonical domain term is missing from CONTEXT.md.

Verification performed:

  • Focused changed-area suite: 294 passed.
  • Full uv run pytest: 22880 passed, 130 skipped, 1 failed. The sole failure was the untouched tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare; this PR changes neither that test, the theme implementation, dependency declarations, nor the lockfile, and the PR's unit CI shards are green.
  • scripts/check_source_language.py and git diff --check: passed.
  • Direct reproductions confirmed the mutable StepView, singleton role dispatch, and two-addendum accumulation.

I covered the repository rules and canonical vocabulary, the complete target diff, the two commits and pre-change implementations, loop/composite/plugin callers, harness and plugin backward-compatibility surfaces, test changes for weakening, and the hook/conduct/role architecture.

Comment thread raven/agent/hook/conduct.py Resolved
Comment thread raven/agent/hook/conduct.py Resolved
Comment thread raven/agent/hook/conduct.py Resolved
Comment thread raven/contracts/agent_conduct.py Resolved

@LivXue LivXue left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: verified against 2f827373 vs its base f69e6cf7 (the #468 branch)

Method note, because it changes how this review should be weighed. My review of
#468 ran a multi-angle sweep that converged 24 candidates down to 15 verified findings.
That tooling was unavailable for this PR (org spend limit), so what follows is a manual
read. It is narrower in coverage than the #468 review, not deeper. Treat an absence of
findings in an area below as "not swept", not as "clean".

Taken on its own, this is the better-built of the two. The timing/judgement split is the
right seam, the AgentConduct paper is precise about what a conduct may and may not
reach, all five bundled plugins are actually ported rather than shimmed, and
research-flow keeping six thousand lines of gate bodies behind a Gate/GateCtx
adapter is the correct trade. Ten new tests, and -- unlike #468 -- all ten are in
tests/, where the default collection actually finds them.

Three things about the stack, before the findings:

1. This PR carries every unfixed finding from #468. Its base is that branch, so
those two commits are in this PR's history, including the blocking one
(registry.py:407: the charter's checks/code enforcement became an opt-in
constructor kwarg, and segments/curator.py:357 builds a bare ToolRegistry() that
does dispatch inside a live charter scope). #468 has to land first, and land fixed.

2. mergeStateStatus: CLEAN here means clean against the #468 branch, not main.
Worth stating because the label reads as merge-ready.

3. The retarget will not be automatic. The description says the base "will retarget
to main once it merges". Because the repo squash-merges, #468's two commits will not
be ancestors of main, so GitHub's retarget leaves this branch's diff re-containing all
of #468's work as if it were new. This needs
git rebase --onto main refactor/charter_and_window_on_memory after #468 lands, not
just a base change.

Findings. One I would call blocking; the rest are graded inline.

B1. Three new required Protocol members have no assembly-time validation, and the
failure is silent and permissive.
MemoryModule gains intake and its bind()
message was correctly updated. PlanningModule gains advise and ActionModule gains
review and salvage -- and neither has a binder at all. Detail at
harness/__init__.py:69.

Then, in descending order: hook/conduct.py:221 (an addendum that fails to strip is
never re-found, and the next iteration splices another copy),
hook/conduct.py:205 (select_tools states an invariant nothing enforces),
harness/conducts.py:51 (an accept verdict keeps or loses its fields depending on how
many plugins are loaded), hook/conduct.py:130 (Verdict.reason is required and has no
reader), and contracts/agent_conduct.py:173 (note() writes through self.__dict__,
and the host calls drain_trail() unconditionally).

None of these argues against the design. The seam is right; these are places where a
newly written contract does not yet hold itself to what it says.

Comment thread raven/agent/harness/__init__.py Resolved
Comment thread raven/agent/hook/conduct.py Resolved
Comment thread raven/agent/hook/conduct.py Resolved
Comment thread raven/agent/harness/conducts.py Resolved
Comment thread raven/agent/hook/conduct.py Resolved
Comment thread raven/contracts/agent_conduct.py Resolved
@ypflll
ypflll force-pushed the refactor/agent_conduct_seam branch 2 times, most recently from 2f82737 to 00f08a0 Compare September 17, 2026 13:03

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the five previously reported conduct-seam defects still need fixes before merge.

At this head, all five existing blocking threads still reproduce. In particular, the new harness-role indirection continues to call each role with singleton conduct batches, so it does not settle the open role-composition finding. I found no additional defect worth raising and did not duplicate the other reviewer's open items.

Coverage: I reviewed the current diff against the updated target, the range-diff and commit history, affected callers and harness binding, backward-compatibility/admission surfaces, test changes for weakening, the stated architecture constraints, and the repository rules in AGENTS.md/CLAUDE.md plus CONTEXT-MAP.md/CONTEXT.md. The Agent Conduct context-map omission also remains.

Verification: the changed-area suite passed (328 tests). The full uv run pytest -x run stopped at the same untouched CLI-theme failure as the prior revision, after 11,232 passed and 13 skipped; all GitHub checks are green. git diff --check and the source-language check passed. Targeted reproductions reconfirmed the four behavioral findings.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

This revision settles all five findings I had open. I replied on and resolved each thread; no new candidate survived refutation. At this fourth review round, I found no defect introduced by the fix that is both reachable in ordinary use and leaves an operator without a working way out. I did not re-grade the other reviewer's threads.

Coverage: the full current diff and the new range-diff/history; AGENTS.md and its identical CLAUDE.md, CONTEXT-MAP.md and CONTEXT.md; affected callers and harness binding; backward compatibility; test changes for weakening; and the conduct/hook/harness architecture constraints.

Verification: the changed-area suite passed (333 tests). The full uv run pytest -x run stopped on the same untouched CLI-theme failure as the prior two revisions after 11,241 passed and 13 skipped. Ruff check and format, git diff --check, and the source-language gate passed. GitHub had not attached check runs to this head at review time; its dirty mergeability value disagreed with both the commit graph (the base is an ancestor) and a clean local merge-tree, so I did not treat that metadata as a code finding.

@ypflll
ypflll force-pushed the refactor/agent_conduct_seam branch from 7a46bf6 to 7ce9501 Compare September 18, 2026 02:20

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

This is a rebase-only revision. The range-diff shows the three PR commits are patch-equivalent to the clean prior head except for adopting the target branch's canonical Charter terminology in harness prose. No settled finding regressed, and I found no new issue.

Coverage: the current full diff against the refreshed target, range-diff and history, AGENTS.md/CLAUDE.md and CONTEXT files, affected callers, backward compatibility, test changes for weakening, and the conduct/hook/harness architecture constraints. All five threads I opened remain resolved.

Verification: the relevant suite passed (410 tests); Ruff check and format, git diff --check, and the source-language gate passed. All completed GitHub checks are green; the unit shards and Windows self-upgrade were still running when reviewed.

Base automatically changed from refactor/charter_and_window_on_memory to main September 18, 2026 02:52
yao pengfei and others added 3 commits September 18, 2026 02:57
A sub-agent's product logic used to be 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: 71 phase methods across five plugins, 42 of them real work, reading
fourteen fields and using six decision fields, every one fitting one of a
small set of verbs. This paper and adapter make those verbs the contract.

- raven/contracts/agent_conduct.py (factory-loop tier): AgentConduct, nine
  optional verbs -- intake, select_tools, advise, system_addendum, review,
  salvage, outbound, archive, observe -- answered against a StepView of fourteen
  read-only fields; Intake and Verdict (Accept / Resample / End) as the
  answers; ConductFactory because the host builds one conduct per turn, so a
  turn's own state is an attribute that dies with it; note()/drain_trail()
  for the diagnostic trail.
- raven/agent/hook/conduct.py: ConductHook seats a conduct in the hook
  chain. Each phase builds the step, asks the verbs that belong to it, and
  renders the answers as the HookDecision the composite already merges. The
  turn is keyed on the metadata dict the loop seats per turn. It strips and
  splices a conduct's system addendum, merges what archive files into the
  turn's observers, and declares rolls_back_iterations once for every
  conduct.
- The five plugins now write conducts. design and ppt: one conduct each.
  code: repository instructions become a system_addendum. oncall: the three
  judgements become one conduct; the context and accounting axes stay hooks.
  research: the gates keep their six phases and the composite its merge
  rules, but they are Gate/GateCtx (plugin-private) rather than AgentHook,
  and the conduct builds every GateCtx from the step and its own turn facts,
  so the gates read what they always read and can no longer write the loop's
  context; the facade splits into a process-level ResearchFlow and a per-turn
  ResearchFlowConduct behind the same constructor.
- Nothing on the loop's metadata dict is written by a plugin any more. The
  AgentHook subclasses that remain are mechanism: fail-closed sentinels,
  oncall's session binding and accounting, and the research facade's shell.

The loop is untouched. Verified at the seam it changed: every plugin's hook
driven through one fixed phase script on the base commit and on this tree,
every decision and stamped observer byte for byte equal; design also through
the real loop. A real-model turn on the discovered Raven-Research fork shows
plain-first escalating (a rollback before the tools ran) and the sufficiency
gate releasing (a note after them), the three-section report and its source.
Full suite green; the contracts ceiling moved to 3,390 with its review.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Second half of the conduct seam. The four verbs a conduct answers that
belong to a role rather than to the seat now go through the role:
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. The composition rules live in
raven/agent/harness/conducts.py (first non-Accept verdict wins, advice
joins in order, first salvage wins, intake threads the text) and the
default roles call them.

run_turn binds the harness for the turn (bind_harness / current_harness,
a ContextVar next to the model binding); ConductHook asks the bound
roles and falls back to the same composition rules when no harness is
bound, which is how plugin tests drive the hook directly. Phase timing,
ordering and rollback stay in the composite hook, untouched.

Verified: full suite 22630 passed; a new adapter test shows a lenient
Action overruling a conduct's Resample; the five-plugin hook-seam A/B
and the playbook-off whole-turn A/B are byte-identical to main; real
model runs of the in-process playbook lane, the ACP fork lane and
Raven-Research behave as before. Contracts ceiling 3,390 -> 3,420 with
its review paragraph.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Review of the conduct seam found eleven things; this is all of them.

Two seats lost incremental streaming. ConductHook declared
rolls_back_iterations on the class, so code-flow and oncall-flow inherited
True where their own hooks had said nothing. The loop reads that flag off
every hook it holds and withholds the reply behind a draft gate if any says
yes, so an ordinary Raven-Code answer stopped streaming. The declaration is
a seat argument now, and the two conducts that never resample say so.

A StepView row is a dict that refuses to be written. The tuple froze the
sequence and left every message inside it the loop's own object: a conduct
assigning step.transcript[0]["content"] rewrote the transcript the loop
prompts with, through a field the paper calls read-only. A dict subclass
rather than a proxy because every reader on the far side of this seam asks
isinstance(m, dict), the loop's helpers included.

The system addendum is found by its text, not by where it went. With two
seats appending, the first seat's strip shifted the second seat's offset, so
the second found nothing and spliced a second copy; three iterations left
base\n\nB\n\nA\n\nB. The bookkeeping is also cleared only once the text is
actually gone, so a strip that cannot find its target no longer forgets the
copy it left behind.

Also:

- Planning is admitted through bind() like Memory and Action, so a role
  missing advise is named at assembly rather than logged under the plugin's
  name by the composite's catch.
- An accepted verdict keeps the fields it was written with whether one
  conduct answered or several; the two-conduct path used to rebuild a bare
  Accept.
- Resample's reason reaches the loop's notes. It is a required positional
  argument that nothing read, which is why four of five call sites wrote the
  same sentence twice.
- The trail is asked for with getattr: a conduct that implements the verbs
  without inheriting AgentConduct is a conduct, and raising there would have
  dropped the answer it just gave.
- select_tools is documented as what it is. "Never wider than offered" was a
  new claim this series invented and the research flow already breaks it by
  contributing its escalation tool; handing a name back is not granting it,
  since the registry still adjudicates every call.
- contracts/harness.py says what the seat passes: the conducts it speaks for,
  with composing across plugins left where it has always been, in the hook
  composite.
- CONTEXT.md defines Agent Conduct and stops saying an agent steers the loop
  with its own hook.

Five new tests assert the claims rather than the code: a conduct cannot write
through the step, two seats each keep one addendum, a contributed tool
survives, a conduct that inherits nothing is still heard, and a reason
reaches the notes.

Verified: full suite 22912 passed, no failure the base branch does not also
show; ruff, format and import contracts clean; the four scripted A/Bs against
the pre-fix head are byte-identical.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
@ypflll
ypflll force-pushed the refactor/agent_conduct_seam branch from 7ce9501 to 62c24d3 Compare September 18, 2026 03:34

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

The former target has merged into main, and this revision retargets/merges main without changing the reviewed conduct implementation. The range-diff keeps the three PR commits equivalent; only the contract-budget measurements and ceiling were refreshed for the larger base, and their test passes. The merge commit's README conflict resolution does not appear in the PR diff against main. No settled finding regressed and I found no new issue.

Coverage: the full current diff against refreshed github/main, range-diff and history, merge conflict resolution, AGENTS.md/CLAUDE.md and CONTEXT files, affected callers, backward compatibility, test changes for weakening, and the conduct/hook/harness architecture constraints. All five threads I opened remain resolved.

Verification: the relevant suite passed (410 tests); Ruff check and format, git diff --check, and the source-language gate passed. All completed GitHub checks are green; unit shards and TUI were still running when reviewed.

@ypflll
ypflll requested a review from LivXue September 18, 2026 03:43

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

This revision only merges current main. The range-diff shows all three PR commits are exactly patch-equivalent to the prior clean head, and the merge has no manual conflict-resolution hunks. No settled finding regressed and I found no new issue.

Coverage: the full current diff against refreshed github/main, range-diff and history, merge result, AGENTS.md/CLAUDE.md and CONTEXT files, affected callers, backward compatibility, test changes for weakening, and the conduct/hook/harness architecture constraints. All five threads I opened remain resolved.

Verification: the relevant suite passed (410 tests); Ruff check and format, git diff --check, and the source-language gate passed. All completed GitHub checks are green; unit shards were still running when reviewed.

@LivXue

LivXue commented Sep 18, 2026

Copy link
Copy Markdown
Member

The description has not moved since the retarget and the rebase, and this repo squash-merges with squash_merge_commit_message=PR_BODY -- so whatever stands here becomes the permanent commit body on main. Three groups, in descending order of how much they matter.

1. The Verification section does not cover the commit that carries the review fixes.

The branch now has three commits of its own -- 3e426664d, b2c12dc51, 62c24d387 -- but the description was written for two, and still says so in five places: "This PR makes those verbs the contract, in two commits" (line 5), "Gates, run at each of the two commits" (line 30), "10 kept, 0 broken at both commits. The suite is green at both" (line 43), "Both commits were run against a worktree of their own parent ... identical at both commits" (line 45), and "Real model, Sonnet 4.5 through OpenRouter, at both commits" (line 49).

The third commit is fix(agent): a seat declares its own rollback, and a step is read-only, which is where the review fixes landed. So as written, the byte-compare oracle and the real-model runs are claimed for everything except the commit a reader would most want them claimed for.

If the gates and the two behaviour-preservation runs were re-done at the third commit, this is a word change (two -> three, both -> all three). If they were not, saying so plainly is better than a number that reads as covering it. Your call which -- I cannot tell from outside.

2. The opening paragraph is now false in all three of its clauses.

Stacked on #468, which moves the charter reads and the window's mid-turn shrinking onto the harness roles. Review that one first; this PR's base is its branch and will retarget to main once it merges.

#468 merged at 02:52:48Z as 869ad78a; the retarget fired two seconds later; the rebase landed at 05:43Z. Verified on the current head: 869ad78a is an ancestor and 1b9db755 is not, and the diff is back to 42 files.

Something like:

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.

3. Four <base> placeholders were never substituted (lines 37-40):

npx commitlint --from <base> --to HEAD --config commitlint.config.cjs
uv run python scripts/check_commit_messages.py <base>..HEAD
uv run python scripts/check_source_language.py <base>..HEAD
uv run python scripts/check_large_files.py <base>..HEAD

origin/main is the value now, and is what #468's description used.


Nothing here is about the code. CI is green on main (17 pass, 1 skipped -- the skipped row is the disabled Claude Mention workflow), the rebase is clean, all eleven review threads are resolved, and I re-verified on fb10260c that the six fixes from my earlier review are all still present after the two rewrites. The only merge-gate item left is an approving review.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

This head only merges current main. The range-diff shows all three PR commits are exactly patch-equivalent to the prior clean head, and the merge contains no manual conflict-resolution hunks. No settled finding regressed and I found no new issue. The existing top-level PR-description note does not change the code stance or ask this reviewer for a reply.

Coverage: the full current diff against refreshed github/main, range-diff and history, merge result, AGENTS.md/CLAUDE.md and CONTEXT files, affected callers, backward compatibility, test changes for weakening, and the conduct/hook/harness architecture constraints. All five threads I opened remain resolved.

Verification: the focused suite passed (333 tests); Ruff check and format, git diff --check, source-language, and large-file gates passed. All four GitHub unit shards and every other completed check are green; the aggregate coverage gate was still pending when reviewed.

@ypflll

ypflll commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

All three are fair, and the answer to the open question in (1) is yes -- re-run, but at the tip rather than per-commit, which is worth stating precisely rather than as "all three".

What was re-run, and where. Baseline main at c65c2059, subject the branch tip 81e6f423 (so all three commits plus the three merge commits). Same scripted provider on both sides, outputs normalised for nonces and temp paths, byte-compared:

scenario bytes
five-plugin hook seam, every phase's decision and the observers it files 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

Real model, Sonnet 4.5 through OpenRouter, at the same 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: plain-first: escalated_tool, then sufficiency-gate: released on the pages at iteration 4 after 2 searches / 2 pages

The gates are covered at the tip by CI rather than by me: 17 pass, 1 skipped.

So the accurate claim is "re-run at the tip, which includes the third commit", not "at each of the three commits". The seam comparison is the one that matters most for that commit, because the 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 would show as a diff rather than as silence.

(2) and (3) accepted as written -- the opening paragraph and the four <base> placeholders. Rewriting the description now with your suggested opening, origin/main substituted, and the Verification section saying what the table above says.

@LivXue LivXue left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 81e6f423.

What I verified myself, at this head:

  • All six findings from my review of 2026-09-17 are settled: bind_action/bind_planning are applied at the construction site; _strip_addendum finds its text by value and clears the bookkeeping only after the removal; Verdict.reason folds into the notes with de-duplication; compose_review keeps the first accepted verdict's fields via replace; drain_trail is asked for with getattr. The sixth, select_tools, was settled by correcting the contract rather than the code -- I had the direction wrong, and the research flow's escalation tool is why.
  • Those six survive the two history rewrites: re-checked after the rebase onto main and again at this head.
  • The rebase is clean: 869ad78a (the #468 squash) is an ancestor, 1b9db755 is not, and the diff is back to its own 42 files.
  • Eleven review threads, zero unresolved. CI at this head: 17 pass, 1 skipped -- the skipped row is the disabled Claude Mention workflow, not a check that passed.
  • The description is accurate as of this head, which matters because it becomes the commit body on main under squash_merge_commit_message=PR_BODY. It now separates the gates (run per-commit locally, and at the tip by CI) from the byte-compare (tip only, stated as such), and it names the local checkout's missing extras rather than claiming a green that the machine cannot produce.

What I am relying on others for. My review of this PR was a manual read, narrower in coverage than the multi-angle sweep I ran on #468 -- I said so in the review and it still holds. The two findings with the most user-visible weight were not mine: the streaming regression (ConductHook declaring rolls_back_iterations on the class, so code-flow and oncall-flow inherited True and an ordinary Raven-Code answer stopped streaming) and the mutable StepView rows (the tuple froze the sequence, not the dicts inside it, so a conduct could rewrite the transcript the loop prompts with through a field the paper calls read-only). Both came from @gloryfromca's reproductions, and both are fixed in 62c24d387.

Not covered by this approval. I did not independently re-run the byte-compare oracle or the real-model lanes; those numbers are the author's, stated with their baselines (main at c65c2059, subject 81e6f423) and reproducible from the description.

One practical note: dismiss_stale_reviews_on_push is on for this repo, so a further main sync push will drop this approval. main has moved to ef6afc57 since this head, and the ruleset does not require the branch to be current -- so this can merge as it stands, and re-syncing first would cost the approval.

@ypflll
ypflll merged commit 6d8c363 into main Sep 18, 2026
22 checks passed
@ypflll
ypflll deleted the refactor/agent_conduct_seam branch September 18, 2026 06:37
ypflll added a commit that referenced this pull request Sep 20, 2026
…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>
LivXue pushed a commit that referenced this pull request Oct 12, 2026
…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>
LivXue pushed a commit that referenced this pull request Oct 12, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants