返回 CodeWhale
mod.rs
根目录 / crates / protocol / src / runtime / mod.rs
1 use std::collections::BTreeMap;
2 use std::path::PathBuf;
3
4 use serde::{Deserialize, Serialize};
5 use serde_json::Value;
6
7 pub const RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION: u32 = 1;
8 pub const RUNTIME_API_VERSION: &str = "1.0";
9
10 #[derive(Debug, Clone, Serialize, Deserialize)]
11 pub 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
29 fn default_runtime_event_envelope_schema_version() -> u32 {
30 RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION
31 }
32
33 // ---------------------------------------------------------------------------
34 // Capability advertisement
35 // ---------------------------------------------------------------------------
36
37 /// Fixed capability map advertised by `GET /v1/runtime/info`.
38 ///
39 /// All fields are required on serialization so clients can rely on the shape.
40 #[derive(Debug, Clone, Serialize, Deserialize)]
41 pub 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 /// Experimental opt-in flags advertised by `GET /v1/runtime/info`.
65 ///
66 /// Fields are additive and default to `false` when omitted by older servers.
67 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
68 pub struct RuntimeExperimentalCapabilities {
69 #[serde(default)]
70 pub environments: bool,
71 }
72
73 // ---------------------------------------------------------------------------
74 // External Tool Bridge protocol types
75 // ---------------------------------------------------------------------------
76
77 /// Specification for a dynamic external tool registered by a runtime client.
78 ///
79 /// Example JSON from the spec:
80 ///
81 /// ```json
82 /// {
83 /// "namespace": "tau_bench",
84 /// "name": "get_reservation",
85 /// "description": "Look up an airline reservation.",
86 /// "input_schema": {
87 /// "type": "object",
88 /// "properties": {
89 /// "reservation_id": { "type": "string" }
90 /// },
91 /// "required": ["reservation_id"],
92 /// "additionalProperties": false
93 /// }
94 /// }
95 /// ```
96 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97 pub struct DynamicToolSpec {
98 /// Optional namespace that groups related tools (e.g. `"tau_bench"`).
99 /// When present, the runtime may expose the tool as
100 /// `<namespace>::<name>` to the model.
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub namespace: Option<String>,
103
104 /// Short tool name. Combined with `namespace` it forms a unique tool id.
105 pub name: String,
106
107 /// Human-readable description exposed to the model.
108 pub description: String,
109
110 /// JSON Schema describing the tool's input parameters.
111 pub input_schema: Value,
112
113 /// If true, the runtime may defer schema validation / tool loading until
114 /// the model actually calls the tool.
115 ///
116 /// Defaults to `false` so that older clients omitting this field still
117 /// behave the same way.
118 #[serde(default)]
119 pub defer_loading: bool,
120 }
121
122 /// Lifecycle status of a dynamic tool item shown in thread detail and event
123 /// payloads.
124 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
125 #[serde(rename_all = "snake_case")]
126 pub enum DynamicToolItemStatus {
127 InProgress,
128 Completed,
129 Failed,
130 }
131
132 /// Parameters identifying a dynamic tool call request emitted by the runtime.
133 ///
134 /// This is the typed payload for `tool_call.requested` events and also the
135 /// natural identifier used when the runtime looks up a pending call.
136 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137 pub struct DynamicToolCallParams {
138 pub thread_id: String,
139 pub turn_id: String,
140 pub call_id: String,
141
142 /// Optional namespace that was registered with the tool.
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub namespace: Option<String>,
145
146 /// Tool name that the model invoked.
147 pub tool: String,
148
149 /// Arguments supplied by the model, validated against `input_schema`.
150 pub arguments: Value,
151 }
152
153 /// Result submitted by a runtime client after executing a dynamic tool.
154 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155 pub struct DynamicToolCallResult {
156 /// Whether the client-side tool execution succeeded.
157 pub success: bool,
158
159 /// Content fragments returned by the tool.
160 ///
161 /// Defaults to an empty vector when omitted so clients can send a minimal
162 /// `{ "success": false }` payload.
163 #[serde(default)]
164 pub content: Vec<DynamicToolCallContent>,
165 }
166
167 /// A single content fragment inside a [`DynamicToolCallResult`].
168 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169 #[serde(tag = "type", rename_all = "snake_case")]
170 pub enum DynamicToolCallContent {
171 InputText { text: String },
172 InputImage { image_url: String },
173 }
174
175 // ---------------------------------------------------------------------------
176 // Environment targeting protocol types
177 // ---------------------------------------------------------------------------
178
179 /// Environment target selected for a turn's shell/filesystem work.
180 ///
181 /// Example JSON:
182 ///
183 /// ```json
184 /// {
185 /// "environment_id": "local",
186 /// "cwd": "/workspace"
187 /// }
188 /// ```
189 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190 pub struct TurnEnvironmentParams {
191 pub environment_id: String,
192 pub cwd: PathBuf,
193 }
194
195 #[cfg(test)]
196 mod 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(&params).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 // Verify the exact JSON tag names expected by the spec.
285 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 // Verify JSON from the spec deserializes directly.
332 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 }
386
386 lines RUST