Skip to main content

adk_agent/workflow/
loop_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, Content, Event, EventStream,
6    InvocationContext, ReadonlyContext, Result, Session, State,
7};
8use async_stream::stream;
9use async_trait::async_trait;
10use std::collections::HashMap;
11use std::sync::{Arc, RwLock};
12
13/// Default maximum iterations for LoopAgent when none is specified.
14/// Prevents infinite loops from consuming unbounded resources.
15pub const DEFAULT_LOOP_MAX_ITERATIONS: u32 = 1000;
16
17/// Loop agent executes sub-agents repeatedly for N iterations or until escalation
18pub struct LoopAgent {
19    name: String,
20    description: String,
21    sub_agents: Vec<Arc<dyn Agent>>,
22    max_iterations: u32,
23    skills_index: Option<Arc<SkillIndex>>,
24    skill_policy: SelectionPolicy,
25    max_skill_chars: usize,
26    before_callbacks: Arc<Vec<BeforeAgentCallback>>,
27    after_callbacks: Arc<Vec<AfterAgentCallback>>,
28}
29
30impl LoopAgent {
31    /// Create a new loop agent with the given name and sub-agents.
32    pub fn new(name: impl Into<String>, sub_agents: Vec<Arc<dyn Agent>>) -> Self {
33        Self {
34            name: name.into(),
35            description: String::new(),
36            sub_agents,
37            max_iterations: DEFAULT_LOOP_MAX_ITERATIONS,
38            skills_index: None,
39            skill_policy: SelectionPolicy::default(),
40            max_skill_chars: 2000,
41            before_callbacks: Arc::new(Vec::new()),
42            after_callbacks: Arc::new(Vec::new()),
43        }
44    }
45
46    /// Set the agent description.
47    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
48        self.description = desc.into();
49        self
50    }
51
52    /// Set the maximum number of loop iterations.
53    pub fn with_max_iterations(mut self, max: u32) -> Self {
54        self.max_iterations = max;
55        self
56    }
57
58    /// Set a preloaded skills index for this agent.
59    #[cfg(feature = "skills")]
60    pub fn with_skills(mut self, index: SkillIndex) -> Self {
61        self.skills_index = Some(Arc::new(index));
62        self
63    }
64
65    /// Auto-load skills from `.skills/` in the current working directory.
66    #[cfg(feature = "skills")]
67    pub fn with_auto_skills(self) -> Result<Self> {
68        self.with_skills_from_root(".")
69    }
70
71    /// Auto-load skills from `.skills/` under a custom root directory.
72    #[cfg(feature = "skills")]
73    pub fn with_skills_from_root(mut self, root: impl AsRef<std::path::Path>) -> Result<Self> {
74        let index = load_skill_index(root).map_err(|e| adk_core::AdkError::agent(e.to_string()))?;
75        self.skills_index = Some(Arc::new(index));
76        Ok(self)
77    }
78
79    /// Customize skill selection behavior.
80    #[cfg(feature = "skills")]
81    pub fn with_skill_policy(mut self, policy: SelectionPolicy) -> Self {
82        self.skill_policy = policy;
83        self
84    }
85
86    /// Limit injected skill content length.
87    #[cfg(feature = "skills")]
88    pub fn with_skill_budget(mut self, max_chars: usize) -> Self {
89        self.max_skill_chars = max_chars;
90        self
91    }
92
93    /// Add a before-agent callback.
94    pub fn before_callback(mut self, callback: BeforeAgentCallback) -> Self {
95        if let Some(callbacks) = Arc::get_mut(&mut self.before_callbacks) {
96            callbacks.push(callback);
97        }
98        self
99    }
100
101    /// Add an after-agent callback.
102    pub fn after_callback(mut self, callback: AfterAgentCallback) -> Self {
103        if let Some(callbacks) = Arc::get_mut(&mut self.after_callbacks) {
104            callbacks.push(callback);
105        }
106        self
107    }
108}
109
110struct HistoryTrackingSession {
111    parent_ctx: Arc<dyn InvocationContext>,
112    history: Arc<RwLock<Vec<Content>>>,
113    state: StateTrackingState,
114}
115
116struct StateTrackingState {
117    values: RwLock<HashMap<String, serde_json::Value>>,
118}
119
120impl StateTrackingState {
121    fn new(parent_ctx: &Arc<dyn InvocationContext>) -> Self {
122        Self { values: RwLock::new(parent_ctx.session().state().all()) }
123    }
124
125    fn apply_delta(&self, delta: &HashMap<String, serde_json::Value>) {
126        if delta.is_empty() {
127            return;
128        }
129
130        let mut values = self.values.write().unwrap_or_else(|e| e.into_inner());
131        for (key, value) in delta {
132            values.insert(key.clone(), value.clone());
133        }
134    }
135}
136
137impl State for StateTrackingState {
138    fn get(&self, key: &str) -> Option<serde_json::Value> {
139        self.values.read().unwrap_or_else(|e| e.into_inner()).get(key).cloned()
140    }
141
142    fn set(&mut self, key: String, value: serde_json::Value) {
143        if let Err(msg) = adk_core::validate_state_key(&key) {
144            tracing::warn!(key = %key, "rejecting invalid state key: {msg}");
145            return;
146        }
147        self.values.write().unwrap_or_else(|e| e.into_inner()).insert(key, value);
148    }
149
150    fn all(&self) -> HashMap<String, serde_json::Value> {
151        self.values.read().unwrap_or_else(|e| e.into_inner()).clone()
152    }
153}
154
155impl HistoryTrackingSession {
156    fn new(parent_ctx: Arc<dyn InvocationContext>) -> Self {
157        Self {
158            history: Arc::new(RwLock::new(parent_ctx.session().conversation_history())),
159            state: StateTrackingState::new(&parent_ctx),
160            parent_ctx,
161        }
162    }
163
164    fn apply_event(&self, event: &Event) {
165        if let Some(content) = &event.llm_response.content {
166            // Consolidate streaming chunks: if the last history entry has the
167            // same role, merge text into it instead of creating a new entry.
168            // This prevents N streaming chunks from becoming N separate Content
169            // entries that bloat context for subsequent agents.
170            let mut history = self.history.write().unwrap_or_else(|e| e.into_inner());
171
172            if event.llm_response.partial {
173                // Partial chunk — merge into last entry if same role
174                if let Some(last) = history.last_mut()
175                    && last.role == content.role
176                {
177                    for part in &content.parts {
178                        if let adk_core::Part::Text { text } = part {
179                            // Append text to the last Text part
180                            if let Some(adk_core::Part::Text { text: existing }) =
181                                last.parts.last_mut()
182                            {
183                                existing.push_str(text);
184                            } else {
185                                last.parts.push(part.clone());
186                            }
187                        } else {
188                            last.parts.push(part.clone());
189                        }
190                    }
191                    return;
192                }
193                // No matching last entry — start a new one
194                history.push(content.clone());
195            } else {
196                // Final event (partial=false) — append as-is.
197                // For non-streaming mode this carries the full content.
198                // For streaming mode the accumulated text is already in the
199                // last history entry from partial merges above, so the final
200                // chunk (which may carry the last fragment or be empty) is
201                // merged if same role, or appended if different.
202                if let Some(last) = history.last_mut() {
203                    if last.role == content.role && !content.parts.is_empty() {
204                        // Merge any remaining text from the final chunk
205                        for part in &content.parts {
206                            if let adk_core::Part::Text { text } = part {
207                                if let Some(adk_core::Part::Text { text: existing }) =
208                                    last.parts.last_mut()
209                                {
210                                    existing.push_str(text);
211                                } else {
212                                    last.parts.push(part.clone());
213                                }
214                            } else {
215                                last.parts.push(part.clone());
216                            }
217                        }
218                    } else if !content.parts.is_empty() {
219                        history.push(content.clone());
220                    }
221                } else {
222                    history.push(content.clone());
223                }
224            }
225        }
226        self.state.apply_delta(&event.actions.state_delta);
227    }
228}
229
230impl Session for HistoryTrackingSession {
231    fn id(&self) -> &str {
232        self.parent_ctx.session().id()
233    }
234
235    fn app_name(&self) -> &str {
236        self.parent_ctx.session().app_name()
237    }
238
239    fn user_id(&self) -> &str {
240        self.parent_ctx.session().user_id()
241    }
242
243    fn state(&self) -> &dyn State {
244        &self.state
245    }
246
247    fn conversation_history(&self) -> Vec<Content> {
248        self.history.read().unwrap_or_else(|e| e.into_inner()).clone()
249    }
250
251    fn conversation_history_for_agent(&self, _agent_name: &str) -> Vec<Content> {
252        self.conversation_history()
253    }
254
255    fn append_to_history(&self, content: Content) {
256        self.history.write().unwrap_or_else(|e| e.into_inner()).push(content);
257    }
258}
259
260/// Builds a [`HistoryTrackingContext`] for the wrapper conformance tests.
261#[cfg(test)]
262pub(crate) fn history_tracking_context_for_test(
263    parent: Arc<dyn InvocationContext>,
264) -> Arc<dyn InvocationContext> {
265    Arc::new(HistoryTrackingContext::new(parent))
266}
267
268struct HistoryTrackingContext {
269    parent_ctx: Arc<dyn InvocationContext>,
270    session: HistoryTrackingSession,
271}
272
273impl HistoryTrackingContext {
274    fn new(parent_ctx: Arc<dyn InvocationContext>) -> Self {
275        let session = HistoryTrackingSession::new(parent_ctx.clone());
276        Self { parent_ctx, session }
277    }
278
279    fn apply_event(&self, event: &Event) {
280        self.session.apply_event(event);
281    }
282}
283
284#[async_trait]
285impl adk_core::ReadonlyContext for HistoryTrackingContext {
286    fn invocation_id(&self) -> &str {
287        self.parent_ctx.invocation_id()
288    }
289
290    fn agent_name(&self) -> &str {
291        self.parent_ctx.agent_name()
292    }
293
294    fn user_id(&self) -> &str {
295        self.parent_ctx.user_id()
296    }
297
298    fn app_name(&self) -> &str {
299        self.parent_ctx.app_name()
300    }
301
302    fn session_id(&self) -> &str {
303        self.parent_ctx.session_id()
304    }
305
306    fn branch(&self) -> &str {
307        self.parent_ctx.branch()
308    }
309
310    fn user_content(&self) -> &Content {
311        self.parent_ctx.user_content()
312    }
313}
314
315#[async_trait]
316impl CallbackContext for HistoryTrackingContext {
317    fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
318        self.parent_ctx.artifacts()
319    }
320
321    fn shared_state(&self) -> Option<Arc<adk_core::SharedState>> {
322        self.parent_ctx.shared_state()
323    }
324}
325
326#[async_trait]
327impl InvocationContext for HistoryTrackingContext {
328    fn agent(&self) -> Arc<dyn Agent> {
329        self.parent_ctx.agent()
330    }
331
332    fn memory(&self) -> Option<Arc<dyn adk_core::Memory>> {
333        self.parent_ctx.memory()
334    }
335
336    fn session(&self) -> &dyn Session {
337        &self.session
338    }
339
340    fn run_config(&self) -> &adk_core::RunConfig {
341        self.parent_ctx.run_config()
342    }
343
344    fn end_invocation(&self) {
345        self.parent_ctx.end_invocation();
346    }
347
348    fn ended(&self) -> bool {
349        self.parent_ctx.ended()
350    }
351
352    fn is_cancelled(&self) -> bool {
353        self.parent_ctx.is_cancelled()
354    }
355
356    fn user_scopes(&self) -> Vec<String> {
357        self.parent_ctx.user_scopes()
358    }
359
360    fn request_metadata(&self) -> HashMap<String, serde_json::Value> {
361        self.parent_ctx.request_metadata()
362    }
363
364    fn authoritative_transfer_targets(&self) -> bool {
365        self.parent_ctx.authoritative_transfer_targets()
366    }
367    fn delegation_depth(&self) -> u32 {
368        self.parent_ctx.delegation_depth()
369    }
370    fn max_delegation_depth(&self) -> Option<u32> {
371        self.parent_ctx.max_delegation_depth()
372    }
373
374    fn orchestration_root_invocation_id(&self) -> &str {
375        self.parent_ctx.orchestration_root_invocation_id()
376    }
377
378    fn orchestration_edge_id(&self) -> Option<&str> {
379        self.parent_ctx.orchestration_edge_id()
380    }
381
382    fn requires_tool_confirmation(&self, tool_name: &str) -> bool {
383        self.parent_ctx.requires_tool_confirmation(tool_name)
384    }
385
386    async fn get_secret(&self, name: &str) -> adk_core::Result<Option<String>> {
387        self.parent_ctx.get_secret(name).await
388    }
389
390    async fn get_secret_for(
391        &self,
392        request: &adk_core::SecretRequest,
393    ) -> adk_core::Result<Option<String>> {
394        self.parent_ctx.get_secret_for(request).await
395    }
396}
397
398#[async_trait]
399impl Agent for LoopAgent {
400    fn name(&self) -> &str {
401        &self.name
402    }
403
404    fn description(&self) -> &str {
405        &self.description
406    }
407
408    fn sub_agents(&self) -> &[Arc<dyn Agent>] {
409        &self.sub_agents
410    }
411
412    fn supports_agent_transfer(&self) -> bool {
413        // Deterministic workflow agent: on cross-turn resumption the runner
414        // must restart from this root so every sub-agent runs again, rather
415        // than resuming a single sub-agent that responded last.
416        false
417    }
418
419    async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream> {
420        let sub_agents = self.sub_agents.clone();
421        let max_iterations = self.max_iterations;
422        let before_callbacks = self.before_callbacks.clone();
423        let after_callbacks = self.after_callbacks.clone();
424        let agent_name = self.name.clone();
425        let run_ctx = super::skill_context::with_skill_injected_context(
426            ctx,
427            self.skills_index.as_ref(),
428            &self.skill_policy,
429            self.max_skill_chars,
430        );
431        let run_ctx = Arc::new(HistoryTrackingContext::new(run_ctx));
432
433        let s = stream! {
434            use futures::StreamExt;
435
436            // ===== BEFORE AGENT CALLBACKS =====
437            for callback in before_callbacks.as_ref() {
438                match callback(run_ctx.clone() as Arc<dyn CallbackContext>).await {
439                    Ok(Some(content)) => {
440                        let mut early_event = Event::new(run_ctx.invocation_id());
441                        early_event.author = agent_name.clone();
442                        early_event.llm_response.content = Some(content);
443                        yield Ok(early_event);
444
445                        for after_cb in after_callbacks.as_ref() {
446                            match after_cb(run_ctx.clone() as Arc<dyn CallbackContext>).await {
447                                Ok(Some(after_content)) => {
448                                    let mut after_event = Event::new(run_ctx.invocation_id());
449                                    after_event.author = agent_name.clone();
450                                    after_event.llm_response.content = Some(after_content);
451                                    yield Ok(after_event);
452                                    return;
453                                }
454                                Ok(None) => continue,
455                                Err(e) => { yield Err(e); return; }
456                            }
457                        }
458                        return;
459                    }
460                    Ok(None) => continue,
461                    Err(e) => { yield Err(e); return; }
462                }
463            }
464
465            let mut remaining = max_iterations;
466
467            loop {
468                let mut should_exit = false;
469
470                for agent in &sub_agents {
471                    let mut stream = agent.run(run_ctx.clone() as Arc<dyn InvocationContext>).await?;
472
473                    while let Some(result) = stream.next().await {
474                        match result {
475                            Ok(event) => {
476                                run_ctx.apply_event(&event);
477                                if event.actions.escalate {
478                                    should_exit = true;
479                                }
480                                yield Ok(event);
481                            }
482                            Err(e) => {
483                                yield Err(e);
484                                return;
485                            }
486                        }
487                    }
488
489                    if should_exit {
490                        break;
491                    }
492                }
493
494                if should_exit {
495                    break;
496                }
497
498                remaining -= 1;
499                if remaining == 0 {
500                    break;
501                }
502            }
503
504            // ===== AFTER AGENT CALLBACKS =====
505            for callback in after_callbacks.as_ref() {
506                match callback(run_ctx.clone() as Arc<dyn CallbackContext>).await {
507                    Ok(Some(content)) => {
508                        let mut after_event = Event::new(run_ctx.invocation_id());
509                        after_event.author = agent_name.clone();
510                        after_event.llm_response.content = Some(content);
511                        yield Ok(after_event);
512                        break;
513                    }
514                    Ok(None) => continue,
515                    Err(e) => { yield Err(e); return; }
516                }
517            }
518        };
519
520        Ok(Box::pin(s))
521    }
522}