use std::time::Duration;
use futures_util::Stream;
pub(crate) trait StreamEvent: Send + 'static {
fn keepalive() -> Self;
fn content(text: String) -> Self;
fn call_start(index: usize, name: String) -> Self;
fn call_arguments(index: usize, fragment: String) -> Self;
fn call_end(index: usize, arguments: String) -> Self;
}
pub(crate) fn map_tool_event<E: StreamEvent>(
event: crate::policy::parser::ToolCallEvent,
) -> Vec<E> {
match event {
crate::policy::parser::ToolCallEvent::Text(text) if text.is_empty() => Vec::new(),
crate::policy::parser::ToolCallEvent::Text(text) => vec![E::content(text)],
crate::policy::parser::ToolCallEvent::CallStart { index, name } => {
vec![E::call_start(index, name)]
}
crate::policy::parser::ToolCallEvent::CallArguments { fragment, .. }
if fragment.is_empty() =>
{
Vec::new()
}
crate::policy::parser::ToolCallEvent::CallArguments { index, fragment } => {
vec![E::call_arguments(index, fragment)]
}
crate::policy::parser::ToolCallEvent::CallEnd { index, arguments } => {
vec![E::call_end(index, arguments)]
}
}
}
pub(crate) fn with_keepalive<E: StreamEvent>(
events: tokio::sync::mpsc::Receiver<E>,
interval: Duration,
) -> impl Stream<Item = E> {
futures_util::stream::unfold(events, move |mut events| async move {
match tokio::time::timeout(interval, events.recv()).await {
Err(_elapsed) => Some((E::keepalive(), events)),
Ok(Some(event)) => Some((event, events)),
Ok(None) => None,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
enum Ev {
Keepalive,
Content(String),
CallStart(usize, String),
CallArguments(usize, String),
CallEnd(usize, String),
}
impl StreamEvent for Ev {
fn keepalive() -> Self {
Ev::Keepalive
}
fn content(text: String) -> Self {
Ev::Content(text)
}
fn call_start(index: usize, name: String) -> Self {
Ev::CallStart(index, name)
}
fn call_arguments(index: usize, fragment: String) -> Self {
Ev::CallArguments(index, fragment)
}
fn call_end(index: usize, arguments: String) -> Self {
Ev::CallEnd(index, arguments)
}
}
#[test]
fn an_empty_text_run_or_argument_fragment_produces_no_event() {
let none: Vec<Ev> =
map_tool_event(crate::policy::parser::ToolCallEvent::Text(String::new()));
assert!(none.is_empty(), "an empty text run is not an event");
let none: Vec<Ev> = map_tool_event(crate::policy::parser::ToolCallEvent::CallArguments {
index: 0,
fragment: String::new(),
});
assert!(none.is_empty(), "an empty fragment is not an event");
}
#[test]
fn every_non_empty_parser_event_maps_to_exactly_one_protocol_event() {
assert_eq!(
map_tool_event::<Ev>(crate::policy::parser::ToolCallEvent::Text("hi".into())),
vec![Ev::Content("hi".into())]
);
assert_eq!(
map_tool_event::<Ev>(crate::policy::parser::ToolCallEvent::CallStart {
index: 2,
name: "search".into()
}),
vec![Ev::CallStart(2, "search".into())]
);
assert_eq!(
map_tool_event::<Ev>(crate::policy::parser::ToolCallEvent::CallArguments {
index: 2,
fragment: "{\"q\"".into()
}),
vec![Ev::CallArguments(2, "{\"q\"".into())]
);
assert_eq!(
map_tool_event::<Ev>(crate::policy::parser::ToolCallEvent::CallEnd {
index: 2,
arguments: "{}".into()
}),
vec![Ev::CallEnd(2, "{}".into())]
);
}
#[tokio::test]
async fn a_silent_generator_still_emits_and_a_live_one_is_untouched() {
use futures_util::StreamExt;
let (tx, rx) = tokio::sync::mpsc::channel(4);
let mut stream = Box::pin(with_keepalive(rx, Duration::from_millis(10)));
tx.send(Ev::Content("a".into())).await.unwrap();
assert_eq!(stream.next().await, Some(Ev::Content("a".into())));
assert_eq!(stream.next().await, Some(Ev::Keepalive));
drop(tx);
assert_eq!(stream.next().await, None);
}
}