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