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 async fn execute(
2368 &self,
2369 ctx: Arc<dyn adk_core::ToolContext>,
2370 args: serde_json::Value,
2371 ) -> Result<serde_json::Value> {
2372 let _permit = self.limiter.acquire().await.map_err(|_| {
2373 adk_core::AdkError::new(
2374 adk_core::ErrorComponent::Tool,
2375 adk_core::ErrorCategory::Unavailable,
2376 "tool.team.delegation_limiter_closed",
2377 "team delegation limiter was closed",
2378 )
2379 })?;
2380 let root_invocation_id = ctx.orchestration_root_invocation_id().to_string();
2381 self.runtime.check_budget(&root_invocation_id)?;
2382 if let Some(schema) = &self.relationship.policy.input_schema {
2383 let validator = jsonschema::validator_for(schema).map_err(|error| {
2384 adk_core::AdkError::new(
2385 adk_core::ErrorComponent::Tool,
2386 adk_core::ErrorCategory::Internal,
2387 "tool.team.invalid_compiled_input_schema",
2388 format!("invalid relationship input schema: {error}"),
2389 )
2390 })?;
2391 if !validator.is_valid(&args) {
2392 return Err(adk_core::AdkError::new(
2393 adk_core::ErrorComponent::Tool,
2394 adk_core::ErrorCategory::InvalidInput,
2395 "tool.team.delegate_input_invalid",
2396 format!(
2397 "arguments for delegation from '{}' to '{}' do not satisfy the relationship input schema",
2398 self.relationship.from, self.relationship.to
2399 ),
2400 ));
2401 }
2402 }
2403 if let Some(policy) = self.relationship.policy.circuit_breaker {
2404 let mut circuit = self.circuit.lock().unwrap_or_else(|error| error.into_inner());
2405 if let Some(opened_at) = circuit.opened_at {
2406 if opened_at.elapsed() < std::time::Duration::from_millis(policy.reset_after_ms) {
2407 return self.handle_failure(format!(
2408 "delegation circuit from '{}' to '{}' is open",
2409 self.relationship.from, self.relationship.to
2410 ));
2411 }
2412 circuit.opened_at = None;
2413 }
2414 }
2415
2416 let execution_id = ctx.orchestration_edge_id().map_or_else(
2417 || ctx.function_call_id().to_string(),
2418 |parent| format!("{parent}/{}", ctx.function_call_id()),
2419 );
2420 let lifecycle = TeamLifecycleContext {
2421 team: self.runtime.team_name().to_string(),
2422 invocation_id: root_invocation_id.clone(),
2423 phase: TeamLifecyclePhase::Relationship,
2424 member: None,
2425 edge_id: Some(execution_id.clone()),
2426 from: Some(self.relationship.from.clone()),
2427 to: Some(self.relationship.to.clone()),
2428 kind: Some(RelationshipKind::Delegate),
2429 attempt: Some(1),
2430 };
2431 let relationship_span = adk_telemetry::team_relationship_span_with_context(
2432 self.runtime.team_name(),
2433 &self.relationship.from,
2434 &self.relationship.to,
2435 "delegate",
2436 &execution_id,
2437 &root_invocation_id,
2438 ctx.session_id(),
2439 );
2440 let relationship_started = std::time::Instant::now();
2441 if let TeamLifecycleDecision::Terminate { reason } =
2442 self.runtime.before_lifecycle(&lifecycle).instrument(relationship_span.clone()).await?
2443 {
2444 self.runtime
2445 .after_lifecycle(
2446 &lifecycle,
2447 &TeamLifecycleOutcome::Terminated { reason: reason.clone() },
2448 )
2449 .await?;
2450 return Err(TeamRuntimeError::PolicyDenied {
2451 operation: format!(
2452 "delegation from '{}' to '{}'",
2453 self.relationship.from, self.relationship.to
2454 ),
2455 reason,
2456 }
2457 .into());
2458 }
2459 self.runtime.start_edge(
2460 &root_invocation_id,
2461 TeamEdgeStart {
2462 execution_id: Some(execution_id.clone()),
2463 parent_id: ctx.orchestration_edge_id().map(str::to_string),
2464 from: &self.relationship.from,
2465 to: &self.relationship.to,
2466 kind: RelationshipKind::Delegate,
2467 attempt: 1,
2468 },
2469 )?;
2470 let mut checkpoint = adk_core::Event::new(ctx.invocation_id());
2471 checkpoint.author = self.relationship.from.clone();
2472 self.runtime.record_event(&root_invocation_id, Some(&execution_id), &mut checkpoint)?;
2473 ctx.emit_event(checkpoint).await;
2474
2475 let (max_attempts, backoff_ms) = match self.relationship.policy.failure {
2476 RelationshipFailureStrategy::Retry { max_attempts, backoff_ms }
2477 | RelationshipFailureStrategy::RetryThenFallback { max_attempts, backoff_ms, .. } => {
2478 (max_attempts, backoff_ms)
2479 }
2480 RelationshipFailureStrategy::Inherit
2481 | RelationshipFailureStrategy::Propagate
2482 | RelationshipFailureStrategy::ReturnError
2483 | RelationshipFailureStrategy::Fallback { .. } => (1, 0),
2484 };
2485 let mut last_error = None;
2486 for attempt in 1..=max_attempts {
2487 match self
2488 .inner
2489 .execute(ctx.clone(), args.clone())
2490 .instrument(relationship_span.clone())
2491 .await
2492 {
2493 Ok(value) => {
2494 if let Some(schema) = &self.relationship.policy.output_schema {
2495 let validator = jsonschema::validator_for(schema).map_err(|error| {
2496 adk_core::AdkError::new(
2497 adk_core::ErrorComponent::Tool,
2498 adk_core::ErrorCategory::Internal,
2499 "tool.team.invalid_compiled_output_schema",
2500 format!("invalid relationship output schema: {error}"),
2501 )
2502 })?;
2503 if !validator.is_valid(&value) {
2504 last_error = Some(format!(
2505 "delegate '{}' returned a value that does not satisfy the relationship output schema",
2506 self.relationship.to
2507 ));
2508 } else {
2509 self.record_success(&root_invocation_id, &execution_id, &lifecycle)
2510 .await?;
2511 relationship_span.record("team.status", "completed");
2512 relationship_span.record(
2513 "team.duration_ms",
2514 relationship_started.elapsed().as_millis() as u64,
2515 );
2516 return Ok(value);
2517 }
2518 } else {
2519 self.record_success(&root_invocation_id, &execution_id, &lifecycle).await?;
2520 relationship_span.record("team.status", "completed");
2521 relationship_span.record(
2522 "team.duration_ms",
2523 relationship_started.elapsed().as_millis() as u64,
2524 );
2525 return Ok(value);
2526 }
2527 }
2528 Err(error) => last_error = Some(error.to_string()),
2529 }
2530 if attempt < max_attempts && backoff_ms > 0 {
2531 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
2532 }
2533 }
2534
2535 if let Some(fallback) = &self.fallback {
2536 match fallback.execute(ctx, args).await {
2537 Ok(value) => {
2538 self.record_success(&root_invocation_id, &execution_id, &lifecycle).await?;
2539 relationship_span.record("team.status", "completed");
2540 relationship_span.record(
2541 "team.duration_ms",
2542 relationship_started.elapsed().as_millis() as u64,
2543 );
2544 return Ok(value);
2545 }
2546 Err(error) => {
2547 last_error = Some(format!(
2548 "primary delegate '{}' failed and fallback '{}' also failed: {error}",
2549 self.relationship.to,
2550 fallback.name()
2551 ));
2552 }
2553 }
2554 }
2555
2556 let error = last_error.unwrap_or_else(|| "delegated execution failed".to_string());
2557 self.runtime.finish_edge(&root_invocation_id, &execution_id, Some(error.clone()));
2558 if let Some(policy) = self.relationship.policy.circuit_breaker {
2559 let mut circuit = self.circuit.lock().unwrap_or_else(|failure| failure.into_inner());
2560 circuit.consecutive_failures = circuit.consecutive_failures.saturating_add(1);
2561 if circuit.consecutive_failures >= policy.failure_threshold {
2562 circuit.opened_at = Some(std::time::Instant::now());
2563 }
2564 }
2565 self.runtime
2566 .after_lifecycle(
2567 &lifecycle,
2568 &TeamLifecycleOutcome::Failed { code: None, message: error.clone() },
2569 )
2570 .await?;
2571 relationship_span.record("team.status", "failed");
2572 relationship_span
2573 .record("team.duration_ms", relationship_started.elapsed().as_millis() as u64);
2574 self.handle_failure(error)
2575 }
2576}
2577
2578#[cfg(feature = "team-tools")]
2579impl BoundedDelegateTool {
2580 async fn record_success(
2581 &self,
2582 invocation_id: &str,
2583 execution_id: &str,
2584 lifecycle: &TeamLifecycleContext,
2585 ) -> Result<()> {
2586 self.runtime.finish_edge(invocation_id, execution_id, None);
2587 {
2588 let mut circuit = self.circuit.lock().unwrap_or_else(|error| error.into_inner());
2589 circuit.consecutive_failures = 0;
2590 circuit.opened_at = None;
2591 }
2592 self.runtime.after_lifecycle(lifecycle, &TeamLifecycleOutcome::Succeeded).await
2593 }
2594
2595 fn handle_failure(&self, error: String) -> Result<serde_json::Value> {
2596 let return_error =
2597 matches!(self.relationship.policy.failure, RelationshipFailureStrategy::ReturnError)
2598 || matches!(self.relationship.policy.failure, RelationshipFailureStrategy::Inherit)
2599 && self.team_failure == TeamFailurePolicy::ReturnDelegateError;
2600 if return_error {
2601 Ok(serde_json::json!({
2602 "error": error,
2603 "agent": self.relationship.to,
2604 }))
2605 } else {
2606 Err(adk_core::AdkError::new(
2607 adk_core::ErrorComponent::Tool,
2608 adk_core::ErrorCategory::Internal,
2609 "tool.team.delegation_failed",
2610 error,
2611 ))
2612 }
2613 }
2614}
2615
2616#[cfg(test)]
2617mod tests {
2618 use super::*;
2619 use adk_core::{Content, Event, EventStream, ReadonlyContext, Session, State};
2620 use async_stream::stream;
2621 use std::sync::atomic::{AtomicUsize, Ordering};
2622
2623 struct StubAgent(String);
2624
2625 #[async_trait]
2626 impl Agent for StubAgent {
2627 fn name(&self) -> &str {
2628 &self.0
2629 }
2630
2631 fn description(&self) -> &str {
2632 "stub"
2633 }
2634
2635 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
2636 &[]
2637 }
2638
2639 fn capabilities(&self) -> adk_core::AgentCapabilities {
2640 adk_core::AgentCapabilities {
2641 runtime_tools: true,
2642 handoff: true,
2643 relationship_confirmation: true,
2644 checkpoint_resume: false,
2645 shared_state: true,
2646 invocation_metadata: true,
2647 }
2648 }
2649
2650 async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
2651 Ok(Box::pin(stream! { yield Ok(Event::new("stub")); }))
2652 }
2653 }
2654
2655 struct TestState;
2656
2657 impl State for TestState {
2658 fn get(&self, _key: &str) -> Option<serde_json::Value> {
2659 None
2660 }
2661
2662 fn set(&mut self, _key: String, _value: serde_json::Value) {}
2663
2664 fn all(&self) -> HashMap<String, serde_json::Value> {
2665 HashMap::new()
2666 }
2667 }
2668
2669 struct TestSession;
2670
2671 impl Session for TestSession {
2672 fn id(&self) -> &str {
2673 "team-session"
2674 }
2675
2676 fn app_name(&self) -> &str {
2677 "team-app"
2678 }
2679
2680 fn user_id(&self) -> &str {
2681 "team-user"
2682 }
2683
2684 fn state(&self) -> &dyn State {
2685 &TestState
2686 }
2687
2688 fn conversation_history(&self) -> Vec<Content> {
2689 Vec::new()
2690 }
2691 }
2692
2693 struct TestContext {
2694 content: Content,
2695 config: RunConfig,
2696 session: TestSession,
2697 }
2698
2699 #[async_trait]
2700 impl ReadonlyContext for TestContext {
2701 fn invocation_id(&self) -> &str {
2702 "team-invocation"
2703 }
2704
2705 fn agent_name(&self) -> &str {
2706 "support_team"
2707 }
2708
2709 fn user_id(&self) -> &str {
2710 "team-user"
2711 }
2712
2713 fn app_name(&self) -> &str {
2714 "team-app"
2715 }
2716
2717 fn session_id(&self) -> &str {
2718 "team-session"
2719 }
2720
2721 fn branch(&self) -> &str {
2722 ""
2723 }
2724
2725 fn user_content(&self) -> &Content {
2726 &self.content
2727 }
2728 }
2729
2730 #[async_trait]
2731 impl adk_core::CallbackContext for TestContext {
2732 fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
2733 None
2734 }
2735 }
2736
2737 #[async_trait]
2738 impl InvocationContext for TestContext {
2739 fn agent(&self) -> Arc<dyn Agent> {
2740 panic!("agent identity is not used by this test")
2741 }
2742
2743 fn memory(&self) -> Option<Arc<dyn adk_core::Memory>> {
2744 None
2745 }
2746
2747 fn session(&self) -> &dyn Session {
2748 &self.session
2749 }
2750
2751 fn run_config(&self) -> &RunConfig {
2752 &self.config
2753 }
2754
2755 fn end_invocation(&self) {}
2756
2757 fn ended(&self) -> bool {
2758 false
2759 }
2760 }
2761
2762 fn valid_spec() -> TeamSpec {
2763 TeamSpec {
2764 name: "support_team".to_string(),
2765 description: "Routes support work".to_string(),
2766 coordinator: "supervisor".to_string(),
2767 members: vec![
2768 TeamMemberSpec::new("supervisor"),
2769 TeamMemberSpec::new("billing"),
2770 TeamMemberSpec::new("technical"),
2771 ],
2772 relationships: vec![
2773 TeamRelationship::new("supervisor", "billing", RelationshipKind::Handoff),
2774 TeamRelationship::new("supervisor", "technical", RelationshipKind::Handoff),
2775 ],
2776 policy: TeamPolicy::default(),
2777 }
2778 }
2779
2780 #[test]
2781 fn validates_and_round_trips_schema() {
2782 let spec = valid_spec();
2783 spec.validate().unwrap();
2784 let json = serde_json::to_string(&spec).unwrap();
2785 assert_eq!(serde_json::from_str::<TeamSpec>(&json).unwrap(), spec);
2786 let schema = schemars::schema_for!(TeamSpec);
2787 assert_eq!(schema.get("title").and_then(serde_json::Value::as_str), Some("TeamSpec"));
2788 }
2789
2790 #[test]
2791 fn rejects_duplicates_cycles_and_unreachable_members() {
2792 let mut duplicate = valid_spec();
2793 duplicate.members.push(TeamMemberSpec::new("billing"));
2794 assert_eq!(duplicate.validate(), Err(TeamError::DuplicateMember("billing".to_string())));
2795
2796 let mut cyclic = valid_spec();
2797 cyclic.relationships.push(TeamRelationship::new(
2798 "billing",
2799 "supervisor",
2800 RelationshipKind::Delegate,
2801 ));
2802 assert!(matches!(cyclic.validate(), Err(TeamError::CyclicTopology(_))));
2803
2804 let mut unreachable = valid_spec();
2805 unreachable.relationships.pop();
2806 assert_eq!(
2807 unreachable.validate(),
2808 Err(TeamError::UnreachableMember("technical".to_string()))
2809 );
2810 }
2811
2812 #[test]
2813 fn validates_relationship_contracts_budgets_and_fallback_edges() {
2814 let mut invalid_schema = valid_spec();
2815 invalid_schema.relationships[0].policy.input_schema = Some(serde_json::json!({"type": 7}));
2816 assert!(matches!(
2817 invalid_schema.validate(),
2818 Err(TeamError::InvalidRelationshipPolicy { .. })
2819 ));
2820
2821 let mut ignored_handoff_contract = valid_spec();
2822 ignored_handoff_contract.relationships[0].policy.input_schema =
2823 Some(serde_json::json!({"type": "object"}));
2824 let error = ignored_handoff_contract.validate().unwrap_err();
2825 assert!(error.to_string().contains("applies only to Delegate"));
2826
2827 let mut zero_budget = valid_spec();
2828 zero_budget.policy.budget.max_events = Some(0);
2829 assert_eq!(zero_budget.validate(), Err(TeamError::InvalidBudget("maxEvents")));
2830
2831 let mut undeclared_fallback = valid_spec();
2832 undeclared_fallback.relationships[0].policy.failure =
2833 RelationshipFailureStrategy::Fallback { target: "supervisor".to_string() };
2834 assert!(matches!(
2835 undeclared_fallback.validate(),
2836 Err(TeamError::InvalidRelationshipPolicy { .. })
2837 ));
2838 }
2839
2840 #[test]
2841 fn compiles_exact_handoff_allowlists() {
2842 let team = valid_spec()
2843 .compile(vec![
2844 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
2845 Arc::new(StubAgent("billing".into())),
2846 Arc::new(StubAgent("technical".into())),
2847 ])
2848 .unwrap();
2849 assert_eq!(
2850 team.transfer_targets_for("supervisor").unwrap(),
2851 vec!["billing".to_string(), "technical".to_string()]
2852 );
2853 assert_eq!(team.transfer_targets_for("billing"), Some(Vec::new()));
2854 assert!(team.strict_transfer_policy());
2855 let topology = team.topology().unwrap();
2856 assert_eq!(topology.root, "support_team");
2857 assert_eq!(topology.coordinator, "supervisor");
2858 assert_eq!(topology.members.len(), 3);
2859 assert!(topology.members[0].coordinator);
2860 assert_eq!(topology.relationships.len(), 2);
2861 assert!(topology.relationships.iter().all(|relationship| {
2862 relationship.kind == adk_core::AgentRelationshipKind::Handoff
2863 && relationship.from == "supervisor"
2864 }));
2865 }
2866
2867 #[test]
2868 fn rejects_missing_agent_binding() {
2869 let error = valid_spec()
2870 .compile(vec![
2871 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
2872 Arc::new(StubAgent("billing".into())),
2873 ])
2874 .unwrap_err();
2875 assert_eq!(error, TeamError::MissingAgent("technical".to_string()));
2876 }
2877
2878 struct EmittingAgent {
2879 name: String,
2880 target: Option<String>,
2881 runs: Arc<AtomicUsize>,
2882 }
2883
2884 #[cfg(feature = "team-tools")]
2885 struct FlakyAgent {
2886 name: String,
2887 failures_before_success: usize,
2888 runs: Arc<AtomicUsize>,
2889 }
2890
2891 #[cfg(feature = "team-tools")]
2892 #[async_trait]
2893 impl Agent for FlakyAgent {
2894 fn name(&self) -> &str {
2895 &self.name
2896 }
2897
2898 fn description(&self) -> &str {
2899 "deterministic failure test member"
2900 }
2901
2902 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
2903 &[]
2904 }
2905
2906 async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
2907 let attempt = self.runs.fetch_add(1, Ordering::SeqCst) + 1;
2908 if attempt <= self.failures_before_success {
2909 return Err(adk_core::AdkError::agent(format!(
2910 "planned failure from '{}'",
2911 self.name
2912 )));
2913 }
2914 let mut event = Event::new(ctx.invocation_id());
2915 event.author = self.name.clone();
2916 event.llm_response.content = Some(Content::new("model").with_text("recovered"));
2917 Ok(Box::pin(stream! { yield Ok(event); }))
2918 }
2919 }
2920
2921 #[async_trait]
2922 impl Agent for EmittingAgent {
2923 fn name(&self) -> &str {
2924 &self.name
2925 }
2926
2927 fn description(&self) -> &str {
2928 "deterministic test member"
2929 }
2930
2931 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
2932 &[]
2933 }
2934
2935 async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
2936 self.runs.fetch_add(1, Ordering::SeqCst);
2937 let mut event = Event::new(ctx.invocation_id());
2938 event.author = self.name.clone();
2939 event.actions.transfer_to_agent = self.target.clone();
2940 event.llm_response.content = Some(Content::new("model").with_text(&self.name));
2941 Ok(Box::pin(stream! { yield Ok(event); }))
2942 }
2943 }
2944
2945 #[tokio::test]
2946 async fn handoff_transfers_control_without_invoking_target_inline() {
2947 let supervisor_runs = Arc::new(AtomicUsize::new(0));
2948 let specialist_runs = Arc::new(AtomicUsize::new(0));
2949 let team = TeamSpec {
2950 name: "handoff_team".to_string(),
2951 description: String::new(),
2952 coordinator: "supervisor".to_string(),
2953 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("specialist")],
2954 relationships: vec![TeamRelationship::new(
2955 "supervisor",
2956 "specialist",
2957 RelationshipKind::Handoff,
2958 )],
2959 policy: TeamPolicy::default(),
2960 }
2961 .compile(vec![
2962 Arc::new(EmittingAgent {
2963 name: "supervisor".to_string(),
2964 target: Some("specialist".to_string()),
2965 runs: supervisor_runs.clone(),
2966 }) as Arc<dyn Agent>,
2967 Arc::new(EmittingAgent {
2968 name: "specialist".to_string(),
2969 target: None,
2970 runs: specialist_runs.clone(),
2971 }),
2972 ])
2973 .unwrap();
2974 let context = Arc::new(TestContext {
2975 content: Content::new("user").with_text("route"),
2976 config: RunConfig::default(),
2977 session: TestSession,
2978 });
2979 let events = team.run(context).await.unwrap().collect::<Vec<_>>().await;
2980 assert_eq!(events.len(), 1);
2981 assert_eq!(
2982 events[0].as_ref().unwrap().actions.transfer_to_agent.as_deref(),
2983 Some("specialist")
2984 );
2985 assert_eq!(supervisor_runs.load(Ordering::SeqCst), 1);
2986 assert_eq!(specialist_runs.load(Ordering::SeqCst), 0);
2987 }
2988
2989 #[tokio::test]
2990 async fn restored_active_handoff_resumes_at_frozen_target() {
2991 let supervisor_runs = Arc::new(AtomicUsize::new(0));
2992 let specialist_runs = Arc::new(AtomicUsize::new(0));
2993 let team = TeamSpec {
2994 name: "resume_team".to_string(),
2995 description: String::new(),
2996 coordinator: "supervisor".to_string(),
2997 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("specialist")],
2998 relationships: vec![TeamRelationship::new(
2999 "supervisor",
3000 "specialist",
3001 RelationshipKind::Handoff,
3002 )],
3003 policy: TeamPolicy::default(),
3004 }
3005 .compile([
3006 Arc::new(EmittingAgent {
3007 name: "supervisor".to_string(),
3008 target: Some("specialist".to_string()),
3009 runs: supervisor_runs.clone(),
3010 }) as Arc<dyn Agent>,
3011 Arc::new(EmittingAgent {
3012 name: "specialist".to_string(),
3013 target: None,
3014 runs: specialist_runs.clone(),
3015 }),
3016 ])
3017 .unwrap();
3018 team.runtime
3019 .start_edge(
3020 "team-invocation",
3021 TeamEdgeStart {
3022 execution_id: Some("persisted-handoff".to_string()),
3023 parent_id: None,
3024 from: "supervisor",
3025 to: "specialist",
3026 kind: RelationshipKind::Handoff,
3027 attempt: 1,
3028 },
3029 )
3030 .unwrap();
3031 let context = Arc::new(TestContext {
3032 content: Content::new("user").with_text("resume"),
3033 config: RunConfig::default(),
3034 session: TestSession,
3035 });
3036 let events = team.run(context).await.unwrap().collect::<Vec<_>>().await;
3037 assert_eq!(events.len(), 1);
3038 assert_eq!(events[0].as_ref().unwrap().author, "specialist");
3039 assert_eq!(supervisor_runs.load(Ordering::SeqCst), 0);
3040 assert_eq!(specialist_runs.load(Ordering::SeqCst), 1);
3041 assert_eq!(
3042 team.execution_snapshot("team-invocation").unwrap().edges[0].status,
3043 TeamExecutionStatus::Completed
3044 );
3045 }
3046
3047 #[tokio::test]
3048 async fn runner_handoff_keeps_one_completed_team_receipt() {
3049 let supervisor_runs = Arc::new(AtomicUsize::new(0));
3050 let specialist_runs = Arc::new(AtomicUsize::new(0));
3051 let team = Arc::new(
3052 TeamSpec {
3053 name: "runner_receipt_team".to_string(),
3054 description: String::new(),
3055 coordinator: "supervisor".to_string(),
3056 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("specialist")],
3057 relationships: vec![TeamRelationship::new(
3058 "supervisor",
3059 "specialist",
3060 RelationshipKind::Handoff,
3061 )],
3062 policy: TeamPolicy::default(),
3063 }
3064 .compile([
3065 Arc::new(EmittingAgent {
3066 name: "supervisor".to_string(),
3067 target: Some("specialist".to_string()),
3068 runs: supervisor_runs.clone(),
3069 }) as Arc<dyn Agent>,
3070 Arc::new(EmittingAgent {
3071 name: "specialist".to_string(),
3072 target: None,
3073 runs: specialist_runs.clone(),
3074 }),
3075 ])
3076 .unwrap(),
3077 );
3078 let sessions = Arc::new(adk_session::InMemorySessionService::new());
3079 adk_session::SessionService::create(
3080 sessions.as_ref(),
3081 adk_session::CreateRequest {
3082 app_name: "runner-receipt".to_string(),
3083 user_id: "user".to_string(),
3084 session_id: Some("session".to_string()),
3085 state: HashMap::new(),
3086 },
3087 )
3088 .await
3089 .unwrap();
3090 let runner = adk_runner::Runner::builder()
3091 .app_name("runner-receipt")
3092 .agent(team.clone() as Arc<dyn Agent>)
3093 .session_service(sessions)
3094 .build()
3095 .unwrap();
3096 let stream = runner
3097 .run_str("user", "session", Content::new("user").with_text("route"))
3098 .await
3099 .unwrap();
3100 let results = stream.collect::<Vec<_>>().await;
3101 assert!(results.into_iter().all(|result| result.is_ok()));
3102 assert_eq!(supervisor_runs.load(Ordering::SeqCst), 1);
3103 assert_eq!(specialist_runs.load(Ordering::SeqCst), 1);
3104 let receipts = team.execution_snapshots();
3105 assert_eq!(receipts.len(), 1);
3106 assert_eq!(receipts[0].edges.len(), 1);
3107 assert_eq!(receipts[0].edges[0].status, TeamExecutionStatus::Completed);
3108 }
3109
3110 #[tokio::test]
3111 async fn exact_allowlist_rejects_undeclared_handoff() {
3112 let team = valid_spec()
3113 .compile(vec![
3114 Arc::new(EmittingAgent {
3115 name: "supervisor".to_string(),
3116 target: Some("undeclared".to_string()),
3117 runs: Arc::new(AtomicUsize::new(0)),
3118 }) as Arc<dyn Agent>,
3119 Arc::new(StubAgent("billing".to_string())),
3120 Arc::new(StubAgent("technical".to_string())),
3121 ])
3122 .unwrap();
3123 let context = Arc::new(TestContext {
3124 content: Content::new("user").with_text("route"),
3125 config: RunConfig::default(),
3126 session: TestSession,
3127 });
3128 let results = team.run(context).await.unwrap().collect::<Vec<_>>().await;
3129 let error = results[0].as_ref().unwrap_err();
3130 assert_eq!(error.code, "agent.team.policy_denied");
3131 assert!(error.to_string().contains("exact edge is not declared"));
3132 }
3133
3134 #[cfg(feature = "team-tools")]
3135 #[tokio::test]
3136 async fn delegate_edge_lowers_to_only_the_declared_agent_tool() {
3137 let spec = TeamSpec {
3138 name: "delegate_team".to_string(),
3139 description: String::new(),
3140 coordinator: "supervisor".to_string(),
3141 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("researcher")],
3142 relationships: vec![TeamRelationship::new(
3143 "supervisor",
3144 "researcher",
3145 RelationshipKind::Delegate,
3146 )],
3147 policy: TeamPolicy::default(),
3148 };
3149 let team = spec
3150 .compile(vec![
3151 Arc::new(StubAgent("supervisor".to_string())) as Arc<dyn Agent>,
3152 Arc::new(StubAgent("researcher".to_string())),
3153 ])
3154 .unwrap();
3155 let mut config = RunConfig::default();
3156 team.configure_run(team.name(), &mut config);
3157 assert!(config.transfer_targets.is_empty());
3158 assert_eq!(config.runtime_toolsets.len(), 1);
3159 let context = Arc::new(TestContext {
3160 content: Content::new("user").with_text("research"),
3161 config: config.clone(),
3162 session: TestSession,
3163 });
3164 let tools = config.runtime_toolsets[0]
3165 .toolset()
3166 .tools(context as Arc<dyn ReadonlyContext>)
3167 .await
3168 .unwrap();
3169 assert_eq!(tools.iter().map(|tool| tool.name()).collect::<Vec<_>>(), vec!["researcher"]);
3170 }
3171
3172 #[cfg(feature = "team-tools")]
3173 struct ScriptedModel {
3174 responses: std::sync::Mutex<std::collections::VecDeque<adk_core::LlmResponse>>,
3175 }
3176
3177 #[cfg(feature = "team-tools")]
3178 impl ScriptedModel {
3179 fn new(responses: Vec<adk_core::LlmResponse>) -> Self {
3180 Self { responses: std::sync::Mutex::new(responses.into()) }
3181 }
3182
3183 fn text(text: &str) -> adk_core::LlmResponse {
3184 adk_core::LlmResponse {
3185 content: Some(Content::new("model").with_text(text)),
3186 usage_metadata: None,
3187 finish_reason: Some(adk_core::FinishReason::Stop),
3188 citation_metadata: None,
3189 partial: false,
3190 turn_complete: true,
3191 interrupted: false,
3192 error_code: None,
3193 error_message: None,
3194 provider_metadata: None,
3195 interaction_id: None,
3196 }
3197 }
3198
3199 fn call(name: &str) -> adk_core::LlmResponse {
3200 adk_core::LlmResponse {
3201 content: Some(Content {
3202 role: "model".to_string(),
3203 parts: vec![adk_core::Part::FunctionCall {
3204 name: name.to_string(),
3205 args: serde_json::json!({"request": "research this"}),
3206 id: Some("delegate-1".to_string()),
3207 thought_signature: None,
3208 }],
3209 }),
3210 ..Self::text("")
3211 }
3212 }
3213
3214 fn transfer(target: &str) -> adk_core::LlmResponse {
3215 adk_core::LlmResponse {
3216 content: Some(Content {
3217 role: "model".to_string(),
3218 parts: vec![adk_core::Part::FunctionCall {
3219 name: "transfer_to_agent".to_string(),
3220 args: serde_json::json!({"agent_name": target}),
3221 id: Some("handoff-1".to_string()),
3222 thought_signature: None,
3223 }],
3224 }),
3225 ..Self::text("")
3226 }
3227 }
3228 }
3229
3230 #[cfg(feature = "team-tools")]
3231 #[async_trait]
3232 impl adk_core::Llm for ScriptedModel {
3233 fn name(&self) -> &str {
3234 "scripted-team-model"
3235 }
3236
3237 async fn generate_content(
3238 &self,
3239 _request: adk_core::LlmRequest,
3240 _stream: bool,
3241 ) -> Result<adk_core::LlmResponseStream> {
3242 let response = self
3243 .responses
3244 .lock()
3245 .expect("script lock")
3246 .pop_front()
3247 .expect("script should have another response");
3248 Ok(Box::pin(stream! { yield Ok(response); }))
3249 }
3250 }
3251
3252 #[cfg(feature = "team-tools")]
3253 #[tokio::test]
3254 async fn delegation_returns_to_the_scripted_supervisor() {
3255 let supervisor = Arc::new(
3256 crate::LlmAgentBuilder::new("supervisor")
3257 .model(Arc::new(ScriptedModel::new(vec![
3258 ScriptedModel::call("researcher"),
3259 ScriptedModel::text("supervisor final"),
3260 ])))
3261 .build()
3262 .unwrap(),
3263 ) as Arc<dyn Agent>;
3264 let researcher = Arc::new(
3265 crate::LlmAgentBuilder::new("researcher")
3266 .model(Arc::new(ScriptedModel::new(vec![ScriptedModel::text("research result")])))
3267 .build()
3268 .unwrap(),
3269 ) as Arc<dyn Agent>;
3270 let team = TeamSpec {
3271 name: "scripted_delegate_team".to_string(),
3272 description: String::new(),
3273 coordinator: "supervisor".to_string(),
3274 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("researcher")],
3275 relationships: vec![TeamRelationship::new(
3276 "supervisor",
3277 "researcher",
3278 RelationshipKind::Delegate,
3279 )],
3280 policy: TeamPolicy::default(),
3281 }
3282 .compile([supervisor, researcher])
3283 .unwrap();
3284 let context = Arc::new(TestContext {
3285 content: Content::new("user").with_text("delegate"),
3286 config: RunConfig::default(),
3287 session: TestSession,
3288 });
3289 let results = team.run(context).await.unwrap().collect::<Vec<_>>().await;
3290 let events = results.into_iter().collect::<Result<Vec<_>>>().unwrap();
3291 assert!(events.iter().any(|event| event.author == "researcher"));
3292 assert!(events.iter().all(|event| event.actions.transfer_to_agent.is_none()));
3293 let active_receipt = events
3294 .iter()
3295 .filter_map(|event| event.actions.state_delta.get(TEAM_EXECUTION_STATE_KEY))
3296 .filter_map(|value| serde_json::from_value::<TeamExecutionSnapshot>(value.clone()).ok())
3297 .find(|snapshot| {
3298 snapshot
3299 .edges
3300 .first()
3301 .is_some_and(|edge| edge.status == TeamExecutionStatus::Running)
3302 })
3303 .expect("delegate start should emit a durable receipt before child execution");
3304 assert_eq!(active_receipt.edges[0].status, TeamExecutionStatus::Running);
3305 assert!(events.iter().any(|event| {
3306 event.author == "supervisor"
3307 && event
3308 .content()
3309 .into_iter()
3310 .flat_map(|content| &content.parts)
3311 .any(|part| part.text() == Some("supervisor final"))
3312 }));
3313 }
3314
3315 #[cfg(feature = "team-tools")]
3316 #[tokio::test]
3317 async fn nested_handoff_inside_delegation_is_consumed_losslessly() {
3318 let supervisor = Arc::new(
3319 crate::LlmAgentBuilder::new("supervisor")
3320 .model(Arc::new(ScriptedModel::new(vec![
3321 ScriptedModel::call("researcher"),
3322 ScriptedModel::text("supervisor resumed"),
3323 ])))
3324 .build()
3325 .unwrap(),
3326 ) as Arc<dyn Agent>;
3327 let researcher = Arc::new(
3328 crate::LlmAgentBuilder::new("researcher")
3329 .model(Arc::new(ScriptedModel::new(vec![ScriptedModel::transfer("writer")])))
3330 .build()
3331 .unwrap(),
3332 ) as Arc<dyn Agent>;
3333 let writer = Arc::new(
3334 crate::LlmAgentBuilder::new("writer")
3335 .model(Arc::new(ScriptedModel::new(vec![ScriptedModel::text("writer result")])))
3336 .build()
3337 .unwrap(),
3338 ) as Arc<dyn Agent>;
3339 let team = TeamSpec {
3340 name: "nested_team".to_string(),
3341 description: String::new(),
3342 coordinator: "supervisor".to_string(),
3343 members: vec![
3344 TeamMemberSpec::new("supervisor"),
3345 TeamMemberSpec::new("researcher"),
3346 TeamMemberSpec::new("writer"),
3347 ],
3348 relationships: vec![
3349 TeamRelationship::new("supervisor", "researcher", RelationshipKind::Delegate),
3350 TeamRelationship::new("researcher", "writer", RelationshipKind::Handoff),
3351 ],
3352 policy: TeamPolicy::default(),
3353 }
3354 .compile([supervisor, researcher, writer])
3355 .unwrap();
3356 let context = Arc::new(TestContext {
3357 content: Content::new("user").with_text("delegate then hand off"),
3358 config: RunConfig::default(),
3359 session: TestSession,
3360 });
3361 let events = team
3362 .run(context)
3363 .await
3364 .unwrap()
3365 .collect::<Vec<_>>()
3366 .await
3367 .into_iter()
3368 .collect::<Result<Vec<_>>>()
3369 .unwrap();
3370 assert!(events.iter().all(|event| event.actions.transfer_to_agent.is_none()));
3371 assert!(events.iter().any(|event| event.author == "writer"));
3372 assert!(events.iter().any(|event| {
3373 event.author == "supervisor"
3374 && event
3375 .content()
3376 .into_iter()
3377 .flat_map(|content| &content.parts)
3378 .any(|part| part.text() == Some("supervisor resumed"))
3379 }));
3380 let snapshot = team.execution_snapshot("team-invocation").unwrap();
3381 assert_eq!(snapshot.usage.delegations, 1);
3382 assert_eq!(snapshot.usage.handoffs, 1);
3383 assert_eq!(snapshot.edges[1].parent_id.as_deref(), Some("delegate-1"));
3384 }
3385
3386 #[cfg(feature = "team-tools")]
3387 #[tokio::test]
3388 async fn delegate_retry_and_exact_fallback_are_enforced() {
3389 let supervisor = Arc::new(
3390 crate::LlmAgentBuilder::new("supervisor")
3391 .model(Arc::new(ScriptedModel::new(vec![
3392 ScriptedModel::call("primary"),
3393 ScriptedModel::text("supervisor final"),
3394 ])))
3395 .build()
3396 .unwrap(),
3397 ) as Arc<dyn Agent>;
3398 let primary_runs = Arc::new(AtomicUsize::new(0));
3399 let backup_runs = Arc::new(AtomicUsize::new(0));
3400 let primary = Arc::new(FlakyAgent {
3401 name: "primary".to_string(),
3402 failures_before_success: usize::MAX,
3403 runs: primary_runs.clone(),
3404 }) as Arc<dyn Agent>;
3405 let backup = Arc::new(FlakyAgent {
3406 name: "backup".to_string(),
3407 failures_before_success: 0,
3408 runs: backup_runs.clone(),
3409 }) as Arc<dyn Agent>;
3410 let primary_edge =
3411 TeamRelationship::new("supervisor", "primary", RelationshipKind::Delegate).with_policy(
3412 RelationshipPolicy {
3413 failure: RelationshipFailureStrategy::RetryThenFallback {
3414 max_attempts: 2,
3415 backoff_ms: 0,
3416 target: "backup".to_string(),
3417 },
3418 ..RelationshipPolicy::default()
3419 },
3420 );
3421 let team = TeamSpec {
3422 name: "failure_team".to_string(),
3423 description: String::new(),
3424 coordinator: "supervisor".to_string(),
3425 members: vec![
3426 TeamMemberSpec::new("supervisor"),
3427 TeamMemberSpec::new("primary"),
3428 TeamMemberSpec::new("backup"),
3429 ],
3430 relationships: vec![
3431 primary_edge,
3432 TeamRelationship::new("supervisor", "backup", RelationshipKind::Delegate),
3433 ],
3434 policy: TeamPolicy::default(),
3435 }
3436 .compile([supervisor, primary, backup])
3437 .unwrap();
3438 let context = Arc::new(TestContext {
3439 content: Content::new("user").with_text("use a fallback"),
3440 config: RunConfig::default(),
3441 session: TestSession,
3442 });
3443 let events = team
3444 .run(context)
3445 .await
3446 .unwrap()
3447 .collect::<Vec<_>>()
3448 .await
3449 .into_iter()
3450 .collect::<Result<Vec<_>>>()
3451 .unwrap();
3452 assert_eq!(primary_runs.load(Ordering::SeqCst), 2);
3453 assert_eq!(backup_runs.load(Ordering::SeqCst), 1);
3454 assert!(events.iter().any(|event| event.author == "backup"));
3455 assert!(events.iter().any(|event| event.author == "supervisor"));
3456 }
3457
3458 #[cfg(feature = "team-tools")]
3459 #[tokio::test]
3460 async fn relationship_approval_interrupts_before_delegate_execution() {
3461 let supervisor = Arc::new(
3462 crate::LlmAgentBuilder::new("supervisor")
3463 .model(Arc::new(ScriptedModel::new(vec![ScriptedModel::call("researcher")])))
3464 .build()
3465 .unwrap(),
3466 ) as Arc<dyn Agent>;
3467 let researcher_runs = Arc::new(AtomicUsize::new(0));
3468 let researcher = Arc::new(FlakyAgent {
3469 name: "researcher".to_string(),
3470 failures_before_success: 0,
3471 runs: researcher_runs.clone(),
3472 }) as Arc<dyn Agent>;
3473 let team = TeamSpec {
3474 name: "approval_team".to_string(),
3475 description: String::new(),
3476 coordinator: "supervisor".to_string(),
3477 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("researcher")],
3478 relationships: vec![
3479 TeamRelationship::new("supervisor", "researcher", RelationshipKind::Delegate)
3480 .with_policy(RelationshipPolicy {
3481 approval: RelationshipApprovalPolicy::Required,
3482 ..RelationshipPolicy::default()
3483 }),
3484 ],
3485 policy: TeamPolicy::default(),
3486 }
3487 .compile([supervisor, researcher])
3488 .unwrap();
3489 let context = Arc::new(TestContext {
3490 content: Content::new("user").with_text("approval required"),
3491 config: RunConfig::default(),
3492 session: TestSession,
3493 });
3494 let events = team
3495 .run(context)
3496 .await
3497 .unwrap()
3498 .collect::<Vec<_>>()
3499 .await
3500 .into_iter()
3501 .collect::<Result<Vec<_>>>()
3502 .unwrap();
3503 assert!(events.iter().any(|event| event.actions.tool_confirmation.is_some()));
3504 assert_eq!(researcher_runs.load(Ordering::SeqCst), 0);
3505 }
3506
3507 #[cfg(feature = "team-tools")]
3508 #[tokio::test]
3509 async fn output_contract_failure_can_return_to_supervisor_as_data() {
3510 let supervisor = Arc::new(
3511 crate::LlmAgentBuilder::new("supervisor")
3512 .model(Arc::new(ScriptedModel::new(vec![
3513 ScriptedModel::call("researcher"),
3514 ScriptedModel::text("handled contract failure"),
3515 ])))
3516 .build()
3517 .unwrap(),
3518 ) as Arc<dyn Agent>;
3519 let researcher_runs = Arc::new(AtomicUsize::new(0));
3520 let researcher = Arc::new(FlakyAgent {
3521 name: "researcher".to_string(),
3522 failures_before_success: 0,
3523 runs: researcher_runs.clone(),
3524 }) as Arc<dyn Agent>;
3525 let team = TeamSpec {
3526 name: "contract_team".to_string(),
3527 description: String::new(),
3528 coordinator: "supervisor".to_string(),
3529 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("researcher")],
3530 relationships: vec![
3531 TeamRelationship::new("supervisor", "researcher", RelationshipKind::Delegate)
3532 .with_policy(RelationshipPolicy {
3533 output_schema: Some(serde_json::json!({
3534 "type": "object",
3535 "properties": { "score": { "type": "number" } },
3536 "required": ["score"]
3537 })),
3538 failure: RelationshipFailureStrategy::ReturnError,
3539 ..RelationshipPolicy::default()
3540 }),
3541 ],
3542 policy: TeamPolicy::default(),
3543 }
3544 .compile([supervisor, researcher])
3545 .unwrap();
3546 let context = Arc::new(TestContext {
3547 content: Content::new("user").with_text("validate output"),
3548 config: RunConfig::default(),
3549 session: TestSession,
3550 });
3551 let events = team
3552 .run(context)
3553 .await
3554 .unwrap()
3555 .collect::<Vec<_>>()
3556 .await
3557 .into_iter()
3558 .collect::<Result<Vec<_>>>()
3559 .unwrap();
3560 assert_eq!(researcher_runs.load(Ordering::SeqCst), 1);
3561 assert!(events.iter().any(|event| {
3562 event.author == "supervisor"
3563 && event
3564 .content()
3565 .into_iter()
3566 .flat_map(|content| &content.parts)
3567 .any(|part| part.text() == Some("handled contract failure"))
3568 }));
3569 }
3570
3571 struct RecordingLifecycleHook {
3572 calls: Arc<std::sync::Mutex<Vec<String>>>,
3573 }
3574
3575 #[async_trait]
3576 impl TeamLifecycleHook for RecordingLifecycleHook {
3577 fn name(&self) -> &str {
3578 "recording"
3579 }
3580
3581 async fn before(&self, context: &TeamLifecycleContext) -> Result<TeamLifecycleDecision> {
3582 self.calls
3583 .lock()
3584 .expect("lifecycle calls lock")
3585 .push(format!("before:{:?}", context.phase));
3586 Ok(TeamLifecycleDecision::Continue)
3587 }
3588
3589 async fn after(
3590 &self,
3591 context: &TeamLifecycleContext,
3592 outcome: &TeamLifecycleOutcome,
3593 ) -> Result<()> {
3594 self.calls
3595 .lock()
3596 .expect("lifecycle calls lock")
3597 .push(format!("after:{:?}:{outcome:?}", context.phase));
3598 Ok(())
3599 }
3600 }
3601
3602 #[tokio::test]
3603 async fn invokes_team_and_member_lifecycle_hooks_around_execution() {
3604 let calls = Arc::new(std::sync::Mutex::new(Vec::new()));
3605 let team = valid_spec()
3606 .compile_with_hooks(
3607 [
3608 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
3609 Arc::new(StubAgent("billing".into())),
3610 Arc::new(StubAgent("technical".into())),
3611 ],
3612 [Arc::new(RecordingLifecycleHook { calls: calls.clone() })
3613 as Arc<dyn TeamLifecycleHook>],
3614 )
3615 .unwrap();
3616 let context = Arc::new(TestContext {
3617 content: Content::new("user").with_text("run hooks"),
3618 config: RunConfig::default(),
3619 session: TestSession,
3620 });
3621 team.run(context)
3622 .await
3623 .unwrap()
3624 .collect::<Vec<_>>()
3625 .await
3626 .into_iter()
3627 .collect::<Result<Vec<_>>>()
3628 .unwrap();
3629 let calls = calls.lock().expect("lifecycle calls lock");
3630 assert_eq!(calls.first().map(String::as_str), Some("before:Team"));
3631 assert!(calls.iter().any(|call| call == "before:Member"));
3632 assert!(calls.iter().any(|call| call.starts_with("after:Member:Succeeded")));
3633 assert!(calls.iter().any(|call| call.starts_with("after:Team:Succeeded")));
3634 }
3635
3636 struct DenyRelationshipHook;
3637
3638 #[async_trait]
3639 impl TeamLifecycleHook for DenyRelationshipHook {
3640 fn name(&self) -> &str {
3641 "deny-relationship"
3642 }
3643
3644 async fn before(&self, context: &TeamLifecycleContext) -> Result<TeamLifecycleDecision> {
3645 if context.phase == TeamLifecyclePhase::Relationship {
3646 Ok(TeamLifecycleDecision::Terminate {
3647 reason: "operator policy denied transition".to_string(),
3648 })
3649 } else {
3650 Ok(TeamLifecycleDecision::Continue)
3651 }
3652 }
3653 }
3654
3655 #[tokio::test]
3656 async fn relationship_hook_denies_handoff_before_target_runs() {
3657 let supervisor_runs = Arc::new(AtomicUsize::new(0));
3658 let specialist_runs = Arc::new(AtomicUsize::new(0));
3659 let spec = TeamSpec {
3660 name: "governed_handoff".to_string(),
3661 description: String::new(),
3662 coordinator: "supervisor".to_string(),
3663 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("specialist")],
3664 relationships: vec![TeamRelationship::new(
3665 "supervisor",
3666 "specialist",
3667 RelationshipKind::Handoff,
3668 )],
3669 policy: TeamPolicy::default(),
3670 };
3671 let team = spec
3672 .compile_with_hooks(
3673 [
3674 Arc::new(EmittingAgent {
3675 name: "supervisor".to_string(),
3676 target: Some("specialist".to_string()),
3677 runs: supervisor_runs.clone(),
3678 }) as Arc<dyn Agent>,
3679 Arc::new(EmittingAgent {
3680 name: "specialist".to_string(),
3681 target: None,
3682 runs: specialist_runs.clone(),
3683 }),
3684 ],
3685 [Arc::new(DenyRelationshipHook) as Arc<dyn TeamLifecycleHook>],
3686 )
3687 .unwrap();
3688 let context = Arc::new(TestContext {
3689 content: Content::new("user").with_text("route"),
3690 config: RunConfig::default(),
3691 session: TestSession,
3692 });
3693 let error = team
3694 .run(context)
3695 .await
3696 .unwrap()
3697 .collect::<Vec<_>>()
3698 .await
3699 .into_iter()
3700 .find_map(std::result::Result::err)
3701 .expect("relationship hook should deny handoff");
3702 assert_eq!(error.code, "agent.team.policy_denied");
3703 assert_eq!(supervisor_runs.load(Ordering::SeqCst), 1);
3704 assert_eq!(specialist_runs.load(Ordering::SeqCst), 0);
3705 }
3706
3707 #[cfg(feature = "team-tools")]
3708 struct LegacyRuntimeToolBlindAgent;
3709
3710 #[cfg(feature = "team-tools")]
3711 #[async_trait]
3712 impl Agent for LegacyRuntimeToolBlindAgent {
3713 fn name(&self) -> &str {
3714 "supervisor"
3715 }
3716
3717 fn description(&self) -> &str {
3718 "does not consume runtime tools"
3719 }
3720
3721 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
3722 &[]
3723 }
3724
3725 async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
3726 Ok(Box::pin(futures::stream::empty()))
3727 }
3728 }
3729
3730 #[cfg(feature = "team-tools")]
3731 #[test]
3732 fn compile_rejects_delegate_source_without_runtime_tool_capability() {
3733 let spec = TeamSpec {
3734 name: "capability_team".to_string(),
3735 description: String::new(),
3736 coordinator: "supervisor".to_string(),
3737 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("worker")],
3738 relationships: vec![TeamRelationship::new(
3739 "supervisor",
3740 "worker",
3741 RelationshipKind::Delegate,
3742 )],
3743 policy: TeamPolicy::default(),
3744 };
3745 assert!(matches!(
3746 spec.compile([
3747 Arc::new(LegacyRuntimeToolBlindAgent) as Arc<dyn Agent>,
3748 Arc::new(StubAgent("worker".into())),
3749 ]),
3750 Err(TeamError::UnsupportedAgentCapability { capability: "runtimeTools", .. })
3751 ));
3752 }
3753
3754 #[test]
3755 fn validates_replay_and_reports_topology_coverage() {
3756 let team = valid_spec()
3757 .compile([
3758 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
3759 Arc::new(StubAgent("billing".into())),
3760 Arc::new(StubAgent("technical".into())),
3761 ])
3762 .unwrap();
3763 team.runtime
3764 .start_edge(
3765 "evaluation",
3766 TeamEdgeStart {
3767 execution_id: Some("edge-1".to_string()),
3768 parent_id: None,
3769 from: "supervisor",
3770 to: "billing",
3771 kind: RelationshipKind::Handoff,
3772 attempt: 1,
3773 },
3774 )
3775 .unwrap();
3776 team.runtime.finish_edge("evaluation", "edge-1", None);
3777 let analysis = team.analyze_execution("evaluation").unwrap().unwrap();
3778 assert_eq!(analysis.declared_relationships, 2);
3779 assert_eq!(analysis.covered_relationships, 1);
3780 assert_eq!(analysis.coverage_basis_points, 5_000);
3781 assert_eq!(analysis.uncovered, ["supervisor -Handoff-> technical"]);
3782 }
3783
3784 #[cfg(feature = "team-tools")]
3785 struct CheckpointAgent;
3786
3787 #[cfg(feature = "team-tools")]
3788 #[async_trait]
3789 impl Agent for CheckpointAgent {
3790 fn name(&self) -> &str {
3791 "worker"
3792 }
3793
3794 fn description(&self) -> &str {
3795 "checkpoint-aware worker"
3796 }
3797
3798 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
3799 &[]
3800 }
3801
3802 fn capabilities(&self) -> adk_core::AgentCapabilities {
3803 adk_core::AgentCapabilities {
3804 runtime_tools: false,
3805 handoff: true,
3806 relationship_confirmation: false,
3807 checkpoint_resume: true,
3808 shared_state: true,
3809 invocation_metadata: true,
3810 }
3811 }
3812
3813 async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
3814 Ok(Box::pin(futures::stream::empty()))
3815 }
3816 }
3817
3818 #[cfg(feature = "team-tools")]
3819 #[test]
3820 fn exposes_checkpoint_resume_plan_without_replaying_delegate() {
3821 let spec = TeamSpec {
3822 name: "durable_team".to_string(),
3823 description: String::new(),
3824 coordinator: "supervisor".to_string(),
3825 members: vec![TeamMemberSpec::new("supervisor"), TeamMemberSpec::new("worker")],
3826 relationships: vec![
3827 TeamRelationship::new("supervisor", "worker", RelationshipKind::Delegate)
3828 .with_policy(RelationshipPolicy {
3829 resume: TeamResumePolicy::RequireCheckpoint {
3830 token_state_key: "temp:worker_checkpoint".to_string(),
3831 },
3832 ..RelationshipPolicy::default()
3833 }),
3834 ],
3835 policy: TeamPolicy::default(),
3836 };
3837 let team = spec
3838 .compile([
3839 Arc::new(StubAgent("supervisor".into())) as Arc<dyn Agent>,
3840 Arc::new(CheckpointAgent),
3841 ])
3842 .unwrap();
3843 team.runtime
3844 .start_edge(
3845 "durable-run",
3846 TeamEdgeStart {
3847 execution_id: Some("delegate-edge".to_string()),
3848 parent_id: None,
3849 from: "supervisor",
3850 to: "worker",
3851 kind: RelationshipKind::Delegate,
3852 attempt: 1,
3853 },
3854 )
3855 .unwrap();
3856 assert_eq!(
3857 team.resume_plan("durable-run"),
3858 Some(TeamResumePlan::DelegateCheckpoint {
3859 edge_id: "delegate-edge".to_string(),
3860 target: "worker".to_string(),
3861 token_state_key: "temp:worker_checkpoint".to_string(),
3862 })
3863 );
3864 }
3865}