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, Part, 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(HistoryContent {
132                    content: compaction.compacted_content.clone(),
133                    invocation_id: None,
134                });
135                compaction_boundary = Some(compaction.end_timestamp);
136                break;
137            }
138        }
139
140        for event in events.iter() {
141            // Skip the compaction event itself
142            if event.actions.compaction.is_some() {
143                continue;
144            }
145
146            // Streamed deltas are superseded by their terminal event, which carries the
147            // complete response; the persisted session holds only that terminal event.
148            if event.llm_response.partial {
149                continue;
150            }
151
152            // Skip events that were already compacted
153            if let Some(boundary) = compaction_boundary
154                && event.timestamp <= boundary
155            {
156                continue;
157            }
158
159            // Skip events from sibling branches (and from branches nested below
160            // this one). Ancestor branches stay visible so a sub-agent still sees
161            // the conversation that led to it.
162            if !adk_core::event_belongs_to_branch(branch, &event.branch) {
163                continue;
164            }
165
166            // When filtering for a specific agent, skip events from other agents.
167            // Keep: user messages and the agent's own events.
168            // Skip: other agents' events entirely (model-role, function calls,
169            // and function/tool responses). This prevents the sub-agent from
170            // seeing orphaned function responses without their preceding calls.
171            if let Some(name) = agent_name
172                && event.author != "user"
173                && event.author != name
174            {
175                continue;
176            }
177
178            if let Some(content) = &event.llm_response.content {
179                let mut mapped_content = content.clone();
180                mapped_content.role = match (event.author.as_str(), content.role.as_str()) {
181                    ("user", _) => "user",
182                    (_, "function" | "tool") => content.role.as_str(),
183                    _ => "model",
184                }
185                .to_string();
186                history.push(HistoryContent {
187                    content: mapped_content,
188                    invocation_id: Some(event.invocation_id.clone()),
189                });
190            }
191        }
192
193        close_interrupted_tool_calls(history)
194    }
195}
196
197struct HistoryContent {
198    content: Content,
199    invocation_id: Option<String>,
200}
201
202#[derive(Clone, Copy, PartialEq, Eq, Hash)]
203struct PartLocation {
204    content: usize,
205    part: usize,
206}
207
208struct PendingFunctionCall {
209    location: PartLocation,
210    id: Option<String>,
211    name: String,
212    user_turn: usize,
213    invocation_id: Option<String>,
214}
215
216/// Makes persisted conversation history valid for the next model turn without
217/// rewriting the source events.
218///
219/// A runner interruption can happen after a model function call is persisted
220/// but before its tool response is available. Responses from overlapping runs
221/// may also be persisted after a later user message, so known responses are
222/// paired first. An unmatched call is closed only when a later user turn exists
223/// and has no durable response in the retained history. Such calls are omitted
224/// from the model projection rather than assigned a result that did not occur.
225fn close_interrupted_tool_calls(history: Vec<HistoryContent>) -> Vec<Content> {
226    let mut pending = Vec::<PendingFunctionCall>::new();
227    let mut removed_parts = std::collections::HashSet::<PartLocation>::new();
228    let mut responses_after = HashMap::<usize, Vec<(usize, String, Part)>>::new();
229    let mut user_turn = 0;
230
231    // Reconcile against the complete retained history first. Concurrent runs can
232    // persist a response after another run's user event, and that response still
233    // completes the original call.
234    for (content_index, history_content) in history.iter().enumerate() {
235        let content = &history_content.content;
236        for (part_index, part) in content.parts.iter().enumerate() {
237            match part {
238                Part::FunctionCall { name, id, .. } => {
239                    pending.push(PendingFunctionCall {
240                        location: PartLocation { content: content_index, part: part_index },
241                        id: id.clone(),
242                        name: name.clone(),
243                        user_turn,
244                        invocation_id: history_content.invocation_id.clone(),
245                    });
246                }
247                Part::FunctionResponse { function_response, id, .. } => {
248                    // Idless responses prefer their own invocation. Fall back
249                    // by name only when provenance is missing and unambiguous.
250                    let matching_index = match id.as_deref() {
251                        Some(response_id) => {
252                            pending.iter().position(|call| call.id.as_deref() == Some(response_id))
253                        }
254                        None => pending
255                            .iter()
256                            .position(|call| {
257                                call.name == function_response.name
258                                    && history_content.invocation_id.is_some()
259                                    && call.invocation_id.as_deref()
260                                        == history_content.invocation_id.as_deref()
261                            })
262                            .or_else(|| {
263                                let mut same_name =
264                                    pending.iter().enumerate().filter(|(_, call)| {
265                                        call.name == function_response.name
266                                            && (call.invocation_id.is_none()
267                                                || history_content.invocation_id.is_none())
268                                    });
269                                let first = same_name.next().map(|(index, _)| index);
270                                match (first, same_name.next()) {
271                                    (Some(index), None) => Some(index),
272                                    _ => None,
273                                }
274                            }),
275                    };
276                    if let Some(index) = matching_index {
277                        let call = pending.remove(index);
278                        if call.user_turn < user_turn {
279                            removed_parts
280                                .insert(PartLocation { content: content_index, part: part_index });
281                            let mut insertion_index = call.location.content;
282                            while let Some(next) = history.get(insertion_index + 1) {
283                                if next.content.parts.is_empty()
284                                    || !next
285                                        .content
286                                        .parts
287                                        .iter()
288                                        .all(|part| matches!(part, Part::FunctionResponse { .. }))
289                                {
290                                    break;
291                                }
292                                insertion_index += 1;
293                            }
294                            responses_after.entry(insertion_index).or_default().push((
295                                call.location.part,
296                                content.role.clone(),
297                                part.clone(),
298                            ));
299                        }
300                    }
301                }
302                _ => {}
303            }
304        }
305        if content.role == "user"
306            && content.parts.iter().any(|part| !matches!(part, Part::FunctionResponse { .. }))
307        {
308            user_turn += 1;
309        }
310    }
311
312    for call in pending {
313        if call.user_turn == user_turn {
314            continue;
315        }
316        removed_parts.insert(call.location);
317    }
318
319    let mut normalized = Vec::with_capacity(history.len());
320    for (content_index, history_content) in history.into_iter().enumerate() {
321        let mut content = history_content.content;
322        content.parts = content
323            .parts
324            .into_iter()
325            .enumerate()
326            .filter(|(part_index, _)| {
327                !removed_parts.contains(&PartLocation { content: content_index, part: *part_index })
328            })
329            .map(|(_, part)| part)
330            .collect();
331        if !content.parts.is_empty() {
332            normalized.push(content);
333        }
334        if let Some(mut responses) = responses_after.remove(&content_index) {
335            responses.sort_by_key(|(call_part, _, _)| *call_part);
336            normalized.extend(
337                responses
338                    .into_iter()
339                    .map(|(_, role, response)| Content { role, parts: vec![response] }),
340            );
341        }
342    }
343
344    normalized
345}
346
347impl adk_core::Session for MutableSession {
348    fn id(&self) -> &str {
349        self.inner.id()
350    }
351
352    fn app_name(&self) -> &str {
353        self.inner.app_name()
354    }
355
356    fn user_id(&self) -> &str {
357        self.inner.user_id()
358    }
359
360    fn state(&self) -> &dyn adk_core::State {
361        self
362    }
363
364    fn conversation_history(&self) -> Vec<adk_core::Content> {
365        self.conversation_history_for_agent_impl(None, "")
366    }
367
368    fn conversation_history_for_agent(&self, agent_name: &str) -> Vec<adk_core::Content> {
369        self.conversation_history_for_agent_impl(Some(agent_name), "")
370    }
371
372    fn conversation_history_scoped(
373        &self,
374        agent_name: Option<&str>,
375        branch: &str,
376    ) -> Vec<adk_core::Content> {
377        self.conversation_history_for_agent_impl(agent_name, branch)
378    }
379}
380
381impl adk_core::State for MutableSession {
382    fn get(&self, key: &str) -> Option<serde_json::Value> {
383        let Ok(state) = self.state.read() else {
384            tracing::error!("state RwLock poisoned in State::get — returning None");
385            return None;
386        };
387        state.get(key).cloned()
388    }
389
390    fn set(&mut self, key: String, value: serde_json::Value) {
391        if let Err(msg) = adk_core::validate_state_key(&key) {
392            tracing::warn!(key = %key, "rejecting invalid state key: {msg}");
393            return;
394        }
395        let Ok(mut state) = self.state.write() else {
396            tracing::error!("state RwLock poisoned in State::set — value dropped");
397            return;
398        };
399        state.insert(key, value);
400    }
401
402    fn all(&self) -> HashMap<String, serde_json::Value> {
403        let Ok(state) = self.state.read() else {
404            tracing::error!("state RwLock poisoned in State::all — returning empty");
405            return HashMap::new();
406        };
407        state.clone()
408    }
409}
410
411/// Runtime context for a single agent invocation.
412///
413/// Holds the agent, session, identity, and optional services (artifacts, memory,
414/// secrets) needed during execution. Created by the [`Runner`](crate::Runner) for
415/// each `run()` call.
416pub struct InvocationContext {
417    identity: ExecutionIdentity,
418    agent: Arc<dyn Agent>,
419    user_content: Content,
420    artifacts: Option<Arc<dyn Artifacts>>,
421    memory: Option<Arc<dyn Memory>>,
422    run_config: RunConfig,
423    ended: Arc<AtomicBool>,
424    /// Mutable session that allows state to be updated during execution.
425    /// This is shared across all agents in a workflow, enabling state
426    /// propagation between sequential/parallel agents.
427    session: Arc<MutableSession>,
428    /// Optional request context from the server's auth middleware bridge.
429    /// When present, `user_id()` returns `request_context.user_id` and
430    /// `user_scopes()` returns `request_context.scopes`.
431    request_context: Option<RequestContext>,
432    /// Optional shared state for parallel agent coordination.
433    shared_state: Option<Arc<adk_core::SharedState>>,
434    /// Optional secret service for retrieving secrets at runtime.
435    /// When present, `get_secret()` delegates to this service.
436    secret_service: Option<Arc<dyn SecretService>>,
437    /// Optional cooperative cancellation token.
438    ///
439    /// When present, `is_cancelled()` reflects this token, letting agents and
440    /// tools detect external cancellation (`Runner::interrupt()` or
441    /// `RunConfig::cancellation_token`) during long-running work.
442    cancellation_token: Option<tokio_util::sync::CancellationToken>,
443    /// Stable root invocation for a multi-hop orchestration tree.
444    orchestration_root_invocation_id: String,
445}
446
447impl InvocationContext {
448    /// Create a new invocation context from validated typed identifiers.
449    pub fn new_typed(
450        invocation_id: String,
451        agent: Arc<dyn Agent>,
452        user_id: UserId,
453        app_name: AppName,
454        session_id: SessionId,
455        user_content: Content,
456        session: Arc<dyn AdkSession>,
457    ) -> adk_core::Result<Self> {
458        let orchestration_root_invocation_id = invocation_id.clone();
459        let identity = ExecutionIdentity {
460            adk: AdkIdentity { app_name, user_id, session_id },
461            invocation_id: InvocationId::try_from(invocation_id)?,
462            branch: String::new(),
463            agent_name: agent.name().to_string(),
464        };
465        Ok(Self {
466            identity,
467            agent,
468            user_content,
469            artifacts: None,
470            memory: None,
471            run_config: RunConfig::default(),
472            ended: Arc::new(AtomicBool::new(false)),
473            session: Arc::new(MutableSession::new(session)),
474            request_context: None,
475            shared_state: None,
476            secret_service: None,
477            cancellation_token: None,
478            orchestration_root_invocation_id,
479        })
480    }
481
482    /// Create a new invocation context from raw string identifiers.
483    ///
484    /// Validates and converts the string identifiers into typed wrappers.
485    /// Prefer [`new_typed`](Self::new_typed) when you already have validated types.
486    pub fn new(
487        invocation_id: String,
488        agent: Arc<dyn Agent>,
489        user_id: String,
490        app_name: String,
491        session_id: String,
492        user_content: Content,
493        session: Arc<dyn AdkSession>,
494    ) -> adk_core::Result<Self> {
495        Self::new_typed(
496            invocation_id,
497            agent,
498            UserId::try_from(user_id)?,
499            AppName::try_from(app_name)?,
500            SessionId::try_from(session_id)?,
501            user_content,
502            session,
503        )
504    }
505
506    /// Create an invocation context that reuses an existing mutable session and
507    /// validated typed identifiers.
508    pub fn with_mutable_session_typed(
509        invocation_id: String,
510        agent: Arc<dyn Agent>,
511        user_id: UserId,
512        app_name: AppName,
513        session_id: SessionId,
514        user_content: Content,
515        session: Arc<MutableSession>,
516    ) -> adk_core::Result<Self> {
517        let orchestration_root_invocation_id = invocation_id.clone();
518        let identity = ExecutionIdentity {
519            adk: AdkIdentity { app_name, user_id, session_id },
520            invocation_id: InvocationId::try_from(invocation_id)?,
521            branch: String::new(),
522            agent_name: agent.name().to_string(),
523        };
524        Ok(Self {
525            identity,
526            agent,
527            user_content,
528            artifacts: None,
529            memory: None,
530            run_config: RunConfig::default(),
531            ended: Arc::new(AtomicBool::new(false)),
532            session,
533            request_context: None,
534            shared_state: None,
535            secret_service: None,
536            cancellation_token: None,
537            orchestration_root_invocation_id,
538        })
539    }
540
541    /// Create an InvocationContext with an existing MutableSession.
542    /// This allows sharing the same mutable session across multiple contexts
543    /// (e.g., for agent transfers).
544    pub fn with_mutable_session(
545        invocation_id: String,
546        agent: Arc<dyn Agent>,
547        user_id: String,
548        app_name: String,
549        session_id: String,
550        user_content: Content,
551        session: Arc<MutableSession>,
552    ) -> adk_core::Result<Self> {
553        Self::with_mutable_session_typed(
554            invocation_id,
555            agent,
556            UserId::try_from(user_id)?,
557            AppName::try_from(app_name)?,
558            SessionId::try_from(session_id)?,
559            user_content,
560            session,
561        )
562    }
563
564    /// Set the event branch identifier for this context.
565    pub fn with_branch(mut self, branch: String) -> Self {
566        self.identity.branch = branch;
567        self
568    }
569
570    /// Attach an artifact storage service to this context.
571    pub fn with_artifacts(mut self, artifacts: Arc<dyn Artifacts>) -> Self {
572        self.artifacts = Some(artifacts);
573        self
574    }
575
576    /// Attach a memory service for RAG/semantic search.
577    pub fn with_memory(mut self, memory: Arc<dyn Memory>) -> Self {
578        self.memory = Some(memory);
579        self
580    }
581
582    /// Set the run configuration (streaming mode, history limits, etc.).
583    pub fn with_run_config(mut self, config: RunConfig) -> Self {
584        self.run_config = config;
585        self
586    }
587
588    /// Set the request context from the server's auth middleware bridge.
589    ///
590    /// When set, `user_id()` returns `request_context.user_id` (overriding
591    /// the session-scoped identity), and `user_scopes()` returns
592    /// `request_context.scopes`. This is the explicit authenticated user
593    /// override — `RequestContext` remains separate from `ExecutionIdentity`
594    /// and `AdkIdentity` (it does not carry session or invocation IDs).
595    pub fn with_request_context(mut self, ctx: RequestContext) -> Self {
596        self.request_context = Some(ctx);
597        self
598    }
599
600    /// Set the shared state for parallel agent coordination.
601    pub fn with_shared_state(mut self, shared: Arc<adk_core::SharedState>) -> Self {
602        self.shared_state = Some(shared);
603        self
604    }
605
606    /// Set the secret service for runtime secret retrieval.
607    ///
608    /// When configured, tools can call `ctx.get_secret("name")` to retrieve
609    /// secrets from the configured provider (e.g., AWS Secrets Manager,
610    /// Azure Key Vault, GCP Secret Manager).
611    pub fn with_secret_service(mut self, service: Arc<dyn SecretService>) -> Self {
612        self.secret_service = Some(service);
613        self
614    }
615
616    /// Attach a cooperative cancellation token.
617    ///
618    /// When set, [`is_cancelled`](adk_core::InvocationContext::is_cancelled)
619    /// reflects this token, so agents and tools can detect external
620    /// cancellation during long-running work.
621    pub fn with_cancellation_token(mut self, token: tokio_util::sync::CancellationToken) -> Self {
622        self.cancellation_token = Some(token);
623        self
624    }
625
626    /// Preserve the stable orchestration root across a Runner handoff.
627    pub fn with_orchestration_root_invocation_id(mut self, invocation_id: String) -> Self {
628        self.orchestration_root_invocation_id = invocation_id;
629        self
630    }
631
632    /// Get a reference to the mutable session.
633    /// This allows the Runner to apply state deltas when events are processed.
634    pub fn mutable_session(&self) -> &Arc<MutableSession> {
635        &self.session
636    }
637}
638
639#[async_trait]
640impl ReadonlyContext for InvocationContext {
641    fn invocation_id(&self) -> &str {
642        self.identity.invocation_id.as_ref()
643    }
644
645    fn agent_name(&self) -> &str {
646        self.agent.name()
647    }
648
649    fn user_id(&self) -> &str {
650        // Explicit authenticated user override: when a RequestContext is
651        // present (set via with_request_context from the auth middleware
652        // bridge), the authenticated user_id takes precedence over the
653        // session-scoped identity. This keeps auth binding explicit and
654        // ensures the runtime reflects the verified caller identity.
655        self.request_context.as_ref().map_or(self.identity.adk.user_id.as_ref(), |rc| &rc.user_id)
656    }
657
658    fn app_name(&self) -> &str {
659        self.identity.adk.app_name.as_ref()
660    }
661
662    fn session_id(&self) -> &str {
663        self.identity.adk.session_id.as_ref()
664    }
665
666    fn branch(&self) -> &str {
667        &self.identity.branch
668    }
669
670    fn user_content(&self) -> &Content {
671        &self.user_content
672    }
673
674    fn state(&self) -> Option<&dyn adk_core::State> {
675        // Expose the runner's mutable session view so a toolset can observe a
676        // state delta emitted by an earlier tool result on the next model turn.
677        Some(adk_core::Session::state(self.session.as_ref()))
678    }
679}
680
681#[async_trait]
682impl CallbackContext for InvocationContext {
683    fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
684        self.artifacts.clone()
685    }
686
687    fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
688        self.shared_state.clone()
689    }
690}
691
692#[async_trait]
693impl InvocationContextTrait for InvocationContext {
694    fn agent(&self) -> Arc<dyn Agent> {
695        self.agent.clone()
696    }
697
698    fn memory(&self) -> Option<Arc<dyn Memory>> {
699        self.memory.clone()
700    }
701
702    fn session(&self) -> &dyn adk_core::Session {
703        self.session.as_ref()
704    }
705
706    fn run_config(&self) -> &RunConfig {
707        &self.run_config
708    }
709
710    fn end_invocation(&self) {
711        self.ended.store(true, std::sync::atomic::Ordering::SeqCst);
712    }
713
714    fn ended(&self) -> bool {
715        self.ended.load(std::sync::atomic::Ordering::SeqCst)
716    }
717
718    fn is_cancelled(&self) -> bool {
719        self.cancellation_token
720            .as_ref()
721            .is_some_and(tokio_util::sync::CancellationToken::is_cancelled)
722    }
723
724    fn user_scopes(&self) -> Vec<String> {
725        self.request_context.as_ref().map_or_else(Vec::new, |rc| rc.scopes.clone())
726    }
727
728    fn request_metadata(&self) -> HashMap<String, serde_json::Value> {
729        self.request_context.as_ref().map_or_else(HashMap::new, |rc| {
730            rc.metadata
731                .iter()
732                .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
733                .collect()
734        })
735    }
736
737    fn orchestration_root_invocation_id(&self) -> &str {
738        &self.orchestration_root_invocation_id
739    }
740
741    async fn get_secret(&self, name: &str) -> adk_core::Result<Option<String>> {
742        let request = adk_core::SecretRequest::new(name)
743            .with_identity(self.app_name(), self.user_id(), self.session_id())
744            .with_invocation_id(self.invocation_id());
745        self.get_secret_for(&request).await
746    }
747
748    async fn get_secret_for(
749        &self,
750        request: &adk_core::SecretRequest,
751    ) -> adk_core::Result<Option<String>> {
752        match &self.secret_service {
753            Some(service) => service.get_secret_for(request).await.map(Some),
754            None => Ok(None),
755        }
756    }
757}