luaskills 0.4.1

LuaSkills core runtime library for loading, invoking, and managing Lua skill packages.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
use semver::Version;
use serde::Deserialize;

// ============================================================
// Lua Skill metadata (loaded from skill.yaml only)
// ============================================================

/// Skill-scoped LanceDB logging level.
/// Skill 级 LanceDB 宿主日志级别配置。
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum SkillLanceDbLogLevel {
    /// Disable host-side LanceDB logs except hard failures.
    /// 除硬错误外关闭宿主侧 LanceDB 日志。
    Off,
    /// Emit informational host-side LanceDB logs.
    /// 输出信息级宿主 LanceDB 日志。
    #[default]
    Info,
    /// Emit only warning/error host-side LanceDB logs.
    /// 仅输出告警/错误级宿主 LanceDB 日志。
    Warning,
}

impl SkillLanceDbLogLevel {
    /// Return the stable wire name of the current log level.
    /// 返回当前日志级别对应的稳定名称。
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Off => "off",
            Self::Info => "info",
            Self::Warning => "warning",
        }
    }
}

/// Skill-scoped SQLite logging level.
/// Skill 级 SQLite 宿主日志级别配置。
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum SkillSqliteLogLevel {
    /// Disable host-side SQLite logs except hard failures.
    /// 除硬错误外关闭宿主侧 SQLite 日志。
    Off,
    /// Emit informational host-side SQLite logs.
    /// 输出信息级宿主 SQLite 日志。
    #[default]
    Info,
    /// Emit only warning/error host-side SQLite logs.
    /// 仅输出告警/错误级宿主 SQLite 日志。
    Warning,
}

impl SkillSqliteLogLevel {
    /// Return the stable wire name of the current log level.
    /// 返回当前日志级别对应的稳定名称。
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Off => "off",
            Self::Info => "info",
            Self::Warning => "warning",
        }
    }
}

/// Skill-scoped LanceDB configuration object.
/// Skill 级 LanceDB 配置对象。
#[derive(Deserialize, Debug, Clone, Default)]
pub struct SkillLanceDbMeta {
    /// Whether the current skill should receive a dedicated host-managed LanceDB instance.
    /// 当前 skill 是否需要启用宿主管理的专属 LanceDB 实例。
    #[serde(default)]
    pub enable: bool,
    /// Host-side LanceDB log level.
    /// 宿主侧 LanceDB 日志级别。
    #[serde(default)]
    pub log_level: SkillLanceDbLogLevel,
    /// Whether slow-operation logging is enabled.
    /// 是否开启慢操作日志。
    #[serde(default)]
    pub slow_log_enabled: bool,
    /// Slow-operation threshold in milliseconds.
    /// 慢操作阈值(毫秒)。
    #[serde(default = "default_lancedb_slow_log_threshold_ms")]
    pub slow_log_threshold_ms: u64,
}

/// Skill-scoped SQLite configuration object.
/// Skill 级 SQLite 配置对象。
#[derive(Deserialize, Debug, Clone, Default)]
pub struct SkillSqliteMeta {
    /// Whether the current skill should receive a dedicated host-managed SQLite instance.
    /// 当前 skill 是否需要启用宿主管理的专属 SQLite 实例。
    #[serde(default)]
    pub enable: bool,
    /// Host-side SQLite log level.
    /// 宿主侧 SQLite 日志级别。
    #[serde(default)]
    pub log_level: SkillSqliteLogLevel,
    /// Whether slow-operation logging is enabled.
    /// 是否开启慢操作日志。
    #[serde(default)]
    pub slow_log_enabled: bool,
    /// Slow-operation threshold in milliseconds.
    /// 慢操作阈值(毫秒)。
    #[serde(default = "default_sqlite_slow_log_threshold_ms")]
    pub slow_log_threshold_ms: u64,
}

/// Default slow-operation threshold for host-side LanceDB logs.
/// 宿主侧 LanceDB 慢操作日志默认阈值。
fn default_lancedb_slow_log_threshold_ms() -> u64 {
    800
}

