1pub use cookie_agent_server::{
4 ClientDelivery, ClientError, ClientProtocol, validate_websocket_url,
5};
6
7use std::{ops::Deref, sync::Arc};
8
9use cookie_agent_protocol::Transport;
10use cookie_agent_server::{Server, WebSocketTransport, in_process_pair};
11
12#[derive(Clone)]
14pub struct Client(cookie_agent_protocol::Client);
15
16impl Client {
17 pub fn connect_stream<T: Transport + 'static>(transport: T) -> Self {
18 Self(cookie_agent_protocol::Client::connect_display_stream(
19 transport,
20 ))
21 }
22
23 pub fn connect_in_process(server: Arc<Server>) -> Self {
24 let (client, service) = in_process_pair(128);
25 tokio::spawn(async move {
26 let _ = server.serve_stream(service).await;
27 });
28 Self::connect_stream(client)
29 }
30
31 pub async fn connect_websocket_with_token(url: &str, token: &str) -> Result<Self, ClientError> {
32 WebSocketTransport::connect_with_token(url, token)
33 .await
34 .map(Self::connect_stream)
35 .map_err(|error| {
36 ClientError::WebSocket(cookie_agent_protocol::diagnostics::sanitize(
37 &cookie_agent_protocol::diagnostics::error_chain(&error),
38 4096,
39 ))
40 })
41 }
42}
43
44impl Deref for Client {
45 type Target = cookie_agent_protocol::Client;
46
47 fn deref(&self) -> &Self::Target {
48 &self.0
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use crate::state::{DeliveryOutcome, StateStore, ToolStatus};
56 use base64::{Engine as _, engine::general_purpose::STANDARD};
57 use cookie_agent_protocol::{EventPayload, MessageFrame, SessionId, StoredEvent, ToolCallId};
58
59 #[tokio::test]
60 async fn sustained_raw_output_with_fast_tui_consumer() {
61 sustained_raw_output(false).await;
62 }
63
64 #[tokio::test]
65 async fn sustained_raw_output_with_stalled_tui_consumer() {
66 sustained_raw_output(true).await;
67 }
68
69 async fn sustained_raw_output(stalled: bool) {
70 let session_id = SessionId::new_v7();
71 let run_id = cookie_agent_protocol::RunId::new_v7();
72 let call_id = ToolCallId::new_v7();
73 let owner = cookie_agent_protocol::AssistantToolCallRef {
74 model_turn_seq: 1,
75 content_index: 0,
76 model_call_id: cookie_agent_protocol::ModelCallId::new("test").unwrap(),
77 provider_item_id: None,
78 };
79 let events = [
80 EventPayload::ToolCallStarted {
81 start: cookie_agent_protocol::ToolCallStart {
82 tool_call_id: call_id,
83 owner: owner.clone(),
84 output: Default::default(),
85 presentation: cookie_agent_protocol::ToolCallPresentation {
86 title: cookie_agent_protocol::SafeDisplayText::new("Test").unwrap(),
87 primary_argument: None,
88 },
89 operation_fingerprint: serde_json::from_value(
90 serde_json::json!({"digest": "1".repeat(64)}),
91 )
92 .unwrap(),
93 },
94 },
95 EventPayload::ToolCallProgress {
96 tool_call_id: call_id,
97 message: cookie_agent_protocol::SafeDisplayText::new("working").unwrap(),
98 display: Some("live display".into()),
99 },
100 EventPayload::ToolCallTerminated {
101 termination: cookie_agent_protocol::ToolCallTermination {
102 tool_call_id: call_id,
103 owner,
104 outcome: cookie_agent_protocol::ToolTerminationOutcome::Completed,
105 error: None,
106 result: Some(cookie_agent_protocol::PersistedToolResult {
107 title: cookie_agent_protocol::SafeDisplayText::new("Test").unwrap(),
108 output: "authoritative preview".into(),
109 display: Some("final display".into()),
110 retained_output: None,
111 metadata: serde_json::json!({}),
112 truncation: None,
113 attachments: Vec::new(),
114 additional_messages: Vec::new(),
115 }),
116 },
117 },
118 EventPayload::RunCompleted { final_text: None },
119 ];
120 let (transport, mut server) = in_process_pair(1);
121 let client = Client::connect_stream(transport);
122 let mut receiver = client.subscribe_deliveries().unwrap();
123 let consume = move |mut receiver: tokio::sync::mpsc::UnboundedReceiver<ClientDelivery>| async move {
124 let mut store = StateStore::default();
125 for seq in 1..=4 {
126 let delivery = receiver.recv().await.unwrap();
127 assert!(matches!(delivery, ClientDelivery::Live { .. }));
128 assert_eq!(store.apply_delivery(delivery), DeliveryOutcome::Applied);
129 assert_eq!(store.sessions[&session_id].last_seq, seq);
130 if seq == 2 {
131 assert_eq!(
132 store.sessions[&session_id].tools[&call_id].detail,
133 "live display"
134 );
135 assert_eq!(
136 store.sessions[&session_id].tools[&call_id].status,
137 ToolStatus::Running
138 );
139 }
140 }
141 assert!(receiver.is_empty());
142 assert_eq!(
143 store.sessions[&session_id].tools[&call_id].detail,
144 "final display"
145 );
146 assert_eq!(
147 store.sessions[&session_id].tools[&call_id].status,
148 ToolStatus::Completed
149 );
150 };
151 let consumer = if stalled {
152 None
153 } else {
154 let (_, empty) = tokio::sync::mpsc::unbounded_channel();
155 Some(tokio::spawn(consume(std::mem::replace(
156 &mut receiver,
157 empty,
158 ))))
159 };
160 let producer = tokio::spawn(async move {
161 let data = STANDARD.encode(vec![b'x'; 64 * 1024]);
162 for (index, payload) in events.into_iter().enumerate() {
163 if index == 2 {
164 for chunk in 0..1600 {
165 let delta = cookie_agent_protocol::OutputDelta {
166 call_id,
167 stream: cookie_agent_protocol::OutputStream::Single,
168 byte_offset: chunk * 64 * 1024,
169 data: data.clone(),
170 };
171 server.send(MessageFrame::Value(serde_json::json!({
172 "jsonrpc": "2.0", "method": "events.tool_output_delta", "params": delta
173 }))).await.unwrap();
174 if chunk % 400 == 0 {
175 server.send(MessageFrame::Value(serde_json::json!({
176 "jsonrpc": "2.0", "method": "events.tool_output_snapshot",
177 "params": {"stream": delta.stream, "snapshot": {
178 "call_id": call_id, "start_offset": delta.byte_offset,
179 "end_offset": delta.byte_offset + 64 * 1024, "chunks": [delta]
180 }}
181 }))).await.unwrap();
182 server.send(MessageFrame::Value(serde_json::json!({
183 "jsonrpc": "2.0", "method": "events.tool_output_gap",
184 "params": {"call_id": call_id, "stream": delta.stream, "next_offset": delta.byte_offset}
185 }))).await.unwrap();
186 }
187 }
188 }
189 let event = StoredEvent {
190 engine_version: None,
191 origin: None,
192 session_id,
193 run_id: Some(run_id),
194 seq: index as u64 + 1,
195 timestamp: jiff::Timestamp::now(),
196 payload,
197 };
198 server.send(MessageFrame::Value(serde_json::json!({
199 "jsonrpc": "2.0", "method": "events.subscription",
200 "params": cookie_agent_protocol::EventSubscriptionMessage::Event { event: Box::new(event) }
201 }))).await.unwrap();
202 }
203 let request = server.recv().await.unwrap().unwrap();
204 let MessageFrame::Value(request) = request else {
205 panic!("expected request")
206 };
207 server
208 .send(MessageFrame::Value(serde_json::json!({
209 "jsonrpc": "2.0", "id": request["id"], "result": null
210 })))
211 .await
212 .unwrap();
213 });
214 client
216 .call::<_, serde_json::Value>("test.fence", &())
217 .await
218 .unwrap();
219 if let Some(consumer) = consumer {
220 consumer.await.unwrap();
221 } else {
222 assert_eq!(
223 receiver.len(),
224 4,
225 "100 MiB raw output must not enter the TUI queue"
226 );
227 consume(receiver).await;
228 }
229 producer.await.unwrap();
230 client.shutdown();
231 }
232}