use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub args: serde_json::Value,
#[serde(default)]
pub provider_data: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolResult {
pub id: String,
pub content: String,
pub is_error: bool,
#[serde(default)]
pub code: Option<i32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub images: Vec<crate::Image>,
}
impl ToolResult {
pub fn success(id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
id: id.into(),
content: content.into(),
is_error: false,
code: None,
images: Vec::new(),
}
}
pub fn failure(id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
id: id.into(),
content: content.into(),
is_error: true,
code: None,
images: Vec::new(),
}
}
pub fn with_code(mut self, code: i32) -> Self {
self.code = Some(code);
self
}
pub fn with_images(mut self, images: Vec<crate::Image>) -> Self {
self.images = images;
self
}
}
#[derive(Debug, Clone)]
pub struct ToolContext {
pub cwd: PathBuf,
pub run_id: String,
pub cancelled: Arc<AtomicBool>,
}
impl Default for ToolContext {
fn default() -> Self {
Self {
cwd: PathBuf::from("."),
run_id: String::new(),
cancelled: Arc::new(AtomicBool::new(false)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ReplaySafety {
#[default]
Unsafe,
Safe,
}
#[async_trait]
pub trait ToolResolver: Send + Sync {
fn specs(&self) -> Vec<crate::request::ToolSpec>;
fn replay_safety(&self, _name: &str) -> ReplaySafety {
ReplaySafety::Unsafe
}
async fn resolve(&self, call: ToolCall, ctx: &ToolContext) -> ToolResult;
async fn resolve_streaming(
&self,
call: ToolCall,
ctx: &ToolContext,
progress: &mut dyn ToolProgress,
) -> ToolResult {
let _ = progress;
self.resolve(call, ctx).await
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolStatus {
pub phase: String,
pub context: Option<crate::output::Usage>,
pub spent: Option<crate::output::Usage>,
pub model: Option<String>,
}
pub trait ToolProgress: Send {
fn output(&mut self, chunk: &str);
fn status(&mut self, status: ToolStatus) {
let _ = status;
}
}
pub struct OutputOnly<F: FnMut(&str) + Send>(
pub F,
);
impl<F: FnMut(&str) + Send> std::fmt::Debug for OutputOnly<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("OutputOnly").field(&"<closure>").finish()
}
}
impl<F: FnMut(&str) + Send> ToolProgress for OutputOnly<F> {
fn output(&mut self, chunk: &str) {
(self.0)(chunk);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_result_without_a_code_field_loads_from_an_old_history() {
let raw = r#"{"id":"c1","content":"ok","is_error":false}"#;
let result: ToolResult = serde_json::from_str(raw).unwrap();
assert_eq!(result.code, None);
assert_eq!(result.images, Vec::new());
assert_eq!(result.with_code(3).code, Some(3));
}
#[test]
fn images_round_trip_and_absent_images_stay_out_of_the_json() {
let result = ToolResult::success("c1", "here").with_images(vec![crate::Image::new(
crate::ImageMediaType::Png,
"aGVsbG8=",
)]);
let text = serde_json::to_string(&result).unwrap();
assert!(text.contains("aGVsbG8="), "{text}");
let back: ToolResult = serde_json::from_str(&text).unwrap();
assert_eq!(back, result);
let bare = serde_json::to_value(ToolResult::success("c1", "here")).unwrap();
assert!(bare.get("images").is_none(), "{bare}");
}
}