Skip to main content

luminarys_sdk/
types.rs

1//! Protocol types shared between the skill and the host.
2
3use serde::{Deserialize, Serialize};
4
5/// Protocol version.
6pub const VERSION: u32 = 1;
7
8/// Sent by the host to the skill's `skill_handle` export.
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[serde(default)]
11pub struct InvokeRequest {
12    #[serde(rename = "version", default)]
13    pub version: u32,
14    #[serde(rename = "request_id", default)]
15    pub request_id: String,
16    #[serde(rename = "trace_id", default)]
17    pub trace_id: String,
18    #[serde(rename = "session_id", default)]
19    pub session_id: String,
20    #[serde(rename = "llm_session_id", default)]
21    pub llm_session_id: String,
22    #[serde(rename = "method", default)]
23    pub method: String,
24    #[serde(rename = "skill_id", default)]
25    pub skill_id: String,
26    /// Skill ID of the caller when invoked via `call_module`; empty otherwise.
27    #[serde(rename = "caller_id", default, skip_serializing_if = "String::is_empty")]
28    pub caller_id: String,
29    #[serde(rename = "state", default, with = "crate::bytes_or_null")]
30    pub state: Vec<u8>,
31    #[serde(rename = "payload", default, with = "crate::bytes_or_null")]
32    pub payload: Vec<u8>,
33    #[serde(rename = "deadline_ns", default)]
34    pub deadline_ns: i64,
35}
36
37/// Returned by the skill's `skill_handle` export.
38#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39#[serde(default)]
40pub struct InvokeResponse {
41    #[serde(rename = "version", default)]
42    pub version: u32,
43    #[serde(rename = "state", default, with = "crate::bytes_or_null")]
44    pub state: Vec<u8>,
45    #[serde(rename = "payload", default, with = "crate::bytes_or_null")]
46    pub payload: Vec<u8>,
47    #[serde(rename = "error", default, skip_serializing_if = "String::is_empty")]
48    pub error: String,
49    #[serde(rename = "commands", default, skip_serializing_if = "Vec::is_empty", with = "crate::vec_or_null")]
50    pub commands: Vec<Command>,
51    /// Extra text appended to the MCP tool result for the LLM.
52    #[serde(rename = "llm_context", default, skip_serializing_if = "String::is_empty")]
53    pub llm_context: String,
54}
55
56/// Instruction returned by the skill.
57#[derive(Debug, Clone, Serialize, Deserialize, Default)]
58#[serde(default)]
59pub struct Command {
60    #[serde(rename = "type")]
61    pub kind: CommandType,
62    #[serde(rename = "payload", default, skip_serializing_if = "Option::is_none")]
63    pub payload: Option<rmpv::Value>,
64}
65
66/// Command types.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
68#[serde(rename_all = "snake_case")]
69pub enum CommandType {
70    #[default]
71    CallModule,
72    BatchInvoke,
73    Schedule,
74    EmitEvent,
75    Subscribe,
76    StoreKv,
77    LoadKv,
78    Spawn,
79    Terminate,
80}
81
82/// One call inside a `batch_invoke` command.
83#[derive(Debug, Clone, Serialize, Deserialize, Default)]
84#[serde(default)]
85pub struct BatchItem {
86    #[serde(rename = "index")]
87    pub index: u32,
88    #[serde(rename = "skill_id")]
89    pub skill_id: String,
90    #[serde(rename = "method")]
91    pub method: String,
92    #[serde(rename = "payload", with = "crate::bytes_or_null")]
93    pub payload: Vec<u8>,
94}
95
96/// Outcome of one [`BatchItem`] after execution.
97#[derive(Debug, Clone, Serialize, Deserialize, Default)]
98#[serde(default)]
99pub struct BatchItemResult {
100    #[serde(rename = "index")]
101    pub index: u32,
102    #[serde(rename = "payload", default, with = "crate::bytes_or_null", skip_serializing_if = "Vec::is_empty")]
103    pub payload: Vec<u8>,
104    #[serde(rename = "error", default, skip_serializing_if = "String::is_empty")]
105    pub error: String,
106}
107
108/// Delivered to the batch-callback method.
109#[derive(Debug, Clone, Serialize, Deserialize, Default)]
110#[serde(default)]
111pub struct BatchResult {
112    #[serde(rename = "batch_id")]
113    pub batch_id: String,
114    #[serde(rename = "items", with = "crate::vec_or_null")]
115    pub items: Vec<BatchItemResult>,
116}
117
118/// One entry from the dialogue history.
119#[derive(Debug, Clone, Serialize, Deserialize, Default)]
120#[serde(default)]
121pub struct HistoryMessage {
122    #[serde(rename = "role")]
123    pub role: String,
124    #[serde(rename = "content")]
125    pub content: String,
126}
127
128/// Prompt request for `prompt_complete`.
129#[derive(Debug, Clone, Serialize, Deserialize, Default)]
130#[serde(default)]
131pub struct PromptRequest {
132    #[serde(rename = "prompt")]
133    pub prompt: String,
134    #[serde(rename = "temperature")]
135    pub temperature: f64,
136    #[serde(rename = "max_tokens")]
137    pub max_tokens: u32,
138}
139
140/// Response from `prompt_complete`.
141#[derive(Debug, Clone, Serialize, Deserialize, Default)]
142#[serde(default)]
143pub struct PromptResponse {
144    #[serde(rename = "content")]
145    pub content: String,
146    #[serde(rename = "error", default, skip_serializing_if = "String::is_empty")]
147    pub error: String,
148}
149
150/// Generic FS request.
151#[derive(Debug, Clone, Serialize, Deserialize, Default)]
152#[serde(default)]
153pub struct FsRequest {
154    #[serde(rename = "path")]
155    pub path: String,
156    #[serde(rename = "content", default, with = "crate::bytes_or_null", skip_serializing_if = "Vec::is_empty")]
157    pub content: Vec<u8>,
158}
159
160/// Generic FS response.
161#[derive(Debug, Clone, Serialize, Deserialize, Default)]
162#[serde(default)]
163pub struct FsResponse {
164    #[serde(rename = "content", default, with = "crate::bytes_or_null", skip_serializing_if = "Vec::is_empty")]
165    pub content: Vec<u8>,
166    #[serde(rename = "error", default, skip_serializing_if = "String::is_empty")]
167    pub error: String,
168}
169
170/// Request for `fs_mkdir`.
171#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172#[serde(default)]
173pub struct FsMkdirRequest {
174    #[serde(rename = "path")]
175    pub path: String,
176}
177
178/// Request for `fs_ls`.
179#[derive(Debug, Clone, Serialize, Deserialize, Default)]
180#[serde(default)]
181pub struct FsLsRequest {
182    #[serde(rename = "path")]
183    pub path: String,
184    /// When `true`, populates `mod_time`, `mode`, and `mode_str`.
185    #[serde(rename = "long")]
186    pub long: bool,
187}
188
189/// One entry returned by `fs_ls`.
190#[derive(Debug, Clone, Serialize, Deserialize, Default)]
191#[serde(default)]
192pub struct DirEntry {
193    #[serde(rename = "name")]
194    pub name: String,
195    #[serde(rename = "size")]
196    pub size: i64,
197    #[serde(rename = "is_dir")]
198    pub is_dir: bool,
199    #[serde(rename = "mod_time", default, skip_serializing_if = "is_zero_i64")]
200    pub mod_time: i64,
201    #[serde(rename = "mode", default, skip_serializing_if = "is_zero_u32")]
202    pub mode: u32,
203    #[serde(rename = "mode_str", default, skip_serializing_if = "String::is_empty")]
204    pub mode_str: String,
205}
206
207/// Response from `fs_ls`.
208#[derive(Debug, Clone, Serialize, Deserialize, Default)]
209#[serde(default)]
210pub struct FsLsResponse {
211    #[serde(rename = "entries", with = "crate::vec_or_null")]
212    pub entries: Vec<DirEntry>,
213    #[serde(rename = "error", default, skip_serializing_if = "String::is_empty")]
214    pub error: String,
215}
216
217/// Request for `fs_chmod`.
218#[derive(Debug, Clone, Serialize, Deserialize, Default)]
219#[serde(default)]
220pub struct FsChmodRequest {
221    #[serde(rename = "path")]
222    pub path: String,
223    #[serde(rename = "mode")]
224    pub mode: u32,
225    #[serde(rename = "recursive")]
226    pub recursive: bool,
227}
228
229/// Unix permission bit constants.
230pub mod perm {
231    pub const OWNER_READ: u32 = 0o400;
232    pub const OWNER_WRITE: u32 = 0o200;
233    pub const OWNER_EXEC: u32 = 0o100;
234    pub const GROUP_READ: u32 = 0o040;
235    pub const GROUP_WRITE: u32 = 0o020;
236    pub const GROUP_EXEC: u32 = 0o010;
237    pub const OTHER_READ: u32 = 0o004;
238    pub const OTHER_WRITE: u32 = 0o002;
239    pub const OTHER_EXEC: u32 = 0o001;
240
241    pub const READ_ONLY: u32 = 0o444;
242    pub const DEFAULT: u32 = 0o644;
243    pub const DEFAULT_DIR: u32 = 0o755;
244    pub const PRIVATE: u32 = 0o600;
245    pub const PRIVATE_DIR: u32 = 0o700;
246}
247
248/// Request for `fs_read_lines`.
249#[derive(Debug, Clone, Serialize, Deserialize, Default)]
250#[serde(default)]
251pub struct FsReadLinesRequest {
252    #[serde(rename = "path")]
253    pub path: String,
254    #[serde(rename = "offset", default)]
255    pub offset: i64,
256    #[serde(rename = "limit", default)]
257    pub limit: i64,
258}
259
260/// Response from `fs_read_lines`.
261#[derive(Debug, Clone, Serialize, Deserialize, Default)]
262#[serde(default)]
263pub struct TextFileContent {
264    #[serde(rename = "lines", with = "crate::vec_or_null")]
265    pub lines: Vec<String>,
266    #[serde(rename = "total_lines")]
267    pub total_lines: i64,
268    #[serde(rename = "offset")]
269    pub offset: i64,
270    #[serde(rename = "is_truncated")]
271    pub is_truncated: bool,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, Default)]
275#[serde(default)]
276pub(crate) struct TextFileContentResponse {
277    #[serde(rename = "lines", with = "crate::vec_or_null")]
278    pub lines: Vec<String>,
279    #[serde(rename = "total_lines")]
280    pub total_lines: i64,
281    #[serde(rename = "offset")]
282    pub offset: i64,
283    #[serde(rename = "is_truncated")]
284    pub is_truncated: bool,
285    #[serde(rename = "error", default)]
286    pub error: String,
287}
288
289/// Options for `fs_grep`.
290#[derive(Debug, Clone, Default, Serialize, Deserialize)]
291#[serde(default)]
292pub struct GrepOptions {
293    #[serde(rename = "pattern")]
294    pub pattern: String,
295    #[serde(rename = "path", default, skip_serializing_if = "String::is_empty")]
296    pub path: String,
297    #[serde(rename = "fixed", default, skip_serializing_if = "is_false")]
298    pub fixed: bool,
299    #[serde(rename = "case_insensitive", default, skip_serializing_if = "is_false")]
300    pub case_insensitive: bool,
301    #[serde(rename = "max_depth", default, skip_serializing_if = "is_zero_i64")]
302    pub max_depth: i64,
303    #[serde(rename = "max_count", default, skip_serializing_if = "is_zero_i64")]
304    pub max_count: i64,
305    #[serde(rename = "workers", default, skip_serializing_if = "is_zero_i64")]
306    pub workers: i64,
307    #[serde(rename = "type_filter", default, skip_serializing_if = "String::is_empty")]
308    pub type_filter: String,
309    #[serde(rename = "include", default, skip_serializing_if = "Vec::is_empty", with = "crate::vec_or_null")]
310    pub include: Vec<String>,
311    #[serde(rename = "exclude", default, skip_serializing_if = "Vec::is_empty", with = "crate::vec_or_null")]
312    pub exclude: Vec<String>,
313    #[serde(rename = "ignore_dirs", default, skip_serializing_if = "Option::is_none")]
314    pub ignore_dirs: Option<Vec<String>>,
315    #[serde(rename = "with_lines", default, skip_serializing_if = "is_false")]
316    pub with_lines: bool,
317    #[serde(rename = "filename_only", default, skip_serializing_if = "is_false")]
318    pub filename_only: bool,
319}
320
321/// All matches found in one file.
322#[derive(Debug, Clone, Serialize, Deserialize, Default)]
323#[serde(default)]
324pub struct GrepFileMatch {
325    #[serde(rename = "path")]
326    pub path: String,
327    #[serde(rename = "matches", with = "crate::vec_or_null")]
328    pub matches: Vec<GrepLineMatch>,
329}
330
331/// One matching line inside a file.
332#[derive(Debug, Clone, Serialize, Deserialize, Default)]
333#[serde(default)]
334pub struct GrepLineMatch {
335    /// 1-based line number.
336    #[serde(rename = "line_num")]
337    pub line_num: i64,
338    #[serde(rename = "line", default, skip_serializing_if = "String::is_empty")]
339    pub line: String,
340    #[serde(rename = "ranges", default, skip_serializing_if = "Vec::is_empty", with = "crate::vec_or_null")]
341    pub ranges: Vec<GrepRange>,
342}
343
344/// `[start, end)` byte-offset pair within a line.
345#[derive(Debug, Clone, Serialize, Deserialize, Default)]
346#[serde(default)]
347pub struct GrepRange {
348    #[serde(rename = "start")]
349    pub start: i64,
350    #[serde(rename = "end")]
351    pub end: i64,
352}
353
354/// Options for `fs_glob`.
355#[derive(Debug, Clone, Default, Serialize, Deserialize)]
356#[serde(default)]
357pub struct GlobOptions {
358    #[serde(rename = "patterns", with = "crate::vec_or_null")]
359    pub patterns: Vec<String>,
360    #[serde(rename = "path", default, skip_serializing_if = "String::is_empty")]
361    pub path: String,
362    #[serde(rename = "match_hidden", default, skip_serializing_if = "is_false")]
363    pub match_hidden: bool,
364    #[serde(rename = "ignore_dirs", default, skip_serializing_if = "Option::is_none")]
365    pub ignore_dirs: Option<Vec<String>>,
366    #[serde(rename = "max_depth", default, skip_serializing_if = "is_zero_i64")]
367    pub max_depth: i64,
368    #[serde(rename = "only_files", default, skip_serializing_if = "is_false")]
369    pub only_files: bool,
370    #[serde(rename = "only_dirs", default, skip_serializing_if = "is_false")]
371    pub only_dirs: bool,
372}
373
374/// One result from `fs_glob`.
375#[derive(Debug, Clone, Serialize, Deserialize, Default)]
376#[serde(default)]
377pub struct GlobEntry {
378    #[serde(rename = "path")]
379    pub path: String,
380    #[serde(rename = "is_dir")]
381    pub is_dir: bool,
382}
383
384/// Options for `tcp_request`.
385#[derive(Debug, Clone, Serialize, Deserialize, Default)]
386pub struct TcpRequestOptions {
387    #[serde(rename = "addr")]
388    pub addr: String,
389    #[serde(rename = "data", default, with = "crate::bytes_or_null")]
390    pub data: Vec<u8>,
391    #[serde(rename = "tls", default)]
392    pub tls: bool,
393    #[serde(rename = "insecure", default)]
394    pub insecure: bool,
395    #[serde(rename = "timeout_ms", default)]
396    pub timeout_ms: i64,
397    #[serde(rename = "max_bytes", default)]
398    pub max_bytes: i64,
399}
400
401/// Result from `tcp_request`.
402#[derive(Debug, Clone, Serialize, Deserialize, Default)]
403#[serde(default)]
404pub struct TcpRequestResult {
405    #[serde(rename = "data", default, with = "crate::bytes_or_null")]
406    pub data: Vec<u8>,
407    #[serde(rename = "error", default)]
408    pub error: String,
409}
410
411/// Request for `shell_exec`.
412#[derive(Debug, Clone, Serialize, Deserialize, Default)]
413pub struct ShellExecRequest {
414    /// The shell command to execute (required).
415    #[serde(rename = "command")]
416    pub command: String,
417    /// Working directory (optional).
418    #[serde(rename = "workdir", default)]
419    pub workdir: String,
420    /// Timeout in milliseconds (0 = 30s default).
421    #[serde(rename = "timeout_ms", default)]
422    pub timeout_ms: i64,
423    /// Return only the last N lines of output. 0 = all lines.
424    #[serde(rename = "tail", default)]
425    pub tail: i64,
426    /// Filter output lines by regex pattern.
427    #[serde(rename = "grep", default)]
428    pub grep: String,
429    /// Start the process in background and return immediately with PID.
430    #[serde(rename = "as_daemon", default, skip_serializing_if = "is_false")]
431    pub as_daemon: bool,
432    /// Output file for daemon mode.
433    #[serde(rename = "log_file", default, skip_serializing_if = "String::is_empty")]
434    pub log_file: String,
435}
436
437/// Result from `shell_exec`.
438#[derive(Debug, Clone, Serialize, Deserialize, Default)]
439pub struct ShellExecResult {
440    /// Combined stdout+stderr output.
441    #[serde(rename = "output", default)]
442    pub output: String,
443    /// Process exit code (0 = success).
444    #[serde(rename = "exit_code", default)]
445    pub exit_code: i64,
446    /// Process ID (only set when as_daemon=true).
447    #[serde(rename = "pid", default)]
448    pub pid: i64,
449    /// Log file path (only set when as_daemon=true).
450    #[serde(rename = "log_file", default, skip_serializing_if = "String::is_empty")]
451    pub log_file: String,
452    #[serde(rename = "error", default)]
453    pub error: String,
454}
455
456/// Host OS and hardware information.
457#[derive(Debug, Clone, Serialize, Deserialize, Default)]
458#[serde(default)]
459pub struct SysInfoResult {
460    pub os: String,
461    pub arch: String,
462    pub hostname: String,
463    pub num_cpu: i64,
464}
465
466/// Current host time.
467#[derive(Debug, Clone, Serialize, Deserialize, Default)]
468#[serde(default)]
469pub struct TimeNowResult {
470    pub unix: i64,
471    pub unix_nano: i64,
472    pub rfc3339: String,
473    pub timezone: String,
474    pub utc_offset: i64,
475}
476
477/// Disk usage information.
478#[derive(Debug, Clone, Serialize, Deserialize, Default)]
479#[serde(default)]
480pub struct DiskUsageResult {
481    pub total_bytes: i64,
482    pub free_bytes: i64,
483    pub used_bytes: i64,
484    pub used_pct: f64,
485    #[serde(default)]
486    pub error: String,
487}
488
489/// Allowed directory entry.
490#[derive(Debug, Clone, Serialize, Deserialize, Default)]
491#[serde(default)]
492pub struct AllowedDir {
493    pub path: String,
494    pub mode: String,
495}
496
497/// Error returned from a host call.
498#[derive(Debug, Clone)]
499pub struct SkillError(pub String);
500
501impl std::fmt::Display for SkillError {
502    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503        write!(f, "abi error: {}", self.0)
504    }
505}
506
507impl std::error::Error for SkillError {}
508
509impl From<rmp_serde::encode::Error> for SkillError {
510    fn from(e: rmp_serde::encode::Error) -> Self {
511        SkillError(format!("msgpack encode: {e}"))
512    }
513}
514
515impl From<rmp_serde::decode::Error> for SkillError {
516    fn from(e: rmp_serde::decode::Error) -> Self {
517        SkillError(format!("msgpack decode: {e}"))
518    }
519}
520
521#[inline]
522fn is_false(v: &bool) -> bool { !v }
523
524#[inline]
525fn is_zero_i64(v: &i64) -> bool { *v == 0 }
526
527#[inline]
528fn is_zero_u32(v: &u32) -> bool { *v == 0 }