use serde::{Deserialize, Serialize};
use starweaver_model::{ContentPart, ModelError, ModelMessage, ToolReturnPart};
use thiserror::Error;
use starweaver_usage::UsageLimitError;
use crate::{
capability::CapabilityOrderError,
executor::{AgentExecutionNode, AgentExecutorError},
output::{OutputMedia, OutputValue},
run::{AgentRunResult, AgentRunState},
};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AgentInput {
pub content: Vec<ContentPart>,
}
impl AgentInput {
#[must_use]
pub fn new(content: impl Into<Vec<ContentPart>>) -> Self {
Self {
content: content.into(),
}
}
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::new(vec![ContentPart::text(text)])
}
#[must_use]
pub fn parts(content: impl Into<Vec<ContentPart>>) -> Self {
Self::new(content)
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.content.is_empty()
}
pub(in crate::agent) fn text_projection(&self) -> String {
self.content
.iter()
.filter_map(|part| match part {
ContentPart::Text { text } => Some(text.as_str()),
ContentPart::ImageUrl { .. }
| ContentPart::FileUrl { .. }
| ContentPart::Binary { .. }
| ContentPart::ResourceRef { .. }
| ContentPart::DataUrl { .. } => None,
})
.collect::<Vec<_>>()
.join("\n")
}
}
impl From<String> for AgentInput {
fn from(text: String) -> Self {
Self::text(text)
}
}
impl From<&str> for AgentInput {
fn from(text: &str) -> Self {
Self::text(text)
}
}
impl From<ContentPart> for AgentInput {
fn from(content: ContentPart) -> Self {
Self::new(vec![content])
}
}
impl From<Vec<ContentPart>> for AgentInput {
fn from(content: Vec<ContentPart>) -> Self {
Self::new(content)
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentEndStrategy {
#[default]
Early,
Graceful,
Exhaustive,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentToolExecutionMode {
#[default]
Parallel,
Sequential,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AgentRuntimePolicy {
pub max_steps: usize,
pub output_retries: usize,
#[serde(default)]
pub end_strategy: AgentEndStrategy,
#[serde(default)]
pub tool_execution: AgentToolExecutionMode,
}
impl Default for AgentRuntimePolicy {
fn default() -> Self {
Self {
max_steps: 10_000,
output_retries: 1,
end_strategy: AgentEndStrategy::Early,
tool_execution: AgentToolExecutionMode::Parallel,
}
}
}
#[derive(Debug, Error)]
pub enum AgentError {
#[error(transparent)]
Model(#[from] ModelError),
#[error("capability error: {0}")]
Capability(String),
#[error("agent run cancelled: {reason}")]
Cancelled {
reason: String,
},
#[error(transparent)]
CapabilityOrder(#[from] CapabilityOrderError),
#[error("structured output error: {0}")]
StructuredOutput(String),
#[error("dynamic instruction error: {0}")]
DynamicInstruction(String),
#[error("output retry limit exceeded after {retries} retries")]
OutputRetryLimitExceeded {
retries: usize,
},
#[error("tool {tool:?} exceeded max retries count of {max_retries}")]
ToolRetryLimitExceeded {
tool: String,
max_retries: usize,
},
#[error("step limit exceeded after {steps} steps")]
StepLimitExceeded {
steps: usize,
},
#[error(transparent)]
UsageLimit(#[from] UsageLimitError),
#[error("agent execution suspended at {node:?}: {reason}")]
ExecutionSuspended {
node: AgentExecutionNode,
reason: String,
},
#[error(transparent)]
Executor(#[from] AgentExecutorError),
#[error("tool calls require starweaver-tools runtime support")]
ToolCallsRequireTools,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AgentResult {
pub output: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured_output: Option<serde_json::Value>,
pub messages: Vec<ModelMessage>,
pub state: AgentRunState,
pub history_len: usize,
}
impl AgentResult {
#[must_use]
pub fn all_messages(&self) -> &[ModelMessage] {
&self.messages
}
#[must_use]
pub fn new_messages(&self) -> &[ModelMessage] {
&self.messages[self.history_len..]
}
#[must_use]
pub fn media_outputs(&self) -> Vec<OutputMedia> {
self.messages
.iter()
.rev()
.find_map(|message| match message {
ModelMessage::Response(response) => Some(
response
.parts
.iter()
.filter_map(OutputMedia::from_response_part)
.collect::<Vec<_>>(),
),
ModelMessage::Request(_) => None,
})
.unwrap_or_default()
}
#[must_use]
pub fn image_outputs(&self) -> Vec<OutputMedia> {
self.media_outputs()
.into_iter()
.filter(OutputMedia::is_image)
.collect()
}
#[must_use]
pub fn output_value(&self) -> OutputValue {
let media = self.media_outputs();
if !media.is_empty() {
OutputValue::Media(media)
} else if let Some(value) = self.structured_output.clone() {
OutputValue::Json(value)
} else {
OutputValue::Text(self.output.clone())
}
}
#[must_use]
pub const fn has_pending_hitl(&self) -> bool {
self.state.has_pending_hitl()
}
#[must_use]
pub fn pending_approvals(&self) -> &[ToolReturnPart] {
self.state.pending_approvals()
}
#[must_use]
pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
self.state.pending_deferred_tools()
}
pub fn structured<T>(&self) -> Result<T, AgentError>
where
T: serde::de::DeserializeOwned,
{
let value = self
.structured_output
.clone()
.ok_or_else(|| AgentError::StructuredOutput("missing structured output".to_string()))?;
serde_json::from_value(value)
.map_err(|error| AgentError::StructuredOutput(error.to_string()))
}
}
impl From<AgentRunResult> for AgentResult {
fn from(result: AgentRunResult) -> Self {
Self {
output: result.output,
structured_output: result.state.structured_output.clone(),
messages: result.state.message_history.clone(),
state: result.state,
history_len: 0,
}
}
}