use serde::{Deserialize, Serialize};
use crate::driver::{ChatResponse, Usage};
use crate::event::EventData;
use crate::id::{SessionId, TurnId};
use crate::message::{Message, ToolCall};
use crate::tool::ToolOutput;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SealReason {
NoProgress,
BudgetExhausted,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TurnOutcome {
Success {
response: String,
},
Failed {
error: String,
},
MaxIterations,
Cancelled,
Sealed(SealReason),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PendingCall {
pub call: ToolCall,
pub started: bool,
pub output: Option<ToolOutput>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "phase", rename_all = "snake_case")]
pub enum TurnPhase {
PendingReason,
PendingAct { calls: Vec<PendingCall> },
Completed(TurnOutcome),
}
#[derive(Debug, Clone, PartialEq)]
pub enum TurnAction {
Reason,
ExecuteTool { call: ToolCall },
Complete(TurnOutcome),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TurnState {
pub session_id: SessionId,
pub turn_id: TurnId,
pub max_iterations: usize,
pub phase: TurnPhase,
pub iterations: usize,
pub tool_calls_executed: usize,
pub usage: Usage,
}
impl TurnState {
pub fn start(
session_id: SessionId,
max_iterations: usize,
input: &Message,
) -> (Self, Vec<EventData>) {
let state = Self {
session_id,
turn_id: TurnId::new(),
max_iterations,
phase: TurnPhase::PendingReason,
iterations: 0,
tool_calls_executed: 0,
usage: Usage::default(),
};
let effects = vec![
EventData::TurnStarted,
EventData::InputMessage {
message: input.clone(),
},
];
(state, effects)
}
pub fn next_action(&self) -> TurnAction {
match &self.phase {
TurnPhase::PendingReason => TurnAction::Reason,
TurnPhase::PendingAct { calls } => {
match calls.iter().find(|pending| pending.output.is_none()) {
Some(pending) => TurnAction::ExecuteTool {
call: pending.call.clone(),
},
None => TurnAction::Reason,
}
}
TurnPhase::Completed(outcome) => TurnAction::Complete(outcome.clone()),
}
}
pub fn pending_tool_actions(&self) -> Vec<TurnAction> {
match &self.phase {
TurnPhase::PendingAct { calls } => calls
.iter()
.filter(|pending| !pending.started)
.map(|pending| TurnAction::ExecuteTool {
call: pending.call.clone(),
})
.collect(),
_ => Vec::new(),
}
}
pub fn on_reason_started(&self, model: Option<&str>) -> Vec<EventData> {
vec![EventData::OutputMessageStarted {
model: model.map(str::to_string),
iteration: Some(self.iterations as u32 + 1),
}]
}
pub fn on_reason_completed(&mut self, response: &ChatResponse) -> Vec<EventData> {
self.iterations += 1;
self.usage.add(response.usage);
let mut effects = vec![EventData::OutputMessageCompleted {
message: response.message.clone(),
}];
if response.message.tool_calls.is_empty() {
let outcome = TurnOutcome::Success {
response: response.message.text(),
};
effects.push(EventData::TurnCompleted {
iterations: self.iterations,
tool_calls: self.tool_calls_executed,
});
self.phase = TurnPhase::Completed(outcome);
} else if self.iterations >= self.max_iterations {
let outcome = TurnOutcome::MaxIterations;
effects.push(EventData::TurnFailed {
error: format!("turn exceeded max iterations ({})", self.max_iterations),
});
self.phase = TurnPhase::Completed(outcome);
} else {
let calls = response
.message
.tool_calls
.iter()
.map(|call| PendingCall {
call: call.clone(),
started: false,
output: None,
})
.collect();
self.phase = TurnPhase::PendingAct { calls };
}
effects
}
pub fn on_tool_started(&mut self, call_id: &str) -> Vec<EventData> {
let TurnPhase::PendingAct { calls } = &mut self.phase else {
return Vec::new();
};
let Some(pending) = calls.iter_mut().find(|p| p.call.id == call_id) else {
return Vec::new();
};
if pending.started {
return Vec::new();
}
pending.started = true;
vec![EventData::ToolStarted {
call: pending.call.clone(),
}]
}
pub fn on_tool_completed(&mut self, call_id: &str, output: &ToolOutput) -> Vec<EventData> {
let TurnPhase::PendingAct { calls } = &mut self.phase else {
return Vec::new();
};
let Some(pending) = calls.iter_mut().find(|p| p.call.id == call_id) else {
return Vec::new();
};
if pending.output.is_some() {
return Vec::new();
}
pending.output = Some(output.clone());
self.tool_calls_executed += 1;
let effects = vec![EventData::ToolCompleted {
call_id: pending.call.id.clone(),
name: pending.call.name.clone(),
output: output.content.clone(),
is_error: output.is_error,
}];
if calls.iter().all(|p| p.output.is_some()) {
self.phase = TurnPhase::PendingReason;
}
effects
}
pub fn on_failure(&mut self, error: impl Into<String>) -> Vec<EventData> {
let error = error.into();
self.phase = TurnPhase::Completed(TurnOutcome::Failed {
error: error.clone(),
});
vec![EventData::TurnFailed { error }]
}
pub fn on_cancel(&mut self) -> Vec<EventData> {
self.phase = TurnPhase::Completed(TurnOutcome::Cancelled);
vec![EventData::TurnCancelled]
}
pub fn on_seal(&mut self, reason: SealReason) -> Vec<EventData> {
self.phase = TurnPhase::Completed(TurnOutcome::Sealed(reason));
vec![EventData::TurnSealed { reason }]
}
pub fn is_complete(&self) -> bool {
matches!(self.phase, TurnPhase::Completed(_))
}
pub fn outcome(&self) -> Option<&TurnOutcome> {
match &self.phase {
TurnPhase::Completed(outcome) => Some(outcome),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::Message;
use serde_json::json;
fn tool_call_response(names: &[&str]) -> ChatResponse {
let calls = names
.iter()
.enumerate()
.map(|(index, name)| ToolCall {
id: format!("call_{index}"),
name: (*name).to_string(),
arguments: json!({}),
})
.collect();
ChatResponse {
message: Message::assistant_with_calls("", calls),
usage: Usage::default(),
}
}
fn text_response(text: &str) -> ChatResponse {
ChatResponse {
message: Message::assistant(text),
usage: Usage::default(),
}
}
#[test]
fn happy_path_transitions() {
let input = Message::user("hi");
let (mut state, effects) = TurnState::start(SessionId::new(), 4, &input);
assert_eq!(effects.len(), 2);
assert_eq!(state.next_action(), TurnAction::Reason);
state.on_reason_completed(&tool_call_response(&["a", "b"]));
let TurnAction::ExecuteTool { call } = state.next_action() else {
panic!("expected tool action");
};
assert_eq!(call.name, "a");
assert_eq!(state.on_tool_started(&call.id).len(), 1);
assert_eq!(state.on_tool_started(&call.id).len(), 0); state.on_tool_completed(&call.id, &ToolOutput::text("ok"));
let TurnAction::ExecuteTool { call } = state.next_action() else {
panic!("expected second tool action");
};
assert_eq!(call.name, "b");
state.on_tool_started(&call.id);
state.on_tool_completed(&call.id, &ToolOutput::text("ok"));
assert_eq!(state.next_action(), TurnAction::Reason);
let effects = state.on_reason_completed(&text_response("done"));
assert!(matches!(
effects.last(),
Some(EventData::TurnCompleted { .. })
));
assert_eq!(
state.outcome(),
Some(&TurnOutcome::Success {
response: "done".into()
})
);
assert_eq!(state.iterations, 2);
assert_eq!(state.tool_calls_executed, 2);
}
#[test]
fn max_iterations_seals_instead_of_acting() {
let input = Message::user("go");
let (mut state, _) = TurnState::start(SessionId::new(), 1, &input);
let effects = state.on_reason_completed(&tool_call_response(&["a"]));
assert!(matches!(effects.last(), Some(EventData::TurnFailed { .. })));
assert_eq!(state.outcome(), Some(&TurnOutcome::MaxIterations));
}
#[test]
fn state_survives_serialization_mid_turn() {
let input = Message::user("hi");
let (mut state, _) = TurnState::start(SessionId::new(), 4, &input);
state.on_reason_completed(&tool_call_response(&["a"]));
let TurnAction::ExecuteTool { call } = state.next_action() else {
panic!("expected tool action");
};
state.on_tool_started(&call.id);
let json = serde_json::to_string(&state).unwrap();
let mut restored: TurnState = serde_json::from_str(&json).unwrap();
assert_eq!(restored, state);
assert!(matches!(
restored.next_action(),
TurnAction::ExecuteTool { .. }
));
assert_eq!(restored.on_tool_started(&call.id).len(), 0);
restored.on_tool_completed(&call.id, &ToolOutput::text("ok"));
assert_eq!(restored.next_action(), TurnAction::Reason);
}
#[test]
fn pending_tool_actions_returns_the_whole_not_started_batch() {
let input = Message::user("hi");
let (mut state, _) = TurnState::start(SessionId::new(), 4, &input);
state.on_reason_completed(&tool_call_response(&["a", "b", "c"]));
let actions = state.pending_tool_actions();
assert_eq!(actions.len(), 3);
let TurnAction::ExecuteTool { call } = &actions[0] else {
panic!("expected tool action");
};
state.on_tool_started(&call.id);
assert_eq!(state.pending_tool_actions().len(), 2);
}
#[test]
fn batch_completes_out_of_order() {
let input = Message::user("hi");
let (mut state, _) = TurnState::start(SessionId::new(), 4, &input);
state.on_reason_completed(&tool_call_response(&["a", "b"]));
let ids: Vec<String> = state
.pending_tool_actions()
.into_iter()
.map(|action| {
let TurnAction::ExecuteTool { call } = action else {
unreachable!()
};
call.id
})
.collect();
for id in &ids {
state.on_tool_started(id);
}
state.on_tool_completed(&ids[1], &ToolOutput::text("b done"));
let TurnAction::ExecuteTool { call: remaining } = state.next_action() else {
panic!("expected the still-incomplete call");
};
assert_eq!(remaining.id, ids[0]);
state.on_tool_completed(&ids[0], &ToolOutput::text("a done"));
assert_eq!(state.next_action(), TurnAction::Reason);
assert_eq!(state.tool_calls_executed, 2);
}
}