Skip to main content

adk_agent/workflow/
parallel_agent.rs

1#[cfg(feature = "skills")]
2use crate::skill_shim::load_skill_index;
3use crate::skill_shim::{SelectionPolicy, SkillIndex};
4use adk_core::{
5    AfterAgentCallback, Agent, BeforeAgentCallback, CallbackContext, Event, EventStream,
6    InvocationContext, Result, SharedState,
7};
8use async_stream::stream;
9use async_trait::async_trait;
10use std::sync::Arc;
11
12use super::branch_context::{BranchContext, derive_sub_branch};
13use super::shared_state_context::SharedStateContext;
14
15/// Parallel agent executes sub-agents concurrently
16pub struct ParallelAgent {
17    name: String,
18    description: String,
19    sub_agents: Vec<Arc<dyn Agent>>,
20    skills_index: Option<Arc<SkillIndex>>,
21    skill_policy: SelectionPolicy,
22    max_skill_chars: usize,
23    before_callbacks: Arc<Vec<BeforeAgentCallback>>,
24    after_callbacks: Arc<Vec<AfterAgentCallback>>,
25    shared_state_enabled: bool,
26}
27
28impl ParallelAgent {
29    /// Create a new parallel agent with the given name and sub-agents.
30    pub fn new(name: impl Into<String>, sub_agents: Vec<Arc<dyn Agent>>) -> Self {
31        Self {
32            name: name.into(),
33            description: String::new(),
34            sub_agents,
35            skills_index: None,
36            skill_policy: SelectionPolicy::default(),
37            max_skill_chars: 2000,
38            before_callbacks: Arc::new(Vec::new()),
39            after_callbacks: Arc::new(Vec::new()),
40            shared_state_enabled: false,
41        }
42    }
43
44    /// Set the agent description.
45    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
46        self.description = desc.into();
47        self
48    }
49
50    /// Add a before-agent callback.
51    pub fn before_callback(mut self, callback: BeforeAgentCallback) -> Self {
52        if let Some(callbacks) = Arc::get_mut(&mut self.before_callbacks) {
53            callbacks.push(callback);
54        }
55        self
56    }
57
58    /// Add an after-agent callback.
59    pub fn after_callback(mut self, callback: AfterAgentCallback) -> Self {
60        if let Some(callbacks) = Arc::get_mut(&mut self.after_callbacks) {
61            callbacks.push(callback);
62        }
63        self
64    }
65
66    /// Set a preloaded skills index for this agent.
67    #[cfg(feature = "skills")]
68    pub fn with_skills(mut self, index: SkillIndex) -> Self {
69        self.skills_index = Some(Arc::new(index));
70        self
71    }
72
73    /// Auto-load skills from `.skills/` in the current working directory.
74    #[cfg(feature = "skills")]
75    pub fn with_auto_skills(self) -> Result<Self> {
76        self.with_skills_from_root(".")
77    }
78
79    /// Auto-load skills from `.skills/` under a custom root directory.
80    #[cfg(feature = "skills")]
81    pub fn with_skills_from_root(mut self, root: impl AsRef<std::path::Path>) -> Result<Self> {
82        let index = load_skill_index(root).map_err(|e| adk_core::AdkError::agent(e.to_string()))?;
83        self.skills_index = Some(Arc::new(index));
84        Ok(self)
85    }
86
87    /// Customize skill selection behavior.
88    #[cfg(feature = "skills")]
89    pub fn with_skill_policy(mut self, policy: SelectionPolicy) -> Self {
90        self.skill_policy = policy;
91        self
92    }
93
94    /// Limit injected skill content length.
95    #[cfg(feature = "skills")]
96    pub fn with_skill_budget(mut self, max_chars: usize) -> Self {
97        self.max_skill_chars = max_chars;
98        self
99    }
100
101    /// Enables shared state coordination for sub-agents.
102    ///
103    /// When enabled, a fresh `SharedState` instance is created for each
104    /// `run()` invocation and injected into each sub-agent's context.
105    /// Sub-agents can then use `ctx.shared_state()` to access the store.
106    pub fn with_shared_state(mut self) -> Self {
107        self.shared_state_enabled = true;
108        self
109    }
110}
111
112#[async_trait]
113impl Agent for ParallelAgent {
114    fn name(&self) -> &str {
115        &self.name
116    }
117
118    fn description(&self) -> &str {
119        &self.description
120    }
121
122    fn sub_agents(&self) -> &[Arc<dyn Agent>] {
123        &self.sub_agents
124    }
125
126    fn supports_agent_transfer(&self) -> bool {
127        // Deterministic workflow agent: on cross-turn resumption the runner
128        // must restart from this root so every sub-agent runs again, rather
129        // than resuming a single sub-agent that responded last.
130        false
131    }
132
133    async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
134        let sub_agents = self.sub_agents.clone();
135        let run_ctx = super::skill_context::with_skill_injected_context(
136            ctx,
137            self.skills_index.as_ref(),
138            &self.skill_policy,
139            self.max_skill_chars,
140        );
141        let before_callbacks = self.before_callbacks.clone();
142        let after_callbacks = self.after_callbacks.clone();
143        let agent_name = self.name.clone();
144        let invocation_id = run_ctx.invocation_id().to_string();
145        let shared_state_enabled = self.shared_state_enabled;
146
147        let s = stream! {
148            use futures::stream::{StreamExt, select_all};
149
150            for callback in before_callbacks.as_ref() {
151                match callback(run_ctx.clone() as Arc<dyn CallbackContext>).await {
152                    Ok(Some(content)) => {
153                        let mut early_event = Event::new(&invocation_id);
154                        early_event.author = agent_name.clone();
155                        early_event.llm_response.content = Some(content);
156                        yield Ok(early_event);
157
158                        for after_callback in after_callbacks.as_ref() {
159                            match after_callback(run_ctx.clone() as Arc<dyn CallbackContext>).await {
160                                Ok(Some(after_content)) => {
161                                    let mut after_event = Event::new(&invocation_id);
162                                    after_event.author = agent_name.clone();
163                                    after_event.llm_response.content = Some(after_content);
164                                    yield Ok(after_event);
165                                    return;
166                                }
167                                Ok(None) => continue,
168                                Err(e) => {
169                                    yield Err(e);
170                                    return;
171                                }
172                            }
173                        }
174                        return;
175                    }
176                    Ok(None) => continue,
177                    Err(e) => {
178                        yield Err(e);
179                        return;
180                    }
181                }
182            }
183
184
185            // Create shared state if enabled (fresh per run)
186            let shared = if shared_state_enabled {
187                Some(Arc::new(SharedState::new()))
188            } else {
189                None
190            };
191
192            // Each sub-agent gets its own stream that resolves `run()` and drains
193            // the resulting events. Merging these with `select_all` polls every
194            // sub-agent concurrently, which is what makes this agent parallel:
195            // `Agent::run` only *builds* an `EventStream`, so awaiting the run
196            // futures together is not enough — the streams themselves have to be
197            // polled together. Draining one stream to completion before touching
198            // the next made nominally parallel branches run one at a time.
199            //
200            // Polling from a single task also gives the backpressure the ADK
201            // Python and Go implementations arrange explicitly (a resume signal
202            // and an ack channel respectively): a sub-agent cannot run ahead
203            // while an already-produced event is still being consumed upstream,
204            // so the runner's per-event persistence stays in step with execution.
205            //
206            // Dropping the merged stream drops every sub-agent stream with it, so
207            // a consumer that stops early tears down in-flight sub-agents instead
208            // of leaving them running.
209            let mut merged = {
210                // Item is (sub-agent index, event result). The index lets a failure
211                // be attributed to the branch that produced it.
212                type BranchStream =
213                    std::pin::Pin<Box<dyn futures::Stream<Item = (usize, Result<Event>)> + Send>>;
214                let mut per_agent: Vec<BranchStream> = Vec::with_capacity(sub_agents.len());
215
216                for (index, agent) in sub_agents.into_iter().enumerate() {
217                    let base: Arc<dyn InvocationContext> = if let Some(ref shared) = shared {
218                        Arc::new(SharedStateContext::new(run_ctx.clone(), shared.clone()))
219                    } else {
220                        run_ctx.clone()
221                    };
222
223                    // Each sub-agent runs on its own branch, so a history read
224                    // scoped by branch excludes what its siblings produced while
225                    // still seeing the conversation that led to the fan-out. The
226                    // shape mirrors ADK Python (`{parent}.{agent}.{sub_agent}`)
227                    // and ADK Go.
228                    let branch = derive_sub_branch(
229                        base.branch(),
230                        &format!("{agent_name}.{}", agent.name()),
231                    );
232                    let ctx: Arc<dyn InvocationContext> =
233                        Arc::new(BranchContext::new(base, branch.clone()));
234
235                    per_agent.push(Box::pin(stream! {
236                        match agent.run(ctx).await {
237                            Ok(mut events) => {
238                                while let Some(event_result) = events.next().await {
239                                    let failed = event_result.is_err();
240                                    // Record which branch produced the event so a
241                                    // later branch-scoped history read can exclude
242                                    // it from siblings. A nested workflow may have
243                                    // already stamped a deeper branch; leave it.
244                                    let event_result = event_result.map(|mut event| {
245                                        if event.branch.is_empty() {
246                                            event.branch = branch.clone();
247                                        }
248                                        event
249                                    });
250                                    yield (index, event_result);
251                                    if failed {
252                                        // Abandon this branch, leave the others running.
253                                        break;
254                                    }
255                                }
256                            }
257                            Err(e) => yield (index, Err(e)),
258                        }
259                    }));
260                }
261
262                select_all(per_agent)
263            };
264
265            // Errors are collected with their sub-agent index so the reported
266            // error stays deterministic. With branches running concurrently,
267            // "whichever failed first" would be a race; the lowest index matches
268            // the declared sub-agent order this agent was constructed with.
269            let mut failures: Vec<(usize, adk_core::AdkError)> = Vec::new();
270
271            while let Some((index, event_result)) = merged.next().await {
272                match event_result {
273                    Ok(event) => yield Ok(event),
274                    Err(e) => failures.push((index, e)),
275                }
276            }
277
278            // After all agents complete, propagate the first error if any
279            if let Some((_, e)) = failures.into_iter().min_by_key(|(index, _)| *index) {
280                yield Err(e);
281                return;
282            }
283
284            for callback in after_callbacks.as_ref() {
285                match callback(run_ctx.clone() as Arc<dyn CallbackContext>).await {
286                    Ok(Some(content)) => {
287                        let mut after_event = Event::new(&invocation_id);
288                        after_event.author = agent_name.clone();
289                        after_event.llm_response.content = Some(content);
290                        yield Ok(after_event);
291                        break;
292                    }
293                    Ok(None) => continue,
294                    Err(e) => {
295                        yield Err(e);
296                        return;
297                    }
298                }
299            }
300        };
301
302        Ok(Box::pin(s))
303    }
304}