Skip to main content

bamboo_engine/runtime/
config.rs

1use std::collections::BTreeSet;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use bamboo_agent_core::storage::AttachmentReader;
6use bamboo_agent_core::storage::Storage;
7use bamboo_agent_core::tools::ToolSchema;
8use bamboo_agent_core::GoldConfidence;
9use bamboo_compression::TokenBudget;
10use bamboo_config::MemoryConfig;
11use bamboo_config::PermissionMode;
12use bamboo_domain::ReasoningEffort;
13use bamboo_domain::RuntimeSessionPersistence;
14use bamboo_llm::LLMProvider;
15use bamboo_metrics::MetricsCollector;
16use bamboo_skills::SkillManager;
17use bamboo_tools::ToolRegistry;
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone, Default)]
21pub struct AuxiliaryModelConfig {
22    pub fast_model_name: Option<String>,
23    pub fast_model_provider: Option<Arc<dyn LLMProvider>>,
24    pub background_model_name: Option<String>,
25    pub planning_model_name: Option<String>,
26    pub search_model_name: Option<String>,
27    pub summarization_model_name: Option<String>,
28    pub background_model_provider: Option<Arc<dyn LLMProvider>>,
29    pub summarization_model_provider: Option<Arc<dyn LLMProvider>>,
30}
31
32fn default_gold_max_output_tokens() -> u32 {
33    1024
34}
35
36fn default_gold_max_auto_continuations() -> u32 {
37    3
38}
39
40fn default_gold_min_confidence() -> GoldConfidence {
41    GoldConfidence::Medium
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45#[serde(default)]
46pub struct GoldConfig {
47    /// Master switch for Gold observe-only evaluation.
48    #[serde(default)]
49    pub enabled: bool,
50    /// Independent switch for Phase 2 low-risk auto-answer.
51    ///
52    /// Kept separate from `enabled` so Phase 1 observe-only users do not
53    /// implicitly opt into automatic clarification responses.
54    #[serde(default)]
55    pub auto_answer_enabled: bool,
56    /// Independent switch for Phase 3 server-side auto-continue.
57    ///
58    /// Kept separate from both `enabled` and `auto_answer_enabled` so users can
59    /// opt into terminal auto-resume explicitly without enabling other Gold
60    /// automation behaviors.
61    #[serde(default)]
62    pub auto_continue_enabled: bool,
63    /// Optional dedicated model for Gold evaluation. Falls back to fast model,
64    /// then the main chat model when absent.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub model_name: Option<String>,
67    /// The user's goal for this session.
68    ///
69    /// Unlike `evaluation_prompt` (which only tunes the *judge*), the goal is
70    /// surfaced to the *main* executing agent as a persistent system-prompt
71    /// block so it actively works toward it. The Gold evaluator also measures
72    /// progress against this text.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub goal: Option<String>,
75    /// Optional custom prompt suffix appended to the built-in Gold evaluator
76    /// prompt. This tunes the judge only; it does not set the goal.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub evaluation_prompt: Option<String>,
79    /// Output token limit for the Gold evaluator call.
80    #[serde(default = "default_gold_max_output_tokens")]
81    pub max_output_tokens: u32,
82    /// Maximum number of automatic Gold continuations allowed per session.
83    #[serde(default = "default_gold_max_auto_continuations")]
84    pub max_auto_continuations: u32,
85    /// Minimum evaluator confidence required before Gold auto-continues or
86    /// auto-answers. Defaults to `medium` so the loop fires on reasonably
87    /// confident verdicts rather than only `high`.
88    #[serde(default = "default_gold_min_confidence")]
89    pub min_auto_continue_confidence: GoldConfidence,
90}
91
92impl Default for GoldConfig {
93    fn default() -> Self {
94        Self {
95            enabled: false,
96            auto_answer_enabled: false,
97            auto_continue_enabled: false,
98            model_name: None,
99            goal: None,
100            evaluation_prompt: None,
101            max_output_tokens: default_gold_max_output_tokens(),
102            max_auto_continuations: default_gold_max_auto_continuations(),
103            min_auto_continue_confidence: default_gold_min_confidence(),
104        }
105    }
106}
107
108impl GoldConfig {
109    /// The session goal text, falling back to the legacy `evaluation_prompt`
110    /// for sessions created before the dedicated `goal` field existed.
111    ///
112    /// Returns `None` when neither field holds non-empty text.
113    pub fn effective_goal(&self) -> Option<&str> {
114        self.goal
115            .as_deref()
116            .or(self.evaluation_prompt.as_deref())
117            .map(str::trim)
118            .filter(|value| !value.is_empty())
119    }
120}
121
122fn default_guardian_max_reviews() -> u32 {
123    2
124}
125
126/// Configuration for the guardian adversarial-review terminal gate.
127///
128/// Mirrors [`GoldConfig`]: a plain, serde-defaulting struct surfaced per run.
129/// When `enabled` is false (the default) the guardian gate is inactive and the
130/// terminal completion path is unchanged.
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132#[serde(default)]
133pub struct GuardianConfig {
134    /// Master switch for the guardian review gate.
135    #[serde(default)]
136    pub enabled: bool,
137    /// Optional dedicated reviewer model. Falls back to the run's main model.
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub model_name: Option<String>,
140    /// Maximum guardian review passes per run (budget; mirrors
141    /// [`GoldConfig::max_auto_continuations`]).
142    #[serde(default = "default_guardian_max_reviews")]
143    pub max_reviews: u32,
144}
145
146impl Default for GuardianConfig {
147    fn default() -> Self {
148        Self {
149            enabled: false,
150            model_name: None,
151            max_reviews: default_guardian_max_reviews(),
152        }
153    }
154}
155
156/// Late-bound spawner for the guardian reviewer child.
157///
158/// The runner cannot construct a child directly: the `SpawnScheduler` is built
159/// *after* the `Agent` that drives the runner (a construction-order cycle), so
160/// the terminal gate spawns the reviewer through this trait object, injected
161/// per-request on [`AgentLoopConfig`] exactly like `auxiliary_model_resolver`.
162/// The implementation lives in the server (it captures the already-built
163/// scheduler + child-session adapter); the engine holds only the trait, keeping
164/// the engine free of any dependency on server/AppState types.
165#[async_trait::async_trait]
166pub trait GuardianSpawner: Send + Sync {
167    /// Create a read-only reviewer child for `parent_session_id`, seeded with
168    /// `review_prompt`, enqueue it to run, and return its session id so the
169    /// caller can register a wait on it.
170    async fn spawn_guardian_review(
171        &self,
172        parent_session: &bamboo_agent_core::Session,
173        review_prompt: String,
174        model: String,
175        disabled_tools: Option<BTreeSet<String>>,
176    ) -> Result<String, String>;
177}
178
179/// Hidden resume-message `runtime_kind` metadata value for a bash-completion
180/// self-resume (issue #84 Phase 2b). Shared by the producer (the self-resume
181/// task that appends the resume message) and the consumer (the suspend-
182/// finalization discriminant arm that preserves it), so a typo in one cannot
183/// desync from the other and silently drop the resume trigger.
184pub const BASH_COMPLETION_RESUME_KIND: &str = "bash_completion_resume";
185
186/// Re-exported so peers that already `use crate::runtime::config::{BashResumeHook, …}`
187/// (the runtime/spawn threading) can name the completion sink the same way,
188/// rather than reaching into `bamboo_agent_core` separately.
189pub use bamboo_agent_core::BashCompletionSink;
190
191/// Late-bound hook that arranges a self-resume for a session suspended waiting
192/// on background Bash shells (issue #84 Phase 2b). Injected per-request on
193/// [`AgentLoopConfig`] exactly like [`GuardianSpawner`]; the implementation
194/// lives in the session-app layer (on the completion coordinator) where the
195/// resume port ([`crate::session_app::resume::ResumeExecutionPort`]) is
196/// reachable.
197///
198/// The hook spawns a detached task that **polls the live background-shell
199/// registry** until every captured shell is no longer running, then clears the
200/// wait and resumes the session. Polling — not the one-shot `BashCompleted`
201/// event — is the liveness guarantee: even if a shell completes between the
202/// suspend snapshot and the hook's first poll, or before any event subscriber
203/// exists, the registry will report it as not-running and the session resumes.
204pub trait BashResumeHook: Send + Sync {
205    /// Arrange a detached self-resume for `session_id`, which has just been
206    /// durably suspended waiting on the background shells in `bash_ids`.
207    fn arrange_bash_self_resume(&self, session_id: String, bash_ids: Vec<String>);
208}
209
210/// A child sub-agent's request to have a gated tool approved by its parent.
211///
212/// A non-bypassed child cannot answer its own permission prompt (no human is
213/// attached to a child session), so the request is delegated up to the parent.
214#[derive(Debug, Clone)]
215pub struct ChildApprovalRequest {
216    pub child_session_id: String,
217    pub parent_session_id: String,
218    /// The gated tool call on the child to re-execute once approved.
219    pub child_tool_call_id: String,
220    pub tool_name: String,
221    /// Permission type as a string (e.g. "WriteFile", "ExecuteCommand").
222    pub permission_type: String,
223    /// The concrete resource the permission applies to (path, command, …).
224    pub resource: String,
225    /// Human-facing approval question to surface on the parent.
226    pub question: String,
227    /// The raw `awaiting_permission_approval` payload the child's executor built,
228    /// so the parent can reuse the existing grant-extraction path verbatim.
229    pub approval_payload: serde_json::Value,
230}
231
232/// What the executor should do after delegating a child's approval upward.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum ChildApprovalOutcome {
235    /// Registered on the parent; the child must SUSPEND and await the decision.
236    Delegated,
237    /// Parent policy auto-approved (bypass / existing grant); proceed to execute.
238    AutoApproved,
239    /// Parent policy auto-denied; the executor must deny the tool.
240    AutoDenied,
241}
242
243/// Late-bound delegate that routes a child's approval request up to its parent.
244///
245/// Injected per-request on [`AgentLoopConfig`] exactly like [`GuardianSpawner`];
246/// the trait lives in the engine, the implementation in the server (it owns the
247/// parent session store + pending-question + notification machinery).
248#[async_trait::async_trait]
249pub trait ApprovalDelegate: Send + Sync {
250    /// Register `request` on its parent (or auto-resolve by policy) and report
251    /// what the child's executor should do next.
252    async fn delegate_child_approval(
253        &self,
254        request: ChildApprovalRequest,
255    ) -> Result<ChildApprovalOutcome, String>;
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum ImageFallbackMode {
260    Placeholder,
261    Error,
262    Ocr,
263    /// Use a vision-capable LLM to describe the image, then replace the image
264    /// with the textual description so that text-only models can understand
265    /// the content.
266    Vision,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct ImageFallbackConfig {
271    pub mode: ImageFallbackMode,
272    /// Vision model name for `Vision` mode. Falls back to the session's main model
273    /// when `None`.
274    pub vision_model: Option<String>,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub struct PromptMemoryFlags {
279    pub project_prompt_injection: bool,
280    pub relevant_recall: bool,
281    pub relevant_recall_rerank: bool,
282    pub project_first_dream: bool,
283    pub ledger_agenda: bool,
284}
285
286impl Default for PromptMemoryFlags {
287    fn default() -> Self {
288        Self {
289            project_prompt_injection: true,
290            relevant_recall: true,
291            relevant_recall_rerank: false,
292            project_first_dream: true,
293            ledger_agenda: true,
294        }
295    }
296}
297
298impl From<&MemoryConfig> for PromptMemoryFlags {
299    fn from(value: &MemoryConfig) -> Self {
300        Self {
301            project_prompt_injection: value.project_prompt_injection,
302            relevant_recall: value.relevant_recall,
303            relevant_recall_rerank: value.relevant_recall_rerank,
304            project_first_dream: value.project_first_dream,
305            ledger_agenda: value.ledger_agenda_injection,
306        }
307    }
308}
309
310/// Configuration for the agent loop.
311///
312/// # One-config-per-run invariant (#44)
313///
314/// These values are SNAPSHOTTED once, from the live `Config` under a brief read
315/// lock, at the start of `AgentRuntime::execute()`. The entire multi-round run —
316/// which can last minutes — then uses this frozen snapshot. Changing config
317/// (model names, provider, `disabled_tools`/`disabled_skills`, memory flags,
318/// token budget, …) while a run is in flight does NOT affect that run; the new
319/// values are picked up on the NEXT execution (i.e. the next user turn / session
320/// restart), not the next round of the current run.
321///
322/// This is intentional: a run sees a stable configuration, so its behavior can't
323/// shift underneath it mid-execution. The deliberate exceptions are the
324/// late-bound, per-request trait objects that resolve LIVE each time they're
325/// used rather than being snapshotted — `auxiliary_model_resolver` (auxiliary
326/// model selection) and `guardian_spawner` (the reviewer child). If a frozen
327/// field ever needs to become live-per-round, follow that resolver pattern
328/// rather than widening the snapshot.
329#[non_exhaustive]
330pub struct AgentLoopConfig {
331    pub(crate) max_rounds: usize,
332    pub(crate) system_prompt: Option<String>,
333    /// Skill IDs that are disabled globally for this execution.
334    pub(crate) disabled_skill_ids: BTreeSet<String>,
335    /// Optional explicit skill selection for this execution.
336    /// When set, only these skill IDs are considered for skill context and allowlists.
337    pub(crate) selected_skill_ids: Option<Vec<String>>,
338    /// Optional active skill mode for this execution.
339    ///
340    /// When set, skill discovery prefers `skills-<mode>` directories over generic
341    /// directories for the same skill id.
342    pub(crate) selected_skill_mode: Option<String>,
343    pub(crate) additional_tool_schemas: Vec<ToolSchema>,
344    pub(crate) tool_registry: Arc<ToolRegistry>,
345    pub(crate) skill_manager: Option<Arc<SkillManager>>,
346    /// If true, skip appending the initial user message (already present in session).
347    pub(crate) skip_initial_user_message: bool,
348    /// Optional storage for persisting session changes
349    pub(crate) storage: Option<Arc<dyn Storage>>,
350    /// Optional runtime persistence for non-authoritative session saves.
351    /// When set, engine save sites use this instead of `storage` for writes.
352    pub(crate) persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
353    /// Optional attachment reader for resolving `bamboo-attachment://...` references
354    /// into `data:` URLs for upstream providers. This must not mutate session storage.
355    pub(crate) attachment_reader: Option<Arc<dyn AttachmentReader>>,
356    /// Optional asynchronous metrics collector
357    pub(crate) metrics_collector: Option<MetricsCollector>,
358    /// Model name used for metrics attribution
359    pub(crate) model_name: Option<String>,
360    /// Fast/cheap model for lightweight tasks (task evaluation, search, etc.).
361    ///
362    /// Call sites may fall back to `model_name` when this is unset.
363    pub(crate) fast_model_name: Option<String>,
364    /// Optional provider override for lightweight fast-model LLM calls.
365    pub(crate) fast_model_provider: Option<Arc<dyn LLMProvider>>,
366    /// Fast/cheap model for memory/background tasks.
367    ///
368    /// This must not silently fall back to the main interaction model.
369    pub(crate) background_model_name: Option<String>,
370
371    /// Model for planning/coordination tasks (task decomposition, architecture).
372    /// Falls back to `model_name` when unset.
373    pub(crate) planning_model_name: Option<String>,
374    /// Model for search/navigation tasks (grep, file listing, symbol resolution).
375    /// Falls back to `fast_model_name` when unset.
376    pub(crate) search_model_name: Option<String>,
377    /// Custom instructions for conversation summarization, injected into the
378    /// LLM summary prompt. Lets users control what the summary focuses on.
379    ///
380    /// Resolution order: session-level > config-level > built-in defaults.
381    pub(crate) compression_instructions: Option<String>,
382    /// Dedicated model for summarization. Falls back to `background_model_name`.
383    pub(crate) summarization_model_name: Option<String>,
384    /// Optional provider override for memory/background model LLM calls.
385    ///
386    /// When set, memory recall rerank and other memory/background tasks use this
387    /// provider instead of the shared agent loop provider.
388    pub(crate) background_model_provider: Option<Arc<dyn LLMProvider>>,
389    /// Optional provider override for summarization / context compression calls.
390    ///
391    /// When set, conversation/task summarization uses this provider instead of
392    /// the shared agent loop provider.
393    pub(crate) summarization_model_provider: Option<Arc<dyn LLMProvider>>,
394    /// Provider routing key used for provider-specific request behavior.
395    ///
396    /// In multi-instance mode this may be the instance id.
397    pub(crate) provider_name: Option<String>,
398    /// Underlying provider type (for example `openai`, `anthropic`, `copilot`).
399    ///
400    /// This is distinct from `provider_name` so provider-specific behavior can
401    /// remain correct when routing keys are instance ids.
402    pub(crate) provider_type: Option<String>,
403    /// Optional request-time reasoning effort override.
404    pub(crate) reasoning_effort: Option<ReasoningEffort>,
405    /// Bamboo application data directory (typically `~/.bamboo`).
406    ///
407    /// Used by runtime features that persist auxiliary artifacts outside the
408    /// session store, such as durable plan mode files under `~/.bamboo/plan`.
409    pub(crate) app_data_dir: Option<PathBuf>,
410    /// Tool names that should be excluded from schemas sent to the LLM.
411    pub(crate) disabled_tools: BTreeSet<String>,
412    /// Token budget for context management (optional, defaults to model's limits)
413    pub(crate) token_budget: Option<TokenBudget>,
414    /// Legacy `config.json` `model_limits` value, snapshotted from the live
415    /// in-memory Config when this loop config is built. Consulted only by
416    /// `resolve_token_budget` as a last-resort fallback when `model_limits.json`
417    /// fails to load — so the engine never does a fresh disk-reading
418    /// `Config::new()` (which would also clobber the global env-var cache). #38.
419    pub(crate) legacy_model_limits: Option<serde_json::Value>,
420    /// Optional image fallback behavior applied to *LLM requests only* (never persisted).
421    ///
422    /// This is intended for text-only provider paths where image parts must be degraded
423    /// (placeholder / OCR / error) without leaking into stored session history or UI.
424    pub(crate) image_fallback: Option<ImageFallbackConfig>,
425    /// Feature flags controlling prompt-time memory injection behavior.
426    pub(crate) prompt_memory_flags: PromptMemoryFlags,
427    /// Maximum tool calls allowed per round (default: 80).
428    pub(crate) max_tool_calls_per_round: usize,
429    /// Maximum consecutive failures per tool before circuit breaker (default: 3).
430    pub(crate) max_consecutive_failures_per_tool: usize,
431    /// Per-tool execution timeout in seconds (default: 120).
432    pub(crate) per_tool_timeout_secs: u64,
433    /// Parallel batch execution timeout in seconds (default: 300).
434    pub(crate) parallel_batch_timeout_secs: u64,
435    /// Resolved LLM stream transport/semantic watchdog policy. The same value
436    /// is passed to main response streams and auxiliary silent streams.
437    pub(crate) stream_timeout: bamboo_config::StreamTimeoutConfig,
438    /// Permission mode for this execution (default: None = use PermissionConfig's mode).
439    pub(crate) permission_mode: Option<PermissionMode>,
440    /// Optional Gold observe-only evaluator configuration.
441    ///
442    /// When `None` or `enabled == false`, Gold evaluation is disabled and the
443    /// existing execute/respond/resume loop remains unchanged.
444    pub(crate) gold_config: Option<GoldConfig>,
445    /// Optional guardian adversarial-review gate configuration. When `None` or
446    /// `enabled == false`, the guardian terminal gate is inactive.
447    pub(crate) guardian_config: Option<GuardianConfig>,
448    /// Late-bound spawner for the guardian reviewer child. `None` (the default)
449    /// leaves the guardian gate inert even when `guardian_config.enabled` is set,
450    /// since the runner cannot create a child without it. Wired by the server.
451    pub(crate) guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
452    /// Late-bound hook that arranges a self-resume for a session suspended
453    /// waiting on background Bash shells (issue #84 Phase 2b). `None` (the
454    /// default) leaves the bash suspend gate inert: the gate refuses to suspend
455    /// without a wired hook, so a session can never strand itself without a
456    /// resume path. Wired by the server (the completion coordinator impl).
457    pub(crate) bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
458    /// Late-bound sink that pushes a completed background Bash shell's result
459    /// into this session's loop (issue #84 Phase 2b follow-up) — injected at the
460    /// next round boundary while the loop is actively iterating, or delivered via
461    /// resume when it is idle. Threaded onto the tool dispatch context (like
462    /// `can_async_resume`) so the Bash tool can hand it to the shell's
463    /// completion-poll task. `None` (the default) leaves the push inert; the
464    /// durable end-of-turn suspend/poll backstop (`bash_resume_hook`) still runs.
465    /// Wired by the server (the completion coordinator impl).
466    pub(crate) bash_completion_sink: Option<Arc<dyn bamboo_agent_core::BashCompletionSink>>,
467    /// Late-bound delegate that routes a child's gated-tool approval request up
468    /// to its parent (Phase 2). `None` (the default) leaves child gating on its
469    /// legacy path. Wired by the server.
470    pub(crate) approval_delegate: Option<Arc<dyn ApprovalDelegate>>,
471    /// Enable dynamic per-round model routing based on task complexity.
472    /// When true, the pipeline classifies complexity at each round end and
473    /// stores the result in session metadata.
474    pub(crate) features_dynamic_model_routing: bool,
475    /// Optional per-round resolver for auxiliary model settings that should
476    /// follow live global config rather than stay frozen for the whole run.
477    ///
478    /// The main chat model remains session/request scoped; this hook is only
479    /// for fast/background/planning/search/summarization helpers.
480    pub(crate) auxiliary_model_resolver:
481        Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
482    /// Optional per-round resolver for the disabled tool/skill sets so they follow
483    /// LIVE global config instead of staying frozen for the whole run. Returns the
484    /// current `(disabled_tools, disabled_skill_ids)`. When `None`, the snapshotted
485    /// `disabled_tools` / `disabled_skill_ids` fields below are used (#44 behavior).
486    /// Re-resolved each round at the tool-schema filter, so disabling/re-enabling a
487    /// tool mid-run takes effect on the next round. #136.
488    pub(crate) disabled_filter_resolver:
489        Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
490    /// Server-level usage guidance contributed by the run's tool executor —
491    /// chiefly the `instructions` connected MCP servers return from `initialize`.
492    /// Captured once at config construction (from `ToolExecutor::tool_guidance`)
493    /// and appended to the tool-guide section of the system prompt, so a server's
494    /// own how-to-use notes appear only while that server is loaded for the run.
495    pub(crate) mcp_tool_guidance: Option<String>,
496    /// Per-run resource guardrails (issue #221): already resolved — the
497    /// per-request override merged over the config-level default (see
498    /// [`AgentRuntime::execute`](crate::runtime::runtime::AgentRuntime::execute)).
499    /// Checked after every round; exceeding a configured limit gracefully
500    /// stops the run (mirrors the `max_rounds` exhaustion path).
501    pub(crate) run_budget: bamboo_config::RunBudgetConfig,
502}
503
504impl Default for AgentLoopConfig {
505    fn default() -> Self {
506        Self {
507            max_rounds: 200,
508            system_prompt: None,
509            disabled_skill_ids: BTreeSet::new(),
510            selected_skill_ids: None,
511            selected_skill_mode: None,
512            additional_tool_schemas: Vec::new(),
513            tool_registry: Arc::new(ToolRegistry::new()),
514            skill_manager: None,
515            skip_initial_user_message: false,
516            storage: None,
517            persistence: None,
518            attachment_reader: None,
519            metrics_collector: None,
520            model_name: None,
521            fast_model_name: None,
522            fast_model_provider: None,
523            background_model_name: None,
524            planning_model_name: None,
525            search_model_name: None,
526            compression_instructions: None,
527            summarization_model_name: None,
528            background_model_provider: None,
529            summarization_model_provider: None,
530            provider_name: None,
531            provider_type: None,
532            reasoning_effort: None,
533            app_data_dir: None,
534            disabled_tools: BTreeSet::new(),
535            token_budget: None,
536            legacy_model_limits: None,
537            image_fallback: None,
538            prompt_memory_flags: PromptMemoryFlags::default(),
539            max_tool_calls_per_round: 80,
540            max_consecutive_failures_per_tool: 3,
541            per_tool_timeout_secs: 120,
542            parallel_batch_timeout_secs: 300,
543            stream_timeout: bamboo_config::StreamTimeoutConfig::default(),
544            permission_mode: None,
545            gold_config: None,
546            guardian_config: None,
547            guardian_spawner: None,
548            bash_resume_hook: None,
549            bash_completion_sink: None,
550            approval_delegate: None,
551            features_dynamic_model_routing: false,
552            auxiliary_model_resolver: None,
553            disabled_filter_resolver: None,
554            mcp_tool_guidance: None,
555            run_budget: bamboo_config::RunBudgetConfig::default(),
556        }
557    }
558}
559
560impl AgentLoopConfig {
561    /// Live `(disabled_tools, disabled_skill_ids)` for the current round: the
562    /// resolver if one is wired (#136 — follows live global config between
563    /// rounds), else the per-run snapshot (#44 frozen behavior). `Cow` avoids
564    /// cloning the snapshot in the common no-resolver path (SDK / tests).
565    pub(crate) fn resolve_disabled_filters(
566        &self,
567    ) -> (
568        std::borrow::Cow<'_, BTreeSet<String>>,
569        std::borrow::Cow<'_, BTreeSet<String>>,
570    ) {
571        match &self.disabled_filter_resolver {
572            Some(resolver) => {
573                let (tools, skills) = resolver();
574                (
575                    std::borrow::Cow::Owned(tools),
576                    std::borrow::Cow::Owned(skills),
577                )
578            }
579            None => (
580                std::borrow::Cow::Borrowed(&self.disabled_tools),
581                std::borrow::Cow::Borrowed(&self.disabled_skill_ids),
582            ),
583        }
584    }
585
586    /// The active session goal to surface to the main agent, or `None` when
587    /// Gold is disabled or no goal is set. Falls back to the legacy
588    /// `evaluation_prompt` for back-compat via [`GoldConfig::effective_goal`].
589    pub fn active_goal(&self) -> Option<&str> {
590        self.gold_config
591            .as_ref()
592            .filter(|cfg| cfg.enabled)
593            .and_then(GoldConfig::effective_goal)
594    }
595
596    /// Whether the Codex-style autonomous goal loop is active for this run.
597    ///
598    /// This requires Gold to be enabled, a goal to be set, AND auto-continue to
599    /// be on. Only then is the `update_goal` self-report tool surfaced to the
600    /// model and the terminal double-check allowed to veto a premature stop.
601    /// When Gold is enabled without auto-continue, the evaluator stays purely
602    /// observational (legacy behavior).
603    pub fn goal_loop_active(&self) -> bool {
604        self.gold_config.as_ref().is_some_and(|cfg| {
605            cfg.enabled && cfg.auto_continue_enabled && cfg.effective_goal().is_some()
606        })
607    }
608
609    /// Whether the guardian review gate is active for this run: a spawner is
610    /// wired (so the runner can actually create the reviewer child) AND the
611    /// config is present and enabled.
612    pub fn guardian_active(&self) -> bool {
613        self.guardian_spawner.is_some()
614            && self.guardian_config.as_ref().is_some_and(|cfg| cfg.enabled)
615    }
616
617    /// Maximum guardian review passes for this run (the budget). `0` when no
618    /// guardian config is set.
619    pub fn guardian_max_reviews(&self) -> u32 {
620        self.guardian_config
621            .as_ref()
622            .map_or(0, |cfg| cfg.max_reviews)
623    }
624
625    /// The reviewer model override, if a guardian config sets one.
626    pub fn guardian_model(&self) -> Option<&str> {
627        self.guardian_config
628            .as_ref()
629            .and_then(|cfg| cfg.model_name.as_deref())
630    }
631
632    /// Whether child→parent approval delegation is wired for this run.
633    pub fn delegation_active(&self) -> bool {
634        self.approval_delegate.is_some()
635    }
636}
637
638#[cfg(test)]
639mod tests;