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