Skip to main content

adk_tool/
agent_tool.rs

1//! AgentTool - Use agents as callable tools
2//!
3//! This module provides `AgentTool` which wraps an `Agent` instance to make it
4//! callable as a `Tool`. This enables powerful composition patterns where a
5//! coordinator agent can invoke specialized sub-agents.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use adk_tool::AgentTool;
11//! use adk_agent::LlmAgentBuilder;
12//!
13//! // Create a specialized agent
14//! let math_agent = LlmAgentBuilder::new("math_expert")
15//!     .description("Solves mathematical problems")
16//!     .instruction("You are a math expert. Solve problems step by step.")
17//!     .model(model.clone())
18//!     .build()?;
19//!
20//! // Wrap it as a tool
21//! let math_tool = AgentTool::new(Arc::new(math_agent));
22//!
23//! // Use in coordinator agent
24//! let coordinator = LlmAgentBuilder::new("coordinator")
25//!     .instruction("Help users by delegating to specialists")
26//!     .tools(vec![Arc::new(math_tool)])
27//!     .build()?;
28//! ```
29
30use adk_core::{
31    Agent, Artifacts, CallbackContext, Content, Event, InvocationContext, Memory, Part,
32    ReadonlyContext, Result, RunConfig, Session, State, Tool, ToolContext,
33};
34use async_trait::async_trait;
35use futures::StreamExt;
36use serde_json::{Value, json};
37use std::collections::{HashMap, HashSet};
38use std::sync::{Arc, atomic::AtomicBool};
39use std::time::Duration;
40
41/// Controls which parent session data is copied into an agent-tool invocation.
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub enum AgentToolSessionSnapshot {
44    /// Start each delegated invocation with empty history and state.
45    #[default]
46    Isolated,
47    /// Copy the parent's current conversation history and state into the isolated child session.
48    Parent,
49}
50
51/// Controls how an [`AgentTool`] reports delegated execution failures.
52#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53pub enum AgentToolFailureMode {
54    /// Return the legacy JSON error object as a successful tool result.
55    #[default]
56    ReturnErrorObject,
57    /// Propagate the failure through the tool's [`Result`].
58    Propagate,
59}
60
61/// Merge behavior for state produced by an agent-as-tool invocation.
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63pub enum AgentToolStateMergePolicy {
64    /// Preserve legacy last-writer-wins behavior.
65    #[default]
66    Overwrite,
67    /// Reject a child write when the parent value changed after delegation began.
68    RejectConflicts,
69}
70
71#[derive(Clone)]
72struct AgentToolRuntimeConfig {
73    session_snapshot: AgentToolSessionSnapshot,
74    failure_mode: AgentToolFailureMode,
75    forward_memory: bool,
76    forward_shared_state: bool,
77    forward_events: bool,
78    max_delegation_depth: Option<u32>,
79    reject_child_handoffs: bool,
80    execute_child_handoffs: bool,
81    handoff_agents: Arc<HashMap<String, Arc<dyn Agent>>>,
82    history_max_events: Option<usize>,
83    state_keys: Option<Arc<HashSet<String>>>,
84    output_state_keys: Option<Arc<HashSet<String>>>,
85    state_merge_policy: AgentToolStateMergePolicy,
86    state_merge_exempt_keys: Arc<HashSet<String>>,
87    artifact_prefixes: Option<Arc<Vec<String>>>,
88}
89
90impl Default for AgentToolRuntimeConfig {
91    fn default() -> Self {
92        Self {
93            session_snapshot: AgentToolSessionSnapshot::Isolated,
94            failure_mode: AgentToolFailureMode::ReturnErrorObject,
95            forward_memory: false,
96            forward_shared_state: true,
97            forward_events: false,
98            max_delegation_depth: None,
99            reject_child_handoffs: false,
100            execute_child_handoffs: false,
101            handoff_agents: Arc::new(HashMap::new()),
102            history_max_events: None,
103            state_keys: None,
104            output_state_keys: None,
105            state_merge_policy: AgentToolStateMergePolicy::Overwrite,
106            state_merge_exempt_keys: Arc::new(HashSet::new()),
107            artifact_prefixes: None,
108        }
109    }
110}
111
112struct AgentToolChildConfig {
113    forward_artifacts: bool,
114    artifact_prefixes: Option<Arc<Vec<String>>>,
115    forward_memory: bool,
116    forward_shared_state: bool,
117    session_snapshot: AgentToolSessionSnapshot,
118    run_config: RunConfig,
119    delegation_depth: u32,
120    max_delegation_depth: Option<u32>,
121    history_max_events: Option<usize>,
122    state_keys: Option<Arc<HashSet<String>>>,
123    orchestration_root_invocation_id: String,
124    orchestration_edge_id: String,
125}
126
127/// Configuration options for AgentTool behavior.
128#[derive(Debug, Clone)]
129pub struct AgentToolConfig {
130    /// Skip summarization after sub-agent execution.
131    /// When true, returns the raw output from the sub-agent.
132    pub skip_summarization: bool,
133
134    /// Forward artifacts between parent and sub-agent.
135    /// When true, the sub-agent can access parent's artifacts.
136    pub forward_artifacts: bool,
137
138    /// Optional timeout for sub-agent execution.
139    pub timeout: Option<Duration>,
140
141    /// Custom input schema for the tool.
142    /// If None, defaults to `{"request": "string"}`.
143    pub input_schema: Option<Value>,
144
145    /// Custom output schema for the tool.
146    pub output_schema: Option<Value>,
147}
148
149impl Default for AgentToolConfig {
150    fn default() -> Self {
151        Self {
152            skip_summarization: false,
153            forward_artifacts: true,
154            timeout: None,
155            input_schema: None,
156            output_schema: None,
157        }
158    }
159}
160
161/// AgentTool wraps an Agent to make it callable as a Tool.
162///
163/// When the parent LLM generates a function call targeting this tool,
164/// the framework executes the wrapped agent, captures its final response,
165/// and returns it as the tool's result.
166pub struct AgentTool {
167    agent: Arc<dyn Agent>,
168    config: AgentToolConfig,
169    runtime: AgentToolRuntimeConfig,
170}
171
172impl AgentTool {
173    /// Create a new AgentTool wrapping the given agent.
174    pub fn new(agent: Arc<dyn Agent>) -> Self {
175        Self {
176            agent,
177            config: AgentToolConfig::default(),
178            runtime: AgentToolRuntimeConfig::default(),
179        }
180    }
181
182    /// Create a new AgentTool with custom configuration.
183    pub fn with_config(agent: Arc<dyn Agent>, config: AgentToolConfig) -> Self {
184        Self { agent, config, runtime: AgentToolRuntimeConfig::default() }
185    }
186
187    /// Set whether to skip summarization.
188    pub fn skip_summarization(mut self, skip: bool) -> Self {
189        self.config.skip_summarization = skip;
190        self
191    }
192
193    /// Set whether to forward artifacts.
194    pub fn forward_artifacts(mut self, forward: bool) -> Self {
195        self.config.forward_artifacts = forward;
196        self
197    }
198
199    /// Set timeout for sub-agent execution.
200    pub fn timeout(mut self, timeout: Duration) -> Self {
201        self.config.timeout = Some(timeout);
202        self
203    }
204
205    /// Set custom input schema.
206    pub fn input_schema(mut self, schema: Value) -> Self {
207        self.config.input_schema = Some(schema);
208        self
209    }
210
211    /// Set custom output schema.
212    pub fn output_schema(mut self, schema: Value) -> Self {
213        self.config.output_schema = Some(schema);
214        self
215    }
216
217    /// Choose which parent session data is snapshotted into the isolated child session.
218    pub fn session_snapshot(mut self, snapshot: AgentToolSessionSnapshot) -> Self {
219        self.runtime.session_snapshot = snapshot;
220        self
221    }
222
223    /// Set whether the wrapped agent can access the parent's memory service.
224    pub fn forward_memory(mut self, forward: bool) -> Self {
225        self.runtime.forward_memory = forward;
226        self
227    }
228
229    /// Set whether the wrapped agent can access the parent's parallel shared state.
230    ///
231    /// This defaults to `true` to preserve the historical AgentTool behavior.
232    pub fn forward_shared_state(mut self, forward: bool) -> Self {
233        self.runtime.forward_shared_state = forward;
234        self
235    }
236
237    /// Set whether child events are emitted through the parent tool context.
238    ///
239    /// The default is `false` for backward compatibility. State and artifact
240    /// deltas are merged regardless of this setting.
241    pub fn forward_events(mut self, forward: bool) -> Self {
242        self.runtime.forward_events = forward;
243        self
244    }
245
246    /// Choose whether delegated failures are returned as JSON or propagated.
247    pub fn failure_mode(mut self, mode: AgentToolFailureMode) -> Self {
248        self.runtime.failure_mode = mode;
249        self
250    }
251
252    /// Set whether delegated failures should be propagated as tool errors.
253    pub fn propagate_failures(self, propagate: bool) -> Self {
254        self.failure_mode(if propagate {
255            AgentToolFailureMode::Propagate
256        } else {
257            AgentToolFailureMode::ReturnErrorObject
258        })
259    }
260
261    /// Bound the number of nested agent-as-tool delegations.
262    pub fn max_delegation_depth(mut self, max_depth: u32) -> Self {
263        self.runtime.max_delegation_depth = Some(max_depth);
264        self
265    }
266
267    /// Reject child handoff events that cannot be executed by `AgentTool` itself.
268    pub fn reject_child_handoffs(mut self, reject: bool) -> Self {
269        self.runtime.reject_child_handoffs = reject;
270        self
271    }
272
273    /// Executes child handoffs using the supplied exact target registry.
274    ///
275    /// Disabled by default for backward compatibility. When enabled, a child
276    /// transfer remains inside the nested invocation until the terminal member
277    /// returns, after which its result is returned to the original caller.
278    pub fn execute_child_handoffs(
279        mut self,
280        agents: impl IntoIterator<Item = Arc<dyn Agent>>,
281    ) -> Self {
282        self.runtime.execute_child_handoffs = true;
283        self.runtime.reject_child_handoffs = false;
284        self.runtime.handoff_agents =
285            Arc::new(agents.into_iter().map(|agent| (agent.name().to_string(), agent)).collect());
286        self
287    }
288
289    /// Limits the parent history copied into a delegated session.
290    ///
291    /// A value of zero copies no history while still allowing a filtered state
292    /// snapshot when parent-session forwarding is enabled.
293    pub fn history_max_events(mut self, max_events: usize) -> Self {
294        self.runtime.history_max_events = Some(max_events);
295        self
296    }
297
298    /// Copies only these exact state keys into a delegated session.
299    pub fn state_keys(mut self, keys: impl IntoIterator<Item = impl Into<String>>) -> Self {
300        self.runtime.state_keys = Some(Arc::new(keys.into_iter().map(Into::into).collect()));
301        self
302    }
303
304    /// Allows delegated state writes only to these exact keys.
305    pub fn output_state_keys(mut self, keys: impl IntoIterator<Item = impl Into<String>>) -> Self {
306        self.runtime.output_state_keys = Some(Arc::new(keys.into_iter().map(Into::into).collect()));
307        self
308    }
309
310    /// Selects how child state writes merge with concurrent parent updates.
311    pub fn state_merge_policy(mut self, policy: AgentToolStateMergePolicy) -> Self {
312        self.runtime.state_merge_policy = policy;
313        self
314    }
315
316    /// Exempts trusted framework-owned keys from concurrent merge checks.
317    ///
318    /// Output allowlists still apply. This is intended for runtime bookkeeping
319    /// that is deliberately updated by both the parent and delegated context.
320    pub fn state_merge_exempt_keys(
321        mut self,
322        keys: impl IntoIterator<Item = impl Into<String>>,
323    ) -> Self {
324        self.runtime.state_merge_exempt_keys = Arc::new(keys.into_iter().map(Into::into).collect());
325        self
326    }
327
328    /// Allows artifact writes only when their names start with one of these prefixes.
329    pub fn artifact_prefixes(
330        mut self,
331        prefixes: impl IntoIterator<Item = impl Into<String>>,
332    ) -> Self {
333        self.runtime.artifact_prefixes =
334            Some(Arc::new(prefixes.into_iter().map(Into::into).collect()));
335        self
336    }
337
338    fn failure(&self, message: String) -> Result<Value> {
339        match self.runtime.failure_mode {
340            AgentToolFailureMode::ReturnErrorObject => Ok(json!({
341                "error": message,
342                "agent": self.agent.name()
343            })),
344            AgentToolFailureMode::Propagate => Err(adk_core::AdkError::tool(message)),
345        }
346    }
347
348    fn state_policy_failure(&self, message: String) -> Result<Value> {
349        match self.runtime.failure_mode {
350            AgentToolFailureMode::ReturnErrorObject => Ok(json!({
351                "error": message,
352                "agent": self.agent.name(),
353                "code": "tool.agent.state_policy_violation"
354            })),
355            AgentToolFailureMode::Propagate => Err(adk_core::AdkError::new(
356                adk_core::ErrorComponent::Tool,
357                adk_core::ErrorCategory::InvalidInput,
358                "tool.agent.state_policy_violation",
359                message,
360            )),
361        }
362    }
363
364    /// Generate the default parameters schema for this agent tool.
365    fn default_parameters_schema(&self) -> Value {
366        json!({
367            "type": "object",
368            "properties": {
369                "request": {
370                    "type": "string",
371                    "description": format!("The request to send to the {} agent", self.agent.name())
372                }
373            },
374            "required": ["request"]
375        })
376    }
377
378    /// Extract the request text from the tool arguments.
379    fn extract_request(&self, args: &Value) -> String {
380        // Try to get "request" field first
381        if let Some(request) = args.get("request").and_then(|v| v.as_str()) {
382            return request.to_string();
383        }
384
385        // If custom schema, try to serialize the whole args
386        if self.config.input_schema.is_some() {
387            return serde_json::to_string(args).unwrap_or_default();
388        }
389
390        // Fallback: convert args to string
391        match args {
392            Value::String(s) => s.clone(),
393            Value::Object(map) => {
394                // Try to find any string field
395                for value in map.values() {
396                    if let Value::String(s) = value {
397                        return s.clone();
398                    }
399                }
400                serde_json::to_string(args).unwrap_or_default()
401            }
402            _ => serde_json::to_string(args).unwrap_or_default(),
403        }
404    }
405
406    /// Extract the final response text from agent events.
407    fn extract_response(events: &[Event]) -> Value {
408        // Collect all text responses from final events
409        let mut responses = Vec::new();
410
411        for event in events.iter().rev() {
412            if event.is_final_response() {
413                if let Some(content) = &event.llm_response.content {
414                    for part in &content.parts {
415                        if let Part::Text { text } = part {
416                            responses.push(text.clone());
417                        }
418                    }
419                }
420                break; // Only get the last final response
421            }
422        }
423
424        if responses.is_empty() {
425            // Try to get any text from the last event
426            if let Some(last_event) = events.last()
427                && let Some(content) = &last_event.llm_response.content
428            {
429                for part in &content.parts {
430                    if let Part::Text { text } = part {
431                        return json!({ "response": text });
432                    }
433                }
434            }
435            json!({ "response": "No response from agent" })
436        } else {
437            json!({ "response": responses.concat() })
438        }
439    }
440
441    fn project_parent_history(history: Vec<Content>, max_events: Option<usize>) -> Vec<Content> {
442        let mut pending_calls = HashMap::<String, usize>::new();
443        let mut projected = Vec::with_capacity(history.len());
444        let mut balanced_boundaries = vec![0];
445        let mut open_group_start = None;
446
447        for content in history {
448            let was_pending = !pending_calls.is_empty();
449            let has_function_call =
450                content.parts.iter().any(|part| matches!(part, Part::FunctionCall { .. }));
451            let has_function_response =
452                content.parts.iter().any(|part| matches!(part, Part::FunctionResponse { .. }));
453
454            // Progress from a delegated agent may be emitted while its caller's
455            // tool call is still open. It is useful to stream to observers, but
456            // it is not valid provider history between that call and response.
457            if was_pending && !has_function_call && !has_function_response {
458                continue;
459            }
460            if !was_pending && has_function_response && !has_function_call {
461                continue;
462            }
463
464            if !was_pending && has_function_call {
465                open_group_start = Some(projected.len());
466            }
467            for part in &content.parts {
468                match part {
469                    Part::FunctionCall { name, id, .. } => {
470                        let key = id.as_ref().unwrap_or(name);
471                        *pending_calls.entry(key.clone()).or_default() += 1;
472                    }
473                    Part::FunctionResponse { function_response, id, .. } => {
474                        let key = id.as_ref().unwrap_or(&function_response.name);
475                        if let Some(count) = pending_calls.get_mut(key) {
476                            *count -= 1;
477                            if *count == 0 {
478                                pending_calls.remove(key);
479                            }
480                        }
481                    }
482                    _ => {}
483                }
484            }
485            projected.push(content);
486            if pending_calls.is_empty() {
487                open_group_start = None;
488                balanced_boundaries.push(projected.len());
489            }
490        }
491
492        // ToolContext execution occurs before the caller's FunctionResponse is
493        // added to its session. Never expose that currently-open call (or any
494        // trailing content after it) as child model history.
495        if let Some(group_start) = open_group_start {
496            projected.truncate(group_start);
497        }
498        let balanced_end = projected.len();
499
500        if let Some(max_events) = max_events {
501            let desired_start = balanced_end.saturating_sub(max_events);
502            let balanced_start = balanced_boundaries
503                .into_iter()
504                .find(|boundary| *boundary >= desired_start)
505                .unwrap_or(balanced_end);
506            projected.drain(..balanced_start);
507        }
508
509        projected
510    }
511}
512
513#[async_trait]
514impl Tool for AgentTool {
515    fn name(&self) -> &str {
516        self.agent.name()
517    }
518
519    fn description(&self) -> &str {
520        self.agent.description()
521    }
522
523    fn parameters_schema(&self) -> Option<Value> {
524        Some(self.config.input_schema.clone().unwrap_or_else(|| self.default_parameters_schema()))
525    }
526
527    fn response_schema(&self) -> Option<Value> {
528        self.config.output_schema.clone()
529    }
530
531    fn is_long_running(&self) -> bool {
532        // Agent execution could take time, but we wait for completion
533        false
534    }
535
536    fn is_agent_delegation(&self) -> bool {
537        true
538    }
539
540    #[adk_telemetry::instrument(
541        skip(self, ctx, args),
542        fields(
543            agent_tool.name = %self.agent.name(),
544            agent_tool.description = %self.agent.description(),
545            function_call.id = %ctx.function_call_id()
546        )
547    )]
548    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
549        adk_telemetry::debug!("Executing agent tool: {}", self.agent.name());
550
551        let parent_run_config = ctx.run_config().cloned().unwrap_or_default();
552        let child_depth = ctx.delegation_depth().saturating_add(1);
553        let max_depth = self.runtime.max_delegation_depth.or(ctx.max_delegation_depth());
554        if max_depth.is_some_and(|max| child_depth > max) {
555            return self.failure(format!(
556                "agent delegation depth {child_depth} exceeds the configured maximum of {}",
557                max_depth.unwrap_or_default()
558            ));
559        }
560        if ctx.is_cancelled() {
561            return self.failure("agent delegation was cancelled before execution".to_string());
562        }
563
564        // Extract the request from args
565        let request_text = self.extract_request(&args);
566        let parent_state_baseline =
567            ctx.session().map(|session| session.state().all()).unwrap_or_default();
568
569        // Create user content for the sub-agent
570        let user_content = Content::new("user").with_text(&request_text);
571
572        // Create an isolated context for the sub-agent
573        let sub_ctx = Arc::new(AgentToolInvocationContext::new(
574            ctx.clone(),
575            self.agent.clone(),
576            user_content.clone(),
577            AgentToolChildConfig {
578                forward_artifacts: self.config.forward_artifacts,
579                artifact_prefixes: self.runtime.artifact_prefixes.clone(),
580                forward_memory: self.runtime.forward_memory,
581                forward_shared_state: self.runtime.forward_shared_state,
582                session_snapshot: self.runtime.session_snapshot,
583                run_config: parent_run_config,
584                delegation_depth: child_depth,
585                max_delegation_depth: max_depth,
586                history_max_events: self.runtime.history_max_events,
587                state_keys: self.runtime.state_keys.clone(),
588                orchestration_root_invocation_id: ctx
589                    .orchestration_root_invocation_id()
590                    .to_string(),
591                orchestration_edge_id: ctx.orchestration_edge_id().map_or_else(
592                    || ctx.function_call_id().to_string(),
593                    |parent| format!("{parent}/{}", ctx.function_call_id()),
594                ),
595            },
596        ));
597
598        // Execute the sub-agent
599        let execution = async {
600            let mut active_agent = self.agent.clone();
601            let mut active_ctx = sub_ctx.clone();
602            let mut transfer_depth = 0_u32;
603            let mut events = Vec::new();
604            let mut state_delta = HashMap::new();
605            let mut artifact_delta = HashMap::new();
606
607            loop {
608                let mut event_stream = active_agent.run(active_ctx.clone()).await?;
609                let mut transfer = None;
610                while let Some(result) = event_stream.next().await {
611                    match result {
612                        Ok(event) => {
613                            if let Some(target) = &event.actions.transfer_to_agent {
614                                if self.runtime.reject_child_handoffs {
615                                    return Err(adk_core::AdkError::tool(format!(
616                                        "agent '{}' requested handoff to '{target}', but AgentTool cannot execute child handoffs in this configuration",
617                                        active_agent.name()
618                                    )));
619                                }
620                                if self.runtime.execute_child_handoffs {
621                                    transfer = Some(target.clone());
622                                }
623                            }
624                            state_delta.extend(event.actions.state_delta.clone());
625                            artifact_delta.extend(event.actions.artifact_delta.clone());
626                            sub_ctx.session.apply_event(&event);
627                            if self.runtime.forward_events {
628                                let mut forwarded = event.clone();
629                                if self.runtime.execute_child_handoffs && transfer.is_some() {
630                                    // The AgentTool consumes this control-flow edge internally.
631                                    // Do not let the parent Runner execute the same handoff again.
632                                    forwarded.actions.transfer_to_agent = None;
633                                }
634                                ctx.emit_event(forwarded).await;
635                            }
636                            events.push(event);
637                            if transfer.is_some() {
638                                break;
639                            }
640                        }
641                        Err(error) => {
642                            adk_telemetry::error!("Error in sub-agent execution: {error}");
643                            return Err(error);
644                        }
645                    }
646                }
647
648                let Some(target_name) = transfer else {
649                    break;
650                };
651                transfer_depth = transfer_depth.saturating_add(1);
652                let max_transfer_depth = active_ctx.run_config.max_transfer_depth.unwrap_or(10);
653                if transfer_depth > max_transfer_depth {
654                    return Err(adk_core::AdkError::tool(format!(
655                        "nested handoff depth {transfer_depth} exceeds the configured maximum of {max_transfer_depth}"
656                    )));
657                }
658                let target = self
659                    .runtime
660                    .handoff_agents
661                    .get(&target_name)
662                    .cloned()
663                    .ok_or_else(|| {
664                        adk_core::AdkError::tool(format!(
665                            "agent '{}' requested nested handoff to unregistered target '{target_name}'",
666                            active_agent.name()
667                        ))
668                    })?;
669                active_ctx = Arc::new(active_ctx.for_agent(target.clone()));
670                active_agent = target;
671            }
672
673            Ok((events, state_delta, artifact_delta))
674        };
675
676        // Apply timeout if configured
677        let result = if let Some(timeout_duration) = self.config.timeout {
678            match tokio::time::timeout(timeout_duration, execution).await {
679                Ok(r) => r,
680                Err(_) => {
681                    return self.failure(format!(
682                        "agent '{}' execution timed out after {timeout_duration:?}",
683                        self.agent.name()
684                    ));
685                }
686            }
687        } else {
688            execution.await
689        };
690
691        match result {
692            Ok((events, mut state_delta, artifact_delta)) => {
693                if let Some(allowed) = &self.runtime.output_state_keys {
694                    if let Some(key) = state_delta.keys().find(|key| !allowed.contains(*key)) {
695                        return self.state_policy_failure(format!(
696                            "agent '{}' attempted unauthorized state write to '{key}'",
697                            self.agent.name()
698                        ));
699                    }
700                    state_delta.retain(|key, _| allowed.contains(key));
701                }
702                if self.runtime.state_merge_policy == AgentToolStateMergePolicy::RejectConflicts
703                    && let Some(parent_session) = ctx.session()
704                    && let Some(key) = state_delta.keys().find(|key| {
705                        !self.runtime.state_merge_exempt_keys.contains(*key)
706                            && parent_session.state().get(key)
707                                != parent_state_baseline.get(*key).cloned()
708                    })
709                {
710                    return self.state_policy_failure(format!(
711                        "agent '{}' state write to '{key}' conflicts with a concurrent parent update",
712                        self.agent.name()
713                    ));
714                }
715                if let Some(prefixes) = &self.runtime.artifact_prefixes
716                    && let Some(name) = artifact_delta
717                        .keys()
718                        .find(|name| !prefixes.iter().any(|prefix| name.starts_with(prefix)))
719                {
720                    return self.state_policy_failure(format!(
721                        "agent '{}' attempted unauthorized artifact write to '{name}'",
722                        self.agent.name()
723                    ));
724                }
725                // Forward state_delta and artifact_delta to parent context
726                if !state_delta.is_empty()
727                    || !artifact_delta.is_empty()
728                    || self.config.skip_summarization
729                {
730                    let mut parent_actions = ctx.actions();
731                    parent_actions.state_delta.extend(state_delta);
732                    parent_actions.artifact_delta.extend(artifact_delta);
733                    parent_actions.skip_summarization |= self.config.skip_summarization;
734                    ctx.set_actions(parent_actions);
735                }
736
737                // Extract and return the response
738                let response = Self::extract_response(&events);
739
740                adk_telemetry::debug!(
741                    "Agent tool {} completed with {} events",
742                    self.agent.name(),
743                    events.len()
744                );
745
746                Ok(response)
747            }
748            Err(e) => self.failure(format!("agent execution failed: {e}")),
749        }
750    }
751}
752
753// Internal context for sub-agent execution
754struct AgentToolInvocationContext {
755    parent_ctx: Arc<dyn ToolContext>,
756    agent: Arc<dyn Agent>,
757    user_content: Content,
758    invocation_id: String,
759    ended: Arc<AtomicBool>,
760    forward_artifacts: bool,
761    artifact_prefixes: Option<Arc<Vec<String>>>,
762    forward_memory: bool,
763    forward_shared_state: bool,
764    session: Arc<AgentToolSession>,
765    run_config: RunConfig,
766    delegation_depth: u32,
767    max_delegation_depth: Option<u32>,
768    orchestration_root_invocation_id: String,
769    orchestration_edge_id: String,
770}
771
772impl AgentToolInvocationContext {
773    fn new(
774        parent_ctx: Arc<dyn ToolContext>,
775        agent: Arc<dyn Agent>,
776        user_content: Content,
777        child_config: AgentToolChildConfig,
778    ) -> Self {
779        let AgentToolChildConfig {
780            forward_artifacts,
781            artifact_prefixes,
782            forward_memory,
783            forward_shared_state,
784            session_snapshot,
785            mut run_config,
786            delegation_depth,
787            max_delegation_depth,
788            history_max_events,
789            state_keys,
790            orchestration_root_invocation_id,
791            orchestration_edge_id,
792        } = child_config;
793        let invocation_id = format!("agent-tool-{}", uuid::Uuid::new_v4());
794        let (mut state, mut history) = match (session_snapshot, parent_ctx.session()) {
795            (AgentToolSessionSnapshot::Parent, Some(session)) => {
796                (session.state().all(), session.conversation_history())
797            }
798            _ => (HashMap::new(), Vec::new()),
799        };
800        if let Some(keys) = state_keys {
801            state.retain(|key, _| keys.contains(key));
802        }
803        history = AgentTool::project_parent_history(history, history_max_events);
804        run_config.streaming_mode = adk_core::StreamingMode::None;
805        Self {
806            session: Arc::new(AgentToolSession::new(
807                invocation_id.clone(),
808                parent_ctx.app_name().to_string(),
809                parent_ctx.user_id().to_string(),
810                state,
811                history,
812            )),
813            parent_ctx,
814            agent,
815            user_content,
816            invocation_id,
817            ended: Arc::new(AtomicBool::new(false)),
818            forward_artifacts,
819            artifact_prefixes,
820            forward_memory,
821            forward_shared_state,
822            run_config,
823            delegation_depth,
824            max_delegation_depth,
825            orchestration_root_invocation_id,
826            orchestration_edge_id,
827        }
828    }
829
830    fn for_agent(&self, agent: Arc<dyn Agent>) -> Self {
831        let mut run_config = self.run_config.clone();
832        agent.configure_run(agent.name(), &mut run_config);
833        Self {
834            parent_ctx: self.parent_ctx.clone(),
835            agent,
836            user_content: self.user_content.clone(),
837            invocation_id: self.invocation_id.clone(),
838            ended: self.ended.clone(),
839            forward_artifacts: self.forward_artifacts,
840            artifact_prefixes: self.artifact_prefixes.clone(),
841            forward_memory: self.forward_memory,
842            forward_shared_state: self.forward_shared_state,
843            session: self.session.clone(),
844            run_config,
845            delegation_depth: self.delegation_depth,
846            max_delegation_depth: self.max_delegation_depth,
847            orchestration_root_invocation_id: self.orchestration_root_invocation_id.clone(),
848            orchestration_edge_id: self.orchestration_edge_id.clone(),
849        }
850    }
851}
852
853#[async_trait]
854impl ReadonlyContext for AgentToolInvocationContext {
855    fn invocation_id(&self) -> &str {
856        &self.invocation_id
857    }
858
859    fn agent_name(&self) -> &str {
860        self.agent.name()
861    }
862
863    fn user_id(&self) -> &str {
864        self.parent_ctx.user_id()
865    }
866
867    fn app_name(&self) -> &str {
868        self.parent_ctx.app_name()
869    }
870
871    fn session_id(&self) -> &str {
872        // Use a unique session ID for the sub-agent
873        &self.invocation_id
874    }
875
876    fn branch(&self) -> &str {
877        ""
878    }
879
880    fn user_content(&self) -> &Content {
881        &self.user_content
882    }
883}
884
885#[async_trait]
886impl CallbackContext for AgentToolInvocationContext {
887    fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
888        if !self.forward_artifacts {
889            return None;
890        }
891        let artifacts = self.parent_ctx.artifacts()?;
892        self.artifact_prefixes.as_ref().map_or(Some(artifacts.clone()), |prefixes| {
893            Some(Arc::new(AgentToolArtifacts {
894                inner: artifacts,
895                allowed_write_prefixes: prefixes.clone(),
896            }) as Arc<dyn Artifacts>)
897        })
898    }
899
900    fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
901        self.forward_shared_state.then(|| self.parent_ctx.shared_state()).flatten()
902    }
903}
904
905struct AgentToolArtifacts {
906    inner: Arc<dyn Artifacts>,
907    allowed_write_prefixes: Arc<Vec<String>>,
908}
909
910#[async_trait]
911impl Artifacts for AgentToolArtifacts {
912    async fn save(&self, name: &str, data: &Part) -> Result<i64> {
913        if !self.allowed_write_prefixes.iter().any(|prefix| name.starts_with(prefix)) {
914            return Err(adk_core::AdkError::new(
915                adk_core::ErrorComponent::Artifact,
916                adk_core::ErrorCategory::Forbidden,
917                "artifact.agent_tool.write_denied",
918                format!("delegated artifact write to '{name}' is outside the allowed prefixes"),
919            ));
920        }
921        self.inner.save(name, data).await
922    }
923
924    async fn load(&self, name: &str) -> Result<Part> {
925        self.inner.load(name).await
926    }
927
928    async fn list(&self) -> Result<Vec<String>> {
929        self.inner.list().await
930    }
931}
932
933#[async_trait]
934impl InvocationContext for AgentToolInvocationContext {
935    fn agent(&self) -> Arc<dyn Agent> {
936        self.agent.clone()
937    }
938
939    fn memory(&self) -> Option<Arc<dyn Memory>> {
940        self.forward_memory.then(|| self.parent_ctx.memory()).flatten()
941    }
942
943    fn session(&self) -> &dyn Session {
944        self.session.as_ref()
945    }
946
947    fn run_config(&self) -> &RunConfig {
948        &self.run_config
949    }
950
951    fn end_invocation(&self) {
952        self.ended.store(true, std::sync::atomic::Ordering::SeqCst);
953    }
954
955    fn ended(&self) -> bool {
956        self.ended.load(std::sync::atomic::Ordering::SeqCst)
957    }
958
959    fn is_cancelled(&self) -> bool {
960        self.parent_ctx.is_cancelled()
961    }
962
963    /// Authenticated scopes are forwarded so a scope-guarded tool used by the
964    /// wrapped agent sees the caller's grants rather than an empty set.
965    fn user_scopes(&self) -> Vec<String> {
966        self.parent_ctx.user_scopes()
967    }
968
969    fn request_metadata(&self) -> HashMap<String, Value> {
970        self.parent_ctx.request_metadata()
971    }
972
973    fn delegation_depth(&self) -> u32 {
974        self.delegation_depth
975    }
976
977    fn max_delegation_depth(&self) -> Option<u32> {
978        self.max_delegation_depth
979    }
980
981    fn orchestration_root_invocation_id(&self) -> &str {
982        &self.orchestration_root_invocation_id
983    }
984
985    fn orchestration_edge_id(&self) -> Option<&str> {
986        Some(&self.orchestration_edge_id)
987    }
988
989    /// Secret access is forwarded so the wrapped agent's tools can resolve
990    /// secrets through the same provider as the calling agent.
991    async fn get_secret(&self, name: &str) -> adk_core::Result<Option<String>> {
992        self.parent_ctx.get_secret(name).await
993    }
994
995    async fn get_secret_for(
996        &self,
997        request: &adk_core::SecretRequest,
998    ) -> adk_core::Result<Option<String>> {
999        // The parent here is a `ToolContext`, which carries no identity of its own, so
1000        // only the stated purpose survives the hop. An agent invoked as a tool
1001        // therefore presents that agent's identity rather than the inner tool's.
1002        match &request.purpose {
1003            Some(purpose) => self.parent_ctx.get_secret_for_purpose(&request.name, purpose).await,
1004            None => self.parent_ctx.get_secret(&request.name).await,
1005        }
1006    }
1007
1008    // Cancellation and request metadata are forwarded above through the
1009    // backward-compatible ToolContext capability methods.
1010}
1011
1012// Minimal session for sub-agent execution
1013struct AgentToolSession {
1014    id: String,
1015    app_name: String,
1016    user_id: String,
1017    state: std::sync::RwLock<HashMap<String, Value>>,
1018    history: std::sync::RwLock<Vec<Content>>,
1019}
1020
1021impl AgentToolSession {
1022    fn new(
1023        id: String,
1024        app_name: String,
1025        user_id: String,
1026        state: HashMap<String, Value>,
1027        history: Vec<Content>,
1028    ) -> Self {
1029        Self {
1030            id,
1031            app_name,
1032            user_id,
1033            state: std::sync::RwLock::new(state),
1034            history: std::sync::RwLock::new(history),
1035        }
1036    }
1037
1038    fn apply_event(&self, event: &Event) {
1039        if !event.actions.state_delta.is_empty()
1040            && let Ok(mut state) = self.state.write()
1041        {
1042            for (key, value) in &event.actions.state_delta {
1043                if adk_core::validate_state_key(key).is_ok() {
1044                    state.insert(key.clone(), value.clone());
1045                }
1046            }
1047        }
1048        if let Some(content) = &event.llm_response.content
1049            && let Ok(mut history) = self.history.write()
1050        {
1051            history.push(content.clone());
1052        }
1053    }
1054}
1055
1056impl Session for AgentToolSession {
1057    fn id(&self) -> &str {
1058        &self.id
1059    }
1060
1061    fn app_name(&self) -> &str {
1062        &self.app_name
1063    }
1064
1065    fn user_id(&self) -> &str {
1066        &self.user_id
1067    }
1068
1069    fn state(&self) -> &dyn State {
1070        self
1071    }
1072
1073    fn conversation_history(&self) -> Vec<Content> {
1074        self.history.read().map(|history| history.clone()).unwrap_or_default()
1075    }
1076}
1077
1078impl State for AgentToolSession {
1079    fn get(&self, key: &str) -> Option<Value> {
1080        self.state.read().ok()?.get(key).cloned()
1081    }
1082
1083    fn set(&mut self, key: String, value: Value) {
1084        if let Err(msg) = adk_core::validate_state_key(&key) {
1085            tracing::warn!(key = %key, "rejecting invalid state key: {msg}");
1086            return;
1087        }
1088        if let Ok(mut state) = self.state.write() {
1089            state.insert(key, value);
1090        }
1091    }
1092
1093    fn all(&self) -> HashMap<String, Value> {
1094        self.state.read().ok().map(|s| s.clone()).unwrap_or_default()
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100    use super::*;
1101    use adk_core::{EventActions, MemoryEntry, StreamingMode};
1102    use std::sync::Mutex;
1103
1104    struct MockAgent {
1105        name: String,
1106        description: String,
1107    }
1108
1109    #[async_trait]
1110    impl Agent for MockAgent {
1111        fn name(&self) -> &str {
1112            &self.name
1113        }
1114
1115        fn description(&self) -> &str {
1116            &self.description
1117        }
1118
1119        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1120            &[]
1121        }
1122
1123        async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1124            use async_stream::stream;
1125
1126            let name = self.name.clone();
1127            let s = stream! {
1128                let mut event = Event::new("mock-inv");
1129                event.author = name;
1130                event.llm_response.content = Some(Content::new("model").with_text("Mock response"));
1131                yield Ok(event);
1132            };
1133
1134            Ok(Box::pin(s))
1135        }
1136    }
1137
1138    #[test]
1139    fn test_agent_tool_creation() {
1140        let agent = Arc::new(MockAgent {
1141            name: "test_agent".to_string(),
1142            description: "A test agent".to_string(),
1143        });
1144
1145        let tool = AgentTool::new(agent);
1146        assert_eq!(tool.name(), "test_agent");
1147        assert_eq!(tool.description(), "A test agent");
1148    }
1149
1150    #[test]
1151    fn test_agent_tool_config() {
1152        let agent =
1153            Arc::new(MockAgent { name: "test".to_string(), description: "test".to_string() });
1154
1155        let tool = AgentTool::new(agent)
1156            .skip_summarization(true)
1157            .forward_artifacts(false)
1158            .timeout(Duration::from_secs(30));
1159
1160        assert!(tool.config.skip_summarization);
1161        assert!(!tool.config.forward_artifacts);
1162        assert_eq!(tool.config.timeout, Some(Duration::from_secs(30)));
1163    }
1164
1165    #[test]
1166    fn test_parameters_schema() {
1167        let agent = Arc::new(MockAgent {
1168            name: "calculator".to_string(),
1169            description: "Performs calculations".to_string(),
1170        });
1171
1172        let tool = AgentTool::new(agent);
1173        let schema = tool.parameters_schema().unwrap();
1174
1175        assert_eq!(schema["type"], "object");
1176        assert!(schema["properties"]["request"].is_object());
1177    }
1178
1179    #[test]
1180    fn test_extract_request() {
1181        let agent =
1182            Arc::new(MockAgent { name: "test".to_string(), description: "test".to_string() });
1183
1184        let tool = AgentTool::new(agent);
1185
1186        // Test with request field
1187        let args = json!({"request": "solve 2+2"});
1188        assert_eq!(tool.extract_request(&args), "solve 2+2");
1189
1190        // Test with string value
1191        let args = json!("direct request");
1192        assert_eq!(tool.extract_request(&args), "direct request");
1193    }
1194
1195    #[test]
1196    fn test_extract_response() {
1197        let mut event = Event::new("inv-123");
1198        event.llm_response.content = Some(Content {
1199            role: "model".to_string(),
1200            parts: vec![
1201                Part::Text { text: "The answer ".to_string() },
1202                Part::Text { text: "is 4".to_string() },
1203            ],
1204        });
1205
1206        let events = vec![event];
1207        let response = AgentTool::extract_response(&events);
1208
1209        assert_eq!(response["response"], "The answer is 4");
1210    }
1211
1212    #[test]
1213    fn parent_history_projection_keeps_only_complete_tool_exchanges() {
1214        let history = vec![
1215            Content::new("user").with_text("first request"),
1216            Content {
1217                role: "model".to_string(),
1218                parts: vec![Part::FunctionCall {
1219                    name: "completed_tool".to_string(),
1220                    args: json!({}),
1221                    id: Some("call-complete".to_string()),
1222                    thought_signature: None,
1223                }],
1224            },
1225            Content::new("model").with_text("forwarded child progress"),
1226            Content {
1227                role: "function".to_string(),
1228                parts: vec![Part::FunctionResponse {
1229                    function_response: adk_core::FunctionResponseData::new(
1230                        "completed_tool",
1231                        json!({"ok": true}),
1232                    ),
1233                    id: Some("call-complete".to_string()),
1234                    annotations: None,
1235                }],
1236            },
1237            Content::new("model").with_text("completed result"),
1238            Content {
1239                role: "model".to_string(),
1240                parts: vec![Part::FunctionCall {
1241                    name: "delegated_agent".to_string(),
1242                    args: json!({"request": "current request"}),
1243                    id: Some("call-open".to_string()),
1244                    thought_signature: None,
1245                }],
1246            },
1247        ];
1248
1249        let projected = AgentTool::project_parent_history(history.clone(), None);
1250        assert_eq!(projected.len(), 4);
1251        assert_eq!(projected.last().expect("projected history").role, "model");
1252        assert!(projected.iter().all(|content| {
1253            content.parts.iter().all(
1254                |part| !matches!(part, Part::Text { text } if text == "forwarded child progress"),
1255            )
1256        }));
1257
1258        let bounded = AgentTool::project_parent_history(history, Some(2));
1259        assert_eq!(bounded.len(), 1);
1260        assert_eq!(bounded[0].parts, vec![Part::Text { text: "completed result".to_string() }]);
1261    }
1262
1263    struct TestSession {
1264        state: std::sync::RwLock<HashMap<String, Value>>,
1265        history: Vec<Content>,
1266    }
1267
1268    impl State for TestSession {
1269        fn get(&self, key: &str) -> Option<Value> {
1270            self.state.read().ok()?.get(key).cloned()
1271        }
1272
1273        fn set(&mut self, key: String, value: Value) {
1274            self.state.get_mut().expect("state lock").insert(key, value);
1275        }
1276
1277        fn all(&self) -> HashMap<String, Value> {
1278            self.state.read().expect("state lock").clone()
1279        }
1280    }
1281
1282    impl Session for TestSession {
1283        fn id(&self) -> &str {
1284            "parent-session"
1285        }
1286
1287        fn app_name(&self) -> &str {
1288            "parent-app"
1289        }
1290
1291        fn user_id(&self) -> &str {
1292            "parent-user"
1293        }
1294
1295        fn state(&self) -> &dyn State {
1296            self
1297        }
1298
1299        fn conversation_history(&self) -> Vec<Content> {
1300            self.history.clone()
1301        }
1302    }
1303
1304    struct TestMemory;
1305
1306    #[async_trait]
1307    impl Memory for TestMemory {
1308        async fn search(&self, _query: &str) -> Result<Vec<MemoryEntry>> {
1309            Ok(Vec::new())
1310        }
1311    }
1312
1313    struct TestToolContext {
1314        actions: Mutex<EventActions>,
1315        session: TestSession,
1316        memory: Arc<dyn Memory>,
1317        run_config: RunConfig,
1318        cancelled: bool,
1319        shared_state: Arc<adk_core::SharedState>,
1320        emitted_events: Mutex<Vec<Event>>,
1321        delegation_depth: u32,
1322        max_delegation_depth: Option<u32>,
1323    }
1324
1325    impl TestToolContext {
1326        fn new() -> Self {
1327            Self {
1328                actions: Mutex::new(EventActions::default()),
1329                session: TestSession {
1330                    state: std::sync::RwLock::new(HashMap::from([(
1331                        "parent-key".to_string(),
1332                        json!("parent-value"),
1333                    )])),
1334                    history: vec![Content::new("user").with_text("parent history")],
1335                },
1336                memory: Arc::new(TestMemory),
1337                run_config: RunConfig::default(),
1338                cancelled: false,
1339                shared_state: Arc::new(adk_core::SharedState::new()),
1340                emitted_events: Mutex::new(Vec::new()),
1341                delegation_depth: 0,
1342                max_delegation_depth: None,
1343            }
1344        }
1345    }
1346
1347    #[async_trait]
1348    impl ReadonlyContext for TestToolContext {
1349        fn invocation_id(&self) -> &str {
1350            "parent-invocation"
1351        }
1352        fn agent_name(&self) -> &str {
1353            "parent-agent"
1354        }
1355        fn user_id(&self) -> &str {
1356            "parent-user"
1357        }
1358        fn app_name(&self) -> &str {
1359            "parent-app"
1360        }
1361        fn session_id(&self) -> &str {
1362            "parent-session"
1363        }
1364        fn branch(&self) -> &str {
1365            ""
1366        }
1367        fn user_content(&self) -> &Content {
1368            &self.session.history[0]
1369        }
1370    }
1371
1372    #[async_trait]
1373    impl CallbackContext for TestToolContext {
1374        fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
1375            None
1376        }
1377
1378        fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
1379            Some(self.shared_state.clone())
1380        }
1381    }
1382
1383    #[async_trait]
1384    impl ToolContext for TestToolContext {
1385        fn function_call_id(&self) -> &str {
1386            "call-1"
1387        }
1388        fn actions(&self) -> EventActions {
1389            self.actions.lock().expect("actions lock").clone()
1390        }
1391        fn set_actions(&self, actions: EventActions) {
1392            *self.actions.lock().expect("actions lock") = actions;
1393        }
1394        async fn search_memory(&self, query: &str) -> Result<Vec<MemoryEntry>> {
1395            self.memory.search(query).await
1396        }
1397        fn memory(&self) -> Option<Arc<dyn Memory>> {
1398            Some(self.memory.clone())
1399        }
1400        fn session(&self) -> Option<&dyn Session> {
1401            Some(&self.session)
1402        }
1403        fn run_config(&self) -> Option<&RunConfig> {
1404            Some(&self.run_config)
1405        }
1406        fn is_cancelled(&self) -> bool {
1407            self.cancelled
1408        }
1409        fn request_metadata(&self) -> HashMap<String, Value> {
1410            HashMap::from([("request-id".to_string(), json!("req-1"))])
1411        }
1412        fn delegation_depth(&self) -> u32 {
1413            self.delegation_depth
1414        }
1415        fn max_delegation_depth(&self) -> Option<u32> {
1416            self.max_delegation_depth
1417        }
1418        async fn emit_event(&self, event: Event) {
1419            self.emitted_events.lock().expect("event lock").push(event);
1420        }
1421    }
1422
1423    struct ContextProbeAgent;
1424
1425    #[async_trait]
1426    impl Agent for ContextProbeAgent {
1427        fn name(&self) -> &str {
1428            "probe"
1429        }
1430
1431        fn description(&self) -> &str {
1432            "records delegated context behavior"
1433        }
1434
1435        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1436            &[]
1437        }
1438
1439        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1440            use async_stream::stream;
1441            let stream = stream! {
1442                assert_eq!(ctx.session_id(), ctx.session().id());
1443                assert_eq!(ctx.app_name(), ctx.session().app_name());
1444                assert_eq!(ctx.user_id(), ctx.session().user_id());
1445                assert_eq!(ctx.session().state().get("parent-key"), Some(json!("parent-value")));
1446                assert_eq!(ctx.session().conversation_history().len(), 1);
1447                assert!(ctx.memory().is_some());
1448                assert!(ctx.shared_state().is_some());
1449                assert!(!ctx.is_cancelled());
1450                assert_eq!(ctx.request_metadata().get("request-id"), Some(&json!("req-1")));
1451                assert_eq!(ctx.run_config().streaming_mode, StreamingMode::None);
1452                assert_eq!(ctx.delegation_depth(), 3);
1453                assert_eq!(ctx.max_delegation_depth(), Some(4));
1454
1455                let mut first = Event::new(ctx.invocation_id());
1456                first.author = "probe".to_string();
1457                first.actions.state_delta.insert("child-key".to_string(), json!(42));
1458                first.actions.artifact_delta.insert("report.txt".to_string(), 2);
1459                first.llm_response.content = Some(Content::new("model").with_text("first"));
1460                yield Ok(first);
1461
1462                assert_eq!(ctx.session().state().get("child-key"), Some(json!(42)));
1463                assert_eq!(ctx.session().conversation_history().len(), 2);
1464                let mut final_event = Event::new(ctx.invocation_id());
1465                final_event.author = "probe".to_string();
1466                final_event.llm_response.content = Some(Content::new("model").with_text("done"));
1467                yield Ok(final_event);
1468            };
1469            Ok(Box::pin(stream))
1470        }
1471    }
1472
1473    #[tokio::test]
1474    async fn forwards_snapshot_runtime_context_and_applies_child_events_immediately() {
1475        let mut parent = TestToolContext::new();
1476        parent.delegation_depth = 2;
1477        parent.max_delegation_depth = Some(4);
1478        let parent = Arc::new(parent);
1479        let tool = AgentTool::new(Arc::new(ContextProbeAgent))
1480            .session_snapshot(AgentToolSessionSnapshot::Parent)
1481            .forward_memory(true)
1482            .forward_events(true)
1483            .skip_summarization(true);
1484
1485        let response = tool.execute(parent.clone(), json!({"request": "inspect"})).await.unwrap();
1486
1487        assert_eq!(response["response"], "done");
1488        let actions = parent.actions();
1489        assert_eq!(actions.state_delta.get("child-key"), Some(&json!(42)));
1490        assert_eq!(actions.artifact_delta.get("report.txt"), Some(&2));
1491        assert!(actions.skip_summarization);
1492        assert_eq!(parent.session.state().get("child-key"), None);
1493        assert_eq!(parent.emitted_events.lock().expect("event lock").len(), 2);
1494    }
1495
1496    struct WritePolicyAgent;
1497
1498    #[async_trait]
1499    impl Agent for WritePolicyAgent {
1500        fn name(&self) -> &str {
1501            "write_policy"
1502        }
1503
1504        fn description(&self) -> &str {
1505            "emits state and artifact writes"
1506        }
1507
1508        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1509            &[]
1510        }
1511
1512        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1513            let mut event = Event::new(ctx.invocation_id());
1514            event.actions.state_delta.insert("child-key".to_string(), json!(42));
1515            event.actions.artifact_delta.insert("report.txt".to_string(), 2);
1516            event.llm_response.content = Some(Content::new("model").with_text("done"));
1517            Ok(Box::pin(futures::stream::once(async { Ok(event) })))
1518        }
1519    }
1520
1521    #[tokio::test]
1522    async fn enforces_delegated_state_and_artifact_write_allowlists() {
1523        let state_tool = AgentTool::new(Arc::new(WritePolicyAgent))
1524            .session_snapshot(AgentToolSessionSnapshot::Parent)
1525            .output_state_keys(["allowed-key"])
1526            .propagate_failures(true);
1527        let error = state_tool
1528            .execute(Arc::new(TestToolContext::new()), json!({"request": "inspect"}))
1529            .await
1530            .unwrap_err();
1531        assert!(error.to_string().contains("unauthorized state write to 'child-key'"));
1532
1533        let artifact_tool = AgentTool::new(Arc::new(WritePolicyAgent))
1534            .session_snapshot(AgentToolSessionSnapshot::Parent)
1535            .output_state_keys(["child-key"])
1536            .artifact_prefixes(["team/"])
1537            .propagate_failures(true);
1538        let error = artifact_tool
1539            .execute(Arc::new(TestToolContext::new()), json!({"request": "inspect"}))
1540            .await
1541            .unwrap_err();
1542        assert!(error.to_string().contains("unauthorized artifact write to 'report.txt'"));
1543    }
1544
1545    struct ConflictingStateAgent {
1546        parent: Arc<TestToolContext>,
1547    }
1548
1549    #[async_trait]
1550    impl Agent for ConflictingStateAgent {
1551        fn name(&self) -> &str {
1552            "conflicting_state"
1553        }
1554
1555        fn description(&self) -> &str {
1556            "changes parent state while returning a child write"
1557        }
1558
1559        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1560            &[]
1561        }
1562
1563        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1564            self.parent
1565                .session
1566                .state
1567                .write()
1568                .expect("parent state lock")
1569                .insert("parent-key".to_string(), json!("concurrent-update"));
1570            let mut event = Event::new(ctx.invocation_id());
1571            event.actions.state_delta.insert("parent-key".to_string(), json!("child-update"));
1572            event.llm_response.content = Some(Content::new("model").with_text("done"));
1573            Ok(Box::pin(futures::stream::once(async { Ok(event) })))
1574        }
1575    }
1576
1577    #[tokio::test]
1578    async fn rejects_delegated_state_merge_conflicts_transactionally() {
1579        let parent = Arc::new(TestToolContext::new());
1580        let tool = AgentTool::new(Arc::new(ConflictingStateAgent { parent: parent.clone() }))
1581            .session_snapshot(AgentToolSessionSnapshot::Parent)
1582            .state_merge_policy(AgentToolStateMergePolicy::RejectConflicts)
1583            .propagate_failures(true);
1584        let error = tool.execute(parent.clone(), json!({"request": "write"})).await.unwrap_err();
1585        assert!(error.to_string().contains("conflicts with a concurrent parent update"));
1586        assert!(parent.actions().state_delta.is_empty());
1587    }
1588
1589    #[tokio::test]
1590    async fn permits_explicit_framework_state_merge_exemptions() {
1591        let parent = Arc::new(TestToolContext::new());
1592        let tool = AgentTool::new(Arc::new(ConflictingStateAgent { parent: parent.clone() }))
1593            .session_snapshot(AgentToolSessionSnapshot::Parent)
1594            .state_merge_policy(AgentToolStateMergePolicy::RejectConflicts)
1595            .state_merge_exempt_keys(["parent-key"])
1596            .propagate_failures(true);
1597
1598        tool.execute(parent.clone(), json!({"request": "write"})).await.unwrap();
1599        assert_eq!(parent.actions().state_delta.get("parent-key"), Some(&json!("child-update")));
1600    }
1601
1602    struct TestArtifacts;
1603
1604    #[async_trait]
1605    impl Artifacts for TestArtifacts {
1606        async fn save(&self, _name: &str, _data: &Part) -> Result<i64> {
1607            Ok(1)
1608        }
1609
1610        async fn load(&self, _name: &str) -> Result<Part> {
1611            Ok(Part::Text { text: "artifact".to_string() })
1612        }
1613
1614        async fn list(&self) -> Result<Vec<String>> {
1615            Ok(Vec::new())
1616        }
1617    }
1618
1619    #[tokio::test]
1620    async fn rejects_artifact_writes_at_the_storage_boundary() {
1621        let artifacts = AgentToolArtifacts {
1622            inner: Arc::new(TestArtifacts),
1623            allowed_write_prefixes: Arc::new(vec!["research/".to_string()]),
1624        };
1625        let data = Part::Text { text: "data".to_string() };
1626        let error = artifacts.save("private/report.txt", &data).await.unwrap_err();
1627        assert_eq!(error.code, "artifact.agent_tool.write_denied");
1628        assert_eq!(artifacts.save("research/report.txt", &data).await.unwrap(), 1);
1629    }
1630
1631    struct FailingAgent;
1632
1633    #[async_trait]
1634    impl Agent for FailingAgent {
1635        fn name(&self) -> &str {
1636            "failing"
1637        }
1638        fn description(&self) -> &str {
1639            "always fails"
1640        }
1641        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1642            &[]
1643        }
1644        async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1645            Err(adk_core::AdkError::agent("planned failure"))
1646        }
1647    }
1648
1649    #[tokio::test]
1650    async fn preserves_legacy_failure_object_and_supports_propagation() {
1651        let legacy = AgentTool::new(Arc::new(FailingAgent));
1652        let value = legacy
1653            .execute(Arc::new(TestToolContext::new()), json!({"request": "fail"}))
1654            .await
1655            .unwrap();
1656        assert!(value["error"].as_str().unwrap().contains("planned failure"));
1657
1658        let strict = AgentTool::new(Arc::new(FailingAgent)).propagate_failures(true);
1659        let error = strict
1660            .execute(Arc::new(TestToolContext::new()), json!({"request": "fail"}))
1661            .await
1662            .unwrap_err();
1663        assert!(error.to_string().contains("planned failure"));
1664    }
1665
1666    #[tokio::test]
1667    async fn enforces_inherited_and_tool_specific_delegation_depth() {
1668        let mut inherited_ctx = TestToolContext::new();
1669        inherited_ctx.delegation_depth = 2;
1670        inherited_ctx.max_delegation_depth = Some(2);
1671        let tool = AgentTool::new(Arc::new(MockAgent {
1672            name: "child".to_string(),
1673            description: "child".to_string(),
1674        }))
1675        .propagate_failures(true);
1676        let error = tool
1677            .execute(Arc::new(inherited_ctx), json!({"request": "too deep"}))
1678            .await
1679            .unwrap_err();
1680        assert!(error.to_string().contains("depth 3"));
1681
1682        let local_limit = AgentTool::new(Arc::new(MockAgent {
1683            name: "child".to_string(),
1684            description: "child".to_string(),
1685        }))
1686        .max_delegation_depth(0)
1687        .propagate_failures(true);
1688        assert!(
1689            local_limit
1690                .execute(Arc::new(TestToolContext::new()), json!({"request": "too deep"}))
1691                .await
1692                .is_err()
1693        );
1694    }
1695
1696    struct HandoffAgent;
1697
1698    #[async_trait]
1699    impl Agent for HandoffAgent {
1700        fn name(&self) -> &str {
1701            "handoff"
1702        }
1703        fn description(&self) -> &str {
1704            "requests a handoff"
1705        }
1706        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1707            &[]
1708        }
1709        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1710            let mut event = Event::new(ctx.invocation_id());
1711            event.actions.transfer_to_agent = Some("other".to_string());
1712            Ok(Box::pin(futures::stream::iter([Ok(event)])))
1713        }
1714    }
1715
1716    #[tokio::test]
1717    async fn strict_mode_rejects_unconsumed_child_handoff() {
1718        let tool = AgentTool::new(Arc::new(HandoffAgent))
1719            .reject_child_handoffs(true)
1720            .propagate_failures(true);
1721        let error = tool
1722            .execute(Arc::new(TestToolContext::new()), json!({"request": "handoff"}))
1723            .await
1724            .unwrap_err();
1725        assert!(error.to_string().contains("cannot execute child handoffs"));
1726    }
1727
1728    #[tokio::test]
1729    async fn executes_registered_child_handoff_and_returns_final_result() {
1730        let target = Arc::new(MockAgent {
1731            name: "other".to_string(),
1732            description: "handoff target".to_string(),
1733        }) as Arc<dyn Agent>;
1734        let tool = AgentTool::new(Arc::new(HandoffAgent))
1735            .execute_child_handoffs([target])
1736            .forward_events(true)
1737            .propagate_failures(true);
1738        let parent = Arc::new(TestToolContext::new());
1739        let response = tool.execute(parent.clone(), json!({"request": "handoff"})).await.unwrap();
1740        assert_eq!(response["response"], "Mock response");
1741        let forwarded = parent.emitted_events.lock().expect("event lock");
1742        assert_eq!(forwarded.len(), 2);
1743        assert!(forwarded[0].actions.transfer_to_agent.is_none());
1744        assert_eq!(forwarded[1].author, "other");
1745    }
1746
1747    #[tokio::test]
1748    async fn cancellation_is_observed_before_child_execution() {
1749        let mut ctx = TestToolContext::new();
1750        ctx.cancelled = true;
1751        let tool = AgentTool::new(Arc::new(MockAgent {
1752            name: "child".to_string(),
1753            description: "child".to_string(),
1754        }))
1755        .propagate_failures(true);
1756        let error = tool.execute(Arc::new(ctx), json!({"request": "go"})).await.unwrap_err();
1757        assert!(error.to_string().contains("cancelled"));
1758    }
1759
1760    struct IsolatedProbeAgent;
1761
1762    #[async_trait]
1763    impl Agent for IsolatedProbeAgent {
1764        fn name(&self) -> &str {
1765            "isolated"
1766        }
1767        fn description(&self) -> &str {
1768            "checks legacy isolation defaults"
1769        }
1770        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1771            &[]
1772        }
1773        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1774            assert_eq!(ctx.session_id(), ctx.session().id());
1775            assert_eq!(ctx.app_name(), ctx.session().app_name());
1776            assert_eq!(ctx.user_id(), ctx.session().user_id());
1777            assert!(ctx.session().state().all().is_empty());
1778            assert!(ctx.session().conversation_history().is_empty());
1779            assert!(ctx.memory().is_none());
1780            let mut event = Event::new(ctx.invocation_id());
1781            event.llm_response.content = Some(Content::new("model").with_text("isolated"));
1782            Ok(Box::pin(futures::stream::iter([Ok(event)])))
1783        }
1784    }
1785
1786    #[tokio::test]
1787    async fn default_keeps_each_child_session_isolated() {
1788        let tool = AgentTool::new(Arc::new(IsolatedProbeAgent));
1789        let parent = Arc::new(TestToolContext::new());
1790        let response = tool.execute(parent.clone(), json!({"request": "inspect"})).await.unwrap();
1791        assert_eq!(response["response"], "isolated");
1792        assert!(parent.emitted_events.lock().expect("event lock").is_empty());
1793    }
1794
1795    struct ProjectionProbeAgent;
1796
1797    #[async_trait]
1798    impl Agent for ProjectionProbeAgent {
1799        fn name(&self) -> &str {
1800            "projection"
1801        }
1802
1803        fn description(&self) -> &str {
1804            "checks explicit history and state projections"
1805        }
1806
1807        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1808            &[]
1809        }
1810
1811        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1812            assert!(ctx.session().conversation_history().is_empty());
1813            assert!(ctx.session().state().all().is_empty());
1814            let mut event = Event::new(ctx.invocation_id());
1815            event.llm_response.content = Some(Content::new("model").with_text("projected"));
1816            Ok(Box::pin(futures::stream::once(async { Ok(event) })))
1817        }
1818    }
1819
1820    #[tokio::test]
1821    async fn applies_exact_history_and_state_projection() {
1822        let tool = AgentTool::new(Arc::new(ProjectionProbeAgent))
1823            .session_snapshot(AgentToolSessionSnapshot::Parent)
1824            .history_max_events(0)
1825            .state_keys(["not-present"])
1826            .propagate_failures(true);
1827        let response = tool
1828            .execute(Arc::new(TestToolContext::new()), json!({"request": "inspect"}))
1829            .await
1830            .unwrap();
1831        assert_eq!(response["response"], "projected");
1832    }
1833
1834    struct StateWithoutHistoryProbe;
1835
1836    #[async_trait]
1837    impl Agent for StateWithoutHistoryProbe {
1838        fn name(&self) -> &str {
1839            "state_without_history"
1840        }
1841
1842        fn description(&self) -> &str {
1843            "checks independent state and history projections"
1844        }
1845
1846        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1847            &[]
1848        }
1849
1850        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1851            assert!(ctx.session().conversation_history().is_empty());
1852            assert_eq!(ctx.session().state().get("parent-key"), Some(json!("parent-value")));
1853            let mut event = Event::new(ctx.invocation_id());
1854            event.llm_response.content = Some(Content::new("model").with_text("independent"));
1855            Ok(Box::pin(futures::stream::once(async { Ok(event) })))
1856        }
1857    }
1858
1859    #[tokio::test]
1860    async fn projects_state_independently_from_history() {
1861        let tool = AgentTool::new(Arc::new(StateWithoutHistoryProbe))
1862            .session_snapshot(AgentToolSessionSnapshot::Parent)
1863            .history_max_events(0)
1864            .state_keys(["parent-key"])
1865            .propagate_failures(true);
1866        let response = tool
1867            .execute(Arc::new(TestToolContext::new()), json!({"request": "inspect"}))
1868            .await
1869            .unwrap();
1870        assert_eq!(response["response"], "independent");
1871    }
1872
1873    struct PendingAgent;
1874
1875    #[async_trait]
1876    impl Agent for PendingAgent {
1877        fn name(&self) -> &str {
1878            "pending"
1879        }
1880        fn description(&self) -> &str {
1881            "never completes"
1882        }
1883        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1884            &[]
1885        }
1886        async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1887            Ok(Box::pin(futures::stream::pending()))
1888        }
1889    }
1890
1891    #[tokio::test]
1892    async fn timeout_obeys_failure_mode() {
1893        let tool = AgentTool::new(Arc::new(PendingAgent))
1894            .timeout(Duration::from_millis(1))
1895            .propagate_failures(true);
1896        let error = tool
1897            .execute(Arc::new(TestToolContext::new()), json!({"request": "wait"}))
1898            .await
1899            .unwrap_err();
1900        assert!(error.to_string().contains("timed out"));
1901    }
1902}