Skip to main content

bamboo_engine/runtime/
agent.rs

1//! Stable public API for the agent runtime.
2//!
3//! [`Agent`] wraps an [`AgentRuntime`] with method-based access and serves as
4//! the primary entry point for SDK consumers.
5
6use std::sync::Arc;
7
8use bamboo_agent_core::Session;
9
10use crate::runtime::{AgentRuntime, AgentRuntimeBuilder, ExecuteRequest};
11use bamboo_domain::RuntimeSessionPersistence;
12
13// ---------------------------------------------------------------------------
14// Agent — stable public object
15// ---------------------------------------------------------------------------
16
17/// Stable public entry point for agent execution.
18///
19/// Wraps an [`AgentRuntime`] and provides:
20/// - [`Agent::execute()`] — run the agent loop on a session
21/// - [`Agent::storage()`] — access the shared storage backend
22///
23/// Clone is cheap (inner is `Arc`).
24#[derive(Clone)]
25pub struct Agent {
26    runtime: Arc<AgentRuntime>,
27}
28
29/// Opaque ownership lease for one direct logical-session execution.
30///
31/// SDK facades acquire this before any pre-execution side effect, then transfer
32/// it into [`Agent::execute_direct_registered`]. Dropping it early invokes the
33/// same abandoned-owner recovery as cancellation during provider execution.
34pub struct DirectExecutionLease {
35    target_session_id: String,
36    router: Option<Arc<crate::session_activation::SessionActivationRouter>>,
37    registration: Option<crate::session_activation::SessionRunRegistration>,
38}
39
40impl DirectExecutionLease {
41    /// Release ownership before returning a handled pre-execution stop to an
42    /// SDK caller, so its next resume does not race the asynchronous Drop path.
43    pub async fn abandon(mut self) {
44        if let Some(registration) = self.registration.take() {
45            registration.abandon().await;
46        }
47    }
48}
49
50/// Opaque, single-use SDK submission receipt. The owned User is the genuine
51/// portable-hook result; subsequent F preparation may change only its prompt.
52pub struct SdkInputSubmission {
53    session_id: String,
54    execution_id: String,
55    user: bamboo_agent_core::Message,
56    hook_runner: Arc<crate::runtime::hooks::HookRunner>,
57}
58
59impl SdkInputSubmission {
60    pub fn execution_id(&self) -> &str {
61        &self.execution_id
62    }
63    pub fn user(&self) -> &bamboo_agent_core::Message {
64        &self.user
65    }
66}
67
68impl Agent {
69    /// Wrap an existing [`AgentRuntime`] in an `Agent`.
70    pub fn from_runtime(runtime: Arc<AgentRuntime>) -> Self {
71        Agent { runtime }
72    }
73
74    /// One execution's immutable persistence capability. Shared tool/provider
75    /// resources remain on the existing runtime; default callers stay unbound.
76    #[doc(hidden)]
77    pub fn with_execution_persistence(
78        &self,
79        persistence: Arc<dyn RuntimeSessionPersistence>,
80    ) -> Self {
81        let mut runtime = (*self.runtime).clone();
82        runtime.persistence = persistence;
83        runtime.inherited_child_wait_captured = true;
84        Self::from_runtime(Arc::new(runtime))
85    }
86
87    /// Install the registered SDK adapter on this execution's runtime only.
88    #[doc(hidden)]
89    pub fn with_sdk_skill_execution_host(
90        &self,
91        host: Arc<dyn crate::runtime::config::SdkSkillExecutionHost>,
92    ) -> Self {
93        self.with_skill_execution_host(host)
94    }
95
96    /// Attach an execution-local registered host; this creates no caller authority.
97    #[doc(hidden)]
98    pub fn with_skill_execution_host(
99        &self,
100        host: Arc<dyn crate::runtime::config::SkillExecutionHost>,
101    ) -> Self {
102        let mut runtime = (*self.runtime).clone();
103        runtime.sdk_skill_execution_host = Some(host);
104        Self::from_runtime(Arc::new(runtime))
105    }
106
107    /// Run the genuine portable submission hook before F and append. No old
108    /// metadata receipt may skip this new submission's hook.
109    #[doc(hidden)]
110    pub async fn precheck_sdk_user_input(
111        &self,
112        session: &mut Session,
113        user: &bamboo_agent_core::Message,
114        app_data_dir: Option<std::path::PathBuf>,
115    ) -> crate::runtime::runner::Result<SdkInputSubmission> {
116        if user.role != bamboo_agent_core::Role::User
117            || crate::runtime::config::UntrustedInputObservation::new(&user.id, None).is_none()
118            || session.messages.iter().any(|message| message.id == user.id)
119        {
120            return Err(bamboo_agent_core::AgentError::Tool(
121                "invalid new SDK User".into(),
122            ));
123        }
124        let hook_runner = {
125            let config = self.runtime.config.read().await;
126            Arc::new(
127                self.runtime
128                    .hook_runner
129                    .with_lifecycle_config(&config.lifecycle_hooks, app_data_dir),
130            )
131        };
132        session.metadata.remove("runtime.plugin_prompt_prechecked");
133        let prompt = hook_runner
134            .apply_portable_user_prompt(session, &user.content)
135            .await?;
136        let mut transformed = user.clone();
137        transformed.content = prompt.clone();
138        if let Some(parts) = transformed.content_parts.as_mut() {
139            if let Some(bamboo_domain::MessagePart::Text { text }) =
140                parts.iter_mut().find(|part| matches!(part, bamboo_domain::MessagePart::Text { text } if text == &user.content))
141            {
142                *text = prompt;
143            }
144        }
145        Ok(SdkInputSubmission {
146            session_id: session.id.clone(),
147            execution_id: crate::runtime::runner::round_prelude::new_execution_id(),
148            user: transformed,
149            hook_runner,
150        })
151    }
152
153    /// Actual SDK producer append. The private pending receipt becomes checked
154    /// only after startup hooks and a durable same-repository checkpoint.
155    #[doc(hidden)]
156    pub fn append_sdk_user_input(
157        &self,
158        session: &mut Session,
159        user: bamboo_agent_core::Message,
160        receipt: Option<SdkInputSubmission>,
161        request: Option<bamboo_domain::SessionSkillRequest>,
162    ) -> crate::runtime::runner::Result<crate::runtime::config::UntrustedExecutionInputs> {
163        use crate::runtime::config::{
164            sdk_message_digest, sdk_message_header_digest, SdkPendingInput,
165            UntrustedExecutionInputs, UntrustedInputObservation,
166        };
167        let invalid = || bamboo_agent_core::AgentError::Tool("invalid SDK append receipt".into());
168        let header_digest = sdk_message_header_digest(&user);
169        let input_id = user.id.clone();
170        if let Some(receipt) = receipt.as_ref() {
171            if receipt.session_id != session.id
172                || receipt.user.role != bamboo_agent_core::Role::User
173                || user.role != bamboo_agent_core::Role::User
174                || sdk_message_header_digest(&receipt.user) != header_digest
175                || session
176                    .messages
177                    .iter()
178                    .any(|message| message.id == input_id)
179            {
180                return Err(invalid());
181            }
182        }
183        // Preserve the legacy String append even when optional observation data
184        // is malformed. Typed callers fail before append on an invalid receipt.
185        let final_digest = receipt.as_ref().map(|_| sdk_message_digest(&user));
186        if receipt.is_some() {
187            crate::runtime::hooks::HookRunner::mark_user_prompt_prechecked(session, &user.content);
188        }
189        session.add_message(user);
190        let observation =
191            UntrustedInputObservation::new(&input_id, request.as_ref()).ok_or_else(invalid)?;
192        let carrier = UntrustedExecutionInputs::new(vec![observation]).ok_or_else(invalid)?;
193        let (execution_id, hook_runner) = receipt
194            .map(|receipt| (receipt.execution_id, Some(receipt.hook_runner)))
195            .unwrap_or_else(|| {
196                (
197                    crate::runtime::runner::round_prelude::new_execution_id(),
198                    None,
199                )
200            });
201        Ok(carrier.bind_sdk_append(
202            execution_id,
203            SdkPendingInput {
204                session_id: session.id.clone(),
205                input_id,
206                header_digest,
207                final_digest,
208            },
209            hook_runner,
210        ))
211    }
212
213    /// Return a new builder.
214    pub fn builder() -> AgentBuilder {
215        AgentBuilder::new()
216    }
217
218    /// Execute the agent loop with the given request.
219    pub async fn execute(
220        &self,
221        session: &mut Session,
222        req: ExecuteRequest,
223    ) -> crate::runtime::runner::Result<()> {
224        self.runtime.execute(session, req).await
225    }
226
227    /// Explicit bounded caller-data handoff; no currentness or Skill authority.
228    /// Ordinary execute/direct/spawn entrypoints continue to supply None.
229    pub async fn execute_with_inputs(
230        &self,
231        session: &mut Session,
232        req: ExecuteRequest,
233        inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
234    ) -> crate::runtime::runner::Result<()> {
235        self.runtime.execute_with_inputs(session, req, inputs).await
236    }
237
238    /// Execute a caller-owned session under a complete logical-session
239    /// activation lifecycle.
240    ///
241    /// Server/child entry points already own an external runner reservation and
242    /// therefore use [`execute`](Self::execute) plus their existing terminal
243    /// handshake. Direct SDK callers have no runner registry, so this wrapper
244    /// registers the current run before entering the provider loop, marks it
245    /// finalizing immediately after return, migrates any terminal-window legacy
246    /// ingress, and lets the router reserve at most one successor for work the
247    /// completed reasoning turn did not admit.
248    pub async fn execute_direct(
249        &self,
250        session: &mut Session,
251        req: ExecuteRequest,
252    ) -> crate::runtime::runner::Result<()> {
253        let lease = self.begin_direct_execution(&session.id).await?;
254        self.check_direct_execution_admission(session, &lease)
255            .await?;
256        self.prepare_external_session_for_execution(session).await?;
257        self.execute_direct_registered(session, req, lease).await
258    }
259
260    /// Check counted-state admission under an already acquired opaque lease.
261    /// SDK facades call this before Project preparation or approved-tool replay.
262    /// This can refuse/fence an interrupted generation; it never replays it.
263    pub fn check_direct_execution_admission<'a>(
264        &'a self,
265        session: &'a mut Session,
266        lease: &'a DirectExecutionLease,
267    ) -> impl std::future::Future<Output = crate::runtime::runner::Result<()>> + Send + 'a {
268        let matches_target = lease.target_session_id == session.id;
269        // Keep the lease borrowed for this future's lifetime without carrying
270        // its non-Sync FnOnce cleanup across await. Only real private proof
271        // enters the Send future; it is revalidated against the current owner.
272        let ownership = lease.registration.as_ref().map(|run| run.ownership());
273        async move {
274            if !matches_target {
275                return Err(bamboo_agent_core::AgentError::LLM(
276                    "direct execution lease target does not match session".into(),
277                ));
278            }
279            super::counted_loop::admit_registered(
280                session,
281                ownership.as_ref(),
282                self.activation_router(),
283                self.persistence().as_ref(),
284            )
285            .await
286        }
287    }
288
289    /// Validate and publish Project/Workspace context for a caller-owned
290    /// session before any external pre-execution side effect.
291    ///
292    /// SDK facades that acquire a direct lease themselves call this before
293    /// approved-tool replay. [`execute_direct`](Self::execute_direct) also calls
294    /// it, so the lower-level escape hatch cannot bypass legacy migration,
295    /// Project validation, or runtime workspace publication.
296    pub async fn prepare_external_session_for_execution(
297        &self,
298        session: &mut Session,
299    ) -> crate::runtime::runner::Result<()> {
300        crate::session_app::execution_prep::prepare_external_session_for_execution(
301            session,
302            self.runtime.project_context_resolver.as_deref(),
303        )
304        .await
305    }
306
307    /// Resolve a proposed SDK Project assignment without publishing a runtime
308    /// workspace. The caller must persist the validated candidate before the
309    /// ordinary execution handoff publishes it or replays an approved tool.
310    pub async fn prepare_external_project_assignment_read_only(
311        &self,
312        session: &mut Session,
313    ) -> crate::runtime::runner::Result<()> {
314        let resolver = self
315            .runtime
316            .project_context_resolver
317            .as_deref()
318            .ok_or_else(|| {
319                bamboo_agent_core::AgentError::ProjectContext(
320                    "Project assignment requires a ProjectContextResolver".to_string(),
321                )
322            })?;
323        resolver
324            .refresh_session_prompt_read_only(session)
325            .await
326            .map(|_| ())
327            .map_err(|error| bamboo_agent_core::AgentError::ProjectContext(error.to_string()))
328    }
329
330    /// Acquire direct logical-session ownership before an SDK facade performs
331    /// pre-execution work such as replaying an approved mutating tool.
332    pub async fn begin_direct_execution(
333        &self,
334        target_session_id: &str,
335    ) -> crate::runtime::runner::Result<DirectExecutionLease> {
336        let Some(router) = self.activation_router().cloned() else {
337            return Ok(DirectExecutionLease {
338                target_session_id: target_session_id.to_string(),
339                router: None,
340                registration: None,
341            });
342        };
343        let run_id = format!("sdk-direct-{}", uuid::Uuid::new_v4());
344        let registration = router
345            .register_run(target_session_id, &run_id)
346            .await
347            .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
348        Ok(DirectExecutionLease {
349            target_session_id: target_session_id.to_string(),
350            router: Some(router),
351            registration: Some(registration),
352        })
353    }
354
355    /// Execute and finalize a direct run whose ownership was acquired by
356    /// [`begin_direct_execution`](Self::begin_direct_execution).
357    pub async fn execute_direct_registered(
358        &self,
359        session: &mut Session,
360        req: ExecuteRequest,
361        lease: DirectExecutionLease,
362    ) -> crate::runtime::runner::Result<()> {
363        self.execute_direct_registered_with_inputs(session, req, lease, None)
364            .await
365    }
366
367    /// Existing direct lifecycle with a separately owned, untrusted input handoff.
368    pub async fn execute_direct_registered_with_inputs(
369        &self,
370        session: &mut Session,
371        req: ExecuteRequest,
372        lease: DirectExecutionLease,
373        inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
374    ) -> crate::runtime::runner::Result<()> {
375        self.check_direct_execution_admission(session, &lease)
376            .await?;
377        let inherited = self.persistence().inherited_child_wait().or_else(|| {
378            (!self.runtime.inherited_child_wait_captured)
379                .then(|| bamboo_domain::InheritedChildWait::capture(session))
380                .flatten()
381        });
382        let agent = if let Some(inherited) = inherited {
383            inherited
384                .validate_session(session)
385                .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
386            if self.persistence().inherited_child_wait().is_some() {
387                self.clone()
388            } else {
389                self.with_execution_persistence(
390                    self.persistence()
391                        .bind_inherited_child_wait(inherited)
392                        .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?,
393                )
394            }
395        } else {
396            self.with_execution_persistence(self.persistence().clone())
397        };
398        agent
399            .execute_direct_registered_bound(session, req, lease, inputs)
400            .await
401    }
402
403    async fn execute_direct_registered_bound(
404        &self,
405        session: &mut Session,
406        mut req: ExecuteRequest,
407        mut lease: DirectExecutionLease,
408        inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
409    ) -> crate::runtime::runner::Result<()> {
410        if lease.target_session_id != session.id {
411            return Err(bamboo_agent_core::AgentError::LLM(format!(
412                "direct execution lease target {} does not match session {}",
413                lease.target_session_id, session.id
414            )));
415        }
416        let Some(router) = lease.router.take() else {
417            return self.execute_with_inputs(session, req, inputs).await;
418        };
419        let mut registration = lease.registration.take().ok_or_else(|| {
420            bamboo_agent_core::AgentError::LLM(
421                "direct execution lease is missing its router registration".to_string(),
422            )
423        })?;
424        if let Some(counted) = req.counted_loop.as_mut() {
425            counted.bind_owner(registration.ownership(), session);
426        }
427        let result = self.execute_with_inputs(session, req, inputs).await;
428
429        // Freeze what this provider execution actually consumed. Compatibility
430        // migration and concurrent deliveries below must remain newer work.
431        let executed_admitted_generation = session
432            .session_inbox_admission()
433            .map_or(0, |state| state.last_admitted_sequence);
434        registration.begin_finalization().await;
435
436        let legacy_migration = crate::runtime::runner::state_bridge::migrate_legacy_pending_only(
437            session,
438            Some(self.storage()),
439            Some(self.persistence()),
440            self.session_inbox(),
441        )
442        .await;
443        if let Some(generation) = legacy_migration.highest_generation {
444            session.session_inbox_admission_mut().observe(generation);
445        }
446        let pending_generation = session
447            .session_inbox_admission()
448            .and_then(|state| state.pending_activation_generation());
449        if let Some(generation) = pending_generation {
450            let activation_ready = if let Some(inbox) = self.session_inbox() {
451                match inbox
452                    .mark_activation_eligible(
453                        &session.id,
454                        generation,
455                        bamboo_domain::SessionActivationPolicy::InterruptSpecificWait,
456                    )
457                    .await
458                {
459                    Ok(()) => true,
460                    Err(error) => {
461                        tracing::error!(
462                            session_id = %session.id,
463                            %error,
464                            "failed to persist direct SDK SessionInbox activation watermark"
465                        );
466                        false
467                    }
468                }
469            } else {
470                false
471            };
472            if activation_ready {
473                if let Err(error) = bamboo_domain::SessionActivationPort::request_activation(
474                    router.as_ref(),
475                    &session.id,
476                    generation,
477                )
478                .await
479                {
480                    tracing::error!(
481                        session_id = %session.id,
482                        %error,
483                        "failed to hand direct SDK SessionInbox generation to activation router"
484                    );
485                }
486            }
487        }
488
489        // Keep the owner receiver alive until finalizing is visible. Persist
490        // the observed-generation marker before a successor can start.
491        if let Err(error) = self.persistence().checkpoint_runtime_session(session).await {
492            tracing::warn!(
493                session_id = %session.id,
494                %error,
495                "failed to checkpoint direct SDK terminal SessionInbox state"
496            );
497        }
498        if let Err(error) = registration.finish(executed_admitted_generation).await {
499            tracing::error!(
500                session_id = %session.id,
501                %error,
502                "direct SDK SessionInbox finalization failed"
503            );
504        }
505
506        result
507    }
508
509    /// Access the shared storage backend.
510    pub fn storage(&self) -> &Arc<dyn bamboo_agent_core::storage::Storage> {
511        &self.runtime.storage
512    }
513
514    /// Access the runtime persistence adapter for non-authoritative saves.
515    pub fn persistence(&self) -> &Arc<dyn RuntimeSessionPersistence> {
516        &self.runtime.persistence
517    }
518
519    pub fn session_inbox(&self) -> Option<&Arc<dyn bamboo_domain::SessionInboxPort>> {
520        self.runtime.session_inbox.as_ref()
521    }
522
523    /// Execute the same durable SessionInbox boundary used by the agent loop
524    /// before its first provider call. This compatibility method reports only
525    /// the merge count; it cannot confirm successful ACK. Execution entry points
526    /// must use [`Self::admit_session_inbox_at_safe_boundary_checked`] instead.
527    pub async fn admit_session_inbox_at_safe_boundary(
528        &self,
529        session: &mut bamboo_agent_core::Session,
530    ) -> usize {
531        crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
532            session,
533            Some(self.storage()),
534            Some(self.persistence()),
535            self.session_inbox(),
536        )
537        .await
538        .merged
539    }
540
541    /// Confirm the durable admission boundary before entering provider execution.
542    /// An ACK error is unresolved even after receipt publication. Preserve the
543    /// checkpoint and existing claim recovery, but reject this activation.
544    pub async fn admit_session_inbox_at_safe_boundary_checked(
545        &self,
546        session: &mut bamboo_agent_core::Session,
547    ) -> Result<usize, bamboo_agent_core::AgentError> {
548        let refresh = crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
549            session,
550            Some(self.storage()),
551            Some(self.persistence()),
552            self.session_inbox(),
553        )
554        .await;
555        if let Some(error) = refresh.admission_error {
556            return Err(bamboo_agent_core::AgentError::Tool(error));
557        }
558        Ok(refresh.merged)
559    }
560
561    pub fn activation_router(
562        &self,
563    ) -> Option<&Arc<crate::session_activation::SessionActivationRouter>> {
564        self.runtime.activation_router.as_ref()
565    }
566
567    pub fn session_messenger(&self) -> Option<&Arc<crate::SessionMessenger>> {
568        self.runtime.session_messenger.as_ref()
569    }
570
571    /// Access the runtime's default tool executor (the root/full tool surface
572    /// assembled at build time).
573    ///
574    /// Exposed so callers can compose additional one-off dispatches against the
575    /// SAME executor the loop itself uses — e.g. re-executing a single
576    /// previously-gated tool call after a permission approval — without forking
577    /// or reaching into `AgentLoopConfig` (which stays unconstructible outside
578    /// the engine). This is a read-only accessor alongside `storage()` /
579    /// `persistence()`; it does not touch the sealed loop config.
580    pub fn default_tools(&self) -> &Arc<dyn bamboo_agent_core::tools::ToolExecutor> {
581        &self.runtime.default_tools
582    }
583}
584
585// ---------------------------------------------------------------------------
586// AgentBuilder
587// ---------------------------------------------------------------------------
588
589/// Builder for [`Agent`].
590///
591/// Delegates to [`AgentRuntimeBuilder`] internally.
592pub struct AgentBuilder {
593    inner: AgentRuntimeBuilder,
594}
595
596impl AgentBuilder {
597    pub fn new() -> Self {
598        Self {
599            inner: AgentRuntimeBuilder::new(),
600        }
601    }
602
603    pub fn storage(mut self, v: Arc<dyn bamboo_agent_core::storage::Storage>) -> Self {
604        self.inner = self.inner.storage(v);
605        self
606    }
607
608    pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
609        self.inner = self.inner.persistence(v);
610        self
611    }
612
613    pub fn session_inbox(mut self, v: Arc<dyn bamboo_domain::SessionInboxPort>) -> Self {
614        self.inner = self.inner.session_inbox(v);
615        self
616    }
617
618    pub fn activation_router(
619        mut self,
620        v: Arc<crate::session_activation::SessionActivationRouter>,
621    ) -> Self {
622        self.inner = self.inner.activation_router(v);
623        self
624    }
625
626    pub fn session_messenger(mut self, v: Arc<crate::SessionMessenger>) -> Self {
627        self.inner = self.inner.session_messenger(v);
628        self
629    }
630
631    pub fn attachment_reader(
632        mut self,
633        v: Arc<dyn bamboo_agent_core::storage::AttachmentReader>,
634    ) -> Self {
635        self.inner = self.inner.attachment_reader(v);
636        self
637    }
638
639    pub fn skill_manager(mut self, v: Arc<bamboo_skills::SkillManager>) -> Self {
640        self.inner = self.inner.skill_manager(v);
641        self
642    }
643
644    pub fn project_context_resolver(
645        mut self,
646        v: Arc<crate::project_context::ProjectContextResolver>,
647    ) -> Self {
648        self.inner = self.inner.project_context_resolver(v);
649        self
650    }
651
652    pub fn metrics_collector(mut self, v: bamboo_metrics::MetricsCollector) -> Self {
653        self.inner = self.inner.metrics_collector(v);
654        self
655    }
656
657    pub fn config(mut self, v: Arc<tokio::sync::RwLock<bamboo_llm::Config>>) -> Self {
658        self.inner = self.inner.config(v);
659        self
660    }
661
662    /// Resolve model limits from a host-owned last-known-good section on each
663    /// round instead of reopening the instance's configuration file.
664    pub fn model_limits_section(
665        mut self,
666        section: Arc<bamboo_config::LiveSection<bamboo_config::ModelLimitsSection>>,
667    ) -> Self {
668        self.inner = self.inner.model_limits_section(section);
669        self
670    }
671
672    pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
673        self.inner = self.inner.permission_config(v);
674        self
675    }
676
677    pub fn permission_mode(mut self, v: bamboo_domain::PermissionMode) -> Self {
678        self.inner = self.inner.permission_mode(v);
679        self
680    }
681
682    pub fn provider(mut self, v: Arc<dyn bamboo_llm::LLMProvider>) -> Self {
683        self.inner = self.inner.provider(v);
684        self
685    }
686
687    pub fn memory_store(mut self, v: bamboo_memory::memory_store::MemoryStore) -> Self {
688        self.inner = self.inner.memory_store(v);
689        self
690    }
691
692    pub fn default_tools(mut self, v: Arc<dyn bamboo_agent_core::tools::ToolExecutor>) -> Self {
693        self.inner = self.inner.default_tools(v);
694        self
695    }
696
697    /// Install an immutable lifecycle-hook registry for this agent runtime.
698    pub fn hook_runner(mut self, v: Arc<crate::runtime::HookRunner>) -> Self {
699        self.inner = self.inner.hook_runner(v);
700        self
701    }
702
703    pub fn build(self) -> Result<Agent, &'static str> {
704        let runtime = self.inner.build()?;
705        Ok(Agent {
706            runtime: Arc::new(runtime),
707        })
708    }
709}
710
711impl Default for AgentBuilder {
712    fn default() -> Self {
713        Self::new()
714    }
715}