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
204impl AgentError {
205    /// Return the stable category safe for durable and client-visible surfaces.
206    #[must_use]
207    pub const fn public_code(&self) -> &'static str {
208        match self {
209            Self::Model(_) => "model_error",
210            Self::Capability(_) => "capability_error",
211            Self::Cancelled { .. } => "cancelled",
212            Self::CapabilityOrder(_) => "capability_order_error",
213            Self::StructuredOutput(_) => "structured_output_error",
214            Self::DynamicInstruction(_) => "dynamic_instruction_error",
215            Self::OutputRetryLimitExceeded { .. } => "output_retry_limit_exceeded",
216            Self::ToolRetryLimitExceeded { .. } => "tool_retry_limit_exceeded",
217            Self::StepLimitExceeded { .. } => "step_limit_exceeded",
218            Self::UsageLimit(_) => "usage_limit_exceeded",
219            Self::ExecutionSuspended { .. } => "execution_suspended",
220            Self::Executor(_) => "executor_error",
221            Self::ToolCallsRequireTools => "tool_calls_require_tools",
222        }
223    }
224
225    /// Return a diagnostic safe for durable events and client-visible surfaces.
226    ///
227    /// Free-form provider, capability, instruction, executor, and cancellation details are
228    /// intentionally omitted because they can contain request content, credentials, or host
229    /// internals. The original typed error remains available to local callers and telemetry.
230    #[must_use]
231    pub fn public_message(&self) -> String {
232        match self {
233            Self::Model(error) => error.public_message(),
234            Self::Capability(_) => "agent capability failed".to_string(),
235            Self::Cancelled { .. } => "agent run cancelled".to_string(),
236            Self::CapabilityOrder(_) => "agent capability ordering failed".to_string(),
237            Self::StructuredOutput(_) => "structured output could not be processed".to_string(),
238            Self::DynamicInstruction(_) => "dynamic instruction generation failed".to_string(),
239            Self::OutputRetryLimitExceeded { retries } => {
240                format!("output retry limit exceeded after {retries} retries")
241            }
242            Self::ToolRetryLimitExceeded { max_retries, .. } => {
243                format!("tool retry limit exceeded after {max_retries} retries")
244            }
245            Self::StepLimitExceeded { steps } => {
246                format!("step limit exceeded after {steps} steps")
247            }
248            Self::UsageLimit(_) => "usage limit exceeded".to_string(),
249            Self::ExecutionSuspended { .. } => "agent execution suspended".to_string(),
250            Self::Executor(_) => "agent executor failed".to_string(),
251            Self::ToolCallsRequireTools => {
252                "tool calls require starweaver-tools runtime support".to_string()
253            }
254        }
255    }
256}
257
258/// Bare agent result.
259#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
260pub struct AgentResult {
261    /// Final text output.
262    pub output: String,
263    /// Parsed structured output when an output schema is configured.
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub structured_output: Option<serde_json::Value>,
266    /// Canonical message history.
267    pub messages: Vec<ModelMessage>,
268    /// Final run state.
269    pub state: AgentRunState,
270    /// Number of messages supplied as prior history.
271    pub history_len: usize,
272}
273
274impl AgentResult {
275    /// Return all messages visible to the run.
276    #[must_use]
277    pub fn all_messages(&self) -> &[ModelMessage] {
278        &self.messages
279    }
280
281    /// Return messages produced by this run.
282    #[must_use]
283    pub fn new_messages(&self) -> &[ModelMessage] {
284        &self.messages[self.history_len..]
285    }
286
287    /// Return media/file outputs from the latest model response.
288    #[must_use]
289    pub fn media_outputs(&self) -> Vec<OutputMedia> {
290        self.messages
291            .iter()
292            .rev()
293            .find_map(|message| match message {
294                ModelMessage::Response(response) => Some(
295                    response
296                        .parts
297                        .iter()
298                        .filter_map(OutputMedia::from_response_part)
299                        .collect::<Vec<_>>(),
300                ),
301                ModelMessage::Request(_) => None,
302            })
303            .unwrap_or_default()
304    }
305
306    /// Return image outputs from the latest model response.
307    #[must_use]
308    pub fn image_outputs(&self) -> Vec<OutputMedia> {
309        self.media_outputs()
310            .into_iter()
311            .filter(OutputMedia::is_image)
312            .collect()
313    }
314
315    /// Return the final output as text, JSON, or media wrappers.
316    #[must_use]
317    pub fn output_value(&self) -> OutputValue {
318        let media = self.media_outputs();
319        if !media.is_empty() {
320            OutputValue::Media(media)
321        } else if let Some(value) = self.structured_output.clone() {
322            OutputValue::Json(value)
323        } else {
324            OutputValue::Text(self.output.clone())
325        }
326    }
327
328    /// Return true when the run result is waiting for approval or deferred tool results.
329    #[must_use]
330    pub const fn has_pending_hitl(&self) -> bool {
331        self.state.has_pending_hitl()
332    }
333
334    /// Return pending approval-required tool returns.
335    #[must_use]
336    pub fn pending_approvals(&self) -> &[ToolReturnPart] {
337        self.state.pending_approvals()
338    }
339
340    /// Return pending deferred tool returns.
341    #[must_use]
342    pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
343        self.state.pending_deferred_tools()
344    }
345
346    /// Parse structured output into a Rust type.
347    ///
348    /// # Errors
349    ///
350    /// Returns an error when no structured output is present or deserialization fails.
351    pub fn structured<T>(&self) -> Result<T, AgentError>
352    where
353        T: serde::de::DeserializeOwned,
354    {
355        let value = self
356            .structured_output
357            .clone()
358            .ok_or_else(|| AgentError::StructuredOutput("missing structured output".to_string()))?;
359        serde_json::from_value(value)
360            .map_err(|error| AgentError::StructuredOutput(error.to_string()))
361    }
362}
363
364impl From<AgentRunResult> for AgentResult {
365    fn from(result: AgentRunResult) -> Self {
366        Self {
367            output: result.output,
368            structured_output: result.state.structured_output.clone(),
369            messages: result.state.message_history.clone(),
370            state: result.state,
371            history_len: 0,
372        }
373    }
374}