1use 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 #[serde(default)]
65 pub thread_goals: bool,
66 #[serde(default)]
71 pub memory: bool,
72 #[serde(default)]
76 pub mcp_server_management: bool,
77 #[serde(default)]
80 pub skill_lifecycle: bool,
81}
82
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
87pub struct RuntimeExperimentalCapabilities {
88 #[serde(default)]
89 pub environments: bool,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
116pub struct DynamicToolSpec {
117 #[serde(skip_serializing_if = "Option::is_none")]
121 pub namespace: Option<String>,
122
123 pub name: String,
125
126 pub description: String,
128
129 pub input_schema: Value,
131
132 #[serde(default)]
138 pub defer_loading: bool,
139}
140
141#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
144#[serde(rename_all = "snake_case")]
145pub enum DynamicToolItemStatus {
146 InProgress,
147 Completed,
148 Failed,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
156pub struct DynamicToolCallParams {
157 pub thread_id: String,
158 pub turn_id: String,
159 pub call_id: String,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub namespace: Option<String>,
164
165 pub tool: String,
167
168 pub arguments: Value,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
174pub struct DynamicToolCallResult {
175 pub success: bool,
177
178 #[serde(default)]
183 pub content: Vec<DynamicToolCallContent>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
188#[serde(tag = "type", rename_all = "snake_case")]
189pub enum DynamicToolCallContent {
190 InputText { text: String },
191 InputImage { image_url: String },
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
209pub struct TurnEnvironmentParams {
210 pub environment_id: String,
211 pub cwd: PathBuf,
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use serde_json::json;
218
219 #[test]
220 fn dynamic_tool_spec_roundtrip() {
221 let spec = DynamicToolSpec {
222 namespace: Some("tau_bench".into()),
223 name: "get_reservation".into(),
224 description: "Look up an airline reservation.".into(),
225 input_schema: json!({
226 "type": "object",
227 "properties": {
228 "reservation_id": { "type": "string" }
229 },
230 "required": ["reservation_id"],
231 "additionalProperties": false
232 }),
233 defer_loading: false,
234 };
235
236 let serialized = serde_json::to_string(&spec).unwrap();
237 let deserialized: DynamicToolSpec = serde_json::from_str(&serialized).unwrap();
238 assert_eq!(spec, deserialized);
239 }
240
241 #[test]
242 fn dynamic_tool_spec_omits_defer_loading_defaults_false() {
243 let json = r#"{
244 "namespace": "tau_bench",
245 "name": "get_reservation",
246 "description": "Look up an airline reservation.",
247 "input_schema": { "type": "object" }
248 }"#;
249
250 let spec: DynamicToolSpec = serde_json::from_str(json).unwrap();
251 assert_eq!(spec.namespace, Some("tau_bench".into()));
252 assert_eq!(spec.name, "get_reservation");
253 assert!(!spec.defer_loading);
254 }
255
256 #[test]
257 fn dynamic_tool_item_status_snake_case() {
258 assert_eq!(
259 serde_json::to_string(&DynamicToolItemStatus::InProgress).unwrap(),
260 "\"in_progress\""
261 );
262 assert_eq!(
263 serde_json::from_str::<DynamicToolItemStatus>("\"completed\"").unwrap(),
264 DynamicToolItemStatus::Completed
265 );
266 assert_eq!(
267 serde_json::from_str::<DynamicToolItemStatus>("\"failed\"").unwrap(),
268 DynamicToolItemStatus::Failed
269 );
270 }
271
272 #[test]
273 fn dynamic_tool_call_params_roundtrip() {
274 let params = DynamicToolCallParams {
275 thread_id: "thr_123".into(),
276 turn_id: "turn_456".into(),
277 call_id: "call_abc".into(),
278 namespace: Some("tau_bench".into()),
279 tool: "get_reservation".into(),
280 arguments: json!({ "reservation_id": "ABC123" }),
281 };
282
283 let serialized = serde_json::to_string(¶ms).unwrap();
284 let deserialized: DynamicToolCallParams = serde_json::from_str(&serialized).unwrap();
285 assert_eq!(params, deserialized);
286 }
287
288 #[test]
289 fn dynamic_tool_call_content_roundtrip() {
290 let content = vec![
291 DynamicToolCallContent::InputText {
292 text: "{\"status\":\"confirmed\"}".into(),
293 },
294 DynamicToolCallContent::InputImage {
295 image_url: "http://example.com/receipt.png".into(),
296 },
297 ];
298
299 let value = serde_json::to_value(&content).unwrap();
300 let deserialized: Vec<DynamicToolCallContent> = serde_json::from_value(value).unwrap();
301 assert_eq!(content, deserialized);
302
303 assert_eq!(
305 serde_json::to_string(&DynamicToolCallContent::InputText { text: "x".into() }).unwrap(),
306 r#"{"type":"input_text","text":"x"}"#
307 );
308 assert_eq!(
309 serde_json::to_string(&DynamicToolCallContent::InputImage {
310 image_url: "y".into()
311 })
312 .unwrap(),
313 r#"{"type":"input_image","image_url":"y"}"#
314 );
315 }
316
317 #[test]
318 fn dynamic_tool_call_result_defaults_empty_content() {
319 let json = r#"{ "success": false }"#;
320 let result: DynamicToolCallResult = serde_json::from_str(json).unwrap();
321 assert!(!result.success);
322 assert!(result.content.is_empty());
323 }
324
325 #[test]
326 fn dynamic_tool_call_result_roundtrip_with_content() {
327 let result = DynamicToolCallResult {
328 success: true,
329 content: vec![DynamicToolCallContent::InputText {
330 text: "done".into(),
331 }],
332 };
333
334 let serialized = serde_json::to_string(&result).unwrap();
335 let deserialized: DynamicToolCallResult = serde_json::from_str(&serialized).unwrap();
336 assert_eq!(result, deserialized);
337 }
338
339 #[test]
340 fn turn_environment_params_roundtrip() {
341 let env = TurnEnvironmentParams {
342 environment_id: "local".into(),
343 cwd: PathBuf::from("/workspace"),
344 };
345
346 let serialized = serde_json::to_string(&env).unwrap();
347 let deserialized: TurnEnvironmentParams = serde_json::from_str(&serialized).unwrap();
348 assert_eq!(env, deserialized);
349
350 let from_spec = r#"{
352 "environment_id": "local",
353 "cwd": "/workspace"
354 }"#;
355 let parsed: TurnEnvironmentParams = serde_json::from_str(from_spec).unwrap();
356 assert_eq!(parsed.environment_id, "local");
357 assert_eq!(parsed.cwd, PathBuf::from("/workspace"));
358 }
359
360 #[test]
361 fn runtime_capabilities_serializes_expected_shape() {
362 let caps = RuntimeCapabilities {
363 account_session: true,
364 threads: true,
365 turns: true,
366 turn_steer: true,
367 turn_interrupt: true,
368 event_replay: true,
369 external_tools: false,
370 environments: false,
371 worker_runtime: false,
372 fleet_run_create: true,
373 fleet_run_start: true,
374 fleet_event_replay: true,
375 fleet_event_stream: true,
376 fleet_local_target: true,
377 thread_goals: true,
378 memory: true,
379 mcp_server_management: false,
380 skill_lifecycle: false,
381 };
382 let value = serde_json::to_value(&caps).unwrap();
383 let obj = value.as_object().unwrap();
384 assert_eq!(obj.get("threads").unwrap(), &json!(true));
385 assert_eq!(obj.get("account_session").unwrap(), &json!(true));
386 assert_eq!(obj.get("external_tools").unwrap(), &json!(false));
387 assert!(obj.contains_key("worker_runtime"));
388 assert_eq!(obj.get("fleet_run_create").unwrap(), &json!(true));
389 assert_eq!(obj.get("fleet_event_stream").unwrap(), &json!(true));
390 assert_eq!(obj.get("thread_goals").unwrap(), &json!(true));
391 assert_eq!(obj.get("memory").unwrap(), &json!(true));
392 }
393
394 #[test]
395 fn runtime_event_envelope_schema_version_default() {
396 let json = r#"{
397 "seq": 1,
398 "event": "test",
399 "kind": "test",
400 "thread_id": "thr_1",
401 "timestamp": "2026-06-12T00:00:00Z",
402 "payload": {}
403 }"#;
404 let envelope: RuntimeEventEnvelope = serde_json::from_str(json).unwrap();
405 assert_eq!(
406 envelope.schema_version,
407 RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION
408 );
409 }
410}