starweaver_runtime/agent/
types.rs1use serde::{Deserialize, Serialize};
4use starweaver_model::{ContentPart, ModelError, ModelMessage};
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 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::ImageUrl { .. }
56 | ContentPart::FileUrl { .. }
57 | ContentPart::Binary { .. }
58 | ContentPart::ResourceRef { .. }
59 | ContentPart::DataUrl { .. } => None,
60 })
61 .collect::<Vec<_>>()
62 .join("\n")
63 }
64}
65
66impl From<String> for AgentInput {
67 fn from(text: String) -> Self {
68 Self::text(text)
69 }
70}
71
72impl From<&str> for AgentInput {
73 fn from(text: &str) -> Self {
74 Self::text(text)
75 }
76}
77
78impl From<ContentPart> for AgentInput {
79 fn from(content: ContentPart) -> Self {
80 Self::new(vec![content])
81 }
82}
83
84impl From<Vec<ContentPart>> for AgentInput {
85 fn from(content: Vec<ContentPart>) -> Self {
86 Self::new(content)
87 }
88}
89
90#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
92#[serde(rename_all = "snake_case")]
93pub enum AgentEndStrategy {
94 #[default]
96 Early,
97 Graceful,
99 Exhaustive,
101}
102
103#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105pub struct AgentRuntimePolicy {
106 pub max_steps: usize,
108 pub output_retries: usize,
110 #[serde(default)]
112 pub end_strategy: AgentEndStrategy,
113}
114
115impl Default for AgentRuntimePolicy {
116 fn default() -> Self {
117 Self {
118 max_steps: 10_000,
119 output_retries: 1,
120 end_strategy: AgentEndStrategy::Early,
121 }
122 }
123}
124
125#[derive(Debug, Error)]
127pub enum AgentError {
128 #[error(transparent)]
130 Model(#[from] ModelError),
131 #[error("capability error: {0}")]
133 Capability(String),
134 #[error("agent run cancelled: {reason}")]
136 Cancelled {
137 reason: String,
139 },
140 #[error(transparent)]
142 CapabilityOrder(#[from] CapabilityOrderError),
143 #[error("structured output error: {0}")]
145 StructuredOutput(String),
146 #[error("dynamic instruction error: {0}")]
148 DynamicInstruction(String),
149 #[error("output retry limit exceeded after {retries} retries")]
151 OutputRetryLimitExceeded {
152 retries: usize,
154 },
155 #[error("tool {tool:?} exceeded max retries count of {max_retries}")]
157 ToolRetryLimitExceeded {
158 tool: String,
160 max_retries: usize,
162 },
163 #[error("step limit exceeded after {steps} steps")]
165 StepLimitExceeded {
166 steps: usize,
168 },
169 #[error(transparent)]
171 UsageLimit(#[from] UsageLimitError),
172 #[error("agent execution suspended at {node:?}: {reason}")]
174 ExecutionSuspended {
175 node: AgentExecutionNode,
177 reason: String,
179 },
180 #[error(transparent)]
182 Executor(#[from] AgentExecutorError),
183 #[error("tool calls require starweaver-tools runtime support")]
185 ToolCallsRequireTools,
186}
187
188#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
190pub struct AgentResult {
191 pub output: String,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub structured_output: Option<serde_json::Value>,
196 pub messages: Vec<ModelMessage>,
198 pub state: AgentRunState,
200 pub history_len: usize,
202}
203
204impl AgentResult {
205 #[must_use]
207 pub fn all_messages(&self) -> &[ModelMessage] {
208 &self.messages
209 }
210
211 #[must_use]
213 pub fn new_messages(&self) -> &[ModelMessage] {
214 &self.messages[self.history_len..]
215 }
216
217 #[must_use]
219 pub fn media_outputs(&self) -> Vec<OutputMedia> {
220 self.messages
221 .iter()
222 .rev()
223 .find_map(|message| match message {
224 ModelMessage::Response(response) => Some(
225 response
226 .parts
227 .iter()
228 .filter_map(OutputMedia::from_response_part)
229 .collect::<Vec<_>>(),
230 ),
231 ModelMessage::Request(_) => None,
232 })
233 .unwrap_or_default()
234 }
235
236 #[must_use]
238 pub fn image_outputs(&self) -> Vec<OutputMedia> {
239 self.media_outputs()
240 .into_iter()
241 .filter(OutputMedia::is_image)
242 .collect()
243 }
244
245 #[must_use]
247 pub fn output_value(&self) -> OutputValue {
248 let media = self.media_outputs();
249 if !media.is_empty() {
250 OutputValue::Media(media)
251 } else if let Some(value) = self.structured_output.clone() {
252 OutputValue::Json(value)
253 } else {
254 OutputValue::Text(self.output.clone())
255 }
256 }
257
258 pub fn structured<T>(&self) -> Result<T, AgentError>
264 where
265 T: serde::de::DeserializeOwned,
266 {
267 let value = self
268 .structured_output
269 .clone()
270 .ok_or_else(|| AgentError::StructuredOutput("missing structured output".to_string()))?;
271 serde_json::from_value(value)
272 .map_err(|error| AgentError::StructuredOutput(error.to_string()))
273 }
274}
275
276impl From<AgentRunResult> for AgentResult {
277 fn from(result: AgentRunResult) -> Self {
278 Self {
279 output: result.output,
280 structured_output: result.state.structured_output.clone(),
281 messages: result.state.message_history.clone(),
282 state: result.state,
283 history_len: 0,
284 }
285 }
286}