codewhale_protocol/runtime/
mod.rs1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7pub const RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION: u32 = 1;
8pub const RUNTIME_API_VERSION: &str = "1.0";
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RuntimeEventEnvelope {
12 #[serde(default = "default_runtime_event_envelope_schema_version")]
13 pub schema_version: u32,
14 pub seq: u64,
15 pub event: String,
16 pub kind: String,
17 pub thread_id: String,
18 pub turn_id: Option<String>,
19 pub item_id: Option<String>,
20 pub timestamp: String,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub created_at: Option<String>,
23 pub payload: Value,
24 #[serde(default)]
25 #[serde(flatten)]
26 pub extra: BTreeMap<String, Value>,
27}
28
29fn default_runtime_event_envelope_schema_version() -> u32 {
30 RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct RuntimeCapabilities {
42 #[serde(default)]
43 pub account_session: bool,
44 pub threads: bool,
45 pub turns: bool,
46 pub turn_steer: bool,
47 pub turn_interrupt: bool,
48 pub event_replay: bool,
49 pub external_tools: bool,
50 pub environments: bool,
51 pub worker_runtime: bool,
52 #[serde(default)]
53 pub fleet_run_create: bool,
54 #[serde(default)]
55 pub fleet_run_start: bool,
56 #[serde(default)]
57 pub fleet_event_replay: bool,
58 #[serde(default)]
59 pub fleet_event_stream: bool,
60 #[serde(default)]
61 pub fleet_local_target: bool,
62}
63
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct RuntimeExperimentalCapabilities {
69 #[serde(default)]
70 pub environments: bool,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct DynamicToolSpec {
98 #[serde(skip_serializing_if = "Option::is_none")]
102 pub namespace: Option<String>,
103
104 pub name: String,
106
107 pub description: String,
109
110 pub input_schema: Value,
112
113 #[serde(default)]
119 pub defer_loading: bool,
120}
121
122#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
125#[serde(rename_all = "snake_case")]
126pub enum DynamicToolItemStatus {
127 InProgress,
128 Completed,
129 Failed,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137pub struct DynamicToolCallParams {
138 pub thread_id: String,
139 pub turn_id: String,
140 pub call_id: String,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub namespace: Option<String>,
145
146 pub tool: String,
148
149 pub arguments: Value,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155pub struct DynamicToolCallResult {
156 pub success: bool,
158
159 #[serde(default)]
164 pub content: Vec<DynamicToolCallContent>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169#[serde(tag = "type", rename_all = "snake_case")]
170pub enum DynamicToolCallContent {
171 InputText { text: String },
172 InputImage { image_url: String },
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct TurnEnvironmentParams {
191 pub environment_id: String,
192 pub cwd: PathBuf,
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use serde_json::json;
199
200 #[test]
201 fn dynamic_tool_spec_roundtrip() {
202 let spec = DynamicToolSpec {
203 namespace: Some("tau_bench".into()),
204 name: "get_reservation".into(),
205 description: "Look up an airline reservation.".into(),
206 input_schema: json!({
207 "type": "object",
208 "properties": {
209 "reservation_id": { "type": "string" }
210 },
211 "required": ["reservation_id"],
212 "additionalProperties": false
213 }),
214 defer_loading: false,
215 };
216
217 let serialized = serde_json::to_string(&spec).unwrap();
218 let deserialized: DynamicToolSpec = serde_json::from_str(&serialized).unwrap();
219 assert_eq!(spec, deserialized);
220 }
221
222 #[test]
223 fn dynamic_tool_spec_omits_defer_loading_defaults_false() {
224 let json = r#"{
225 "namespace": "tau_bench",
226 "name": "get_reservation",
227 "description": "Look up an airline reservation.",
228 "input_schema": { "type": "object" }
229 }"#;
230
231 let spec: DynamicToolSpec = serde_json::from_str(json).unwrap();
232 assert_eq!(spec.namespace, Some("tau_bench".into()));
233 assert_eq!(spec.name, "get_reservation");
234 assert!(!spec.defer_loading);
235 }
236
237 #[test]
238 fn dynamic_tool_item_status_snake_case() {
239 assert_eq!(
240 serde_json::to_string(&DynamicToolItemStatus::InProgress).unwrap(),
241 "\"in_progress\""
242 );
243 assert_eq!(
244 serde_json::from_str::<DynamicToolItemStatus>("\"completed\"").unwrap(),
245 DynamicToolItemStatus::Completed
246 );
247 assert_eq!(
248 serde_json::from_str::<DynamicToolItemStatus>("\"failed\"").unwrap(),
249 DynamicToolItemStatus::Failed
250 );
251 }
252
253 #[test]
254 fn dynamic_tool_call_params_roundtrip() {
255 let params = DynamicToolCallParams {
256 thread_id: "thr_123".into(),
257 turn_id: "turn_456".into(),
258 call_id: "call_abc".into(),
259 namespace: Some("tau_bench".into()),
260 tool: "get_reservation".into(),
261 arguments: json!({ "reservation_id": "ABC123" }),
262 };
263
264 let serialized = serde_json::to_string(¶ms).unwrap();
265 let deserialized: DynamicToolCallParams = serde_json::from_str(&serialized).unwrap();
266 assert_eq!(params, deserialized);
267 }
268
269 #[test]
270 fn dynamic_tool_call_content_roundtrip() {
271 let content = vec![
272 DynamicToolCallContent::InputText {
273 text: "{\"status\":\"confirmed\"}".into(),
274 },
275 DynamicToolCallContent::InputImage {
276 image_url: "http://example.com/receipt.png".into(),
277 },
278 ];
279
280 let value = serde_json::to_value(&content).unwrap();
281 let deserialized: Vec<DynamicToolCallContent> = serde_json::from_value(value).unwrap();
282 assert_eq!(content, deserialized);
283
284 assert_eq!(
286 serde_json::to_string(&DynamicToolCallContent::InputText { text: "x".into() }).unwrap(),
287 r#"{"type":"input_text","text":"x"}"#
288 );
289 assert_eq!(
290 serde_json::to_string(&DynamicToolCallContent::InputImage {
291 image_url: "y".into()
292 })
293 .unwrap(),
294 r#"{"type":"input_image","image_url":"y"}"#
295 );
296 }
297
298 #[test]
299 fn dynamic_tool_call_result_defaults_empty_content() {
300 let json = r#"{ "success": false }"#;
301 let result: DynamicToolCallResult = serde_json::from_str(json).unwrap();
302 assert!(!result.success);
303 assert!(result.content.is_empty());
304 }
305
306 #[test]
307 fn dynamic_tool_call_result_roundtrip_with_content() {
308 let result = DynamicToolCallResult {
309 success: true,
310 content: vec![DynamicToolCallContent::InputText {
311 text: "done".into(),
312 }],
313 };
314
315 let serialized = serde_json::to_string(&result).unwrap();
316 let deserialized: DynamicToolCallResult = serde_json::from_str(&serialized).unwrap();
317 assert_eq!(result, deserialized);
318 }
319
320 #[test]
321 fn turn_environment_params_roundtrip() {
322 let env = TurnEnvironmentParams {
323 environment_id: "local".into(),
324 cwd: PathBuf::from("/workspace"),
325 };
326
327 let serialized = serde_json::to_string(&env).unwrap();
328 let deserialized: TurnEnvironmentParams = serde_json::from_str(&serialized).unwrap();
329 assert_eq!(env, deserialized);
330
331 let from_spec = r#"{
333 "environment_id": "local",
334 "cwd": "/workspace"
335 }"#;
336 let parsed: TurnEnvironmentParams = serde_json::from_str(from_spec).unwrap();
337 assert_eq!(parsed.environment_id, "local");
338 assert_eq!(parsed.cwd, PathBuf::from("/workspace"));
339 }
340
341 #[test]
342 fn runtime_capabilities_serializes_expected_shape() {
343 let caps = RuntimeCapabilities {
344 account_session: true,
345 threads: true,
346 turns: true,
347 turn_steer: true,
348 turn_interrupt: true,
349 event_replay: true,
350 external_tools: false,
351 environments: false,
352 worker_runtime: false,
353 fleet_run_create: true,
354 fleet_run_start: true,
355 fleet_event_replay: true,
356 fleet_event_stream: true,
357 fleet_local_target: true,
358 };
359 let value = serde_json::to_value(&caps).unwrap();
360 let obj = value.as_object().unwrap();
361 assert_eq!(obj.get("threads").unwrap(), &json!(true));
362 assert_eq!(obj.get("account_session").unwrap(), &json!(true));
363 assert_eq!(obj.get("external_tools").unwrap(), &json!(false));
364 assert!(obj.contains_key("worker_runtime"));
365 assert_eq!(obj.get("fleet_run_create").unwrap(), &json!(true));
366 assert_eq!(obj.get("fleet_event_stream").unwrap(), &json!(true));
367 }
368
369 #[test]
370 fn runtime_event_envelope_schema_version_default() {
371 let json = r#"{
372 "seq": 1,
373 "event": "test",
374 "kind": "test",
375 "thread_id": "thr_1",
376 "timestamp": "2026-06-12T00:00:00Z",
377 "payload": {}
378 }"#;
379 let envelope: RuntimeEventEnvelope = serde_json::from_str(json).unwrap();
380 assert_eq!(
381 envelope.schema_version,
382 RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION
383 );
384 }
385}