bamboo_engine/runtime/config.rs
1use std::collections::BTreeSet;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use bamboo_agent_core::composition::CompositionExecutor;
6use bamboo_agent_core::storage::AttachmentReader;
7use bamboo_agent_core::storage::Storage;
8use bamboo_agent_core::tools::ToolSchema;
9use bamboo_agent_core::GoldConfidence;
10use bamboo_compression::TokenBudget;
11use bamboo_config::MemoryConfig;
12use bamboo_config::PermissionMode;
13use bamboo_domain::ReasoningEffort;
14use bamboo_domain::RuntimeSessionPersistence;
15use bamboo_llm::LLMProvider;
16use bamboo_metrics::MetricsCollector;
17use bamboo_skills::SkillManager;
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
123fn default_guardian_max_reviews() -> u32 {
124 2
125}
126
127/// Configuration for the guardian adversarial-review terminal gate.
128///
129/// Mirrors [`GoldConfig`]: a plain, serde-defaulting struct surfaced per run.
130/// When `enabled` is false (the default) the guardian gate is inactive and the
131/// terminal completion path is unchanged.
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133#[serde(default)]
134pub struct GuardianConfig {
135 /// Master switch for the guardian review gate.
136 #[serde(default)]
137 pub enabled: bool,
138 /// Optional dedicated reviewer model. Falls back to the run's main model.
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub model_name: Option<String>,
141 /// Maximum guardian review passes per run (budget; mirrors
142 /// [`GoldConfig::max_auto_continuations`]).
143 #[serde(default = "default_guardian_max_reviews")]
144 pub max_reviews: u32,
145}
146
147impl Default for GuardianConfig {
148 fn default() -> Self {
149 Self {
150 enabled: false,
151 model_name: None,
152 max_reviews: default_guardian_max_reviews(),
153 }
154 }
155}
156
157/// Late-bound spawner for the guardian reviewer child.
158///
159/// The runner cannot construct a child directly: the `SpawnScheduler` is built
160/// *after* the `Agent` that drives the runner (a construction-order cycle), so
161/// the terminal gate spawns the reviewer through this trait object, injected
162/// per-request on [`AgentLoopConfig`] exactly like `auxiliary_model_resolver`.
163/// The implementation lives in the server (it captures the already-built
164/// scheduler + child-session adapter); the engine holds only the trait, keeping
165/// the engine free of any dependency on server/AppState types.
166#[async_trait::async_trait]
167pub trait GuardianSpawner: Send + Sync {
168 /// Create a read-only reviewer child for `parent_session_id`, seeded with
169 /// `review_prompt`, enqueue it to run, and return its session id so the
170 /// caller can register a wait on it.
171 async fn spawn_guardian_review(
172 &self,
173 parent_session: &bamboo_agent_core::Session,
174 review_prompt: String,
175 model: String,
176 disabled_tools: Option<BTreeSet<String>>,
177 ) -> Result<String, String>;
178}
179
180/// Hidden resume-message `runtime_kind` metadata value for a bash-completion
181/// self-resume (issue #84 Phase 2b). Shared by the producer (the self-resume
182/// task that appends the resume message) and the consumer (the suspend-
183/// finalization discriminant arm that preserves it), so a typo in one cannot
184/// desync from the other and silently drop the resume trigger.
185pub const BASH_COMPLETION_RESUME_KIND: &str = "bash_completion_resume";
186
187/// Late-bound hook that arranges a self-resume for a session suspended waiting
188/// on background Bash shells (issue #84 Phase 2b). Injected per-request on
189/// [`AgentLoopConfig`] exactly like [`GuardianSpawner`]; the implementation
190/// lives in the session-app layer (on the completion coordinator) where the
191/// resume port ([`crate::session_app::resume::ResumeExecutionPort`]) is
192/// reachable.
193///
194/// The hook spawns a detached task that **polls the live background-shell
195/// registry** until every captured shell is no longer running, then clears the
196/// wait and resumes the session. Polling — not the one-shot `BashCompleted`
197/// event — is the liveness guarantee: even if a shell completes between the
198/// suspend snapshot and the hook's first poll, or before any event subscriber
199/// exists, the registry will report it as not-running and the session resumes.
200pub trait BashResumeHook: Send + Sync {
201 /// Arrange a detached self-resume for `session_id`, which has just been
202 /// durably suspended waiting on the background shells in `bash_ids`.
203 fn arrange_bash_self_resume(&self, session_id: String, bash_ids: Vec<String>);
204}
205
206/// A child sub-agent's request to have a gated tool approved by its parent.
207///
208/// A non-bypassed child cannot answer its own permission prompt (no human is
209/// attached to a child session), so the request is delegated up to the parent.
210#[derive(Debug, Clone)]
211pub struct ChildApprovalRequest {
212 pub child_session_id: String,
213 pub parent_session_id: String,
214 /// The gated tool call on the child to re-execute once approved.
215 pub child_tool_call_id: String,
216 pub tool_name: String,
217 /// Permission type as a string (e.g. "WriteFile", "ExecuteCommand").
218 pub permission_type: String,
219 /// The concrete resource the permission applies to (path, command, …).
220 pub resource: String,
221 /// Human-facing approval question to surface on the parent.
222 pub question: String,
223 /// The raw `awaiting_permission_approval` payload the child's executor built,
224 /// so the parent can reuse the existing grant-extraction path verbatim.
225 pub approval_payload: serde_json::Value,
226}
227
228/// What the executor should do after delegating a child's approval upward.
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum ChildApprovalOutcome {
231 /// Registered on the parent; the child must SUSPEND and await the decision.
232 Delegated,
233 /// Parent policy auto-approved (bypass / existing grant); proceed to execute.
234 AutoApproved,
235 /// Parent policy auto-denied; the executor must deny the tool.
236 AutoDenied,
237}
238
239/// Late-bound delegate that routes a child's approval request up to its parent.
240///
241/// Injected per-request on [`AgentLoopConfig`] exactly like [`GuardianSpawner`];
242/// the trait lives in the engine, the implementation in the server (it owns the
243/// parent session store + pending-question + notification machinery).
244#[async_trait::async_trait]
245pub trait ApprovalDelegate: Send + Sync {
246 /// Register `request` on its parent (or auto-resolve by policy) and report
247 /// what the child's executor should do next.
248 async fn delegate_child_approval(
249 &self,
250 request: ChildApprovalRequest,
251 ) -> Result<ChildApprovalOutcome, String>;
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum ImageFallbackMode {
256 Placeholder,
257 Error,
258 Ocr,
259 /// Use a vision-capable LLM to describe the image, then replace the image
260 /// with the textual description so that text-only models can understand
261 /// the content.
262 Vision,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct ImageFallbackConfig {
267 pub mode: ImageFallbackMode,
268 /// Vision model name for `Vision` mode. Falls back to the session's main model
269 /// when `None`.
270 pub vision_model: Option<String>,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub struct PromptMemoryFlags {
275 pub project_prompt_injection: bool,
276 pub relevant_recall: bool,
277 pub relevant_recall_rerank: bool,
278 pub project_first_dream: bool,
279}
280
281impl Default for PromptMemoryFlags {
282 fn default() -> Self {
283 Self {
284 project_prompt_injection: true,
285 relevant_recall: true,
286 relevant_recall_rerank: false,
287 project_first_dream: true,
288 }
289 }
290}
291
292impl From<&MemoryConfig> for PromptMemoryFlags {
293 fn from(value: &MemoryConfig) -> Self {
294 Self {
295 project_prompt_injection: value.project_prompt_injection,
296 relevant_recall: value.relevant_recall,
297 relevant_recall_rerank: value.relevant_recall_rerank,
298 project_first_dream: value.project_first_dream,
299 }
300 }
301}
302
303/// Configuration for the agent loop.
304#[non_exhaustive]
305pub struct AgentLoopConfig {
306 pub(crate) max_rounds: usize,
307 pub(crate) system_prompt: Option<String>,
308 /// Skill IDs that are disabled globally for this execution.
309 pub(crate) disabled_skill_ids: BTreeSet<String>,
310 /// Optional explicit skill selection for this execution.
311 /// When set, only these skill IDs are considered for skill context and allowlists.
312 pub(crate) selected_skill_ids: Option<Vec<String>>,
313 /// Optional active skill mode for this execution.
314 ///
315 /// When set, skill discovery prefers `skills-<mode>` directories over generic
316 /// directories for the same skill id.
317 pub(crate) selected_skill_mode: Option<String>,
318 pub(crate) additional_tool_schemas: Vec<ToolSchema>,
319 pub(crate) tool_registry: Arc<ToolRegistry>,
320 pub(crate) composition_executor: Option<Arc<CompositionExecutor>>,
321 pub(crate) skill_manager: Option<Arc<SkillManager>>,
322 /// If true, skip appending the initial user message (already present in session).
323 pub(crate) skip_initial_user_message: bool,
324 /// Optional storage for persisting session changes
325 pub(crate) storage: Option<Arc<dyn Storage>>,
326 /// Optional runtime persistence for non-authoritative session saves.
327 /// When set, engine save sites use this instead of `storage` for writes.
328 pub(crate) persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
329 /// Optional attachment reader for resolving `bamboo-attachment://...` references
330 /// into `data:` URLs for upstream providers. This must not mutate session storage.
331 pub(crate) attachment_reader: Option<Arc<dyn AttachmentReader>>,
332 /// Optional asynchronous metrics collector
333 pub(crate) metrics_collector: Option<MetricsCollector>,
334 /// Model name used for metrics attribution
335 pub(crate) model_name: Option<String>,
336 /// Fast/cheap model for lightweight tasks (task evaluation, search, etc.).
337 ///
338 /// Call sites may fall back to `model_name` when this is unset.
339 pub(crate) fast_model_name: Option<String>,
340 /// Optional provider override for lightweight fast-model LLM calls.
341 pub(crate) fast_model_provider: Option<Arc<dyn LLMProvider>>,
342 /// Fast/cheap model for memory/background tasks.
343 ///
344 /// This must not silently fall back to the main interaction model.
345 pub(crate) background_model_name: Option<String>,
346
347 /// Model for planning/coordination tasks (task decomposition, architecture).
348 /// Falls back to `model_name` when unset.
349 pub(crate) planning_model_name: Option<String>,
350 /// Model for search/navigation tasks (grep, file listing, symbol resolution).
351 /// Falls back to `fast_model_name` when unset.
352 pub(crate) search_model_name: Option<String>,
353 /// Custom instructions for conversation summarization, injected into the
354 /// LLM summary prompt. Lets users control what the summary focuses on.
355 ///
356 /// Resolution order: session-level > config-level > built-in defaults.
357 pub(crate) compression_instructions: Option<String>,
358 /// Dedicated model for summarization. Falls back to `background_model_name`.
359 pub(crate) summarization_model_name: Option<String>,
360 /// Optional provider override for memory/background model LLM calls.
361 ///
362 /// When set, memory recall rerank and other memory/background tasks use this
363 /// provider instead of the shared agent loop provider.
364 pub(crate) background_model_provider: Option<Arc<dyn LLMProvider>>,
365 /// Optional provider override for summarization / context compression calls.
366 ///
367 /// When set, conversation/task summarization uses this provider instead of
368 /// the shared agent loop provider.
369 pub(crate) summarization_model_provider: Option<Arc<dyn LLMProvider>>,
370 /// Provider routing key used for provider-specific request behavior.
371 ///
372 /// In multi-instance mode this may be the instance id.
373 pub(crate) provider_name: Option<String>,
374 /// Underlying provider type (for example `openai`, `anthropic`, `copilot`).
375 ///
376 /// This is distinct from `provider_name` so provider-specific behavior can
377 /// remain correct when routing keys are instance ids.
378 pub(crate) provider_type: Option<String>,
379 /// Optional request-time reasoning effort override.
380 pub(crate) reasoning_effort: Option<ReasoningEffort>,
381 /// Bamboo application data directory (typically `~/.bamboo`).
382 ///
383 /// Used by runtime features that persist auxiliary artifacts outside the
384 /// session store, such as durable plan mode files under `~/.bamboo/plan`.
385 pub(crate) app_data_dir: Option<PathBuf>,
386 /// Tool names that should be excluded from schemas sent to the LLM.
387 pub(crate) disabled_tools: BTreeSet<String>,
388 /// Token budget for context management (optional, defaults to model's limits)
389 pub(crate) token_budget: Option<TokenBudget>,
390 /// Optional image fallback behavior applied to *LLM requests only* (never persisted).
391 ///
392 /// This is intended for text-only provider paths where image parts must be degraded
393 /// (placeholder / OCR / error) without leaking into stored session history or UI.
394 pub(crate) image_fallback: Option<ImageFallbackConfig>,
395 /// Feature flags controlling prompt-time memory injection behavior.
396 pub(crate) prompt_memory_flags: PromptMemoryFlags,
397 /// Maximum tool calls allowed per round (default: 80).
398 pub(crate) max_tool_calls_per_round: usize,
399 /// Maximum consecutive failures per tool before circuit breaker (default: 3).
400 pub(crate) max_consecutive_failures_per_tool: usize,
401 /// Tool names that require strict argument validation.
402 pub(crate) strict_argument_tool_names: Vec<String>,
403 /// Per-tool execution timeout in seconds (default: 120).
404 pub(crate) per_tool_timeout_secs: u64,
405 /// Parallel batch execution timeout in seconds (default: 300).
406 pub(crate) parallel_batch_timeout_secs: u64,
407 /// Permission mode for this execution (default: None = use PermissionConfig's mode).
408 pub(crate) permission_mode: Option<PermissionMode>,
409 /// Optional Gold observe-only evaluator configuration.
410 ///
411 /// When `None` or `enabled == false`, Gold evaluation is disabled and the
412 /// existing execute/respond/resume loop remains unchanged.
413 pub(crate) gold_config: Option<GoldConfig>,
414 /// Optional guardian adversarial-review gate configuration. When `None` or
415 /// `enabled == false`, the guardian terminal gate is inactive.
416 pub(crate) guardian_config: Option<GuardianConfig>,
417 /// Late-bound spawner for the guardian reviewer child. `None` (the default)
418 /// leaves the guardian gate inert even when `guardian_config.enabled` is set,
419 /// since the runner cannot create a child without it. Wired by the server.
420 pub(crate) guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
421 /// Late-bound hook that arranges a self-resume for a session suspended
422 /// waiting on background Bash shells (issue #84 Phase 2b). `None` (the
423 /// default) leaves the bash suspend gate inert: the gate refuses to suspend
424 /// without a wired hook, so a session can never strand itself without a
425 /// resume path. Wired by the server (the completion coordinator impl).
426 pub(crate) bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
427 /// Late-bound delegate that routes a child's gated-tool approval request up
428 /// to its parent (Phase 2). `None` (the default) leaves child gating on its
429 /// legacy path. Wired by the server.
430 pub(crate) approval_delegate: Option<Arc<dyn ApprovalDelegate>>,
431 /// Enable dynamic per-round model routing based on task complexity.
432 /// When true, the pipeline classifies complexity at each round end and
433 /// stores the result in session metadata.
434 pub(crate) features_dynamic_model_routing: bool,
435 /// Optional per-round resolver for auxiliary model settings that should
436 /// follow live global config rather than stay frozen for the whole run.
437 ///
438 /// The main chat model remains session/request scoped; this hook is only
439 /// for fast/background/planning/search/summarization helpers.
440 pub(crate) auxiliary_model_resolver:
441 Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
442 /// Server-level usage guidance contributed by the run's tool executor —
443 /// chiefly the `instructions` connected MCP servers return from `initialize`.
444 /// Captured once at config construction (from `ToolExecutor::tool_guidance`)
445 /// and appended to the tool-guide section of the system prompt, so a server's
446 /// own how-to-use notes appear only while that server is loaded for the run.
447 pub(crate) mcp_tool_guidance: Option<String>,
448}
449
450impl Default for AgentLoopConfig {
451 fn default() -> Self {
452 Self {
453 max_rounds: 200,
454 system_prompt: None,
455 disabled_skill_ids: BTreeSet::new(),
456 selected_skill_ids: None,
457 selected_skill_mode: None,
458 additional_tool_schemas: Vec::new(),
459 tool_registry: Arc::new(ToolRegistry::new()),
460 composition_executor: None,
461 skill_manager: None,
462 skip_initial_user_message: false,
463 storage: None,
464 persistence: None,
465 attachment_reader: None,
466 metrics_collector: None,
467 model_name: None,
468 fast_model_name: None,
469 fast_model_provider: None,
470 background_model_name: None,
471 planning_model_name: None,
472 search_model_name: None,
473 compression_instructions: None,
474 summarization_model_name: None,
475 background_model_provider: None,
476 summarization_model_provider: None,
477 provider_name: None,
478 provider_type: None,
479 reasoning_effort: None,
480 app_data_dir: None,
481 disabled_tools: BTreeSet::new(),
482 token_budget: None,
483 image_fallback: None,
484 prompt_memory_flags: PromptMemoryFlags::default(),
485 max_tool_calls_per_round: 80,
486 max_consecutive_failures_per_tool: 3,
487 strict_argument_tool_names: vec![
488 "Write".into(),
489 "Edit".into(),
490 "NotebookEdit".into(),
491 "apply_patch".into(),
492 "Bash".into(),
493 "Task".into(),
494 "SubAgent".into(),
495 "scheduler".into(),
496 "sub_session_manager".into(),
497 "session_note".into(),
498 "memory_note".into(),
499 ],
500 per_tool_timeout_secs: 120,
501 parallel_batch_timeout_secs: 300,
502 permission_mode: None,
503 gold_config: None,
504 guardian_config: None,
505 guardian_spawner: None,
506 bash_resume_hook: None,
507 approval_delegate: None,
508 features_dynamic_model_routing: false,
509 auxiliary_model_resolver: None,
510 mcp_tool_guidance: None,
511 }
512 }
513}
514
515impl AgentLoopConfig {
516 /// The active session goal to surface to the main agent, or `None` when
517 /// Gold is disabled or no goal is set. Falls back to the legacy
518 /// `evaluation_prompt` for back-compat via [`GoldConfig::effective_goal`].
519 pub fn active_goal(&self) -> Option<&str> {
520 self.gold_config
521 .as_ref()
522 .filter(|cfg| cfg.enabled)
523 .and_then(GoldConfig::effective_goal)
524 }
525
526 /// Whether the Codex-style autonomous goal loop is active for this run.
527 ///
528 /// This requires Gold to be enabled, a goal to be set, AND auto-continue to
529 /// be on. Only then is the `update_goal` self-report tool surfaced to the
530 /// model and the terminal double-check allowed to veto a premature stop.
531 /// When Gold is enabled without auto-continue, the evaluator stays purely
532 /// observational (legacy behavior).
533 pub fn goal_loop_active(&self) -> bool {
534 self.gold_config.as_ref().is_some_and(|cfg| {
535 cfg.enabled && cfg.auto_continue_enabled && cfg.effective_goal().is_some()
536 })
537 }
538
539 /// Whether the guardian review gate is active for this run: a spawner is
540 /// wired (so the runner can actually create the reviewer child) AND the
541 /// config is present and enabled.
542 pub fn guardian_active(&self) -> bool {
543 self.guardian_spawner.is_some()
544 && self.guardian_config.as_ref().is_some_and(|cfg| cfg.enabled)
545 }
546
547 /// Maximum guardian review passes for this run (the budget). `0` when no
548 /// guardian config is set.
549 pub fn guardian_max_reviews(&self) -> u32 {
550 self.guardian_config
551 .as_ref()
552 .map_or(0, |cfg| cfg.max_reviews)
553 }
554
555 /// The reviewer model override, if a guardian config sets one.
556 pub fn guardian_model(&self) -> Option<&str> {
557 self.guardian_config
558 .as_ref()
559 .and_then(|cfg| cfg.model_name.as_deref())
560 }
561
562 /// Whether child→parent approval delegation is wired for this run.
563 pub fn delegation_active(&self) -> bool {
564 self.approval_delegate.is_some()
565 }
566}
567
568#[cfg(test)]
569mod tests;