@webskill/sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1975 @@
1
+ import { parse } from "yaml";
2
+
3
+ //#region ../core/dist/index.js
4
+ /** 所有公开 API 抛出的结构化错误,code 供上层可编程处理 */
5
+ var WebSkillError = class extends Error {
6
+ code;
7
+ details;
8
+ constructor(code, message, details) {
9
+ super(message);
10
+ this.name = "WebSkillError";
11
+ this.code = code;
12
+ this.details = details;
13
+ }
14
+ };
15
+ const ROOT = "/";
16
+ /** 内存文件系统:路径一律规范化为 `/` 分隔,写入自动创建父目录 */
17
+ var MemoryFS = class {
18
+ kind = "memory";
19
+ #entries = /* @__PURE__ */ new Map();
20
+ constructor() {
21
+ this.#entries.set(ROOT, {
22
+ type: "directory",
23
+ content: /* @__PURE__ */ new Uint8Array(0),
24
+ mtimeMs: Date.now()
25
+ });
26
+ }
27
+ #normalize(path) {
28
+ const out = [];
29
+ for (const seg of path.replace(/\\/g, "/").split("/")) {
30
+ if (seg === "" || seg === ".") continue;
31
+ if (seg === "..") out.pop();
32
+ else out.push(seg);
33
+ }
34
+ return ROOT + out.join("/");
35
+ }
36
+ #parentOf(key) {
37
+ const i = key.lastIndexOf("/");
38
+ return i <= 0 ? ROOT : key.slice(0, i);
39
+ }
40
+ #ensureParents(key) {
41
+ const chain = [];
42
+ let dir = this.#parentOf(key);
43
+ while (!this.#entries.has(dir)) {
44
+ chain.push(dir);
45
+ dir = this.#parentOf(dir);
46
+ }
47
+ for (const d of chain.reverse()) this.#entries.set(d, {
48
+ type: "directory",
49
+ content: /* @__PURE__ */ new Uint8Array(0),
50
+ mtimeMs: Date.now()
51
+ });
52
+ }
53
+ #getFile(key) {
54
+ const entry = this.#entries.get(key);
55
+ if (!entry || entry.type !== "file") throw new WebSkillError("FS_NOT_FOUND", `File not found: ${key}`);
56
+ return entry;
57
+ }
58
+ #getDirectory(key) {
59
+ const entry = this.#entries.get(key);
60
+ if (!entry || entry.type !== "directory") throw new WebSkillError("FS_NOT_FOUND", `Directory not found: ${key}`);
61
+ return entry;
62
+ }
63
+ async readText(path) {
64
+ return new TextDecoder().decode(this.#getFile(this.#normalize(path)).content);
65
+ }
66
+ async writeText(path, content) {
67
+ await this.writeBinary(path, new TextEncoder().encode(content));
68
+ }
69
+ async readBinary(path) {
70
+ return this.#getFile(this.#normalize(path)).content;
71
+ }
72
+ async writeBinary(path, content) {
73
+ const key = this.#normalize(path);
74
+ this.#ensureParents(key);
75
+ this.#entries.set(key, {
76
+ type: "file",
77
+ content,
78
+ mtimeMs: Date.now()
79
+ });
80
+ }
81
+ async exists(path) {
82
+ return this.#entries.has(this.#normalize(path));
83
+ }
84
+ async stat(path) {
85
+ const key = this.#normalize(path);
86
+ const entry = this.#entries.get(key);
87
+ if (!entry) throw new WebSkillError("FS_NOT_FOUND", `Path not found: ${key}`);
88
+ return {
89
+ path: key,
90
+ type: entry.type,
91
+ size: entry.type === "file" ? entry.content.length : void 0,
92
+ mtimeMs: entry.mtimeMs
93
+ };
94
+ }
95
+ async list(path) {
96
+ const key = this.#normalize(path);
97
+ this.#getDirectory(key);
98
+ const prefix = key === ROOT ? ROOT : key + "/";
99
+ const out = [];
100
+ for (const [k, entry] of this.#entries) {
101
+ if (k === key || !k.startsWith(prefix)) continue;
102
+ if (k.slice(prefix.length).includes("/")) continue;
103
+ out.push({
104
+ path: k,
105
+ type: entry.type,
106
+ size: entry.type === "file" ? entry.content.length : void 0,
107
+ mtimeMs: entry.mtimeMs
108
+ });
109
+ }
110
+ return out;
111
+ }
112
+ async mkdir(path) {
113
+ const key = this.#normalize(path);
114
+ if (this.#entries.has(key)) return;
115
+ this.#ensureParents(key);
116
+ this.#entries.set(key, {
117
+ type: "directory",
118
+ content: /* @__PURE__ */ new Uint8Array(0),
119
+ mtimeMs: Date.now()
120
+ });
121
+ }
122
+ async remove(path, options) {
123
+ const key = this.#normalize(path);
124
+ if (!this.#entries.get(key)) throw new WebSkillError("FS_NOT_FOUND", `Path not found: ${key}`);
125
+ if (key === ROOT) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", "Cannot delete root directory");
126
+ const prefix = key + "/";
127
+ const descendants = [...this.#entries.keys()].filter((k) => k.startsWith(prefix));
128
+ if (descendants.length > 0 && !options?.recursive) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Directory not empty, use recursive: ${key}`);
129
+ for (const k of descendants) this.#entries.delete(k);
130
+ this.#entries.delete(key);
131
+ }
132
+ };
133
+ /** 统一分隔符为 `/`,去除 `.` 段与重复分隔符(不解析 `..`) */
134
+ function normalizePath(path) {
135
+ return path.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".").join("/");
136
+ }
137
+ /**
138
+ * 将相对路径安全地解析到 root 之内。
139
+ * 拒绝空路径、绝对路径(`/`、`\\`、`C:\` 形式)与任何 `..` 段。
140
+ */
141
+ function resolveInsideRoot(root, relativePath) {
142
+ const fail = (reason) => {
143
+ throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Path outside root: ${reason} (path: ${JSON.stringify(relativePath)})`);
144
+ };
145
+ if (relativePath.trim() === "") fail("empty path");
146
+ if (relativePath.startsWith("/") || relativePath.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(relativePath)) fail("absolute paths not allowed");
147
+ const segments = relativePath.replace(/\\/g, "/").split("/");
148
+ if (segments.some((s) => s === "..")) fail("`..` segments not allowed");
149
+ const clean = segments.filter((s) => s !== "" && s !== ".");
150
+ if (clean.length === 0) fail("path resolves to empty");
151
+ const normalizedRoot = normalizePath(root);
152
+ const joined = normalizedRoot === "" ? clean.join("/") : `${normalizedRoot}/${clean.join("/")}`;
153
+ return root.startsWith("/") || root.startsWith("\\") ? `/${joined}` : joined;
154
+ }
155
+ const FRONTMATTER_RE = /^---[^\S\r\n]*\r?\n([\s\S]*?)\r?\n---[^\S\r\n]*(?:\r?\n|$)([\s\S]*)$/;
156
+ const invalid = (message, details) => {
157
+ throw new WebSkillError("SKILL_INVALID_METADATA", message, details);
158
+ };
159
+ function parseSkillMarkdown(raw) {
160
+ const match = FRONTMATTER_RE.exec(raw);
161
+ if (!match) invalid("SKILL.md missing YAML frontmatter (must start with a `---` line)");
162
+ const [, yamlText, bodyRaw] = match;
163
+ let data;
164
+ try {
165
+ data = parse(yamlText);
166
+ } catch (cause) {
167
+ invalid("frontmatter YAML parse failed", cause);
168
+ }
169
+ if (typeof data !== "object" || data === null || Array.isArray(data)) invalid("frontmatter must be a YAML object");
170
+ const metadata = data;
171
+ if (typeof metadata["name"] !== "string" || metadata["name"].trim() === "") invalid("frontmatter missing required `name` field");
172
+ if (typeof metadata["description"] !== "string" || metadata["description"].trim() === "") invalid("frontmatter missing required `description` field");
173
+ return {
174
+ metadata,
175
+ body: bodyRaw.trim()
176
+ };
177
+ }
178
+ const SKILL_NAME_MAX_LENGTH = 64;
179
+ /** Agent Skills 官方命名规范:小写字母/数字/连字符,不允许首尾或连续连字符 */
180
+ const SKILL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
181
+ function isValidSkillName(name) {
182
+ return name.length <= 64 && SKILL_NAME_PATTERN.test(name);
183
+ }
184
+ const SCRIPT_EXTENSION_RE = /\.(ts|js)$/;
185
+ /**
186
+ * 技能合规规则的唯一实现。SkillDiscovery 与 validateSkills 都必须调用本函数,
187
+ * 禁止各自重复实现规则(避免两套规则漂移)。
188
+ */
189
+ function checkSkillRules(input) {
190
+ const { dirName } = input;
191
+ if (!input.hasSkillMd) return [{
192
+ code: "SKILL_NOT_FOUND",
193
+ severity: "warning",
194
+ message: `Directory ${dirName} missing SKILL.md, not treated as a skill, skipped`
195
+ }];
196
+ if (input.parseError) return [{
197
+ code: input.parseError.code,
198
+ severity: "error",
199
+ message: `SKILL.md parse failed for directory ${dirName}: ${input.parseError.message}`
200
+ }];
201
+ const issues = [];
202
+ const metadata = input.metadata;
203
+ if (metadata) {
204
+ if (!isValidSkillName(metadata.name)) issues.push({
205
+ code: "SKILL_INVALID_NAME",
206
+ severity: "error",
207
+ message: `Skill name ${JSON.stringify(metadata.name)} does not conform to naming convention (lowercase letters/digits/hyphens, ≤64 chars)`
208
+ });
209
+ if (metadata.name !== dirName) issues.push({
210
+ code: "SKILL_INVALID_NAME",
211
+ severity: "error",
212
+ message: `Skill name ${JSON.stringify(metadata.name)} does not match directory name ${JSON.stringify(dirName)}`
213
+ });
214
+ if (input.existingNames?.has(metadata.name)) issues.push({
215
+ code: "SKILL_DUPLICATE_NAME",
216
+ severity: "error",
217
+ message: `Duplicate skill name ${JSON.stringify(metadata.name)}`
218
+ });
219
+ }
220
+ for (const fileName of input.scriptFileNames ?? []) if (!SCRIPT_EXTENSION_RE.test(fileName)) issues.push({
221
+ code: "SKILL_UNSUPPORTED_SCRIPT",
222
+ severity: "error",
223
+ message: `Unsupported script file in scripts/ of directory ${dirName}: ${fileName} (only .ts/.js allowed)`
224
+ });
225
+ return issues;
226
+ }
227
+ /** 由条目列表构建 Catalog,保证按名称排序 */
228
+ function buildCatalog(entries) {
229
+ return { skills: [...entries].sort((a, b) => a.name.localeCompare(b.name)) };
230
+ }
231
+ /** 按技能名读取技能根目录内的文件,全部读取强制经过路径安全检查 */
232
+ var SkillReader = class {
233
+ #fs;
234
+ #index;
235
+ constructor(fs, index) {
236
+ this.#fs = fs;
237
+ this.#index = index;
238
+ }
239
+ #rootFor(skillName) {
240
+ const root = this.#index.get(skillName);
241
+ if (!root) throw new WebSkillError("SKILL_NOT_FOUND", `Unknown skill: ${skillName}`);
242
+ return root;
243
+ }
244
+ async readSkillMarkdown(skillName) {
245
+ return this.readSkillFile(skillName, "SKILL.md");
246
+ }
247
+ async readSkillFile(skillName, relativePath) {
248
+ return this.#fs.readText(resolveInsideRoot(this.#rootFor(skillName), relativePath));
249
+ }
250
+ async readSkillBinary(skillName, relativePath) {
251
+ return this.#fs.readBinary(resolveInsideRoot(this.#rootFor(skillName), relativePath));
252
+ }
253
+ };
254
+ const baseName$1 = (path) => path.split("/").pop() ?? path;
255
+ var SkillDiscovery = class {
256
+ #fs;
257
+ #roots;
258
+ /** name → skillRoot,与 SkillReader 共享同一 Map 引用 */
259
+ #index = /* @__PURE__ */ new Map();
260
+ #reader;
261
+ constructor(fs, roots) {
262
+ this.#fs = fs;
263
+ this.#roots = roots;
264
+ this.#reader = new SkillReader(fs, this.#index);
265
+ }
266
+ async discover() {
267
+ this.#index.clear();
268
+ const entries = [];
269
+ const issues = [];
270
+ const claimedNames = /* @__PURE__ */ new Set();
271
+ for (const root of this.#roots) {
272
+ if (!await this.#fs.exists(root)) {
273
+ issues.push({
274
+ code: "FS_NOT_FOUND",
275
+ severity: "warning",
276
+ message: `Skill root directory not found, skipped: ${root}`,
277
+ path: root
278
+ });
279
+ continue;
280
+ }
281
+ for (const child of await this.#fs.list(root)) {
282
+ if (child.type !== "directory") continue;
283
+ const dirName = baseName$1(child.path);
284
+ const skillRoot = `${root}/${dirName}`;
285
+ const skillMdPath = `${skillRoot}/SKILL.md`;
286
+ const hasSkillMd = await this.#fs.exists(skillMdPath);
287
+ let metadata;
288
+ let parseError;
289
+ if (hasSkillMd) try {
290
+ metadata = parseSkillMarkdown(await this.#fs.readText(skillMdPath)).metadata;
291
+ } catch (e) {
292
+ parseError = e instanceof WebSkillError ? e : new WebSkillError("SKILL_INVALID_METADATA", String(e));
293
+ }
294
+ const scriptFileNames = await this.#listScriptFileNames(skillRoot);
295
+ const candidateIssues = checkSkillRules({
296
+ dirName,
297
+ hasSkillMd,
298
+ metadata,
299
+ parseError,
300
+ scriptFileNames,
301
+ existingNames: claimedNames
302
+ });
303
+ for (const issue of candidateIssues) issue.path ??= skillRoot;
304
+ issues.push(...candidateIssues);
305
+ if (metadata) claimedNames.add(metadata.name);
306
+ if (candidateIssues.some((i) => i.severity === "error") || !metadata) continue;
307
+ this.#index.set(metadata.name, skillRoot);
308
+ entries.push({
309
+ name: metadata.name,
310
+ description: metadata.description,
311
+ version: typeof metadata["version"] === "string" ? metadata["version"] : void 0,
312
+ license: typeof metadata["license"] === "string" ? metadata["license"] : void 0,
313
+ root: skillRoot,
314
+ source: "local",
315
+ hasScripts: scriptFileNames.length > 0,
316
+ hasReferences: await this.#fs.exists(`${skillRoot}/references`),
317
+ hasAssets: await this.#fs.exists(`${skillRoot}/assets`)
318
+ });
319
+ }
320
+ }
321
+ entries.sort((a, b) => a.name.localeCompare(b.name));
322
+ return {
323
+ entries,
324
+ issues
325
+ };
326
+ }
327
+ async catalog() {
328
+ const { entries, issues } = await this.discover();
329
+ return {
330
+ catalog: buildCatalog(entries),
331
+ issues
332
+ };
333
+ }
334
+ /** 按需加载技能完整文档;技能不存在时抛 SKILL_NOT_FOUND */
335
+ async loadDocument(skillName) {
336
+ if (this.#index.size === 0) await this.discover();
337
+ const { metadata, body } = parseSkillMarkdown(await this.#reader.readSkillMarkdown(skillName));
338
+ const skillRoot = this.#index.get(skillName);
339
+ return {
340
+ metadata,
341
+ body,
342
+ location: {
343
+ skillRoot,
344
+ skillMdPath: `${skillRoot}/SKILL.md`,
345
+ source: "local"
346
+ }
347
+ };
348
+ }
349
+ async #listScriptFileNames(skillRoot) {
350
+ const scriptsDir = `${skillRoot}/scripts`;
351
+ if (!await this.#fs.exists(scriptsDir)) return [];
352
+ return (await this.#fs.list(scriptsDir)).filter((s) => s.type === "file").map((s) => baseName$1(s.path));
353
+ }
354
+ };
355
+ /**
356
+ * 合规校验。内部直接复用 SkillDiscovery 的扫描逻辑,
357
+ * 规则判定同样落在 checkSkillRules 单一来源上。
358
+ */
359
+ async function validateSkills(fs, roots) {
360
+ const { issues } = await new SkillDiscovery(fs, roots).discover();
361
+ return {
362
+ ok: !issues.some((i) => i.severity === "error"),
363
+ issues
364
+ };
365
+ }
366
+ function renderCatalogJson(catalog) {
367
+ return JSON.stringify(catalog, null, 2);
368
+ }
369
+ const jsonRenderer = {
370
+ format: "json",
371
+ render: renderCatalogJson
372
+ };
373
+ /** 转义 XML 五个特殊字符:& < > " ' */
374
+ function escapeXml(text) {
375
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
376
+ }
377
+ function renderAvailableSkillsXml(catalog) {
378
+ const lines = ["<available_skills>"];
379
+ for (const skill of catalog.skills) {
380
+ lines.push(" <skill>");
381
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
382
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
383
+ if (skill.version !== void 0) lines.push(` <version>${escapeXml(skill.version)}</version>`);
384
+ if (skill.license !== void 0) lines.push(` <license>${escapeXml(skill.license)}</license>`);
385
+ lines.push(` <source>${escapeXml(skill.source)}</source>`);
386
+ lines.push(" </skill>");
387
+ }
388
+ lines.push("</available_skills>");
389
+ return lines.join("\n");
390
+ }
391
+ const xmlRenderer = {
392
+ format: "xml",
393
+ render: renderAvailableSkillsXml
394
+ };
395
+
396
+ //#endregion
397
+ //#region ../runtime/dist/index.js
398
+ /**
399
+ * 预设响应序列的确定性 LlmClient;handler 形式可按会话状态动态应答。
400
+ * 默认不含 stream(存量行为);构造传 { streaming: true } 启用流式:
401
+ * 队列项 { stream: [...] } 按预设 delta 序列产出,LlmResponse 项自动合成增量。
402
+ */
403
+ var MockLlmClient = class {
404
+ calls = [];
405
+ stream;
406
+ #queue;
407
+ constructor(responses, options) {
408
+ this.#queue = [...responses];
409
+ if (options?.streaming) this.stream = (input) => this.#streamImpl(input);
410
+ }
411
+ async complete(input) {
412
+ this.calls.push(input);
413
+ const next = this.#queue.shift();
414
+ if (!next) throw new WebSkillError("LLM_REQUEST_FAILED", "MockLlmClient: response queue exhausted");
415
+ if (typeof next === "function") return next(input);
416
+ if ("stream" in next) {
417
+ const events = typeof next.stream === "function" ? next.stream(input) : next.stream;
418
+ let content = "";
419
+ let toolCalls;
420
+ for (const event of events) if (event.type === "text-delta") content += event.delta;
421
+ else if (event.type === "tool-calls") toolCalls = event.toolCalls;
422
+ else if (event.type === "done" && event.content !== void 0) content = event.content;
423
+ return {
424
+ content,
425
+ toolCalls
426
+ };
427
+ }
428
+ return next;
429
+ }
430
+ async *#streamImpl(input) {
431
+ this.calls.push(input);
432
+ const next = this.#queue.shift();
433
+ if (!next) throw new WebSkillError("LLM_REQUEST_FAILED", "MockLlmClient: response queue exhausted");
434
+ if (typeof next !== "function" && "stream" in next) {
435
+ const events = typeof next.stream === "function" ? next.stream(input) : next.stream;
436
+ for (const event of events) yield event;
437
+ return;
438
+ }
439
+ const response = typeof next === "function" ? await next(input) : next;
440
+ if (response.content) {
441
+ const chunkSize = Math.max(1, Math.ceil(response.content.length / 3));
442
+ for (let i = 0; i < response.content.length; i += chunkSize) yield {
443
+ type: "text-delta",
444
+ delta: response.content.slice(i, i + chunkSize)
445
+ };
446
+ }
447
+ if (response.toolCalls?.length) yield {
448
+ type: "tool-calls",
449
+ toolCalls: response.toolCalls
450
+ };
451
+ yield { type: "done" };
452
+ }
453
+ };
454
+ const toOpenAiMessage = (msg) => {
455
+ if (msg.role === "tool") return {
456
+ role: "tool",
457
+ tool_call_id: msg.toolCallId,
458
+ content: msg.content
459
+ };
460
+ if (msg.role === "assistant" && msg.toolCalls?.length) return {
461
+ role: "assistant",
462
+ content: msg.content === "" ? null : msg.content,
463
+ tool_calls: msg.toolCalls.map((call) => ({
464
+ id: call.id,
465
+ type: "function",
466
+ function: {
467
+ name: call.name,
468
+ arguments: JSON.stringify(call.arguments)
469
+ }
470
+ }))
471
+ };
472
+ return {
473
+ role: msg.role,
474
+ content: msg.content
475
+ };
476
+ };
477
+ const toOpenAiTools = (tools) => tools.map((tool) => ({
478
+ type: "function",
479
+ function: {
480
+ name: tool.name,
481
+ ...tool.description ? { description: tool.description } : {},
482
+ parameters: tool.inputSchema
483
+ }
484
+ }));
485
+ const errorMessage = (e) => e instanceof Error ? e.message : String(e);
486
+ /** OpenAI chat completions 协议客户端(兼容端点通用) */
487
+ var OpenAiCompatibleClient = class {
488
+ #config;
489
+ #fetch;
490
+ constructor(config) {
491
+ this.#config = config;
492
+ this.#fetch = config.fetchImpl ?? fetch;
493
+ }
494
+ #requireConfig() {
495
+ const { baseUrl, apiKey, model } = this.#config;
496
+ if (!baseUrl || !apiKey || !model) throw new WebSkillError("LLM_UNAVAILABLE", "LLM client is not configured: baseUrl, apiKey and model are all required");
497
+ return {
498
+ baseUrl: baseUrl.replace(/\/+$/, ""),
499
+ apiKey,
500
+ model
501
+ };
502
+ }
503
+ async complete(input) {
504
+ const res = await this.#postChat(input, false);
505
+ let data;
506
+ try {
507
+ data = await res.json();
508
+ } catch (e) {
509
+ throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM response: ${errorMessage(e)}`, e);
510
+ }
511
+ return this.#parseResponse(data);
512
+ }
513
+ /** SSE 流式:fetch + ReadableStream 按行解析 data: 帧(Node/浏览器通用) */
514
+ async *stream(input) {
515
+ const res = await this.#postChat(input, true);
516
+ if (!res.body) throw new WebSkillError("LLM_REQUEST_FAILED", "LLM streaming response has no body");
517
+ const toolCallsByIndex = /* @__PURE__ */ new Map();
518
+ const decoder = new TextDecoder();
519
+ const reader = res.body.getReader();
520
+ let buffer = "";
521
+ let done = false;
522
+ const handleFrame = function* (data) {
523
+ if (data === "[DONE]") {
524
+ done = true;
525
+ return;
526
+ }
527
+ let chunk;
528
+ try {
529
+ chunk = JSON.parse(data);
530
+ } catch (e) {
531
+ throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage(e)}`, data);
532
+ }
533
+ const delta = chunk.choices?.[0]?.delta;
534
+ if (!delta) return;
535
+ if (typeof delta["content"] === "string" && delta["content"] !== "") yield {
536
+ type: "text-delta",
537
+ delta: delta["content"]
538
+ };
539
+ const toolDeltas = delta["tool_calls"];
540
+ for (const td of toolDeltas ?? []) {
541
+ const index = td.index ?? 0;
542
+ const acc = toolCallsByIndex.get(index) ?? {
543
+ id: "",
544
+ name: "",
545
+ arguments: ""
546
+ };
547
+ if (td.id) acc.id = td.id;
548
+ if (td.function?.name) acc.name = td.function.name;
549
+ if (td.function?.arguments) acc.arguments += td.function.arguments;
550
+ toolCallsByIndex.set(index, acc);
551
+ }
552
+ };
553
+ try {
554
+ for (;;) {
555
+ const { value, done: readerDone } = await reader.read();
556
+ if (readerDone) break;
557
+ buffer += decoder.decode(value, { stream: true });
558
+ buffer = buffer.replace(/\r\n/g, "\n");
559
+ let newline;
560
+ while ((newline = buffer.indexOf("\n")) >= 0) {
561
+ const line = buffer.slice(0, newline).trim();
562
+ buffer = buffer.slice(newline + 1);
563
+ if (line === "" || line.startsWith(":")) continue;
564
+ if (!line.startsWith("data:")) continue;
565
+ yield* handleFrame(line.slice(5).trim());
566
+ if (done) break;
567
+ }
568
+ if (done) break;
569
+ }
570
+ } finally {
571
+ reader.releaseLock();
572
+ }
573
+ if (toolCallsByIndex.size > 0) yield {
574
+ type: "tool-calls",
575
+ toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
576
+ let args = {};
577
+ try {
578
+ args = JSON.parse(acc.arguments || "{}");
579
+ } catch {}
580
+ return {
581
+ id: acc.id || `call-${index}`,
582
+ name: acc.name,
583
+ arguments: args
584
+ };
585
+ })
586
+ };
587
+ yield { type: "done" };
588
+ }
589
+ async #postChat(input, stream) {
590
+ const { baseUrl, apiKey, model } = this.#requireConfig();
591
+ const body = {
592
+ model: input.model ?? model,
593
+ messages: input.messages.map(toOpenAiMessage)
594
+ };
595
+ if (input.tools?.length) {
596
+ body["tools"] = toOpenAiTools(input.tools);
597
+ body["tool_choice"] = "auto";
598
+ }
599
+ if (input.temperature !== void 0) body["temperature"] = input.temperature;
600
+ if (stream) body["stream"] = true;
601
+ let res;
602
+ try {
603
+ res = await this.#fetch(`${baseUrl}/chat/completions`, {
604
+ method: "POST",
605
+ headers: {
606
+ "content-type": "application/json",
607
+ authorization: `Bearer ${apiKey}`
608
+ },
609
+ body: JSON.stringify(body),
610
+ signal: input.signal ?? (this.#config.requestTimeoutMs ? AbortSignal.timeout(this.#config.requestTimeoutMs) : null)
611
+ });
612
+ } catch (e) {
613
+ throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed: ${errorMessage(e)}`, e);
614
+ }
615
+ if (!res.ok) {
616
+ const detail = await res.text().catch(() => "");
617
+ throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`);
618
+ }
619
+ return res;
620
+ }
621
+ /** 轻量探测(GET /models),集成测试据此决定 skip */
622
+ async checkAvailability() {
623
+ try {
624
+ const { baseUrl, apiKey } = this.#requireConfig();
625
+ return (await this.#fetch(`${baseUrl}/models`, { headers: { authorization: `Bearer ${apiKey}` } })).ok;
626
+ } catch {
627
+ return false;
628
+ }
629
+ }
630
+ #parseResponse(data) {
631
+ try {
632
+ const choice = data.choices?.[0]?.message;
633
+ if (!choice) throw new Error("response has no choices[0].message");
634
+ const toolCalls = choice["tool_calls"]?.map((tc) => {
635
+ const rawArgs = tc.function?.arguments ?? "{}";
636
+ return {
637
+ id: tc.id,
638
+ name: tc.function?.name ?? "",
639
+ arguments: JSON.parse(rawArgs)
640
+ };
641
+ });
642
+ const content = choice["content"];
643
+ return {
644
+ content: typeof content === "string" ? content : void 0,
645
+ toolCalls: toolCalls?.length ? toolCalls : void 0,
646
+ raw: data
647
+ };
648
+ } catch (e) {
649
+ throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to interpret LLM response: ${errorMessage(e)}`, data);
650
+ }
651
+ }
652
+ };
653
+ function toVercelToolSpecs(tools) {
654
+ const out = {};
655
+ for (const tool of tools) out[tool.name] = {
656
+ ...tool.description ? { description: tool.description } : {},
657
+ inputSchema: tool.inputSchema
658
+ };
659
+ return out;
660
+ }
661
+ /** 接受 generateText 的返回值形状:{ text?, toolCalls?: [{ toolCallId, toolName, input|args }] } */
662
+ function fromVercelResult(result) {
663
+ const r = result;
664
+ const toolCalls = (r.toolCalls ?? []).map((tc, i) => {
665
+ const args = tc.input ?? tc.args ?? {};
666
+ return {
667
+ id: typeof tc.toolCallId === "string" ? tc.toolCallId : `call-${i}`,
668
+ name: typeof tc.toolName === "string" ? tc.toolName : "",
669
+ arguments: typeof args === "object" && args !== null ? args : {}
670
+ };
671
+ });
672
+ return {
673
+ content: typeof r.text === "string" && r.text !== "" ? r.text : void 0,
674
+ toolCalls: toolCalls.length ? toolCalls : void 0,
675
+ raw: result
676
+ };
677
+ }
678
+ /**
679
+ * Vercel streamText fullStream 片段 → LlmStreamEvent 映射。
680
+ * 未知片段类型返回 undefined(调用方跳过)。
681
+ */
682
+ function fromVercelStreamPart(part) {
683
+ const p = part ?? {};
684
+ switch (p["type"]) {
685
+ case "text-delta": {
686
+ const delta = p["text"] ?? p["delta"];
687
+ return typeof delta === "string" && delta !== "" ? {
688
+ type: "text-delta",
689
+ delta
690
+ } : void 0;
691
+ }
692
+ case "tool-call": return {
693
+ type: "tool-calls",
694
+ toolCalls: [{
695
+ id: typeof p["toolCallId"] === "string" ? p["toolCallId"] : "call-0",
696
+ name: typeof p["toolName"] === "string" ? p["toolName"] : "",
697
+ arguments: typeof p["input"] === "object" && p["input"] !== null ? p["input"] : {}
698
+ }]
699
+ };
700
+ case "finish": return { type: "done" };
701
+ default: return;
702
+ }
703
+ }
704
+ const INSTRUCTIONS$1 = `You are an agent that completes tasks by using skills.
705
+
706
+ Skills are disclosed progressively: the catalog below only shows metadata. To use a skill:
707
+ 1. Call the "read_skill_file" tool to load the skill's SKILL.md instructions. Reading SKILL.md activates the skill's script tools, which are named "<skillName>__<scriptName>".
708
+ 2. Call the activated script tools to do the work.
709
+ 3. If a tool call fails, read the error, adjust your approach and retry, or continue without it.
710
+ 4. When the task is complete, answer in plain text without calling any tool.`;
711
+ /** 渐进披露路由(默认):system prompt 只注入 Catalog 元数据 + 工具使用说明 */
712
+ var ProgressiveRouter = class {
713
+ async route(catalog) {
714
+ return {
715
+ strategy: "progressive",
716
+ catalog,
717
+ systemPrompt: `${INSTRUCTIONS$1}\n\n${renderAvailableSkillsXml(catalog)}`
718
+ };
719
+ }
720
+ };
721
+ const INSTRUCTIONS = `You are an agent that completes tasks by using skills.
722
+ The full instructions of every available skill are injected below. Follow them directly.
723
+ Script tools are named "<skillName>__<scriptName>".
724
+ When the task is complete, answer in plain text without calling any tool.`;
725
+ /** 全量披露路由(对照用):注入每个技能的完整 SKILL.md 正文 */
726
+ var FullDisclosureRouter = class {
727
+ #discovery;
728
+ constructor(discovery) {
729
+ this.#discovery = discovery;
730
+ }
731
+ async route(catalog) {
732
+ const sections = [];
733
+ for (const entry of catalog.skills) {
734
+ const doc = await this.#discovery.loadDocument(entry.name);
735
+ sections.push(`<skill name="${entry.name}">\n${doc.body}\n</skill>`);
736
+ }
737
+ return {
738
+ strategy: "full-disclosure",
739
+ catalog,
740
+ systemPrompt: `${INSTRUCTIONS}\n\n${sections.join("\n\n")}`
741
+ };
742
+ }
743
+ };
744
+ /**
745
+ * 工具名解析规则(单一实现,Agent 循环使用):
746
+ * 1. `<skillName>__<scriptName>` 且前缀是已激活技能 → 本地脚本
747
+ * 2. `endpoint:` 前缀 → TOOL_UNSUPPORTED(MCP 阶段实现)
748
+ * 3. `mcp#` 前缀 → TOOL_UNSUPPORTED(WebMCP 阶段实现)
749
+ * 4. 其余 → TOOL_NOT_FOUND
750
+ */
751
+ function resolveToolName(name, activatedSkills) {
752
+ const sep = name.indexOf("__");
753
+ if (sep > 0) {
754
+ const skillName = name.slice(0, sep);
755
+ const scriptName = name.slice(sep + 2);
756
+ if (activatedSkills.has(skillName) && scriptName !== "") return {
757
+ kind: "script",
758
+ skillName,
759
+ scriptName
760
+ };
761
+ }
762
+ if (name.startsWith("endpoint:")) return {
763
+ kind: "unsupported",
764
+ code: "TOOL_UNSUPPORTED",
765
+ message: `Tool "${name}" uses an MCP endpoint, which is not supported yet`
766
+ };
767
+ if (name.startsWith("mcp#")) return {
768
+ kind: "unsupported",
769
+ code: "TOOL_UNSUPPORTED",
770
+ message: `Tool "${name}" is a WebMCP tool, which is not supported yet`
771
+ };
772
+ return {
773
+ kind: "not-found",
774
+ code: "TOOL_NOT_FOUND",
775
+ message: `Unknown tool "${name}". Activate the owning skill first by reading its SKILL.md`
776
+ };
777
+ }
778
+ /** ToolDefinition → LLM 工具 spec;缺 schema 时用宽松 object 兜底并在描述中标记 */
779
+ function toLlmToolSpec(tool) {
780
+ if (tool.inputSchema) return {
781
+ name: tool.name,
782
+ ...tool.description ? { description: tool.description } : {},
783
+ inputSchema: tool.inputSchema
784
+ };
785
+ const note = "Input schema unavailable; call with no arguments or a simple object.";
786
+ return {
787
+ name: tool.name,
788
+ description: tool.description ? `${tool.description} (${note})` : note,
789
+ inputSchema: {
790
+ type: "object",
791
+ properties: {}
792
+ }
793
+ };
794
+ }
795
+ const MCP_CONTENT_RE = /^(text|json|image|resource|audio)$/;
796
+ /**
797
+ * 脚本返回值归一到 MCP content 格式:数组沿用(逐项归一),
798
+ * 字符串包装为 text,其他值序列化包装。Node/浏览器执行器共享。
799
+ */
800
+ function normalizeToolContent(value) {
801
+ if (Array.isArray(value)) return value.map((item) => {
802
+ if (typeof item === "object" && item !== null && typeof item.type === "string" && MCP_CONTENT_RE.test(item.type)) {
803
+ const { type, ...rest } = item;
804
+ if (type === "json") return {
805
+ type: "json",
806
+ data: rest["data"] ?? rest
807
+ };
808
+ return {
809
+ type: "text",
810
+ text: typeof rest["text"] === "string" ? rest["text"] : JSON.stringify(rest)
811
+ };
812
+ }
813
+ return {
814
+ type: "text",
815
+ text: typeof item === "string" ? item : JSON.stringify(item)
816
+ };
817
+ });
818
+ if (typeof value === "string") return [{
819
+ type: "text",
820
+ text: value
821
+ }];
822
+ if (value === void 0 || value === null) return [{
823
+ type: "text",
824
+ text: ""
825
+ }];
826
+ return [{
827
+ type: "text",
828
+ text: typeof value === "object" ? JSON.stringify(value) : String(value)
829
+ }];
830
+ }
831
+ const READ_SKILL_FILE_TOOL_NAME = "read_skill_file";
832
+ const READ_SKILL_FILE_INPUT_SCHEMA = {
833
+ type: "object",
834
+ properties: {
835
+ skillName: {
836
+ type: "string",
837
+ description: "Name of the skill to read (must match the catalog)"
838
+ },
839
+ path: {
840
+ type: "string",
841
+ description: "File path relative to the skill root. Defaults to \"SKILL.md\". Reading SKILL.md activates the skill."
842
+ }
843
+ },
844
+ required: ["skillName"]
845
+ };
846
+ /** 内置工具:读取技能文件(读 SKILL.md 是技能激活的入口) */
847
+ const READ_SKILL_FILE_TOOL = {
848
+ name: READ_SKILL_FILE_TOOL_NAME,
849
+ description: "Read a file from a skill directory. Read \"SKILL.md\" first to get the skill instructions and activate its script tools.",
850
+ inputSchema: READ_SKILL_FILE_INPUT_SCHEMA,
851
+ source: "builtin"
852
+ };
853
+ const ASK_USER_TOOL_NAME = "ask_user";
854
+ const ASK_USER_INPUT_SCHEMA = {
855
+ type: "object",
856
+ properties: { question: {
857
+ type: "string",
858
+ description: "The question to ask the user"
859
+ } },
860
+ required: ["question"]
861
+ };
862
+ /** 内建工具:LLM 信息不足时主动向用户提问;仅当配置了 UiBridge 时注册 */
863
+ const ASK_USER_TOOL = {
864
+ name: ASK_USER_TOOL_NAME,
865
+ description: "Ask the user a question when information is missing and continue with their answer.",
866
+ inputSchema: ASK_USER_INPUT_SCHEMA,
867
+ source: "builtin"
868
+ };
869
+ /**
870
+ * 脚本执行上下文:只暴露 readReference / writeArtifact 两个显式能力,
871
+ * 不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
872
+ */
873
+ function createScriptContext(deps) {
874
+ const { fs, artifactStore, skillName, skillRoot, runId, confirm } = deps;
875
+ return {
876
+ skillName,
877
+ runId,
878
+ async readReference(relativePath) {
879
+ return fs.readText(resolveInsideRoot(skillRoot, `references/${relativePath}`));
880
+ },
881
+ async writeArtifact(path, content, options) {
882
+ return typeof content === "string" ? artifactStore.createTextArtifact({
883
+ runId,
884
+ path,
885
+ content,
886
+ mimeType: options?.mimeType
887
+ }) : artifactStore.createBinaryArtifact({
888
+ runId,
889
+ path,
890
+ content,
891
+ mimeType: options?.mimeType
892
+ });
893
+ },
894
+ ...confirm ? { confirm } : {}
895
+ };
896
+ }
897
+ /**
898
+ * 默认的结果渲染构造:LLM 最终输出 → markdown block,
899
+ * run.artifacts → file blocks;summary 取 terminationReason。
900
+ */
901
+ function buildRenderResult(run, output) {
902
+ const blocks = [];
903
+ if (output.trim() !== "") blocks.push({
904
+ type: "markdown",
905
+ text: output
906
+ });
907
+ for (const artifact of run.artifacts) blocks.push({
908
+ type: "file",
909
+ path: artifact.path,
910
+ ...artifact.mimeType ? { mimeType: artifact.mimeType } : {},
911
+ size: artifact.size
912
+ });
913
+ return {
914
+ runId: run.id,
915
+ summary: run.terminationReason,
916
+ blocks,
917
+ artifacts: run.artifacts
918
+ };
919
+ }
920
+ /**
921
+ * JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
922
+ * providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
923
+ * type 映射:string→text、number/integer→number、boolean→boolean、enum→select、其余→textarea。
924
+ */
925
+ function schemaToForm(schema, providedArgs) {
926
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
927
+ const fields = [];
928
+ for (const [name, prop] of Object.entries(schema.properties ?? {})) {
929
+ const provided = providedArgs?.[name];
930
+ const field = {
931
+ name,
932
+ label: name,
933
+ type: mapFieldType(prop),
934
+ ...required.has(name) ? { required: true } : {},
935
+ ...typeof prop.description === "string" ? { description: prop.description } : {},
936
+ ...provided !== void 0 ? { defaultValue: provided } : prop.default !== void 0 ? { defaultValue: prop.default } : {}
937
+ };
938
+ if (Array.isArray(prop.enum)) field.options = prop.enum.map((v) => ({
939
+ label: String(v),
940
+ value: v
941
+ }));
942
+ fields.push(field);
943
+ }
944
+ return fields;
945
+ }
946
+ function mapFieldType(prop) {
947
+ if (Array.isArray(prop.enum)) return "select";
948
+ switch (prop.type) {
949
+ case "string": return "text";
950
+ case "number":
951
+ case "integer": return "number";
952
+ case "boolean": return "boolean";
953
+ default: return "textarea";
954
+ }
955
+ }
956
+ /**
957
+ * 测试用 UiBridge:按请求 id 或类型预设应答(永不 resolve 的 handler 可模拟超时);
958
+ * 记录全部请求供断言。
959
+ */
960
+ var MockUiBridge = class {
961
+ requests = [];
962
+ #answers = /* @__PURE__ */ new Map();
963
+ #fallback;
964
+ /** key 为请求 id 或请求类型('ask' | 'confirm' | 'form' | 'select') */
965
+ respondTo(key, answer) {
966
+ this.#answers.set(key, answer);
967
+ return this;
968
+ }
969
+ setFallback(answer) {
970
+ this.#fallback = answer;
971
+ return this;
972
+ }
973
+ async request(input) {
974
+ this.requests.push(input);
975
+ const answer = this.#answers.get(input.id) ?? this.#answers.get(input.type) ?? this.#fallback;
976
+ if (!answer) return {
977
+ id: input.id,
978
+ cancelled: true
979
+ };
980
+ return typeof answer === "function" ? answer(input) : answer;
981
+ }
982
+ };
983
+ /** 生命周期事件总线:只读观测,支持按阶段或通配订阅 */
984
+ var EventBus = class {
985
+ #listeners = /* @__PURE__ */ new Map();
986
+ /** 返回取消订阅函数 */
987
+ on(phase, listener) {
988
+ const key = phase;
989
+ let set = this.#listeners.get(key);
990
+ if (!set) {
991
+ set = /* @__PURE__ */ new Set();
992
+ this.#listeners.set(key, set);
993
+ }
994
+ set.add(listener);
995
+ return () => set.delete(listener);
996
+ }
997
+ /** 当前订阅者数量(流式 delta 零订阅零开销判断用) */
998
+ listenerCount() {
999
+ let count = 0;
1000
+ for (const set of this.#listeners.values()) count += set.size;
1001
+ return count;
1002
+ }
1003
+ emit(event) {
1004
+ const frozen = Object.freeze(event);
1005
+ for (const listener of this.#listeners.get(event.phase) ?? []) listener(frozen);
1006
+ for (const listener of this.#listeners.get("*") ?? []) listener(frozen);
1007
+ }
1008
+ };
1009
+ const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
1010
+ /** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
1011
+ var HookRunner = class {
1012
+ #hooks = /* @__PURE__ */ new Map();
1013
+ timeoutMs;
1014
+ failOnHookError;
1015
+ onWarning;
1016
+ constructor(options = {}) {
1017
+ this.timeoutMs = options.timeoutMs ?? 5e3;
1018
+ this.failOnHookError = options.failOnHookError ?? false;
1019
+ this.onWarning = options.onWarning;
1020
+ }
1021
+ register(phase, hook) {
1022
+ const key = phase;
1023
+ const list = this.#hooks.get(key) ?? [];
1024
+ list.push(hook);
1025
+ this.#hooks.set(key, list);
1026
+ return this;
1027
+ }
1028
+ async run(phase, ctx) {
1029
+ const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
1030
+ for (const hook of hooks) try {
1031
+ await this.#withTimeout(Promise.resolve().then(() => hook(ctx)), phase);
1032
+ } catch (e) {
1033
+ if (this.failOnHookError) throw e instanceof WebSkillError ? e : new WebSkillError("RUN_FAILED", messageOf$1(e), e);
1034
+ this.onWarning?.(messageOf$1(e));
1035
+ }
1036
+ }
1037
+ #withTimeout(promise, phase) {
1038
+ return new Promise((resolve, reject) => {
1039
+ const timer = setTimeout(() => reject(new WebSkillError("RUN_TIMEOUT", `Lifecycle hook for phase "${phase}" timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
1040
+ promise.then((v) => {
1041
+ clearTimeout(timer);
1042
+ resolve(v);
1043
+ }, (e) => {
1044
+ clearTimeout(timer);
1045
+ reject(e instanceof Error ? e : new Error(String(e)));
1046
+ });
1047
+ });
1048
+ }
1049
+ };
1050
+ /** 内存 MemoryStore:测试与浏览器降级用 */
1051
+ var InMemoryStore = class {
1052
+ #data = /* @__PURE__ */ new Map();
1053
+ async get(scope, key) {
1054
+ return this.#data.get(scope)?.get(key);
1055
+ }
1056
+ async set(scope, key, value) {
1057
+ let bucket = this.#data.get(scope);
1058
+ if (!bucket) {
1059
+ bucket = /* @__PURE__ */ new Map();
1060
+ this.#data.set(scope, bucket);
1061
+ }
1062
+ bucket.set(key, value);
1063
+ }
1064
+ async delete(scope, key) {
1065
+ this.#data.get(scope)?.delete(key);
1066
+ }
1067
+ async list(scope) {
1068
+ return [...(this.#data.get(scope) ?? /* @__PURE__ */ new Map()).entries()].map(([key, value]) => ({
1069
+ key,
1070
+ value
1071
+ }));
1072
+ }
1073
+ async clear(scope) {
1074
+ if (scope === void 0) this.#data.clear();
1075
+ else this.#data.delete(scope);
1076
+ }
1077
+ };
1078
+ const encode = (s) => encodeURIComponent(s);
1079
+ /**
1080
+ * 基于 FileSystemProvider 的 MemoryStore:
1081
+ * <root>/<encodeURIComponent(scope)>/<encodeURIComponent(key)>.json
1082
+ * scope/key 全部编码防路径穿越;环境无关,构造必须显式传 fs。
1083
+ */
1084
+ var FsMemoryStore = class {
1085
+ #root;
1086
+ #fs;
1087
+ constructor(deps) {
1088
+ this.#root = deps.root.replace(/\/+$/, "");
1089
+ this.#fs = deps.fs;
1090
+ }
1091
+ #scopeDir(scope) {
1092
+ return `${this.#root}/${encode(scope)}`;
1093
+ }
1094
+ #keyPath(scope, key) {
1095
+ return `${this.#scopeDir(scope)}/${encode(key)}.json`;
1096
+ }
1097
+ async get(scope, key) {
1098
+ const path = this.#keyPath(scope, key);
1099
+ if (!await this.#fs.exists(path)) return void 0;
1100
+ return JSON.parse(await this.#fs.readText(path));
1101
+ }
1102
+ async set(scope, key, value) {
1103
+ await this.#fs.writeText(this.#keyPath(scope, key), JSON.stringify(value, null, 2));
1104
+ }
1105
+ async delete(scope, key) {
1106
+ const path = this.#keyPath(scope, key);
1107
+ if (await this.#fs.exists(path)) await this.#fs.remove(path);
1108
+ }
1109
+ async list(scope) {
1110
+ const dir = this.#scopeDir(scope);
1111
+ if (!await this.#fs.exists(dir)) return [];
1112
+ const out = [];
1113
+ for (const entry of await this.#fs.list(dir)) {
1114
+ if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
1115
+ const fileName = entry.path.split("/").pop() ?? "";
1116
+ const key = decodeURIComponent(fileName.replace(/\.json$/, ""));
1117
+ out.push({
1118
+ key,
1119
+ value: JSON.parse(await this.#fs.readText(entry.path))
1120
+ });
1121
+ }
1122
+ return out.sort((a, b) => a.key.localeCompare(b.key));
1123
+ }
1124
+ async clear(scope) {
1125
+ if (scope !== void 0) {
1126
+ const dir = this.#scopeDir(scope);
1127
+ if (await this.#fs.exists(dir)) await this.#fs.remove(dir, { recursive: true });
1128
+ return;
1129
+ }
1130
+ if (!await this.#fs.exists(this.#root)) return;
1131
+ for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
1132
+ }
1133
+ };
1134
+ /** 内存 ArtifactStore:测试与浏览器降级用;索引随进程生命周期存在 */
1135
+ var MemoryArtifactStore = class {
1136
+ #byRun = /* @__PURE__ */ new Map();
1137
+ #seq = 0;
1138
+ async createTextArtifact(input) {
1139
+ return this.#record({
1140
+ runId: input.runId,
1141
+ path: input.path,
1142
+ type: "text",
1143
+ size: new TextEncoder().encode(input.content).length,
1144
+ mimeType: input.mimeType,
1145
+ metadata: input.metadata
1146
+ });
1147
+ }
1148
+ async createBinaryArtifact(input) {
1149
+ return this.#record({
1150
+ runId: input.runId,
1151
+ path: input.path,
1152
+ type: "binary",
1153
+ size: input.content.length,
1154
+ mimeType: input.mimeType,
1155
+ metadata: input.metadata
1156
+ });
1157
+ }
1158
+ async listArtifacts(runId) {
1159
+ return [...this.#byRun.get(runId) ?? []];
1160
+ }
1161
+ #record(input) {
1162
+ const artifact = {
1163
+ id: `art-${++this.#seq}`,
1164
+ runId: input.runId,
1165
+ path: input.path,
1166
+ type: input.type,
1167
+ mimeType: input.mimeType,
1168
+ size: input.size,
1169
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1170
+ metadata: input.metadata
1171
+ };
1172
+ const list = this.#byRun.get(input.runId) ?? [];
1173
+ list.push(artifact);
1174
+ this.#byRun.set(input.runId, list);
1175
+ return artifact;
1176
+ }
1177
+ };
1178
+ const INDEX_FILE = "index.json";
1179
+ /**
1180
+ * 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
1181
+ * 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
1182
+ * 环境无关(OPFS/Node 均可),构造必须显式传 fs。
1183
+ */
1184
+ var FsArtifactStore = class {
1185
+ #root;
1186
+ #fs;
1187
+ constructor(deps) {
1188
+ this.#root = deps.root.replace(/\/+$/, "");
1189
+ this.#fs = deps.fs;
1190
+ }
1191
+ async createTextArtifact(input) {
1192
+ const size = new TextEncoder().encode(input.content).length;
1193
+ await this.#fs.writeText(this.#artifactPath(input.runId, input.path), input.content);
1194
+ return this.#persist({
1195
+ ...input,
1196
+ type: "text",
1197
+ size
1198
+ });
1199
+ }
1200
+ async createBinaryArtifact(input) {
1201
+ await this.#fs.writeBinary(this.#artifactPath(input.runId, input.path), input.content);
1202
+ return this.#persist({
1203
+ ...input,
1204
+ type: "binary",
1205
+ size: input.content.length
1206
+ });
1207
+ }
1208
+ async listArtifacts(runId) {
1209
+ const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
1210
+ if (!await this.#fs.exists(indexPath)) return [];
1211
+ const raw = await this.#fs.readText(indexPath);
1212
+ return JSON.parse(raw).artifacts ?? [];
1213
+ }
1214
+ #artifactPath(runId, artifactPath) {
1215
+ return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
1216
+ }
1217
+ async #persist(input) {
1218
+ const artifact = {
1219
+ id: `${input.runId}/${input.path}`,
1220
+ runId: input.runId,
1221
+ path: input.path,
1222
+ type: input.type,
1223
+ mimeType: input.mimeType,
1224
+ size: input.size,
1225
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1226
+ metadata: input.metadata
1227
+ };
1228
+ const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
1229
+ await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
1230
+ return artifact;
1231
+ }
1232
+ };
1233
+ const isRecord = (v) => typeof v === "object" && v !== null;
1234
+ const isNonEmptyString = (v) => typeof v === "string" && v !== "";
1235
+ /** 入站校验:非法 kind / 缺 id / 缺字段 → undefined(调用方忽略并记 warning) */
1236
+ function parseBridgeRequest(data) {
1237
+ if (!isRecord(data)) return void 0;
1238
+ const { kind, id } = data;
1239
+ if (!isNonEmptyString(id)) return void 0;
1240
+ switch (kind) {
1241
+ case "readReference": return isNonEmptyString(data["path"]) ? {
1242
+ kind,
1243
+ id,
1244
+ path: data["path"]
1245
+ } : void 0;
1246
+ case "writeArtifact": {
1247
+ const { path, content, mimeType } = data;
1248
+ const validContent = typeof content === "string" || Array.isArray(content) && content.every((n) => typeof n === "number");
1249
+ if (!isNonEmptyString(path) || !validContent) return void 0;
1250
+ return {
1251
+ kind,
1252
+ id,
1253
+ path,
1254
+ content,
1255
+ ...isNonEmptyString(mimeType) ? { mimeType } : {}
1256
+ };
1257
+ }
1258
+ case "confirm": return isNonEmptyString(data["message"]) ? {
1259
+ kind,
1260
+ id,
1261
+ message: data["message"]
1262
+ } : void 0;
1263
+ default: return;
1264
+ }
1265
+ }
1266
+ function bridgeError(id, code, message) {
1267
+ return {
1268
+ id,
1269
+ ok: false,
1270
+ error: {
1271
+ code,
1272
+ message
1273
+ }
1274
+ };
1275
+ }
1276
+ /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
1277
+ var TraceRecorder = class {
1278
+ #runId;
1279
+ #clock;
1280
+ #events = [];
1281
+ #seq = 0;
1282
+ constructor(runId, clock = {}) {
1283
+ this.#runId = runId;
1284
+ this.#clock = clock;
1285
+ }
1286
+ record(type, opts) {
1287
+ const event = {
1288
+ id: this.#clock.createId?.() ?? `evt-${++this.#seq}`,
1289
+ runId: this.#runId,
1290
+ ts: this.#clock.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
1291
+ type,
1292
+ ...opts?.message !== void 0 ? { message: opts.message } : {},
1293
+ ...opts?.data !== void 0 ? { data: opts.data } : {}
1294
+ };
1295
+ this.#events.push(event);
1296
+ return event;
1297
+ }
1298
+ list() {
1299
+ return [...this.#events];
1300
+ }
1301
+ };
1302
+ /** 交互终态(取消/超时):从工具执行深处直接终止 run */
1303
+ var RunTerminated = class extends Error {
1304
+ outcome;
1305
+ constructor(outcome) {
1306
+ super(outcome.message);
1307
+ this.outcome = outcome;
1308
+ }
1309
+ };
1310
+ /** bridge.request 自身异常(非超时):恢复 running 后转为工具错误回喂 */
1311
+ var BridgeRequestError = class extends Error {};
1312
+ const baseName = (p) => p.split("/").pop() ?? p;
1313
+ const messageOf = (e) => e instanceof Error ? e.message : String(e);
1314
+ const toolError = (code, message) => ({
1315
+ ok: false,
1316
+ content: [],
1317
+ error: {
1318
+ code,
1319
+ message
1320
+ }
1321
+ });
1322
+ /**
1323
+ * 多轮 Agent 循环。
1324
+ * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
1325
+ * 缺参/确认/提问经 UiBridge 行内 await 暂停恢复;取消/超时/LLM 异常/护栏超限才终止 run。
1326
+ */
1327
+ var AgentLoop = class {
1328
+ #deps;
1329
+ #config;
1330
+ #policy;
1331
+ constructor(deps, config = {}) {
1332
+ this.#deps = deps;
1333
+ this.#config = {
1334
+ maxTurns: config.maxTurns ?? 10,
1335
+ totalTimeoutMs: config.totalTimeoutMs ?? 12e4,
1336
+ toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
1337
+ temperature: config.temperature,
1338
+ renderResult: config.renderResult
1339
+ };
1340
+ this.#policy = {
1341
+ missingParams: deps.interaction?.missingParams ?? "user",
1342
+ confirmations: deps.interaction?.confirmations ?? "required",
1343
+ interactionTimeoutMs: deps.interaction?.interactionTimeoutMs ?? 3e5
1344
+ };
1345
+ }
1346
+ async run(input) {
1347
+ const { llm, artifactStore } = this.#deps;
1348
+ const now = this.#deps.clock?.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1349
+ const runId = input.runId ?? `run-${Math.random().toString(36).slice(2, 10)}`;
1350
+ const trace = new TraceRecorder(runId, this.#deps.clock);
1351
+ const startedAt = now();
1352
+ const startMs = Date.parse(startedAt);
1353
+ const run = {
1354
+ id: runId,
1355
+ sessionId: input.sessionId,
1356
+ status: "running",
1357
+ phase: "route",
1358
+ userPrompt: input.userPrompt,
1359
+ startedAt,
1360
+ activeSkillNames: [],
1361
+ artifacts: [],
1362
+ trace: []
1363
+ };
1364
+ const state = {
1365
+ runId,
1366
+ run,
1367
+ trace,
1368
+ reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
1369
+ activated: /* @__PURE__ */ new Set(),
1370
+ activatedTools: /* @__PURE__ */ new Map(),
1371
+ toolTimeoutMs: this.#config.toolTimeoutMs,
1372
+ now,
1373
+ interactionSeq: 0
1374
+ };
1375
+ if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1376
+ const route = this.#deps.catalogFilter ? {
1377
+ ...input.route,
1378
+ catalog: { skills: await this.#deps.catalogFilter(input.route.catalog.skills) }
1379
+ } : input.route;
1380
+ trace.record("skill.routed", { data: {
1381
+ strategy: route.strategy,
1382
+ skillCount: route.catalog.skills.length
1383
+ } });
1384
+ await this.#lifecycle("route", state, { strategy: route.strategy });
1385
+ const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
1386
+ try {
1387
+ return await source.listToolSpecs();
1388
+ } catch (e) {
1389
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
1390
+ return [];
1391
+ }
1392
+ }))).flat();
1393
+ const messages = [{
1394
+ role: "system",
1395
+ content: route.systemPrompt
1396
+ }, {
1397
+ role: "user",
1398
+ content: input.userPrompt
1399
+ }];
1400
+ const finish = async (status, reason, output, errorCode) => {
1401
+ run.status = status;
1402
+ run.terminationReason = reason;
1403
+ run.endedAt = now();
1404
+ run.activeSkillNames = [...state.activated].sort();
1405
+ run.artifacts = await artifactStore.listArtifacts(runId);
1406
+ if (status === "completed") trace.record("run.completed", { data: { reason } });
1407
+ else if (status === "cancelled") trace.record("run.cancelled", {
1408
+ message: output,
1409
+ data: { reason }
1410
+ });
1411
+ else trace.record("run.failed", {
1412
+ message: output,
1413
+ data: {
1414
+ reason,
1415
+ ...errorCode ? { code: errorCode } : {}
1416
+ }
1417
+ });
1418
+ const bridge = this.#deps.uiBridge;
1419
+ if (status === "completed" && bridge?.renderResult && this.#config.renderResult !== "off") try {
1420
+ const request = buildRenderResult(run, output);
1421
+ await bridge.renderResult(request);
1422
+ trace.record("ui.rendered", { data: { blockCount: request.blocks.length } });
1423
+ } catch (e) {
1424
+ trace.record("run.warning", { message: `renderResult failed: ${messageOf(e)}` });
1425
+ }
1426
+ try {
1427
+ await this.#lifecycle(status === "completed" ? "complete" : "fail", state, { reason });
1428
+ } catch (e) {
1429
+ trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
1430
+ }
1431
+ run.trace = trace.list();
1432
+ return {
1433
+ output,
1434
+ run
1435
+ };
1436
+ };
1437
+ try {
1438
+ for (let turn = 1;; turn++) {
1439
+ if (turn > this.#config.maxTurns) return await finish("failed", "max-turns", `Agent loop exceeded the maximum of ${this.#config.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1440
+ if (Date.parse(now()) - startMs > this.#config.totalTimeoutMs) return await finish("failed", "timeout", `Agent loop exceeded the total timeout of ${this.#config.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1441
+ const toolSpecs = [
1442
+ toLlmToolSpec(READ_SKILL_FILE_TOOL),
1443
+ ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
1444
+ ...[...state.activatedTools.values()].map(toLlmToolSpec),
1445
+ ...externalSpecs
1446
+ ];
1447
+ trace.record("llm.request", { data: {
1448
+ turn,
1449
+ messageCount: messages.length,
1450
+ toolCount: toolSpecs.length
1451
+ } });
1452
+ let response;
1453
+ try {
1454
+ response = llm.stream ? await this.#completeStreaming(llm, state, {
1455
+ model: this.#deps.model,
1456
+ messages,
1457
+ tools: toolSpecs,
1458
+ temperature: this.#config.temperature
1459
+ }) : await llm.complete({
1460
+ model: this.#deps.model,
1461
+ messages,
1462
+ tools: toolSpecs,
1463
+ temperature: this.#config.temperature
1464
+ });
1465
+ } catch (e) {
1466
+ const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
1467
+ return await finish("failed", "llm-error", messageOf(e), code);
1468
+ }
1469
+ trace.record("llm.response", { data: {
1470
+ turn,
1471
+ hasToolCalls: Boolean(response.toolCalls?.length),
1472
+ contentLength: response.content?.length ?? 0
1473
+ } });
1474
+ if (!response.toolCalls?.length) return await finish("completed", "final-answer", response.content ?? "");
1475
+ await this.#lifecycle("execute", state, { turn });
1476
+ messages.push({
1477
+ role: "assistant",
1478
+ content: response.content ?? "",
1479
+ toolCalls: response.toolCalls
1480
+ });
1481
+ for (const call of response.toolCalls) {
1482
+ let result;
1483
+ try {
1484
+ result = await this.#executeCall(call, state);
1485
+ } catch (e) {
1486
+ if (e instanceof RunTerminated) return await finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
1487
+ throw e;
1488
+ }
1489
+ messages.push({
1490
+ role: "tool",
1491
+ toolCallId: call.id,
1492
+ content: JSON.stringify(result)
1493
+ });
1494
+ }
1495
+ }
1496
+ } catch (e) {
1497
+ if (e instanceof WebSkillError && e.code === "RUN_FAILED") return await finish("failed", "hook-error", messageOf(e), "RUN_FAILED");
1498
+ throw e;
1499
+ }
1500
+ }
1501
+ /**
1502
+ * 流式 LLM 调用:text-delta 仅在 EventBus 有订阅者时发射 llm.delta 事件,
1503
+ * 并同步转发 uiBridge.onTextDelta;tool-calls/done 组装为 LlmResponse 走原有分支。
1504
+ */
1505
+ async #completeStreaming(llm, state, input) {
1506
+ const emitDelta = (delta) => {
1507
+ const bus = this.#deps.eventBus;
1508
+ if (bus && bus.listenerCount() > 0) bus.emit({
1509
+ phase: "execute",
1510
+ runId: state.runId,
1511
+ sessionId: state.run.sessionId,
1512
+ ts: state.now(),
1513
+ data: {
1514
+ type: "llm.delta",
1515
+ delta
1516
+ }
1517
+ });
1518
+ this.#deps.uiBridge?.onTextDelta?.(state.runId, delta);
1519
+ };
1520
+ let content = "";
1521
+ let doneContent;
1522
+ const toolCalls = [];
1523
+ for await (const event of llm.stream(input)) if (event.type === "text-delta") {
1524
+ content += event.delta;
1525
+ emitDelta(event.delta);
1526
+ } else if (event.type === "tool-calls") toolCalls.push(...event.toolCalls);
1527
+ else if (event.type === "done") doneContent = event.content;
1528
+ return {
1529
+ content: doneContent ?? (content === "" ? void 0 : content),
1530
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0
1531
+ };
1532
+ }
1533
+ /** 生命周期接线:更新 phase、发事件、跑钩子 */
1534
+ async #lifecycle(phase, state, data) {
1535
+ state.run.phase = phase;
1536
+ const event = {
1537
+ phase,
1538
+ runId: state.runId,
1539
+ sessionId: state.run.sessionId,
1540
+ ts: state.now(),
1541
+ ...data ? { data } : {}
1542
+ };
1543
+ this.#deps.eventBus?.emit(event);
1544
+ if (this.#deps.hooks) await this.#deps.hooks.run(phase, {
1545
+ event,
1546
+ run: state.run
1547
+ });
1548
+ }
1549
+ /**
1550
+ * 交互暂停/恢复(行内 await,不做可序列化快照,见 deferred-items D3):
1551
+ * interrupted → bridge.request(带超时)→ 合并结果恢复;
1552
+ * 取消 → RunTerminated(cancelled);超时 → RunTerminated(interaction-timeout)。
1553
+ */
1554
+ async #interact(state, request, traceData) {
1555
+ const bridge = this.#deps.uiBridge;
1556
+ if (!bridge) throw new Error("interact() requires a UiBridge");
1557
+ const { run } = state;
1558
+ run.status = "interrupted";
1559
+ run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
1560
+ await this.#lifecycle("interact", state, {
1561
+ interactionId: request.id,
1562
+ type: request.type
1563
+ });
1564
+ state.trace.record("ui.requested", { data: {
1565
+ interactionId: request.id,
1566
+ type: request.type,
1567
+ ...traceData
1568
+ } });
1569
+ let response;
1570
+ try {
1571
+ response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs);
1572
+ } catch (e) {
1573
+ run.status = "running";
1574
+ run.interruptExpiresAt = void 0;
1575
+ if (e instanceof WebSkillError && e.code === "RUN_INTERACTION_TIMEOUT") throw new RunTerminated({
1576
+ status: "failed",
1577
+ reason: "interaction-timeout",
1578
+ message: e.message,
1579
+ code: "RUN_INTERACTION_TIMEOUT"
1580
+ });
1581
+ state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf(e)}` });
1582
+ throw new BridgeRequestError(messageOf(e));
1583
+ }
1584
+ if (response.cancelled) throw new RunTerminated({
1585
+ status: "cancelled",
1586
+ reason: "user-cancelled",
1587
+ message: "Interaction cancelled by user",
1588
+ code: "RUN_CANCELLED"
1589
+ });
1590
+ run.status = "running";
1591
+ run.interruptExpiresAt = void 0;
1592
+ state.trace.record("ui.resumed", { data: {
1593
+ interactionId: request.id,
1594
+ type: request.type
1595
+ } });
1596
+ await this.#lifecycle("execute", state, { resumed: request.id });
1597
+ await this.#appendParamHistory(state, request, response.value);
1598
+ return response.value;
1599
+ }
1600
+ #withInteractionTimeout(promise, timeoutMs) {
1601
+ return new Promise((resolve, reject) => {
1602
+ const timer = setTimeout(() => reject(new WebSkillError("RUN_INTERACTION_TIMEOUT", `Interaction timed out after ${timeoutMs}ms`)), timeoutMs);
1603
+ promise.then((v) => {
1604
+ clearTimeout(timer);
1605
+ resolve(v);
1606
+ }, (e) => {
1607
+ clearTimeout(timer);
1608
+ reject(e instanceof Error ? e : new Error(String(e)));
1609
+ });
1610
+ });
1611
+ }
1612
+ async #executeCall(call, state) {
1613
+ state.trace.record("tool.started", { data: {
1614
+ name: call.name,
1615
+ callId: call.id
1616
+ } });
1617
+ let result;
1618
+ if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
1619
+ else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
1620
+ else {
1621
+ const resolution = resolveToolName(call.name, state.activated);
1622
+ if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
1623
+ else {
1624
+ const source = (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
1625
+ result = source ? await source.call(call.name, call.arguments) : toolError(resolution.code, resolution.message);
1626
+ }
1627
+ }
1628
+ if (result.ok) state.trace.record("tool.completed", { data: {
1629
+ name: call.name,
1630
+ callId: call.id
1631
+ } });
1632
+ else state.trace.record("tool.failed", {
1633
+ message: result.error?.message,
1634
+ data: {
1635
+ name: call.name,
1636
+ callId: call.id,
1637
+ code: result.error?.code
1638
+ }
1639
+ });
1640
+ for (const artifact of result.artifacts ?? []) state.trace.record("artifact.created", { data: {
1641
+ artifactId: artifact.id,
1642
+ path: artifact.path
1643
+ } });
1644
+ return result;
1645
+ }
1646
+ async #handleAskUser(call, state) {
1647
+ const question = call.arguments["question"];
1648
+ if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires a non-empty \"question\" string argument");
1649
+ try {
1650
+ const value = await this.#interact(state, {
1651
+ type: "ask",
1652
+ id: this.#nextInteractionId(state),
1653
+ message: question
1654
+ }, { tool: call.name });
1655
+ return {
1656
+ ok: true,
1657
+ content: [{
1658
+ type: "text",
1659
+ text: typeof value === "string" ? value : JSON.stringify(value ?? "")
1660
+ }]
1661
+ };
1662
+ } catch (e) {
1663
+ if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `ask_user failed: ${e.message}`);
1664
+ throw e;
1665
+ }
1666
+ }
1667
+ async #handleReadSkillFile(call, state) {
1668
+ const skillName = call.arguments["skillName"];
1669
+ if (typeof skillName !== "string" || skillName === "") return toolError("TOOL_EXECUTION_FAILED", "read_skill_file requires a non-empty \"skillName\" string argument");
1670
+ const pathArg = call.arguments["path"] ?? "SKILL.md";
1671
+ if (typeof pathArg !== "string") return toolError("TOOL_EXECUTION_FAILED", "read_skill_file \"path\" must be a string");
1672
+ if (skillName.startsWith("mcp://")) return this.#handleReadExternalSkill(skillName, pathArg, state);
1673
+ try {
1674
+ const text = await state.reader.readSkillFile(skillName, pathArg);
1675
+ let note = "";
1676
+ if (pathArg === "SKILL.md" && !state.activated.has(skillName)) note = await this.#activateSkill(skillName, state);
1677
+ return {
1678
+ ok: true,
1679
+ content: [{
1680
+ type: "text",
1681
+ text: text + note
1682
+ }]
1683
+ };
1684
+ } catch (e) {
1685
+ if (e instanceof WebSkillError && e.code === "SKILL_NOT_FOUND" && (this.#deps.skillProviders?.length ?? 0) > 0) return this.#handleReadExternalSkill(skillName, pathArg, state);
1686
+ return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `read_skill_file failed: ${messageOf(e)}`);
1687
+ }
1688
+ }
1689
+ /** 经外部技能提供者读取技能文档/资源;skillKey 为技能名或 mcp:// URI */
1690
+ async #handleReadExternalSkill(skillKey, pathArg, state) {
1691
+ for (const provider of this.#deps.skillProviders ?? []) try {
1692
+ let name = skillKey;
1693
+ if (skillKey.startsWith("mcp://")) {
1694
+ const entry = (await provider.listSkills()).find((e) => e.root === skillKey);
1695
+ if (!entry) continue;
1696
+ name = entry.name;
1697
+ }
1698
+ if (pathArg === "SKILL.md") {
1699
+ const doc = await provider.loadSkill(name);
1700
+ if (!state.activated.has(name)) {
1701
+ state.activated.add(name);
1702
+ state.trace.record("skill.activated", { data: {
1703
+ skillName: name,
1704
+ source: "external"
1705
+ } });
1706
+ await this.#lifecycle("activate", state, { skillName: name });
1707
+ await this.#writeActivationMemory(name, state);
1708
+ }
1709
+ return {
1710
+ ok: true,
1711
+ content: [{
1712
+ type: "text",
1713
+ text: doc.body
1714
+ }]
1715
+ };
1716
+ }
1717
+ if (provider.readFile) return {
1718
+ ok: true,
1719
+ content: [{
1720
+ type: "text",
1721
+ text: await provider.readFile(name, pathArg)
1722
+ }]
1723
+ };
1724
+ } catch {
1725
+ continue;
1726
+ }
1727
+ return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}`);
1728
+ }
1729
+ /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用 */
1730
+ async #activateSkill(skillName, state) {
1731
+ state.activated.add(skillName);
1732
+ state.trace.record("skill.activated", { data: { skillName } });
1733
+ await this.#lifecycle("activate", state, { skillName });
1734
+ await this.#writeActivationMemory(skillName, state);
1735
+ const root = this.#deps.skillIndex.get(skillName);
1736
+ if (!root) return "";
1737
+ const loaded = [];
1738
+ let scriptFiles = [];
1739
+ try {
1740
+ scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
1741
+ } catch {
1742
+ return "";
1743
+ }
1744
+ for (const file of scriptFiles) {
1745
+ const match = /^(.*)\.(ts|js)$/.exec(file);
1746
+ if (!match?.[1]) continue;
1747
+ try {
1748
+ const def = await this.#deps.executor.loadDefinition(root, match[1]);
1749
+ state.activatedTools.set(def.name, def);
1750
+ loaded.push(def.name);
1751
+ } catch {}
1752
+ }
1753
+ return loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
1754
+ }
1755
+ async #handleScriptTool(call, skillName, scriptName, state) {
1756
+ const root = this.#deps.skillIndex.get(skillName);
1757
+ if (!root) return toolError("TOOL_NOT_FOUND", `Skill "${skillName}" is not in the catalog`);
1758
+ let args = call.arguments;
1759
+ const def = state.activatedTools.get(call.name);
1760
+ const missing = (def?.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
1761
+ if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def?.inputSchema) try {
1762
+ const value = await this.#interact(state, {
1763
+ type: "form",
1764
+ id: this.#nextInteractionId(state),
1765
+ title: `Missing parameters for ${call.name}`,
1766
+ fields: schemaToForm(def.inputSchema, args)
1767
+ }, {
1768
+ tool: call.name,
1769
+ missing
1770
+ });
1771
+ if (typeof value === "object" && value !== null) args = {
1772
+ ...args,
1773
+ ...value
1774
+ };
1775
+ } catch (e) {
1776
+ if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `Parameter form failed: ${e.message}`);
1777
+ throw e;
1778
+ }
1779
+ const context = createScriptContext({
1780
+ fs: this.#deps.fs,
1781
+ artifactStore: this.#deps.artifactStore,
1782
+ skillName,
1783
+ skillRoot: root,
1784
+ runId: state.runId,
1785
+ confirm: (message) => this.#confirm(message, state)
1786
+ });
1787
+ try {
1788
+ const before = new Set((await this.#deps.artifactStore.listArtifacts(state.runId)).map((a) => a.id));
1789
+ const result = await this.#deps.executor.execute({
1790
+ skillRoot: root,
1791
+ scriptName,
1792
+ args,
1793
+ context,
1794
+ timeoutMs: state.toolTimeoutMs
1795
+ });
1796
+ const fresh = (await this.#deps.artifactStore.listArtifacts(state.runId)).filter((a) => !before.has(a.id));
1797
+ if (fresh.length > 0) result.artifacts = [...result.artifacts ?? [], ...fresh];
1798
+ await this.#bumpSkillStat(skillName, result.ok ? "successes" : "failures", state);
1799
+ return result;
1800
+ } catch (e) {
1801
+ if (e instanceof RunTerminated) throw e;
1802
+ await this.#bumpSkillStat(skillName, "failures", state);
1803
+ return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `Tool "${call.name}" failed: ${messageOf(e)}`);
1804
+ }
1805
+ }
1806
+ /** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
1807
+ async #confirm(message, state) {
1808
+ if (this.#policy.confirmations === "auto-approve") return true;
1809
+ if (!this.#deps.uiBridge) {
1810
+ state.trace.record("run.warning", { message: "No UiBridge configured; confirm() auto-approved" });
1811
+ return true;
1812
+ }
1813
+ try {
1814
+ return await this.#interact(state, {
1815
+ type: "confirm",
1816
+ id: this.#nextInteractionId(state),
1817
+ message,
1818
+ defaultValue: true
1819
+ }, { kind: "confirm" }) !== false;
1820
+ } catch (e) {
1821
+ if (e instanceof BridgeRequestError) {
1822
+ state.trace.record("run.warning", { message: `confirm() bridge failed, auto-approved: ${e.message}` });
1823
+ return true;
1824
+ }
1825
+ throw e;
1826
+ }
1827
+ }
1828
+ #nextInteractionId(state) {
1829
+ return `int-${++state.interactionSeq}`;
1830
+ }
1831
+ async #memoryGet(scope, key) {
1832
+ try {
1833
+ return await this.#deps.memory?.get(scope, key);
1834
+ } catch {
1835
+ return;
1836
+ }
1837
+ }
1838
+ async #memorySet(scope, key, value, state) {
1839
+ try {
1840
+ await this.#deps.memory?.set(scope, key, value);
1841
+ } catch (e) {
1842
+ state.trace.record("run.warning", { message: `Memory write failed: ${messageOf(e)}` });
1843
+ }
1844
+ }
1845
+ async #writeActivationMemory(skillName, state) {
1846
+ if (!this.#deps.memory) return;
1847
+ const sessionScope = `session:${state.run.sessionId}`;
1848
+ await this.#memorySet(sessionScope, "activeSkillNames", [...state.activated].sort(), state);
1849
+ await this.#bumpSkillStat(skillName, "activations", state);
1850
+ if (this.#deps.longTerm?.enabled) {
1851
+ const userScope = `user:${this.#deps.longTerm.userId}`;
1852
+ const usage = await this.#memoryGet(userScope, "skillUsage") ?? {};
1853
+ usage[skillName] = (usage[skillName] ?? 0) + 1;
1854
+ await this.#memorySet(userScope, "skillUsage", usage, state);
1855
+ }
1856
+ }
1857
+ async #bumpSkillStat(skillName, field, state) {
1858
+ if (!this.#deps.memory) return;
1859
+ const scope = `skill:${skillName}`;
1860
+ const stats = await this.#memoryGet(scope, "stats") ?? {
1861
+ activations: 0,
1862
+ successes: 0,
1863
+ failures: 0
1864
+ };
1865
+ stats[field] = (stats[field] ?? 0) + 1;
1866
+ await this.#memorySet(scope, "stats", stats, state);
1867
+ }
1868
+ async #appendParamHistory(state, request, value) {
1869
+ if (!this.#deps.memory) return;
1870
+ const scope = `session:${state.run.sessionId}`;
1871
+ const history = await this.#memoryGet(scope, "paramHistory") ?? [];
1872
+ history.push({
1873
+ ts: state.now(),
1874
+ interactionId: request.id,
1875
+ type: request.type,
1876
+ value
1877
+ });
1878
+ await this.#memorySet(scope, "paramHistory", history, state);
1879
+ }
1880
+ };
1881
+ /**
1882
+ * Catalog 合并:同名本地优先;外部条目的 mcp:// 根 URI 保留,
1883
+ * 调用方可凭 URI 强制选择外部技能(绕过本地优先)。
1884
+ */
1885
+ function mergeCatalogEntries(localEntries, providerEntries) {
1886
+ const byName = new Map(localEntries.map((e) => [e.name, e]));
1887
+ for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
1888
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
1889
+ }
1890
+ /** runtime 门面:组合 discovery / router / agent loop / lifecycle */
1891
+ var WebSkillRuntime = class {
1892
+ #deps;
1893
+ #discovery;
1894
+ #router;
1895
+ #session;
1896
+ #events;
1897
+ #catalogCache;
1898
+ constructor(deps) {
1899
+ this.#deps = deps;
1900
+ this.#discovery = new SkillDiscovery(deps.fs, deps.roots);
1901
+ this.#router = deps.router ?? new ProgressiveRouter();
1902
+ this.#events = deps.eventBus ?? new EventBus();
1903
+ this.#session = {
1904
+ id: `session-${Math.random().toString(36).slice(2, 10)}`,
1905
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1906
+ };
1907
+ }
1908
+ get session() {
1909
+ return this.#session;
1910
+ }
1911
+ /** 生命周期事件总线(只读观测) */
1912
+ get events() {
1913
+ return this.#events;
1914
+ }
1915
+ async discover() {
1916
+ const result = await this.#discovery.discover();
1917
+ this.#catalogCache = {
1918
+ catalog: buildCatalog(result.entries),
1919
+ index: new Map(result.entries.map((e) => [e.name, e.root]))
1920
+ };
1921
+ return result;
1922
+ }
1923
+ async run(userPrompt) {
1924
+ if (!this.#catalogCache) await this.discover();
1925
+ const cache = this.#catalogCache;
1926
+ if (!cache) throw new Error("discover() did not populate the catalog cache");
1927
+ const providerEntries = (await Promise.all((this.#deps.skillProviders ?? []).map(async (p) => {
1928
+ try {
1929
+ return await p.listSkills();
1930
+ } catch {
1931
+ return [];
1932
+ }
1933
+ }))).flat();
1934
+ const catalog = providerEntries.length > 0 ? { skills: mergeCatalogEntries(cache.catalog.skills, providerEntries) } : cache.catalog;
1935
+ const filteredCatalog = this.#deps.catalogFilter ? { skills: await this.#deps.catalogFilter(catalog.skills) } : catalog;
1936
+ const route = await this.#router.route(filteredCatalog);
1937
+ const result = await new AgentLoop({
1938
+ llm: this.#deps.llm,
1939
+ executor: this.#deps.executor,
1940
+ artifactStore: this.#deps.artifactStore,
1941
+ fs: this.#deps.fs,
1942
+ skillIndex: cache.index,
1943
+ model: this.#deps.model,
1944
+ uiBridge: this.#deps.uiBridge,
1945
+ interaction: this.#deps.interaction,
1946
+ memory: this.#deps.memory,
1947
+ hooks: this.#deps.hooks,
1948
+ eventBus: this.#events,
1949
+ longTerm: this.#deps.longTerm,
1950
+ externalTools: this.#deps.externalTools,
1951
+ skillProviders: this.#deps.skillProviders,
1952
+ catalogFilter: this.#deps.catalogFilter
1953
+ }, this.#deps.config).run({
1954
+ sessionId: this.#session.id,
1955
+ userPrompt,
1956
+ route
1957
+ });
1958
+ if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
1959
+ prompt: userPrompt,
1960
+ run: result.run
1961
+ }).catch((e) => {
1962
+ result.run.trace.push({
1963
+ id: `evt-miss-${Date.now()}`,
1964
+ runId: result.run.id,
1965
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1966
+ type: "run.warning",
1967
+ message: `onSkillMiss hook failed: ${e instanceof Error ? e.message : String(e)}`
1968
+ });
1969
+ });
1970
+ return result;
1971
+ }
1972
+ };
1973
+
1974
+ //#endregion
1975
+ export { schemaToForm as A, checkSkillRules as B, createScriptContext as C, normalizeToolContent as D, mergeCatalogEntries as E, SKILL_NAME_PATTERN as F, parseSkillMarkdown as G, isValidSkillName as H, SkillDiscovery as I, resolveInsideRoot as J, renderAvailableSkillsXml as K, SkillReader as L, toVercelToolSpecs as M, MemoryFS as N, parseBridgeRequest as O, SKILL_NAME_MAX_LENGTH as P, WebSkillError as R, buildRenderResult as S, fromVercelStreamPart as T, jsonRenderer as U, escapeXml as V, normalizePath as W, xmlRenderer as X, validateSkills as Y, READ_SKILL_FILE_TOOL as _, EventBus as a, WebSkillRuntime as b, FullDisclosureRouter as c, MemoryArtifactStore as d, MockLlmClient as f, READ_SKILL_FILE_INPUT_SCHEMA as g, ProgressiveRouter as h, AgentLoop as i, toLlmToolSpec as j, resolveToolName as k, HookRunner as l, OpenAiCompatibleClient as m, ASK_USER_TOOL as n, FsArtifactStore as o, MockUiBridge as p, renderCatalogJson as q, ASK_USER_TOOL_NAME as r, FsMemoryStore as s, ASK_USER_INPUT_SCHEMA as t, InMemoryStore as u, READ_SKILL_FILE_TOOL_NAME as v, fromVercelResult as w, bridgeError as x, TraceRecorder as y, buildCatalog as z };