Skip to main content

ag_ui/client/transport/
replay.rs

1//! A transport that replays a scripted list of events.
2//!
3//! Testing a client against a live agent is slow, flaky, and needs a model. It
4//! is also unnecessary: the agent's half of the conversation is just a list of
5//! events. [`ReplayTransport`] serves one — and records the
6//! [`RunAgentInput`](https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/input/struct.RunAgentInput.html)s it was handed, which is how a test asserts that a resume
7//! carried the right answers.
8//!
9//! ```
10//! use ag_ui::client::transport::ReplayTransport;
11//! use ag_ui::Event;
12//!
13//! let transport = ReplayTransport::new([
14//!     Event::run_started("thread-1", "run-1"),
15//!     Event::run_finished_success("thread-1", "run-1"),
16//! ]);
17//! ```
18
19use std::collections::VecDeque;
20use std::sync::{Arc, Mutex, MutexGuard};
21
22use crate::{Event, RunAgentInput};
23
24use crate::client::error::Error;
25use crate::client::transport::{EventStream, Transport, TransportFuture};
26
27/// A [`Transport`] that answers each run from a script.
28///
29/// Cloning shares the script and the recording, so a test can keep a handle
30/// after handing one to a [`Session`](crate::client::Session).
31#[derive(Clone, Debug, Default)]
32pub struct ReplayTransport {
33    inner: Arc<Mutex<Script>>,
34}
35
36#[derive(Debug, Default)]
37struct Script {
38    runs: VecDeque<Vec<Event>>,
39    requests: Vec<RunAgentInput>,
40}
41
42impl ReplayTransport {
43    /// A transport that answers the first run with these events, and every
44    /// later run with an error.
45    pub fn new(events: impl IntoIterator<Item = Event>) -> Self {
46        Self::with_runs([events.into_iter().collect::<Vec<_>>()])
47    }
48
49    /// A transport that answers each run with the next list in the script.
50    ///
51    /// This is what a human-in-the-loop round trip needs: the first run pauses
52    /// on an interrupt, the second — the resume — carries on.
53    pub fn with_runs(runs: impl IntoIterator<Item = Vec<Event>>) -> Self {
54        Self {
55            inner: Arc::new(Mutex::new(Script {
56                runs: runs.into_iter().collect(),
57                requests: Vec::new(),
58            })),
59        }
60    }
61
62    /// Every request this transport has been handed, in order.
63    pub fn requests(&self) -> Vec<RunAgentInput> {
64        self.lock().requests.clone()
65    }
66
67    /// The most recent request, if there has been one.
68    pub fn last_request(&self) -> Option<RunAgentInput> {
69        self.lock().requests.last().cloned()
70    }
71
72    /// How many runs are left in the script.
73    pub fn remaining(&self) -> usize {
74        self.lock().runs.len()
75    }
76
77    /// A poisoned lock still holds a perfectly good script — a test that
78    /// panicked mid-assert should fail on that panic, not on this mutex.
79    fn lock(&self) -> MutexGuard<'_, Script> {
80        self.inner.lock().unwrap_or_else(|error| error.into_inner())
81    }
82}
83
84impl Transport for ReplayTransport {
85    fn run(&self, input: RunAgentInput) -> TransportFuture {
86        let mut script = self.lock();
87        script.requests.push(input);
88        let next = script.runs.pop_front();
89        drop(script);
90
91        Box::pin(async move {
92            let Some(events) = next else {
93                return Err(Error::Transport(
94                    "the replay script has no runs left".into(),
95                ));
96            };
97            let stream = futures_util::stream::iter(events.into_iter().map(Ok));
98            Ok(Box::pin(stream) as EventStream)
99        })
100    }
101}