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}