Skip to main content

agentsight_capture/runners/
agent.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 eunomia-bpf org.
3
4use super::{EventStream, Runner, RunnerError};
5use crate::analyzers::Analyzer;
6use async_trait::async_trait;
7use futures::stream::select_all;
8
9/// AgentRunner composes multiple runners into a single unified stream
10/// with optional global analyzers applied to the merged stream
11#[derive(Default)]
12pub struct AgentRunner {
13    runners: Vec<Box<dyn Runner>>,
14    analyzers: Vec<Box<dyn Analyzer>>,
15}
16
17impl AgentRunner {
18    /// Create a new AgentRunner
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Add a pre-configured runner with its analyzer chain
24    pub fn add_runner(mut self, runner: Box<dyn Runner>) -> Self {
25        self.runners.push(runner);
26        self
27    }
28
29    /// Add analyzer that will be applied to the merged stream
30    pub fn add_global_analyzer(mut self, analyzer: Box<dyn Analyzer>) -> Self {
31        self.analyzers.push(analyzer);
32        self
33    }
34
35    /// Get the number of configured runners
36    pub fn runner_count(&self) -> usize {
37        self.runners.len()
38    }
39
40    /// Get the number of configured global analyzers
41    pub fn analyzer_count(&self) -> usize {
42        self.analyzers.len()
43    }
44}
45
46#[async_trait]
47impl Runner for AgentRunner {
48    async fn run(&mut self) -> Result<EventStream, RunnerError> {
49        if self.runners.is_empty() {
50            return Err("No runners configured for AgentRunner".into());
51        }
52
53        // Start all runners concurrently and collect their streams
54        let mut streams = Vec::new();
55        for runner in &mut self.runners {
56            let stream = runner.run().await?;
57            streams.push(stream);
58        }
59
60        // Merge all streams into a single stream
61        let merged_stream = select_all(streams);
62
63        // Apply global analyzers to the merged stream
64        let mut final_stream = Box::pin(merged_stream) as EventStream;
65        for analyzer in &mut self.analyzers {
66            final_stream = analyzer
67                .process(final_stream)
68                .await
69                .map_err(|e| format!("Global analyzer error: {}", e))?;
70        }
71
72        Ok(final_stream)
73    }
74
75    fn add_analyzer(mut self, analyzer: Box<dyn Analyzer>) -> Self {
76        self.analyzers.push(analyzer);
77        self
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::analyzers::{HTTPParser, MaterializingAnalyzer, SSEProcessor};
85    use crate::runners::FakeRunner;
86    use crate::view::MaterializedView;
87    use futures::stream::StreamExt;
88    use std::time::Duration;
89    use tokio::time::timeout;
90
91    fn materializer() -> MaterializingAnalyzer {
92        MaterializingAnalyzer::with_view(MaterializedView::shared_bounded())
93    }
94
95    #[tokio::test]
96    async fn test_agent_runner_basic_composition() {
97        let fake_runner1 = FakeRunner::new().event_count(2).delay_ms(10);
98
99        let fake_runner2 = FakeRunner::new().event_count(3).delay_ms(15);
100
101        let mut agent = AgentRunner::new()
102            .add_runner(Box::new(fake_runner1))
103            .add_runner(Box::new(fake_runner2));
104
105        assert_eq!(agent.runner_count(), 2);
106        assert_eq!(agent.analyzer_count(), 0);
107
108        let stream = agent.run().await.unwrap();
109        let events: Vec<_> = stream.collect().await;
110
111        // Should have events from both runners (4 + 6 = 10 total)
112        // FakeRunner generates 2 events per event_count (request + response pairs)
113        assert_eq!(events.len(), 10);
114
115        // Verify events come from SSL source (FakeRunner only generates SSL events)
116        let ssl_events = events.iter().filter(|e| e.source == "ssl").count();
117        assert_eq!(ssl_events, 10); // All events are SSL events from FakeRunner
118    }
119
120    #[tokio::test]
121    async fn test_agent_runner_with_global_analyzers() {
122        let fake_runner = FakeRunner::new()
123            .event_count(2)
124            .delay_ms(10)
125            .add_analyzer(Box::new(HTTPParser::new().disable_raw_data()));
126
127        let mut agent = AgentRunner::new()
128            .add_runner(Box::new(fake_runner))
129            .add_global_analyzer(Box::new(materializer()));
130
131        assert_eq!(agent.runner_count(), 1);
132        assert_eq!(agent.analyzer_count(), 1);
133
134        let stream = agent.run().await.unwrap();
135        let events: Vec<_> = stream.collect().await;
136
137        // Should have at least the original runner events.
138        assert!(events.len() >= 4);
139    }
140
141    #[tokio::test]
142    async fn test_agent_runner_multiple_runners_with_analyzers() {
143        let fake_runner1 = FakeRunner::new()
144            .event_count(1)
145            .delay_ms(10)
146            .add_analyzer(Box::new(SSEProcessor::new_with_timeout(5000)));
147
148        let fake_runner2 = FakeRunner::new()
149            .event_count(1)
150            .delay_ms(10)
151            .add_analyzer(Box::new(HTTPParser::new()));
152
153        let mut agent = AgentRunner::new()
154            .add_runner(Box::new(fake_runner1))
155            .add_runner(Box::new(fake_runner2));
156
157        let stream = agent.run().await.unwrap();
158        let events: Vec<_> = stream.collect().await;
159
160        // Should have events from both runners (2 events per event_count each)
161        assert!(events.len() >= 4, "Should have at least 4 events");
162
163        // Check for events from different sources (SSL from FakeRunner, potentially processed by analyzers)
164        let sources: std::collections::HashSet<_> = events.iter().map(|e| &e.source).collect();
165        assert!(
166            !sources.is_empty(),
167            "Should have events from at least one source"
168        );
169    }
170
171    #[tokio::test]
172    async fn test_agent_runner_empty_runners() {
173        let mut agent = AgentRunner::new();
174
175        assert_eq!(agent.runner_count(), 0);
176
177        let result = agent.run().await;
178        assert!(result.is_err(), "Should fail with no runners configured");
179
180        if let Err(e) = result {
181            let error_msg = format!("{}", e);
182            assert!(
183                error_msg.contains("No runners configured"),
184                "Should have descriptive error message"
185            );
186        }
187    }
188
189    #[tokio::test]
190    async fn test_agent_runner_streaming_behavior() {
191        use std::sync::Arc;
192        use std::time::Instant;
193        use tokio::sync::Mutex;
194
195        let event_timestamps = Arc::new(Mutex::new(Vec::new()));
196
197        // Custom analyzer that records timestamps
198        struct TimestampRecorder {
199            timestamps: Arc<Mutex<Vec<Instant>>>,
200        }
201
202        impl TimestampRecorder {
203            fn new(timestamps: Arc<Mutex<Vec<Instant>>>) -> Self {
204                Self { timestamps }
205            }
206        }
207
208        #[async_trait]
209        impl Analyzer for TimestampRecorder {
210            async fn process(
211                &mut self,
212                stream: EventStream,
213            ) -> Result<EventStream, Box<dyn std::error::Error + Send + Sync>> {
214                let timestamps = self.timestamps.clone();
215                let recorded_stream = stream.map(move |event| {
216                    let timestamps_clone = timestamps.clone();
217                    tokio::spawn(async move {
218                        let mut guard = timestamps_clone.lock().await;
219                        guard.push(Instant::now());
220                    });
221                    event
222                });
223                Ok(Box::pin(recorded_stream))
224            }
225        }
226
227        let fake_runner = FakeRunner::new().event_count(3).delay_ms(50); // Longer delay to ensure streaming behavior
228
229        let mut agent = AgentRunner::new()
230            .add_runner(Box::new(fake_runner))
231            .add_global_analyzer(Box::new(TimestampRecorder::new(Arc::clone(
232                &event_timestamps,
233            ))));
234
235        let start_time = Instant::now();
236        let stream = agent.run().await.unwrap();
237        let events: Vec<_> = stream.collect().await;
238        let total_time = start_time.elapsed();
239
240        // Wait for async timestamp recording to complete
241        tokio::time::sleep(Duration::from_millis(100)).await;
242
243        assert_eq!(events.len(), 6); // 3 event_count × 2 events per count (request + response)
244
245        let timestamps_guard = event_timestamps.lock().await;
246        assert!(
247            timestamps_guard.len() >= 3,
248            "Should have recorded multiple timestamps"
249        );
250
251        // Should take some time due to delays, indicating streaming behavior
252        assert!(
253            total_time >= Duration::from_millis(100),
254            "Should demonstrate streaming behavior"
255        );
256    }
257
258    #[tokio::test]
259    async fn test_agent_runner_concurrent_processing() {
260        // Test multiple agent runners running concurrently
261        let mut handles = Vec::new();
262
263        for i in 0..3 {
264            let handle = tokio::spawn(async move {
265                let fake_runner = FakeRunner::new().event_count(2).delay_ms(10);
266
267                let mut agent = AgentRunner::new().add_runner(Box::new(fake_runner));
268
269                let stream = agent.run().await.unwrap();
270                let events: Vec<_> = stream.collect().await;
271
272                (i, events.len())
273            });
274            handles.push(handle);
275        }
276
277        // Wait for all agents to complete
278        let mut results = Vec::new();
279        for handle in handles {
280            results.push(handle.await.unwrap());
281        }
282
283        // All agents should have processed events
284        assert_eq!(results.len(), 3);
285        for (agent_id, event_count) in results {
286            assert_eq!(
287                event_count, 4,
288                "Agent {} should have 4 events (2 event_count × 2 events)",
289                agent_id
290            );
291        }
292    }
293
294    #[tokio::test]
295    async fn test_agent_runner_error_handling() {
296        // Test that agent runner handles runner failures gracefully
297
298        // Create a mock runner that fails
299        struct FailingRunner;
300
301        #[async_trait]
302        impl Runner for FailingRunner {
303            async fn run(&mut self) -> Result<EventStream, RunnerError> {
304                Err("Simulated runner failure".into())
305            }
306
307            fn add_analyzer(self, _analyzer: Box<dyn Analyzer>) -> Self {
308                self
309            }
310        }
311
312        let mut agent = AgentRunner::new().add_runner(Box::new(FailingRunner));
313
314        let result = agent.run().await;
315        assert!(result.is_err(), "Should propagate runner error");
316
317        if let Err(e) = result {
318            let error_msg = format!("{}", e);
319            assert!(
320                error_msg.contains("Simulated runner failure"),
321                "Should contain original error message"
322            );
323        }
324    }
325
326    #[tokio::test]
327    async fn test_agent_runner_with_timeout() {
328        // Test agent runner with timeout to ensure it doesn't hang
329        let fake_runner = FakeRunner::new().event_count(5).delay_ms(10);
330
331        let mut agent = AgentRunner::new().add_runner(Box::new(fake_runner));
332
333        let result = timeout(Duration::from_secs(5), async {
334            let stream = agent.run().await.unwrap();
335            let events: Vec<_> = stream.collect().await;
336            events.len()
337        })
338        .await;
339
340        assert!(result.is_ok(), "AgentRunner should complete within timeout");
341        assert_eq!(
342            result.unwrap(),
343            10,
344            "Should process all events (5 event_count × 2 events)"
345        );
346    }
347
348    #[tokio::test]
349    async fn test_agent_runner_fluent_interface() {
350        // Test that the fluent interface works correctly
351        let fake_runner1 = FakeRunner::new().event_count(1).delay_ms(10);
352        let fake_runner2 = FakeRunner::new().event_count(1).delay_ms(10);
353
354        let agent = AgentRunner::new()
355            .add_runner(Box::new(fake_runner1))
356            .add_runner(Box::new(fake_runner2))
357            .add_global_analyzer(Box::new(materializer()))
358            .add_analyzer(Box::new(materializer()));
359
360        assert_eq!(agent.runner_count(), 2);
361        assert_eq!(agent.analyzer_count(), 2); // Both global analyzers should be present
362    }
363}