#![deny(missing_docs)]
#![warn(clippy::pedantic, clippy::nursery)]
#![allow(
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::missing_errors_doc
)]
#[allow(missing_docs, clippy::all)]
pub mod core;
#[allow(missing_docs, clippy::all)]
pub mod transport;
pub use core::{
agent::{Agent, LoggingMiddleware, Middleware, MiddlewareStack},
codec::Encoding,
error::{AgentError, CodecError, TransportError},
jsonrpc,
types::*,
};
pub use transport::{AgentEndpoint, Discovery, Router};
#[cfg(feature = "fast")]
pub use transport::fast::{FastClient, FastMessage};
#[cfg(feature = "a2a")]
pub use transport::a2a::A2AClient;
pub use async_trait::async_trait;
#[cfg(feature = "transport-log")]
pub use transport::log::{
Direction, LogEntry, TransportKind, TransportLogger, TransportLoggerBuilder,
};
pub mod prelude {
pub use crate::{
Agent, AgentCard, AgentError, Artifact, Message, Middleware, MiddlewareStack, Part, Role,
Task, TaskRequest, TaskResponse, TaskState, TaskStatus,
};
pub use async_trait::async_trait;
}
#[allow(dead_code)]
#[derive(Clone, Copy)]
enum TransportContext {
Fast,
A2a,
}
#[cfg(feature = "transport-log")]
struct LoggedAgent {
inner: std::sync::Arc<dyn Agent>,
logger: transport::log::TransportLogger,
transport: transport::log::TransportKind,
}
#[cfg(feature = "transport-log")]
#[async_trait]
impl Agent for LoggedAgent {
fn card(&self) -> AgentCard {
self.inner.card()
}
async fn handle_task(&self, request: TaskRequest) -> Result<TaskResponse, AgentError> {
let start = std::time::Instant::now();
let task_id = request.id.clone();
let session_id = request.session_id.clone();
let result = self.inner.handle_task(request).await;
#[allow(clippy::cast_possible_truncation)]
let duration_us = start.elapsed().as_micros() as u64;
let (status, error) = match &result {
Ok(_) => ("ok", None),
Err(e) => ("error", Some(e.to_string())),
};
self.logger.record(transport::log::LogEntry {
ts: transport::log::now_iso8601(),
transport: self.transport,
direction: transport::log::Direction::Inbound,
task_id,
session_id,
duration_us,
llm_us: None,
transport_us: None,
status,
error,
payload_bytes: None,
});
result
}
async fn handle_cancel(&self, task_id: &str) -> Result<TaskStatus, AgentError> {
self.inner.handle_cancel(task_id).await
}
}
#[allow(dead_code)]
pub struct ServerBuilder {
agent: std::sync::Arc<dyn Agent>,
fast_path: Option<String>,
http_addr: Option<String>,
#[cfg(feature = "transport-log")]
logger: Option<transport::log::TransportLogger>,
}
pub fn serve(agent: impl Agent + 'static) -> ServerBuilder {
ServerBuilder {
agent: std::sync::Arc::new(agent),
fast_path: None,
http_addr: None,
#[cfg(feature = "transport-log")]
logger: None,
}
}
impl ServerBuilder {
#[must_use]
#[cfg(feature = "fast")]
pub fn fast(mut self, socket_path: impl Into<String>) -> Self {
self.fast_path = Some(socket_path.into());
self
}
#[must_use]
#[cfg(feature = "a2a")]
pub fn http(mut self, addr: impl Into<String>) -> Self {
self.http_addr = Some(addr.into());
self
}
#[must_use]
#[cfg(feature = "transport-log")]
pub fn with_transport_logger(mut self, logger: transport::log::TransportLogger) -> Self {
self.logger = Some(logger);
self
}
pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
#[allow(unused_mut)]
let mut handles: Vec<tokio::task::JoinHandle<Result<(), TransportError>>> = Vec::new();
#[cfg(feature = "fast")]
if let Some(ref path) = self.fast_path {
let agent = self.make_agent(TransportContext::Fast);
let path = path.clone();
handles.push(tokio::spawn(async move {
transport::fast::serve(agent, path).await
}));
}
#[cfg(feature = "a2a")]
if let Some(ref addr) = self.http_addr {
let agent = self.make_agent(TransportContext::A2a);
let addr = addr.clone();
handles.push(tokio::spawn(async move {
transport::a2a::serve(agent, addr).await
}));
}
if handles.is_empty() {
return Err("no transports configured — call .fast() and/or .http()".into());
}
let (result, _idx, _remaining) = futures_util::future::select_all(handles).await;
result??;
Ok(())
}
#[allow(dead_code, unused_variables)]
fn make_agent(&self, context: TransportContext) -> std::sync::Arc<dyn Agent> {
#[cfg(feature = "transport-log")]
if let Some(ref logger) = self.logger {
let transport = match context {
TransportContext::Fast => transport::log::TransportKind::Fast,
TransportContext::A2a => transport::log::TransportKind::A2a,
};
return std::sync::Arc::new(LoggedAgent {
inner: self.agent.clone(),
logger: logger.clone(),
transport,
});
}
self.agent.clone()
}
}
pub struct MessaggeroClient {
#[cfg(feature = "fast")]
fast: Option<FastClient>,
#[cfg(feature = "a2a")]
a2a: Option<A2AClient>,
}
impl MessaggeroClient {
#[cfg(feature = "fast")]
pub async fn connect_fast(
socket_path: impl AsRef<std::path::Path>,
) -> Result<Self, TransportError> {
let client = FastClient::connect(socket_path).await?;
Ok(Self {
fast: Some(client),
#[cfg(feature = "a2a")]
a2a: None,
})
}
#[cfg(feature = "a2a")]
pub fn connect_http(base_url: impl Into<String>) -> Self {
Self {
#[cfg(feature = "fast")]
fast: None,
a2a: Some(A2AClient::new(base_url)),
}
}
#[must_use]
#[cfg(feature = "transport-log")]
pub fn with_transport_logger(mut self, logger: &transport::log::TransportLogger) -> Self {
#[cfg(feature = "fast")]
if let Some(ref mut fc) = self.fast {
fc.set_logger(logger);
}
#[cfg(feature = "a2a")]
if let Some(ref mut a2a) = self.a2a {
a2a.set_logger(logger);
}
self
}
pub async fn send_task(
&mut self,
request: TaskRequest,
) -> Result<TaskResponse, TransportError> {
#[cfg(not(any(feature = "fast", feature = "a2a")))]
let _ = request;
#[cfg(feature = "fast")]
if let Some(ref mut fast) = self.fast {
let msg = fast.send_task(request).await?;
return match msg {
FastMessage::TaskResponse(resp) => Ok(resp),
FastMessage::Error(e) => Err(TransportError::Request(e)),
other => Err(TransportError::Request(format!(
"unexpected response: {other:?}"
))),
};
}
#[cfg(feature = "a2a")]
if let Some(ref a2a) = self.a2a {
return a2a.send_task(request).await;
}
Err(TransportError::Connection("no transport configured".into()))
}
#[cfg(feature = "a2a")]
pub async fn agent_card(&self) -> Result<AgentCard, TransportError> {
self.a2a
.as_ref()
.ok_or_else(|| TransportError::Connection("not connected via HTTP".into()))?
.agent_card()
.await
}
}