sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
//! Neutral tool events. Connector adapters translate to wire types.
|
||||
|
||||
use crate::tool::ToolKind;
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Opaque permission-request id, allocated by the dispatcher.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct PermissionRequestId(Arc<str>);
|
||||
|
||||
impl PermissionRequestId {
|
||||
/// Construct a new permission-request id from any string-like value.
|
||||
pub fn new(value: impl Into<Arc<str>>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
/// Borrow the inner id as a string slice.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a tool is operating (file path + optional line).
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ToolLocation {
|
||||
pub path: String,
|
||||
pub line: Option<u32>,
|
||||
}
|
||||
|
||||
/// Result content shape. Mirrors what most providers accept as a tool result.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ToolResultContent {
|
||||
Text { text: Arc<str> },
|
||||
Json { value: serde_json::Value },
|
||||
Image { mime: Arc<str>, #[serde(with = "serde_bytes_arc")] data: Bytes },
|
||||
Parts { parts: Vec<ToolResultContent> },
|
||||
}
|
||||
|
||||
impl ToolResultContent {
|
||||
pub fn text(s: impl Into<Arc<str>>) -> Self { Self::Text { text: s.into() } }
|
||||
}
|
||||
|
||||
/// Events emitted by a running tool. Transport-agnostic.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ToolEvent {
|
||||
Started { title: Arc<str>, kind: ToolKind, location: Option<ToolLocation> },
|
||||
TitleUpdate { title: Arc<str>, location: Option<ToolLocation> },
|
||||
PartialOutput { content: ToolResultContent },
|
||||
Status { message: Arc<str> },
|
||||
PermissionRequested { request_id: PermissionRequestId, summary: Arc<str> },
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Sink a tool emits events into. Cheap to clone.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ToolEventSink {
|
||||
tx: mpsc::UnboundedSender<ToolEvent>,
|
||||
}
|
||||
|
||||
impl ToolEventSink {
|
||||
pub fn new() -> (Self, mpsc::UnboundedReceiver<ToolEvent>) {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
(Self { tx }, rx)
|
||||
}
|
||||
|
||||
/// Best-effort emit. Drops the event if the receiver is gone.
|
||||
pub fn emit(&self, event: ToolEvent) {
|
||||
let _ = self.tx.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
mod serde_bytes_arc {
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub fn serialize<S: Serializer>(b: &Bytes, s: S) -> Result<S::Ok, S::Error> {
|
||||
serde_bytes::Bytes::new(b.as_ref()).serialize(s)
|
||||
}
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Bytes, D::Error> {
|
||||
let v: Vec<u8> = serde_bytes::ByteBuf::deserialize(d)?.into_vec();
|
||||
Ok(Bytes::from(v))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sink_round_trips_event() {
|
||||
let (sink, mut rx) = ToolEventSink::new();
|
||||
sink.emit(ToolEvent::Status { message: "hi".into() });
|
||||
let got = rx.recv().await.unwrap();
|
||||
match got {
|
||||
ToolEvent::Status { message } => assert_eq!(&*message, "hi"),
|
||||
_ => panic!("wrong variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_content_text_helper() {
|
||||
match ToolResultContent::text("hello") {
|
||||
ToolResultContent::Text { text } => assert_eq!(&*text, "hello"),
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_event_serde_round_trip() {
|
||||
let ev = ToolEvent::PartialOutput { content: ToolResultContent::text("x") };
|
||||
let json = serde_json::to_string(&ev).unwrap();
|
||||
let _back: ToolEvent = serde_json::from_str(&json).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_request_id_constructor_and_accessor() {
|
||||
let id = PermissionRequestId::new("abc");
|
||||
assert_eq!(id.as_str(), "abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_request_id_serde_is_transparent_string() {
|
||||
let id = PermissionRequestId::new("foo");
|
||||
let json = serde_json::to_string(&id).unwrap();
|
||||
assert_eq!(json, "\"foo\"");
|
||||
let back: PermissionRequestId = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user