use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use serde_json::json;
use tkach::message::{Content, StopReason, Usage};
use tkach::provider::Response;
use tkach::providers::Mock;
use tkach::tools::SubAgent;
use tkach::{Agent, AutoApprove, CancellationToken, Message, ThinkingConfig, ThinkingEffort};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let research_thinking_seen = Arc::new(AtomicUsize::new(0));
let research_thinking_clone = Arc::clone(&research_thinking_seen);
let research_child = Arc::new(Mock::new(move |req| {
if matches!(
req.thinking,
Some(ThinkingConfig::Effort(ThinkingEffort::High))
) {
research_thinking_clone.fetch_add(1, Ordering::SeqCst);
}
Ok(Response {
content: vec![Content::text("research summary")],
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
})
}));
let reasoning_child = Arc::new(Mock::with_text("reasoning answer"));
let writer_child = Arc::new(Mock::with_text("file written"));
let parent_turn = Arc::new(AtomicUsize::new(0));
let parent_turn_clone = Arc::clone(&parent_turn);
let parent = Mock::new(move |_req| {
let turn = parent_turn_clone.fetch_add(1, Ordering::SeqCst);
match turn {
0 => Ok(Response {
content: vec![Content::ToolUse {
id: "call-research".into(),
name: "research".into(),
input: json!({"prompt": "Summarise the repository"}),
}],
stop_reason: StopReason::ToolUse,
usage: Usage::default(),
}),
1 => Ok(Response {
content: vec![Content::ToolUse {
id: "call-reasoning".into(),
name: "reasoning".into(),
input: json!({"prompt": "Decide next step"}),
}],
stop_reason: StopReason::ToolUse,
usage: Usage::default(),
}),
2 => Ok(Response {
content: vec![Content::ToolUse {
id: "call-writer".into(),
name: "writer".into(),
input: json!({"prompt": "Write the result"}),
}],
stop_reason: StopReason::ToolUse,
usage: Usage::default(),
}),
_ => Ok(Response {
content: vec![Content::text("done")],
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
}),
}
});
let research = SubAgent::new(research_child, "mock-haiku")
.name("research")
.description("Read-only repository research helper")
.tools_allow(["read", "glob", "grep", "web_fetch"])
.filter_tool_definitions(true)
.thinking(ThinkingConfig::Effort(ThinkingEffort::High));
let reasoning = SubAgent::new(reasoning_child, "mock-sonnet")
.name("reasoning")
.description("Autonomous reasoning, no human-in-the-loop prompts")
.approval_handler(Arc::new(AutoApprove));
let trace_count = Arc::new(AtomicUsize::new(0));
let trace_count_clone = Arc::clone(&trace_count);
let writer = SubAgent::new(writer_child, "mock-sonnet")
.name("writer")
.description("Mutating writer with full trace observability")
.tools_allow(["read", "edit", "write", "bash"])
.trace_hook(move |_ev| {
trace_count_clone.fetch_add(1, Ordering::SeqCst);
});
let agent = Agent::builder()
.provider(parent)
.model("mock-parent")
.tools(tkach::tools::defaults())
.tool(research)
.tool(reasoning)
.tool(writer)
.working_dir(std::env::current_dir()?)
.build()?;
let result = agent
.run(
vec![Message::user_text("delegate to all three subagents")],
CancellationToken::new(),
)
.await?;
assert_eq!(result.text, "done");
assert_eq!(parent_turn.load(Ordering::SeqCst), 4);
assert_eq!(
research_thinking_seen.load(Ordering::SeqCst),
1,
"research child must observe Request.thinking = Effort(High)"
);
assert!(
trace_count.load(Ordering::SeqCst) >= 4,
"writer trace_hook must fire for ContentDelta + MessageDelta + Usage + Done; got {}",
trace_count.load(Ordering::SeqCst)
);
println!(
"specialised sub-agents completed: result={}, trace_events={}",
result.text,
trace_count.load(Ordering::SeqCst)
);
Ok(())
}