1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
9use std::sync::Arc;
10
11#[cfg(feature = "team-tools")]
12use std::sync::{Mutex, OnceLock};
13
14use adk_core::{Agent, EventStream, InvocationContext, Result, RunConfig};
15use async_trait::async_trait;
16use futures::StreamExt;
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20use tracing::Instrument;
21
22mod blackboard;
23mod discovery;
24mod evaluation;
25mod lifecycle;
26mod runtime;
27mod templates;
28pub use blackboard::{
29 BlackboardHistoryPolicy, BlackboardPolicy, BlackboardSchedule, BlackboardSpec,
30 BlackboardTransition, CompiledBlackboardTeam,
31};
32pub use discovery::{
33 StaticTeamAgentRegistry, TeamAgentDescriptor, TeamAgentHealth, TeamAgentRegistry,
34};
35pub use evaluation::{
36 TeamExecutionAnalysis, TeamReplayError, analyze_team_execution, validate_team_replay,
37};
38pub use lifecycle::{
39 TeamLifecycleContext, TeamLifecycleDecision, TeamLifecycleHook, TeamLifecycleOutcome,
40 TeamLifecyclePhase,
41};
42use runtime::{EventDisposition, TeamEdgeStart, TeamRuntimeRegistry};
43pub use runtime::{
44 ResolvedTeamMember, TEAM_EDGE_ID_KEY, TEAM_EXECUTION_STATE_KEY, TEAM_ROOT_INVOCATION_KEY,
45 TeamEdgeExecution, TeamExecutionSnapshot, TeamExecutionStatus, TeamExecutionUsage,
46};
47pub use templates::{TeamArchitectureTemplate, TeamManagerBranch, WorkflowArchitectureTemplate};
48
49#[cfg(feature = "team-tools")]
50use adk_core::{RuntimeToolset, Tool, Toolset};
51#[cfg(feature = "team-tools")]
52use adk_tool::{
53 AgentTool, AgentToolFailureMode, AgentToolSessionSnapshot, AgentToolStateMergePolicy,
54};
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
58#[serde(rename_all = "camelCase", deny_unknown_fields)]
59pub struct TeamSpec {
60 pub name: String,
62 #[serde(default, skip_serializing_if = "String::is_empty")]
64 pub description: String,
65 pub coordinator: String,
67 pub members: Vec<TeamMemberSpec>,
69 #[serde(default, skip_serializing_if = "Vec::is_empty")]
71 pub relationships: Vec<TeamRelationship>,
72 #[serde(default)]
74 pub policy: TeamPolicy,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
79#[serde(rename_all = "camelCase", deny_unknown_fields)]
80pub struct TeamMemberSpec {
81 pub name: String,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub description: Option<String>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub required_capabilities: Vec<String>,
89 #[serde(default)]
91 pub registry: TeamRegistryRequirement,
92}
93
94impl TeamMemberSpec {
95 pub fn new(name: impl Into<String>) -> Self {
97 Self {
98 name: name.into(),
99 description: None,
100 required_capabilities: Vec::new(),
101 registry: TeamRegistryRequirement::default(),
102 }
103 }
104
105 pub fn with_description(mut self, description: impl Into<String>) -> Self {
107 self.description = Some(description.into());
108 self
109 }
110
111 pub fn with_capabilities(
113 mut self,
114 capabilities: impl IntoIterator<Item = impl Into<String>>,
115 ) -> Self {
116 self.required_capabilities = capabilities.into_iter().map(Into::into).collect();
117 self
118 }
119
120 pub fn with_registry_requirement(mut self, requirement: TeamRegistryRequirement) -> Self {
122 self.registry = requirement;
123 self
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
129#[serde(rename_all = "camelCase", deny_unknown_fields)]
130pub struct TeamRegistryRequirement {
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub version: Option<String>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub digest: Option<String>,
137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
139 pub trust_labels: Vec<String>,
140 #[serde(default)]
142 pub require_healthy: bool,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148pub struct TeamRelationship {
149 pub from: String,
151 pub to: String,
153 pub kind: RelationshipKind,
155 #[serde(default)]
157 pub policy: RelationshipPolicy,
158}
159
160impl TeamRelationship {
161 pub fn new(from: impl Into<String>, to: impl Into<String>, kind: RelationshipKind) -> Self {
163 Self { from: from.into(), to: to.into(), kind, policy: RelationshipPolicy::default() }
164 }
165
166 pub fn with_policy(mut self, policy: RelationshipPolicy) -> Self {
168 self.policy = policy;
169 self
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
175#[serde(rename_all = "camelCase", deny_unknown_fields)]
176pub struct RelationshipPolicy {
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub input_schema: Option<serde_json::Value>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub output_schema: Option<serde_json::Value>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub context: Option<TeamContextPolicy>,
186 #[serde(default)]
188 pub history: TeamHistoryPolicy,
189 #[serde(default, skip_serializing_if = "Vec::is_empty")]
192 pub state_keys: Vec<String>,
193 #[serde(default, skip_serializing_if = "Vec::is_empty")]
195 pub state_write_keys: Vec<String>,
196 #[serde(default)]
198 pub state_merge: TeamStateMergePolicy,
199 #[serde(default, skip_serializing_if = "Vec::is_empty")]
201 pub artifact_prefixes: Vec<String>,
202 #[serde(default)]
204 pub resume: TeamResumePolicy,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub timeout_ms: Option<u64>,
208 #[serde(default)]
210 pub approval: RelationshipApprovalPolicy,
211 #[serde(default)]
213 pub failure: RelationshipFailureStrategy,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub circuit_breaker: Option<CircuitBreakerPolicy>,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
221#[serde(rename_all = "camelCase", tag = "mode")]
222pub enum TeamHistoryPolicy {
223 #[default]
225 Inherit,
226 None,
228 Full,
230 Last {
232 max_events: usize,
234 },
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
239#[serde(rename_all = "camelCase")]
240pub enum RelationshipApprovalPolicy {
241 #[default]
243 Never,
244 Required,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
250#[serde(rename_all = "camelCase", tag = "strategy")]
251pub enum RelationshipFailureStrategy {
252 #[default]
254 Inherit,
255 Propagate,
257 ReturnError,
259 Retry {
261 max_attempts: u32,
263 #[serde(default)]
265 backoff_ms: u64,
266 },
267 Fallback {
269 target: String,
271 },
272 RetryThenFallback {
274 max_attempts: u32,
276 #[serde(default)]
278 backoff_ms: u64,
279 target: String,
281 },
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
286#[serde(rename_all = "camelCase", deny_unknown_fields)]
287pub struct CircuitBreakerPolicy {
288 pub failure_threshold: u32,
290 pub reset_after_ms: u64,
292}
293
294#[derive(
296 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
297)]
298#[serde(rename_all = "camelCase")]
299pub enum RelationshipKind {
300 Delegate,
302 Handoff,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
308#[serde(rename_all = "camelCase", deny_unknown_fields)]
309pub struct TeamPolicy {
310 pub max_transfer_depth: u32,
312 pub max_delegation_depth: u32,
314 pub max_concurrent_delegations: usize,
316 pub context: TeamContextPolicy,
318 pub failure: TeamFailurePolicy,
320 #[serde(default)]
322 pub budget: TeamBudget,
323 #[serde(default)]
325 pub termination: TeamTerminationPolicy,
326}
327
328impl Default for TeamPolicy {
329 fn default() -> Self {
330 Self {
331 max_transfer_depth: 8,
332 max_delegation_depth: 4,
333 max_concurrent_delegations: 4,
334 context: TeamContextPolicy::Shared,
335 failure: TeamFailurePolicy::Propagate,
336 budget: TeamBudget::default(),
337 termination: TeamTerminationPolicy::default(),
338 }
339 }
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
344#[serde(rename_all = "camelCase", deny_unknown_fields)]
345pub struct TeamBudget {
346 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub max_events: Option<u64>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub max_model_requests: Option<u64>,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub max_tool_calls: Option<u64>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub max_tokens: Option<u64>,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub max_cost_microusd: Option<u64>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub max_delegations: Option<u64>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub max_handoffs: Option<u64>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub max_wall_time_ms: Option<u64>,
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
374#[serde(rename_all = "camelCase", deny_unknown_fields)]
375pub struct TeamTerminationPolicy {
376 pub stop_on_escalation: bool,
378 #[serde(default, skip_serializing_if = "Vec::is_empty")]
380 pub final_authors: Vec<String>,
381 #[serde(default, skip_serializing_if = "Vec::is_empty")]
383 pub text_markers: Vec<String>,
384}
385
386impl Default for TeamTerminationPolicy {
387 fn default() -> Self {
388 Self { stop_on_escalation: true, final_authors: Vec::new(), text_markers: Vec::new() }
389 }
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
394#[serde(rename_all = "camelCase")]
395pub enum TeamContextPolicy {
396 Isolated,
398 Shared,
400}
401
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
404#[serde(rename_all = "camelCase")]
405pub enum TeamStateMergePolicy {
406 #[default]
408 RejectConflicts,
409 Overwrite,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
415#[serde(rename_all = "camelCase", tag = "strategy")]
416pub enum TeamResumePolicy {
417 #[default]
419 FailClosed,
420 RequireCheckpoint {
423 token_state_key: String,
425 },
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
430#[serde(rename_all = "camelCase", tag = "action")]
431pub enum TeamResumePlan {
432 Coordinator,
434 Handoff {
436 edge_id: String,
438 target: String,
440 },
441 DelegateCheckpoint {
443 edge_id: String,
445 target: String,
447 token_state_key: String,
449 },
450 UnsafeDelegate {
452 edge_id: String,
454 target: String,
456 reason: String,
458 },
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
463#[serde(rename_all = "camelCase")]
464pub enum TeamFailurePolicy {
465 Propagate,
467 ReturnDelegateError,
469}
470
471#[derive(Debug, Error, PartialEq, Eq)]
473pub enum TeamError {
474 #[error("team {field} must not be empty")]
476 EmptyName {
477 field: &'static str,
479 },
480 #[error("duplicate team member '{0}'")]
482 DuplicateMember(String),
483 #[error("team name '{0}' must not also be a member name")]
485 TeamNameCollision(String),
486 #[error("team coordinator '{0}' is not a declared member")]
488 UnknownCoordinator(String),
489 #[error("relationship {endpoint} references unknown member '{name}'")]
491 UnknownRelationshipMember {
492 endpoint: &'static str,
494 name: String,
496 },
497 #[error("self relationship is not allowed for member '{0}'")]
499 SelfRelationship(String),
500 #[error("duplicate {kind:?} relationship from '{from}' to '{to}'")]
502 DuplicateRelationship {
503 from: String,
505 to: String,
507 kind: RelationshipKind,
509 },
510 #[error("team topology contains a cycle involving '{0}'")]
512 CyclicTopology(String),
513 #[error("team member '{0}' is unreachable from the coordinator")]
515 UnreachableMember(String),
516 #[error("team policy '{0}' must be greater than zero")]
518 InvalidPolicy(&'static str),
519 #[error("invalid capability requirement for member '{member}': {reason}")]
521 InvalidMemberCapability {
522 member: String,
524 reason: String,
526 },
527 #[error("invalid relationship policy from '{from}' to '{to}': {reason}")]
529 InvalidRelationshipPolicy {
530 from: String,
532 to: String,
534 reason: String,
536 },
537 #[error("team budget '{0}' must be greater than zero when configured")]
539 InvalidBudget(&'static str),
540 #[error("team termination final author '{0}' is not a declared member")]
542 UnknownTerminationAuthor(String),
543 #[error("no agent registered for team member '{0}'")]
545 MissingAgent(String),
546 #[error("duplicate agent binding for team member '{0}'")]
548 DuplicateAgentBinding(String),
549 #[error("agent binding '{0}' is not declared by the team")]
551 UnexpectedAgentBinding(String),
552 #[error(
554 "agent '{member}' does not support required runtime capability '{capability}' for {relationship}"
555 )]
556 UnsupportedAgentCapability {
557 member: String,
559 capability: &'static str,
561 relationship: String,
563 },
564 #[error("TeamSpec contains Delegate relationships; enable the adk-agent 'team-tools' feature")]
566 DelegationFeatureDisabled,
567 #[error("team registry failed: {0}")]
569 Registry(String),
570 #[error(
572 "no registry candidate for team member '{member}' with required capabilities {capabilities:?}"
573 )]
574 NoRegistryCandidate {
575 member: String,
577 capabilities: Vec<String>,
579 },
580 #[error("incompatible team execution snapshot: {0}")]
582 IncompatibleExecutionSnapshot(String),
583}
584
585#[derive(Debug, Error)]
587pub enum TeamRuntimeError {
588 #[error("team lifecycle hook '{hook}' failed during {phase:?}: {message}")]
590 LifecycleHook {
591 hook: String,
593 phase: TeamLifecyclePhase,
595 message: String,
597 },
598 #[error("team policy denied {operation}: {reason}")]
600 PolicyDenied {
601 operation: String,
603 reason: String,
605 },
606 #[error("team budget exceeded: {0}")]
608 BudgetExceeded(String),
609 #[error("unsafe team resume: {0}")]
611 UnsafeResume(String),
612 #[error("team state policy rejected delegated output: {0}")]
614 StatePolicy(String),
615}
616
617impl From<TeamRuntimeError> for adk_core::AdkError {
618 fn from(error: TeamRuntimeError) -> Self {
619 use adk_core::{AdkError, ErrorCategory, ErrorComponent};
620 let (category, code) = match &error {
621 TeamRuntimeError::LifecycleHook { .. } => {
622 (ErrorCategory::Internal, "agent.team.lifecycle_hook_failed")
623 }
624 TeamRuntimeError::PolicyDenied { .. } => {
625 (ErrorCategory::Forbidden, "agent.team.policy_denied")
626 }
627 TeamRuntimeError::BudgetExceeded(_) => {
628 (ErrorCategory::RateLimited, "agent.team.budget_exceeded")
629 }
630 TeamRuntimeError::UnsafeResume(_) => {
631 (ErrorCategory::Unsupported, "agent.team.resume_unsafe")
632 }
633 TeamRuntimeError::StatePolicy(_) => {
634 (ErrorCategory::InvalidInput, "agent.team.state_policy_violation")
635 }
636 };
637 AdkError::new(ErrorComponent::Agent, category, code, error.to_string())
638 }
639}
640
641impl From<TeamError> for adk_core::AdkError {
642 fn from(error: TeamError) -> Self {
643 use adk_core::{AdkError, ErrorCategory, ErrorComponent, ErrorDetails};
644
645 let message = error.to_string();
646 let (category, code) = match &error {
647 TeamError::MissingAgent(_)
648 | TeamError::NoRegistryCandidate { .. }
649 | TeamError::UnknownRelationshipMember { .. }
650 | TeamError::UnknownCoordinator(_) => {
651 (ErrorCategory::NotFound, "agent.team.binding_not_found")
652 }
653 TeamError::UnsupportedAgentCapability { .. } | TeamError::DelegationFeatureDisabled => {
654 (ErrorCategory::Unsupported, "agent.team.capability_unsupported")
655 }
656 TeamError::Registry(_) => (ErrorCategory::Unavailable, "agent.team.registry_failed"),
657 TeamError::IncompatibleExecutionSnapshot(_) => {
658 (ErrorCategory::InvalidInput, "agent.team.snapshot_incompatible")
659 }
660 TeamError::EmptyName { .. }
661 | TeamError::DuplicateMember(_)
662 | TeamError::TeamNameCollision(_)
663 | TeamError::SelfRelationship(_)
664 | TeamError::DuplicateRelationship { .. }
665 | TeamError::CyclicTopology(_)
666 | TeamError::UnreachableMember(_)
667 | TeamError::InvalidPolicy(_)
668 | TeamError::InvalidMemberCapability { .. }
669 | TeamError::InvalidRelationshipPolicy { .. }
670 | TeamError::InvalidBudget(_)
671 | TeamError::UnknownTerminationAuthor(_)
672 | TeamError::DuplicateAgentBinding(_)
673 | TeamError::UnexpectedAgentBinding(_) => {
674 (ErrorCategory::InvalidInput, "agent.team.invalid_spec")
675 }
676 };
677 let mut details = ErrorDetails::default();
678 details
679 .metadata
680 .insert("teamError".to_string(), serde_json::Value::String(format!("{error:?}")));
681 AdkError::new(ErrorComponent::Agent, category, code, message).with_details(details)
682 }
683}
684
685impl TeamSpec {
686 pub fn validate(&self) -> std::result::Result<(), TeamError> {
688 if self.name.trim().is_empty() {
689 return Err(TeamError::EmptyName { field: "name" });
690 }
691 if self.coordinator.trim().is_empty() {
692 return Err(TeamError::EmptyName { field: "coordinator" });
693 }
694 if self.policy.max_transfer_depth == 0 {
695 return Err(TeamError::InvalidPolicy("maxTransferDepth"));
696 }
697 if self.policy.max_delegation_depth == 0 {
698 return Err(TeamError::InvalidPolicy("maxDelegationDepth"));
699 }
700 if self.policy.max_concurrent_delegations == 0 {
701 return Err(TeamError::InvalidPolicy("maxConcurrentDelegations"));
702 }
703
704 let mut names = BTreeSet::new();
705 for member in &self.members {
706 if member.name.trim().is_empty() {
707 return Err(TeamError::EmptyName { field: "member.name" });
708 }
709 if !names.insert(member.name.as_str()) {
710 return Err(TeamError::DuplicateMember(member.name.clone()));
711 }
712 let mut capabilities = BTreeSet::new();
713 for capability in &member.required_capabilities {
714 if capability.trim().is_empty() {
715 return Err(TeamError::InvalidMemberCapability {
716 member: member.name.clone(),
717 reason: "capability names must not be empty".to_string(),
718 });
719 }
720 if !capabilities.insert(capability.as_str()) {
721 return Err(TeamError::InvalidMemberCapability {
722 member: member.name.clone(),
723 reason: format!("duplicate capability '{capability}'"),
724 });
725 }
726 }
727 if member.registry.version.as_ref().is_some_and(|value| value.trim().is_empty()) {
728 return Err(TeamError::InvalidMemberCapability {
729 member: member.name.clone(),
730 reason: "registry version must not be empty".to_string(),
731 });
732 }
733 if member.registry.digest.as_ref().is_some_and(|value| value.trim().is_empty()) {
734 return Err(TeamError::InvalidMemberCapability {
735 member: member.name.clone(),
736 reason: "registry digest must not be empty".to_string(),
737 });
738 }
739 let mut trust_labels = BTreeSet::new();
740 for label in &member.registry.trust_labels {
741 if label.trim().is_empty() || !trust_labels.insert(label.as_str()) {
742 return Err(TeamError::InvalidMemberCapability {
743 member: member.name.clone(),
744 reason: format!("invalid or duplicate registry trust label '{label}'"),
745 });
746 }
747 }
748 }
749 if !names.contains(self.coordinator.as_str()) {
750 return Err(TeamError::UnknownCoordinator(self.coordinator.clone()));
751 }
752 if names.contains(self.name.as_str()) {
753 return Err(TeamError::TeamNameCollision(self.name.clone()));
754 }
755
756 let mut edges = BTreeSet::new();
757 let mut adjacency: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
758 for relationship in &self.relationships {
759 if !names.contains(relationship.from.as_str()) {
760 return Err(TeamError::UnknownRelationshipMember {
761 endpoint: "source",
762 name: relationship.from.clone(),
763 });
764 }
765 if !names.contains(relationship.to.as_str()) {
766 return Err(TeamError::UnknownRelationshipMember {
767 endpoint: "target",
768 name: relationship.to.clone(),
769 });
770 }
771 if relationship.from == relationship.to {
772 return Err(TeamError::SelfRelationship(relationship.from.clone()));
773 }
774 let edge = (relationship.from.as_str(), relationship.to.as_str(), relationship.kind);
775 if !edges.insert(edge) {
776 return Err(TeamError::DuplicateRelationship {
777 from: relationship.from.clone(),
778 to: relationship.to.clone(),
779 kind: relationship.kind,
780 });
781 }
782 relationship.validate_policy(&names, &self.relationships)?;
783 adjacency.entry(relationship.from.as_str()).or_default().push(relationship.to.as_str());
784 }
785
786 for (name, limit) in [
787 ("maxEvents", self.policy.budget.max_events),
788 ("maxModelRequests", self.policy.budget.max_model_requests),
789 ("maxToolCalls", self.policy.budget.max_tool_calls),
790 ("maxTokens", self.policy.budget.max_tokens),
791 ("maxCostMicrousd", self.policy.budget.max_cost_microusd),
792 ("maxDelegations", self.policy.budget.max_delegations),
793 ("maxHandoffs", self.policy.budget.max_handoffs),
794 ("maxWallTimeMs", self.policy.budget.max_wall_time_ms),
795 ] {
796 if limit == Some(0) {
797 return Err(TeamError::InvalidBudget(name));
798 }
799 }
800 for author in &self.policy.termination.final_authors {
801 if !names.contains(author.as_str()) {
802 return Err(TeamError::UnknownTerminationAuthor(author.clone()));
803 }
804 }
805 if self.policy.termination.text_markers.iter().any(|marker| marker.is_empty()) {
806 return Err(TeamError::InvalidPolicy("termination.textMarkers"));
807 }
808
809 fn visit<'a>(
810 node: &'a str,
811 adjacency: &BTreeMap<&'a str, Vec<&'a str>>,
812 visiting: &mut HashSet<&'a str>,
813 visited: &mut HashSet<&'a str>,
814 ) -> std::result::Result<(), TeamError> {
815 if visiting.contains(node) {
816 return Err(TeamError::CyclicTopology(node.to_string()));
817 }
818 if !visited.insert(node) {
819 return Ok(());
820 }
821 visiting.insert(node);
822 for target in adjacency.get(node).into_iter().flatten() {
823 visit(target, adjacency, visiting, visited)?;
824 }
825 visiting.remove(node);
826 Ok(())
827 }
828
829 let mut visiting = HashSet::new();
830 let mut reachable = HashSet::new();
831 visit(&self.coordinator, &adjacency, &mut visiting, &mut reachable)?;
832 if let Some(unreachable) = names.iter().find(|name| !reachable.contains(**name)) {
833 return Err(TeamError::UnreachableMember((*unreachable).to_string()));
834 }
835 Ok(())
836 }
837
838 pub fn compile(
843 &self,
844 agents: impl IntoIterator<Item = Arc<dyn Agent>>,
845 ) -> std::result::Result<CompiledTeam, TeamError> {
846 self.validate()?;
847 #[cfg(not(feature = "team-tools"))]
848 if self.relationships.iter().any(|edge| edge.kind == RelationshipKind::Delegate) {
849 return Err(TeamError::DelegationFeatureDisabled);
850 }
851 self.compile_inner(agents, Vec::new())
852 }
853
854 pub fn compile_with_hooks(
859 &self,
860 agents: impl IntoIterator<Item = Arc<dyn Agent>>,
861 hooks: impl IntoIterator<Item = Arc<dyn TeamLifecycleHook>>,
862 ) -> std::result::Result<CompiledTeam, TeamError> {
863 self.validate()?;
864 #[cfg(not(feature = "team-tools"))]
865 if self.relationships.iter().any(|edge| edge.kind == RelationshipKind::Delegate) {
866 return Err(TeamError::DelegationFeatureDisabled);
867 }
868 self.compile_inner(agents, hooks.into_iter().collect())
869 }
870
871 fn compile_inner(
872 &self,
873 agents: impl IntoIterator<Item = Arc<dyn Agent>>,
874 hooks: Vec<Arc<dyn TeamLifecycleHook>>,
875 ) -> std::result::Result<CompiledTeam, TeamError> {
876 let declared: HashSet<&str> =
877 self.members.iter().map(|member| member.name.as_str()).collect();
878 let mut registry: HashMap<String, Arc<dyn Agent>> = HashMap::new();
879 for agent in agents {
880 let name = agent.name().to_string();
881 if !declared.contains(name.as_str()) {
882 return Err(TeamError::UnexpectedAgentBinding(name));
883 }
884 if registry.insert(name.clone(), agent).is_some() {
885 return Err(TeamError::DuplicateAgentBinding(name));
886 }
887 }
888 for member in &self.members {
889 if !registry.contains_key(&member.name) {
890 return Err(TeamError::MissingAgent(member.name.clone()));
891 }
892 }
893
894 let roster: Vec<ResolvedTeamMember> = self
895 .members
896 .iter()
897 .map(|member| ResolvedTeamMember {
898 member: member.name.clone(),
899 binding: member.name.clone(),
900 capabilities: member.required_capabilities.clone(),
901 version: None,
902 digest: None,
903 trust_labels: Vec::new(),
904 })
905 .collect();
906 self.compile_registry(registry, roster, hooks)
907 }
908
909 fn compile_registry(
910 &self,
911 registry: HashMap<String, Arc<dyn Agent>>,
912 roster: Vec<ResolvedTeamMember>,
913 hooks: Vec<Arc<dyn TeamLifecycleHook>>,
914 ) -> std::result::Result<CompiledTeam, TeamError> {
915 for relationship in &self.relationships {
916 let source = registry
917 .get(&relationship.from)
918 .expect("validated compilation has every member binding");
919 let capabilities = source.capabilities();
920 let (supported, capability) = match relationship.kind {
921 RelationshipKind::Delegate => (capabilities.runtime_tools, "runtimeTools"),
922 RelationshipKind::Handoff => (capabilities.handoff, "handoff"),
923 };
924 if !supported {
925 return Err(TeamError::UnsupportedAgentCapability {
926 member: relationship.from.clone(),
927 capability,
928 relationship: format!("{:?} edge to '{}'", relationship.kind, relationship.to),
929 });
930 }
931 if relationship.policy.approval == RelationshipApprovalPolicy::Required
932 && !capabilities.relationship_confirmation
933 {
934 return Err(TeamError::UnsupportedAgentCapability {
935 member: relationship.from.clone(),
936 capability: "relationshipConfirmation",
937 relationship: format!(
938 "approved {:?} edge to '{}'",
939 relationship.kind, relationship.to
940 ),
941 });
942 }
943 if matches!(relationship.policy.resume, TeamResumePolicy::RequireCheckpoint { .. }) {
944 let target = registry
945 .get(&relationship.to)
946 .expect("validated compilation has every member binding");
947 if !target.capabilities().checkpoint_resume {
948 return Err(TeamError::UnsupportedAgentCapability {
949 member: relationship.to.clone(),
950 capability: "checkpointResume",
951 relationship: format!(
952 "checkpoint-resumable delegation from '{}'",
953 relationship.from
954 ),
955 });
956 }
957 }
958 }
959 let runtime = Arc::new(TeamRuntimeRegistry::new(
960 self.name.clone(),
961 roster,
962 self.policy.budget.clone(),
963 self.policy.termination.clone(),
964 hooks,
965 ));
966 #[cfg(feature = "team-tools")]
967 let member_registry = Arc::new(OnceLock::new());
968 let members: Vec<Arc<dyn Agent>> = self
969 .members
970 .iter()
971 .map(|member| {
972 let relationships: Vec<TeamRelationship> = self
973 .relationships
974 .iter()
975 .filter(|relationship| relationship.from == member.name)
976 .cloned()
977 .collect();
978 Arc::new(TeamMemberAgent {
979 name: member.name.clone(),
980 description: member.description.clone().unwrap_or_else(|| {
981 registry
982 .get(&member.name)
983 .expect("all member bindings were checked before lowering")
984 .description()
985 .to_string()
986 }),
987 agent: registry
988 .get(&member.name)
989 .expect("all member bindings were checked before lowering")
990 .clone(),
991 relationships,
992 incoming_relationships: self
993 .relationships
994 .iter()
995 .filter(|relationship| relationship.to == member.name)
996 .cloned()
997 .collect(),
998 #[cfg(feature = "team-tools")]
999 members: member_registry.clone(),
1000 #[cfg(feature = "team-tools")]
1001 team_name: self.name.clone(),
1002 policy: self.policy.clone(),
1003 runtime: runtime.clone(),
1004 }) as Arc<dyn Agent>
1005 })
1006 .collect();
1007 #[cfg(feature = "team-tools")]
1008 member_registry
1009 .set(members.iter().map(|agent| (agent.name().to_string(), agent.clone())).collect())
1010 .map_err(|_| TeamError::InvalidPolicy("memberRegistry"))?;
1011
1012 let coordinator_index = self
1013 .members
1014 .iter()
1015 .position(|member| member.name == self.coordinator)
1016 .expect("validated coordinator must be present");
1017 Ok(CompiledTeam {
1018 spec: self.clone(),
1019 name: self.name.clone(),
1020 description: self.description.clone(),
1021 coordinator_name: self.coordinator.clone(),
1022 coordinator: members[coordinator_index].clone(),
1023 members,
1024 handoffs: self
1025 .members
1026 .iter()
1027 .map(|member| {
1028 let targets = self
1029 .relationships
1030 .iter()
1031 .filter(|edge| {
1032 edge.from == member.name && edge.kind == RelationshipKind::Handoff
1033 })
1034 .map(|edge| edge.to.clone())
1035 .collect();
1036 (member.name.clone(), targets)
1037 })
1038 .collect(),
1039 policy: self.policy.clone(),
1040 runtime,
1041 })
1042 }
1043}
1044
1045impl TeamRelationship {
1046 fn validate_policy(
1047 &self,
1048 names: &BTreeSet<&str>,
1049 relationships: &[TeamRelationship],
1050 ) -> std::result::Result<(), TeamError> {
1051 let invalid = |reason: String| TeamError::InvalidRelationshipPolicy {
1052 from: self.from.clone(),
1053 to: self.to.clone(),
1054 reason,
1055 };
1056 if self.kind == RelationshipKind::Handoff {
1057 let unsupported = if self.policy.input_schema.is_some() {
1058 Some("inputSchema")
1059 } else if self.policy.output_schema.is_some() {
1060 Some("outputSchema")
1061 } else if self.policy.context.is_some() {
1062 Some("context")
1063 } else if self.policy.history != TeamHistoryPolicy::Inherit {
1064 Some("history")
1065 } else if !self.policy.state_keys.is_empty() {
1066 Some("stateKeys")
1067 } else if !self.policy.state_write_keys.is_empty() {
1068 Some("stateWriteKeys")
1069 } else if self.policy.state_merge != TeamStateMergePolicy::RejectConflicts {
1070 Some("stateMerge")
1071 } else if !self.policy.artifact_prefixes.is_empty() {
1072 Some("artifactPrefixes")
1073 } else if self.policy.resume != TeamResumePolicy::FailClosed {
1074 Some("resume")
1075 } else if self.policy.timeout_ms.is_some() {
1076 Some("timeoutMs")
1077 } else if self.policy.approval != RelationshipApprovalPolicy::Never {
1078 Some("approval")
1079 } else if self.policy.circuit_breaker.is_some() {
1080 Some("circuitBreaker")
1081 } else {
1082 None
1083 };
1084 if let Some(field) = unsupported {
1085 return Err(invalid(format!(
1086 "{field} applies only to Delegate relationships; use team-wide handoff budgets or failure policy"
1087 )));
1088 }
1089 }
1090 for (label, schema) in [
1091 ("inputSchema", self.policy.input_schema.as_ref()),
1092 ("outputSchema", self.policy.output_schema.as_ref()),
1093 ] {
1094 if let Some(schema) = schema {
1095 jsonschema::validator_for(schema).map_err(|error| {
1096 invalid(format!("{label} is not valid JSON Schema: {error}"))
1097 })?;
1098 }
1099 }
1100 if self.policy.timeout_ms == Some(0) {
1101 return Err(invalid("timeoutMs must be greater than zero".to_string()));
1102 }
1103 if let TeamHistoryPolicy::Last { max_events: 0 } = self.policy.history {
1104 return Err(invalid("history.maxEvents must be greater than zero".to_string()));
1105 }
1106 let mut state_keys = BTreeSet::new();
1107 for key in &self.policy.state_keys {
1108 adk_core::validate_state_key(key)
1109 .map_err(|reason| invalid(format!("invalid state key '{key}': {reason}")))?;
1110 if !state_keys.insert(key.as_str()) {
1111 return Err(invalid(format!("duplicate state key '{key}'")));
1112 }
1113 }
1114 let mut state_write_keys = BTreeSet::new();
1115 for key in &self.policy.state_write_keys {
1116 adk_core::validate_state_key(key)
1117 .map_err(|reason| invalid(format!("invalid state write key '{key}': {reason}")))?;
1118 if !state_write_keys.insert(key.as_str()) {
1119 return Err(invalid(format!("duplicate state write key '{key}'")));
1120 }
1121 }
1122 let mut artifact_prefixes = BTreeSet::new();
1123 for prefix in &self.policy.artifact_prefixes {
1124 if prefix.is_empty() {
1125 return Err(invalid("artifact prefixes must not be empty".to_string()));
1126 }
1127 if !artifact_prefixes.insert(prefix.as_str()) {
1128 return Err(invalid(format!("duplicate artifact prefix '{prefix}'")));
1129 }
1130 }
1131 if let TeamResumePolicy::RequireCheckpoint { token_state_key } = &self.policy.resume {
1132 adk_core::validate_state_key(token_state_key).map_err(|reason| {
1133 invalid(format!("invalid resume token state key '{token_state_key}': {reason}"))
1134 })?;
1135 }
1136 match &self.policy.failure {
1137 RelationshipFailureStrategy::Retry { max_attempts: 0, .. }
1138 | RelationshipFailureStrategy::RetryThenFallback { max_attempts: 0, .. } => {
1139 return Err(invalid("maxAttempts must be greater than zero".to_string()));
1140 }
1141 RelationshipFailureStrategy::Fallback { target }
1142 | RelationshipFailureStrategy::RetryThenFallback { target, .. } => {
1143 if !names.contains(target.as_str()) {
1144 return Err(invalid(format!("fallback target '{target}' is not a member")));
1145 }
1146 if target == &self.to {
1147 return Err(invalid(
1148 "fallback target must differ from the primary target".to_string(),
1149 ));
1150 }
1151 let declared = relationships.iter().any(|candidate| {
1152 candidate.from == self.from
1153 && candidate.to == *target
1154 && candidate.kind == self.kind
1155 });
1156 if !declared {
1157 return Err(invalid(format!(
1158 "fallback target '{target}' must be declared as an exact {:?} edge from '{}'",
1159 self.kind, self.from
1160 )));
1161 }
1162 }
1163 RelationshipFailureStrategy::Inherit
1164 | RelationshipFailureStrategy::Propagate
1165 | RelationshipFailureStrategy::ReturnError
1166 | RelationshipFailureStrategy::Retry { .. } => {}
1167 }
1168 if let Some(circuit) = self.policy.circuit_breaker
1169 && (circuit.failure_threshold == 0 || circuit.reset_after_ms == 0)
1170 {
1171 return Err(invalid(
1172 "circuitBreaker failureThreshold and resetAfterMs must be greater than zero"
1173 .to_string(),
1174 ));
1175 }
1176 Ok(())
1177 }
1178}
1179
1180pub struct CompiledTeam {
1182 spec: TeamSpec,
1183 name: String,
1184 description: String,
1185 coordinator_name: String,
1186 coordinator: Arc<dyn Agent>,
1187 members: Vec<Arc<dyn Agent>>,
1188 handoffs: HashMap<String, Vec<String>>,
1189 policy: TeamPolicy,
1190 runtime: Arc<TeamRuntimeRegistry>,
1191}
1192
1193impl std::fmt::Debug for CompiledTeam {
1194 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1195 formatter
1196 .debug_struct("CompiledTeam")
1197 .field("name", &self.name)
1198 .field("coordinator", &self.coordinator_name)
1199 .field("member_count", &self.members.len())
1200 .finish()
1201 }
1202}
1203
1204impl CompiledTeam {
1205 pub fn spec(&self) -> &TeamSpec {
1207 &self.spec
1208 }
1209
1210 pub fn coordinator_name(&self) -> &str {
1212 &self.coordinator_name
1213 }
1214
1215 pub fn handoff_targets(&self, member: &str) -> Option<&[String]> {
1217 self.handoffs.get(member).map(Vec::as_slice)
1218 }
1219
1220 pub fn execution_snapshot(&self, invocation_id: &str) -> Option<TeamExecutionSnapshot> {
1222 self.runtime.snapshot(invocation_id)
1223 }
1224
1225 pub fn execution_snapshots(&self) -> Vec<TeamExecutionSnapshot> {
1227 self.runtime.snapshots()
1228 }
1229
1230 pub fn restore_execution_snapshot(
1236 &self,
1237 snapshot: TeamExecutionSnapshot,
1238 ) -> std::result::Result<(), TeamError> {
1239 self.runtime.restore(snapshot)
1240 }
1241
1242 pub fn resume_plan(&self, invocation_id: &str) -> Option<TeamResumePlan> {
1248 let snapshot = self.runtime.snapshot(invocation_id)?;
1249 let Some(active) =
1250 snapshot.edges.iter().rev().find(|edge| edge.status == TeamExecutionStatus::Running)
1251 else {
1252 return Some(TeamResumePlan::Coordinator);
1253 };
1254 match active.kind {
1255 RelationshipKind::Handoff => Some(TeamResumePlan::Handoff {
1256 edge_id: active.id.clone(),
1257 target: active.to.clone(),
1258 }),
1259 RelationshipKind::Delegate => {
1260 let policy = self.spec.relationships.iter().find(|relationship| {
1261 relationship.kind == RelationshipKind::Delegate
1262 && relationship.from == active.from
1263 && relationship.to == active.to
1264 });
1265 match policy.map(|relationship| &relationship.policy.resume) {
1266 Some(TeamResumePolicy::RequireCheckpoint { token_state_key }) => {
1267 Some(TeamResumePlan::DelegateCheckpoint {
1268 edge_id: active.id.clone(),
1269 target: active.to.clone(),
1270 token_state_key: token_state_key.clone(),
1271 })
1272 }
1273 Some(TeamResumePolicy::FailClosed) | None => {
1274 Some(TeamResumePlan::UnsafeDelegate {
1275 edge_id: active.id.clone(),
1276 target: active.to.clone(),
1277 reason: "the edge is fail-closed and has no checkpoint resume contract"
1278 .to_string(),
1279 })
1280 }
1281 }
1282 }
1283 }
1284 }
1285
1286 pub fn analyze_execution(
1288 &self,
1289 invocation_id: &str,
1290 ) -> std::result::Result<Option<TeamExecutionAnalysis>, TeamReplayError> {
1291 let Some(snapshot) = self.runtime.snapshot(invocation_id) else {
1292 return Ok(None);
1293 };
1294 validate_team_replay(&self.spec, &snapshot)?;
1295 Ok(Some(analyze_team_execution(&self.spec, &snapshot)))
1296 }
1297}
1298
1299#[async_trait]
1300impl Agent for CompiledTeam {
1301 fn name(&self) -> &str {
1302 &self.name
1303 }
1304
1305 fn description(&self) -> &str {
1306 &self.description
1307 }
1308
1309 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1310 &self.members
1311 }
1312
1313 fn capabilities(&self) -> adk_core::AgentCapabilities {
1314 self.coordinator.capabilities()
1315 }
1316
1317 fn topology(&self) -> Option<adk_core::AgentTopology> {
1318 Some(adk_core::AgentTopology {
1319 root: self.name.clone(),
1320 coordinator: self.coordinator_name.clone(),
1321 members: self
1322 .members
1323 .iter()
1324 .map(|member| adk_core::AgentTopologyMember {
1325 name: member.name().to_string(),
1326 description: member.description().to_string(),
1327 coordinator: member.name() == self.coordinator_name,
1328 capabilities: member.capabilities(),
1329 })
1330 .collect(),
1331 relationships: self
1332 .spec
1333 .relationships
1334 .iter()
1335 .map(|relationship| adk_core::AgentTopologyRelationship {
1336 from: relationship.from.clone(),
1337 to: relationship.to.clone(),
1338 kind: match relationship.kind {
1339 RelationshipKind::Delegate => adk_core::AgentRelationshipKind::Delegate,
1340 RelationshipKind::Handoff => adk_core::AgentRelationshipKind::Handoff,
1341 },
1342 })
1343 .collect(),
1344 })
1345 }
1346
1347 fn configure_run(&self, agent_name: &str, config: &mut RunConfig) {
1348 let member_name =
1349 if agent_name == self.name { self.coordinator_name.as_str() } else { agent_name };
1350 config.max_transfer_depth =
1351 Some(config.max_transfer_depth.map_or(self.policy.max_transfer_depth, |current| {
1352 current.min(self.policy.max_transfer_depth)
1353 }));
1354 if let Some(member) = self.members.iter().find(|member| member.name() == member_name) {
1355 member.configure_run(member_name, config);
1356 }
1357 }
1358
1359 fn transfer_targets_for(&self, agent_name: &str) -> Option<Vec<String>> {
1360 let member_name =
1361 if agent_name == self.name { self.coordinator_name.as_str() } else { agent_name };
1362 Some(self.handoffs.get(member_name).cloned().unwrap_or_default())
1363 }
1364
1365 fn strict_transfer_policy(&self) -> bool {
1366 true
1367 }
1368
1369 async fn govern_transfer(
1370 &self,
1371 request: &adk_core::AgentTransferRequest,
1372 ) -> Result<adk_core::AgentTransferDecision> {
1373 let declared = self.spec.relationships.iter().any(|relationship| {
1374 relationship.kind == RelationshipKind::Handoff
1375 && relationship.from == request.from
1376 && relationship.to == request.to
1377 });
1378 if !declared {
1379 return Ok(adk_core::AgentTransferDecision::Deny {
1380 reason: "the exact handoff edge is not declared by TeamSpec".to_string(),
1381 });
1382 }
1383 if request.depth > self.policy.max_transfer_depth {
1384 return Ok(adk_core::AgentTransferDecision::Deny {
1385 reason: format!(
1386 "handoff depth {} exceeds team maximum {}",
1387 request.depth, self.policy.max_transfer_depth
1388 ),
1389 });
1390 }
1391 Ok(adk_core::AgentTransferDecision::Allow)
1392 }
1393
1394 async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
1395 let root_invocation_id = ctx.orchestration_root_invocation_id().to_string();
1396 let lifecycle = TeamLifecycleContext {
1397 team: self.name.clone(),
1398 invocation_id: root_invocation_id.clone(),
1399 phase: TeamLifecyclePhase::Team,
1400 member: None,
1401 edge_id: None,
1402 from: None,
1403 to: None,
1404 kind: None,
1405 attempt: None,
1406 };
1407 let team_span = adk_telemetry::team_run_span_with_context(
1408 &self.name,
1409 &root_invocation_id,
1410 ctx.session_id(),
1411 &self.coordinator_name,
1412 );
1413 if let TeamLifecycleDecision::Terminate { reason } =
1414 self.runtime.before_lifecycle(&lifecycle).instrument(team_span.clone()).await?
1415 {
1416 self.runtime
1417 .after_lifecycle(
1418 &lifecycle,
1419 &TeamLifecycleOutcome::Terminated { reason: reason.clone() },
1420 )
1421 .await?;
1422 return Err(TeamRuntimeError::PolicyDenied {
1423 operation: "team start".to_string(),
1424 reason,
1425 }
1426 .into());
1427 }
1428 if let Some(target) = self.runtime.resume_handoff_target(&root_invocation_id)? {
1429 let member = self
1430 .members
1431 .iter()
1432 .find(|member| member.name() == target)
1433 .cloned()
1434 .ok_or_else(|| {
1435 adk_core::AdkError::new(
1436 adk_core::ErrorComponent::Agent,
1437 adk_core::ErrorCategory::NotFound,
1438 "agent.team.resume_target_missing",
1439 format!(
1440 "restored handoff target '{target}' is not in the frozen team roster"
1441 ),
1442 )
1443 })?;
1444 let inner = member.run(ctx).await;
1445 return wrap_lifecycle_stream(inner, self.runtime.clone(), lifecycle, team_span).await;
1446 }
1447 let inner = self.coordinator.run(ctx).await;
1448 wrap_lifecycle_stream(inner, self.runtime.clone(), lifecycle, team_span).await
1449 }
1450}
1451
1452async fn wrap_lifecycle_stream(
1453 inner: Result<EventStream>,
1454 runtime: Arc<TeamRuntimeRegistry>,
1455 lifecycle: TeamLifecycleContext,
1456 span: tracing::Span,
1457) -> Result<EventStream> {
1458 let mut inner = match inner {
1459 Ok(stream) => stream,
1460 Err(error) => {
1461 let outcome = TeamLifecycleOutcome::Failed {
1462 code: Some(error.code.to_string()),
1463 message: error.to_string(),
1464 };
1465 runtime.after_lifecycle(&lifecycle, &outcome).await?;
1466 span.record("team.status", "failed");
1467 return Err(error);
1468 }
1469 };
1470 Ok(Box::pin(async_stream::stream! {
1471 while let Some(result) = inner.next().await {
1472 match result {
1473 Ok(event) => {
1474 span.in_scope(|| tracing::trace!("team execution event"));
1475 yield Ok(event);
1476 }
1477 Err(error) => {
1478 let outcome = TeamLifecycleOutcome::Failed {
1479 code: Some(error.code.to_string()),
1480 message: error.to_string(),
1481 };
1482 if let Err(hook_error) = runtime.after_lifecycle(&lifecycle, &outcome).await {
1483 span.record("team.status", "failed");
1484 yield Err(hook_error);
1485 } else {
1486 span.record("team.status", "failed");
1487 yield Err(error);
1488 }
1489 return;
1490 }
1491 }
1492 }
1493 if let Err(error) = runtime
1494 .after_lifecycle(&lifecycle, &TeamLifecycleOutcome::Succeeded)
1495 .await
1496 {
1497 span.record("team.status", "failed");
1498 yield Err(error);
1499 } else {
1500 span.record("team.status", "completed");
1501 }
1502 }))
1503}
1504
1505struct TeamMemberAgent {
1506 name: String,
1507 description: String,
1508 agent: Arc<dyn Agent>,
1509 relationships: Vec<TeamRelationship>,
1510 incoming_relationships: Vec<TeamRelationship>,
1511 #[cfg(feature = "team-tools")]
1512 members: Arc<OnceLock<HashMap<String, Arc<dyn Agent>>>>,
1513 #[cfg(feature = "team-tools")]
1514 team_name: String,
1515 policy: TeamPolicy,
1516 runtime: Arc<TeamRuntimeRegistry>,
1517}
1518
1519#[async_trait]
1520impl Agent for TeamMemberAgent {
1521 fn name(&self) -> &str {
1522 &self.name
1523 }
1524
1525 fn description(&self) -> &str {
1526 &self.description
1527 }
1528
1529 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
1530 &[]
1531 }
1532
1533 fn supports_agent_transfer(&self) -> bool {
1534 self.agent.supports_agent_transfer()
1535 }
1536
1537 fn capabilities(&self) -> adk_core::AgentCapabilities {
1538 self.agent.capabilities()
1539 }
1540
1541 fn configure_run(&self, _agent_name: &str, config: &mut RunConfig) {
1542 let handoff_targets: Vec<String> = self
1543 .relationships
1544 .iter()
1545 .filter(|relationship| relationship.kind == RelationshipKind::Handoff)
1546 .map(|relationship| relationship.to.clone())
1547 .collect();
1548 config.transfer_targets = handoff_targets;
1549 config.parent_agent = None;
1550 config.max_transfer_depth =
1551 Some(config.max_transfer_depth.map_or(self.policy.max_transfer_depth, |current| {
1552 current.min(self.policy.max_transfer_depth)
1553 }));
1554 #[cfg(feature = "team-tools")]
1555 {
1556 config
1557 .runtime_toolsets
1558 .retain(|runtime| !runtime.toolset().name().starts_with("__adk_team_"));
1559 let delegates: Vec<&TeamRelationship> = self
1560 .relationships
1561 .iter()
1562 .filter(|relationship| relationship.kind == RelationshipKind::Delegate)
1563 .collect();
1564 if !delegates.is_empty() {
1565 let members = self
1566 .members
1567 .get()
1568 .expect("compiled team initializes member registry before execution");
1569 let limiter =
1570 Arc::new(tokio::sync::Semaphore::new(self.policy.max_concurrent_delegations));
1571 let build_tool = |relationship: &TeamRelationship| -> Arc<dyn Tool> {
1572 let target = members
1573 .get(&relationship.to)
1574 .expect("validated relationship target has a compiled binding")
1575 .clone();
1576 let context = relationship.policy.context.unwrap_or(self.policy.context);
1577 let history = relationship.policy.history;
1578 let snapshot = match (context, history) {
1579 (TeamContextPolicy::Shared, _)
1580 | (
1581 TeamContextPolicy::Isolated,
1582 TeamHistoryPolicy::Full | TeamHistoryPolicy::Last { .. },
1583 ) => AgentToolSessionSnapshot::Parent,
1584 (
1585 TeamContextPolicy::Isolated,
1586 TeamHistoryPolicy::Inherit | TeamHistoryPolicy::None,
1587 ) => AgentToolSessionSnapshot::Isolated,
1588 };
1589 let mut tool = AgentTool::new(target)
1590 .session_snapshot(snapshot)
1591 .forward_artifacts(context == TeamContextPolicy::Shared)
1592 .forward_shared_state(context == TeamContextPolicy::Shared)
1593 .forward_events(true)
1594 .forward_memory(context == TeamContextPolicy::Shared)
1595 .failure_mode(AgentToolFailureMode::Propagate)
1596 .max_delegation_depth(self.policy.max_delegation_depth)
1597 .execute_child_handoffs(members.values().cloned());
1598 if let Some(schema) = &relationship.policy.input_schema {
1599 tool = tool.input_schema(schema.clone());
1600 }
1601 if let Some(schema) = &relationship.policy.output_schema {
1602 tool = tool.output_schema(schema.clone());
1603 }
1604 if let Some(timeout_ms) = relationship.policy.timeout_ms {
1605 tool = tool.timeout(std::time::Duration::from_millis(timeout_ms));
1606 }
1607 match history {
1608 TeamHistoryPolicy::None => tool = tool.history_max_events(0),
1609 TeamHistoryPolicy::Last { max_events } => {
1610 tool = tool.history_max_events(max_events);
1611 }
1612 TeamHistoryPolicy::Inherit | TeamHistoryPolicy::Full => {}
1613 }
1614 if context == TeamContextPolicy::Isolated {
1615 tool = tool.state_keys(Vec::<String>::new());
1616 } else if !relationship.policy.state_keys.is_empty() {
1617 tool = tool.state_keys(relationship.policy.state_keys.clone());
1618 }
1619 if !relationship.policy.state_write_keys.is_empty() {
1620 let mut writable_keys = relationship.policy.state_write_keys.clone();
1621 writable_keys.push(runtime::TEAM_EXECUTION_STATE_KEY.to_string());
1622 tool = tool.output_state_keys(writable_keys);
1623 }
1624 tool = tool.state_merge_policy(match relationship.policy.state_merge {
1625 TeamStateMergePolicy::RejectConflicts => {
1626 AgentToolStateMergePolicy::RejectConflicts
1627 }
1628 TeamStateMergePolicy::Overwrite => AgentToolStateMergePolicy::Overwrite,
1629 });
1630 tool = tool.state_merge_exempt_keys([runtime::TEAM_EXECUTION_STATE_KEY]);
1631 if !relationship.policy.artifact_prefixes.is_empty() {
1632 tool =
1633 tool.artifact_prefixes(relationship.policy.artifact_prefixes.clone());
1634 }
1635 Arc::new(tool)
1636 };
1637 let tools = delegates
1638 .iter()
1639 .map(|relationship| {
1640 let fallback_target = match &relationship.policy.failure {
1641 RelationshipFailureStrategy::Fallback { target }
1642 | RelationshipFailureStrategy::RetryThenFallback { target, .. } => {
1643 Some(target.as_str())
1644 }
1645 RelationshipFailureStrategy::Inherit
1646 | RelationshipFailureStrategy::Propagate
1647 | RelationshipFailureStrategy::ReturnError
1648 | RelationshipFailureStrategy::Retry { .. } => None,
1649 };
1650 let fallback = fallback_target.and_then(|target| {
1651 self.relationships
1652 .iter()
1653 .find(|candidate| {
1654 candidate.kind == RelationshipKind::Delegate
1655 && candidate.to == target
1656 })
1657 .map(&build_tool)
1658 });
1659 Arc::new(BoundedDelegateTool {
1660 inner: build_tool(relationship),
1661 fallback,
1662 limiter: limiter.clone(),
1663 relationship: (*relationship).clone(),
1664 team_failure: self.policy.failure,
1665 runtime: self.runtime.clone(),
1666 circuit: Arc::new(Mutex::new(CircuitState::default())),
1667 }) as Arc<dyn Tool>
1668 })
1669 .collect();
1670 config.runtime_toolsets.push(RuntimeToolset::new(Arc::new(TeamToolset {
1671 name: format!("__adk_team_{}_{}", self.team_name, self.name),
1672 tools,
1673 })));
1674 }
1675 }
1676 }
1677
1678 async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
1679 let root_invocation_id = ctx.orchestration_root_invocation_id().to_string();
1680 self.runtime.check_budget(&root_invocation_id)?;
1681 let member_lifecycle = TeamLifecycleContext {
1682 team: self.runtime.team_name().to_string(),
1683 invocation_id: root_invocation_id.clone(),
1684 phase: TeamLifecyclePhase::Member,
1685 member: Some(self.name.clone()),
1686 edge_id: ctx.orchestration_edge_id().map(str::to_string),
1687 from: None,
1688 to: None,
1689 kind: None,
1690 attempt: None,
1691 };
1692 let member_span = adk_telemetry::team_member_span_with_context(
1693 self.runtime.team_name(),
1694 &self.name,
1695 &root_invocation_id,
1696 ctx.session_id(),
1697 );
1698 if let TeamLifecycleDecision::Terminate { reason } =
1699 self.runtime.before_lifecycle(&member_lifecycle).instrument(member_span.clone()).await?
1700 {
1701 self.runtime
1702 .after_lifecycle(
1703 &member_lifecycle,
1704 &TeamLifecycleOutcome::Terminated { reason: reason.clone() },
1705 )
1706 .await?;
1707 return Err(TeamRuntimeError::PolicyDenied {
1708 operation: format!("member '{}' start", self.name),
1709 reason,
1710 }
1711 .into());
1712 }
1713 let mut config = ctx.run_config().clone();
1714 self.configure_run(self.name(), &mut config);
1715 let required_confirmations = self
1716 .relationships
1717 .iter()
1718 .filter(|relationship| {
1719 relationship.kind == RelationshipKind::Delegate
1720 && relationship.policy.approval == RelationshipApprovalPolicy::Required
1721 })
1722 .map(|relationship| relationship.to.clone())
1723 .collect();
1724 let configured_ctx = Arc::new(TeamInvocationContext {
1725 inner: ctx.clone(),
1726 agent: self.agent.clone(),
1727 config,
1728 max_delegation_depth: self.policy.max_delegation_depth,
1729 required_confirmations,
1730 });
1731 let incoming = self.incoming_execution(ctx.as_ref(), &root_invocation_id);
1732 let incoming_relationship_span = incoming.as_ref().map(|(relationship, edge)| {
1733 adk_telemetry::team_relationship_span_with_context(
1734 self.runtime.team_name(),
1735 &relationship.from,
1736 &relationship.to,
1737 "handoff",
1738 &edge.id,
1739 &root_invocation_id,
1740 ctx.session_id(),
1741 )
1742 });
1743 let incoming_lifecycle =
1744 incoming.as_ref().map(|(relationship, edge)| TeamLifecycleContext {
1745 team: self.runtime.team_name().to_string(),
1746 invocation_id: root_invocation_id.clone(),
1747 phase: TeamLifecyclePhase::Relationship,
1748 member: None,
1749 edge_id: Some(edge.id.clone()),
1750 from: Some(relationship.from.clone()),
1751 to: Some(relationship.to.clone()),
1752 kind: Some(relationship.kind),
1753 attempt: Some(edge.attempt),
1754 });
1755 let incoming_relationship_started = std::time::Instant::now();
1756 let (max_attempts, backoff_ms) = incoming.as_ref().map_or((1, 0), |(relationship, _)| {
1757 match relationship.policy.failure {
1758 RelationshipFailureStrategy::Retry { max_attempts, backoff_ms }
1759 | RelationshipFailureStrategy::RetryThenFallback {
1760 max_attempts, backoff_ms, ..
1761 } => (max_attempts, backoff_ms),
1762 RelationshipFailureStrategy::Inherit
1763 | RelationshipFailureStrategy::Propagate
1764 | RelationshipFailureStrategy::ReturnError
1765 | RelationshipFailureStrategy::Fallback { .. } => (1, 0),
1766 }
1767 });
1768 let mut immediate_error = None;
1769 let mut inner = None;
1770 for attempt in 1..=max_attempts {
1771 match self
1772 .agent
1773 .run(configured_ctx.clone())
1774 .instrument(incoming_relationship_span.clone().unwrap_or_else(tracing::Span::none))
1775 .await
1776 {
1777 Ok(stream) => {
1778 inner = Some(stream);
1779 break;
1780 }
1781 Err(error) => {
1782 immediate_error = Some(error);
1783 if attempt < max_attempts && backoff_ms > 0 {
1784 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
1785 }
1786 }
1787 }
1788 }
1789 let Some(mut inner) = inner else {
1790 let error = immediate_error.unwrap_or_else(|| {
1791 adk_core::AdkError::new(
1792 adk_core::ErrorComponent::Agent,
1793 adk_core::ErrorCategory::Internal,
1794 "agent.team.member_start_failed",
1795 format!("team member '{}' failed to start", self.name),
1796 )
1797 });
1798 if let Some((_, edge)) = &incoming {
1799 self.runtime.finish_edge(&root_invocation_id, &edge.id, Some(error.to_string()));
1800 }
1801 if let Some(span) = &incoming_relationship_span {
1802 span.record("team.status", "failed");
1803 span.record(
1804 "team.duration_ms",
1805 incoming_relationship_started.elapsed().as_millis() as u64,
1806 );
1807 }
1808 if let Some(lifecycle) = &incoming_lifecycle {
1809 self.runtime
1810 .after_lifecycle(
1811 lifecycle,
1812 &TeamLifecycleOutcome::Failed {
1813 code: Some(error.code.to_string()),
1814 message: error.to_string(),
1815 },
1816 )
1817 .await?;
1818 }
1819 self.runtime.fail(&root_invocation_id, error.to_string());
1820 if let Some((relationship, incoming_edge)) = &incoming {
1821 match &relationship.policy.failure {
1822 RelationshipFailureStrategy::Fallback { target }
1823 | RelationshipFailureStrategy::RetryThenFallback { target, .. } => {
1824 let mut event = adk_core::Event::new(ctx.invocation_id());
1825 event.author = relationship.from.clone();
1826 event.actions.transfer_to_agent = Some(target.clone());
1827 event.llm_response.turn_complete = true;
1828 let edge_id = self.runtime.start_edge(
1829 &root_invocation_id,
1830 TeamEdgeStart {
1831 execution_id: None,
1832 parent_id: incoming_edge.parent_id.clone(),
1833 from: &relationship.from,
1834 to: target,
1835 kind: RelationshipKind::Handoff,
1836 attempt: 1,
1837 },
1838 )?;
1839 self.runtime.record_event(
1840 &root_invocation_id,
1841 Some(&edge_id),
1842 &mut event,
1843 )?;
1844 return wrap_lifecycle_stream(
1845 Ok(Box::pin(futures::stream::once(async { Ok(event) }))),
1846 self.runtime.clone(),
1847 member_lifecycle,
1848 member_span,
1849 )
1850 .await;
1851 }
1852 RelationshipFailureStrategy::ReturnError => {
1853 let mut event = adk_core::Event::new(ctx.invocation_id());
1854 event.author = self.name.clone();
1855 event.llm_response.turn_complete = true;
1856 event.llm_response.content = Some(
1857 adk_core::Content::new("model").with_text(
1858 serde_json::json!({
1859 "error": error.to_string(),
1860 "agent": self.name,
1861 })
1862 .to_string(),
1863 ),
1864 );
1865 self.runtime.record_event(
1866 &root_invocation_id,
1867 Some(&incoming_edge.id),
1868 &mut event,
1869 )?;
1870 return wrap_lifecycle_stream(
1871 Ok(Box::pin(futures::stream::once(async { Ok(event) }))),
1872 self.runtime.clone(),
1873 member_lifecycle,
1874 member_span,
1875 )
1876 .await;
1877 }
1878 RelationshipFailureStrategy::Inherit
1879 | RelationshipFailureStrategy::Propagate
1880 | RelationshipFailureStrategy::Retry { .. } => {}
1881 }
1882 }
1883 return wrap_lifecycle_stream(
1884 Err(error),
1885 self.runtime.clone(),
1886 member_lifecycle,
1887 member_span,
1888 )
1889 .await;
1890 };
1891 let relationships = self.relationships.clone();
1892 let member = self.name.clone();
1893 let inner_name = self.agent.name().to_string();
1894 let runtime = self.runtime.clone();
1895 let incoming_edge_id = incoming.as_ref().map(|(_, edge)| edge.id.clone());
1896 let stream = async_stream::stream! {
1897 let mut incoming_finished = false;
1898 while let Some(result) = inner.next().await {
1899 match result {
1900 Ok(mut event) => {
1901 if event.author.is_empty() || event.author == inner_name {
1902 event.author.clone_from(&member);
1903 }
1904 let mut event_edge_id = ctx
1905 .orchestration_edge_id()
1906 .map(str::to_string)
1907 .or_else(|| incoming_edge_id.clone());
1908 if let Some(target) = &event.actions.transfer_to_agent {
1909 let Some(_relationship) = relationships.iter().find(|relationship| {
1910 relationship.kind == RelationshipKind::Handoff
1911 && relationship.to == *target
1912 }) else {
1913 let allowed = relationships
1914 .iter()
1915 .filter(|relationship| relationship.kind == RelationshipKind::Handoff)
1916 .map(|relationship| relationship.to.as_str())
1917 .collect::<Vec<_>>()
1918 .join(", ");
1919 let error: adk_core::AdkError = TeamRuntimeError::PolicyDenied {
1920 operation: format!(
1921 "handoff from '{member}' to '{target}' (allowed: {allowed})"
1922 ),
1923 reason: "the exact edge is not declared".to_string(),
1924 }
1925 .into();
1926 runtime.fail(&root_invocation_id, error.to_string());
1927 yield Err(error);
1928 return;
1929 };
1930 let proposed_edge_id = uuid::Uuid::new_v4().to_string();
1931 let relationship_lifecycle = TeamLifecycleContext {
1932 team: runtime.team_name().to_string(),
1933 invocation_id: root_invocation_id.clone(),
1934 phase: TeamLifecyclePhase::Relationship,
1935 member: None,
1936 edge_id: Some(proposed_edge_id.clone()),
1937 from: Some(member.clone()),
1938 to: Some(target.clone()),
1939 kind: Some(RelationshipKind::Handoff),
1940 attempt: Some(1),
1941 };
1942 match runtime.before_lifecycle(&relationship_lifecycle).await {
1943 Ok(TeamLifecycleDecision::Continue) => {}
1944 Ok(TeamLifecycleDecision::Terminate { reason }) => {
1945 if let Err(error) = runtime
1946 .after_lifecycle(
1947 &relationship_lifecycle,
1948 &TeamLifecycleOutcome::Terminated {
1949 reason: reason.clone(),
1950 },
1951 )
1952 .await
1953 {
1954 yield Err(error);
1955 } else {
1956 yield Err(TeamRuntimeError::PolicyDenied {
1957 operation: format!(
1958 "handoff from '{member}' to '{target}'"
1959 ),
1960 reason,
1961 }
1962 .into());
1963 }
1964 return;
1965 }
1966 Err(error) => {
1967 yield Err(error);
1968 return;
1969 }
1970 }
1971 match runtime.start_edge(
1972 &root_invocation_id,
1973 TeamEdgeStart {
1974 execution_id: Some(proposed_edge_id),
1975 parent_id: event_edge_id.clone(),
1976 from: &member,
1977 to: target,
1978 kind: RelationshipKind::Handoff,
1979 attempt: 1,
1980 },
1981 ) {
1982 Ok(edge_id) => {
1983 if let Some(incoming_id) = &incoming_edge_id {
1984 runtime.finish_edge(&root_invocation_id, incoming_id, None);
1985 incoming_finished = true;
1986 if let Some(span) = &incoming_relationship_span {
1987 span.record("team.status", "completed");
1988 span.record(
1989 "team.duration_ms",
1990 incoming_relationship_started.elapsed().as_millis() as u64,
1991 );
1992 }
1993 if let Some(lifecycle) = &incoming_lifecycle
1994 && let Err(error) = runtime
1995 .after_lifecycle(
1996 lifecycle,
1997 &TeamLifecycleOutcome::Succeeded,
1998 )
1999 .await
2000 {
2001 yield Err(error);
2002 return;
2003 }
2004 }
2005 event_edge_id = Some(edge_id);
2006 }
2007 Err(error) => {
2008 yield Err(error);
2009 return;
2010 }
2011 }
2012 } else if event.is_final_response()
2013 && let Some(incoming_id) = &incoming_edge_id
2014 {
2015 runtime.finish_edge(&root_invocation_id, incoming_id, None);
2016 incoming_finished = true;
2017 if let Some(span) = &incoming_relationship_span {
2018 span.record("team.status", "completed");
2019 span.record(
2020 "team.duration_ms",
2021 incoming_relationship_started.elapsed().as_millis() as u64,
2022 );
2023 }
2024 if let Some(lifecycle) = &incoming_lifecycle
2025 && let Err(error) = runtime
2026 .after_lifecycle(lifecycle, &TeamLifecycleOutcome::Succeeded)
2027 .await
2028 {
2029 yield Err(error);
2030 return;
2031 }
2032 }
2033 match runtime.record_event(
2034 &root_invocation_id,
2035 event_edge_id.as_deref(),
2036 &mut event,
2037 ) {
2038 Ok(EventDisposition::Continue) => yield Ok(event),
2039 Ok(EventDisposition::Terminate) => {
2040 ctx.end_invocation();
2041 yield Ok(event);
2042 return;
2043 }
2044 Err(error) => {
2045 yield Err(error);
2046 return;
2047 }
2048 }
2049 }
2050 Err(error) => {
2051 if let Some(incoming_id) = &incoming_edge_id {
2052 runtime.finish_edge(
2053 &root_invocation_id,
2054 incoming_id,
2055 Some(error.to_string()),
2056 );
2057 }
2058 runtime.fail(&root_invocation_id, error.to_string());
2059 if let Some(span) = &incoming_relationship_span {
2060 span.record("team.status", "failed");
2061 span.record(
2062 "team.duration_ms",
2063 incoming_relationship_started.elapsed().as_millis() as u64,
2064 );
2065 }
2066 if let Some(lifecycle) = &incoming_lifecycle {
2067 let outcome = TeamLifecycleOutcome::Failed {
2068 code: Some(error.code.to_string()),
2069 message: error.to_string(),
2070 };
2071 if let Err(hook_error) = runtime.after_lifecycle(lifecycle, &outcome).await {
2072 yield Err(hook_error);
2073 return;
2074 }
2075 }
2076 yield Err(error);
2077 return;
2078 }
2079 }
2080 }
2081 if !incoming_finished && let Some(incoming_id) = &incoming_edge_id {
2082 runtime.finish_edge(&root_invocation_id, incoming_id, None);
2083 if let Some(span) = &incoming_relationship_span {
2084 span.record("team.status", "completed");
2085 span.record(
2086 "team.duration_ms",
2087 incoming_relationship_started.elapsed().as_millis() as u64,
2088 );
2089 }
2090 if let Some(lifecycle) = &incoming_lifecycle
2091 && let Err(error) = runtime
2092 .after_lifecycle(lifecycle, &TeamLifecycleOutcome::Succeeded)
2093 .await
2094 {
2095 yield Err(error);
2096 }
2097 }
2098 };
2099 wrap_lifecycle_stream(
2100 Ok(Box::pin(stream)),
2101 self.runtime.clone(),
2102 member_lifecycle,
2103 member_span,
2104 )
2105 .await
2106 }
2107}
2108
2109impl TeamMemberAgent {
2110 fn incoming_execution(
2111 &self,
2112 ctx: &dyn InvocationContext,
2113 root_invocation_id: &str,
2114 ) -> Option<(TeamRelationship, TeamEdgeExecution)> {
2115 let snapshot = self.runtime.snapshot(root_invocation_id).or_else(|| {
2116 ctx.session()
2117 .state()
2118 .get(TEAM_EXECUTION_STATE_KEY)
2119 .and_then(|value| serde_json::from_value::<TeamExecutionSnapshot>(value).ok())
2120 })?;
2121 let edge = snapshot
2122 .edges
2123 .iter()
2124 .rev()
2125 .find(|edge| {
2126 edge.kind == RelationshipKind::Handoff
2127 && edge.status == TeamExecutionStatus::Running
2128 && edge.to == self.name
2129 })?
2130 .clone();
2131 let relationship = self
2132 .incoming_relationships
2133 .iter()
2134 .find(|relationship| {
2135 relationship.kind == RelationshipKind::Handoff
2136 && relationship.from == edge.from
2137 && relationship.to == edge.to
2138 })
2139 .cloned()?;
2140 Some((relationship, edge))
2141 }
2142}
2143
2144struct TeamInvocationContext {
2147 inner: Arc<dyn InvocationContext>,
2148 agent: Arc<dyn Agent>,
2149 config: RunConfig,
2150 max_delegation_depth: u32,
2151 required_confirmations: HashSet<String>,
2152}
2153
2154#[async_trait]
2155impl adk_core::ReadonlyContext for TeamInvocationContext {
2156 fn invocation_id(&self) -> &str {
2157 self.inner.invocation_id()
2158 }
2159
2160 fn agent_name(&self) -> &str {
2161 self.agent.name()
2162 }
2163
2164 fn user_id(&self) -> &str {
2165 self.inner.user_id()
2166 }
2167
2168 fn app_name(&self) -> &str {
2169 self.inner.app_name()
2170 }
2171
2172 fn session_id(&self) -> &str {
2173 self.inner.session_id()
2174 }
2175
2176 fn branch(&self) -> &str {
2177 self.inner.branch()
2178 }
2179
2180 fn user_content(&self) -> &adk_core::Content {
2181 self.inner.user_content()
2182 }
2183}
2184
2185#[async_trait]
2186impl adk_core::CallbackContext for TeamInvocationContext {
2187 fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
2188 self.inner.artifacts()
2189 }
2190
2191 fn tool_outcome(&self) -> Option<adk_core::ToolOutcome> {
2192 self.inner.tool_outcome()
2193 }
2194
2195 fn tool_name(&self) -> Option<&str> {
2196 self.inner.tool_name()
2197 }
2198
2199 fn tool_input(&self) -> Option<&serde_json::Value> {
2200 self.inner.tool_input()
2201 }
2202
2203 fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
2204 self.inner.shared_state()
2205 }
2206}
2207
2208#[async_trait]
2209impl InvocationContext for TeamInvocationContext {
2210 fn agent(&self) -> Arc<dyn Agent> {
2211 self.agent.clone()
2212 }
2213
2214 fn memory(&self) -> Option<Arc<dyn adk_core::Memory>> {
2215 self.inner.memory()
2216 }
2217
2218 fn session(&self) -> &dyn adk_core::Session {
2219 self.inner.session()
2220 }
2221
2222 fn run_config(&self) -> &RunConfig {
2223 &self.config
2224 }
2225
2226 fn end_invocation(&self) {
2227 self.inner.end_invocation();
2228 }
2229
2230 fn ended(&self) -> bool {
2231 self.inner.ended()
2232 }
2233
2234 fn is_cancelled(&self) -> bool {
2235 self.inner.is_cancelled()
2236 }
2237
2238 fn user_scopes(&self) -> Vec<String> {
2239 self.inner.user_scopes()
2240 }
2241
2242 fn request_metadata(&self) -> HashMap<String, serde_json::Value> {
2243 self.inner.request_metadata()
2244 }
2245
2246 fn authoritative_transfer_targets(&self) -> bool {
2247 true
2248 }
2249
2250 fn delegation_depth(&self) -> u32 {
2251 self.inner.delegation_depth()
2252 }
2253
2254 fn max_delegation_depth(&self) -> Option<u32> {
2255 Some(
2256 self.inner.max_delegation_depth().map_or(self.max_delegation_depth, |current| {
2257 current.min(self.max_delegation_depth)
2258 }),
2259 )
2260 }
2261
2262 fn orchestration_root_invocation_id(&self) -> &str {
2263 self.inner.orchestration_root_invocation_id()
2264 }
2265
2266 fn orchestration_edge_id(&self) -> Option<&str> {
2267 self.inner.orchestration_edge_id()
2268 }
2269
2270 fn requires_tool_confirmation(&self, tool_name: &str) -> bool {
2271 self.required_confirmations.contains(tool_name)
2272 || self.inner.requires_tool_confirmation(tool_name)
2273 }
2274
2275 async fn get_secret(&self, name: &str) -> Result<Option<String>> {
2276 self.inner.get_secret(name).await
2277 }
2278
2279 async fn get_secret_for(&self, request: &adk_core::SecretRequest) -> Result<Option<String>> {
2280 self.inner.get_secret_for(request).await
2281 }
2282}
2283
2284#[cfg(feature = "team-tools")]
2285struct TeamToolset {
2286 name: String,
2287 tools: Vec<Arc<dyn Tool>>,
2288}
2289
2290#[cfg(feature = "team-tools")]
2291#[async_trait]
2292impl Toolset for TeamToolset {
2293 fn name(&self) -> &str {
2294 &self.name
2295 }
2296
2297 async fn tools(&self, _ctx: Arc<dyn adk_core::ReadonlyContext>) -> Result<Vec<Arc<dyn Tool>>> {
2298 Ok(self.tools.clone())
2299 }
2300}
2301
2302#[cfg(feature = "team-tools")]
2303struct BoundedDelegateTool {
2304 inner: Arc<dyn Tool>,
2305 fallback: Option<Arc<dyn Tool>>,
2306 limiter: Arc<tokio::sync::Semaphore>,
2307 relationship: TeamRelationship,
2308 team_failure: TeamFailurePolicy,
2309 runtime: Arc<TeamRuntimeRegistry>,
2310 circuit: Arc<Mutex<CircuitState>>,
2311}
2312
2313#[cfg(feature = "team-tools")]
2314#[derive(Default)]
2315struct CircuitState {
2316 consecutive_failures: u32,
2317 opened_at: Option<std::time::Instant>,
2318}
2319
2320#[cfg(feature = "team-tools")]
2321#[async_trait]
2322impl Tool for BoundedDelegateTool {
2323 fn name(&self) -> &str {
2324 self.inner.name()
2325 }
2326
2327 fn description(&self) -> &str {
2328 self.inner.description()
2329 }
2330
2331 fn declaration(&self) -> serde_json::Value {
2332 self.inner.declaration()
2333 }
2334
2335 fn enhanced_description(&self) -> String {
2336 self.inner.enhanced_description()
2337 }
2338
2339 fn is_long_running(&self) -> bool {
2340 self.inner.is_long_running()
2341 }
2342
2343 fn is_builtin(&self) -> bool {
2344 self.inner.is_builtin()
2345 }
2346
2347 fn parameters_schema(&self) -> Option<serde_json::Value> {
2348 self.inner.parameters_schema()
2349 }
2350
2351 fn response_schema(&self) -> Option<serde_json::Value> {
2352 self.inner.response_schema()
2353 }
2354
2355 fn required_scopes(&self) -> &[&str] {
2356 self.inner.required_scopes()
2357 }
2358
2359 fn is_read_only(&self) -> bool {
2360 self.inner.is_read_only()
2361 }
2362
2363 fn is_concurrency_safe(&self) -> bool {
2364 self.inner.is_concurrency_safe()
2365 }
2366
2367 fn is_agent_delegation(&self) -> bool {
2368 self.inner.is_agent_delegation()
2369 }
2370
2371 async fn execute(
2372 &self,
2373 ctx: Arc<dyn adk_core::ToolContext>,
2374 args: serde_json::Value,
2375 ) -> Result<serde_json::Value> {
2376 let _permit = self.limiter.acquire().await.map_err(|_| {
2377 adk_core::AdkError::new(
2378 adk_core::ErrorComponent::Tool,
2379 adk_core::ErrorCategory::Unavailable,
2380 "tool.team.delegation_limiter_closed",
2381 "team delegation limiter was closed",
2382 )
2383 })?;
2384 let root_invocation_id = ctx.orchestration_root_invocation_id().to_string();
2385 self.runtime.check_budget(&root_invocation_id)?;
2386 if let Some(schema) = &self.relationship.policy.input_schema {
2387 let validator = jsonschema::validator_for(schema).map_err(|error| {
2388 adk_core::AdkError::new(
2389 adk_core::ErrorComponent::Tool,
2390 adk_core::ErrorCategory::Internal,
2391 "tool.team.invalid_compiled_input_schema",
2392 format!("invalid relationship input schema: {error}"),
2393 )
2394 })?;
2395 if !validator.is_valid(&args) {
2396 return Err(adk_core::AdkError::new(
2397 adk_core::ErrorComponent::Tool,
2398 adk_core::ErrorCategory::InvalidInput,
2399 "tool.team.delegate_input_invalid",
2400 format!(
2401 "arguments for delegation from '{}' to '{}' do not satisfy the relationship input schema",
2402 self.relationship.from, self.relationship.to
2403 ),
2404 ));
2405 }
2406 }
2407 if let Some(policy) = self.relationship.policy.circuit_breaker {
2408 let mut circuit = self.circuit.lock().unwrap_or_else(|error| error.into_inner());
2409 if let Some(opened_at) = circuit.opened_at {
2410 if opened_at.elapsed() < std::time::Duration::from_millis(policy.reset_after_ms) {
2411 return self.handle_failure(format!(
2412 "delegation circuit from '{}' to '{}' is open",
2413 self.relationship.from, self.relationship.to
2414 ));
2415 }
2416 circuit.opened_at = None;
2417 }
2418 }
2419
2420 let execution_id = ctx.orchestration_edge_id().map_or_else(
2421 || ctx.function_call_id().to_string(),
2422 |parent| format!("{parent}/{}", ctx.function_call_id()),
2423 );
2424 let lifecycle = TeamLifecycleContext {
2425 team: self.runtime.team_name().to_string(),
2426 invocation_id: root_invocation_id.clone(),
2427 phase: TeamLifecyclePhase::Relationship,
2428 member: None,
2429 edge_id: Some(execution_id.clone()),
2430 from: Some(self.relationship.from.clone()),
2431 to: Some(self.relationship.to.clone()),
2432 kind: Some(RelationshipKind::Delegate),
2433 attempt: Some(1),
2434 };
2435 let relationship_span = adk_telemetry::team_relationship_span_with_context(
2436 self.runtime.team_name(),
2437 &self.relationship.from,
2438 &self.relationship.to,
2439 "delegate",
2440 &execution_id,
2441 &root_invocation_id,
2442 ctx.session_id(),
2443 );
2444 let relationship_started = std::time::Instant::now();
2445 if let TeamLifecycleDecision::Terminate { reason } =
2446 self.runtime.before_lifecycle(&lifecycle).instrument(relationship_span.clone()).await?
2447 {
2448 self.runtime
2449 .after_lifecycle(
2450 &lifecycle,
2451 &TeamLifecycleOutcome::Terminated { reason: reason.clone() },
2452 )
2453 .await?;
2454 return Err(TeamRuntimeError::PolicyDenied {
2455 operation: format!(
2456 "delegation from '{}' to '{}'",
2457 self.relationship.from, self.relationship.to
2458 ),
2459 reason,
2460 }
2461 .into());
2462 }
2463 self.runtime.start_edge(
2464 &root_invocation_id,
2465 TeamEdgeStart {
2466 execution_id: Some(execution_id.clone()),
2467 parent_id: ctx.orchestration_edge_id().map(str::to_string),
2468 from: &self.relationship.from,
2469 to: &self.relationship.to,
2470 kind: RelationshipKind::Delegate,
2471 attempt: 1,
2472 },
2473 )?;
2474 let mut checkpoint = adk_core::Event::new(ctx.invocation_id());
2475 checkpoint.author = self.relationship.from.clone();
2476 self.runtime.record_event(&root_invocation_id, Some(&execution_id), &mut checkpoint)?;
2477 ctx.emit_event(checkpoint).await;
2478
2479 let (max_attempts, backoff_ms) = match self.relationship.policy.failure {
2480 RelationshipFailureStrategy::Retry { max_attempts, backoff_ms }
2481 | RelationshipFailureStrategy::RetryThenFallback { max_attempts, backoff_ms, .. } => {
2482 (max_attempts, backoff_ms)
2483 }
2484 RelationshipFailureStrategy::Inherit
2485 | RelationshipFailureStrategy::Propagate
2486 | RelationshipFailureStrategy::ReturnError
2487 | RelationshipFailureStrategy::Fallback { .. } => (1, 0),
2488 };
2489 let mut last_error = None;
2490 for attempt in 1..=max_attempts {
2491 match self
2492 .inner
2493 .execute(ctx.clone(), args.clone())
2494 .instrument(relationship_span.clone())
2495 .await
2496 {
2497 Ok(value) => {
2498 if let Some(schema) = &self.relationship.policy.output_schema {
2499 let validator = jsonschema::validator_for(schema).map_err(|error| {
2500 adk_core::AdkError::new(
2501 adk_core::ErrorComponent::Tool,
2502 adk_core::ErrorCategory::Internal,
2503 "tool.team.invalid_compiled_output_schema",
2504 format!("invalid relationship output schema: {error}"),
2505 )
2506 })?;
2507 if !validator.is_valid(&value) {
2508 last_error = Some(format!(
2509 "delegate '{}' returned a value that does not satisfy the relationship output schema",
2510 self.relationship.to
2511 ));
2512 } else {
2513 self.record_success(&root_invocation_id, &execution_id, &lifecycle)
2514 .await?;
2515 relationship_span.record("team.status", "completed");
2516 relationship_span.record(
2517 "team.duration_ms",
2518 relationship_started.elapsed().as_millis() as u64,
2519 );
2520 return Ok(value);
2521 }
2522 } else {
2523 self.record_success(&root_invocation_id, &execution_id, &lifecycle).await?;
2524 relationship_span.record("team.status", "completed");
2525 relationship_span.record(
2526 "team.duration_ms",
2527 relationship_started.elapsed().as_millis() as u64,
2528 );
2529 return Ok(value);
2530 }
2531 }
2532 Err(error) => last_error = Some(error.to_string()),
2533 }
2534 if attempt < max_attempts && backoff_ms > 0 {
2535 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
2536 }
2537 }
2538
2539 if let Some(fallback) = &self.fallback {
2540 match fallback.execute(ctx, args).await {
2541 Ok(value) => {
2542 self.record_success(&root_invocation_id, &execution_id, &lifecycle).await?;
2543 relationship_span.record("team.status", "completed");
2544 relationship_span.record(
2545 "team.duration_ms",
2546 relationship_started.elapsed().as_millis() as u64,
2547 );
2548 return Ok(value);
2549 }
2550 Err(error) => {
2551 last_error = Some(format!(
2552 "primary delegate '{}' failed and fallback '{}' also failed: {error}",
2553 self.relationship.to,
2554 fallback.name()
2555 ));
2556 }
2557 }
2558 }
2559
2560 let error = last_error.unwrap_or_else(|| "delegated execution failed".to_string());
2561 self.runtime.finish_edge(&root_invocation_id, &execution_id, Some(error.clone()));
2562 if let Some(policy) = self.relationship.policy.circuit_breaker {
2563 let mut circuit = self.circuit.lock().unwrap_or_else(|failure| failure.into_inner());
2564 circuit.consecutive_failures = circuit.consecutive_failures.saturating_add(1);
2565 if circuit.consecutive_failures >= policy.failure_threshold {
2566 circuit.opened_at = Some(std::time::Instant::now());
2567 }
2568 }
2569 self.runtime
2570 .after_lifecycle(
2571 &lifecycle,
2572 &TeamLifecycleOutcome::Failed { code: None, message: error.clone() },
2573 )
2574 .await?;
2575 relationship_span.record("team.status", "failed");
2576 relationship_span
2577 .record("team.duration_ms", relationship_started.elapsed().as_millis() as u64);
2578 self.handle_failure(error)
2579 }
2580}
2581
2582#[cfg(feature = "team-tools")]
2583impl BoundedDelegateTool {
2584 async fn record_success(
2585 &self,
2586 invocation_id: &str,
2587 execution_id: &str,
2588 lifecycle: &TeamLifecycleContext,
2589 ) -> Result<()> {
2590 self.runtime.finish_edge(invocation_id, execution_id, None);
2591 {
2592 let mut circuit = self.circuit.lock().unwrap_or_else(|error| error.into_inner());
2593 circuit.consecutive_failures = 0;
2594 circuit.opened_at = None;
2595 }
2596 self.runtime.after_lifecycle(lifecycle, &TeamLifecycleOutcome::Succeeded).await
2597 }
2598
2599 fn handle_failure(&self, error: String) -> Result<serde_json::Value> {
2600 let return_error =
2601 matches!(self.relationship.policy.failure, RelationshipFailureStrategy::ReturnError)
2602 || matches!(self.relationship.policy.failure, RelationshipFailureStrategy::Inherit)
2603 && self.team_failure == TeamFailurePolicy::ReturnDelegateError;
2604 if return_error {
2605 Ok(serde_json::json!({
2606 "error": error,
2607 "agent": self.relationship.to,
2608 }))
2609 } else {
2610 Err(adk_core::AdkError::new(
2611 adk_core::ErrorComponent::Tool,
2612 adk_core::ErrorCategory::Internal,
2613 "tool.team.delegation_failed",
2614 error,
2615 ))
2616 }
2617 }
2618}
2619
2620#[cfg(test)]
2621mod tests {
2622 use super::*;
2623 use adk_core::{Content, Event, EventStream, ReadonlyContext, Session, State};
2624 use async_stream::stream;
2625 use std::sync::atomic::{AtomicUsize, Ordering};
2626
2627 struct StubAgent(String);
2628
2629 #[async_trait]
2630 impl Agent for StubAgent {
2631 fn name(&self) -> &str {
2632 &self.0
2633 }
2634
2635 fn description(&self) -> &str {
2636 "stub"
2637 }
2638
2639 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
2640 &[]
2641 }
2642
2643 fn capabilities(&self) -> adk_core::AgentCapabilities {
2644 adk_core::AgentCapabilities {
2645 runtime_tools: true,
2646 handoff: true,
2647 relationship_confirmation: true,
2648 checkpoint_resume: false,
2649 shared_state: true,
2650 invocation_metadata: true,
2651 }
2652 }
2653
2654 async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
2655 Ok(Box::pin(stream! { yield Ok(Event::new("stub")); }))
2656 }
2657 }
2658
2659 struct TestState;
2660
2661 impl State for TestState {
2662 fn get(&self, _key: &str) -> Option<serde_json::Value> {
2663 None
2664 }
2665
2666 fn set(&mut self, _key: String, _value: serde_json::Value) {}
2667
2668 fn all(&self) -> HashMap<String, serde_json::Value> {
2669 HashMap::new()
2670 }
2671 }
2672
2673 struct TestSession;
2674
2675 impl Session for TestSession {
2676 fn id(&self) -> &str {
2677 "team-session"
2678 }
2679
2680 fn app_name(&self) -> &str {
2681 "team-app"
2682 }
2683
2684 fn user_id(&self) -> &str {
2685 "team-user"
2686 }
2687
2688 fn state(&self) -> &dyn State {
2689 &TestState
2690 }
2691
2692 fn conversation_history(&self) -> Vec<Content> {
2693 Vec::new()
2694 }
2695 }
2696
2697 struct TestContext {
2698 content: Content,
2699 config: RunConfig,
2700 session: TestSession,
2701 }
2702
2703 #[async_trait]
2704 impl ReadonlyContext for TestContext {
2705 fn invocation_id(&self) -> &str {
2706 "team-invocation"
2707 }
2708
2709 fn agent_name(&self) -> &str {
2710 "support_team"
2711 }
2712
2713 fn user_id(&self) -> &str {
2714 "team-user"
2715 }
2716
2717 fn app_name(&self) -> &str {
2718 "team-app"
2719 }
2720
2721 fn session_id(&self) -> &str {
2722 "team-session"
2723 }
2724
2725 fn branch(&self) -> &str {
2726 ""
2727 }
2728
2729 fn user_content(&self) -> &Content {
2730 &self.content
2731 }
2732 }
2733
2734 #[async_trait]
2735 impl adk_core::CallbackContext for TestContext {
2736 fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
2737 None
2738 }
2739 }
2740
2741 #[async_trait]
2742 impl InvocationContext for TestContext {
2743 fn agent(&self) -> Arc<dyn Agent> {
2744 panic!("agent identity is not used by this test")
2745 }
2746
2747 fn memory(&self) -> Option<Arc<dyn adk_core::Memory>> {
2748 None
2749 }
2750
2751 fn session(&self) -> &dyn Session {
2752 &self.session
2753 }
2754
2755 fn run_config(&self) -> &RunConfig {
2756 &self.config
2757 }
2758
2759 fn end_invocation(&self) {}
2760
2761 fn ended(&self) -> bool {
2762 false
2763 }
2764 }
2765
2766 fn valid_spec() -> TeamSpec {
2767 TeamSpec {
2768 name: "support_team".to_string(),
2769 description: "Routes support work".to_string(),
2770 coordinator: "supervisor".to_string(),
2771 members: vec![
2772 TeamMemberSpec::new("supervisor"),
2773 TeamMemberSpec::new("billing"),
2774 TeamMemberSpec::new("technical"),
2775 ],
2776 relationships: vec![
2777 TeamRelationship::new("supervisor", "billing", RelationshipKind::Handoff),
2778 TeamRelationship::new("supervisor", "technical", RelationshipKind::Handoff),
2779 ],
2780 policy: TeamPolicy::default(),
2781 }
2782 }
2783
2784 #[test]
2785 fn validates_and_round_trips_schema() {
2786 let spec = valid_spec();
2787 spec.validate().unwrap();
2788 let json = serde_json::to_string(&spec).unwrap();
2789 assert_eq!(serde_json::from_str::<TeamSpec>(&json).unwrap(), spec);
2790 let schema = schemars::schema_for!(TeamSpec);
2791 assert_eq!(schema.get("title").and_then(serde_json::Value::as_str), Some("TeamSpec"));
2792 }
2793
2794 #[test]
2795 fn rejects_duplicates_cycles_and_unreachable_members() {
2796 let mut duplicate = valid_spec();
2797 duplicate.members.push(TeamMemberSpec::new("billing"));
2798 assert_eq!(duplicate.validate(), Err(TeamError::DuplicateMember("billing".to_string())));
2799
2800 let mut cyclic = valid_spec();
2801 cyclic.relationships.push(TeamRelationship::new(
2802 "billing",
2803 "supervisor",
2804 RelationshipKind::Delegate,
2805 ));
2806 assert!(matches!(cyclic.validate(), Err(TeamError::CyclicTopology(_))));
2807
2808 let mut unreachable = valid_spec();
2809 unreachable.relationships.pop();
2810 assert_eq!(
2811 unreachable.validate(),
2812 Err(TeamError::UnreachableMember("technical".to_string()))
2813 );
2814 }
2815
2816 #[test]
2817 fn validates_relationship_contracts_budgets_and_fallback_edges() {
2818 let mut invalid_schema = valid_spec();
2819 invalid_schema.relationships[0].policy.input_schema = Some(serde_json::json!({"type": 7}));
2820 assert!(matches!(
2821 invalid_schema.validate(),
2822 Err(TeamError::InvalidRelationshipPolicy { .. })
2823 ));
2824
2825 let mut ignored_handoff_contract = valid_spec();
2826 ignored_handoff_contract.relationships[0].policy.input_schema =
2827 Some(serde_json::json!({"type": "object"}));
2828 let error = ignored_handoff_contract.validate().unwrap_err();
2829 assert!(error.to_string().contains("applies only to Delegate"));
2830
2831 let mut zero_budget = valid_spec();
2832 zero_budget.policy.budget.max_events = Some(0);
2833 assert_eq!(zero_budget.validate(), Err(TeamError::InvalidBudget("maxEvents")));
2834
2835 let mut undeclared_fallback = valid_spec();
2836 undeclared_fallback.relationships[0].policy.failure =
2837 RelationshipFailureStrategy::Fallback { target: "supervisor".to_string() };
2838 assert!(matches!(
2839 undeclared_fallback.validate(),
2840 Err(TeamError::InvalidRelationshipPolicy { .. })
2841 ));
2842 }
2843
2844 #[test]
2845 fn compiles_exact_handoff_allowlists() {
2846 let team = valid_spec()
2847 .compile(vec![
2848 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
2849 Arc::new(StubAgent("billing".into())),
2850 Arc::new(StubAgent("technical".into())),
2851 ])
2852 .unwrap();
2853 assert_eq!(
2854 team.transfer_targets_for("supervisor").unwrap(),
2855 vec!["billing".to_string(), "technical".to_string()]
2856 );
2857 assert_eq!(team.transfer_targets_for("billing"), Some(Vec::new()));
2858 assert!(team.strict_transfer_policy());
2859 let topology = team.topology().unwrap();
2860 assert_eq!(topology.root, "support_team");
2861 assert_eq!(topology.coordinator, "supervisor");
2862 assert_eq!(topology.members.len(), 3);
2863 assert!(topology.members[0].coordinator);
2864 assert_eq!(topology.relationships.len(), 2);
2865 assert!(topology.relationships.iter().all(|relationship| {
2866 relationship.kind == adk_core::AgentRelationshipKind::Handoff
2867 && relationship.from == "supervisor"
2868 }));
2869 }
2870
2871 #[test]
2872 fn rejects_missing_agent_binding() {
2873 let error = valid_spec()
2874 .compile(vec![
2875 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
2876 Arc::new(StubAgent("billing".into())),
2877 ])
2878 .unwrap_err();
2879 assert_eq!(error, TeamError::MissingAgent("technical".to_string()));
2880 }
2881
2882 struct EmittingAgent {
2883 name: String,
2884 target: Option<String>,
2885 runs: Arc<AtomicUsize>,
2886 }
2887
2888 #[cfg(feature = "team-tools")]
2889 struct FlakyAgent {
2890 name: String,
2891 failures_before_success: usize,
2892 runs: Arc<AtomicUsize>,
2893 }
2894
2895 #[cfg(feature = "team-tools")]
2896 #[async_trait]
2897 impl Agent for FlakyAgent {
2898 fn name(&self) -> &str {
2899 &self.name
2900 }
2901
2902 fn description(&self) -> &str {
2903 "deterministic failure test member"
2904 }
2905
2906 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
2907 &[]
2908 }
2909
2910 async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
2911 let attempt = self.runs.fetch_add(1, Ordering::SeqCst) + 1;
2912 if attempt <= self.failures_before_success {
2913 return Err(adk_core::AdkError::agent(format!(
2914 "planned failure from '{}'",
2915 self.name
2916 )));
2917 }
2918 let mut event = Event::new(ctx.invocation_id());
2919 event.author = self.name.clone();
2920 event.llm_response.content = Some(Content::new("model").with_text("recovered"));
2921 Ok(Box::pin(stream! { yield Ok(event); }))
2922 }
2923 }
2924
2925 #[async_trait]
2926 impl Agent for EmittingAgent {
2927 fn name(&self) -> &str {
2928 &self.name
2929 }
2930
2931 fn description(&self) -> &str {
2932 "deterministic test member"
2933 }
2934
2935 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
2936 &[]
2937 }
2938
2939 async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
2940 self.runs.fetch_add(1, Ordering::SeqCst);
2941 let mut event = Event::new(ctx.invocation_id());
2942 event.author = self.name.clone();
2943 event.actions.transfer_to_agent = self.target.clone();
2944 event.llm_response.content = Some(Content::new("model").with_text(&self.name));
2945 Ok(Box::pin(stream! { yield Ok(event); }))
2946 }
2947 }
2948
2949 #[tokio::test]
2950 async fn handoff_transfers_control_without_invoking_target_inline() {
2951 let supervisor_runs = Arc::new(AtomicUsize::new(0));
2952 let specialist_runs = Arc::new(AtomicUsize::new(0));
2953 let team = TeamSpec {
2954 name: "handoff_team".to_string(),
2955 description: String::new(),
2956 coordinator: "supervisor".to_string(),
2957 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("specialist")],
2958 relationships: vec![TeamRelationship::new(
2959 "supervisor",
2960 "specialist",
2961 RelationshipKind::Handoff,
2962 )],
2963 policy: TeamPolicy::default(),
2964 }
2965 .compile(vec![
2966 Arc::new(EmittingAgent {
2967 name: "supervisor".to_string(),
2968 target: Some("specialist".to_string()),
2969 runs: supervisor_runs.clone(),
2970 }) as Arc<dyn Agent>,
2971 Arc::new(EmittingAgent {
2972 name: "specialist".to_string(),
2973 target: None,
2974 runs: specialist_runs.clone(),
2975 }),
2976 ])
2977 .unwrap();
2978 let context = Arc::new(TestContext {
2979 content: Content::new("user").with_text("route"),
2980 config: RunConfig::default(),
2981 session: TestSession,
2982 });
2983 let events = team.run(context).await.unwrap().collect::<Vec<_>>().await;
2984 assert_eq!(events.len(), 1);
2985 assert_eq!(
2986 events[0].as_ref().unwrap().actions.transfer_to_agent.as_deref(),
2987 Some("specialist")
2988 );
2989 assert_eq!(supervisor_runs.load(Ordering::SeqCst), 1);
2990 assert_eq!(specialist_runs.load(Ordering::SeqCst), 0);
2991 }
2992
2993 #[tokio::test]
2994 async fn restored_active_handoff_resumes_at_frozen_target() {
2995 let supervisor_runs = Arc::new(AtomicUsize::new(0));
2996 let specialist_runs = Arc::new(AtomicUsize::new(0));
2997 let team = TeamSpec {
2998 name: "resume_team".to_string(),
2999 description: String::new(),
3000 coordinator: "supervisor".to_string(),
3001 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("specialist")],
3002 relationships: vec![TeamRelationship::new(
3003 "supervisor",
3004 "specialist",
3005 RelationshipKind::Handoff,
3006 )],
3007 policy: TeamPolicy::default(),
3008 }
3009 .compile([
3010 Arc::new(EmittingAgent {
3011 name: "supervisor".to_string(),
3012 target: Some("specialist".to_string()),
3013 runs: supervisor_runs.clone(),
3014 }) as Arc<dyn Agent>,
3015 Arc::new(EmittingAgent {
3016 name: "specialist".to_string(),
3017 target: None,
3018 runs: specialist_runs.clone(),
3019 }),
3020 ])
3021 .unwrap();
3022 team.runtime
3023 .start_edge(
3024 "team-invocation",
3025 TeamEdgeStart {
3026 execution_id: Some("persisted-handoff".to_string()),
3027 parent_id: None,
3028 from: "supervisor",
3029 to: "specialist",
3030 kind: RelationshipKind::Handoff,
3031 attempt: 1,
3032 },
3033 )
3034 .unwrap();
3035 let context = Arc::new(TestContext {
3036 content: Content::new("user").with_text("resume"),
3037 config: RunConfig::default(),
3038 session: TestSession,
3039 });
3040 let events = team.run(context).await.unwrap().collect::<Vec<_>>().await;
3041 assert_eq!(events.len(), 1);
3042 assert_eq!(events[0].as_ref().unwrap().author, "specialist");
3043 assert_eq!(supervisor_runs.load(Ordering::SeqCst), 0);
3044 assert_eq!(specialist_runs.load(Ordering::SeqCst), 1);
3045 assert_eq!(
3046 team.execution_snapshot("team-invocation").unwrap().edges[0].status,
3047 TeamExecutionStatus::Completed
3048 );
3049 }
3050
3051 #[tokio::test]
3052 async fn runner_handoff_keeps_one_completed_team_receipt() {
3053 let supervisor_runs = Arc::new(AtomicUsize::new(0));
3054 let specialist_runs = Arc::new(AtomicUsize::new(0));
3055 let team = Arc::new(
3056 TeamSpec {
3057 name: "runner_receipt_team".to_string(),
3058 description: String::new(),
3059 coordinator: "supervisor".to_string(),
3060 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("specialist")],
3061 relationships: vec![TeamRelationship::new(
3062 "supervisor",
3063 "specialist",
3064 RelationshipKind::Handoff,
3065 )],
3066 policy: TeamPolicy::default(),
3067 }
3068 .compile([
3069 Arc::new(EmittingAgent {
3070 name: "supervisor".to_string(),
3071 target: Some("specialist".to_string()),
3072 runs: supervisor_runs.clone(),
3073 }) as Arc<dyn Agent>,
3074 Arc::new(EmittingAgent {
3075 name: "specialist".to_string(),
3076 target: None,
3077 runs: specialist_runs.clone(),
3078 }),
3079 ])
3080 .unwrap(),
3081 );
3082 let sessions = Arc::new(adk_session::InMemorySessionService::new());
3083 adk_session::SessionService::create(
3084 sessions.as_ref(),
3085 adk_session::CreateRequest {
3086 app_name: "runner-receipt".to_string(),
3087 user_id: "user".to_string(),
3088 session_id: Some("session".to_string()),
3089 state: HashMap::new(),
3090 },
3091 )
3092 .await
3093 .unwrap();
3094 let runner = adk_runner::Runner::builder()
3095 .app_name("runner-receipt")
3096 .agent(team.clone() as Arc<dyn Agent>)
3097 .session_service(sessions)
3098 .build()
3099 .unwrap();
3100 let stream = runner
3101 .run_str("user", "session", Content::new("user").with_text("route"))
3102 .await
3103 .unwrap();
3104 let results = stream.collect::<Vec<_>>().await;
3105 assert!(results.into_iter().all(|result| result.is_ok()));
3106 assert_eq!(supervisor_runs.load(Ordering::SeqCst), 1);
3107 assert_eq!(specialist_runs.load(Ordering::SeqCst), 1);
3108 let receipts = team.execution_snapshots();
3109 assert_eq!(receipts.len(), 1);
3110 assert_eq!(receipts[0].edges.len(), 1);
3111 assert_eq!(receipts[0].edges[0].status, TeamExecutionStatus::Completed);
3112 }
3113
3114 #[tokio::test]
3115 async fn exact_allowlist_rejects_undeclared_handoff() {
3116 let team = valid_spec()
3117 .compile(vec![
3118 Arc::new(EmittingAgent {
3119 name: "supervisor".to_string(),
3120 target: Some("undeclared".to_string()),
3121 runs: Arc::new(AtomicUsize::new(0)),
3122 }) as Arc<dyn Agent>,
3123 Arc::new(StubAgent("billing".to_string())),
3124 Arc::new(StubAgent("technical".to_string())),
3125 ])
3126 .unwrap();
3127 let context = Arc::new(TestContext {
3128 content: Content::new("user").with_text("route"),
3129 config: RunConfig::default(),
3130 session: TestSession,
3131 });
3132 let results = team.run(context).await.unwrap().collect::<Vec<_>>().await;
3133 let error = results[0].as_ref().unwrap_err();
3134 assert_eq!(error.code, "agent.team.policy_denied");
3135 assert!(error.to_string().contains("exact edge is not declared"));
3136 }
3137
3138 #[cfg(feature = "team-tools")]
3139 #[tokio::test]
3140 async fn delegate_edge_lowers_to_only_the_declared_agent_tool() {
3141 let spec = TeamSpec {
3142 name: "delegate_team".to_string(),
3143 description: String::new(),
3144 coordinator: "supervisor".to_string(),
3145 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("researcher")],
3146 relationships: vec![TeamRelationship::new(
3147 "supervisor",
3148 "researcher",
3149 RelationshipKind::Delegate,
3150 )],
3151 policy: TeamPolicy::default(),
3152 };
3153 let team = spec
3154 .compile(vec![
3155 Arc::new(StubAgent("supervisor".to_string())) as Arc<dyn Agent>,
3156 Arc::new(StubAgent("researcher".to_string())),
3157 ])
3158 .unwrap();
3159 let mut config = RunConfig::default();
3160 team.configure_run(team.name(), &mut config);
3161 assert!(config.transfer_targets.is_empty());
3162 assert_eq!(config.runtime_toolsets.len(), 1);
3163 let context = Arc::new(TestContext {
3164 content: Content::new("user").with_text("research"),
3165 config: config.clone(),
3166 session: TestSession,
3167 });
3168 let tools = config.runtime_toolsets[0]
3169 .toolset()
3170 .tools(context as Arc<dyn ReadonlyContext>)
3171 .await
3172 .unwrap();
3173 assert_eq!(tools.iter().map(|tool| tool.name()).collect::<Vec<_>>(), vec!["researcher"]);
3174 }
3175
3176 #[cfg(feature = "team-tools")]
3177 struct ScriptedModel {
3178 responses: std::sync::Mutex<std::collections::VecDeque<adk_core::LlmResponse>>,
3179 requests: std::sync::Mutex<Vec<Vec<Content>>>,
3181 }
3182
3183 #[cfg(feature = "team-tools")]
3184 impl ScriptedModel {
3185 fn new(responses: Vec<adk_core::LlmResponse>) -> Self {
3186 Self {
3187 responses: std::sync::Mutex::new(responses.into()),
3188 requests: std::sync::Mutex::new(Vec::new()),
3189 }
3190 }
3191
3192 fn text(text: &str) -> adk_core::LlmResponse {
3193 adk_core::LlmResponse {
3194 content: Some(Content::new("model").with_text(text)),
3195 usage_metadata: None,
3196 finish_reason: Some(adk_core::FinishReason::Stop),
3197 citation_metadata: None,
3198 partial: false,
3199 turn_complete: true,
3200 interrupted: false,
3201 error_code: None,
3202 error_message: None,
3203 provider_metadata: None,
3204 interaction_id: None,
3205 }
3206 }
3207
3208 fn call(name: &str) -> adk_core::LlmResponse {
3209 adk_core::LlmResponse {
3210 content: Some(Content {
3211 role: "model".to_string(),
3212 parts: vec![adk_core::Part::FunctionCall {
3213 name: name.to_string(),
3214 args: serde_json::json!({"request": "research this"}),
3215 id: Some("delegate-1".to_string()),
3216 thought_signature: None,
3217 }],
3218 }),
3219 ..Self::text("")
3220 }
3221 }
3222
3223 fn transfer(target: &str) -> adk_core::LlmResponse {
3224 adk_core::LlmResponse {
3225 content: Some(Content {
3226 role: "model".to_string(),
3227 parts: vec![adk_core::Part::FunctionCall {
3228 name: "transfer_to_agent".to_string(),
3229 args: serde_json::json!({"agent_name": target}),
3230 id: Some("handoff-1".to_string()),
3231 thought_signature: None,
3232 }],
3233 }),
3234 ..Self::text("")
3235 }
3236 }
3237 }
3238
3239 #[cfg(feature = "team-tools")]
3240 #[async_trait]
3241 impl adk_core::Llm for ScriptedModel {
3242 fn name(&self) -> &str {
3243 "scripted-team-model"
3244 }
3245
3246 async fn generate_content(
3247 &self,
3248 request: adk_core::LlmRequest,
3249 _stream: bool,
3250 ) -> Result<adk_core::LlmResponseStream> {
3251 self.requests.lock().expect("request log lock").push(request.contents);
3252 let response = self
3253 .responses
3254 .lock()
3255 .expect("script lock")
3256 .pop_front()
3257 .expect("script should have another response");
3258 Ok(Box::pin(stream! { yield Ok(response); }))
3259 }
3260 }
3261
3262 #[cfg(feature = "team-tools")]
3263 #[tokio::test]
3264 async fn delegation_returns_to_the_scripted_supervisor() {
3265 let supervisor = Arc::new(
3266 crate::LlmAgentBuilder::new("supervisor")
3267 .model(Arc::new(ScriptedModel::new(vec![
3268 ScriptedModel::call("researcher"),
3269 ScriptedModel::text("supervisor final"),
3270 ])))
3271 .build()
3272 .unwrap(),
3273 ) as Arc<dyn Agent>;
3274 let researcher = Arc::new(
3275 crate::LlmAgentBuilder::new("researcher")
3276 .model(Arc::new(ScriptedModel::new(vec![ScriptedModel::text("research result")])))
3277 .build()
3278 .unwrap(),
3279 ) as Arc<dyn Agent>;
3280 let team = TeamSpec {
3281 name: "scripted_delegate_team".to_string(),
3282 description: String::new(),
3283 coordinator: "supervisor".to_string(),
3284 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("researcher")],
3285 relationships: vec![TeamRelationship::new(
3286 "supervisor",
3287 "researcher",
3288 RelationshipKind::Delegate,
3289 )],
3290 policy: TeamPolicy::default(),
3291 }
3292 .compile([supervisor, researcher])
3293 .unwrap();
3294 let context = Arc::new(TestContext {
3295 content: Content::new("user").with_text("delegate"),
3296 config: RunConfig::default(),
3297 session: TestSession,
3298 });
3299 let results = team.run(context).await.unwrap().collect::<Vec<_>>().await;
3300 let events = results.into_iter().collect::<Result<Vec<_>>>().unwrap();
3301 assert!(events.iter().any(|event| event.author == "researcher"));
3302 assert!(events.iter().all(|event| event.actions.transfer_to_agent.is_none()));
3303 let active_receipt = events
3304 .iter()
3305 .filter_map(|event| event.actions.state_delta.get(TEAM_EXECUTION_STATE_KEY))
3306 .filter_map(|value| serde_json::from_value::<TeamExecutionSnapshot>(value.clone()).ok())
3307 .find(|snapshot| {
3308 snapshot
3309 .edges
3310 .first()
3311 .is_some_and(|edge| edge.status == TeamExecutionStatus::Running)
3312 })
3313 .expect("delegate start should emit a durable receipt before child execution");
3314 assert_eq!(active_receipt.edges[0].status, TeamExecutionStatus::Running);
3315 assert!(events.iter().any(|event| {
3316 event.author == "supervisor"
3317 && event
3318 .content()
3319 .into_iter()
3320 .flat_map(|content| &content.parts)
3321 .any(|part| part.text() == Some("supervisor final"))
3322 }));
3323 }
3324
3325 #[cfg(feature = "team-tools")]
3326 #[tokio::test]
3327 async fn nested_handoff_inside_delegation_is_consumed_losslessly() {
3328 let supervisor = Arc::new(
3329 crate::LlmAgentBuilder::new("supervisor")
3330 .model(Arc::new(ScriptedModel::new(vec![
3331 ScriptedModel::call("researcher"),
3332 ScriptedModel::text("supervisor resumed"),
3333 ])))
3334 .build()
3335 .unwrap(),
3336 ) as Arc<dyn Agent>;
3337 let researcher = Arc::new(
3338 crate::LlmAgentBuilder::new("researcher")
3339 .model(Arc::new(ScriptedModel::new(vec![ScriptedModel::transfer("writer")])))
3340 .build()
3341 .unwrap(),
3342 ) as Arc<dyn Agent>;
3343 let writer = Arc::new(
3344 crate::LlmAgentBuilder::new("writer")
3345 .model(Arc::new(ScriptedModel::new(vec![ScriptedModel::text("writer result")])))
3346 .build()
3347 .unwrap(),
3348 ) as Arc<dyn Agent>;
3349 let team = TeamSpec {
3350 name: "nested_team".to_string(),
3351 description: String::new(),
3352 coordinator: "supervisor".to_string(),
3353 members: vec![
3354 TeamMemberSpec::new("supervisor"),
3355 TeamMemberSpec::new("researcher"),
3356 TeamMemberSpec::new("writer"),
3357 ],
3358 relationships: vec![
3359 TeamRelationship::new("supervisor", "researcher", RelationshipKind::Delegate),
3360 TeamRelationship::new("researcher", "writer", RelationshipKind::Handoff),
3361 ],
3362 policy: TeamPolicy::default(),
3363 }
3364 .compile([supervisor, researcher, writer])
3365 .unwrap();
3366 let context = Arc::new(TestContext {
3367 content: Content::new("user").with_text("delegate then hand off"),
3368 config: RunConfig::default(),
3369 session: TestSession,
3370 });
3371 let events = team
3372 .run(context)
3373 .await
3374 .unwrap()
3375 .collect::<Vec<_>>()
3376 .await
3377 .into_iter()
3378 .collect::<Result<Vec<_>>>()
3379 .unwrap();
3380 assert!(events.iter().all(|event| event.actions.transfer_to_agent.is_none()));
3381 assert!(events.iter().any(|event| event.author == "writer"));
3382 assert!(events.iter().any(|event| {
3383 event.author == "supervisor"
3384 && event
3385 .content()
3386 .into_iter()
3387 .flat_map(|content| &content.parts)
3388 .any(|part| part.text() == Some("supervisor resumed"))
3389 }));
3390 let snapshot = team.execution_snapshot("team-invocation").unwrap();
3391 assert_eq!(snapshot.usage.delegations, 1);
3392 assert_eq!(snapshot.usage.handoffs, 1);
3393 assert_eq!(snapshot.edges[1].parent_id.as_deref(), Some("delegate-1"));
3394 }
3395
3396 #[cfg(feature = "team-tools")]
3397 #[tokio::test]
3398 async fn delegate_retry_and_exact_fallback_are_enforced() {
3399 let supervisor = Arc::new(
3400 crate::LlmAgentBuilder::new("supervisor")
3401 .model(Arc::new(ScriptedModel::new(vec![
3402 ScriptedModel::call("primary"),
3403 ScriptedModel::text("supervisor final"),
3404 ])))
3405 .build()
3406 .unwrap(),
3407 ) as Arc<dyn Agent>;
3408 let primary_runs = Arc::new(AtomicUsize::new(0));
3409 let backup_runs = Arc::new(AtomicUsize::new(0));
3410 let primary = Arc::new(FlakyAgent {
3411 name: "primary".to_string(),
3412 failures_before_success: usize::MAX,
3413 runs: primary_runs.clone(),
3414 }) as Arc<dyn Agent>;
3415 let backup = Arc::new(FlakyAgent {
3416 name: "backup".to_string(),
3417 failures_before_success: 0,
3418 runs: backup_runs.clone(),
3419 }) as Arc<dyn Agent>;
3420 let primary_edge =
3421 TeamRelationship::new("supervisor", "primary", RelationshipKind::Delegate).with_policy(
3422 RelationshipPolicy {
3423 failure: RelationshipFailureStrategy::RetryThenFallback {
3424 max_attempts: 2,
3425 backoff_ms: 0,
3426 target: "backup".to_string(),
3427 },
3428 ..RelationshipPolicy::default()
3429 },
3430 );
3431 let team = TeamSpec {
3432 name: "failure_team".to_string(),
3433 description: String::new(),
3434 coordinator: "supervisor".to_string(),
3435 members: vec![
3436 TeamMemberSpec::new("supervisor"),
3437 TeamMemberSpec::new("primary"),
3438 TeamMemberSpec::new("backup"),
3439 ],
3440 relationships: vec![
3441 primary_edge,
3442 TeamRelationship::new("supervisor", "backup", RelationshipKind::Delegate),
3443 ],
3444 policy: TeamPolicy::default(),
3445 }
3446 .compile([supervisor, primary, backup])
3447 .unwrap();
3448 let context = Arc::new(TestContext {
3449 content: Content::new("user").with_text("use a fallback"),
3450 config: RunConfig::default(),
3451 session: TestSession,
3452 });
3453 let events = team
3454 .run(context)
3455 .await
3456 .unwrap()
3457 .collect::<Vec<_>>()
3458 .await
3459 .into_iter()
3460 .collect::<Result<Vec<_>>>()
3461 .unwrap();
3462 assert_eq!(primary_runs.load(Ordering::SeqCst), 2);
3463 assert_eq!(backup_runs.load(Ordering::SeqCst), 1);
3464 assert!(events.iter().any(|event| event.author == "backup"));
3465 assert!(events.iter().any(|event| event.author == "supervisor"));
3466 }
3467
3468 #[cfg(feature = "team-tools")]
3469 #[tokio::test]
3470 async fn relationship_approval_interrupts_before_delegate_execution() {
3471 let supervisor = Arc::new(
3472 crate::LlmAgentBuilder::new("supervisor")
3473 .model(Arc::new(ScriptedModel::new(vec![ScriptedModel::call("researcher")])))
3474 .build()
3475 .unwrap(),
3476 ) as Arc<dyn Agent>;
3477 let researcher_runs = Arc::new(AtomicUsize::new(0));
3478 let researcher = Arc::new(FlakyAgent {
3479 name: "researcher".to_string(),
3480 failures_before_success: 0,
3481 runs: researcher_runs.clone(),
3482 }) as Arc<dyn Agent>;
3483 let team = TeamSpec {
3484 name: "approval_team".to_string(),
3485 description: String::new(),
3486 coordinator: "supervisor".to_string(),
3487 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("researcher")],
3488 relationships: vec![
3489 TeamRelationship::new("supervisor", "researcher", RelationshipKind::Delegate)
3490 .with_policy(RelationshipPolicy {
3491 approval: RelationshipApprovalPolicy::Required,
3492 ..RelationshipPolicy::default()
3493 }),
3494 ],
3495 policy: TeamPolicy::default(),
3496 }
3497 .compile([supervisor, researcher])
3498 .unwrap();
3499 let context = Arc::new(TestContext {
3500 content: Content::new("user").with_text("approval required"),
3501 config: RunConfig::default(),
3502 session: TestSession,
3503 });
3504 let events = team
3505 .run(context)
3506 .await
3507 .unwrap()
3508 .collect::<Vec<_>>()
3509 .await
3510 .into_iter()
3511 .collect::<Result<Vec<_>>>()
3512 .unwrap();
3513 assert!(events.iter().any(|event| event.actions.tool_confirmation.is_some()));
3514 assert_eq!(researcher_runs.load(Ordering::SeqCst), 0);
3515 }
3516
3517 #[cfg(feature = "team-tools")]
3520 async fn run_approved_delegate_with_decision(
3521 decision: adk_core::ToolConfirmationDecision,
3522 ) -> (Vec<Event>, usize, Vec<Vec<Content>>) {
3523 let model = Arc::new(ScriptedModel::new(vec![
3524 ScriptedModel::call("researcher"),
3525 ScriptedModel::text("supervisor final"),
3526 ]));
3527 let supervisor = Arc::new(
3528 crate::LlmAgentBuilder::new("supervisor")
3529 .model(model.clone())
3530 .tool_confirmation_policy(adk_core::ToolConfirmationPolicy::Never)
3531 .build()
3532 .unwrap(),
3533 ) as Arc<dyn Agent>;
3534 let researcher_runs = Arc::new(AtomicUsize::new(0));
3535 let researcher = Arc::new(FlakyAgent {
3536 name: "researcher".to_string(),
3537 failures_before_success: 0,
3538 runs: researcher_runs.clone(),
3539 }) as Arc<dyn Agent>;
3540 let team = TeamSpec {
3541 name: "approval_team".to_string(),
3542 description: String::new(),
3543 coordinator: "supervisor".to_string(),
3544 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("researcher")],
3545 relationships: vec![
3546 TeamRelationship::new("supervisor", "researcher", RelationshipKind::Delegate)
3547 .with_policy(RelationshipPolicy {
3548 approval: RelationshipApprovalPolicy::Required,
3549 ..RelationshipPolicy::default()
3550 }),
3551 ],
3552 policy: TeamPolicy::default(),
3553 }
3554 .compile([supervisor, researcher])
3555 .unwrap();
3556 let config = RunConfig::builder()
3557 .tool_confirmation_decisions(HashMap::from([("delegate-1".to_string(), decision)]))
3558 .build();
3559 let context = Arc::new(TestContext {
3560 content: Content::new("user").with_text("approval decided"),
3561 config,
3562 session: TestSession,
3563 });
3564 let events = team
3565 .run(context)
3566 .await
3567 .unwrap()
3568 .collect::<Vec<_>>()
3569 .await
3570 .into_iter()
3571 .collect::<Result<Vec<_>>>()
3572 .unwrap();
3573 let requests = model.requests.lock().expect("request log lock").clone();
3574 (events, researcher_runs.load(Ordering::SeqCst), requests)
3575 }
3576
3577 #[cfg(feature = "team-tools")]
3578 #[tokio::test]
3579 async fn relationship_approval_deny_blocks_delegate_execution() {
3580 let (events, researcher_runs, requests) =
3581 run_approved_delegate_with_decision(adk_core::ToolConfirmationDecision::Deny).await;
3582
3583 assert_eq!(researcher_runs, 0);
3584 assert!(events.iter().all(|event| event.actions.tool_confirmation.is_none()));
3585 let denied = serde_json::to_value(Content {
3586 role: "function".to_string(),
3587 parts: vec![adk_core::Part::FunctionResponse {
3588 function_response: adk_core::FunctionResponseData::new(
3589 "researcher",
3590 serde_json::json!({
3591 "error": "Tool 'researcher' execution denied by confirmation policy"
3592 }),
3593 ),
3594 id: Some("delegate-1".to_string()),
3595 annotations: None,
3596 }],
3597 })
3598 .unwrap();
3599 let denial_event = events
3600 .iter()
3601 .find(|event| {
3602 event.actions.tool_confirmation_decision
3603 == Some(adk_core::ToolConfirmationDecision::Deny)
3604 })
3605 .expect("the denied delegate call should produce a function response event");
3606 assert_eq!(serde_json::to_value(denial_event.content()).unwrap(), denied);
3607 assert_eq!(requests.len(), 2);
3608 assert!(
3609 requests[1].iter().any(|content| serde_json::to_value(content).unwrap() == denied),
3610 "the follow-up model request should carry the denial"
3611 );
3612 }
3613
3614 #[cfg(feature = "team-tools")]
3615 #[tokio::test]
3616 async fn relationship_approval_approve_runs_delegate() {
3617 let (events, researcher_runs, requests) =
3618 run_approved_delegate_with_decision(adk_core::ToolConfirmationDecision::Approve).await;
3619
3620 assert_eq!(researcher_runs, 1);
3621 assert!(events.iter().all(|event| event.actions.tool_confirmation.is_none()));
3622 assert!(events.iter().any(|event| {
3623 event.actions.tool_confirmation_decision
3624 == Some(adk_core::ToolConfirmationDecision::Approve)
3625 }));
3626 assert!(events.iter().any(|event| event.author == "researcher"));
3627 assert_eq!(requests.len(), 2);
3628 }
3629
3630 #[cfg(feature = "team-tools")]
3631 #[tokio::test]
3632 async fn output_contract_failure_can_return_to_supervisor_as_data() {
3633 let supervisor = Arc::new(
3634 crate::LlmAgentBuilder::new("supervisor")
3635 .model(Arc::new(ScriptedModel::new(vec![
3636 ScriptedModel::call("researcher"),
3637 ScriptedModel::text("handled contract failure"),
3638 ])))
3639 .build()
3640 .unwrap(),
3641 ) as Arc<dyn Agent>;
3642 let researcher_runs = Arc::new(AtomicUsize::new(0));
3643 let researcher = Arc::new(FlakyAgent {
3644 name: "researcher".to_string(),
3645 failures_before_success: 0,
3646 runs: researcher_runs.clone(),
3647 }) as Arc<dyn Agent>;
3648 let team = TeamSpec {
3649 name: "contract_team".to_string(),
3650 description: String::new(),
3651 coordinator: "supervisor".to_string(),
3652 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("researcher")],
3653 relationships: vec![
3654 TeamRelationship::new("supervisor", "researcher", RelationshipKind::Delegate)
3655 .with_policy(RelationshipPolicy {
3656 output_schema: Some(serde_json::json!({
3657 "type": "object",
3658 "properties": { "score": { "type": "number" } },
3659 "required": ["score"]
3660 })),
3661 failure: RelationshipFailureStrategy::ReturnError,
3662 ..RelationshipPolicy::default()
3663 }),
3664 ],
3665 policy: TeamPolicy::default(),
3666 }
3667 .compile([supervisor, researcher])
3668 .unwrap();
3669 let context = Arc::new(TestContext {
3670 content: Content::new("user").with_text("validate output"),
3671 config: RunConfig::default(),
3672 session: TestSession,
3673 });
3674 let events = team
3675 .run(context)
3676 .await
3677 .unwrap()
3678 .collect::<Vec<_>>()
3679 .await
3680 .into_iter()
3681 .collect::<Result<Vec<_>>>()
3682 .unwrap();
3683 assert_eq!(researcher_runs.load(Ordering::SeqCst), 1);
3684 assert!(events.iter().any(|event| {
3685 event.author == "supervisor"
3686 && event
3687 .content()
3688 .into_iter()
3689 .flat_map(|content| &content.parts)
3690 .any(|part| part.text() == Some("handled contract failure"))
3691 }));
3692 }
3693
3694 struct RecordingLifecycleHook {
3695 calls: Arc<std::sync::Mutex<Vec<String>>>,
3696 }
3697
3698 #[async_trait]
3699 impl TeamLifecycleHook for RecordingLifecycleHook {
3700 fn name(&self) -> &str {
3701 "recording"
3702 }
3703
3704 async fn before(&self, context: &TeamLifecycleContext) -> Result<TeamLifecycleDecision> {
3705 self.calls
3706 .lock()
3707 .expect("lifecycle calls lock")
3708 .push(format!("before:{:?}", context.phase));
3709 Ok(TeamLifecycleDecision::Continue)
3710 }
3711
3712 async fn after(
3713 &self,
3714 context: &TeamLifecycleContext,
3715 outcome: &TeamLifecycleOutcome,
3716 ) -> Result<()> {
3717 self.calls
3718 .lock()
3719 .expect("lifecycle calls lock")
3720 .push(format!("after:{:?}:{outcome:?}", context.phase));
3721 Ok(())
3722 }
3723 }
3724
3725 #[tokio::test]
3726 async fn invokes_team_and_member_lifecycle_hooks_around_execution() {
3727 let calls = Arc::new(std::sync::Mutex::new(Vec::new()));
3728 let team = valid_spec()
3729 .compile_with_hooks(
3730 [
3731 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
3732 Arc::new(StubAgent("billing".into())),
3733 Arc::new(StubAgent("technical".into())),
3734 ],
3735 [Arc::new(RecordingLifecycleHook { calls: calls.clone() })
3736 as Arc<dyn TeamLifecycleHook>],
3737 )
3738 .unwrap();
3739 let context = Arc::new(TestContext {
3740 content: Content::new("user").with_text("run hooks"),
3741 config: RunConfig::default(),
3742 session: TestSession,
3743 });
3744 team.run(context)
3745 .await
3746 .unwrap()
3747 .collect::<Vec<_>>()
3748 .await
3749 .into_iter()
3750 .collect::<Result<Vec<_>>>()
3751 .unwrap();
3752 let calls = calls.lock().expect("lifecycle calls lock");
3753 assert_eq!(calls.first().map(String::as_str), Some("before:Team"));
3754 assert!(calls.iter().any(|call| call == "before:Member"));
3755 assert!(calls.iter().any(|call| call.starts_with("after:Member:Succeeded")));
3756 assert!(calls.iter().any(|call| call.starts_with("after:Team:Succeeded")));
3757 }
3758
3759 struct DenyRelationshipHook;
3760
3761 #[async_trait]
3762 impl TeamLifecycleHook for DenyRelationshipHook {
3763 fn name(&self) -> &str {
3764 "deny-relationship"
3765 }
3766
3767 async fn before(&self, context: &TeamLifecycleContext) -> Result<TeamLifecycleDecision> {
3768 if context.phase == TeamLifecyclePhase::Relationship {
3769 Ok(TeamLifecycleDecision::Terminate {
3770 reason: "operator policy denied transition".to_string(),
3771 })
3772 } else {
3773 Ok(TeamLifecycleDecision::Continue)
3774 }
3775 }
3776 }
3777
3778 #[tokio::test]
3779 async fn relationship_hook_denies_handoff_before_target_runs() {
3780 let supervisor_runs = Arc::new(AtomicUsize::new(0));
3781 let specialist_runs = Arc::new(AtomicUsize::new(0));
3782 let spec = TeamSpec {
3783 name: "governed_handoff".to_string(),
3784 description: String::new(),
3785 coordinator: "supervisor".to_string(),
3786 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("specialist")],
3787 relationships: vec![TeamRelationship::new(
3788 "supervisor",
3789 "specialist",
3790 RelationshipKind::Handoff,
3791 )],
3792 policy: TeamPolicy::default(),
3793 };
3794 let team = spec
3795 .compile_with_hooks(
3796 [
3797 Arc::new(EmittingAgent {
3798 name: "supervisor".to_string(),
3799 target: Some("specialist".to_string()),
3800 runs: supervisor_runs.clone(),
3801 }) as Arc<dyn Agent>,
3802 Arc::new(EmittingAgent {
3803 name: "specialist".to_string(),
3804 target: None,
3805 runs: specialist_runs.clone(),
3806 }),
3807 ],
3808 [Arc::new(DenyRelationshipHook) as Arc<dyn TeamLifecycleHook>],
3809 )
3810 .unwrap();
3811 let context = Arc::new(TestContext {
3812 content: Content::new("user").with_text("route"),
3813 config: RunConfig::default(),
3814 session: TestSession,
3815 });
3816 let error = team
3817 .run(context)
3818 .await
3819 .unwrap()
3820 .collect::<Vec<_>>()
3821 .await
3822 .into_iter()
3823 .find_map(std::result::Result::err)
3824 .expect("relationship hook should deny handoff");
3825 assert_eq!(error.code, "agent.team.policy_denied");
3826 assert_eq!(supervisor_runs.load(Ordering::SeqCst), 1);
3827 assert_eq!(specialist_runs.load(Ordering::SeqCst), 0);
3828 }
3829
3830 #[cfg(feature = "team-tools")]
3831 struct LegacyRuntimeToolBlindAgent;
3832
3833 #[cfg(feature = "team-tools")]
3834 #[async_trait]
3835 impl Agent for LegacyRuntimeToolBlindAgent {
3836 fn name(&self) -> &str {
3837 "supervisor"
3838 }
3839
3840 fn description(&self) -> &str {
3841 "does not consume runtime tools"
3842 }
3843
3844 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
3845 &[]
3846 }
3847
3848 async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
3849 Ok(Box::pin(futures::stream::empty()))
3850 }
3851 }
3852
3853 #[cfg(feature = "team-tools")]
3854 #[test]
3855 fn compile_rejects_delegate_source_without_runtime_tool_capability() {
3856 let spec = TeamSpec {
3857 name: "capability_team".to_string(),
3858 description: String::new(),
3859 coordinator: "supervisor".to_string(),
3860 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("worker")],
3861 relationships: vec![TeamRelationship::new(
3862 "supervisor",
3863 "worker",
3864 RelationshipKind::Delegate,
3865 )],
3866 policy: TeamPolicy::default(),
3867 };
3868 assert!(matches!(
3869 spec.compile([
3870 Arc::new(LegacyRuntimeToolBlindAgent) as Arc<dyn Agent>,
3871 Arc::new(StubAgent("worker".into())),
3872 ]),
3873 Err(TeamError::UnsupportedAgentCapability { capability: "runtimeTools", .. })
3874 ));
3875 }
3876
3877 #[test]
3878 fn validates_replay_and_reports_topology_coverage() {
3879 let team = valid_spec()
3880 .compile([
3881 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
3882 Arc::new(StubAgent("billing".into())),
3883 Arc::new(StubAgent("technical".into())),
3884 ])
3885 .unwrap();
3886 team.runtime
3887 .start_edge(
3888 "evaluation",
3889 TeamEdgeStart {
3890 execution_id: Some("edge-1".to_string()),
3891 parent_id: None,
3892 from: "supervisor",
3893 to: "billing",
3894 kind: RelationshipKind::Handoff,
3895 attempt: 1,
3896 },
3897 )
3898 .unwrap();
3899 team.runtime.finish_edge("evaluation", "edge-1", None);
3900 let analysis = team.analyze_execution("evaluation").unwrap().unwrap();
3901 assert_eq!(analysis.declared_relationships, 2);
3902 assert_eq!(analysis.covered_relationships, 1);
3903 assert_eq!(analysis.coverage_basis_points, 5_000);
3904 assert_eq!(analysis.uncovered, ["supervisor -Handoff-> technical"]);
3905 }
3906
3907 #[cfg(feature = "team-tools")]
3908 struct CheckpointAgent;
3909
3910 #[cfg(feature = "team-tools")]
3911 #[async_trait]
3912 impl Agent for CheckpointAgent {
3913 fn name(&self) -> &str {
3914 "worker"
3915 }
3916
3917 fn description(&self) -> &str {
3918 "checkpoint-aware worker"
3919 }
3920
3921 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
3922 &[]
3923 }
3924
3925 fn capabilities(&self) -> adk_core::AgentCapabilities {
3926 adk_core::AgentCapabilities {
3927 runtime_tools: false,
3928 handoff: true,
3929 relationship_confirmation: false,
3930 checkpoint_resume: true,
3931 shared_state: true,
3932 invocation_metadata: true,
3933 }
3934 }
3935
3936 async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
3937 Ok(Box::pin(futures::stream::empty()))
3938 }
3939 }
3940
3941 #[cfg(feature = "team-tools")]
3942 #[test]
3943 fn exposes_checkpoint_resume_plan_without_replaying_delegate() {
3944 let spec = TeamSpec {
3945 name: "durable_team".to_string(),
3946 description: String::new(),
3947 coordinator: "supervisor".to_string(),
3948 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("worker")],
3949 relationships: vec![
3950 TeamRelationship::new("supervisor", "worker", RelationshipKind::Delegate)
3951 .with_policy(RelationshipPolicy {
3952 resume: TeamResumePolicy::RequireCheckpoint {
3953 token_state_key: "temp:worker_checkpoint".to_string(),
3954 },
3955 ..RelationshipPolicy::default()
3956 }),
3957 ],
3958 policy: TeamPolicy::default(),
3959 };
3960 let team = spec
3961 .compile([
3962 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
3963 Arc::new(CheckpointAgent),
3964 ])
3965 .unwrap();
3966 team.runtime
3967 .start_edge(
3968 "durable-run",
3969 TeamEdgeStart {
3970 execution_id: Some("delegate-edge".to_string()),
3971 parent_id: None,
3972 from: "supervisor",
3973 to: "worker",
3974 kind: RelationshipKind::Delegate,
3975 attempt: 1,
3976 },
3977 )
3978 .unwrap();
3979 assert_eq!(
3980 team.resume_plan("durable-run"),
3981 Some(TeamResumePlan::DelegateCheckpoint {
3982 edge_id: "delegate-edge".to_string(),
3983 target: "worker".to_string(),
3984 token_state_key: "temp:worker_checkpoint".to_string(),
3985 })
3986 );
3987 }
3988}