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