use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::Arc;
use bamboo_agent_core::composition::CompositionExecutor;
use bamboo_agent_core::storage::AttachmentReader;
use bamboo_agent_core::storage::Storage;
use bamboo_agent_core::tools::ToolSchema;
use bamboo_agent_core::GoldConfidence;
use bamboo_compression::TokenBudget;
use bamboo_config::MemoryConfig;
use bamboo_config::PermissionMode;
use bamboo_domain::ReasoningEffort;
use bamboo_domain::RuntimeSessionPersistence;
use bamboo_llm::LLMProvider;
use bamboo_metrics::MetricsCollector;
use bamboo_skills::SkillManager;
use bamboo_tools::ToolRegistry;
use serde::{Deserialize, Serialize};
#[derive(Clone, Default)]
pub struct AuxiliaryModelConfig {
pub fast_model_name: Option<String>,
pub fast_model_provider: Option<Arc<dyn LLMProvider>>,
pub background_model_name: Option<String>,
pub planning_model_name: Option<String>,
pub search_model_name: Option<String>,
pub summarization_model_name: Option<String>,
pub background_model_provider: Option<Arc<dyn LLMProvider>>,
pub summarization_model_provider: Option<Arc<dyn LLMProvider>>,
}
fn default_gold_max_output_tokens() -> u32 {
1024
}
fn default_gold_max_auto_continuations() -> u32 {
3
}
fn default_gold_min_confidence() -> GoldConfidence {
GoldConfidence::Medium
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct GoldConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub auto_answer_enabled: bool,
#[serde(default)]
pub auto_continue_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub goal: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evaluation_prompt: Option<String>,
#[serde(default = "default_gold_max_output_tokens")]
pub max_output_tokens: u32,
#[serde(default = "default_gold_max_auto_continuations")]
pub max_auto_continuations: u32,
#[serde(default = "default_gold_min_confidence")]
pub min_auto_continue_confidence: GoldConfidence,
}
impl Default for GoldConfig {
fn default() -> Self {
Self {
enabled: false,
auto_answer_enabled: false,
auto_continue_enabled: false,
model_name: None,
goal: None,
evaluation_prompt: None,
max_output_tokens: default_gold_max_output_tokens(),
max_auto_continuations: default_gold_max_auto_continuations(),
min_auto_continue_confidence: default_gold_min_confidence(),
}
}
}
impl GoldConfig {
pub fn effective_goal(&self) -> Option<&str> {
self.goal
.as_deref()
.or(self.evaluation_prompt.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty())
}
}
fn default_guardian_max_reviews() -> u32 {
2
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct GuardianConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_name: Option<String>,
#[serde(default = "default_guardian_max_reviews")]
pub max_reviews: u32,
}
impl Default for GuardianConfig {
fn default() -> Self {
Self {
enabled: false,
model_name: None,
max_reviews: default_guardian_max_reviews(),
}
}
}
#[async_trait::async_trait]
pub trait GuardianSpawner: Send + Sync {
async fn spawn_guardian_review(
&self,
parent_session: &bamboo_agent_core::Session,
review_prompt: String,
model: String,
disabled_tools: Option<BTreeSet<String>>,
) -> Result<String, String>;
}
pub const BASH_COMPLETION_RESUME_KIND: &str = "bash_completion_resume";
pub trait BashResumeHook: Send + Sync {
fn arrange_bash_self_resume(&self, session_id: String, bash_ids: Vec<String>);
}
#[derive(Debug, Clone)]
pub struct ChildApprovalRequest {
pub child_session_id: String,
pub parent_session_id: String,
pub child_tool_call_id: String,
pub tool_name: String,
pub permission_type: String,
pub resource: String,
pub question: String,
pub approval_payload: serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChildApprovalOutcome {
Delegated,
AutoApproved,
AutoDenied,
}
#[async_trait::async_trait]
pub trait ApprovalDelegate: Send + Sync {
async fn delegate_child_approval(
&self,
request: ChildApprovalRequest,
) -> Result<ChildApprovalOutcome, String>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFallbackMode {
Placeholder,
Error,
Ocr,
Vision,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageFallbackConfig {
pub mode: ImageFallbackMode,
pub vision_model: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PromptMemoryFlags {
pub project_prompt_injection: bool,
pub relevant_recall: bool,
pub relevant_recall_rerank: bool,
pub project_first_dream: bool,
}
impl Default for PromptMemoryFlags {
fn default() -> Self {
Self {
project_prompt_injection: true,
relevant_recall: true,
relevant_recall_rerank: false,
project_first_dream: true,
}
}
}
impl From<&MemoryConfig> for PromptMemoryFlags {
fn from(value: &MemoryConfig) -> Self {
Self {
project_prompt_injection: value.project_prompt_injection,
relevant_recall: value.relevant_recall,
relevant_recall_rerank: value.relevant_recall_rerank,
project_first_dream: value.project_first_dream,
}
}
}
#[non_exhaustive]
pub struct AgentLoopConfig {
pub(crate) max_rounds: usize,
pub(crate) system_prompt: Option<String>,
pub(crate) disabled_skill_ids: BTreeSet<String>,
pub(crate) selected_skill_ids: Option<Vec<String>>,
pub(crate) selected_skill_mode: Option<String>,
pub(crate) additional_tool_schemas: Vec<ToolSchema>,
pub(crate) tool_registry: Arc<ToolRegistry>,
pub(crate) composition_executor: Option<Arc<CompositionExecutor>>,
pub(crate) skill_manager: Option<Arc<SkillManager>>,
pub(crate) skip_initial_user_message: bool,
pub(crate) storage: Option<Arc<dyn Storage>>,
pub(crate) persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
pub(crate) attachment_reader: Option<Arc<dyn AttachmentReader>>,
pub(crate) metrics_collector: Option<MetricsCollector>,
pub(crate) model_name: Option<String>,
pub(crate) fast_model_name: Option<String>,
pub(crate) fast_model_provider: Option<Arc<dyn LLMProvider>>,
pub(crate) background_model_name: Option<String>,
pub(crate) planning_model_name: Option<String>,
pub(crate) search_model_name: Option<String>,
pub(crate) compression_instructions: Option<String>,
pub(crate) summarization_model_name: Option<String>,
pub(crate) background_model_provider: Option<Arc<dyn LLMProvider>>,
pub(crate) summarization_model_provider: Option<Arc<dyn LLMProvider>>,
pub(crate) provider_name: Option<String>,
pub(crate) provider_type: Option<String>,
pub(crate) reasoning_effort: Option<ReasoningEffort>,
pub(crate) app_data_dir: Option<PathBuf>,
pub(crate) disabled_tools: BTreeSet<String>,
pub(crate) token_budget: Option<TokenBudget>,
pub(crate) legacy_model_limits: Option<serde_json::Value>,
pub(crate) image_fallback: Option<ImageFallbackConfig>,
pub(crate) prompt_memory_flags: PromptMemoryFlags,
pub(crate) max_tool_calls_per_round: usize,
pub(crate) max_consecutive_failures_per_tool: usize,
pub(crate) strict_argument_tool_names: Vec<String>,
pub(crate) per_tool_timeout_secs: u64,
pub(crate) parallel_batch_timeout_secs: u64,
pub(crate) permission_mode: Option<PermissionMode>,
pub(crate) gold_config: Option<GoldConfig>,
pub(crate) guardian_config: Option<GuardianConfig>,
pub(crate) guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
pub(crate) bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
pub(crate) approval_delegate: Option<Arc<dyn ApprovalDelegate>>,
pub(crate) features_dynamic_model_routing: bool,
pub(crate) auxiliary_model_resolver:
Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
pub(crate) mcp_tool_guidance: Option<String>,
}
impl Default for AgentLoopConfig {
fn default() -> Self {
Self {
max_rounds: 200,
system_prompt: None,
disabled_skill_ids: BTreeSet::new(),
selected_skill_ids: None,
selected_skill_mode: None,
additional_tool_schemas: Vec::new(),
tool_registry: Arc::new(ToolRegistry::new()),
composition_executor: None,
skill_manager: None,
skip_initial_user_message: false,
storage: None,
persistence: None,
attachment_reader: None,
metrics_collector: None,
model_name: None,
fast_model_name: None,
fast_model_provider: None,
background_model_name: None,
planning_model_name: None,
search_model_name: None,
compression_instructions: None,
summarization_model_name: None,
background_model_provider: None,
summarization_model_provider: None,
provider_name: None,
provider_type: None,
reasoning_effort: None,
app_data_dir: None,
disabled_tools: BTreeSet::new(),
token_budget: None,
legacy_model_limits: None,
image_fallback: None,
prompt_memory_flags: PromptMemoryFlags::default(),
max_tool_calls_per_round: 80,
max_consecutive_failures_per_tool: 3,
strict_argument_tool_names: vec![
"Write".into(),
"Edit".into(),
"NotebookEdit".into(),
"apply_patch".into(),
"Bash".into(),
"Task".into(),
"SubAgent".into(),
"scheduler".into(),
"sub_session_manager".into(),
"session_note".into(),
"memory_note".into(),
],
per_tool_timeout_secs: 120,
parallel_batch_timeout_secs: 300,
permission_mode: None,
gold_config: None,
guardian_config: None,
guardian_spawner: None,
bash_resume_hook: None,
approval_delegate: None,
features_dynamic_model_routing: false,
auxiliary_model_resolver: None,
mcp_tool_guidance: None,
}
}
}
impl AgentLoopConfig {
pub fn active_goal(&self) -> Option<&str> {
self.gold_config
.as_ref()
.filter(|cfg| cfg.enabled)
.and_then(GoldConfig::effective_goal)
}
pub fn goal_loop_active(&self) -> bool {
self.gold_config.as_ref().is_some_and(|cfg| {
cfg.enabled && cfg.auto_continue_enabled && cfg.effective_goal().is_some()
})
}
pub fn guardian_active(&self) -> bool {
self.guardian_spawner.is_some()
&& self.guardian_config.as_ref().is_some_and(|cfg| cfg.enabled)
}
pub fn guardian_max_reviews(&self) -> u32 {
self.guardian_config
.as_ref()
.map_or(0, |cfg| cfg.max_reviews)
}
pub fn guardian_model(&self) -> Option<&str> {
self.guardian_config
.as_ref()
.and_then(|cfg| cfg.model_name.as_deref())
}
pub fn delegation_active(&self) -> bool {
self.approval_delegate.is_some()
}
}
#[cfg(test)]
mod tests;