Skip to main content

adk_runner/
context.rs

1use adk_core::{
2    AdkIdentity, Agent, AppName, Artifacts, CallbackContext, Content, Event, ExecutionIdentity,
3    InvocationContext as InvocationContextTrait, InvocationId, Memory, ReadonlyContext,
4    RequestContext, RunConfig, SecretService, SessionId, UserId,
5};
6use adk_session::Session as AdkSession;
7use async_trait::async_trait;
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock, atomic::AtomicBool};
10
11/// MutableSession wraps a session with shared mutable state.
12///
13/// This mirrors ADK-Go's MutableSession pattern where state changes from
14/// events are immediately visible to all agents sharing the same context.
15/// This is critical for SequentialAgent/LoopAgent patterns where downstream
16/// agents need to read state set by upstream agents via output_key.
17pub struct MutableSession {
18    /// The original session snapshot (for metadata like id, app_name, user_id)
19    inner: Arc<dyn AdkSession>,
20    /// Shared mutable state - updated when events are processed
21    /// This is the key difference from the old SessionAdapter which used immutable snapshots
22    state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
23    /// Accumulated events during this invocation (uses adk_core::Event which is re-exported by adk_session)
24    events: Arc<RwLock<Vec<Event>>>,
25}
26
27impl MutableSession {
28    /// Create a new MutableSession from a session snapshot.
29    /// The state is copied from the session and becomes mutable.
30    pub fn new(session: Arc<dyn AdkSession>) -> Self {
31        // Clone the initial state from the session
32        let initial_state = session.state().all();
33        // Clone the initial events
34        let initial_events = session.events().all();
35
36        Self {
37            inner: session,
38            state: Arc::new(RwLock::new(initial_state)),
39            events: Arc::new(RwLock::new(initial_events)),
40        }
41    }
42
43    /// Apply state delta from an event to the mutable state.
44    /// This is called by the Runner when events are yielded.
45    pub fn apply_state_delta(&self, delta: &HashMap<String, serde_json::Value>) {
46        if delta.is_empty() {
47            return;
48        }
49
50        let Ok(mut state) = self.state.write() else {
51            tracing::error!("state RwLock poisoned in apply_state_delta — skipping delta");
52            return;
53        };
54        for (key, value) in delta {
55            // Skip temp: prefixed keys (they shouldn't persist)
56            if !key.starts_with("temp:") {
57                state.insert(key.clone(), value.clone());
58            }
59        }
60    }
61
62    /// Append an event to the session's event list.
63    /// This keeps the in-memory view consistent.
64    pub fn append_event(&self, event: Event) {
65        let Ok(mut events) = self.events.write() else {
66            tracing::error!("events RwLock poisoned in append_event — event dropped");
67            return;
68        };
69        events.push(event);
70    }
71
72    /// Get a snapshot of all events in the session.
73    /// Used by the runner for compaction decisions.
74    pub fn events_snapshot(&self) -> Vec<Event> {
75        let Ok(events) = self.events.read() else {
76            tracing::error!("events RwLock poisoned in events_snapshot — returning empty");
77            return Vec::new();
78        };
79        events.clone()
80    }
81
82    /// Replace all events with a new list (used by intra-invocation compaction).
83    pub fn replace_events(&self, new_events: Vec<Event>) {
84        let Ok(mut events) = self.events.write() else {
85            tracing::error!("events RwLock poisoned in replace_events — events unchanged");
86            return;
87        };
88        *events = new_events;
89    }
90
91    /// Return the number of accumulated events without cloning the full list.
92    pub fn events_len(&self) -> usize {
93        let Ok(events) = self.events.read() else {
94            tracing::error!("events RwLock poisoned in events_len — returning 0");
95            return 0;
96        };
97        events.len()
98    }
99
100    /// Build conversation history, optionally filtered for a specific agent.
101    ///
102    /// When `agent_name` is `Some`, events authored by other agents (not "user",
103    /// not the named agent, and not function/tool responses) are excluded. This
104    /// prevents a transferred sub-agent from seeing the parent's tool calls
105    /// mapped as "model" role, which would cause the LLM to think work is
106    /// already done.
107    ///
108    /// When `agent_name` is `None`, all events are included (backward-compatible).
109    ///
110    /// `branch` additionally scopes history to a conversation branch: an event is
111    /// kept when its branch equals `branch` or is an ancestor of it, so
112    /// concurrent `ParallelAgent` branches do not read each other's output. An
113    /// empty `branch`, or an event without one, matches everything.
114    pub fn conversation_history_for_agent_impl(
115        &self,
116        agent_name: Option<&str>,
117        branch: &str,
118    ) -> Vec<adk_core::Content> {
119        let Ok(events) = self.events.read() else {
120            tracing::error!("events RwLock poisoned in conversation_history — returning empty");
121            return Vec::new();
122        };
123        let mut history = Vec::new();
124
125        // Find the most recent compaction event — everything before its
126        // end_timestamp has been summarized and should be replaced by the
127        // compacted content.
128        let mut compaction_boundary = None;
129        for event in events.iter().rev() {
130            if let Some(ref compaction) = event.actions.compaction {
131                history.push(compaction.compacted_content.clone());
132                compaction_boundary = Some(compaction.end_timestamp);
133                break;
134            }
135        }
136
137        for event in events.iter() {
138            // Skip the compaction event itself
139            if event.actions.compaction.is_some() {
140                continue;
141            }
142
143            // Skip events that were already compacted
144            if let Some(boundary) = compaction_boundary
145                && event.timestamp <= boundary
146            {
147                continue;
148            }
149
150            // Skip events from sibling branches (and from branches nested below
151            // this one). Ancestor branches stay visible so a sub-agent still sees
152            // the conversation that led to it.
153            if !adk_core::event_belongs_to_branch(branch, &event.branch) {
154                continue;
155            }
156
157            // When filtering for a specific agent, skip events from other agents.
158            // Keep: user messages and the agent's own events.
159            // Skip: other agents' events entirely (model-role, function calls,
160            // and function/tool responses). This prevents the sub-agent from
161            // seeing orphaned function responses without their preceding calls.
162            if let Some(name) = agent_name
163                && event.author != "user"
164                && event.author != name
165            {
166                continue;
167            }
168
169            if let Some(content) = &event.llm_response.content {
170                let mut mapped_content = content.clone();
171                mapped_content.role = match (event.author.as_str(), content.role.as_str()) {
172                    ("user", _) => "user",
173                    (_, "function" | "tool") => content.role.as_str(),
174                    _ => "model",
175                }
176                .to_string();
177                history.push(mapped_content);
178            }
179        }
180
181        history
182    }
183}
184
185impl adk_core::Session for MutableSession {
186    fn id(&self) -> &str {
187        self.inner.id()
188    }
189
190    fn app_name(&self) -> &str {
191        self.inner.app_name()
192    }
193
194    fn user_id(&self) -> &str {
195        self.inner.user_id()
196    }
197
198    fn state(&self) -> &dyn adk_core::State {
199        self
200    }
201
202    fn conversation_history(&self) -> Vec<adk_core::Content> {
203        self.conversation_history_for_agent_impl(None, "")
204    }
205
206    fn conversation_history_for_agent(&self, agent_name: &str) -> Vec<adk_core::Content> {
207        self.conversation_history_for_agent_impl(Some(agent_name), "")
208    }
209
210    fn conversation_history_scoped(
211        &self,
212        agent_name: Option<&str>,
213        branch: &str,
214    ) -> Vec<adk_core::Content> {
215        self.conversation_history_for_agent_impl(agent_name, branch)
216    }
217}
218
219impl adk_core::State for MutableSession {
220    fn get(&self, key: &str) -> Option<serde_json::Value> {
221        let Ok(state) = self.state.read() else {
222            tracing::error!("state RwLock poisoned in State::get — returning None");
223            return None;
224        };
225        state.get(key).cloned()
226    }
227
228    fn set(&mut self, key: String, value: serde_json::Value) {
229        if let Err(msg) = adk_core::validate_state_key(&key) {
230            tracing::warn!(key = %key, "rejecting invalid state key: {msg}");
231            return;
232        }
233        let Ok(mut state) = self.state.write() else {
234            tracing::error!("state RwLock poisoned in State::set — value dropped");
235            return;
236        };
237        state.insert(key, value);
238    }
239
240    fn all(&self) -> HashMap<String, serde_json::Value> {
241        let Ok(state) = self.state.read() else {
242            tracing::error!("state RwLock poisoned in State::all — returning empty");
243            return HashMap::new();
244        };
245        state.clone()
246    }
247}
248
249/// Runtime context for a single agent invocation.
250///
251/// Holds the agent, session, identity, and optional services (artifacts, memory,
252/// secrets) needed during execution. Created by the [`Runner`](crate::Runner) for
253/// each `run()` call.
254pub struct InvocationContext {
255    identity: ExecutionIdentity,
256    agent: Arc<dyn Agent>,
257    user_content: Content,
258    artifacts: Option<Arc<dyn Artifacts>>,
259    memory: Option<Arc<dyn Memory>>,
260    run_config: RunConfig,
261    ended: Arc<AtomicBool>,
262    /// Mutable session that allows state to be updated during execution.
263    /// This is shared across all agents in a workflow, enabling state
264    /// propagation between sequential/parallel agents.
265    session: Arc<MutableSession>,
266    /// Optional request context from the server's auth middleware bridge.
267    /// When present, `user_id()` returns `request_context.user_id` and
268    /// `user_scopes()` returns `request_context.scopes`.
269    request_context: Option<RequestContext>,
270    /// Optional shared state for parallel agent coordination.
271    shared_state: Option<Arc<adk_core::SharedState>>,
272    /// Optional secret service for retrieving secrets at runtime.
273    /// When present, `get_secret()` delegates to this service.
274    secret_service: Option<Arc<dyn SecretService>>,
275    /// Optional cooperative cancellation token.
276    ///
277    /// When present, `is_cancelled()` reflects this token, letting agents and
278    /// tools detect external cancellation (`Runner::interrupt()` or
279    /// `RunConfig::cancellation_token`) during long-running work.
280    cancellation_token: Option<tokio_util::sync::CancellationToken>,
281}
282
283impl InvocationContext {
284    /// Create a new invocation context from validated typed identifiers.
285    pub fn new_typed(
286        invocation_id: String,
287        agent: Arc<dyn Agent>,
288        user_id: UserId,
289        app_name: AppName,
290        session_id: SessionId,
291        user_content: Content,
292        session: Arc<dyn AdkSession>,
293    ) -> adk_core::Result<Self> {
294        let identity = ExecutionIdentity {
295            adk: AdkIdentity { app_name, user_id, session_id },
296            invocation_id: InvocationId::try_from(invocation_id)?,
297            branch: String::new(),
298            agent_name: agent.name().to_string(),
299        };
300        Ok(Self {
301            identity,
302            agent,
303            user_content,
304            artifacts: None,
305            memory: None,
306            run_config: RunConfig::default(),
307            ended: Arc::new(AtomicBool::new(false)),
308            session: Arc::new(MutableSession::new(session)),
309            request_context: None,
310            shared_state: None,
311            secret_service: None,
312            cancellation_token: None,
313        })
314    }
315
316    /// Create a new invocation context from raw string identifiers.
317    ///
318    /// Validates and converts the string identifiers into typed wrappers.
319    /// Prefer [`new_typed`](Self::new_typed) when you already have validated types.
320    pub fn new(
321        invocation_id: String,
322        agent: Arc<dyn Agent>,
323        user_id: String,
324        app_name: String,
325        session_id: String,
326        user_content: Content,
327        session: Arc<dyn AdkSession>,
328    ) -> adk_core::Result<Self> {
329        Self::new_typed(
330            invocation_id,
331            agent,
332            UserId::try_from(user_id)?,
333            AppName::try_from(app_name)?,
334            SessionId::try_from(session_id)?,
335            user_content,
336            session,
337        )
338    }
339
340    /// Create an invocation context that reuses an existing mutable session and
341    /// validated typed identifiers.
342    pub fn with_mutable_session_typed(
343        invocation_id: String,
344        agent: Arc<dyn Agent>,
345        user_id: UserId,
346        app_name: AppName,
347        session_id: SessionId,
348        user_content: Content,
349        session: Arc<MutableSession>,
350    ) -> adk_core::Result<Self> {
351        let identity = ExecutionIdentity {
352            adk: AdkIdentity { app_name, user_id, session_id },
353            invocation_id: InvocationId::try_from(invocation_id)?,
354            branch: String::new(),
355            agent_name: agent.name().to_string(),
356        };
357        Ok(Self {
358            identity,
359            agent,
360            user_content,
361            artifacts: None,
362            memory: None,
363            run_config: RunConfig::default(),
364            ended: Arc::new(AtomicBool::new(false)),
365            session,
366            request_context: None,
367            shared_state: None,
368            secret_service: None,
369            cancellation_token: None,
370        })
371    }
372
373    /// Create an InvocationContext with an existing MutableSession.
374    /// This allows sharing the same mutable session across multiple contexts
375    /// (e.g., for agent transfers).
376    pub fn with_mutable_session(
377        invocation_id: String,
378        agent: Arc<dyn Agent>,
379        user_id: String,
380        app_name: String,
381        session_id: String,
382        user_content: Content,
383        session: Arc<MutableSession>,
384    ) -> adk_core::Result<Self> {
385        Self::with_mutable_session_typed(
386            invocation_id,
387            agent,
388            UserId::try_from(user_id)?,
389            AppName::try_from(app_name)?,
390            SessionId::try_from(session_id)?,
391            user_content,
392            session,
393        )
394    }
395
396    /// Set the event branch identifier for this context.
397    pub fn with_branch(mut self, branch: String) -> Self {
398        self.identity.branch = branch;
399        self
400    }
401
402    /// Attach an artifact storage service to this context.
403    pub fn with_artifacts(mut self, artifacts: Arc<dyn Artifacts>) -> Self {
404        self.artifacts = Some(artifacts);
405        self
406    }
407
408    /// Attach a memory service for RAG/semantic search.
409    pub fn with_memory(mut self, memory: Arc<dyn Memory>) -> Self {
410        self.memory = Some(memory);
411        self
412    }
413
414    /// Set the run configuration (streaming mode, history limits, etc.).
415    pub fn with_run_config(mut self, config: RunConfig) -> Self {
416        self.run_config = config;
417        self
418    }
419
420    /// Set the request context from the server's auth middleware bridge.
421    ///
422    /// When set, `user_id()` returns `request_context.user_id` (overriding
423    /// the session-scoped identity), and `user_scopes()` returns
424    /// `request_context.scopes`. This is the explicit authenticated user
425    /// override — `RequestContext` remains separate from `ExecutionIdentity`
426    /// and `AdkIdentity` (it does not carry session or invocation IDs).
427    pub fn with_request_context(mut self, ctx: RequestContext) -> Self {
428        self.request_context = Some(ctx);
429        self
430    }
431
432    /// Set the shared state for parallel agent coordination.
433    pub fn with_shared_state(mut self, shared: Arc<adk_core::SharedState>) -> Self {
434        self.shared_state = Some(shared);
435        self
436    }
437
438    /// Set the secret service for runtime secret retrieval.
439    ///
440    /// When configured, tools can call `ctx.get_secret("name")` to retrieve
441    /// secrets from the configured provider (e.g., AWS Secrets Manager,
442    /// Azure Key Vault, GCP Secret Manager).
443    pub fn with_secret_service(mut self, service: Arc<dyn SecretService>) -> Self {
444        self.secret_service = Some(service);
445        self
446    }
447
448    /// Attach a cooperative cancellation token.
449    ///
450    /// When set, [`is_cancelled`](adk_core::InvocationContext::is_cancelled)
451    /// reflects this token, so agents and tools can detect external
452    /// cancellation during long-running work.
453    pub fn with_cancellation_token(mut self, token: tokio_util::sync::CancellationToken) -> Self {
454        self.cancellation_token = Some(token);
455        self
456    }
457
458    /// Get a reference to the mutable session.
459    /// This allows the Runner to apply state deltas when events are processed.
460    pub fn mutable_session(&self) -> &Arc<MutableSession> {
461        &self.session
462    }
463}
464
465#[async_trait]
466impl ReadonlyContext for InvocationContext {
467    fn invocation_id(&self) -> &str {
468        self.identity.invocation_id.as_ref()
469    }
470
471    fn agent_name(&self) -> &str {
472        self.agent.name()
473    }
474
475    fn user_id(&self) -> &str {
476        // Explicit authenticated user override: when a RequestContext is
477        // present (set via with_request_context from the auth middleware
478        // bridge), the authenticated user_id takes precedence over the
479        // session-scoped identity. This keeps auth binding explicit and
480        // ensures the runtime reflects the verified caller identity.
481        self.request_context.as_ref().map_or(self.identity.adk.user_id.as_ref(), |rc| &rc.user_id)
482    }
483
484    fn app_name(&self) -> &str {
485        self.identity.adk.app_name.as_ref()
486    }
487
488    fn session_id(&self) -> &str {
489        self.identity.adk.session_id.as_ref()
490    }
491
492    fn branch(&self) -> &str {
493        &self.identity.branch
494    }
495
496    fn user_content(&self) -> &Content {
497        &self.user_content
498    }
499}
500
501#[async_trait]
502impl CallbackContext for InvocationContext {
503    fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
504        self.artifacts.clone()
505    }
506
507    fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
508        self.shared_state.clone()
509    }
510}
511
512#[async_trait]
513impl InvocationContextTrait for InvocationContext {
514    fn agent(&self) -> Arc<dyn Agent> {
515        self.agent.clone()
516    }
517
518    fn memory(&self) -> Option<Arc<dyn Memory>> {
519        self.memory.clone()
520    }
521
522    fn session(&self) -> &dyn adk_core::Session {
523        self.session.as_ref()
524    }
525
526    fn run_config(&self) -> &RunConfig {
527        &self.run_config
528    }
529
530    fn end_invocation(&self) {
531        self.ended.store(true, std::sync::atomic::Ordering::SeqCst);
532    }
533
534    fn ended(&self) -> bool {
535        self.ended.load(std::sync::atomic::Ordering::SeqCst)
536    }
537
538    fn is_cancelled(&self) -> bool {
539        self.cancellation_token
540            .as_ref()
541            .is_some_and(tokio_util::sync::CancellationToken::is_cancelled)
542    }
543
544    fn user_scopes(&self) -> Vec<String> {
545        self.request_context.as_ref().map_or_else(Vec::new, |rc| rc.scopes.clone())
546    }
547
548    fn request_metadata(&self) -> HashMap<String, serde_json::Value> {
549        self.request_context.as_ref().map_or_else(HashMap::new, |rc| {
550            rc.metadata
551                .iter()
552                .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
553                .collect()
554        })
555    }
556
557    async fn get_secret(&self, name: &str) -> adk_core::Result<Option<String>> {
558        let request = adk_core::SecretRequest::new(name)
559            .with_identity(self.app_name(), self.user_id(), self.session_id())
560            .with_invocation_id(self.invocation_id());
561        self.get_secret_for(&request).await
562    }
563
564    async fn get_secret_for(
565        &self,
566        request: &adk_core::SecretRequest,
567    ) -> adk_core::Result<Option<String>> {
568        match &self.secret_service {
569            Some(service) => service.get_secret_for(request).await.map(Some),
570            None => Ok(None),
571        }
572    }
573}