Skip to main content

autoagents_core/agent/
actor.rs

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/// Marker type for actor-based agents.
28///
29/// Actor agents run inside a runtime, can subscribe to topics, receive
30/// messages, and emit protocol `Event`s for streaming updates.
31#[derive(Clone, Copy)]
32pub struct ActorAgent {}
33
34impl AgentType for ActorAgent {
35    fn type_name() -> &'static str {
36        "protocol_agent"
37    }
38}
39
40/// Handle for an actor-based agent that contains both the agent and the
41/// address of its actor. Use `addr()` to send messages directly or publish
42/// `Task`s to subscribed `Topic<Task>`.
43#[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    /// Get the actor reference (`ActorRef<Task>`) for direct messaging.
53    pub fn addr(&self) -> ActorRef<Task> {
54        self.actor_ref.clone()
55    }
56
57    /// Get a clone of the agent reference for querying metadata or invoking
58    /// methods that require `Arc<BaseAgent<..>>`.
59    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    /// Build the BaseAgent and return a wrapper that includes the actor reference
91    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        // Create agent actor
105        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        // Subscribe to topics
112        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        //Run Hook
147        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        // Execute the agent's logic using the executor
158        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    /// Return a live executor output stream without the full task lifecycle.
173    ///
174    /// **Event channel:** Does **not** emit terminal protocol events (`TaskComplete`,
175    /// `TaskError`) on the agent event channel. In-stream failures appear only as `Err`
176    /// items on the returned stream. Lifecycle hooks (`on_run_start`, `on_run_complete`) are
177    /// also skipped.
178    ///
179    /// Use [`Self::run_stream_to_completion`] when dispatching through a runtime, waiting on
180    /// `TaskComplete` / `TaskError`, or matching the pub/sub actor path (which calls
181    /// `run_stream_to_completion` internally).
182    ///
183    /// Mid-run events (`StreamChunk`, tool-call events, etc.) may still be emitted by the
184    /// executor while the returned stream is polled.
185    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    /// Execute a streaming task to completion inside the actor, draining the
241    /// stream and running lifecycle hooks before returning.
242    ///
243    /// This is the **event-aware** streaming entry point: emits `TaskError` on hook abort,
244    /// stream setup failure, in-stream item errors, and empty streams; emits `TaskComplete`
245    /// on success. Pub/sub [`AgentActor`](AgentActor) dispatch uses this method when
246    /// `stream()` is enabled.
247    ///
248    /// When the executor stream yields multiple successful outputs, only the
249    /// **last** item is used for `TaskComplete` and the returned agent output.
250    /// Intermediate items are not emitted as terminal events.
251    ///
252    /// For incremental output without terminal events, see [`Self::run_stream`].
253    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        //Run Hook
347        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        // Run agent
360        let result = if agent.stream() {
361            agent.run_stream_to_completion(message).await
362        } else {
363            agent.run(message).await
364        };
365
366        // Task-level failures are surfaced on the event channel as `TaskError` (or
367        // `TaskComplete` on success) by the run helpers above. Do not propagate them
368        // to ractor — returning `Err` here terminates the actor and breaks long-running
369        // pub/sub agents after a single bad task.
370        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}