use std::sync::Arc;
use agentyk::{
Agent, EventData, EventLog, EventRequest, FnTool, InMemoryEventLog, JsonlEventLog, ModelSpec,
Result, SimDriver, SimTurn, ToolContext, ToolOutput, TurnAction, TurnOutcome, TurnState, atoms,
messages_from_events,
};
use serde_json::json;
fn add_tool() -> FnTool {
FnTool::new(
"add",
"Add two numbers.",
json!({
"type": "object",
"properties": {"a": {"type": "number"}, "b": {"type": "number"}},
"required": ["a", "b"]
}),
|args| async move {
let a = args["a"].as_f64().unwrap_or(0.0);
let b = args["b"].as_f64().unwrap_or(0.0);
ToolOutput::text((a + b).to_string())
},
)
}
async fn record(
log: &dyn agentyk::EventLog,
state: &TurnState,
effects: Vec<EventData>,
) -> Result<()> {
for data in effects {
log.append(EventRequest::with_turn(
state.session_id,
state.turn_id,
data,
))
.await?;
}
Ok(())
}
#[tokio::test]
async fn durable_host_drives_turn_across_a_crash() -> Result<()> {
let dir = tempfile::tempdir()?;
let log_path = dir.path().join("events.jsonl");
let build_agent = |script: Vec<SimTurn>| {
Agent::builder()
.system_prompt("You do arithmetic.")
.model(ModelSpec::llmsim())
.driver(SimDriver::new(script))
.tool(add_tool())
.build()
};
let checkpoint: String;
{
let agent = build_agent(vec![SimTurn::tool_call("add", json!({"a": 20, "b": 22}))])?;
let log = JsonlEventLog::new(&log_path)?;
let session_id = agentyk::SessionId::new();
let assembled =
atoms::assemble(agent.system_prompt(), agent.capabilities(), session_id).await?;
let driver = agent.driver_for_model().expect("driver");
let input = agentyk::Message::user("what is 20 + 22?");
let (mut state, effects) = TurnState::start(session_id, 4, &input);
record(&log, &state, effects).await?;
assert_eq!(state.next_action(), TurnAction::Reason);
let started = state.on_reason_started(Some(&agent.model().model));
record(&log, &state, started).await?;
let history = messages_from_events(&log.read(session_id).await?);
let response = atoms::reason(driver.as_ref(), agent.model(), &assembled, history).await?;
let effects = state.on_reason_completed(&response);
record(&log, &state, effects).await?;
checkpoint = serde_json::to_string(&state)?;
}
let agent = build_agent(vec![SimTurn::text("The answer is 42.")])?;
let log = JsonlEventLog::new(&log_path)?;
let mut state: TurnState = serde_json::from_str(&checkpoint)?;
assert!(!state.is_complete());
let assembled =
atoms::assemble("You do arithmetic.", agent.capabilities(), state.session_id).await?;
let driver = agent.driver_for_model().expect("driver");
loop {
match state.next_action() {
TurnAction::Reason => {
let started = state.on_reason_started(Some(&agent.model().model));
record(&log, &state, started).await?;
let history = messages_from_events(&log.read(state.session_id).await?);
let response =
atoms::reason(driver.as_ref(), agent.model(), &assembled, history).await?;
let effects = state.on_reason_completed(&response);
record(&log, &state, effects).await?;
}
TurnAction::ExecuteTool { call } => {
let effects = state.on_tool_started(&call.id);
record(&log, &state, effects).await?;
let context = ToolContext {
session_id: state.session_id,
turn_id: state.turn_id,
..Default::default()
};
let output = atoms::act(&assembled, &call, &context).await;
let effects = state.on_tool_completed(&call.id, &output);
record(&log, &state, effects).await?;
}
TurnAction::Complete(outcome) => {
assert_eq!(
outcome,
TurnOutcome::Success {
response: "The answer is 42.".into()
}
);
break;
}
}
}
let events = log.read(state.session_id).await?;
let types: Vec<&str> = events.iter().map(|e| e.event_type.as_str()).collect();
assert_eq!(
types,
vec![
"turn.started",
"input.message",
"output.message.started",
"output.message.completed",
"tool.started",
"tool.completed",
"output.message.started",
"output.message.completed",
"turn.completed",
]
);
assert!(events.iter().any(|e| matches!(
&e.data,
EventData::ToolCompleted { output, .. } if output == "42"
)));
assert_eq!(state.iterations, 2);
assert_eq!(state.tool_calls_executed, 1);
Ok(())
}
#[tokio::test]
async fn manual_drive_matches_in_process_executor() -> Result<()> {
let script = || {
vec![
SimTurn::tool_call("add", json!({"a": 1, "b": 2})),
SimTurn::text("3"),
]
};
let agent = Agent::builder()
.system_prompt("s")
.model(ModelSpec::llmsim())
.driver(SimDriver::new(script()))
.tool(add_tool())
.build()?;
let executor_log = Arc::new(InMemoryEventLog::new());
let mut session = agent.session_with_log(executor_log.clone());
session.run("q").await?;
let executor_types: Vec<String> = session
.events()
.await?
.iter()
.map(|e| e.event_type.clone())
.collect();
let agent = Agent::builder()
.system_prompt("s")
.model(ModelSpec::llmsim())
.driver(SimDriver::new(script()))
.tool(add_tool())
.build()?;
let log = InMemoryEventLog::new();
let session_id = agentyk::SessionId::new();
let assembled = atoms::assemble("s", agent.capabilities(), session_id).await?;
let driver = agent.driver_for_model().expect("driver");
let (mut state, effects) = TurnState::start(session_id, 16, &agentyk::Message::user("q"));
record(&log, &state, effects).await?;
loop {
match state.next_action() {
TurnAction::Reason => {
let started = state.on_reason_started(Some(&agent.model().model));
record(&log, &state, started).await?;
let history = messages_from_events(&log.read(session_id).await?);
let response =
atoms::reason(driver.as_ref(), agent.model(), &assembled, history).await?;
let effects = state.on_reason_completed(&response);
record(&log, &state, effects).await?;
}
TurnAction::ExecuteTool { call } => {
let effects = state.on_tool_started(&call.id);
record(&log, &state, effects).await?;
let context = ToolContext {
session_id,
turn_id: state.turn_id,
..Default::default()
};
let output = atoms::act(&assembled, &call, &context).await;
let effects = state.on_tool_completed(&call.id, &output);
record(&log, &state, effects).await?;
}
TurnAction::Complete(_) => break,
}
}
let manual_types: Vec<String> = log
.read(session_id)
.await?
.iter()
.map(|e| e.event_type.clone())
.collect();
assert_eq!(executor_types, manual_types);
Ok(())
}