Remove ProfileCapabilities, rename ProviderProfile to AgentProfile, fix layer separation

Move model facts (knowledge_cutoff, context_window) to fabro-model catalog as
source of truth. Move request-shaping (auto-thinking, 1M beta headers, Gemini
safety settings) into fabro-llm adapters. Delete ProfileCapabilities struct and
all dead code (supports_reasoning, supports_streaming, supports_parallel_tool_calls,
OpenAiProfile.reasoning_effort). Fix "powered by OpenAI" mislabeling for
Kimi/ZAI/Minimax/Inception providers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-23 12:38:05 -04:00
parent c7703fc9a0
commit 57d0317d7f
No known key found for this signature in database
19 changed files with 224 additions and 346 deletions

View file

@ -40,7 +40,7 @@ User Input
### Key Components
- **`Session`** -- Manages the full agentic loop: LLM calls, tool execution, steering, follow-ups, abort handling, and event emission.
- **`ProviderProfile`** (trait) -- Defines how to build system prompts, which tools to register, and what capabilities a provider supports. Ships with `AnthropicProfile`, `OpenAiProfile`, and `GeminiProfile`.
- **`AgentProfile`** (trait) -- Defines how to build system prompts, which tools to register, and what capabilities a provider supports. Ships with `AnthropicProfile`, `OpenAiProfile`, and `GeminiProfile`.
- **`Sandbox`** (trait) -- Abstracts filesystem, shell, grep, and glob operations. `LocalSandbox` provides a real implementation; the trait enables sandboxing and testing.
- **`ToolRegistry`** -- Maps tool names to definitions and async executor functions. Tools are registered per-profile.
- **`History`** -- Ordered list of `Turn` variants (`User`, `Assistant`, `ToolResults`, `System`, `Steering`) that converts to LLM messages.
@ -54,10 +54,10 @@ User Input
The main entry point. Created with an LLM client, a provider profile, a sandbox, and a config.
### `ProviderProfile`
### `AgentProfile`
```rust
pub trait ProviderProfile: Send + Sync {
pub trait AgentProfile: Send + Sync {
fn id(&self) -> String;
fn model(&self) -> String;
fn tool_registry(&self) -> &ToolRegistry;
@ -68,7 +68,7 @@ pub trait ProviderProfile: Send + Sync {
project_docs: &[String],
user_instructions: Option<&str>,
) -> String;
fn capabilities(&self) -> ProfileCapabilities;
fn capabilities(&self) -> AgentProfile;
fn knowledge_cutoff(&self) -> &str;
// ... default methods for tools(), provider_options(), supports_*()
}

View file

@ -7,18 +7,10 @@ use crate::subagent::{
};
use crate::tool_registry::ToolRegistry;
use fabro_llm::types::ToolDefinition;
use fabro_model::Provider;
use fabro_model::{Catalog, Provider};
use std::sync::Arc;
/// Static capabilities of a provider profile.
pub struct ProfileCapabilities {
pub supports_reasoning: bool,
pub supports_streaming: bool,
pub supports_parallel_tool_calls: bool,
pub context_window_size: usize,
}
pub trait ProviderProfile: Send + Sync {
pub trait AgentProfile: Send + Sync {
fn provider(&self) -> Provider;
fn model(&self) -> &str;
fn tool_registry(&self) -> &ToolRegistry;
@ -31,31 +23,22 @@ pub trait ProviderProfile: Send + Sync {
user_instructions: Option<&str>,
skills: &[Skill],
) -> String;
fn capabilities(&self) -> ProfileCapabilities;
fn knowledge_cutoff(&self) -> &str;
fn tools(&self) -> Vec<ToolDefinition> {
self.tool_registry().definitions()
}
fn provider_options(&self) -> Option<serde_json::Value> {
None
}
fn supports_reasoning(&self) -> bool {
self.capabilities().supports_reasoning
}
fn supports_streaming(&self) -> bool {
self.capabilities().supports_streaming
}
fn supports_parallel_tool_calls(&self) -> bool {
self.capabilities().supports_parallel_tool_calls
fn knowledge_cutoff(&self) -> Option<String> {
Catalog::builtin()
.get(self.model())
.and_then(|m| m.knowledge_cutoff().map(str::to_string))
}
fn context_window_size(&self) -> usize {
self.capabilities().context_window_size
Catalog::builtin()
.get(self.model())
.map(|m| m.context_window() as usize)
.unwrap_or(200_000)
}
fn register_subagent_tools(
@ -92,11 +75,8 @@ mod tests {
}
#[test]
fn profile_capabilities() {
fn profile_context_window_defaults() {
let profile = TestProfile::new();
assert!(!profile.supports_reasoning());
assert!(!profile.supports_streaming());
assert!(!profile.supports_parallel_tool_calls());
assert_eq!(profile.context_window_size(), 200_000);
}
@ -119,12 +99,6 @@ mod tests {
assert!(prompt.contains("Always use TDD"));
}
#[test]
fn profile_provider_options_none() {
let profile = TestProfile::new();
assert!(profile.provider_options().is_none());
}
#[test]
fn profile_tools_empty_registry() {
let profile = TestProfile::new();

View file

@ -1,7 +1,7 @@
use crate::config::ToolApprovalFn;
use crate::{
subagent::{SessionFactory, SubAgentManager},
AgentEvent, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, ProviderProfile,
AgentEvent, AgentProfile, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile,
Session, SessionConfig, Turn,
};
use clap::{Args, Parser};
@ -188,7 +188,7 @@ fn build_profile(
provider: Provider,
model: &str,
llm_client: Option<Client>,
) -> Box<dyn ProviderProfile> {
) -> Box<dyn AgentProfile> {
let summarizer = build_summarizer(provider, llm_client);
match provider {
Provider::OpenAi => Box::new(OpenAiProfile::with_summarizer(model, summarizer)),
@ -436,7 +436,7 @@ pub async fn run_with_args_and_client(
let factory_hooks = config.tool_hooks.clone();
let factory: SessionFactory = Arc::new(move || {
let child_summarizer = build_summarizer(provider, Some(factory_client.clone()));
let child_profile: Arc<dyn ProviderProfile> = match provider {
let child_profile: Arc<dyn AgentProfile> = match provider {
Provider::OpenAi => Arc::new(OpenAiProfile::with_summarizer(
&factory_model,
child_summarizer,
@ -469,7 +469,7 @@ pub async fn run_with_args_and_client(
)
});
profile.register_subagent_tools(manager, factory, 0);
let profile: Arc<dyn ProviderProfile> = Arc::from(profile);
let profile: Arc<dyn AgentProfile> = Arc::from(profile);
let mut session = Session::new(client, profile, env, config);

View file

@ -1,8 +1,8 @@
use crate::agent_profile::AgentProfile;
use crate::error::AgentError;
use crate::event::EventEmitter;
use crate::file_tracker::FileTracker;
use crate::history::History;
use crate::provider_profile::ProviderProfile;
use crate::types::{AgentEvent, Turn};
use fabro_llm::client::Client;
use fabro_llm::types::{Message, Request};
@ -14,7 +14,7 @@ use tracing::debug;
pub fn check_context_usage(
system_prompt: &str,
history: &History,
provider_profile: &dyn ProviderProfile,
provider_profile: &dyn AgentProfile,
threshold_percent: usize,
emitter: &EventEmitter,
session_id: &str,
@ -43,7 +43,7 @@ pub fn check_context_usage(
pub async fn compact_context(
history: &mut History,
llm_client: &Client,
provider_profile: &dyn ProviderProfile,
provider_profile: &dyn AgentProfile,
system_prompt: &str,
file_tracker: &FileTracker,
preserve_count: usize,
@ -338,7 +338,7 @@ mod tests {
let emitter = EventEmitter::new();
let mut rx = emitter.subscribe();
// TestProfile has context_window=200_000 by default; use a small one
let profile = crate::test_support::TestProfile::parallel_with_context_window(
let profile = crate::test_support::TestProfile::with_context_window(
crate::tool_registry::ToolRegistry::new(),
100,
);

View file

@ -1,6 +1,7 @@
#[cfg(feature = "docker")]
pub mod docker_sandbox;
pub mod agent_profile;
pub mod cli;
pub mod compaction;
pub mod config;
@ -13,7 +14,6 @@ pub mod loop_detection;
pub mod mcp_integration;
pub mod memory;
pub mod profiles;
pub mod provider_profile;
pub mod read_before_write_sandbox;
pub mod sandbox;
pub mod session;
@ -26,6 +26,7 @@ pub mod truncation;
pub mod types;
pub mod v4a_patch;
pub use agent_profile::AgentProfile;
pub use config::{SessionConfig, ToolApprovalAdapter, ToolHookCallback, ToolHookDecision};
#[cfg(feature = "docker")]
pub use docker_sandbox::{DockerSandbox, DockerSandboxConfig};
@ -37,7 +38,6 @@ pub use local_sandbox::LocalSandbox;
pub use loop_detection::detect_loop;
pub use memory::discover_memory;
pub use profiles::{AnthropicProfile, EnvContext, GeminiProfile, OpenAiProfile};
pub use provider_profile::{ProfileCapabilities, ProviderProfile};
pub use read_before_write_sandbox::ReadBeforeWriteSandbox;
pub use sandbox::{
format_lines_numbered, shell_quote, DirEntry, ExecResult, GrepOptions, Sandbox, SandboxEvent,

View file

@ -1,12 +1,12 @@
use crate::agent_profile::AgentProfile;
use crate::config::SessionConfig;
use crate::profiles::assemble_system_prompt;
use crate::profiles::BaseProfile;
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
use crate::sandbox::Sandbox;
use crate::skills::Skill;
use crate::tool_registry::ToolRegistry;
use crate::tools::{make_edit_file_tool, register_core_tools, WebFetchSummarizer};
use fabro_model::{Catalog, Provider};
use fabro_model::Provider;
use super::EnvContext;
@ -52,7 +52,7 @@ impl AnthropicProfile {
}
}
impl ProviderProfile for AnthropicProfile {
impl AgentProfile for AnthropicProfile {
fn provider(&self) -> Provider {
self.base.provider
}
@ -162,49 +162,6 @@ in the project. Keep changes minimal and focused on the task.";
skills,
)
}
fn capabilities(&self) -> ProfileCapabilities {
let context_window_size = Catalog::builtin()
.get(self.model())
.map(|info| info.context_window() as usize)
.unwrap_or_else(|| {
if self.model().contains("opus-4-6") {
1_000_000
} else {
200_000
}
});
ProfileCapabilities {
supports_reasoning: true,
supports_streaming: true,
supports_parallel_tool_calls: true,
context_window_size,
}
}
fn provider_options(&self) -> Option<serde_json::Value> {
let model = self.model();
if model.contains("opus-4-6") {
Some(serde_json::json!({
"anthropic": {
"thinking": {"type": "adaptive"},
"beta_headers": ["context-1m-2025-08-07"]
}
}))
} else if model.contains("sonnet-4-6") {
Some(serde_json::json!({
"anthropic": {
"thinking": {"type": "adaptive"}
}
}))
} else {
None
}
}
fn knowledge_cutoff(&self) -> &'static str {
"May 2025"
}
}
#[cfg(test)]
@ -220,18 +177,18 @@ mod tests {
}
#[test]
fn anthropic_profile_capabilities() {
let profile = AnthropicProfile::new("claude-sonnet-4-20250514");
assert!(profile.supports_reasoning());
assert!(profile.supports_streaming());
assert!(profile.supports_parallel_tool_calls());
fn anthropic_context_window_from_catalog() {
let profile = AnthropicProfile::new("claude-opus-4-6");
assert_eq!(profile.context_window_size(), 1_000_000);
let profile = AnthropicProfile::new("claude-sonnet-4-6");
assert_eq!(profile.context_window_size(), 200_000);
}
#[test]
fn anthropic_opus_4_6_has_1m_context_window() {
fn anthropic_knowledge_cutoff_from_catalog() {
let profile = AnthropicProfile::new("claude-opus-4-6");
assert_eq!(profile.context_window_size(), 1_000_000);
assert_eq!(profile.knowledge_cutoff(), Some("May 2025".to_string()));
}
#[test]
@ -332,47 +289,6 @@ mod tests {
assert!(names.contains(&"web_fetch".to_string()));
}
#[test]
fn anthropic_provider_options_include_thinking_for_opus_4_6() {
let profile = AnthropicProfile::new("claude-opus-4-6");
let options = profile.provider_options();
assert!(options.is_some(), "provider_options should return Some");
let options = options.unwrap();
let thinking_type = options["anthropic"]["thinking"]["type"].as_str();
assert_eq!(
thinking_type,
Some("adaptive"),
"thinking type should be adaptive"
);
let beta_headers = options["anthropic"]["beta_headers"]
.as_array()
.expect("beta_headers should be an array");
assert_eq!(beta_headers[0].as_str(), Some("context-1m-2025-08-07"));
}
#[test]
fn anthropic_provider_options_include_thinking_for_sonnet_4_6() {
let profile = AnthropicProfile::new("claude-sonnet-4-6");
let options = profile.provider_options();
assert!(options.is_some(), "provider_options should return Some");
let options = options.unwrap();
let thinking_type = options["anthropic"]["thinking"]["type"].as_str();
assert_eq!(
thinking_type,
Some("adaptive"),
"thinking type should be adaptive"
);
}
#[test]
fn anthropic_provider_options_none_for_older_models() {
let profile = AnthropicProfile::new("claude-sonnet-4-5");
assert!(
profile.provider_options().is_none(),
"older models should not have provider_options"
);
}
#[test]
fn anthropic_register_subagent_tools() {
use crate::subagent::{SessionFactory, SubAgentManager};

View file

@ -1,7 +1,7 @@
use crate::agent_profile::AgentProfile;
use crate::config::SessionConfig;
use crate::profiles::assemble_system_prompt;
use crate::profiles::BaseProfile;
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
use crate::sandbox::Sandbox;
use crate::skills::Skill;
use crate::tool_registry::ToolRegistry;
@ -9,7 +9,7 @@ use crate::tools::{
make_edit_file_tool, make_list_dir_tool, make_read_many_files_tool, register_core_tools,
WebFetchSummarizer,
};
use fabro_model::{Catalog, Provider};
use fabro_model::Provider;
use super::EnvContext;
@ -46,7 +46,7 @@ impl GeminiProfile {
}
}
impl ProviderProfile for GeminiProfile {
impl AgentProfile for GeminiProfile {
fn provider(&self) -> Provider {
self.base.provider
}
@ -197,34 +197,6 @@ in the project.";
skills,
)
}
fn capabilities(&self) -> ProfileCapabilities {
let context_window_size = Catalog::builtin()
.get(self.model())
.map(|info| info.context_window() as usize)
.unwrap_or(1_000_000);
ProfileCapabilities {
supports_reasoning: true,
supports_streaming: true,
supports_parallel_tool_calls: true,
context_window_size,
}
}
fn provider_options(&self) -> Option<serde_json::Value> {
Some(serde_json::json!({
"gemini": {
"safety_settings": {
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_ONLY_HIGH"
}
}
}))
}
fn knowledge_cutoff(&self) -> &'static str {
"January 2025"
}
}
#[cfg(test)]
@ -241,12 +213,9 @@ mod tests {
}
#[test]
fn gemini_profile_capabilities() {
let profile = GeminiProfile::new("gemini-2.0-flash");
assert!(profile.supports_reasoning());
assert!(profile.supports_streaming());
assert!(profile.supports_parallel_tool_calls());
assert_eq!(profile.context_window_size(), 1_000_000);
fn gemini_context_window_from_catalog() {
let profile = GeminiProfile::new("gemini-3.1-pro-preview");
assert_eq!(profile.context_window_size(), 1_048_576);
}
#[test]
@ -307,18 +276,6 @@ mod tests {
assert!(prompt.contains("linux"));
}
#[test]
fn gemini_provider_options_returns_safety_settings() {
let profile = GeminiProfile::new("gemini-2.0-flash");
let options = profile.provider_options();
assert!(options.is_some());
let options = options.unwrap();
let safety = &options["gemini"]["safety_settings"];
assert!(safety.is_object());
assert_eq!(safety["category"], "HARM_CATEGORY_DANGEROUS_CONTENT");
assert_eq!(safety["threshold"], "BLOCK_ONLY_HIGH");
}
#[test]
fn gemini_tools_registered() {
let profile = GeminiProfile::new("gemini-2.0-flash");

View file

@ -1,19 +1,18 @@
use crate::agent_profile::AgentProfile;
use crate::config::SessionConfig;
use crate::profiles::assemble_system_prompt;
use crate::profiles::BaseProfile;
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
use crate::sandbox::Sandbox;
use crate::skills::Skill;
use crate::tool_registry::ToolRegistry;
use crate::tools::{register_core_tools, WebFetchSummarizer};
use crate::v4a_patch::make_apply_patch_tool;
use fabro_model::{Catalog, Provider};
use fabro_model::Provider;
use super::EnvContext;
pub struct OpenAiProfile {
base: BaseProfile,
reasoning_effort: Option<String>,
}
impl OpenAiProfile {
@ -39,14 +38,9 @@ impl OpenAiProfile {
model: model.into(),
registry,
},
reasoning_effort: None,
}
}
pub fn set_reasoning_effort(&mut self, effort: Option<String>) {
self.reasoning_effort = effort;
}
/// Override the provider identity (e.g. for Z.AI or Minimax, which use the
/// OpenAI Chat Completions protocol but route to different adapters).
#[must_use]
@ -54,9 +48,20 @@ impl OpenAiProfile {
self.base.provider = provider;
self
}
fn provider_display_name(&self) -> &str {
match self.base.provider {
Provider::OpenAi => "OpenAI",
Provider::Kimi => "Moonshot",
Provider::Zai => "Zhipu AI",
Provider::Minimax => "MiniMax",
Provider::Inception => "Inception",
other => other.as_str(),
}
}
}
impl ProviderProfile for OpenAiProfile {
impl AgentProfile for OpenAiProfile {
fn provider(&self) -> Provider {
self.base.provider
}
@ -81,8 +86,9 @@ impl ProviderProfile for OpenAiProfile {
user_instructions: Option<&str>,
skills: &[Skill],
) -> String {
let core_prompt = "\
You are a coding agent powered by OpenAI, running in a terminal-based agentic coding assistant. \
let provider_name = self.provider_display_name();
let core_prompt = format!("\
You are a coding agent powered by {provider_name}, running in a terminal-based agentic coding assistant. \
You are expected to be precise, safe, and helpful.
You can receive user prompts and context such as files in the workspace, communicate with the \
@ -95,7 +101,7 @@ Be concise, direct, and friendly. Communicate efficiently, keeping the user clea
about ongoing actions without unnecessary detail. Prioritize actionable guidance, clearly \
stating assumptions, environment prerequisites, and next steps.
{env_block}
{{env_block}}
# AGENTS.md
@ -178,10 +184,10 @@ information instead of returning the full page. URLs must start with http:// or
# Coding Best Practices
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions \
in the project.";
in the project.");
assemble_system_prompt(
core_prompt,
&core_prompt,
env,
env_context,
memory,
@ -189,35 +195,6 @@ in the project.";
skills,
)
}
fn capabilities(&self) -> ProfileCapabilities {
let context_window_size = Catalog::builtin()
.get(self.model())
.map(|info| info.context_window() as usize)
.unwrap_or(128_000);
ProfileCapabilities {
supports_reasoning: true,
supports_streaming: true,
supports_parallel_tool_calls: true,
context_window_size,
}
}
fn provider_options(&self) -> Option<serde_json::Value> {
self.reasoning_effort.as_ref().map(|effort| {
serde_json::json!({
"openai": {
"reasoning": {
"effort": effort
}
}
})
})
}
fn knowledge_cutoff(&self) -> &'static str {
"April 2025"
}
}
#[cfg(test)]
@ -232,15 +209,6 @@ mod tests {
assert_eq!(profile.model(), "o3-mini");
}
#[test]
fn openai_profile_capabilities() {
let profile = OpenAiProfile::new("o3-mini");
assert!(profile.supports_reasoning());
assert!(profile.supports_streaming());
assert!(profile.supports_parallel_tool_calls());
assert_eq!(profile.context_window_size(), 128_000);
}
#[test]
fn openai_system_prompt_contains_env_context() {
let profile = OpenAiProfile::new("o3-mini");
@ -301,38 +269,6 @@ mod tests {
assert!(prompt.contains("# User Instructions"));
}
#[test]
fn openai_provider_options_default_none() {
let profile = OpenAiProfile::new("o3-mini");
assert!(profile.provider_options().is_none());
}
#[test]
fn openai_provider_options_with_reasoning_effort() {
let mut profile = OpenAiProfile::new("o3-mini");
profile.set_reasoning_effort(Some("high".to_string()));
let options = profile.provider_options().unwrap();
assert_eq!(
options,
serde_json::json!({
"openai": {
"reasoning": {
"effort": "high"
}
}
})
);
}
#[test]
fn openai_provider_options_cleared() {
let mut profile = OpenAiProfile::new("o3-mini");
profile.set_reasoning_effort(Some("high".to_string()));
assert!(profile.provider_options().is_some());
profile.set_reasoning_effort(None);
assert!(profile.provider_options().is_none());
}
#[test]
fn openai_subagent_tools_registered() {
use crate::subagent::SessionFactory;
@ -362,4 +298,37 @@ mod tests {
assert!(names.contains(&"web_search".to_string()));
assert!(names.contains(&"web_fetch".to_string()));
}
#[test]
fn kimi_provider_prompt_says_moonshot() {
let profile = OpenAiProfile::new("kimi-k2.5").with_provider(Provider::Kimi);
let env = MockSandbox::linux();
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
assert!(prompt.contains("powered by Moonshot"));
assert!(!prompt.contains("powered by OpenAI"));
}
#[test]
fn zai_provider_prompt_says_zhipu() {
let profile = OpenAiProfile::new("glm-4.7").with_provider(Provider::Zai);
let env = MockSandbox::linux();
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
assert!(prompt.contains("powered by Zhipu AI"));
}
#[test]
fn minimax_provider_prompt_says_minimax() {
let profile = OpenAiProfile::new("minimax-m2.5").with_provider(Provider::Minimax);
let env = MockSandbox::linux();
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
assert!(prompt.contains("powered by MiniMax"));
}
#[test]
fn inception_provider_prompt_says_inception() {
let profile = OpenAiProfile::new("mercury-2").with_provider(Provider::Inception);
let env = MockSandbox::linux();
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
assert!(prompt.contains("powered by Inception"));
}
}

View file

@ -1,3 +1,4 @@
use crate::agent_profile::AgentProfile;
use crate::config::SessionConfig;
use crate::error::{AbortReason, AgentError};
use crate::event::EventEmitter;
@ -6,7 +7,6 @@ use crate::history::History;
use crate::loop_detection::detect_loop;
use crate::memory::discover_memory;
use crate::profiles::EnvContext;
use crate::provider_profile::ProviderProfile;
use crate::sandbox::Sandbox;
use crate::skills::{
default_skill_dirs, discover_skills, expand_skill, make_use_skill_tool, Skill,
@ -32,7 +32,7 @@ pub struct Session {
event_emitter: EventEmitter,
state: SessionState,
llm_client: Client,
provider_profile: Arc<dyn ProviderProfile>,
provider_profile: Arc<dyn AgentProfile>,
sandbox: Arc<dyn Sandbox>,
steering_queue: Arc<Mutex<VecDeque<String>>>,
followup_queue: Arc<Mutex<VecDeque<String>>>,
@ -50,7 +50,7 @@ impl Session {
#[must_use]
pub fn new(
llm_client: Client,
provider_profile: Arc<dyn ProviderProfile>,
provider_profile: Arc<dyn AgentProfile>,
sandbox: Arc<dyn Sandbox>,
config: SessionConfig,
) -> Self {
@ -332,7 +332,7 @@ impl Session {
is_git_repo,
current_date: today,
model: model_name,
knowledge_cutoff: self.provider_profile.knowledge_cutoff().to_string(),
knowledge_cutoff: self.provider_profile.knowledge_cutoff().unwrap_or_default(),
git_status_short,
git_recent_commits,
}
@ -780,7 +780,7 @@ impl Session {
// Execute tool calls (parallel or sequential based on provider)
let results = crate::tool_execution::execute_tool_calls(
&tool_calls,
self.provider_profile.supports_parallel_tool_calls(),
true,
self.provider_profile.tool_registry(),
self.sandbox.clone(),
self.config.tool_hooks.as_ref(),
@ -913,7 +913,7 @@ impl Session {
reasoning_effort: self.config.reasoning_effort.clone(),
speed: self.config.speed.clone(),
metadata: None,
provider_options: self.provider_profile.provider_options(),
provider_options: None,
}
}
}
@ -1490,7 +1490,7 @@ mod tests {
let provider = Arc::new(MockLlmProvider::new(responses));
let client = make_client(provider).await;
let profile = Arc::new(TestProfile::parallel(registry));
let profile = Arc::new(TestProfile::with_tools(registry));
let env = Arc::new(MockSandbox::default());
let mut session = Session::new(client, profile, env, SessionConfig::default());
let mut rx = session.subscribe();
@ -1541,7 +1541,7 @@ mod tests {
let provider = Arc::new(MockLlmProvider::new(responses));
let client = make_client(provider).await;
let registry = ToolRegistry::new();
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
let env = Arc::new(MockSandbox::default());
let mut session = Session::new(client, profile, env, SessionConfig::default());
let mut rx = session.subscribe();
@ -1590,7 +1590,7 @@ mod tests {
let client = make_client(provider).await;
let registry = ToolRegistry::new();
// Large context window so short input stays well under 80%
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 200_000));
let profile = Arc::new(TestProfile::with_context_window(registry, 200_000));
let env = Arc::new(MockSandbox::default());
let mut session = Session::new(client, profile, env, SessionConfig::default());
let mut rx = session.subscribe();
@ -2151,7 +2151,7 @@ mod tests {
let provider = Arc::new(MockLlmProvider::new(responses));
let client = make_client(provider).await;
let registry = ToolRegistry::new();
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
let env = Arc::new(MockSandbox::default());
let config = SessionConfig {
enable_context_compaction: true,
@ -2194,7 +2194,7 @@ mod tests {
let provider = Arc::new(MockLlmProvider::new(responses));
let client = make_client(provider).await;
let registry = ToolRegistry::new();
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
let env = Arc::new(MockSandbox::default());
let config = SessionConfig {
enable_context_compaction: false,
@ -2277,7 +2277,7 @@ mod tests {
});
let client = make_client(provider as Arc<dyn ProviderAdapter>).await;
let registry = ToolRegistry::new();
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
let env = Arc::new(MockSandbox::default());
let config = SessionConfig {
enable_context_compaction: true,
@ -2381,7 +2381,7 @@ mod tests {
let client = make_client(provider.clone() as Arc<dyn ProviderAdapter>).await;
// Tiny context window to force compaction
let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100));
let profile = Arc::new(TestProfile::with_context_window(registry, 100));
let env = Arc::new(MockSandbox::default());
let config = SessionConfig {
enable_context_compaction: true,
@ -2480,8 +2480,7 @@ mod tests {
let provider = Arc::new(MockLlmProvider::new(responses));
let client = make_client(provider).await;
let profile: Arc<dyn crate::provider_profile::ProviderProfile> =
Arc::new(TestProfile::new());
let profile: Arc<dyn crate::agent_profile::AgentProfile> = Arc::new(TestProfile::new());
let env: Arc<dyn crate::sandbox::Sandbox> = Arc::new(MockSandbox::default());
let mut session = Session::new(client, profile, env, config);

View file

@ -1,8 +1,8 @@
pub use fabro_sandbox::test_support::{MockSandbox, MutableMockSandbox};
use crate::agent_profile::AgentProfile;
use crate::config::SessionConfig;
use crate::profiles::EnvContext;
use crate::provider_profile::{ProfileCapabilities, ProviderProfile};
use crate::sandbox::*;
use crate::session::Session;
use crate::skills::Skill;
@ -21,7 +21,6 @@ use std::sync::{Arc, Mutex};
pub struct TestProfile {
pub registry: ToolRegistry,
pub parallel_tool_calls: bool,
pub context_window: usize,
}
@ -29,7 +28,6 @@ impl TestProfile {
pub fn new() -> Self {
Self {
registry: ToolRegistry::new(),
parallel_tool_calls: false,
context_window: 200_000,
}
}
@ -37,29 +35,19 @@ impl TestProfile {
pub fn with_tools(registry: ToolRegistry) -> Self {
Self {
registry,
parallel_tool_calls: false,
context_window: 200_000,
}
}
pub fn parallel(registry: ToolRegistry) -> Self {
pub fn with_context_window(registry: ToolRegistry, context_window: usize) -> Self {
Self {
registry,
parallel_tool_calls: true,
context_window: 200_000,
}
}
pub fn parallel_with_context_window(registry: ToolRegistry, context_window: usize) -> Self {
Self {
registry,
parallel_tool_calls: true,
context_window,
}
}
}
impl ProviderProfile for TestProfile {
impl AgentProfile for TestProfile {
fn provider(&self) -> Provider {
Provider::Anthropic
}
@ -98,17 +86,8 @@ impl ProviderProfile for TestProfile {
}
}
fn capabilities(&self) -> ProfileCapabilities {
ProfileCapabilities {
supports_reasoning: false,
supports_streaming: false,
supports_parallel_tool_calls: self.parallel_tool_calls,
context_window_size: self.context_window,
}
}
fn knowledge_cutoff(&self) -> &'static str {
"May 2025"
fn context_window_size(&self) -> usize {
self.context_window
}
}

View file

@ -1,4 +1,4 @@
use fabro_agent::{AnthropicProfile, GeminiProfile, OpenAiProfile, ProviderProfile};
use fabro_agent::{AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile};
use fabro_model::{Catalog, Provider};
#[test]
@ -10,7 +10,7 @@ fn profile_context_window_matches_catalog_for_default_models() {
.unwrap_or_else(|| panic!("no default model for {:?} in catalog", provider));
let model = &catalog_info.id;
let profile: Box<dyn ProviderProfile> = match provider {
let profile: Box<dyn AgentProfile> = match provider {
Provider::OpenAi => Box::new(OpenAiProfile::new(model)),
Provider::Kimi
| Provider::Zai

View file

@ -2,7 +2,7 @@ use std::path::Path;
use std::sync::Arc;
use fabro_agent::{
AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, ProviderProfile, Session,
AgentProfile, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, Session,
SessionConfig, SubAgentManager, WebFetchSummarizer,
};
use fabro_llm::client::Client;
@ -38,7 +38,7 @@ fn build_summarizer(provider: Provider, client: &Client) -> WebFetchSummarizer {
}
}
fn build_profile(provider: Provider, model: &str, client: &Client) -> Box<dyn ProviderProfile> {
fn build_profile(provider: Provider, model: &str, client: &Client) -> Box<dyn AgentProfile> {
let summarizer = Some(build_summarizer(provider, client));
match provider {
Provider::Anthropic => Box::new(AnthropicProfile::with_summarizer(model, summarizer)),
@ -66,7 +66,7 @@ async fn make_session(provider: Provider, model: &str, cwd: &Path) -> Session {
let factory_model: String = model.to_string();
let factory_cwd = cwd.to_path_buf();
let factory: fabro_agent::subagent::SessionFactory = Arc::new(move || {
let sub_profile: Arc<dyn ProviderProfile> = {
let sub_profile: Arc<dyn AgentProfile> = {
let summarizer = Some(build_summarizer(provider, &factory_client));
match provider {
Provider::Anthropic => Arc::new(AnthropicProfile::with_summarizer(
@ -99,7 +99,7 @@ async fn make_session(provider: Provider, model: &str, cwd: &Path) -> Session {
});
profile.register_subagent_tools(manager, factory, 0);
let profile: Arc<dyn ProviderProfile> = Arc::from(profile);
let profile: Arc<dyn AgentProfile> = Arc::from(profile);
let config = SessionConfig {
max_turns: 20,
..SessionConfig::default()
@ -115,7 +115,7 @@ async fn make_session_with_config(
) -> Session {
dotenvy::dotenv().ok();
let client = Client::from_env().await.expect("Client::from_env failed");
let profile: Arc<dyn ProviderProfile> = Arc::from(build_profile(provider, model, &client));
let profile: Arc<dyn AgentProfile> = Arc::from(build_profile(provider, model, &client));
let env = Arc::new(LocalSandbox::new(cwd.to_path_buf()));
Session::new(client, profile, env, config)
}

View file

@ -596,10 +596,13 @@ fn apply_cache_control_to_conversation_prefix(messages: &mut [ApiMessage]) {
/// Collect beta headers from `provider_options` and merge with the caching header
/// when auto-caching is active.
const CONTEXT_1M_BETA_HEADER: &str = "context-1m-2025-08-07";
fn build_beta_header(
provider_options: Option<&serde_json::Value>,
include_cache_header: bool,
include_fast_mode_header: bool,
include_1m_context: bool,
) -> Option<String> {
let mut headers: Vec<String> = Vec::new();
@ -627,6 +630,11 @@ fn build_beta_header(
headers.push(FAST_MODE_BETA_HEADER.to_string());
}
// Add 1M context header for models with >= 1M context window
if include_1m_context && !headers.iter().any(|h| h == CONTEXT_1M_BETA_HEADER) {
headers.push(CONTEXT_1M_BETA_HEADER.to_string());
}
if headers.is_empty() {
None
} else {
@ -1134,7 +1142,16 @@ fn build_api_request(
(explicit_thinking, None)
}
} else {
(explicit_thinking, None)
// Auto-set adaptive thinking for known effort-capable models when no
// explicit thinking config or reasoning_effort is provided.
let thinking = explicit_thinking.or_else(|| {
if model_info.is_some_and(|m| m.features.effort) {
Some(serde_json::json!({"type": "adaptive"}))
} else {
None
}
});
(thinking, None)
};
let is_fast = request.speed.as_deref() == Some("fast");
@ -1168,9 +1185,13 @@ fn build_api_request(
.header("x-api-key", &adapter.http.api_key)
.header("anthropic-version", "2023-06-01");
if let Some(beta_str) =
build_beta_header(request.provider_options.as_ref(), auto_cache, is_fast)
{
let include_1m_context = model_info.is_some_and(|m| m.context_window() >= 1_000_000);
if let Some(beta_str) = build_beta_header(
request.provider_options.as_ref(),
auto_cache,
is_fast,
include_1m_context,
) {
req_builder = req_builder.header("anthropic-beta", beta_str);
}
} else {
@ -1548,13 +1569,13 @@ mod tests {
#[test]
fn beta_header_includes_cache_header() {
let result = build_beta_header(None, true, false);
let result = build_beta_header(None, true, false, false);
assert_eq!(result, Some(CACHE_BETA_HEADER.to_string()));
}
#[test]
fn beta_header_no_cache_no_user_headers() {
let result = build_beta_header(None, false, false);
let result = build_beta_header(None, false, false, false);
assert_eq!(result, None);
}
@ -1565,7 +1586,7 @@ mod tests {
"beta_headers": ["interleaved-thinking-2025-05-14"]
}
});
let result = build_beta_header(Some(&opts), true, false);
let result = build_beta_header(Some(&opts), true, false, false);
assert_eq!(
result,
Some(format!(
@ -1581,7 +1602,7 @@ mod tests {
"beta_headers": [CACHE_BETA_HEADER]
}
});
let result = build_beta_header(Some(&opts), true, false);
let result = build_beta_header(Some(&opts), true, false, false);
// Should not duplicate the header
assert_eq!(result, Some(CACHE_BETA_HEADER.to_string()));
}
@ -1593,7 +1614,7 @@ mod tests {
"beta_headers": ["interleaved-thinking-2025-05-14"]
}
});
let result = build_beta_header(Some(&opts), false, false);
let result = build_beta_header(Some(&opts), false, false, false);
assert_eq!(result, Some("interleaved-thinking-2025-05-14".to_string()));
}
@ -2002,7 +2023,7 @@ mod tests {
];
// No user headers — only cache header should appear
let header = build_beta_header(None, true, false).unwrap_or_default();
let header = build_beta_header(None, true, false, false).unwrap_or_default();
for dep in &deprecated {
assert!(
!header.contains(dep),
@ -2016,7 +2037,7 @@ mod tests {
"beta_headers": ["interleaved-thinking-2025-05-14"]
}
});
let header = build_beta_header(Some(&opts), true, false).unwrap_or_default();
let header = build_beta_header(Some(&opts), true, false, false).unwrap_or_default();
for dep in &deprecated {
assert!(
!header.contains(dep),
@ -2173,7 +2194,7 @@ mod tests {
#[test]
fn beta_header_includes_both_cache_and_fast_mode() {
let result = build_beta_header(None, true, true);
let result = build_beta_header(None, true, true, false);
let header = result.expect("should produce a header");
assert!(
header.contains(CACHE_BETA_HEADER),

View file

@ -439,6 +439,7 @@ fn build_api_request(request: &Request) -> serde_json::Value {
let mut body = serde_json::to_value(&api_request).unwrap_or_default();
merge_provider_options(&mut body, request.provider_options.as_ref());
apply_default_safety_settings(&mut body);
body
}
@ -466,6 +467,22 @@ fn merge_provider_options(
}
}
/// Apply default safety settings if none were provided via provider_options.
fn apply_default_safety_settings(body: &mut serde_json::Value) {
if body.get("safety_settings").is_some() {
return;
}
if let Some(body_map) = body.as_object_mut() {
body_map.insert(
"safety_settings".to_string(),
serde_json::json!([{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_ONLY_HIGH"
}]),
);
}
}
/// Convert `UsageMetadata` from the Gemini API into a unified `Usage`.
fn parse_usage(metadata: Option<&UsageMetadata>) -> Usage {
metadata.map_or_else(Usage::default, |u| {

View file

@ -6,6 +6,7 @@
"display_name": "Claude Opus 4.6",
"limits": { "context_window": 1000000, "max_output": 128000 },
"training": "2025-08-01",
"knowledge_cutoff": "May 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 15.0,
@ -22,6 +23,7 @@
"display_name": "Claude Sonnet 4.5",
"limits": { "context_window": 200000, "max_output": 64000 },
"training": "2025-08-01",
"knowledge_cutoff": "May 2025",
"features": { "tools": true, "vision": true, "reasoning": true },
"costs": {
"input_cost_per_mtok": 3.0,
@ -38,6 +40,7 @@
"display_name": "Claude Sonnet 4.6",
"limits": { "context_window": 200000, "max_output": 64000 },
"training": "2025-08-01",
"knowledge_cutoff": "May 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 3.0,
@ -55,6 +58,7 @@
"display_name": "Claude Haiku 4.5",
"limits": { "context_window": 200000, "max_output": 8192 },
"training": "2025-08-01",
"knowledge_cutoff": "May 2025",
"features": { "tools": true, "vision": true, "reasoning": false },
"costs": {
"input_cost_per_mtok": 0.8,
@ -71,6 +75,7 @@
"display_name": "GPT-5.2",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"knowledge_cutoff": "April 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 1.75,
@ -87,6 +92,7 @@
"display_name": "GPT-5 Mini",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"knowledge_cutoff": "April 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.25,
@ -103,6 +109,7 @@
"display_name": "GPT-5.2 Codex",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"knowledge_cutoff": "April 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 1.75,
@ -119,6 +126,7 @@
"display_name": "GPT-5.3 Codex",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"knowledge_cutoff": "April 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 1.75,
@ -135,6 +143,7 @@
"display_name": "GPT-5.3 Codex Spark",
"limits": { "context_window": 131072, "max_output": 128000 },
"training": "2025-08-31",
"knowledge_cutoff": "April 2025",
"features": { "tools": true, "vision": false, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": null,
@ -151,6 +160,7 @@
"display_name": "GPT-5.4",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"knowledge_cutoff": "April 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 2.5,
@ -168,6 +178,7 @@
"display_name": "GPT-5.4 Pro",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"knowledge_cutoff": "April 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 30.0,
@ -184,6 +195,7 @@
"display_name": "GPT-5.4 Mini",
"limits": { "context_window": 400000, "max_output": 128000 },
"training": "2025-08-31",
"knowledge_cutoff": "April 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.75,
@ -200,6 +212,7 @@
"display_name": "Gemini 3.1 Pro (Preview)",
"limits": { "context_window": 1048576, "max_output": 65536 },
"training": "2025-01-01",
"knowledge_cutoff": "January 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 2.0,
@ -217,6 +230,7 @@
"display_name": "Gemini 3.1 Pro Custom Tools (Preview)",
"limits": { "context_window": 1048576, "max_output": 65536 },
"training": "2025-01-01",
"knowledge_cutoff": "January 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 2.0,
@ -233,6 +247,7 @@
"display_name": "Gemini 3 Flash (Preview)",
"limits": { "context_window": 1048576, "max_output": 65536 },
"training": "2025-01-01",
"knowledge_cutoff": "January 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.5,
@ -249,6 +264,7 @@
"display_name": "Gemini 3.1 Flash Lite (Preview)",
"limits": { "context_window": 1048576, "max_output": 65536 },
"training": "2025-01-01",
"knowledge_cutoff": "January 2025",
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.25,
@ -265,6 +281,7 @@
"display_name": "Kimi K2.5",
"limits": { "context_window": 262144, "max_output": 16000 },
"training": "2025-10-01",
"knowledge_cutoff": "October 2025",
"features": { "tools": true, "vision": true, "reasoning": false },
"costs": {
"input_cost_per_mtok": 0.6,
@ -282,6 +299,7 @@
"display_name": "GLM 4.7",
"limits": { "context_window": 202752, "max_output": 16384 },
"training": null,
"knowledge_cutoff": null,
"features": { "tools": true, "vision": false, "reasoning": false },
"costs": {
"input_cost_per_mtok": 0.6,
@ -299,6 +317,7 @@
"display_name": "Minimax M2.5",
"limits": { "context_window": 196608, "max_output": 16384 },
"training": null,
"knowledge_cutoff": null,
"features": { "tools": true, "vision": false, "reasoning": false },
"costs": {
"input_cost_per_mtok": 0.3,
@ -316,6 +335,7 @@
"display_name": "Mercury 2",
"limits": { "context_window": 131072, "max_output": 50000 },
"training": null,
"knowledge_cutoff": null,
"features": { "tools": true, "vision": false, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.25,

View file

@ -344,6 +344,7 @@ mod tests {
max_output: Some(4096),
},
training: None,
knowledge_cutoff: None,
features: ModelFeatures {
tools: true,
vision: false,
@ -448,6 +449,9 @@ mod tests {
training: Some(
"2025-08-01",
),
knowledge_cutoff: Some(
"May 2025",
),
features: ModelFeatures {
tools: true,
vision: true,
@ -515,6 +519,9 @@ mod tests {
training: Some(
"2025-01-01",
),
knowledge_cutoff: Some(
"January 2025",
),
features: ModelFeatures {
tools: true,
vision: true,
@ -569,6 +576,9 @@ mod tests {
training: Some(
"2025-10-01",
),
knowledge_cutoff: Some(
"October 2025",
),
features: ModelFeatures {
tools: true,
vision: true,
@ -628,6 +638,7 @@ mod tests {
),
},
training: None,
knowledge_cutoff: None,
features: ModelFeatures {
tools: true,
vision: false,
@ -677,6 +688,9 @@ mod tests {
training: Some(
"2025-08-31",
),
knowledge_cutoff: Some(
"April 2025",
),
features: ModelFeatures {
tools: true,
vision: true,
@ -724,6 +738,9 @@ mod tests {
training: Some(
"2025-08-31",
),
knowledge_cutoff: Some(
"April 2025",
),
features: ModelFeatures {
tools: true,
vision: true,
@ -797,6 +814,9 @@ mod tests {
training: Some(
"2025-08-31",
),
knowledge_cutoff: Some(
"April 2025",
),
features: ModelFeatures {
tools: true,
vision: false,

View file

@ -38,6 +38,7 @@ pub struct Model {
pub display_name: String,
pub limits: ModelLimits,
pub training: Option<String>,
pub knowledge_cutoff: Option<String>,
pub features: ModelFeatures,
pub costs: ModelCosts,
pub estimated_output_tps: Option<f64>,
@ -91,6 +92,10 @@ impl Model {
self.training.as_deref()
}
pub fn knowledge_cutoff(&self) -> Option<&str> {
self.knowledge_cutoff.as_deref()
}
pub fn input_cost_per_mtok(&self) -> Option<f64> {
self.costs.input_cost_per_mtok
}
@ -135,6 +140,7 @@ mod tests {
assert!(info.supports_reasoning());
assert!(info.supports_effort());
assert_eq!(info.training(), Some("2025-08-01"));
assert_eq!(info.knowledge_cutoff(), Some("May 2025"));
assert_eq!(info.input_cost_per_mtok(), Some(15.0));
assert_eq!(info.output_cost_per_mtok(), Some(75.0));
assert_eq!(info.cache_input_cost_per_mtok(), Some(1.5));

View file

@ -3,8 +3,8 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;
use fabro_agent::{
AnthropicProfile, GeminiProfile, OpenAiProfile, ProviderProfile, Sandbox, Session,
SessionConfig, SessionEvent, Turn,
AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile, Sandbox, Session, SessionConfig,
SessionEvent, Turn,
};
use fabro_llm::client::Client;
use fabro_llm::provider::Provider;
@ -151,7 +151,7 @@ pub async fn run_retro_agent(
};
profile.tool_registry_mut().register(submit_tool);
let profile: Arc<dyn ProviderProfile> = Arc::from(profile);
let profile: Arc<dyn AgentProfile> = Arc::from(profile);
let config = SessionConfig {
max_tool_rounds_per_input: 20,
@ -323,7 +323,7 @@ fn spawn_retro_event_forwarder(
})
}
fn build_profile(provider: Provider, model: &str) -> Box<dyn ProviderProfile> {
fn build_profile(provider: Provider, model: &str) -> Box<dyn AgentProfile> {
match provider {
Provider::OpenAi => Box::new(OpenAiProfile::new(model)),
Provider::Kimi

View file

@ -5,7 +5,7 @@ use async_trait::async_trait;
use fabro_agent::{
subagent::{SessionFactory, SubAgentManager},
AgentEvent, AnthropicProfile, GeminiProfile, OpenAiProfile, ProviderProfile, Sandbox, Session,
AgentEvent, AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile, Sandbox, Session,
SessionConfig, Turn,
};
use fabro_llm::client::Client;
@ -20,7 +20,7 @@ use crate::handler::agent::{CodergenBackend, CodergenResult};
use crate::outcome::StageUsage;
use fabro_graphviz::graph::Node;
fn build_profile(model: &str, provider: Provider) -> Box<dyn ProviderProfile> {
fn build_profile(model: &str, provider: Provider) -> Box<dyn AgentProfile> {
match provider {
Provider::OpenAi => Box::new(OpenAiProfile::new(model)),
Provider::Kimi
@ -214,7 +214,7 @@ impl AgentApiBackend {
let factory_env = Arc::clone(sandbox);
let factory_tool_env = env.clone();
let factory: SessionFactory = Arc::new(move || {
let child_profile: Arc<dyn ProviderProfile> = match provider {
let child_profile: Arc<dyn AgentProfile> = match provider {
Provider::OpenAi => Arc::new(OpenAiProfile::new(&factory_model)),
Provider::Kimi
| Provider::Zai
@ -239,7 +239,7 @@ impl AgentApiBackend {
});
profile.register_subagent_tools(manager, factory, 0);
let profile: Arc<dyn ProviderProfile> = Arc::from(profile);
let profile: Arc<dyn AgentProfile> = Arc::from(profile);
let mut session = Session::new(client, profile, Arc::clone(sandbox), config);
if !env.is_empty() {