use std::sync::Arc;
use std::time::{Duration, Instant};
use serde_json::{Value, json};
use tkach::message::{Content, StopReason, Usage};
use tkach::provider::Response;
use tkach::providers::Mock;
use tkach::tools::SubAgent;
use tkach::{
Agent, CancellationToken, Message, Tool, ToolClass, ToolConcurrency, ToolContext, ToolError,
ToolOutput,
};
struct SlowReader {
delay_ms: u64,
}
#[async_trait::async_trait]
impl Tool for SlowReader {
fn name(&self) -> &str {
"slow_reader"
}
fn description(&self) -> &str {
"ReadOnly tool that sleeps then echoes its label"
}
fn input_schema(&self) -> Value {
json!({ "type": "object", "properties": { "label": { "type": "string" } } })
}
fn class(&self) -> ToolClass {
ToolClass::ReadOnly
}
async fn execute(&self, input: Value, _ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
let label = input["label"].as_str().unwrap_or("anon").to_string();
tokio::time::sleep(Duration::from_millis(self.delay_ms)).await;
Ok(ToolOutput::text(format!("read[{label}]")))
}
}
#[tokio::main]
async fn main() {
let inner_delay_ms: u64 = 200;
let sub_provider: Arc<dyn tkach::LlmProvider> = Arc::new(Mock::new(move |req| {
let has_tool_result = req.messages.iter().any(|m| {
m.content
.iter()
.any(|c| matches!(c, Content::ToolResult { .. }))
});
if has_tool_result {
return Ok(Response {
content: vec![Content::text("research summary")],
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
});
}
let label = req
.messages
.iter()
.find_map(|m| {
m.content.iter().find_map(|c| match c {
Content::Text { text, .. } => Some(text.clone()),
_ => None,
})
})
.unwrap_or_else(|| "anon".to_string());
Ok(Response {
content: vec![Content::ToolUse {
id: "r1".into(),
name: "slow_reader".into(),
input: json!({ "label": label }),
}],
stop_reason: StopReason::ToolUse,
usage: Usage::default(),
})
}));
let parent_mock = Mock::new(move |req| {
let has_results = req.messages.iter().any(|m| {
m.content
.iter()
.any(|c| matches!(c, Content::ToolResult { .. }))
});
if has_results {
return Ok(Response {
content: vec![Content::text("all three sub-agents done")],
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
});
}
Ok(Response {
content: vec![
Content::ToolUse {
id: "a1".into(),
name: "agent".into(),
input: json!({ "prompt": "topic-A" }),
},
Content::ToolUse {
id: "a2".into(),
name: "agent".into(),
input: json!({ "prompt": "topic-B" }),
},
Content::ToolUse {
id: "a3".into(),
name: "agent".into(),
input: json!({ "prompt": "topic-C" }),
},
],
stop_reason: StopReason::ToolUse,
usage: Usage::default(),
})
});
let agent = Agent::builder()
.provider(parent_mock)
.model("mock-parent")
.tool(SlowReader {
delay_ms: inner_delay_ms,
})
.tool(SubAgent::new(Arc::clone(&sub_provider), "mock-sub").max_turns(3))
.tool_concurrency("agent", ToolConcurrency::on())
.build()
.unwrap();
let started = Instant::now();
let result = agent
.run(
vec![Message::user_text("delegate to three sub-agents")],
CancellationToken::new(),
)
.await
.expect("agent run");
let elapsed = started.elapsed();
let parallel_threshold = Duration::from_millis(inner_delay_ms + (inner_delay_ms * 3 / 4));
assert!(
elapsed < parallel_threshold,
"expected parallel wall-time (< {parallel_threshold:?}), got {elapsed:?} \
— looks like sub-agents serialised through the width-1 pool"
);
println!("result: {}", result.text);
println!("delta messages: {}", result.new_messages.len());
println!("wall time: {elapsed:?} (per-sub-agent inner delay: {inner_delay_ms}ms)");
println!();
println!("Without tool_concurrency('agent', on()), this batch would take ≈ 3× inner delay.");
println!("With the promotion, all three sub-agents share the concurrent-mutator pool.");
}