use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ToolInvocationMode {
#[default]
OneShot,
Streaming,
LongRunning,
}
impl ToolInvocationMode {
pub fn is_detached(self) -> bool {
!matches!(self, ToolInvocationMode::OneShot)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ToolHandle {
pub id: String,
}
impl ToolHandle {
pub fn new(id: impl Into<String>) -> Self {
Self { id: id.into() }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ToolStreamChunk {
Text { text: String },
Data { data: Value },
Progress {
fraction: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
Done {
#[serde(default, skip_serializing_if = "Option::is_none")]
result: Option<Value>,
},
Error { message: String },
}
impl ToolStreamChunk {
pub fn is_terminal(&self) -> bool {
matches!(
self,
ToolStreamChunk::Done { .. } | ToolStreamChunk::Error { .. }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ToolControl {
Poll,
Cancel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ToolStatus {
Running,
Succeeded,
Failed,
Cancelled,
}
impl ToolStatus {
pub fn is_terminal(self) -> bool {
!matches!(self, ToolStatus::Running)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ToolStreamEvent {
pub handle: ToolHandle,
pub chunk: ToolStreamChunk,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mode_defaults_to_one_shot_and_roundtrips() {
assert_eq!(ToolInvocationMode::default(), ToolInvocationMode::OneShot);
assert!(!ToolInvocationMode::OneShot.is_detached());
assert!(ToolInvocationMode::Streaming.is_detached());
assert!(ToolInvocationMode::LongRunning.is_detached());
let j = serde_json::to_string(&ToolInvocationMode::LongRunning).unwrap();
assert_eq!(j, "\"long_running\"");
let back: ToolInvocationMode = serde_json::from_str(&j).unwrap();
assert_eq!(back, ToolInvocationMode::LongRunning);
}
#[test]
fn chunk_tagged_serialization() {
let c = ToolStreamChunk::Text {
text: "hello".into(),
};
let j = serde_json::to_value(&c).unwrap();
assert_eq!(j["kind"], "text");
assert_eq!(j["text"], "hello");
let back: ToolStreamChunk = serde_json::from_value(j).unwrap();
assert_eq!(back, c);
}
#[test]
fn terminal_chunks_and_status() {
assert!(ToolStreamChunk::Done { result: None }.is_terminal());
assert!(ToolStreamChunk::Error {
message: "x".into()
}
.is_terminal());
assert!(!ToolStreamChunk::Text { text: "x".into() }.is_terminal());
assert!(ToolStatus::Succeeded.is_terminal());
assert!(ToolStatus::Cancelled.is_terminal());
assert!(!ToolStatus::Running.is_terminal());
}
#[test]
fn stream_event_roundtrips() {
let ev = ToolStreamEvent {
handle: ToolHandle::new("h1"),
chunk: ToolStreamChunk::Progress {
fraction: 0.5,
message: Some("halfway".into()),
},
};
let j = serde_json::to_string(&ev).unwrap();
let back: ToolStreamEvent = serde_json::from_str(&j).unwrap();
assert_eq!(back, ev);
}
#[test]
fn control_wire_forms() {
assert_eq!(
serde_json::to_string(&ToolControl::Cancel).unwrap(),
"\"cancel\""
);
assert_eq!(
serde_json::to_string(&ToolControl::Poll).unwrap(),
"\"poll\""
);
}
}