klieo_flows/
sequential.rs1use crate::error::FlowError;
4use crate::flow::Flow;
5use async_trait::async_trait;
6use klieo_core::agent::AgentContext;
7use serde_json::Value;
8use std::sync::Arc;
9
10pub struct SequentialFlow {
14 name: String,
15 steps: Vec<Arc<dyn Flow>>,
16}
17
18impl SequentialFlow {
19 pub fn new(name: impl Into<String>) -> Self {
21 Self {
22 name: name.into(),
23 steps: Vec::new(),
24 }
25 }
26
27 pub fn step(mut self, flow: Arc<dyn Flow>) -> Self {
29 self.steps.push(flow);
30 self
31 }
32
33 pub fn step_agent<A>(self, agent: A) -> Self
42 where
43 A: klieo_core::agent::Agent + 'static,
44 {
45 self.step(std::sync::Arc::new(crate::flow::AgentFlow::new(agent))
46 as std::sync::Arc<dyn crate::flow::Flow>)
47 }
48}
49
50#[async_trait]
51impl Flow for SequentialFlow {
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!("sequential_flow.run", flow = %self.name);
58 let _guard = span.enter();
59 let mut current = input;
60 for (i, step) in self.steps.iter().enumerate() {
61 crate::flow::bail_if_cancelled(&ctx)?;
62 tracing::debug!(step_index = i, step_name = step.name(), "sequential step");
63 current = step.run(ctx.clone(), current).await?;
64 }
65 Ok(current)
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::test_helpers::{cancelled_ctx, ctx};
73
74 struct Boom;
75 #[async_trait]
76 impl Flow for Boom {
77 fn name(&self) -> &str {
78 "boom"
79 }
80 async fn run(&self, _c: AgentContext, _i: Value) -> Result<Value, FlowError> {
81 panic!("step must not run under a cancelled context")
82 }
83 }
84
85 #[tokio::test]
86 async fn cancelled_ctx_returns_cancelled_before_any_step() {
87 let f = SequentialFlow::new("s").step(Arc::new(Boom));
88 let err = f
89 .run(cancelled_ctx(), serde_json::json!({}))
90 .await
91 .unwrap_err();
92 assert!(matches!(err, FlowError::Cancelled), "got {err:?}");
93 }
94
95 struct IncrementFlow {
96 name: String,
97 }
98
99 #[async_trait]
100 impl Flow for IncrementFlow {
101 fn name(&self) -> &str {
102 &self.name
103 }
104 async fn run(&self, _ctx: AgentContext, input: Value) -> Result<Value, FlowError> {
105 let n = input.get("n").and_then(|v| v.as_u64()).unwrap_or(0);
106 Ok(serde_json::json!({ "n": n + 1 }))
107 }
108 }
109
110 struct FailFlow;
111
112 #[async_trait]
113 impl Flow for FailFlow {
114 fn name(&self) -> &str {
115 "fail"
116 }
117 async fn run(&self, _ctx: AgentContext, _input: Value) -> Result<Value, FlowError> {
118 Err(FlowError::Agent("boom".into()))
119 }
120 }
121
122 #[tokio::test]
123 async fn empty_pipeline_returns_input_unchanged() {
124 let f = SequentialFlow::new("empty");
125 let out = f.run(ctx(), serde_json::json!({"n": 7})).await.unwrap();
126 assert_eq!(out, serde_json::json!({"n": 7}));
127 }
128
129 #[tokio::test]
130 async fn three_steps_increment_in_order() {
131 let f = SequentialFlow::new("incs")
132 .step(Arc::new(IncrementFlow { name: "a".into() }))
133 .step(Arc::new(IncrementFlow { name: "b".into() }))
134 .step(Arc::new(IncrementFlow { name: "c".into() }));
135 let out = f.run(ctx(), serde_json::json!({"n": 0})).await.unwrap();
136 assert_eq!(out, serde_json::json!({"n": 3}));
137 }
138
139 #[tokio::test]
140 async fn step_2_error_short_circuits() {
141 let f = SequentialFlow::new("with_fail")
142 .step(Arc::new(IncrementFlow { name: "a".into() }))
143 .step(Arc::new(FailFlow))
144 .step(Arc::new(IncrementFlow { name: "c".into() }));
145 let err = f.run(ctx(), serde_json::json!({"n": 0})).await.unwrap_err();
146 match err {
147 FlowError::Agent(s) => assert_eq!(s, "boom"),
148 other => panic!("expected Agent, got {other:?}"),
149 }
150 }
151
152 #[tokio::test]
153 async fn name_returns_configured_name() {
154 let f = SequentialFlow::new("my_pipe");
155 assert_eq!(f.name(), "my_pipe");
156 }
157}
158
159#[cfg(test)]
160mod step_agent_tests {
161 use super::*;
162 use crate::test_helpers::ctx;
163 use async_trait::async_trait;
164 use klieo_core::agent::{Agent, AgentContext};
165 use klieo_core::error::Error as CoreError;
166 use klieo_core::llm::ToolDef;
167 use serde::{Deserialize, Serialize};
168
169 #[derive(Deserialize, Serialize, PartialEq, Debug)]
170 struct EchoIn {
171 msg: String,
172 }
173 #[derive(Deserialize, Serialize, PartialEq, Debug)]
174 struct EchoOut {
175 echoed: String,
176 }
177
178 struct EchoAgent;
179
180 #[async_trait]
181 impl Agent for EchoAgent {
182 type Input = EchoIn;
183 type Output = EchoOut;
184 type Error = CoreError;
185 fn name(&self) -> &str {
186 "echo"
187 }
188 fn system_prompt(&self) -> &str {
189 ""
190 }
191 fn tools(&self) -> &[ToolDef] {
192 &[]
193 }
194 async fn run(&self, _ctx: AgentContext, input: EchoIn) -> Result<EchoOut, CoreError> {
195 Ok(EchoOut { echoed: input.msg })
196 }
197 }
198
199 #[tokio::test]
200 async fn step_agent_chains_typed_agent_without_upcast_boilerplate() {
201 let pipeline = SequentialFlow::new("step-agent-smoke").step_agent(EchoAgent);
202 let out = pipeline
203 .run(ctx(), serde_json::json!({"msg": "hi"}))
204 .await
205 .expect("run");
206 assert_eq!(out, serde_json::json!({"echoed": "hi"}));
207 }
208}