diff --git a/lib/crates/fabro-agent/README.md b/lib/crates/fabro-agent/README.md index 4c8e99b0d..aa122327c 100644 --- a/lib/crates/fabro-agent/README.md +++ b/lib/crates/fabro-agent/README.md @@ -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_*() } diff --git a/lib/crates/fabro-agent/src/provider_profile.rs b/lib/crates/fabro-agent/src/agent_profile.rs similarity index 70% rename from lib/crates/fabro-agent/src/provider_profile.rs rename to lib/crates/fabro-agent/src/agent_profile.rs index 93c57f716..4594e70ae 100644 --- a/lib/crates/fabro-agent/src/provider_profile.rs +++ b/lib/crates/fabro-agent/src/agent_profile.rs @@ -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 { self.tool_registry().definitions() } - fn provider_options(&self) -> Option { - 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 { + 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(); diff --git a/lib/crates/fabro-agent/src/cli.rs b/lib/crates/fabro-agent/src/cli.rs index abaf712a2..7723c3b47 100644 --- a/lib/crates/fabro-agent/src/cli.rs +++ b/lib/crates/fabro-agent/src/cli.rs @@ -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, -) -> Box { +) -> Box { 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 = match provider { + let child_profile: Arc = 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 = Arc::from(profile); + let profile: Arc = Arc::from(profile); let mut session = Session::new(client, profile, env, config); diff --git a/lib/crates/fabro-agent/src/compaction.rs b/lib/crates/fabro-agent/src/compaction.rs index 082c4b9b8..cd56d4a16 100644 --- a/lib/crates/fabro-agent/src/compaction.rs +++ b/lib/crates/fabro-agent/src/compaction.rs @@ -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, ); diff --git a/lib/crates/fabro-agent/src/lib.rs b/lib/crates/fabro-agent/src/lib.rs index e150b663e..0ec1cd80e 100644 --- a/lib/crates/fabro-agent/src/lib.rs +++ b/lib/crates/fabro-agent/src/lib.rs @@ -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, diff --git a/lib/crates/fabro-agent/src/profiles/anthropic.rs b/lib/crates/fabro-agent/src/profiles/anthropic.rs index b08926bcd..f37abe76d 100644 --- a/lib/crates/fabro-agent/src/profiles/anthropic.rs +++ b/lib/crates/fabro-agent/src/profiles/anthropic.rs @@ -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 { - 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}; diff --git a/lib/crates/fabro-agent/src/profiles/gemini.rs b/lib/crates/fabro-agent/src/profiles/gemini.rs index c4b977352..f9a47d6a4 100644 --- a/lib/crates/fabro-agent/src/profiles/gemini.rs +++ b/lib/crates/fabro-agent/src/profiles/gemini.rs @@ -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 { - 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"); diff --git a/lib/crates/fabro-agent/src/profiles/openai.rs b/lib/crates/fabro-agent/src/profiles/openai.rs index 3465d33bb..7ddc4384b 100644 --- a/lib/crates/fabro-agent/src/profiles/openai.rs +++ b/lib/crates/fabro-agent/src/profiles/openai.rs @@ -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, } impl OpenAiProfile { @@ -39,14 +38,9 @@ impl OpenAiProfile { model: model.into(), registry, }, - reasoning_effort: None, } } - pub fn set_reasoning_effort(&mut self, effort: Option) { - 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 { - 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")); + } } diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs index 53ccb3ddf..361ea6ed8 100644 --- a/lib/crates/fabro-agent/src/session.rs +++ b/lib/crates/fabro-agent/src/session.rs @@ -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, + provider_profile: Arc, sandbox: Arc, steering_queue: Arc>>, followup_queue: Arc>>, @@ -50,7 +50,7 @@ impl Session { #[must_use] pub fn new( llm_client: Client, - provider_profile: Arc, + provider_profile: Arc, sandbox: Arc, 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).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).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 = - Arc::new(TestProfile::new()); + let profile: Arc = Arc::new(TestProfile::new()); let env: Arc = Arc::new(MockSandbox::default()); let mut session = Session::new(client, profile, env, config); diff --git a/lib/crates/fabro-agent/src/test_support.rs b/lib/crates/fabro-agent/src/test_support.rs index 91c97a616..be701c1e2 100644 --- a/lib/crates/fabro-agent/src/test_support.rs +++ b/lib/crates/fabro-agent/src/test_support.rs @@ -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 } } diff --git a/lib/crates/fabro-agent/tests/guardrails.rs b/lib/crates/fabro-agent/tests/guardrails.rs index f1d778cae..b676128b4 100644 --- a/lib/crates/fabro-agent/tests/guardrails.rs +++ b/lib/crates/fabro-agent/tests/guardrails.rs @@ -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 = match provider { + let profile: Box = match provider { Provider::OpenAi => Box::new(OpenAiProfile::new(model)), Provider::Kimi | Provider::Zai diff --git a/lib/crates/fabro-agent/tests/parity_matrix.rs b/lib/crates/fabro-agent/tests/parity_matrix.rs index 44c2d1552..a6e39c085 100644 --- a/lib/crates/fabro-agent/tests/parity_matrix.rs +++ b/lib/crates/fabro-agent/tests/parity_matrix.rs @@ -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 { +fn build_profile(provider: Provider, model: &str, client: &Client) -> Box { 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 = { + let sub_profile: Arc = { 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 = Arc::from(profile); + let profile: Arc = 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 = Arc::from(build_profile(provider, model, &client)); + let profile: Arc = Arc::from(build_profile(provider, model, &client)); let env = Arc::new(LocalSandbox::new(cwd.to_path_buf())); Session::new(client, profile, env, config) } diff --git a/lib/crates/fabro-llm/src/providers/anthropic.rs b/lib/crates/fabro-llm/src/providers/anthropic.rs index 9a4b7a71a..cd021aa2c 100644 --- a/lib/crates/fabro-llm/src/providers/anthropic.rs +++ b/lib/crates/fabro-llm/src/providers/anthropic.rs @@ -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 { let mut headers: Vec = 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), diff --git a/lib/crates/fabro-llm/src/providers/gemini.rs b/lib/crates/fabro-llm/src/providers/gemini.rs index ef73894d4..377c3fab2 100644 --- a/lib/crates/fabro-llm/src/providers/gemini.rs +++ b/lib/crates/fabro-llm/src/providers/gemini.rs @@ -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| { diff --git a/lib/crates/fabro-model/src/catalog.json b/lib/crates/fabro-model/src/catalog.json index 64693b07b..543fd2544 100644 --- a/lib/crates/fabro-model/src/catalog.json +++ b/lib/crates/fabro-model/src/catalog.json @@ -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, diff --git a/lib/crates/fabro-model/src/catalog.rs b/lib/crates/fabro-model/src/catalog.rs index 62def1dd7..917aa7ca0 100644 --- a/lib/crates/fabro-model/src/catalog.rs +++ b/lib/crates/fabro-model/src/catalog.rs @@ -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, diff --git a/lib/crates/fabro-model/src/types.rs b/lib/crates/fabro-model/src/types.rs index 9531c1282..ebee2de53 100644 --- a/lib/crates/fabro-model/src/types.rs +++ b/lib/crates/fabro-model/src/types.rs @@ -38,6 +38,7 @@ pub struct Model { pub display_name: String, pub limits: ModelLimits, pub training: Option, + pub knowledge_cutoff: Option, pub features: ModelFeatures, pub costs: ModelCosts, pub estimated_output_tps: Option, @@ -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 { 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)); diff --git a/lib/crates/fabro-retro/src/retro_agent.rs b/lib/crates/fabro-retro/src/retro_agent.rs index b6252fb32..de4cbe2ef 100644 --- a/lib/crates/fabro-retro/src/retro_agent.rs +++ b/lib/crates/fabro-retro/src/retro_agent.rs @@ -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 = Arc::from(profile); + let profile: Arc = 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 { +fn build_profile(provider: Provider, model: &str) -> Box { match provider { Provider::OpenAi => Box::new(OpenAiProfile::new(model)), Provider::Kimi diff --git a/lib/crates/fabro-workflows/src/backend/api.rs b/lib/crates/fabro-workflows/src/backend/api.rs index 732132e9b..493d40841 100644 --- a/lib/crates/fabro-workflows/src/backend/api.rs +++ b/lib/crates/fabro-workflows/src/backend/api.rs @@ -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 { +fn build_profile(model: &str, provider: Provider) -> Box { 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 = match provider { + let child_profile: Arc = 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 = Arc::from(profile); + let profile: Arc = Arc::from(profile); let mut session = Session::new(client, profile, Arc::clone(sandbox), config); if !env.is_empty() {