Skip to main content

adk_agent/workflow/
conditional_agent.rs

1//! Rule-based conditional routing agent.
2//!
3//! `ConditionalAgent` provides **synchronous, rule-based** conditional routing.
4//! The condition function is evaluated synchronously and must return a boolean.
5//!
6//! # When to Use
7//!
8//! Use `ConditionalAgent` for **deterministic** routing decisions:
9//! - A/B testing based on session state or flags
10//! - Environment-based routing (e.g., production vs staging)
11//! - Feature flag checks
12//!
13//! # For Intelligent Routing
14//!
15//! If you need **LLM-based intelligent routing** where the model classifies
16//! user intent and routes accordingly, use [`LlmConditionalAgent`] instead:
17//!
18//! ```rust,ignore
19//! // LLM decides which agent to route to
20//! let router = LlmConditionalAgent::builder("router", model)
21//!     .instruction("Classify as 'technical' or 'general'")
22//!     .route("technical", tech_agent)
23//!     .route("general", general_agent)
24//!     .build()?;
25//! ```
26//!
27//! See [`crate::workflow::LlmConditionalAgent`] for details.
28
29#[cfg(feature = "skills")]
30use crate::skill_shim::load_skill_index;
31use crate::skill_shim::{SelectionPolicy, SkillIndex};
32use adk_core::{
33    AfterAgentCallback, Agent, BeforeAgentCallback, CallbackContext, Event, EventStream,
34    InvocationContext, Result,
35};
36use async_stream::stream;
37use async_trait::async_trait;
38use futures::StreamExt;
39use std::sync::Arc;
40
41type ConditionFn = Arc<dyn Fn(&dyn InvocationContext) -> bool + Send + Sync>;
42
43/// Rule-based conditional routing agent.
44///
45/// Executes one of two sub-agents based on a synchronous condition function.
46/// For LLM-based intelligent routing, use [`crate::LlmConditionalAgent`] instead.
47///
48/// # Example
49///
50/// ```rust,ignore
51/// // Route based on session state flag
52/// let router = ConditionalAgent::new(
53///     "premium_router",
54///     |ctx| ctx.session().state().get("is_premium").map(|v| v.as_bool()).flatten().unwrap_or(false),
55///     Arc::new(premium_agent),
56/// ).with_else(Arc::new(basic_agent));
57/// ```
58pub struct ConditionalAgent {
59    name: String,
60    description: String,
61    condition: ConditionFn,
62    if_agent: Arc<dyn Agent>,
63    else_agent: Option<Arc<dyn Agent>>,
64    /// Cached list of all branch agents for tree discovery via `sub_agents()`.
65    all_agents: Vec<Arc<dyn Agent>>,
66    skills_index: Option<Arc<SkillIndex>>,
67    skill_policy: SelectionPolicy,
68    max_skill_chars: usize,
69    before_callbacks: Arc<Vec<BeforeAgentCallback>>,
70    after_callbacks: Arc<Vec<AfterAgentCallback>>,
71}
72
73impl ConditionalAgent {
74    /// Create a new conditional agent with a condition function and the if-branch agent.
75    pub fn new<F>(name: impl Into<String>, condition: F, if_agent: Arc<dyn Agent>) -> Self
76    where
77        F: Fn(&dyn InvocationContext) -> bool + Send + Sync + 'static,
78    {
79        let all_agents = vec![if_agent.clone()];
80        Self {
81            name: name.into(),
82            description: String::new(),
83            condition: Arc::new(condition),
84            if_agent,
85            else_agent: None,
86            all_agents,
87            skills_index: None,
88            skill_policy: SelectionPolicy::default(),
89            max_skill_chars: 2000,
90            before_callbacks: Arc::new(Vec::new()),
91            after_callbacks: Arc::new(Vec::new()),
92        }
93    }
94
95    /// Set the agent description.
96    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
97        self.description = desc.into();
98        self
99    }
100
101    /// Set the else-branch agent executed when the condition is false.
102    pub fn with_else(mut self, else_agent: Arc<dyn Agent>) -> Self {
103        self.all_agents.push(else_agent.clone());
104        self.else_agent = Some(else_agent);
105        self
106    }
107
108    /// Add a before-agent callback.
109    pub fn before_callback(mut self, callback: BeforeAgentCallback) -> Self {
110        if let Some(callbacks) = Arc::get_mut(&mut self.before_callbacks) {
111            callbacks.push(callback);
112        }
113        self
114    }
115
116    /// Add an after-agent callback.
117    pub fn after_callback(mut self, callback: AfterAgentCallback) -> Self {
118        if let Some(callbacks) = Arc::get_mut(&mut self.after_callbacks) {
119            callbacks.push(callback);
120        }
121        self
122    }
123
124    /// Set a preloaded skills index for this agent.
125    #[cfg(feature = "skills")]
126    pub fn with_skills(mut self, index: SkillIndex) -> Self {
127        self.skills_index = Some(Arc::new(index));
128        self
129    }
130
131    /// Auto-load skills from `.skills/` in the current working directory.
132    #[cfg(feature = "skills")]
133    pub fn with_auto_skills(self) -> Result<Self> {
134        self.with_skills_from_root(".")
135    }
136
137    /// Auto-load skills from `.skills/` under a custom root directory.
138    #[cfg(feature = "skills")]
139    pub fn with_skills_from_root(mut self, root: impl AsRef<std::path::Path>) -> Result<Self> {
140        let index = load_skill_index(root).map_err(|e| adk_core::AdkError::agent(e.to_string()))?;
141        self.skills_index = Some(Arc::new(index));
142        Ok(self)
143    }
144
145    /// Customize skill selection behavior.
146    #[cfg(feature = "skills")]
147    pub fn with_skill_policy(mut self, policy: SelectionPolicy) -> Self {
148        self.skill_policy = policy;
149        self
150    }
151
152    /// Limit injected skill content length.
153    #[cfg(feature = "skills")]
154    pub fn with_skill_budget(mut self, max_chars: usize) -> Self {
155        self.max_skill_chars = max_chars;
156        self
157    }
158}
159
160#[async_trait]
161impl Agent for ConditionalAgent {
162    fn name(&self) -> &str {
163        &self.name
164    }
165
166    fn description(&self) -> &str {
167        &self.description
168    }
169
170    fn sub_agents(&self) -> &[Arc<dyn Agent>] {
171        &self.all_agents
172    }
173
174    fn supports_agent_transfer(&self) -> bool {
175        // Deterministic workflow agent: on cross-turn resumption the runner
176        // must restart from this root so the condition is re-evaluated, rather
177        // than resuming a single branch that responded last.
178        false
179    }
180
181    async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
182        let run_ctx = super::skill_context::with_skill_injected_context(
183            ctx,
184            self.skills_index.as_ref(),
185            &self.skill_policy,
186            self.max_skill_chars,
187        );
188        let before_callbacks = self.before_callbacks.clone();
189        let after_callbacks = self.after_callbacks.clone();
190        let if_agent = self.if_agent.clone();
191        let else_agent = self.else_agent.clone();
192        let agent_name = self.name.clone();
193        let invocation_id = run_ctx.invocation_id().to_string();
194        let condition = self.condition.clone();
195
196        let s = stream! {
197            for callback in before_callbacks.as_ref() {
198                match callback(run_ctx.clone() as Arc<dyn CallbackContext>).await {
199                    Ok(Some(content)) => {
200                        let mut early_event = Event::new(&invocation_id);
201                        early_event.author = agent_name.clone();
202                        early_event.llm_response.content = Some(content);
203                        yield Ok(early_event);
204
205                        for after_callback in after_callbacks.as_ref() {
206                            match after_callback(run_ctx.clone() as Arc<dyn CallbackContext>).await {
207                                Ok(Some(after_content)) => {
208                                    let mut after_event = Event::new(&invocation_id);
209                                    after_event.author = agent_name.clone();
210                                    after_event.llm_response.content = Some(after_content);
211                                    yield Ok(after_event);
212                                    return;
213                                }
214                                Ok(None) => continue,
215                                Err(e) => {
216                                    yield Err(e);
217                                    return;
218                                }
219                            }
220                        }
221                        return;
222                    }
223                    Ok(None) => continue,
224                    Err(e) => {
225                        yield Err(e);
226                        return;
227                    }
228                }
229            }
230
231            let target_agent = if condition(run_ctx.as_ref()) {
232                Some(if_agent)
233            } else {
234                else_agent
235            };
236
237            if let Some(agent) = target_agent {
238                let mut stream = match agent.run(run_ctx.clone()).await {
239                    Ok(stream) => stream,
240                    Err(e) => {
241                        yield Err(e);
242                        return;
243                    }
244                };
245
246                while let Some(result) = stream.next().await {
247                    match result {
248                        Ok(event) => yield Ok(event),
249                        Err(e) => {
250                            yield Err(e);
251                            return;
252                        }
253                    }
254                }
255            }
256
257            for callback in after_callbacks.as_ref() {
258                match callback(run_ctx.clone() as Arc<dyn CallbackContext>).await {
259                    Ok(Some(content)) => {
260                        let mut after_event = Event::new(&invocation_id);
261                        after_event.author = agent_name.clone();
262                        after_event.llm_response.content = Some(content);
263                        yield Ok(after_event);
264                        break;
265                    }
266                    Ok(None) => continue,
267                    Err(e) => {
268                        yield Err(e);
269                        return;
270                    }
271                }
272            }
273        };
274
275        Ok(Box::pin(s))
276    }
277}