Skip to main content

prodigy/cook/execution/mapreduce/pure/
parallel_execution.rs

1//! Parallel execution patterns using Effects
2//!
3//! This module demonstrates how to use Stillwater's Effect::par_all_limit
4//! for bounded parallel execution of agents in MapReduce workflows.
5//!
6//! Key benefits:
7//! - Type-safe parallel execution with error handling
8//! - Automatic concurrency limiting (respects max_parallel)
9//! - Composable with other effects
10//! - Testable without actual I/O
11
12use crate::cook::execution::errors::MapReduceError;
13use crate::cook::execution::mapreduce::effects::commands::{
14    execute_commands_effect, CommandResult,
15};
16use crate::cook::execution::mapreduce::effects::merge::{merge_to_parent_effect, MergeResult};
17use crate::cook::execution::mapreduce::effects::worktree::{create_worktree_effect, Worktree};
18use crate::cook::execution::mapreduce::environment::MapEnv;
19use serde_json::Value;
20use stillwater::{from_async, par_all_limit, BoxedEffect, Effect, EffectExt};
21
22/// Result from executing a single agent
23#[derive(Debug, Clone)]
24pub struct AgentExecutionResult {
25    pub worktree: Worktree,
26    pub command_result: CommandResult,
27    pub merge_result: MergeResult,
28}
29
30/// Create an effect that executes a single agent (worktree -> commands -> merge)
31///
32/// This composes three effects sequentially using `and_then`:
33/// 1. Create worktree
34/// 2. Execute commands in worktree
35/// 3. Merge results back
36///
37/// # Example
38///
39/// ```ignore
40/// let item = json!({"id": 1, "task": "process"});
41/// let effect = execute_agent_effect(&item, "agent-0", "main");
42/// let result = effect.run_async(&env).await?;
43/// ```
44pub fn execute_agent_effect(
45    item: &Value,
46    agent_name: &str,
47    parent_branch: &str,
48) -> BoxedEffect<AgentExecutionResult, MapReduceError, MapEnv> {
49    let item = item.clone();
50    let agent_name = agent_name.to_string();
51    let parent_branch = parent_branch.to_string();
52
53    // Compose effects sequentially with and_then
54    create_worktree_effect(&agent_name, &parent_branch)
55        .and_then(move |worktree| {
56            let item = item.clone();
57            let parent_branch = parent_branch.clone();
58            let worktree_clone = worktree.clone();
59
60            execute_commands_effect(&item, &worktree).and_then(move |command_result| {
61                let worktree = worktree_clone;
62                let worktree_clone2 = worktree.clone();
63
64                merge_to_parent_effect(&worktree, &parent_branch).map(move |merge_result| {
65                    AgentExecutionResult {
66                        worktree: worktree_clone2,
67                        command_result,
68                        merge_result,
69                    }
70                })
71            })
72        })
73        .boxed()
74}
75
76/// Execute multiple agents in parallel with bounded concurrency
77///
78/// This is the KEY function demonstrating Effect::par_all_limit for parallel execution.
79/// It takes a list of work items and executes them in parallel with a maximum
80/// concurrency limit (respecting max_parallel from config).
81///
82/// # Benefits over manual tokio::spawn coordination:
83/// - Automatic error collection and handling
84/// - Respects concurrency limits
85/// - Type-safe
86/// - Composable with other effects
87/// - Testable with mock environments
88///
89/// # Example
90///
91/// ```ignore
92/// let items = vec![json!({"id": 1}), json!({"id": 2}), json!({"id": 3})];
93/// let effect = execute_agents_parallel(&items, "main", 2);
94/// let results = effect.run_async(&env).await?;
95/// // Executes agents with max 2 concurrent at a time
96/// ```
97pub fn execute_agents_parallel(
98    items: &[Value],
99    parent_branch: &str,
100    max_parallel: usize,
101) -> impl Effect<Output = Vec<AgentExecutionResult>, Error = Vec<MapReduceError>, Env = MapEnv> {
102    let parent_branch = parent_branch.to_string();
103    let items: Vec<Value> = items.to_vec();
104
105    // Wrap in from_async to have access to environment for par_all_limit
106    from_async(move |env: &MapEnv| {
107        let parent_branch = parent_branch.clone();
108        let items = items.clone();
109        let env = env.clone(); // Clone env for use in async block
110
111        async move {
112            // Create an effect for each work item
113            let agent_effects: Vec<BoxedEffect<AgentExecutionResult, MapReduceError, MapEnv>> =
114                items
115                    .iter()
116                    .enumerate()
117                    .map(|(index, item)| {
118                        let agent_name = format!("agent-{}", index);
119                        execute_agent_effect(item, &agent_name, &parent_branch)
120                    })
121                    .collect();
122
123            // Execute all effects in parallel with bounded concurrency
124            // par_all_limit returns Vec<E> for errors, collecting all failures
125            par_all_limit(agent_effects, max_parallel, &env).await
126        }
127    })
128}
129
130/// Pure function to partition work items for parallel batching
131///
132/// This demonstrates how to plan parallel execution without I/O.
133/// Used for testing and validation before actual execution.
134pub fn plan_parallel_batches(item_count: usize, max_parallel: usize) -> Vec<Vec<usize>> {
135    let mut batches = Vec::new();
136    let mut current_batch = Vec::new();
137
138    for i in 0..item_count {
139        current_batch.push(i);
140        if current_batch.len() >= max_parallel {
141            batches.push(current_batch.clone());
142            current_batch.clear();
143        }
144    }
145
146    if !current_batch.is_empty() {
147        batches.push(current_batch);
148    }
149
150    batches
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_plan_parallel_batches() {
159        // Test with exact multiple of max_parallel
160        let batches = plan_parallel_batches(6, 2);
161        assert_eq!(batches.len(), 3);
162        assert_eq!(batches[0], vec![0, 1]);
163        assert_eq!(batches[1], vec![2, 3]);
164        assert_eq!(batches[2], vec![4, 5]);
165
166        // Test with remainder
167        let batches = plan_parallel_batches(5, 2);
168        assert_eq!(batches.len(), 3);
169        assert_eq!(batches[0], vec![0, 1]);
170        assert_eq!(batches[1], vec![2, 3]);
171        assert_eq!(batches[2], vec![4]);
172
173        // Test with max_parallel larger than item_count
174        let batches = plan_parallel_batches(3, 10);
175        assert_eq!(batches.len(), 1);
176        assert_eq!(batches[0], vec![0, 1, 2]);
177    }
178
179    #[test]
180    fn test_plan_parallel_batches_edge_cases() {
181        // Empty
182        let batches = plan_parallel_batches(0, 2);
183        assert_eq!(batches.len(), 0);
184
185        // Single item
186        let batches = plan_parallel_batches(1, 2);
187        assert_eq!(batches.len(), 1);
188        assert_eq!(batches[0], vec![0]);
189
190        // Max parallel = 1 (sequential)
191        let batches = plan_parallel_batches(5, 1);
192        assert_eq!(batches.len(), 5);
193        for (i, batch) in batches.iter().enumerate() {
194            assert_eq!(batch, &vec![i]);
195        }
196    }
197}