use futures::StreamExt;
use molo::agent::{Agent, AgentError};
use molo::memory::InMemoryMemory;
use molo::provider::{ChatRequest, FakeProvider, FakeReply, Provider, ProviderError};
use molo::tool::{SharedState, Tool, ToolError, ToolSchema};
use molo::{Memory, Message, ToolCall, ToolRegistry};
struct Echo;
#[async_trait::async_trait]
impl Tool for Echo {
fn schema(&self) -> ToolSchema {
ToolSchema {
name: "echo".into(),
description: "Returns the input text as-is.".into(),
parameters: serde_json::json!({}),
}
}
async fn call(
&self,
arguments: serde_json::Value,
_state: &SharedState,
) -> Result<String, ToolError> {
Ok(format!("echo: {arguments}"))
}
}
struct SimpleAgent {
provider: FakeProvider,
memory: InMemoryMemory,
tools: ToolRegistry,
max_tool_rounds: usize,
}
impl SimpleAgent {
fn new(provider: FakeProvider, tools: ToolRegistry) -> Self {
Self {
provider,
memory: InMemoryMemory::default(),
tools,
max_tool_rounds: 5,
}
}
}
#[async_trait::async_trait]
impl Agent for SimpleAgent {
async fn run(&mut self, input: &str) -> Result<String, AgentError> {
self.memory.record(Message::user(input)).await?;
let schemas = self.tools.schemas();
for _ in 0..self.max_tool_rounds {
let response = self
.provider
.chat(ChatRequest {
messages: self.memory.context().await?,
tools: schemas.clone(),
..Default::default()
})
.await?;
let Message::Assistant {
content,
tool_calls,
..
} = response.message
else {
unreachable!("the reply must be an Assistant message by contract")
};
if !content.is_empty() || !tool_calls.is_empty() {
self.memory
.record(Message::Assistant {
content: content.clone(),
reasoning: None,
tool_calls: tool_calls.clone(),
})
.await?;
}
if tool_calls.is_empty() {
return Ok(content); }
for call in tool_calls {
let content = self
.tools
.call(&call.name, &call.arguments, &SharedState::new())
.await
.unwrap_or_else(|e| e.to_string());
self.memory
.record(Message::ToolResult {
id: call.id,
content,
})
.await?;
}
}
Err(AgentError::TooManyToolRounds(self.max_tool_rounds))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let fake = FakeProvider::new([
FakeReply::ToolCalls {
content: String::new(),
calls: vec![ToolCall {
id: "c1".into(),
name: "echo".into(),
arguments: r#"{"text":"hello"}"#.into(),
}],
},
FakeReply::Text("Task complete".into()),
]);
let mut tools = ToolRegistry::new();
tools.register(Echo);
let mut agent = SimpleAgent::new(fake, tools);
let answer = agent.run("say something to echo").await?;
println!("1. tool round → final answer: {answer}");
let requests = agent.provider.requests();
println!("2. received {} chat requests in total:", requests.len());
for (i, request) in requests.iter().enumerate() {
let roles: Vec<&str> = request
.messages
.iter()
.map(|m| match m {
Message::System(_) => "system",
Message::User(_) => "user",
Message::Assistant { .. } => "assistant",
Message::ToolResult { .. } => "tool",
})
.collect();
println!(
" request {}: {} messages [{roles:?}]",
i + 1,
request.messages.len()
);
}
let tool_result_passed_back = requests[1]
.messages
.iter()
.any(|m| matches!(m, Message::ToolResult { content, .. } if content.contains("echo")));
println!(" → tool result fed back to the model: {tool_result_passed_back}");
let fake = FakeProvider::new([FakeReply::TextWithReasoning {
content: "answer".into(),
reasoning: "thinking".into(),
}]);
println!("3. stream_chat event stream:");
let mut stream = fake.stream_chat(ChatRequest::default()).await?;
while let Some(event) = stream.next().await {
println!(" {event:?}");
}
let fake = FakeProvider::new([FakeReply::Error(ProviderError::Api {
status: 429,
message: "rate limited".into(),
})]);
let mut agent = SimpleAgent::new(fake, ToolRegistry::new());
match agent.run("ask a question").await {
Err(AgentError::Provider(e)) => println!("4. error round: Agent propagated {e}"),
Ok(_) => println!("4. unexpected success (an error round should fail the run)"),
Err(e) => println!("4. unexpected error: {e}"),
}
let fake = FakeProvider::new([FakeReply::Text("only one round".into())]);
let mut agent = SimpleAgent::new(fake, ToolRegistry::new());
agent.run("first question").await?;
match agent.run("second question").await {
Err(AgentError::Provider(e)) => println!("5. script exhausted: {e}"),
Ok(_) => println!("5. unexpected success (script exhaustion should error)"),
Err(e) => println!("5. unexpected error: {e}"),
}
Ok(())
}