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