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