1use std::collections::HashMap;
4use std::sync::Arc;
5
6use swink_agent::{Agent, AgentMessage, AgentResult, ContentBlock, LlmMessage};
7use tokio_util::sync::CancellationToken;
8
9use super::events::PipelineEvent;
10use super::output::{PipelineError, PipelineOutput, StepResult};
11use super::registry::PipelineRegistry;
12use super::types::{Pipeline, PipelineId};
13
14pub trait AgentFactory: Send + Sync {
18 fn create(&self, name: &str) -> Result<Agent, PipelineError>;
20}
21
22pub struct SimpleAgentFactory {
26 builders: HashMap<String, Arc<dyn Fn() -> Agent + Send + Sync>>,
27}
28
29impl SimpleAgentFactory {
30 pub fn new() -> Self {
32 Self {
33 builders: HashMap::new(),
34 }
35 }
36
37 pub fn register(
39 &mut self,
40 name: impl Into<String>,
41 builder: impl Fn() -> Agent + Send + Sync + 'static,
42 ) {
43 self.builders.insert(name.into(), Arc::new(builder));
44 }
45}
46
47impl Default for SimpleAgentFactory {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl AgentFactory for SimpleAgentFactory {
54 fn create(&self, name: &str) -> Result<Agent, PipelineError> {
55 let builder = self
56 .builders
57 .get(name)
58 .ok_or_else(|| PipelineError::AgentNotFound {
59 name: name.to_owned(),
60 })?;
61 Ok(builder())
62 }
63}
64
65pub struct PipelineExecutor {
69 factory: Arc<dyn AgentFactory>,
70 registry: Arc<PipelineRegistry>,
71 event_handler: Option<Arc<dyn Fn(PipelineEvent) + Send + Sync>>,
72}
73
74impl PipelineExecutor {
75 pub fn new(factory: Arc<dyn AgentFactory>, registry: Arc<PipelineRegistry>) -> Self {
77 Self {
78 factory,
79 registry,
80 event_handler: None,
81 }
82 }
83
84 #[must_use]
86 pub fn with_event_handler(
87 mut self,
88 handler: impl Fn(PipelineEvent) + Send + Sync + 'static,
89 ) -> Self {
90 self.event_handler = Some(Arc::new(handler));
91 self
92 }
93
94 fn emit(&self, event: PipelineEvent) {
96 if let Some(handler) = &self.event_handler {
97 handler(event);
98 }
99 }
100
101 pub async fn run(
103 &self,
104 pipeline_id: &PipelineId,
105 input: String,
106 cancellation_token: CancellationToken,
107 ) -> Result<PipelineOutput, PipelineError> {
108 let Some(pipeline) = self.registry.get(pipeline_id) else {
109 let err = PipelineError::PipelineNotFound {
110 id: pipeline_id.clone(),
111 };
112 self.emit(PipelineEvent::Failed {
113 pipeline_id: pipeline_id.clone(),
114 error_message: err.to_string(),
115 });
116 return Err(err);
117 };
118
119 let result = match pipeline {
120 Pipeline::Sequential {
121 id,
122 name,
123 steps,
124 pass_context,
125 } => {
126 self.run_sequential(id, name, steps, pass_context, input, cancellation_token)
127 .await
128 }
129 Pipeline::Parallel {
130 id,
131 name,
132 branches,
133 merge_strategy,
134 } => {
135 super::parallel::run_parallel(
136 &self.factory,
137 self.event_handler.as_ref(),
138 id,
139 name,
140 branches,
141 merge_strategy,
142 input,
143 cancellation_token,
144 )
145 .await
146 }
147 Pipeline::Loop {
148 id,
149 name,
150 body,
151 exit_condition,
152 max_iterations,
153 } => {
154 super::loop_exec::run_loop(
155 &self.factory,
156 self.event_handler.as_ref(),
157 id,
158 name,
159 body,
160 exit_condition,
161 max_iterations,
162 input,
163 cancellation_token,
164 )
165 .await
166 }
167 };
168
169 if let Err(err) = &result {
170 self.emit(PipelineEvent::Failed {
171 pipeline_id: pipeline_id.clone(),
172 error_message: err.to_string(),
173 });
174 }
175
176 result
177 }
178
179 async fn run_sequential(
180 &self,
181 id: PipelineId,
182 name: String,
183 steps: Vec<String>,
184 pass_context: bool,
185 input: String,
186 cancellation_token: CancellationToken,
187 ) -> Result<PipelineOutput, PipelineError> {
188 let start = std::time::Instant::now();
189 let mut step_results = Vec::new();
190 let mut current_input = input;
191 let mut total_usage = swink_agent::Usage::default();
192 let mut context_messages: Vec<LlmMessage> = Vec::new();
194
195 self.emit(PipelineEvent::Started {
196 pipeline_id: id.clone(),
197 pipeline_name: name,
198 });
199
200 for (index, agent_name) in steps.iter().enumerate() {
201 if cancellation_token.is_cancelled() {
202 return Err(PipelineError::Cancelled);
203 }
204
205 self.emit(PipelineEvent::StepStarted {
206 pipeline_id: id.clone(),
207 step_index: index,
208 agent_name: agent_name.clone(),
209 });
210
211 let step_start = std::time::Instant::now();
212 let mut agent = self.factory.create(agent_name)?;
213
214 let messages = if pass_context && !context_messages.is_empty() {
216 let mut msgs: Vec<AgentMessage> = context_messages
217 .iter()
218 .map(|llm| AgentMessage::Llm(llm.clone()))
219 .collect();
220 msgs.push(user_msg(¤t_input));
221 msgs
222 } else {
223 vec![user_msg(¤t_input)]
224 };
225
226 let result =
227 agent
228 .prompt_async(messages)
229 .await
230 .map_err(|e| PipelineError::StepFailed {
231 step_index: index,
232 agent_name: agent_name.clone(),
233 source: Box::new(e),
234 })?;
235
236 let response = extract_text_response(&result);
237 let step_duration = step_start.elapsed();
238
239 total_usage += result.usage.clone();
240
241 self.emit(PipelineEvent::StepCompleted {
242 pipeline_id: id.clone(),
243 step_index: index,
244 agent_name: agent_name.clone(),
245 duration: step_duration,
246 usage: result.usage.clone(),
247 });
248
249 step_results.push(StepResult {
250 agent_name: agent_name.clone(),
251 response: response.clone(),
252 duration: step_duration,
253 usage: result.usage.clone(),
254 });
255
256 if pass_context {
258 context_messages.push(LlmMessage::User(
260 swink_agent::UserMessage::new(vec![ContentBlock::Text {
261 text: current_input.clone(),
262 }])
263 .with_timestamp(0),
264 ));
265 for msg in &result.messages {
267 if let AgentMessage::Llm(llm @ LlmMessage::Assistant(_)) = msg {
268 context_messages.push(llm.clone());
269 }
270 }
271 }
272
273 current_input = response;
274 }
275
276 let total_duration = start.elapsed();
277 let final_response = step_results
278 .last()
279 .map(|s| s.response.clone())
280 .unwrap_or_default();
281
282 self.emit(PipelineEvent::Completed {
283 pipeline_id: id.clone(),
284 total_duration,
285 total_usage: total_usage.clone(),
286 });
287
288 Ok(PipelineOutput {
289 pipeline_id: id,
290 final_response,
291 steps: step_results,
292 total_duration,
293 total_usage,
294 })
295 }
296}
297
298fn user_msg(text: &str) -> AgentMessage {
300 AgentMessage::Llm(LlmMessage::User(
301 swink_agent::UserMessage::new(vec![ContentBlock::Text {
302 text: text.to_string(),
303 }])
304 .with_timestamp(0),
305 ))
306}
307
308fn extract_text_response(result: &AgentResult) -> String {
310 result
311 .messages
312 .iter()
313 .rev()
314 .find_map(|m| match m {
315 AgentMessage::Llm(LlmMessage::Assistant(msg)) => Some(msg),
316 _ => None,
317 })
318 .map(|msg| {
319 msg.content
320 .iter()
321 .filter_map(|b| match b {
322 ContentBlock::Text { text } => Some(text.as_str()),
323 _ => None,
324 })
325 .collect::<String>()
326 })
327 .unwrap_or_default()
328}
329
330#[cfg(all(test, feature = "testkit"))]
331mod tests {
332 use super::*;
333 use std::sync::Arc;
334 use swink_agent::AgentOptions;
335 use swink_agent::testing::{MockStreamFn, default_convert, default_model, text_only_events};
336
337 fn make_agent() -> Agent {
338 let options = AgentOptions::new(
339 "test",
340 default_model(),
341 Arc::new(MockStreamFn::new(vec![])),
342 default_convert,
343 );
344 Agent::new(options)
345 }
346
347 fn make_text_agent(text: &str) -> Agent {
348 let events = text_only_events(text);
349 let options = AgentOptions::new(
350 "test",
351 default_model(),
352 Arc::new(MockStreamFn::new(vec![events])),
353 default_convert,
354 );
355 Agent::new(options)
356 }
357
358 #[test]
361 fn factory_create_registered_agent_succeeds() {
362 let mut factory = SimpleAgentFactory::new();
363 factory.register("test-agent", make_agent);
364
365 let result = factory.create("test-agent");
366 assert!(result.is_ok());
367 }
368
369 #[test]
370 fn factory_create_unknown_returns_agent_not_found() {
371 let factory = SimpleAgentFactory::new();
372
373 let result = factory.create("nonexistent");
374 assert!(matches!(
375 result,
376 Err(PipelineError::AgentNotFound { name }) if name == "nonexistent"
377 ));
378 }
379
380 fn build_executor(factory: SimpleAgentFactory, registry: PipelineRegistry) -> PipelineExecutor {
383 PipelineExecutor::new(Arc::new(factory), Arc::new(registry))
384 }
385
386 #[tokio::test]
387 async fn sequential_two_step_pipeline() {
388 let mut factory = SimpleAgentFactory::new();
389 factory.register("agent-a", || make_text_agent("hello"));
390 factory.register("agent-b", || make_text_agent("world"));
391
392 let registry = PipelineRegistry::new();
393 let pipeline = Pipeline::sequential("two-step", vec!["agent-a".into(), "agent-b".into()]);
394 let id = pipeline.id().clone();
395 registry.register(pipeline);
396
397 let executor = build_executor(factory, registry);
398 let token = CancellationToken::new();
399
400 let output = executor.run(&id, "input".into(), token).await.unwrap();
401 assert_eq!(output.final_response, "world");
402 assert_eq!(output.steps.len(), 2);
403 assert_eq!(output.steps[0].agent_name, "agent-a");
404 assert_eq!(output.steps[0].response, "hello");
405 assert_eq!(output.steps[1].agent_name, "agent-b");
406 assert_eq!(output.steps[1].response, "world");
407 }
408
409 #[tokio::test]
410 async fn sequential_missing_step_agent_halts_with_error() {
411 let mut factory = SimpleAgentFactory::new();
413 factory.register("agent-a", || make_text_agent("step-one"));
414 factory.register("agent-c", || make_text_agent("step-three"));
416
417 let registry = PipelineRegistry::new();
418 let pipeline = Pipeline::sequential(
419 "three-step",
420 vec!["agent-a".into(), "agent-b".into(), "agent-c".into()],
421 );
422 let id = pipeline.id().clone();
423 registry.register(pipeline);
424
425 let executor = build_executor(factory, registry);
426 let token = CancellationToken::new();
427
428 let result = executor.run(&id, "input".into(), token).await;
429 assert!(result.is_err(), "expected error when step agent not found");
430 assert!(
431 matches!(result.unwrap_err(), PipelineError::AgentNotFound { name } if name == "agent-b"),
432 "expected AgentNotFound for agent-b"
433 );
434 }
435
436 #[tokio::test]
437 async fn sequential_missing_agent_returns_agent_not_found() {
438 let factory = SimpleAgentFactory::new(); let registry = PipelineRegistry::new();
441 let pipeline = Pipeline::sequential("missing", vec!["ghost".into()]);
442 let id = pipeline.id().clone();
443 registry.register(pipeline);
444
445 let executor = build_executor(factory, registry);
446 let token = CancellationToken::new();
447
448 let result = executor.run(&id, "input".into(), token).await;
449 assert!(matches!(
450 result,
451 Err(PipelineError::AgentNotFound { name }) if name == "ghost"
452 ));
453 }
454
455 #[tokio::test]
456 async fn sequential_zero_steps_returns_empty() {
457 let factory = SimpleAgentFactory::new();
458
459 let registry = PipelineRegistry::new();
460 let pipeline = Pipeline::sequential("empty", vec![]);
461 let id = pipeline.id().clone();
462 registry.register(pipeline);
463
464 let executor = build_executor(factory, registry);
465 let token = CancellationToken::new();
466
467 let output = executor.run(&id, "input".into(), token).await.unwrap();
468 assert!(output.steps.is_empty());
469 assert!(output.final_response.is_empty());
470 }
471
472 #[tokio::test]
473 async fn run_unknown_pipeline_returns_not_found() {
474 let factory = SimpleAgentFactory::new();
475 let registry = PipelineRegistry::new();
476
477 let executor = build_executor(factory, registry);
478 let token = CancellationToken::new();
479 let unknown_id = PipelineId::new("nonexistent");
480
481 let result = executor.run(&unknown_id, "input".into(), token).await;
482 assert!(matches!(
483 result,
484 Err(PipelineError::PipelineNotFound { id }) if id == unknown_id
485 ));
486 }
487}