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