use agentix::{
AgentEvent, Content, Message, ReasoningEffort, Request, ToolBundle, UserContent, agent, tool,
};
use futures::StreamExt;
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
struct CallLog(Arc<Mutex<Vec<String>>>);
impl CallLog {
fn push(&self, name: &str) {
self.0.lock().expect("call log poisoned").push(name.into());
}
fn snapshot(&self) -> Vec<String> {
self.0.lock().expect("call log poisoned").clone()
}
}
struct OrderTools {
calls: CallLog,
}
#[tool]
impl agentix::Tool for OrderTools {
async fn get_customer(&self, customer_id: String) -> String {
self.calls.push("get_customer");
format!("{customer_id}: Ada Lovelace, tier=enterprise, home_currency=USD")
}
async fn list_invoices(&self, customer_id: String) -> String {
self.calls.push("list_invoices");
format!("{customer_id}: INV-001 paid, INV-002 open amount=1250.50 USD")
}
async fn get_invoice(&self, invoice_id: String) -> String {
self.calls.push("get_invoice");
format!("{invoice_id}: open balance 1250.50 USD, due 2026-05-30")
}
async fn convert_currency(&self, amount: f64, from: String, to: String) -> String {
self.calls.push("convert_currency");
let converted = if from == "USD" && to == "EUR" {
amount * 0.92
} else {
amount
};
format!("{amount:.2} {from} = {converted:.2} {to}")
}
async fn write_audit_note(&self, summary: String) -> String {
self.calls.push("write_audit_note");
format!("AUDIT-OK: {summary}")
}
}
fn text(content: &[Content]) -> String {
content
.iter()
.filter_map(|p| {
if let Content::Text { text } = p {
Some(text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join(" ")
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let model = std::env::var("AGENTIX_CODEX_MODEL").unwrap_or_else(|_| "gpt-5.5".into());
let http = reqwest::Client::new();
let calls = CallLog::default();
let tools = ToolBundle::default()
+ OrderTools {
calls: calls.clone(),
};
let request = Request::codex()
.model(model)
.reasoning_effort(ReasoningEffort::Low)
.system_prompt(
"You are testing an agent loop. Use the provided tools exactly as \
instructed. Call exactly one tool per assistant turn, then wait \
for its result before calling the next tool. Do not call tools in \
parallel. After the audit tool succeeds, produce a final answer \
containing the marker EXACT_FINAL.",
);
let history = vec![Message::User(vec![UserContent::Text {
text: "Run this five-step workflow for customer C-100: \
1 get_customer, 2 list_invoices, 3 get_invoice for the open \
invoice, 4 convert the open USD balance to EUR, 5 write an \
audit note. Then give the final result with EXACT_FINAL."
.into(),
}])];
let mut stream = agent(tools, http, request, history, Some(50_000));
let mut final_text = String::new();
let mut tool_results = 0usize;
let mut current_batch_tool_calls = 0usize;
let mut batch_has_result = false;
let mut saw_parallel_batch = false;
while let Some(event) = stream.next().await {
match event {
AgentEvent::Token(t) => {
print!("{t}");
final_text.push_str(&t);
}
AgentEvent::Reasoning(t) => print!("\x1b[2m{t}\x1b[0m"),
AgentEvent::ToolCallStart(tc) => {
if batch_has_result {
current_batch_tool_calls = 0;
batch_has_result = false;
}
current_batch_tool_calls += 1;
if current_batch_tool_calls > 1 {
saw_parallel_batch = true;
}
println!("\nCALL {}({})", tc.name, tc.arguments);
}
AgentEvent::ToolResult {
name, ref content, ..
} => {
tool_results += 1;
batch_has_result = true;
println!("RESULT {name}: {}", text(content));
}
AgentEvent::Usage(u) => eprintln!("\n[tokens: {}]", u.total_tokens),
AgentEvent::Done(total) => {
eprintln!("\n[total tokens: {}]", total.total_tokens);
break;
}
AgentEvent::Warning(w) => eprintln!("\n[warn] {w}"),
AgentEvent::Error(e) => return Err(e.into()),
AgentEvent::ToolProgress { .. } | AgentEvent::ToolCallChunk(_) => {}
}
}
println!();
let observed = calls.snapshot();
let expected = vec![
"get_customer",
"list_invoices",
"get_invoice",
"convert_currency",
"write_audit_note",
];
if observed != expected {
return Err(format!("unexpected tool order: expected {expected:?}, got {observed:?}").into());
}
if tool_results < expected.len() {
return Err(format!(
"expected at least {} tool results, got {tool_results}",
expected.len()
)
.into());
}
if saw_parallel_batch {
return Err("Codex called multiple tools in one assistant turn; this example expects one resume boundary per tool".into());
}
if !final_text.contains("EXACT_FINAL") {
return Err("final answer did not contain EXACT_FINAL marker".into());
}
eprintln!("[ok] Codex agent multi-turn workflow completed");
Ok(())
}