systemprompt_models/ai/
execution_plan.rs1use std::sync::LazyLock;
13
14use regex::Regex;
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "type", rename_all = "snake_case")]
20pub enum PlanningResult {
21 DirectResponse {
22 content: String,
23 },
24 ToolCalls {
25 reasoning: String,
26 calls: Vec<PlannedToolCall>,
27 },
28}
29
30impl PlanningResult {
31 pub fn direct_response(content: impl Into<String>) -> Self {
32 Self::DirectResponse {
33 content: content.into(),
34 }
35 }
36
37 pub fn tool_calls(reasoning: impl Into<String>, calls: Vec<PlannedToolCall>) -> Self {
38 Self::ToolCalls {
39 reasoning: reasoning.into(),
40 calls,
41 }
42 }
43
44 pub const fn is_direct(&self) -> bool {
45 matches!(self, Self::DirectResponse { .. })
46 }
47
48 pub const fn is_tool_calls(&self) -> bool {
49 matches!(self, Self::ToolCalls { .. })
50 }
51
52 pub const fn tool_count(&self) -> usize {
53 match self {
54 Self::DirectResponse { .. } => 0,
55 Self::ToolCalls { calls, .. } => calls.len(),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct PlannedToolCall {
62 pub tool_name: String,
63 pub arguments: Value,
65}
66
67impl PlannedToolCall {
68 pub fn new(tool_name: impl Into<String>, arguments: Value) -> Self {
70 Self {
71 tool_name: tool_name.into(),
72 arguments,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ToolCallResult {
79 pub tool_name: String,
80 pub arguments: Value,
82 pub success: bool,
83 pub output: Value,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub meta: Option<Value>,
88 pub error: Option<String>,
89 pub duration_ms: u64,
90}
91
92impl ToolCallResult {
93 pub const fn success(
94 tool_name: String,
95 arguments: Value,
97 output: Value,
99 duration_ms: u64,
100 ) -> Self {
101 Self {
102 tool_name,
103 arguments,
104 success: true,
105 output,
106 meta: None,
107 error: None,
108 duration_ms,
109 }
110 }
111
112 #[must_use]
113 pub fn with_meta(mut self, meta: Option<Value>) -> Self {
115 self.meta = meta;
116 self
117 }
118
119 pub fn failure(
120 tool_name: String,
121 arguments: Value,
123 error: impl Into<String>,
124 duration_ms: u64,
125 ) -> Self {
126 Self {
127 tool_name,
128 arguments,
129 success: false,
130 output: Value::Null,
131 meta: None,
132 error: Some(error.into()),
133 duration_ms,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
139pub struct ExecutionState {
140 pub results: Vec<ToolCallResult>,
141 pub halted: bool,
142 pub halt_reason: Option<String>,
143}
144
145impl ExecutionState {
146 pub fn new() -> Self {
147 Self::default()
148 }
149
150 pub fn add_result(&mut self, result: ToolCallResult) {
151 if !result.success && !self.halted {
152 self.halted = true;
153 self.halt_reason.clone_from(&result.error);
154 }
155 self.results.push(result);
156 }
157
158 pub fn successful_results(&self) -> Vec<&ToolCallResult> {
159 self.results.iter().filter(|r| r.success).collect()
160 }
161
162 pub fn failed_results(&self) -> Vec<&ToolCallResult> {
163 self.results.iter().filter(|r| !r.success).collect()
164 }
165
166 pub fn total_duration_ms(&self) -> u64 {
167 self.results.iter().map(|r| r.duration_ms).sum()
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct TemplateRef {
173 pub tool_index: usize,
174 pub field_path: Vec<String>,
175}
176
177#[expect(
178 clippy::expect_used,
179 reason = "compile-time-constant regex; failure is a programmer bug, not runtime input"
180)]
181static TEMPLATE_REF_REGEX: LazyLock<Regex> = LazyLock::new(|| {
182 Regex::new(r"^\$(\d+)\.output\.(.+)$")
183 .expect("TEMPLATE_REF_REGEX is a valid regex - this is a compile-time constant")
184});
185
186impl TemplateRef {
187 pub fn parse(template: &str) -> Option<Self> {
188 let caps = TEMPLATE_REF_REGEX.captures(template)?;
189
190 let tool_index = caps.get(1)?.as_str().parse().ok()?;
191 let path = caps.get(2)?.as_str();
192 let field_path = path.split('.').map(String::from).collect();
193
194 Some(Self {
195 tool_index,
196 field_path,
197 })
198 }
199
200 pub fn format(&self) -> String {
201 format!("${}.output.{}", self.tool_index, self.field_path.join("."))
202 }
203}