1use crate::agent::executor::event_helper::EventHelper;
2use crate::agent::executor::turn_engine::{
3 TurnDelta, TurnEngine, TurnEngineConfig, TurnEngineError, TurnEngineOutput, record_task_state,
4};
5use crate::agent::hooks::HookOutcome;
6use crate::agent::task::Task;
7use crate::agent::{AgentDeriveT, AgentExecutor, AgentHooks, Context, ExecutorConfig};
8use crate::channel::channel;
9use crate::tool::{ToolCallResult, ToolT};
10use crate::utils::stream_from_producer;
11use async_trait::async_trait;
12use autoagents_llm::ToolCall;
13use autoagents_llm::error::LLMError;
14#[cfg(target_arch = "wasm32")]
15use futures::SinkExt;
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use std::ops::Deref;
19use std::sync::Arc;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct BasicAgentOutput {
24 pub response: String,
25 pub done: bool,
26}
27
28impl From<BasicAgentOutput> for Value {
29 fn from(output: BasicAgentOutput) -> Self {
30 serde_json::to_value(output).unwrap_or(Value::Null)
31 }
32}
33impl From<BasicAgentOutput> for String {
34 fn from(output: BasicAgentOutput) -> Self {
35 output.response
36 }
37}
38
39impl BasicAgentOutput {
40 pub fn try_parse<T: for<'de> serde::Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
43 serde_json::from_str::<T>(&self.response)
44 }
45
46 pub fn parse_or_map<T, F>(&self, fallback: F) -> T
49 where
50 T: for<'de> serde::Deserialize<'de>,
51 F: FnOnce(&str) -> T,
52 {
53 self.try_parse::<T>()
54 .unwrap_or_else(|_| fallback(&self.response))
55 }
56}
57
58#[derive(Debug, thiserror::Error)]
60pub enum BasicExecutorError {
61 #[error("LLM error: {0}")]
62 LLMError(
63 #[from]
64 #[source]
65 LLMError,
66 ),
67
68 #[error("Other error: {0}")]
69 Other(String),
70}
71
72impl From<TurnEngineError> for BasicExecutorError {
73 fn from(error: TurnEngineError) -> Self {
74 match error {
75 TurnEngineError::LLMError(err) => err.into(),
76 TurnEngineError::Aborted => {
77 BasicExecutorError::Other("Run aborted by hook".to_string())
78 }
79 TurnEngineError::Other(err) => BasicExecutorError::Other(err),
80 }
81 }
82}
83
84#[derive(Debug)]
89pub struct BasicAgent<T: AgentDeriveT> {
90 inner: Arc<T>,
91}
92
93impl<T: AgentDeriveT> Clone for BasicAgent<T> {
94 fn clone(&self) -> Self {
95 Self {
96 inner: Arc::clone(&self.inner),
97 }
98 }
99}
100
101impl<T: AgentDeriveT> BasicAgent<T> {
102 pub fn new(inner: T) -> Self {
103 Self {
104 inner: Arc::new(inner),
105 }
106 }
107}
108
109impl<T: AgentDeriveT> Deref for BasicAgent<T> {
110 type Target = T;
111
112 fn deref(&self) -> &Self::Target {
113 &self.inner
114 }
115}
116
117#[async_trait]
119impl<T: AgentDeriveT> AgentDeriveT for BasicAgent<T> {
120 type Output = <T as AgentDeriveT>::Output;
121
122 fn description(&self) -> &str {
123 self.inner.description()
124 }
125
126 fn output_schema(&self) -> Option<Value> {
127 self.inner.output_schema()
128 }
129
130 fn name(&self) -> &str {
131 self.inner.name()
132 }
133
134 fn tools(&self) -> Vec<Box<dyn ToolT>> {
135 self.inner.tools()
136 }
137}
138
139#[async_trait]
140impl<T> AgentHooks for BasicAgent<T>
141where
142 T: AgentDeriveT + AgentHooks + Send + Sync + 'static,
143{
144 async fn on_agent_create(&self) {
145 self.inner.on_agent_create().await
146 }
147
148 async fn on_run_start(&self, task: &Task, ctx: &Context) -> HookOutcome {
149 self.inner.on_run_start(task, ctx).await
150 }
151
152 async fn on_run_complete(&self, task: &Task, result: &Self::Output, ctx: &Context) {
153 self.inner.on_run_complete(task, result, ctx).await
154 }
155
156 async fn on_turn_start(&self, turn_index: usize, ctx: &Context) {
157 self.inner.on_turn_start(turn_index, ctx).await
158 }
159
160 async fn on_turn_complete(&self, turn_index: usize, ctx: &Context) {
161 self.inner.on_turn_complete(turn_index, ctx).await
162 }
163
164 async fn on_tool_call(&self, tool_call: &ToolCall, ctx: &Context) -> HookOutcome {
165 self.inner.on_tool_call(tool_call, ctx).await
166 }
167
168 async fn on_tool_start(&self, tool_call: &ToolCall, ctx: &Context) {
169 self.inner.on_tool_start(tool_call, ctx).await
170 }
171
172 async fn on_tool_result(&self, tool_call: &ToolCall, result: &ToolCallResult, ctx: &Context) {
173 self.inner.on_tool_result(tool_call, result, ctx).await
174 }
175
176 async fn on_tool_error(&self, tool_call: &ToolCall, err: Value, ctx: &Context) {
177 self.inner.on_tool_error(tool_call, err, ctx).await
178 }
179 async fn on_agent_shutdown(&self) {
180 self.inner.on_agent_shutdown().await
181 }
182}
183
184#[cfg_attr(all(target_arch = "wasm32", target_os = "wasi"), async_trait(?Send))]
186#[cfg_attr(not(all(target_arch = "wasm32", target_os = "wasi")), async_trait)]
187impl<T: AgentDeriveT + AgentHooks> AgentExecutor for BasicAgent<T> {
188 type Output = BasicAgentOutput;
189 type Error = BasicExecutorError;
190
191 fn config(&self) -> ExecutorConfig {
192 ExecutorConfig { max_turns: 1 }
193 }
194
195 async fn execute(
196 &self,
197 task: &Task,
198 context: Arc<Context>,
199 ) -> Result<Self::Output, Self::Error> {
200 record_task_state(&context, task);
201 let tx_event = context.tx().ok();
202 EventHelper::send_task_started(
203 &tx_event,
204 task.submission_id,
205 context.config().id,
206 context.config().name.clone(),
207 task.prompt.clone(),
208 )
209 .await;
210
211 let engine = TurnEngine::new(TurnEngineConfig::basic(self.config().max_turns));
212 let mut turn_state = engine.turn_state(&context);
213 let turn_result = engine
214 .run_turn(
215 self,
216 task,
217 &context,
218 &mut turn_state,
219 0,
220 self.config().max_turns,
221 )
222 .await?;
223
224 let output = extract_turn_output(turn_result);
225
226 Ok(BasicAgentOutput {
227 response: output.response,
228 done: true,
229 })
230 }
231
232 async fn execute_stream(
233 &self,
234 task: &Task,
235 context: Arc<Context>,
236 ) -> Result<crate::utils::BoxRuntimeStream<Result<Self::Output, Self::Error>>, Self::Error>
237 {
238 record_task_state(&context, task);
239 let tx_event = context.tx().ok();
240 EventHelper::send_task_started(
241 &tx_event,
242 task.submission_id,
243 context.config().id,
244 context.config().name.clone(),
245 task.prompt.clone(),
246 )
247 .await;
248
249 let engine = TurnEngine::new(TurnEngineConfig::basic(self.config().max_turns));
250 let mut turn_state = engine.turn_state(&context);
251 let context_clone = context.clone();
252 let task = task.clone();
253 let executor = self.clone();
254
255 #[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
256 let (mut tx, rx) = channel::<Result<BasicAgentOutput, BasicExecutorError>>(100);
257
258 let producer = async move {
259 let turn_stream = engine
260 .run_turn_stream(
261 executor,
262 &task,
263 context_clone.clone(),
264 &mut turn_state,
265 0,
266 1,
267 )
268 .await;
269
270 let mut final_response = String::default();
271 match turn_stream {
272 Ok(mut stream) => {
273 use futures::StreamExt;
274 while let Some(delta_result) = stream.next().await {
275 match delta_result {
276 Ok(TurnDelta::Text(content)) => {
277 let _ = tx
278 .send(Ok(BasicAgentOutput {
279 response: content,
280 done: false,
281 }))
282 .await;
283 }
284 Ok(TurnDelta::ReasoningContent(_)) => {}
285 Ok(TurnDelta::ToolResults(_)) => {}
286 Ok(TurnDelta::Done(result)) => {
287 let output = extract_turn_output(result);
288 final_response = output.response.clone();
289 let _ = tx
290 .send(Ok(BasicAgentOutput {
291 response: output.response,
292 done: true,
293 }))
294 .await;
295 break;
296 }
297 Err(err) => {
298 let _ = tx.send(Err(err.into())).await;
299 return;
300 }
301 }
302 }
303 }
304 Err(err) => {
305 let _ = tx.send(Err(err.into())).await;
306 return;
307 }
308 }
309
310 let tx_event = context_clone.tx().ok();
311 EventHelper::send_stream_complete(&tx_event, task.submission_id).await;
312 let output = BasicAgentOutput {
313 response: final_response,
314 done: true,
315 };
316 let result =
317 serde_json::to_string_pretty(&output).unwrap_or_else(|_| output.response.clone());
318 EventHelper::send_task_completed(
319 &tx_event,
320 task.submission_id,
321 context_clone.config().id,
322 context_clone.config().name.clone(),
323 result,
324 )
325 .await;
326 };
327
328 Ok(stream_from_producer(rx, producer))
329 }
330}
331
332fn extract_turn_output(
333 result: crate::agent::executor::TurnResult<TurnEngineOutput>,
334) -> TurnEngineOutput {
335 match result {
336 crate::agent::executor::TurnResult::Complete(output) => output,
337 crate::agent::executor::TurnResult::Continue(Some(output)) => output,
338 crate::agent::executor::TurnResult::Continue(None) => TurnEngineOutput {
339 response: String::default(),
340 reasoning_content: String::default(),
341 tool_calls: Vec::default(),
342 },
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use crate::agent::AgentDeriveT;
350 use crate::tests::{ConfigurableLLMProvider, MockAgentImpl, MockLLMProvider};
351 use async_trait::async_trait;
352 use autoagents_llm::chat::{StreamChoice, StreamDelta, StreamResponse};
353 use std::sync::Arc;
354
355 #[derive(Debug, Clone)]
356 struct AbortAgent;
357
358 #[async_trait]
359 impl AgentDeriveT for AbortAgent {
360 type Output = String;
361
362 fn description(&self) -> &str {
363 "abort"
364 }
365
366 fn output_schema(&self) -> Option<Value> {
367 None
368 }
369
370 fn name(&self) -> &str {
371 "abort_agent"
372 }
373
374 fn tools(&self) -> Vec<Box<dyn ToolT>> {
375 vec![]
376 }
377 }
378
379 #[async_trait]
380 impl AgentHooks for AbortAgent {
381 async fn on_run_start(&self, _task: &Task, _ctx: &Context) -> HookOutcome {
382 HookOutcome::Abort
383 }
384 }
385
386 #[tokio::test]
387 async fn test_basic_agent_execute() {
388 use crate::agent::task::Task;
389 use crate::agent::{AgentConfig, Context};
390 use autoagents_protocol::ActorID;
391
392 let mock_agent = MockAgentImpl::new("test_agent", "Test agent description");
393 let basic_agent = BasicAgent::new(mock_agent);
394
395 let llm = Arc::new(MockLLMProvider {});
396 let config = AgentConfig {
397 id: ActorID::new_v4(),
398 name: "test_agent".to_string(),
399 description: "Test agent description".to_string(),
400 output_schema: None,
401 };
402
403 let context = Context::new(llm, None).with_config(config);
404
405 let context_arc = Arc::new(context);
406 let task = Task::new("Test task");
407 let result = basic_agent.execute(&task, context_arc).await;
408
409 assert!(result.is_ok());
410 let output = result.unwrap();
411 assert_eq!(output.response, "Mock response");
412 assert!(output.done);
413 }
414
415 #[test]
416 fn test_basic_agent_metadata_and_output_conversion() {
417 let mock_agent = MockAgentImpl::new("test_agent", "Test agent description");
418 let basic_agent = BasicAgent::new(mock_agent);
419
420 let config = basic_agent.config();
421 assert_eq!(config.max_turns, 1);
422
423 let cloned = basic_agent.clone();
424 assert_eq!(cloned.name(), "test_agent");
425 assert_eq!(cloned.description(), "Test agent description");
426
427 let output = BasicAgentOutput {
428 response: "Test response".to_string(),
429 done: true,
430 };
431 let value: Value = output.clone().into();
432 assert_eq!(value["response"], "Test response");
433 let string: String = output.into();
434 assert_eq!(string, "Test response");
435 }
436
437 #[test]
438 fn test_basic_agent_output_try_parse_success() {
439 let output = BasicAgentOutput {
440 response: r#"{"name":"test","value":42}"#.to_string(),
441 done: true,
442 };
443 #[derive(serde::Deserialize, PartialEq, Debug)]
444 struct Data {
445 name: String,
446 value: i32,
447 }
448 let parsed: Data = output.try_parse().unwrap();
449 assert_eq!(
450 parsed,
451 Data {
452 name: "test".to_string(),
453 value: 42
454 }
455 );
456 }
457
458 #[test]
459 fn test_basic_agent_output_try_parse_failure() {
460 let output = BasicAgentOutput {
461 response: "not json".to_string(),
462 done: true,
463 };
464 let result = output.try_parse::<serde_json::Value>();
465 assert!(result.is_err());
466 }
467
468 #[test]
469 fn test_basic_agent_output_parse_or_map_fallback() {
470 let output = BasicAgentOutput {
471 response: "plain text".to_string(),
472 done: true,
473 };
474 let result: String = output.parse_or_map(|s| s.to_uppercase());
475 assert_eq!(result, "PLAIN TEXT");
476 }
477
478 #[test]
479 fn test_basic_agent_output_parse_or_map_success() {
480 let output = BasicAgentOutput {
481 response: r#""hello""#.to_string(),
482 done: true,
483 };
484 let result: String = output.parse_or_map(|s| s.to_uppercase());
485 assert_eq!(result, "hello");
486 }
487
488 #[test]
489 fn test_error_from_turn_engine_llm() {
490 let err: BasicExecutorError =
491 TurnEngineError::LLMError(LLMError::Generic("bad".to_string())).into();
492 assert!(matches!(err, BasicExecutorError::LLMError(_)));
493 assert!(err.to_string().contains("bad"));
494 }
495
496 #[test]
497 fn test_error_from_turn_engine_aborted() {
498 let err: BasicExecutorError = TurnEngineError::Aborted.into();
499 assert!(matches!(err, BasicExecutorError::Other(_)));
500 assert!(err.to_string().contains("aborted"));
501 }
502
503 #[test]
504 fn test_error_from_turn_engine_other() {
505 let err: BasicExecutorError = TurnEngineError::Other("misc".to_string()).into();
506 assert!(matches!(err, BasicExecutorError::Other(_)));
507 assert!(err.to_string().contains("misc"));
508 }
509
510 #[test]
511 fn test_extract_turn_output_complete() {
512 let result = crate::agent::executor::TurnResult::Complete(
513 crate::agent::executor::turn_engine::TurnEngineOutput {
514 response: "done".to_string(),
515 reasoning_content: String::default(),
516 tool_calls: Vec::new(),
517 },
518 );
519 let output = extract_turn_output(result);
520 assert_eq!(output.response, "done");
521 }
522
523 #[test]
524 fn test_extract_turn_output_continue_some() {
525 let result = crate::agent::executor::TurnResult::Continue(Some(
526 crate::agent::executor::turn_engine::TurnEngineOutput {
527 response: "partial".to_string(),
528 reasoning_content: String::default(),
529 tool_calls: Vec::new(),
530 },
531 ));
532 let output = extract_turn_output(result);
533 assert_eq!(output.response, "partial");
534 }
535
536 #[test]
537 fn test_extract_turn_output_continue_none() {
538 let result = crate::agent::executor::TurnResult::Continue(None);
539 let output = extract_turn_output(result);
540 assert!(output.response.is_empty());
541 assert!(output.tool_calls.is_empty());
542 }
543
544 #[tokio::test]
545 async fn test_basic_agent_execute_stream_returns_output() {
546 use crate::agent::{AgentConfig, Context};
547 use autoagents_protocol::ActorID;
548 use futures::StreamExt;
549
550 let llm = Arc::new(ConfigurableLLMProvider {
551 structured_stream: vec![
552 StreamResponse {
553 choices: vec![StreamChoice {
554 delta: StreamDelta {
555 content: Some("Hello ".to_string()),
556 reasoning_content: None,
557 tool_calls: None,
558 },
559 }],
560 usage: None,
561 },
562 StreamResponse {
563 choices: vec![StreamChoice {
564 delta: StreamDelta {
565 content: Some("world".to_string()),
566 reasoning_content: None,
567 tool_calls: None,
568 },
569 }],
570 usage: None,
571 },
572 ],
573 ..ConfigurableLLMProvider::default()
574 });
575
576 let mock_agent = MockAgentImpl::new("stream_agent", "desc");
577 let basic_agent = BasicAgent::new(mock_agent);
578 let config = AgentConfig {
579 id: ActorID::new_v4(),
580 name: "stream_agent".to_string(),
581 description: "desc".to_string(),
582 output_schema: None,
583 };
584 let context = Arc::new(Context::new(llm, None).with_config(config));
585 let task = Task::new("Test task");
586
587 let mut stream = basic_agent.execute_stream(&task, context).await.unwrap();
588 let mut final_output = None;
589 while let Some(item) = stream.next().await {
590 let output = item.unwrap();
591 if output.done {
592 final_output = Some(output);
593 break;
594 }
595 }
596
597 let output = final_output.expect("final output");
598 assert_eq!(output.response, "Hello world");
599 assert!(output.done);
600 }
601
602 #[tokio::test]
603 async fn test_basic_agent_execute_stream_ignores_reasoning_output() {
604 use crate::agent::{AgentConfig, Context};
605 use autoagents_protocol::ActorID;
606 use futures::StreamExt;
607
608 let llm = Arc::new(ConfigurableLLMProvider {
609 structured_stream: vec![
610 StreamResponse {
611 choices: vec![StreamChoice {
612 delta: StreamDelta {
613 content: None,
614 reasoning_content: Some("plan".to_string()),
615 tool_calls: None,
616 },
617 }],
618 usage: None,
619 },
620 StreamResponse {
621 choices: vec![StreamChoice {
622 delta: StreamDelta {
623 content: Some("done".to_string()),
624 reasoning_content: None,
625 tool_calls: None,
626 },
627 }],
628 usage: None,
629 },
630 ],
631 ..ConfigurableLLMProvider::default()
632 });
633
634 let mock_agent = MockAgentImpl::new("stream_agent_reasoning", "desc");
635 let basic_agent = BasicAgent::new(mock_agent);
636 let config = AgentConfig {
637 id: ActorID::new_v4(),
638 name: "stream_agent_reasoning".to_string(),
639 description: "desc".to_string(),
640 output_schema: None,
641 };
642 let context = Arc::new(Context::new(llm, None).with_config(config));
643 let task = Task::new("Test task");
644
645 let mut stream = basic_agent.execute_stream(&task, context).await.unwrap();
646 let mut outputs = Vec::new();
647 while let Some(item) = stream.next().await {
648 outputs.push(item.unwrap());
649 }
650
651 assert_eq!(outputs.len(), 2);
652 assert_eq!(outputs[0].response, "done");
653 assert!(!outputs[0].done);
654 assert_eq!(outputs[1].response, "done");
655 assert!(outputs[1].done);
656 }
657
658 #[tokio::test]
659 async fn test_basic_agent_run_aborts_on_hook() {
660 use crate::agent::AgentBuilder;
661 use crate::agent::direct::DirectAgent;
662 use crate::agent::error::RunnableAgentError;
663
664 let agent = BasicAgent::new(AbortAgent);
665 let llm = Arc::new(MockLLMProvider {});
666 let handle = AgentBuilder::<_, DirectAgent>::new(agent)
667 .llm(llm)
668 .build()
669 .await
670 .expect("build should succeed");
671 let task = Task::new("abort");
672
673 let err = handle.agent.run(task).await.expect_err("expected abort");
674 assert!(matches!(err, RunnableAgentError::Abort));
675 }
676
677 #[tokio::test]
678 async fn test_basic_agent_run_stream_aborts_on_hook() {
679 use crate::agent::AgentBuilder;
680 use crate::agent::direct::DirectAgent;
681 use crate::agent::error::RunnableAgentError;
682
683 let agent = BasicAgent::new(AbortAgent);
684 let llm = Arc::new(MockLLMProvider {});
685 let handle = AgentBuilder::<_, DirectAgent>::new(agent)
686 .llm(llm)
687 .build()
688 .await
689 .expect("build should succeed");
690 let task = Task::new("abort");
691
692 let err = match handle.agent.run_stream(task).await {
693 Ok(_) => panic!("expected abort"),
694 Err(err) => err,
695 };
696 assert!(matches!(err, RunnableAgentError::Abort));
697 }
698}