1use 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub enum AgentToolSessionSnapshot {
44 #[default]
46 Isolated,
47 Parent,
49}
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53pub enum AgentToolFailureMode {
54 #[default]
56 ReturnErrorObject,
57 Propagate,
59}
60
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63pub enum AgentToolStateMergePolicy {
64 #[default]
66 Overwrite,
67 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#[derive(Debug, Clone)]
129pub struct AgentToolConfig {
130 pub skip_summarization: bool,
133
134 pub forward_artifacts: bool,
137
138 pub timeout: Option<Duration>,
140
141 pub input_schema: Option<Value>,
144
145 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
161pub struct AgentTool {
167 agent: Arc<dyn Agent>,
168 config: AgentToolConfig,
169 runtime: AgentToolRuntimeConfig,
170}
171
172impl AgentTool {
173 pub fn new(agent: Arc<dyn Agent>) -> Self {
175 Self {
176 agent,
177 config: AgentToolConfig::default(),
178 runtime: AgentToolRuntimeConfig::default(),
179 }
180 }
181
182 pub fn with_config(agent: Arc<dyn Agent>, config: AgentToolConfig) -> Self {
184 Self { agent, config, runtime: AgentToolRuntimeConfig::default() }
185 }
186
187 pub fn skip_summarization(mut self, skip: bool) -> Self {
189 self.config.skip_summarization = skip;
190 self
191 }
192
193 pub fn forward_artifacts(mut self, forward: bool) -> Self {
195 self.config.forward_artifacts = forward;
196 self
197 }
198
199 pub fn timeout(mut self, timeout: Duration) -> Self {
201 self.config.timeout = Some(timeout);
202 self
203 }
204
205 pub fn input_schema(mut self, schema: Value) -> Self {
207 self.config.input_schema = Some(schema);
208 self
209 }
210
211 pub fn output_schema(mut self, schema: Value) -> Self {
213 self.config.output_schema = Some(schema);
214 self
215 }
216
217 pub fn session_snapshot(mut self, snapshot: AgentToolSessionSnapshot) -> Self {
219 self.runtime.session_snapshot = snapshot;
220 self
221 }
222
223 pub fn forward_memory(mut self, forward: bool) -> Self {
225 self.runtime.forward_memory = forward;
226 self
227 }
228
229 pub fn forward_shared_state(mut self, forward: bool) -> Self {
233 self.runtime.forward_shared_state = forward;
234 self
235 }
236
237 pub fn forward_events(mut self, forward: bool) -> Self {
242 self.runtime.forward_events = forward;
243 self
244 }
245
246 pub fn failure_mode(mut self, mode: AgentToolFailureMode) -> Self {
248 self.runtime.failure_mode = mode;
249 self
250 }
251
252 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 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 pub fn reject_child_handoffs(mut self, reject: bool) -> Self {
269 self.runtime.reject_child_handoffs = reject;
270 self
271 }
272
273 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 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 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 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 pub fn state_merge_policy(mut self, policy: AgentToolStateMergePolicy) -> Self {
312 self.runtime.state_merge_policy = policy;
313 self
314 }
315
316 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 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 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 fn extract_request(&self, args: &Value) -> String {
380 if let Some(request) = args.get("request").and_then(|v| v.as_str()) {
382 return request.to_string();
383 }
384
385 if self.config.input_schema.is_some() {
387 return serde_json::to_string(args).unwrap_or_default();
388 }
389
390 match args {
392 Value::String(s) => s.clone(),
393 Value::Object(map) => {
394 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 fn extract_response(events: &[Event]) -> Value {
408 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; }
422 }
423
424 if responses.is_empty() {
425 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 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 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 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 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 let user_content = Content::new("user").with_text(&request_text);
567
568 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 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 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 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 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 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
749struct 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 &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 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 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 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 }
1007
1008struct 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 let args = json!({"request": "solve 2+2"});
1184 assert_eq!(tool.extract_request(&args), "solve 2+2");
1185
1186 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}