use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::run::{AgentRunState, RunStatus};
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentNode {
StartRun,
PrepareRequest,
DrainMessages,
ModelRequest,
HandleResponse,
ExecuteTools,
FinalizeRun,
DrainIdleMessages,
Complete,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct GraphDecision {
pub next: AgentNode,
pub checkpoint: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AgentGraphStep {
pub index: usize,
pub current: AgentNode,
pub decision: GraphDecision,
pub run_step: usize,
pub status: RunStatus,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AgentGraphTrace {
pub steps: Vec<AgentGraphStep>,
pub terminal: AgentNode,
}
impl AgentGraphTrace {
#[must_use]
pub fn steps(&self) -> &[AgentGraphStep] {
&self.steps
}
#[must_use]
pub const fn is_complete(&self) -> bool {
matches!(self.terminal, AgentNode::Complete)
}
}
impl GraphDecision {
const fn checkpoint(next: AgentNode) -> Self {
Self {
next,
checkpoint: true,
}
}
const fn step(next: AgentNode) -> Self {
Self {
next,
checkpoint: false,
}
}
}
#[derive(Debug, Error)]
pub enum GraphError {
#[error("invalid graph state at {node:?}: {reason}")]
InvalidState {
node: AgentNode,
reason: String,
},
}
pub fn next_node(
current: AgentNode,
state: &AgentRunState,
max_steps: usize,
) -> Result<GraphDecision, GraphError> {
if state.run_step >= max_steps
&& !matches!(
current,
AgentNode::FinalizeRun | AgentNode::DrainIdleMessages | AgentNode::Complete
)
{
return Ok(GraphDecision::checkpoint(AgentNode::FinalizeRun));
}
match current {
AgentNode::StartRun => Ok(GraphDecision::checkpoint(AgentNode::PrepareRequest)),
AgentNode::PrepareRequest => Ok(GraphDecision::checkpoint(AgentNode::DrainMessages)),
AgentNode::DrainMessages => Ok(GraphDecision::step(AgentNode::ModelRequest)),
AgentNode::ModelRequest => {
if state.latest_response.is_some() {
Ok(GraphDecision::checkpoint(AgentNode::HandleResponse))
} else {
Err(GraphError::InvalidState {
node: current,
reason: "model response is required".to_string(),
})
}
}
AgentNode::HandleResponse => {
if !state.pending_tool_calls.is_empty() {
Ok(GraphDecision::checkpoint(AgentNode::ExecuteTools))
} else if state.output.is_some() {
Ok(GraphDecision::checkpoint(AgentNode::FinalizeRun))
} else if !state.pending_tool_returns.is_empty() {
Ok(GraphDecision::checkpoint(AgentNode::PrepareRequest))
} else {
Err(GraphError::InvalidState {
node: current,
reason: "tool calls, output, or retry signal is required".to_string(),
})
}
}
AgentNode::ExecuteTools => {
if state.pending_tool_returns.is_empty() {
Err(GraphError::InvalidState {
node: current,
reason: "tool returns are required after tool execution".to_string(),
})
} else {
Ok(GraphDecision::checkpoint(AgentNode::PrepareRequest))
}
}
AgentNode::FinalizeRun => {
if matches!(
state.status,
RunStatus::Completed | RunStatus::Failed | RunStatus::Cancelled
) || state.output.is_some()
{
Ok(GraphDecision::checkpoint(AgentNode::DrainIdleMessages))
} else {
Err(GraphError::InvalidState {
node: current,
reason: "terminal output or terminal status is required".to_string(),
})
}
}
AgentNode::DrainIdleMessages => {
if state.idle_messages.is_empty() {
Ok(GraphDecision::checkpoint(AgentNode::Complete))
} else {
Ok(GraphDecision::checkpoint(AgentNode::PrepareRequest))
}
}
AgentNode::Complete => Ok(GraphDecision::step(AgentNode::Complete)),
}
}
pub fn inspect_next_node(
current: AgentNode,
state: &AgentRunState,
max_steps: usize,
) -> Result<AgentGraphStep, GraphError> {
let decision = next_node(current, state, max_steps)?;
Ok(AgentGraphStep {
index: 0,
current,
decision,
run_step: state.run_step,
status: state.status,
})
}
pub fn inspect_graph(
start: AgentNode,
state: &AgentRunState,
max_steps: usize,
max_transitions: usize,
) -> Result<AgentGraphTrace, GraphError> {
let mut current = start;
let mut steps = Vec::new();
for index in 0..max_transitions {
let decision = next_node(current, state, max_steps)?;
let next = decision.next;
steps.push(AgentGraphStep {
index,
current,
decision,
run_step: state.run_step,
status: state.status,
});
current = next;
if matches!(current, AgentNode::Complete) {
break;
}
}
Ok(AgentGraphTrace {
steps,
terminal: current,
})
}