/// Default slow-operation threshold for host-side SQLite logs.
/// 宿主侧 SQLite 慢操作日志默认阈值。
fn default_sqlite_slow_log_threshold_ms() -> u64 {
    500
}

/// Help node metadata used by the new help structure.
/// 新 help 结构使用的帮助节点元数据。
#[derive(Deserialize, Debug, Clone, Default)]
pub struct SkillHelpNodeMeta {
    /// Optional node name used by topic/workflow nodes.
    /// 供 topic/workflow 节点使用的可选名称。
    #[serde(default)]
    pub name: String,
    /// Optional human-readable description of the node.
    /// 当前节点的人类可读描述。
    #[serde(default)]
    pub description: String,
    /// Relative file path of the help payload under `help/`.
    /// 位于 `help/` 目录下的帮助载荷相对路径。
    #[serde(default)]
    pub file: String,
}

/// Top-level help metadata declared by a skill.
/// skill 顶层声明的 help 元数据。
#[derive(Deserialize, Debug, Clone, Default)]
pub struct SkillHelpMeta {
    /// Main help node used for skill-level overview.
    /// 用于 skill 级总览的主帮助节点。
    #[serde(default)]
    pub main: SkillHelpNodeMeta,
    /// Topic or workflow help nodes listed under the main help.
    /// 挂在主帮助下的 topic 或 workflow 子节点。
    #[serde(default)]
    pub topics: Vec<SkillHelpNodeMeta>,
}

/// Shared parameter metadata used by tool entries.
/// 工具入口共用的参数元数据。
#[derive(Deserialize, Debug, Clone)]
pub struct SkillParam {
    /// Parameter name.
    /// 参数名称。
    pub name: String,
    /// Parameter type string used by JSON Schema.
    /// JSON Schema 使用的参数类型字符串。
    #[serde(rename = "type")]
    pub param_type: String,
    /// Parameter description.
    /// 参数描述。
    pub description: String,
    /// Whether the parameter is required.
    /// 参数是否必填。
    #[serde(default)]
    pub required: bool,
}

/// Strict top-level entry metadata used by the new LuaSkills package layout.
/// 新 LuaSkills 包结构使用的严格顶层入口元数据。
#[derive(Deserialize, Debug, Clone)]
pub struct SkillToolMeta {
    /// Local entry name used inside the skill namespace.
    /// skill 命名空间内部使用的局部入口名称。
    pub name: String,
    /// Human-readable entry description shown in tools/list.
    /// 展示在 tools/list 中的人类可读描述。
    #[serde(default)]
    pub description: String,
    /// Relative Lua entry filename under `runtime/`.
    /// 位于 `runtime/` 目录下的相对 Lua 入口文件路径。
    pub lua_entry: String,
    /// Lua module registration name.
    /// Lua 模块注册名称。
    pub lua_module: String,
    /// Parameter definitions specific to this entry.
    /// 当前入口独有的参数定义。
    #[serde(default)]
    pub parameters: Vec<SkillParam>,
    /// Optional help topic/workflow reference name.
    /// 可选的帮助 topic/workflow 引用名称。
    #[serde(default)]
    pub help: String,
}

