Skip to main content

board_watch/
lib.rs

1//! A terminal client for any AG-UI agent — and the awkward agent it is aimed
2//! at.
3//!
4//! Round two of dogfooding this SDK, from the consuming side. `task-board` was
5//! an agent with a client attached; this is a *client*, and the server here
6//! exists only to give it something hostile to read.
7//!
8//! - [`watch`] is the application: send a line, render the run, answer what it
9//!   pauses on, draw the board.
10//! - [`view`] is what it prints, including a small A2UI renderer.
11//! - [`trace`] is the same conversation one level down — events exactly as they
12//!   arrived, and a resume built without a session.
13//! - [`fake`] is the backend: chunked text, tool arguments split mid-escape,
14//!   parallel calls, a run that never finishes, and hand-framed streams the
15//!   protocol forbids.
16//! - [`board`] is the client's own model of the agent's state, declared
17//!   independently of the server's.
18//!
19//! Nothing needs a key or a network beyond loopback.
20
21pub mod board;
22pub mod fake;
23pub mod trace;
24pub mod view;
25pub mod watch;
26
27pub use board::Board;
28pub use watch::{Console, Policy, Watch};
29
30use ag_ui::Event;
31use ag_ui::client::transport::ReplayTransport;
32
33/// Reads the tools this client is willing to have called.
34///
35/// # Why a client needs this at all
36///
37/// In AG-UI the *client* offers the tools and the agent picks from them, so an
38/// agent that executes `add_task` only sees it if the front-end sent it. There
39/// is no handshake: nothing in the protocol lets a generic client ask an agent
40/// what it needs, and an agent handed none simply fails the run. A client that
41/// is not written against one specific agent therefore has to be *configured*
42/// with the tool set, which is what this loads.
43///
44/// # Errors
45///
46/// The file's contents, if they are not a JSON array of tool definitions.
47pub fn load_tools(json: &str) -> serde_json::Result<Vec<ag_ui::Tool>> {
48    serde_json::from_str(json)
49}
50
51/// Reads a recorded run — a JSON array of events — into a transport.
52///
53/// The reason [`Transport`](ag_ui::client::Transport) is a trait: a fixture on
54/// disk substitutes for a server, and nothing above it changes. `board-watch
55/// replay` is the whole client with the network taken out.
56///
57/// # Errors
58///
59/// The file's contents, if they are not a JSON array of events this build
60/// knows. An unknown event type is an error rather than a skipped line — see
61/// `docs/DESIGN.md`.
62pub fn replay_fixture(json: &str) -> serde_json::Result<ReplayTransport> {
63    let runs: Vec<Vec<Event>> = serde_json::from_str(json)?;
64    Ok(ReplayTransport::with_runs(runs))
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn a_fixture_is_a_list_of_runs() {
73        let transport = replay_fixture(
74            r#"[[{"type":"RUN_STARTED","threadId":"t","runId":"r"}],
75                [{"type":"RUN_STARTED","threadId":"t","runId":"r2"}]]"#,
76        )
77        .expect("two runs");
78        assert_eq!(transport.remaining(), 2);
79    }
80
81    /// The protocol's own commitment, from the consuming side: an event this
82    /// build does not know stops the load rather than being skipped.
83    #[test]
84    fn an_unknown_event_type_is_refused() {
85        let error = replay_fixture(r#"[[{"type":"TELEPATHY","vibes":9}]]"#)
86            .expect_err("an unknown event should not load");
87        assert!(error.to_string().contains("TELEPATHY"), "{error}");
88    }
89}