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