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, 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/// 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 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/// Strategy for handling ordinary tool calls returned alongside a final output tool call.
92#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
93#[serde(rename_all = "snake_case")]
94pub enum AgentEndStrategy {
95    /// Stop as soon as a valid output function returns final output.
96    #[default]
97    Early,
98    /// Execute remaining ordinary tools, then complete with the first valid final output.
99    Graceful,
100    /// Execute all ordinary tools, then complete with the first valid final output.
101    Exhaustive,
102}
103
104/// Runtime scheduling mode for a batch of model-returned tool calls.
105#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
106#[serde(rename_all = "snake_case")]
107pub enum AgentToolExecutionMode {
108    /// Execute independent tool calls concurrently when no tool requests sequential execution.
109    #[default]
110    Parallel,
111    /// Execute tool calls one at a time in model-returned order.
112    Sequential,
113}
114
115/// Runtime policy for bare agent runs.
116#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
117pub struct AgentRuntimePolicy {
118    /// Maximum model requests in one run.
119    pub max_steps: usize,
120    /// Maximum output validation retries.
121    pub output_retries: usize,
122    /// How to handle ordinary tool calls returned alongside a final output function.
123    #[serde(default)]
124    pub end_strategy: AgentEndStrategy,
125    /// How to schedule batches of model-returned tool calls.
126    #[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/// Bare agent runtime error.
142#[derive(Debug, Error)]
143pub enum AgentError {
144    /// Model adapter failed.
145    #[error(transparent)]
146    Model(#[from] ModelError),
147    /// Capability hook failed.
148    #[error("capability error: {0}")]
149    Capability(String),
150    /// Runtime execution was cancelled cooperatively.
151    #[error("agent run cancelled: {reason}")]
152    Cancelled {
153        /// Human-readable cancellation reason.
154        reason: String,
155    },
156    /// Capability ordering failed.
157    #[error(transparent)]
158    CapabilityOrder(#[from] CapabilityOrderError),
159    /// Structured output parsing failed.
160    #[error("structured output error: {0}")]
161    StructuredOutput(String),
162    /// Dynamic instruction generation failed.
163    #[error("dynamic instruction error: {0}")]
164    DynamicInstruction(String),
165    /// Output retry budget was exceeded.
166    #[error("output retry limit exceeded after {retries} retries")]
167    OutputRetryLimitExceeded {
168        /// Retry count.
169        retries: usize,
170    },
171    /// Tool retry budget was exceeded.
172    #[error("tool {tool:?} exceeded max retries count of {max_retries}")]
173    ToolRetryLimitExceeded {
174        /// Tool name.
175        tool: String,
176        /// Retry limit for this tool.
177        max_retries: usize,
178    },
179    /// Maximum step count was exceeded.
180    #[error("step limit exceeded after {steps} steps")]
181    StepLimitExceeded {
182        /// Step count.
183        steps: usize,
184    },
185    /// Usage limit was exceeded.
186    #[error(transparent)]
187    UsageLimit(#[from] UsageLimitError),
188    /// Execution was suspended at a durable checkpoint.
189    #[error("agent execution suspended at {node:?}: {reason}")]
190    ExecutionSuspended {
191        /// Suspended execution node.
192        node: AgentExecutionNode,
193        /// Suspend reason.
194        reason: String,
195    },
196    /// Durable executor failed.
197    #[error(transparent)]
198    Executor(#[from] AgentExecutorError),
199    /// Model returned tool calls before tool execution exists in this bare runtime.
200    #[error("tool calls require starweaver-tools runtime support")]
201    ToolCallsRequireTools,
202}
203
204/// Bare agent result.
205#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
206pub struct AgentResult {
207    /// Final text output.
208    pub output: String,
209    /// Parsed structured output when an output schema is configured.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub structured_output: Option<serde_json::Value>,
212    /// Canonical message history.
213    pub messages: Vec<ModelMessage>,
214    /// Final run state.
215    pub state: AgentRunState,
216    /// Number of messages supplied as prior history.
217    pub history_len: usize,
218}
219
220impl AgentResult {
221    /// Return all messages visible to the run.
222    #[must_use]
223    pub fn all_messages(&self) -> &[ModelMessage] {
224        &self.messages
225    }
226
227    /// Return messages produced by this run.
228    #[must_use]
229    pub fn new_messages(&self) -> &[ModelMessage] {
230        &self.messages[self.history_len..]
231    }
232
233    /// Return media/file outputs from the latest model response.
234    #[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    /// Return image outputs from the latest model response.
253    #[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    /// Return the final output as text, JSON, or media wrappers.
262    #[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    /// Return true when the run result is waiting for approval or deferred tool results.
275    #[must_use]
276    pub const fn has_pending_hitl(&self) -> bool {
277        self.state.has_pending_hitl()
278    }
279
280    /// Return pending approval-required tool returns.
281    #[must_use]
282    pub fn pending_approvals(&self) -> &[ToolReturnPart] {
283        self.state.pending_approvals()
284    }
285
286    /// Return pending deferred tool returns.
287    #[must_use]
288    pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
289        self.state.pending_deferred_tools()
290    }
291
292    /// Parse structured output into a Rust type.
293    ///
294    /// # Errors
295    ///
296    /// Returns an error when no structured output is present or deserialization fails.
297    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}