1#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2pub struct RemoteSchemaContract {
3 pub canonical: serde_json::Value,
4 #[serde(
5 default,
6 skip_serializing_if = "RemoteSchemaProjectionPolicy::is_default"
7 )]
8 pub projection: RemoteSchemaProjectionPolicy,
9}
10
11impl RemoteSchemaContract {
12 fn new(canonical: serde_json::Value) -> Self {
13 Self {
14 canonical,
15 projection: RemoteSchemaProjectionPolicy::default(),
16 }
17 }
18}
19
20impl Default for RemoteSchemaContract {
21 fn default() -> Self {
22 Self::new(serde_json::Value::Null)
23 }
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
27pub struct RemoteSchemaProjectionPolicy {
28 #[serde(default, skip_serializing_if = "RemoteProjectionMode::is_auto")]
29 pub mode: RemoteProjectionMode,
30 #[serde(default, skip_serializing_if = "Vec::is_empty")]
31 pub overrides: Vec<RemoteSchemaProjectionOverride>,
32}
33
34impl RemoteSchemaProjectionPolicy {
35 fn is_default(&self) -> bool {
36 self.mode == RemoteProjectionMode::Auto && self.overrides.is_empty()
37 }
38}
39
40impl Default for RemoteSchemaProjectionPolicy {
41 fn default() -> Self {
42 Self {
43 mode: RemoteProjectionMode::Auto,
44 overrides: Vec::new(),
45 }
46 }
47}
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
50#[serde(rename_all = "snake_case")]
51pub enum RemoteProjectionMode {
52 #[default]
53 Auto,
54 ExplicitOnly,
55 Exact,
56}
57
58impl RemoteProjectionMode {
59 fn is_auto(&self) -> bool {
60 *self == Self::Auto
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
65pub struct RemoteSchemaProjectionOverride {
66 pub dialect: String,
67 pub schema: serde_json::Value,
68}
69
70fn default_remote_input_schema() -> RemoteSchemaContract {
71 RemoteSchemaContract::new(serde_json::json!({
72 "type": "object",
73 "properties": {},
74 "additionalProperties": true
75 }))
76}
77
78#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
79pub struct RemoteLlmRequest {
80 pub protocol_version: u32,
81 pub request_id: String,
82 pub scope: RemoteLlmRequestScope,
83 pub model_intent: RemoteModelIntent,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub messages: Vec<RemoteLlmMessage>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
87 pub attachments: Vec<RemoteLlmAttachment>,
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub tools: Vec<RemoteLlmToolSpec>,
90 #[serde(default)]
91 pub tool_choice: RemoteLlmToolChoice,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub output_spec: Option<RemoteLlmOutputSpec>,
94 #[serde(default, skip_serializing_if = "RemoteGenerationOptions::is_empty")]
95 pub generation: RemoteGenerationOptions,
96 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
97 pub metadata: HashMap<String, serde_json::Value>,
98}
99
100impl RemoteLlmRequest {
101 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
102 ensure_protocol_version(self.protocol_version)?;
103 require_non_empty("RemoteLlmRequest", "request_id", &self.request_id)?;
104 self.scope.validate()?;
105 self.model_intent.validate()?;
106 self.generation.validate("RemoteLlmRequest")?;
107 for (index, message) in self.messages.iter().enumerate() {
108 message.validate(index)?;
109 }
110 for (index, attachment) in self.attachments.iter().enumerate() {
111 attachment.validate(index)?;
112 }
113 for tool in &self.tools {
114 tool.validate()?;
115 }
116 if let Some(output_spec) = &self.output_spec {
117 output_spec.validate()?;
118 }
119 Ok(())
120 }
121}
122
123#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
124pub struct RemoteLlmResponse {
125 pub protocol_version: u32,
126 pub request_id: String,
127 #[serde(default)]
128 pub full_text: String,
129 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub output_parts: Vec<RemoteLlmOutputPart>,
131 #[serde(default)]
132 pub usage: RemoteUsage,
133 #[serde(default)]
134 pub terminal_reason: RemoteLlmTerminalReason,
135 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub diagnostics: Vec<RemoteDiagnostic>,
137 #[serde(default, skip_serializing_if = "RemoteProviderMetadata::is_empty")]
138 pub provider_metadata: RemoteProviderMetadata,
139}
140
141impl RemoteLlmResponse {
142 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
143 ensure_protocol_version(self.protocol_version)?;
144 require_non_empty("RemoteLlmResponse", "request_id", &self.request_id)?;
145 Ok(())
146 }
147}
148
149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
150pub struct RemoteModelIntent {
151 pub model: String,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub variant: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub provider: Option<String>,
156 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
157 pub metadata: HashMap<String, String>,
158}
159
160impl RemoteModelIntent {
161 pub fn new(model: impl Into<String>) -> Self {
162 Self {
163 model: model.into(),
164 variant: None,
165 provider: None,
166 metadata: HashMap::new(),
167 }
168 }
169
170 fn validate(&self) -> Result<(), RemoteProtocolError> {
171 require_non_empty("RemoteModelIntent", "model", &self.model)
172 }
173}
174
175#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
176pub struct RemoteGenerationOptions {
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub output_token_cap: Option<u64>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub temperature: Option<String>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub top_p: Option<String>,
183 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub stop: Vec<String>,
185 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
186 pub provider_options: HashMap<String, String>,
187}
188
189impl RemoteGenerationOptions {
190 pub fn is_empty(&self) -> bool {
191 self.output_token_cap.is_none()
192 && self.temperature.is_none()
193 && self.top_p.is_none()
194 && self.stop.is_empty()
195 && self.provider_options.is_empty()
196 }
197
198 fn validate(&self, type_name: &'static str) -> Result<(), RemoteProtocolError> {
199 if self.output_token_cap == Some(0) {
200 return Err(RemoteProtocolError::InvalidEnvelope {
201 type_name,
202 message: "generation.output_token_cap must be greater than zero".to_string(),
203 });
204 }
205 Ok(())
206 }
207}
208
209#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
210pub struct RemoteLlmRequestScope {
211 pub session_id: String,
212 pub agent_frame_id: String,
213 pub request_id: String,
214}
215
216impl RemoteLlmRequestScope {
217 pub fn new(
218 session_id: impl Into<String>,
219 agent_frame_id: impl Into<String>,
220 request_id: impl Into<String>,
221 ) -> Self {
222 Self {
223 session_id: session_id.into(),
224 agent_frame_id: agent_frame_id.into(),
225 request_id: request_id.into(),
226 }
227 }
228
229 fn validate(&self) -> Result<(), RemoteProtocolError> {
230 require_non_empty("RemoteLlmRequestScope", "session_id", &self.session_id)?;
231 require_non_empty(
232 "RemoteLlmRequestScope",
233 "agent_frame_id",
234 &self.agent_frame_id,
235 )?;
236 require_non_empty("RemoteLlmRequestScope", "request_id", &self.request_id)?;
237 Ok(())
238 }
239}
240
241#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
242#[serde(rename_all = "snake_case")]
243pub enum RemoteLlmRole {
244 #[default]
245 User,
246 Assistant,
247 System,
248}
249
250#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
251pub struct RemoteLlmMessage {
252 pub role: RemoteLlmRole,
253 #[serde(default, skip_serializing_if = "Vec::is_empty")]
254 pub content: Vec<RemoteLlmContentBlock>,
255}
256
257impl RemoteLlmMessage {
258 fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
259 if self.content.is_empty() {
260 return Err(RemoteProtocolError::InvalidEnvelope {
261 type_name: "RemoteLlmMessage",
262 message: format!("message at index {index} must contain at least one block"),
263 });
264 }
265 for block in &self.content {
266 block.validate()?;
267 }
268 Ok(())
269 }
270}
271
272#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
273#[serde(tag = "type", rename_all = "snake_case")]
274pub enum RemoteLlmContentBlock {
275 Text {
276 text: String,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 response_meta: Option<RemoteResponseTextMeta>,
279 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
280 cache_breakpoint: bool,
281 },
282 ImageAttachment {
283 attachment_index: usize,
284 },
285 ToolCall {
286 call_id: String,
287 tool_name: String,
288 input_json: String,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 replay: Option<RemoteProviderReplayMeta>,
291 },
292 ToolResult {
293 call_id: String,
294 content: String,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 tool_name: Option<String>,
297 },
298 Reasoning {
299 text: String,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
301 replay: Option<RemoteProviderReasoningReplay>,
302 },
303}
304
305impl RemoteLlmContentBlock {
306 fn validate(&self) -> Result<(), RemoteProtocolError> {
307 match self {
308 Self::ToolCall {
309 call_id, tool_name, ..
310 } => {
311 require_non_empty("RemoteLlmContentBlock::ToolCall", "call_id", call_id)?;
312 require_non_empty("RemoteLlmContentBlock::ToolCall", "tool_name", tool_name)
313 }
314 Self::ToolResult { call_id, .. } => {
315 require_non_empty("RemoteLlmContentBlock::ToolResult", "call_id", call_id)
316 }
317 Self::Text { .. } | Self::ImageAttachment { .. } | Self::Reasoning { .. } => Ok(()),
318 }
319 }
320}
321
322#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
323pub struct RemoteResponseTextMeta {
324 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub id: Option<String>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub status: Option<String>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub phase: Option<String>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub provider_payload: Option<String>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub origin_provider: Option<String>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub origin_model: Option<String>,
336}
337
338#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
339pub struct RemoteProviderReplayMeta {
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub item_id: Option<String>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub opaque: Option<String>,
344}
345
346#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
347pub struct RemoteProviderReasoningReplay {
348 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub item_id: Option<String>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub encrypted_content: Option<String>,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub signature: Option<String>,
354 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
355 pub redacted: bool,
356 #[serde(default, skip_serializing_if = "Vec::is_empty")]
357 pub summary: Vec<String>,
358}
359
360#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
361pub struct RemoteLlmAttachment {
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub id: Option<String>,
364 pub mime: String,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub data_base64: Option<String>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub reference: Option<RemoteAttachmentRef>,
369 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
370 pub metadata: HashMap<String, String>,
371}
372
373impl RemoteLlmAttachment {
374 fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
375 if self.mime.trim().is_empty() {
376 return Err(RemoteProtocolError::InvalidEnvelope {
377 type_name: "RemoteLlmAttachment",
378 message: format!("attachment at index {index} requires a non-empty mime"),
379 });
380 }
381 if let Some(reference) = &self.reference {
382 reference.validate()?;
383 }
384 Ok(())
385 }
386}
387
388#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
389pub struct RemoteAttachmentRef {
390 pub id: String,
391 pub mime: String,
392 pub byte_len: u64,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub width: Option<u32>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub height: Option<u32>,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub label: Option<String>,
399 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
400 pub metadata: HashMap<String, String>,
401}
402
403impl RemoteAttachmentRef {
404 fn validate(&self) -> Result<(), RemoteProtocolError> {
405 require_non_empty("RemoteAttachmentRef", "id", &self.id)?;
406 require_non_empty("RemoteAttachmentRef", "mime", &self.mime)
407 }
408}
409
410#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
411pub struct RemoteLlmToolSpec {
412 pub name: String,
413 #[serde(default)]
414 pub description: String,
415 #[serde(default = "default_remote_input_schema")]
416 pub input_schema: RemoteSchemaContract,
417 #[serde(default)]
418 pub output_schema: RemoteSchemaContract,
419}
420
421impl RemoteLlmToolSpec {
422 fn validate(&self) -> Result<(), RemoteProtocolError> {
423 require_non_empty("RemoteLlmToolSpec", "name", &self.name)
424 }
425}
426
427#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
428#[serde(rename_all = "snake_case")]
429pub enum RemoteLlmToolChoice {
430 #[default]
431 Auto,
432 None,
433 Required,
434}
435
436#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
437#[serde(tag = "type", rename_all = "snake_case")]
438pub enum RemoteLlmOutputSpec {
439 JsonObject,
440 JsonSchema {
441 name: String,
442 schema: RemoteSchemaContract,
443 strict: bool,
444 },
445}
446
447impl RemoteLlmOutputSpec {
448 fn validate(&self) -> Result<(), RemoteProtocolError> {
449 match self {
450 Self::JsonObject => Ok(()),
451 Self::JsonSchema { name, .. } => {
452 require_non_empty("RemoteLlmOutputSpec::JsonSchema", "name", name)
453 }
454 }
455 }
456}
457
458#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
459#[serde(tag = "type", rename_all = "snake_case")]
460pub enum RemoteLlmOutputPart {
461 Text {
462 text: String,
463 #[serde(default, skip_serializing_if = "Option::is_none")]
464 response_meta: Option<RemoteResponseTextMeta>,
465 },
466 Reasoning {
467 text: String,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
469 replay: Option<RemoteProviderReasoningReplay>,
470 },
471 ToolCall {
472 call_id: String,
473 tool_name: String,
474 input_json: String,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
476 replay: Option<RemoteProviderReplayMeta>,
477 },
478}
479
480#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
481#[serde(rename_all = "snake_case")]
482pub enum RemoteLlmTerminalReason {
483 Stop,
484 ToolUse,
485 OutputLimit,
486 ContextOverflow,
487 ContentFilter,
488 ProviderError,
489 Cancelled,
490 #[default]
491 Unknown,
492}
493
494#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
495pub struct RemoteProviderMetadata {
496 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub usage: Option<serde_json::Value>,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
499 pub request_body: Option<String>,
500 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub http_summary: Option<String>,
502 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
503 pub data: HashMap<String, serde_json::Value>,
504}
505
506impl RemoteProviderMetadata {
507 pub fn is_empty(&self) -> bool {
508 self.usage.is_none()
509 && self.request_body.is_none()
510 && self.http_summary.is_none()
511 && self.data.is_empty()
512 }
513}
514
515#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
516pub struct RemoteDiagnostic {
517 pub kind: String,
518 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub code: Option<String>,
520 pub message: String,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub data: Option<serde_json::Value>,
523}