feat(conductor): multi-agent fan-out + verifier-gated merge worker (Rust) - #59
Conversation
Registers four iii functions:
- conductor::dispatch fans a task across N agent specs in parallel, each
in its own git worktree under
~/.iii/conductor/worktrees/. Records a RunState in
state::set scope=conductor.
- conductor::status reads a RunState by run_id.
- conductor::list lists all RunStates.
- conductor::merge picks the first finished agent that produced a
non-empty diff and passed every gate. Loser
worktrees are pruned; the winner's branch survives
for review.
Agent kinds covered: claude, codex, gemini, aider, cursor, amp, opencode,
qwen for local CLI shellouts; remote for cross-protocol agents reachable
through any iii function id (typically registered by iii-mcp-client or
iii-a2a-client).
Verifier gates are themselves iii functions. The conductor passes the
agent's worktree as cwd and treats { ok: false } as a stop. The eval,
guardrails, and proof workers in this repo register suitable gates.
Functions are tagged metadata.public = true; expose them over MCP/A2A
through iii-worker-manager's expose_functions policy.
Tests cover the merge winner-picking logic against passing, mixed, and
fully-failed runs.
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (14)
📝 WalkthroughWalkthroughIntroduces a new Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Conductor as Conductor Worker
participant Agents
participant RemoteFn as Remote Functions
participant Git as Git Worktrees
participant State as Persistent State
Client->>Conductor: dispatch(task, agents, cwd)
Conductor->>State: writeRun (initial RunState)
par Parallel Agent Execution
Conductor->>Agents: runLocalAgent (Agent 1)
Agents->>Git: createWorktree (branch-sanitized)
Git-->>Agents: worktree path
Agents-->>Conductor: {ok, stdout, stderr}
Conductor->>RemoteFn: sdk.trigger (Remote Agent)
RemoteFn-->>Conductor: result or error
end
Conductor->>Git: diffAgainst baseRef (per agent worktree)
Git-->>Conductor: diff content
loop For Each Finished Agent
Conductor->>RemoteFn: runGate (gate function)
RemoteFn-->>Conductor: {ok, reason?}
end
Conductor->>State: writeRun (completed RunState)
Conductor-->>Client: {ok, run_id, agents, gates}
Client->>Conductor: merge(run_id)
Conductor->>State: readRun
State-->>Conductor: RunState
alt Has Finished Agent with Non-Empty Diff & Passing Gates
Conductor->>Git: removeWorktree (losers)
Git-->>Conductor: {ok}
Conductor->>State: writeRun (winnerIndex set)
Conductor-->>Client: {ok: true, winner, losers}
else No Eligible Agent
Conductor-->>Client: {ok: false, reason, losers: []}
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 3 minutes and 23 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@conductor/Dockerfile`:
- Around line 9-16: The image currently runs as root; create and switch to a
non-root runtime user by adding a dedicated user/group and changing ownership of
/app before the final image CMD. Update the Dockerfile to add a non-root user
(e.g., create a user like "conductor" or "appuser"), chown /app (or the copied
files) to that user after copying files (refer to the COPY and WORKDIR steps),
and add a USER instruction to run the container as that non-root user prior to
the CMD invocation so node /app/dist/index.js runs without root privileges.
In `@conductor/src/dispatch.ts`:
- Around line 27-53: The remote-agent branch (when spec.kind === 'remote')
currently calls sdk.trigger with the shared cwd and never records worktreePath
or diff, so remote agents share the checkout and produce no mergeable result;
modify this branch to create a per-agent worktreePath (e.g., create a temporary
worktree or per-agent checkout), pass that worktreePath as cwd in the payload to
sdk.trigger (instead of the shared cwd), capture the remote result and compute
the git diff for that worktree, and return a result object that includes agent:
spec, status, startedAt, finishedAt, exitCode/output, and the new worktreePath
and diff fields so mergeRun() can accept and merge remote agents; also keep the
existing error handling around sdk.trigger but ensure errors include the
worktree cleanup and accurate timestamps.
- Around line 128-143: The current winner selection iterates run.agents in input
order (variable a) which picks the first eligible agent instead of the one that
actually finished first; change the logic to pick the agent with the earliest
completion timestamp instead: filter agents to those with status === 'finished',
allPassed(a.gateResults) (or true if absent), and a.diff non-empty, then choose
the agent with the smallest finishedAt/ completionTime/finishedTimestamp field
(or if such a field does not exist, add and set finishedAt when status
transitions to 'finished'); set winner to that agent ({ index, agent: a.agent,
diff: a.diff, branch: a.branch }) and put all other eligible indices into losers
to preserve current semantics.
- Around line 106-119: The run currently only persists the run once at the end,
so intermediate agent state transitions (from runAgentInWorktree and
runAllGates) are lost if the worker crashes; after each agent state is updated
in the loop (i.e., after setting run.agents[i] = state and after gateResults are
attached), call await writeRun(sdk, run) to persist the partial progress so
completed agents and gate results survive restarts; keep using
runAgentInWorktree, runAllGates and writeRun as the points to snapshot state.
In `@conductor/src/gates.ts`:
- Around line 25-29: The current runAllGates function stores results in a Record
keyed by GateSpec.function_id which can overwrite entries when function_id is
duplicated; change runAllGates to preserve order and duplicates by returning an
array of outcomes (e.g., GateOutcome[]) or a list of objects that include the
original GateSpec metadata and outcome. Update runAllGates to iterate gates,
call await runGate(sdk, gate, cwd) for each, and push a result entry that
contains gate (or its description and function_id) plus the GateOutcome into an
array, then return that array instead of the Record keyed by function_id; ensure
callers expecting Record<string,GateOutcome> are updated to handle the new array
shape.
In `@conductor/src/git.ts`:
- Around line 38-46: The createWorktree function may call git worktree add
before the worktree root exists; ensure the directory root (and any parent dirs)
exist before invoking git by creating the directory (use fs.mkdir with {
recursive: true }) for the computed path or at least for root, handle any mkdir
errors and return { ok: false, reason: <error message> } if creation fails, then
proceed to call git(repoCwd, ['worktree', 'add', '-b', branch, path]) as before;
reference createWorktree, root, path and git to locate where to add the mkdir
and error handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8771e350-d55c-44dc-823c-a35ffaf597ab
📒 Files selected for processing (15)
conductor/Dockerfileconductor/README.mdconductor/iii.worker.yamlconductor/package.jsonconductor/src/agents.tsconductor/src/dispatch.tsconductor/src/gates.tsconductor/src/git.tsconductor/src/iii.tsconductor/src/index.tsconductor/src/state.tsconductor/src/types.tsconductor/tests/dispatch.test.tsconductor/tests/gates.test.tsconductor/tsconfig.json
Replaces the initial TypeScript scaffold with a Rust binary crate so the
conductor can ship as cross-compiled artifacts via _rust-binary.yml,
matching the rest of the iii-engine-adjacent workers (mcp, a2a,
introspect, llm-router, image-resize).
- Cargo.toml declares iii-conductor 0.1.0 with iii-sdk =0.11.3,
edition 2021, rust-version 1.85.
- iii.worker.yaml flips to language: rust, deploy: binary,
manifest: Cargo.toml, bin: iii-conductor.
- Dockerfile + package.json + tsconfig.json + node_modules/* dropped.
- src/ ports the prior modules to Rust (types, git, agents, gates,
state, dispatch) plus a new main.rs that wires four iii functions:
conductor::dispatch, conductor::status, conductor::list,
conductor::merge — all with metadata.public = true and full
JSON-Schema request/response shapes for MCP/A2A consumers.
- state.rs follows the llm-router envelope-tolerant pattern for
state::get / state::list to handle both 0.11.0 and 0.11.2 engines.
- tests/merge.rs exercises the gate aggregator and the winner-pick
selection logic across passing, mixed, and failing runs.
Verification:
cargo fmt --all -- --check clean
cargo clippy --all-targets -- -D warnings clean
cargo test 5 passed
Audited the 6 findings against the Rust port. Four applied, one is N/A (non-root Dockerfile, replaced by binary deploy), one was already covered (create_worktree calls create_dir_all on root before git worktree add). 1. Remote agents are now isolated. They go through the same worktree create / diff capture / gate run path as local CLI agents. The agent's worktree path is passed in the trigger payload as cwd, so a remote handler that writes to cwd produces a real diff and can win the merge. 2. Run state is persisted after every agent transitions, not only at the start and end of the fan-out. Fan-out now uses FuturesUnordered, so completed agents are written back to state::set as soon as their gates finish — surviving worker restarts mid-run. 3. merge_run picks the agent with the smallest finished_at among the eligible set, not the first input-order entry. New tests prove a slower agent earlier in the input list does not win over a faster agent later in the list. 4. AgentRunState.gate_results is now Vec<GateRunResult> with the function_id, description, ok, and reason fields, preserving order and duplicates. Previously a HashMap<String, GateOutcome> would silently collapse duplicate function_ids and lose ordering. Tests: 9 pass (was 5). Added coverage for completion-order winner pick, duplicate function_id preservation, empty-diff skip, all-failed branch, and a Vec round-trip via serde. cargo fmt --all -- --check clean cargo clippy --all-targets -- -D warnings clean cargo test 9 passed
- Remote agents now also get a per-agent worktree and the worktree path is passed as cwd in the trigger payload. - Gate results are an ordered Vec<GateRunResult>, not a function-id-keyed map; duplicates are preserved. - merge picks the eligible agent with the smallest finished_at, not the first input-order eligible entry. - Run state is persisted after every agent transitions, not only at the start and end.
Plan-devex-review pass landed the following fixes:
- Add Install section with `iii worker add conductor` and the runtime
peers it depends on (engine, worker-manager, mcp/a2a transports).
- Add Quick start with a copy-paste demo dispatch that uses `bash`-stub
agents and a no-op verifier so the fan-out + merge mechanics are
visible without `claude` / `codex` / a real verifier worker installed.
- Document `timeout_ms` default (600 000 ms / 10 min, per agent).
- Replace the fictional `verify::tests` / `verify::lint` / etc. list
with the truthful contract: gates are caller-provided iii functions
of shape `(input: { cwd }) -> { ok, reason? }`. There is no
pre-built `verify::*` worker in this repo.
- Add Idempotency section — dispatch is fire-and-forget, caller dedupes
via `conductor::list`.
- Add Errors table with three concrete `IIIError::Handler` strings,
each with cause and fix.
- Add Versioning policy — 0.x means field shapes can change between any
minor bump; pin `=0.1.x` and read CHANGELOG.
- Add CHANGELOG.md with v0.1.0 entry and known gaps tracked for v0.2.
Pass scores (before -> after):
Getting Started 3 -> 8
Error Messages 2 -> 6
Documentation 4 -> 8
Upgrade Path 1 -> 6
Cargo.lock should not be tracked for this worker. Add a per-worker .gitignore that excludes Cargo.lock and target/ so future builds don't re-stage them.
Found during live smoke test against a running iii engine: agents that write *new* files (the common case for "add a /healthz endpoint" style tasks) produced empty diffs, because `git diff <base_ref> -- .` does not include untracked files. The agent finished with status=Finished but diff="" so it was filtered out of the merge eligibility check and could never win. Stage everything in the worktree first, then diff against the cached index. Tracked changes still surface; new files now do too. Verified end-to-end: - 2-agent run with stub bash agents writing distinct files: each diff ~144 chars (was 0 chars). - Winner picked by smallest finished_at (claude won at 663338ms vs codex at 663641ms). - Loser's worktree pruned, winner's branch survived. - Same flow over MCP via iii-mcp: tools/list returned conductor__dispatch, conductor__status, conductor__list, conductor__merge; tools/call on conductor__status returned the full RunState including winner_index. Tests: 9 cargo test, clippy + fmt clean.
Adds the conductor worker (merged in #59) to the Create Tag workflow's choice list and to the Release workflow's tag-push trigger so `gh workflow run create-tag.yml -f worker=conductor` works and `conductor/v*` tags fire the release dispatcher. Both lists kept alphabetical between coding and eval.
Summary
Adds
conductor/, a Rust binary worker that fans out a task across N agent CLIs in parallel, each in its own git worktree (local and remote), runs configurable verifier gates against every result, and picks the agent that finished first among the eligible set.Four iii functions:
conductor::dispatchtaskacrossagents[], store run state, run gates per agentconductor::statusRunStatebyrun_idconductor::listconductor::mergefinished_atwinner pick over the eligible set, loser worktree cleanupAll four are registered with
metadata.public = truesoiii-worker-managercan expose them throughiii-mcpandiii-a2a. JSON-Schema request/response shapes are declared on every registration so MCP tool discovery returns full type info.Agent kinds
claude,codex,gemini,aider,cursor,amp,opencode,qwen. Default arg vector per kind, overridable viabin/args.remote: any iiifunction_id— typically registered byiii-mcp-client(mcp.<server>::<tool>) oriii-a2a-client(a2a.<session>::<skill>). Remote agents now also get a per-agent worktree, and the worktree path is passed ascwdin the trigger payload, so remote handlers that write tocwdproduce a real diff and can win the merge.Verifier gates
Gates are themselves iii functions of the shape
(input: { cwd }) -> { ok: bool, reason?: string }. The conductor invokes each gate with the agent's worktree ascwdand treatsok: falseas a stop. Gate results are stored as an orderedVec<GateRunResult>so duplicatefunction_ids with different descriptions (e.g.verify::testsfor unit and again for integration) are preserved end-to-end. Recommended pairings already in this repo:eval,guardrails,proof.Run state
Stored under
state::setscopeconductor, keyruns::<run_id>. Persisted after every agent transitions, not only at start and end — fan-out usesFuturesUnorderedand writes the run back to state as each agent completes its gates, so a worker restart mid-run preserves the progress of every agent that already finished.State helpers follow the same envelope-tolerant pattern as
llm-router/state.rsso both 0.11.0 ({ items: [...] }) and 0.11.2 (bare arrays) engine responses are accepted.Files
Verification
cargo fmt --all -- --check— cleancargo clippy --all-targets --all-features -- -D warnings— cleancargo test— 9 tests passCodeRabbit findings (from the earlier TS scaffold) — all addressed
FuturesUnorderedloop persists after every agentmin_by_key(finished_at)over eligible setVec<GateRunResult>withfunction_id/descriptionpreservedgit worktree addcreate_dir_all(parent)runs before addTest plan
cargo build --releaseproducestarget/release/iii-conductorcargo test— 9 tests passiii-worker-manager(expose_functions: [match("conductor::*")]) +iii-mcp+iii-conductorconductor_dispatch,conductor_status,conductor_list,conductor_mergetoolsconductor::mergepicks the agent with the smallestfinished_atkind: "remote"registered viaiii-a2a-client: confirm a non-empty diff is captured and the remote agent can winOut of scope (follow-ups)
iii-streamchannel for live UI consumers — easy follow-up.run_idfrom a fingerprint of(task, cwd, agents)— currently UUID v4. Required if dispatch must be safely retriable.--worktreeandisolation: worktreefor subagents; reimplementing it adds nothing. The conductor usesgit worktree adddirectly.