Skip to main content

systemprompt_models/execution/step/
mod.rs

1//! Execution step model — a single unit of an agent run with status,
2//! timing, and per-kind content payload.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7mod content;
8mod enums;
9
10pub use content::{PlannedTool, StepContent};
11pub use enums::{StepId, StepStatus, StepType};
12
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::{SkillId, TaskId};
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "camelCase")]
19pub struct ExecutionStep {
20    pub step_id: StepId,
21    pub task_id: TaskId,
22    pub status: StepStatus,
23    pub started_at: DateTime<Utc>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub completed_at: Option<DateTime<Utc>>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub duration_ms: Option<i32>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub error_message: Option<String>,
30    pub content: StepContent,
31}
32
33impl ExecutionStep {
34    #[must_use]
35    pub fn new(task_id: TaskId, content: StepContent) -> Self {
36        let status = if content.is_instant() {
37            StepStatus::Completed
38        } else {
39            StepStatus::InProgress
40        };
41        let now = Utc::now();
42        let (completed_at, duration_ms) = if content.is_instant() {
43            (Some(now), Some(0))
44        } else {
45            (None, None)
46        };
47
48        Self {
49            step_id: StepId::new(),
50            task_id,
51            status,
52            started_at: now,
53            completed_at,
54            duration_ms,
55            error_message: None,
56            content,
57        }
58    }
59
60    #[must_use]
61    pub fn understanding(task_id: TaskId) -> Self {
62        Self::new(task_id, StepContent::understanding())
63    }
64
65    #[must_use]
66    pub fn planning(
67        task_id: TaskId,
68        reasoning: Option<String>,
69        planned_tools: Option<Vec<PlannedTool>>,
70    ) -> Self {
71        Self::new(task_id, StepContent::planning(reasoning, planned_tools))
72    }
73
74    #[must_use]
75    pub fn skill_usage(task_id: TaskId, skill_id: SkillId, skill_name: impl Into<String>) -> Self {
76        Self::new(task_id, StepContent::skill_usage(skill_id, skill_name))
77    }
78
79    #[must_use]
80    pub fn tool_execution(
81        task_id: TaskId,
82        tool_name: impl Into<String>,
83        tool_arguments: serde_json::Value,
84    ) -> Self {
85        Self::new(
86            task_id,
87            StepContent::tool_execution(tool_name, tool_arguments),
88        )
89    }
90
91    #[must_use]
92    pub fn completion(task_id: TaskId) -> Self {
93        Self::new(task_id, StepContent::completion())
94    }
95
96    #[must_use]
97    pub const fn step_type(&self) -> StepType {
98        self.content.step_type()
99    }
100
101    #[must_use]
102    pub fn title(&self) -> String {
103        self.content.title()
104    }
105
106    #[must_use]
107    pub fn tool_name(&self) -> Option<&str> {
108        self.content.tool_name()
109    }
110
111    #[must_use]
112    pub const fn tool_arguments(&self) -> Option<&serde_json::Value> {
113        self.content.tool_arguments()
114    }
115
116    #[must_use]
117    pub const fn tool_result(&self) -> Option<&serde_json::Value> {
118        self.content.tool_result()
119    }
120
121    #[must_use]
122    pub fn reasoning(&self) -> Option<&str> {
123        self.content.reasoning()
124    }
125
126    pub fn complete(&mut self, result: Option<serde_json::Value>) {
127        let now = Utc::now();
128        self.status = StepStatus::Completed;
129        self.completed_at = Some(now);
130        let duration = (now - self.started_at).num_milliseconds();
131        self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
132        if let Some(r) = result {
133            self.content = self.content.clone().with_tool_result(r);
134        }
135    }
136
137    pub fn fail(&mut self, error: String) {
138        let now = Utc::now();
139        self.status = StepStatus::Failed;
140        self.completed_at = Some(now);
141        let duration = (now - self.started_at).num_milliseconds();
142        self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
143        self.error_message = Some(error);
144    }
145}
146
147#[derive(Debug, Clone)]
148pub struct TrackedStep {
149    pub step_id: StepId,
150    pub started_at: DateTime<Utc>,
151}
152
153pub type StepDetail = StepContent;