1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use systemprompt_identifiers::{SkillId, TaskId};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
6pub struct StepId(pub String);
7
8impl StepId {
9 pub fn new() -> Self {
10 Self(uuid::Uuid::new_v4().to_string())
11 }
12
13 pub fn as_str(&self) -> &str {
14 &self.0
15 }
16}
17
18impl Default for StepId {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl From<String> for StepId {
25 fn from(s: String) -> Self {
26 Self(s)
27 }
28}
29
30impl std::fmt::Display for StepId {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 write!(f, "{}", self.0)
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
37#[serde(rename_all = "snake_case")]
38pub enum StepStatus {
39 #[default]
40 Pending,
41 InProgress,
42 Completed,
43 Failed,
44}
45
46impl std::fmt::Display for StepStatus {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Pending => write!(f, "pending"),
50 Self::InProgress => write!(f, "in_progress"),
51 Self::Completed => write!(f, "completed"),
52 Self::Failed => write!(f, "failed"),
53 }
54 }
55}
56
57impl std::str::FromStr for StepStatus {
58 type Err = String;
59
60 fn from_str(s: &str) -> Result<Self, Self::Err> {
61 match s.to_lowercase().as_str() {
62 "pending" => Ok(Self::Pending),
63 "in_progress" | "running" | "active" => Ok(Self::InProgress),
64 "completed" | "done" | "success" => Ok(Self::Completed),
65 "failed" | "error" => Ok(Self::Failed),
66 _ => Err(format!("Invalid step status: {s}")),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
72#[serde(rename_all = "snake_case")]
73pub enum StepType {
74 #[default]
75 Understanding,
76 Planning,
77 SkillUsage,
78 ToolExecution,
79 Completion,
80}
81
82impl std::fmt::Display for StepType {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 Self::Understanding => write!(f, "understanding"),
86 Self::Planning => write!(f, "planning"),
87 Self::SkillUsage => write!(f, "skill_usage"),
88 Self::ToolExecution => write!(f, "tool_execution"),
89 Self::Completion => write!(f, "completion"),
90 }
91 }
92}
93
94impl std::str::FromStr for StepType {
95 type Err = String;
96
97 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 match s.to_lowercase().as_str() {
99 "understanding" => Ok(Self::Understanding),
100 "planning" => Ok(Self::Planning),
101 "skill_usage" => Ok(Self::SkillUsage),
102 "tool_execution" | "toolexecution" => Ok(Self::ToolExecution),
103 "completion" => Ok(Self::Completion),
104 _ => Err(format!("Invalid step type: {s}")),
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub struct PlannedTool {
111 pub tool_name: String,
112 pub arguments: serde_json::Value,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116#[serde(tag = "type", rename_all = "snake_case")]
117pub enum StepContent {
118 Understanding,
119 Planning {
120 #[serde(skip_serializing_if = "Option::is_none")]
121 reasoning: Option<String>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 planned_tools: Option<Vec<PlannedTool>>,
124 },
125 SkillUsage {
126 skill_id: SkillId,
127 skill_name: String,
128 },
129 ToolExecution {
130 tool_name: String,
131 tool_arguments: serde_json::Value,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 tool_result: Option<serde_json::Value>,
134 },
135 Completion,
136}
137
138impl StepContent {
139 pub const fn understanding() -> Self {
140 Self::Understanding
141 }
142
143 pub const fn planning(
144 reasoning: Option<String>,
145 planned_tools: Option<Vec<PlannedTool>>,
146 ) -> Self {
147 Self::Planning {
148 reasoning,
149 planned_tools,
150 }
151 }
152
153 pub fn skill_usage(skill_id: SkillId, skill_name: impl Into<String>) -> Self {
154 Self::SkillUsage {
155 skill_id,
156 skill_name: skill_name.into(),
157 }
158 }
159
160 pub fn tool_execution(tool_name: impl Into<String>, tool_arguments: serde_json::Value) -> Self {
161 Self::ToolExecution {
162 tool_name: tool_name.into(),
163 tool_arguments,
164 tool_result: None,
165 }
166 }
167
168 pub const fn completion() -> Self {
169 Self::Completion
170 }
171
172 pub const fn step_type(&self) -> StepType {
173 match self {
174 Self::Understanding => StepType::Understanding,
175 Self::Planning { .. } => StepType::Planning,
176 Self::SkillUsage { .. } => StepType::SkillUsage,
177 Self::ToolExecution { .. } => StepType::ToolExecution,
178 Self::Completion => StepType::Completion,
179 }
180 }
181
182 pub fn title(&self) -> String {
183 match self {
184 Self::Understanding => "Analyzing request...".to_string(),
185 Self::Planning { .. } => "Planning response...".to_string(),
186 Self::SkillUsage { skill_name, .. } => format!("Using {} skill...", skill_name),
187 Self::ToolExecution { tool_name, .. } => format!("Running {}...", tool_name),
188 Self::Completion => "Complete".to_string(),
189 }
190 }
191
192 pub const fn is_instant(&self) -> bool {
193 !matches!(self, Self::ToolExecution { .. })
194 }
195
196 pub fn tool_name(&self) -> Option<&str> {
197 match self {
198 Self::ToolExecution { tool_name, .. } => Some(tool_name),
199 Self::SkillUsage { skill_name, .. } => Some(skill_name),
200 Self::Understanding | Self::Planning { .. } | Self::Completion => None,
201 }
202 }
203
204 pub const fn tool_arguments(&self) -> Option<&serde_json::Value> {
205 match self {
206 Self::ToolExecution { tool_arguments, .. } => Some(tool_arguments),
207 Self::Understanding
208 | Self::Planning { .. }
209 | Self::SkillUsage { .. }
210 | Self::Completion => None,
211 }
212 }
213
214 pub const fn tool_result(&self) -> Option<&serde_json::Value> {
215 match self {
216 Self::ToolExecution { tool_result, .. } => tool_result.as_ref(),
217 Self::Understanding
218 | Self::Planning { .. }
219 | Self::SkillUsage { .. }
220 | Self::Completion => None,
221 }
222 }
223
224 pub fn reasoning(&self) -> Option<&str> {
225 match self {
226 Self::Planning { reasoning, .. } => reasoning.as_deref(),
227 Self::Understanding
228 | Self::SkillUsage { .. }
229 | Self::ToolExecution { .. }
230 | Self::Completion => None,
231 }
232 }
233
234 pub fn planned_tools(&self) -> Option<&[PlannedTool]> {
235 match self {
236 Self::Planning { planned_tools, .. } => planned_tools.as_deref(),
237 Self::Understanding
238 | Self::SkillUsage { .. }
239 | Self::ToolExecution { .. }
240 | Self::Completion => None,
241 }
242 }
243
244 pub fn with_tool_result(self, result: serde_json::Value) -> Self {
245 match self {
246 Self::ToolExecution {
247 tool_name,
248 tool_arguments,
249 ..
250 } => Self::ToolExecution {
251 tool_name,
252 tool_arguments,
253 tool_result: Some(result),
254 },
255 other @ (Self::Understanding
256 | Self::Planning { .. }
257 | Self::SkillUsage { .. }
258 | Self::Completion) => other,
259 }
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
264#[serde(rename_all = "camelCase")]
265pub struct ExecutionStep {
266 pub step_id: StepId,
267 pub task_id: TaskId,
268 pub status: StepStatus,
269 pub started_at: DateTime<Utc>,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub completed_at: Option<DateTime<Utc>>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub duration_ms: Option<i32>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub error_message: Option<String>,
276 pub content: StepContent,
277}
278
279impl ExecutionStep {
280 pub fn new(task_id: TaskId, content: StepContent) -> Self {
281 let status = if content.is_instant() {
282 StepStatus::Completed
283 } else {
284 StepStatus::InProgress
285 };
286 let now = Utc::now();
287 let (completed_at, duration_ms) = if content.is_instant() {
288 (Some(now), Some(0))
289 } else {
290 (None, None)
291 };
292
293 Self {
294 step_id: StepId::new(),
295 task_id,
296 status,
297 started_at: now,
298 completed_at,
299 duration_ms,
300 error_message: None,
301 content,
302 }
303 }
304
305 pub fn understanding(task_id: TaskId) -> Self {
306 Self::new(task_id, StepContent::understanding())
307 }
308
309 pub fn planning(
310 task_id: TaskId,
311 reasoning: Option<String>,
312 planned_tools: Option<Vec<PlannedTool>>,
313 ) -> Self {
314 Self::new(task_id, StepContent::planning(reasoning, planned_tools))
315 }
316
317 pub fn skill_usage(task_id: TaskId, skill_id: SkillId, skill_name: impl Into<String>) -> Self {
318 Self::new(task_id, StepContent::skill_usage(skill_id, skill_name))
319 }
320
321 pub fn tool_execution(
322 task_id: TaskId,
323 tool_name: impl Into<String>,
324 tool_arguments: serde_json::Value,
325 ) -> Self {
326 Self::new(
327 task_id,
328 StepContent::tool_execution(tool_name, tool_arguments),
329 )
330 }
331
332 pub fn completion(task_id: TaskId) -> Self {
333 Self::new(task_id, StepContent::completion())
334 }
335
336 pub const fn step_type(&self) -> StepType {
337 self.content.step_type()
338 }
339
340 pub fn title(&self) -> String {
341 self.content.title()
342 }
343
344 pub fn tool_name(&self) -> Option<&str> {
345 self.content.tool_name()
346 }
347
348 pub const fn tool_arguments(&self) -> Option<&serde_json::Value> {
349 self.content.tool_arguments()
350 }
351
352 pub const fn tool_result(&self) -> Option<&serde_json::Value> {
353 self.content.tool_result()
354 }
355
356 pub fn reasoning(&self) -> Option<&str> {
357 self.content.reasoning()
358 }
359
360 pub fn complete(&mut self, result: Option<serde_json::Value>) {
361 let now = Utc::now();
362 self.status = StepStatus::Completed;
363 self.completed_at = Some(now);
364 let duration = (now - self.started_at).num_milliseconds();
365 self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
366 if let Some(r) = result {
367 self.content = self.content.clone().with_tool_result(r);
368 }
369 }
370
371 pub fn fail(&mut self, error: String) {
372 let now = Utc::now();
373 self.status = StepStatus::Failed;
374 self.completed_at = Some(now);
375 let duration = (now - self.started_at).num_milliseconds();
376 self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
377 self.error_message = Some(error);
378 }
379}
380
381#[derive(Debug, Clone)]
382pub struct TrackedStep {
383 pub step_id: StepId,
384 pub started_at: DateTime<Utc>,
385}
386
387pub type StepDetail = StepContent;