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