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, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
621#[serde(rename_all = "snake_case")]
622pub enum RemoteTurnStatus {
623    #[default]
624    Completed,
625    Failed,
626    Cancelled,
627    InProgress,
628}
629
630#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
631#[serde(tag = "type", rename_all = "snake_case")]
632pub enum RemoteTurnOutcome {
633    Finished { finish: RemoteTurnFinish },
634    AgentFrameSwitch { frame_id: String },
635    Stopped { stop: RemoteTurnStop },
636}
637
638impl Default for RemoteTurnOutcome {
639    fn default() -> Self {
640        Self::Stopped {
641            stop: RemoteTurnStop::Incomplete,
642        }
643    }
644}
645
646#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
647#[serde(tag = "type", rename_all = "snake_case")]
648pub enum RemoteTurnFinish {
649    AssistantMessage {
650        text: String,
651    },
652    SubmittedValue {
653        value: serde_json::Value,
654    },
655    ToolValue {
656        tool_name: String,
657        value: serde_json::Value,
658    },
659}
660
661#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
662#[serde(tag = "type", rename_all = "snake_case")]
663pub enum RemoteTurnStop {
664    Cancelled,
665    Incomplete,
666    InvalidInput,
667    MaxTurns,
668    ToolFailure,
669    ProviderError,
670    PluginAbort,
671    RuntimeError,
672    SubmittedError {
673        value: serde_json::Value,
674    },
675    ToolError {
676        tool_name: String,
677        value: serde_json::Value,
678    },
679}
680
681#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
682pub struct RemoteAssistantOutput {
683    #[serde(default)]
684    pub safe_text: String,
685    #[serde(default)]
686    pub raw_text: String,
687    #[serde(default)]
688    pub state: RemoteAssistantOutputState,
689}
690
691#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
692#[serde(rename_all = "snake_case")]
693pub enum RemoteAssistantOutputState {
694    #[default]
695    Usable,
696    EmptyOutput,
697    TracebackOnly,
698    RecoveredFromError,
699}
700
701#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
702pub struct RemoteTurnUsageSummary {
703    #[serde(default)]
704    pub parent: RemoteUsage,
705    #[serde(default, skip_serializing_if = "Vec::is_empty")]
706    pub children: Vec<RemoteTokenLedgerEntry>,
707    #[serde(default)]
708    pub total: RemoteUsage,
709}
710
711#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
712pub struct RemoteExecutionSummary {
713    #[serde(default)]
714    pub had_tool_calls: bool,
715    #[serde(default)]
716    pub had_code_execution: bool,
717}
718
719#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
720pub struct RemoteToolCallSummary {
721    #[serde(default, skip_serializing_if = "Option::is_none")]
722    pub call_id: Option<String>,
723    pub tool_name: String,
724    #[serde(default)]
725    pub args: serde_json::Value,
726    pub outcome: RemoteToolCallOutcome,
727    pub duration_ms: u64,
728}
729
730#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
731#[serde(tag = "status", content = "payload", rename_all = "snake_case")]
732pub enum RemoteToolCallOutcome {
733    Success(serde_json::Value),
734    Failure(serde_json::Value),
735    Cancelled(serde_json::Value),
736}
737
738#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
739pub struct RemoteTurnIssue {
740    pub kind: String,
741    #[serde(default, skip_serializing_if = "Option::is_none")]
742    pub code: Option<String>,
743    #[serde(default, skip_serializing_if = "Option::is_none")]
744    pub terminal_reason: Option<RemoteLlmTerminalReason>,
745    pub message: String,
746    #[serde(default, skip_serializing_if = "Option::is_none")]
747    pub raw: Option<String>,
748}
749
750#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
751pub struct RemotePromptLayer {
752    #[serde(default, skip_serializing_if = "Option::is_none")]
753    pub template: Option<RemotePromptTemplate>,
754    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
755    pub slots: HashMap<RemotePromptSlot, RemotePromptSlotLayer>,
756}
757
758impl RemotePromptLayer {
759    pub fn new() -> Self {
760        Self::default()
761    }
762
763    pub fn is_empty(&self) -> bool {
764        self.template.is_none() && self.slots.is_empty()
765    }
766}
767
768impl Default for RemotePromptLayer {
769    fn default() -> Self {
770        Self {
771            template: None,
772            slots: HashMap::new(),
773        }
774    }
775}
776
777#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
778#[serde(rename_all = "snake_case")]
779pub enum RemotePromptBuiltin {
780    MainAgentIntro,
781    ExecutionInstructions,
782    CoreGuidance,
783}
784
785#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
786#[serde(rename_all = "snake_case")]
787pub enum RemotePromptSlot {
788    Intro,
789    Execution,
790    Guidance,
791    ProjectInstructions,
792    RuntimeContext,
793    Environment,
794}
795
796#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
797#[serde(tag = "kind", rename_all = "snake_case")]
798pub enum RemotePromptTemplateEntry {
799    Text { content: String },
800    Builtin { builtin: RemotePromptBuiltin },
801    Slot { slot: RemotePromptSlot },
802}
803
804#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
805pub struct RemotePromptTemplateSection {
806    #[serde(default, skip_serializing_if = "Option::is_none")]
807    pub title: Option<String>,
808    #[serde(default, skip_serializing_if = "Vec::is_empty")]
809    pub entries: Vec<RemotePromptTemplateEntry>,
810}
811
812#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
813pub struct RemotePromptTemplate {
814    pub sections: Vec<RemotePromptTemplateSection>,
815}
816
817#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
818pub struct RemotePromptSlotLayer {
819    #[serde(default)]
820    pub reset: bool,
821    #[serde(default, skip_serializing_if = "Vec::is_empty")]
822    pub contributions: Vec<RemotePromptContribution>,
823}
824
825#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
826pub struct RemotePromptContribution {
827    pub slot: RemotePromptSlot,
828    #[serde(default, skip_serializing_if = "Option::is_none")]
829    pub title: Option<String>,
830    #[serde(default)]
831    pub priority: i32,
832    #[serde(
833        default,
834        skip_serializing_if = "RemotePromptContributionGate::is_empty"
835    )]
836    pub gate: RemotePromptContributionGate,
837    pub content: String,
838}
839
840#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
841pub struct RemotePromptContributionGate {
842    #[serde(default, skip_serializing_if = "Vec::is_empty")]
843    pub tools: Vec<String>,
844    #[serde(default)]
845    pub minimum_availability: RemoteToolAvailability,
846}
847
848impl RemotePromptContributionGate {
849    pub fn is_empty(&self) -> bool {
850        self.tools.is_empty()
851    }
852}
853
854#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
855pub struct RemoteToolGrant {
856    pub protocol_version: u32,
857    #[serde(default, skip_serializing_if = "Option::is_none")]
858    pub id: Option<String>,
859    pub name: String,
860    #[serde(default, skip_serializing_if = "String::is_empty")]
861    pub description: String,
862    #[serde(default = "default_input_schema")]
863    pub input_schema: serde_json::Value,
864    #[serde(default)]
865    pub output_schema: serde_json::Value,
866    #[serde(default, skip_serializing_if = "Vec::is_empty")]
867    pub input_schema_projections: Vec<RemoteSchemaProjectionOverride>,
868    #[serde(default, skip_serializing_if = "Vec::is_empty")]
869    pub output_schema_projections: Vec<RemoteSchemaProjectionOverride>,
870    #[serde(default, skip_serializing_if = "RemoteToolOutputContract::is_static")]
871    pub output_contract: RemoteToolOutputContract,
872    #[serde(default, skip_serializing_if = "Vec::is_empty")]
873    pub examples: Vec<String>,
874    #[serde(default, skip_serializing_if = "Option::is_none")]
875    pub availability: Option<RemoteToolAvailability>,
876    #[serde(default, skip_serializing_if = "Option::is_none")]
877    pub activation: Option<RemoteToolActivation>,
878    #[serde(default, skip_serializing_if = "Option::is_none")]
879    pub argument_projection: Option<RemoteToolArgumentProjectionPolicy>,
880    #[serde(default, skip_serializing_if = "Option::is_none")]
881    pub scheduling: Option<RemoteToolScheduling>,
882    #[serde(default, skip_serializing_if = "Option::is_none")]
883    pub retry_policy: Option<RemoteToolRetryPolicy>,
884    #[serde(default, skip_serializing_if = "Option::is_none")]
885    pub agent_surface: Option<RemoteToolAgentSurface>,
886}
887
888impl RemoteToolGrant {
889    pub fn call_path(&self) -> Result<String, RemoteProtocolError> {
890        let surface = self.required_surface()?;
891        Ok(format!(
892            "{}.{}",
893            surface.module_path.join("."),
894            surface.operation
895        ))
896    }
897
898    pub fn validate(&self) -> Result<(), RemoteProtocolError> {
899        ensure_protocol_version(self.protocol_version)?;
900        if self.name.trim().is_empty() {
901            return Err(RemoteProtocolError::InvalidToolGrant {
902                tool_name: self.name.clone(),
903                message: "tool grant name cannot be empty".to_string(),
904            });
905        }
906        self.required_surface()?;
907        Ok(())
908    }
909
910    pub fn validate_all(grants: &[Self]) -> Result<(), RemoteProtocolError> {
911        let mut seen = HashSet::new();
912        for grant in grants {
913            grant.validate()?;
914            let call_path = grant.call_path()?;
915            if !seen.insert(call_path.clone()) {
916                return Err(RemoteProtocolError::DuplicateRemoteCallPath { call_path });
917            }
918        }
919        Ok(())
920    }
921
922    fn required_surface(&self) -> Result<&RemoteToolAgentSurface, RemoteProtocolError> {
923        let Some(surface) = &self.agent_surface else {
924            return Err(RemoteProtocolError::MissingToolSurface {
925                tool_name: self.name.clone(),
926            });
927        };
928        if surface.module_path.is_empty() {
929            return Err(RemoteProtocolError::InvalidToolGrant {
930                tool_name: self.name.clone(),
931                message: "remote tool grant requires an explicit module path".to_string(),
932            });
933        }
934        if surface
935            .module_path
936            .iter()
937            .any(|part| part.trim().is_empty())
938        {
939            return Err(RemoteProtocolError::InvalidToolGrant {
940                tool_name: self.name.clone(),
941                message: "remote tool grant module path cannot contain empty segments".to_string(),
942            });
943        }
944        if surface.operation.trim().is_empty() {
945            return Err(RemoteProtocolError::InvalidToolGrant {
946                tool_name: self.name.clone(),
947                message: "remote tool grant requires an explicit operation".to_string(),
948            });
949        }
950        Ok(surface)
951    }
952}
953
954fn default_input_schema() -> serde_json::Value {
955    serde_json::json!({
956        "type": "object",
957        "properties": {},
958        "additionalProperties": true
959    })
960}
961
962#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
963pub struct RemoteToolAgentSurface {
964    pub module_path: Vec<String>,
965    pub operation: String,
966    #[serde(default, skip_serializing_if = "Option::is_none")]
967    pub authority_type: Option<String>,
968    #[serde(default, skip_serializing_if = "Vec::is_empty")]
969    pub aliases: Vec<String>,
970}
971
972impl RemoteToolAgentSurface {
973    pub fn new(
974        module_path: impl IntoIterator<Item = impl Into<String>>,
975        operation: impl Into<String>,
976    ) -> Self {
977        Self {
978            module_path: module_path.into_iter().map(Into::into).collect(),
979            operation: operation.into(),
980            authority_type: None,
981            aliases: Vec::new(),
982        }
983    }
984}
985
986#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
987pub struct RemoteSchemaProjectionOverride {
988    pub profile: String,
989    pub schema: serde_json::Value,
990}
991
992#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
993#[serde(rename_all = "snake_case")]
994pub enum RemoteToolAvailability {
995    Off,
996    Searchable,
997    Callable,
998    #[default]
999    Showcased,
1000}
1001
1002#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1003#[serde(rename_all = "snake_case")]
1004pub enum RemoteToolActivation {
1005    #[default]
1006    Always,
1007    Internal,
1008}
1009
1010#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1011#[serde(rename_all = "snake_case")]
1012pub enum RemoteToolScheduling {
1013    #[default]
1014    Parallel,
1015    Serial,
1016}
1017
1018#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1019#[serde(tag = "kind", rename_all = "snake_case")]
1020pub enum RemoteToolOutputContract {
1021    #[default]
1022    Static,
1023    FromInputSchema {
1024        input_field: String,
1025        #[serde(default, skip_serializing_if = "Option::is_none")]
1026        default_schema: Option<serde_json::Value>,
1027    },
1028}
1029
1030impl RemoteToolOutputContract {
1031    fn is_static(&self) -> bool {
1032        matches!(self, Self::Static)
1033    }
1034}
1035
1036#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1037#[serde(tag = "kind", rename_all = "snake_case")]
1038pub enum RemoteToolArgumentProjectionPolicy {
1039    #[default]
1040    MaterializeProjectedValues,
1041    PreserveProjectedRefsInField {
1042        field: String,
1043    },
1044}
1045
1046#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1047#[serde(tag = "type", rename_all = "snake_case")]
1048pub enum RemoteToolRetryPolicy {
1049    #[default]
1050    Never,
1051    Safe {
1052        max_attempts: u32,
1053        base_delay_ms: u64,
1054        max_delay_ms: u64,
1055    },
1056    Idempotent {
1057        max_attempts: u32,
1058        base_delay_ms: u64,
1059        max_delay_ms: u64,
1060    },
1061}
1062
1063#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1064pub struct RemoteToolCallRequest {
1065    pub protocol_version: u32,
1066    pub tool_name: String,
1067    pub call_path: String,
1068    pub args: serde_json::Value,
1069    pub session_id: String,
1070    #[serde(default, skip_serializing_if = "Option::is_none")]
1071    pub tool_call_id: Option<String>,
1072    #[serde(default, skip_serializing_if = "Option::is_none")]
1073    pub replay_key: Option<String>,
1074    pub attempt_number: u32,
1075    pub max_attempts: u32,
1076    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1077    pub headers: HashMap<String, String>,
1078}
1079
1080impl RemoteToolCallRequest {
1081    pub fn validate(&self) -> Result<(), RemoteProtocolError> {
1082        ensure_protocol_version(self.protocol_version)?;
1083        if self.tool_name.trim().is_empty() {
1084            return Err(RemoteProtocolError::UnknownRemoteTool {
1085                tool_name: self.tool_name.clone(),
1086            });
1087        }
1088        if self.call_path.trim().is_empty() {
1089            return Err(RemoteProtocolError::RemoteToolTransport(
1090                "remote tool call request requires a non-empty call_path".to_string(),
1091            ));
1092        }
1093        if self.session_id.trim().is_empty() {
1094            return Err(RemoteProtocolError::RemoteToolTransport(
1095                "remote tool call request requires a non-empty session_id".to_string(),
1096            ));
1097        }
1098        Ok(())
1099    }
1100}
1101
1102#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1103#[serde(tag = "status", rename_all = "snake_case")]
1104pub enum RemoteToolCallResponse {
1105    Success {
1106        protocol_version: u32,
1107        #[serde(default)]
1108        value: serde_json::Value,
1109    },
1110    Failure {
1111        protocol_version: u32,
1112        #[serde(default = "default_failure_code")]
1113        code: String,
1114        message: String,
1115        #[serde(default, skip_serializing_if = "Option::is_none")]
1116        raw: Option<serde_json::Value>,
1117        #[serde(default, skip_serializing_if = "Option::is_none")]
1118        retry_after_ms: Option<u64>,
1119    },
1120    Cancelled {
1121        protocol_version: u32,
1122        message: String,
1123        #[serde(default, skip_serializing_if = "Option::is_none")]
1124        raw: Option<serde_json::Value>,
1125    },
1126}
1127
1128impl RemoteToolCallResponse {
1129    pub fn protocol_version(&self) -> u32 {
1130        match self {
1131            Self::Success {
1132                protocol_version, ..
1133            }
1134            | Self::Failure {
1135                protocol_version, ..
1136            }
1137            | Self::Cancelled {
1138                protocol_version, ..
1139            } => *protocol_version,
1140        }
1141    }
1142
1143    pub fn validate(&self) -> Result<(), RemoteProtocolError> {
1144        ensure_protocol_version(self.protocol_version())
1145    }
1146}
1147
1148fn default_failure_code() -> String {
1149    "remote_tool_error".to_string()
1150}
1151
1152#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1153pub struct RemoteUsage {
1154    pub input_tokens: i64,
1155    pub output_tokens: i64,
1156    pub cached_input_tokens: i64,
1157    #[serde(default)]
1158    pub reasoning_tokens: i64,
1159}
1160
1161impl RemoteUsage {
1162    pub fn add(&mut self, other: &Self) {
1163        self.input_tokens += other.input_tokens;
1164        self.output_tokens += other.output_tokens;
1165        self.cached_input_tokens += other.cached_input_tokens;
1166        self.reasoning_tokens += other.reasoning_tokens;
1167    }
1168}
1169
1170#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1171pub struct RemoteTokenLedgerEntry {
1172    pub source: String,
1173    pub model: String,
1174    pub usage: RemoteUsage,
1175}
1176
1177#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1178pub struct RemoteTurnActivity {
1179    pub protocol_version: u32,
1180    pub sequence: u64,
1181    pub id: String,
1182    pub correlation_id: String,
1183    #[serde(flatten)]
1184    pub event: RemoteTurnEvent,
1185}
1186
1187impl RemoteTurnActivity {
1188    pub fn validate(&self) -> Result<(), RemoteProtocolError> {
1189        ensure_protocol_version(self.protocol_version)?;
1190        require_non_empty("RemoteTurnActivity", "id", &self.id)?;
1191        require_non_empty("RemoteTurnActivity", "correlation_id", &self.correlation_id)
1192    }
1193}
1194
1195#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
1196#[serde(tag = "type", rename_all = "snake_case")]
1197pub enum RemoteTurnEvent {
1198    ModelRequestStarted {
1199        protocol_iteration: usize,
1200    },
1201    AssistantProseDelta {
1202        text: String,
1203    },
1204    ReasoningDelta {
1205        text: String,
1206    },
1207    CodeBlockStarted {
1208        language: String,
1209        code: String,
1210        #[serde(default, skip_serializing_if = "Option::is_none")]
1211        graph_key: Option<String>,
1212    },
1213    CodeBlockCompleted {
1214        language: String,
1215        output: String,
1216        #[serde(default, skip_serializing_if = "Option::is_none")]
1217        error: Option<String>,
1218        success: bool,
1219        duration_ms: u64,
1220        tool_call_ids: Vec<String>,
1221        #[serde(default, skip_serializing_if = "Option::is_none")]
1222        graph_key: Option<String>,
1223    },
1224    ToolCallStarted {
1225        #[serde(default, skip_serializing_if = "Option::is_none")]
1226        call_id: Option<String>,
1227        name: String,
1228        args: serde_json::Value,
1229    },
1230    ToolCallCompleted {
1231        #[serde(default, skip_serializing_if = "Option::is_none")]
1232        call_id: Option<String>,
1233        name: String,
1234        args: serde_json::Value,
1235        output: serde_json::Value,
1236        duration_ms: u64,
1237    },
1238    SubmittedValue {
1239        value: serde_json::Value,
1240    },
1241    ToolValue {
1242        tool_name: String,
1243        value: serde_json::Value,
1244    },
1245    Usage {
1246        protocol_iteration: usize,
1247        usage: RemoteUsage,
1248        cumulative: RemoteUsage,
1249    },
1250    ChildUsage {
1251        session_id: String,
1252        source: String,
1253        model: String,
1254        protocol_iteration: usize,
1255        usage: RemoteUsage,
1256        cumulative: RemoteUsage,
1257    },
1258    RetryStatus {
1259        wait_seconds: u64,
1260        attempt: usize,
1261        max_attempts: usize,
1262        reason: String,
1263    },
1264    RuntimeDiagnostic {
1265        kind: String,
1266        data: serde_json::Value,
1267    },
1268    Error {
1269        message: String,
1270    },
1271}
1272
1273pub trait RemoteToolRegistry {
1274    fn grants(&self) -> Vec<RemoteToolGrant>;
1275
1276    fn validate_registry(&self) -> Result<(), RemoteProtocolError> {
1277        RemoteToolGrant::validate_all(&self.grants())
1278    }
1279}
1280
1281pub fn assert_remote_tool_registry_reopenable(
1282    before: &dyn RemoteToolRegistry,
1283    after_reopen: &dyn RemoteToolRegistry,
1284) -> Result<(), RemoteProtocolError> {
1285    let before_grants = before.grants();
1286    let after_grants = after_reopen.grants();
1287    RemoteToolGrant::validate_all(&before_grants)?;
1288    RemoteToolGrant::validate_all(&after_grants)?;
1289    let before_paths = remote_registry_call_paths(&before_grants)?;
1290    let after_paths = remote_registry_call_paths(&after_grants)?;
1291    if before_paths != after_paths {
1292        return Err(RemoteProtocolError::RemoteToolRegistryReopenMismatch {
1293            before_call_paths: before_paths,
1294            after_call_paths: after_paths,
1295        });
1296    }
1297    Ok(())
1298}
1299
1300fn remote_registry_call_paths(
1301    grants: &[RemoteToolGrant],
1302) -> Result<Vec<String>, RemoteProtocolError> {
1303    let mut call_paths = grants
1304        .iter()
1305        .map(RemoteToolGrant::call_path)
1306        .collect::<Result<Vec<_>, _>>()?;
1307    call_paths.sort();
1308    Ok(call_paths)
1309}
1310
1311fn require_non_empty(
1312    type_name: &'static str,
1313    field: &'static str,
1314    value: &str,
1315) -> Result<(), RemoteProtocolError> {
1316    if value.trim().is_empty() {
1317        Err(RemoteProtocolError::MissingRequiredField { type_name, field })
1318    } else {
1319        Ok(())
1320    }
1321}
1322
1323#[derive(Debug, thiserror::Error)]
1324pub enum RemoteProtocolError {
1325    #[error("unsupported remote protocol version {actual}; expected {expected}")]
1326    UnsupportedProtocolVersion { actual: u32, expected: u32 },
1327    #[error(
1328        "mismatched protocol version in {parent}.{child}: got {child_version}, expected {parent_version}"
1329    )]
1330    MismatchedNestedProtocolVersion {
1331        parent: &'static str,
1332        child: &'static str,
1333        parent_version: u32,
1334        child_version: u32,
1335    },
1336    #[error("{type_name}.{field} is required")]
1337    MissingRequiredField {
1338        type_name: &'static str,
1339        field: &'static str,
1340    },
1341    #[error("invalid {type_name}: {message}")]
1342    InvalidEnvelope {
1343        type_name: &'static str,
1344        message: String,
1345    },
1346    #[error("invalid image blob `{id}`: {message}")]
1347    InvalidImageBlob { id: String, message: String },
1348    #[error("invalid attachment reference `{id}`: {message}")]
1349    InvalidAttachmentRef { id: String, message: String },
1350    #[error("turn input is not remote-safe: {0}")]
1351    NonRemoteSafeTurnInput(String),
1352    #[error("remote tool grant `{tool_name}` is missing an explicit agent surface")]
1353    MissingToolSurface { tool_name: String },
1354    #[error("invalid remote tool grant `{tool_name}`: {message}")]
1355    InvalidToolGrant { tool_name: String, message: String },
1356    #[error("duplicate remote tool call path `{call_path}`")]
1357    DuplicateRemoteCallPath { call_path: String },
1358    #[error(
1359        "remote tool registry changed across reopen: before={before_call_paths:?}, after={after_call_paths:?}"
1360    )]
1361    RemoteToolRegistryReopenMismatch {
1362        before_call_paths: Vec<String>,
1363        after_call_paths: Vec<String>,
1364    },
1365    #[error("unknown remote tool `{tool_name}`")]
1366    UnknownRemoteTool { tool_name: String },
1367    #[error("remote tool transport failed: {0}")]
1368    RemoteToolTransport(String),
1369    #[error("failed to serialize remote activity: {0}")]
1370    ActivitySerialization(#[from] serde_json::Error),
1371    #[error("failed to write remote activity: {0}")]
1372    ActivityWrite(String),
1373}
1374
1375#[cfg(feature = "core-conversions")]
1376mod core_conversions;
1377
1378#[cfg(feature = "core-conversions")]
1379pub use core_conversions::{
1380    RemoteToolProvider, RemoteToolTransport, RemoteTurnActivitySink, replay_collected_activities,
1381};
1382
1383#[cfg(test)]
1384mod tests;