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    pub error: Option<String>,
80    pub duration_ms: u64,
81}
82
83impl ToolCallResult {
84    pub const fn success(
85        tool_name: String,
86        arguments: Value,
87        output: Value,
88        duration_ms: u64,
89    ) -> Self {
90        Self {
91            tool_name,
92            arguments,
93            success: true,
94            output,
95            error: None,
96            duration_ms,
97        }
98    }
99
100    pub fn failure(
101        tool_name: String,
102        arguments: Value,
103        error: impl Into<String>,
104        duration_ms: u64,
105    ) -> Self {
106        Self {
107            tool_name,
108            arguments,
109            success: false,
110            output: Value::Null,
111            error: Some(error.into()),
112            duration_ms,
113        }
114    }
115}
116
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
118pub struct ExecutionState {
119    pub results: Vec<ToolCallResult>,
120    pub halted: bool,
121    pub halt_reason: Option<String>,
122}
123
124impl ExecutionState {
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    pub fn add_result(&mut self, result: ToolCallResult) {
130        if !result.success && !self.halted {
131            self.halted = true;
132            self.halt_reason.clone_from(&result.error);
133        }
134        self.results.push(result);
135    }
136
137    pub fn successful_results(&self) -> Vec<&ToolCallResult> {
138        self.results.iter().filter(|r| r.success).collect()
139    }
140
141    pub fn failed_results(&self) -> Vec<&ToolCallResult> {
142        self.results.iter().filter(|r| !r.success).collect()
143    }
144
145    pub fn total_duration_ms(&self) -> u64 {
146        self.results.iter().map(|r| r.duration_ms).sum()
147    }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct TemplateRef {
152    pub tool_index: usize,
153    pub field_path: Vec<String>,
154}
155
156impl TemplateRef {
157    pub fn parse(template: &str) -> Option<Self> {
158        let re = Regex::new(r"^\$(\d+)\.output\.(.+)$").ok()?;
159        let caps = re.captures(template)?;
160
161        let tool_index = caps.get(1)?.as_str().parse().ok()?;
162        let path = caps.get(2)?.as_str();
163        let field_path = path.split('.').map(String::from).collect();
164
165        Some(Self {
166            tool_index,
167            field_path,
168        })
169    }
170
171    pub fn format(&self) -> String {
172        format!("${}.output.{}", self.tool_index, self.field_path.join("."))
173    }
174}