use molo::agent::{Agent, MessageChunk, ReActAgent, SubAgentPool};
use molo::provider::OpenAiProvider;
use molo::tool::{SharedState, Tool, ToolError, ToolRegistry, ToolSchema};
use futures::stream::StreamExt;
use schemars::JsonSchema;
use serde::Deserialize;
use std::io::Write;
#[derive(Debug, Deserialize, JsonSchema)]
struct SpawnArgs {
name: String,
system_prompt: String,
task: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct SendArgs {
name: String,
message: String,
}
struct SpawnAgent {
pool: SubAgentPool,
base_url: String,
api_key: String,
model: String,
}
#[async_trait::async_trait]
impl Tool for SpawnAgent {
fn schema(&self) -> ToolSchema {
ToolSchema {
name: "spawn_agent".into(),
description: "Creates and names a sub-agent and immediately runs the task; afterwards send_agent can continue the conversation by name"
.into(),
parameters: serde_json::to_value(schemars::schema_for!(SpawnArgs))
.expect("tool schema must serialize"),
}
}
async fn call(
&self,
arguments: serde_json::Value,
_state: &SharedState,
) -> Result<String, ToolError> {
let args: SpawnArgs = serde_json::from_value(arguments)?;
let reply = self
.pool
.spawn_react(
&args.name,
OpenAiProvider::new(
self.base_url.clone(),
self.api_key.clone(),
self.model.clone(),
),
ToolRegistry::new(),
&args.system_prompt,
&args.task,
)
.await
.map_err(ToolError::from)?;
Ok(reply)
}
}
struct SendAgent {
pool: SubAgentPool,
}
#[async_trait::async_trait]
impl Tool for SendAgent {
fn schema(&self) -> ToolSchema {
ToolSchema {
name: "send_agent".into(),
description: "Hands a new task to a previously created sub-agent (by name); it answers with its previous memory; if the name does not exist, spawn_agent first".into(),
parameters: serde_json::to_value(schemars::schema_for!(SendArgs))
.expect("tool schema must serialize"),
}
}
async fn call(
&self,
arguments: serde_json::Value,
_state: &SharedState,
) -> Result<String, ToolError> {
let args: SendArgs = serde_json::from_value(arguments)?;
self.pool
.send(&args.name, &args.message)
.await
.map_err(ToolError::from)
}
}
struct ListAgents {
pool: SubAgentPool,
}
#[async_trait::async_trait]
impl Tool for ListAgents {
fn schema(&self) -> ToolSchema {
ToolSchema {
name: "list_agents".into(),
description: "Lists the names of all created sub-agents".into(),
parameters: serde_json::json!({ "type": "object", "properties": {} }),
}
}
async fn call(
&self,
_arguments: serde_json::Value,
_state: &SharedState,
) -> Result<String, ToolError> {
let names = self.pool.names().await;
Ok(names.join(", "))
}
}
enum Mode {
Stream,
Chat,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let base_url =
std::env::var("MOLO_BASE_URL").unwrap_or_else(|_| "https://api.openai.com/v1".to_string());
let api_key = std::env::var("MOLO_API_KEY").unwrap_or_default();
let model = std::env::var("MOLO_MODEL").unwrap_or_else(|_| "gpt-4o-mini".to_string());
let mode = match std::env::args().nth(1).as_deref() {
Some("chat") => Mode::Chat,
_ => Mode::Stream,
};
let pool = SubAgentPool::new();
let mut registry = ToolRegistry::new();
registry.register(SpawnAgent {
pool: pool.clone(),
base_url: base_url.clone(),
api_key: api_key.clone(),
model: model.clone(),
});
registry.register(SendAgent { pool: pool.clone() });
registry.register(ListAgents { pool });
let mut agent = ReActAgent::new(
OpenAiProvider::new(base_url, api_key, model),
registry,
"You are the main agent. Delegate sub-agents when specialized capability \
is needed: create with spawn_agent (give a name and a task); when the \
user mentions @name or a follow-up is needed, use send_agent; when \
unsure of a name, list_agents first.",
);
let mut input = String::new();
loop {
input.clear();
println!("user:");
let read = std::io::stdin().read_line(&mut input)?;
if read == 0 {
break; }
let input = input.trim();
if input.is_empty() {
continue;
}
if input == "exit" || input == "quit" {
break;
}
match mode {
Mode::Stream => {
let mut stream = agent.run_stream(input).await?;
let mut prefix_printed = false;
while let Some(event) = stream.next().await {
match event? {
MessageChunk::Delta(delta) => {
if !prefix_printed {
print!("assistant: ");
std::io::stdout().flush()?;
prefix_printed = true;
}
print!("{delta}");
std::io::stdout().flush()?;
}
MessageChunk::ToolCall {
id,
name,
arguments,
} => {
println!("\n → calling {name}(#{id}), arguments: {arguments}");
}
MessageChunk::ToolResult { id, name, content } => {
println!(" → {name}(#{id}) returned: {content}");
}
MessageChunk::Done(_) => break,
MessageChunk::Cancelled => {
println!("\n[cancelled]");
break;
}
_ => {}
}
}
println!();
}
Mode::Chat => {
let answer = agent.run(input).await?;
if !answer.is_empty() {
println!("assistant: {answer}");
}
}
}
}
Ok(())
}