#![cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
use async_trait::async_trait;
use crate::error::{Error, ErrorCode, Result};
use crate::server::roots::ListRootsResult;
use crate::server::server_request_dispatcher::ServerRequestDispatcher;
use crate::shared::peer::PeerHandle;
use crate::types::sampling::{
CreateMessageParams, CreateMessageResult, CreateMessageResultWithTools,
};
use crate::types::{ProgressToken, ServerRequest};
#[derive(Debug)]
pub struct DispatchPeerHandle {
dispatcher: Arc<ServerRequestDispatcher>,
}
impl DispatchPeerHandle {
pub fn new(dispatcher: Arc<ServerRequestDispatcher>) -> Self {
Self { dispatcher }
}
}
#[async_trait]
impl PeerHandle for DispatchPeerHandle {
async fn sample(&self, params: CreateMessageParams) -> Result<CreateMessageResult> {
let value = self
.dispatcher
.dispatch(ServerRequest::CreateMessage(Box::new(params)))
.await?;
serde_json::from_value::<CreateMessageResult>(value).map_err(|e| {
Error::protocol(
ErrorCode::INTERNAL_ERROR,
format!("Invalid sample response: {e}"),
)
})
}
async fn sample_with_tools(
&self,
params: CreateMessageParams,
) -> Result<CreateMessageResultWithTools> {
let value = self
.dispatcher
.dispatch(ServerRequest::CreateMessage(Box::new(params)))
.await?;
if let Ok(with_tools) =
serde_json::from_value::<CreateMessageResultWithTools>(value.clone())
{
return Ok(with_tools);
}
let legacy = serde_json::from_value::<CreateMessageResult>(value).map_err(|e| {
Error::protocol(
ErrorCode::INTERNAL_ERROR,
format!("Invalid sample_with_tools response: {e}"),
)
})?;
Ok(CreateMessageResultWithTools::from_single(legacy))
}
async fn list_roots(&self) -> Result<ListRootsResult> {
let value = self.dispatcher.dispatch(ServerRequest::ListRoots).await?;
serde_json::from_value::<ListRootsResult>(value).map_err(|e| {
Error::protocol(
ErrorCode::INTERNAL_ERROR,
format!("Invalid list_roots response: {e}"),
)
})
}
async fn progress_notify(
&self,
_token: ProgressToken,
_progress: f64,
_total: Option<f64>,
_message: Option<String>,
) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::sync::mpsc;
fn build_dispatcher_with_short_timeout() -> (
Arc<ServerRequestDispatcher>,
mpsc::Receiver<(String, ServerRequest)>,
) {
let (tx, rx) = mpsc::channel::<(String, ServerRequest)>(4);
let dispatcher = Arc::new(
ServerRequestDispatcher::new_with_channel(tx).with_timeout(Duration::from_millis(40)),
);
(dispatcher, rx)
}
#[tokio::test]
async fn test_peer_handle_trait_shape() {
let (dispatcher, _rx) = build_dispatcher_with_short_timeout();
let peer: Arc<dyn PeerHandle> = Arc::new(DispatchPeerHandle::new(dispatcher));
let _clone = peer.clone();
}
#[tokio::test]
async fn test_peer_progress_notify_always_ok() {
let (dispatcher, _rx) = build_dispatcher_with_short_timeout();
let peer = DispatchPeerHandle::new(dispatcher);
let result = peer
.progress_notify(
ProgressToken::String("tok-1".to_string()),
0.5,
Some(1.0),
None,
)
.await;
assert!(result.is_ok(), "progress_notify is a no-op for this phase");
}
#[tokio::test]
async fn test_peer_sample_propagates_dispatcher_timeout() {
let (dispatcher, _rx) = build_dispatcher_with_short_timeout();
let peer = DispatchPeerHandle::new(dispatcher);
let params = CreateMessageParams::new(Vec::new());
let start = std::time::Instant::now();
let result = peer.sample(params).await;
let elapsed = start.elapsed();
assert!(
result.is_err(),
"sample must return Err when dispatcher times out"
);
assert!(
elapsed < Duration::from_millis(500),
"timeout must fire within 500ms (was {:?})",
elapsed
);
}
fn build_dispatcher_with_long_timeout() -> (
Arc<ServerRequestDispatcher>,
mpsc::Receiver<(String, ServerRequest)>,
) {
let (tx, rx) = mpsc::channel::<(String, ServerRequest)>(4);
let dispatcher = Arc::new(
ServerRequestDispatcher::new_with_channel(tx).with_timeout(Duration::from_secs(2)),
);
(dispatcher, rx)
}
#[tokio::test]
async fn test_sample_with_tools_decodes_with_tools_response() {
let (dispatcher, mut rx) = build_dispatcher_with_long_timeout();
let peer = DispatchPeerHandle::new(dispatcher.clone());
let fut = tokio::spawn(async move {
peer.sample_with_tools(CreateMessageParams::new(Vec::new()))
.await
});
let (cid, _req) = rx.recv().await.expect("outbound dispatch");
let response = serde_json::json!({
"model": "host-model",
"role": "assistant",
"content": [
{ "type": "tool_use", "name": "search", "id": "call-1", "input": {"q": "rust"} }
]
});
dispatcher
.handle_response(&cid, response)
.await
.expect("handle_response");
let result = fut.await.unwrap().expect("sample_with_tools succeeds");
assert_eq!(result.model, "host-model");
assert_eq!(result.content.len(), 1);
match &result.content[0] {
crate::types::sampling::SamplingMessageContent::ToolUse { name, id, .. } => {
assert_eq!(name, "search");
assert_eq!(id, "call-1");
},
other => panic!("tool_use block must survive decode, got {other:?}"),
}
}
#[tokio::test]
async fn test_sample_with_tools_falls_back_to_legacy_result() {
let (dispatcher, mut rx) = build_dispatcher_with_long_timeout();
let peer = DispatchPeerHandle::new(dispatcher.clone());
let fut = tokio::spawn(async move {
peer.sample_with_tools(CreateMessageParams::new(Vec::new()))
.await
});
let (cid, _req) = rx.recv().await.expect("outbound dispatch");
let response = serde_json::json!({
"content": { "type": "text", "text": "legacy answer" },
"model": "old-model",
"stopReason": "endTurn"
});
dispatcher
.handle_response(&cid, response)
.await
.expect("handle_response");
let result = fut.await.unwrap().expect("legacy fallback succeeds");
assert_eq!(result.model, "old-model");
assert_eq!(result.stop_reason.as_deref(), Some("endTurn"));
assert_eq!(
result.content.len(),
1,
"single content lifts to one element"
);
match &result.content[0] {
crate::types::sampling::SamplingMessageContent::Text { text, .. } => {
assert_eq!(text, "legacy answer");
},
other => panic!("legacy content must lift to Text, got {other:?}"),
}
}
}