//! A background-agent orchestrator, gated: run a batch of coding tasks across a //! heterogeneous fleet of coding-agent backends — each in its own isolated //! sandbox, on its own branch — where every dispatch is capability-gated, //! joule-budgeted, and sealed **before** the agent runs. //! //! This is the shape the cloud background-agent products converged on (tracker → //! branch → dispatch → PR). The orchestrator is blind to which backend runs a //! task; it only knows each backend quotes a joule cost and reports an outcome. //! What it adds over a bare meta-harness is the one artifact none of them //! produce: an ordered, third-party-verifiable ledger of every decision — who was //! authorised to run what, for how many joules, and how it turned out. //! //! The batch is a **pipeline, not a pool**: the AST work waits for the tokenizer, //! the review waits for both, the ship waits for the review, and the announcement //! waits for the ship. Every edge is one task id naming another in this run — no //! endpoint in anyone else's system, so there is no boundary for a renamed field //! or a stale credential to break. The ship is refused for want of a capability, //! and the announcement behind it is therefore never started at all. //! //! ```text //! cargo run -p jcp-orchestrate --example background_agents //! ``` use std::cell::Cell; use ed25519_dalek::SigningKey; use jcp_orchestrate::{Harness, HarnessOutcome, Orchestrator, Task}; /// A coding-agent backend that completes at a fixed quoted cost. Stands in for /// any agent CLI/framework running in its own sandbox container — the /// orchestrator neither knows nor cares which. struct Backend { id: String, kinds: Vec, cost_uj: u64, } impl Harness for Backend { fn id(&self) -> &str { &self.id } fn handles(&self, kind: &str) -> bool { self.kinds.iter().any(|k| k == kind) } fn estimate(&self, _task: &Task) -> u64 { self.cost_uj } fn run(&self, _task: &Task) -> HarnessOutcome { HarnessOutcome::done(self.cost_uj) } } /// A backend whose sandbox flakes `fails` times (transient container/network /// failure) before the task lands — the bounded-retry path. struct Flaky { id: String, kind: String, cost_uj: u64, fails: Cell, } impl Harness for Flaky { fn id(&self) -> &str { &self.id } fn handles(&self, kind: &str) -> bool { kind == self.kind } fn estimate(&self, _task: &Task) -> u64 { self.cost_uj } fn run(&self, _task: &Task) -> HarnessOutcome { let left = self.fails.get(); if left > 0 { self.fails.set(left - 1); HarnessOutcome::retry(self.cost_uj, "sandbox cold-start timeout") } else { HarnessOutcome::done(self.cost_uj) } } } fn main() { // The principal that authorises the fleet, and the steward that seals every // decision. A third party trusts the steward key, not the orchestrator. let mut fleet = Orchestrator::new( SigningKey::from_bytes(&[1u8; 32]), SigningKey::from_bytes(&[7u8; 32]), "did:example:background-agents", 3, // max attempts per item ); // Two backends can `implement`; the first has budget for two tasks, then the // fleet fails over to the second. A `review` backend flakes twice before it // lands. Nothing is granted `deploy` — a capability no backend holds. fleet .register(Box::new(Backend { id: "coder-alpha".into(), kinds: vec!["implement".into()], cost_uj: 120 }), &["implement"], 300) .unwrap(); fleet .register(Box::new(Backend { id: "coder-beta".into(), kinds: vec!["implement".into()], cost_uj: 120 }), &["implement"], 1_000) .unwrap(); fleet .register(Box::new(Flaky { id: "reviewer".into(), kind: "review".into(), cost_uj: 90, fails: Cell::new(2) }), &["review"], 1_000) .unwrap(); // The pipeline. Submitted in no particular order — the edges, not the queue, // decide what runs when. // // tokenize ──┬──► ast ────┐ // └──► errors ─┴──► review ──► ship ──► announce // (denied) (blocked) // // `review` is the one task declared replay-safe: its backend flakes on a cold // sandbox, and a retry is honoured only for a task that names what makes the // second run the same work as the first. Reviewing a PR twice under the same // key leaves one review, so it can say so. fleet.submit(Task::new("release/announce", "notify", b"post the release note".to_vec()).after(["ops/ship-prod"])); fleet.submit(Task::new("ops/ship-prod", "deploy", b"push to production".to_vec()).after(["review/pr-482"])); fleet.submit( Task::new("review/pr-482", "review", b"review the tokenizer PR".to_vec()) .after(["feat/parser-ast", "feat/parser-errors"]) .idempotent("review/pr-482@head"), ); fleet.submit(Task::new("feat/parser-ast", "implement", b"build the AST layer".to_vec()).after(["feat/parser-tokenize"])); fleet.submit(Task::new("feat/parser-errors", "implement", b"structured parse errors".to_vec()).after(["feat/parser-tokenize"])); fleet.submit(Task::new("feat/parser-tokenize", "implement", b"add tokenizer to the parser".to_vec())); let report = fleet.run(); println!("── run report ──"); println!(" completed : {}", report.done); println!(" denied : {} (no backend holds that capability)", report.denied); println!(" blocked : {} (a step it waited on never completed)", report.blocked); println!(" failed : {}", report.failed); println!(" authorised: {} µJ across the fleet", report.total_uj); println!(" sealed : {} decision receipts", report.receipts); println!("\n── per branch ──"); for id in [ "feat/parser-tokenize", "feat/parser-ast", "feat/parser-errors", "review/pr-482", "ops/ship-prod", "release/announce", ] { println!(" {id:22} → {:?}", fleet.state_of(id).unwrap()); } println!("\n── joules per backend ──"); for b in ["coder-alpha", "coder-beta", "reviewer"] { println!(" {b:12} {} µJ", fleet.spent_uj(b)); } // The whole run is one ordered, verifiable ledger — the audit a bare // background-agent orchestrator has no way to produce. Every allow was sealed // before its backend ran, and so was every deny: `coder-alpha` refusing the // third branch for want of joules is on the record, not silently skipped. // Two items seal nothing, for two different reasons. `ops/ship-prod` was // refused because no backend holds `deploy`: there was no grant to decide // against. `release/announce` was never offered to anyone — the step it // waited on did not complete, so no grant was consulted and no joules were // spent. A pool would have run the announcement anyway. let steward = fleet.steward().to_string(); let pk = fleet.steward_pubkey(); let verified = fleet.receipts().iter().filter(|r| jcp_receipt::extract(r, &steward, &pk).is_ok()).count(); println!("\naudit: {verified}/{} sealed decisions verify under the steward key", fleet.receipts().len()); }