use crate::{bus::Bus, reply::CorrelationId, topic::Topic, BusError, Result as BusResult};
use agent_mesh_protocol::pyo3_module::{PyAgentKey, PyFingerprint, PyUserKey};
use agent_mesh_protocol::AgentKey;
use pyo3::exceptions::{PyRuntimeError, PyTimeoutError};
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyModule, PyType};
use pyo3::Py;
type PyAnyObj = Py<PyAny>;
use pyo3_async_runtimes::tokio::future_into_py;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
#[pyclass(
name = "Topic",
module = "agent_mesh._agent_mesh.bus",
frozen,
skip_from_py_object
)]
#[derive(Clone)]
pub struct PyTopic {
pub(crate) inner: Topic,
}
#[pymethods]
impl PyTopic {
#[new]
fn new(user_fp: &PyFingerprint, name: String) -> Self {
Self {
inner: Topic::new(user_fp.inner, name),
}
}
fn wire(&self) -> String {
self.inner.wire()
}
#[classmethod]
fn parse_wire(_cls: &Bound<'_, PyType>, s: &str) -> Option<Self> {
Topic::parse_wire(s).map(|t| Self { inner: t })
}
#[getter]
fn name(&self) -> &str {
&self.inner.name
}
#[getter]
fn user_fp(&self) -> PyFingerprint {
PyFingerprint {
inner: self.inner.user_fp,
}
}
fn __str__(&self) -> String {
self.inner.wire()
}
fn __repr__(&self) -> String {
format!("Topic('{}')", self.inner.wire())
}
}
#[pyclass(
name = "CorrelationId",
module = "agent_mesh._agent_mesh.bus",
frozen,
skip_from_py_object
)]
#[derive(Clone)]
pub struct PyCorrelationId {
pub(crate) inner: CorrelationId,
}
#[pymethods]
impl PyCorrelationId {
#[classmethod]
fn new_random(_cls: &Bound<'_, PyType>) -> Self {
Self {
inner: CorrelationId::new_random(),
}
}
fn hex(&self) -> String {
self.inner.hex()
}
}
#[pyclass(
name = "BroadcastReceiver",
module = "agent_mesh._agent_mesh.bus",
skip_from_py_object
)]
pub struct PyBroadcastReceiver {
inner: Arc<Mutex<tokio::sync::broadcast::Receiver<Vec<u8>>>>,
}
#[pymethods]
impl PyBroadcastReceiver {
fn recv<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
future_into_py(py, async move {
let mut guard = inner.lock().await;
match guard.recv().await {
Ok(body) => Ok(Some(body)),
Err(_) => Ok(None),
}
})
}
}
#[pyclass(
name = "Bus",
module = "agent_mesh._agent_mesh.bus",
skip_from_py_object
)]
pub struct PyBus {
inner: Arc<Mutex<Option<Arc<Bus>>>>,
}
impl PyBus {
async fn snapshot(inner: &Arc<Mutex<Option<Arc<Bus>>>>) -> PyResult<Arc<Bus>> {
let guard = inner.lock().await;
guard
.as_ref()
.cloned()
.ok_or_else(|| PyRuntimeError::new_err("bus has been closed"))
}
}
#[pymethods]
impl PyBus {
#[classmethod]
fn bind<'py>(
_cls: &Bound<'py, PyType>,
py: Python<'py>,
user: &PyUserKey,
agent: &PyAgentKey,
port: u16,
) -> PyResult<Bound<'py, PyAny>> {
let user_dir =
tempfile::tempdir().map_err(|e| PyRuntimeError::new_err(format!("temp dir: {e}")))?;
let user_path = user_dir.path().join("user.key");
user.inner
.save(&user_path)
.map_err(|e| PyRuntimeError::new_err(format!("user save: {e}")))?;
let agent_seed = agent.inner.signing_key_bytes();
let agent_cert = agent.inner.cert().clone();
future_into_py(py, async move {
let user = agent_mesh_protocol::UserKey::load(&user_path)
.map_err(|e| PyRuntimeError::new_err(format!("user load: {e}")))?;
let agent = AgentKey::from_seed_and_cert(&agent_seed, agent_cert)
.map_err(|e| PyRuntimeError::new_err(format!("agent rebuild: {e}")))?;
let bus = Bus::bind(&user, agent, port).await.map_err(bus_err_to_py)?;
drop(user_dir);
Ok(Self {
inner: Arc::new(Mutex::new(Some(Arc::new(bus)))),
})
})
}
fn user_fingerprint<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
future_into_py(py, async move {
let bus = Self::snapshot(&inner).await?;
Ok(PyFingerprint {
inner: bus.user_fingerprint(),
})
})
}
fn agent_fingerprint<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
future_into_py(py, async move {
let bus = Self::snapshot(&inner).await?;
Ok(PyFingerprint {
inner: bus.agent_fingerprint(),
})
})
}
fn local_port<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
future_into_py(py, async move {
let bus = Self::snapshot(&inner).await?;
Ok(bus.local_port())
})
}
fn request<'py>(
&self,
py: Python<'py>,
peer_fp: &PyFingerprint,
topic: &PyTopic,
body: Vec<u8>,
timeout_ms: u64,
) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
let peer_fp = peer_fp.inner;
let topic = topic.inner.clone();
future_into_py(py, async move {
let bus = Self::snapshot(&inner).await?;
let bytes = bus
.request(peer_fp, &topic, body, Duration::from_millis(timeout_ms))
.await
.map_err(bus_err_to_py)?;
Python::attach(|py| Ok(PyBytes::new(py, &bytes).unbind()))
})
}
fn publish_to<'py>(
&self,
py: Python<'py>,
peer_fp: &PyFingerprint,
topic: &PyTopic,
body: Vec<u8>,
) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
let peer_fp = peer_fp.inner;
let topic = topic.inner.clone();
future_into_py(py, async move {
let bus = Self::snapshot(&inner).await?;
bus.publish_to(peer_fp, &topic, body)
.await
.map_err(bus_err_to_py)?;
Ok(())
})
}
fn subscribe<'py>(&self, py: Python<'py>, topic: &PyTopic) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
let topic = topic.inner.clone();
future_into_py(py, async move {
let bus = Self::snapshot(&inner).await?;
let rx = bus.subscribe(&topic).await;
Ok(PyBroadcastReceiver {
inner: Arc::new(Mutex::new(rx)),
})
})
}
fn handle_requests(&self, topic: &PyTopic, callback: PyAnyObj) -> PyResult<()> {
let inner = self.inner.clone();
let topic = topic.inner.clone();
let callback = Arc::new(callback);
let handle = pyo3_async_runtimes::tokio::get_runtime();
handle.spawn(async move {
if let Ok(bus) = Self::snapshot(&inner).await {
bus.handle_requests(topic, move |body: Vec<u8>| {
let cb = callback.clone();
async move { invoke_py_handler(cb, body).await }
});
}
});
Ok(())
}
fn close<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
future_into_py(py, async move {
let mut guard = inner.lock().await;
if let Some(bus_arc) = guard.take() {
drop(guard);
match Arc::try_unwrap(bus_arc) {
Ok(bus) => bus.close().await.map_err(bus_err_to_py)?,
Err(_) => {
}
}
}
Ok(())
})
}
}
async fn invoke_py_handler(callback: Arc<PyAnyObj>, body: Vec<u8>) -> BusResult<Vec<u8>> {
let result: PyResult<Vec<u8>> = Python::attach(|py| {
let args = (PyBytes::new(py, &body),);
let value = callback.call1(py, args)?;
let bound = value.bind(py);
if bound.hasattr("__await__").unwrap_or(false) {
return Err(pyo3::exceptions::PyRuntimeError::new_err(
"async handlers are not supported in this release — \
handler must return bytes directly. Wrap async work \
in asyncio.run() inside the handler if needed.",
));
}
value.extract::<Vec<u8>>(py)
});
result.map_err(|e| BusError::Transport(transport_err(format!("handler: {e}"))))
}
fn transport_err(msg: String) -> agent_mesh_transport::TransportError {
agent_mesh_transport::TransportError::Iroh(msg)
}
fn bus_err_to_py(e: BusError) -> PyErr {
match &e {
BusError::Timeout(_) => PyTimeoutError::new_err(e.to_string()),
_ => PyRuntimeError::new_err(e.to_string()),
}
}
pub fn register(py: Python<'_>, parent: &Bound<'_, PyModule>) -> PyResult<()> {
let m = PyModule::new(py, "bus")?;
m.add_class::<PyTopic>()?;
m.add_class::<PyCorrelationId>()?;
m.add_class::<PyBus>()?;
m.add_class::<PyBroadcastReceiver>()?;
parent.add_submodule(&m)?;
Ok(())
}