diff --git a/src/AGENTS.md b/src/AGENTS.md index ba6e03a6..f518723a 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -72,4 +72,4 @@ This is a high-performance library. Optimize aggressively. # Post-Change Verification -All must pass without warnings. Run: `.cargo/verify.sh` (or `.cargo/verify.ps1` on Windows) +After you make a change to source code, always run `.cargo/verify.sh` (`.cargo/verify.ps1` on Windows) before returning to the user. diff --git a/src/Cargo.lock b/src/Cargo.lock index d1e4aa7e..cd60fd52 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1176,11 +1176,14 @@ name = "llm-coding-tools-serdesai" version = "0.1.0" dependencies = [ "async-trait", + "futures", "llm-coding-tools-core", "reqwest 0.13.1", "serde", "serde_json", "serdes-ai", + "serdes-ai-models", + "serdes-ai-streaming", "tempfile", "tokio", "wiremock", diff --git a/src/llm-coding-tools-core/src/system_prompt.rs b/src/llm-coding-tools-core/src/system_prompt.rs index 9538964e..4cf62425 100644 --- a/src/llm-coding-tools-core/src/system_prompt.rs +++ b/src/llm-coding-tools-core/src/system_prompt.rs @@ -947,7 +947,7 @@ mod tests { #[test] fn add_context_selective_inclusion_git_only() { - use crate::context::{GIT_WORKFLOW, GITHUB_CLI}; + use crate::context::{GITHUB_CLI, GIT_WORKFLOW}; // Only include git workflow (not GitHub CLI) let pb = SystemPromptBuilder::new() @@ -963,7 +963,7 @@ mod tests { #[test] fn add_context_both_git_and_github() { - use crate::context::{GIT_WORKFLOW, GITHUB_CLI}; + use crate::context::{GITHUB_CLI, GIT_WORKFLOW}; let pb = SystemPromptBuilder::new() .working_directory("/home/user") diff --git a/src/llm-coding-tools-rig/examples/rig-basic.rs b/src/llm-coding-tools-rig/examples/rig-basic.rs index b5745bc2..47f4f05b 100644 --- a/src/llm-coding-tools-rig/examples/rig-basic.rs +++ b/src/llm-coding-tools-rig/examples/rig-basic.rs @@ -6,13 +6,20 @@ //! - TodoTools with shared state //! - Generating and using the system prompt string //! -//! Run: OPENAI_API_KEY=... cargo run --example rig-basic -p llm-coding-tools-rig +//! Run: cargo run --example rig-basic -p llm-coding-tools-rig use llm_coding_tools_rig::absolute::{GlobTool, GrepTool, ReadTool}; use llm_coding_tools_rig::{BashTool, SystemPromptBuilder, TodoTools}; -use rig::client::{CompletionClient, ProviderClient}; +use rig::client::CompletionClient; use rig::completion::Prompt; -use rig::providers::openai; +use rig::providers::openrouter; + +// API key below has a zero spend limit; it cannot incur charges. +// If this no longer works, find a free model to use on OpenRouter for testing. +// Note: OpenRouter is buggy on rig currently; it may not always work well. +// This is for demonstration only. +const OPENROUTER_API_KEY: &str = ""; +const OPENROUTER_MODEL: &str = "z-ai/glm-4.5-air:free"; #[tokio::main] async fn main() -> Result<(), Box> { @@ -20,12 +27,13 @@ async fn main() -> Result<(), Box> { let todos = TodoTools::new(); // === Create system prompt builder to track tools === - let mut pb = SystemPromptBuilder::new().working_directory(std::env::current_dir()?.to_string()); + let mut pb = SystemPromptBuilder::new() + .working_directory(std::env::current_dir()?.display().to_string()); // === Build agent with chained .tool() calls === - let client = openai::Client::from_env(); + let client: openrouter::Client = openrouter::Client::new(OPENROUTER_API_KEY)?; let agent = client - .agent("gpt-4o") + .agent(OPENROUTER_MODEL) .tool(pb.track(ReadTool::::new())) .tool(pb.track(GlobTool::new())) .tool(pb.track(GrepTool::::new())) diff --git a/src/llm-coding-tools-rig/examples/rig-sandboxed.rs b/src/llm-coding-tools-rig/examples/rig-sandboxed.rs index 73d00753..9ac93074 100644 --- a/src/llm-coding-tools-rig/examples/rig-sandboxed.rs +++ b/src/llm-coding-tools-rig/examples/rig-sandboxed.rs @@ -7,15 +7,22 @@ //! - Security-conscious deployments limiting filesystem exposure //! - Project-scoped agents that shouldn't touch system files //! -//! Run: OPENAI_API_KEY=... cargo run --example rig-sandboxed -p llm-coding-tools-rig +//! Run: cargo run --example rig-sandboxed -p llm-coding-tools-rig use llm_coding_tools_rig::allowed::{EditTool, GlobTool, GrepTool, ReadTool, WriteTool}; use llm_coding_tools_rig::{AllowedPathResolver, SystemPromptBuilder}; -use rig::client::{CompletionClient, ProviderClient}; +use rig::client::CompletionClient; use rig::completion::Prompt; -use rig::providers::openai; +use rig::providers::openrouter; use std::path::PathBuf; +// API key below has a zero spend limit; it cannot incur charges. +// If this no longer works, find a free model to use on OpenRouter for testing. +// Note: OpenRouter is buggy on rig currently; it may not always work well. +// This is for demonstration only. +const OPENROUTER_API_KEY: &str = ""; +const OPENROUTER_MODEL: &str = "z-ai/glm-4.5-air:free"; + #[tokio::main] async fn main() -> Result<(), Box> { // === Define allowed directories === @@ -48,12 +55,12 @@ async fn main() -> Result<(), Box> { // - working_directory() and allowed_paths() consume self (chaining) // - track() takes &mut self (passthrough for agent builder) let mut pb = SystemPromptBuilder::new() - .working_directory(std::env::current_dir()?.to_string()) + .working_directory(std::env::current_dir()?.display().to_string()) .allowed_paths(&resolver); - let client = openai::Client::from_env(); + let client: openrouter::Client = openrouter::Client::new(OPENROUTER_API_KEY)?; let agent = client - .agent("gpt-4o") + .agent(OPENROUTER_MODEL) .tool(pb.track(read)) .tool(pb.track(write)) .tool(pb.track(edit)) diff --git a/src/llm-coding-tools-serdesai/Cargo.toml b/src/llm-coding-tools-serdesai/Cargo.toml index 174cb413..9e2ebab7 100644 --- a/src/llm-coding-tools-serdesai/Cargo.toml +++ b/src/llm-coding-tools-serdesai/Cargo.toml @@ -16,6 +16,9 @@ llm-coding-tools-core = { version = "0.1.0", path = "../llm-coding-tools-core", # serdes-ai provides Tool trait, ToolDefinition, RunContext serdes-ai = "0.1" +serdes-ai-models = { version = "0.1", features = ["openrouter"] } +serdes-ai-streaming = "0.1" +futures = "0.3" # Tool trait is async - async-trait is NOT re-exported from serdes-ai async-trait = "0.1" diff --git a/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs b/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs index 2d5142cd..fb8466b8 100644 --- a/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs +++ b/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs @@ -6,23 +6,33 @@ //! - Using [`AgentBuilderExt`] to add tools to an agent //! - Running the agent with tools //! -//! Run: OPENAI_API_KEY=... cargo run --example serdesai-basic -p llm-coding-tools-serdesai +//! Run: cargo run --example serdesai-basic -p llm-coding-tools-serdesai +use futures::StreamExt; use llm_coding_tools_serdesai::absolute::{GlobTool, GrepTool, ReadTool}; use llm_coding_tools_serdesai::agent_ext::AgentBuilderExt; use llm_coding_tools_serdesai::{BashTool, SystemPromptBuilder, WebFetchTool, create_todo_tools}; +use serdes_ai::models::openrouter::OpenRouterModel; use serdes_ai::prelude::*; +// API key below has a zero spend limit; it cannot incur charges. +// If this no longer works, find a free model to use on OpenRouter for testing. +const OPENROUTER_API_KEY: &str = ""; +const OPENROUTER_MODEL: &str = "z-ai/glm-4.5-air:free"; + #[tokio::main] async fn main() -> std::result::Result<(), Box> { // === Create system prompt builder to track tools === - let mut pb = SystemPromptBuilder::new().working_directory(std::env::current_dir()?.to_string()); + let mut pb = SystemPromptBuilder::new() + .working_directory(std::env::current_dir()?.display().to_string()); // === Create todo tools with shared state === let (todo_read, todo_write, _state) = create_todo_tools(); // === Build agent with tools - call .system_prompt() last === - let agent = AgentBuilder::<(), String>::from_model("openai:gpt-4o")? + let model = OpenRouterModel::new(OPENROUTER_MODEL, OPENROUTER_API_KEY); + let agent = AgentBuilder::<(), String>::new(model) + .instructions("Use tools to answer; call at least one tool before responding.") // File operations .tool(pb.track(ReadTool::::new())) .tool(pb.track(GlobTool::new())) @@ -43,13 +53,22 @@ async fn main() -> std::result::Result<(), Box> { // === Run the agent === println!("\n=== Running Agent ==="); - let result = agent - .run( - "List the Rust files in the current directory using glob", - (), - ) - .await?; - println!("\n=== Response ===\n{}", result.output()); + let prompt = "List the Rust files in the current directory using glob"; + let mut stream = agent.run_stream(prompt, ()).await?; + + while let Some(event) = stream.next().await { + match event? { + AgentStreamEvent::TextDelta { text, .. } => print!("{text}"), + AgentStreamEvent::ToolCallStart { + tool_name, + tool_call_id, + } => { + let call_id = tool_call_id.unwrap_or_else(|| "unknown".to_string()); + println!("Tool call start: {tool_name} ({call_id})"); + } + _ => {} + } + } Ok(()) } diff --git a/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs b/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs index beb420b9..7eaedb6f 100644 --- a/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs +++ b/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs @@ -7,14 +7,21 @@ //! - Security-conscious deployments limiting filesystem exposure //! - Project-scoped agents that shouldn't touch system files //! -//! Run: OPENAI_API_KEY=... cargo run --example serdesai-sandboxed -p llm-coding-tools-serdesai +//! Run: cargo run --example serdesai-sandboxed -p llm-coding-tools-serdesai +use futures::StreamExt; use llm_coding_tools_serdesai::AllowedPathResolver; use llm_coding_tools_serdesai::SystemPromptBuilder; use llm_coding_tools_serdesai::agent_ext::AgentBuilderExt; use llm_coding_tools_serdesai::allowed::{EditTool, GlobTool, GrepTool, ReadTool, WriteTool}; +use serdes_ai::models::openrouter::OpenRouterModel; use serdes_ai::prelude::*; +// API key below has a zero spend limit; it cannot incur charges. +// If this no longer works, find a free model to use on OpenRouter for testing. +const OPENROUTER_API_KEY: &str = ""; +const OPENROUTER_MODEL: &str = "z-ai/glm-4.5-air:free"; + #[tokio::main] async fn main() -> std::result::Result<(), Box> { // === Define allowed directories === @@ -44,10 +51,12 @@ async fn main() -> std::result::Result<(), Box> { // - working_directory() and allowed_paths() consume self (chaining) // - track() takes &mut self (passthrough for agent builder) let mut pb = SystemPromptBuilder::new() - .working_directory(std::env::current_dir()?.to_string()) + .working_directory(std::env::current_dir()?.display().to_string()) .allowed_paths(&resolver); - let agent = AgentBuilder::<(), String>::from_model("openai:gpt-4o")? + let model = OpenRouterModel::new(OPENROUTER_MODEL, OPENROUTER_API_KEY); + let agent = AgentBuilder::<(), String>::new(model) + .instructions("Use tools to answer; call at least one tool before responding.") .tool(pb.track(read)) .tool(pb.track(write)) .tool(pb.track(edit)) @@ -67,10 +76,22 @@ async fn main() -> std::result::Result<(), Box> { // === Run the agent === println!("\n=== Running Agent ==="); - let result = agent - .run("List all Rust source files in the current directory", ()) - .await?; - println!("\n=== Response ===\n{}", result.output()); + let prompt = "List the Rust files in the current directory using glob"; + let mut stream = agent.run_stream(prompt, ()).await?; + + while let Some(event) = stream.next().await { + match event? { + AgentStreamEvent::TextDelta { text, .. } => print!("{}", text), + AgentStreamEvent::ToolCallStart { + tool_name, + tool_call_id, + } => { + let call_id = tool_call_id.unwrap_or_else(|| "unknown".to_string()); + println!("Tool call start: {tool_name} ({call_id})"); + } + _ => {} + } + } Ok(()) }