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