use crate::error::FlowError;
use klieo_core::agent::AgentContext;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
type StageFn<I, O> = dyn Fn(AgentContext, I) -> Pin<Box<dyn Future<Output = Result<O, FlowError>> + Send>>
+ Send
+ Sync;
pub struct TypedSequentialFlow<I, O> {
name: String,
stage: Arc<StageFn<I, O>>,
}
impl<I, O> TypedSequentialFlow<I, O>
where
I: Send + 'static,
O: Send + 'static,
{
pub fn new<F, Fut>(name: impl Into<String>, stage: F) -> Self
where
F: Fn(AgentContext, I) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<O, FlowError>> + Send + 'static,
{
let boxed: Arc<StageFn<I, O>> = Arc::new(move |ctx, input| Box::pin(stage(ctx, input)));
Self {
name: name.into(),
stage: boxed,
}
}
pub fn then<F, Fut, P>(self, stage: F) -> TypedSequentialFlow<I, P>
where
F: Fn(AgentContext, O) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<P, FlowError>> + Send + 'static,
P: Send + 'static,
{
let prev = self.stage;
let next = Arc::new(stage);
let combined: Arc<StageFn<I, P>> = Arc::new(move |ctx: AgentContext, input: I| {
let prev = Arc::clone(&prev);
let next = Arc::clone(&next);
Box::pin(async move {
let intermediate = prev(ctx.clone(), input).await?;
next(ctx, intermediate).await
})
});
TypedSequentialFlow {
name: self.name,
stage: combined,
}
}
pub async fn run(&self, ctx: AgentContext, input: I) -> Result<O, FlowError> {
let span = tracing::info_span!("typed_sequential_flow.run", flow = %self.name);
let _guard = span.enter();
(self.stage)(ctx, input).await
}
pub fn name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::ctx;
#[derive(Debug, PartialEq)]
struct Ticket {
subject: String,
}
#[derive(Debug, PartialEq)]
struct Classified {
ticket: Ticket,
category: String,
}
#[derive(Debug, PartialEq)]
struct Reply(String);
#[tokio::test]
async fn single_stage_pipeline_runs() {
let flow: TypedSequentialFlow<Ticket, Classified> =
TypedSequentialFlow::new("classify", |_ctx, t: Ticket| async move {
Ok(Classified {
ticket: t,
category: "billing".into(),
})
});
let out = flow
.run(
ctx(),
Ticket {
subject: "refund".into(),
},
)
.await
.unwrap();
assert_eq!(out.category, "billing");
assert_eq!(out.ticket.subject, "refund");
}
#[tokio::test]
async fn two_stages_compose_typed_values() {
let flow = TypedSequentialFlow::new("triage", |_ctx, t: Ticket| async move {
Ok::<_, FlowError>(Classified {
ticket: t,
category: "billing".into(),
})
})
.then(|_ctx, c: Classified| async move {
Ok::<_, FlowError>(Reply(format!("re[{}]: {}", c.category, c.ticket.subject)))
});
let reply = flow
.run(
ctx(),
Ticket {
subject: "refund".into(),
},
)
.await
.unwrap();
assert_eq!(reply, Reply("re[billing]: refund".into()));
}
#[tokio::test]
async fn first_stage_error_short_circuits() {
let flow = TypedSequentialFlow::new("triage", |_ctx, _t: Ticket| async {
Err::<Classified, _>(FlowError::Agent("first failed".into()))
})
.then(|_ctx, _c: Classified| async { Ok::<_, FlowError>(Reply("never reached".into())) });
let err = flow
.run(
ctx(),
Ticket {
subject: "x".into(),
},
)
.await
.unwrap_err();
match err {
FlowError::Agent(s) => assert_eq!(s, "first failed"),
other => panic!("expected Agent, got {other:?}"),
}
}
#[tokio::test]
async fn second_stage_error_propagates() {
let flow = TypedSequentialFlow::new("triage", |_ctx, t: Ticket| async move {
Ok::<_, FlowError>(Classified {
ticket: t,
category: "billing".into(),
})
})
.then(|_ctx, _c: Classified| async {
Err::<Reply, _>(FlowError::Agent("second failed".into()))
});
let err = flow
.run(
ctx(),
Ticket {
subject: "x".into(),
},
)
.await
.unwrap_err();
match err {
FlowError::Agent(s) => assert_eq!(s, "second failed"),
other => panic!("expected Agent, got {other:?}"),
}
}
#[tokio::test]
async fn three_stages_thread_in_order() {
let flow = TypedSequentialFlow::new("counter", |_ctx, n: u32| async move {
Ok::<_, FlowError>(n + 1)
})
.then(|_ctx, n: u32| async move { Ok::<_, FlowError>(n * 2) })
.then(|_ctx, n: u32| async move { Ok::<_, FlowError>(n.to_string()) });
let out = flow.run(ctx(), 0_u32).await.unwrap();
assert_eq!(out, "2");
}
#[tokio::test]
async fn name_returns_configured_name() {
let flow: TypedSequentialFlow<u32, u32> =
TypedSequentialFlow::new("mine", |_ctx, n: u32| async move { Ok(n) });
assert_eq!(flow.name(), "mine");
}
}