/// Strict skill-level metadata loaded only from skill.yaml.
/// 仅从 skill.yaml 加载的严格 skill 级元数据。
#[derive(Deserialize, Debug, Clone)]
pub struct SkillMeta {
    /// Internal skill name, for example "vulcan-codekit".
    /// 内部 skill 名称,例如 "vulcan-codekit"。
    pub name: String,
    /// Semantic package version declared by the current skill manifest.
    /// 当前技能清单声明的语义化包版本。
    pub version: String,
    /// Whether the current skill is allowed to load. Defaults to enabled when omitted.
    /// 当前 skill 是否允许被加载;省略时默认启用。
    #[serde(default = "default_skill_enable_flag")]
    pub enable: bool,
    /// Debug mode: reload Lua source from disk on each invocation.
    /// 调试模式:每次调用时都从磁盘热加载 Lua 源文件。
    #[serde(default)]
    pub debug: bool,
    /// Structured LanceDB configuration used by the host-managed binding.
    /// 宿主管理的 LanceDB 绑定所使用的结构化配置对象。
    #[serde(default)]
    pub lancedb: SkillLanceDbMeta,
    /// Structured SQLite configuration used by the host-managed binding.
    /// 宿主管理的 SQLite 绑定所使用的结构化配置对象。
    #[serde(default)]
    pub sqlite: SkillSqliteMeta,
    /// Top-level entry declarations used by the strict LuaSkills package layout.
    /// 严格 LuaSkills 包结构使用的顶层入口声明。
    #[serde(default)]
    pub entries: Vec<SkillToolMeta>,
    /// New help metadata used to replace prompt-based guidance.
    /// 用于替代 prompt 说明的新 help 元数据。
    #[serde(default)]
    pub help: SkillHelpMeta,
    /// Stable runtime skill identifier derived from the physical directory name.
    /// 从物理目录名称派生出的稳定运行时技能标识符。
    #[serde(skip)]
    resolved_skill_id: String,
}

/// Return whether one LuaSkills identifier follows the strict lowercase-hyphen rule.
/// 判断某个 LuaSkills 标识符是否满足严格的小写短横线规则。
pub fn is_valid_luaskills_identifier(value: &str) -> bool {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return false;
    }

    let mut chars = trimmed.chars();
    let Some(first_char) = chars.next() else {
        return false;
    };
    if !first_char.is_ascii_lowercase() {
        return false;
    }
    if trimmed.ends_with('-') {
        return false;
    }

    chars.all(|character| {
        character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
    })
}

/// Validate one LuaSkills identifier and return a bilingual error when it is invalid.
/// 校验单个 LuaSkills 标识符,并在非法时返回双语错误文本。
pub fn validate_luaskills_identifier(value: &str, label: &str) -> Result<(), String> {
    if is_valid_luaskills_identifier(value) {
        return Ok(());
    }

    Err(format!("{label} must match ^[a-z]([a-z0-9-]*[a-z0-9])?$"))
}

/// Validate one LuaSkills semantic version string and return an English error when it is invalid.
/// 校验单个 LuaSkills 语义化版本字符串,并在非法时返回英文错误。
pub fn validate_luaskills_version(value: &str, label: &str) -> Result<(), String> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return Err(format!("{label} must not be empty"));
    }
    Version::parse(trimmed)
        .map(|_| ())
        .map_err(|error| format!("{label} must be a valid semantic version: {}", error))
}

/// Return the default enabled flag for one skill manifest.
/// 返回单个技能清单的默认启用标记。
fn default_skill_enable_flag() -> bool {
    true
}

impl SkillMeta {
    /// Return the effective LanceDB configuration.
    /// 返回生效的 LanceDB 配置。
    pub fn effective_lancedb(&self) -> SkillLanceDbMeta {
        self.lancedb.clone()
    }

    /// Return the effective SQLite configuration.
    /// 返回生效的 SQLite 配置。
    pub fn effective_sqlite(&self) -> SkillSqliteMeta {
        self.sqlite.clone()
    }

    /// Bind the stable runtime skill identifier derived from the physical directory name.
    /// 绑定从物理目录名称派生出的稳定运行时技能标识符。
    pub fn bind_directory_skill_id(&mut self, skill_id: String) {
        self.resolved_skill_id = skill_id;
    }

    /// Return the effective skill id used by canonical entry names.
    /// 返回用于 canonical 入口名的生效 skill id。
    pub fn effective_skill_id(&self) -> &str {
        self.resolved_skill_id.trim()
    }

    /// Return the semantic package version declared by the current skill.
    /// 返回当前技能声明的语义化包版本。
    pub fn version(&self) -> &str {
        self.version.trim()
    }

