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    /// Create an Understanding step
140    pub const fn understanding() -> Self {
141        Self::Understanding
142    }
143
144    /// Create a Planning step with optional reasoning and planned tools
145    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    /// Create an understanding step
308    pub fn understanding(task_id: TaskId) -> Self {
309        Self::new(task_id, StepContent::understanding())
310    }
311
312    /// Create a planning step
313    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    /// Create a skill usage step
322    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    /// Create a tool execution step
327    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    /// Create a completion step
339    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;