use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use a2a_rs::domain::{
A2AError, ContextId, ListTasksParams, ListTasksResult, Message, Task as WireTask,
TaskArtifactUpdateEvent, TaskId, TaskPushNotificationConfig, TaskState, TaskStatus,
TaskStatusUpdateEvent,
};
use a2a_rs::port::{
AsyncMessageHandler, AsyncNotificationManager, AsyncStreamingHandler, AsyncTaskLifecycle,
AsyncTaskQuery, RequestContext, StreamingSubscriber,
};
use futures_util::TryStreamExt;
use serde_json::{Value, json};
use crate::a2a::Principal;
use crate::runtime::a2a_server::A2aBridge;
pub struct RuntimePorts {
bridge: Arc<A2aBridge>,
updates: Arc<a2a_rs::adapter::InMemoryStreamingHandler>,
}
impl RuntimePorts {
pub fn new(
bridge: Arc<A2aBridge>,
updates: Arc<a2a_rs::adapter::InMemoryStreamingHandler>,
) -> RuntimePorts {
RuntimePorts { bridge, updates }
}
async fn call(&self, method: &str, params: Value, who: &Principal) -> Result<Value, A2AError> {
let bridge = Arc::clone(&self.bridge);
let method = method.to_string();
let who = who.clone();
let v = tokio::task::spawn_blocking(move || bridge.call(&method, params, who))
.await
.map_err(|e| A2AError::Internal(format!("the runtime call did not complete: {e}")))?;
match v.get("_error") {
Some(e) => Err(from_error_object(e)),
None => Ok(v),
}
}
pub fn updates(&self) -> Arc<a2a_rs::adapter::InMemoryStreamingHandler> {
Arc::clone(&self.updates)
}
}
tokio::task_local! {
static CALLER: Principal;
static STREAMABLE: StreamAuthz;
}
#[derive(Clone, Default)]
struct StreamAuthz(Arc<Mutex<HashMap<String, bool>>>);
impl StreamAuthz {
fn record(&self, task_id: &str, allowed: bool) {
if let Ok(mut seen) = self.0.lock() {
seen.insert(task_id.to_string(), allowed);
}
}
fn verdict(&self, task_id: &str) -> Option<bool> {
self.0
.lock()
.ok()
.and_then(|seen| seen.get(task_id).copied())
}
}
fn streamable() -> Option<StreamAuthz> {
STREAMABLE.try_with(StreamAuthz::clone).ok()
}
pub async fn with_caller<F, T>(who: Principal, f: F) -> T
where
F: std::future::Future<Output = T>,
{
CALLER
.scope(who, STREAMABLE.scope(StreamAuthz::default(), f))
.await
}
pub fn caller() -> Principal {
CALLER
.try_with(|p| p.clone())
.unwrap_or_else(|_| Principal::anonymous())
}
fn from_error_object(e: &Value) -> A2AError {
let code = e.get("code").and_then(Value::as_i64).unwrap_or(-32603);
let msg = e
.get("message")
.and_then(Value::as_str)
.unwrap_or("internal error")
.to_string();
match code as i32 {
-32001 => A2AError::TaskNotFound(msg),
-32002 => A2AError::TaskNotCancelable(msg),
-32003 => A2AError::PushNotificationNotSupported,
-32004 => A2AError::UnsupportedOperation(msg),
-32601 => A2AError::MethodNotFound(msg),
-32602 => A2AError::InvalidParams(msg),
_ => A2AError::Internal(msg),
}
}
fn task_from(v: Value) -> Result<WireTask, A2AError> {
let body = match v.get("task") {
Some(t) => t.clone(),
None => v,
};
serde_json::from_value(body).map_err(A2AError::JsonParse)
}
#[async_trait::async_trait]
impl AsyncMessageHandler for RuntimePorts {
async fn process_message(
&self,
task_id: &str,
message: &Message,
ctx: &RequestContext,
) -> Result<WireTask, A2AError> {
let _ = ctx;
let who = caller();
let mut params =
json!({"message": serde_json::to_value(message).map_err(A2AError::JsonParse)?});
if !task_id.is_empty() {
params["taskId"] = json!(task_id);
}
let task = task_from(self.call("SendMessage", params, &who).await?)?;
if let Some(seen) = streamable() {
seen.record(&task.id, true);
}
Ok(task)
}
}
#[async_trait::async_trait]
impl AsyncTaskLifecycle for RuntimePorts {
async fn create(&self, _id: &TaskId, _context_id: &ContextId) -> Result<WireTask, A2AError> {
Err(A2AError::UnsupportedOperation(
"agentd creates tasks from messages; there is no out-of-band create".to_string(),
))
}
async fn get(&self, id: &TaskId, history_length: Option<u32>) -> Result<WireTask, A2AError> {
let who = caller();
let got = self.call("GetTask", json!({"id": id.as_str()}), &who).await;
if let Some(seen) = streamable() {
seen.record(id.as_str(), got.is_ok());
}
let mut t = task_from(got?)?;
if let Some(n) = history_length {
t = t.with_limited_history(Some(n));
}
Ok(t)
}
async fn update_status(
&self,
_id: &TaskId,
_state: TaskState,
_message: Option<Message>,
) -> Result<WireTask, A2AError> {
Err(A2AError::UnsupportedOperation(
"task state follows the work; it is not settable from outside".to_string(),
))
}
async fn cancel(&self, id: &TaskId) -> Result<WireTask, A2AError> {
let who = caller();
task_from(
self.call("CancelTask", json!({"id": id.as_str()}), &who)
.await?,
)
}
async fn exists(&self, id: &TaskId) -> Result<bool, A2AError> {
match self.get(id, None).await {
Ok(_) => Ok(true),
Err(A2AError::TaskNotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}
}
#[async_trait::async_trait]
impl AsyncTaskQuery for RuntimePorts {
async fn list(&self, params: &ListTasksParams) -> Result<ListTasksResult, A2AError> {
let who = caller();
let mut req = json!({});
if let Some(c) = ¶ms.context_id {
req["contextId"] = json!(c);
}
let v = self.call("ListTasks", req, &who).await?;
serde_json::from_value(v).map_err(A2AError::JsonParse)
}
}
#[async_trait::async_trait]
impl AsyncNotificationManager for RuntimePorts {
async fn set_config(
&self,
config: &TaskPushNotificationConfig,
) -> Result<TaskPushNotificationConfig, A2AError> {
let who = caller();
let params = json!({
"taskId": config.task_id,
"pushNotificationConfig": serde_json::to_value(config).map_err(A2AError::JsonParse)?,
});
let v = self.call("PushConfigSet", params, &who).await?;
serde_json::from_value(v).map_err(A2AError::JsonParse)
}
async fn get_config(
&self,
params: &a2a_rs::domain::GetTaskPushNotificationConfigParams,
) -> Result<TaskPushNotificationConfig, A2AError> {
let who = caller();
let req = json!({
"taskId": params.id,
"pushNotificationConfigId": params.push_notification_config_id,
});
let v = self.call("PushConfigGet", req, &who).await?;
serde_json::from_value(v).map_err(A2AError::JsonParse)
}
async fn list_configs(
&self,
params: &a2a_rs::domain::ListTaskPushNotificationConfigsParams,
) -> Result<Vec<TaskPushNotificationConfig>, A2AError> {
let who = caller();
let v = self
.call("PushConfigList", json!({"taskId": params.id}), &who)
.await?;
serde_json::from_value(v["configs"].clone()).map_err(A2AError::JsonParse)
}
async fn delete_config(
&self,
params: &a2a_rs::domain::DeleteTaskPushNotificationConfigParams,
) -> Result<(), A2AError> {
let who = caller();
let req = json!({
"taskId": params.id,
"pushNotificationConfigId": params.push_notification_config_id,
});
self.call("PushConfigDelete", req, &who).await?;
Ok(())
}
}
pub struct SharedStreaming(pub Arc<a2a_rs::adapter::InMemoryStreamingHandler>);
#[async_trait::async_trait]
impl AsyncStreamingHandler for SharedStreaming {
async fn add_status_subscriber(
&self,
task_id: &str,
subscriber: Box<dyn StreamingSubscriber<TaskStatusUpdateEvent> + Send + Sync>,
) -> Result<String, A2AError> {
self.0.add_status_subscriber(task_id, subscriber).await
}
async fn add_artifact_subscriber(
&self,
task_id: &str,
subscriber: Box<dyn StreamingSubscriber<TaskArtifactUpdateEvent> + Send + Sync>,
) -> Result<String, A2AError> {
self.0.add_artifact_subscriber(task_id, subscriber).await
}
async fn remove_subscription(&self, subscription_id: &str) -> Result<(), A2AError> {
self.0.remove_subscription(subscription_id).await
}
async fn remove_task_subscribers(&self, task_id: &str) -> Result<(), A2AError> {
self.0.remove_task_subscribers(task_id).await
}
async fn get_subscriber_count(&self, task_id: &str) -> Result<usize, A2AError> {
self.0.get_subscriber_count(task_id).await
}
async fn broadcast_status_update(
&self,
task_id: &str,
update: TaskStatusUpdateEvent,
) -> Result<(), A2AError> {
self.0.broadcast_status_update(task_id, update).await
}
async fn broadcast_artifact_update(
&self,
task_id: &str,
update: TaskArtifactUpdateEvent,
) -> Result<(), A2AError> {
self.0.broadcast_artifact_update(task_id, update).await
}
async fn status_update_stream(
&self,
task_id: &str,
) -> Result<
std::pin::Pin<
Box<dyn futures_util::Stream<Item = Result<TaskStatusUpdateEvent, A2AError>> + Send>,
>,
A2AError,
> {
self.0.status_update_stream(task_id).await
}
async fn artifact_update_stream(
&self,
task_id: &str,
) -> Result<
std::pin::Pin<
Box<dyn futures_util::Stream<Item = Result<TaskArtifactUpdateEvent, A2AError>> + Send>,
>,
A2AError,
> {
self.0.artifact_update_stream(task_id).await
}
async fn combined_update_stream(
&self,
task_id: &str,
from_event_id: Option<u64>,
) -> Result<
std::pin::Pin<
Box<dyn futures_util::Stream<Item = Result<a2a_rs::port::SeqEvent, A2AError>> + Send>,
>,
A2AError,
> {
let seen = streamable();
if seen.as_ref().and_then(|s| s.verdict(task_id)) == Some(false) {
return Err(A2AError::TaskNotFound(task_id.to_string()));
}
let inner = self
.0
.combined_update_stream(task_id, from_event_id)
.await?;
if seen.as_ref().and_then(|s| s.verdict(task_id)) == Some(true) {
return Ok(inner);
}
let id = task_id.to_string();
Ok(Box::pin(
futures_util::stream::once(async move {
match seen.and_then(|s| s.verdict(&id)) {
Some(true) => Ok(inner),
_ => Err(A2AError::TaskNotFound(id)),
}
})
.try_flatten(),
))
}
}
pub struct StreamSink {
updates: Arc<a2a_rs::adapter::InMemoryStreamingHandler>,
handle: tokio::runtime::Handle,
log: crate::obs::log::Logger,
}
impl StreamSink {
pub fn new(
updates: Arc<a2a_rs::adapter::InMemoryStreamingHandler>,
handle: tokio::runtime::Handle,
log: crate::obs::log::Logger,
) -> StreamSink {
StreamSink {
updates,
handle,
log,
}
}
pub fn status(
&self,
task_id: &str,
context_id: &str,
state: TaskState,
message: Option<&str>,
at_ms: u64,
) {
let ev = crate::a2a::wire::status_event(task_id, context_id, state, message, at_ms);
self.spawn_status(task_id.to_string(), ev);
}
pub fn push(&self, task: &crate::a2a::tasks::Task, allow_private: bool) {
let event = serde_json::to_value(crate::a2a::wire::task(task)).unwrap_or_default();
for target in &task.push {
let target = target.clone();
let event = event.clone();
let task_id = task.id.clone();
let log = self.log.clone();
self.handle.spawn(async move {
let outcome = tokio::task::spawn_blocking(move || {
crate::a2a::push::deliver(&target, &event, allow_private)
})
.await;
if let Ok(Err(e)) = outcome {
log.warn(
"a2a.push.failed",
serde_json::json!({"task": task_id, "err": e}),
);
}
});
}
}
pub fn artifact(&self, task_id: &str, context_id: &str, artifact: a2a_rs::domain::Artifact) {
let ev = crate::a2a::wire::artifact_event(task_id, context_id, artifact, true);
let updates = Arc::clone(&self.updates);
let id = task_id.to_string();
self.handle.spawn(async move {
let _ = updates.broadcast_artifact_update(&id, ev).await;
});
}
fn spawn_status(&self, id: String, ev: TaskStatusUpdateEvent) {
let updates = Arc::clone(&self.updates);
self.handle.spawn(async move {
let _ = updates.broadcast_status_update(&id, ev).await;
});
}
}
pub fn status_of(ev: &TaskStatusUpdateEvent) -> &TaskStatus {
&ev.status
}