Skip to main content

starweaver_runtime/agent/
types.rs

1//! Agent runtime public types.
2
3use 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/// User input for an agent run.
17#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18pub struct AgentInput {
19    /// Ordered multimodal user content parts.
20    pub content: Vec<ContentPart>,
21}
22
23impl AgentInput {
24    /// Build input from ordered user content parts.
25    #[must_use]
26    pub fn new(content: impl Into<Vec<ContentPart>>) -> Self {
27        Self {
28            content: content.into(),
29        }
30    }
31
32    /// Build text-only input.
33    #[must_use]
34    pub fn text(text: impl Into<String>) -> Self {
35        Self::new(vec![ContentPart::text(text)])
36    }
37
38    /// Build input from ordered user content parts.
39    #[must_use]
40    pub fn parts(content: impl Into<Vec<ContentPart>>) -> Self {
41        Self::new(content)
42    }
43
44    /// Return true when no content parts are present.
45    #[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/// Strategy for handling ordinary tool calls returned alongside a final output tool call.
91#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
92#[serde(rename_all = "snake_case")]
93pub enum AgentEndStrategy {
94    /// Stop as soon as a valid output function returns final output.
95    #[default]
96    Early,
97    /// Execute remaining ordinary tools, then complete with the first valid final output.
98    Graceful,
99    /// Execute all ordinary tools, then complete with the first valid final output.
100    Exhaustive,
101}
102
103/// Runtime policy for bare agent runs.
104#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105pub struct AgentRuntimePolicy {
106    /// Maximum model requests in one run.
107    pub max_steps: usize,
108    /// Maximum output validation retries.
109    pub output_retries: usize,
110    /// How to handle ordinary tool calls returned alongside a final output function.
111    #[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/// Bare agent runtime error.
126#[derive(Debug, Error)]
127pub enum AgentError {
128    /// Model adapter failed.
129    #[error(transparent)]
130    Model(#[from] ModelError),
131    /// Capability hook failed.
132    #[error("capability error: {0}")]
133    Capability(String),
134    /// Runtime execution was cancelled cooperatively.
135    #[error("agent run cancelled: {reason}")]
136    Cancelled {
137        /// Human-readable cancellation reason.
138        reason: String,
139    },
140    /// Capability ordering failed.
141    #[error(transparent)]
142    CapabilityOrder(#[from] CapabilityOrderError),
143    /// Structured output parsing failed.
144    #[error("structured output error: {0}")]
145    StructuredOutput(String),
146    /// Dynamic instruction generation failed.
147    #[error("dynamic instruction error: {0}")]
148    DynamicInstruction(String),
149    /// Output retry budget was exceeded.
150    #[error("output retry limit exceeded after {retries} retries")]
151    OutputRetryLimitExceeded {
152        /// Retry count.
153        retries: usize,
154    },
155    /// Tool retry budget was exceeded.
156    #[error("tool {tool:?} exceeded max retries count of {max_retries}")]
157    ToolRetryLimitExceeded {
158        /// Tool name.
159        tool: String,
160        /// Retry limit for this tool.
161        max_retries: usize,
162    },
163    /// Maximum step count was exceeded.
164    #[error("step limit exceeded after {steps} steps")]
165    StepLimitExceeded {
166        /// Step count.
167        steps: usize,
168    },
169    /// Usage limit was exceeded.
170    #[error(transparent)]
171    UsageLimit(#[from] UsageLimitError),
172    /// Execution was suspended at a durable checkpoint.
173    #[error("agent execution suspended at {node:?}: {reason}")]
174    ExecutionSuspended {
175        /// Suspended execution node.
176        node: AgentExecutionNode,
177        /// Suspend reason.
178        reason: String,
179    },
180    /// Durable executor failed.
181    #[error(transparent)]
182    Executor(#[from] AgentExecutorError),
183    /// Model returned tool calls before tool execution exists in this bare runtime.
184    #[error("tool calls require starweaver-tools runtime support")]
185    ToolCallsRequireTools,
186}
187
188/// Bare agent result.
189#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
190pub struct AgentResult {
191    /// Final text output.
192    pub output: String,
193    /// Parsed structured output when an output schema is configured.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub structured_output: Option<serde_json::Value>,
196    /// Canonical message history.
197    pub messages: Vec<ModelMessage>,
198    /// Final run state.
199    pub state: AgentRunState,
200    /// Number of messages supplied as prior history.
201    pub history_len: usize,
202}
203
204impl AgentResult {
205    /// Return all messages visible to the run.
206    #[must_use]
207    pub fn all_messages(&self) -> &[ModelMessage] {
208        &self.messages
209    }
210
211    /// Return messages produced by this run.
212    #[must_use]
213    pub fn new_messages(&self) -> &[ModelMessage] {
214        &self.messages[self.history_len..]
215    }
216
217    /// Return media/file outputs from the latest model response.
218    #[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    /// Return image outputs from the latest model response.
237    #[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    /// Return the final output as text, JSON, or media wrappers.
246    #[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    /// Parse structured output into a Rust type.
259    ///
260    /// # Errors
261    ///
262    /// Returns an error when no structured output is present or deserialization fails.
263    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}