use std::sync::atomic::{AtomicUsize, Ordering};
use io_harness::provider::{CompletionRequest, CompletionResponse, ToolCall};
use io_harness::{
run_tree, run_with, ApproveAll, Containment, DenyAll, Policy, Provider, RunOutcome, Store,
TaskContract, Verification,
};
use serde_json::json;
struct MockScript {
steps: Vec<Vec<ToolCall>>,
at: AtomicUsize,
}
impl MockScript {
fn new(steps: Vec<Vec<ToolCall>>) -> Self {
Self {
steps,
at: AtomicUsize::new(0),
}
}
}
impl Provider for MockScript {
async fn complete(&self, _req: CompletionRequest) -> io_harness::Result<CompletionResponse> {
let i = self.at.fetch_add(1, Ordering::SeqCst);
Ok(CompletionResponse {
tool_calls: self.steps.get(i).cloned().unwrap_or_default(),
..Default::default()
})
}
}
fn call(name: &str, args: serde_json::Value) -> ToolCall {
ToolCall {
name: name.into(),
arguments: args,
}
}
fn spawn(goal: &str, file: &str, needle: &str) -> ToolCall {
call(
"spawn_agent",
json!({ "goal": goal, "verify_file": file, "verify_contains": needle }),
)
}
fn write(path: &str, content: &str) -> ToolCall {
call("write_file", json!({ "path": path, "content": content }))
}
fn ws() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
fn containment() -> Containment {
Containment::new(10, 4, 3, 1_000_000)
}
#[tokio::test]
async fn a_parent_spawns_three_children_over_a_shared_workspace() {
let dir = ws();
let contract = TaskContract::workspace(
"Split the work across three sub-agents, then combine.",
dir.path(),
Verification::WorkspaceFileContains {
file: "combined.txt".into(),
needle: "abc".into(),
},
)
.with_max_steps(6);
let script = MockScript::new(vec![
vec![spawn("write a", "a.txt", "A")],
vec![write("a.txt", "A")],
vec![spawn("write b", "b.txt", "B")],
vec![write("b.txt", "B")],
vec![spawn("write c", "c.txt", "C")],
vec![write("c.txt", "C")],
vec![write("combined.txt", "abc")],
]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&script,
&store,
&Policy::permissive(),
&ApproveAll,
&containment(),
)
.await
.unwrap();
assert_eq!(result.outcome, RunOutcome::Success { steps: 4 });
for (f, c) in [
("a.txt", "A"),
("b.txt", "B"),
("c.txt", "C"),
("combined.txt", "abc"),
] {
assert_eq!(std::fs::read_to_string(dir.path().join(f)).unwrap(), c);
}
let children = store.children(result.run_id).unwrap();
assert_eq!(children.len(), 3, "three child runs recorded");
let spawns = store
.agent_events(result.run_id)
.unwrap()
.into_iter()
.filter(|e| e.kind == "spawn")
.count();
assert_eq!(spawns, 3, "three spawn edges recorded");
}
#[tokio::test]
async fn spawn_agent_composes_the_childs_result_back() {
let dir = ws();
let contract = TaskContract::workspace(
"Delegate producing the answer to a sub-agent.",
dir.path(),
Verification::WorkspaceFileContains {
file: "result.txt".into(),
needle: "42".into(),
},
);
let script = MockScript::new(vec![
vec![spawn("produce the answer", "result.txt", "42")],
vec![write("result.txt", "42")], ]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&script,
&store,
&Policy::permissive(),
&ApproveAll,
&containment(),
)
.await
.unwrap();
assert_eq!(result.outcome, RunOutcome::Success { steps: 1 });
assert_eq!(
std::fs::read_to_string(dir.path().join("result.txt")).unwrap(),
"42"
);
assert_eq!(store.children(result.run_id).unwrap().len(), 1);
}
#[tokio::test]
async fn a_spawn_beyond_the_agent_cap_is_refused_but_the_rest_complete() {
let dir = ws();
let cap3 = Containment::new(3, 4, 3, 1_000_000);
let contract = TaskContract::workspace(
"Try to spawn three children under an agent cap of three.",
dir.path(),
Verification::WorkspaceFileContains {
file: "done.txt".into(),
needle: "ok".into(),
},
)
.with_max_steps(6);
let script = MockScript::new(vec![
vec![spawn("write a", "a.txt", "A")],
vec![write("a.txt", "A")],
vec![spawn("write b", "b.txt", "B")],
vec![write("b.txt", "B")],
vec![spawn("write c", "c.txt", "C")], vec![write("done.txt", "ok")],
]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&script,
&store,
&Policy::permissive(),
&ApproveAll,
&cap3,
)
.await
.unwrap();
assert_eq!(result.outcome, RunOutcome::Success { steps: 4 });
assert_eq!(store.children(result.run_id).unwrap().len(), 2);
assert!(!dir.path().join("c.txt").exists());
let refused: Vec<_> = store
.agent_events(result.run_id)
.unwrap()
.into_iter()
.filter(|e| e.kind == "spawn_refused")
.collect();
assert_eq!(refused.len(), 1);
assert_eq!(refused[0].detail.as_deref(), Some("agents"));
}
#[tokio::test]
async fn a_spawn_beyond_max_depth_is_refused_and_depth_counts_from_the_root() {
let dir = ws();
let depth2 = Containment::new(10, 4, 2, 1_000_000);
let contract = TaskContract::workspace(
"Nest agents until the depth cap refuses the deepest spawn.",
dir.path(),
Verification::WorkspaceFileContains {
file: "top.txt".into(),
needle: "T".into(),
},
)
.with_max_steps(4);
let script = MockScript::new(vec![
vec![spawn("child", "child.txt", "C")], vec![spawn("grandchild", "gc.txt", "G")], vec![spawn("great-grandchild", "ggc.txt", "X")], vec![write("gc.txt", "G")], vec![write("child.txt", "C")], vec![write("top.txt", "T")], ]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&script,
&store,
&Policy::permissive(),
&ApproveAll,
&depth2,
)
.await
.unwrap();
assert_eq!(result.outcome, RunOutcome::Success { steps: 2 });
let child = store.children(result.run_id).unwrap()[0];
let grandchild = store.children(child).unwrap()[0];
assert_eq!(store.depth(grandchild).unwrap(), 2);
assert!(
store.children(grandchild).unwrap().is_empty(),
"the refused spawn created no run"
);
assert!(!dir.path().join("ggc.txt").exists());
let refused: Vec<_> = store
.agent_events(grandchild)
.unwrap()
.into_iter()
.filter(|e| e.kind == "spawn_refused")
.collect();
assert_eq!(refused.len(), 1);
assert_eq!(refused[0].detail.as_deref(), Some("depth"));
}
#[tokio::test]
async fn one_approver_serves_the_whole_tree_approve() {
let dir = ws();
let contract = TaskContract::workspace(
"Delegate a sensitive write to a child.",
dir.path(),
Verification::WorkspaceFileContains {
file: "out.txt".into(),
needle: "OK".into(),
},
)
.with_max_steps(2);
let script = MockScript::new(vec![
vec![spawn("write out", "out.txt", "OK")],
vec![write("out.txt", "OK")], ]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&script,
&store,
&Policy::default(),
&ApproveAll,
&containment(),
)
.await
.unwrap();
assert_eq!(result.outcome, RunOutcome::Success { steps: 1 });
let child = store.children(result.run_id).unwrap()[0];
let approved = store
.events(child)
.unwrap()
.into_iter()
.filter(|e| e.decision.as_deref() == Some("approve"))
.count();
assert!(approved >= 1, "the child's sensitive write was approved");
}
#[tokio::test]
async fn one_approver_serves_the_whole_tree_deny() {
let dir = ws();
let contract = TaskContract::workspace(
"Delegate a sensitive write the approver will refuse.",
dir.path(),
Verification::WorkspaceFileContains {
file: "out.txt".into(),
needle: "OK".into(),
},
)
.with_max_steps(2);
let script = MockScript::new(vec![
vec![call(
"spawn_agent",
json!({ "goal": "write out", "verify_file": "out.txt", "verify_contains": "OK", "max_steps": 2 }),
)],
vec![write("out.txt", "OK")],
vec![write("out.txt", "OK")],
]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&script,
&store,
&Policy::default(),
&DenyAll,
&containment(),
)
.await
.unwrap();
assert!(matches!(result.outcome, RunOutcome::StepCapReached { .. }));
assert!(!dir.path().join("out.txt").exists());
let child = store.children(result.run_id).unwrap()[0];
let denied = store
.events(child)
.unwrap()
.into_iter()
.filter(|e| e.decision.as_deref() == Some("deny"))
.count();
assert!(
denied >= 1,
"the child's write was denied by the tree's approver"
);
}
#[tokio::test]
async fn sub_agents_are_opt_in_a_plain_run_never_exposes_spawn() {
let dir = ws();
let contract = TaskContract::workspace(
"A plain workspace run that ignores a stray spawn call.",
dir.path(),
Verification::WorkspaceFileContains {
file: "f.txt".into(),
needle: "V".into(),
},
)
.with_max_steps(3);
let script = MockScript::new(vec![
vec![spawn("should be inert", "f.txt", "V")], vec![write("f.txt", "V")],
]);
let store = Store::memory().unwrap();
let result = run_with(
&contract,
&script,
&store,
&Policy::permissive(),
&ApproveAll,
)
.await
.unwrap();
assert_eq!(result.outcome, RunOutcome::Success { steps: 2 });
assert!(store.children(result.run_id).unwrap().is_empty());
assert!(store.agent_events(result.run_id).unwrap().is_empty());
}
use io_harness::provider::Usage;
use std::sync::atomic::AtomicUsize as Counter;
struct ConcurrencyProbe {
fanout: usize,
active: Counter,
peak: Counter,
}
impl ConcurrencyProbe {
fn new(fanout: usize) -> Self {
Self {
fanout,
active: Counter::new(0),
peak: Counter::new(0),
}
}
}
impl Provider for ConcurrencyProbe {
async fn complete(&self, req: CompletionRequest) -> io_harness::Result<CompletionResponse> {
let now = self.active.fetch_add(1, Ordering::SeqCst) + 1;
self.peak.fetch_max(now, Ordering::SeqCst);
tokio::task::yield_now().await; self.active.fetch_sub(1, Ordering::SeqCst);
let is_parent = req.user.contains("ROOT-FANOUT");
let usage = Some(Usage {
total_tokens: 1,
..Default::default()
});
if is_parent {
let calls: Vec<ToolCall> = (0..self.fanout)
.map(|i| {
call(
"spawn_agent",
json!({
"goal": format!("c{i}"),
"verify_file": format!("c{i}.txt"),
"verify_contains": "never-satisfied",
"max_steps": 1
}),
)
})
.collect();
Ok(CompletionResponse {
tool_calls: calls,
usage,
..Default::default()
})
} else {
Ok(CompletionResponse {
usage,
..Default::default()
})
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_fanout_to_over_100_stays_bounded_and_completes() {
let dir = ws();
let fanout = 120usize;
let containment = Containment::new(fanout as u32 + 1, 16, 3, 10_000_000);
let contract = TaskContract::workspace(
"ROOT-FANOUT: spawn a large fleet of sub-agents at once.",
dir.path(),
Verification::WorkspaceFileContains {
file: "never.txt".into(),
needle: "never".into(),
},
)
.with_max_steps(1);
let probe = ConcurrencyProbe::new(fanout);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&probe,
&store,
&Policy::permissive(),
&ApproveAll,
&containment,
)
.await
.unwrap();
assert!(matches!(result.outcome, RunOutcome::StepCapReached { .. }));
assert_eq!(store.children(result.run_id).unwrap().len(), fanout);
let peak = probe.peak.load(Ordering::SeqCst);
assert!(peak > 1, "children actually overlapped (peak {peak})");
assert!(
peak <= 16,
"never more than max_concurrent at once (peak {peak})"
);
}
struct TokenBurner {
children: usize,
per_call: u64,
}
impl Provider for TokenBurner {
async fn complete(&self, req: CompletionRequest) -> io_harness::Result<CompletionResponse> {
let usage = Some(Usage {
total_tokens: self.per_call,
..Default::default()
});
if req.user.contains("BUDGET-ROOT") {
let calls = (0..self.children)
.map(|i| {
call(
"spawn_agent",
json!({ "goal": format!("burn {i}"), "verify_file": format!("b{i}.txt"), "verify_contains": "nope", "max_steps": 1 }),
)
})
.collect();
Ok(CompletionResponse {
tool_calls: calls,
usage,
..Default::default()
})
} else {
Ok(CompletionResponse {
usage,
..Default::default()
})
}
}
}
#[tokio::test]
async fn the_aggregate_ceiling_halts_the_whole_tree() {
let dir = ws();
let containment = Containment::new(10, 1, 3, 100);
let contract = TaskContract::workspace(
"BUDGET-ROOT: spawn children until the tree's spend ceiling stops it.",
dir.path(),
Verification::WorkspaceFileContains {
file: "x.txt".into(),
needle: "x".into(),
},
)
.with_max_steps(2);
let provider = TokenBurner {
children: 4,
per_call: 30,
};
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&provider,
&store,
&Policy::permissive(),
&ApproveAll,
&containment,
)
.await
.unwrap();
assert!(
matches!(result.outcome, RunOutcome::BudgetCeilingReached { .. }),
"got {:?}",
result.outcome
);
let root_draws: u64 = store
.agent_events(result.run_id)
.unwrap()
.into_iter()
.filter_map(|e| {
if e.kind == "budget_draw" {
e.tokens
} else {
None
}
})
.sum();
assert!(root_draws <= 100);
}
use io_harness::approve::{Approver, Decision, DecisionFuture, Request};
use io_harness::resume_with_decision;
struct DeferOnce {
asked: Counter,
}
impl Approver for DeferOnce {
fn decide<'a>(&'a self, _r: &'a Request) -> DecisionFuture<'a> {
let first = self.asked.fetch_add(1, Ordering::SeqCst) == 0;
Box::pin(async move {
if first {
Decision::Defer
} else {
Decision::approve()
}
})
}
}
#[tokio::test]
async fn a_child_defer_persists_and_resumes_via_resume_with_decision() {
let dir = ws();
let contract = TaskContract::workspace(
"Delegate a sensitive write; the child will defer to a human.",
dir.path(),
Verification::WorkspaceFileContains {
file: "out.txt".into(),
needle: "OK".into(),
},
)
.with_max_steps(3);
let script = MockScript::new(vec![
vec![call(
"spawn_agent",
json!({ "goal": "write out", "verify_file": "out.txt", "verify_contains": "OK" }),
)],
vec![write("out.txt", "OK")], ]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&script,
&store,
&Policy::default(),
&DeferOnce {
asked: Counter::new(0),
},
&containment(),
)
.await
.unwrap();
let request_id = match result.outcome {
RunOutcome::AwaitingApproval { request_id, .. } => request_id,
other => panic!("expected AwaitingApproval, got {other:?}"),
};
assert!(!dir.path().join("out.txt").exists());
let pending = store
.pending(request_id)
.unwrap()
.expect("pending survives");
assert_eq!(pending.act, "write");
assert_eq!(pending.target, "out.txt");
let child_run = pending.run_id;
assert_eq!(store.parent(child_run).unwrap(), Some(result.run_id));
let child_contract = TaskContract::workspace(
"write out",
dir.path(),
Verification::WorkspaceFileContains {
file: "out.txt".into(),
needle: "OK".into(),
},
);
let resumed = resume_with_decision(
&child_contract,
&script,
&store,
child_run,
request_id,
Decision::approve(),
&Policy::default(),
&ApproveAll,
)
.await
.unwrap();
assert!(matches!(resumed.outcome, RunOutcome::Success { .. }));
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).unwrap(),
"OK"
);
}
struct Slow {
per_call: std::time::Duration,
}
impl Provider for Slow {
async fn complete(&self, _req: CompletionRequest) -> io_harness::Result<CompletionResponse> {
tokio::time::sleep(self.per_call).await;
Ok(CompletionResponse::default())
}
}
#[tokio::test]
async fn a_tree_halts_on_its_duration_ceiling_instead_of_ignoring_it() {
let dir = ws();
let mut containment = Containment::new(10, 1, 3, 1_000_000);
containment.max_total_duration = Some(std::time::Duration::from_millis(200));
let contract = TaskContract::workspace(
"Run until something stops you.",
dir.path(),
Verification::WorkspaceFileContains {
file: "never.txt".into(),
needle: "never".into(),
},
)
.with_max_steps(20);
let provider = Slow {
per_call: std::time::Duration::from_millis(400),
};
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&provider,
&store,
&Policy::permissive(),
&ApproveAll,
&containment,
)
.await
.unwrap();
let steps = match result.outcome {
RunOutcome::BudgetCeilingReached { steps } => steps,
other => panic!("expected the duration ceiling to halt the tree, got {other:?}"),
};
assert!(
steps < 20,
"the ceiling must stop the tree well short of its step cap, stopped at {steps}"
);
}
#[tokio::test]
async fn a_tree_with_no_duration_ceiling_is_unaffected() {
let dir = ws();
let containment = Containment::new(10, 1, 3, 1_000_000);
assert!(containment.max_total_duration.is_none());
let contract = TaskContract::workspace(
"Run until something stops you.",
dir.path(),
Verification::WorkspaceFileContains {
file: "never.txt".into(),
needle: "never".into(),
},
)
.with_max_steps(3);
let provider = Slow {
per_call: std::time::Duration::from_millis(50),
};
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&provider,
&store,
&Policy::permissive(),
&ApproveAll,
&containment,
)
.await
.unwrap();
assert!(
matches!(result.outcome, RunOutcome::StepCapReached { steps: 3 }),
"got {:?}",
result.outcome
);
}
struct Staggered {
children: usize,
}
impl Provider for Staggered {
async fn complete(&self, req: CompletionRequest) -> io_harness::Result<CompletionResponse> {
if req.user.contains("ORDER-ROOT") {
let calls = (0..self.children)
.map(|i| {
call(
"spawn_agent",
json!({
"goal": format!("ordered-{i}"),
"verify_file": format!("o{i}.txt"),
"verify_contains": "nope",
"max_steps": 1
}),
)
})
.collect();
return Ok(CompletionResponse {
tool_calls: calls,
..Default::default()
});
}
for i in 0..self.children {
if req.user.contains(&format!("ordered-{i}")) {
let ms = (self.children - i) as u64 * 120;
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
break;
}
}
Ok(CompletionResponse::default())
}
}
#[tokio::test]
async fn children_are_composed_in_spawn_order_not_completion_order() {
let dir = ws();
let n = 4usize;
let containment = Containment::new(10, 4, 3, 1_000_000);
let contract = TaskContract::workspace(
"ORDER-ROOT: spawn several children that finish out of order.",
dir.path(),
Verification::WorkspaceFileContains {
file: "never.txt".into(),
needle: "never".into(),
},
)
.with_max_steps(1);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&Staggered { children: n },
&store,
&Policy::permissive(),
&ApproveAll,
&containment,
)
.await
.unwrap();
let root_step = store
.steps(result.run_id)
.unwrap()
.into_iter()
.find(|s| s.step == 1)
.expect("the root's spawning step");
for text in [&root_step.result, &root_step.decision] {
let mut positions = Vec::new();
for i in 0..n {
let needle = format!("ordered-{i}");
match text.find(&needle) {
Some(at) => positions.push((i, at)),
None => break,
}
}
if positions.len() == n {
let mut sorted = positions.clone();
sorted.sort_by_key(|(_, at)| *at);
assert_eq!(
positions, sorted,
"children must appear in spawn order; got {positions:?} in {text:?}"
);
}
}
let children = store.children(result.run_id).unwrap();
assert_eq!(children.len(), n, "all children were spawned");
let mut ascending = children.clone();
ascending.sort_unstable();
assert_eq!(
children, ascending,
"child run ids must be allocated in spawn order"
);
}
#[tokio::test]
async fn each_agent_keeps_its_own_durable_ledger_under_its_own_run_id() {
let dir = ws();
std::fs::write(dir.path().join("seed.txt"), "SEED-CONTENT").unwrap();
let contract = TaskContract::workspace(
"delegate a read and a write",
dir.path(),
Verification::WorkspaceFileContains {
file: "out.txt".into(),
needle: "OK".into(),
},
)
.with_max_steps(3);
let script = MockScript::new(vec![
vec![call(
"spawn_agent",
json!({ "goal": "read the seed then write out", "verify_file": "out.txt", "verify_contains": "OK" }),
)],
vec![call("read_file", json!({ "path": "seed.txt" }))],
vec![write("out.txt", "OK")],
]);
let store = Store::memory().unwrap();
let result = run_tree(
&contract,
&script,
&store,
&Policy::permissive(),
&ApproveAll,
&containment(),
)
.await
.unwrap();
let ids = store.tree_run_ids(result.run_id).unwrap();
let children: Vec<i64> = ids.into_iter().filter(|id| *id != result.run_id).collect();
assert!(!children.is_empty(), "the tree spawned at least one child");
let child_rows = store.observations(children[0]).unwrap();
assert!(
child_rows.iter().any(|o| o.text.contains("SEED-CONTENT")),
"the child's own observations are durable under the child's run id, got {child_rows:?}"
);
assert!(
!store
.observations(result.run_id)
.unwrap()
.iter()
.any(|o| o.text.contains("SEED-CONTENT")),
"a child's observations belong to the child, not to its parent's ledger"
);
}