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        // JSON: MCP tool-call arguments / result are the tool's own JSON.
84        tool_arguments: serde_json::Value,
85    ) -> Self {
86        Self::new(
87            task_id,
88            StepContent::tool_execution(tool_name, tool_arguments),
89        )
90    }
91
92    #[must_use]
93    pub fn completion(task_id: TaskId) -> Self {
94        Self::new(task_id, StepContent::completion())
95    }
96
97    #[must_use]
98    pub const fn step_type(&self) -> StepType {
99        self.content.step_type()
100    }
101
102    #[must_use]
103    pub fn title(&self) -> String {
104        self.content.title()
105    }
106
107    #[must_use]
108    pub fn tool_name(&self) -> Option<&str> {
109        self.content.tool_name()
110    }
111
112    #[must_use]
113    // JSON: MCP tool-call arguments / result are the tool's own JSON.
114    pub const fn tool_arguments(&self) -> Option<&serde_json::Value> {
115        self.content.tool_arguments()
116    }
117
118    #[must_use]
119    // JSON: MCP tool-call arguments / result are the tool's own JSON.
120    pub const fn tool_result(&self) -> Option<&serde_json::Value> {
121        self.content.tool_result()
122    }
123
124    #[must_use]
125    pub fn reasoning(&self) -> Option<&str> {
126        self.content.reasoning()
127    }
128
129    // JSON: MCP tool-call arguments / result are the tool's own JSON.
130    pub fn complete(&mut self, result: Option<serde_json::Value>) {
131        let now = Utc::now();
132        self.status = StepStatus::Completed;
133        self.completed_at = Some(now);
134        let duration = (now - self.started_at).num_milliseconds();
135        self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
136        if let Some(r) = result {
137            self.content = self.content.clone().with_tool_result(r);
138        }
139    }
140
141    pub fn fail(&mut self, error: String) {
142        let now = Utc::now();
143        self.status = StepStatus::Failed;
144        self.completed_at = Some(now);
145        let duration = (now - self.started_at).num_milliseconds();
146        self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
147        self.error_message = Some(error);
148    }
149}
150
151#[derive(Debug, Clone)]
152pub struct TrackedStep {
153    pub step_id: StepId,
154    pub started_at: DateTime<Utc>,
155}
156
157pub type StepDetail = StepContent;