use minillmlib::{
ArgumentStream, ChatNode, CompletionParameters, GeneratorInfo, NodeCompletionParameters,
ToolDefinition,
};
use std::collections::HashMap;
use std::io::Write;
const MODEL: &str = "google/gemini-2.5-flash-lite";
const MAX_TURNS: usize = 8;
struct DraftNotesSession {
call_id: String,
args: ArgumentStream,
consumer: tokio::task::JoinHandle<usize>,
}
impl DraftNotesSession {
fn start(call_id: &str) -> Self {
println!("\n[draft_notes {call_id}] started, receiving decoded payload live:");
let mut args = ArgumentStream::lenient();
let mut content = args.field("content");
let consumer = tokio::spawn(async move {
let mut bytes = 0;
while let Some(text) = content.delta().await {
print!("{text}");
std::io::stdout().flush().ok();
bytes += text.len();
}
bytes
});
Self {
call_id: call_id.to_string(),
args,
consumer,
}
}
fn feed(&mut self, fragment: &str) -> minillmlib::Result<()> {
self.args.feed(fragment)
}
async fn finish(mut self) -> String {
if let Err(e) = self.args.finish() {
eprintln!("\n[draft_notes {}] malformed call: {e}", self.call_id);
}
let bytes = self.consumer.await.unwrap_or(0);
println!(
"\n[draft_notes {}] done ({} decoded bytes streamed)",
self.call_id, bytes
);
"notes saved".to_string()
}
}
fn get_weather(args: &serde_json::Value) -> String {
format!(
"15 degrees and sunny in {}",
args["city"].as_str().unwrap_or("unknown")
)
}
fn tool_params() -> NodeCompletionParameters {
NodeCompletionParameters::new().with_params(
CompletionParameters::new()
.with_max_tokens(600)
.with_tool(ToolDefinition::new(
"get_weather",
"Get the current weather for a city",
serde_json::json!({
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"],
}),
))
.with_tool(ToolDefinition::new(
"draft_notes",
"Save working notes. Use it to record your findings.",
serde_json::json!({
"type": "object",
"properties": { "content": { "type": "string" } },
"required": ["content"],
}),
)),
)
}
#[tokio::main]
async fn main() -> minillmlib::Result<()> {
minillmlib::init();
let gen = GeneratorInfo::openrouter(MODEL);
let params = tool_params();
let root = ChatNode::root(
"You are a helpful assistant. When asked about weather, use get_weather. \
Record what you learned with draft_notes before giving your final answer.",
);
let mut node = root.add_user("What's the weather in Paris and in Lyon? Then note it down.");
for turn in 1..=MAX_TURNS {
println!("\n=== model turn {turn} ===");
let mut stream = node.complete_streaming(&gen, Some(¶ms)).await?;
let mut streaming: HashMap<u64, DraftNotesSession> = HashMap::new();
while let Some(chunk) = stream.next_chunk().await {
let chunk = chunk?;
if !chunk.delta.is_empty() {
print!("{}", chunk.delta);
std::io::stdout().flush().ok();
}
for delta in chunk.tool_calls.as_deref().unwrap_or_default() {
if delta.name.as_deref() == Some("draft_notes") {
let id = delta.id.as_deref().unwrap_or_default();
streaming.insert(delta.index, DraftNotesSession::start(id));
}
if let Some(frag) = &delta.arguments_fragment {
if let Some(session) = streaming.get_mut(&delta.index) {
session.feed(frag)?;
}
}
}
}
let response = stream.collect().await?;
let assistant = node.append_response(&response);
let Some(calls) = assistant.tool_calls() else {
println!("\n\n=== final answer delivered in {turn} turn(s) ===");
return Ok(());
};
let mut sessions: HashMap<String, DraftNotesSession> = streaming
.into_values()
.map(|s| (s.call_id.clone(), s))
.collect();
let mut current = assistant;
for call in &calls {
let output = match call.name.as_str() {
"draft_notes" => match sessions.remove(&call.id) {
Some(session) => session.finish().await,
None => "notes saved".to_string(),
},
"get_weather" => get_weather(&call.arguments_json()?),
other => format!("Error: unknown tool '{other}'"),
};
println!("\n[{}] -> {}", call.name, output);
current = current.add_tool_result(&call.id, output);
}
node = current;
}
Err(minillmlib::MiniLLMError::InvalidParameter(format!(
"agent did not finish within {MAX_TURNS} turns"
)))
}