Reaktor Conductor
:::info Current implementation update — 13 September 2026 The Agent Workspace design and delivery plan updates the implementation status below: shared native continuation and local context exist, and Compare/Council now reach the shared workspace and desktop. It also specifies interactive harness integration and the Pencil pane revamp. Treat older missing-feature statements in this original review as historical where that update supersedes them. :::
reaktor-conductor is the outbound half of Reaktor's agent story. Reaktor drives external harnesses rather than being driven by them: you write a prompt, choose who works on it and how they collaborate, and Claude Code and Codex run as ordinary supervised subprocesses under one conversation that Reaktor owns.
The inbound half is a different module and a different direction. In the control plane an agent calls Reaktor, reads a snapshot, and proposes a validated patch. Here Reaktor calls the agent. Both are needed and neither replaces the other.
1. What it does
Three things the operator controls, and one the framework insists on.
You define the agents. An AgentSpec is data: an identity, instructions, which harness runs it, which model, what it may touch, and its budget. The built-in roster is a sample you replace, not a set of roles baked into the framework. Identity belongs to the spec rather than the runtime, so moving an agent from Codex to Claude Code changes one field and leaves its history intact.
You define how they collaborate. A Protocol is also data. Ask one agent. Ask everyone at once. Run a council, where proposals are made independently before anyone sees another answer. Or write a pipeline of named stages, which is any ordered topology you like. Protocols serialize to JSON, so a topology is a file you keep and share.
You can decline to choose. Protocol.Planned hands the task to a planner agent, which returns the team and the topology it wants, including agents that did not exist a moment earlier. Reaktor then runs what the planner designed.
Reaktor owns the conversation. Provider sessions are a cache. The canonical record is a Reaktor document, which is what lets a Codex proposal and a Claude critique sit in one history that outlives either tool.
2. Normal harnesses, not a special mode
The runtimes shell out to the CLIs exactly as you would run them:
claude -p "<prompt>" --output-format stream-json --verbose
codex exec --json --skip-git-repo-check -C <dir> -s read-only "<prompt>"
Nothing is patched and no wrapper is installed. Each harness keeps its own tools, subagents, skills, hooks, permission handling and MCP servers, and by default it reads your own configuration, so an agent here behaves like the same agent run by hand. Two consequences follow, and both are deliberate.
Anything the CLI supports and this model has not modelled is still reachable through AgentSpec.harnessArgs, appended verbatim to the command. The layer never caps what a harness can do.
Your configuration is also inherited when it is broken. Set ToolPolicy.isolateOperatorConfig, or pass --isolate-config, for a run that depends on less outside the spec. That was not a hypothetical while writing this: the config.toml on the development machine names a model the installed Codex rejects, and the refusal it returned is one of the captured fixtures in HarnessParserTest. Note the limit, because the flag reads as a stronger promise than it keeps: only CodexRuntime acts on it, as --ignore-user-config, so a Claude Code agent run under --isolate-config still inherits the operator's settings, hooks, skills and MCP servers.
3. The thread is a causal graph
A conversation is stored as events with parents, not as a list of messages.
┌──> architect · proposal ──┐
user · prompt ──────┤ ├──> synthesis
└──> skeptic · proposal ────┘
Three agents answering one question share a parent. A synthesis that reconciles them has several. That gives branching, joining, forks, retries and "what did this answer actually depend on" without a second model, while a renderer is still free to draw it as one flat conversation.
The integrity rule is small and does the heavy lifting. Appending an event requires every parent to already exist, so an event can only point backwards, and a cycle is not something the API rejects but something it cannot express. Ids are unique, an event cannot be its own parent, and an agent outside the thread cannot author into it.
ProviderSession records where a harness left its own session, so a later turn could resume cheaply. Today none does: both runtimes accept a session to resume and Conductor never supplies one, so the ids are written down and never read, and section 14 treats that as the cheapest saving available. The record is explicitly a cache either way. Delete every session id and the thread still describes each agent completely, which is the property that keeps a project from depending on one vendor's conversation format.
4. Context packets and blind rounds
A long conversation must not become a long prompt. The ContextCompiler turns the thread into the one string a harness receives, and it decides per agent and per turn what that includes: the agent's instructions, how much history, which peers, and how much of each.
The interesting policy is Visibility. In a blind round peer material is dropped by the compiler even when the caller supplies it, so independence is a property of the system rather than an instruction a model is asked to honour. That matters because agents that can see each other's answers converge on the first one, and three models that agree with each other cost three times as much as one.
Two smaller rules fell out of writing the tests. An agent never receives its own output as a peer, and anything rendered as a peer is removed from history, so no event is paid for twice in one prompt.
The boundary matters for the second half of this document. CompileRequest carries a thread, an agent, a task string, a visibility policy and peer events, and truncation is by event count and character count rather than tokens, so this compiler decides everything about what a harness reads of the conversation and nothing about what it reads of the code. The repository arrives as the directory the process is started in, and the harness discovers the rest for itself, every turn.
5. Protocols
| Protocol | Shape | Use |
|---|---|---|
Ask | One agent, full context | The ordinary case |
All | Everyone at once, blind or shared | Breadth in one round |
Council | Blind proposals, then cross-critique, then synthesis | Disagreement is the point |
Pipeline | Named stages in order | A relay you designed |
Planned | A planner returns the team and topology, then it runs | You do not want to choose |
The synthesis instruction asks for consensus, the best proposal, remaining disagreement, and uncertain assumptions, because a synthesis that hides a real disagreement is worse than no synthesis. A second model agreeing remains review evidence, never a correctness proof, which is the standard the Astra plan sets in section 11 and section 4.
Results from a parallel round commit in roster order rather than completion order, so the same run over the same thread produces the same document and a diff means something.
A worked example
Two files, neither of which the framework knows anything about:
[
{"id":"threat","name":"Threat Modeller","runtime":"Codex",
"instructions":"Find the attack. Name the concrete path an attacker takes."},
{"id":"ops","name":"Operator","runtime":"Codex",
"instructions":"Ask what happens at 3am. Name the failure that pages someone."}
]
{"type":"pipeline","stages":[
{"agent":"threat","instruction":"Give the single most likely attack path.","kind":"Proposal","seesPeers":false},
{"agent":"ops","instruction":"Given that path, name the one alert you would add.","kind":"Critique"}
]}
./gradlew :reaktor-conductor:conduct --args="--roster team.json --plan review.json --dir . --prompt '<the design under review>'"
Run against a real description of the BestBuds realtime-token path, the threat modeller independently described the chat-join hole recorded as S-1 in Verified Foundations, and the operator turned it into a specific alert. Neither agent was told a vulnerability existed.
6. Running a harness is an ordinary Reaktor run
Both runtimes sit on reaktor-tooling's SupervisedProcessExecutor, so an agent turn gets what every other supervised task gets: an argv vector with no shell, independent stdout and stderr line streams, ownership of the whole process tree on cancel and timeout, secret redaction, and a plan fingerprint. Reusing it was also the point of section 15 of the Astra plan, which says to reuse the existing supervised executor and source-definition seals rather than growing a second process story.
A read-only agent is classified ReadOnly and one allowed to edit is LocalArtifactWrite. Writes are opt-in, which is what lets a council run today: it can read the repository and argue about it, and it cannot change anything.
A turn also reports what it consumed. AgentUsage carries input tokens, output tokens, cost and duration onto the event a turn produces, and AgentBudget puts a timeout on the process and a spend cap on the harness that accepts one. Conductor reads neither back, and section 18 is what reading them would be for.
Event schemas are verified, not guessed. Both parsers were written against output captured from the real CLIs on 7 September 2026, and the fixtures in HarnessParserTest are that captured output.
| Harness | Stream | Notable detail |
|---|---|---|
claude 2.1.183 | system/init, assistant, one result | subtype can read success while is_error is true, so the parser keys on is_error |
codex-cli 0.131.0 | thread.started, turn.started, item.completed, turn.completed or turn.failed | The prompt must be positional, or the process waits on stdin |
Unknown event types are ignored rather than fatal, because these formats belong to someone else and will grow fields without warning.
7. Where it sits
The name needs one sentence of explanation. reaktor-agent is already taken twice over: the control plane uses it for the inbound surface, and the cloud model uses it for the outbound scoped agent that runs in each cell and attaches a customer-owned cluster. This module is neither, so it is reaktor-conductor.
Its commonMain has no project dependency at all. The agents, the thread, the protocols and the compiler are a plain library, testable without spawning anything, and only jvmMain knows what a subprocess is. That is the dual-surface rule applied: a graph projection can be added later as an opt-in wrapper that reimplements none of the behaviour.
On scope, the Astra plan is explicit and this module should follow it. A single-agent path reachable identically from the CLI and from MCP is a December v1 commitment. Multi-agent coordination is placed in the following nine months. So Ask is the supported path and Council, Pipeline and Planned should stay behind an experimental flag until they have been measured against the plan's own standard: a subsystem that consumes more time than it saves should be kept out of the common loop.
8. What is missing
Each item names what requires it.
- The governed execution broker. Today the CLI runs harnesses directly. Astra section 11 requires that the CLI, IDEs, Studio, MCP and A2A share one set of operation contracts, and Astra section 20 requires that no ambient agent mutation path exists. Until the broker lands, this module must stay read-only in anything that matters. The door (agent workspace §2) is a first piece of it: third-party tool calls pass an effect policy with single-use approvals. It is not yet the broker, because the caller is identified only by a seat label and graph operations stay off it.
- No message-outcome dimension. Astra section 9 fixes a closed set of outcomes, and a harness killed at its deadline is
OutcomeUnknown, not a failure. Today it is recorded as a failure, which is wrong. - No receipts. The canonical loop ends by retaining a receipt binding executor identity, the argv fingerprint, model identity and artifact digests. Runs currently end at an event.
- Proposals carry no provenance triple. Astra section 11 requires a common base revision, dependency read sets, changed owners and affected obligations. Without a read set, merging independent changes is unimplementable, a council has nothing to conflict-detect on, and the one trigger in section 16 that would catch an agent working outside its declared scope cannot fire.
- The executor's event sink defaults to no-op, so run events are not yet appended to any ledger.
- Context packets record no inclusion reasons. Astra requires that a query record why each item was included.
- Parent edges are untyped. This one was found by the agents themselves during a live council: an untyped parent list is a harder flat list, and edges want types such as replies-to, supersedes and depends-on.
- No worktree isolation.
WorkspaceMode.Isolatedis declared and unused, so parallel writing agents would share one checkout. It is also what stops any delegation policy from permitting parallel writers, whatever section 16 computes. - Configuration isolation reaches one harness.
ToolPolicy.isolateOperatorConfigbecomes--ignore-user-configfor Codex and is ignored by the Claude Code runtime, while the CLI applies it to every spec in a roster, so a mixed roster is given a guarantee half of it does not honour. - Every turn opens a cold provider session.
Conductorbuilds each request without a session to resume even though both runtimes accept one andProviderSessionis recorded on every agent turn, so each turn pays for its own prefix again. Requires one field, and dropping the--ephemeralthatCodexRuntimeadds whenever there is no session to resume. - The budget is declared and unenforced. The timeout reaches both harnesses, the spend cap reaches only the one that accepts it, and nothing sums
AgentUsageacross a run or a thread. Requires a run total before it requires a policy, which is what section 18 measures. - The compiler sees only the thread.
CompileRequestcarries no fact about the repository, and truncation is by events and characters rather than tokens. Requires the context packet of section 11, which is itself blocked on source scanning and on the absence of any symbol-metadata source. - A check cannot be recorded as a check.
EventKindhas no verification member, so a reduced test result or a counterexample reaches the thread as aNoteor aFailurecarrying prose. Requires the structured result of section 13. - The Claude success path is unverified end to end. A nested
claudecould not reach credentials from inside a Claude Code session, so its envelope shapes are real and captured but a successful turn is not. One command from an ordinary terminal closes this.
Those are the gaps in what is built. What follows is what the module is for, and none of it is built yet.
9. What a turn actually costs
A harness turn is priced by what it has to read before it can act. Conductor.run hands an agent exactly one fact about the repository, workingDirectory, and the harness spends the front of its context rediscovering what this project already knows somewhere else: where a symbol is defined, what depends on it, which tests cover it, what the last run failed on, what was decided and why. That rediscovery is billed at the rate of whichever model is running, and it is billed again on the next turn, because nothing carries it forward.
So the quantity worth maximising is verified change per unit of provider allowance. That is a decomposition of the outcome the Astra plan already fixes in section 21 — accepted changes that satisfy an independent behaviour contract per unit of total developer time and cost — and not a replacement for it. The distinction is load-bearing: a ratio with model allowance alone underneath improves every time work is handed back to the person, so allowance is one term, human time is another, and a measurement that drops the second is measuring the wrong thing.
Each section below ends with the smallest change that would test its claim, because a claim about cost that cannot be falsified cheaply is itself expensive.
10. The task is the durable unit, not the turn
Durability is not the gap here, and it is worth saying so before borrowing the argument that usually comes with this idea. Section 3 already makes ThreadDocument canonical, round-trips it through JSON and treats the provider session as a cache, so a context window that dies is not a project that lost its place.
The gap is narrower and more awkward. The durable record holds a conversation, not a task. ThreadEvent.text is the whole payload, so a result, a change set and a verification outcome all arrive as prose an agent has to re-read to act on, and EventKind — Prompt, Proposal, Critique, Revision, Synthesis, Note, Failure — has no member for a check or an observation.
What the record should grow into already has a name, and it is not in this module. The desktop master plan defines a Mission as a persistent outcome with scope, success criteria, owner, plan, change set, evidence and history, which is the whole of what a task record and its state need to carry. Acceptance criteria are the obligations section 11 of the Astra plan already specifies rather than a new kind of assertion, and scope is a graph slice against a common base revision, which section 8 lists as missing under the provenance triple. A thread then becomes one projection of a mission rather than the thing itself.
A fourth task noun is the specific thing to avoid. reaktor-tooling owns TaskId and TaskPlan, this module already mints TaskId("conductor:<kind>:<agent>") for every process it spawns, and TeamPlan already means which agents and what topology, so a new AgentTask would put three unrelated tasks and two unrelated plans on one classpath.
Smallest change that tests it: carry a mission id in ThreadEvent.attributes, the extension point that already exists, so runs can be attributed to an outcome before any mission store does.
11. Compiling the task's context, not the thread's
ContextCompiler is a one-method fun interface that turns a ThreadDocument and a Visibility into the prompt string of section 4; CompileRequest carries a thread, an agent, a task string, a visibility policy and peer events, and nothing about the repository; truncation is by event count and character count. It knows nothing about files, symbols or tests. The proposed task-context compiler is a second compiler over the task rather than the conversation, and the shipped one becomes the section of its output that renders history.
For source code, structure beats similarity. The useful ordering is the symbol graph, the type graph, module dependencies, port connections, the call graph, ownership, the tests that cover the symbol, git history, runtime traces, and semantic retrieval last — because a dependency cone is computed and an embedding neighbourhood is a guess at one. Given a ServiceNode wired to a ProviderPort<Foo>, what surrounds that node is a query, so an agent should be told rather than sent to find out.
The object has a specification already, in section 11 of the Astra plan: selected entities and revisions, owners, relevant source spans, contracts, capability choices, dependency and impact slices, protocol obligations, recent traces, verification results and unknowns, with the rule that a query records why each item was included. That last rule closes one of the open items in section 8 for free, and it is the reason to link that specification rather than fork it.
What blocks this is not the design. Source scanning is marked not built in graph sources, and nothing in the tree emits symbol metadata: reaktor-compiler's two processors generate JavaScript promise wrappers, disabled at the top of process(), and FlexBuffer coders, and neither is an index. The architecture snapshot that does exist is built from a live Graph and describes a running system, which is the wrong moment, because the agent needs the cone at edit time. Two things do exist and are enough to start: reaktor-tooling's KotlinSourceIndex, which maps a qualified name to a file and a line, and the Gradle project graph, which gives module-granular dependencies. Module granularity is a poor cone and still a better one than a grep.
The dependency rule from section 7 survives only if the seam is drawn correctly: the compiler interface stays in commonMain with no project dependency, and every source of facts is a JVM implementation behind it. A packet also does not need a new addressing scheme — the protocol already mints handles that are opaque to the client and already reports what a slice omitted and why, so a fresh code:// or test:// namespace would be a second identity system for the same objects.
Smallest change that tests it: compile a packet for one real task from KotlinSourceIndex and the project graph alone, and compare the inputTokens already recorded on the resulting event against the same task run without one.
12. Reducing what the tools say
Nothing in this corpus covers this, which is why it comes first.
The numbers are small and embarrassing. ./gradlew :reaktor-core:jvmTest --console=plain --rerun-tasks prints 165 lines, of which 33 are compiler warnings and not one names a test that ran; ./gradlew projects, which compiles nothing at all, still prints 130 lines. A failing run is worse, and a model that is handed it pays for every line twice, once to read it and once to summarise it back. The reduction is deterministic, so paying frontier rates for it is the clearest waste in the loop.
The data is already on disk. Gradle writes JUnit XML for every test task, carrying per-suite counts, failures and timings; the tree holds hundreds of those files at any time and nothing reads them. The missing piece is a reducer, not a source.
FAILED 3 / 812
GraphTransactionTest.concurrentMutation
expected COMMITTED
actual CONFLICT
group: transaction ordering, 3 cases
log: artifact://run/8241
The same treatment applies to compiler diagnostics, stack traces, a diff, a query plan and the harness's own transcript. Two shapes exist to copy rather than invent. The prune plan specifies collectors that run external tools through reaktor-tooling's SupervisedProcessExecutor, the same primitive this module's runtimes use, with parsers turning their output into measures; that is its phase 0 and is not built either. Formal methods already defines Finding, carrying a code, a severity, an invariant id, the elements involved, a witness and what is missing, so a failed test and a counterexample can reach an agent in one shape. This module also has the pattern one level up: CliEventParser turns a harness's line stream into typed events and an outcome, which is the same job against a different vocabulary.
One boundary. Today a harness runs its own build commands inside its own sandbox, so this module never sees that output, and the reducer belongs where the command runs — reaching the agent either as a tool it calls or as a section of the packet in section 11.
Smallest change that tests it: reduce one command's output and read the inputTokens on the next event.
13. Verification the model does not pay for
A frontier model should never spend a turn answering a determined question. Did this compile. Which tests cover this node. Does every implementation of this interface still satisfy it. Those have machine answers, and a model that reasons its way to them is paying to guess at something a build already knows.
The loop is designed, with more rungs than this section needs, in section 11 of formal methods: a proposed patch, schema and type validation, the obligations it affects, deterministic passes and then heavier methods only where the claim demands them, a report, a Finding carrying a witness, bounded retries, a plan, approval, commit. Finding.missing is the field that makes the loop converge, because it tells the next turn what to add rather than that something is wrong.
What that pipeline does not have is a cost model. It runs a solver per affected obligation with no budget attached, which that document files as an open question of its own, and the Astra plan's measurement rule says validation overhead is measured alongside the work it saves. What this module adds is the counter: verification is only free if someone counts it, and it is the same counter section 18 describes.
The vocabulary to reuse is EffectKind — reads and writes against a store, calls, emissions, spawns, timers, clocks, randomness, secrets, navigation, and an explicit opaque case — each carrying idempotence, reversibility, scope and durability. Declared effects are what turn "this changed a security boundary" from a note for a reviewer into a check, which is what section 16 needs before its most useful trigger can fire.
Two limits, stated plainly. Affected-test selection has no data source in either repository: there is no coverage tooling, and test selection today is a string match on a class name. And this module cannot record a check as a check: EventKind has no verification member, so a result reaches the thread as a Note or a Failure with the outcome carried in attributes rather than in its kind. The retry discipline needs no restating and should be imported verbatim: retries are bounded, and a repair is applied last and not by the agent that proposed it.
Smallest change that tests it: attach the reduced result from section 12 to the thread as a typed attribute rather than prose, so a later turn reads the outcome instead of re-running the command.
14. A surface the provider's cache can hold
A prompt cache is a prefix match. OpenAI's own account of the Codex loop is explicit that a hit requires an exact prefix, and that changing the tool list, the model, the sandbox configuration, the approval mode or the working directory mid-conversation can cost the cache, which is why their harness appends configuration changes as new messages instead of editing earlier ones (unrolling the Codex agent loop). A turn that loses the prefix is paid for in full.
The consequence for Reaktor is a small, stable, versioned surface, with the volatile part — who may write what, to where — checked behind it rather than written into the schemas. A capability check that rejects a call is cheap; a tool list that changes shape per caller is not, because it moves the front of every prompt.
At the initial design snapshot, a desktop with its kernel installed advertised eleven read tools, and the risk it named was uncontrolled growth from eleven. That growth has happened: since 23 September one door entry fronts Reaktor's own servers and every trusted third-party server, 271 tools in the BestBuds workspace. The proposed response is a small stable default surface with providers mounted per task profile (agent workspace §2). And the layer is not a new protocol: the Astra plan settles that MCP is a distribution interface and A2A an interoperability interface, with Reaktor's own versioned domain protocol underneath both. That protocol is rgp/1, which is already stricter than cache stability asks for — the version is negotiated at Mesh.hello with hard failure on a major mismatch, method names are interned at hello, and the codec for each method is chosen by schema fingerprint. So the agent-facing surface is the projection of one set of operation contracts into MCP and the CLI, reusing Graph.search, Graph.describe, Graph.resolve, Edit.prepare and Edit.execute. That protocol keeps reads and writes in separate domains because collapsing them into one mutation surface is the most damaging thing it could do, and it keeps preparation and execution apart for the same reason.
The cheapest instance of this is inside this module and costs one field and one flag. Both runtimes accept a session to resume, ProviderSession is recorded on every agent turn, and Conductor never sets it, so every turn opens a cold session and pays for its own prefix again. On Codex there is a second half to it: the runtime passes --ephemeral whenever no session is being resumed, so the id it records names a session that was deliberately not written to disk.
Smallest change that tests it: set AgentRequest.resume from the recorded session, stop passing --ephemeral on the turn that opens a thread, and compare inputTokens across two turns of the same thread.
15. Choosing what runs the turn
Model choice is a property of the task and is currently a property of the agent: AgentSpec.model is one static string per spec, passed through to the harness, and nothing chooses per task.
The policy is written by task property, never by product name, because product names are wrong within a quarter. Determined work — find a symbol, list dependents, parse a build log, apply a mechanical rename — should not reach a model at all. Reduction work — summarise a log, rank retrieved symbols, cluster failures, compress the state of a previous run — is what a cheap or local model is for. Implementation against a pattern that already exists in the codebase is the middle tier's job. Novel design, concurrency and distributed semantics, type-system decisions and cross-runtime equivalence are what the expensive tier exists for, and are worth its price. One published pattern is worth naming because it needs no new type here: plan with the strong model and execute with the cheap one, which is a Pipeline whose stages name agents with different model values.
The case for a local model is not that it competes with a frontier model. It is that a frontier model with a smaller, better prompt beats the same model with a larger one, and everything in the previous paragraph's first two classes is work that never needed the expensive tier. Browser-local AI already specifies this compression pipeline including provenance for what was omitted, and the Koog workbench owns the JVM and Ollama tier. What neither owns is the case this module has: a local model that shrinks what a Claude Code or Codex subprocess must read.
Two tensions are worth stating rather than discovering. Routing per turn fights section 14, because each switch is a new prefix, so a route is chosen per task and held. And section 2's inherited operator configuration surrounds a routed model with settings the router did not choose, and can fail the run outright, as that section records; the switch that turns it off reaches Codex only.
The decision needs no new field. AgentSpec.attributes is the carrier the prune plan designates for telling an agent its module tier and budget before it writes a line, and a router that fills model, harnessArgs and those attributes on a spec is a function returning a spec rather than a parallel type. Per-plan economics and the model-to-task-class table that goes with them are operator data, so they live in the roster the operator already writes and in a private note where they can go stale without damaging this document.
Smallest change that tests it: route two task classes by an attribute, hold the route for the whole task, and compare cost per accepted change against a single-model baseline.
16. When a second call is worth buying
Two questions have one shape — should another expensive call happen at all. A second model reviewing the first is one form, a second agent working in parallel is the other.
The review half is settled three times over in this corpus and should not be re-argued: section 4 already says that three models agreeing cost three times as much as one, section 5 that a second model agreeing is review evidence and never a correctness proof, and formal methods that a repair is applied last and not by the agent that proposed it. What is new is only the predicate. Buy the second opinion when something measured says the first answer is suspect: a compile failure, a test regression, a change to a public surface, a changed security boundary, an unexpectedly large change, or a change outside the declared scope. No new topology is needed for it — conditional review is Ask followed by Ask, and the missing piece is the predicate. The last trigger is not computable today, because proposals carry no read set, which section 8 already records.
The parallel half has a live hazard. Frontier models over-delegate: Anthropic's own guidance notes that recent models will spawn subagents for exploration where a direct search would have been faster, and recommends delegation for genuinely parallel, context-isolated workstreams and direct work for everything sequential or small (prompting best practices). The gates worth encoding are the obvious ones: below two independent units, do not delegate; over a shared mutable working set, do not delegate; at four or more independent units in separate modules, delegate. The hazard is that the workspace field is on every spec and read by nothing: WorkspaceMode.Isolated has no implementation and the sandbox is derived from ToolPolicy.allowWrites instead, so parallel writing agents would share one checkout, and no gate may permit parallel writers until worktree isolation lands. That dead enum is also the right place for a per-task write scope, rather than another field.
Smallest change that tests it: compute the predicate from the reduced verification result of section 12 and gate one second Ask on it, instead of running a council.
17. One graph, many agent-facing files
The reaktor root holds AGENTS.md, CLAUDE.md, GEMINI.md and LLM_CONTEXT.md side by side, and with the product manual and its neighbours next door the two roots carry seven such files, 731 lines and about 59 KB of instruction, all of it maintained by hand. They drift, and the evidence is specific rather than theoretical: AGENTS.md records that README.md and LLM_CONTEXT.md predate the control-plane work and tells the reader to trust the code over both; LLM_CONTEXT.md has not been touched since December 2025 and half of it describes screens that live in a different repository; and the BestBuds CLAUDE.md spends ten of its thirteen lines on rules for a tool that is not installed and not in the tree. Every one of those files is loaded into a context window before any work starts.
The checkable half should be generated from one source: the module map, the canonical commands, invariant ids, which harness reads what, and where decisions live. It should also be small. The most useful published finding on this is that an agent wants a map rather than a manual, with a short entry point that teaches it where to look next (harness engineering). The precedent is already in this repository, where the docs sidebar and the landing page's map region are generated from one manifest and nothing hand-edits them.
The counter-argument is the strong one, and the section is dishonest without it. The private diligence review calls the AGENTS.md culture — the design bars, the layer rules, the instruction to trust the code — the best agent-facing engineering doctrine it reviewed. Generating over that half would destroy the asset. So the split between the generated half and the written half is the design, not a detail of it.
The tie to this module is one sentence: an agent that receives a compiled packet needs a smaller AGENTS.md, not a larger one, so whichever of the two ships first should shrink the other.
Smallest change that tests it: generate the module map and the command list from settings.gradle.kts and the pipeline definition, delete those paragraphs from the hand-written files, and see whether anything is lost.
18. What it costs to know
Every claim in this half is currently unmeasurable in the module that makes it, and the instruments are already installed. AgentUsage records input tokens, output tokens, cost and duration on every agent turn; the captured Claude result line carries cost and duration and is expected to carry both token counts once a successful Claude turn is observed, and Codex reports the two token counts and no cost. AgentBudget carries a timeout that reaches both harnesses through the process plan and a spend cap that reaches Claude and is dropped by Codex without a word. Conductor reads neither, nothing sums either across a run or a thread, and the executor's event sink defaults to no-op, so nothing reaches a ledger at all.
Beyond the totals, the thing to record is allowance consumption per run, beside model identity, effort, retries, context size and outcome, because that is the denominator of section 9 and the only way to tell an expensive turn that earned its price from one that did not. Per-turn usage is collection: both harnesses report it in their own event streams. Allowance is not. Each CLI reports the plan's state to its own interactive session and neither exposes it to the headless invocations of section 2, so allowance has to be summed from usage or read out of band, and finding where each CLI keeps it is a prerequisite rather than a detail.
A gateway can do the summing for one of the two harnesses. Claude Code can send its traffic through a gateway while the subscription stays the credential, by setting only its base URL, so every request of a subscription run is logged with its token counts and the run's metadata. Agent workspace §24 designs this over Cloudflare AI Gateway, pending one live test. Codex on a ChatGPT login is not expected to route through the same gateway, so its consumption comes from its own usage events and OpenTelemetry export.
The experiment this half exists to win is already specified in section 21 of the Astra plan: the substrate track, which pins the same model, configuration and budget with and without Reaktor and measures the difference. The same section requires that human intervention and time be recorded in the same row, which is the guard against optimising the ratio by handing work back to the person. The delivery channel is additive too — the prune plan routes findings into the next turn's context with the same Finding shape in its own design, so a budget code family needs no new mechanism, only new codes.
Smallest change that tests it: sum AgentUsage per run and print it when conduct exits.
19. Names that are already taken
This half was written against the module's own exports, and several of the obvious names are occupied by something else on the same classpath. Reusing them quietly is how a codebase acquires two meanings for one word.
| The obvious name | What this classpath already means by it | What to use instead |
|---|---|---|
AgentBudget | Per-agent wall clock and spend cap, wired into the process plan | A task-level allocation is a different type with a different name |
ContextCompiler | Thread to prompt string, one method, shipped and tested | The task-context compiler of section 11, a second type behind the same seam |
Plan | TeamPlan here, plus TaskPlan and AdHocProcessPlan in reaktor-tooling | Name the plan for what it plans |
Provider | RuntimeKind, with ProviderSession already using the word for the harness's session | RuntimeKind |
Model | AgentSpec.model, a string on the spec | The field, not a new type |
Failure | EventKind.Failure | Say what failed |
AgentTask | TaskId and ToolingTask in tooling; this module mints its own per turn | Mission, per section 10 |
Step | Stage, in Pipeline | Stage |
| Worker | Agent, everywhere in this module | Agent |
One item is a law rather than a name. An agent context graph standing beside the source, runtime, build and decision graphs is refused by the unified system graph, whose first requirement is that a new view does not create another semantic store and whose agent section adds that a special agent API must not become another source of semantic state. The ontology there already lists agent tasks under the product and collaboration layer, and the Astra plan calls the product and build views related, distinct projections. So it is a layer and a projection, not a fifth graph — and the genuinely new idea inside it survives that reframing untouched: one packet, projected differently per harness and per budget, from a single store.
The module's own name is still open and this half does not settle it. Section 7 explains why this is reaktor-conductor; the prune plan records a later decision to move the agent layer to reaktor-agent with this code beneath it. Nothing here depends on which wins.
20. What to build first, and what would stop it
Two changes come first because they are inside the shipped model: set the provider session to resume and stop making Codex sessions ephemeral, and sum the usage that is already recorded on each agent turn. Together they produce the baseline every later claim is measured against, and one of them may pay for itself immediately.
Tool-output reduction is second, because it is deterministic, needs no graph, reuses the executor and the parser shape this module already has, and its input is already written to disk.
Then a gate rather than a milestone. If those two do not move input tokens per accepted change on the substrate track, stop. Everything after this point is more expensive to build and rests on the same assumption, and section 7's standard — a subsystem that consumes more time than it saves stays out of the common loop — applies to this work before it applies to anything else.
What justifies the spend past the gate is a property this work has and prompt tuning does not. A packet store, a task record and a held route are Reaktor-side machinery rather than strings fitted to one model, so a gain measured against today's models survives their replacement. That is the difference between a framework a model can work on and a harness that makes whichever model is current materially more effective.
The task-context compiler is third, behind the commonMain seam, starting from the source index and the project graph and blocked beyond that on source scanning and on the absence of any symbol-metadata source, both of which are other documents' work. Routing, delegation policy and generated agent files come last, once there is data on which tasks actually needed the expensive tier, because a routing table written before the measurement is a guess with a configuration file.
What this half may not assume: the governed broker, the ledger and worktree isolation are section 8 items, and each of them gates the write path. Nothing above assumes an agent that writes.
Related docs
- The Reaktor Control Plane - The inbound direction: agents reading snapshots and proposing validated patches.
- Astra Reaktor Plan and Roadmap - Section 11 defines the agent-native interface and the canonical loop; sections 16, 17 and 20 fix the v1 scope and the release gates this module follows, and section 21 defines the measurement.
- Reaktor Formal Methods - Where a counterexample returned to an agent comes from, and the
Findingshape a critique should eventually produce. - Verified Foundations - The defect and security registers, including the hole a live council rediscovered.
- Reaktor CLI - The task catalog this module's entry point should eventually be projected from rather than sit beside.
- Koog in reaktorDesktop - The in-process alternative, and the JVM and Ollama tier this module's routing would defer to.
- The Unified System Graph - The law that a new view does not create another semantic store, and the layer that already holds agent tasks.
- Graph Sources - Why a task-context compiler is blocked today, and the four ways a system enters the graph.
- Reaktor Graph Protocol - The domain names an agent-facing surface should project, and why preparation and execution stay apart.
- Browser-Local AI - The local compression tier this module would borrow rather than build.
References
Key code anchors: reaktor-conductor/src/commonMain/.../{AgentModel,Thread,ContextCompiler,Protocol,Conductor}.kt, reaktor-conductor/src/jvmMain/.../cli/{CliAgentRuntime,ClaudeCodeRuntime,CodexRuntime,ConductorCli}.kt, and the captured-output fixtures in reaktor-conductor/src/jvmTest/.../HarnessParserTest.kt. The process primitive is reaktor-tooling/src/jvmMain/.../SupervisedProcessExecutor.kt.
The design half is written against those same files: the request built without a session and the usage recorded onto each event in Conductor.kt, AgentUsage and EventKind in Thread.kt, AgentBudget, WorkspaceMode, model and attributes in AgentModel.kt, and Visibility and CompileRequest in ContextCompiler.kt. Outside the module it leans on reaktor-tooling/src/jvmMain/.../KotlinSourceIndex.kt, the disabled processor at reaktor-compiler/src/main/.../ReaktorProcessor.kt, and the JUnit XML that every Gradle test task already writes to <module>/build/test-results/.