1use std::sync::Arc;
4
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use starweaver_core::{CheckpointId, ConversationId, Metadata, RunId, TraceContext};
8use starweaver_usage::Usage;
9use thiserror::Error;
10
11use crate::run::{AgentRunState, RunStatus};
12
13#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum AgentExecutionNode {
17 RunStart,
19 PrepareModelRequest,
21 BeforeModelRequest,
23 ModelResponse,
25 ToolCall,
27 ToolReturn,
29 ValidateOutput,
31 RunComplete,
33 RunFailed,
35}
36
37#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
39pub struct AgentResumeCursor {
40 pub model_request_attempt: usize,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub tool_call_batch_id: Option<String>,
45 pub output_validation_attempt: usize,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub stream_cursor: Option<usize>,
50 pub message_cursor: usize,
52}
53
54#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
56pub struct AgentResumeEvidence {
57 pub node: AgentExecutionNode,
59 pub status: RunStatus,
61 pub run_step: usize,
63 pub cursor: AgentResumeCursor,
65 pub usage: Usage,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub context_revision: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub environment_ref: Option<String>,
73 pub pending_approval_count: usize,
75 pub deferred_tool_count: usize,
77 #[serde(default, skip_serializing_if = "TraceContext::is_empty")]
79 pub trace_context: TraceContext,
80 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
82 pub metadata: Metadata,
83}
84
85impl AgentResumeEvidence {
86 #[must_use]
88 pub fn new(node: AgentExecutionNode, state: &AgentRunState) -> Self {
89 Self {
90 node,
91 status: state.status,
92 run_step: state.run_step,
93 cursor: AgentResumeCursor {
94 model_request_attempt: state.run_step,
95 tool_call_batch_id: (!state.pending_tool_calls.is_empty())
96 .then(|| format!("tool_batch_{}", state.run_step)),
97 output_validation_attempt: 0,
98 stream_cursor: None,
99 message_cursor: state.message_history.len(),
100 },
101 usage: state.usage.clone(),
102 context_revision: state
103 .metadata
104 .get("context_revision")
105 .and_then(serde_json::Value::as_str)
106 .map(str::to_string),
107 environment_ref: state
108 .metadata
109 .get("environment_ref")
110 .and_then(serde_json::Value::as_str)
111 .map(str::to_string),
112 pending_approval_count: state.pending_approval_tool_returns.len(),
113 deferred_tool_count: state.deferred_tool_returns.len(),
114 trace_context: TraceContext::default(),
115 metadata: Metadata::default(),
116 }
117 }
118
119 #[must_use]
121 pub const fn with_stream_cursor(mut self, stream_cursor: usize) -> Self {
122 self.cursor.stream_cursor = Some(stream_cursor);
123 self
124 }
125
126 #[must_use]
128 pub fn with_trace_context(mut self, trace_context: TraceContext) -> Self {
129 self.trace_context = trace_context;
130 self
131 }
132
133 #[must_use]
135 pub fn with_metadata(mut self, metadata: Metadata) -> Self {
136 self.metadata = metadata;
137 self
138 }
139}
140
141#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
143pub struct AgentCheckpoint {
144 pub checkpoint_id: CheckpointId,
146 pub run_id: RunId,
148 pub conversation_id: ConversationId,
150 pub node: AgentExecutionNode,
152 pub run_step: usize,
154 pub resume: AgentResumeEvidence,
156 pub state: AgentRunState,
158 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
160 pub metadata: Metadata,
161}
162
163impl AgentCheckpoint {
164 #[must_use]
166 pub fn new(node: AgentExecutionNode, state: &AgentRunState) -> Self {
167 Self {
168 checkpoint_id: CheckpointId::new(),
169 run_id: state.run_id.clone(),
170 conversation_id: state.conversation_id.clone(),
171 node,
172 run_step: state.run_step,
173 resume: AgentResumeEvidence::new(node, state),
174 state: state.clone(),
175 metadata: Metadata::default(),
176 }
177 }
178
179 #[must_use]
181 pub fn with_metadata(mut self, metadata: Metadata) -> Self {
182 self.metadata = metadata;
183 self
184 }
185
186 #[must_use]
188 pub fn with_stream_cursor(mut self, stream_cursor: usize) -> Self {
189 self.resume = self.resume.with_stream_cursor(stream_cursor);
190 self
191 }
192}
193
194#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
196#[serde(tag = "kind", rename_all = "snake_case")]
197pub enum AgentExecutionDecision {
198 Continue,
200 Suspend {
202 reason: String,
204 },
205}
206
207#[derive(Debug, Error)]
209pub enum AgentExecutorError {
210 #[error("executor failed: {0}")]
212 Failed(String),
213}
214
215#[async_trait]
217pub trait AgentExecutor: Send + Sync {
218 async fn checkpoint(
220 &self,
221 checkpoint: AgentCheckpoint,
222 ) -> Result<AgentExecutionDecision, AgentExecutorError>;
223}
224
225pub type DynAgentExecutor = Arc<dyn AgentExecutor>;
227
228#[derive(Clone, Debug, Default)]
230pub struct DirectAgentExecutor;
231
232#[async_trait]
233impl AgentExecutor for DirectAgentExecutor {
234 async fn checkpoint(
235 &self,
236 _checkpoint: AgentCheckpoint,
237 ) -> Result<AgentExecutionDecision, AgentExecutorError> {
238 Ok(AgentExecutionDecision::Continue)
239 }
240}