gemini_adk_rs/text/
dispatch.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use async_trait::async_trait;
6
7use super::TextAgent;
8use crate::context::AgentEvent;
9use crate::error::AgentError;
10use crate::middleware::MiddlewareChain;
11use crate::state::State;
12
13/// Background task handle: agent name → join handle yielding `Ok(json)`/`Err(msg)`.
14type TaskMap = HashMap<String, tokio::task::JoinHandle<Result<String, String>>>;
15
16/// Shared registry for dispatched background tasks.
17#[derive(Clone, Default)]
18pub struct TaskRegistry {
19    pub(crate) inner: Arc<tokio::sync::Mutex<TaskMap>>,
20}
21
22impl TaskRegistry {
23    /// Create a new empty task registry.
24    pub fn new() -> Self {
25        Self::default()
26    }
27}
28
29/// Fire-and-forget background task launcher with global task budget.
30///
31/// Launches each child agent as a background `tokio::spawn` task,
32/// stores handles in a `TaskRegistry`, and returns immediately.
33pub struct DispatchTextAgent {
34    name: String,
35    children: Vec<(String, Arc<dyn TextAgent>)>,
36    registry: TaskRegistry,
37    budget: Arc<tokio::sync::Semaphore>,
38    middleware: MiddlewareChain,
39}
40
41impl DispatchTextAgent {
42    /// Create a new dispatch agent with named children and a concurrency budget.
43    pub fn new(
44        name: impl Into<String>,
45        children: Vec<(String, Arc<dyn TextAgent>)>,
46        registry: TaskRegistry,
47        budget: Arc<tokio::sync::Semaphore>,
48    ) -> Self {
49        Self {
50            name: name.into(),
51            children,
52            registry,
53            budget,
54            middleware: MiddlewareChain::new(),
55        }
56    }
57
58    /// Attach a middleware chain. `AgentEvent::AgentStarted` is emitted
59    /// through it as each child task is launched, and `AgentEvent::AgentCompleted`
60    /// from inside the detached task when the child finishes (the chain is
61    /// cloned into the task, so this fires even after `run` has returned).
62    pub fn with_middleware_chain(mut self, chain: MiddlewareChain) -> Self {
63        self.middleware = chain;
64        self
65    }
66}
67
68#[async_trait]
69impl TextAgent for DispatchTextAgent {
70    fn name(&self) -> &str {
71        &self.name
72    }
73
74    async fn run(&self, state: &State) -> Result<String, AgentError> {
75        let mut registry = self.registry.inner.lock().await;
76
77        for (task_name, agent) in &self.children {
78            let agent = agent.clone();
79            let state = state.clone();
80            let budget = self.budget.clone();
81            let task_name_owned = task_name.clone();
82
83            let handle = tokio::spawn(async move {
84                let _permit = budget
85                    .acquire()
86                    .await
87                    .map_err(|e| format!("Semaphore closed: {e}"))?;
88                agent
89                    .run(&state)
90                    .await
91                    .map_err(|e| format!("Task '{task_name_owned}' failed: {e}"))
92            });
93
94            registry.insert(task_name.clone(), handle);
95        }
96
97        let _ = state.set(
98            "_dispatch_status",
99            self.children
100                .iter()
101                .map(|(name, _)| (name.clone(), "running".to_string()))
102                .collect::<HashMap<String, String>>(),
103        );
104
105        Ok(String::new())
106    }
107}
108
109// ── JoinTextAgent ─────────────────────────────────────────────────────────
110
111/// Waits for dispatched background tasks and collects their results.
112pub struct JoinTextAgent {
113    name: String,
114    registry: TaskRegistry,
115    target_names: Option<Vec<String>>,
116    timeout: Option<Duration>,
117    middleware: MiddlewareChain,
118}
119
120impl JoinTextAgent {
121    /// Create a new join agent that waits for dispatched tasks.
122    pub fn new(name: impl Into<String>, registry: TaskRegistry) -> Self {
123        Self {
124            name: name.into(),
125            registry,
126            target_names: None,
127            timeout: None,
128            middleware: MiddlewareChain::new(),
129        }
130    }
131
132    /// Attach a middleware chain. `AgentEvent::AgentCompleted` is emitted
133    /// through it for each task as it is joined successfully, and
134    /// `AgentEvent::Timeout` when a task exceeds the join timeout.
135    pub fn with_middleware_chain(mut self, chain: MiddlewareChain) -> Self {
136        self.middleware = chain;
137        self
138    }
139
140    /// Only wait for specific named tasks.
141    pub fn targets(mut self, names: Vec<String>) -> Self {
142        self.target_names = Some(names);
143        self
144    }
145
146    /// Set a timeout for waiting.
147    pub fn timeout(mut self, timeout: Duration) -> Self {
148        self.timeout = Some(timeout);
149        self
150    }
151}
152
153#[async_trait]
154impl TextAgent for JoinTextAgent {
155    fn name(&self) -> &str {
156        &self.name
157    }
158
159    async fn run(&self, state: &State) -> Result<String, AgentError> {
160        let mut registry = self.registry.inner.lock().await;
161
162        // Select tasks to wait for.
163        let tasks: HashMap<String, _> = if let Some(targets) = &self.target_names {
164            targets
165                .iter()
166                .filter_map(|name| registry.remove(name).map(|h| (name.clone(), h)))
167                .collect()
168        } else {
169            std::mem::take(&mut *registry)
170        };
171        drop(registry);
172
173        let mut results = Vec::new();
174
175        for (task_name, handle) in tasks {
176            let result = if let Some(timeout) = self.timeout {
177                match tokio::time::timeout(timeout, handle).await {
178                    Ok(Ok(Ok(text))) => {
179                        let _ = state.set(format!("_result_{task_name}"), &text);
180                        Ok(text)
181                    }
182                    Ok(Ok(Err(e))) => Err(AgentError::Other(e)),
183                    Ok(Err(e)) => Err(AgentError::Other(format!("Join error: {e}"))),
184                    Err(_) => {
185                        let _ = self.middleware.run_on_event(&AgentEvent::Timeout).await;
186                        Err(AgentError::Timeout)
187                    }
188                }
189            } else {
190                match handle.await {
191                    Ok(Ok(text)) => {
192                        let _ = state.set(format!("_result_{task_name}"), &text);
193                        Ok(text)
194                    }
195                    Ok(Err(e)) => Err(AgentError::Other(e)),
196                    Err(e) => Err(AgentError::Other(format!("Join error: {e}"))),
197                }
198            };
199
200            results.push(result?);
201        }
202
203        let combined = results.join("\n");
204        let _ = state.set("output", &combined);
205        Ok(combined)
206    }
207}