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;