Skip to main content

lash_remote_protocol/protocol/
llm.rs

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}