1#![doc = include_str!("../README.md")]
2
3use graph_flow::{
4 Context, ExecutionResult, ExecutionStatus, FlowRunner, Graph, InMemorySessionStorage,
5 NextAction, Session, SessionStorage, Task, TaskResult,
6 error::{GraphError, Result},
7};
8use serde::{Deserialize, Serialize};
9use std::future::Future;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::Duration;
13use tokio::sync::mpsc;
14use tokio::task::JoinHandle;
15
16pub type StreamSender = mpsc::Sender<StreamEvent>;
18pub type StreamReceiver = mpsc::Receiver<StreamEvent>;
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub enum StreamEvent {
24 TaskStarted { task_id: String },
26 Token { task_id: String, delta: String },
28 TaskFinished { task_id: String },
30 TaskFailed { task_id: String, error: String },
32}
33
34tokio::task_local! {
35 static STREAM_TX: StreamSender;
36}
37
38pub async fn emit(event: StreamEvent) {
40 if let Ok(tx) = STREAM_TX.try_with(|tx| tx.clone()) {
41 let _ = tx.send(event).await;
42 }
43}
44
45pub async fn emit_started(task_id: impl Into<String>) {
47 emit(StreamEvent::TaskStarted {
48 task_id: task_id.into(),
49 })
50 .await;
51}
52
53pub async fn emit_token(task_id: impl Into<String>, delta: impl Into<String>) {
55 emit(StreamEvent::Token {
56 task_id: task_id.into(),
57 delta: delta.into(),
58 })
59 .await;
60}
61
62pub async fn emit_finished(task_id: impl Into<String>) {
64 emit(StreamEvent::TaskFinished {
65 task_id: task_id.into(),
66 })
67 .await;
68}
69
70pub async fn emit_failed(task_id: impl Into<String>, error: impl Into<String>) {
72 emit(StreamEvent::TaskFailed {
73 task_id: task_id.into(),
74 error: error.into(),
75 })
76 .await;
77}
78
79pub async fn forward_text_stream<S>(task_id: impl Into<String>, mut stream: S)
81where
82 S: futures_core::Stream<Item = String> + Unpin,
83{
84 use tokio_stream::StreamExt;
85
86 let task_id = task_id.into();
87 while let Some(delta) = stream.next().await {
88 emit_token(task_id.clone(), delta).await;
89 }
90 emit_finished(task_id).await;
91}
92
93pub fn run_streaming<F>(buffer: usize, fut: F) -> (StreamReceiver, JoinHandle<F::Output>)
95where
96 F: Future + Send + 'static,
97 F::Output: Send + 'static,
98{
99 let (tx, rx) = mpsc::channel(buffer);
100 let handle = tokio::spawn(STREAM_TX.scope(tx, fut));
101 (rx, handle)
102}
103
104pub fn spawn_task<T>(
106 task: Arc<T>,
107 context: Context,
108 buffer: usize,
109) -> (StreamReceiver, JoinHandle<Result<TaskResult>>)
110where
111 T: Task + Send + Sync + 'static,
112{
113 run_streaming(buffer, async move { task.run(context).await })
114}
115
116pub async fn collect_text<T>(task: Arc<T>, context: Context, buffer: usize) -> Result<String>
118where
119 T: Task + Send + Sync + 'static,
120{
121 let (mut rx, handle) = spawn_task(task, context, buffer);
122 let mut text = String::new();
123 while let Some(event) = rx.recv().await {
124 if let StreamEvent::Token { delta, .. } = event {
125 text.push_str(&delta);
126 }
127 }
128 handle.await.map_err(|e| GraphError::Other(e.into()))??;
129 Ok(text)
130}
131
132pub fn spawn_graph(
134 flow_runner: FlowRunner,
135 session_id: impl Into<String>,
136 buffer: usize,
137) -> (StreamReceiver, JoinHandle<Result<ExecutionResult>>) {
138 let session_id = session_id.into();
139 run_streaming(buffer, async move {
140 run_to_completion(&flow_runner, &session_id).await
141 })
142}
143
144async fn run_to_completion(flow_runner: &FlowRunner, session_id: &str) -> Result<ExecutionResult> {
145 loop {
146 let result = flow_runner.run(session_id).await?;
147 if !matches!(result.status, ExecutionStatus::Paused { .. }) {
148 return Ok(result);
149 }
150 }
151}
152
153static SUBGRAPH_SESSION_COUNTER: AtomicU64 = AtomicU64::new(0);
154
155pub struct SubgraphTask {
157 id: String,
158 graph: Arc<Graph>,
159 storage: Arc<dyn SessionStorage>,
160}
161
162impl SubgraphTask {
163 pub fn new(id: impl Into<String>, graph: Arc<Graph>) -> Self {
165 Self {
166 id: id.into(),
167 graph,
168 storage: Arc::new(InMemorySessionStorage::new()),
169 }
170 }
171
172 pub fn with_storage(mut self, storage: Arc<dyn SessionStorage>) -> Self {
174 self.storage = storage;
175 self
176 }
177}
178
179#[async_trait::async_trait]
180impl Task for SubgraphTask {
181 fn id(&self) -> &str {
182 &self.id
183 }
184
185 async fn run(&self, context: Context) -> Result<TaskResult> {
186 let start_task_id = self.graph.start_task_id().ok_or_else(|| {
187 GraphError::TaskNotFound(format!("subgraph '{}' has no start task", self.graph.id))
188 })?;
189
190 let suffix = SUBGRAPH_SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
191 let session_id = format!("{}:{}", self.id, suffix);
192
193 let mut session = Session::new_from_task(session_id.clone(), start_task_id)
194 .with_graph_id(self.graph.id.clone());
195 session.context = context;
196 self.storage.save(session).await?;
197
198 let runner = FlowRunner::new(self.graph.clone(), self.storage.clone());
199 let result = run_to_completion(&runner, &session_id).await?;
200
201 let next_action = match result.status {
202 ExecutionStatus::WaitingForInput => NextAction::WaitForInput,
203 _ => NextAction::Continue,
204 };
205 Ok(TaskResult::new(result.response, next_action))
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211pub struct RecordedEvent {
212 pub event: StreamEvent,
213 pub at: Duration,
214}
215
216#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
221pub struct Recording {
222 pub events: Vec<RecordedEvent>,
223}
224
225impl Recording {
226 pub fn replay(self, buffer: usize) -> StreamReceiver {
228 let (tx, rx) = mpsc::channel(buffer);
229 tokio::spawn(async move {
230 let mut last = Duration::ZERO;
231 for recorded in self.events {
232 let wait = recorded.at.saturating_sub(last);
233 if !wait.is_zero() {
234 tokio::time::sleep(wait).await;
235 }
236 last = recorded.at;
237 if tx.send(recorded.event).await.is_err() {
238 return;
239 }
240 }
241 });
242 rx
243 }
244}
245
246pub async fn record(mut rx: StreamReceiver) -> Recording {
248 let start = std::time::Instant::now();
249 let mut events = Vec::new();
250 while let Some(event) = rx.recv().await {
251 events.push(RecordedEvent {
252 event,
253 at: start.elapsed(),
254 });
255 }
256 Recording { events }
257}
258
259fn map_key(prefix: &Option<String>, child_id: &str, field: &str) -> String {
260 match prefix {
261 Some(p) => format!("{p}.{child_id}.{field}"),
262 None => format!("map.{child_id}.{field}"),
263 }
264}
265
266pub struct DynamicMapTask<F> {
274 id: String,
275 children_fn: F,
276 prefix: Option<String>,
277 next_action: NextAction,
278}
279
280impl<F> DynamicMapTask<F>
281where
282 F: Fn(&Context) -> Vec<Arc<dyn Task>> + Send + Sync,
283{
284 pub fn new(id: impl Into<String>, children_fn: F) -> Self {
288 Self {
289 id: id.into(),
290 children_fn,
291 prefix: None,
292 next_action: NextAction::Continue,
293 }
294 }
295
296 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
299 self.prefix = Some(prefix.into());
300 self
301 }
302
303 pub fn with_next_action(mut self, next_action: NextAction) -> Self {
304 self.next_action = next_action;
305 self
306 }
307}
308
309#[async_trait::async_trait]
310impl<F> Task for DynamicMapTask<F>
311where
312 F: Fn(&Context) -> Vec<Arc<dyn Task>> + Send + Sync,
313{
314 fn id(&self) -> &str {
315 &self.id
316 }
317
318 async fn run(&self, context: Context) -> Result<TaskResult> {
319 let children = (self.children_fn)(&context);
320 let mut set = tokio::task::JoinSet::new();
321
322 for child in children {
323 let ctx = context.clone();
324 set.spawn(async move {
325 let child_id = child.id().to_string();
326 (child_id, child.run(ctx).await)
327 });
328 }
329
330 let mut first_error = None;
331 let mut completed = 0usize;
332
333 while let Some(joined) = set.join_next().await {
334 match joined {
335 Err(join_err) => {
336 first_error.get_or_insert_with(|| {
337 GraphError::TaskExecutionFailed(format!(
338 "DynamicMapTask child join error: {join_err}"
339 ))
340 });
341 }
342 Ok((child_id, Err(e))) => {
343 first_error.get_or_insert_with(|| {
344 GraphError::TaskExecutionFailed(format!(
345 "DynamicMapTask child '{child_id}' failed: {e}"
346 ))
347 });
348 }
349 Ok((child_id, Ok(result))) => {
350 if let Some(response) = result.response {
351 context.set(map_key(&self.prefix, &child_id, "response"), response)?;
352 }
353 completed += 1;
354 }
355 }
356 }
357
358 if let Some(err) = first_error {
359 return Err(err);
360 }
361
362 let summary = format!(
363 "DynamicMapTask '{}' mapped over {completed} item(s)",
364 self.id
365 );
366 Ok(TaskResult::new(Some(summary), self.next_action.clone()))
367 }
368}
369
370pub struct EnsembleTask<T, R> {
375 id: String,
376 inner: Arc<T>,
377 runs: usize,
378 reducer: R,
379 next_action: NextAction,
380}
381
382impl<T, R> EnsembleTask<T, R>
383where
384 T: Task + 'static,
385 R: Fn(Vec<String>) -> String + Send + Sync,
386{
387 pub fn new(id: impl Into<String>, inner: T, runs: usize, reducer: R) -> Self {
389 Self {
390 id: id.into(),
391 inner: Arc::new(inner),
392 runs: runs.max(1),
393 reducer,
394 next_action: NextAction::Continue,
395 }
396 }
397
398 pub fn with_next_action(mut self, next_action: NextAction) -> Self {
399 self.next_action = next_action;
400 self
401 }
402}
403
404#[async_trait::async_trait]
405impl<T, R> Task for EnsembleTask<T, R>
406where
407 T: Task + 'static,
408 R: Fn(Vec<String>) -> String + Send + Sync,
409{
410 fn id(&self) -> &str {
411 &self.id
412 }
413
414 async fn run(&self, context: Context) -> Result<TaskResult> {
415 let mut set = tokio::task::JoinSet::new();
416 for _ in 0..self.runs {
417 let inner = self.inner.clone();
418 let ctx = context.clone();
419 set.spawn(async move { inner.run(ctx).await });
420 }
421
422 let mut responses = Vec::with_capacity(self.runs);
423 let mut first_error = None;
424
425 while let Some(joined) = set.join_next().await {
426 match joined {
427 Err(join_err) => {
428 first_error.get_or_insert_with(|| {
429 GraphError::TaskExecutionFailed(format!(
430 "EnsembleTask run join error: {join_err}"
431 ))
432 });
433 }
434 Ok(Err(e)) => {
435 first_error.get_or_insert_with(|| {
436 GraphError::TaskExecutionFailed(format!("EnsembleTask run failed: {e}"))
437 });
438 }
439 Ok(Ok(result)) => {
440 if let Some(response) = result.response {
441 responses.push(response);
442 }
443 }
444 }
445 }
446
447 if let Some(err) = first_error {
448 return Err(err);
449 }
450
451 let combined = (self.reducer)(responses);
452 Ok(TaskResult::new(Some(combined), self.next_action.clone()))
453 }
454}
455
456pub fn majority_vote(responses: Vec<String>) -> String {
460 let mut counts: Vec<(String, usize)> = Vec::new();
461 for response in responses {
462 match counts.iter_mut().find(|(r, _)| *r == response) {
463 Some(entry) => entry.1 += 1,
464 None => counts.push((response, 1)),
465 }
466 }
467 counts
468 .into_iter()
469 .max_by_key(|(_, count)| *count)
470 .map(|(response, _)| response)
471 .unwrap_or_default()
472}