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    #[adk_telemetry::instrument(
537        skip(self, ctx, args),
538        fields(
539            agent_tool.name = %self.agent.name(),
540            agent_tool.description = %self.agent.description(),
541            function_call.id = %ctx.function_call_id()
542        )
543    )]
544    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
545        adk_telemetry::debug!("Executing agent tool: {}", self.agent.name());
546
547        let parent_run_config = ctx.run_config().cloned().unwrap_or_default();
548        let child_depth = ctx.delegation_depth().saturating_add(1);
549        let max_depth = self.runtime.max_delegation_depth.or(ctx.max_delegation_depth());
550        if max_depth.is_some_and(|max| child_depth > max) {
551            return self.failure(format!(
552                "agent delegation depth {child_depth} exceeds the configured maximum of {}",
553                max_depth.unwrap_or_default()
554            ));
555        }
556        if ctx.is_cancelled() {
557            return self.failure("agent delegation was cancelled before execution".to_string());
558        }
559
560        // Extract the request from args
561        let request_text = self.extract_request(&args);
562        let parent_state_baseline =
563            ctx.session().map(|session| session.state().all()).unwrap_or_default();
564
565        // Create user content for the sub-agent
566        let user_content = Content::new("user").with_text(&request_text);
567
568        // Create an isolated context for the sub-agent
569        let sub_ctx = Arc::new(AgentToolInvocationContext::new(
570            ctx.clone(),
571            self.agent.clone(),
572            user_content.clone(),
573            AgentToolChildConfig {
574                forward_artifacts: self.config.forward_artifacts,
575                artifact_prefixes: self.runtime.artifact_prefixes.clone(),
576                forward_memory: self.runtime.forward_memory,
577                forward_shared_state: self.runtime.forward_shared_state,
578                session_snapshot: self.runtime.session_snapshot,
579                run_config: parent_run_config,
580                delegation_depth: child_depth,
581                max_delegation_depth: max_depth,
582                history_max_events: self.runtime.history_max_events,
583                state_keys: self.runtime.state_keys.clone(),
584                orchestration_root_invocation_id: ctx
585                    .orchestration_root_invocation_id()
586                    .to_string(),
587                orchestration_edge_id: ctx.orchestration_edge_id().map_or_else(
588                    || ctx.function_call_id().to_string(),
589                    |parent| format!("{parent}/{}", ctx.function_call_id()),
590                ),
591            },
592        ));
593
594        // Execute the sub-agent
595        let execution = async {
596            let mut active_agent = self.agent.clone();
597            let mut active_ctx = sub_ctx.clone();
598            let mut transfer_depth = 0_u32;
599            let mut events = Vec::new();
600            let mut state_delta = HashMap::new();
601            let mut artifact_delta = HashMap::new();
602
603            loop {
604                let mut event_stream = active_agent.run(active_ctx.clone()).await?;
605                let mut transfer = None;
606                while let Some(result) = event_stream.next().await {
607                    match result {
608                        Ok(event) => {
609                            if let Some(target) = &event.actions.transfer_to_agent {
610                                if self.runtime.reject_child_handoffs {
611                                    return Err(adk_core::AdkError::tool(format!(
612                                        "agent '{}' requested handoff to '{target}', but AgentTool cannot execute child handoffs in this configuration",
613                                        active_agent.name()
614                                    )));
615                                }
616                                if self.runtime.execute_child_handoffs {
617                                    transfer = Some(target.clone());
618                                }
619                            }
620                            state_delta.extend(event.actions.state_delta.clone());
621                            artifact_delta.extend(event.actions.artifact_delta.clone());
622                            sub_ctx.session.apply_event(&event);
623                            if self.runtime.forward_events {
624                                let mut forwarded = event.clone();
625                                if self.runtime.execute_child_handoffs && transfer.is_some() {
626                                    // The AgentTool consumes this control-flow edge internally.
627                                    // Do not let the parent Runner execute the same handoff again.
628                                    forwarded.actions.transfer_to_agent = None;
629                                }
630                                ctx.emit_event(forwarded).await;
631                            }
632                            events.push(event);
633                            if transfer.is_some() {
634                                break;
635                            }
636                        }
637                        Err(error) => {
638                            adk_telemetry::error!("Error in sub-agent execution: {error}");
639                            return Err(error);
640                        }
641                    }
642                }
643
644                let Some(target_name) = transfer else {
645                    break;
646                };
647                transfer_depth = transfer_depth.saturating_add(1);
648                let max_transfer_depth = active_ctx.run_config.max_transfer_depth.unwrap_or(10);
649                if transfer_depth > max_transfer_depth {
650                    return Err(adk_core::AdkError::tool(format!(
651                        "nested handoff depth {transfer_depth} exceeds the configured maximum of {max_transfer_depth}"
652                    )));
653                }
654                let target = self
655                    .runtime
656                    .handoff_agents
657                    .get(&target_name)
658                    .cloned()
659                    .ok_or_else(|| {
660                        adk_core::AdkError::tool(format!(
661                            "agent '{}' requested nested handoff to unregistered target '{target_name}'",
662                            active_agent.name()
663                        ))
664                    })?;
665                active_ctx = Arc::new(active_ctx.for_agent(target.clone()));
666                active_agent = target;
667            }
668
669            Ok((events, state_delta, artifact_delta))
670        };
671
672        // Apply timeout if configured
673        let result = if let Some(timeout_duration) = self.config.timeout {
674            match tokio::time::timeout(timeout_duration, execution).await {
675                Ok(r) => r,
676                Err(_) => {
677                    return self.failure(format!(
678                        "agent '{}' execution timed out after {timeout_duration:?}",
679                        self.agent.name()
680                    ));
681                }
682            }
683        } else {
684            execution.await
685        };
686
687        match result {
688            Ok((events, mut state_delta, artifact_delta)) => {
689                if let Some(allowed) = &self.runtime.output_state_keys {
690                    if let Some(key) = state_delta.keys().find(|key| !allowed.contains(*key)) {
691                        return self.state_policy_failure(format!(
692                            "agent '{}' attempted unauthorized state write to '{key}'",
693                            self.agent.name()
694                        ));
695                    }
696                    state_delta.retain(|key, _| allowed.contains(key));
697                }
698                if self.runtime.state_merge_policy == AgentToolStateMergePolicy::RejectConflicts
699                    && let Some(parent_session) = ctx.session()
700                    && let Some(key) = state_delta.keys().find(|key| {
701                        !self.runtime.state_merge_exempt_keys.contains(*key)
702                            && parent_session.state().get(key)
703                                != parent_state_baseline.get(*key).cloned()
704                    })
705                {
706                    return self.state_policy_failure(format!(
707                        "agent '{}' state write to '{key}' conflicts with a concurrent parent update",
708                        self.agent.name()
709                    ));
710                }
711                if let Some(prefixes) = &self.runtime.artifact_prefixes
712                    && let Some(name) = artifact_delta
713                        .keys()
714                        .find(|name| !prefixes.iter().any(|prefix| name.starts_with(prefix)))
715                {
716                    return self.state_policy_failure(format!(
717                        "agent '{}' attempted unauthorized artifact write to '{name}'",
718                        self.agent.name()
719                    ));
720                }
721                // Forward state_delta and artifact_delta to parent context
722                if !state_delta.is_empty()
723                    || !artifact_delta.is_empty()
724                    || self.config.skip_summarization
725                {
726                    let mut parent_actions = ctx.actions();
727                    parent_actions.state_delta.extend(state_delta);
728                    parent_actions.artifact_delta.extend(artifact_delta);
729                    parent_actions.skip_summarization |= self.config.skip_summarization;
730                    ctx.set_actions(parent_actions);
731                }
732
733                // Extract and return the response
734                let response = Self::extract_response(&events);
735
736                adk_telemetry::debug!(
737                    "Agent tool {} completed with {} events",
738                    self.agent.name(),
739                    events.len()
740                );
741
742                Ok(response)
743            }
744            Err(e) => self.failure(format!("agent execution failed: {e}")),
745        }
746    }
747}
748
749// Internal context for sub-agent execution
750struct AgentToolInvocationContext {
751    parent_ctx: Arc<dyn ToolContext>,
752    agent: Arc<dyn Agent>,
753    user_content: Content,
754    invocation_id: String,
755    ended: Arc<AtomicBool>,
756    forward_artifacts: bool,
757    artifact_prefixes: Option<Arc<Vec<String>>>,
758    forward_memory: bool,
759    forward_shared_state: bool,
760    session: Arc<AgentToolSession>,
761    run_config: RunConfig,
762    delegation_depth: u32,
763    max_delegation_depth: Option<u32>,
764    orchestration_root_invocation_id: String,
765    orchestration_edge_id: String,
766}
767
768impl AgentToolInvocationContext {
769    fn new(
770        parent_ctx: Arc<dyn ToolContext>,
771        agent: Arc<dyn Agent>,
772        user_content: Content,
773        child_config: AgentToolChildConfig,
774    ) -> Self {
775        let AgentToolChildConfig {
776            forward_artifacts,
777            artifact_prefixes,
778            forward_memory,
779            forward_shared_state,
780            session_snapshot,
781            mut run_config,
782            delegation_depth,
783            max_delegation_depth,
784            history_max_events,
785            state_keys,
786            orchestration_root_invocation_id,
787            orchestration_edge_id,
788        } = child_config;
789        let invocation_id = format!("agent-tool-{}", uuid::Uuid::new_v4());
790        let (mut state, mut history) = match (session_snapshot, parent_ctx.session()) {
791            (AgentToolSessionSnapshot::Parent, Some(session)) => {
792                (session.state().all(), session.conversation_history())
793            }
794            _ => (HashMap::new(), Vec::new()),
795        };
796        if let Some(keys) = state_keys {
797            state.retain(|key, _| keys.contains(key));
798        }
799        history = AgentTool::project_parent_history(history, history_max_events);
800        run_config.streaming_mode = adk_core::StreamingMode::None;
801        Self {
802            session: Arc::new(AgentToolSession::new(
803                invocation_id.clone(),
804                parent_ctx.app_name().to_string(),
805                parent_ctx.user_id().to_string(),
806                state,
807                history,
808            )),
809            parent_ctx,
810            agent,
811            user_content,
812            invocation_id,
813            ended: Arc::new(AtomicBool::new(false)),
814            forward_artifacts,
815            artifact_prefixes,
816            forward_memory,
817            forward_shared_state,
818            run_config,
819            delegation_depth,
820            max_delegation_depth,
821            orchestration_root_invocation_id,
822            orchestration_edge_id,
823        }
824    }
825
826    fn for_agent(&self, agent: Arc<dyn Agent>) -> Self {
827        let mut run_config = self.run_config.clone();
828        agent.configure_run(agent.name(), &mut run_config);
829        Self {
830            parent_ctx: self.parent_ctx.clone(),
831            agent,
832            user_content: self.user_content.clone(),
833            invocation_id: self.invocation_id.clone(),
834            ended: self.ended.clone(),
835            forward_artifacts: self.forward_artifacts,
836            artifact_prefixes: self.artifact_prefixes.clone(),
837            forward_memory: self.forward_memory,
838            forward_shared_state: self.forward_shared_state,
839            session: self.session.clone(),
840            run_config,
841            delegation_depth: self.delegation_depth,
842            max_delegation_depth: self.max_delegation_depth,
843            orchestration_root_invocation_id: self.orchestration_root_invocation_id.clone(),
844            orchestration_edge_id: self.orchestration_edge_id.clone(),
845        }
846    }
847}
848
849#[async_trait]
850impl ReadonlyContext for AgentToolInvocationContext {
851    fn invocation_id(&self) -> &str {
852        &self.invocation_id
853    }
854
855    fn agent_name(&self) -> &str {
856        self.agent.name()
857    }
858
859    fn user_id(&self) -> &str {
860        self.parent_ctx.user_id()
861    }
862
863    fn app_name(&self) -> &str {
864        self.parent_ctx.app_name()
865    }
866
867    fn session_id(&self) -> &str {
868        // Use a unique session ID for the sub-agent
869        &self.invocation_id
870    }
871
872    fn branch(&self) -> &str {
873        ""
874    }
875
876    fn user_content(&self) -> &Content {
877        &self.user_content
878    }
879}
880
881#[async_trait]
882impl CallbackContext for AgentToolInvocationContext {
883    fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
884        if !self.forward_artifacts {
885            return None;
886        }
887        let artifacts = self.parent_ctx.artifacts()?;
888        self.artifact_prefixes.as_ref().map_or(Some(artifacts.clone()), |prefixes| {
889            Some(Arc::new(AgentToolArtifacts {
890                inner: artifacts,
891                allowed_write_prefixes: prefixes.clone(),
892            }) as Arc<dyn Artifacts>)
893        })
894    }
895
896    fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
897        self.forward_shared_state.then(|| self.parent_ctx.shared_state()).flatten()
898    }
899}
900
901struct AgentToolArtifacts {
902    inner: Arc<dyn Artifacts>,
903    allowed_write_prefixes: Arc<Vec<String>>,
904}
905
906#[async_trait]
907impl Artifacts for AgentToolArtifacts {
908    async fn save(&self, name: &str, data: &Part) -> Result<i64> {
909        if !self.allowed_write_prefixes.iter().any(|prefix| name.starts_with(prefix)) {
910            return Err(adk_core::AdkError::new(
911                adk_core::ErrorComponent::Artifact,
912                adk_core::ErrorCategory::Forbidden,
913                "artifact.agent_tool.write_denied",
914                format!("delegated artifact write to '{name}' is outside the allowed prefixes"),
915            ));
916        }
917        self.inner.save(name, data).await
918    }
919
920    async fn load(&self, name: &str) -> Result<Part> {
921        self.inner.load(name).await
922    }
923
924    async fn list(&self) -> Result<Vec<String>> {
925        self.inner.list().await
926    }
927}
928
929#[async_trait]
930impl InvocationContext for AgentToolInvocationContext {
931    fn agent(&self) -> Arc<dyn Agent> {
932        self.agent.clone()
933    }
934
935    fn memory(&self) -> Option<Arc<dyn Memory>> {
936        self.forward_memory.then(|| self.parent_ctx.memory()).flatten()
937    }
938
939    fn session(&self) -> &dyn Session {
940        self.session.as_ref()
941    }
942
943    fn run_config(&self) -> &RunConfig {
944        &self.run_config
945    }
946
947    fn end_invocation(&self) {
948        self.ended.store(true, std::sync::atomic::Ordering::SeqCst);
949    }
950
951    fn ended(&self) -> bool {
952        self.ended.load(std::sync::atomic::Ordering::SeqCst)
953    }
954
955    fn is_cancelled(&self) -> bool {
956        self.parent_ctx.is_cancelled()
957    }
958
959    /// Authenticated scopes are forwarded so a scope-guarded tool used by the
960    /// wrapped agent sees the caller's grants rather than an empty set.
961    fn user_scopes(&self) -> Vec<String> {
962        self.parent_ctx.user_scopes()
963    }
964
965    fn request_metadata(&self) -> HashMap<String, Value> {
966        self.parent_ctx.request_metadata()
967    }
968
969    fn delegation_depth(&self) -> u32 {
970        self.delegation_depth
971    }
972
973    fn max_delegation_depth(&self) -> Option<u32> {
974        self.max_delegation_depth
975    }
976
977    fn orchestration_root_invocation_id(&self) -> &str {
978        &self.orchestration_root_invocation_id
979    }
980
981    fn orchestration_edge_id(&self) -> Option<&str> {
982        Some(&self.orchestration_edge_id)
983    }
984
985    /// Secret access is forwarded so the wrapped agent's tools can resolve
986    /// secrets through the same provider as the calling agent.
987    async fn get_secret(&self, name: &str) -> adk_core::Result<Option<String>> {
988        self.parent_ctx.get_secret(name).await
989    }
990
991    async fn get_secret_for(
992        &self,
993        request: &adk_core::SecretRequest,
994    ) -> adk_core::Result<Option<String>> {
995        // The parent here is a `ToolContext`, which carries no identity of its own, so
996        // only the stated purpose survives the hop. An agent invoked as a tool
997        // therefore presents that agent's identity rather than the inner tool's.
998        match &request.purpose {
999            Some(purpose) => self.parent_ctx.get_secret_for_purpose(&request.name, purpose).await,
1000            None => self.parent_ctx.get_secret(&request.name).await,
1001        }
1002    }
1003
1004    // Cancellation and request metadata are forwarded above through the
1005    // backward-compatible ToolContext capability methods.
1006}
1007
1008// Minimal session for sub-agent execution
1009struct AgentToolSession {
1010    id: String,
1011    app_name: String,
1012    user_id: String,
1013    state: std::sync::RwLock<HashMap<String, Value>>,
1014    history: std::sync::RwLock<Vec<Content>>,
1015}
1016
1017impl AgentToolSession {
1018    fn new(
1019        id: String,
1020        app_name: String,
1021        user_id: String,
1022        state: HashMap<String, Value>,
1023        history: Vec<Content>,
1024    ) -> Self {
1025        Self {
1026            id,
1027            app_name,
1028            user_id,
1029            state: std::sync::RwLock::new(state),
1030            history: std::sync::RwLock::new(history),
1031        }
1032    }
1033
1034    fn apply_event(&self, event: &Event) {
1035        if !event.actions.state_delta.is_empty()
1036            && let Ok(mut state) = self.state.write()
1037        {
1038            for (key, value) in &event.actions.state_delta {
1039                if adk_core::validate_state_key(key).is_ok() {
1040                    state.insert(key.clone(), value.clone());
1041                }
1042            }
1043        }
1044        if let Some(content) = &event.llm_response.content
1045            && let Ok(mut history) = self.history.write()
1046        {
1047            history.push(content.clone());
1048        }
1049    }
1050}
1051
1052impl Session for AgentToolSession {
1053    fn id(&self) -> &str {
1054        &self.id
1055    }
1056
1057    fn app_name(&self) -> &str {
1058        &self.app_name
1059    }
1060
1061    fn user_id(&self) -> &str {
1062        &self.user_id
1063    }
1064
1065    fn state(&self) -> &dyn State {
1066        self
1067    }
1068
1069    fn conversation_history(&self) -> Vec<Content> {
1070        self.history.read().map(|history| history.clone()).unwrap_or_default()
1071    }
1072}
1073
1074impl State for AgentToolSession {
1075    fn get(&self, key: &str) -> Option<Value> {
1076        self.state.read().ok()?.get(key).cloned()
1077    }
1078
1079    fn set(&mut self, key: String, value: Value) {
1080        if let Err(msg) = adk_core::validate_state_key(&key) {
1081            tracing::warn!(key = %key, "rejecting invalid state key: {msg}");
1082            return;
1083        }
1084        if let Ok(mut state) = self.state.write() {
1085            state.insert(key, value);
1086        }
1087    }
1088
1089    fn all(&self) -> HashMap<String, Value> {
1090        self.state.read().ok().map(|s| s.clone()).unwrap_or_default()
1091    }
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096    use super::*;
1097    use adk_core::{EventActions, MemoryEntry, StreamingMode};
1098    use std::sync::Mutex;
1099
1100    struct MockAgent {
1101        name: String,
1102        description: String,
1103    }
1104
1105    #[async_trait]
1106    impl Agent for MockAgent {
1107        fn name(&self) -> &str {
1108            &self.name
1109        }
1110
1111        fn description(&self) -> &str {
1112            &self.description
1113        }
1114
1115        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1116            &[]
1117        }
1118
1119        async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1120            use async_stream::stream;
1121
1122            let name = self.name.clone();
1123            let s = stream! {
1124                let mut event = Event::new("mock-inv");
1125                event.author = name;
1126                event.llm_response.content = Some(Content::new("model").with_text("Mock response"));
1127                yield Ok(event);
1128            };
1129
1130            Ok(Box::pin(s))
1131        }
1132    }
1133
1134    #[test]
1135    fn test_agent_tool_creation() {
1136        let agent = Arc::new(MockAgent {
1137            name: "test_agent".to_string(),
1138            description: "A test agent".to_string(),
1139        });
1140
1141        let tool = AgentTool::new(agent);
1142        assert_eq!(tool.name(), "test_agent");
1143        assert_eq!(tool.description(), "A test agent");
1144    }
1145
1146    #[test]
1147    fn test_agent_tool_config() {
1148        let agent =
1149            Arc::new(MockAgent { name: "test".to_string(), description: "test".to_string() });
1150
1151        let tool = AgentTool::new(agent)
1152            .skip_summarization(true)
1153            .forward_artifacts(false)
1154            .timeout(Duration::from_secs(30));
1155
1156        assert!(tool.config.skip_summarization);
1157        assert!(!tool.config.forward_artifacts);
1158        assert_eq!(tool.config.timeout, Some(Duration::from_secs(30)));
1159    }
1160
1161    #[test]
1162    fn test_parameters_schema() {
1163        let agent = Arc::new(MockAgent {
1164            name: "calculator".to_string(),
1165            description: "Performs calculations".to_string(),
1166        });
1167
1168        let tool = AgentTool::new(agent);
1169        let schema = tool.parameters_schema().unwrap();
1170
1171        assert_eq!(schema["type"], "object");
1172        assert!(schema["properties"]["request"].is_object());
1173    }
1174
1175    #[test]
1176    fn test_extract_request() {
1177        let agent =
1178            Arc::new(MockAgent { name: "test".to_string(), description: "test".to_string() });
1179
1180        let tool = AgentTool::new(agent);
1181
1182        // Test with request field
1183        let args = json!({"request": "solve 2+2"});
1184        assert_eq!(tool.extract_request(&args), "solve 2+2");
1185
1186        // Test with string value
1187        let args = json!("direct request");
1188        assert_eq!(tool.extract_request(&args), "direct request");
1189    }
1190
1191    #[test]
1192    fn test_extract_response() {
1193        let mut event = Event::new("inv-123");
1194        event.llm_response.content = Some(Content {
1195            role: "model".to_string(),
1196            parts: vec![
1197                Part::Text { text: "The answer ".to_string() },
1198                Part::Text { text: "is 4".to_string() },
1199            ],
1200        });
1201
1202        let events = vec![event];
1203        let response = AgentTool::extract_response(&events);
1204
1205        assert_eq!(response["response"], "The answer is 4");
1206    }
1207
1208    #[test]
1209    fn parent_history_projection_keeps_only_complete_tool_exchanges() {
1210        let history = vec![
1211            Content::new("user").with_text("first request"),
1212            Content {
1213                role: "model".to_string(),
1214                parts: vec![Part::FunctionCall {
1215                    name: "completed_tool".to_string(),
1216                    args: json!({}),
1217                    id: Some("call-complete".to_string()),
1218                    thought_signature: None,
1219                }],
1220            },
1221            Content::new("model").with_text("forwarded child progress"),
1222            Content {
1223                role: "function".to_string(),
1224                parts: vec![Part::FunctionResponse {
1225                    function_response: adk_core::FunctionResponseData::new(
1226                        "completed_tool",
1227                        json!({"ok": true}),
1228                    ),
1229                    id: Some("call-complete".to_string()),
1230                    annotations: None,
1231                }],
1232            },
1233            Content::new("model").with_text("completed result"),
1234            Content {
1235                role: "model".to_string(),
1236                parts: vec![Part::FunctionCall {
1237                    name: "delegated_agent".to_string(),
1238                    args: json!({"request": "current request"}),
1239                    id: Some("call-open".to_string()),
1240                    thought_signature: None,
1241                }],
1242            },
1243        ];
1244
1245        let projected = AgentTool::project_parent_history(history.clone(), None);
1246        assert_eq!(projected.len(), 4);
1247        assert_eq!(projected.last().expect("projected history").role, "model");
1248        assert!(projected.iter().all(|content| {
1249            content.parts.iter().all(
1250                |part| !matches!(part, Part::Text { text } if text == "forwarded child progress"),
1251            )
1252        }));
1253
1254        let bounded = AgentTool::project_parent_history(history, Some(2));
1255        assert_eq!(bounded.len(), 1);
1256        assert_eq!(bounded[0].parts, vec![Part::Text { text: "completed result".to_string() }]);
1257    }
1258
1259    struct TestSession {
1260        state: std::sync::RwLock<HashMap<String, Value>>,
1261        history: Vec<Content>,
1262    }
1263
1264    impl State for TestSession {
1265        fn get(&self, key: &str) -> Option<Value> {
1266            self.state.read().ok()?.get(key).cloned()
1267        }
1268
1269        fn set(&mut self, key: String, value: Value) {
1270            self.state.get_mut().expect("state lock").insert(key, value);
1271        }
1272
1273        fn all(&self) -> HashMap<String, Value> {
1274            self.state.read().expect("state lock").clone()
1275        }
1276    }
1277
1278    impl Session for TestSession {
1279        fn id(&self) -> &str {
1280            "parent-session"
1281        }
1282
1283        fn app_name(&self) -> &str {
1284            "parent-app"
1285        }
1286
1287        fn user_id(&self) -> &str {
1288            "parent-user"
1289        }
1290
1291        fn state(&self) -> &dyn State {
1292            self
1293        }
1294
1295        fn conversation_history(&self) -> Vec<Content> {
1296            self.history.clone()
1297        }
1298    }
1299
1300    struct TestMemory;
1301
1302    #[async_trait]
1303    impl Memory for TestMemory {
1304        async fn search(&self, _query: &str) -> Result<Vec<MemoryEntry>> {
1305            Ok(Vec::new())
1306        }
1307    }
1308
1309    struct TestToolContext {
1310        actions: Mutex<EventActions>,
1311        session: TestSession,
1312        memory: Arc<dyn Memory>,
1313        run_config: RunConfig,
1314        cancelled: bool,
1315        shared_state: Arc<adk_core::SharedState>,
1316        emitted_events: Mutex<Vec<Event>>,
1317        delegation_depth: u32,
1318        max_delegation_depth: Option<u32>,
1319    }
1320
1321    impl TestToolContext {
1322        fn new() -> Self {
1323            Self {
1324                actions: Mutex::new(EventActions::default()),
1325                session: TestSession {
1326                    state: std::sync::RwLock::new(HashMap::from([(
1327                        "parent-key".to_string(),
1328                        json!("parent-value"),
1329                    )])),
1330                    history: vec![Content::new("user").with_text("parent history")],
1331                },
1332                memory: Arc::new(TestMemory),
1333                run_config: RunConfig::default(),
1334                cancelled: false,
1335                shared_state: Arc::new(adk_core::SharedState::new()),
1336                emitted_events: Mutex::new(Vec::new()),
1337                delegation_depth: 0,
1338                max_delegation_depth: None,
1339            }
1340        }
1341    }
1342
1343    #[async_trait]
1344    impl ReadonlyContext for TestToolContext {
1345        fn invocation_id(&self) -> &str {
1346            "parent-invocation"
1347        }
1348        fn agent_name(&self) -> &str {
1349            "parent-agent"
1350        }
1351        fn user_id(&self) -> &str {
1352            "parent-user"
1353        }
1354        fn app_name(&self) -> &str {
1355            "parent-app"
1356        }
1357        fn session_id(&self) -> &str {
1358            "parent-session"
1359        }
1360        fn branch(&self) -> &str {
1361            ""
1362        }
1363        fn user_content(&self) -> &Content {
1364            &self.session.history[0]
1365        }
1366    }
1367
1368    #[async_trait]
1369    impl CallbackContext for TestToolContext {
1370        fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
1371            None
1372        }
1373
1374        fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
1375            Some(self.shared_state.clone())
1376        }
1377    }
1378
1379    #[async_trait]
1380    impl ToolContext for TestToolContext {
1381        fn function_call_id(&self) -> &str {
1382            "call-1"
1383        }
1384        fn actions(&self) -> EventActions {
1385            self.actions.lock().expect("actions lock").clone()
1386        }
1387        fn set_actions(&self, actions: EventActions) {
1388            *self.actions.lock().expect("actions lock") = actions;
1389        }
1390        async fn search_memory(&self, query: &str) -> Result<Vec<MemoryEntry>> {
1391            self.memory.search(query).await
1392        }
1393        fn memory(&self) -> Option<Arc<dyn Memory>> {
1394            Some(self.memory.clone())
1395        }
1396        fn session(&self) -> Option<&dyn Session> {
1397            Some(&self.session)
1398        }
1399        fn run_config(&self) -> Option<&RunConfig> {
1400            Some(&self.run_config)
1401        }
1402        fn is_cancelled(&self) -> bool {
1403            self.cancelled
1404        }
1405        fn request_metadata(&self) -> HashMap<String, Value> {
1406            HashMap::from([("request-id".to_string(), json!("req-1"))])
1407        }
1408        fn delegation_depth(&self) -> u32 {
1409            self.delegation_depth
1410        }
1411        fn max_delegation_depth(&self) -> Option<u32> {
1412            self.max_delegation_depth
1413        }
1414        async fn emit_event(&self, event: Event) {
1415            self.emitted_events.lock().expect("event lock").push(event);
1416        }
1417    }
1418
1419    struct ContextProbeAgent;
1420
1421    #[async_trait]
1422    impl Agent for ContextProbeAgent {
1423        fn name(&self) -> &str {
1424            "probe"
1425        }
1426
1427        fn description(&self) -> &str {
1428            "records delegated context behavior"
1429        }
1430
1431        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1432            &[]
1433        }
1434
1435        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1436            use async_stream::stream;
1437            let stream = stream! {
1438                assert_eq!(ctx.session_id(), ctx.session().id());
1439                assert_eq!(ctx.app_name(), ctx.session().app_name());
1440                assert_eq!(ctx.user_id(), ctx.session().user_id());
1441                assert_eq!(ctx.session().state().get("parent-key"), Some(json!("parent-value")));
1442                assert_eq!(ctx.session().conversation_history().len(), 1);
1443                assert!(ctx.memory().is_some());
1444                assert!(ctx.shared_state().is_some());
1445                assert!(!ctx.is_cancelled());
1446                assert_eq!(ctx.request_metadata().get("request-id"), Some(&json!("req-1")));
1447                assert_eq!(ctx.run_config().streaming_mode, StreamingMode::None);
1448                assert_eq!(ctx.delegation_depth(), 3);
1449                assert_eq!(ctx.max_delegation_depth(), Some(4));
1450
1451                let mut first = Event::new(ctx.invocation_id());
1452                first.author = "probe".to_string();
1453                first.actions.state_delta.insert("child-key".to_string(), json!(42));
1454                first.actions.artifact_delta.insert("report.txt".to_string(), 2);
1455                first.llm_response.content = Some(Content::new("model").with_text("first"));
1456                yield Ok(first);
1457
1458                assert_eq!(ctx.session().state().get("child-key"), Some(json!(42)));
1459                assert_eq!(ctx.session().conversation_history().len(), 2);
1460                let mut final_event = Event::new(ctx.invocation_id());
1461                final_event.author = "probe".to_string();
1462                final_event.llm_response.content = Some(Content::new("model").with_text("done"));
1463                yield Ok(final_event);
1464            };
1465            Ok(Box::pin(stream))
1466        }
1467    }
1468
1469    #[tokio::test]
1470    async fn forwards_snapshot_runtime_context_and_applies_child_events_immediately() {
1471        let mut parent = TestToolContext::new();
1472        parent.delegation_depth = 2;
1473        parent.max_delegation_depth = Some(4);
1474        let parent = Arc::new(parent);
1475        let tool = AgentTool::new(Arc::new(ContextProbeAgent))
1476            .session_snapshot(AgentToolSessionSnapshot::Parent)
1477            .forward_memory(true)
1478            .forward_events(true)
1479            .skip_summarization(true);
1480
1481        let response = tool.execute(parent.clone(), json!({"request": "inspect"})).await.unwrap();
1482
1483        assert_eq!(response["response"], "done");
1484        let actions = parent.actions();
1485        assert_eq!(actions.state_delta.get("child-key"), Some(&json!(42)));
1486        assert_eq!(actions.artifact_delta.get("report.txt"), Some(&2));
1487        assert!(actions.skip_summarization);
1488        assert_eq!(parent.session.state().get("child-key"), None);
1489        assert_eq!(parent.emitted_events.lock().expect("event lock").len(), 2);
1490    }
1491
1492    struct WritePolicyAgent;
1493
1494    #[async_trait]
1495    impl Agent for WritePolicyAgent {
1496        fn name(&self) -> &str {
1497            "write_policy"
1498        }
1499
1500        fn description(&self) -> &str {
1501            "emits state and artifact writes"
1502        }
1503
1504        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1505            &[]
1506        }
1507
1508        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1509            let mut event = Event::new(ctx.invocation_id());
1510            event.actions.state_delta.insert("child-key".to_string(), json!(42));
1511            event.actions.artifact_delta.insert("report.txt".to_string(), 2);
1512            event.llm_response.content = Some(Content::new("model").with_text("done"));
1513            Ok(Box::pin(futures::stream::once(async { Ok(event) })))
1514        }
1515    }
1516
1517    #[tokio::test]
1518    async fn enforces_delegated_state_and_artifact_write_allowlists() {
1519        let state_tool = AgentTool::new(Arc::new(WritePolicyAgent))
1520            .session_snapshot(AgentToolSessionSnapshot::Parent)
1521            .output_state_keys(["allowed-key"])
1522            .propagate_failures(true);
1523        let error = state_tool
1524            .execute(Arc::new(TestToolContext::new()), json!({"request": "inspect"}))
1525            .await
1526            .unwrap_err();
1527        assert!(error.to_string().contains("unauthorized state write to 'child-key'"));
1528
1529        let artifact_tool = AgentTool::new(Arc::new(WritePolicyAgent))
1530            .session_snapshot(AgentToolSessionSnapshot::Parent)
1531            .output_state_keys(["child-key"])
1532            .artifact_prefixes(["team/"])
1533            .propagate_failures(true);
1534        let error = artifact_tool
1535            .execute(Arc::new(TestToolContext::new()), json!({"request": "inspect"}))
1536            .await
1537            .unwrap_err();
1538        assert!(error.to_string().contains("unauthorized artifact write to 'report.txt'"));
1539    }
1540
1541    struct ConflictingStateAgent {
1542        parent: Arc<TestToolContext>,
1543    }
1544
1545    #[async_trait]
1546    impl Agent for ConflictingStateAgent {
1547        fn name(&self) -> &str {
1548            "conflicting_state"
1549        }
1550
1551        fn description(&self) -> &str {
1552            "changes parent state while returning a child write"
1553        }
1554
1555        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1556            &[]
1557        }
1558
1559        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1560            self.parent
1561                .session
1562                .state
1563                .write()
1564                .expect("parent state lock")
1565                .insert("parent-key".to_string(), json!("concurrent-update"));
1566            let mut event = Event::new(ctx.invocation_id());
1567            event.actions.state_delta.insert("parent-key".to_string(), json!("child-update"));
1568            event.llm_response.content = Some(Content::new("model").with_text("done"));
1569            Ok(Box::pin(futures::stream::once(async { Ok(event) })))
1570        }
1571    }
1572
1573    #[tokio::test]
1574    async fn rejects_delegated_state_merge_conflicts_transactionally() {
1575        let parent = Arc::new(TestToolContext::new());
1576        let tool = AgentTool::new(Arc::new(ConflictingStateAgent { parent: parent.clone() }))
1577            .session_snapshot(AgentToolSessionSnapshot::Parent)
1578            .state_merge_policy(AgentToolStateMergePolicy::RejectConflicts)
1579            .propagate_failures(true);
1580        let error = tool.execute(parent.clone(), json!({"request": "write"})).await.unwrap_err();
1581        assert!(error.to_string().contains("conflicts with a concurrent parent update"));
1582        assert!(parent.actions().state_delta.is_empty());
1583    }
1584
1585    #[tokio::test]
1586    async fn permits_explicit_framework_state_merge_exemptions() {
1587        let parent = Arc::new(TestToolContext::new());
1588        let tool = AgentTool::new(Arc::new(ConflictingStateAgent { parent: parent.clone() }))
1589            .session_snapshot(AgentToolSessionSnapshot::Parent)
1590            .state_merge_policy(AgentToolStateMergePolicy::RejectConflicts)
1591            .state_merge_exempt_keys(["parent-key"])
1592            .propagate_failures(true);
1593
1594        tool.execute(parent.clone(), json!({"request": "write"})).await.unwrap();
1595        assert_eq!(parent.actions().state_delta.get("parent-key"), Some(&json!("child-update")));
1596    }
1597
1598    struct TestArtifacts;
1599
1600    #[async_trait]
1601    impl Artifacts for TestArtifacts {
1602        async fn save(&self, _name: &str, _data: &Part) -> Result<i64> {
1603            Ok(1)
1604        }
1605
1606        async fn load(&self, _name: &str) -> Result<Part> {
1607            Ok(Part::Text { text: "artifact".to_string() })
1608        }
1609
1610        async fn list(&self) -> Result<Vec<String>> {
1611            Ok(Vec::new())
1612        }
1613    }
1614
1615    #[tokio::test]
1616    async fn rejects_artifact_writes_at_the_storage_boundary() {
1617        let artifacts = AgentToolArtifacts {
1618            inner: Arc::new(TestArtifacts),
1619            allowed_write_prefixes: Arc::new(vec!["research/".to_string()]),
1620        };
1621        let data = Part::Text { text: "data".to_string() };
1622        let error = artifacts.save("private/report.txt", &data).await.unwrap_err();
1623        assert_eq!(error.code, "artifact.agent_tool.write_denied");
1624        assert_eq!(artifacts.save("research/report.txt", &data).await.unwrap(), 1);
1625    }
1626
1627    struct FailingAgent;
1628
1629    #[async_trait]
1630    impl Agent for FailingAgent {
1631        fn name(&self) -> &str {
1632            "failing"
1633        }
1634        fn description(&self) -> &str {
1635            "always fails"
1636        }
1637        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1638            &[]
1639        }
1640        async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1641            Err(adk_core::AdkError::agent("planned failure"))
1642        }
1643    }
1644
1645    #[tokio::test]
1646    async fn preserves_legacy_failure_object_and_supports_propagation() {
1647        let legacy = AgentTool::new(Arc::new(FailingAgent));
1648        let value = legacy
1649            .execute(Arc::new(TestToolContext::new()), json!({"request": "fail"}))
1650            .await
1651            .unwrap();
1652        assert!(value["error"].as_str().unwrap().contains("planned failure"));
1653
1654        let strict = AgentTool::new(Arc::new(FailingAgent)).propagate_failures(true);
1655        let error = strict
1656            .execute(Arc::new(TestToolContext::new()), json!({"request": "fail"}))
1657            .await
1658            .unwrap_err();
1659        assert!(error.to_string().contains("planned failure"));
1660    }
1661
1662    #[tokio::test]
1663    async fn enforces_inherited_and_tool_specific_delegation_depth() {
1664        let mut inherited_ctx = TestToolContext::new();
1665        inherited_ctx.delegation_depth = 2;
1666        inherited_ctx.max_delegation_depth = Some(2);
1667        let tool = AgentTool::new(Arc::new(MockAgent {
1668            name: "child".to_string(),
1669            description: "child".to_string(),
1670        }))
1671        .propagate_failures(true);
1672        let error = tool
1673            .execute(Arc::new(inherited_ctx), json!({"request": "too deep"}))
1674            .await
1675            .unwrap_err();
1676        assert!(error.to_string().contains("depth 3"));
1677
1678        let local_limit = AgentTool::new(Arc::new(MockAgent {
1679            name: "child".to_string(),
1680            description: "child".to_string(),
1681        }))
1682        .max_delegation_depth(0)
1683        .propagate_failures(true);
1684        assert!(
1685            local_limit
1686                .execute(Arc::new(TestToolContext::new()), json!({"request": "too deep"}))
1687                .await
1688                .is_err()
1689        );
1690    }
1691
1692    struct HandoffAgent;
1693
1694    #[async_trait]
1695    impl Agent for HandoffAgent {
1696        fn name(&self) -> &str {
1697            "handoff"
1698        }
1699        fn description(&self) -> &str {
1700            "requests a handoff"
1701        }
1702        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1703            &[]
1704        }
1705        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1706            let mut event = Event::new(ctx.invocation_id());
1707            event.actions.transfer_to_agent = Some("other".to_string());
1708            Ok(Box::pin(futures::stream::iter([Ok(event)])))
1709        }
1710    }
1711
1712    #[tokio::test]
1713    async fn strict_mode_rejects_unconsumed_child_handoff() {
1714        let tool = AgentTool::new(Arc::new(HandoffAgent))
1715            .reject_child_handoffs(true)
1716            .propagate_failures(true);
1717        let error = tool
1718            .execute(Arc::new(TestToolContext::new()), json!({"request": "handoff"}))
1719            .await
1720            .unwrap_err();
1721        assert!(error.to_string().contains("cannot execute child handoffs"));
1722    }
1723
1724    #[tokio::test]
1725    async fn executes_registered_child_handoff_and_returns_final_result() {
1726        let target = Arc::new(MockAgent {
1727            name: "other".to_string(),
1728            description: "handoff target".to_string(),
1729        }) as Arc<dyn Agent>;
1730        let tool = AgentTool::new(Arc::new(HandoffAgent))
1731            .execute_child_handoffs([target])
1732            .forward_events(true)
1733            .propagate_failures(true);
1734        let parent = Arc::new(TestToolContext::new());
1735        let response = tool.execute(parent.clone(), json!({"request": "handoff"})).await.unwrap();
1736        assert_eq!(response["response"], "Mock response");
1737        let forwarded = parent.emitted_events.lock().expect("event lock");
1738        assert_eq!(forwarded.len(), 2);
1739        assert!(forwarded[0].actions.transfer_to_agent.is_none());
1740        assert_eq!(forwarded[1].author, "other");
1741    }
1742
1743    #[tokio::test]
1744    async fn cancellation_is_observed_before_child_execution() {
1745        let mut ctx = TestToolContext::new();
1746        ctx.cancelled = true;
1747        let tool = AgentTool::new(Arc::new(MockAgent {
1748            name: "child".to_string(),
1749            description: "child".to_string(),
1750        }))
1751        .propagate_failures(true);
1752        let error = tool.execute(Arc::new(ctx), json!({"request": "go"})).await.unwrap_err();
1753        assert!(error.to_string().contains("cancelled"));
1754    }
1755
1756    struct IsolatedProbeAgent;
1757
1758    #[async_trait]
1759    impl Agent for IsolatedProbeAgent {
1760        fn name(&self) -> &str {
1761            "isolated"
1762        }
1763        fn description(&self) -> &str {
1764            "checks legacy isolation defaults"
1765        }
1766        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1767            &[]
1768        }
1769        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1770            assert_eq!(ctx.session_id(), ctx.session().id());
1771            assert_eq!(ctx.app_name(), ctx.session().app_name());
1772            assert_eq!(ctx.user_id(), ctx.session().user_id());
1773            assert!(ctx.session().state().all().is_empty());
1774            assert!(ctx.session().conversation_history().is_empty());
1775            assert!(ctx.memory().is_none());
1776            let mut event = Event::new(ctx.invocation_id());
1777            event.llm_response.content = Some(Content::new("model").with_text("isolated"));
1778            Ok(Box::pin(futures::stream::iter([Ok(event)])))
1779        }
1780    }
1781
1782    #[tokio::test]
1783    async fn default_keeps_each_child_session_isolated() {
1784        let tool = AgentTool::new(Arc::new(IsolatedProbeAgent));
1785        let parent = Arc::new(TestToolContext::new());
1786        let response = tool.execute(parent.clone(), json!({"request": "inspect"})).await.unwrap();
1787        assert_eq!(response["response"], "isolated");
1788        assert!(parent.emitted_events.lock().expect("event lock").is_empty());
1789    }
1790
1791    struct ProjectionProbeAgent;
1792
1793    #[async_trait]
1794    impl Agent for ProjectionProbeAgent {
1795        fn name(&self) -> &str {
1796            "projection"
1797        }
1798
1799        fn description(&self) -> &str {
1800            "checks explicit history and state projections"
1801        }
1802
1803        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1804            &[]
1805        }
1806
1807        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1808            assert!(ctx.session().conversation_history().is_empty());
1809            assert!(ctx.session().state().all().is_empty());
1810            let mut event = Event::new(ctx.invocation_id());
1811            event.llm_response.content = Some(Content::new("model").with_text("projected"));
1812            Ok(Box::pin(futures::stream::once(async { Ok(event) })))
1813        }
1814    }
1815
1816    #[tokio::test]
1817    async fn applies_exact_history_and_state_projection() {
1818        let tool = AgentTool::new(Arc::new(ProjectionProbeAgent))
1819            .session_snapshot(AgentToolSessionSnapshot::Parent)
1820            .history_max_events(0)
1821            .state_keys(["not-present"])
1822            .propagate_failures(true);
1823        let response = tool
1824            .execute(Arc::new(TestToolContext::new()), json!({"request": "inspect"}))
1825            .await
1826            .unwrap();
1827        assert_eq!(response["response"], "projected");
1828    }
1829
1830    struct StateWithoutHistoryProbe;
1831
1832    #[async_trait]
1833    impl Agent for StateWithoutHistoryProbe {
1834        fn name(&self) -> &str {
1835            "state_without_history"
1836        }
1837
1838        fn description(&self) -> &str {
1839            "checks independent state and history projections"
1840        }
1841
1842        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1843            &[]
1844        }
1845
1846        async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1847            assert!(ctx.session().conversation_history().is_empty());
1848            assert_eq!(ctx.session().state().get("parent-key"), Some(json!("parent-value")));
1849            let mut event = Event::new(ctx.invocation_id());
1850            event.llm_response.content = Some(Content::new("model").with_text("independent"));
1851            Ok(Box::pin(futures::stream::once(async { Ok(event) })))
1852        }
1853    }
1854
1855    #[tokio::test]
1856    async fn projects_state_independently_from_history() {
1857        let tool = AgentTool::new(Arc::new(StateWithoutHistoryProbe))
1858            .session_snapshot(AgentToolSessionSnapshot::Parent)
1859            .history_max_events(0)
1860            .state_keys(["parent-key"])
1861            .propagate_failures(true);
1862        let response = tool
1863            .execute(Arc::new(TestToolContext::new()), json!({"request": "inspect"}))
1864            .await
1865            .unwrap();
1866        assert_eq!(response["response"], "independent");
1867    }
1868
1869    struct PendingAgent;
1870
1871    #[async_trait]
1872    impl Agent for PendingAgent {
1873        fn name(&self) -> &str {
1874            "pending"
1875        }
1876        fn description(&self) -> &str {
1877            "never completes"
1878        }
1879        fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1880            &[]
1881        }
1882        async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
1883            Ok(Box::pin(futures::stream::pending()))
1884        }
1885    }
1886
1887    #[tokio::test]
1888    async fn timeout_obeys_failure_mode() {
1889        let tool = AgentTool::new(Arc::new(PendingAgent))
1890            .timeout(Duration::from_millis(1))
1891            .propagate_failures(true);
1892        let error = tool
1893            .execute(Arc::new(TestToolContext::new()), json!({"request": "wait"}))
1894            .await
1895            .unwrap_err();
1896        assert!(error.to_string().contains("timed out"));
1897    }
1898}