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