starweaver_runtime/agent/
types.rs1use serde::{Deserialize, Serialize};
4use starweaver_model::{ContentPart, ModelError, ModelMessage, ToolReturnPart};
5use thiserror::Error;
6
7use starweaver_usage::UsageLimitError;
8
9use crate::{
10 capability::CapabilityOrderError,
11 executor::{AgentExecutionNode, AgentExecutorError},
12 output::{OutputMedia, OutputValue},
13 run::{AgentRunResult, AgentRunState},
14};
15
16#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18pub struct AgentInput {
19 pub content: Vec<ContentPart>,
21}
22
23impl AgentInput {
24 #[must_use]
26 pub fn new(content: impl Into<Vec<ContentPart>>) -> Self {
27 Self {
28 content: content.into(),
29 }
30 }
31
32 #[must_use]
34 pub fn text(text: impl Into<String>) -> Self {
35 Self::new(vec![ContentPart::text(text)])
36 }
37
38 #[must_use]
40 pub fn parts(content: impl Into<Vec<ContentPart>>) -> Self {
41 Self::new(content)
42 }
43
44 #[must_use]
46 pub const fn is_empty(&self) -> bool {
47 self.content.is_empty()
48 }
49
50 pub(in crate::agent) fn text_projection(&self) -> String {
51 self.content
52 .iter()
53 .filter_map(|part| match part {
54 ContentPart::Text { text } => Some(text.as_str()),
55 ContentPart::CachePoint { .. }
56 | ContentPart::ImageUrl { .. }
57 | ContentPart::FileUrl { .. }
58 | ContentPart::Binary { .. }
59 | ContentPart::ResourceRef { .. }
60 | ContentPart::DataUrl { .. } => None,
61 })
62 .collect::<Vec<_>>()
63 .join("\n")
64 }
65}
66
67impl From<String> for AgentInput {
68 fn from(text: String) -> Self {
69 Self::text(text)
70 }
71}
72
73impl From<&str> for AgentInput {
74 fn from(text: &str) -> Self {
75 Self::text(text)
76 }
77}
78
79impl From<ContentPart> for AgentInput {
80 fn from(content: ContentPart) -> Self {
81 Self::new(vec![content])
82 }
83}
84
85impl From<Vec<ContentPart>> for AgentInput {
86 fn from(content: Vec<ContentPart>) -> Self {
87 Self::new(content)
88 }
89}
90
91#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
93#[serde(rename_all = "snake_case")]
94pub enum AgentEndStrategy {
95 #[default]
97 Early,
98 Graceful,
100 Exhaustive,
102}
103
104#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
106#[serde(rename_all = "snake_case")]
107pub enum AgentToolExecutionMode {
108 #[default]
110 Parallel,
111 Sequential,
113}
114
115#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
117pub struct AgentRuntimePolicy {
118 pub max_steps: usize,
120 pub output_retries: usize,
122 #[serde(default)]
124 pub end_strategy: AgentEndStrategy,
125 #[serde(default)]
127 pub tool_execution: AgentToolExecutionMode,
128}
129
130impl Default for AgentRuntimePolicy {
131 fn default() -> Self {
132 Self {
133 max_steps: 10_000,
134 output_retries: 1,
135 end_strategy: AgentEndStrategy::Early,
136 tool_execution: AgentToolExecutionMode::Parallel,
137 }
138 }
139}
140
141#[derive(Debug, Error)]
143pub enum AgentError {
144 #[error(transparent)]
146 Model(#[from] ModelError),
147 #[error("capability error: {0}")]
149 Capability(String),
150 #[error("agent run cancelled: {reason}")]
152 Cancelled {
153 reason: String,
155 },
156 #[error(transparent)]
158 CapabilityOrder(#[from] CapabilityOrderError),
159 #[error("structured output error: {0}")]
161 StructuredOutput(String),
162 #[error("dynamic instruction error: {0}")]
164 DynamicInstruction(String),
165 #[error("output retry limit exceeded after {retries} retries")]
167 OutputRetryLimitExceeded {
168 retries: usize,
170 },
171 #[error("tool {tool:?} exceeded max retries count of {max_retries}")]
173 ToolRetryLimitExceeded {
174 tool: String,
176 max_retries: usize,
178 },
179 #[error("step limit exceeded after {steps} steps")]
181 StepLimitExceeded {
182 steps: usize,
184 },
185 #[error(transparent)]
187 UsageLimit(#[from] UsageLimitError),
188 #[error("agent execution suspended at {node:?}: {reason}")]
190 ExecutionSuspended {
191 node: AgentExecutionNode,
193 reason: String,
195 },
196 #[error(transparent)]
198 Executor(#[from] AgentExecutorError),
199 #[error("tool calls require starweaver-tools runtime support")]
201 ToolCallsRequireTools,
202}
203
204#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
206pub struct AgentResult {
207 pub output: String,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub structured_output: Option<serde_json::Value>,
212 pub messages: Vec<ModelMessage>,
214 pub state: AgentRunState,
216 pub history_len: usize,
218}
219
220impl AgentResult {
221 #[must_use]
223 pub fn all_messages(&self) -> &[ModelMessage] {
224 &self.messages
225 }
226
227 #[must_use]
229 pub fn new_messages(&self) -> &[ModelMessage] {
230 &self.messages[self.history_len..]
231 }
232
233 #[must_use]
235 pub fn media_outputs(&self) -> Vec<OutputMedia> {
236 self.messages
237 .iter()
238 .rev()
239 .find_map(|message| match message {
240 ModelMessage::Response(response) => Some(
241 response
242 .parts
243 .iter()
244 .filter_map(OutputMedia::from_response_part)
245 .collect::<Vec<_>>(),
246 ),
247 ModelMessage::Request(_) => None,
248 })
249 .unwrap_or_default()
250 }
251
252 #[must_use]
254 pub fn image_outputs(&self) -> Vec<OutputMedia> {
255 self.media_outputs()
256 .into_iter()
257 .filter(OutputMedia::is_image)
258 .collect()
259 }
260
261 #[must_use]
263 pub fn output_value(&self) -> OutputValue {
264 let media = self.media_outputs();
265 if !media.is_empty() {
266 OutputValue::Media(media)
267 } else if let Some(value) = self.structured_output.clone() {
268 OutputValue::Json(value)
269 } else {
270 OutputValue::Text(self.output.clone())
271 }
272 }
273
274 #[must_use]
276 pub const fn has_pending_hitl(&self) -> bool {
277 self.state.has_pending_hitl()
278 }
279
280 #[must_use]
282 pub fn pending_approvals(&self) -> &[ToolReturnPart] {
283 self.state.pending_approvals()
284 }
285
286 #[must_use]
288 pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
289 self.state.pending_deferred_tools()
290 }
291
292 pub fn structured<T>(&self) -> Result<T, AgentError>
298 where
299 T: serde::de::DeserializeOwned,
300 {
301 let value = self
302 .structured_output
303 .clone()
304 .ok_or_else(|| AgentError::StructuredOutput("missing structured output".to_string()))?;
305 serde_json::from_value(value)
306 .map_err(|error| AgentError::StructuredOutput(error.to_string()))
307 }
308}
309
310impl From<AgentRunResult> for AgentResult {
311 fn from(result: AgentRunResult) -> Self {
312 Self {
313 output: result.output,
314 structured_output: result.state.structured_output.clone(),
315 messages: result.state.message_history.clone(),
316 state: result.state,
317 history_len: 0,
318 }
319 }
320}