pub struct SubAgentTool { /* private fields */ }Expand description
A sub-agent as a tool: wrap another reasoning loop as a Tool; once
registered into the main loop’s
ToolRegistry, the model can delegate on
demand during conversation.
Two forms, chosen by conversation need:
from_agent: persistent — holds the sub-agent instance and continues the same conversation across calls (context accumulates), suited to repeatedly consulting a “resident expert”;from_factory: dynamic — each call creates a transient sub-agent through the factory, discarded when done, suited to one-shot delegation; the factory sees this call’s arguments and can freely assemble the sub-agent (type / tool subset / system prompt).
The sub-agent only needs to implement the Agent trait and be Send.
On call, the model’s arguments are passed to the sub-agent as their JSON
text form, and the sub-agent’s answer text is returned as the tool result
to the main loop. Calls to the same tool instance are serialized
(internal lock); different tool instances run in parallel.
Cancellation semantics: a sub-agent run receives no cancellation signal (tool calls have no cancellation parameter); when the main loop is cancelled, unfinished sub-agent calls abort as their future is dropped (non-cooperative), and already-recorded messages are kept.
§Examples
Any type implementing the Agent trait can be a sub-agent — this
example uses a minimal application-written type (independent of the
built-in loops):
use molo::agent::{Agent, AgentError, SubAgentTool};
use molo::tool::{SharedState, Tool, ToolContext};
use molo::{Message, RunContext, RunMetadata, RunOutput, RunRequest, RunSummary};
use serde_json::json;
/// Demo sub-agent: echoes the input back verbatim.
struct Echo;
#[molo::async_trait]
impl Agent for Echo {
async fn run_request_with_context(
&mut self,
request: RunRequest,
context: RunContext,
) -> Result<RunOutput, AgentError> {
let answer = format!("echo: {}", input_text(request));
Ok(run_output(context.run_id, answer))
}
}
fn input_text(request: RunRequest) -> String {
request.input.as_text().unwrap_or("").to_string()
}
fn run_output(run_id: String, answer: String) -> RunOutput {
RunOutput {
run_id,
answer: answer.clone(),
summary: RunSummary::default(),
final_message: Message::assistant(answer),
artifacts: Vec::new(),
metadata: RunMetadata::new(),
}
}
let tool = SubAgentTool::from_agent(
"echo",
"Hand the content to the echo sub-agent",
json!({ "type": "object", "properties": { "message": { "type": "string" } } }),
Box::new(Echo),
);
let run = RunContext::new("sub-agent-doc");
let state = SharedState::default();
let result = tool
.call(
json!({ "message": "Hello" }),
ToolContext::new(&run, &state, "call-sub-agent", "echo"),
)
.await?;
assert_eq!(result, "echo: {\"message\":\"Hello\"}");Implementations§
Source§impl SubAgentTool
impl SubAgentTool
Sourcepub fn from_agent(
name: impl Into<String>,
description: impl Into<String>,
parameters: Value,
agent: Box<dyn Agent + Send>,
) -> Self
pub fn from_agent( name: impl Into<String>, description: impl Into<String>, parameters: Value, agent: Box<dyn Agent + Send>, ) -> Self
Persistent form: holds the sub-agent instance and continues the same conversation across calls.
§Parameters
name/description: the name and purpose exposed to the model (the model’s basis for choosing the tool);parameters: the JSON Schema for the sub-task input (prefer generating it withschemars::schema_for!from a serde struct);agent: the sub-agent instance (implements theAgenttrait and isSend).
Sourcepub fn from_factory(
name: impl Into<String>,
description: impl Into<String>,
parameters: Value,
factory: impl Fn(Value) -> Result<Box<dyn Agent + Send>, ToolError> + Send + Sync + 'static,
) -> Self
pub fn from_factory( name: impl Into<String>, description: impl Into<String>, parameters: Value, factory: impl Fn(Value) -> Result<Box<dyn Agent + Send>, ToolError> + Send + Sync + 'static, ) -> Self
Dynamic form: each call creates a transient sub-agent through the factory, discarded when done.
The factory receives this call’s arguments (JSON) and can assemble
the sub-agent accordingly — choosing the type, trimming the tool
subset, or writing a system prompt; failing to parse the arguments
returns ToolError::InvalidArguments.
§Parameters
name/description: the name and purpose exposed to the model (the model’s basis for choosing the tool);parameters: the JSON Schema for the sub-task input (prefer generating it withschemars::schema_for!from a serde struct);factory: the factory run on each call, returning the new sub-agent.
Sourcepub fn from_react(
name: impl Into<String>,
description: impl Into<String>,
provider: impl Provider + Clone + 'static,
tools: ToolRegistry,
parameters: Value,
) -> Self
pub fn from_react( name: impl Into<String>, description: impl Into<String>, provider: impl Provider + Clone + 'static, tools: ToolRegistry, parameters: Value, ) -> Self
Convenience form: each call builds a standard ReAct sub-agent from
the system_prompt / task fields in the model’s arguments — the
model (main agent) defines the sub-agent’s system prompt and task at
call time, no hand-written factory needed.
The parameters schema should include two fields (both degrade
gracefully when missing): system_prompt (the sub-agent’s system
prompt, empty when missing) and task (the sub-agent’s task, empty
input when missing). What’s passed to the sub-agent as input is the
text of the task field (not the whole argument JSON).
§Parameters
name/description: the name and purpose exposed to the model (the model’s basis for choosing the tool);provider: the sub-agent’s provider (must beClone— every call builds an independent loop); sharing one provider instance between the main and sub agents is fine;tools: the sub-agent’s tool set;parameters: the JSON Schema for the sub-task input.
§Examples
use molo::agent::SubAgentTool;
use molo::provider::{FakeProvider, FakeReply};
use molo::RunContext;
use molo::tool::{SharedState, Tool, ToolContext, ToolRegistry};
use serde_json::json;
let fake = FakeProvider::new([FakeReply::Text("sub answer".into())]);
let tool = SubAgentTool::from_react(
"delegate",
"Delegate a sub-task, defining the sub-agent's system prompt and task",
fake,
ToolRegistry::new(),
json!({ "type": "object", "properties": {
"system_prompt": { "type": "string" },
"task": { "type": "string" },
} }),
);
// The model defines the system prompt and task → a new sub-agent is
// created and run
let run = RunContext::new("sub-agent-doc");
let state = SharedState::default();
let result = tool
.call(
json!({ "system_prompt": "You are a reviewer", "task": "Review this code" }),
ToolContext::new(&run, &state, "call-sub-agent", "delegate"),
)
.await?;
assert_eq!(result, "sub answer");