Skip to main content

systemprompt_models/execution/
step.rs

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;