1use crate::error::FlowError;
4use async_trait::async_trait;
5use klieo_core::agent::{Agent, AgentContext};
6use serde_json::Value;
7use std::sync::Arc;
8
9pub(crate) fn bail_if_cancelled(ctx: &AgentContext) -> Result<(), FlowError> {
14 if ctx.cancel.is_cancelled() {
15 return Err(FlowError::Cancelled);
16 }
17 Ok(())
18}
19
20#[async_trait]
29pub trait Flow: Send + Sync {
30 fn name(&self) -> &str;
32
33 async fn run(&self, ctx: AgentContext, input: Value) -> Result<Value, FlowError>;
35}
36
37pub struct AgentFlow<A: Agent> {
45 agent: Arc<A>,
46}
47
48impl<A: Agent + 'static> AgentFlow<A> {
49 pub fn new(agent: A) -> Self {
51 Self {
52 agent: Arc::new(agent),
53 }
54 }
55
56 pub fn from_arc(agent: Arc<A>) -> Self {
59 Self { agent }
60 }
61}
62
63#[async_trait]
64impl<A: Agent + 'static> Flow for AgentFlow<A> {
65 fn name(&self) -> &str {
66 self.agent.name()
67 }
68
69 async fn run(&self, ctx: AgentContext, input: Value) -> Result<Value, FlowError> {
70 let span = tracing::info_span!("agent_flow.run", agent = %self.agent.name());
71 let _guard = span.enter();
72 let typed_input: A::Input = serde_json::from_value(input)?;
73 let typed_output = self
74 .agent
75 .run(ctx, typed_input)
76 .await
77 .map_err(|e| FlowError::Agent(e.to_string()))?;
78 Ok(serde_json::to_value(typed_output)?)
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::test_helpers::ctx;
86 use async_trait::async_trait;
87 use klieo_core::agent::{Agent, AgentContext};
88 use klieo_core::error::Error as CoreError;
89 use klieo_core::llm::ToolDef;
90 use serde::{Deserialize, Serialize};
91
92 #[derive(Deserialize, Serialize, PartialEq, Debug)]
93 struct EchoIn {
94 msg: String,
95 }
96 #[derive(Deserialize, Serialize, PartialEq, Debug)]
97 struct EchoOut {
98 echoed: String,
99 }
100
101 struct EchoAgent;
102
103 #[async_trait]
104 impl Agent for EchoAgent {
105 type Input = EchoIn;
106 type Output = EchoOut;
107 type Error = CoreError;
108 fn name(&self) -> &str {
109 "echo"
110 }
111 fn system_prompt(&self) -> &str {
112 ""
113 }
114 fn tools(&self) -> &[ToolDef] {
115 &[]
116 }
117 async fn run(&self, _ctx: AgentContext, input: EchoIn) -> Result<EchoOut, CoreError> {
118 Ok(EchoOut { echoed: input.msg })
119 }
120 }
121
122 struct FailingAgent;
123
124 #[async_trait]
125 impl Agent for FailingAgent {
126 type Input = EchoIn;
127 type Output = EchoOut;
128 type Error = CoreError;
129 fn name(&self) -> &str {
130 "failing"
131 }
132 fn system_prompt(&self) -> &str {
133 ""
134 }
135 fn tools(&self) -> &[ToolDef] {
136 &[]
137 }
138 async fn run(&self, _ctx: AgentContext, _input: EchoIn) -> Result<EchoOut, CoreError> {
139 Err(CoreError::Cancelled)
140 }
141 }
142
143 #[tokio::test]
144 async fn name_passes_through() {
145 let f = AgentFlow::new(EchoAgent);
146 assert_eq!(f.name(), "echo");
147 }
148
149 #[tokio::test]
150 async fn input_output_round_trip() {
151 let f = AgentFlow::new(EchoAgent);
152 let out = f
153 .run(ctx(), serde_json::json!({"msg": "hi"}))
154 .await
155 .unwrap();
156 assert_eq!(out, serde_json::json!({"echoed": "hi"}));
157 }
158
159 #[tokio::test]
160 async fn agent_error_is_stringified() {
161 let f = AgentFlow::new(FailingAgent);
162 let err = f
163 .run(ctx(), serde_json::json!({"msg": "x"}))
164 .await
165 .unwrap_err();
166 match err {
167 FlowError::Agent(s) => assert!(s.contains("cancelled")),
168 other => panic!("expected FlowError::Agent, got {other:?}"),
169 }
170 }
171}