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