Skip to main content

systemprompt_models/ai/
execution_plan.rs

1//! Multi-step tool-execution planning and result tracking.
2//!
3//! A [`PlanningResult`] is either a direct response or a sequence of
4//! [`PlannedToolCall`]s. As calls run, [`ExecutionState`] accumulates
5//! [`ToolCallResult`]s and halts on the first failure. [`TemplateRef`] parses
6//! the `$N.output.field` references that let a later call consume an earlier
7//! call's output.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum PlanningResult {
19    DirectResponse {
20        content: String,
21    },
22    ToolCalls {
23        reasoning: String,
24        calls: Vec<PlannedToolCall>,
25    },
26}
27
28impl PlanningResult {
29    pub fn direct_response(content: impl Into<String>) -> Self {
30        Self::DirectResponse {
31            content: content.into(),
32        }
33    }
34
35    pub fn tool_calls(reasoning: impl Into<String>, calls: Vec<PlannedToolCall>) -> Self {
36        Self::ToolCalls {
37            reasoning: reasoning.into(),
38            calls,
39        }
40    }
41
42    pub const fn is_direct(&self) -> bool {
43        matches!(self, Self::DirectResponse { .. })
44    }
45
46    pub const fn is_tool_calls(&self) -> bool {
47        matches!(self, Self::ToolCalls { .. })
48    }
49
50    pub const fn tool_count(&self) -> usize {
51        match self {
52            Self::DirectResponse { .. } => 0,
53            Self::ToolCalls { calls, .. } => calls.len(),
54        }
55    }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct PlannedToolCall {
60    pub tool_name: String,
61    pub arguments: Value,
62}
63
64impl PlannedToolCall {
65    pub fn new(tool_name: impl Into<String>, arguments: Value) -> Self {
66        Self {
67            tool_name: tool_name.into(),
68            arguments,
69        }
70    }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ToolCallResult {
75    pub tool_name: String,
76    pub arguments: Value,
77    pub success: bool,
78    pub output: Value,
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub meta: Option<Value>,
81    pub error: Option<String>,
82    pub duration_ms: u64,
83}
84
85impl ToolCallResult {
86    pub const fn success(
87        tool_name: String,
88        arguments: Value,
89        output: Value,
90        duration_ms: u64,
91    ) -> Self {
92        Self {
93            tool_name,
94            arguments,
95            success: true,
96            output,
97            meta: None,
98            error: None,
99            duration_ms,
100        }
101    }
102
103    #[must_use]
104    pub fn with_meta(mut self, meta: Option<Value>) -> Self {
105        self.meta = meta;
106        self
107    }
108
109    pub fn failure(
110        tool_name: String,
111        arguments: Value,
112        error: impl Into<String>,
113        duration_ms: u64,
114    ) -> Self {
115        Self {
116            tool_name,
117            arguments,
118            success: false,
119            output: Value::Null,
120            meta: None,
121            error: Some(error.into()),
122            duration_ms,
123        }
124    }
125}
126
127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128pub struct ExecutionState {
129    pub results: Vec<ToolCallResult>,
130    pub halted: bool,
131    pub halt_reason: Option<String>,
132}
133
134impl ExecutionState {
135    pub fn new() -> Self {
136        Self::default()
137    }
138
139    pub fn add_result(&mut self, result: ToolCallResult) {
140        if !result.success && !self.halted {
141            self.halted = true;
142            self.halt_reason.clone_from(&result.error);
143        }
144        self.results.push(result);
145    }
146
147    pub fn successful_results(&self) -> Vec<&ToolCallResult> {
148        self.results.iter().filter(|r| r.success).collect()
149    }
150
151    pub fn failed_results(&self) -> Vec<&ToolCallResult> {
152        self.results.iter().filter(|r| !r.success).collect()
153    }
154
155    pub fn total_duration_ms(&self) -> u64 {
156        self.results.iter().map(|r| r.duration_ms).sum()
157    }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct TemplateRef {
162    pub tool_index: usize,
163    pub field_path: Vec<String>,
164}
165
166impl TemplateRef {
167    pub fn parse(template: &str) -> Option<Self> {
168        let re = Regex::new(r"^\$(\d+)\.output\.(.+)$").ok()?;
169        let caps = re.captures(template)?;
170
171        let tool_index = caps.get(1)?.as_str().parse().ok()?;
172        let path = caps.get(2)?.as_str();
173        let field_path = path.split('.').map(String::from).collect();
174
175        Some(Self {
176            tool_index,
177            field_path,
178        })
179    }
180
181    pub fn format(&self) -> String {
182        format!("${}.output.{}", self.tool_index, self.field_path.join("."))
183    }
184}