Skip to main content

adk_agent/team/
lifecycle.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use super::{RelationshipKind, TeamRuntimeError};
8
9/// Lifecycle boundary exposed by portable team execution.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
11#[serde(rename_all = "camelCase")]
12pub enum TeamLifecyclePhase {
13    /// The compiled team root is starting or finishing.
14    Team,
15    /// One concrete member is starting or finishing.
16    Member,
17    /// One exact delegate or handoff relationship is starting or finishing.
18    Relationship,
19}
20
21/// Immutable context supplied to a [`TeamLifecycleHook`].
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
23#[serde(rename_all = "camelCase")]
24pub struct TeamLifecycleContext {
25    /// Team root name.
26    pub team: String,
27    /// Stable root invocation identifier.
28    pub invocation_id: String,
29    /// Lifecycle boundary.
30    pub phase: TeamLifecyclePhase,
31    /// Member associated with a member lifecycle boundary.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub member: Option<String>,
34    /// Relationship execution identifier.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub edge_id: Option<String>,
37    /// Relationship source.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub from: Option<String>,
40    /// Relationship target.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub to: Option<String>,
43    /// Relationship semantics.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub kind: Option<RelationshipKind>,
46    /// One-based attempt number.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub attempt: Option<u32>,
49}
50
51/// Result observed after a team lifecycle boundary.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
53#[serde(rename_all = "camelCase", tag = "status")]
54pub enum TeamLifecycleOutcome {
55    /// The operation completed normally.
56    Succeeded,
57    /// The operation failed.
58    Failed {
59        /// Stable ADK error code when one is available.
60        #[serde(default, skip_serializing_if = "Option::is_none")]
61        code: Option<String>,
62        /// Human-readable failure message.
63        message: String,
64    },
65    /// Policy terminated the operation before execution.
66    Terminated {
67        /// Policy reason.
68        reason: String,
69    },
70}
71
72/// Decision returned before a team lifecycle boundary executes.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
74#[serde(rename_all = "camelCase", tag = "decision")]
75pub enum TeamLifecycleDecision {
76    /// Continue normal execution.
77    Continue,
78    /// Deny the exact pending operation.
79    Terminate {
80        /// Auditable policy reason.
81        reason: String,
82    },
83}
84
85/// Async team execution hook.
86///
87/// Runner's ordinary plugins still wrap the complete agent run. This hook adds
88/// the team-specific relationship boundary that generic plugins cannot infer.
89#[async_trait]
90pub trait TeamLifecycleHook: Send + Sync {
91    /// Stable hook name used in diagnostics and telemetry.
92    fn name(&self) -> &str;
93
94    /// Lower values run first before an operation and last after it.
95    fn priority(&self) -> i32 {
96        0
97    }
98
99    /// Runs immediately before the described operation.
100    async fn before(
101        &self,
102        _context: &TeamLifecycleContext,
103    ) -> adk_core::Result<TeamLifecycleDecision> {
104        Ok(TeamLifecycleDecision::Continue)
105    }
106
107    /// Runs after success, failure, or policy termination.
108    async fn after(
109        &self,
110        _context: &TeamLifecycleContext,
111        _outcome: &TeamLifecycleOutcome,
112    ) -> adk_core::Result<()> {
113        Ok(())
114    }
115}
116
117pub(crate) struct TeamLifecycleManager {
118    hooks: Vec<Arc<dyn TeamLifecycleHook>>,
119}
120
121impl TeamLifecycleManager {
122    pub(crate) fn new(mut hooks: Vec<Arc<dyn TeamLifecycleHook>>) -> Self {
123        hooks.sort_by_key(|hook| hook.priority());
124        Self { hooks }
125    }
126
127    pub(crate) async fn before(
128        &self,
129        context: &TeamLifecycleContext,
130    ) -> adk_core::Result<TeamLifecycleDecision> {
131        for hook in &self.hooks {
132            match hook.before(context).await.map_err(|error| TeamRuntimeError::LifecycleHook {
133                hook: hook.name().to_string(),
134                phase: context.phase,
135                message: error.to_string(),
136            })? {
137                TeamLifecycleDecision::Continue => {}
138                decision @ TeamLifecycleDecision::Terminate { .. } => return Ok(decision),
139            }
140        }
141        Ok(TeamLifecycleDecision::Continue)
142    }
143
144    pub(crate) async fn after(
145        &self,
146        context: &TeamLifecycleContext,
147        outcome: &TeamLifecycleOutcome,
148    ) -> adk_core::Result<()> {
149        for hook in self.hooks.iter().rev() {
150            hook.after(context, outcome).await.map_err(|error| {
151                TeamRuntimeError::LifecycleHook {
152                    hook: hook.name().to_string(),
153                    phase: context.phase,
154                    message: error.to_string(),
155                }
156            })?;
157        }
158        Ok(())
159    }
160}