1use std::collections::{BTreeMap, HashMap};
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9use crate::ensure_protocol_version;
10use crate::registry_errors::{RemoteProtocolError, require_non_empty};
11use crate::usage_activity::RemoteUsage;
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
14pub struct RemoteSchemaContract {
15 pub canonical: serde_json::Value,
16 #[serde(
17 default,
18 skip_serializing_if = "RemoteSchemaProjectionPolicy::is_default"
19 )]
20 pub projection: RemoteSchemaProjectionPolicy,
21}
22
23impl RemoteSchemaContract {
24 fn new(canonical: serde_json::Value) -> Self {
25 Self {
26 canonical,
27 projection: RemoteSchemaProjectionPolicy::default(),
28 }
29 }
30}
31
32impl Default for RemoteSchemaContract {
33 fn default() -> Self {
34 Self::new(serde_json::Value::Null)
35 }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
39pub struct RemoteSchemaProjectionPolicy {
40 #[serde(default, skip_serializing_if = "RemoteProjectionMode::is_auto")]
41 pub mode: RemoteProjectionMode,
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub overrides: Vec<RemoteSchemaProjectionOverride>,
44}
45
46impl RemoteSchemaProjectionPolicy {
47 fn is_default(&self) -> bool {
48 self.mode == RemoteProjectionMode::Auto && self.overrides.is_empty()
49 }
50}
51
52impl Default for RemoteSchemaProjectionPolicy {
53 fn default() -> Self {
54 Self {
55 mode: RemoteProjectionMode::Auto,
56 overrides: Vec::new(),
57 }
58 }
59}
60
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
62#[serde(rename_all = "snake_case")]
63pub enum RemoteProjectionMode {
64 #[default]
65 Auto,
66 ExplicitOnly,
67 Exact,
68}
69
70impl RemoteProjectionMode {
71 fn is_auto(&self) -> bool {
72 *self == Self::Auto
73 }
74}
75
76#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
77pub struct RemoteSchemaProjectionOverride {
78 pub dialect: String,
79 pub schema: serde_json::Value,
80}
81
82pub(crate) fn default_remote_input_schema() -> RemoteSchemaContract {
83 RemoteSchemaContract::new(serde_json::json!({
84 "type": "object",
85 "properties": {},
86 "additionalProperties": true
87 }))
88}
89
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
91pub struct RemoteLlmRequest {
92 pub protocol_version: u32,
93 pub request_id: String,
94 pub scope: RemoteLlmRequestScope,
95 pub model_intent: RemoteModelIntent,
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub messages: Vec<RemoteLlmMessage>,
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
99 pub attachments: Vec<RemoteLlmAttachment>,
100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub tools: Vec<RemoteLlmToolSpec>,
102 #[serde(default)]
103 pub tool_choice: RemoteLlmToolChoice,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub output_spec: Option<RemoteLlmOutputSpec>,
106 #[serde(default, skip_serializing_if = "RemoteGenerationOptions::is_empty")]
107 pub generation: RemoteGenerationOptions,
108 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
109 pub metadata: HashMap<String, serde_json::Value>,
110}
111
112impl RemoteLlmRequest {
113 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
114 ensure_protocol_version(self.protocol_version)?;
115 require_non_empty("RemoteLlmRequest", "request_id", &self.request_id)?;
116 self.scope.validate()?;
117 self.model_intent.validate()?;
118 self.generation.validate("RemoteLlmRequest")?;
119 for (index, message) in self.messages.iter().enumerate() {
120 message.validate(index)?;
121 }
122 for (index, attachment) in self.attachments.iter().enumerate() {
123 attachment.validate(index)?;
124 }
125 for tool in &self.tools {
126 tool.validate()?;
127 }
128 if let Some(output_spec) = &self.output_spec {
129 output_spec.validate()?;
130 }
131 Ok(())
132 }
133}
134
135#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
136pub struct RemoteLlmResponse {
137 pub protocol_version: u32,
138 pub request_id: String,
139 #[serde(default)]
140 pub full_text: String,
141 #[serde(default, skip_serializing_if = "Vec::is_empty")]
142 pub output_parts: Vec<RemoteLlmOutputPart>,
143 #[serde(default)]
144 pub usage: RemoteUsage,
145 #[serde(default)]
146 pub terminal_reason: RemoteLlmTerminalReason,
147 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub diagnostics: Vec<RemoteDiagnostic>,
149 #[serde(default, skip_serializing_if = "RemoteProviderMetadata::is_empty")]
150 pub provider_metadata: RemoteProviderMetadata,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub execution_evidence: Option<RemoteExecutionEvidence>,
153}
154
155#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
156pub struct RemoteExecutionEvidence {
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub served_model: Option<String>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub provider_response_id: Option<String>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub provider_request_id: Option<String>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub reasoning_output_tokens: Option<u64>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub provider_finish_reason: Option<String>,
167}
168
169impl RemoteLlmResponse {
170 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
171 ensure_protocol_version(self.protocol_version)?;
172 require_non_empty("RemoteLlmResponse", "request_id", &self.request_id)?;
173 Ok(())
174 }
175}
176
177#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
178pub struct RemoteModelIntent {
179 pub model: String,
180 #[serde(default)]
181 pub variant: RemoteReasoningSelection,
182 #[serde(default, skip_serializing_if = "RemoteModelCapability::is_empty")]
185 pub capability: RemoteModelCapability,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub provider: Option<String>,
188 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
189 pub metadata: HashMap<String, String>,
190}
191
192#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
196pub struct RemoteModelCapability {
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub reasoning: Option<RemoteReasoningCapability>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub cache_control: Option<RemoteCacheControlDialect>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub stream_termination: Option<RemoteStreamTermination>,
203}
204
205impl RemoteModelCapability {
206 pub fn is_empty(&self) -> bool {
207 self.reasoning.is_none()
208 && self.cache_control.is_none()
209 && self.stream_termination.is_none()
210 }
211}
212
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
215#[serde(rename_all = "snake_case")]
216pub enum RemoteCacheControlDialect {
217 Anthropic,
218 Gemini,
219}
220
221#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
223#[serde(rename_all = "snake_case")]
224pub enum RemoteStreamTermination {
225 RequireTerminalEvidence,
226 EofTolerated,
227}
228
229#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231pub struct RemoteReasoningCapability {
232 #[serde(default)]
233 pub efforts: Vec<String>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub default_effort: Option<String>,
236 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
237 pub aliases: BTreeMap<String, String>,
238 #[serde(default)]
239 pub encoding: RemoteReasoningEncoding,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub disable: Option<RemoteReasoningDisableEncoding>,
242 #[serde(default)]
243 pub mandatory: bool,
244}
245
246#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
247#[serde(rename_all = "snake_case")]
248pub enum RemoteReasoningSelection {
249 #[default]
250 ProviderDefault,
251 Disabled,
252 Effort(String),
253}
254
255#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
256#[serde(rename_all = "snake_case")]
257pub enum RemoteReasoningDisableEncoding {
258 Native,
259 Omit,
260 Effort(String),
261 Budget(u32),
262 ToggleFalse,
263}
264
265#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
267#[serde(rename_all = "snake_case")]
268pub enum RemoteReasoningEncoding {
269 #[default]
270 Effort,
271 Budget(BTreeMap<String, u32>),
272}
273
274impl RemoteModelIntent {
275 pub fn new(model: impl Into<String>) -> Self {
276 Self {
277 model: model.into(),
278 variant: RemoteReasoningSelection::ProviderDefault,
279 capability: RemoteModelCapability::default(),
280 provider: None,
281 metadata: HashMap::new(),
282 }
283 }
284
285 pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
286 require_non_empty("RemoteModelIntent", "model", &self.model)
287 }
288}
289
290#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
291pub struct RemoteGenerationOptions {
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub output_token_cap: Option<u64>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub temperature: Option<String>,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub top_p: Option<String>,
298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
299 pub stop: Vec<String>,
300 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
301 pub provider_options: HashMap<String, String>,
302}
303
304impl RemoteGenerationOptions {
305 pub fn is_empty(&self) -> bool {
306 self.output_token_cap.is_none()
307 && self.temperature.is_none()
308 && self.top_p.is_none()
309 && self.stop.is_empty()
310 && self.provider_options.is_empty()
311 }
312
313 pub(crate) fn validate(&self, type_name: &'static str) -> Result<(), RemoteProtocolError> {
314 if self.output_token_cap == Some(0) {
315 return Err(RemoteProtocolError::InvalidEnvelope {
316 type_name,
317 message: "generation.output_token_cap must be greater than zero".to_string(),
318 });
319 }
320 Ok(())
321 }
322}
323
324#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
325pub struct RemoteLlmRequestScope {
326 pub session_id: String,
327 pub agent_frame_id: String,
328 pub request_id: String,
329}
330
331impl RemoteLlmRequestScope {
332 pub fn new(
333 session_id: impl Into<String>,
334 agent_frame_id: impl Into<String>,
335 request_id: impl Into<String>,
336 ) -> Self {
337 Self {
338 session_id: session_id.into(),
339 agent_frame_id: agent_frame_id.into(),
340 request_id: request_id.into(),
341 }
342 }
343
344 fn validate(&self) -> Result<(), RemoteProtocolError> {
345 require_non_empty("RemoteLlmRequestScope", "session_id", &self.session_id)?;
346 require_non_empty(
347 "RemoteLlmRequestScope",
348 "agent_frame_id",
349 &self.agent_frame_id,
350 )?;
351 require_non_empty("RemoteLlmRequestScope", "request_id", &self.request_id)?;
352 Ok(())
353 }
354}
355
356#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
357#[serde(rename_all = "snake_case")]
358pub enum RemoteLlmRole {
359 #[default]
360 User,
361 Assistant,
362 System,
363}
364
365#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
366pub struct RemoteLlmMessage {
367 pub role: RemoteLlmRole,
368 #[serde(default, skip_serializing_if = "Vec::is_empty")]
369 pub content: Vec<RemoteLlmContentBlock>,
370}
371
372impl RemoteLlmMessage {
373 fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
374 if self.content.is_empty() {
375 return Err(RemoteProtocolError::InvalidEnvelope {
376 type_name: "RemoteLlmMessage",
377 message: format!("message at index {index} must contain at least one block"),
378 });
379 }
380 for block in &self.content {
381 block.validate()?;
382 }
383 Ok(())
384 }
385}
386
387#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
388#[serde(tag = "type", rename_all = "snake_case")]
389pub enum RemoteLlmContentBlock {
390 Text {
391 text: String,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
393 response_meta: Option<RemoteResponseTextMeta>,
394 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
395 cache_breakpoint: bool,
396 },
397 ImageAttachment {
398 attachment_index: usize,
399 },
400 ToolCall {
401 call_id: String,
402 tool_name: String,
403 input_json: String,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
405 replay: Option<RemoteProviderReplayMeta>,
406 },
407 ToolResult {
408 call_id: String,
409 content: String,
410 #[serde(default, skip_serializing_if = "Option::is_none")]
411 tool_name: Option<String>,
412 },
413 Reasoning {
414 text: String,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 replay: Option<RemoteProviderReasoningReplay>,
417 },
418}
419
420impl RemoteLlmContentBlock {
421 fn validate(&self) -> Result<(), RemoteProtocolError> {
422 match self {
423 Self::ToolCall {
424 call_id, tool_name, ..
425 } => {
426 require_non_empty("RemoteLlmContentBlock::ToolCall", "call_id", call_id)?;
427 require_non_empty("RemoteLlmContentBlock::ToolCall", "tool_name", tool_name)
428 }
429 Self::ToolResult { call_id, .. } => {
430 require_non_empty("RemoteLlmContentBlock::ToolResult", "call_id", call_id)
431 }
432 Self::Text { .. } | Self::ImageAttachment { .. } | Self::Reasoning { .. } => Ok(()),
433 }
434 }
435}
436
437#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
438pub struct RemoteResponseTextMeta {
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub id: Option<String>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub status: Option<String>,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub phase: Option<String>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub provider_payload: Option<String>,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub origin_provider: Option<String>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub origin_model: Option<String>,
451}
452
453#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
454pub struct RemoteProviderReplayMeta {
455 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub item_id: Option<String>,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub opaque: Option<String>,
459}
460
461#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
462pub struct RemoteProviderReasoningReplay {
463 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub item_id: Option<String>,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 pub encrypted_content: Option<String>,
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 pub signature: Option<String>,
469 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
470 pub redacted: bool,
471 #[serde(default, skip_serializing_if = "Vec::is_empty")]
472 pub summary: Vec<String>,
473}
474
475#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
476pub struct RemoteLlmAttachment {
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub id: Option<String>,
479 pub mime: String,
480 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub data_base64: Option<String>,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub reference: Option<RemoteAttachmentRef>,
484 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
485 pub metadata: HashMap<String, String>,
486}
487
488impl RemoteLlmAttachment {
489 fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
490 if self.mime.trim().is_empty() {
491 return Err(RemoteProtocolError::InvalidEnvelope {
492 type_name: "RemoteLlmAttachment",
493 message: format!("attachment at index {index} requires a non-empty mime"),
494 });
495 }
496 if let Some(reference) = &self.reference {
497 reference.validate()?;
498 }
499 Ok(())
500 }
501}
502
503#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
504pub struct RemoteAttachmentRef {
505 pub id: String,
506 pub mime: String,
507 pub byte_len: u64,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
509 pub width: Option<u32>,
510 #[serde(default, skip_serializing_if = "Option::is_none")]
511 pub height: Option<u32>,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub label: Option<String>,
514 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
515 pub metadata: HashMap<String, String>,
516}
517
518impl RemoteAttachmentRef {
519 pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
520 require_non_empty("RemoteAttachmentRef", "id", &self.id)?;
521 require_non_empty("RemoteAttachmentRef", "mime", &self.mime)
522 }
523}
524
525#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
526pub struct RemoteLlmToolSpec {
527 pub name: String,
528 #[serde(default)]
529 pub description: String,
530 #[serde(default = "default_remote_input_schema")]
531 pub input_schema: RemoteSchemaContract,
532 #[serde(default)]
533 pub output_schema: RemoteSchemaContract,
534}
535
536impl RemoteLlmToolSpec {
537 pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
538 require_non_empty("RemoteLlmToolSpec", "name", &self.name)
539 }
540}
541
542#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
543#[serde(rename_all = "snake_case")]
544pub enum RemoteLlmToolChoice {
545 #[default]
546 Auto,
547 None,
548 Required,
549}
550
551#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
552#[serde(tag = "type", rename_all = "snake_case")]
553pub enum RemoteLlmOutputSpec {
554 JsonObject,
555 JsonSchema {
556 name: String,
557 schema: RemoteSchemaContract,
558 strict: bool,
559 },
560}
561
562impl RemoteLlmOutputSpec {
563 fn validate(&self) -> Result<(), RemoteProtocolError> {
564 match self {
565 Self::JsonObject => Ok(()),
566 Self::JsonSchema { name, .. } => {
567 require_non_empty("RemoteLlmOutputSpec::JsonSchema", "name", name)
568 }
569 }
570 }
571}
572
573#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
574#[serde(tag = "type", rename_all = "snake_case")]
575pub enum RemoteLlmOutputPart {
576 Text {
577 text: String,
578 #[serde(default, skip_serializing_if = "Option::is_none")]
579 response_meta: Option<RemoteResponseTextMeta>,
580 },
581 Reasoning {
582 text: String,
583 #[serde(default, skip_serializing_if = "Option::is_none")]
584 replay: Option<RemoteProviderReasoningReplay>,
585 },
586 ToolCall {
587 call_id: String,
588 tool_name: String,
589 input_json: String,
590 #[serde(default, skip_serializing_if = "Option::is_none")]
591 replay: Option<RemoteProviderReplayMeta>,
592 },
593}
594
595#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
596#[serde(rename_all = "snake_case")]
597pub enum RemoteLlmTerminalReason {
598 Stop,
599 ToolUse,
600 OutputLimit,
601 ContextOverflow,
602 ContentFilter,
603 ProviderError,
604 Cancelled,
605 #[default]
606 Unknown,
607}
608
609#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
611#[serde(rename_all = "snake_case")]
612pub enum RemoteProviderFailureKind {
613 Transport,
614 Timeout,
615 Http,
616 Stream,
617 Auth,
618 Validation,
619 Quota,
620 Unsupported,
621 #[default]
622 #[serde(other)]
623 Unknown,
624}
625
626#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
627pub struct RemoteProviderMetadata {
628 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub usage: Option<serde_json::Value>,
630 #[serde(default, skip_serializing_if = "Option::is_none")]
631 pub request_body: Option<String>,
632 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub http_summary: Option<String>,
634 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
635 pub data: HashMap<String, serde_json::Value>,
636}
637
638impl RemoteProviderMetadata {
639 pub fn is_empty(&self) -> bool {
640 self.usage.is_none()
641 && self.request_body.is_none()
642 && self.http_summary.is_none()
643 && self.data.is_empty()
644 }
645}
646
647#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
648pub struct RemoteDiagnostic {
649 pub kind: String,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
651 pub code: Option<String>,
652 pub message: String,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub data: Option<serde_json::Value>,
655}