Skip to main content

lash_remote_protocol/
lib.rs

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;