use serde::{Deserialize, Serialize};
use crate::content::{
Annotation, CodeExecutionLanguage, Content, FileSearchResultItem, GoogleMapsResultItem,
GoogleSearchResultItem, Resolution, UrlContextResultItem,
};
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct StepError {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Vec<serde_json::Value>>,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum FunctionResultPayload {
Text(String),
Json(serde_json::Value),
Contents(Vec<Content>),
}
impl FunctionResultPayload {
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(t) => Some(t),
_ => None,
}
}
#[must_use]
pub fn as_json(&self) -> Option<&serde_json::Value> {
match self {
Self::Json(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn as_contents(&self) -> Option<&[Content]> {
match self {
Self::Contents(c) => Some(c),
_ => None,
}
}
#[must_use]
pub fn to_value(&self) -> serde_json::Value {
match self {
Self::Text(t) => serde_json::Value::String(t.clone()),
Self::Json(v) => v.clone(),
Self::Contents(c) => serde_json::to_value(c).unwrap_or(serde_json::Value::Null),
}
}
}
impl From<serde_json::Value> for FunctionResultPayload {
fn from(value: serde_json::Value) -> Self {
match value {
serde_json::Value::String(s) => Self::Text(s),
other => Self::Json(other),
}
}
}
impl From<&str> for FunctionResultPayload {
fn from(value: &str) -> Self {
Self::Text(value.to_string())
}
}
impl From<String> for FunctionResultPayload {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl From<Vec<Content>> for FunctionResultPayload {
fn from(value: Vec<Content>) -> Self {
Self::Contents(value)
}
}
impl Serialize for FunctionResultPayload {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Text(t) => serializer.serialize_str(t),
Self::Json(v) => v.serialize(serializer),
Self::Contents(c) => c.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for FunctionResultPayload {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
Ok(Self::from_value(value))
}
}
impl FunctionResultPayload {
#[must_use]
pub fn from_value(value: serde_json::Value) -> Self {
match value {
serde_json::Value::String(s) => Self::Text(s),
serde_json::Value::Array(items) => {
let looks_like_contents = !items.is_empty()
&& items
.iter()
.all(|v| v.is_object() && v.get("type").is_some_and(|t| t.is_string()));
if looks_like_contents {
match serde_json::from_value::<Vec<Content>>(serde_json::Value::Array(
items.clone(),
)) {
Ok(contents) => Self::Contents(contents),
Err(_) => Self::Json(serde_json::Value::Array(items)),
}
} else {
Self::Json(serde_json::Value::Array(items))
}
}
other => Self::Json(other),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Step {
UserInput {
content: Vec<Content>,
},
ModelOutput {
content: Vec<Content>,
error: Option<StepError>,
},
Thought {
signature: Option<String>,
summary: Vec<Content>,
},
FunctionCall {
id: String,
name: String,
arguments: serde_json::Value,
signature: Option<String>,
},
FunctionResult {
call_id: String,
name: Option<String>,
result: FunctionResultPayload,
is_error: Option<bool>,
signature: Option<String>,
},
CodeExecutionCall {
id: String,
language: CodeExecutionLanguage,
code: String,
signature: Option<String>,
},
CodeExecutionResult {
call_id: String,
result: String,
is_error: bool,
signature: Option<String>,
},
UrlContextCall {
id: String,
urls: Vec<String>,
signature: Option<String>,
},
UrlContextResult {
call_id: String,
result: Vec<UrlContextResultItem>,
is_error: Option<bool>,
signature: Option<String>,
},
GoogleSearchCall {
id: String,
queries: Vec<String>,
search_type: Option<crate::tools::SearchType>,
signature: Option<String>,
},
GoogleSearchResult {
call_id: String,
result: Vec<GoogleSearchResultItem>,
is_error: Option<bool>,
signature: Option<String>,
},
McpServerToolCall {
id: String,
name: String,
server_name: String,
arguments: serde_json::Value,
},
McpServerToolResult {
call_id: String,
name: Option<String>,
server_name: Option<String>,
result: FunctionResultPayload,
},
FileSearchCall {
id: String,
signature: Option<String>,
},
FileSearchResult {
call_id: String,
result: Vec<FileSearchResultItem>,
signature: Option<String>,
},
GoogleMapsCall {
id: String,
queries: Vec<String>,
signature: Option<String>,
},
GoogleMapsResult {
call_id: String,
result: Vec<GoogleMapsResultItem>,
signature: Option<String>,
},
Unknown {
step_type: String,
data: serde_json::Value,
},
}
impl Step {
#[must_use]
pub const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown { .. })
}
#[must_use]
pub fn unknown_step_type(&self) -> Option<&str> {
match self {
Self::Unknown { step_type, .. } => Some(step_type),
_ => None,
}
}
#[must_use]
pub fn unknown_data(&self) -> Option<&serde_json::Value> {
match self {
Self::Unknown { data, .. } => Some(data),
_ => None,
}
}
pub fn user_text(text: impl Into<String>) -> Self {
Self::UserInput {
content: vec![Content::text(text)],
}
}
#[must_use]
pub fn user_input(content: Vec<Content>) -> Self {
Self::UserInput { content }
}
pub fn model_text(text: impl Into<String>) -> Self {
Self::ModelOutput {
content: vec![Content::text(text)],
error: None,
}
}
#[must_use]
pub fn model_output(content: Vec<Content>) -> Self {
Self::ModelOutput {
content,
error: None,
}
}
pub fn thought(signature: impl Into<String>) -> Self {
Self::Thought {
signature: Some(signature.into()),
summary: Vec::new(),
}
}
pub fn function_call(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
Self::FunctionCall {
id: id.into(),
name: name.into(),
arguments,
signature: None,
}
}
pub fn function_result(
name: impl Into<String>,
call_id: impl Into<String>,
result: impl Into<FunctionResultPayload>,
) -> Self {
Self::FunctionResult {
call_id: call_id.into(),
name: Some(name.into()),
result: result.into(),
is_error: None,
signature: None,
}
}
pub fn function_result_error(
name: impl Into<String>,
call_id: impl Into<String>,
result: impl Into<FunctionResultPayload>,
) -> Self {
Self::FunctionResult {
call_id: call_id.into(),
name: Some(name.into()),
result: result.into(),
is_error: Some(true),
signature: None,
}
}
#[must_use]
pub fn content(&self) -> Option<&[Content]> {
match self {
Self::UserInput { content } | Self::ModelOutput { content, .. } => Some(content),
_ => None,
}
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
self.content()?.iter().find_map(Content::as_text)
}
#[must_use]
pub fn signature(&self) -> Option<&str> {
match self {
Self::Thought { signature, .. }
| Self::FunctionCall { signature, .. }
| Self::FunctionResult { signature, .. }
| Self::CodeExecutionCall { signature, .. }
| Self::CodeExecutionResult { signature, .. }
| Self::UrlContextCall { signature, .. }
| Self::UrlContextResult { signature, .. }
| Self::GoogleSearchCall { signature, .. }
| Self::GoogleSearchResult { signature, .. }
| Self::FileSearchCall { signature, .. }
| Self::FileSearchResult { signature, .. }
| Self::GoogleMapsCall { signature, .. }
| Self::GoogleMapsResult { signature, .. } => signature.as_deref(),
_ => None,
}
}
#[must_use]
pub fn step_type(&self) -> &str {
match self {
Self::UserInput { .. } => "user_input",
Self::ModelOutput { .. } => "model_output",
Self::Thought { .. } => "thought",
Self::FunctionCall { .. } => "function_call",
Self::FunctionResult { .. } => "function_result",
Self::CodeExecutionCall { .. } => "code_execution_call",
Self::CodeExecutionResult { .. } => "code_execution_result",
Self::UrlContextCall { .. } => "url_context_call",
Self::UrlContextResult { .. } => "url_context_result",
Self::GoogleSearchCall { .. } => "google_search_call",
Self::GoogleSearchResult { .. } => "google_search_result",
Self::McpServerToolCall { .. } => "mcp_server_tool_call",
Self::McpServerToolResult { .. } => "mcp_server_tool_result",
Self::FileSearchCall { .. } => "file_search_call",
Self::FileSearchResult { .. } => "file_search_result",
Self::GoogleMapsCall { .. } => "google_maps_call",
Self::GoogleMapsResult { .. } => "google_maps_result",
Self::Unknown { step_type, .. } => step_type,
}
}
}
impl Serialize for Step {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
match self {
Self::UserInput { content } => {
map.serialize_entry("type", "user_input")?;
map.serialize_entry("content", content)?;
}
Self::ModelOutput { content, error } => {
map.serialize_entry("type", "model_output")?;
map.serialize_entry("content", content)?;
if let Some(e) = error {
map.serialize_entry("error", e)?;
}
}
Self::Thought { signature, summary } => {
map.serialize_entry("type", "thought")?;
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
if !summary.is_empty() {
map.serialize_entry("summary", summary)?;
}
}
Self::FunctionCall {
id,
name,
arguments,
signature,
} => {
map.serialize_entry("type", "function_call")?;
map.serialize_entry("id", id)?;
map.serialize_entry("name", name)?;
map.serialize_entry("arguments", arguments)?;
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::FunctionResult {
call_id,
name,
result,
is_error,
signature,
} => {
map.serialize_entry("type", "function_result")?;
map.serialize_entry("call_id", call_id)?;
if let Some(n) = name {
map.serialize_entry("name", n)?;
}
map.serialize_entry("result", result)?;
if let Some(e) = is_error {
map.serialize_entry("is_error", e)?;
}
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::CodeExecutionCall {
id,
language,
code,
signature,
} => {
map.serialize_entry("type", "code_execution_call")?;
map.serialize_entry("id", id)?;
map.serialize_entry(
"arguments",
&serde_json::json!({ "language": language, "code": code }),
)?;
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::CodeExecutionResult {
call_id,
result,
is_error,
signature,
} => {
map.serialize_entry("type", "code_execution_result")?;
map.serialize_entry("call_id", call_id)?;
map.serialize_entry("result", result)?;
map.serialize_entry("is_error", is_error)?;
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::UrlContextCall {
id,
urls,
signature,
} => {
map.serialize_entry("type", "url_context_call")?;
map.serialize_entry("id", id)?;
map.serialize_entry("arguments", &serde_json::json!({ "urls": urls }))?;
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::UrlContextResult {
call_id,
result,
is_error,
signature,
} => {
map.serialize_entry("type", "url_context_result")?;
map.serialize_entry("call_id", call_id)?;
map.serialize_entry("result", result)?;
if let Some(e) = is_error {
map.serialize_entry("is_error", e)?;
}
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::GoogleSearchCall {
id,
queries,
search_type,
signature,
} => {
map.serialize_entry("type", "google_search_call")?;
map.serialize_entry("id", id)?;
map.serialize_entry("arguments", &serde_json::json!({ "queries": queries }))?;
if let Some(st) = search_type {
map.serialize_entry("search_type", st)?;
}
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::GoogleSearchResult {
call_id,
result,
is_error,
signature,
} => {
map.serialize_entry("type", "google_search_result")?;
map.serialize_entry("call_id", call_id)?;
map.serialize_entry("result", result)?;
if let Some(e) = is_error {
map.serialize_entry("is_error", e)?;
}
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::McpServerToolCall {
id,
name,
server_name,
arguments,
} => {
map.serialize_entry("type", "mcp_server_tool_call")?;
map.serialize_entry("id", id)?;
map.serialize_entry("name", name)?;
map.serialize_entry("server_name", server_name)?;
map.serialize_entry("arguments", arguments)?;
}
Self::McpServerToolResult {
call_id,
name,
server_name,
result,
} => {
map.serialize_entry("type", "mcp_server_tool_result")?;
map.serialize_entry("call_id", call_id)?;
if let Some(n) = name {
map.serialize_entry("name", n)?;
}
if let Some(sn) = server_name {
map.serialize_entry("server_name", sn)?;
}
map.serialize_entry("result", result)?;
}
Self::FileSearchCall { id, signature } => {
map.serialize_entry("type", "file_search_call")?;
map.serialize_entry("id", id)?;
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::FileSearchResult {
call_id,
result,
signature,
} => {
map.serialize_entry("type", "file_search_result")?;
map.serialize_entry("call_id", call_id)?;
if !result.is_empty() {
map.serialize_entry("result", result)?;
}
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::GoogleMapsCall {
id,
queries,
signature,
} => {
map.serialize_entry("type", "google_maps_call")?;
map.serialize_entry("id", id)?;
map.serialize_entry("arguments", &serde_json::json!({ "queries": queries }))?;
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::GoogleMapsResult {
call_id,
result,
signature,
} => {
map.serialize_entry("type", "google_maps_result")?;
map.serialize_entry("call_id", call_id)?;
map.serialize_entry("result", result)?;
if let Some(s) = signature {
map.serialize_entry("signature", s)?;
}
}
Self::Unknown { step_type, data } => {
map.serialize_entry("type", step_type)?;
match data {
serde_json::Value::Object(obj) => {
for (key, value) in obj {
if key != "type" {
map.serialize_entry(key, value)?;
}
}
}
other if !other.is_null() => {
map.serialize_entry("data", other)?;
}
_ => {}
}
}
}
map.end()
}
}
fn string_vec_from_arguments(arguments: Option<&serde_json::Value>, key: &str) -> Vec<String> {
arguments
.and_then(|args| args.get(key))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
impl<'de> Deserialize<'de> for Step {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[cfg(feature = "strict-unknown")]
use serde::de::Error as _;
let value = serde_json::Value::deserialize(deserializer)?;
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum KnownStep {
UserInput {
#[serde(default)]
content: Vec<Content>,
},
ModelOutput {
#[serde(default)]
content: Vec<Content>,
#[serde(default)]
error: Option<StepError>,
},
Thought {
#[serde(default)]
signature: Option<String>,
#[serde(default)]
summary: Vec<Content>,
},
FunctionCall {
id: String,
name: String,
#[serde(default)]
arguments: serde_json::Value,
#[serde(default)]
signature: Option<String>,
},
FunctionResult {
call_id: String,
#[serde(default)]
name: Option<String>,
#[serde(default)]
result: Option<FunctionResultPayload>,
#[serde(default)]
is_error: Option<bool>,
#[serde(default)]
signature: Option<String>,
},
CodeExecutionCall {
id: String,
#[serde(default)]
arguments: Option<serde_json::Value>,
#[serde(default)]
signature: Option<String>,
},
CodeExecutionResult {
call_id: String,
#[serde(default)]
result: String,
#[serde(default)]
is_error: Option<bool>,
#[serde(default)]
signature: Option<String>,
},
UrlContextCall {
id: String,
#[serde(default)]
arguments: Option<serde_json::Value>,
#[serde(default)]
signature: Option<String>,
},
UrlContextResult {
call_id: String,
#[serde(default)]
result: Vec<UrlContextResultItem>,
#[serde(default)]
is_error: Option<bool>,
#[serde(default)]
signature: Option<String>,
},
GoogleSearchCall {
id: String,
#[serde(default)]
arguments: Option<serde_json::Value>,
#[serde(default)]
search_type: Option<crate::tools::SearchType>,
#[serde(default)]
signature: Option<String>,
},
GoogleSearchResult {
call_id: String,
#[serde(default)]
result: Vec<GoogleSearchResultItem>,
#[serde(default)]
is_error: Option<bool>,
#[serde(default)]
signature: Option<String>,
},
McpServerToolCall {
id: String,
name: String,
server_name: String,
#[serde(default)]
arguments: serde_json::Value,
},
McpServerToolResult {
call_id: String,
#[serde(default)]
name: Option<String>,
#[serde(default)]
server_name: Option<String>,
#[serde(default)]
result: Option<FunctionResultPayload>,
},
FileSearchCall {
id: String,
#[serde(default)]
signature: Option<String>,
},
FileSearchResult {
call_id: String,
#[serde(default)]
result: Vec<FileSearchResultItem>,
#[serde(default)]
signature: Option<String>,
},
GoogleMapsCall {
id: String,
#[serde(default)]
arguments: Option<serde_json::Value>,
#[serde(default)]
signature: Option<String>,
},
GoogleMapsResult {
call_id: String,
#[serde(default)]
result: Vec<GoogleMapsResultItem>,
#[serde(default)]
signature: Option<String>,
},
}
match serde_json::from_value::<KnownStep>(value.clone()) {
Ok(known) => Ok(match known {
KnownStep::UserInput { content } => Step::UserInput { content },
KnownStep::ModelOutput { content, error } => Step::ModelOutput { content, error },
KnownStep::Thought { signature, summary } => Step::Thought { signature, summary },
KnownStep::FunctionCall {
id,
name,
arguments,
signature,
} => Step::FunctionCall {
id,
name,
arguments,
signature,
},
KnownStep::FunctionResult {
call_id,
name,
result,
is_error,
signature,
} => Step::FunctionResult {
call_id,
name,
result: result.unwrap_or(FunctionResultPayload::Json(serde_json::Value::Null)),
is_error,
signature,
},
KnownStep::CodeExecutionCall {
id,
arguments,
signature,
} => {
let language = arguments
.as_ref()
.and_then(|a| a.get("language"))
.and_then(|l| {
serde_json::from_value::<CodeExecutionLanguage>(l.clone()).ok()
})
.unwrap_or_default();
let code = arguments
.as_ref()
.and_then(|a| a.get("code"))
.and_then(|c| c.as_str())
.unwrap_or_default()
.to_string();
Step::CodeExecutionCall {
id,
language,
code,
signature,
}
}
KnownStep::CodeExecutionResult {
call_id,
result,
is_error,
signature,
} => Step::CodeExecutionResult {
call_id,
result,
is_error: is_error.unwrap_or(false),
signature,
},
KnownStep::UrlContextCall {
id,
arguments,
signature,
} => Step::UrlContextCall {
id,
urls: string_vec_from_arguments(arguments.as_ref(), "urls"),
signature,
},
KnownStep::UrlContextResult {
call_id,
result,
is_error,
signature,
} => Step::UrlContextResult {
call_id,
result,
is_error,
signature,
},
KnownStep::GoogleSearchCall {
id,
arguments,
search_type,
signature,
} => Step::GoogleSearchCall {
id,
queries: string_vec_from_arguments(arguments.as_ref(), "queries"),
search_type,
signature,
},
KnownStep::GoogleSearchResult {
call_id,
result,
is_error,
signature,
} => Step::GoogleSearchResult {
call_id,
result,
is_error,
signature,
},
KnownStep::McpServerToolCall {
id,
name,
server_name,
arguments,
} => Step::McpServerToolCall {
id,
name,
server_name,
arguments,
},
KnownStep::McpServerToolResult {
call_id,
name,
server_name,
result,
} => Step::McpServerToolResult {
call_id,
name,
server_name,
result: result.unwrap_or(FunctionResultPayload::Json(serde_json::Value::Null)),
},
KnownStep::FileSearchCall { id, signature } => {
Step::FileSearchCall { id, signature }
}
KnownStep::FileSearchResult {
call_id,
result,
signature,
} => Step::FileSearchResult {
call_id,
result,
signature,
},
KnownStep::GoogleMapsCall {
id,
arguments,
signature,
} => Step::GoogleMapsCall {
id,
queries: string_vec_from_arguments(arguments.as_ref(), "queries"),
signature,
},
KnownStep::GoogleMapsResult {
call_id,
result,
signature,
} => Step::GoogleMapsResult {
call_id,
result,
signature,
},
}),
Err(parse_error) => {
let step_type = value
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("<missing type>")
.to_string();
tracing::warn!(
"Encountered unknown Step type '{}'. Parse error: {}. \
This may indicate a new API feature or a malformed response. \
The step will be preserved in the Unknown variant.",
step_type,
parse_error
);
#[cfg(feature = "strict-unknown")]
{
Err(D::Error::custom(format!(
"Unknown Step type '{}'. \
Strict mode is enabled via the 'strict-unknown' feature flag. \
Either update the library or disable strict mode.",
step_type
)))
}
#[cfg(not(feature = "strict-unknown"))]
{
Ok(Step::Unknown {
step_type,
data: value,
})
}
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum StepDelta {
Text {
text: String,
},
Image {
data: Option<String>,
uri: Option<String>,
mime_type: Option<String>,
resolution: Option<Resolution>,
},
Audio {
data: Option<String>,
uri: Option<String>,
mime_type: Option<String>,
rate: Option<u32>,
sample_rate: Option<u32>,
channels: Option<u32>,
},
Video {
data: Option<String>,
uri: Option<String>,
mime_type: Option<String>,
resolution: Option<Resolution>,
},
Document {
data: Option<String>,
uri: Option<String>,
mime_type: Option<String>,
},
ThoughtSummary {
content: Option<Content>,
},
ThoughtSignature {
signature: Option<String>,
},
TextAnnotation {
annotations: Vec<Annotation>,
},
ArgumentsDelta {
arguments: String,
},
FunctionResult {
call_id: String,
name: Option<String>,
result: FunctionResultPayload,
is_error: Option<bool>,
},
CodeExecutionCall {
language: Option<CodeExecutionLanguage>,
code: Option<String>,
signature: Option<String>,
},
CodeExecutionResult {
result: String,
is_error: Option<bool>,
signature: Option<String>,
},
UrlContextCall {
urls: Vec<String>,
signature: Option<String>,
},
UrlContextResult {
result: Vec<UrlContextResultItem>,
is_error: Option<bool>,
signature: Option<String>,
},
GoogleSearchCall {
queries: Vec<String>,
signature: Option<String>,
},
GoogleSearchResult {
result: Vec<GoogleSearchResultItem>,
is_error: Option<bool>,
signature: Option<String>,
},
McpServerToolCall {
name: String,
server_name: String,
arguments: serde_json::Value,
},
McpServerToolResult {
name: Option<String>,
server_name: Option<String>,
result: FunctionResultPayload,
},
FileSearchCall {
signature: Option<String>,
},
FileSearchResult {
result: Vec<FileSearchResultItem>,
signature: Option<String>,
},
GoogleMapsCall {
queries: Vec<String>,
signature: Option<String>,
},
GoogleMapsResult {
result: Vec<GoogleMapsResultItem>,
signature: Option<String>,
},
Unknown {
delta_type: String,
data: serde_json::Value,
},
}
impl StepDelta {
#[must_use]
pub const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown { .. })
}
#[must_use]
pub fn unknown_delta_type(&self) -> Option<&str> {
match self {
Self::Unknown { delta_type, .. } => Some(delta_type),
_ => None,
}
}
#[must_use]
pub fn unknown_data(&self) -> Option<&serde_json::Value> {
match self {
Self::Unknown { data, .. } => Some(data),
_ => None,
}
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text { text } => Some(text),
_ => None,
}
}
#[must_use]
pub fn as_arguments_delta(&self) -> Option<&str> {
match self {
Self::ArgumentsDelta { arguments } => Some(arguments),
_ => None,
}
}
}
impl Serialize for StepDelta {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
macro_rules! opt_entry {
($map:expr, $key:literal, $val:expr) => {
if let Some(v) = $val {
$map.serialize_entry($key, v)?;
}
};
}
let mut map = serializer.serialize_map(None)?;
match self {
Self::Text { text } => {
map.serialize_entry("type", "text")?;
map.serialize_entry("text", text)?;
}
Self::Image {
data,
uri,
mime_type,
resolution,
} => {
map.serialize_entry("type", "image")?;
opt_entry!(map, "data", data);
opt_entry!(map, "uri", uri);
opt_entry!(map, "mime_type", mime_type);
opt_entry!(map, "resolution", resolution);
}
Self::Audio {
data,
uri,
mime_type,
rate,
sample_rate,
channels,
} => {
map.serialize_entry("type", "audio")?;
opt_entry!(map, "data", data);
opt_entry!(map, "uri", uri);
opt_entry!(map, "mime_type", mime_type);
opt_entry!(map, "rate", rate);
opt_entry!(map, "sample_rate", sample_rate);
opt_entry!(map, "channels", channels);
}
Self::Video {
data,
uri,
mime_type,
resolution,
} => {
map.serialize_entry("type", "video")?;
opt_entry!(map, "data", data);
opt_entry!(map, "uri", uri);
opt_entry!(map, "mime_type", mime_type);
opt_entry!(map, "resolution", resolution);
}
Self::Document {
data,
uri,
mime_type,
} => {
map.serialize_entry("type", "document")?;
opt_entry!(map, "data", data);
opt_entry!(map, "uri", uri);
opt_entry!(map, "mime_type", mime_type);
}
Self::ThoughtSummary { content } => {
map.serialize_entry("type", "thought_summary")?;
opt_entry!(map, "content", content);
}
Self::ThoughtSignature { signature } => {
map.serialize_entry("type", "thought_signature")?;
opt_entry!(map, "signature", signature);
}
Self::TextAnnotation { annotations } => {
map.serialize_entry("type", "text_annotation_delta")?;
map.serialize_entry("annotations", annotations)?;
}
Self::ArgumentsDelta { arguments } => {
map.serialize_entry("type", "arguments_delta")?;
map.serialize_entry("arguments", arguments)?;
}
Self::FunctionResult {
call_id,
name,
result,
is_error,
} => {
map.serialize_entry("type", "function_result")?;
map.serialize_entry("call_id", call_id)?;
opt_entry!(map, "name", name);
map.serialize_entry("result", result)?;
opt_entry!(map, "is_error", is_error);
}
Self::CodeExecutionCall {
language,
code,
signature,
} => {
map.serialize_entry("type", "code_execution_call")?;
map.serialize_entry(
"arguments",
&serde_json::json!({ "language": language, "code": code }),
)?;
opt_entry!(map, "signature", signature);
}
Self::CodeExecutionResult {
result,
is_error,
signature,
} => {
map.serialize_entry("type", "code_execution_result")?;
map.serialize_entry("result", result)?;
opt_entry!(map, "is_error", is_error);
opt_entry!(map, "signature", signature);
}
Self::UrlContextCall { urls, signature } => {
map.serialize_entry("type", "url_context_call")?;
map.serialize_entry("arguments", &serde_json::json!({ "urls": urls }))?;
opt_entry!(map, "signature", signature);
}
Self::UrlContextResult {
result,
is_error,
signature,
} => {
map.serialize_entry("type", "url_context_result")?;
map.serialize_entry("result", result)?;
opt_entry!(map, "is_error", is_error);
opt_entry!(map, "signature", signature);
}
Self::GoogleSearchCall { queries, signature } => {
map.serialize_entry("type", "google_search_call")?;
map.serialize_entry("arguments", &serde_json::json!({ "queries": queries }))?;
opt_entry!(map, "signature", signature);
}
Self::GoogleSearchResult {
result,
is_error,
signature,
} => {
map.serialize_entry("type", "google_search_result")?;
map.serialize_entry("result", result)?;
opt_entry!(map, "is_error", is_error);
opt_entry!(map, "signature", signature);
}
Self::McpServerToolCall {
name,
server_name,
arguments,
} => {
map.serialize_entry("type", "mcp_server_tool_call")?;
map.serialize_entry("name", name)?;
map.serialize_entry("server_name", server_name)?;
map.serialize_entry("arguments", arguments)?;
}
Self::McpServerToolResult {
name,
server_name,
result,
} => {
map.serialize_entry("type", "mcp_server_tool_result")?;
opt_entry!(map, "name", name);
opt_entry!(map, "server_name", server_name);
map.serialize_entry("result", result)?;
}
Self::FileSearchCall { signature } => {
map.serialize_entry("type", "file_search_call")?;
opt_entry!(map, "signature", signature);
}
Self::FileSearchResult { result, signature } => {
map.serialize_entry("type", "file_search_result")?;
map.serialize_entry("result", result)?;
opt_entry!(map, "signature", signature);
}
Self::GoogleMapsCall { queries, signature } => {
map.serialize_entry("type", "google_maps_call")?;
map.serialize_entry("arguments", &serde_json::json!({ "queries": queries }))?;
opt_entry!(map, "signature", signature);
}
Self::GoogleMapsResult { result, signature } => {
map.serialize_entry("type", "google_maps_result")?;
map.serialize_entry("result", result)?;
opt_entry!(map, "signature", signature);
}
Self::Unknown { delta_type, data } => {
map.serialize_entry("type", delta_type)?;
match data {
serde_json::Value::Object(obj) => {
for (key, value) in obj {
if key != "type" {
map.serialize_entry(key, value)?;
}
}
}
other if !other.is_null() => {
map.serialize_entry("data", other)?;
}
_ => {}
}
}
}
map.end()
}
}
impl<'de> Deserialize<'de> for StepDelta {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum KnownDelta {
Text {
#[serde(default)]
text: String,
},
Image {
#[serde(default)]
data: Option<String>,
#[serde(default)]
uri: Option<String>,
#[serde(default)]
mime_type: Option<String>,
#[serde(default)]
resolution: Option<Resolution>,
},
Audio {
#[serde(default)]
data: Option<String>,
#[serde(default)]
uri: Option<String>,
#[serde(default)]
mime_type: Option<String>,
#[serde(default)]
rate: Option<u32>,
#[serde(default)]
sample_rate: Option<u32>,
#[serde(default)]
channels: Option<u32>,
},
Video {
#[serde(default)]
data: Option<String>,
#[serde(default)]
uri: Option<String>,
#[serde(default)]
mime_type: Option<String>,
#[serde(default)]
resolution: Option<Resolution>,
},
Document {
#[serde(default)]
data: Option<String>,
#[serde(default)]
uri: Option<String>,
#[serde(default)]
mime_type: Option<String>,
},
ThoughtSummary {
#[serde(default)]
content: Option<Content>,
},
ThoughtSignature {
#[serde(default)]
signature: Option<String>,
},
#[serde(rename = "text_annotation_delta")]
TextAnnotation {
#[serde(default)]
annotations: Vec<Annotation>,
},
ArgumentsDelta {
#[serde(default)]
arguments: String,
},
FunctionResult {
call_id: String,
#[serde(default)]
name: Option<String>,
#[serde(default)]
result: Option<FunctionResultPayload>,
#[serde(default)]
is_error: Option<bool>,
},
CodeExecutionCall {
#[serde(default)]
arguments: Option<serde_json::Value>,
#[serde(default)]
signature: Option<String>,
},
CodeExecutionResult {
#[serde(default)]
result: String,
#[serde(default)]
is_error: Option<bool>,
#[serde(default)]
signature: Option<String>,
},
UrlContextCall {
#[serde(default)]
arguments: Option<serde_json::Value>,
#[serde(default)]
signature: Option<String>,
},
UrlContextResult {
#[serde(default)]
result: Vec<UrlContextResultItem>,
#[serde(default)]
is_error: Option<bool>,
#[serde(default)]
signature: Option<String>,
},
GoogleSearchCall {
#[serde(default)]
arguments: Option<serde_json::Value>,
#[serde(default)]
signature: Option<String>,
},
GoogleSearchResult {
#[serde(default)]
result: Vec<GoogleSearchResultItem>,
#[serde(default)]
is_error: Option<bool>,
#[serde(default)]
signature: Option<String>,
},
McpServerToolCall {
name: String,
server_name: String,
#[serde(default)]
arguments: serde_json::Value,
},
McpServerToolResult {
#[serde(default)]
name: Option<String>,
#[serde(default)]
server_name: Option<String>,
#[serde(default)]
result: Option<FunctionResultPayload>,
},
FileSearchCall {
#[serde(default)]
signature: Option<String>,
},
FileSearchResult {
#[serde(default)]
result: Vec<FileSearchResultItem>,
#[serde(default)]
signature: Option<String>,
},
GoogleMapsCall {
#[serde(default)]
arguments: Option<serde_json::Value>,
#[serde(default)]
signature: Option<String>,
},
GoogleMapsResult {
#[serde(default)]
result: Vec<GoogleMapsResultItem>,
#[serde(default)]
signature: Option<String>,
},
}
match serde_json::from_value::<KnownDelta>(value.clone()) {
Ok(known) => Ok(match known {
KnownDelta::Text { text } => StepDelta::Text { text },
KnownDelta::Image {
data,
uri,
mime_type,
resolution,
} => StepDelta::Image {
data,
uri,
mime_type,
resolution,
},
KnownDelta::Audio {
data,
uri,
mime_type,
rate,
sample_rate,
channels,
} => StepDelta::Audio {
data,
uri,
mime_type,
rate,
sample_rate,
channels,
},
KnownDelta::Video {
data,
uri,
mime_type,
resolution,
} => StepDelta::Video {
data,
uri,
mime_type,
resolution,
},
KnownDelta::Document {
data,
uri,
mime_type,
} => StepDelta::Document {
data,
uri,
mime_type,
},
KnownDelta::ThoughtSummary { content } => StepDelta::ThoughtSummary { content },
KnownDelta::ThoughtSignature { signature } => {
StepDelta::ThoughtSignature { signature }
}
KnownDelta::TextAnnotation { annotations } => {
StepDelta::TextAnnotation { annotations }
}
KnownDelta::ArgumentsDelta { arguments } => StepDelta::ArgumentsDelta { arguments },
KnownDelta::FunctionResult {
call_id,
name,
result,
is_error,
} => StepDelta::FunctionResult {
call_id,
name,
result: result.unwrap_or(FunctionResultPayload::Json(serde_json::Value::Null)),
is_error,
},
KnownDelta::CodeExecutionCall {
arguments,
signature,
} => {
StepDelta::CodeExecutionCall {
language: arguments.as_ref().and_then(|a| a.get("language")).and_then(
|l| serde_json::from_value::<CodeExecutionLanguage>(l.clone()).ok(),
),
code: arguments
.as_ref()
.and_then(|a| a.get("code"))
.and_then(|c| c.as_str())
.map(String::from),
signature,
}
}
KnownDelta::CodeExecutionResult {
result,
is_error,
signature,
} => StepDelta::CodeExecutionResult {
result,
is_error,
signature,
},
KnownDelta::UrlContextCall {
arguments,
signature,
} => StepDelta::UrlContextCall {
urls: string_vec_from_arguments(arguments.as_ref(), "urls"),
signature,
},
KnownDelta::UrlContextResult {
result,
is_error,
signature,
} => StepDelta::UrlContextResult {
result,
is_error,
signature,
},
KnownDelta::GoogleSearchCall {
arguments,
signature,
} => StepDelta::GoogleSearchCall {
queries: string_vec_from_arguments(arguments.as_ref(), "queries"),
signature,
},
KnownDelta::GoogleSearchResult {
result,
is_error,
signature,
} => StepDelta::GoogleSearchResult {
result,
is_error,
signature,
},
KnownDelta::McpServerToolCall {
name,
server_name,
arguments,
} => StepDelta::McpServerToolCall {
name,
server_name,
arguments,
},
KnownDelta::McpServerToolResult {
name,
server_name,
result,
} => StepDelta::McpServerToolResult {
name,
server_name,
result: result.unwrap_or(FunctionResultPayload::Json(serde_json::Value::Null)),
},
KnownDelta::FileSearchCall { signature } => StepDelta::FileSearchCall { signature },
KnownDelta::FileSearchResult { result, signature } => {
StepDelta::FileSearchResult { result, signature }
}
KnownDelta::GoogleMapsCall {
arguments,
signature,
} => StepDelta::GoogleMapsCall {
queries: string_vec_from_arguments(arguments.as_ref(), "queries"),
signature,
},
KnownDelta::GoogleMapsResult { result, signature } => {
StepDelta::GoogleMapsResult { result, signature }
}
}),
Err(parse_error) => {
let delta_type = value
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("<missing type>")
.to_string();
tracing::warn!(
"Encountered unknown StepDelta type '{}'. Parse error: {}. \
The delta will be preserved in the Unknown variant.",
delta_type,
parse_error
);
Ok(StepDelta::Unknown {
delta_type,
data: value,
})
}
}
}
}
#[derive(Debug, Default)]
pub(crate) struct StepAccumulator {
steps: std::collections::BTreeMap<usize, AccumulatedStep>,
last_cumulative_usage: Option<crate::response::UsageMetadata>,
}
#[derive(Debug)]
struct AccumulatedStep {
step: Step,
args_buffer: String,
}
impl StepAccumulator {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub(crate) fn start(&mut self, index: usize, step: Step) {
self.steps.insert(
index,
AccumulatedStep {
step,
args_buffer: String::new(),
},
);
}
pub(crate) fn apply_delta(&mut self, index: usize, delta: &StepDelta) {
let entry = self.steps.entry(index).or_insert_with(|| AccumulatedStep {
step: Step::ModelOutput {
content: Vec::new(),
error: None,
},
args_buffer: String::new(),
});
match delta {
StepDelta::Text { text } => {
if let Step::UserInput { content } | Step::ModelOutput { content, .. } =
&mut entry.step
{
if let Some(Content::Text { text: Some(t), .. }) = content.last_mut() {
t.push_str(text);
} else {
content.push(Content::text(text.clone()));
}
}
}
StepDelta::TextAnnotation { annotations } => {
if let Step::UserInput { content } | Step::ModelOutput { content, .. } =
&mut entry.step
&& let Some(Content::Text {
annotations: annots,
..
}) = content.last_mut()
{
annots
.get_or_insert_with(Vec::new)
.extend(annotations.iter().cloned());
}
}
StepDelta::Image {
data,
uri,
mime_type,
resolution,
} => {
if let Step::UserInput { content } | Step::ModelOutput { content, .. } =
&mut entry.step
{
if let (
Some(Content::Image {
data: Some(existing),
..
}),
Some(new_data),
) = (content.last_mut(), data.as_ref())
{
existing.push_str(new_data);
} else {
content.push(Content::Image {
data: data.clone(),
uri: uri.clone(),
mime_type: mime_type.clone(),
resolution: resolution.clone(),
});
}
}
}
StepDelta::Audio {
data,
uri,
mime_type,
rate,
sample_rate,
channels,
} => {
if let Step::UserInput { content } | Step::ModelOutput { content, .. } =
&mut entry.step
{
if let (
Some(Content::Audio {
data: Some(existing),
..
}),
Some(new_data),
) = (content.last_mut(), data.as_ref())
{
existing.push_str(new_data);
} else {
content.push(Content::Audio {
data: data.clone(),
uri: uri.clone(),
mime_type: mime_type.clone(),
sample_rate: sample_rate.or(*rate),
channels: *channels,
});
}
}
}
StepDelta::Video {
data,
uri,
mime_type,
resolution,
} => {
if let Step::UserInput { content } | Step::ModelOutput { content, .. } =
&mut entry.step
{
if let (
Some(Content::Video {
data: Some(existing),
..
}),
Some(new_data),
) = (content.last_mut(), data.as_ref())
{
existing.push_str(new_data);
} else {
content.push(Content::Video {
data: data.clone(),
uri: uri.clone(),
mime_type: mime_type.clone(),
resolution: resolution.clone(),
});
}
}
}
StepDelta::Document {
data,
uri,
mime_type,
} => {
if let Step::UserInput { content } | Step::ModelOutput { content, .. } =
&mut entry.step
{
if let (
Some(Content::Document {
data: Some(existing),
..
}),
Some(new_data),
) = (content.last_mut(), data.as_ref())
{
existing.push_str(new_data);
} else {
content.push(Content::Document {
data: data.clone(),
uri: uri.clone(),
mime_type: mime_type.clone(),
});
}
}
}
StepDelta::ThoughtSummary { content } => {
if let Step::Thought { summary, .. } = &mut entry.step
&& let Some(c) = content
{
if let (
Some(Content::Text { text: Some(t), .. }),
Content::Text {
text: Some(new), ..
},
) = (summary.last_mut(), c)
{
t.push_str(new);
} else {
summary.push(c.clone());
}
}
}
StepDelta::ThoughtSignature { signature } => {
if let Step::Thought { signature: sig, .. } = &mut entry.step
&& let Some(fragment) = signature
{
sig.get_or_insert_with(String::new).push_str(fragment);
}
}
StepDelta::ArgumentsDelta { arguments } => {
entry.args_buffer.push_str(arguments);
}
StepDelta::FunctionResult {
call_id,
name,
result,
is_error,
} => {
let signature = match &entry.step {
Step::FunctionResult { signature, .. } => signature.clone(),
_ => None,
};
entry.step = Step::FunctionResult {
call_id: call_id.clone(),
name: name.clone(),
result: result.clone(),
is_error: *is_error,
signature,
};
}
StepDelta::CodeExecutionCall {
language,
code,
signature,
} => {
if let Step::CodeExecutionCall {
language: lang,
code: existing_code,
signature: sig,
..
} = &mut entry.step
{
if let Some(l) = language {
*lang = l.clone();
}
if let Some(c) = code {
existing_code.push_str(c);
}
if signature.is_some() {
*sig = signature.clone();
}
}
}
StepDelta::CodeExecutionResult {
result,
is_error,
signature,
} => {
if let Step::CodeExecutionResult {
result: existing,
is_error: err,
signature: sig,
..
} = &mut entry.step
{
existing.push_str(result);
if let Some(e) = is_error {
*err = *e;
}
if signature.is_some() {
*sig = signature.clone();
}
}
}
StepDelta::UrlContextCall { urls, signature } => {
if let Step::UrlContextCall {
urls: existing,
signature: sig,
..
} = &mut entry.step
{
existing.extend(urls.iter().cloned());
if signature.is_some() {
*sig = signature.clone();
}
}
}
StepDelta::UrlContextResult {
result,
is_error,
signature,
} => {
if let Step::UrlContextResult {
result: existing,
is_error: err,
signature: sig,
..
} = &mut entry.step
{
existing.extend(result.iter().cloned());
if is_error.is_some() {
*err = *is_error;
}
if signature.is_some() {
*sig = signature.clone();
}
}
}
StepDelta::GoogleSearchCall { queries, signature } => {
if let Step::GoogleSearchCall {
queries: existing,
signature: sig,
..
} = &mut entry.step
{
existing.extend(queries.iter().cloned());
if signature.is_some() {
*sig = signature.clone();
}
}
}
StepDelta::GoogleSearchResult {
result,
is_error,
signature,
} => {
if let Step::GoogleSearchResult {
result: existing,
is_error: err,
signature: sig,
..
} = &mut entry.step
{
existing.extend(result.iter().cloned());
if is_error.is_some() {
*err = *is_error;
}
if signature.is_some() {
*sig = signature.clone();
}
}
}
StepDelta::McpServerToolCall {
name,
server_name,
arguments,
} => {
if let Step::McpServerToolCall {
name: n,
server_name: sn,
arguments: args,
..
} = &mut entry.step
{
*n = name.clone();
*sn = server_name.clone();
*args = arguments.clone();
}
}
StepDelta::McpServerToolResult {
name,
server_name,
result,
} => {
if let Step::McpServerToolResult {
name: n,
server_name: sn,
result: r,
..
} = &mut entry.step
{
if name.is_some() {
*n = name.clone();
}
if server_name.is_some() {
*sn = server_name.clone();
}
*r = result.clone();
}
}
StepDelta::FileSearchCall { signature } => {
if let Step::FileSearchCall { signature: sig, .. } = &mut entry.step
&& signature.is_some()
{
*sig = signature.clone();
}
}
StepDelta::FileSearchResult { result, signature } => {
if let Step::FileSearchResult {
result: existing,
signature: sig,
..
} = &mut entry.step
{
existing.extend(result.iter().cloned());
if signature.is_some() {
*sig = signature.clone();
}
}
}
StepDelta::GoogleMapsCall { queries, signature } => {
if let Step::GoogleMapsCall {
queries: existing,
signature: sig,
..
} = &mut entry.step
{
existing.extend(queries.iter().cloned());
if signature.is_some() {
*sig = signature.clone();
}
}
}
StepDelta::GoogleMapsResult { result, signature } => {
if let Step::GoogleMapsResult {
result: existing,
signature: sig,
..
} = &mut entry.step
{
existing.extend(result.iter().cloned());
if signature.is_some() {
*sig = signature.clone();
}
}
}
StepDelta::Unknown { delta_type, .. } => {
tracing::debug!(
"Skipping unknown StepDelta type '{}' during accumulation",
delta_type
);
}
}
}
pub(crate) fn stop(&mut self, index: usize) {
if let Some(entry) = self.steps.get_mut(&index) {
Self::finalize_entry(entry);
}
}
pub(crate) fn record_cumulative_usage(&mut self, usage: crate::response::UsageMetadata) {
self.last_cumulative_usage = Some(usage);
}
pub(crate) fn take_cumulative_usage(&mut self) -> Option<crate::response::UsageMetadata> {
self.last_cumulative_usage.take()
}
fn finalize_entry(entry: &mut AccumulatedStep) {
if entry.args_buffer.is_empty() {
return;
}
if let Step::FunctionCall { arguments, .. } = &mut entry.step {
match serde_json::from_str::<serde_json::Value>(&entry.args_buffer) {
Ok(parsed) => *arguments = parsed,
Err(e) => {
tracing::warn!(
"Failed to parse accumulated arguments_delta buffer as JSON: {}. \
Preserving raw string.",
e
);
*arguments = serde_json::Value::String(std::mem::take(&mut entry.args_buffer));
}
}
}
entry.args_buffer.clear();
}
pub(crate) fn finish(mut self) -> Vec<Step> {
for entry in self.steps.values_mut() {
Self::finalize_entry(entry);
}
self.steps.into_values().map(|e| e.step).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_step_user_input_roundtrip() {
let json_str = r#"{"type":"user_input","content":[{"type":"text","text":"Hello"}]}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
assert!(matches!(&step, Step::UserInput { content } if content.len() == 1));
assert_eq!(step.as_text(), Some("Hello"));
let out = serde_json::to_value(&step).unwrap();
assert_eq!(out["type"], "user_input");
assert_eq!(out["content"][0]["text"], "Hello");
}
#[test]
fn test_step_model_output_with_error() {
let json_str = r#"{
"type": "model_output",
"content": [{"type": "text", "text": "Partial"}],
"error": {"code": 8, "message": "quota exhausted"}
}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
match &step {
Step::ModelOutput { content, error } => {
assert_eq!(content.len(), 1);
let error = error.as_ref().unwrap();
assert_eq!(error.code, Some(8));
assert_eq!(error.message.as_deref(), Some("quota exhausted"));
}
other => panic!("Expected ModelOutput, got {other:?}"),
}
}
#[test]
fn test_step_thought_roundtrip() {
let json_str = r#"{
"type": "thought",
"signature": "sig-abc",
"summary": [{"type": "text", "text": "Thinking about it"}]
}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
match &step {
Step::Thought { signature, summary } => {
assert_eq!(signature.as_deref(), Some("sig-abc"));
assert_eq!(summary.len(), 1);
}
other => panic!("Expected Thought, got {other:?}"),
}
assert_eq!(step.signature(), Some("sig-abc"));
let out = serde_json::to_value(&step).unwrap();
assert_eq!(out["type"], "thought");
assert_eq!(out["signature"], "sig-abc");
}
#[test]
fn test_step_function_call_roundtrip() {
let json_str = r#"{
"type": "function_call",
"id": "call_1",
"name": "get_weather",
"arguments": {"city": "Tokyo"}
}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
match &step {
Step::FunctionCall {
id,
name,
arguments,
signature,
} => {
assert_eq!(id, "call_1");
assert_eq!(name, "get_weather");
assert_eq!(arguments["city"], "Tokyo");
assert_eq!(*signature, None);
}
other => panic!("Expected FunctionCall, got {other:?}"),
}
let out = serde_json::to_value(&step).unwrap();
assert_eq!(out["type"], "function_call");
assert_eq!(out["id"], "call_1");
assert_eq!(out["arguments"]["city"], "Tokyo");
assert!(out.get("signature").is_none());
}
#[test]
fn test_step_function_call_roundtrip_preserves_signature() {
let json_str = r#"{
"type": "function_call",
"id": "call_2",
"name": "get_weather",
"arguments": {"city": "Paris"},
"signature": "sig-fc-123"
}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
match &step {
Step::FunctionCall { signature, .. } => {
assert_eq!(signature.as_deref(), Some("sig-fc-123"));
}
other => panic!("Expected FunctionCall, got {other:?}"),
}
assert_eq!(step.signature(), Some("sig-fc-123"));
let out = serde_json::to_value(&step).unwrap();
assert_eq!(out["type"], "function_call");
assert_eq!(out["signature"], "sig-fc-123");
}
#[test]
fn test_step_function_result_roundtrip_preserves_signature() {
let json_str = r#"{
"type": "function_result",
"call_id": "call_2",
"name": "get_weather",
"result": "sunny",
"signature": "sig-fr-456"
}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
match &step {
Step::FunctionResult { signature, .. } => {
assert_eq!(signature.as_deref(), Some("sig-fr-456"));
}
other => panic!("Expected FunctionResult, got {other:?}"),
}
assert_eq!(step.signature(), Some("sig-fr-456"));
let out = serde_json::to_value(&step).unwrap();
assert_eq!(out["type"], "function_result");
assert_eq!(out["signature"], "sig-fr-456");
let constructed = Step::function_result("get_weather", "call_2", "sunny");
let out = serde_json::to_value(&constructed).unwrap();
assert!(out.get("signature").is_none());
}
#[test]
fn test_step_function_result_payload_union() {
let s: Step = serde_json::from_str(
r#"{"type":"function_result","call_id":"c1","result":"22 degrees"}"#,
)
.unwrap();
match &s {
Step::FunctionResult { result, .. } => {
assert_eq!(result.as_text(), Some("22 degrees"));
}
other => panic!("Expected FunctionResult, got {other:?}"),
}
let s: Step = serde_json::from_str(
r#"{"type":"function_result","call_id":"c2","result":{"temp":22},"is_error":false}"#,
)
.unwrap();
match &s {
Step::FunctionResult {
result, is_error, ..
} => {
assert_eq!(result.as_json().unwrap()["temp"], 22);
assert_eq!(*is_error, Some(false));
}
other => panic!("Expected FunctionResult, got {other:?}"),
}
let s: Step = serde_json::from_str(
r#"{"type":"function_result","call_id":"c3","result":[{"type":"text","text":"hi"}]}"#,
)
.unwrap();
match &s {
Step::FunctionResult { result, .. } => {
let contents = result.as_contents().unwrap();
assert_eq!(contents.len(), 1);
assert_eq!(contents[0].as_text(), Some("hi"));
}
other => panic!("Expected FunctionResult, got {other:?}"),
}
}
#[test]
fn test_step_code_execution_nested_arguments_roundtrip() {
let json_str = r#"{
"type": "code_execution_call",
"id": "exec_1",
"arguments": {"language": "python", "code": "print(42)"},
"signature": "sig-1"
}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
match &step {
Step::CodeExecutionCall {
id,
language,
code,
signature,
} => {
assert_eq!(id, "exec_1");
assert_eq!(*language, CodeExecutionLanguage::Python);
assert_eq!(code, "print(42)");
assert_eq!(signature.as_deref(), Some("sig-1"));
}
other => panic!("Expected CodeExecutionCall, got {other:?}"),
}
let out = serde_json::to_value(&step).unwrap();
assert_eq!(out["arguments"]["language"], "python");
assert_eq!(out["arguments"]["code"], "print(42)");
assert_eq!(out["signature"], "sig-1");
}
#[test]
fn test_step_google_search_call_with_search_type() {
let json_str = r#"{
"type": "google_search_call",
"id": "s1",
"arguments": {"queries": ["rust serde"]},
"search_type": "web_search",
"signature": "sig"
}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
match &step {
Step::GoogleSearchCall {
queries,
search_type,
..
} => {
assert_eq!(queries, &["rust serde".to_string()]);
assert!(matches!(
search_type,
Some(crate::tools::SearchType::WebSearch)
));
}
other => panic!("Expected GoogleSearchCall, got {other:?}"),
}
}
#[test]
fn test_step_mcp_server_tool_call_roundtrip() {
let json_str = r#"{
"type": "mcp_server_tool_call",
"id": "m1",
"name": "read_file",
"server_name": "fs",
"arguments": {"path": "/tmp/x"}
}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
match &step {
Step::McpServerToolCall {
name, server_name, ..
} => {
assert_eq!(name, "read_file");
assert_eq!(server_name, "fs");
}
other => panic!("Expected McpServerToolCall, got {other:?}"),
}
let out = serde_json::to_value(&step).unwrap();
assert_eq!(out["server_name"], "fs");
}
#[test]
#[cfg(not(feature = "strict-unknown"))]
fn test_step_unknown_preserves_data_and_roundtrips() {
let json_str = r#"{"type":"quantum_step","novel_field":42}"#;
let step: Step = serde_json::from_str(json_str).unwrap();
assert!(step.is_unknown());
assert_eq!(step.unknown_step_type(), Some("quantum_step"));
assert_eq!(step.unknown_data().unwrap()["novel_field"], 42);
let out = serde_json::to_value(&step).unwrap();
assert_eq!(out["type"], "quantum_step");
assert_eq!(out["novel_field"], 42);
}
#[test]
fn test_step_type_accessor() {
assert_eq!(Step::user_text("x").step_type(), "user_input");
assert_eq!(
Step::function_call("id", "f", json!({})).step_type(),
"function_call"
);
assert_eq!(
Step::Unknown {
step_type: "future".into(),
data: serde_json::Value::Null
}
.step_type(),
"future"
);
}
#[test]
fn test_step_delta_text() {
let delta: StepDelta = serde_json::from_str(r#"{"type":"text","text":"Hel"}"#).unwrap();
assert_eq!(delta.as_text(), Some("Hel"));
let out = serde_json::to_value(&delta).unwrap();
assert_eq!(out, json!({"type": "text", "text": "Hel"}));
}
#[test]
fn test_step_delta_arguments_delta() {
let delta: StepDelta =
serde_json::from_str(r#"{"type":"arguments_delta","arguments":"{\"city\": \"To"}"#)
.unwrap();
assert_eq!(delta.as_arguments_delta(), Some("{\"city\": \"To"));
}
#[test]
fn test_step_delta_audio_with_rate_and_channels() {
let delta: StepDelta = serde_json::from_str(
r#"{"type":"audio","data":"QUJD","mime_type":"audio/l16","rate":24000,"sample_rate":24000,"channels":1}"#,
)
.unwrap();
match &delta {
StepDelta::Audio {
sample_rate,
channels,
rate,
..
} => {
assert_eq!(*sample_rate, Some(24000));
assert_eq!(*rate, Some(24000));
assert_eq!(*channels, Some(1));
}
other => panic!("Expected Audio, got {other:?}"),
}
}
#[test]
fn test_step_delta_thought_summary_and_signature() {
let summary: StepDelta = serde_json::from_str(
r#"{"type":"thought_summary","content":{"type":"text","text":"Analyzing"}}"#,
)
.unwrap();
assert!(matches!(summary, StepDelta::ThoughtSummary { .. }));
let sig: StepDelta =
serde_json::from_str(r#"{"type":"thought_signature","signature":"abc123"}"#).unwrap();
assert!(matches!(
sig,
StepDelta::ThoughtSignature { signature: Some(s) } if s == "abc123"
));
}
#[test]
fn test_step_delta_text_annotation() {
let delta: StepDelta = serde_json::from_str(
r#"{"type":"text_annotation_delta","annotations":[
{"type":"url_citation","url":"https://example.com","title":"Example","start_index":0,"end_index":5}
]}"#,
)
.unwrap();
match &delta {
StepDelta::TextAnnotation { annotations } => {
assert_eq!(annotations.len(), 1);
assert!(matches!(annotations[0], Annotation::UrlCitation { .. }));
}
other => panic!("Expected TextAnnotation, got {other:?}"),
}
}
#[test]
fn test_step_delta_unknown_roundtrip() {
let delta: StepDelta = serde_json::from_str(r#"{"type":"hologram","frames":3}"#).unwrap();
assert!(delta.is_unknown());
assert_eq!(delta.unknown_delta_type(), Some("hologram"));
assert_eq!(delta.unknown_data().unwrap()["frames"], 3);
let out = serde_json::to_value(&delta).unwrap();
assert_eq!(out["type"], "hologram");
assert_eq!(out["frames"], 3);
}
#[test]
fn test_function_result_payload_from_value() {
assert!(matches!(
FunctionResultPayload::from_value(json!("text")),
FunctionResultPayload::Text(_)
));
assert!(matches!(
FunctionResultPayload::from_value(json!({"a": 1})),
FunctionResultPayload::Json(_)
));
assert!(matches!(
FunctionResultPayload::from_value(json!([{"type": "text", "text": "hi"}])),
FunctionResultPayload::Contents(_)
));
assert!(matches!(
FunctionResultPayload::from_value(json!([1, 2, 3])),
FunctionResultPayload::Json(_)
));
}
#[test]
fn test_function_result_payload_roundtrip() {
for payload in [
FunctionResultPayload::Text("hello".into()),
FunctionResultPayload::Json(json!({"k": [1, 2]})),
FunctionResultPayload::Contents(vec![Content::text("block")]),
] {
let serialized = serde_json::to_string(&payload).unwrap();
let back: FunctionResultPayload = serde_json::from_str(&serialized).unwrap();
assert_eq!(payload, back);
}
}
#[test]
fn test_accumulator_text_stream() {
let mut acc = StepAccumulator::new();
acc.start(0, Step::model_output(vec![]));
acc.apply_delta(
0,
&StepDelta::Text {
text: "Hello ".into(),
},
);
acc.apply_delta(
0,
&StepDelta::Text {
text: "world".into(),
},
);
acc.stop(0);
let steps = acc.finish();
assert_eq!(steps.len(), 1);
assert_eq!(steps[0].as_text(), Some("Hello world"));
}
#[test]
fn test_accumulator_image_deltas_accumulate_into_one_block() {
let mut acc = StepAccumulator::new();
acc.start(0, Step::model_output(vec![]));
acc.apply_delta(
0,
&StepDelta::Image {
data: Some("AAAA".into()),
uri: None,
mime_type: Some("image/png".into()),
resolution: None,
},
);
acc.apply_delta(
0,
&StepDelta::Image {
data: Some("BBBB".into()),
uri: None,
mime_type: Some("image/png".into()),
resolution: None,
},
);
acc.stop(0);
let steps = acc.finish();
match &steps[0] {
Step::ModelOutput { content, .. } => {
assert_eq!(content.len(), 1);
match &content[0] {
Content::Image { data, .. } => assert_eq!(data.as_deref(), Some("AAAABBBB")),
other => panic!("Expected Image, got {other:?}"),
}
}
other => panic!("Expected ModelOutput, got {other:?}"),
}
}
#[test]
fn test_accumulator_video_and_document_deltas_accumulate() {
let mut acc = StepAccumulator::new();
acc.start(0, Step::model_output(vec![]));
acc.apply_delta(
0,
&StepDelta::Video {
data: Some("VV".into()),
uri: None,
mime_type: Some("video/mp4".into()),
resolution: None,
},
);
acc.apply_delta(
0,
&StepDelta::Video {
data: Some("WW".into()),
uri: None,
mime_type: Some("video/mp4".into()),
resolution: None,
},
);
acc.apply_delta(
0,
&StepDelta::Document {
data: Some("DD".into()),
uri: None,
mime_type: Some("application/pdf".into()),
},
);
acc.apply_delta(
0,
&StepDelta::Document {
data: Some("EE".into()),
uri: None,
mime_type: Some("application/pdf".into()),
},
);
acc.stop(0);
let steps = acc.finish();
match &steps[0] {
Step::ModelOutput { content, .. } => {
assert_eq!(content.len(), 2);
match &content[0] {
Content::Video { data, .. } => assert_eq!(data.as_deref(), Some("VVWW")),
other => panic!("Expected Video, got {other:?}"),
}
match &content[1] {
Content::Document { data, .. } => assert_eq!(data.as_deref(), Some("DDEE")),
other => panic!("Expected Document, got {other:?}"),
}
}
other => panic!("Expected ModelOutput, got {other:?}"),
}
}
#[test]
fn test_accumulator_image_uri_delta_pushes_new_block() {
let mut acc = StepAccumulator::new();
acc.start(0, Step::model_output(vec![]));
acc.apply_delta(
0,
&StepDelta::Image {
data: Some("AAAA".into()),
uri: None,
mime_type: Some("image/png".into()),
resolution: None,
},
);
acc.apply_delta(
0,
&StepDelta::Image {
data: None,
uri: Some("https://example.com/img.png".into()),
mime_type: Some("image/png".into()),
resolution: None,
},
);
acc.stop(0);
let steps = acc.finish();
match &steps[0] {
Step::ModelOutput { content, .. } => assert_eq!(content.len(), 2),
other => panic!("Expected ModelOutput, got {other:?}"),
}
}
#[test]
fn test_accumulator_function_call_arguments_delta() {
let mut acc = StepAccumulator::new();
acc.start(
0,
Step::FunctionCall {
id: "c1".into(),
name: "get_weather".into(),
arguments: serde_json::Value::Null,
signature: None,
},
);
acc.apply_delta(
0,
&StepDelta::ArgumentsDelta {
arguments: "{\"city\": ".into(),
},
);
acc.apply_delta(
0,
&StepDelta::ArgumentsDelta {
arguments: "\"Tokyo\"}".into(),
},
);
acc.stop(0);
let steps = acc.finish();
match &steps[0] {
Step::FunctionCall { arguments, .. } => assert_eq!(arguments["city"], "Tokyo"),
other => panic!("Expected FunctionCall, got {other:?}"),
}
}
#[test]
fn test_accumulator_thought_summary_and_signature() {
let mut acc = StepAccumulator::new();
acc.start(
0,
Step::Thought {
signature: None,
summary: vec![],
},
);
acc.apply_delta(
0,
&StepDelta::ThoughtSummary {
content: Some(Content::text("Consider ")),
},
);
acc.apply_delta(
0,
&StepDelta::ThoughtSummary {
content: Some(Content::text("the problem")),
},
);
acc.apply_delta(
0,
&StepDelta::ThoughtSignature {
signature: Some("sig-xyz".into()),
},
);
let steps = acc.finish();
match &steps[0] {
Step::Thought { signature, summary } => {
assert_eq!(signature.as_deref(), Some("sig-xyz"));
assert_eq!(summary[0].as_text(), Some("Consider the problem"));
}
other => panic!("Expected Thought, got {other:?}"),
}
}
#[test]
fn test_accumulator_delta_without_start_creates_model_output() {
let mut acc = StepAccumulator::new();
acc.apply_delta(
0,
&StepDelta::Text {
text: "orphan".into(),
},
);
let steps = acc.finish();
assert_eq!(steps[0].as_text(), Some("orphan"));
}
#[test]
fn test_accumulator_orders_steps_by_index() {
let mut acc = StepAccumulator::new();
acc.start(2, Step::model_text("second"));
acc.start(1, Step::thought("sig"));
let steps = acc.finish();
assert!(matches!(steps[0], Step::Thought { .. }));
assert!(matches!(steps[1], Step::ModelOutput { .. }));
}
}