1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6use crate::run::{AgentRunState, RunStatus};
7
8#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
10#[serde(rename_all = "snake_case")]
11pub enum AgentNode {
12 StartRun,
14 PrepareRequest,
16 DrainMessages,
18 ModelRequest,
20 HandleResponse,
22 ExecuteTools,
24 FinalizeRun,
26 DrainIdleMessages,
28 Complete,
30}
31
32#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
34pub struct GraphDecision {
35 pub next: AgentNode,
37 pub checkpoint: bool,
39}
40
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
43pub struct AgentGraphStep {
44 pub index: usize,
46 pub current: AgentNode,
48 pub decision: GraphDecision,
50 pub run_step: usize,
52 pub status: RunStatus,
54}
55
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
58pub struct AgentGraphTrace {
59 pub steps: Vec<AgentGraphStep>,
61 pub terminal: AgentNode,
63}
64
65impl AgentGraphTrace {
66 #[must_use]
68 pub fn steps(&self) -> &[AgentGraphStep] {
69 &self.steps
70 }
71
72 #[must_use]
74 pub const fn is_complete(&self) -> bool {
75 matches!(self.terminal, AgentNode::Complete)
76 }
77}
78
79impl GraphDecision {
80 const fn checkpoint(next: AgentNode) -> Self {
81 Self {
82 next,
83 checkpoint: true,
84 }
85 }
86
87 const fn step(next: AgentNode) -> Self {
88 Self {
89 next,
90 checkpoint: false,
91 }
92 }
93}
94
95#[derive(Debug, Error)]
97pub enum GraphError {
98 #[error("invalid graph state at {node:?}: {reason}")]
100 InvalidState {
101 node: AgentNode,
103 reason: String,
105 },
106}
107
108pub fn next_node(
114 current: AgentNode,
115 state: &AgentRunState,
116 max_steps: usize,
117) -> Result<GraphDecision, GraphError> {
118 if state.run_step >= max_steps
119 && !matches!(
120 current,
121 AgentNode::FinalizeRun | AgentNode::DrainIdleMessages | AgentNode::Complete
122 )
123 {
124 return Ok(GraphDecision::checkpoint(AgentNode::FinalizeRun));
125 }
126
127 match current {
128 AgentNode::StartRun => Ok(GraphDecision::checkpoint(AgentNode::PrepareRequest)),
129 AgentNode::PrepareRequest => Ok(GraphDecision::checkpoint(AgentNode::DrainMessages)),
130 AgentNode::DrainMessages => Ok(GraphDecision::step(AgentNode::ModelRequest)),
131 AgentNode::ModelRequest => {
132 if state.latest_response.is_some() {
133 Ok(GraphDecision::checkpoint(AgentNode::HandleResponse))
134 } else {
135 Err(GraphError::InvalidState {
136 node: current,
137 reason: "model response is required".to_string(),
138 })
139 }
140 }
141 AgentNode::HandleResponse => {
142 if !state.pending_tool_calls.is_empty() {
143 Ok(GraphDecision::checkpoint(AgentNode::ExecuteTools))
144 } else if state.output.is_some() {
145 Ok(GraphDecision::checkpoint(AgentNode::FinalizeRun))
146 } else if !state.pending_tool_returns.is_empty() {
147 Ok(GraphDecision::checkpoint(AgentNode::PrepareRequest))
148 } else {
149 Err(GraphError::InvalidState {
150 node: current,
151 reason: "tool calls, output, or retry signal is required".to_string(),
152 })
153 }
154 }
155 AgentNode::ExecuteTools => {
156 if state.pending_tool_returns.is_empty() {
157 Err(GraphError::InvalidState {
158 node: current,
159 reason: "tool returns are required after tool execution".to_string(),
160 })
161 } else {
162 Ok(GraphDecision::checkpoint(AgentNode::PrepareRequest))
163 }
164 }
165 AgentNode::FinalizeRun => {
166 if matches!(
167 state.status,
168 RunStatus::Completed | RunStatus::Failed | RunStatus::Cancelled
169 ) || state.output.is_some()
170 {
171 Ok(GraphDecision::checkpoint(AgentNode::DrainIdleMessages))
172 } else {
173 Err(GraphError::InvalidState {
174 node: current,
175 reason: "terminal output or terminal status is required".to_string(),
176 })
177 }
178 }
179 AgentNode::DrainIdleMessages => {
180 if state.idle_messages.is_empty() {
181 Ok(GraphDecision::checkpoint(AgentNode::Complete))
182 } else {
183 Ok(GraphDecision::checkpoint(AgentNode::PrepareRequest))
184 }
185 }
186 AgentNode::Complete => Ok(GraphDecision::step(AgentNode::Complete)),
187 }
188}
189
190pub fn inspect_next_node(
196 current: AgentNode,
197 state: &AgentRunState,
198 max_steps: usize,
199) -> Result<AgentGraphStep, GraphError> {
200 let decision = next_node(current, state, max_steps)?;
201 Ok(AgentGraphStep {
202 index: 0,
203 current,
204 decision,
205 run_step: state.run_step,
206 status: state.status,
207 })
208}
209
210pub fn inspect_graph(
220 start: AgentNode,
221 state: &AgentRunState,
222 max_steps: usize,
223 max_transitions: usize,
224) -> Result<AgentGraphTrace, GraphError> {
225 let mut current = start;
226 let mut steps = Vec::new();
227 for index in 0..max_transitions {
228 let decision = next_node(current, state, max_steps)?;
229 let next = decision.next;
230 steps.push(AgentGraphStep {
231 index,
232 current,
233 decision,
234 run_step: state.run_step,
235 status: state.status,
236 });
237 current = next;
238 if matches!(current, AgentNode::Complete) {
239 break;
240 }
241 }
242 Ok(AgentGraphTrace {
243 steps,
244 terminal: current,
245 })
246}