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 pub threads: bool,
43 pub turns: bool,
44 pub turn_steer: bool,
45 pub turn_interrupt: bool,
46 pub event_replay: bool,
47 pub external_tools: bool,
48 pub environments: bool,
49 pub worker_runtime: bool,
50}
51
52#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct RuntimeExperimentalCapabilities {
57 #[serde(default)]
58 pub environments: bool,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
85pub struct DynamicToolSpec {
86 #[serde(skip_serializing_if = "Option::is_none")]
90 pub namespace: Option<String>,
91
92 pub name: String,
94
95 pub description: String,
97
98 pub input_schema: Value,
100
101 #[serde(default)]
107 pub defer_loading: bool,
108}
109
110#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
113#[serde(rename_all = "snake_case")]
114pub enum DynamicToolItemStatus {
115 InProgress,
116 Completed,
117 Failed,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
125pub struct DynamicToolCallParams {
126 pub thread_id: String,
127 pub turn_id: String,
128 pub call_id: String,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub namespace: Option<String>,
133
134 pub tool: String,
136
137 pub arguments: Value,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
143pub struct DynamicToolCallResult {
144 pub success: bool,
146
147 #[serde(default)]
152 pub content: Vec<DynamicToolCallContent>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
157#[serde(tag = "type", rename_all = "snake_case")]
158pub enum DynamicToolCallContent {
159 InputText { text: String },
160 InputImage { image_url: String },
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
178pub struct TurnEnvironmentParams {
179 pub environment_id: String,
180 pub cwd: PathBuf,
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use serde_json::json;
187
188 #[test]
189 fn dynamic_tool_spec_roundtrip() {
190 let spec = DynamicToolSpec {
191 namespace: Some("tau_bench".into()),
192 name: "get_reservation".into(),
193 description: "Look up an airline reservation.".into(),
194 input_schema: json!({
195 "type": "object",
196 "properties": {
197 "reservation_id": { "type": "string" }
198 },
199 "required": ["reservation_id"],
200 "additionalProperties": false
201 }),
202 defer_loading: false,
203 };
204
205 let serialized = serde_json::to_string(&spec).unwrap();
206 let deserialized: DynamicToolSpec = serde_json::from_str(&serialized).unwrap();
207 assert_eq!(spec, deserialized);
208 }
209
210 #[test]
211 fn dynamic_tool_spec_omits_defer_loading_defaults_false() {
212 let json = r#"{
213 "namespace": "tau_bench",
214 "name": "get_reservation",
215 "description": "Look up an airline reservation.",
216 "input_schema": { "type": "object" }
217 }"#;
218
219 let spec: DynamicToolSpec = serde_json::from_str(json).unwrap();
220 assert_eq!(spec.namespace, Some("tau_bench".into()));
221 assert_eq!(spec.name, "get_reservation");
222 assert!(!spec.defer_loading);
223 }
224
225 #[test]
226 fn dynamic_tool_item_status_snake_case() {
227 assert_eq!(
228 serde_json::to_string(&DynamicToolItemStatus::InProgress).unwrap(),
229 "\"in_progress\""
230 );
231 assert_eq!(
232 serde_json::from_str::<DynamicToolItemStatus>("\"completed\"").unwrap(),
233 DynamicToolItemStatus::Completed
234 );
235 assert_eq!(
236 serde_json::from_str::<DynamicToolItemStatus>("\"failed\"").unwrap(),
237 DynamicToolItemStatus::Failed
238 );
239 }
240
241 #[test]
242 fn dynamic_tool_call_params_roundtrip() {
243 let params = DynamicToolCallParams {
244 thread_id: "thr_123".into(),
245 turn_id: "turn_456".into(),
246 call_id: "call_abc".into(),
247 namespace: Some("tau_bench".into()),
248 tool: "get_reservation".into(),
249 arguments: json!({ "reservation_id": "ABC123" }),
250 };
251
252 let serialized = serde_json::to_string(¶ms).unwrap();
253 let deserialized: DynamicToolCallParams = serde_json::from_str(&serialized).unwrap();
254 assert_eq!(params, deserialized);
255 }
256
257 #[test]
258 fn dynamic_tool_call_content_roundtrip() {
259 let content = vec![
260 DynamicToolCallContent::InputText {
261 text: "{\"status\":\"confirmed\"}".into(),
262 },
263 DynamicToolCallContent::InputImage {
264 image_url: "http://example.com/receipt.png".into(),
265 },
266 ];
267
268 let value = serde_json::to_value(&content).unwrap();
269 let deserialized: Vec<DynamicToolCallContent> = serde_json::from_value(value).unwrap();
270 assert_eq!(content, deserialized);
271
272 assert_eq!(
274 serde_json::to_string(&DynamicToolCallContent::InputText { text: "x".into() }).unwrap(),
275 r#"{"type":"input_text","text":"x"}"#
276 );
277 assert_eq!(
278 serde_json::to_string(&DynamicToolCallContent::InputImage {
279 image_url: "y".into()
280 })
281 .unwrap(),
282 r#"{"type":"input_image","image_url":"y"}"#
283 );
284 }
285
286 #[test]
287 fn dynamic_tool_call_result_defaults_empty_content() {
288 let json = r#"{ "success": false }"#;
289 let result: DynamicToolCallResult = serde_json::from_str(json).unwrap();
290 assert!(!result.success);
291 assert!(result.content.is_empty());
292 }
293
294 #[test]
295 fn dynamic_tool_call_result_roundtrip_with_content() {
296 let result = DynamicToolCallResult {
297 success: true,
298 content: vec![DynamicToolCallContent::InputText {
299 text: "done".into(),
300 }],
301 };
302
303 let serialized = serde_json::to_string(&result).unwrap();
304 let deserialized: DynamicToolCallResult = serde_json::from_str(&serialized).unwrap();
305 assert_eq!(result, deserialized);
306 }
307
308 #[test]
309 fn turn_environment_params_roundtrip() {
310 let env = TurnEnvironmentParams {
311 environment_id: "local".into(),
312 cwd: PathBuf::from("/workspace"),
313 };
314
315 let serialized = serde_json::to_string(&env).unwrap();
316 let deserialized: TurnEnvironmentParams = serde_json::from_str(&serialized).unwrap();
317 assert_eq!(env, deserialized);
318
319 let from_spec = r#"{
321 "environment_id": "local",
322 "cwd": "/workspace"
323 }"#;
324 let parsed: TurnEnvironmentParams = serde_json::from_str(from_spec).unwrap();
325 assert_eq!(parsed.environment_id, "local");
326 assert_eq!(parsed.cwd, PathBuf::from("/workspace"));
327 }
328
329 #[test]
330 fn runtime_capabilities_serializes_expected_shape() {
331 let caps = RuntimeCapabilities {
332 threads: true,
333 turns: true,
334 turn_steer: true,
335 turn_interrupt: true,
336 event_replay: true,
337 external_tools: false,
338 environments: false,
339 worker_runtime: false,
340 };
341 let value = serde_json::to_value(&caps).unwrap();
342 let obj = value.as_object().unwrap();
343 assert_eq!(obj.get("threads").unwrap(), &json!(true));
344 assert_eq!(obj.get("external_tools").unwrap(), &json!(false));
345 assert!(obj.contains_key("worker_runtime"));
346 }
347
348 #[test]
349 fn runtime_event_envelope_schema_version_default() {
350 let json = r#"{
351 "seq": 1,
352 "event": "test",
353 "kind": "test",
354 "thread_id": "thr_1",
355 "timestamp": "2026-06-12T00:00:00Z",
356 "payload": {}
357 }"#;
358 let envelope: RuntimeEventEnvelope = serde_json::from_str(json).unwrap();
359 assert_eq!(
360 envelope.schema_version,
361 RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION
362 );
363 }
364}