mod support;
use std::sync::{Arc, Mutex};
use support::*;
use yoagent::extension::*;
use yoagent::*;
type Sink = Arc<Mutex<Vec<serde_json::Value>>>;
struct Audit(Sink);
struct AuditRun {
run_id: String,
label: Option<String>,
records: Mutex<Vec<serde_json::Value>>,
sink: Sink,
}
#[async_trait::async_trait]
impl Extension for Audit {
fn name(&self) -> &str {
"audit"
}
async fn start_run(&self, run: &RunContext<'_>) -> Result<Box<dyn RunHooks>, ExtensionError> {
Ok(Box::new(AuditRun {
run_id: run.run_id.to_string(),
label: run.label.map(String::from),
records: Mutex::default(),
sink: self.0.clone(),
}))
}
}
#[async_trait::async_trait]
impl RunHooks for AuditRun {
fn on_event(&self, event: &AgentEvent) {
let record = match event {
AgentEvent::ToolExecutionStart {
tool_name, args, ..
} => {
serde_json::json!({ "event": "tool_call", "tool": tool_name, "args": args })
}
AgentEvent::ToolExecutionEnd {
tool_name,
is_error,
..
} => serde_json::json!({ "event": "tool_done", "tool": tool_name, "error": is_error }),
AgentEvent::MessageEnd {
message: AgentMessage::Llm(Message::Assistant { usage, model, .. }),
} => serde_json::json!({
"event": "model", "model": model, "input": usage.input, "output": usage.output
}),
AgentEvent::InputRejected { reason } => {
serde_json::json!({ "event": "rejected", "reason": reason })
}
_ => return,
};
self.records.lock().unwrap().push(record);
}
async fn finish(&mut self, outcome: &RunOutcome) {
let end = match outcome.end() {
RunEnd::Completed => "completed".to_string(),
RunEnd::Stopped { reason } => format!("stopped: {reason}"),
RunEnd::Rejected { reason } => format!("rejected: {reason}"),
RunEnd::Cancelled => "cancelled".to_string(),
RunEnd::Failed { error, .. } => format!("failed: {error}"),
other => format!("{other:?}"),
};
let mut records = std::mem::take(&mut *self.records.lock().unwrap());
records.push(serde_json::json!({ "event": "end", "outcome": end }));
for mut record in records {
record["run"] = self.run_id[..8].into();
record["label"] = self.label.clone().into();
println!("{record}");
self.sink.lock().unwrap().push(record);
}
}
}
#[derive(Clone)]
struct NoPasswords;
#[async_trait::async_trait]
impl RunHooks for NoPasswords {
async fn on_input(&mut self, input: &InputContext<'_>) -> InputDecision {
if input.text.contains("password=") {
InputDecision::Reject("the prompt contains a password".into())
} else {
InputDecision::Pass
}
}
}
struct Now;
#[async_trait::async_trait]
impl AgentTool for Now {
fn name(&self) -> &str {
"now"
}
fn label(&self) -> &str {
"Now"
}
fn description(&self) -> &str {
"The current time (simulated)"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({ "type": "object", "properties": {} })
}
async fn execute(&self, _: serde_json::Value, _: ToolContext) -> Result<ToolResult, ToolError> {
Ok(ToolResult {
content: vec![Content::Text {
text: "2026-10-07T09:00:00Z".into(),
}],
details: serde_json::Value::Null,
})
}
}
#[tokio::main]
async fn main() {
let sink: Sink = Arc::default();
let script = vec![
call("now", serde_json::json!({})),
answer("It is 09:00 UTC."),
];
let mut agent = new_agent(script)
.with_tools(vec![Box::new(Now)])
.with_run_label("session-42")
.with_extension(Audit(sink.clone()))
.with_extension(ClonedHooks::new("no-passwords", NoPasswords));
run(&mut agent, "What time is it? Use the `now` tool.").await;
run(&mut agent, "Log in with password=hunter2").await;
let sink = sink.lock().unwrap();
let kinds: Vec<&str> = sink.iter().filter_map(|r| r["event"].as_str()).collect();
let ends: Vec<&str> = sink.iter().filter_map(|r| r["outcome"].as_str()).collect();
let runs: std::collections::HashSet<&str> =
sink.iter().filter_map(|r| r["run"].as_str()).collect();
println!();
check(
kinds
== [
"model",
"tool_call",
"tool_done",
"model",
"end",
"rejected",
"end",
],
"the first run's model calls and tool call were logged in order, then the rejection",
);
check(
ends == ["completed", "rejected: the prompt contains a password"],
"finish reported how each run ended",
);
check(
runs.len() == 2 && sink.iter().all(|r| r["label"] == "session-42"),
"each run has its own id and carries the host's label",
);
}