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 {
141 Self::Understanding
142 }
143
144 pub const fn planning(
146 reasoning: Option<String>,
147 planned_tools: Option<Vec<PlannedTool>>,
148 ) -> Self {
149 Self::Planning {
150 reasoning,
151 planned_tools,
152 }
153 }
154
155 pub fn skill_usage(skill_id: SkillId, skill_name: impl Into<String>) -> Self {
156 Self::SkillUsage {
157 skill_id,
158 skill_name: skill_name.into(),
159 }
160 }
161
162 pub fn tool_execution(tool_name: impl Into<String>, tool_arguments: serde_json::Value) -> Self {
163 Self::ToolExecution {
164 tool_name: tool_name.into(),
165 tool_arguments,
166 tool_result: None,
167 }
168 }
169
170 pub const fn completion() -> Self {
171 Self::Completion
172 }
173
174 pub const fn step_type(&self) -> StepType {
175 match self {
176 Self::Understanding => StepType::Understanding,
177 Self::Planning { .. } => StepType::Planning,
178 Self::SkillUsage { .. } => StepType::SkillUsage,
179 Self::ToolExecution { .. } => StepType::ToolExecution,
180 Self::Completion => StepType::Completion,
181 }
182 }
183
184 pub fn title(&self) -> String {
185 match self {
186 Self::Understanding => "Analyzing request...".to_string(),
187 Self::Planning { .. } => "Planning response...".to_string(),
188 Self::SkillUsage { skill_name, .. } => format!("Using {} skill...", skill_name),
189 Self::ToolExecution { tool_name, .. } => format!("Running {}...", tool_name),
190 Self::Completion => "Complete".to_string(),
191 }
192 }
193
194 pub const fn is_instant(&self) -> bool {
195 !matches!(self, Self::ToolExecution { .. })
196 }
197
198 pub fn tool_name(&self) -> Option<&str> {
199 match self {
200 Self::ToolExecution { tool_name, .. } => Some(tool_name),
201 Self::SkillUsage { skill_name, .. } => Some(skill_name),
202 Self::Understanding | Self::Planning { .. } | Self::Completion => None,
203 }
204 }
205
206 pub const fn tool_arguments(&self) -> Option<&serde_json::Value> {
207 match self {
208 Self::ToolExecution { tool_arguments, .. } => Some(tool_arguments),
209 Self::Understanding
210 | Self::Planning { .. }
211 | Self::SkillUsage { .. }
212 | Self::Completion => None,
213 }
214 }
215
216 pub const fn tool_result(&self) -> Option<&serde_json::Value> {
217 match self {
218 Self::ToolExecution { tool_result, .. } => tool_result.as_ref(),
219 Self::Understanding
220 | Self::Planning { .. }
221 | Self::SkillUsage { .. }
222 | Self::Completion => None,
223 }
224 }
225
226 pub fn reasoning(&self) -> Option<&str> {
227 match self {
228 Self::Planning { reasoning, .. } => reasoning.as_deref(),
229 Self::Understanding
230 | Self::SkillUsage { .. }
231 | Self::ToolExecution { .. }
232 | Self::Completion => None,
233 }
234 }
235
236 pub fn planned_tools(&self) -> Option<&[PlannedTool]> {
237 match self {
238 Self::Planning { planned_tools, .. } => planned_tools.as_deref(),
239 Self::Understanding
240 | Self::SkillUsage { .. }
241 | Self::ToolExecution { .. }
242 | Self::Completion => None,
243 }
244 }
245
246 pub fn with_tool_result(self, result: serde_json::Value) -> Self {
247 match self {
248 Self::ToolExecution {
249 tool_name,
250 tool_arguments,
251 ..
252 } => Self::ToolExecution {
253 tool_name,
254 tool_arguments,
255 tool_result: Some(result),
256 },
257 other @ (Self::Understanding
258 | Self::Planning { .. }
259 | Self::SkillUsage { .. }
260 | Self::Completion) => other,
261 }
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
266#[serde(rename_all = "camelCase")]
267pub struct ExecutionStep {
268 pub step_id: StepId,
269 pub task_id: TaskId,
270 pub status: StepStatus,
271 pub started_at: DateTime<Utc>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub completed_at: Option<DateTime<Utc>>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub duration_ms: Option<i32>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub error_message: Option<String>,
278 pub content: StepContent,
279}
280
281impl ExecutionStep {
282 pub fn new(task_id: TaskId, content: StepContent) -> Self {
283 let status = if content.is_instant() {
284 StepStatus::Completed
285 } else {
286 StepStatus::InProgress
287 };
288 let now = Utc::now();
289 let (completed_at, duration_ms) = if content.is_instant() {
290 (Some(now), Some(0))
291 } else {
292 (None, None)
293 };
294
295 Self {
296 step_id: StepId::new(),
297 task_id,
298 status,
299 started_at: now,
300 completed_at,
301 duration_ms,
302 error_message: None,
303 content,
304 }
305 }
306
307 pub fn understanding(task_id: TaskId) -> Self {
309 Self::new(task_id, StepContent::understanding())
310 }
311
312 pub fn planning(
314 task_id: TaskId,
315 reasoning: Option<String>,
316 planned_tools: Option<Vec<PlannedTool>>,
317 ) -> Self {
318 Self::new(task_id, StepContent::planning(reasoning, planned_tools))
319 }
320
321 pub fn skill_usage(task_id: TaskId, skill_id: SkillId, skill_name: impl Into<String>) -> Self {
323 Self::new(task_id, StepContent::skill_usage(skill_id, skill_name))
324 }
325
326 pub fn tool_execution(
328 task_id: TaskId,
329 tool_name: impl Into<String>,
330 tool_arguments: serde_json::Value,
331 ) -> Self {
332 Self::new(
333 task_id,
334 StepContent::tool_execution(tool_name, tool_arguments),
335 )
336 }
337
338 pub fn completion(task_id: TaskId) -> Self {
340 Self::new(task_id, StepContent::completion())
341 }
342
343 pub const fn step_type(&self) -> StepType {
344 self.content.step_type()
345 }
346
347 pub fn title(&self) -> String {
348 self.content.title()
349 }
350
351 pub fn tool_name(&self) -> Option<&str> {
352 self.content.tool_name()
353 }
354
355 pub const fn tool_arguments(&self) -> Option<&serde_json::Value> {
356 self.content.tool_arguments()
357 }
358
359 pub const fn tool_result(&self) -> Option<&serde_json::Value> {
360 self.content.tool_result()
361 }
362
363 pub fn reasoning(&self) -> Option<&str> {
364 self.content.reasoning()
365 }
366
367 pub fn complete(&mut self, result: Option<serde_json::Value>) {
368 let now = Utc::now();
369 self.status = StepStatus::Completed;
370 self.completed_at = Some(now);
371 let duration = (now - self.started_at).num_milliseconds();
372 self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
373 if let Some(r) = result {
374 self.content = self.content.clone().with_tool_result(r);
375 }
376 }
377
378 pub fn fail(&mut self, error: String) {
379 let now = Utc::now();
380 self.status = StepStatus::Failed;
381 self.completed_at = Some(now);
382 let duration = (now - self.started_at).num_milliseconds();
383 self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
384 self.error_message = Some(error);
385 }
386}
387
388#[derive(Debug, Clone)]
389pub struct TrackedStep {
390 pub step_id: StepId,
391 pub started_at: DateTime<Utc>,
392}
393
394pub type StepDetail = StepContent;