adk_runner/runner.rs
1use crate::InvocationContext;
2use crate::cache::CacheManager;
3#[cfg(feature = "artifacts")]
4use adk_artifact::ArtifactService;
5use adk_core::{
6 Agent, AppName, CacheCapable, Content, ContextCacheConfig, EventStream, Memory,
7 ReadonlyContext, Result, RunConfig, SessionId, UserId,
8};
9#[cfg(feature = "plugins")]
10use adk_plugin::PluginManager;
11use adk_session::SessionService;
12#[cfg(feature = "skills")]
13use adk_skill::{SkillInjector, SkillInjectorConfig};
14use async_stream::stream;
15use std::sync::Arc;
16use tokio_util::sync::CancellationToken;
17
18/// A run currently in flight, tracked so it can be interrupted.
19///
20/// Keyed by run ID rather than session ID: a session ID is only unique within an
21/// app and user, and one identity may have several runs in flight at once.
22#[derive(Debug, Clone)]
23struct ActiveRun {
24 identity: adk_core::AdkIdentity,
25 token: CancellationToken,
26}
27
28/// Registry of in-flight runs, keyed by run ID.
29type ActiveRuns = Arc<std::sync::Mutex<std::collections::HashMap<u64, ActiveRun>>>;
30
31/// Deregisters a run when its event stream is dropped.
32///
33/// Removal is by run ID, so a finishing run can never deregister a different run
34/// that happens to share the same session ID.
35struct ActiveRunCleanup {
36 active_runs: ActiveRuns,
37 run_id: u64,
38}
39
40impl Drop for ActiveRunCleanup {
41 fn drop(&mut self) {
42 let mut runs = self.active_runs.lock().unwrap_or_else(|e| e.into_inner());
43 runs.remove(&self.run_id);
44 }
45}
46use tracing::Instrument;
47
48/// Configuration for constructing a [`Runner`].
49///
50/// Use [`Runner::builder()`] for a compile-time-safe way to construct this.
51pub struct RunnerConfig {
52 /// Application name used for session scoping.
53 pub app_name: String,
54 /// The root agent to execute.
55 pub agent: Arc<dyn Agent>,
56 /// Session persistence backend.
57 pub session_service: Arc<dyn SessionService>,
58 #[cfg(feature = "artifacts")]
59 /// Optional artifact storage service.
60 pub artifact_service: Option<Arc<dyn ArtifactService>>,
61 /// Optional memory/RAG service.
62 pub memory_service: Option<Arc<dyn Memory>>,
63 #[cfg(feature = "plugins")]
64 /// Optional plugin manager for lifecycle hooks.
65 pub plugin_manager: Option<Arc<PluginManager>>,
66 /// Optional run configuration (streaming mode, etc.)
67 /// If not provided, uses default (SSE streaming)
68 #[allow(dead_code)]
69 pub run_config: Option<RunConfig>,
70 /// Optional context compaction configuration.
71 /// When set, the runner will periodically summarize older events
72 /// to reduce context size sent to the LLM.
73 pub compaction_config: Option<adk_core::EventsCompactionConfig>,
74 /// Optional context cache configuration for automatic prompt caching lifecycle.
75 /// When set alongside `cache_capable`, the runner will automatically create and
76 /// manage cached content resources for supported providers.
77 ///
78 /// When `cache_capable` is set but this field is `None`, the runner
79 /// automatically uses [`ContextCacheConfig::default()`] (4096 min tokens,
80 /// 600s TTL, refresh every 3 invocations).
81 pub context_cache_config: Option<ContextCacheConfig>,
82 /// Optional cache-capable model reference for automatic cache management.
83 /// Set this to the same model used by the agent if it supports caching.
84 pub cache_capable: Option<Arc<dyn CacheCapable>>,
85 /// Optional request context from the server's auth middleware bridge.
86 /// When set, the runner passes it to `InvocationContext` so that
87 /// `user_scopes()` and `user_id()` reflect the authenticated identity.
88 pub request_context: Option<adk_core::RequestContext>,
89 /// Optional cooperative cancellation token for externally managed runs.
90 pub cancellation_token: Option<CancellationToken>,
91 /// Optional intra-invocation compaction configuration.
92 /// When set, the runner estimates token count before each agent run
93 /// and triggers mid-invocation summarization when the threshold is exceeded.
94 pub intra_compaction_config: Option<adk_core::IntraCompactionConfig>,
95 /// Optional summarizer for intra-invocation compaction.
96 /// Required when `intra_compaction_config` is set.
97 pub intra_compaction_summarizer: Option<Arc<dyn adk_core::BaseEventsSummarizer>>,
98 /// Optional context compaction configuration for token-budget overflow handling.
99 ///
100 /// When set, the runner applies the configured [`CompactionStrategy`](crate::compaction::CompactionStrategy)
101 /// to shrink the event history when the context exceeds the token budget,
102 /// retrying the model request up to `max_retries` times.
103 ///
104 /// This field is only available when the `context-compaction` feature is enabled.
105 #[cfg(feature = "context-compaction")]
106 pub context_compaction: Option<crate::compaction::CompactionConfig>,
107}
108
109/// Agent execution runtime.
110///
111/// Orchestrates session retrieval, agent dispatch, event streaming, context
112/// caching, and compaction. Construct via [`Runner::builder()`] or
113/// [`Runner::new()`].
114pub struct Runner {
115 app_name: String,
116 root_agent: Arc<dyn Agent>,
117 session_service: Arc<dyn SessionService>,
118 #[cfg(feature = "artifacts")]
119 artifact_service: Option<Arc<dyn ArtifactService>>,
120 memory_service: Option<Arc<dyn Memory>>,
121 #[cfg(feature = "plugins")]
122 plugin_manager: Option<Arc<PluginManager>>,
123 #[cfg(feature = "skills")]
124 skill_injector: Option<Arc<SkillInjector>>,
125 run_config: RunConfig,
126 compaction_config: Option<adk_core::EventsCompactionConfig>,
127 context_cache_config: Option<ContextCacheConfig>,
128 cache_capable: Option<Arc<dyn CacheCapable>>,
129 cache_manager: Option<Arc<tokio::sync::Mutex<CacheManager>>>,
130 request_context: Option<adk_core::RequestContext>,
131 cancellation_token: Option<CancellationToken>,
132 intra_compactor: Option<Arc<crate::intra_compaction::IntraInvocationCompactor>>,
133 /// Optional context compaction configuration for token-budget overflow handling.
134 #[cfg(feature = "context-compaction")]
135 context_compaction: Option<Arc<crate::compaction::CompactionConfig>>,
136 /// Per-session cancellation tokens for the interrupt API.
137 /// Each `run()` call registers a token here; `interrupt()` cancels it.
138 active_runs: ActiveRuns,
139 next_run_id: Arc<std::sync::atomic::AtomicU64>,
140}
141
142impl Runner {
143 /// Create a typestate builder for constructing a `Runner`.
144 ///
145 /// The builder enforces at compile time that the three required fields
146 /// (`app_name`, `agent`, `session_service`) are set before `build()` is
147 /// callable.
148 ///
149 /// # Example
150 ///
151 /// ```rust,ignore
152 /// let runner = Runner::builder()
153 /// .app_name("my-app")
154 /// .agent(agent)
155 /// .session_service(session_service)
156 /// .build()?;
157 /// ```
158 pub fn builder() -> crate::builder::RunnerConfigBuilder<
159 crate::builder::NoAppName,
160 crate::builder::NoAgent,
161 crate::builder::NoSessionService,
162 > {
163 crate::builder::RunnerConfigBuilder::new()
164 }
165
166 /// Create a new runner from a [`RunnerConfig`].
167 ///
168 /// Prefer [`Runner::builder()`] for a compile-time-safe construction API.
169 pub fn new(config: RunnerConfig) -> Result<Self> {
170 let run_config = config.run_config.unwrap_or_default();
171
172 // When a cache-capable model is provided but no explicit cache config,
173 // use the default ContextCacheConfig to enable caching automatically.
174 let effective_cache_config = config
175 .context_cache_config
176 .or_else(|| config.cache_capable.as_ref().map(|_| ContextCacheConfig::default()));
177
178 let cache_manager = effective_cache_config
179 .as_ref()
180 .map(|c| Arc::new(tokio::sync::Mutex::new(CacheManager::new(c.clone()))));
181
182 let intra_compactor = config.intra_compaction_config.as_ref().and_then(|ic_config| {
183 config.intra_compaction_summarizer.as_ref().map(|summarizer| {
184 Arc::new(crate::intra_compaction::IntraInvocationCompactor::new(
185 ic_config.clone(),
186 summarizer.clone(),
187 ))
188 })
189 });
190
191 Ok(Self {
192 app_name: config.app_name,
193 root_agent: config.agent,
194 session_service: config.session_service,
195 #[cfg(feature = "artifacts")]
196 artifact_service: config.artifact_service,
197 memory_service: config.memory_service,
198 #[cfg(feature = "plugins")]
199 plugin_manager: config.plugin_manager,
200 #[cfg(feature = "skills")]
201 skill_injector: None,
202 run_config,
203 compaction_config: config.compaction_config,
204 context_cache_config: effective_cache_config,
205 cache_capable: config.cache_capable,
206 cache_manager,
207 request_context: config.request_context,
208 cancellation_token: config.cancellation_token,
209 intra_compactor,
210 #[cfg(feature = "context-compaction")]
211 context_compaction: config.context_compaction.map(Arc::new),
212 active_runs: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
213 next_run_id: Arc::new(std::sync::atomic::AtomicU64::new(1)),
214 })
215 }
216
217 /// Enable skill injection using a pre-built injector.
218 ///
219 /// Skill injection runs before plugin `on_user_message` callbacks.
220 #[cfg(feature = "skills")]
221 pub fn with_skill_injector(mut self, injector: SkillInjector) -> Self {
222 self.skill_injector = Some(Arc::new(injector));
223 self
224 }
225
226 /// Enable skill injection by auto-loading `.skills/` from the given root path.
227 #[cfg(feature = "skills")]
228 #[deprecated(note = "Use with_auto_skills_mut instead")]
229 pub fn with_auto_skills(
230 mut self,
231 root: impl AsRef<std::path::Path>,
232 config: SkillInjectorConfig,
233 ) -> adk_skill::SkillResult<Self> {
234 self.with_auto_skills_mut(root, config)?;
235 Ok(self)
236 }
237
238 /// Enable skill injection by auto-loading `.skills/` from the given root path.
239 ///
240 /// Unlike [`with_auto_skills`](Self::with_auto_skills), this method borrows
241 /// the Runner mutably instead of consuming it. On error, the Runner remains
242 /// valid with no skill injector configured.
243 #[cfg(feature = "skills")]
244 pub fn with_auto_skills_mut(
245 &mut self,
246 root: impl AsRef<std::path::Path>,
247 config: SkillInjectorConfig,
248 ) -> adk_skill::SkillResult<()> {
249 let injector = SkillInjector::from_root(root, config)?;
250 self.skill_injector = Some(Arc::new(injector));
251 Ok(())
252 }
253
254 /// Execute the root agent for the given user and session, returning an event stream.
255 ///
256 /// Retrieves the existing session, resolves the target agent, runs plugins and skills, and
257 /// streams events as the agent executes.
258 pub async fn run(
259 &self,
260 user_id: UserId,
261 session_id: SessionId,
262 user_content: Content,
263 ) -> Result<EventStream> {
264 self.run_with_config(user_id, session_id, user_content, None).await
265 }
266
267 /// Returns the runner's application name.
268 pub fn app_name(&self) -> &str {
269 &self.app_name
270 }
271
272 /// Returns the runner's session service.
273 pub fn session_service(&self) -> &Arc<dyn adk_session::SessionService> {
274 &self.session_service
275 }
276
277 /// Returns the runner's configured [`RunConfig`].
278 ///
279 /// Useful as a base to clone and adjust for [`Self::run_with_config`].
280 pub fn run_config(&self) -> &RunConfig {
281 &self.run_config
282 }
283
284 /// Runs the agent with a per-invocation [`RunConfig`] override.
285 ///
286 /// Passing `None` uses the runner's configured `RunConfig`. Supply one to vary a single
287 /// invocation — injecting `runtime_toolsets` for tools that only exist for the duration of
288 /// that run, for example, as `SandboxRunner` does with tools bound to a live sandbox session.
289 ///
290 /// # Errors
291 ///
292 /// Returns an error if invocation setup fails before the stream is created. Session lookup
293 /// and agent execution failures are yielded by the returned stream.
294 pub async fn run_with_config(
295 &self,
296 user_id: UserId,
297 session_id: SessionId,
298 user_content: Content,
299 run_config: Option<RunConfig>,
300 ) -> Result<EventStream> {
301 let app_name = self.app_name.clone();
302 let typed_app_name = AppName::try_from(app_name.clone())?;
303 let session_service = self.session_service.clone();
304 let root_agent = self.root_agent.clone();
305 #[cfg(feature = "artifacts")]
306 let artifact_service = self.artifact_service.clone();
307 let memory_service = self.memory_service.clone();
308 #[cfg(feature = "plugins")]
309 let plugin_manager = self.plugin_manager.clone();
310 #[cfg(feature = "skills")]
311 let skill_injector = self.skill_injector.clone();
312 let mut run_config = run_config.unwrap_or_else(|| self.run_config.clone());
313 let compaction_config = self.compaction_config.clone();
314 let context_cache_config = self.context_cache_config.clone();
315 let cache_capable = self.cache_capable.clone();
316 let cache_manager_ref = self.cache_manager.clone();
317 let request_context = self.request_context.clone();
318 let cancellation_token = self.cancellation_token.clone();
319 let intra_compactor = self.intra_compactor.clone();
320 #[cfg(feature = "context-compaction")]
321 let context_compaction = self.context_compaction.clone();
322
323 // Built once and used for every persistence write, so a backend with a
324 // composite natural key can bind each event to its tenant.
325 let identity =
326 adk_core::AdkIdentity::new(typed_app_name.clone(), user_id.clone(), session_id.clone());
327
328 // Register this run for the interrupt API. The registration is keyed by a
329 // unique run ID rather than the raw session ID: two identities can share a
330 // session ID, and one identity can have two runs in flight, and both cases
331 // previously overwrote each other's token.
332 let session_token = CancellationToken::new();
333 let run_id = self.next_run_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
334 {
335 let mut runs = self.active_runs.lock().unwrap_or_else(|e| e.into_inner());
336 runs.insert(
337 run_id,
338 ActiveRun { identity: identity.clone(), token: session_token.clone() },
339 );
340 }
341 let active_runs = self.active_runs.clone();
342 // Effective token: cancelled if either the global token or the session token fires
343 let effective_token = if let Some(ref global) = cancellation_token {
344 let combined = CancellationToken::new();
345 let combined_clone = combined.clone();
346 let global_clone = global.clone();
347 let session_clone = session_token.clone();
348 // Watch both tokens — cancel the combined token when either fires
349 let combined_for_global = combined_clone.clone();
350 tokio::spawn(async move {
351 global_clone.cancelled().await;
352 combined_for_global.cancel();
353 });
354 let combined_for_session = combined_clone;
355 tokio::spawn(async move {
356 session_clone.cancelled().await;
357 combined_for_session.cancel();
358 });
359 Some(combined)
360 } else {
361 Some(session_token.clone())
362 };
363
364 // Built here rather than inside the generator: registration happens as soon
365 // as `run` is called, so deregistration must also survive a stream that is
366 // dropped before it is ever polled. Moving the guard into the generator
367 // keeps it alive exactly as long as the stream.
368 let cleanup = ActiveRunCleanup { active_runs: active_runs.clone(), run_id };
369
370 let s = stream! {
371 let _cleanup = cleanup;
372
373 // Use the effective token (combines global + per-session)
374 let cancellation_token = effective_token;
375 // Resolve the existing session.
376 let session = match session_service
377 .get(adk_session::GetRequest {
378 app_name: app_name.clone(),
379 user_id: user_id.to_string(),
380 session_id: session_id.to_string(),
381 num_recent_events: run_config.history_max_events,
382 after: None,
383 })
384 .await
385 {
386 Ok(s) => s,
387 Err(e) => {
388 yield Err(e);
389 return;
390 }
391 };
392
393 // Find which agent should handle this request
394 let agent_to_run = Self::find_agent_to_run(&root_agent, session.as_ref());
395
396 // Clone services for potential reuse in transfer
397 #[cfg(feature = "artifacts")]
398 let artifact_service_clone = artifact_service.clone();
399 let memory_service_clone = memory_service.clone();
400
401 // Create invocation context with MutableSession
402 let invocation_id = format!("inv-{}", uuid::Uuid::new_v4());
403 #[cfg(any(feature = "skills", feature = "plugins"))]
404 let mut effective_user_content = user_content.clone();
405 #[cfg(not(any(feature = "skills", feature = "plugins")))]
406 let effective_user_content = user_content.clone();
407 #[cfg(feature = "skills")]
408 let mut selected_skill_name = String::new();
409 #[cfg(not(feature = "skills"))]
410 let selected_skill_name = String::new();
411 #[cfg(feature = "skills")]
412 let mut selected_skill_id = String::new();
413 #[cfg(not(feature = "skills"))]
414 let selected_skill_id = String::new();
415
416 #[cfg(feature = "skills")]
417 if let Some(injector) = skill_injector.as_ref()
418 && let Some(matched) = adk_skill::apply_skill_injection(
419 &mut effective_user_content,
420 injector.index(),
421 injector.policy(),
422 injector.max_injected_chars(),
423 ) {
424 selected_skill_name = matched.skill.name;
425 selected_skill_id = matched.skill.id;
426 }
427
428 let mut invocation_ctx = match InvocationContext::new_typed(
429 invocation_id.clone(),
430 agent_to_run.clone(),
431 user_id.clone(),
432 typed_app_name.clone(),
433 session_id.clone(),
434 effective_user_content.clone(),
435 Arc::from(session),
436 ) {
437 Ok(ctx) => ctx,
438 Err(e) => {
439 yield Err(e);
440 return;
441 }
442 };
443
444 // Add optional services
445 #[cfg(feature = "artifacts")]
446 if let Some(service) = artifact_service {
447 // Wrap service with ScopedArtifacts to bind session context
448 let scoped = adk_artifact::ScopedArtifacts::new(
449 service,
450 app_name.clone(),
451 user_id.to_string(),
452 session_id.to_string(),
453 );
454 invocation_ctx = invocation_ctx.with_artifacts(Arc::new(scoped));
455 }
456 if let Some(memory) = memory_service {
457 invocation_ctx = invocation_ctx.with_memory(memory);
458 }
459
460 // Apply run config (streaming mode, etc.)
461 invocation_ctx = invocation_ctx.with_run_config(run_config.clone());
462
463 // Apply request context from auth middleware bridge if present
464 if let Some(rc) = request_context.clone() {
465 invocation_ctx = invocation_ctx.with_request_context(rc);
466 }
467
468 // Expose cooperative cancellation to the agent/tools.
469 if let Some(token) = cancellation_token.as_ref() {
470 invocation_ctx = invocation_ctx.with_cancellation_token(token.clone());
471 }
472
473 let mut ctx = Arc::new(invocation_ctx);
474
475 #[cfg(feature = "plugins")]
476 if let Some(manager) = plugin_manager.as_ref() {
477 match manager
478 .run_before_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>)
479 .await
480 {
481 Ok(Some(content)) => {
482 let mut early_event = adk_core::Event::new(ctx.invocation_id());
483 early_event.author = agent_to_run.name().to_string();
484 early_event.llm_response.content = Some(content);
485
486 ctx.mutable_session().append_event(early_event.clone());
487 if let Err(e) = session_service
488 .append_event_for_identity(adk_session::AppendEventRequest {
489 identity: identity.clone(),
490 event: early_event.clone(),
491 })
492 .await {
493 yield Err(e);
494 return;
495 }
496
497 yield Ok(early_event);
498 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
499 return;
500 }
501 Ok(None) => {}
502 Err(e) => {
503 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
504 yield Err(e);
505 return;
506 }
507 }
508
509 match manager
510 .run_on_user_message(
511 ctx.clone() as Arc<dyn adk_core::InvocationContext>,
512 effective_user_content.clone(),
513 )
514 .await
515 {
516 Ok(Some(modified)) => {
517 effective_user_content = modified;
518
519 let mut refreshed_ctx = match InvocationContext::with_mutable_session(
520 ctx.invocation_id().to_string(),
521 agent_to_run.clone(),
522 ctx.user_id().to_string(),
523 ctx.app_name().to_string(),
524 ctx.session_id().to_string(),
525 effective_user_content.clone(),
526 ctx.mutable_session().clone(),
527 ) {
528 Ok(ctx) => ctx,
529 Err(e) => {
530 yield Err(e);
531 return;
532 }
533 };
534
535 #[cfg(feature = "artifacts")]
536 if let Some(service) = artifact_service_clone.clone() {
537 let scoped = adk_artifact::ScopedArtifacts::new(
538 service,
539 ctx.app_name().to_string(),
540 ctx.user_id().to_string(),
541 ctx.session_id().to_string(),
542 );
543 refreshed_ctx = refreshed_ctx.with_artifacts(Arc::new(scoped));
544 }
545 if let Some(memory) = memory_service_clone.clone() {
546 refreshed_ctx = refreshed_ctx.with_memory(memory);
547 }
548 refreshed_ctx = refreshed_ctx.with_run_config(run_config.clone());
549 if let Some(rc) = request_context.clone() {
550 refreshed_ctx = refreshed_ctx.with_request_context(rc);
551 }
552 if let Some(token) = cancellation_token.as_ref() {
553 refreshed_ctx = refreshed_ctx.with_cancellation_token(token.clone());
554 }
555 ctx = Arc::new(refreshed_ctx);
556 }
557 Ok(None) => {}
558 Err(e) => {
559 if let Some(manager) = plugin_manager.as_ref() {
560 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
561 }
562 yield Err(e);
563 return;
564 }
565 }
566 }
567
568 // Append user message to session service (persistent storage)
569 let mut user_event = adk_core::Event::new(ctx.invocation_id());
570 user_event.author = "user".to_string();
571 user_event.llm_response.content = Some(effective_user_content.clone());
572
573 // Also add to mutable session for immediate visibility
574 // Note: adk_session::Event is a re-export of adk_core::Event, so we can use it directly
575 ctx.mutable_session().append_event(user_event.clone());
576
577 if let Err(e) = session_service
578 .append_event_for_identity(adk_session::AppendEventRequest {
579 identity: identity.clone(),
580 event: user_event,
581 })
582 .await {
583 #[cfg(feature = "plugins")]
584 if let Some(manager) = plugin_manager.as_ref() {
585 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
586 }
587 yield Err(e);
588 return;
589 }
590
591 // ===== CONTEXT CACHE LIFECYCLE =====
592 // If context caching is configured and a cache-capable model is available,
593 // create or refresh the cached content before agent execution.
594 // Cache failures are non-fatal — log a warning and proceed without cache.
595 if let (Some(cm_mutex), Some(cache_model)) = (&cache_manager_ref, &cache_capable) {
596 let should_refresh_cache = {
597 let cm = cm_mutex.lock().await;
598 cm.is_enabled() && (cm.active_cache_name().is_none() || cm.needs_refresh())
599 };
600
601 if should_refresh_cache {
602 // Gather system instruction from the agent's description
603 // (the full instruction is resolved inside the agent, but the
604 // description provides a reasonable proxy for cache keying).
605 let system_instruction = agent_to_run.description().to_string();
606 let tools = std::collections::HashMap::new();
607 let ttl = context_cache_config.as_ref().map_or(600, |c| c.ttl_seconds);
608
609 match cache_model.create_cache(&system_instruction, &tools, ttl).await {
610 Ok(name) => {
611 let old_cache = {
612 let mut cm = cm_mutex.lock().await;
613 let old = cm.clear_active_cache();
614 cm.set_active_cache(name);
615 old
616 };
617
618 if let Some(old) = old_cache
619 && let Err(e) = cache_model.delete_cache(&old).await {
620 tracing::warn!(
621 old_cache = %old,
622 error = %e,
623 "failed to delete old cache, proceeding with new cache"
624 );
625 }
626 }
627 Err(e) => {
628 tracing::warn!(
629 error = %e,
630 "cache creation failed, proceeding without cache"
631 );
632 }
633 }
634 }
635
636 // Attach cache name to run config so agents can use it.
637 let cache_name = {
638 let mut cm = cm_mutex.lock().await;
639 if cm.is_enabled() {
640 cm.record_invocation().map(str::to_string)
641 } else {
642 None
643 }
644 };
645
646 if let Some(cache_name) = cache_name {
647 run_config.cached_content = Some(cache_name);
648 // Rebuild the invocation context with the updated run config.
649 let mut refreshed_ctx = match InvocationContext::with_mutable_session(
650 ctx.invocation_id().to_string(),
651 agent_to_run.clone(),
652 ctx.user_id().to_string(),
653 ctx.app_name().to_string(),
654 ctx.session_id().to_string(),
655 effective_user_content.clone(),
656 ctx.mutable_session().clone(),
657 ) {
658 Ok(ctx) => ctx,
659 Err(e) => {
660 yield Err(e);
661 return;
662 }
663 };
664 #[cfg(feature = "artifacts")]
665 if let Some(service) = artifact_service_clone.clone() {
666 let scoped = adk_artifact::ScopedArtifacts::new(
667 service,
668 ctx.app_name().to_string(),
669 ctx.user_id().to_string(),
670 ctx.session_id().to_string(),
671 );
672 refreshed_ctx = refreshed_ctx.with_artifacts(Arc::new(scoped));
673 }
674 if let Some(memory) = memory_service_clone.clone() {
675 refreshed_ctx = refreshed_ctx.with_memory(memory);
676 }
677 refreshed_ctx = refreshed_ctx.with_run_config(run_config.clone());
678 if let Some(rc) = request_context.clone() {
679 refreshed_ctx = refreshed_ctx.with_request_context(rc);
680 }
681 if let Some(token) = cancellation_token.as_ref() {
682 refreshed_ctx = refreshed_ctx.with_cancellation_token(token.clone());
683 }
684 ctx = Arc::new(refreshed_ctx);
685 }
686 }
687
688 // ===== INTRA-INVOCATION COMPACTION =====
689 // If intra-compaction is configured, check if the session events
690 // exceed the token threshold and compact them before the agent runs.
691 if let Some(ref compactor) = intra_compactor {
692 compactor.reset_cycle();
693 let session_events = ctx.mutable_session().as_ref().events_snapshot();
694 match compactor.maybe_compact(&session_events).await {
695 Ok(Some(compacted_events)) => {
696 ctx.mutable_session().replace_events(compacted_events);
697 tracing::info!("intra-invocation compaction applied before agent execution");
698 }
699 Ok(None) => {} // No compaction needed
700 Err(e) => {
701 tracing::warn!(error = %e, "intra-invocation compaction check failed");
702 }
703 }
704 }
705
706 // ===== CONTEXT COMPACTION (TOKEN BUDGET) =====
707 // If context-compaction is configured, proactively check the estimated
708 // token count before calling the agent. If it exceeds the budget,
709 // apply compaction to bring it under the limit.
710 #[cfg(feature = "context-compaction")]
711 if let Some(ref cc_config) = context_compaction {
712 let session_events = ctx.mutable_session().events_snapshot();
713 let estimated = crate::compaction::estimate_event_tokens(&session_events);
714 if estimated > cc_config.context_budget {
715 tracing::info!(
716 estimated_tokens = estimated,
717 budget = cc_config.context_budget,
718 "context exceeds budget, applying proactive compaction"
719 );
720 match crate::compaction::apply_compaction_with_retry(cc_config, session_events).await {
721 Ok(compacted) => {
722 ctx.mutable_session().replace_events(compacted);
723 tracing::info!("proactive context compaction succeeded");
724 }
725 Err(e) => {
726 // Proactive compaction failed — proceed anyway and let the
727 // model reject the request if it's truly too large.
728 tracing::warn!(error = %e, "proactive context compaction failed, proceeding with full context");
729 }
730 }
731 }
732 }
733
734 // Run the agent with instrumentation (ADK-Go style attributes)
735 let agent_span = tracing::info_span!(
736 "agent.execute",
737 "gcp.vertex.agent.invocation_id" = ctx.invocation_id(),
738 "gcp.vertex.agent.session_id" = ctx.session_id(),
739 "gcp.vertex.agent.event_id" = ctx.invocation_id(), // Use invocation_id as event_id for agent spans
740 "gen_ai.conversation.id" = ctx.session_id(),
741 "adk.app_name" = ctx.app_name(),
742 "adk.user_id" = ctx.user_id(),
743 "agent.name" = %agent_to_run.name(),
744 "adk.skills.selected_name" = %selected_skill_name,
745 "adk.skills.selected_id" = %selected_skill_id
746 );
747
748 let mut agent_stream = match agent_to_run.run(ctx.clone()).instrument(agent_span.clone()).await {
749 Ok(s) => s,
750 #[cfg(feature = "context-compaction")]
751 Err(e) if context_compaction.is_some() && crate::compaction::is_token_limit_error(&e) => {
752 // Token limit error on agent.run() — apply compaction and retry
753 let cc_config = context_compaction.as_ref().unwrap();
754 tracing::warn!(
755 error = %e,
756 "agent execution failed with token limit error, attempting compaction"
757 );
758 let session_events = ctx.mutable_session().events_snapshot();
759 match crate::compaction::apply_compaction_with_retry(cc_config, session_events).await {
760 Ok(compacted) => {
761 ctx.mutable_session().replace_events(compacted);
762 tracing::info!("context compaction succeeded after token limit error, retrying agent");
763 // Retry the agent call with compacted context
764 match agent_to_run.run(ctx.clone()).instrument(agent_span).await {
765 Ok(s) => s,
766 Err(retry_err) => {
767 #[cfg(feature = "plugins")]
768 if let Some(manager) = plugin_manager.as_ref() {
769 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
770 }
771 yield Err(retry_err);
772 return;
773 }
774 }
775 }
776 Err(compaction_err) => {
777 #[cfg(feature = "plugins")]
778 if let Some(manager) = plugin_manager.as_ref() {
779 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
780 }
781 yield Err(compaction_err);
782 return;
783 }
784 }
785 }
786 Err(e) => {
787 #[cfg(feature = "plugins")]
788 if let Some(manager) = plugin_manager.as_ref() {
789 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
790 }
791 yield Err(e);
792 return;
793 }
794 };
795
796 // Stream events and check for transfers
797 use futures::StreamExt;
798 let mut transfer_target: Option<String> = None;
799
800 while let Some(result) = {
801 // Race the next event against cancellation so an in-flight
802 // await (LLM streaming, tool I/O) is interrupted promptly
803 // rather than only at poll boundaries. Dropping the stream on
804 // cancellation releases the underlying provider connection.
805 match cancellation_token.as_ref() {
806 Some(token) => {
807 tokio::select! {
808 biased;
809 _ = token.cancelled() => {
810 tracing::info!("cancellation fired during agent stream await");
811 #[cfg(feature = "plugins")]
812 if let Some(manager) = plugin_manager.as_ref() {
813 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
814 }
815 return;
816 }
817 result = agent_stream.next() => result,
818 }
819 }
820 None => agent_stream.next().await,
821 }
822 } {
823 match result {
824 Ok(event) => {
825 #[cfg(feature = "plugins")]
826 let mut event = event;
827
828 #[cfg(feature = "plugins")]
829 if let Some(manager) = plugin_manager.as_ref() {
830 match manager
831 .run_on_event(
832 ctx.clone() as Arc<dyn adk_core::InvocationContext>,
833 event.clone(),
834 )
835 .await
836 {
837 Ok(Some(modified)) => {
838 event = modified;
839 }
840 Ok(None) => {}
841 Err(e) => {
842 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
843 yield Err(e);
844 return;
845 }
846 }
847 }
848
849 // Check for transfer action
850 if let Some(target) = &event.actions.transfer_to_agent {
851 transfer_target = Some(target.clone());
852 }
853
854 // CRITICAL: Apply state_delta to the mutable session immediately.
855 // This is the key fix for state propagation between sequential agents.
856 // When an agent sets output_key, it emits an event with state_delta.
857 // We must apply this to the mutable session so downstream agents
858 // can read the value via ctx.session().state().get().
859 if !event.actions.state_delta.is_empty() {
860 ctx.mutable_session().apply_state_delta(&event.actions.state_delta);
861 }
862
863 // Also add the event to the mutable session's event list
864 ctx.mutable_session().append_event(event.clone());
865
866 // Append event to session service (persistent storage)
867 // Skip partial streaming chunks — only persist the final
868 // event. Streaming chunks share the same event ID, so
869 // persisting each one would violate the primary key
870 // constraint. The final chunk (partial=false) carries the
871 // complete accumulated content.
872 if !event.llm_response.partial
873 && let Err(e) = session_service
874 .append_event_for_identity(adk_session::AppendEventRequest {
875 identity: identity.clone(),
876 event: event.clone(),
877 })
878 .await {
879 #[cfg(feature = "plugins")]
880 if let Some(manager) = plugin_manager.as_ref() {
881 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
882 }
883 yield Err(e);
884 return;
885 }
886 yield Ok(event);
887 }
888 Err(e) => {
889 #[cfg(feature = "plugins")]
890 if let Some(manager) = plugin_manager.as_ref() {
891 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
892 }
893 yield Err(e);
894 return;
895 }
896 }
897 }
898
899 // ===== TRANSFER LOOP =====
900 // Support multi-hop transfers with a max-depth guard.
901 // When an agent emits transfer_to_agent, the runner resolves the
902 // target from the root agent tree, computes transfer_targets
903 // (parent + peers) for the new agent, and runs it. This repeats
904 // until no further transfer is requested or the depth limit is hit.
905 const DEFAULT_MAX_TRANSFER_DEPTH: u32 = 10;
906 let max_depth = run_config.max_transfer_depth.unwrap_or(DEFAULT_MAX_TRANSFER_DEPTH);
907 let mut transfer_depth: u32 = 0;
908 let mut current_transfer_target = transfer_target;
909
910 while let Some(target_name) = current_transfer_target.take() {
911 transfer_depth += 1;
912 if transfer_depth > max_depth {
913 tracing::warn!(
914 depth = transfer_depth,
915 target = %target_name,
916 "max transfer depth exceeded, stopping transfer chain"
917 );
918 break;
919 }
920
921 let target_agent = match Self::find_agent(&root_agent, &target_name) {
922 Some(a) => a,
923 None => {
924 tracing::warn!(target = %target_name, "transfer target not found in agent tree");
925 break;
926 }
927 };
928
929 // Compute transfer_targets for the target agent:
930 // - parent: the agent that transferred to it (or root if applicable)
931 // - peers: siblings in the agent tree
932 // - children: handled by the agent itself via sub_agents()
933 let (parent_name, peer_names) = Self::compute_transfer_context(&root_agent, &target_name);
934
935 let mut transfer_run_config = run_config.clone();
936 let mut targets = Vec::new();
937 if let Some(ref parent) = parent_name {
938 targets.push(parent.clone());
939 }
940 targets.extend(peer_names);
941 transfer_run_config.transfer_targets = targets;
942 transfer_run_config.parent_agent = parent_name;
943
944 // For transfers, we reuse the same mutable session to preserve state
945 let transfer_invocation_id = format!("inv-{}", uuid::Uuid::new_v4());
946 let mut transfer_ctx = match InvocationContext::with_mutable_session(
947 transfer_invocation_id.clone(),
948 target_agent.clone(),
949 ctx.user_id().to_string(),
950 ctx.app_name().to_string(),
951 ctx.session_id().to_string(),
952 effective_user_content.clone(),
953 ctx.mutable_session().clone(),
954 ) {
955 Ok(ctx) => ctx,
956 Err(e) => {
957 yield Err(e);
958 return;
959 }
960 };
961
962 #[cfg(feature = "artifacts")]
963 if let Some(ref service) = artifact_service_clone {
964 let scoped = adk_artifact::ScopedArtifacts::new(
965 service.clone(),
966 ctx.app_name().to_string(),
967 ctx.user_id().to_string(),
968 ctx.session_id().to_string(),
969 );
970 transfer_ctx = transfer_ctx.with_artifacts(Arc::new(scoped));
971 }
972 if let Some(ref memory) = memory_service_clone {
973 transfer_ctx = transfer_ctx.with_memory(memory.clone());
974 }
975 transfer_ctx = transfer_ctx.with_run_config(transfer_run_config);
976 if let Some(rc) = request_context.clone() {
977 transfer_ctx = transfer_ctx.with_request_context(rc);
978 }
979 if let Some(token) = cancellation_token.as_ref() {
980 transfer_ctx = transfer_ctx.with_cancellation_token(token.clone());
981 }
982
983 let transfer_ctx = Arc::new(transfer_ctx);
984
985 // Run the transferred agent
986 let mut transfer_stream = match target_agent.run(transfer_ctx.clone()).await {
987 Ok(s) => s,
988 Err(e) => {
989 #[cfg(feature = "plugins")]
990 if let Some(manager) = plugin_manager.as_ref() {
991 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
992 }
993 yield Err(e);
994 return;
995 }
996 };
997
998 // Stream events from the transferred agent, capturing any further transfer
999 while let Some(result) = {
1000 // Race the next event against cancellation for prompt
1001 // mid-await interruption of the transferred agent.
1002 match cancellation_token.as_ref() {
1003 Some(token) => {
1004 tokio::select! {
1005 biased;
1006 _ = token.cancelled() => {
1007 tracing::info!("cancellation fired during transferred agent stream await");
1008 #[cfg(feature = "plugins")]
1009 if let Some(manager) = plugin_manager.as_ref() {
1010 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
1011 }
1012 return;
1013 }
1014 result = transfer_stream.next() => result,
1015 }
1016 }
1017 None => transfer_stream.next().await,
1018 }
1019 } {
1020 match result {
1021 Ok(event) => {
1022 #[cfg(feature = "plugins")]
1023 let mut event = event;
1024 #[cfg(feature = "plugins")]
1025 if let Some(manager) = plugin_manager.as_ref() {
1026 match manager
1027 .run_on_event(
1028 transfer_ctx.clone() as Arc<dyn adk_core::InvocationContext>,
1029 event.clone(),
1030 )
1031 .await
1032 {
1033 Ok(Some(modified)) => {
1034 event = modified;
1035 }
1036 Ok(None) => {}
1037 Err(e) => {
1038 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
1039 yield Err(e);
1040 return;
1041 }
1042 }
1043 }
1044
1045 // Capture further transfer requests
1046 if let Some(target) = &event.actions.transfer_to_agent {
1047 current_transfer_target = Some(target.clone());
1048 }
1049
1050 // Apply state delta for transferred agent too
1051 if !event.actions.state_delta.is_empty() {
1052 transfer_ctx.mutable_session().apply_state_delta(&event.actions.state_delta);
1053 }
1054
1055 // Add to mutable session
1056 transfer_ctx.mutable_session().append_event(event.clone());
1057
1058 if !event.llm_response.partial
1059 && let Err(e) = session_service
1060 .append_event_for_identity(adk_session::AppendEventRequest {
1061 identity: identity.clone(),
1062 event: event.clone(),
1063 })
1064 .await {
1065 #[cfg(feature = "plugins")]
1066 if let Some(manager) = plugin_manager.as_ref() {
1067 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
1068 }
1069 yield Err(e);
1070 return;
1071 }
1072 yield Ok(event);
1073 }
1074 Err(e) => {
1075 #[cfg(feature = "plugins")]
1076 if let Some(manager) = plugin_manager.as_ref() {
1077 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
1078 }
1079 yield Err(e);
1080 return;
1081 }
1082 }
1083 }
1084 }
1085
1086 // ===== CONTEXT COMPACTION =====
1087 // After all events have been processed, check if compaction should trigger.
1088 // This runs in the background after the invocation completes.
1089 if let Some(ref compaction_cfg) = compaction_config {
1090 let event_count = ctx.mutable_session().as_ref().events_len();
1091
1092 if event_count > 0 {
1093 let all_events = ctx.mutable_session().as_ref().events_snapshot();
1094 let invocation_count = all_events.iter().filter(|e| e.author == "user").count()
1095 as u32;
1096
1097 if invocation_count > 0
1098 && invocation_count.is_multiple_of(compaction_cfg.compaction_interval)
1099 {
1100 // Determine the window of events to compact
1101 // We compact all events except the most recent overlap_size invocations
1102 let overlap = compaction_cfg.overlap_size as usize;
1103
1104 // Find the boundary: keep the last `overlap` user messages and everything after
1105 let user_msg_indices: Vec<usize> = all_events.iter()
1106 .enumerate()
1107 .filter(|(_, e)| e.author == "user")
1108 .map(|(i, _)| i)
1109 .collect();
1110
1111 // Keep the last `overlap` user messages intact.
1112 // When overlap is 0, compact everything.
1113 let compact_up_to = if overlap == 0 {
1114 all_events.len()
1115 } else if user_msg_indices.len() > overlap {
1116 // Compact up to (but not including) the overlap-th-from-last user message
1117 user_msg_indices[user_msg_indices.len() - overlap]
1118 } else {
1119 // Not enough user messages to satisfy overlap — skip compaction
1120 0
1121 };
1122
1123 if compact_up_to > 0 {
1124 let events_to_compact = &all_events[..compact_up_to];
1125
1126 match compaction_cfg.summarizer.summarize_events(events_to_compact).await {
1127 Ok(Some(compaction_event)) => {
1128 // Persist the compaction event
1129 if let Err(e) = session_service
1130 .append_event_for_identity(adk_session::AppendEventRequest {
1131 identity: identity.clone(),
1132 event: compaction_event.clone(),
1133 })
1134 .await {
1135 tracing::warn!(error = %e, "Failed to persist compaction event");
1136 } else {
1137 tracing::info!(
1138 compacted_events = compact_up_to,
1139 "Context compaction completed"
1140 );
1141 }
1142 }
1143 Ok(None) => {
1144 tracing::debug!("Compaction summarizer returned no result");
1145 }
1146 Err(e) => {
1147 // Compaction failure is non-fatal — log and continue
1148 tracing::warn!(error = %e, "Context compaction failed");
1149 }
1150 }
1151 }
1152 }
1153 }
1154 }
1155
1156 #[cfg(feature = "plugins")]
1157 if let Some(manager) = plugin_manager.as_ref() {
1158 manager.run_after_run(ctx.clone() as Arc<dyn adk_core::InvocationContext>).await;
1159 }
1160 };
1161
1162 Ok(Box::pin(s))
1163 }
1164
1165 /// Convenience method that accepts string arguments.
1166 ///
1167 /// Converts `user_id` and `session_id` to their typed equivalents
1168 /// and delegates to [`run()`](Self::run).
1169 ///
1170 /// # Errors
1171 ///
1172 /// Returns an error if either string fails identity validation
1173 /// (empty, contains null bytes, or exceeds length limit).
1174 pub async fn run_str(
1175 &self,
1176 user_id: &str,
1177 session_id: &str,
1178 user_content: Content,
1179 ) -> Result<EventStream> {
1180 let user_id = UserId::try_from(user_id)?;
1181 let session_id = SessionId::try_from(session_id)?;
1182 self.run(user_id, session_id, user_content).await
1183 }
1184
1185 /// Interrupt a running agent for the given session.
1186 ///
1187 /// Cancels the agent's current execution within the event loop. Events
1188 /// already produced and appended to the session are preserved — only
1189 /// future events are stopped. The caller can then issue a new `run()`
1190 /// call with a different instruction to redirect the agent.
1191 ///
1192 /// Returns `true` if a running session was found and interrupted,
1193 /// `false` if no active run exists for that session ID.
1194 ///
1195 /// # Example
1196 ///
1197 /// ```rust,ignore
1198 /// // Start a run in the background
1199 /// let mut stream = runner.run(user_id, session_id, content).await?;
1200 /// tokio::spawn(async move { while stream.next().await.is_some() {} });
1201 ///
1202 /// // Later, interrupt it
1203 /// let was_running = runner.interrupt("session-1");
1204 /// assert!(was_running);
1205 ///
1206 /// // Redirect with a new instruction
1207 /// let mut stream = runner.run(user_id, session_id, new_content).await?;
1208 /// ```
1209 pub fn interrupt(&self, session_id: &str) -> bool {
1210 let runs = self.active_runs.lock().unwrap_or_else(|e| e.into_inner());
1211 let matching: Vec<&ActiveRun> =
1212 runs.values().filter(|run| run.identity.session_id.as_ref() == session_id).collect();
1213 if matching.is_empty() {
1214 tracing::debug!(session.id = session_id, "no active run to interrupt");
1215 return false;
1216 }
1217 tracing::info!(
1218 session.id = session_id,
1219 run.count = matching.len(),
1220 "interrupting running agent"
1221 );
1222 for run in matching {
1223 run.token.cancel();
1224 }
1225 true
1226 }
1227
1228 /// Interrupts runs for one exact identity.
1229 ///
1230 /// A session ID is only unique within an app and user, so this is the precise
1231 /// form of [`Runner::interrupt`] for a `Runner` shared across tenants.
1232 /// Returns `true` when at least one run was cancelled.
1233 ///
1234 /// # Example
1235 ///
1236 /// ```rust,ignore
1237 /// let cancelled = runner.interrupt_identity("my-app", "user-1", "session-1");
1238 /// ```
1239 pub fn interrupt_identity(&self, app_name: &str, user_id: &str, session_id: &str) -> bool {
1240 let runs = self.active_runs.lock().unwrap_or_else(|e| e.into_inner());
1241 let mut cancelled = false;
1242 for run in runs.values() {
1243 if run.identity.app_name.as_ref() == app_name
1244 && run.identity.user_id.as_ref() == user_id
1245 && run.identity.session_id.as_ref() == session_id
1246 {
1247 run.token.cancel();
1248 cancelled = true;
1249 }
1250 }
1251 if !cancelled {
1252 tracing::debug!(
1253 app.name = app_name,
1254 user.id = user_id,
1255 session.id = session_id,
1256 "no active run to interrupt"
1257 );
1258 }
1259 cancelled
1260 }
1261
1262 /// Returns the identity of every run currently in flight.
1263 ///
1264 /// One identity appears once per in-flight run, so a repeated entry means that
1265 /// identity has concurrent runs.
1266 pub fn active_runs(&self) -> Vec<adk_core::AdkIdentity> {
1267 let runs = self.active_runs.lock().unwrap_or_else(|e| e.into_inner());
1268 runs.values().map(|run| run.identity.clone()).collect()
1269 }
1270
1271 /// Returns the session IDs of all currently running agent executions.
1272 ///
1273 /// Session IDs are deduplicated. Use [`Runner::active_runs`] when the app and
1274 /// user dimensions matter.
1275 pub fn active_session_ids(&self) -> Vec<String> {
1276 let runs = self.active_runs.lock().unwrap_or_else(|e| e.into_inner());
1277 let mut ids: Vec<String> =
1278 runs.values().map(|run| run.identity.session_id.as_ref().to_string()).collect();
1279 ids.sort();
1280 ids.dedup();
1281 ids
1282 }
1283
1284 /// Returns a reference to the context compaction configuration, if set.
1285 ///
1286 /// This is used by the runner's generate_content loop to detect token limit
1287 /// errors and apply compaction strategies before retrying.
1288 #[cfg(feature = "context-compaction")]
1289 pub fn context_compaction(&self) -> Option<&crate::compaction::CompactionConfig> {
1290 self.context_compaction.as_deref()
1291 }
1292
1293 /// Find which agent should handle the request based on session history
1294 pub fn find_agent_to_run(
1295 root_agent: &Arc<dyn Agent>,
1296 session: &dyn adk_session::Session,
1297 ) -> Arc<dyn Agent> {
1298 // Look at recent events to find last agent that responded
1299 let events = session.events();
1300 for i in (0..events.len()).rev() {
1301 if let Some(event) = events.at(i) {
1302 // Check for explicit transfer
1303 if let Some(target_name) = &event.actions.transfer_to_agent
1304 && let Some(agent) = Self::find_agent(root_agent, target_name)
1305 {
1306 return agent;
1307 }
1308
1309 if event.author == "user" {
1310 continue;
1311 }
1312
1313 // Try to find this agent in the tree
1314 if let Some(agent) = Self::find_agent(root_agent, &event.author) {
1315 // Check if agent allows transfer up the tree
1316 if Self::is_transferable(root_agent, &agent) {
1317 return agent;
1318 }
1319 }
1320 }
1321 }
1322
1323 // Default to root agent
1324 root_agent.clone()
1325 }
1326
1327 /// Check if an agent found in session history can be resumed directly for
1328 /// the next user message.
1329 ///
1330 /// An agent is a valid direct-resumption target only if it *and* every
1331 /// ancestor up to the root permit agent transfer
1332 /// ([`Agent::supports_agent_transfer`]). A deterministic workflow agent
1333 /// (sequential, parallel, loop, conditional) anywhere on that path returns
1334 /// `false`, which forces resumption to restart from the workflow root
1335 /// rather than a single sub-agent that happened to respond last. This
1336 /// mirrors Google ADK's `_is_transferable_across_agent_tree`.
1337 ///
1338 /// LLM-driven transfer-policy enforcement
1339 /// (`disallow_transfer_to_parent` / `disallow_transfer_to_peers`) is still
1340 /// handled inside `LlmAgent::run()` when it builds the `transfer_to_agent`
1341 /// tool's valid-target list; this check only governs cross-turn resumption.
1342 fn is_transferable(root_agent: &Arc<dyn Agent>, agent: &Arc<dyn Agent>) -> bool {
1343 // Walk the tree from the root down to the target. Every agent on that
1344 // path (root through target, inclusive) must support transfer for the
1345 // target to be a valid direct-resumption point. `Some(true)` = found
1346 // and fully transferable, `Some(false)` = found but a workflow agent
1347 // sits on the path, `None` = target not present in this subtree.
1348 fn path_supports_transfer(current: &Arc<dyn Agent>, target: &str) -> Option<bool> {
1349 if current.name() == target {
1350 return Some(current.supports_agent_transfer());
1351 }
1352 for sub in current.sub_agents() {
1353 if let Some(sub_ok) = path_supports_transfer(sub, target) {
1354 return Some(current.supports_agent_transfer() && sub_ok);
1355 }
1356 }
1357 None
1358 }
1359
1360 path_supports_transfer(root_agent, agent.name()).unwrap_or(true)
1361 }
1362
1363 /// Recursively search agent tree for agent with given name
1364 pub fn find_agent(current: &Arc<dyn Agent>, target_name: &str) -> Option<Arc<dyn Agent>> {
1365 if current.name() == target_name {
1366 return Some(current.clone());
1367 }
1368
1369 for sub_agent in current.sub_agents() {
1370 if let Some(found) = Self::find_agent(sub_agent, target_name) {
1371 return Some(found);
1372 }
1373 }
1374
1375 None
1376 }
1377
1378 /// Compute the parent name and peer names for a given agent in the tree.
1379 /// Returns `(parent_name, peer_names)`.
1380 ///
1381 /// Walks the agent tree to find the parent of `target_name`, then collects
1382 /// the parent's name and the sibling agent names (excluding the target itself).
1383 pub fn compute_transfer_context(
1384 root: &Arc<dyn Agent>,
1385 target_name: &str,
1386 ) -> (Option<String>, Vec<String>) {
1387 // If the target is the root itself, there's no parent or peers
1388 if root.name() == target_name {
1389 return (None, Vec::new());
1390 }
1391
1392 // BFS/DFS to find the parent of target_name
1393 fn find_parent(current: &Arc<dyn Agent>, target: &str) -> Option<Arc<dyn Agent>> {
1394 for sub in current.sub_agents() {
1395 if sub.name() == target {
1396 return Some(current.clone());
1397 }
1398 if let Some(found) = find_parent(sub, target) {
1399 return Some(found);
1400 }
1401 }
1402 None
1403 }
1404
1405 match find_parent(root, target_name) {
1406 Some(parent) => {
1407 let parent_name = parent.name().to_string();
1408 let peers: Vec<String> = parent
1409 .sub_agents()
1410 .iter()
1411 .filter(|a| a.name() != target_name)
1412 .map(|a| a.name().to_string())
1413 .collect();
1414 (Some(parent_name), peers)
1415 }
1416 None => (None, Vec::new()),
1417 }
1418 }
1419}