Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,25 @@ Behind the `openapi` Cargo feature. `OpenApiToolAdapter` parses an OpenAPI 3.0 s
- Actions: `get`, `set`, `list`, `remove`
- Does **not** touch the core agent loop — wired entirely through `SubAgentTool`

### Sub-Agent Multi-Provider Support
### Construction API

`SubAgentTool` supports any provider via `with_model_config()`. Without it, sub-agents default to Anthropic. For non-Anthropic providers (OpenAI, xAI, Groq, etc.), pass the appropriate `ModelConfig`:
The primary constructor is `Agent::from_config(ModelConfig)`: it selects the built-in provider from `config.api`, sets the model id from `config.id`, and resolves the API key from the provider-conventional env var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`, …; see `provider::resolve_api_key`). This removes the provider↔config pairing footgun and the doubly-specified model id.

```rust
let config = ModelConfig::xai("grok-3-mini-fast", "Grok 3 Mini Fast");
let sub = SubAgentTool::new("analyst", Arc::new(OpenAiCompatProvider))
.with_model(&config.id)
.with_api_key(&key)
.with_model_config(config);
// provider auto-selected, key from XAI_API_KEY
let agent = Agent::from_config(ModelConfig::xai("grok-4-1-fast", "Grok 4.1 Fast"));
```

Other constructors:
- `Agent::from_provider(provider, config)` — explicit provider (custom impls, test doubles). Pair with `ModelConfig::mock()` in tests.
- `Agent::from_config_with(&registry, config) -> Result<_, AgentBuildError>` — resolve against a custom `ProviderRegistry`.
- `Agent::set_model(config)` — switch model mid-session (re-resolves the env key; re-selects the provider only when it was registry-resolved, never clobbering an explicit one; explicit keys preserved).
- `Agent::new(provider)` + `with_model`/`with_model_config` — the original builder, still supported.

`SubAgentTool` mirrors these: `from_config`, `from_config_with`, `from_provider`.

`SubAgentTool` mirrors these: `SubAgentTool::from_config(name, config)` and `SubAgentTool::from_provider(name, provider, config)`.

`AgentLoopConfig` also supports `turn_delay: Option<Duration>` — an inter-turn delay to throttle API calls for rate-limit-sensitive providers. Exposed on `SubAgentTool` via `with_turn_delay()`.

### Testing
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ tokio = { version = "1", features = ["full"] }

```rust
use yoagent::agent::Agent;
use yoagent::provider::AnthropicProvider;
use yoagent::provider::ModelConfig;
use yoagent::types::*;

