use crate::error::FlowError;
use async_trait::async_trait;
use klieo_core::agent::{Agent, AgentContext};
use serde_json::Value;
use std::sync::Arc;
pub(crate) fn bail_if_cancelled(ctx: &AgentContext) -> Result<(), FlowError> {
if ctx.cancel.is_cancelled() {
return Err(FlowError::Cancelled);
}
Ok(())
}
#[async_trait]
pub trait Flow: Send + Sync {
fn name(&self) -> &str;
async fn run(&self, ctx: AgentContext, input: Value) -> Result<Value, FlowError>;
}
pub struct AgentFlow<A: Agent> {
agent: Arc<A>,
}
impl<A: Agent + 'static> AgentFlow<A> {
pub fn new(agent: A) -> Self {
Self {
agent: Arc::new(agent),
}
}
pub fn from_arc(agent: Arc<A>) -> Self {
Self { agent }
}
}
#[async_trait]
impl<A: Agent + 'static> Flow for AgentFlow<A> {
fn name(&self) -> &str {
self.agent.name()
}
async fn run(&self, ctx: AgentContext, input: Value) -> Result<Value, FlowError> {
let span = tracing::info_span!("agent_flow.run", agent = %self.agent.name());
let _guard = span.enter();
let typed_input: A::Input = serde_json::from_value(input)?;
let typed_output = self
.agent
.run(ctx, typed_input)
.await
.map_err(|e| FlowError::Agent(e.to_string()))?;
Ok(serde_json::to_value(typed_output)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::ctx;
use async_trait::async_trait;
use klieo_core::agent::{Agent, AgentContext};
use klieo_core::error::Error as CoreError;
use klieo_core::llm::ToolDef;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, PartialEq, Debug)]
struct EchoIn {
msg: String,
}
#[derive(Deserialize, Serialize, PartialEq, Debug)]
struct EchoOut {
echoed: String,
}
struct EchoAgent;
#[async_trait]
impl Agent for EchoAgent {
type Input = EchoIn;
type Output = EchoOut;
type Error = CoreError;
fn name(&self) -> &str {
"echo"
}
fn system_prompt(&self) -> &str {
""
}
fn tools(&self) -> &[ToolDef] {
&[]
}
async fn run(&self, _ctx: AgentContext, input: EchoIn) -> Result<EchoOut, CoreError> {
Ok(EchoOut { echoed: input.msg })
}
}
struct FailingAgent;
#[async_trait]
impl Agent for FailingAgent {
type Input = EchoIn;
type Output = EchoOut;
type Error = CoreError;
fn name(&self) -> &str {
"failing"
}
fn system_prompt(&self) -> &str {
""
}
fn tools(&self) -> &[ToolDef] {
&[]
}
async fn run(&self, _ctx: AgentContext, _input: EchoIn) -> Result<EchoOut, CoreError> {
Err(CoreError::Cancelled)
}
}
#[tokio::test]
async fn name_passes_through() {
let f = AgentFlow::new(EchoAgent);
assert_eq!(f.name(), "echo");
}
#[tokio::test]
async fn input_output_round_trip() {
let f = AgentFlow::new(EchoAgent);
let out = f
.run(ctx(), serde_json::json!({"msg": "hi"}))
.await
.unwrap();
assert_eq!(out, serde_json::json!({"echoed": "hi"}));
}
#[tokio::test]
async fn agent_error_is_stringified() {
let f = AgentFlow::new(FailingAgent);
let err = f
.run(ctx(), serde_json::json!({"msg": "x"}))
.await
.unwrap_err();
match err {
FlowError::Agent(s) => assert!(s.contains("cancelled")),
other => panic!("expected FlowError::Agent, got {other:?}"),
}
}
}