    /// Return whether the manifest itself allows the skill to load.
    /// 返回当前清单本身是否允许技能被加载。
    pub fn is_enabled(&self) -> bool {
        self.enable
    }

    /// Iterate over all top-level entries.
    /// 遍历当前 skill 下的全部顶层入口。
    pub fn entries(&self) -> impl Iterator<Item = &SkillToolMeta> {
        self.entries.iter()
    }

    /// Build the unresolved base name of one entry before conflict indexing.
    /// 构建单个入口在冲突编号前的未解析基础名称。
    pub fn tool_base_name(&self, tool: &SkillToolMeta) -> String {
        format!("{}-{}", self.effective_skill_id(), tool.name.trim())
    }

    /// Find one entry by its strict local name.
    /// 根据严格局部入口名查找单个入口。
    pub fn find_tool_by_local_name(&self, tool_name: &str) -> Option<&SkillToolMeta> {
        self.entries().find(|tool| tool.name.trim() == tool_name)
    }

    /// Return the main help node declared by the skill.
    /// 返回 skill 声明的主帮助节点。
    pub fn main_help(&self) -> &SkillHelpNodeMeta {
        &self.help.main
    }

    /// Iterate over all topic or workflow help nodes declared by the skill.
    /// 遍历当前 skill 声明的全部 topic 或 workflow 帮助节点。
    pub fn help_topics(&self) -> impl Iterator<Item = &SkillHelpNodeMeta> {
        self.help.topics.iter()
    }

    /// Find one help topic or workflow node by its declared name.
    /// 根据声明名称查找单个 help topic 或 workflow 节点。
    pub fn find_help_topic(&self, topic_name: &str) -> Option<&SkillHelpNodeMeta> {
        self.help_topics()
            .find(|topic| topic.name.trim() == topic_name)
    }

    /// Return all entries that reference one help topic/workflow name.
    /// 返回引用某个 help topic/workflow 名称的全部入口。
    pub fn entries_for_help_topic<'a>(
        &'a self,
        topic_name: &'a str,
    ) -> impl Iterator<Item = &'a SkillToolMeta> + 'a {
        self.entries()
            .filter(move |tool| tool.help.trim() == topic_name)
    }
}

#[cfg(test)]
mod tests {
    use super::{
        is_valid_luaskills_identifier, validate_luaskills_identifier, validate_luaskills_version,
    };

    /// Verify that legal lowercase-hyphen identifiers are accepted.
    /// 验证合法的小写短横线标识符会被接受。
    #[test]
    fn valid_luaskills_identifiers_are_accepted() {
        assert!(is_valid_luaskills_identifier("vulcan-codekit"));
        assert!(is_valid_luaskills_identifier("codekit2"));
        assert!(is_valid_luaskills_identifier("vulcan-runtime-tools"));
    }

    /// Verify that invalid identifiers are rejected by both helper functions.
    /// 验证非法标识符会被两个辅助函数共同拒绝。
    #[test]
    fn invalid_luaskills_identifiers_are_rejected() {
        for candidate in [
            "",
            "2codekit",
            "Vulcan-codekit",
            "vulcan_codekit",
            "vulcan-codekit-",
            "__demo",
        ] {
            assert!(!is_valid_luaskills_identifier(candidate));
            assert!(validate_luaskills_identifier(candidate, "skill_id").is_err());
        }
    }

    /// Verify that semantic package versions are accepted only when they parse cleanly.
    /// 验证语义化包版本仅在可被正确解析时才会被接受。
    #[test]
    fn semantic_skill_versions_are_validated() {
        assert!(validate_luaskills_version("0.1.0", "version").is_ok());
        assert!(validate_luaskills_version("1.2.3-beta.1", "version").is_ok());
        assert!(validate_luaskills_version("", "version").is_err());
        assert!(validate_luaskills_version("v1.0.0", "version").is_err());
        assert!(validate_luaskills_version("1", "version").is_err());
    }
}