1use crate::error::FlowError;
4use crate::flow::Flow;
5use async_trait::async_trait;
6use futures::future::try_join_all;
7use klieo_core::agent::AgentContext;
8use serde_json::{Map, Value};
9use std::sync::Arc;
10
11pub struct ParallelFlow {
15 name: String,
16 branches: Vec<(String, Arc<dyn Flow>)>,
17}
18
19impl ParallelFlow {
20 pub fn new(name: impl Into<String>) -> Self {
22 Self {
23 name: name.into(),
24 branches: Vec::new(),
25 }
26 }
27
28 pub fn branch(mut self, key: impl Into<String>, flow: Arc<dyn Flow>) -> Self {
31 self.branches.push((key.into(), flow));
32 self
33 }
34
35 pub fn branch_agent<A>(self, key: impl Into<String>, agent: A) -> Self
39 where
40 A: klieo_core::agent::Agent + 'static,
41 {
42 self.branch(
43 key,
44 std::sync::Arc::new(crate::flow::AgentFlow::new(agent))
45 as std::sync::Arc<dyn crate::flow::Flow>,
46 )
47 }
48}
49
50#[async_trait]
51impl Flow for ParallelFlow {
52 fn name(&self) -> &str {
53 &self.name
54 }
55
56 async fn run(&self, ctx: AgentContext, input: Value) -> Result<Value, FlowError> {
57 let span = tracing::info_span!(
58 "parallel_flow.run",
59 flow = %self.name,
60 branches = self.branches.len()
61 );
62 let _guard = span.enter();
63
64 crate::flow::bail_if_cancelled(&ctx)?;
65 let futs = self.branches.iter().map(|(key, flow)| {
66 let ctx = ctx.clone();
67 let input = input.clone();
68 let key = key.clone();
69 let flow = flow.clone();
70 async move {
71 let out = flow.run(ctx, input).await?;
72 Ok::<(String, Value), FlowError>((key, out))
73 }
74 });
75
76 let results = try_join_all(futs).await?;
77 let mut obj = Map::with_capacity(results.len());
78 for (k, v) in results {
79 obj.insert(k, v);
80 }
81 Ok(Value::Object(obj))
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::test_helpers::{cancelled_ctx, ctx};
89
90 struct Boom;
91 #[async_trait]
92 impl Flow for Boom {
93 fn name(&self) -> &str {
94 "boom"
95 }
96 async fn run(&self, _c: AgentContext, _i: Value) -> Result<Value, FlowError> {
97 panic!("branch must not run under a cancelled context")
98 }
99 }
100
101 #[tokio::test]
102 async fn cancelled_ctx_returns_cancelled_before_fan_out() {
103 let f = ParallelFlow::new("p").branch("b", Arc::new(Boom));
104 let err = f
105 .run(cancelled_ctx(), serde_json::json!({}))
106 .await
107 .unwrap_err();
108 assert!(matches!(err, FlowError::Cancelled), "got {err:?}");
109 }
110
111 struct ConstFlow {
112 name: String,
113 value: Value,
114 }
115
116 #[async_trait]
117 impl Flow for ConstFlow {
118 fn name(&self) -> &str {
119 &self.name
120 }
121 async fn run(&self, _ctx: AgentContext, _input: Value) -> Result<Value, FlowError> {
122 Ok(self.value.clone())
123 }
124 }
125
126 struct FailFlow;
127
128 #[async_trait]
129 impl Flow for FailFlow {
130 fn name(&self) -> &str {
131 "fail"
132 }
133 async fn run(&self, _ctx: AgentContext, _input: Value) -> Result<Value, FlowError> {
134 Err(FlowError::Agent("boom".into()))
135 }
136 }
137
138 #[tokio::test]
139 async fn three_branches_collated_by_key() {
140 let f = ParallelFlow::new("fan")
141 .branch(
142 "a",
143 Arc::new(ConstFlow {
144 name: "a".into(),
145 value: serde_json::json!(1),
146 }),
147 )
148 .branch(
149 "b",
150 Arc::new(ConstFlow {
151 name: "b".into(),
152 value: serde_json::json!("two"),
153 }),
154 )
155 .branch(
156 "c",
157 Arc::new(ConstFlow {
158 name: "c".into(),
159 value: serde_json::json!(true),
160 }),
161 );
162 let out = f.run(ctx(), serde_json::json!(null)).await.unwrap();
163 assert_eq!(out, serde_json::json!({"a": 1, "b": "two", "c": true}));
164 }
165
166 #[tokio::test]
167 async fn one_branch_error_fails_whole_flow() {
168 let f = ParallelFlow::new("p")
169 .branch(
170 "ok",
171 Arc::new(ConstFlow {
172 name: "ok".into(),
173 value: serde_json::json!(1),
174 }),
175 )
176 .branch("bad", Arc::new(FailFlow));
177 let err = f.run(ctx(), serde_json::json!(null)).await.unwrap_err();
178 assert!(matches!(err, FlowError::Agent(s) if s == "boom"));
179 }
180
181 #[tokio::test]
182 async fn empty_branch_list_returns_empty_object() {
183 let f = ParallelFlow::new("empty");
184 let out = f.run(ctx(), serde_json::json!({"x": 1})).await.unwrap();
185 assert_eq!(out, serde_json::json!({}));
186 }
187
188 #[tokio::test]
189 async fn branch_agent_output_appears_under_key() {
190 use async_trait::async_trait;
191 use klieo_core::agent::{Agent, AgentContext as Ctx};
192 use klieo_core::error::Error as CoreError;
193 use klieo_core::llm::ToolDef;
194 use serde::{Deserialize, Serialize};
195
196 #[derive(Deserialize, Serialize)]
197 struct In {
198 msg: String,
199 }
200 #[derive(Deserialize, Serialize, PartialEq, Debug)]
201 struct Out {
202 echoed: String,
203 }
204
205 struct EchoAgent;
206
207 #[async_trait]
208 impl Agent for EchoAgent {
209 type Input = In;
210 type Output = Out;
211 type Error = CoreError;
212 fn name(&self) -> &str {
213 "echo"
214 }
215 fn system_prompt(&self) -> &str {
216 ""
217 }
218 fn tools(&self) -> &[ToolDef] {
219 &[]
220 }
221 async fn run(&self, _ctx: Ctx, input: In) -> Result<Out, CoreError> {
222 Ok(Out { echoed: input.msg })
223 }
224 }
225
226 let f = ParallelFlow::new("p").branch_agent("echo_key", EchoAgent);
227 let out = f
228 .run(ctx(), serde_json::json!({"msg": "hello"}))
229 .await
230 .unwrap();
231 assert_eq!(out["echo_key"]["echoed"], "hello");
232 }
233
234 #[tokio::test]
235 async fn branch_agent_error_propagates_from_parallel_run() {
236 use async_trait::async_trait;
237 use klieo_core::agent::{Agent, AgentContext as Ctx};
238 use klieo_core::error::Error as CoreError;
239 use klieo_core::llm::ToolDef;
240 use serde_json::Value;
241
242 struct AlwaysFailAgent;
243
244 #[async_trait]
245 impl Agent for AlwaysFailAgent {
246 type Input = Value;
247 type Output = Value;
248 type Error = CoreError;
249 fn name(&self) -> &str {
250 "always_fail"
251 }
252 fn system_prompt(&self) -> &str {
253 ""
254 }
255 fn tools(&self) -> &[ToolDef] {
256 &[]
257 }
258 async fn run(&self, _ctx: Ctx, _input: Value) -> Result<Value, CoreError> {
259 Err(CoreError::Other {
260 message: "injected agent failure".into(),
261 source: None,
262 })
263 }
264 }
265
266 let f = ParallelFlow::new("p").branch_agent("failing_key", AlwaysFailAgent);
267 let result = f.run(ctx(), serde_json::json!({})).await;
268 assert!(
269 result.is_err(),
270 "branch_agent error must propagate to ParallelFlow::run"
271 );
272 assert!(
273 matches!(result.unwrap_err(), FlowError::Agent(_)),
274 "error must surface as FlowError::Agent"
275 );
276 }
277
278 #[tokio::test]
279 async fn weird_keys_preserved_verbatim() {
280 let f = ParallelFlow::new("weird")
281 .branch(
282 "dot.key",
283 Arc::new(ConstFlow {
284 name: "n1".into(),
285 value: serde_json::json!(1),
286 }),
287 )
288 .branch(
289 "dash-key",
290 Arc::new(ConstFlow {
291 name: "n2".into(),
292 value: serde_json::json!(2),
293 }),
294 );
295 let out = f.run(ctx(), serde_json::json!(null)).await.unwrap();
296 assert_eq!(out, serde_json::json!({"dot.key": 1, "dash-key": 2}));
297 }
298}