1#[cfg(not(target_arch = "wasm32"))]
2use crate::actor::Topic;
3use crate::agent::base::AgentType;
4use crate::agent::context::Context;
5use crate::agent::error::{AgentBuildError, RunnableAgentError};
6use crate::agent::executor::event_helper::EventHelper;
7use crate::agent::hooks::AgentHooks;
8use crate::agent::state::AgentState;
9use crate::agent::task::Task;
10use crate::agent::{AgentBuilder, AgentDeriveT, AgentExecutor, BaseAgent, HookOutcome};
11use crate::channel::Sender;
12use crate::error::Error;
13#[cfg(not(target_arch = "wasm32"))]
14use crate::runtime::TypedRuntime;
15use async_trait::async_trait;
16use autoagents_protocol::Event;
17#[cfg(target_arch = "wasm32")]
18use futures::SinkExt;
19#[cfg(not(target_arch = "wasm32"))]
20use ractor::Actor;
21#[cfg(not(target_arch = "wasm32"))]
22use ractor::{ActorProcessingErr, ActorRef};
23use serde_json::Value;
24use std::fmt::Debug;
25use std::sync::Arc;
26
27#[derive(Clone, Copy)]
32pub struct ActorAgent {}
33
34impl AgentType for ActorAgent {
35 fn type_name() -> &'static str {
36 "protocol_agent"
37 }
38}
39
40#[cfg(not(target_arch = "wasm32"))]
44#[derive(Clone)]
45pub struct ActorAgentHandle<T: AgentDeriveT + AgentExecutor + AgentHooks + Send + Sync> {
46 pub agent: Arc<BaseAgent<T, ActorAgent>>,
47 pub actor_ref: ActorRef<Task>,
48}
49
50#[cfg(not(target_arch = "wasm32"))]
51impl<T: AgentDeriveT + AgentExecutor + AgentHooks> ActorAgentHandle<T> {
52 pub fn addr(&self) -> ActorRef<Task> {
54 self.actor_ref.clone()
55 }
56
57 pub fn agent(&self) -> Arc<BaseAgent<T, ActorAgent>> {
60 self.agent.clone()
61 }
62}
63
64#[cfg(not(target_arch = "wasm32"))]
65impl<T: AgentDeriveT + AgentExecutor + AgentHooks> Debug for ActorAgentHandle<T> {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("AgentHandle")
68 .field("agent", &self.agent)
69 .finish()
70 }
71}
72
73#[cfg(not(target_arch = "wasm32"))]
74#[derive(Debug)]
75pub struct AgentActor<T: AgentDeriveT + AgentExecutor + AgentHooks>(
76 pub Arc<BaseAgent<T, ActorAgent>>,
77);
78
79#[cfg(not(target_arch = "wasm32"))]
80impl<T: AgentDeriveT + AgentExecutor + AgentHooks> AgentActor<T> {}
81
82#[cfg(not(target_arch = "wasm32"))]
83impl<T: AgentDeriveT + AgentExecutor + AgentHooks> AgentBuilder<T, ActorAgent>
84where
85 T: Send + Sync + 'static,
86 serde_json::Value: From<<T as AgentExecutor>::Output>,
87 <T as AgentDeriveT>::Output: From<<T as AgentExecutor>::Output>,
88 <T as AgentExecutor>::Error: Into<RunnableAgentError>,
89{
90 pub async fn build(self) -> Result<ActorAgentHandle<T>, Error> {
92 let llm = self.llm.ok_or(AgentBuildError::BuildFailure(
93 "LLM provider is required".to_string(),
94 ))?;
95 let runtime = self.runtime.ok_or(AgentBuildError::BuildFailure(
96 "Runtime should be defined".into(),
97 ))?;
98 let tx = runtime.tx();
99
100 let agent: Arc<BaseAgent<T, ActorAgent>> = Arc::new(
101 BaseAgent::<T, ActorAgent>::new(self.inner, llm, self.memory, tx, self.stream).await?,
102 );
103
104 let agent_actor = AgentActor(agent.clone());
106 let actor_ref = Actor::spawn(Some(agent_actor.0.name().into()), agent_actor, ())
107 .await
108 .map_err(AgentBuildError::SpawnError)?
109 .0;
110
111 for topic in self.subscribed_topics {
113 runtime.subscribe(&topic, actor_ref.clone()).await?;
114 }
115
116 Ok(ActorAgentHandle { agent, actor_ref })
117 }
118
119 pub fn subscribe(mut self, topic: Topic<Task>) -> Self {
120 self.subscribed_topics.push(topic);
121 self
122 }
123}
124
125#[cfg(not(target_arch = "wasm32"))]
126impl<T: AgentDeriveT + AgentExecutor + AgentHooks> BaseAgent<T, ActorAgent> {
127 pub fn tx(&self) -> Result<Sender<Event>, RunnableAgentError> {
128 self.tx.clone().ok_or(RunnableAgentError::EmptyTx)
129 }
130
131 pub async fn run(
132 self: Arc<Self>,
133 task: Task,
134 ) -> Result<<T as AgentDeriveT>::Output, RunnableAgentError>
135 where
136 Value: From<<T as AgentExecutor>::Output>,
137 <T as AgentDeriveT>::Output: From<<T as AgentExecutor>::Output>,
138 <T as AgentExecutor>::Error: Into<RunnableAgentError>,
139 {
140 let submission_id = task.submission_id;
141 let tx = self.tx().map_err(|_| RunnableAgentError::EmptyTx)?;
142 let tx_event = Some(tx.clone());
143
144 let context = self.create_context();
145
146 let hook_outcome = self.inner.on_run_start(&task, &context).await;
148 match hook_outcome {
149 HookOutcome::Abort => {
150 return Err(
151 EventHelper::abort_run_from_hook(&tx_event, submission_id, self.id).await,
152 );
153 }
154 HookOutcome::Continue => {}
155 }
156
157 match self.inner().execute(&task, context.clone()).await {
159 Ok(output) => {
160 self.finish_executor_run(&task, &context, submission_id, output)
161 .await
162 }
163 Err(e) => {
164 #[cfg(not(target_arch = "wasm32"))]
165 EventHelper::send_task_error(&tx_event, submission_id, self.id, e.to_string())
166 .await;
167 Err(e.into())
168 }
169 }
170 }
171
172 pub async fn run_stream(
186 self: Arc<Self>,
187 task: Task,
188 ) -> Result<
189 crate::utils::BoxRuntimeStream<Result<<T as AgentDeriveT>::Output, RunnableAgentError>>,
190 RunnableAgentError,
191 >
192 where
193 <T as AgentDeriveT>::Output: From<<T as AgentExecutor>::Output>,
194 <T as AgentExecutor>::Error: Into<RunnableAgentError>,
195 {
196 let context = self.create_context();
197 self.run_stream_with_context(task, context).await
198 }
199
200 async fn run_executor_stream_with_context(
201 self: Arc<Self>,
202 task: Task,
203 context: Arc<Context>,
204 ) -> Result<
205 crate::utils::BoxRuntimeStream<Result<<T as AgentExecutor>::Output, RunnableAgentError>>,
206 RunnableAgentError,
207 >
208 where
209 <T as AgentExecutor>::Error: Into<RunnableAgentError>,
210 {
211 match self.inner().execute_stream(&task, context).await {
212 Ok(stream) => {
213 use futures::StreamExt;
214 let transformed_stream =
215 stream.map(move |result| result.map_err(|error| error.into()));
216 Ok(Box::pin(transformed_stream))
217 }
218 Err(error) => Err(error.into()),
219 }
220 }
221
222 async fn run_stream_with_context(
223 self: Arc<Self>,
224 task: Task,
225 context: Arc<Context>,
226 ) -> Result<
227 crate::utils::BoxRuntimeStream<Result<<T as AgentDeriveT>::Output, RunnableAgentError>>,
228 RunnableAgentError,
229 >
230 where
231 <T as AgentDeriveT>::Output: From<<T as AgentExecutor>::Output>,
232 <T as AgentExecutor>::Error: Into<RunnableAgentError>,
233 {
234 let stream = self.run_executor_stream_with_context(task, context).await?;
235 use futures::StreamExt;
236 let transformed_stream = stream.map(|result| result.map(|output| output.into()));
237 Ok(Box::pin(transformed_stream))
238 }
239
240 pub async fn run_stream_to_completion(
254 self: Arc<Self>,
255 task: Task,
256 ) -> Result<<T as AgentDeriveT>::Output, RunnableAgentError>
257 where
258 Value: From<<T as AgentExecutor>::Output>,
259 <T as AgentDeriveT>::Output: From<<T as AgentExecutor>::Output>,
260 <T as AgentExecutor>::Error: Into<RunnableAgentError>,
261 {
262 let submission_id = task.submission_id;
263 let tx = self.tx().map_err(|_| RunnableAgentError::EmptyTx)?;
264 let tx_event = Some(tx.clone());
265 let context = self.create_context();
266
267 let hook_outcome = self.inner.on_run_start(&task, &context).await;
268 match hook_outcome {
269 HookOutcome::Abort => {
270 return Err(
271 EventHelper::abort_run_from_hook(&tx_event, submission_id, self.id).await,
272 );
273 }
274 HookOutcome::Continue => {}
275 }
276
277 let mut stream = match self
278 .clone()
279 .run_executor_stream_with_context(task.clone(), context.clone())
280 .await
281 {
282 Ok(stream) => stream,
283 Err(e) => {
284 #[cfg(not(target_arch = "wasm32"))]
285 EventHelper::send_task_error(&tx_event, submission_id, self.id, e.to_string())
286 .await;
287 return Err(e);
288 }
289 };
290 use futures::StreamExt;
291
292 let mut last_executor_output = None;
293 while let Some(result) = stream.next().await {
294 match EventHelper::map_executor_stream_item(&tx_event, submission_id, self.id, result)
295 .await
296 {
297 Ok(output) => last_executor_output = Some(output),
298 Err(e) => return Err(e),
299 }
300 }
301
302 let executor_out = match last_executor_output {
303 Some(output) => output,
304 None => {
305 let err = RunnableAgentError::ExecutorError(
306 "Stream completed without output".to_string(),
307 );
308 #[cfg(not(target_arch = "wasm32"))]
309 EventHelper::send_task_error(&tx_event, submission_id, self.id, err.to_string())
310 .await;
311 return Err(err);
312 }
313 };
314
315 self.finish_executor_run(&task, &context, submission_id, executor_out)
316 .await
317 }
318}
319
320#[cfg(not(target_arch = "wasm32"))]
321#[async_trait]
322impl<T: AgentDeriveT + AgentExecutor + AgentHooks> Actor for AgentActor<T>
323where
324 T: Send + Sync + 'static,
325 serde_json::Value: From<<T as AgentExecutor>::Output>,
326 <T as AgentDeriveT>::Output: From<<T as AgentExecutor>::Output>,
327 <T as AgentExecutor>::Error: Into<RunnableAgentError>,
328{
329 type Msg = Task;
330 type State = AgentState;
331 type Arguments = ();
332
333 async fn pre_start(
334 &self,
335 _myself: ActorRef<Self::Msg>,
336 _args: Self::Arguments,
337 ) -> Result<Self::State, ActorProcessingErr> {
338 Ok(AgentState::new())
339 }
340
341 async fn post_stop(
342 &self,
343 _myself: ActorRef<Self::Msg>,
344 _state: &mut Self::State,
345 ) -> Result<(), ActorProcessingErr> {
346 self.0.inner().on_agent_shutdown().await;
348 Ok(())
349 }
350
351 async fn handle(
352 &self,
353 _myself: ActorRef<Self::Msg>,
354 message: Self::Msg,
355 _state: &mut Self::State,
356 ) -> Result<(), ActorProcessingErr> {
357 let agent = self.0.clone();
358
359 let result = if agent.stream() {
361 agent.run_stream_to_completion(message).await
362 } else {
363 agent.run(message).await
364 };
365
366 let _ = result;
371 Ok(())
372 }
373}
374
375#[cfg(test)]
376#[cfg(not(target_arch = "wasm32"))]
377mod tests {
378 use super::*;
379 use crate::actor::{LocalTransport, Topic, Transport};
380 use crate::agent::hooks::HookOutcome;
381 use crate::agent::output::AgentOutputT;
382 use crate::agent::{Context, ExecutorConfig};
383 use crate::runtime::{Runtime, RuntimeError};
384 use crate::tests::{
385 DivergentStreamingAgent, MockAgentImpl, MockLLMProvider, MultiItemStreamAgent,
386 TestAgentOutput, TestError,
387 };
388 use crate::utils::BoxEventStream;
389 use async_trait::async_trait;
390 use futures::{StreamExt, stream};
391 use std::any::{Any, TypeId};
392 use std::sync::Arc;
393 use tokio::sync::{Mutex, mpsc};
394
395 #[derive(Debug)]
396 struct TestRuntime {
397 subscribed: Arc<Mutex<Vec<(String, TypeId)>>>,
398 tx: mpsc::Sender<Event>,
399 }
400
401 impl TestRuntime {
402 fn new() -> Self {
403 let (tx, _rx) = mpsc::channel(4);
404 Self {
405 subscribed: Arc::new(Mutex::new(Vec::new())),
406 tx,
407 }
408 }
409 }
410
411 #[async_trait]
412 impl Runtime for TestRuntime {
413 fn id(&self) -> autoagents_protocol::RuntimeID {
414 autoagents_protocol::RuntimeID::new_v4()
415 }
416
417 async fn subscribe_any(
418 &self,
419 topic_name: &str,
420 topic_type: TypeId,
421 _actor: Arc<dyn crate::actor::AnyActor>,
422 ) -> Result<(), RuntimeError> {
423 let mut subscribed = self.subscribed.lock().await;
424 subscribed.push((topic_name.to_string(), topic_type));
425 Ok(())
426 }
427
428 async fn publish_any(
429 &self,
430 _topic_name: &str,
431 _topic_type: TypeId,
432 _message: Arc<dyn Any + Send + Sync>,
433 ) -> Result<(), RuntimeError> {
434 Ok(())
435 }
436
437 fn tx(&self) -> mpsc::Sender<Event> {
438 self.tx.clone()
439 }
440
441 async fn transport(&self) -> Arc<dyn Transport> {
442 Arc::new(LocalTransport)
443 }
444
445 async fn take_event_receiver(&self) -> Option<BoxEventStream<Event>> {
446 None
447 }
448
449 async fn subscribe_events(&self) -> BoxEventStream<Event> {
450 Box::pin(stream::empty())
451 }
452
453 async fn run(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
454 Ok(())
455 }
456
457 async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
458 Ok(())
459 }
460 }
461
462 #[tokio::test]
463 async fn test_actor_builder_requires_llm() {
464 let mock = MockAgentImpl::new("agent", "desc");
465 let runtime = Arc::new(TestRuntime::new());
466 let err = AgentBuilder::<_, ActorAgent>::new(mock)
467 .runtime(runtime)
468 .build()
469 .await
470 .unwrap_err();
471 assert!(matches!(err, Error::AgentBuildError(_)));
472 }
473
474 #[tokio::test]
475 async fn test_actor_builder_requires_runtime() {
476 let mock = MockAgentImpl::new("agent", "desc");
477 let llm = Arc::new(MockLLMProvider);
478 let err = AgentBuilder::<_, ActorAgent>::new(mock)
479 .llm(llm)
480 .build()
481 .await
482 .unwrap_err();
483 assert!(matches!(err, Error::AgentBuildError(_)));
484 }
485
486 #[tokio::test]
487 async fn test_actor_builder_subscribes_topics() {
488 let mock = MockAgentImpl::new("agent", "desc");
489 let llm = Arc::new(MockLLMProvider);
490 let runtime = Arc::new(TestRuntime::new());
491 let topic = Topic::<Task>::new("jobs");
492
493 let _handle = AgentBuilder::<_, ActorAgent>::new(mock)
494 .llm(llm)
495 .runtime(runtime.clone())
496 .subscribe(topic)
497 .build()
498 .await
499 .expect("build should succeed");
500
501 let subscribed = runtime.subscribed.lock().await;
502 assert_eq!(subscribed.len(), 1);
503 assert_eq!(subscribed[0].0, "jobs");
504 }
505
506 #[tokio::test]
507 async fn test_actor_agent_tx_missing_returns_error() {
508 let mock = MockAgentImpl::new("agent", "desc");
509 let llm = Arc::new(MockLLMProvider);
510 let (tx, _rx) = mpsc::channel(2);
511 let mut agent = BaseAgent::<_, ActorAgent>::new(mock, llm, None, tx, false)
512 .await
513 .unwrap();
514 agent.tx = None;
515 let err = agent.tx().unwrap_err();
516 assert!(matches!(err, RunnableAgentError::EmptyTx));
517 }
518
519 async fn streaming_actor_agent(stream: bool) -> Arc<BaseAgent<MockAgentImpl, ActorAgent>> {
520 let mock = MockAgentImpl::new("stream_agent", "streaming test agent");
521 let llm = Arc::new(MockLLMProvider);
522 let (tx, _rx) = mpsc::channel(8);
523 Arc::new(
524 BaseAgent::<_, ActorAgent>::new(mock, llm, None, tx, stream)
525 .await
526 .expect("agent should build"),
527 )
528 }
529
530 #[tokio::test]
531 async fn test_actor_run_stream_returns_executor_output() {
532 let agent = streaming_actor_agent(true).await;
533 let task = Task::new("stream me");
534 let stream = agent.run_stream(task).await.expect("stream should start");
535 let outputs: Vec<_> = stream.collect().await;
536 assert_eq!(outputs.len(), 1);
537 let output = outputs[0].as_ref().expect("expected stream output");
538 assert!(output.result.contains("stream me"));
539 }
540
541 #[tokio::test]
542 async fn test_actor_run_stream_to_completion_returns_output() {
543 let agent = streaming_actor_agent(true).await;
544 let task = Task::new("complete me");
545 let output = agent
546 .run_stream_to_completion(task)
547 .await
548 .expect("stream should complete");
549 assert!(output.result.contains("complete me"));
550 }
551
552 #[derive(Debug, Clone)]
553 struct AbortStreamingAgent;
554
555 #[async_trait]
556 impl AgentDeriveT for AbortStreamingAgent {
557 type Output = TestAgentOutput;
558
559 fn description(&self) -> &'static str {
560 "abort streaming agent"
561 }
562
563 fn output_schema(&self) -> Option<serde_json::Value> {
564 Some(TestAgentOutput::structured_output_format())
565 }
566
567 fn name(&self) -> &'static str {
568 "abort_streaming_agent"
569 }
570
571 fn tools(&self) -> Vec<Box<dyn crate::tool::ToolT>> {
572 vec![]
573 }
574 }
575
576 #[async_trait]
577 impl AgentExecutor for AbortStreamingAgent {
578 type Output = TestAgentOutput;
579 type Error = TestError;
580
581 fn config(&self) -> ExecutorConfig {
582 ExecutorConfig::default()
583 }
584
585 async fn execute(
586 &self,
587 _task: &Task,
588 _context: Arc<Context>,
589 ) -> Result<Self::Output, Self::Error> {
590 Ok(TestAgentOutput {
591 result: "unused".to_string(),
592 })
593 }
594 }
595
596 #[async_trait]
597 impl AgentHooks for AbortStreamingAgent {
598 async fn on_run_start(&self, _task: &Task, _ctx: &Context) -> HookOutcome {
599 HookOutcome::Abort
600 }
601 }
602
603 #[tokio::test]
604 async fn test_actor_run_aborts_on_hook_emits_task_error() {
605 let llm = Arc::new(MockLLMProvider);
606 let (tx, mut rx) = mpsc::channel(2);
607 let agent = Arc::new(
608 BaseAgent::<_, ActorAgent>::new(AbortStreamingAgent, llm, None, tx, false)
609 .await
610 .expect("agent should build"),
611 );
612
613 let err = agent
614 .run(Task::new("abort"))
615 .await
616 .expect_err("expected hook abort");
617 assert!(matches!(err, RunnableAgentError::Abort));
618
619 let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
620 .await
621 .expect("timed out waiting for TaskError event")
622 .expect("channel closed without event");
623 match event {
624 Event::TaskError { error, .. } => {
625 assert!(error.contains("Abort"));
626 }
627 other => panic!("expected TaskError, got {other:?}"),
628 }
629 }
630
631 #[tokio::test]
632 async fn test_actor_run_stream_to_completion_aborts_on_hook() {
633 let llm = Arc::new(MockLLMProvider);
634 let (tx, mut rx) = mpsc::channel(2);
635 let agent = Arc::new(
636 BaseAgent::<_, ActorAgent>::new(AbortStreamingAgent, llm, None, tx, true)
637 .await
638 .expect("agent should build"),
639 );
640
641 let err = agent
642 .run_stream_to_completion(Task::new("abort"))
643 .await
644 .expect_err("expected hook abort");
645 assert!(matches!(err, RunnableAgentError::Abort));
646
647 let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
648 .await
649 .expect("timed out waiting for TaskError event")
650 .expect("channel closed without event");
651 match event {
652 Event::TaskError { error, .. } => {
653 assert!(error.contains("Abort"));
654 }
655 other => panic!("expected TaskError, got {other:?}"),
656 }
657 }
658
659 #[derive(Debug, Clone)]
660 struct FailingStreamSetupAgent;
661
662 #[async_trait]
663 impl AgentDeriveT for FailingStreamSetupAgent {
664 type Output = TestAgentOutput;
665
666 fn description(&self) -> &'static str {
667 "failing stream setup"
668 }
669
670 fn output_schema(&self) -> Option<serde_json::Value> {
671 Some(TestAgentOutput::structured_output_format())
672 }
673
674 fn name(&self) -> &'static str {
675 "failing_stream_setup"
676 }
677
678 fn tools(&self) -> Vec<Box<dyn crate::tool::ToolT>> {
679 vec![]
680 }
681 }
682
683 #[async_trait]
684 impl AgentExecutor for FailingStreamSetupAgent {
685 type Output = TestAgentOutput;
686 type Error = TestError;
687
688 fn config(&self) -> ExecutorConfig {
689 ExecutorConfig::default()
690 }
691
692 async fn execute(
693 &self,
694 _task: &Task,
695 _context: Arc<Context>,
696 ) -> Result<Self::Output, Self::Error> {
697 Ok(TestAgentOutput {
698 result: "unused".to_string(),
699 })
700 }
701
702 async fn execute_stream(
703 &self,
704 _task: &Task,
705 _context: Arc<Context>,
706 ) -> Result<crate::utils::BoxRuntimeStream<Result<Self::Output, Self::Error>>, Self::Error>
707 {
708 Err(TestError::ExecutionFailed(
709 "stream setup failed".to_string(),
710 ))
711 }
712 }
713
714 impl AgentHooks for FailingStreamSetupAgent {}
715
716 #[tokio::test]
717 async fn test_actor_run_stream_to_completion_execute_stream_setup_error() {
718 let llm = Arc::new(MockLLMProvider);
719 let (tx, mut rx) = mpsc::channel(2);
720 let agent = Arc::new(
721 BaseAgent::<_, ActorAgent>::new(FailingStreamSetupAgent, llm, None, tx, true)
722 .await
723 .expect("agent should build"),
724 );
725
726 let err = agent
727 .run_stream_to_completion(Task::new("fail setup"))
728 .await
729 .expect_err("expected stream setup failure");
730 assert!(matches!(err, RunnableAgentError::ExecutorError(_)));
731
732 let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
733 .await
734 .expect("timed out waiting for TaskError event")
735 .expect("channel closed without event");
736 match event {
737 Event::TaskError { error, .. } => {
738 assert!(error.contains("stream setup failed"));
739 }
740 other => panic!("expected TaskError, got {other:?}"),
741 }
742 }
743
744 #[derive(Debug, Clone)]
745 struct EmptyStreamAgent;
746
747 #[async_trait]
748 impl AgentDeriveT for EmptyStreamAgent {
749 type Output = TestAgentOutput;
750
751 fn description(&self) -> &'static str {
752 "empty stream agent"
753 }
754
755 fn output_schema(&self) -> Option<serde_json::Value> {
756 Some(TestAgentOutput::structured_output_format())
757 }
758
759 fn name(&self) -> &'static str {
760 "empty_stream_agent"
761 }
762
763 fn tools(&self) -> Vec<Box<dyn crate::tool::ToolT>> {
764 vec![]
765 }
766 }
767
768 #[async_trait]
769 impl AgentExecutor for EmptyStreamAgent {
770 type Output = TestAgentOutput;
771 type Error = TestError;
772
773 fn config(&self) -> ExecutorConfig {
774 ExecutorConfig::default()
775 }
776
777 async fn execute(
778 &self,
779 _task: &Task,
780 _context: Arc<Context>,
781 ) -> Result<Self::Output, Self::Error> {
782 Ok(TestAgentOutput {
783 result: "unused".to_string(),
784 })
785 }
786
787 async fn execute_stream(
788 &self,
789 _task: &Task,
790 _context: Arc<Context>,
791 ) -> Result<crate::utils::BoxRuntimeStream<Result<Self::Output, Self::Error>>, Self::Error>
792 {
793 Ok(Box::pin(stream::empty()))
794 }
795 }
796
797 impl AgentHooks for EmptyStreamAgent {}
798
799 #[tokio::test]
800 async fn test_actor_run_stream_to_completion_empty_stream_error() {
801 let llm = Arc::new(MockLLMProvider);
802 let (tx, mut rx) = mpsc::channel(2);
803 let agent = Arc::new(
804 BaseAgent::<_, ActorAgent>::new(EmptyStreamAgent, llm, None, tx, true)
805 .await
806 .expect("agent should build"),
807 );
808
809 let err = agent
810 .run_stream_to_completion(Task::new("empty"))
811 .await
812 .expect_err("expected empty stream failure");
813 assert!(matches!(err, RunnableAgentError::ExecutorError(_)));
814 assert!(err.to_string().contains("without output"));
815
816 let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
817 .await
818 .expect("timed out waiting for TaskError event")
819 .expect("channel closed without event");
820 match event {
821 Event::TaskError { error, .. } => {
822 assert!(error.contains("without output"));
823 }
824 other => panic!("expected TaskError, got {other:?}"),
825 }
826 }
827
828 #[derive(Debug, Clone)]
829 struct StreamItemErrorAgent;
830
831 #[async_trait]
832 impl AgentDeriveT for StreamItemErrorAgent {
833 type Output = TestAgentOutput;
834
835 fn description(&self) -> &'static str {
836 "stream item error agent"
837 }
838
839 fn output_schema(&self) -> Option<serde_json::Value> {
840 Some(TestAgentOutput::structured_output_format())
841 }
842
843 fn name(&self) -> &'static str {
844 "stream_item_error_agent"
845 }
846
847 fn tools(&self) -> Vec<Box<dyn crate::tool::ToolT>> {
848 vec![]
849 }
850 }
851
852 #[async_trait]
853 impl AgentExecutor for StreamItemErrorAgent {
854 type Output = TestAgentOutput;
855 type Error = TestError;
856
857 fn config(&self) -> ExecutorConfig {
858 ExecutorConfig::default()
859 }
860
861 async fn execute(
862 &self,
863 _task: &Task,
864 _context: Arc<Context>,
865 ) -> Result<Self::Output, Self::Error> {
866 Ok(TestAgentOutput {
867 result: "unused".to_string(),
868 })
869 }
870
871 async fn execute_stream(
872 &self,
873 _task: &Task,
874 _context: Arc<Context>,
875 ) -> Result<crate::utils::BoxRuntimeStream<Result<Self::Output, Self::Error>>, Self::Error>
876 {
877 Ok(Box::pin(stream::iter([Err(TestError::ExecutionFailed(
878 "stream item failed".to_string(),
879 ))])))
880 }
881 }
882
883 impl AgentHooks for StreamItemErrorAgent {}
884
885 #[tokio::test]
886 async fn test_actor_run_stream_does_not_emit_task_error_on_item_failure() {
887 let llm = Arc::new(MockLLMProvider);
888 let (tx, mut rx) = mpsc::channel(2);
889 let agent = Arc::new(
890 BaseAgent::<_, ActorAgent>::new(StreamItemErrorAgent, llm, None, tx, true)
891 .await
892 .expect("agent should build"),
893 );
894
895 let mut stream = agent
896 .run_stream(Task::new("stream error"))
897 .await
898 .expect("stream should start");
899 let err = stream
900 .next()
901 .await
902 .expect("stream should yield one item")
903 .expect_err("expected stream item failure");
904 assert!(err.to_string().contains("stream item failed"));
905
906 let event = tokio::time::timeout(std::time::Duration::from_millis(100), rx.recv()).await;
907 match event {
908 Ok(Some(Event::TaskError { .. })) => {
909 panic!("run_stream must not emit TaskError; use run_stream_to_completion")
910 }
911 Ok(Some(_)) | Ok(None) => {}
912 Err(_) => {}
913 }
914 }
915
916 #[tokio::test]
917 async fn test_actor_run_stream_to_completion_stream_item_error() {
918 let llm = Arc::new(MockLLMProvider);
919 let (tx, mut rx) = mpsc::channel(2);
920 let agent = Arc::new(
921 BaseAgent::<_, ActorAgent>::new(StreamItemErrorAgent, llm, None, tx, true)
922 .await
923 .expect("agent should build"),
924 );
925
926 let err = agent
927 .run_stream_to_completion(Task::new("stream error"))
928 .await
929 .expect_err("expected stream item failure");
930 assert!(matches!(err, RunnableAgentError::ExecutorError(_)));
931 assert!(err.to_string().contains("stream item failed"));
932
933 let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
934 .await
935 .expect("timed out waiting for TaskError event")
936 .expect("channel closed without event");
937 match event {
938 Event::TaskError { error, .. } => {
939 assert!(error.contains("stream item failed"));
940 }
941 other => panic!("expected TaskError, got {other:?}"),
942 }
943 }
944
945 #[tokio::test]
946 async fn test_actor_run_stream_to_completion_missing_tx_error() {
947 let llm = Arc::new(MockLLMProvider);
948 let (tx, _rx) = mpsc::channel(2);
949 let mut agent = BaseAgent::<_, ActorAgent>::new(
950 MockAgentImpl::new("agent", "desc"),
951 llm,
952 None,
953 tx,
954 true,
955 )
956 .await
957 .expect("agent should build");
958 agent.tx = None;
959 let agent = Arc::new(agent);
960
961 let err = agent
962 .run_stream_to_completion(Task::new("missing tx"))
963 .await
964 .expect_err("expected missing tx failure");
965 assert!(matches!(err, RunnableAgentError::EmptyTx));
966 }
967
968 #[tokio::test]
969 async fn test_actor_run_stream_to_completion_uses_last_stream_item() {
970 let llm = Arc::new(MockLLMProvider);
971 let (tx, mut rx) = mpsc::channel(2);
972 let agent = Arc::new(
973 BaseAgent::<_, ActorAgent>::new(MultiItemStreamAgent, llm, None, tx, true)
974 .await
975 .expect("agent should build"),
976 );
977
978 let output = agent
979 .run_stream_to_completion(Task::new("chunked"))
980 .await
981 .expect("stream should complete");
982 assert_eq!(output.result, "chunked-3");
983
984 let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
985 .await
986 .expect("timed out waiting for TaskComplete event")
987 .expect("channel closed without event");
988 match event {
989 Event::TaskComplete { result, .. } => {
990 let parsed: Value =
991 serde_json::from_str(&result).expect("TaskComplete result should be JSON");
992 assert_eq!(parsed["sequence"], 3);
993 assert_eq!(parsed["response"], "chunked-3");
994 }
995 other => panic!("expected TaskComplete, got {other:?}"),
996 }
997 }
998
999 #[tokio::test]
1000 async fn test_actor_run_stream_to_completion_task_complete_uses_executor_value_from() {
1001 let llm = Arc::new(MockLLMProvider);
1002 let (tx, mut rx) = mpsc::channel(2);
1003 let agent = Arc::new(
1004 BaseAgent::<_, ActorAgent>::new(DivergentStreamingAgent, llm, None, tx, true)
1005 .await
1006 .expect("agent should build"),
1007 );
1008
1009 let output = agent
1010 .run_stream_to_completion(Task::new("stream payload"))
1011 .await
1012 .expect("stream should complete");
1013 assert_eq!(output.result, "stream payload");
1014
1015 let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
1016 .await
1017 .expect("timed out waiting for TaskComplete event")
1018 .expect("channel closed without event");
1019 match event {
1020 Event::TaskComplete { result, .. } => {
1021 let parsed: Value =
1022 serde_json::from_str(&result).expect("TaskComplete result should be JSON");
1023 assert_eq!(parsed["executor_only"], 42);
1024 assert_eq!(parsed["response"], "stream payload");
1025 }
1026 other => panic!("expected TaskComplete, got {other:?}"),
1027 }
1028 }
1029
1030 #[tokio::test]
1031 async fn test_actor_run_and_stream_to_completion_emit_matching_task_complete() {
1032 let llm = Arc::new(MockLLMProvider);
1033 let (tx_run, mut rx_run) = mpsc::channel(2);
1034 let run_agent = Arc::new(
1035 BaseAgent::<_, ActorAgent>::new(
1036 DivergentStreamingAgent,
1037 llm.clone(),
1038 None,
1039 tx_run,
1040 false,
1041 )
1042 .await
1043 .expect("run agent should build"),
1044 );
1045 let (tx_stream, mut rx_stream) = mpsc::channel(2);
1046 let stream_agent = Arc::new(
1047 BaseAgent::<_, ActorAgent>::new(DivergentStreamingAgent, llm, None, tx_stream, true)
1048 .await
1049 .expect("stream agent should build"),
1050 );
1051
1052 let task = Task::new("parity payload");
1053 run_agent
1054 .clone()
1055 .run(task.clone())
1056 .await
1057 .expect("run should succeed");
1058 stream_agent
1059 .run_stream_to_completion(task)
1060 .await
1061 .expect("stream should complete");
1062
1063 let run_event = tokio::time::timeout(std::time::Duration::from_secs(1), rx_run.recv())
1064 .await
1065 .expect("timed out waiting for run TaskComplete")
1066 .expect("run channel closed without event");
1067 let stream_event =
1068 tokio::time::timeout(std::time::Duration::from_secs(1), rx_stream.recv())
1069 .await
1070 .expect("timed out waiting for stream TaskComplete")
1071 .expect("stream channel closed without event");
1072
1073 match (run_event, stream_event) {
1074 (
1075 Event::TaskComplete {
1076 result: run_result, ..
1077 },
1078 Event::TaskComplete {
1079 result: stream_result,
1080 ..
1081 },
1082 ) => {
1083 assert_eq!(
1084 run_result, stream_result,
1085 "run() and run_stream_to_completion() must serialize TaskComplete identically"
1086 );
1087 }
1088 (run_event, stream_event) => {
1089 panic!("expected TaskComplete events, got {run_event:?} and {stream_event:?}")
1090 }
1091 }
1092 }
1093}