use super::*;
pub(super) fn compose_external_tools_for_profile(
profile: &crate::profile::Profile,
tool_bundles: &BTreeMap<String, Arc<dyn AgentToolDispatcher>>,
mob_handle: MobHandle,
) -> Result<Option<Arc<dyn AgentToolDispatcher>>, MobError> {
let mut dispatchers: Vec<Arc<dyn AgentToolDispatcher>> = Vec::new();
if profile.tools.mob || profile.tools.mob_tasks {
dispatchers.push(Arc::new(MobToolDispatcher::new(
mob_handle,
profile.tools.mob,
profile.tools.mob_tasks,
)));
}
for name in &profile.tools.rust_bundles {
let dispatcher = tool_bundles.get(name).cloned().ok_or_else(|| {
MobError::Internal(format!(
"tool bundle '{name}' is not registered on this mob builder"
))
})?;
dispatchers.push(dispatcher);
}
if dispatchers.is_empty() {
return Ok(None);
}
if dispatchers.len() == 1 {
return Ok(dispatchers.pop());
}
let mut gateway_builder = ToolGatewayBuilder::new();
for dispatcher in dispatchers {
gateway_builder = gateway_builder.add_dispatcher(dispatcher);
}
let gateway = gateway_builder
.build()
.map_err(|e| MobError::Internal(format!("failed to compose tool bundles: {e}")))?;
Ok(Some(Arc::new(gateway)))
}
struct MobToolDispatcher {
handle: MobHandle,
tools: Arc<[Arc<ToolDef>]>,
}
impl MobToolDispatcher {
fn new(handle: MobHandle, enable_mob: bool, enable_mob_tasks: bool) -> Self {
let mut defs: Vec<Arc<ToolDef>> = Vec::new();
if enable_mob {
defs.push(tool_def(
TOOL_SPAWN_MEERKAT,
"Spawn a meerkat from a profile",
json!({
"type": "object",
"properties": {
"profile": {"type": "string"},
"meerkat_id": {"type": "string"},
"initial_message": {"type": "string"},
"backend": {"type": "string", "enum": ["subagent", "external"]},
"runtime_mode": {"type": "string", "enum": ["autonomous_host", "turn_driven"]}
},
"required": ["profile", "meerkat_id"]
}),
));
defs.push(tool_def(
TOOL_SPAWN_MANY_MEERKATS,
"Spawn multiple meerkats in one call. Returns per-item results in input order.",
json!({
"type": "object",
"properties": {
"specs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"profile": {"type": "string"},
"meerkat_id": {"type": "string"},
"initial_message": {"type": "string"},
"backend": {"type": "string", "enum": ["subagent", "external"]},
"runtime_mode": {"type": "string", "enum": ["autonomous_host", "turn_driven"]}
},
"required": ["profile", "meerkat_id"]
}
}
},
"required": ["specs"]
}),
));
defs.push(tool_def(
TOOL_RETIRE_MEERKAT,
"Retire a meerkat and archive its session",
json!({
"type": "object",
"properties": {"meerkat_id": {"type": "string"}},
"required": ["meerkat_id"]
}),
));
defs.push(tool_def(
TOOL_WIRE_PEERS,
"Wire two meerkats with bidirectional trust",
json!({
"type": "object",
"properties": {"a": {"type": "string"}, "b": {"type": "string"}},
"required": ["a", "b"]
}),
));
defs.push(tool_def(
TOOL_UNWIRE_PEERS,
"Unwire two meerkats and revoke bidirectional trust",
json!({
"type": "object",
"properties": {"a": {"type": "string"}, "b": {"type": "string"}},
"required": ["a", "b"]
}),
));
defs.push(tool_def(
TOOL_LIST_MEERKATS,
"List all active meerkats. Response includes meerkat_id, profile, member_ref, session_id, wired_to.",
json!({
"type": "object",
"properties": {}
}),
));
defs.push(tool_def(
TOOL_MOB_LIST_FLOWS,
"List all configured flow IDs for this mob.",
json!({
"type": "object",
"properties": {}
}),
));
defs.push(tool_def(
TOOL_MOB_RUN_FLOW,
"Run a configured flow by ID with optional activation params. Returns run_id.",
json!({
"type": "object",
"properties": {
"flow_id": {"type": "string"},
"params": {"type": "object"}
},
"required": ["flow_id"]
}),
));
defs.push(tool_def(
TOOL_MOB_FLOW_STATUS,
"Get persisted status and ledgers for a flow run.",
json!({
"type": "object",
"properties": {
"run_id": {"type": "string"}
},
"required": ["run_id"]
}),
));
defs.push(tool_def(
TOOL_MOB_CANCEL_FLOW,
"Cancel an in-flight flow run by run_id.",
json!({
"type": "object",
"properties": {
"run_id": {"type": "string"}
},
"required": ["run_id"]
}),
));
}
if enable_mob_tasks {
defs.push(tool_def(
TOOL_MOB_TASK_CREATE,
"Create a shared mob task. Response includes generated task_id.",
json!({
"type": "object",
"properties": {
"subject": {"type": "string"},
"description": {"type": "string"},
"blocked_by": {"type": "array", "items": {"type": "string"}}
},
"required": ["subject", "description"]
}),
));
defs.push(tool_def(
TOOL_MOB_TASK_LIST,
"List shared mob tasks",
json!({
"type": "object",
"properties": {}
}),
));
defs.push(tool_def(
TOOL_MOB_TASK_UPDATE,
"Update task status and owner. If owner is set, status must be in_progress and blocked_by dependencies must be completed.",
json!({
"type": "object",
"properties": {
"task_id": {"type": "string"},
"status": {
"type": "string",
"enum": ["open", "in_progress", "completed", "cancelled"]
},
"owner": {"type": "string", "description": "Optional. Only valid when status is in_progress."}
},
"required": ["task_id", "status"]
}),
));
defs.push(tool_def(
TOOL_MOB_TASK_GET,
"Get a shared task by ID",
json!({
"type": "object",
"properties": {"task_id": {"type": "string"}},
"required": ["task_id"]
}),
));
}
Self {
handle,
tools: defs.into(),
}
}
fn map_mob_error(call: ToolCallView<'_>, error: MobError) -> ToolError {
ToolError::execution_failed(format!("tool '{}' failed: {error}", call.name))
}
fn encode_result(
call: ToolCallView<'_>,
value: serde_json::Value,
) -> Result<ToolResult, ToolError> {
let content = serde_json::to_string(&value)
.map_err(|error| ToolError::execution_failed(format!("encode tool result: {error}")))?;
Ok(ToolResult {
tool_use_id: call.id.to_string(),
content,
is_error: false,
})
}
}
fn tool_def(name: &str, description: &str, input_schema: serde_json::Value) -> Arc<ToolDef> {
Arc::new(ToolDef {
name: name.to_string(),
description: description.to_string(),
input_schema,
})
}
#[derive(Deserialize)]
struct SpawnMeerkatArgs {
profile: String,
meerkat_id: String,
#[serde(default)]
initial_message: Option<String>,
#[serde(default)]
backend: Option<MobBackendKind>,
#[serde(default)]
runtime_mode: Option<crate::MobRuntimeMode>,
}
#[derive(Deserialize)]
struct SpawnManyMeerkatsArgs {
specs: Vec<SpawnMeerkatArgs>,
}
#[derive(Deserialize)]
struct RetireMeerkatArgs {
meerkat_id: String,
}
#[derive(Deserialize)]
struct WirePeersArgs {
a: String,
b: String,
}
#[derive(Deserialize)]
struct TaskCreateArgs {
subject: String,
description: String,
#[serde(default)]
blocked_by: Vec<TaskId>,
}
#[derive(Deserialize)]
struct TaskUpdateArgs {
task_id: TaskId,
status: TaskStatus,
owner: Option<String>,
}
#[derive(Deserialize)]
struct TaskGetArgs {
task_id: TaskId,
}
#[derive(Deserialize)]
struct RunFlowArgs {
flow_id: String,
#[serde(default)]
params: serde_json::Value,
}
#[derive(Deserialize)]
struct FlowStatusArgs {
run_id: String,
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AgentToolDispatcher for MobToolDispatcher {
fn tools(&self) -> Arc<[Arc<ToolDef>]> {
Arc::clone(&self.tools)
}
async fn dispatch(&self, call: ToolCallView<'_>) -> Result<ToolResult, ToolError> {
match call.name {
TOOL_SPAWN_MEERKAT => {
let args: SpawnMeerkatArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
let member_ref = self
.handle
.spawn_with_options(
ProfileName::from(args.profile),
MeerkatId::from(args.meerkat_id),
args.initial_message,
args.runtime_mode,
args.backend,
)
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(
call,
json!({
"member_ref": member_ref,
"session_id": member_ref.session_id(),
}),
)
}
TOOL_SPAWN_MANY_MEERKATS => {
let args: SpawnManyMeerkatsArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
let specs = args
.specs
.into_iter()
.map(|spec| {
SpawnMemberSpec::from_wire(
spec.profile,
spec.meerkat_id,
spec.initial_message,
spec.runtime_mode,
spec.backend,
)
})
.collect::<Vec<_>>();
let results = self.handle.spawn_many(specs).await;
let results = results
.into_iter()
.map(|result| match result {
Ok(member_ref) => json!({
"ok": true,
"member_ref": member_ref,
"session_id": member_ref.session_id(),
}),
Err(error) => json!({
"ok": false,
"error": error.to_string(),
}),
})
.collect::<Vec<_>>();
Self::encode_result(call, json!({ "results": results }))
}
TOOL_RETIRE_MEERKAT => {
let args: RetireMeerkatArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
self.handle
.retire(MeerkatId::from(args.meerkat_id))
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({"ok": true}))
}
TOOL_WIRE_PEERS => {
let args: WirePeersArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
self.handle
.wire(MeerkatId::from(args.a), MeerkatId::from(args.b))
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({"ok": true}))
}
TOOL_UNWIRE_PEERS => {
let args: WirePeersArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
self.handle
.unwire(MeerkatId::from(args.a), MeerkatId::from(args.b))
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({"ok": true}))
}
TOOL_LIST_MEERKATS => {
let meerkats = self.handle.list_members().await;
let meerkats = meerkats
.into_iter()
.map(|entry| {
json!({
"meerkat_id": entry.meerkat_id,
"profile": entry.profile,
"runtime_mode": entry.runtime_mode,
"member_ref": entry.member_ref,
"session_id": entry.session_id(),
"wired_to": entry.wired_to,
})
})
.collect::<Vec<_>>();
Self::encode_result(call, json!({ "meerkats": meerkats }))
}
TOOL_MOB_LIST_FLOWS => {
let flows = self.handle.list_flows();
Self::encode_result(call, json!({ "flows": flows }))
}
TOOL_MOB_RUN_FLOW => {
let args: RunFlowArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
let run_id = self
.handle
.run_flow(FlowId::from(args.flow_id), args.params)
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({ "run_id": run_id }))
}
TOOL_MOB_FLOW_STATUS => {
let args: FlowStatusArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
let run_id = args.run_id.parse::<RunId>().map_err(|error| {
ToolError::invalid_arguments(call.name, format!("invalid run_id: {error}"))
})?;
let run = self
.handle
.flow_status(run_id)
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({ "run": run }))
}
TOOL_MOB_CANCEL_FLOW => {
let args: FlowStatusArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
let run_id = args.run_id.parse::<RunId>().map_err(|error| {
ToolError::invalid_arguments(call.name, format!("invalid run_id: {error}"))
})?;
self.handle
.cancel_flow(run_id)
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({"ok": true}))
}
TOOL_MOB_TASK_CREATE => {
let args: TaskCreateArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
let task_id = self
.handle
.task_create(args.subject, args.description, args.blocked_by)
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({"ok": true, "task_id": task_id}))
}
TOOL_MOB_TASK_LIST => {
let tasks = self
.handle
.task_list()
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({ "tasks": tasks }))
}
TOOL_MOB_TASK_UPDATE => {
let args: TaskUpdateArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
self.handle
.task_update(args.task_id, args.status, args.owner.map(MeerkatId::from))
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({"ok": true}))
}
TOOL_MOB_TASK_GET => {
let args: TaskGetArgs = call
.parse_args()
.map_err(|error| ToolError::invalid_arguments(call.name, error.to_string()))?;
let task = self
.handle
.task_get(&args.task_id)
.await
.map_err(|error| Self::map_mob_error(call, error))?;
Self::encode_result(call, json!({ "task": task }))
}
_ => Err(ToolError::not_found(call.name)),
}
}
}
const TOOL_SPAWN_MEERKAT: &str = "spawn_meerkat";
const TOOL_SPAWN_MANY_MEERKATS: &str = "spawn_many_meerkats";
const TOOL_RETIRE_MEERKAT: &str = "retire_meerkat";
const TOOL_WIRE_PEERS: &str = "wire_peers";
const TOOL_UNWIRE_PEERS: &str = "unwire_peers";
const TOOL_LIST_MEERKATS: &str = "list_meerkats";
const TOOL_MOB_LIST_FLOWS: &str = "mob_list_flows";
const TOOL_MOB_RUN_FLOW: &str = "mob_run_flow";
const TOOL_MOB_FLOW_STATUS: &str = "mob_flow_status";
const TOOL_MOB_CANCEL_FLOW: &str = "mob_cancel_flow";
const TOOL_MOB_TASK_CREATE: &str = "mob_task_create";
const TOOL_MOB_TASK_LIST: &str = "mob_task_list";
const TOOL_MOB_TASK_UPDATE: &str = "mob_task_update";
const TOOL_MOB_TASK_GET: &str = "mob_task_get";