Skip to main content

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}