use async_trait::async_trait;
use serde_json::{json, Value};
use crate::error::{Error, Result};
use crate::mail_route::{process_ancestry, resolve_caller, LiveSessions};
use crate::mail_send::SendOptions;
use crate::mailbox::{mail_root, Mailbox};
use crate::tools::{Tool, ToolContext, ToolRegistry};
use crate::HarnessHomes;
pub fn registry() -> ToolRegistry {
let mut registry = ToolRegistry::new();
registry.register(ListAgents);
registry.register(SendMessage);
registry.register(ReadMessages);
registry
}
fn caller(tool: &str, homes: &HarnessHomes) -> Result<crate::mail_route::Caller> {
resolve_caller(homes, &process_ancestry()).map_err(|message| Error::tool(tool, message))
}
struct ListAgents;
#[async_trait]
impl Tool for ListAgents {
fn name(&self) -> &str {
"list_agents"
}
fn description(&self) -> &str {
"List the coding-agent sessions you can message, of every harness (Claude Code, Codex, \
sessions supercode hosts): each one's name, harness, status (busy, idle, hosted), how it \
is reached, and address. `query` keeps the ones whose name or address contains it."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"query": {"type": "string", "description": "Keep sessions whose name or address contains this."}
},
"additionalProperties": false
})
}
async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<String> {
let query = args
.get("query")
.and_then(Value::as_str)
.map(str::to_lowercase);
let sessions = LiveSessions::read(&HarnessHomes::default());
let rows: Vec<String> = sessions
.all()
.iter()
.filter(|session| {
query.as_deref().is_none_or(|query| {
session.name.to_lowercase().contains(query)
|| session.address.to_string().to_lowercase().contains(query)
})
})
.map(|session| {
format!(
"{} ({}, {}, {}) {}",
session.name,
session.address.harness,
session.status,
session.door.name(),
session.address
)
})
.collect();
Ok(if rows.is_empty() {
"No session to message matches.".into()
} else {
rows.join("\n")
})
}
}
struct SendMessage;
#[async_trait]
impl Tool for SendMessage {
fn name(&self) -> &str {
"send_message"
}
fn description(&self) -> &str {
"Send a message to another coding-agent session, of any harness, on this machine or \
another of your team's (`name@machine`). `to` is a name from list_agents, `name@machine`, \
or an address. To answer a message, send to its `from` with `reply_to` set to its id. \
The result says whether it was delivered, queued, held for approval, refused or stored."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"to": {"type": "string", "description": "A session name, name@machine, or address."},
"message": {"type": "string", "description": "The message."},
"reply_to": {"type": "string", "description": "Id of the message this answers."},
"notify_when_idle": {"type": "boolean", "description": "Also get one notice when the receiver's next turn ends."}
},
"required": ["to", "message"],
"additionalProperties": false
})
}
async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<String> {
let text = |key: &str| args.get(key).and_then(Value::as_str).map(str::to_string);
let (Some(to), Some(message)) = (text("to"), text("message")) else {
return Err(Error::tool(self.name(), "`to` and `message` are required"));
};
if message.trim().is_empty() {
return Err(Error::tool(
self.name(),
"Nothing was sent: the message is empty.",
));
}
let homes = HarnessHomes::default();
let caller = caller(self.name(), &homes)?;
let options = SendOptions {
in_reply_to: text("reply_to"),
notify_when_idle: args
.get("notify_when_idle")
.and_then(Value::as_bool)
.unwrap_or(false),
..Default::default()
};
let outcome = crate::mail_send::send(&homes, &caller, &to, &message, options)
.await
.map_err(|error| Error::tool(self.name(), error.to_string()))?;
if outcome.code == 0 {
Ok(outcome.text)
} else {
Err(Error::tool(self.name(), outcome.text))
}
}
}
struct ReadMessages;
#[async_trait]
impl Tool for ReadMessages {
fn name(&self) -> &str {
"read_messages"
}
fn description(&self) -> &str {
"Read your unread messages from other sessions, each with how to answer it. Messages \
usually arrive in your conversation by themselves; this reads any that are waiting."
}
fn parameters(&self) -> Value {
json!({"type": "object", "properties": {}, "additionalProperties": false})
}
async fn execute(&self, _args: Value, _ctx: &ToolContext) -> Result<String> {
let homes = HarnessHomes::default();
let caller = caller(self.name(), &homes)?;
let mailbox = Mailbox::open(&mail_root(), &caller.address)
.map_err(|error| Error::tool(self.name(), error.to_string()))?;
let claimed = mailbox
.claim_unread()
.map_err(|error| Error::tool(self.name(), error.to_string()))?;
let text = if claimed.is_empty() {
"No unread messages.".to_string()
} else {
claimed
.iter()
.map(|stored| stored.envelope.render())
.collect::<Vec<_>>()
.join("\n\n")
};
for stored in &claimed {
mailbox
.acknowledge(stored)
.map_err(|error| Error::tool(self.name(), error.to_string()))?;
}
Ok(text)
}
}