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
204impl AgentError {
205 #[must_use]
207 pub const fn public_code(&self) -> &'static str {
208 match self {
209 Self::Model(_) => "model_error",
210 Self::Capability(_) => "capability_error",
211 Self::Cancelled { .. } => "cancelled",
212 Self::CapabilityOrder(_) => "capability_order_error",
213 Self::StructuredOutput(_) => "structured_output_error",
214 Self::DynamicInstruction(_) => "dynamic_instruction_error",
215 Self::OutputRetryLimitExceeded { .. } => "output_retry_limit_exceeded",
216 Self::ToolRetryLimitExceeded { .. } => "tool_retry_limit_exceeded",
217 Self::StepLimitExceeded { .. } => "step_limit_exceeded",
218 Self::UsageLimit(_) => "usage_limit_exceeded",
219 Self::ExecutionSuspended { .. } => "execution_suspended",
220 Self::Executor(_) => "executor_error",
221 Self::ToolCallsRequireTools => "tool_calls_require_tools",
222 }
223 }
224
225 #[must_use]
231 pub fn public_message(&self) -> String {
232 match self {
233 Self::Model(error) => error.public_message(),
234 Self::Capability(_) => "agent capability failed".to_string(),
235 Self::Cancelled { .. } => "agent run cancelled".to_string(),
236 Self::CapabilityOrder(_) => "agent capability ordering failed".to_string(),
237 Self::StructuredOutput(_) => "structured output could not be processed".to_string(),
238 Self::DynamicInstruction(_) => "dynamic instruction generation failed".to_string(),
239 Self::OutputRetryLimitExceeded { retries } => {
240 format!("output retry limit exceeded after {retries} retries")
241 }
242 Self::ToolRetryLimitExceeded { max_retries, .. } => {
243 format!("tool retry limit exceeded after {max_retries} retries")
244 }
245 Self::StepLimitExceeded { steps } => {
246 format!("step limit exceeded after {steps} steps")
247 }
248 Self::UsageLimit(_) => "usage limit exceeded".to_string(),
249 Self::ExecutionSuspended { .. } => "agent execution suspended".to_string(),
250 Self::Executor(_) => "agent executor failed".to_string(),
251 Self::ToolCallsRequireTools => {
252 "tool calls require starweaver-tools runtime support".to_string()
253 }
254 }
255 }
256}
257
258#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
260pub struct AgentResult {
261 pub output: String,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub structured_output: Option<serde_json::Value>,
266 pub messages: Vec<ModelMessage>,
268 pub state: AgentRunState,
270 pub history_len: usize,
272}
273
274impl AgentResult {
275 #[must_use]
277 pub fn all_messages(&self) -> &[ModelMessage] {
278 &self.messages
279 }
280
281 #[must_use]
283 pub fn new_messages(&self) -> &[ModelMessage] {
284 &self.messages[self.history_len..]
285 }
286
287 #[must_use]
289 pub fn media_outputs(&self) -> Vec<OutputMedia> {
290 self.messages
291 .iter()
292 .rev()
293 .find_map(|message| match message {
294 ModelMessage::Response(response) => Some(
295 response
296 .parts
297 .iter()
298 .filter_map(OutputMedia::from_response_part)
299 .collect::<Vec<_>>(),
300 ),
301 ModelMessage::Request(_) => None,
302 })
303 .unwrap_or_default()
304 }
305
306 #[must_use]
308 pub fn image_outputs(&self) -> Vec<OutputMedia> {
309 self.media_outputs()
310 .into_iter()
311 .filter(OutputMedia::is_image)
312 .collect()
313 }
314
315 #[must_use]
317 pub fn output_value(&self) -> OutputValue {
318 let media = self.media_outputs();
319 if !media.is_empty() {
320 OutputValue::Media(media)
321 } else if let Some(value) = self.structured_output.clone() {
322 OutputValue::Json(value)
323 } else {
324 OutputValue::Text(self.output.clone())
325 }
326 }
327
328 #[must_use]
330 pub const fn has_pending_hitl(&self) -> bool {
331 self.state.has_pending_hitl()
332 }
333
334 #[must_use]
336 pub fn pending_approvals(&self) -> &[ToolReturnPart] {
337 self.state.pending_approvals()
338 }
339
340 #[must_use]
342 pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
343 self.state.pending_deferred_tools()
344 }
345
346 pub fn structured<T>(&self) -> Result<T, AgentError>
352 where
353 T: serde::de::DeserializeOwned,
354 {
355 let value = self
356 .structured_output
357 .clone()
358 .ok_or_else(|| AgentError::StructuredOutput("missing structured output".to_string()))?;
359 serde_json::from_value(value)
360 .map_err(|error| AgentError::StructuredOutput(error.to_string()))
361 }
362}
363
364impl From<AgentRunResult> for AgentResult {
365 fn from(result: AgentRunResult) -> Self {
366 Self {
367 output: result.output,
368 structured_output: result.state.structured_output.clone(),
369 messages: result.state.message_history.clone(),
370 state: result.state,
371 history_len: 0,
372 }
373 }
374}