1use std::collections::{HashMap, HashSet};
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6pub const REMOTE_PROTOCOL_VERSION: u32 = 2;
7
8pub fn ensure_protocol_version(actual: u32) -> Result<(), RemoteProtocolError> {
9 if actual == REMOTE_PROTOCOL_VERSION {
10 Ok(())
11 } else {
12 Err(RemoteProtocolError::UnsupportedProtocolVersion {
13 actual,
14 expected: REMOTE_PROTOCOL_VERSION,
15 })
16 }
17}
18
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
20pub struct RemoteLlmRequest {
21 pub protocol_version: u32,
22 pub request_id: String,
23 pub model_intent: RemoteModelIntent,
24 #[serde(default, skip_serializing_if = "Vec::is_empty")]
25 pub messages: Vec<RemoteLlmMessage>,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
27 pub attachments: Vec<RemoteLlmAttachment>,
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub tools: Vec<RemoteLlmToolSpec>,
30 #[serde(default)]
31 pub tool_choice: RemoteLlmToolChoice,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub output_spec: Option<RemoteLlmOutputSpec>,
34 #[serde(default, skip_serializing_if = "RemoteGenerationOptions::is_empty")]
35 pub generation: RemoteGenerationOptions,
36 #[serde(default, skip_serializing_if = "RemoteLlmRequestMetadata::is_empty")]
37 pub request_metadata: RemoteLlmRequestMetadata,
38 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
39 pub metadata: HashMap<String, serde_json::Value>,
40}
41
42impl RemoteLlmRequest {
43 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
44 ensure_protocol_version(self.protocol_version)?;
45 require_non_empty("RemoteLlmRequest", "request_id", &self.request_id)?;
46 self.model_intent.validate()?;
47 self.generation.validate("RemoteLlmRequest")?;
48 for (index, message) in self.messages.iter().enumerate() {
49 message.validate(index)?;
50 }
51 for (index, attachment) in self.attachments.iter().enumerate() {
52 attachment.validate(index)?;
53 }
54 for tool in &self.tools {
55 tool.validate()?;
56 }
57 if let Some(output_spec) = &self.output_spec {
58 output_spec.validate()?;
59 }
60 Ok(())
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
65pub struct RemoteLlmResponse {
66 pub protocol_version: u32,
67 pub request_id: String,
68 #[serde(default)]
69 pub full_text: String,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
71 pub output_parts: Vec<RemoteLlmOutputPart>,
72 #[serde(default)]
73 pub usage: RemoteUsage,
74 #[serde(default)]
75 pub terminal_reason: RemoteLlmTerminalReason,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub diagnostics: Vec<RemoteDiagnostic>,
78 #[serde(default, skip_serializing_if = "RemoteProviderMetadata::is_empty")]
79 pub provider_metadata: RemoteProviderMetadata,
80}
81
82impl RemoteLlmResponse {
83 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
84 ensure_protocol_version(self.protocol_version)?;
85 require_non_empty("RemoteLlmResponse", "request_id", &self.request_id)?;
86 Ok(())
87 }
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
91pub struct RemoteModelIntent {
92 pub model: String,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub variant: Option<String>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub provider: Option<String>,
97 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
98 pub metadata: HashMap<String, String>,
99}
100
101impl RemoteModelIntent {
102 pub fn new(model: impl Into<String>) -> Self {
103 Self {
104 model: model.into(),
105 variant: None,
106 provider: None,
107 metadata: HashMap::new(),
108 }
109 }
110
111 fn validate(&self) -> Result<(), RemoteProtocolError> {
112 require_non_empty("RemoteModelIntent", "model", &self.model)
113 }
114}
115
116#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
117pub struct RemoteGenerationOptions {
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub output_token_cap: Option<u64>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub temperature: Option<String>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub top_p: Option<String>,
124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
125 pub stop: Vec<String>,
126 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
127 pub provider_options: HashMap<String, String>,
128}
129
130impl RemoteGenerationOptions {
131 pub fn is_empty(&self) -> bool {
132 self.output_token_cap.is_none()
133 && self.temperature.is_none()
134 && self.top_p.is_none()
135 && self.stop.is_empty()
136 && self.provider_options.is_empty()
137 }
138
139 fn validate(&self, type_name: &'static str) -> Result<(), RemoteProtocolError> {
140 if self.output_token_cap == Some(0) {
141 return Err(RemoteProtocolError::InvalidEnvelope {
142 type_name,
143 message: "generation.output_token_cap must be greater than zero".to_string(),
144 });
145 }
146 Ok(())
147 }
148}
149
150#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
151pub struct RemoteLlmRequestMetadata {
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub session_id: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub idempotency_key: Option<String>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub trace_id: Option<String>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub activity_cursor: Option<String>,
160}
161
162impl RemoteLlmRequestMetadata {
163 pub fn is_empty(&self) -> bool {
164 self.session_id.is_none()
165 && self.idempotency_key.is_none()
166 && self.trace_id.is_none()
167 && self.activity_cursor.is_none()
168 }
169}
170
171#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
172#[serde(rename_all = "snake_case")]
173pub enum RemoteLlmRole {
174 #[default]
175 User,
176 Assistant,
177 System,
178}
179
180#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
181pub struct RemoteLlmMessage {
182 pub role: RemoteLlmRole,
183 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub content: Vec<RemoteLlmContentBlock>,
185}
186
187impl RemoteLlmMessage {
188 fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
189 if self.content.is_empty() {
190 return Err(RemoteProtocolError::InvalidEnvelope {
191 type_name: "RemoteLlmMessage",
192 message: format!("message at index {index} must contain at least one block"),
193 });
194 }
195 for block in &self.content {
196 block.validate()?;
197 }
198 Ok(())
199 }
200}
201
202#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
203#[serde(tag = "type", rename_all = "snake_case")]
204pub enum RemoteLlmContentBlock {
205 Text {
206 text: String,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 response_meta: Option<RemoteResponseTextMeta>,
209 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
210 cache_breakpoint: bool,
211 },
212 ImageAttachment {
213 attachment_index: usize,
214 },
215 ToolCall {
216 call_id: String,
217 tool_name: String,
218 input_json: String,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 replay: Option<RemoteProviderReplayMeta>,
221 },
222 ToolResult {
223 call_id: String,
224 content: String,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 tool_name: Option<String>,
227 },
228 Reasoning {
229 text: String,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 replay: Option<RemoteProviderReasoningReplay>,
232 },
233}
234
235impl RemoteLlmContentBlock {
236 fn validate(&self) -> Result<(), RemoteProtocolError> {
237 match self {
238 Self::ToolCall {
239 call_id, tool_name, ..
240 } => {
241 require_non_empty("RemoteLlmContentBlock::ToolCall", "call_id", call_id)?;
242 require_non_empty("RemoteLlmContentBlock::ToolCall", "tool_name", tool_name)
243 }
244 Self::ToolResult { call_id, .. } => {
245 require_non_empty("RemoteLlmContentBlock::ToolResult", "call_id", call_id)
246 }
247 Self::Text { .. } | Self::ImageAttachment { .. } | Self::Reasoning { .. } => Ok(()),
248 }
249 }
250}
251
252#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
253pub struct RemoteResponseTextMeta {
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub id: Option<String>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub status: Option<String>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub phase: Option<String>,
260}
261
262#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
263pub struct RemoteProviderReplayMeta {
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub item_id: Option<String>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub opaque: Option<String>,
268}
269
270#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
271pub struct RemoteProviderReasoningReplay {
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub item_id: Option<String>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub encrypted_content: Option<String>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub signature: Option<String>,
278 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
279 pub redacted: bool,
280 #[serde(default, skip_serializing_if = "Vec::is_empty")]
281 pub summary: Vec<String>,
282}
283
284#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
285pub struct RemoteLlmAttachment {
286 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub id: Option<String>,
288 pub mime: String,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub data_base64: Option<String>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub reference: Option<RemoteAttachmentRef>,
293 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
294 pub metadata: HashMap<String, String>,
295}
296
297impl RemoteLlmAttachment {
298 fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
299 if self.mime.trim().is_empty() {
300 return Err(RemoteProtocolError::InvalidEnvelope {
301 type_name: "RemoteLlmAttachment",
302 message: format!("attachment at index {index} requires a non-empty mime"),
303 });
304 }
305 if let Some(reference) = &self.reference {
306 reference.validate()?;
307 }
308 Ok(())
309 }
310}
311
312#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
313pub struct RemoteAttachmentRef {
314 pub id: String,
315 pub mime: String,
316 pub byte_len: u64,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub width: Option<u32>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub height: Option<u32>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub label: Option<String>,
323 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
324 pub metadata: HashMap<String, String>,
325}
326
327impl RemoteAttachmentRef {
328 fn validate(&self) -> Result<(), RemoteProtocolError> {
329 require_non_empty("RemoteAttachmentRef", "id", &self.id)?;
330 require_non_empty("RemoteAttachmentRef", "mime", &self.mime)
331 }
332}
333
334#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
335pub struct RemoteLlmToolSpec {
336 pub name: String,
337 #[serde(default)]
338 pub description: String,
339 #[serde(default = "default_input_schema")]
340 pub input_schema: serde_json::Value,
341 #[serde(default)]
342 pub output_schema: serde_json::Value,
343 #[serde(default, skip_serializing_if = "Vec::is_empty")]
344 pub input_schema_projections: Vec<RemoteSchemaProjectionOverride>,
345 #[serde(default, skip_serializing_if = "Vec::is_empty")]
346 pub output_schema_projections: Vec<RemoteSchemaProjectionOverride>,
347}
348
349impl RemoteLlmToolSpec {
350 fn validate(&self) -> Result<(), RemoteProtocolError> {
351 require_non_empty("RemoteLlmToolSpec", "name", &self.name)
352 }
353}
354
355#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
356#[serde(rename_all = "snake_case")]
357pub enum RemoteLlmToolChoice {
358 #[default]
359 Auto,
360 None,
361 Required,
362}
363
364#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
365#[serde(tag = "type", rename_all = "snake_case")]
366pub enum RemoteLlmOutputSpec {
367 JsonObject,
368 JsonSchema {
369 name: String,
370 schema: serde_json::Value,
371 strict: bool,
372 },
373}
374
375impl RemoteLlmOutputSpec {
376 fn validate(&self) -> Result<(), RemoteProtocolError> {
377 match self {
378 Self::JsonObject => Ok(()),
379 Self::JsonSchema { name, .. } => {
380 require_non_empty("RemoteLlmOutputSpec::JsonSchema", "name", name)
381 }
382 }
383 }
384}
385
386#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
387#[serde(tag = "type", rename_all = "snake_case")]
388pub enum RemoteLlmOutputPart {
389 Text {
390 text: String,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 response_meta: Option<RemoteResponseTextMeta>,
393 },
394 Reasoning {
395 text: String,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 replay: Option<RemoteProviderReasoningReplay>,
398 },
399 ToolCall {
400 call_id: String,
401 tool_name: String,
402 input_json: String,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
404 replay: Option<RemoteProviderReplayMeta>,
405 },
406}
407
408#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
409#[serde(rename_all = "snake_case")]
410pub enum RemoteLlmTerminalReason {
411 Stop,
412 ToolUse,
413 OutputLimit,
414 ContextOverflow,
415 ContentFilter,
416 ProviderError,
417 Cancelled,
418 #[default]
419 Unknown,
420}
421
422#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
423pub struct RemoteProviderMetadata {
424 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub usage: Option<serde_json::Value>,
426 #[serde(default, skip_serializing_if = "Option::is_none")]
427 pub request_body: Option<String>,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub http_summary: Option<String>,
430 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
431 pub data: HashMap<String, serde_json::Value>,
432}
433
434impl RemoteProviderMetadata {
435 pub fn is_empty(&self) -> bool {
436 self.usage.is_none()
437 && self.request_body.is_none()
438 && self.http_summary.is_none()
439 && self.data.is_empty()
440 }
441}
442
443#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
444pub struct RemoteDiagnostic {
445 pub kind: String,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub code: Option<String>,
448 pub message: String,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub data: Option<serde_json::Value>,
451}
452
453#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
454pub struct RemoteProtocolTurnOptions {
455 #[serde(default = "empty_protocol_turn_payload")]
456 pub payload: serde_json::Value,
457}
458
459fn empty_protocol_turn_payload() -> serde_json::Value {
460 serde_json::Value::Object(serde_json::Map::new())
461}
462
463impl Default for RemoteProtocolTurnOptions {
464 fn default() -> Self {
465 Self {
466 payload: empty_protocol_turn_payload(),
467 }
468 }
469}
470
471impl RemoteProtocolTurnOptions {
472 pub fn empty() -> Self {
473 Self::default()
474 }
475
476 pub fn is_empty(&self) -> bool {
477 match &self.payload {
478 serde_json::Value::Object(map) => map.is_empty(),
479 _ => false,
480 }
481 }
482}
483
484#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
485pub struct RemoteTurnInput {
486 pub protocol_version: u32,
487 #[serde(default)]
488 pub items: Vec<RemoteInputItem>,
489 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
490 pub image_blobs_base64: HashMap<String, String>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub protocol_turn_options: Option<RemoteProtocolTurnOptions>,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub trace_turn_id: Option<String>,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub prompt_layer: Option<RemotePromptLayer>,
497}
498
499impl RemoteTurnInput {
500 pub fn text(text: impl Into<String>) -> Self {
501 Self {
502 protocol_version: REMOTE_PROTOCOL_VERSION,
503 items: vec![RemoteInputItem::Text { text: text.into() }],
504 image_blobs_base64: HashMap::new(),
505 protocol_turn_options: None,
506 trace_turn_id: None,
507 prompt_layer: None,
508 }
509 }
510
511 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
512 ensure_protocol_version(self.protocol_version)?;
513 for item in &self.items {
514 if let RemoteInputItem::ImageRef { id } = item {
515 require_non_empty("RemoteInputItem::ImageRef", "id", id)?;
516 }
517 }
518 for (id, blob) in &self.image_blobs_base64 {
519 require_non_empty("RemoteTurnInput", "image_blobs_base64 key", id)?;
520 if blob.trim().is_empty() {
521 return Err(RemoteProtocolError::InvalidImageBlob {
522 id: id.clone(),
523 message: "base64 payload cannot be empty".to_string(),
524 });
525 }
526 }
527 Ok(())
528 }
529}
530
531#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
532#[serde(tag = "type", rename_all = "snake_case")]
533pub enum RemoteInputItem {
534 Text { text: String },
535 ImageRef { id: String },
536}
537
538#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
539pub struct RemoteTurnRequest {
540 pub protocol_version: u32,
541 pub session_id: String,
542 pub turn_id: String,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub idempotency_key: Option<String>,
545 pub input: RemoteTurnInput,
546 #[serde(default, skip_serializing_if = "Vec::is_empty")]
547 pub tool_grants: Vec<RemoteToolGrant>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub model_intent: Option<RemoteModelIntent>,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
551 pub activity_cursor: Option<String>,
552 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
553 pub metadata: HashMap<String, serde_json::Value>,
554}
555
556impl RemoteTurnRequest {
557 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
558 ensure_protocol_version(self.protocol_version)?;
559 require_non_empty("RemoteTurnRequest", "session_id", &self.session_id)?;
560 require_non_empty("RemoteTurnRequest", "turn_id", &self.turn_id)?;
561 if self.input.protocol_version != self.protocol_version {
562 return Err(RemoteProtocolError::MismatchedNestedProtocolVersion {
563 parent: "RemoteTurnRequest",
564 child: "input",
565 parent_version: self.protocol_version,
566 child_version: self.input.protocol_version,
567 });
568 }
569 self.input.validate()?;
570 RemoteToolGrant::validate_all(&self.tool_grants)?;
571 if let Some(model_intent) = &self.model_intent {
572 model_intent.validate()?;
573 }
574 Ok(())
575 }
576}
577
578#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
579pub struct RemoteTurnResult {
580 pub protocol_version: u32,
581 pub session_id: String,
582 pub turn_id: String,
583 pub status: RemoteTurnStatus,
584 pub outcome: RemoteTurnOutcome,
585 pub assistant_output: RemoteAssistantOutput,
586 #[serde(default)]
587 pub usage: RemoteTurnUsageSummary,
588 #[serde(default)]
589 pub execution: RemoteExecutionSummary,
590 #[serde(default, skip_serializing_if = "Vec::is_empty")]
591 pub tool_calls: Vec<RemoteToolCallSummary>,
592 #[serde(default, skip_serializing_if = "Vec::is_empty")]
593 pub issues: Vec<RemoteTurnIssue>,
594 #[serde(default, skip_serializing_if = "Vec::is_empty")]
595 pub activities: Vec<RemoteTurnActivity>,
596 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
597 pub metadata: HashMap<String, serde_json::Value>,
598}
599
600impl RemoteTurnResult {
601 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
602 ensure_protocol_version(self.protocol_version)?;
603 require_non_empty("RemoteTurnResult", "session_id", &self.session_id)?;
604 require_non_empty("RemoteTurnResult", "turn_id", &self.turn_id)?;
605 for activity in &self.activities {
606 if activity.protocol_version != self.protocol_version {
607 return Err(RemoteProtocolError::MismatchedNestedProtocolVersion {
608 parent: "RemoteTurnResult",
609 child: "activities",
610 parent_version: self.protocol_version,
611 child_version: activity.protocol_version,
612 });
613 }
614 activity.validate()?;
615 }
616 Ok(())
617 }
618}
619
620#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
621pub struct RemoteHostEventOccurrenceRequest {
622 pub protocol_version: u32,
623 pub source_type: String,
624 pub source_key: String,
625 #[serde(default)]
626 pub payload: serde_json::Value,
627 pub idempotency_key: String,
628 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub source: Option<serde_json::Value>,
630}
631
632impl RemoteHostEventOccurrenceRequest {
633 pub fn new(
634 source_type: impl Into<String>,
635 source_key: impl Into<String>,
636 payload: serde_json::Value,
637 idempotency_key: impl Into<String>,
638 ) -> Self {
639 Self {
640 protocol_version: REMOTE_PROTOCOL_VERSION,
641 source_type: source_type.into(),
642 source_key: source_key.into(),
643 payload,
644 idempotency_key: idempotency_key.into(),
645 source: None,
646 }
647 }
648
649 pub fn with_source(mut self, source: serde_json::Value) -> Self {
650 self.source = Some(source);
651 self
652 }
653
654 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
655 ensure_protocol_version(self.protocol_version)?;
656 require_non_empty(
657 "RemoteHostEventOccurrenceRequest",
658 "source_type",
659 &self.source_type,
660 )?;
661 require_non_empty(
662 "RemoteHostEventOccurrenceRequest",
663 "source_key",
664 &self.source_key,
665 )?;
666 require_non_empty(
667 "RemoteHostEventOccurrenceRequest",
668 "idempotency_key",
669 &self.idempotency_key,
670 )
671 }
672}
673
674#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
675pub struct RemoteHostEventOccurrenceRecord {
676 pub occurrence_id: String,
677 pub source_type: String,
678 pub source_key: String,
679 #[serde(default)]
680 pub payload: serde_json::Value,
681 pub idempotency_key: String,
682 #[serde(default, skip_serializing_if = "Option::is_none")]
683 pub source: Option<serde_json::Value>,
684 pub occurred_at_ms: u64,
685}
686
687#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
688pub struct RemoteHostEventEmitReport {
689 pub protocol_version: u32,
690 #[serde(default, skip_serializing_if = "String::is_empty")]
691 pub occurrence_id: String,
692 #[serde(default, skip_serializing_if = "Vec::is_empty")]
693 pub started_process_ids: Vec<String>,
694}
695
696impl RemoteHostEventEmitReport {
697 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
698 ensure_protocol_version(self.protocol_version)
699 }
700}
701
702#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
703pub struct RemoteTriggerSubscriptionFilter {
704 pub protocol_version: u32,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
706 pub session_id: Option<String>,
707 #[serde(default, skip_serializing_if = "Option::is_none")]
708 pub handle: Option<String>,
709 #[serde(default, skip_serializing_if = "Option::is_none")]
710 pub name: Option<String>,
711 #[serde(default, skip_serializing_if = "Option::is_none")]
712 pub source_type: Option<String>,
713 #[serde(default, skip_serializing_if = "Option::is_none")]
714 pub source_key: Option<String>,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
716 pub target: Option<serde_json::Value>,
717 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub enabled: Option<bool>,
719}
720
721impl Default for RemoteTriggerSubscriptionFilter {
722 fn default() -> Self {
723 Self {
724 protocol_version: REMOTE_PROTOCOL_VERSION,
725 session_id: None,
726 handle: None,
727 name: None,
728 source_type: None,
729 source_key: None,
730 target: None,
731 enabled: None,
732 }
733 }
734}
735
736impl RemoteTriggerSubscriptionFilter {
737 pub fn for_session(session_id: impl Into<String>) -> Self {
738 Self {
739 protocol_version: REMOTE_PROTOCOL_VERSION,
740 session_id: Some(session_id.into()),
741 ..Self::default()
742 }
743 }
744
745 pub fn for_source_type(source_type: impl Into<String>) -> Self {
746 Self {
747 protocol_version: REMOTE_PROTOCOL_VERSION,
748 source_type: Some(source_type.into()),
749 ..Self::default()
750 }
751 }
752
753 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
754 ensure_protocol_version(self.protocol_version)
755 }
756}
757
758#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
759pub struct RemoteTriggerRegistration {
760 pub handle: String,
761 pub source_key: String,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub name: Option<String>,
764 pub source_type: String,
765 #[serde(default)]
766 pub source: serde_json::Value,
767 pub target: RemoteTriggerTargetSummary,
768 #[serde(default = "default_true")]
769 pub enabled: bool,
770}
771
772#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
773pub struct RemoteTriggerTargetSummary {
774 pub process_name: String,
775 #[serde(default)]
776 pub inputs: serde_json::Value,
777}
778
779fn default_true() -> bool {
780 true
781}
782
783#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
784#[serde(tag = "type", rename_all = "snake_case")]
785pub enum RemoteCausalRef {
786 Turn {
787 session_id: String,
788 turn_id: String,
789 },
790 Effect {
791 session_id: String,
792 #[serde(default, skip_serializing_if = "Option::is_none")]
793 turn_id: Option<String>,
794 effect_id: String,
795 },
796 ToolCall {
797 session_id: String,
798 call_id: String,
799 },
800 Process {
801 process_id: String,
802 },
803 ProcessEvent {
804 process_id: String,
805 sequence: u64,
806 },
807 HostEvent {
808 occurrence_id: String,
809 },
810 SessionNode {
811 session_id: String,
812 node_id: String,
813 },
814}
815
816#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
817#[serde(rename_all = "snake_case")]
818pub enum RemoteTurnStatus {
819 #[default]
820 Completed,
821 Failed,
822 Cancelled,
823 InProgress,
824}
825
826#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
827#[serde(tag = "type", rename_all = "snake_case")]
828pub enum RemoteTurnOutcome {
829 Finished { finish: RemoteTurnFinish },
830 AgentFrameSwitch { frame_id: String },
831 Stopped { stop: RemoteTurnStop },
832}
833
834impl Default for RemoteTurnOutcome {
835 fn default() -> Self {
836 Self::Stopped {
837 stop: RemoteTurnStop::Incomplete,
838 }
839 }
840}
841
842#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
843#[serde(tag = "type", rename_all = "snake_case")]
844pub enum RemoteTurnFinish {
845 AssistantMessage {
846 text: String,
847 },
848 SubmittedValue {
849 value: serde_json::Value,
850 },
851 ToolValue {
852 tool_name: String,
853 value: serde_json::Value,
854 },
855}
856
857#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
858#[serde(tag = "type", rename_all = "snake_case")]
859pub enum RemoteTurnStop {
860 Cancelled,
861 Incomplete,
862 InvalidInput,
863 MaxTurns,
864 ToolFailure,
865 ProviderError,
866 PluginAbort,
867 RuntimeError,
868 SubmittedError {
869 value: serde_json::Value,
870 },
871 ToolError {
872 tool_name: String,
873 value: serde_json::Value,
874 },
875}
876
877#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
878pub struct RemoteAssistantOutput {
879 #[serde(default)]
880 pub safe_text: String,
881 #[serde(default)]
882 pub raw_text: String,
883 #[serde(default)]
884 pub state: RemoteAssistantOutputState,
885}
886
887#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
888#[serde(rename_all = "snake_case")]
889pub enum RemoteAssistantOutputState {
890 #[default]
891 Usable,
892 EmptyOutput,
893 TracebackOnly,
894 RecoveredFromError,
895}
896
897#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
898pub struct RemoteTurnUsageSummary {
899 #[serde(default)]
900 pub parent: RemoteUsage,
901 #[serde(default, skip_serializing_if = "Vec::is_empty")]
902 pub children: Vec<RemoteTokenLedgerEntry>,
903 #[serde(default)]
904 pub total: RemoteUsage,
905}
906
907#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
908pub struct RemoteExecutionSummary {
909 #[serde(default)]
910 pub had_tool_calls: bool,
911 #[serde(default)]
912 pub had_code_execution: bool,
913}
914
915#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
916pub struct RemoteToolCallSummary {
917 #[serde(default, skip_serializing_if = "Option::is_none")]
918 pub call_id: Option<String>,
919 pub tool_name: String,
920 #[serde(default)]
921 pub args: serde_json::Value,
922 pub outcome: RemoteToolCallOutcome,
923 pub duration_ms: u64,
924}
925
926#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
927#[serde(tag = "status", content = "payload", rename_all = "snake_case")]
928pub enum RemoteToolCallOutcome {
929 Success(serde_json::Value),
930 Failure(serde_json::Value),
931 Cancelled(serde_json::Value),
932}
933
934#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
935pub struct RemoteTurnIssue {
936 pub kind: String,
937 #[serde(default, skip_serializing_if = "Option::is_none")]
938 pub code: Option<String>,
939 #[serde(default, skip_serializing_if = "Option::is_none")]
940 pub terminal_reason: Option<RemoteLlmTerminalReason>,
941 pub message: String,
942 #[serde(default, skip_serializing_if = "Option::is_none")]
943 pub raw: Option<String>,
944}
945
946#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
947pub struct RemotePromptLayer {
948 #[serde(default, skip_serializing_if = "Option::is_none")]
949 pub template: Option<RemotePromptTemplate>,
950 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
951 pub slots: HashMap<RemotePromptSlot, RemotePromptSlotLayer>,
952}
953
954impl RemotePromptLayer {
955 pub fn new() -> Self {
956 Self::default()
957 }
958
959 pub fn is_empty(&self) -> bool {
960 self.template.is_none() && self.slots.is_empty()
961 }
962}
963
964#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
965#[serde(rename_all = "snake_case")]
966pub enum RemotePromptBuiltin {
967 MainAgentIntro,
968 ExecutionInstructions,
969 CoreGuidance,
970}
971
972#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
973#[serde(rename_all = "snake_case")]
974pub enum RemotePromptSlot {
975 Intro,
976 Execution,
977 Guidance,
978 ProjectInstructions,
979 RuntimeContext,
980 Environment,
981}
982
983#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
984#[serde(tag = "kind", rename_all = "snake_case")]
985pub enum RemotePromptTemplateEntry {
986 Text { content: String },
987 Builtin { builtin: RemotePromptBuiltin },
988 Slot { slot: RemotePromptSlot },
989}
990
991#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
992pub struct RemotePromptTemplateSection {
993 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub title: Option<String>,
995 #[serde(default, skip_serializing_if = "Vec::is_empty")]
996 pub entries: Vec<RemotePromptTemplateEntry>,
997}
998
999#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1000pub struct RemotePromptTemplate {
1001 pub sections: Vec<RemotePromptTemplateSection>,
1002}
1003
1004#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1005pub struct RemotePromptSlotLayer {
1006 #[serde(default)]
1007 pub reset: bool,
1008 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1009 pub contributions: Vec<RemotePromptContribution>,
1010}
1011
1012#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1013pub struct RemotePromptContribution {
1014 pub slot: RemotePromptSlot,
1015 #[serde(default, skip_serializing_if = "Option::is_none")]
1016 pub title: Option<String>,
1017 #[serde(default)]
1018 pub priority: i32,
1019 #[serde(
1020 default,
1021 skip_serializing_if = "RemotePromptContributionGate::is_empty"
1022 )]
1023 pub gate: RemotePromptContributionGate,
1024 pub content: String,
1025}
1026
1027#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1028pub struct RemotePromptContributionGate {
1029 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1030 pub tools: Vec<String>,
1031 #[serde(default)]
1032 pub minimum_availability: RemoteToolAvailability,
1033}
1034
1035impl RemotePromptContributionGate {
1036 pub fn is_empty(&self) -> bool {
1037 self.tools.is_empty()
1038 }
1039}
1040
1041#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1042pub struct RemoteToolGrant {
1043 pub protocol_version: u32,
1044 #[serde(default, skip_serializing_if = "Option::is_none")]
1045 pub id: Option<String>,
1046 pub name: String,
1047 #[serde(default, skip_serializing_if = "String::is_empty")]
1048 pub description: String,
1049 #[serde(default = "default_input_schema")]
1050 pub input_schema: serde_json::Value,
1051 #[serde(default)]
1052 pub output_schema: serde_json::Value,
1053 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1054 pub input_schema_projections: Vec<RemoteSchemaProjectionOverride>,
1055 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1056 pub output_schema_projections: Vec<RemoteSchemaProjectionOverride>,
1057 #[serde(default, skip_serializing_if = "RemoteToolOutputContract::is_static")]
1058 pub output_contract: RemoteToolOutputContract,
1059 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1060 pub examples: Vec<String>,
1061 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 pub availability: Option<RemoteToolAvailability>,
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1064 pub activation: Option<RemoteToolActivation>,
1065 #[serde(default, skip_serializing_if = "Option::is_none")]
1066 pub argument_projection: Option<RemoteToolArgumentProjectionPolicy>,
1067 #[serde(default, skip_serializing_if = "Option::is_none")]
1068 pub scheduling: Option<RemoteToolScheduling>,
1069 #[serde(default, skip_serializing_if = "Option::is_none")]
1070 pub retry_policy: Option<RemoteToolRetryPolicy>,
1071 #[serde(default, skip_serializing_if = "Option::is_none")]
1072 pub agent_surface: Option<RemoteToolAgentSurface>,
1073}
1074
1075impl RemoteToolGrant {
1076 pub fn call_path(&self) -> Result<String, RemoteProtocolError> {
1077 let surface = self.required_surface()?;
1078 Ok(format!(
1079 "{}.{}",
1080 surface.module_path.join("."),
1081 surface.operation
1082 ))
1083 }
1084
1085 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
1086 ensure_protocol_version(self.protocol_version)?;
1087 if self.name.trim().is_empty() {
1088 return Err(RemoteProtocolError::InvalidToolGrant {
1089 tool_name: self.name.clone(),
1090 message: "tool grant name cannot be empty".to_string(),
1091 });
1092 }
1093 self.required_surface()?;
1094 Ok(())
1095 }
1096
1097 pub fn validate_all(grants: &[Self]) -> Result<(), RemoteProtocolError> {
1098 let mut seen = HashSet::new();
1099 for grant in grants {
1100 grant.validate()?;
1101 let call_path = grant.call_path()?;
1102 if !seen.insert(call_path.clone()) {
1103 return Err(RemoteProtocolError::DuplicateRemoteCallPath { call_path });
1104 }
1105 }
1106 Ok(())
1107 }
1108
1109 fn required_surface(&self) -> Result<&RemoteToolAgentSurface, RemoteProtocolError> {
1110 let Some(surface) = &self.agent_surface else {
1111 return Err(RemoteProtocolError::MissingToolSurface {
1112 tool_name: self.name.clone(),
1113 });
1114 };
1115 if surface.module_path.is_empty() {
1116 return Err(RemoteProtocolError::InvalidToolGrant {
1117 tool_name: self.name.clone(),
1118 message: "remote tool grant requires an explicit module path".to_string(),
1119 });
1120 }
1121 if surface
1122 .module_path
1123 .iter()
1124 .any(|part| part.trim().is_empty())
1125 {
1126 return Err(RemoteProtocolError::InvalidToolGrant {
1127 tool_name: self.name.clone(),
1128 message: "remote tool grant module path cannot contain empty segments".to_string(),
1129 });
1130 }
1131 if surface.operation.trim().is_empty() {
1132 return Err(RemoteProtocolError::InvalidToolGrant {
1133 tool_name: self.name.clone(),
1134 message: "remote tool grant requires an explicit operation".to_string(),
1135 });
1136 }
1137 Ok(surface)
1138 }
1139}
1140
1141fn default_input_schema() -> serde_json::Value {
1142 serde_json::json!({
1143 "type": "object",
1144 "properties": {},
1145 "additionalProperties": true
1146 })
1147}
1148
1149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1150pub struct RemoteToolAgentSurface {
1151 pub module_path: Vec<String>,
1152 pub operation: String,
1153 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub authority_type: Option<String>,
1155 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1156 pub aliases: Vec<String>,
1157}
1158
1159impl RemoteToolAgentSurface {
1160 pub fn new(
1161 module_path: impl IntoIterator<Item = impl Into<String>>,
1162 operation: impl Into<String>,
1163 ) -> Self {
1164 Self {
1165 module_path: module_path.into_iter().map(Into::into).collect(),
1166 operation: operation.into(),
1167 authority_type: None,
1168 aliases: Vec::new(),
1169 }
1170 }
1171}
1172
1173#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1174pub struct RemoteSchemaProjectionOverride {
1175 pub profile: String,
1176 pub schema: serde_json::Value,
1177}
1178
1179#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1180#[serde(rename_all = "snake_case")]
1181pub enum RemoteToolAvailability {
1182 Off,
1183 Searchable,
1184 Callable,
1185 #[default]
1186 Showcased,
1187}
1188
1189#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1190#[serde(rename_all = "snake_case")]
1191pub enum RemoteToolActivation {
1192 #[default]
1193 Always,
1194 Internal,
1195}
1196
1197#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1198#[serde(rename_all = "snake_case")]
1199pub enum RemoteToolScheduling {
1200 #[default]
1201 Parallel,
1202 Serial,
1203}
1204
1205#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1206#[serde(tag = "kind", rename_all = "snake_case")]
1207pub enum RemoteToolOutputContract {
1208 #[default]
1209 Static,
1210 FromInputSchema {
1211 input_field: String,
1212 #[serde(default, skip_serializing_if = "Option::is_none")]
1213 default_schema: Option<serde_json::Value>,
1214 },
1215}
1216
1217impl RemoteToolOutputContract {
1218 fn is_static(&self) -> bool {
1219 matches!(self, Self::Static)
1220 }
1221}
1222
1223#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1224#[serde(tag = "kind", rename_all = "snake_case")]
1225pub enum RemoteToolArgumentProjectionPolicy {
1226 #[default]
1227 MaterializeProjectedValues,
1228 PreserveProjectedRefsInField {
1229 field: String,
1230 },
1231}
1232
1233#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1234#[serde(tag = "type", rename_all = "snake_case")]
1235pub enum RemoteToolRetryPolicy {
1236 #[default]
1237 Never,
1238 Safe {
1239 max_attempts: u32,
1240 base_delay_ms: u64,
1241 max_delay_ms: u64,
1242 },
1243 Idempotent {
1244 max_attempts: u32,
1245 base_delay_ms: u64,
1246 max_delay_ms: u64,
1247 },
1248}
1249
1250#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1251pub struct RemoteToolCallRequest {
1252 pub protocol_version: u32,
1253 pub tool_name: String,
1254 pub call_path: String,
1255 pub args: serde_json::Value,
1256 pub session_id: String,
1257 #[serde(default, skip_serializing_if = "Option::is_none")]
1258 pub tool_call_id: Option<String>,
1259 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub replay_key: Option<String>,
1261 pub attempt_number: u32,
1262 pub max_attempts: u32,
1263 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1264 pub headers: HashMap<String, String>,
1265}
1266
1267impl RemoteToolCallRequest {
1268 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
1269 ensure_protocol_version(self.protocol_version)?;
1270 if self.tool_name.trim().is_empty() {
1271 return Err(RemoteProtocolError::UnknownRemoteTool {
1272 tool_name: self.tool_name.clone(),
1273 });
1274 }
1275 if self.call_path.trim().is_empty() {
1276 return Err(RemoteProtocolError::RemoteToolTransport(
1277 "remote tool call request requires a non-empty call_path".to_string(),
1278 ));
1279 }
1280 if self.session_id.trim().is_empty() {
1281 return Err(RemoteProtocolError::RemoteToolTransport(
1282 "remote tool call request requires a non-empty session_id".to_string(),
1283 ));
1284 }
1285 Ok(())
1286 }
1287}
1288
1289#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1290#[serde(tag = "status", rename_all = "snake_case")]
1291pub enum RemoteToolCallResponse {
1292 Success {
1293 protocol_version: u32,
1294 #[serde(default)]
1295 value: serde_json::Value,
1296 },
1297 Failure {
1298 protocol_version: u32,
1299 #[serde(default = "default_failure_code")]
1300 code: String,
1301 message: String,
1302 #[serde(default, skip_serializing_if = "Option::is_none")]
1303 raw: Option<serde_json::Value>,
1304 #[serde(default, skip_serializing_if = "Option::is_none")]
1305 retry_after_ms: Option<u64>,
1306 },
1307 Cancelled {
1308 protocol_version: u32,
1309 message: String,
1310 #[serde(default, skip_serializing_if = "Option::is_none")]
1311 raw: Option<serde_json::Value>,
1312 },
1313}
1314
1315impl RemoteToolCallResponse {
1316 pub fn protocol_version(&self) -> u32 {
1317 match self {
1318 Self::Success {
1319 protocol_version, ..
1320 }
1321 | Self::Failure {
1322 protocol_version, ..
1323 }
1324 | Self::Cancelled {
1325 protocol_version, ..
1326 } => *protocol_version,
1327 }
1328 }
1329
1330 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
1331 ensure_protocol_version(self.protocol_version())
1332 }
1333}
1334
1335fn default_failure_code() -> String {
1336 "remote_tool_error".to_string()
1337}
1338
1339#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1340pub struct RemoteUsage {
1341 pub input_tokens: i64,
1342 pub output_tokens: i64,
1343 pub cached_input_tokens: i64,
1344 #[serde(default)]
1345 pub reasoning_tokens: i64,
1346}
1347
1348impl RemoteUsage {
1349 pub fn add(&mut self, other: &Self) {
1350 self.input_tokens += other.input_tokens;
1351 self.output_tokens += other.output_tokens;
1352 self.cached_input_tokens += other.cached_input_tokens;
1353 self.reasoning_tokens += other.reasoning_tokens;
1354 }
1355}
1356
1357#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1358pub struct RemoteTokenLedgerEntry {
1359 pub source: String,
1360 pub model: String,
1361 pub usage: RemoteUsage,
1362}
1363
1364#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1365pub struct RemoteTurnActivity {
1366 pub protocol_version: u32,
1367 pub sequence: u64,
1368 pub id: String,
1369 pub correlation_id: String,
1370 #[serde(flatten)]
1371 pub event: RemoteTurnEvent,
1372}
1373
1374impl RemoteTurnActivity {
1375 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
1376 ensure_protocol_version(self.protocol_version)?;
1377 require_non_empty("RemoteTurnActivity", "id", &self.id)?;
1378 require_non_empty("RemoteTurnActivity", "correlation_id", &self.correlation_id)
1379 }
1380}
1381
1382#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1383#[serde(tag = "type", rename_all = "snake_case")]
1384pub enum RemoteTurnEvent {
1385 ModelRequestStarted {
1386 protocol_iteration: usize,
1387 },
1388 AssistantProseDelta {
1389 text: String,
1390 },
1391 ReasoningDelta {
1392 text: String,
1393 },
1394 CodeBlockStarted {
1395 language: String,
1396 code: String,
1397 #[serde(default, skip_serializing_if = "Option::is_none")]
1398 graph_key: Option<String>,
1399 },
1400 CodeBlockCompleted {
1401 language: String,
1402 output: String,
1403 #[serde(default, skip_serializing_if = "Option::is_none")]
1404 error: Option<String>,
1405 success: bool,
1406 duration_ms: u64,
1407 tool_call_ids: Vec<String>,
1408 #[serde(default, skip_serializing_if = "Option::is_none")]
1409 graph_key: Option<String>,
1410 },
1411 ToolCallStarted {
1412 #[serde(default, skip_serializing_if = "Option::is_none")]
1413 call_id: Option<String>,
1414 name: String,
1415 args: serde_json::Value,
1416 },
1417 ToolCallCompleted {
1418 #[serde(default, skip_serializing_if = "Option::is_none")]
1419 call_id: Option<String>,
1420 name: String,
1421 args: serde_json::Value,
1422 output: serde_json::Value,
1423 duration_ms: u64,
1424 },
1425 SubmittedValue {
1426 value: serde_json::Value,
1427 },
1428 ToolValue {
1429 tool_name: String,
1430 value: serde_json::Value,
1431 },
1432 Usage {
1433 protocol_iteration: usize,
1434 usage: RemoteUsage,
1435 cumulative: RemoteUsage,
1436 },
1437 ChildUsage {
1438 session_id: String,
1439 source: String,
1440 model: String,
1441 protocol_iteration: usize,
1442 usage: RemoteUsage,
1443 cumulative: RemoteUsage,
1444 },
1445 RetryStatus {
1446 wait_seconds: u64,
1447 attempt: usize,
1448 max_attempts: usize,
1449 reason: String,
1450 },
1451 RuntimeDiagnostic {
1452 kind: String,
1453 data: serde_json::Value,
1454 },
1455 Error {
1456 message: String,
1457 },
1458}
1459
1460pub trait RemoteToolRegistry {
1461 fn grants(&self) -> Vec<RemoteToolGrant>;
1462
1463 fn validate_registry(&self) -> Result<(), RemoteProtocolError> {
1464 RemoteToolGrant::validate_all(&self.grants())
1465 }
1466}
1467
1468pub fn assert_remote_tool_registry_reopenable(
1469 before: &dyn RemoteToolRegistry,
1470 after_reopen: &dyn RemoteToolRegistry,
1471) -> Result<(), RemoteProtocolError> {
1472 let before_grants = before.grants();
1473 let after_grants = after_reopen.grants();
1474 RemoteToolGrant::validate_all(&before_grants)?;
1475 RemoteToolGrant::validate_all(&after_grants)?;
1476 let before_paths = remote_registry_call_paths(&before_grants)?;
1477 let after_paths = remote_registry_call_paths(&after_grants)?;
1478 if before_paths != after_paths {
1479 return Err(RemoteProtocolError::RemoteToolRegistryReopenMismatch {
1480 before_call_paths: before_paths,
1481 after_call_paths: after_paths,
1482 });
1483 }
1484 Ok(())
1485}
1486
1487fn remote_registry_call_paths(
1488 grants: &[RemoteToolGrant],
1489) -> Result<Vec<String>, RemoteProtocolError> {
1490 let mut call_paths = grants
1491 .iter()
1492 .map(RemoteToolGrant::call_path)
1493 .collect::<Result<Vec<_>, _>>()?;
1494 call_paths.sort();
1495 Ok(call_paths)
1496}
1497
1498fn require_non_empty(
1499 type_name: &'static str,
1500 field: &'static str,
1501 value: &str,
1502) -> Result<(), RemoteProtocolError> {
1503 if value.trim().is_empty() {
1504 Err(RemoteProtocolError::MissingRequiredField { type_name, field })
1505 } else {
1506 Ok(())
1507 }
1508}
1509
1510#[derive(Debug, thiserror::Error)]
1511pub enum RemoteProtocolError {
1512 #[error("unsupported remote protocol version {actual}; expected {expected}")]
1513 UnsupportedProtocolVersion { actual: u32, expected: u32 },
1514 #[error(
1515 "mismatched protocol version in {parent}.{child}: got {child_version}, expected {parent_version}"
1516 )]
1517 MismatchedNestedProtocolVersion {
1518 parent: &'static str,
1519 child: &'static str,
1520 parent_version: u32,
1521 child_version: u32,
1522 },
1523 #[error("{type_name}.{field} is required")]
1524 MissingRequiredField {
1525 type_name: &'static str,
1526 field: &'static str,
1527 },
1528 #[error("invalid {type_name}: {message}")]
1529 InvalidEnvelope {
1530 type_name: &'static str,
1531 message: String,
1532 },
1533 #[error("invalid image blob `{id}`: {message}")]
1534 InvalidImageBlob { id: String, message: String },
1535 #[error("invalid attachment reference `{id}`: {message}")]
1536 InvalidAttachmentRef { id: String, message: String },
1537 #[error("turn input is not remote-safe: {0}")]
1538 NonRemoteSafeTurnInput(String),
1539 #[error("remote tool grant `{tool_name}` is missing an explicit agent surface")]
1540 MissingToolSurface { tool_name: String },
1541 #[error("invalid remote tool grant `{tool_name}`: {message}")]
1542 InvalidToolGrant { tool_name: String, message: String },
1543 #[error("duplicate remote tool call path `{call_path}`")]
1544 DuplicateRemoteCallPath { call_path: String },
1545 #[error(
1546 "remote tool registry changed across reopen: before={before_call_paths:?}, after={after_call_paths:?}"
1547 )]
1548 RemoteToolRegistryReopenMismatch {
1549 before_call_paths: Vec<String>,
1550 after_call_paths: Vec<String>,
1551 },
1552 #[error("unknown remote tool `{tool_name}`")]
1553 UnknownRemoteTool { tool_name: String },
1554 #[error("remote tool transport failed: {0}")]
1555 RemoteToolTransport(String),
1556 #[error("failed to serialize remote activity: {0}")]
1557 ActivitySerialization(#[from] serde_json::Error),
1558 #[error("failed to write remote activity: {0}")]
1559 ActivityWrite(String),
1560}
1561
1562#[cfg(feature = "core-conversions")]
1563mod core_conversions;
1564
1565#[cfg(feature = "core-conversions")]
1566pub use core_conversions::{
1567 RemoteToolProvider, RemoteToolTransport, RemoteTurnActivitySink, replay_collected_activities,
1568};
1569
1570#[cfg(test)]
1571mod tests;