#[tokio::main]
async fn main() {
let mut agent = Agent::new(AnthropicProvider)
.with_system_prompt("You are a helpful assistant.")
.with_model("claude-sonnet-5")
.with_api_key(std::env::var("ANTHROPIC_API_KEY").unwrap());
// Provider is selected from the config's protocol; the key is read from
// ANTHROPIC_API_KEY. Call `.with_api_key(key)` to pass one explicitly.
let mut agent = Agent::from_config(ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5"))
.with_system_prompt("You are a helpful assistant.");

let mut rx = agent.prompt("What is Rust's ownership model?").await;

Expand Down Expand Up @@ -131,7 +131,7 @@ use yoagent::SkillSet;

let skills = SkillSet::load(&["./skills"])?;

let agent = Agent::new(AnthropicProvider)
let agent = Agent::from_config(ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5"))
.with_system_prompt("You are a coding assistant.")
.with_skills(skills) // Injects skill index into system prompt
.with_tools(tools);
Expand Down
23 changes: 10 additions & 13 deletions docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

```rust
use yoagent::{Agent, AgentEvent, StreamDelta};
use yoagent::provider::AnthropicProvider;
use yoagent::provider::ModelConfig;
use yoagent::tools::default_tools;

#[tokio::main]
async fn main() {
let mut agent = Agent::new(AnthropicProvider)
// The provider is selected from the config's protocol, and the API key is
// read from ANTHROPIC_API_KEY. Add `.with_api_key(key)` to override it.
let mut agent = Agent::from_config(ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5"))
.with_system_prompt("You are a helpful coding assistant.")
.with_model("claude-sonnet-5")
.with_api_key(std::env::var("ANTHROPIC_API_KEY").unwrap())
.with_tools(default_tools());

let mut rx = agent.prompt("List the files in the current directory").await;
Expand Down Expand Up @@ -45,19 +45,18 @@ async fn main() {

## Example with OpenAI-Compatible Provider

For OpenAI, xAI, Groq, DeepSeek, Mistral, MiniMax, Z.ai, Qwen, Ollama, or any compatible API, use `OpenAiCompatProvider` with a `ModelConfig`:
For OpenAI, xAI, Groq, DeepSeek, Mistral, MiniMax, Z.ai, Qwen, Ollama, or any compatible API, pass the matching `ModelConfig` preset — `from_config` picks the OpenAI-compatible provider and resolves the provider's env key automatically:

```rust
use yoagent::{Agent, AgentEvent};
use yoagent::provider::OpenAiCompatProvider;
use yoagent::provider::ModelConfig;
use yoagent::tools::default_tools;

#[tokio::main]
async fn main() {
let mut agent = Agent::new(OpenAiCompatProvider)
// Provider inferred from the preset; key from OPENAI_API_KEY.
let mut agent = Agent::from_config(ModelConfig::openai("gpt-5.5", "GPT-5.5"))
.with_system_prompt("You are a helpful assistant.")
.with_model("gpt-5.5")
.with_api_key(std::env::var("OPENAI_API_KEY").unwrap())
.with_tools(default_tools());

let mut rx = agent.prompt("What is 2 + 2?").await;
Expand All @@ -82,15 +81,13 @@ async fn main() {

```rust
use yoagent::{Agent, AgentEvent, StreamDelta};
use yoagent::provider::AnthropicProvider;
use yoagent::provider::ModelConfig;
use yoagent::tools::default_tools;

#[tokio::main]
async fn main() {
let mut agent = Agent::new(AnthropicProvider)
let mut agent = Agent::from_config(ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5"))
.with_system_prompt("You are a helpful assistant.")
.with_model("claude-sonnet-5")
.with_api_key(std::env::var("ANTHROPIC_API_KEY").unwrap())
.with_tools(default_tools());

let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
Expand Down
13 changes: 6 additions & 7 deletions examples/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
//! Run with: ANTHROPIC_API_KEY=sk-... cargo run --example basic

use yoagent::agent::Agent;
use yoagent::provider::AnthropicProvider;
use yoagent::provider::ModelConfig;
use yoagent::*;

#[tokio::main]
async fn main() {
let api_key = std::env::var("ANTHROPIC_API_KEY").expect("Set ANTHROPIC_API_KEY");

let mut agent = Agent::new(AnthropicProvider)
.with_system_prompt("You are a helpful assistant. Be concise.")
.with_model("claude-sonnet-5")
.with_api_key(api_key);
// The Anthropic provider is selected from the config's protocol, and the
// API key is read from ANTHROPIC_API_KEY. Add `.with_api_key(key)` to
// pass one explicitly.
let mut agent = Agent::from_config(ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5"))
.with_system_prompt("You are a helpful assistant. Be concise.");

println!("Sending prompt...");

Expand Down
4 changes: 1 addition & 3 deletions examples/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,8 @@ async fn main() {
}
}

let mut agent = Agent::new(provider)
let mut agent = Agent::from_provider(provider, yoagent::provider::ModelConfig::mock())
.with_system_prompt("You are helpful.")
.with_model("mock")
.with_api_key("test")
.with_tools(vec![Box::new(GreetTool)])
// Limit to 5 turns (plenty for this example)
.on_before_turn(|messages, turn| {
Expand Down
94 changes: 40 additions & 54 deletions examples/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

use std::io::{self, BufRead, Write};
use yoagent::agent::Agent;
use yoagent::provider::{AnthropicProvider, GoogleProvider, ModelConfig, OpenAiCompatProvider};
use yoagent::provider::ModelConfig;
use yoagent::skills::SkillSet;
use yoagent::tools::default_tools;
use yoagent::*;
Expand Down Expand Up @@ -126,20 +126,10 @@ async fn main() {
SkillSet::load(&skill_dirs).expect("Failed to load skills")
};

let mut agent = if let Some(ref url) = api_url {
if provider_name.as_deref() == Some("ollama") {
Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::ollama(url, &model))
} else {
Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::local(url, &model))
}
} else if let Some(ref prov) = provider_name {
make_provider_agent(prov, &model)
} else {
Agent::new(AnthropicProvider)
};
agent = agent
let mut agent = build_agent(&api_url, &provider_name, &model)
.with_system_prompt(SYSTEM_PROMPT)
.with_model(&model)
// from_config already resolves the provider-conventional env key; this
// override preserves the CLI's API_KEY fallback (empty = leave to env).
.with_api_key(&api_key)
.with_skills(skills.clone())
.with_tools(default_tools());
Expand Down Expand Up @@ -189,22 +179,8 @@ async fn main() {
}
s if s.starts_with("/model ") => {
let new_model = s.trim_start_matches("/model ").trim();
agent = if let Some(ref url) = api_url {
if provider_name.as_deref() == Some("ollama") {
Agent::new(OpenAiCompatProvider)
.with_model_config(ModelConfig::ollama(url, new_model))
} else {
Agent::new(OpenAiCompatProvider)
.with_model_config(ModelConfig::local(url, new_model))
}
} else if let Some(ref prov) = provider_name {
make_provider_agent(prov, new_model)
} else {
Agent::new(AnthropicProvider)
};
agent = agent
agent = build_agent(&api_url, &provider_name, new_model)
.with_system_prompt(SYSTEM_PROMPT)
.with_model(new_model)
.with_api_key(&api_key)
.with_skills(skills.clone())
.with_tools(default_tools());
Expand Down Expand Up @@ -334,36 +310,46 @@ async fn main() {
println!("\n{DIM} bye 👋{RESET}\n");
}

/// Select the config for the requested provider/URL and build an agent from
/// it. A local/OpenAI-compatible URL wins; then a named provider; else
/// Anthropic. Every branch flows through `from_config`, so the provider,
/// model id, and context window all come from a single `ModelConfig`.
fn build_agent(api_url: &Option<String>, provider_name: &Option<String>, model: &str) -> Agent {
if let Some(url) = api_url {
let config = if provider_name.as_deref() == Some("ollama") {
ModelConfig::ollama(url, model)
} else {
ModelConfig::local(url, model)
};
Agent::from_config(config)
} else if let Some(prov) = provider_name {
make_provider_agent(prov, model)
} else {
Agent::from_config(ModelConfig::anthropic(model, model))
}
}

fn make_provider_agent(provider: &str, model: &str) -> Agent {
match provider {
"zai" => Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::zai(model, model)),
"qwen" => {
Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::qwen(model, model))
}
"openai" => {
Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::openai(model, model))
}
"xai" => Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::xai(model, model)),
"groq" => {
Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::groq(model, model))
}
"deepseek" => {
Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::deepseek(model, model))
}
"mistral" => {
Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::mistral(model, model))
}
"minimax" => {
Agent::new(OpenAiCompatProvider).with_model_config(ModelConfig::minimax(model, model))
}
"ollama" => Agent::new(OpenAiCompatProvider)
.with_model_config(ModelConfig::ollama("http://localhost:11434/v1", model)),
"google" => Agent::new(GoogleProvider).with_model_config(ModelConfig::google(model, model)),
// One `from_config` per provider: the config carries the protocol (so the
// right provider is selected), the model id, context window, and pricing.
// No provider↔config pairing to get wrong, and no model id passed twice.
let config = match provider {
"zai" => ModelConfig::zai(model, model),
"qwen" => ModelConfig::qwen(model, model),
"openai" => ModelConfig::openai(model, model),
"xai" => ModelConfig::xai(model, model),
"groq" => ModelConfig::groq(model, model),
"deepseek" => ModelConfig::deepseek(model, model),
"mistral" => ModelConfig::mistral(model, model),
"minimax" => ModelConfig::minimax(model, model),
"ollama" => ModelConfig::ollama("http://localhost:11434/v1", model),
"google" => ModelConfig::google(model, model),
other => {
eprintln!("Unknown provider: {other}. Supported: zai, qwen, openai, xai, groq, deepseek, mistral, minimax, ollama, google.");
std::process::exit(1);
}
}
};
Agent::from_config(config)
}

fn truncate(s: &str, max: usize) -> &str {
Expand Down
12 changes: 4 additions & 8 deletions examples/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ use yoagent::types::*;
async fn main() {
// --- Phase 1: Initial conversation ---
let provider = MockProvider::text("The capital of France is Paris.");
let mut agent = Agent::new(provider)
.with_system_prompt("You are a helpful assistant.")
.with_model("mock")
.with_api_key("test");
let mut agent = Agent::from_provider(provider, yoagent::provider::ModelConfig::mock())
.with_system_prompt("You are a helpful assistant.");

println!("=== Phase 1: Initial conversation ===");
let mut rx = agent.prompt("What is the capital of France?").await;
Expand All @@ -42,10 +40,8 @@ async fn main() {

// --- Phase 2: Restore and continue ---
let provider2 = MockProvider::text("Paris is also known as the City of Light.");
let mut agent2 = Agent::new(provider2)
.with_system_prompt("You are a helpful assistant.")
.with_model("mock")
.with_api_key("test");
let mut agent2 = Agent::from_provider(provider2, yoagent::provider::ModelConfig::mock())
.with_system_prompt("You are a helpful assistant.");

agent2.restore_messages(&json).expect("Failed to restore");
println!(
Expand Down
24 changes: 10 additions & 14 deletions examples/sub_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,27 @@

use std::sync::Arc;
use yoagent::agent::Agent;
use yoagent::provider::{AnthropicProvider, StreamProvider};
use yoagent::provider::ModelConfig;
use yoagent::sub_agent::SubAgentTool;
use yoagent::tools;
use yoagent::*;

// Each construction resolves the Anthropic provider from the config's protocol
// and reads ANTHROPIC_API_KEY from the environment.
fn sonnet() -> ModelConfig {
ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5")
}

#[tokio::main]
async fn main() {
let api_key = std::env::var("ANTHROPIC_API_KEY").expect("Set ANTHROPIC_API_KEY");
let model = "claude-sonnet-5";
let provider: Arc<dyn StreamProvider> = Arc::new(AnthropicProvider);

// Sub-agent 1: a researcher with file reading tools
let researcher = SubAgentTool::new("researcher", Arc::clone(&provider))
let researcher = SubAgentTool::from_config("researcher", sonnet())
.with_description(
"Searches and reads files to gather information. Delegate research tasks here.",
)
.with_system_prompt(
"You are a research assistant. Read files and summarize findings concisely.",
)
.with_model(model)
.with_api_key(&api_key)
.with_tools(vec![
Arc::new(tools::ReadFileTool::new()),
Arc::new(tools::SearchTool::new()),
Expand All @@ -39,11 +39,9 @@ async fn main() {
.with_max_turns(10);

// Sub-agent 2: a coder with file editing tools
let coder = SubAgentTool::new("coder", Arc::clone(&provider))
let coder = SubAgentTool::from_config("coder", sonnet())
.with_description("Writes and edits code files. Delegate coding tasks here.")
.with_system_prompt("You are a coding assistant. Write clean, correct code. Be concise.")
.with_model(model)
.with_api_key(&api_key)
.with_tools(vec![
Arc::new(tools::ReadFileTool::new()),
Arc::new(tools::WriteFileTool::new()),
Expand All @@ -52,16 +50,14 @@ async fn main() {
.with_max_turns(15);

// Parent agent: coordinates between sub-agents
let mut agent = Agent::new(AnthropicProvider)
let mut agent = Agent::from_config(sonnet())
.with_system_prompt(
"You are a coordinator agent. You have two sub-agents:\n\
- 'researcher': for reading files and gathering information\n\
- 'coder': for writing and editing code\n\n\
Delegate tasks to the appropriate sub-agent. You can run both in parallel \
when the tasks are independent.",
)
.with_model(model)
.with_api_key(api_key)
.with_sub_agent(researcher)
.with_sub_agent(coder);

Expand Down
Loading
Loading