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;