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::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, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
105#[serde(rename_all = "snake_case")]
106pub enum AgentToolExecutionMode {
107 #[default]
109 Parallel,
110 Sequential,
112}
113
114#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
116pub struct AgentRuntimePolicy {
117 pub max_steps: usize,
119 pub output_retries: usize,
121 #[serde(default)]
123 pub end_strategy: AgentEndStrategy,
124 #[serde(default)]
126 pub tool_execution: AgentToolExecutionMode,
127}
128
129impl Default for AgentRuntimePolicy {
130 fn default() -> Self {
131 Self {
132 max_steps: 10_000,
133 output_retries: 1,
134 end_strategy: AgentEndStrategy::Early,
135 tool_execution: AgentToolExecutionMode::Parallel,
136 }
137 }
138}
139
140#[derive(Debug, Error)]
142pub enum AgentError {
143 #[error(transparent)]
145 Model(#[from] ModelError),
146 #[error("capability error: {0}")]
148 Capability(String),
149 #[error("agent run cancelled: {reason}")]
151 Cancelled {
152 reason: String,
154 },
155 #[error(transparent)]
157 CapabilityOrder(#[from] CapabilityOrderError),
158 #[error("structured output error: {0}")]
160 StructuredOutput(String),
161 #[error("dynamic instruction error: {0}")]
163 DynamicInstruction(String),
164 #[error("output retry limit exceeded after {retries} retries")]
166 OutputRetryLimitExceeded {
167 retries: usize,
169 },
170 #[error("tool {tool:?} exceeded max retries count of {max_retries}")]
172 ToolRetryLimitExceeded {
173 tool: String,
175 max_retries: usize,
177 },
178 #[error("step limit exceeded after {steps} steps")]
180 StepLimitExceeded {
181 steps: usize,
183 },
184 #[error(transparent)]
186 UsageLimit(#[from] UsageLimitError),
187 #[error("agent execution suspended at {node:?}: {reason}")]
189 ExecutionSuspended {
190 node: AgentExecutionNode,
192 reason: String,
194 },
195 #[error(transparent)]
197 Executor(#[from] AgentExecutorError),
198 #[error("tool calls require starweaver-tools runtime support")]
200 ToolCallsRequireTools,
201}
202
203#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
205pub struct AgentResult {
206 pub output: String,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub structured_output: Option<serde_json::Value>,
211 pub messages: Vec<ModelMessage>,
213 pub state: AgentRunState,
215 pub history_len: usize,
217}
218
219impl AgentResult {
220 #[must_use]
222 pub fn all_messages(&self) -> &[ModelMessage] {
223 &self.messages
224 }
225
226 #[must_use]
228 pub fn new_messages(&self) -> &[ModelMessage] {
229 &self.messages[self.history_len..]
230 }
231
232 #[must_use]
234 pub fn media_outputs(&self) -> Vec<OutputMedia> {
235 self.messages
236 .iter()
237 .rev()
238 .find_map(|message| match message {
239 ModelMessage::Response(response) => Some(
240 response
241 .parts
242 .iter()
243 .filter_map(OutputMedia::from_response_part)
244 .collect::<Vec<_>>(),
245 ),
246 ModelMessage::Request(_) => None,
247 })
248 .unwrap_or_default()
249 }
250
251 #[must_use]
253 pub fn image_outputs(&self) -> Vec<OutputMedia> {
254 self.media_outputs()
255 .into_iter()
256 .filter(OutputMedia::is_image)
257 .collect()
258 }
259
260 #[must_use]
262 pub fn output_value(&self) -> OutputValue {
263 let media = self.media_outputs();
264 if !media.is_empty() {
265 OutputValue::Media(media)
266 } else if let Some(value) = self.structured_output.clone() {
267 OutputValue::Json(value)
268 } else {
269 OutputValue::Text(self.output.clone())
270 }
271 }
272
273 #[must_use]
275 pub const fn has_pending_hitl(&self) -> bool {
276 self.state.has_pending_hitl()
277 }
278
279 #[must_use]
281 pub fn pending_approvals(&self) -> &[ToolReturnPart] {
282 self.state.pending_approvals()
283 }
284
285 #[must_use]
287 pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
288 self.state.pending_deferred_tools()
289 }
290
291 pub fn structured<T>(&self) -> Result<T, AgentError>
297 where
298 T: serde::de::DeserializeOwned,
299 {
300 let value = self
301 .structured_output
302 .clone()
303 .ok_or_else(|| AgentError::StructuredOutput("missing structured output".to_string()))?;
304 serde_json::from_value(value)
305 .map_err(|error| AgentError::StructuredOutput(error.to_string()))
306 }
307}
308
309impl From<AgentRunResult> for AgentResult {
310 fn from(result: AgentRunResult) -> Self {
311 Self {
312 output: result.output,
313 structured_output: result.state.structured_output.clone(),
314 messages: result.state.message_history.clone(),
315 state: result.state,
316 history_len: 0,
317 }
318 }
319}