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