Skip to main content

adk_agent/
team.rs

1//! Portable, validated multi-agent team composition.
2//!
3//! A [`TeamSpec`] is data: it names members, identifies the coordinator, and
4//! declares exact directed relationships. Compilation binds those names to any
5//! existing [`Agent`] implementations and produces a [`CompiledTeam`] root that
6//! can be passed directly to a Runner.
7
8use 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/// A portable team definition independent of concrete model or runtime types.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
58#[serde(rename_all = "camelCase", deny_unknown_fields)]
59pub struct TeamSpec {
60    /// Stable name of the executable team root.
61    pub name: String,
62    /// Human-readable purpose of the team.
63    #[serde(default, skip_serializing_if = "String::is_empty")]
64    pub description: String,
65    /// Member that handles the initial request.
66    pub coordinator: String,
67    /// Named members expected at compilation time.
68    pub members: Vec<TeamMemberSpec>,
69    /// Exact directed handoff and delegation relationships.
70    #[serde(default, skip_serializing_if = "Vec::is_empty")]
71    pub relationships: Vec<TeamRelationship>,
72    /// Runtime policies enforced by the compiled team.
73    #[serde(default)]
74    pub policy: TeamPolicy,
75}
76
77/// Serializable metadata for one team member.
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
79#[serde(rename_all = "camelCase", deny_unknown_fields)]
80pub struct TeamMemberSpec {
81    /// Name used to bind the member to an [`Agent`].
82    pub name: String,
83    /// Optional portable description for design tools and documentation.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub description: Option<String>,
86    /// Capabilities a discovered binding must provide.
87    #[serde(default, skip_serializing_if = "Vec::is_empty")]
88    pub required_capabilities: Vec<String>,
89    /// Registry trust, version, and integrity constraints for this member.
90    #[serde(default)]
91    pub registry: TeamRegistryRequirement,
92}
93
94impl TeamMemberSpec {
95    /// Creates member metadata for `name`.
96    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    /// Adds a portable description.
106    pub fn with_description(mut self, description: impl Into<String>) -> Self {
107        self.description = Some(description.into());
108        self
109    }
110
111    /// Requires all named capabilities when resolving this member dynamically.
112    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    /// Applies discovery-time trust and integrity constraints.
121    pub fn with_registry_requirement(mut self, requirement: TeamRegistryRequirement) -> Self {
122        self.registry = requirement;
123        self
124    }
125}
126
127/// Portable governance constraints for dynamically discovered bindings.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
129#[serde(rename_all = "camelCase", deny_unknown_fields)]
130pub struct TeamRegistryRequirement {
131    /// Exact semantic or provider version required from the descriptor.
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub version: Option<String>,
134    /// Exact immutable content/configuration digest required from the descriptor.
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub digest: Option<String>,
137    /// At least one of these trust labels must be advertised.
138    #[serde(default, skip_serializing_if = "Vec::is_empty")]
139    pub trust_labels: Vec<String>,
140    /// Reject degraded candidates as well as unavailable ones.
141    #[serde(default)]
142    pub require_healthy: bool,
143}
144
145/// One exact directed relationship between two team members.
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148pub struct TeamRelationship {
149    /// Calling or transferring member.
150    pub from: String,
151    /// Target member.
152    pub to: String,
153    /// Control-flow semantics of this edge.
154    pub kind: RelationshipKind,
155    /// Input, output, safety, and failure contract for this exact edge.
156    #[serde(default)]
157    pub policy: RelationshipPolicy,
158}
159
160impl TeamRelationship {
161    /// Creates a directed relationship.
162    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    /// Applies a contract to this relationship.
167    pub fn with_policy(mut self, policy: RelationshipPolicy) -> Self {
168        self.policy = policy;
169        self
170    }
171}
172
173/// Contract enforced for one exact relationship.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
175#[serde(rename_all = "camelCase", deny_unknown_fields)]
176pub struct RelationshipPolicy {
177    /// Optional JSON Schema for arguments supplied to the relationship tool.
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub input_schema: Option<serde_json::Value>,
180    /// Optional JSON Schema validated against a delegate's returned value.
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub output_schema: Option<serde_json::Value>,
183    /// Override the team-wide context policy for this edge.
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub context: Option<TeamContextPolicy>,
186    /// History projection applied to delegated child sessions.
187    #[serde(default)]
188    pub history: TeamHistoryPolicy,
189    /// Exact state keys copied to delegated child sessions. Empty means all
190    /// keys allowed by the selected context policy.
191    #[serde(default, skip_serializing_if = "Vec::is_empty")]
192    pub state_keys: Vec<String>,
193    /// Exact state keys a delegated member may write back. Empty allows all keys.
194    #[serde(default, skip_serializing_if = "Vec::is_empty")]
195    pub state_write_keys: Vec<String>,
196    /// Merge behavior for delegated state writes.
197    #[serde(default)]
198    pub state_merge: TeamStateMergePolicy,
199    /// Allowed artifact-name prefixes written by a delegate. Empty allows all names.
200    #[serde(default, skip_serializing_if = "Vec::is_empty")]
201    pub artifact_prefixes: Vec<String>,
202    /// Durable recovery contract for an interrupted delegation.
203    #[serde(default)]
204    pub resume: TeamResumePolicy,
205    /// Per-attempt timeout in milliseconds.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub timeout_ms: Option<u64>,
208    /// Whether invoking this edge requires a tool confirmation decision.
209    #[serde(default)]
210    pub approval: RelationshipApprovalPolicy,
211    /// Failure handling for this edge.
212    #[serde(default)]
213    pub failure: RelationshipFailureStrategy,
214    /// Optional circuit breaker for repeatedly failing delegate calls.
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub circuit_breaker: Option<CircuitBreakerPolicy>,
217}
218
219/// Conversation history exposed to a delegated member.
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
221#[serde(rename_all = "camelCase", tag = "mode")]
222pub enum TeamHistoryPolicy {
223    /// Derive history behavior from the selected context policy.
224    #[default]
225    Inherit,
226    /// Do not copy conversation history.
227    None,
228    /// Copy the full available history.
229    Full,
230    /// Copy at most the most recent `max_events` messages.
231    Last {
232        /// Maximum number of messages copied.
233        max_events: usize,
234    },
235}
236
237/// Approval requirement for a relationship invocation.
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
239#[serde(rename_all = "camelCase")]
240pub enum RelationshipApprovalPolicy {
241    /// Invoke without a relationship-specific approval gate.
242    #[default]
243    Never,
244    /// Reuse the runtime's exact-call tool confirmation mechanism.
245    Required,
246}
247
248/// Failure handling for an exact relationship.
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
250#[serde(rename_all = "camelCase", tag = "strategy")]
251pub enum RelationshipFailureStrategy {
252    /// Use the team-wide failure policy.
253    #[default]
254    Inherit,
255    /// Propagate the member failure.
256    Propagate,
257    /// Return a structured error to the delegating member.
258    ReturnError,
259    /// Retry the same target with bounded attempts and backoff.
260    Retry {
261        /// Total attempts, including the initial attempt.
262        max_attempts: u32,
263        /// Delay between attempts in milliseconds.
264        #[serde(default)]
265        backoff_ms: u64,
266    },
267    /// Transfer the failed operation to another explicitly declared target.
268    Fallback {
269        /// Exact fallback member name.
270        target: String,
271    },
272    /// Retry first, then use an explicitly declared fallback target.
273    RetryThenFallback {
274        /// Total attempts against the primary target.
275        max_attempts: u32,
276        /// Delay between attempts in milliseconds.
277        #[serde(default)]
278        backoff_ms: u64,
279        /// Exact fallback member name.
280        target: String,
281    },
282}
283
284/// Opens an edge circuit after repeated failures.
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
286#[serde(rename_all = "camelCase", deny_unknown_fields)]
287pub struct CircuitBreakerPolicy {
288    /// Consecutive failures required to open the circuit.
289    pub failure_threshold: u32,
290    /// Cooldown before a half-open probe, in milliseconds.
291    pub reset_after_ms: u64,
292}
293
294/// Control-flow semantics for a team relationship.
295#[derive(
296    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
297)]
298#[serde(rename_all = "camelCase")]
299pub enum RelationshipKind {
300    /// Invoke the target as a tool, receive its result, and resume the caller.
301    Delegate,
302    /// Transfer active control to the target; the caller does not resume.
303    Handoff,
304}
305
306/// Policies applied uniformly by a compiled team.
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
308#[serde(rename_all = "camelCase", deny_unknown_fields)]
309pub struct TeamPolicy {
310    /// Maximum handoffs in one Runner invocation.
311    pub max_transfer_depth: u32,
312    /// Maximum nested agent-as-tool calls.
313    pub max_delegation_depth: u32,
314    /// Maximum simultaneous delegate calls from a member.
315    pub max_concurrent_delegations: usize,
316    /// Context exposed to delegated members.
317    pub context: TeamContextPolicy,
318    /// How member failures surface to callers.
319    pub failure: TeamFailurePolicy,
320    /// Aggregate resource limits for one root invocation.
321    #[serde(default)]
322    pub budget: TeamBudget,
323    /// Conditions that terminate a running team cleanly.
324    #[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/// Aggregate limits enforced across a root team invocation and its delegates.
343#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
344#[serde(rename_all = "camelCase", deny_unknown_fields)]
345pub struct TeamBudget {
346    /// Maximum emitted events.
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub max_events: Option<u64>,
349    /// Maximum model responses carrying usage metadata.
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub max_model_requests: Option<u64>,
352    /// Maximum tool calls observed in model events.
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub max_tool_calls: Option<u64>,
355    /// Maximum total input and output tokens.
356    #[serde(default, skip_serializing_if = "Option::is_none")]
357    pub max_tokens: Option<u64>,
358    /// Maximum estimated cost in millionths of a US dollar.
359    #[serde(default, skip_serializing_if = "Option::is_none")]
360    pub max_cost_microusd: Option<u64>,
361    /// Maximum delegation relationship executions.
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub max_delegations: Option<u64>,
364    /// Maximum handoff relationship executions.
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub max_handoffs: Option<u64>,
367    /// Maximum wall-clock duration in milliseconds.
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub max_wall_time_ms: Option<u64>,
370}
371
372/// Clean termination conditions independent from hard resource budgets.
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
374#[serde(rename_all = "camelCase", deny_unknown_fields)]
375pub struct TeamTerminationPolicy {
376    /// End when an event requests human escalation.
377    pub stop_on_escalation: bool,
378    /// End after a final response authored by one of these members.
379    #[serde(default, skip_serializing_if = "Vec::is_empty")]
380    pub final_authors: Vec<String>,
381    /// End when emitted text contains one of these exact markers.
382    #[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/// Context sharing policy for agent-as-tool delegation.
393#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
394#[serde(rename_all = "camelCase")]
395pub enum TeamContextPolicy {
396    /// Start delegates with an empty history/state snapshot and no memory.
397    Isolated,
398    /// Snapshot parent history/state and forward memory, artifacts, and shared state.
399    Shared,
400}
401
402/// Merge policy for state returned by an exact delegation edge.
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
404#[serde(rename_all = "camelCase")]
405pub enum TeamStateMergePolicy {
406    /// Reject writes whose parent value changed after delegation began.
407    #[default]
408    RejectConflicts,
409    /// Apply child writes with last-writer-wins semantics.
410    Overwrite,
411}
412
413/// Recovery contract for a delegation that was running when execution stopped.
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
415#[serde(rename_all = "camelCase", tag = "strategy")]
416pub enum TeamResumePolicy {
417    /// Never replay an unresolved delegation automatically.
418    #[default]
419    FailClosed,
420    /// Require the target to advertise checkpoint resume and let the durable
421    /// host provide a continuation token stored under this state key.
422    RequireCheckpoint {
423        /// Session-state key holding the opaque target continuation token.
424        token_state_key: String,
425    },
426}
427
428/// Safe action a durable host can take for a restored execution receipt.
429#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
430#[serde(rename_all = "camelCase", tag = "action")]
431pub enum TeamResumePlan {
432    /// No relationship was active; start or continue with the coordinator.
433    Coordinator,
434    /// Resume control at an active handoff target.
435    Handoff {
436        /// Active relationship execution.
437        edge_id: String,
438        /// Member that owns control.
439        target: String,
440    },
441    /// Resume an unresolved delegate through its checkpoint-aware host adapter.
442    DelegateCheckpoint {
443        /// Active relationship execution.
444        edge_id: String,
445        /// Delegated member.
446        target: String,
447        /// State key containing the opaque continuation token.
448        token_state_key: String,
449    },
450    /// The active delegation cannot be replayed safely.
451    UnsafeDelegate {
452        /// Active relationship execution.
453        edge_id: String,
454        /// Delegated member.
455        target: String,
456        /// Explanation suitable for an operator or durable scheduler.
457        reason: String,
458    },
459}
460
461/// Failure behavior for team relationships.
462#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
463#[serde(rename_all = "camelCase")]
464pub enum TeamFailurePolicy {
465    /// Surface member and timeout failures as failed tool or agent executions.
466    Propagate,
467    /// Return delegate failures as structured tool results; handoff failures still propagate.
468    ReturnDelegateError,
469}
470
471/// Validation or compilation failure for a [`TeamSpec`].
472#[derive(Debug, Error, PartialEq, Eq)]
473pub enum TeamError {
474    /// A required name is empty.
475    #[error("team {field} must not be empty")]
476    EmptyName {
477        /// Name-bearing field that failed validation.
478        field: &'static str,
479    },
480    /// A member name occurs more than once.
481    #[error("duplicate team member '{0}'")]
482    DuplicateMember(String),
483    /// The team root name collides with a member name.
484    #[error("team name '{0}' must not also be a member name")]
485    TeamNameCollision(String),
486    /// The coordinator is not declared as a member.
487    #[error("team coordinator '{0}' is not a declared member")]
488    UnknownCoordinator(String),
489    /// A relationship references a missing member.
490    #[error("relationship {endpoint} references unknown member '{name}'")]
491    UnknownRelationshipMember {
492        /// Whether the source or target was invalid.
493        endpoint: &'static str,
494        /// Invalid member name.
495        name: String,
496    },
497    /// A member points to itself.
498    #[error("self relationship is not allowed for member '{0}'")]
499    SelfRelationship(String),
500    /// The same semantic edge occurs more than once.
501    #[error("duplicate {kind:?} relationship from '{from}' to '{to}'")]
502    DuplicateRelationship {
503        /// Source member.
504        from: String,
505        /// Target member.
506        to: String,
507        /// Repeated relationship kind.
508        kind: RelationshipKind,
509    },
510    /// The directed topology contains a cycle.
511    #[error("team topology contains a cycle involving '{0}'")]
512    CyclicTopology(String),
513    /// A member cannot be reached from the coordinator.
514    #[error("team member '{0}' is unreachable from the coordinator")]
515    UnreachableMember(String),
516    /// A depth or concurrency policy is zero.
517    #[error("team policy '{0}' must be greater than zero")]
518    InvalidPolicy(&'static str),
519    /// A member capability requirement is empty or repeated.
520    #[error("invalid capability requirement for member '{member}': {reason}")]
521    InvalidMemberCapability {
522        /// Member carrying the invalid requirement.
523        member: String,
524        /// Actionable validation failure.
525        reason: String,
526    },
527    /// An exact relationship contract is invalid.
528    #[error("invalid relationship policy from '{from}' to '{to}': {reason}")]
529    InvalidRelationshipPolicy {
530        /// Relationship source.
531        from: String,
532        /// Relationship target.
533        to: String,
534        /// Actionable validation failure.
535        reason: String,
536    },
537    /// A team budget contains a zero bound.
538    #[error("team budget '{0}' must be greater than zero when configured")]
539    InvalidBudget(&'static str),
540    /// A termination rule names an undeclared member.
541    #[error("team termination final author '{0}' is not a declared member")]
542    UnknownTerminationAuthor(String),
543    /// No concrete agent was registered for a member.
544    #[error("no agent registered for team member '{0}'")]
545    MissingAgent(String),
546    /// More than one concrete agent has the same name.
547    #[error("duplicate agent binding for team member '{0}'")]
548    DuplicateAgentBinding(String),
549    /// A concrete agent was supplied but is not declared by the specification.
550    #[error("agent binding '{0}' is not declared by the team")]
551    UnexpectedAgentBinding(String),
552    /// A bound source agent cannot enforce a declared relationship.
553    #[error(
554        "agent '{member}' does not support required runtime capability '{capability}' for {relationship}"
555    )]
556    UnsupportedAgentCapability {
557        /// Portable member name.
558        member: String,
559        /// Missing execution-plane capability.
560        capability: &'static str,
561        /// Relationship that requires the capability.
562        relationship: String,
563    },
564    /// Delegation support was not compiled into `adk-agent`.
565    #[error("TeamSpec contains Delegate relationships; enable the adk-agent 'team-tools' feature")]
566    DelegationFeatureDisabled,
567    /// A dynamic registry operation failed.
568    #[error("team registry failed: {0}")]
569    Registry(String),
570    /// No registry candidate satisfies a portable member requirement.
571    #[error(
572        "no registry candidate for team member '{member}' with required capabilities {capabilities:?}"
573    )]
574    NoRegistryCandidate {
575        /// Portable member name.
576        member: String,
577        /// Required capability identifiers.
578        capabilities: Vec<String>,
579    },
580    /// A persisted execution receipt cannot be restored into this compiled team.
581    #[error("incompatible team execution snapshot: {0}")]
582    IncompatibleExecutionSnapshot(String),
583}
584
585/// Structured failure produced while a compiled team is executing.
586#[derive(Debug, Error)]
587pub enum TeamRuntimeError {
588    /// A lifecycle hook failed.
589    #[error("team lifecycle hook '{hook}' failed during {phase:?}: {message}")]
590    LifecycleHook {
591        /// Hook name.
592        hook: String,
593        /// Lifecycle boundary.
594        phase: TeamLifecyclePhase,
595        /// Hook failure.
596        message: String,
597    },
598    /// A lifecycle policy denied an operation.
599    #[error("team policy denied {operation}: {reason}")]
600    PolicyDenied {
601        /// Operation being denied.
602        operation: String,
603        /// Auditable policy reason.
604        reason: String,
605    },
606    /// A configured aggregate budget was exhausted.
607    #[error("team budget exceeded: {0}")]
608    BudgetExceeded(String),
609    /// A persisted delegation cannot be replayed safely.
610    #[error("unsafe team resume: {0}")]
611    UnsafeResume(String),
612    /// Delegated state violated its write/merge contract.
613    #[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    /// Validates names, policies, edges, acyclicity, and coordinator reachability.
687    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    /// Binds this spec to concrete agents and returns an executable team root.
839    ///
840    /// Any [`Agent`] implementation can be a member. Delegate edges additionally
841    /// require the `team-tools` feature, which lowers them to `AgentTool`s.
842    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    /// Binds concrete agents and ordered async team lifecycle hooks.
855    ///
856    /// Generic Runner plugins continue to apply normally. These hooks add
857    /// policy and observation at exact member and relationship boundaries.
858    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
1180/// Executable root produced by [`TeamSpec::compile`].
1181pub 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    /// Returns the portable source specification used to compile this root.
1206    pub fn spec(&self) -> &TeamSpec {
1207        &self.spec
1208    }
1209
1210    /// Returns the validated portable source specification's coordinator name.
1211    pub fn coordinator_name(&self) -> &str {
1212        &self.coordinator_name
1213    }
1214
1215    /// Returns the exact handoff targets for a member.
1216    pub fn handoff_targets(&self, member: &str) -> Option<&[String]> {
1217        self.handoffs.get(member).map(Vec::as_slice)
1218    }
1219
1220    /// Returns the latest serializable execution receipt for a root invocation.
1221    pub fn execution_snapshot(&self, invocation_id: &str) -> Option<TeamExecutionSnapshot> {
1222        self.runtime.snapshot(invocation_id)
1223    }
1224
1225    /// Returns all execution receipts currently retained by this compiled team.
1226    pub fn execution_snapshots(&self) -> Vec<TeamExecutionSnapshot> {
1227        self.runtime.snapshots()
1228    }
1229
1230    /// Restores a persisted execution receipt before resuming with the same
1231    /// root invocation identifier.
1232    ///
1233    /// The team name and frozen roster must exactly match this compiled team,
1234    /// preventing a resumed execution from silently selecting different agents.
1235    pub fn restore_execution_snapshot(
1236        &self,
1237        snapshot: TeamExecutionSnapshot,
1238    ) -> std::result::Result<(), TeamError> {
1239        self.runtime.restore(snapshot)
1240    }
1241
1242    /// Returns the safe durable-resume action for a retained invocation.
1243    ///
1244    /// This method never replays delegation implicitly. A checkpoint plan must
1245    /// be handed to the target's durable host adapter with the named opaque
1246    /// token; a fail-closed plan requires operator resolution.
1247    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    /// Validates and summarizes one retained execution receipt without model calls.
1287    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
2144/// Context wrapper that makes member-specific policy visible even when the
2145/// member is invoked by `AgentTool` rather than directly by `Runner`.
2146struct 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}