1use std::sync::Arc;
7
8use bamboo_agent_core::Session;
9
10use crate::runtime::{AgentRuntime, AgentRuntimeBuilder, ExecuteRequest};
11use bamboo_domain::RuntimeSessionPersistence;
12
13#[derive(Clone)]
25pub struct Agent {
26 runtime: Arc<AgentRuntime>,
27}
28
29pub 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 pub async fn abandon(mut self) {
44 if let Some(registration) = self.registration.take() {
45 registration.abandon().await;
46 }
47 }
48}
49
50impl Agent {
51 pub fn from_runtime(runtime: Arc<AgentRuntime>) -> Self {
53 Agent { runtime }
54 }
55
56 #[doc(hidden)]
59 pub fn with_execution_persistence(
60 &self,
61 persistence: Arc<dyn RuntimeSessionPersistence>,
62 ) -> Self {
63 let mut runtime = (*self.runtime).clone();
64 runtime.persistence = persistence;
65 runtime.inherited_child_wait_captured = true;
66 Self::from_runtime(Arc::new(runtime))
67 }
68
69 pub fn builder() -> AgentBuilder {
71 AgentBuilder::new()
72 }
73
74 pub async fn execute(
76 &self,
77 session: &mut Session,
78 req: ExecuteRequest,
79 ) -> crate::runtime::runner::Result<()> {
80 self.runtime.execute(session, req).await
81 }
82
83 pub async fn execute_direct(
94 &self,
95 session: &mut Session,
96 req: ExecuteRequest,
97 ) -> crate::runtime::runner::Result<()> {
98 let lease = self.begin_direct_execution(&session.id).await?;
99 self.prepare_external_session_for_execution(session).await?;
100 self.execute_direct_registered(session, req, lease).await
101 }
102
103 pub async fn prepare_external_session_for_execution(
111 &self,
112 session: &mut Session,
113 ) -> crate::runtime::runner::Result<()> {
114 crate::session_app::execution_prep::prepare_external_session_for_execution(
115 session,
116 self.runtime.project_context_resolver.as_deref(),
117 )
118 .await
119 }
120
121 pub async fn prepare_external_project_assignment_read_only(
125 &self,
126 session: &mut Session,
127 ) -> crate::runtime::runner::Result<()> {
128 let resolver = self
129 .runtime
130 .project_context_resolver
131 .as_deref()
132 .ok_or_else(|| {
133 bamboo_agent_core::AgentError::ProjectContext(
134 "Project assignment requires a ProjectContextResolver".to_string(),
135 )
136 })?;
137 resolver
138 .refresh_session_prompt_read_only(session)
139 .await
140 .map(|_| ())
141 .map_err(|error| bamboo_agent_core::AgentError::ProjectContext(error.to_string()))
142 }
143
144 pub async fn begin_direct_execution(
147 &self,
148 target_session_id: &str,
149 ) -> crate::runtime::runner::Result<DirectExecutionLease> {
150 let Some(router) = self.activation_router().cloned() else {
151 return Ok(DirectExecutionLease {
152 target_session_id: target_session_id.to_string(),
153 router: None,
154 registration: None,
155 });
156 };
157 let run_id = format!("sdk-direct-{}", uuid::Uuid::new_v4());
158 let registration = router
159 .register_run(target_session_id, &run_id)
160 .await
161 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
162 Ok(DirectExecutionLease {
163 target_session_id: target_session_id.to_string(),
164 router: Some(router),
165 registration: Some(registration),
166 })
167 }
168
169 pub async fn execute_direct_registered(
172 &self,
173 session: &mut Session,
174 req: ExecuteRequest,
175 lease: DirectExecutionLease,
176 ) -> crate::runtime::runner::Result<()> {
177 let inherited = self.persistence().inherited_child_wait().or_else(|| {
178 (!self.runtime.inherited_child_wait_captured)
179 .then(|| bamboo_domain::InheritedChildWait::capture(session))
180 .flatten()
181 });
182 let agent = if let Some(inherited) = inherited {
183 inherited
184 .validate_session(session)
185 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?;
186 if self.persistence().inherited_child_wait().is_some() {
187 self.clone()
188 } else {
189 self.with_execution_persistence(
190 self.persistence()
191 .bind_inherited_child_wait(inherited)
192 .map_err(|error| bamboo_agent_core::AgentError::LLM(error.to_string()))?,
193 )
194 }
195 } else {
196 self.with_execution_persistence(self.persistence().clone())
197 };
198 agent
199 .execute_direct_registered_bound(session, req, lease)
200 .await
201 }
202
203 async fn execute_direct_registered_bound(
204 &self,
205 session: &mut Session,
206 req: ExecuteRequest,
207 mut lease: DirectExecutionLease,
208 ) -> crate::runtime::runner::Result<()> {
209 if lease.target_session_id != session.id {
210 return Err(bamboo_agent_core::AgentError::LLM(format!(
211 "direct execution lease target {} does not match session {}",
212 lease.target_session_id, session.id
213 )));
214 }
215 let Some(router) = lease.router.take() else {
216 return self.execute(session, req).await;
217 };
218 let mut registration = lease.registration.take().ok_or_else(|| {
219 bamboo_agent_core::AgentError::LLM(
220 "direct execution lease is missing its router registration".to_string(),
221 )
222 })?;
223 let result = self.execute(session, req).await;
224
225 let executed_admitted_generation = session
228 .session_inbox_admission()
229 .map_or(0, |state| state.last_admitted_sequence);
230 registration.begin_finalization().await;
231
232 let legacy_migration = crate::runtime::runner::state_bridge::migrate_legacy_pending_only(
233 session,
234 Some(self.storage()),
235 Some(self.persistence()),
236 self.session_inbox(),
237 )
238 .await;
239 if let Some(generation) = legacy_migration.highest_generation {
240 session.session_inbox_admission_mut().observe(generation);
241 }
242 let pending_generation = session
243 .session_inbox_admission()
244 .and_then(|state| state.pending_activation_generation());
245 if let Some(generation) = pending_generation {
246 let activation_ready = if let Some(inbox) = self.session_inbox() {
247 match inbox
248 .mark_activation_eligible(
249 &session.id,
250 generation,
251 bamboo_domain::SessionActivationPolicy::InterruptSpecificWait,
252 )
253 .await
254 {
255 Ok(()) => true,
256 Err(error) => {
257 tracing::error!(
258 session_id = %session.id,
259 %error,
260 "failed to persist direct SDK SessionInbox activation watermark"
261 );
262 false
263 }
264 }
265 } else {
266 false
267 };
268 if activation_ready {
269 if let Err(error) = bamboo_domain::SessionActivationPort::request_activation(
270 router.as_ref(),
271 &session.id,
272 generation,
273 )
274 .await
275 {
276 tracing::error!(
277 session_id = %session.id,
278 %error,
279 "failed to hand direct SDK SessionInbox generation to activation router"
280 );
281 }
282 }
283 }
284
285 if let Err(error) = self.persistence().checkpoint_runtime_session(session).await {
288 tracing::warn!(
289 session_id = %session.id,
290 %error,
291 "failed to checkpoint direct SDK terminal SessionInbox state"
292 );
293 }
294 if let Err(error) = registration.finish(executed_admitted_generation).await {
295 tracing::error!(
296 session_id = %session.id,
297 %error,
298 "direct SDK SessionInbox finalization failed"
299 );
300 }
301
302 result
303 }
304
305 pub fn storage(&self) -> &Arc<dyn bamboo_agent_core::storage::Storage> {
307 &self.runtime.storage
308 }
309
310 pub fn persistence(&self) -> &Arc<dyn RuntimeSessionPersistence> {
312 &self.runtime.persistence
313 }
314
315 pub fn session_inbox(&self) -> Option<&Arc<dyn bamboo_domain::SessionInboxPort>> {
316 self.runtime.session_inbox.as_ref()
317 }
318
319 pub async fn admit_session_inbox_at_safe_boundary(
324 &self,
325 session: &mut bamboo_agent_core::Session,
326 ) -> usize {
327 crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
328 session,
329 Some(self.storage()),
330 Some(self.persistence()),
331 self.session_inbox(),
332 )
333 .await
334 .merged
335 }
336
337 pub async fn admit_session_inbox_at_safe_boundary_checked(
341 &self,
342 session: &mut bamboo_agent_core::Session,
343 ) -> Result<usize, bamboo_agent_core::AgentError> {
344 let refresh = crate::runtime::runner::state_bridge::refresh_turn_boundary_with_inbox(
345 session,
346 Some(self.storage()),
347 Some(self.persistence()),
348 self.session_inbox(),
349 )
350 .await;
351 if let Some(error) = refresh.admission_error {
352 return Err(bamboo_agent_core::AgentError::Tool(error));
353 }
354 Ok(refresh.merged)
355 }
356
357 pub fn activation_router(
358 &self,
359 ) -> Option<&Arc<crate::session_activation::SessionActivationRouter>> {
360 self.runtime.activation_router.as_ref()
361 }
362
363 pub fn session_messenger(&self) -> Option<&Arc<crate::SessionMessenger>> {
364 self.runtime.session_messenger.as_ref()
365 }
366
367 pub fn default_tools(&self) -> &Arc<dyn bamboo_agent_core::tools::ToolExecutor> {
377 &self.runtime.default_tools
378 }
379}
380
381pub struct AgentBuilder {
389 inner: AgentRuntimeBuilder,
390}
391
392impl AgentBuilder {
393 pub fn new() -> Self {
394 Self {
395 inner: AgentRuntimeBuilder::new(),
396 }
397 }
398
399 pub fn storage(mut self, v: Arc<dyn bamboo_agent_core::storage::Storage>) -> Self {
400 self.inner = self.inner.storage(v);
401 self
402 }
403
404 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
405 self.inner = self.inner.persistence(v);
406 self
407 }
408
409 pub fn session_inbox(mut self, v: Arc<dyn bamboo_domain::SessionInboxPort>) -> Self {
410 self.inner = self.inner.session_inbox(v);
411 self
412 }
413
414 pub fn activation_router(
415 mut self,
416 v: Arc<crate::session_activation::SessionActivationRouter>,
417 ) -> Self {
418 self.inner = self.inner.activation_router(v);
419 self
420 }
421
422 pub fn session_messenger(mut self, v: Arc<crate::SessionMessenger>) -> Self {
423 self.inner = self.inner.session_messenger(v);
424 self
425 }
426
427 pub fn attachment_reader(
428 mut self,
429 v: Arc<dyn bamboo_agent_core::storage::AttachmentReader>,
430 ) -> Self {
431 self.inner = self.inner.attachment_reader(v);
432 self
433 }
434
435 pub fn skill_manager(mut self, v: Arc<bamboo_skills::SkillManager>) -> Self {
436 self.inner = self.inner.skill_manager(v);
437 self
438 }
439
440 pub fn project_context_resolver(
441 mut self,
442 v: Arc<crate::project_context::ProjectContextResolver>,
443 ) -> Self {
444 self.inner = self.inner.project_context_resolver(v);
445 self
446 }
447
448 pub fn metrics_collector(mut self, v: bamboo_metrics::MetricsCollector) -> Self {
449 self.inner = self.inner.metrics_collector(v);
450 self
451 }
452
453 pub fn config(mut self, v: Arc<tokio::sync::RwLock<bamboo_llm::Config>>) -> Self {
454 self.inner = self.inner.config(v);
455 self
456 }
457
458 pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
459 self.inner = self.inner.permission_config(v);
460 self
461 }
462
463 pub fn permission_mode(mut self, v: bamboo_domain::PermissionMode) -> Self {
464 self.inner = self.inner.permission_mode(v);
465 self
466 }
467
468 pub fn provider(mut self, v: Arc<dyn bamboo_llm::LLMProvider>) -> Self {
469 self.inner = self.inner.provider(v);
470 self
471 }
472
473 pub fn memory_store(mut self, v: bamboo_memory::memory_store::MemoryStore) -> Self {
474 self.inner = self.inner.memory_store(v);
475 self
476 }
477
478 pub fn default_tools(mut self, v: Arc<dyn bamboo_agent_core::tools::ToolExecutor>) -> Self {
479 self.inner = self.inner.default_tools(v);
480 self
481 }
482
483 pub fn hook_runner(mut self, v: Arc<crate::runtime::HookRunner>) -> Self {
485 self.inner = self.inner.hook_runner(v);
486 self
487 }
488
489 pub fn build(self) -> Result<Agent, &'static str> {
490 let runtime = self.inner.build()?;
491 Ok(Agent {
492 runtime: Arc::new(runtime),
493 })
494 }
495}
496
497impl Default for AgentBuilder {
498 fn default() -> Self {
499 Self::new()
500 }
501}