Skip to main content

bamboo_engine/runtime/
config.rs

1use std::collections::BTreeSet;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use crate::metrics::MetricsCollector;
6use crate::skills::SkillManager;
7use bamboo_agent_core::composition::CompositionExecutor;
8use bamboo_agent_core::storage::AttachmentReader;
9use bamboo_agent_core::storage::Storage;
10use bamboo_agent_core::tools::ToolSchema;
11use bamboo_agent_core::GoldConfidence;
12use bamboo_compression::TokenBudget;
13use bamboo_domain::ReasoningEffort;
14use bamboo_domain::RuntimeSessionPersistence;
15use bamboo_infrastructure::config::PermissionMode;
16use bamboo_infrastructure::LLMProvider;
17use bamboo_infrastructure::MemoryConfig;
18use bamboo_tools::ToolRegistry;
19use serde::{Deserialize, Serialize};
20
21#[derive(Clone, Default)]
22pub struct AuxiliaryModelConfig {
23    pub fast_model_name: Option<String>,
24    pub fast_model_provider: Option<Arc<dyn LLMProvider>>,
25    pub background_model_name: Option<String>,
26    pub planning_model_name: Option<String>,
27    pub search_model_name: Option<String>,
28    pub summarization_model_name: Option<String>,
29    pub background_model_provider: Option<Arc<dyn LLMProvider>>,
30    pub summarization_model_provider: Option<Arc<dyn LLMProvider>>,
31}
32
33fn default_gold_max_output_tokens() -> u32 {
34    1024
35}
36
37fn default_gold_max_auto_continuations() -> u32 {
38    3
39}
40
41fn default_gold_min_confidence() -> GoldConfidence {
42    GoldConfidence::Medium
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46#[serde(default)]
47pub struct GoldConfig {
48    /// Master switch for Gold observe-only evaluation.
49    #[serde(default)]
50    pub enabled: bool,
51    /// Independent switch for Phase 2 low-risk auto-answer.
52    ///
53    /// Kept separate from `enabled` so Phase 1 observe-only users do not
54    /// implicitly opt into automatic clarification responses.
55    #[serde(default)]
56    pub auto_answer_enabled: bool,
57    /// Independent switch for Phase 3 server-side auto-continue.
58    ///
59    /// Kept separate from both `enabled` and `auto_answer_enabled` so users can
60    /// opt into terminal auto-resume explicitly without enabling other Gold
61    /// automation behaviors.
62    #[serde(default)]
63    pub auto_continue_enabled: bool,
64    /// Optional dedicated model for Gold evaluation. Falls back to fast model,
65    /// then the main chat model when absent.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub model_name: Option<String>,
68    /// The user's goal for this session.
69    ///
70    /// Unlike `evaluation_prompt` (which only tunes the *judge*), the goal is
71    /// surfaced to the *main* executing agent as a persistent system-prompt
72    /// block so it actively works toward it. The Gold evaluator also measures
73    /// progress against this text.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub goal: Option<String>,
76    /// Optional custom prompt suffix appended to the built-in Gold evaluator
77    /// prompt. This tunes the judge only; it does not set the goal.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub evaluation_prompt: Option<String>,
80    /// Output token limit for the Gold evaluator call.
81    #[serde(default = "default_gold_max_output_tokens")]
82    pub max_output_tokens: u32,
83    /// Maximum number of automatic Gold continuations allowed per session.
84    #[serde(default = "default_gold_max_auto_continuations")]
85    pub max_auto_continuations: u32,
86    /// Minimum evaluator confidence required before Gold auto-continues or
87    /// auto-answers. Defaults to `medium` so the loop fires on reasonably
88    /// confident verdicts rather than only `high`.
89    #[serde(default = "default_gold_min_confidence")]
90    pub min_auto_continue_confidence: GoldConfidence,
91}
92
93impl Default for GoldConfig {
94    fn default() -> Self {
95        Self {
96            enabled: false,
97            auto_answer_enabled: false,
98            auto_continue_enabled: false,
99            model_name: None,
100            goal: None,
101            evaluation_prompt: None,
102            max_output_tokens: default_gold_max_output_tokens(),
103            max_auto_continuations: default_gold_max_auto_continuations(),
104            min_auto_continue_confidence: default_gold_min_confidence(),
105        }
106    }
107}
108
109impl GoldConfig {
110    /// The session goal text, falling back to the legacy `evaluation_prompt`
111    /// for sessions created before the dedicated `goal` field existed.
112    ///
113    /// Returns `None` when neither field holds non-empty text.
114    pub fn effective_goal(&self) -> Option<&str> {
115        self.goal
116            .as_deref()
117            .or(self.evaluation_prompt.as_deref())
118            .map(str::trim)
119            .filter(|value| !value.is_empty())
120    }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum ImageFallbackMode {
125    Placeholder,
126    Error,
127    Ocr,
128    /// Use a vision-capable LLM to describe the image, then replace the image
129    /// with the textual description so that text-only models can understand
130    /// the content.
131    Vision,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct ImageFallbackConfig {
136    pub mode: ImageFallbackMode,
137    /// Vision model name for `Vision` mode. Falls back to the session's main model
138    /// when `None`.
139    pub vision_model: Option<String>,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct PromptMemoryFlags {
144    pub project_prompt_injection: bool,
145    pub relevant_recall: bool,
146    pub relevant_recall_rerank: bool,
147    pub project_first_dream: bool,
148}
149
150impl Default for PromptMemoryFlags {
151    fn default() -> Self {
152        Self {
153            project_prompt_injection: true,
154            relevant_recall: true,
155            relevant_recall_rerank: false,
156            project_first_dream: true,
157        }
158    }
159}
160
161impl From<&MemoryConfig> for PromptMemoryFlags {
162    fn from(value: &MemoryConfig) -> Self {
163        Self {
164            project_prompt_injection: value.project_prompt_injection,
165            relevant_recall: value.relevant_recall,
166            relevant_recall_rerank: value.relevant_recall_rerank,
167            project_first_dream: value.project_first_dream,
168        }
169    }
170}
171
172/// Configuration for the agent loop.
173pub struct AgentLoopConfig {
174    pub max_rounds: usize,
175    pub system_prompt: Option<String>,
176    /// Skill IDs that are disabled globally for this execution.
177    pub disabled_skill_ids: BTreeSet<String>,
178    /// Optional explicit skill selection for this execution.
179    /// When set, only these skill IDs are considered for skill context and allowlists.
180    pub selected_skill_ids: Option<Vec<String>>,
181    /// Optional active skill mode for this execution.
182    ///
183    /// When set, skill discovery prefers `skills-<mode>` directories over generic
184    /// directories for the same skill id.
185    pub selected_skill_mode: Option<String>,
186    pub additional_tool_schemas: Vec<ToolSchema>,
187    pub tool_registry: Arc<ToolRegistry>,
188    pub composition_executor: Option<Arc<CompositionExecutor>>,
189    pub skill_manager: Option<Arc<SkillManager>>,
190    /// If true, skip appending the initial user message (already present in session).
191    pub skip_initial_user_message: bool,
192    /// Optional storage for persisting session changes
193    pub storage: Option<Arc<dyn Storage>>,
194    /// Optional runtime persistence for non-authoritative session saves.
195    /// When set, engine save sites use this instead of `storage` for writes.
196    pub persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
197    /// Optional attachment reader for resolving `bamboo-attachment://...` references
198    /// into `data:` URLs for upstream providers. This must not mutate session storage.
199    pub attachment_reader: Option<Arc<dyn AttachmentReader>>,
200    /// Optional asynchronous metrics collector
201    pub metrics_collector: Option<MetricsCollector>,
202    /// Model name used for metrics attribution
203    pub model_name: Option<String>,
204    /// Fast/cheap model for lightweight tasks (task evaluation, search, etc.).
205    ///
206    /// Call sites may fall back to `model_name` when this is unset.
207    pub fast_model_name: Option<String>,
208    /// Optional provider override for lightweight fast-model LLM calls.
209    pub fast_model_provider: Option<Arc<dyn LLMProvider>>,
210    /// Fast/cheap model for memory/background tasks.
211    ///
212    /// This must not silently fall back to the main interaction model.
213    pub background_model_name: Option<String>,
214
215    /// Model for planning/coordination tasks (task decomposition, architecture).
216    /// Falls back to `model_name` when unset.
217    pub planning_model_name: Option<String>,
218    /// Model for search/navigation tasks (grep, file listing, symbol resolution).
219    /// Falls back to `fast_model_name` when unset.
220    pub search_model_name: Option<String>,
221    /// Custom instructions for conversation summarization, injected into the
222    /// LLM summary prompt. Lets users control what the summary focuses on.
223    ///
224    /// Resolution order: session-level > config-level > built-in defaults.
225    pub compression_instructions: Option<String>,
226    /// Dedicated model for summarization. Falls back to `background_model_name`.
227    pub summarization_model_name: Option<String>,
228    /// Optional provider override for memory/background model LLM calls.
229    ///
230    /// When set, memory recall rerank and other memory/background tasks use this
231    /// provider instead of the shared agent loop provider.
232    pub background_model_provider: Option<Arc<dyn LLMProvider>>,
233    /// Optional provider override for summarization / context compression calls.
234    ///
235    /// When set, conversation/task summarization uses this provider instead of
236    /// the shared agent loop provider.
237    pub summarization_model_provider: Option<Arc<dyn LLMProvider>>,
238    /// Provider routing key used for provider-specific request behavior.
239    ///
240    /// In multi-instance mode this may be the instance id.
241    pub provider_name: Option<String>,
242    /// Underlying provider type (for example `openai`, `anthropic`, `copilot`).
243    ///
244    /// This is distinct from `provider_name` so provider-specific behavior can
245    /// remain correct when routing keys are instance ids.
246    pub provider_type: Option<String>,
247    /// Optional request-time reasoning effort override.
248    pub reasoning_effort: Option<ReasoningEffort>,
249    /// Bamboo application data directory (typically `~/.bamboo`).
250    ///
251    /// Used by runtime features that persist auxiliary artifacts outside the
252    /// session store, such as durable plan mode files under `~/.bamboo/plan`.
253    pub app_data_dir: Option<PathBuf>,
254    /// Tool names that should be excluded from schemas sent to the LLM.
255    pub disabled_tools: BTreeSet<String>,
256    /// Token budget for context management (optional, defaults to model's limits)
257    pub token_budget: Option<TokenBudget>,
258    /// Optional image fallback behavior applied to *LLM requests only* (never persisted).
259    ///
260    /// This is intended for text-only provider paths where image parts must be degraded
261    /// (placeholder / OCR / error) without leaking into stored session history or UI.
262    pub image_fallback: Option<ImageFallbackConfig>,
263    /// Feature flags controlling prompt-time memory injection behavior.
264    pub prompt_memory_flags: PromptMemoryFlags,
265    /// Maximum tool calls allowed per round (default: 80).
266    pub max_tool_calls_per_round: usize,
267    /// Maximum consecutive failures per tool before circuit breaker (default: 3).
268    pub max_consecutive_failures_per_tool: usize,
269    /// Tool names that require strict argument validation.
270    pub strict_argument_tool_names: Vec<String>,
271    /// Per-tool execution timeout in seconds (default: 120).
272    pub per_tool_timeout_secs: u64,
273    /// Parallel batch execution timeout in seconds (default: 300).
274    pub parallel_batch_timeout_secs: u64,
275    /// Permission mode for this execution (default: None = use PermissionConfig's mode).
276    pub permission_mode: Option<PermissionMode>,
277    /// Optional Gold observe-only evaluator configuration.
278    ///
279    /// When `None` or `enabled == false`, Gold evaluation is disabled and the
280    /// existing execute/respond/resume loop remains unchanged.
281    pub gold_config: Option<GoldConfig>,
282    /// Enable dynamic per-round model routing based on task complexity.
283    /// When true, the pipeline classifies complexity at each round end and
284    /// stores the result in session metadata.
285    pub features_dynamic_model_routing: bool,
286    /// Optional per-round resolver for auxiliary model settings that should
287    /// follow live global config rather than stay frozen for the whole run.
288    ///
289    /// The main chat model remains session/request scoped; this hook is only
290    /// for fast/background/planning/search/summarization helpers.
291    pub auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
292}
293
294impl Default for AgentLoopConfig {
295    fn default() -> Self {
296        Self {
297            max_rounds: 200,
298            system_prompt: None,
299            disabled_skill_ids: BTreeSet::new(),
300            selected_skill_ids: None,
301            selected_skill_mode: None,
302            additional_tool_schemas: Vec::new(),
303            tool_registry: Arc::new(ToolRegistry::new()),
304            composition_executor: None,
305            skill_manager: None,
306            skip_initial_user_message: false,
307            storage: None,
308            persistence: None,
309            attachment_reader: None,
310            metrics_collector: None,
311            model_name: None,
312            fast_model_name: None,
313            fast_model_provider: None,
314            background_model_name: None,
315            planning_model_name: None,
316            search_model_name: None,
317            compression_instructions: None,
318            summarization_model_name: None,
319            background_model_provider: None,
320            summarization_model_provider: None,
321            provider_name: None,
322            provider_type: None,
323            reasoning_effort: None,
324            app_data_dir: None,
325            disabled_tools: BTreeSet::new(),
326            token_budget: None,
327            image_fallback: None,
328            prompt_memory_flags: PromptMemoryFlags::default(),
329            max_tool_calls_per_round: 80,
330            max_consecutive_failures_per_tool: 3,
331            strict_argument_tool_names: vec![
332                "Write".into(),
333                "Edit".into(),
334                "NotebookEdit".into(),
335                "apply_patch".into(),
336                "Bash".into(),
337                "Task".into(),
338                "SubAgent".into(),
339                "scheduler".into(),
340                "sub_session_manager".into(),
341                "session_note".into(),
342                "memory_note".into(),
343            ],
344            per_tool_timeout_secs: 120,
345            parallel_batch_timeout_secs: 300,
346            permission_mode: None,
347            gold_config: None,
348            features_dynamic_model_routing: false,
349            auxiliary_model_resolver: None,
350        }
351    }
352}
353
354impl AgentLoopConfig {
355    /// The active session goal to surface to the main agent, or `None` when
356    /// Gold is disabled or no goal is set. Falls back to the legacy
357    /// `evaluation_prompt` for back-compat via [`GoldConfig::effective_goal`].
358    pub fn active_goal(&self) -> Option<&str> {
359        self.gold_config
360            .as_ref()
361            .filter(|cfg| cfg.enabled)
362            .and_then(GoldConfig::effective_goal)
363    }
364}
365
366#[cfg(test)]
367mod tests;