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}
200
201impl RemoteModelCapability {
202 pub fn is_empty(&self) -> bool {
203 self.reasoning.is_none()
204 }
205}
206
207#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
209pub struct RemoteReasoningCapability {
210 #[serde(default)]
211 pub efforts: Vec<String>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub default_effort: Option<String>,
214 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
215 pub aliases: BTreeMap<String, String>,
216 #[serde(default)]
217 pub encoding: RemoteReasoningEncoding,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub disable: Option<RemoteReasoningDisableEncoding>,
220 #[serde(default)]
221 pub mandatory: bool,
222}
223
224#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
225#[serde(rename_all = "snake_case")]
226pub enum RemoteReasoningSelection {
227 #[default]
228 ProviderDefault,
229 Disabled,
230 Effort(String),
231}
232
233#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
234#[serde(rename_all = "snake_case")]
235pub enum RemoteReasoningDisableEncoding {
236 Native,
237 Omit,
238 Effort(String),
239 Budget(u32),
240 ToggleFalse,
241}
242
243#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
245#[serde(rename_all = "snake_case")]
246pub enum RemoteReasoningEncoding {
247 #[default]
248 Effort,
249 Budget(BTreeMap<String, u32>),
250}
251
252impl RemoteModelIntent {
253 pub fn new(model: impl Into<String>) -> Self {
254 Self {
255 model: model.into(),
256 variant: RemoteReasoningSelection::ProviderDefault,
257 capability: RemoteModelCapability::default(),
258 provider: None,
259 metadata: HashMap::new(),
260 }
261 }
262
263 pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
264 require_non_empty("RemoteModelIntent", "model", &self.model)
265 }
266}
267
268#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
269pub struct RemoteGenerationOptions {
270 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub output_token_cap: Option<u64>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub temperature: Option<String>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub top_p: Option<String>,
276 #[serde(default, skip_serializing_if = "Vec::is_empty")]
277 pub stop: Vec<String>,
278 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
279 pub provider_options: HashMap<String, String>,
280}
281
282impl RemoteGenerationOptions {
283 pub fn is_empty(&self) -> bool {
284 self.output_token_cap.is_none()
285 && self.temperature.is_none()
286 && self.top_p.is_none()
287 && self.stop.is_empty()
288 && self.provider_options.is_empty()
289 }
290
291 pub(crate) fn validate(&self, type_name: &'static str) -> Result<(), RemoteProtocolError> {
292 if self.output_token_cap == Some(0) {
293 return Err(RemoteProtocolError::InvalidEnvelope {
294 type_name,
295 message: "generation.output_token_cap must be greater than zero".to_string(),
296 });
297 }
298 Ok(())
299 }
300}
301
302#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
303pub struct RemoteLlmRequestScope {
304 pub session_id: String,
305 pub agent_frame_id: String,
306 pub request_id: String,
307}
308
309impl RemoteLlmRequestScope {
310 pub fn new(
311 session_id: impl Into<String>,
312 agent_frame_id: impl Into<String>,
313 request_id: impl Into<String>,
314 ) -> Self {
315 Self {
316 session_id: session_id.into(),
317 agent_frame_id: agent_frame_id.into(),
318 request_id: request_id.into(),
319 }
320 }
321
322 fn validate(&self) -> Result<(), RemoteProtocolError> {
323 require_non_empty("RemoteLlmRequestScope", "session_id", &self.session_id)?;
324 require_non_empty(
325 "RemoteLlmRequestScope",
326 "agent_frame_id",
327 &self.agent_frame_id,
328 )?;
329 require_non_empty("RemoteLlmRequestScope", "request_id", &self.request_id)?;
330 Ok(())
331 }
332}
333
334#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
335#[serde(rename_all = "snake_case")]
336pub enum RemoteLlmRole {
337 #[default]
338 User,
339 Assistant,
340 System,
341}
342
343#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
344pub struct RemoteLlmMessage {
345 pub role: RemoteLlmRole,
346 #[serde(default, skip_serializing_if = "Vec::is_empty")]
347 pub content: Vec<RemoteLlmContentBlock>,
348}
349
350impl RemoteLlmMessage {
351 fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
352 if self.content.is_empty() {
353 return Err(RemoteProtocolError::InvalidEnvelope {
354 type_name: "RemoteLlmMessage",
355 message: format!("message at index {index} must contain at least one block"),
356 });
357 }
358 for block in &self.content {
359 block.validate()?;
360 }
361 Ok(())
362 }
363}
364
365#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
366#[serde(tag = "type", rename_all = "snake_case")]
367pub enum RemoteLlmContentBlock {
368 Text {
369 text: String,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 response_meta: Option<RemoteResponseTextMeta>,
372 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
373 cache_breakpoint: bool,
374 },
375 ImageAttachment {
376 attachment_index: usize,
377 },
378 ToolCall {
379 call_id: String,
380 tool_name: String,
381 input_json: String,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 replay: Option<RemoteProviderReplayMeta>,
384 },
385 ToolResult {
386 call_id: String,
387 content: String,
388 #[serde(default, skip_serializing_if = "Option::is_none")]
389 tool_name: Option<String>,
390 },
391 Reasoning {
392 text: String,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 replay: Option<RemoteProviderReasoningReplay>,
395 },
396}
397
398impl RemoteLlmContentBlock {
399 fn validate(&self) -> Result<(), RemoteProtocolError> {
400 match self {
401 Self::ToolCall {
402 call_id, tool_name, ..
403 } => {
404 require_non_empty("RemoteLlmContentBlock::ToolCall", "call_id", call_id)?;
405 require_non_empty("RemoteLlmContentBlock::ToolCall", "tool_name", tool_name)
406 }
407 Self::ToolResult { call_id, .. } => {
408 require_non_empty("RemoteLlmContentBlock::ToolResult", "call_id", call_id)
409 }
410 Self::Text { .. } | Self::ImageAttachment { .. } | Self::Reasoning { .. } => Ok(()),
411 }
412 }
413}
414
415#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
416pub struct RemoteResponseTextMeta {
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub id: Option<String>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub status: Option<String>,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub phase: Option<String>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub provider_payload: Option<String>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub origin_provider: Option<String>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub origin_model: Option<String>,
429}
430
431#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
432pub struct RemoteProviderReplayMeta {
433 #[serde(default, skip_serializing_if = "Option::is_none")]
434 pub item_id: Option<String>,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
436 pub opaque: Option<String>,
437}
438
439#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
440pub struct RemoteProviderReasoningReplay {
441 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub item_id: Option<String>,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub encrypted_content: Option<String>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub signature: Option<String>,
447 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
448 pub redacted: bool,
449 #[serde(default, skip_serializing_if = "Vec::is_empty")]
450 pub summary: Vec<String>,
451}
452
453#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
454pub struct RemoteLlmAttachment {
455 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub id: Option<String>,
457 pub mime: String,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub data_base64: Option<String>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub reference: Option<RemoteAttachmentRef>,
462 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
463 pub metadata: HashMap<String, String>,
464}
465
466impl RemoteLlmAttachment {
467 fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
468 if self.mime.trim().is_empty() {
469 return Err(RemoteProtocolError::InvalidEnvelope {
470 type_name: "RemoteLlmAttachment",
471 message: format!("attachment at index {index} requires a non-empty mime"),
472 });
473 }
474 if let Some(reference) = &self.reference {
475 reference.validate()?;
476 }
477 Ok(())
478 }
479}
480
481#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
482pub struct RemoteAttachmentRef {
483 pub id: String,
484 pub mime: String,
485 pub byte_len: u64,
486 #[serde(default, skip_serializing_if = "Option::is_none")]
487 pub width: Option<u32>,
488 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub height: Option<u32>,
490 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub label: Option<String>,
492 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
493 pub metadata: HashMap<String, String>,
494}
495
496impl RemoteAttachmentRef {
497 pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
498 require_non_empty("RemoteAttachmentRef", "id", &self.id)?;
499 require_non_empty("RemoteAttachmentRef", "mime", &self.mime)
500 }
501}
502
503#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
504pub struct RemoteLlmToolSpec {
505 pub name: String,
506 #[serde(default)]
507 pub description: String,
508 #[serde(default = "default_remote_input_schema")]
509 pub input_schema: RemoteSchemaContract,
510 #[serde(default)]
511 pub output_schema: RemoteSchemaContract,
512}
513
514impl RemoteLlmToolSpec {
515 pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
516 require_non_empty("RemoteLlmToolSpec", "name", &self.name)
517 }
518}
519
520#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
521#[serde(rename_all = "snake_case")]
522pub enum RemoteLlmToolChoice {
523 #[default]
524 Auto,
525 None,
526 Required,
527}
528
529#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
530#[serde(tag = "type", rename_all = "snake_case")]
531pub enum RemoteLlmOutputSpec {
532 JsonObject,
533 JsonSchema {
534 name: String,
535 schema: RemoteSchemaContract,
536 strict: bool,
537 },
538}
539
540impl RemoteLlmOutputSpec {
541 fn validate(&self) -> Result<(), RemoteProtocolError> {
542 match self {
543 Self::JsonObject => Ok(()),
544 Self::JsonSchema { name, .. } => {
545 require_non_empty("RemoteLlmOutputSpec::JsonSchema", "name", name)
546 }
547 }
548 }
549}
550
551#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
552#[serde(tag = "type", rename_all = "snake_case")]
553pub enum RemoteLlmOutputPart {
554 Text {
555 text: String,
556 #[serde(default, skip_serializing_if = "Option::is_none")]
557 response_meta: Option<RemoteResponseTextMeta>,
558 },
559 Reasoning {
560 text: String,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
562 replay: Option<RemoteProviderReasoningReplay>,
563 },
564 ToolCall {
565 call_id: String,
566 tool_name: String,
567 input_json: String,
568 #[serde(default, skip_serializing_if = "Option::is_none")]
569 replay: Option<RemoteProviderReplayMeta>,
570 },
571}
572
573#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
574#[serde(rename_all = "snake_case")]
575pub enum RemoteLlmTerminalReason {
576 Stop,
577 ToolUse,
578 OutputLimit,
579 ContextOverflow,
580 ContentFilter,
581 ProviderError,
582 Cancelled,
583 #[default]
584 Unknown,
585}
586
587#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
589#[serde(rename_all = "snake_case")]
590pub enum RemoteProviderFailureKind {
591 Transport,
592 Timeout,
593 Http,
594 Stream,
595 Auth,
596 Validation,
597 Quota,
598 Unsupported,
599 #[default]
600 #[serde(other)]
601 Unknown,
602}
603
604#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
605pub struct RemoteProviderMetadata {
606 #[serde(default, skip_serializing_if = "Option::is_none")]
607 pub usage: Option<serde_json::Value>,
608 #[serde(default, skip_serializing_if = "Option::is_none")]
609 pub request_body: Option<String>,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub http_summary: Option<String>,
612 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
613 pub data: HashMap<String, serde_json::Value>,
614}
615
616impl RemoteProviderMetadata {
617 pub fn is_empty(&self) -> bool {
618 self.usage.is_none()
619 && self.request_body.is_none()
620 && self.http_summary.is_none()
621 && self.data.is_empty()
622 }
623}
624
625#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
626pub struct RemoteDiagnostic {
627 pub kind: String,
628 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub code: Option<String>,
630 pub message: String,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub data: Option<serde_json::Value>,
633}