@webskill/sdk 0.2.7 → 0.3.0

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.
Files changed (38) hide show
  1. package/README.md +42 -0
  2. package/dist/browser.d.ts +13 -4
  3. package/dist/browser.js +19 -10
  4. package/dist/{catalogComponents-C_V39rbF-B94i0fW7.js → catalogComponents-C_V39rbF-BOHveMWa.js} +254 -9090
  5. package/dist/client-BCM6Z3yq-qUQNkKrK.js +7787 -0
  6. package/dist/{dist-BQzncxXg.js → dist-8oQRa8Xz.js} +212 -13
  7. package/dist/{dist-CtBLBbEz.js → dist-ZKaM8j06.js} +2 -2
  8. package/dist/{dist-BdOW8N4V.js → dist-rorEJsNi.js} +596 -59
  9. package/dist/governance.d.ts +30 -197
  10. package/dist/governance.js +38 -199
  11. package/dist/{index-BpIK7tJM.d.ts → index-8d-oEDww.d.ts} +215 -2
  12. package/dist/{index-BNQNSDKg.d.ts → index-wiV5X8Rz.d.ts} +21 -4
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +3 -3
  15. package/dist/{jsonRenderRegistry-9GrWP_hE-CMkryt1H.js → jsonRenderRegistry-9GrWP_hE-U6Do3Kid.js} +2 -2
  16. package/dist/mcp.d.ts +5 -3
  17. package/dist/mcp.js +10 -5
  18. package/dist/node.d.ts +338 -4
  19. package/dist/node.js +2338 -4
  20. package/dist/{openUiLibrary-B8-Cvou9-Cu1QZqoT.js → openUiLibrary-B8-Cvou9-BbpNTXS3.js} +2 -2
  21. package/dist/sandboxWorkerEntry.js +12 -3
  22. package/dist/skillVersionStore-DOEI9ptb-BxbYL70B.d.ts +127 -0
  23. package/dist/stdio-CFMoANJJ-BxrTeXh7.js +31 -0
  24. package/dist/{testing-BUoXvm1u.js → testing-CsrG3XLz.js} +1 -1
  25. package/dist/testing.d.ts +1 -1
  26. package/dist/testing.js +1 -1
  27. package/dist/{types-7fnqDVrf-BnRQjVU3.d.ts → types-AmKCKJn_-VGabeXK4.d.ts} +140 -3
  28. package/dist/types-WovEf4ED-CZSDiiBU.js +6215 -0
  29. package/dist/ui-react.d.ts +2 -2
  30. package/dist/ui-react.js +6 -6
  31. package/dist/ui-vue.d.ts +1 -1
  32. package/dist/ui-vue.js +1 -1
  33. package/dist/ui.d.ts +3 -3
  34. package/dist/ui.js +2 -2
  35. package/dist/{webskillLitCatalog-CNaUpasU-CfSRvqCZ.js → webskillLitCatalog-CNaUpasU-BslMcxRZ.js} +1 -1
  36. package/package.json +4 -7
  37. package/dist/dist-CcMIUXeZ.js +0 -1896
  38. package/dist/index-BHL5FWGw.d.ts +0 -240
@@ -1,7 +1,52 @@
1
- import { C as messageOf, N as validateSkills, O as renderAvailableSkillsXml, T as parseSkillMarkdown, c as SkillDiscovery, f as assertSafePathSegment, j as resolveInsideRoot, l as SkillReader, m as buildCatalog, u as WebSkillError } from "./dist-BQzncxXg.js";
1
+ import { A as parseSkillMarkdown, L as resolveInsideRoot, O as messageOf, P as renderAvailableSkillsXml, V as validateSkills, f as SkillDiscovery, g as assertSafePathSegment, m as WebSkillError, p as SkillReader, v as buildCatalog } from "./dist-8oQRa8Xz.js";
2
2
  import { t as MemoryArtifactStore } from "./memoryArtifactStore-C9lFVqPF-yFz6yJj0.js";
3
3
 
4
4
  //#region ../runtime/dist/index.js
5
+ function createSseFrameReader() {
6
+ let buffer = "";
7
+ let data = [];
8
+ const dispatch = (out) => {
9
+ if (data.length === 0) return;
10
+ out.push(data.join("\n"));
11
+ data = [];
12
+ };
13
+ const consumeLine = (line, out) => {
14
+ if (line === "") {
15
+ dispatch(out);
16
+ return;
17
+ }
18
+ if (line.startsWith(":")) return;
19
+ const colon = line.indexOf(":");
20
+ if (colon < 0) return;
21
+ if (line.slice(0, colon) !== "data") return;
22
+ const value = line.slice(colon + 1);
23
+ data.push(value.startsWith(" ") ? value.slice(1) : value);
24
+ };
25
+ return {
26
+ push(chunk) {
27
+ const out = [];
28
+ buffer += chunk;
29
+ buffer = buffer.replace(/\r\n/g, "\n");
30
+ let newline;
31
+ while ((newline = buffer.indexOf("\n")) >= 0) {
32
+ const line = buffer.slice(0, newline);
33
+ buffer = buffer.slice(newline + 1);
34
+ consumeLine(line, out);
35
+ }
36
+ return out;
37
+ },
38
+ flush() {
39
+ const out = [];
40
+ if (buffer !== "") {
41
+ const line = buffer;
42
+ buffer = "";
43
+ consumeLine(line, out);
44
+ }
45
+ dispatch(out);
46
+ return out;
47
+ }
48
+ };
49
+ }
5
50
  const toOpenAiMessage = (msg) => {
6
51
  if (msg.role === "tool") return {
7
52
  role: "tool",
@@ -68,7 +113,7 @@ var OpenAiCompatibleClient = class {
68
113
  const toolCallsByIndex = /* @__PURE__ */ new Map();
69
114
  const decoder = new TextDecoder();
70
115
  const reader = res.body.getReader();
71
- let buffer = "";
116
+ const frames = createSseFrameReader();
72
117
  let done = false;
73
118
  const handleFrame = function* (data) {
74
119
  if (data === "[DONE]") {
@@ -104,19 +149,12 @@ var OpenAiCompatibleClient = class {
104
149
  try {
105
150
  for (;;) {
106
151
  const { value, done: readerDone } = await reader.read();
107
- if (readerDone) break;
108
- buffer += decoder.decode(value, { stream: true });
109
- buffer = buffer.replace(/\r\n/g, "\n");
110
- let newline;
111
- while ((newline = buffer.indexOf("\n")) >= 0) {
112
- const line = buffer.slice(0, newline).trim();
113
- buffer = buffer.slice(newline + 1);
114
- if (line === "" || line.startsWith(":")) continue;
115
- if (!line.startsWith("data:")) continue;
116
- yield* handleFrame(line.slice(5).trim());
152
+ const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
153
+ for (const payload of payloads) {
154
+ yield* handleFrame(payload);
117
155
  if (done) break;
118
156
  }
119
- if (done) break;
157
+ if (done || readerDone) break;
120
158
  }
121
159
  } finally {
122
160
  reader.releaseLock();
@@ -125,13 +163,17 @@ var OpenAiCompatibleClient = class {
125
163
  type: "tool-calls",
126
164
  toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
127
165
  let args = {};
166
+ let parseError;
128
167
  try {
129
168
  args = JSON.parse(acc.arguments || "{}");
130
- } catch {}
169
+ } catch (e) {
170
+ parseError = e instanceof Error ? e.message : String(e);
171
+ }
131
172
  return {
132
173
  id: acc.id || `call-${index}`,
133
174
  name: acc.name,
134
- arguments: args
175
+ arguments: args,
176
+ ...parseError ? { argumentsParseError: parseError } : {}
135
177
  };
136
178
  })
137
179
  };
@@ -292,7 +334,7 @@ var AnthropicClient = class {
292
334
  const toolCallsByIndex = /* @__PURE__ */ new Map();
293
335
  const decoder = new TextDecoder();
294
336
  const reader = res.body.getReader();
295
- let buffer = "";
337
+ const frames = createSseFrameReader();
296
338
  const handleFrame = function* (data) {
297
339
  let chunk;
298
340
  try {
@@ -339,17 +381,9 @@ var AnthropicClient = class {
339
381
  try {
340
382
  for (;;) {
341
383
  const { value, done: readerDone } = await reader.read();
384
+ const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
385
+ for (const payload of payloads) yield* handleFrame(payload);
342
386
  if (readerDone) break;
343
- buffer += decoder.decode(value, { stream: true });
344
- buffer = buffer.replace(/\r\n/g, "\n");
345
- let newline;
346
- while ((newline = buffer.indexOf("\n")) >= 0) {
347
- const line = buffer.slice(0, newline).trim();
348
- buffer = buffer.slice(newline + 1);
349
- if (line === "" || line.startsWith(":") || line.startsWith("event:")) continue;
350
- if (!line.startsWith("data:")) continue;
351
- yield* handleFrame(line.slice(5).trim());
352
- }
353
387
  }
354
388
  } finally {
355
389
  reader.releaseLock();
@@ -358,13 +392,17 @@ var AnthropicClient = class {
358
392
  type: "tool-calls",
359
393
  toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
360
394
  let args = {};
395
+ let parseError;
361
396
  try {
362
397
  args = JSON.parse(acc.arguments || "{}");
363
- } catch {}
398
+ } catch (e) {
399
+ parseError = e instanceof Error ? e.message : String(e);
400
+ }
364
401
  return {
365
402
  id: acc.id || `call-${index}`,
366
403
  name: acc.name,
367
- arguments: args
404
+ arguments: args,
405
+ ...parseError ? { argumentsParseError: parseError } : {}
368
406
  };
369
407
  })
370
408
  };
@@ -520,7 +558,7 @@ var GoogleGenAiClient = class {
520
558
  const toolCalls = [];
521
559
  const decoder = new TextDecoder();
522
560
  const reader = res.body.getReader();
523
- let buffer = "";
561
+ const frames = createSseFrameReader();
524
562
  const handleFrame = function* (data) {
525
563
  let chunk;
526
564
  try {
@@ -548,17 +586,9 @@ var GoogleGenAiClient = class {
548
586
  try {
549
587
  for (;;) {
550
588
  const { value, done: readerDone } = await reader.read();
589
+ const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
590
+ for (const payload of payloads) yield* handleFrame(payload);
551
591
  if (readerDone) break;
552
- buffer += decoder.decode(value, { stream: true });
553
- buffer = buffer.replace(/\r\n/g, "\n");
554
- let newline;
555
- while ((newline = buffer.indexOf("\n")) >= 0) {
556
- const line = buffer.slice(0, newline).trim();
557
- buffer = buffer.slice(newline + 1);
558
- if (line === "" || line.startsWith(":") || line.startsWith("event:")) continue;
559
- if (!line.startsWith("data:")) continue;
560
- yield* handleFrame(line.slice(5).trim());
561
- }
562
592
  }
563
593
  } finally {
564
594
  reader.releaseLock();
@@ -1279,6 +1309,51 @@ var TraceRecorder = class {
1279
1309
  return [...this.#events];
1280
1310
  }
1281
1311
  };
1312
+ /**
1313
+ * LLM 可见名 → 作者在 SKILL.md 里写的形态。
1314
+ * `<已激活技能>__<脚本>` 保持原样(裸标识符按技能作用域单独匹配),
1315
+ * `mcp__x` → `mcp#x`,其余 `a__b` → `endpoint:a/b`。
1316
+ */
1317
+ function canonicalToolName(llmToolName, activated) {
1318
+ const sep = llmToolName.indexOf("__");
1319
+ if (sep > 0 && activated.has(llmToolName.slice(0, sep))) return llmToolName;
1320
+ if (llmToolName.startsWith("mcp__")) return `mcp#${llmToolName.slice(5)}`;
1321
+ if (sep > 0) return `endpoint:${llmToolName.slice(0, sep)}/${llmToolName.slice(sep + 2)}`;
1322
+ return llmToolName;
1323
+ }
1324
+ /**
1325
+ * 只支持整段匹配与尾部 `/*`。通配符越自由,作者越容易写出一个自以为很窄、实际很宽的规则;
1326
+ * 裸 `endpoint:github`(不带 `/*`)判为未匹配——含糊写法应当报警而不是被善意解释。
1327
+ */
1328
+ function matchesPattern(pattern, declaringSkill, llmToolName, canonical) {
1329
+ if (!pattern.includes(":") && !pattern.includes("#")) return llmToolName === `${declaringSkill}__${pattern}`;
1330
+ if (pattern === canonical) return true;
1331
+ if (pattern.endsWith("/*")) {
1332
+ const prefix = pattern.slice(0, -1);
1333
+ return canonical.startsWith(prefix) && canonical.length > prefix.length;
1334
+ }
1335
+ return false;
1336
+ }
1337
+ function denialReason(state, canonical) {
1338
+ const skills = [...state.skillAllowedTools.keys()].sort().map((s) => `"${s}"`);
1339
+ const subject = skills.length === 1 ? `Skill ${skills[0]} declares` : `Skills ${skills.join(", ")} declare`;
1340
+ const slash = canonical.lastIndexOf("/");
1341
+ return `${subject} allowed-tools, but tool "${canonical}" is not listed. It is still available in 0.3.0 but WILL BE REJECTED in 0.4.0. Add "${canonical}"${slash > 0 ? ` or "${canonical.slice(0, slash)}/*"` : ""} to allowed-tools.`;
1342
+ }
1343
+ /**
1344
+ * 多技能语义:无人声明 → 不受限;有人声明 → 命中任一清单,
1345
+ * 或存在一个**未声明**清单的激活技能(否则就是把 A 的声明施加到 B 头上)。
1346
+ */
1347
+ function evaluateToolAccess(state, llmToolName) {
1348
+ if (state.skillAllowedTools.size === 0) return { allowed: true };
1349
+ for (const skill of state.activated) if (!state.skillAllowedTools.has(skill)) return { allowed: true };
1350
+ const canonical = canonicalToolName(llmToolName, state.activated);
1351
+ for (const [skill, patterns] of state.skillAllowedTools) for (const pattern of patterns) if (matchesPattern(pattern, skill, llmToolName, canonical)) return { allowed: true };
1352
+ return {
1353
+ allowed: false,
1354
+ reason: denialReason(state, canonical)
1355
+ };
1356
+ }
1282
1357
  const MAX_SURFACE_PATCHES_PER_SECOND = 240;
1283
1358
  /** 交互终态(取消/超时):从工具执行深处直接终止 run */
1284
1359
  var RunTerminated = class extends Error {
@@ -1373,6 +1448,8 @@ var AgentLoop = class {
1373
1448
  reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
1374
1449
  activated: /* @__PURE__ */ new Set(),
1375
1450
  activatedTools: /* @__PURE__ */ new Map(),
1451
+ skillAllowedTools: /* @__PURE__ */ new Map(),
1452
+ warnedDeniedTools: /* @__PURE__ */ new Set(),
1376
1453
  toolTimeoutMs: this.#config.toolTimeoutMs,
1377
1454
  now,
1378
1455
  interactionSeq: 0,
@@ -1384,6 +1461,7 @@ var AgentLoop = class {
1384
1461
  surfacePatchWindowStartedAt: Date.now(),
1385
1462
  surfacePatchCount: 0,
1386
1463
  processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
1464
+ emittedToolEvents: /* @__PURE__ */ new Set(),
1387
1465
  startMs,
1388
1466
  pausedMs: 0,
1389
1467
  maxTurns: this.#config.maxTurns,
@@ -1447,6 +1525,19 @@ var AgentLoop = class {
1447
1525
  state.timer = void 0;
1448
1526
  }
1449
1527
  }
1528
+ /**
1529
+ * D10 判定的唯一出口(暴露点 + 分发点共用)。
1530
+ * 0.3.0 只记警告并返回 false,调用点照常放行;0.4.0 收紧时改的是调用点对返回值的处理。
1531
+ */
1532
+ #checkToolAccess(state, toolName) {
1533
+ const verdict = evaluateToolAccess(state, toolName);
1534
+ if (verdict.allowed) return true;
1535
+ if (!state.warnedDeniedTools.has(toolName)) {
1536
+ state.warnedDeniedTools.add(toolName);
1537
+ state.trace.record("run.warning", { message: verdict.reason ?? `Tool "${toolName}" is not allowed` });
1538
+ }
1539
+ return false;
1540
+ }
1450
1541
  /** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
1451
1542
  async #turnLoop(state, startTurn, externalSpecs) {
1452
1543
  const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
@@ -1458,11 +1549,12 @@ var AgentLoop = class {
1458
1549
  state.turn = turn;
1459
1550
  if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1460
1551
  if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1552
+ const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs];
1553
+ for (const spec of skillToolSpecs) this.#checkToolAccess(state, spec.name);
1461
1554
  const toolSpecs = [
1462
1555
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
1463
1556
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
1464
- ...[...state.activatedTools.values()].map(toLlmToolSpec),
1465
- ...externalSpecs
1557
+ ...skillToolSpecs
1466
1558
  ];
1467
1559
  trace.record("llm.request", { data: {
1468
1560
  turn,
@@ -1592,6 +1684,7 @@ var AgentLoop = class {
1592
1684
  turn: state.turn,
1593
1685
  activeSkillNames: [...state.activated].sort(),
1594
1686
  activatedTools: [...state.activatedTools.values()],
1687
+ ...state.skillAllowedTools.size > 0 ? { skillAllowedTools: Object.fromEntries([...state.skillAllowedTools].map(([k, v]) => [k, [...v]])) } : {},
1595
1688
  ...pending.interaction ? { pendingInteraction: pending.interaction } : {},
1596
1689
  ...pending.surfaceAction ? { pendingSurfaceAction: pending.surfaceAction } : {},
1597
1690
  interactionExpiresAt: state.run.interruptExpiresAt,
@@ -1641,6 +1734,8 @@ var AgentLoop = class {
1641
1734
  reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
1642
1735
  activated: new Set(snapshot.activeSkillNames),
1643
1736
  activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
1737
+ skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
1738
+ warnedDeniedTools: /* @__PURE__ */ new Set(),
1644
1739
  toolTimeoutMs: snapshot.config.toolTimeoutMs,
1645
1740
  now,
1646
1741
  interactionSeq: snapshot.interactionSeq ?? 0,
@@ -1652,6 +1747,7 @@ var AgentLoop = class {
1652
1747
  surfacePatchWindowStartedAt: Date.now(),
1653
1748
  surfacePatchCount: 0,
1654
1749
  processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
1750
+ emittedToolEvents: /* @__PURE__ */ new Set(),
1655
1751
  startMs,
1656
1752
  pausedMs: snapshot.pausedMs ?? 0,
1657
1753
  maxTurns: snapshot.config.maxTurns,
@@ -1896,9 +1992,11 @@ var AgentLoop = class {
1896
1992
  } });
1897
1993
  this.#emitTool(state, "started", call);
1898
1994
  let result;
1899
- if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
1995
+ if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
1996
+ else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
1900
1997
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
1901
1998
  else {
1999
+ this.#checkToolAccess(state, call.name);
1902
2000
  const resolution = resolveToolName(call.name, state.activated);
1903
2001
  if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
1904
2002
  else {
@@ -2099,8 +2197,18 @@ var AgentLoop = class {
2099
2197
  * 逐工具实时事件(execute 相位,data.type='tool'):chatbot 思维链工具行等的 live 状态源;
2100
2198
  * 与既有 execute 相位事件({turn})并存,监听方按 data.type 区分。
2101
2199
  * data.args 为参数摘要(JSON 截断 100 字符,展开详情用)。
2200
+ *
2201
+ * 幂等:同一 run 内每个 `(callId, status)` 至多投递一次。
2202
+ * 模型在不同轮次重发同一个 tool call id 是真实发生的,消费侧本来各自
2203
+ * 建去重表兑付;幂等是事件流自己的语义,不应该要求每个订阅者重建一次。
2204
+ *
2205
+ * 集合挂在 LoopState 上、**不写进快照**:写进快照会让跨进程恢复的
2206
+ * 消费者永远收不到它本来就没见过的事件。
2102
2207
  */
2103
2208
  #emitTool(state, status, call) {
2209
+ const key = `${call.id}:${status}`;
2210
+ if (state.emittedToolEvents.has(key)) return;
2211
+ state.emittedToolEvents.add(key);
2104
2212
  this.#deps.eventBus?.emit({
2105
2213
  phase: "execute",
2106
2214
  runId: state.runId,
@@ -2253,8 +2361,10 @@ var AgentLoop = class {
2253
2361
  const rawDeps = metadata["dependencies"];
2254
2362
  if (Array.isArray(rawDeps)) dependencies = rawDeps.filter((d) => typeof d === "string");
2255
2363
  const rawAllowed = metadata["allowed-tools"];
2256
- if (rawAllowed !== void 0) if (Array.isArray(rawAllowed)) allowedTools = rawAllowed.filter((e) => typeof e === "string");
2257
- else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
2364
+ if (rawAllowed !== void 0) if (Array.isArray(rawAllowed)) {
2365
+ allowedTools = rawAllowed.filter((e) => typeof e === "string");
2366
+ state.skillAllowedTools.set(skillName, allowedTools);
2367
+ } else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
2258
2368
  } catch (e) {
2259
2369
  state.trace.record("run.warning", { message: `Failed to read or parse SKILL.md of skill "${skillName}": ${messageOf(e)}` });
2260
2370
  }
@@ -2264,13 +2374,17 @@ var AgentLoop = class {
2264
2374
  let scriptFiles;
2265
2375
  try {
2266
2376
  scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
2267
- } catch {
2377
+ } catch (e) {
2268
2378
  scriptFiles = [];
2379
+ if (!(e instanceof WebSkillError && e.code === "FS_NOT_FOUND")) state.trace.record("run.warning", { message: `Failed to list scripts of skill "${skillName}": ${messageOf(e)}` });
2269
2380
  }
2270
2381
  for (const file of scriptFiles) {
2271
2382
  const match = /^(.*)\.(ts|js)$/.exec(file);
2272
2383
  if (!match?.[1]) continue;
2273
- if (allowedTools && !allowedTools.includes(match[1])) continue;
2384
+ if (allowedTools && !allowedTools.includes(match[1])) {
2385
+ state.trace.record("run.warning", { message: `Skill "${skillName}" declares allowed-tools, so its script "${match[1]}" is not registered as a tool. Add "${match[1]}" to allowed-tools if that was not intended.` });
2386
+ continue;
2387
+ }
2274
2388
  try {
2275
2389
  const def = await executor.loadDefinition(root, match[1]);
2276
2390
  await this.#enrichDefinition(root, match[1], def, state);
@@ -2402,10 +2516,11 @@ var AgentLoop = class {
2402
2516
  #nextInteractionId(state) {
2403
2517
  return `int-${++state.interactionSeq}`;
2404
2518
  }
2405
- async #memoryGet(scope, key) {
2519
+ async #memoryGet(scope, key, state) {
2406
2520
  try {
2407
2521
  return await this.#deps.memory?.get(scope, key);
2408
- } catch {
2522
+ } catch (e) {
2523
+ state.trace.record("run.warning", { message: `Memory read failed: ${messageOf(e)}` });
2409
2524
  return;
2410
2525
  }
2411
2526
  }
@@ -2430,7 +2545,7 @@ var AgentLoop = class {
2430
2545
  }
2431
2546
  return;
2432
2547
  }
2433
- await this.#memorySet(scope, key, mutate(await this.#memoryGet(scope, key)), state);
2548
+ await this.#memorySet(scope, key, mutate(await this.#memoryGet(scope, key, state)), state);
2434
2549
  }
2435
2550
  async #writeActivationMemory(skillName, state) {
2436
2551
  if (!this.#deps.memory) return;
@@ -2962,9 +3077,26 @@ const encode = (s) => encodeURIComponent(s);
2962
3077
  var FsMemoryStore = class {
2963
3078
  #root;
2964
3079
  #fs;
3080
+ #onWarning;
2965
3081
  constructor(deps) {
2966
3082
  this.#root = deps.root.replace(/\/+$/, "");
2967
3083
  this.#fs = deps.fs;
3084
+ this.#onWarning = deps.onWarning ?? ((message) => console.warn(message));
3085
+ }
3086
+ /**
3087
+ * 0.2.8 G2:损坏条目此前直接 remove——静默销毁用户数据,且对调用方伪装成「没有这条记忆」。
3088
+ * 改为隔离到 .corrupt 并告警:run 照常继续,但数据留存、故障可见。
3089
+ */
3090
+ async #quarantine(path, error) {
3091
+ const reason = error instanceof Error ? error.message : String(error);
3092
+ const target = `${path}.corrupt`;
3093
+ try {
3094
+ await this.#fs.rename(path, target);
3095
+ this.#onWarning(`[webskill] Corrupted memory entry quarantined to ${target}: ${reason}`);
3096
+ } catch (renameError) {
3097
+ const detail = renameError instanceof Error ? renameError.message : String(renameError);
3098
+ this.#onWarning(`[webskill] Corrupted memory entry at ${path} could not be quarantined (${detail}): ${reason}`);
3099
+ }
2968
3100
  }
2969
3101
  #scopeDir(scope) {
2970
3102
  return `${this.#root}/${encode(scope)}`;
@@ -2977,8 +3109,8 @@ var FsMemoryStore = class {
2977
3109
  if (!await this.#fs.exists(path)) return void 0;
2978
3110
  try {
2979
3111
  return JSON.parse(await this.#fs.readText(path));
2980
- } catch {
2981
- await this.#fs.remove(path);
3112
+ } catch (e) {
3113
+ await this.#quarantine(path, e);
2982
3114
  return;
2983
3115
  }
2984
3116
  }
@@ -3002,8 +3134,8 @@ var FsMemoryStore = class {
3002
3134
  key,
3003
3135
  value: JSON.parse(await this.#fs.readText(entry.path))
3004
3136
  });
3005
- } catch {
3006
- await this.#fs.remove(entry.path);
3137
+ } catch (e) {
3138
+ await this.#quarantine(entry.path, e);
3007
3139
  }
3008
3140
  }
3009
3141
  return out.sort((a, b) => a.key.localeCompare(b.key));
@@ -3018,7 +3150,7 @@ var FsMemoryStore = class {
3018
3150
  for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
3019
3151
  }
3020
3152
  };
3021
- const INDEX_FILE = "index.json";
3153
+ const INDEX_FILE$1 = "index.json";
3022
3154
  /**
3023
3155
  * 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
3024
3156
  * 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
@@ -3027,9 +3159,11 @@ const INDEX_FILE = "index.json";
3027
3159
  var FsArtifactStore = class {
3028
3160
  #root;
3029
3161
  #fs;
3162
+ #onWarning;
3030
3163
  constructor(deps) {
3031
3164
  this.#root = deps.root.replace(/\/+$/, "");
3032
3165
  this.#fs = deps.fs;
3166
+ this.#onWarning = deps.onWarning ?? ((message) => console.warn(message));
3033
3167
  }
3034
3168
  async createTextArtifact(input) {
3035
3169
  const size = new TextEncoder().encode(input.content).length;
@@ -3050,13 +3184,20 @@ var FsArtifactStore = class {
3050
3184
  }
3051
3185
  async listArtifacts(runId) {
3052
3186
  assertSafePathSegment(runId, "runId");
3053
- const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
3187
+ const indexPath = `${this.#root}/${runId}/${INDEX_FILE$1}`;
3054
3188
  if (!await this.#fs.exists(indexPath)) return [];
3055
3189
  const raw = await this.#fs.readText(indexPath);
3056
3190
  try {
3057
3191
  return JSON.parse(raw).artifacts ?? [];
3058
- } catch {
3059
- await this.#fs.remove(indexPath);
3192
+ } catch (e) {
3193
+ const reason = e instanceof Error ? e.message : String(e);
3194
+ try {
3195
+ await this.#fs.rename(indexPath, `${indexPath}.corrupt`);
3196
+ this.#onWarning(`[webskill] Corrupted artifact index for run ${runId} quarantined to ${indexPath}.corrupt: ${reason}`);
3197
+ } catch (renameError) {
3198
+ const detail = renameError instanceof Error ? renameError.message : String(renameError);
3199
+ this.#onWarning(`[webskill] Corrupted artifact index for run ${runId} could not be quarantined (${detail}): ${reason}`);
3200
+ }
3060
3201
  return [];
3061
3202
  }
3062
3203
  }
@@ -3076,7 +3217,7 @@ var FsArtifactStore = class {
3076
3217
  metadata: input.metadata
3077
3218
  };
3078
3219
  const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
3079
- await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
3220
+ await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE$1}`, JSON.stringify({ artifacts: next }, null, 2));
3080
3221
  return artifact;
3081
3222
  }
3082
3223
  };
@@ -3170,6 +3311,20 @@ function networkUrlHost(url) {
3170
3311
  return "(unparseable-url)";
3171
3312
  }
3172
3313
  }
3314
+ /**
3315
+ * 网络策略判定逻辑的可注入源码(单一来源)。
3316
+ *
3317
+ * 0.2.8 C4:此前各注入点直接拼 `isNetworkAllowed.toString()`,依赖**函数名在产物里保持不变**。
3318
+ * SDK 自身不压缩,但消费方一旦跑生产构建,打包器会把导出函数改名(`function Ke(...)`),
3319
+ * 注入后的沙箱里 `isNetworkAllowed` 就是 undefined —— 沙箱内任何 fetch 直接
3320
+ * TOOL_EXECUTION_FAILED,网络白名单形同虚设。dev server 不压缩,所以只在真实产物上暴露。
3321
+ *
3322
+ * 因此改为把函数源码绑定到**固定的变量名**上:`var isNetworkAllowed = function Ke(...) {…};`
3323
+ * ——名字随便压缩,绑定名恒定。两个函数都自包含(不引用模块内其它符号),故可独立绑定。
3324
+ */
3325
+ function networkPolicyLibSource() {
3326
+ return `var isNetworkAllowed = ${isNetworkAllowed.toString()};\nvar networkUrlHost = ${networkUrlHost.toString()};`;
3327
+ }
3173
3328
  /** WebSkillErrorCode 全量白名单(错误码归一用;与 core errors.ts 保持同步) */
3174
3329
  const WHITELIST = /* @__PURE__ */ new Set([
3175
3330
  "FS_NOT_FOUND",
@@ -3281,6 +3436,388 @@ var CapabilityApproval = class CapabilityApproval {
3281
3436
  return "allowed";
3282
3437
  }
3283
3438
  };
3439
+ const RUN_TRACE_SCHEMA_VERSION = 1;
3440
+ /** 终止原因:取最后一条 run.completed/cancelled/failed 事件的 data.reason */
3441
+ function extractEndReason(events) {
3442
+ for (let i = events.length - 1; i >= 0; i--) {
3443
+ const event = events[i];
3444
+ if (event.type === "run.completed" || event.type === "run.cancelled" || event.type === "run.failed") {
3445
+ const reason = event.data?.["reason"];
3446
+ return typeof reason === "string" ? reason : void 0;
3447
+ }
3448
+ }
3449
+ }
3450
+ function summarize(trace) {
3451
+ const endReason = extractEndReason(trace.events);
3452
+ const durationMs = trace.endedAt === void 0 ? void 0 : Date.parse(trace.endedAt) - Date.parse(trace.startedAt);
3453
+ return {
3454
+ runId: trace.runId,
3455
+ startedAt: trace.startedAt,
3456
+ status: trace.status,
3457
+ activeSkills: trace.activeSkills,
3458
+ eventCount: trace.events.length,
3459
+ turnCount: trace.events.filter((e) => e.type === "llm.request").length,
3460
+ ...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
3461
+ ...trace.endedAt !== void 0 ? { endedAt: trace.endedAt } : {},
3462
+ ...endReason !== void 0 ? { endReason } : {},
3463
+ ...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {}
3464
+ };
3465
+ }
3466
+ function parseTraceFile(raw, path) {
3467
+ let data;
3468
+ try {
3469
+ data = JSON.parse(raw);
3470
+ } catch {
3471
+ return;
3472
+ }
3473
+ if (typeof data !== "object" || data === null) return void 0;
3474
+ if (typeof data.runId !== "string" || typeof data.startedAt !== "string") return void 0;
3475
+ const schemaVersion = typeof data.schemaVersion === "number" ? data.schemaVersion : 0;
3476
+ if (schemaVersion > 1) throw new WebSkillError("RUN_TRACE_INCOMPATIBLE", `Run trace "${path}" declares schemaVersion ${schemaVersion}, which is newer than the supported 1`);
3477
+ return {
3478
+ schemaVersion,
3479
+ runId: data.runId,
3480
+ ...typeof data.sessionId === "string" ? { sessionId: data.sessionId } : {},
3481
+ startedAt: data.startedAt,
3482
+ ...typeof data.endedAt === "string" ? { endedAt: data.endedAt } : {},
3483
+ status: typeof data.status === "string" ? data.status : "unknown",
3484
+ activeSkills: Array.isArray(data.activeSkills) ? data.activeSkills.filter((s) => typeof s === "string") : [],
3485
+ events: Array.isArray(data.events) ? data.events : []
3486
+ };
3487
+ }
3488
+ const INDEX_FILE = "index.jsonl";
3489
+ /**
3490
+ * `FileSystemProvider` 后端:`<root>/<runId>.json` 存全量 trace,
3491
+ * `<root>/index.jsonl` 是 append-only 的摘要旁路索引。
3492
+ *
3493
+ * 索引存在的唯一理由是**读次数**:0.0.1 的实现为了拼一份列表要整读每个 trace
3494
+ * 文件,点一次指标再来一遍。有了索引,`list()` 与 `metrics()` 各只读一个文件。
3495
+ *
3496
+ * 索引与 trace 目录发散(写完 trace 崩在追加前)的检测不引入新的读放大:
3497
+ * 目录列举本来就要做,比较 `*.json` 个数与索引行数即可;不等则整体重建。
3498
+ * @stable
3499
+ */
3500
+ var FsRunTraceStore = class {
3501
+ #root;
3502
+ #fs;
3503
+ #onError;
3504
+ constructor(deps) {
3505
+ this.#root = deps.root.replace(/\/+$/, "");
3506
+ this.#fs = deps.fs;
3507
+ this.#onError = deps.onError ?? ((error, run) => {
3508
+ console.warn(`Failed to persist run trace "${run.id}": ${messageOf(error)}`);
3509
+ });
3510
+ }
3511
+ #path(runId) {
3512
+ return resolveInsideRoot(this.#root, `${runId}.json`);
3513
+ }
3514
+ async put(run) {
3515
+ const trace = {
3516
+ schemaVersion: 1,
3517
+ runId: run.id,
3518
+ sessionId: run.sessionId,
3519
+ startedAt: run.startedAt,
3520
+ ...run.endedAt !== void 0 ? { endedAt: run.endedAt } : {},
3521
+ status: run.status === "completed" || run.status === "cancelled" ? run.status : "failed",
3522
+ activeSkills: run.activeSkillNames,
3523
+ events: run.trace
3524
+ };
3525
+ try {
3526
+ await this.#fs.writeText(this.#path(run.id), JSON.stringify(trace, null, 2));
3527
+ await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
3528
+ } catch (e) {
3529
+ this.#onError(e, run);
3530
+ }
3531
+ }
3532
+ async get(runId) {
3533
+ assertSafePathSegment(runId, "run id");
3534
+ const path = this.#path(runId);
3535
+ let raw;
3536
+ try {
3537
+ raw = await this.#fs.readText(path);
3538
+ } catch (e) {
3539
+ if (e instanceof WebSkillError) throw e;
3540
+ throw new WebSkillError("FS_NOT_FOUND", `Trace not found for run ${JSON.stringify(runId)}: ${path}`);
3541
+ }
3542
+ const trace = parseTraceFile(raw, path);
3543
+ if (!trace) throw new WebSkillError("VALIDATION_FAILED", `Trace file is corrupted or malformed: ${path}`);
3544
+ return trace;
3545
+ }
3546
+ async list(filter = {}) {
3547
+ const { summaries } = await this.#readIndex();
3548
+ const matched = applyFilter(summaries, filter);
3549
+ const offset = Math.max(0, filter.offset ?? 0);
3550
+ const sliced = matched.slice(offset);
3551
+ return filter.limit !== void 0 ? sliced.slice(0, Math.max(0, filter.limit)) : sliced;
3552
+ }
3553
+ async metrics(filter = {}) {
3554
+ const { summaries, skipped } = await this.#readIndex();
3555
+ const runs = applyFilter(summaries, filter);
3556
+ const succeeded = runs.filter((r) => r.status === "completed").length;
3557
+ const failedRuns = runs.filter((r) => r.status === "failed");
3558
+ const durations = runs.map((r) => r.durationMs).filter((ms) => ms !== void 0);
3559
+ return {
3560
+ totalRuns: runs.length,
3561
+ succeeded,
3562
+ failed: failedRuns.length,
3563
+ successRate: runs.length === 0 ? 0 : succeeded / runs.length,
3564
+ avgTurns: runs.length === 0 ? 0 : runs.reduce((sum, r) => sum + r.turnCount, 0) / runs.length,
3565
+ skippedRuns: skipped,
3566
+ avgDurationMs: durations.length === 0 ? 0 : durations.reduce((a, b) => a + b, 0) / durations.length,
3567
+ recentFailures: failedRuns.slice(0, 5)
3568
+ };
3569
+ }
3570
+ /** 健康路径恒为「一次目录列举 + 一次索引读」,与 run 数无关。 */
3571
+ async #readIndex() {
3572
+ if (!await this.#fs.exists(this.#root)) return {
3573
+ summaries: [],
3574
+ skipped: 0
3575
+ };
3576
+ const traceCount = (await this.#fs.list(this.#root)).filter((f) => f.type === "file" && f.path.endsWith(".json")).length;
3577
+ const indexPath = `${this.#root}/${INDEX_FILE}`;
3578
+ let lines = [];
3579
+ if (await this.#fs.exists(indexPath)) lines = (await this.#fs.readText(indexPath)).split("\n").filter((line) => line.trim() !== "");
3580
+ if (lines.length !== traceCount) return {
3581
+ summaries: await this.#rebuildIndex(),
3582
+ skipped: 0
3583
+ };
3584
+ const summaries = [];
3585
+ let skipped = 0;
3586
+ for (const line of lines) try {
3587
+ summaries.push(JSON.parse(line));
3588
+ } catch {
3589
+ skipped += 1;
3590
+ }
3591
+ if (skipped > 0) return {
3592
+ summaries: await this.#rebuildIndex(),
3593
+ skipped
3594
+ };
3595
+ return {
3596
+ summaries: sortByStartedAtDesc(summaries),
3597
+ skipped: 0
3598
+ };
3599
+ }
3600
+ /** 异常路径,允许 O(N):整读全部 trace 文件后用 temp + rename 原子替换索引 */
3601
+ async #rebuildIndex() {
3602
+ const summaries = [];
3603
+ for (const entry of await this.#fs.list(this.#root)) {
3604
+ if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
3605
+ let trace;
3606
+ try {
3607
+ trace = parseTraceFile(await this.#fs.readText(entry.path), entry.path);
3608
+ } catch {
3609
+ continue;
3610
+ }
3611
+ if (!trace) continue;
3612
+ summaries.push(summarize(trace));
3613
+ }
3614
+ const sorted = sortByStartedAtDesc(summaries);
3615
+ const indexPath = `${this.#root}/${INDEX_FILE}`;
3616
+ const tempPath = `${this.#root}/${INDEX_FILE}.rebuilding`;
3617
+ try {
3618
+ await this.#fs.writeText(tempPath, sorted.map((s) => `${JSON.stringify(s)}\n`).join(""));
3619
+ await this.#fs.rename(tempPath, indexPath);
3620
+ } catch {
3621
+ await this.#fs.remove(tempPath).catch(() => void 0);
3622
+ }
3623
+ return sorted;
3624
+ }
3625
+ };
3626
+ /** ISO 时间戳可按字典序比较 */
3627
+ function sortByStartedAtDesc(summaries) {
3628
+ return [...summaries].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
3629
+ }
3630
+ function applyFilter(summaries, filter) {
3631
+ let runs = summaries;
3632
+ if (filter.status !== void 0 && filter.status !== "") runs = runs.filter((r) => r.status === filter.status);
3633
+ const q = filter.search?.trim().toLowerCase();
3634
+ if (q !== void 0 && q !== "") runs = runs.filter((r) => r.runId.toLowerCase().includes(q) || r.activeSkills.some((s) => s.toLowerCase().includes(q)));
3635
+ return runs;
3636
+ }
3637
+ /**
3638
+ * 从 run 的 trace 推导终态工具调用列表。
3639
+ *
3640
+ * 消费者过去要靠自己遍历 trace 才能拿到这份列表,而遍历 trace 的同一段代码
3641
+ * 又顺手承担了「补发漏掉的 live 事件」的职责——两件事绑在一起,谁也删不掉。
3642
+ * 幂等归 runtime(见 `AgentLoop` 的 per-run 已发集合)之后,
3643
+ * 推导终态列表就是纯函数,独立导出。
3644
+ * @stable
3645
+ */
3646
+ function summarizeToolCalls(run) {
3647
+ const calls = [];
3648
+ for (const event of run.trace) {
3649
+ if (event.type !== "tool.completed" && event.type !== "tool.failed") continue;
3650
+ const name = event.data?.["name"];
3651
+ const callId = event.data?.["callId"];
3652
+ if (typeof name !== "string" || typeof callId !== "string") continue;
3653
+ const args = event.data?.["args"];
3654
+ const durationMs = event.data?.["durationMs"];
3655
+ calls.push({
3656
+ callId,
3657
+ name,
3658
+ status: event.type === "tool.completed" ? "completed" : "failed",
3659
+ ...typeof args === "string" ? { args } : {},
3660
+ ...typeof durationMs === "number" ? { durationMs } : {}
3661
+ });
3662
+ }
3663
+ return calls;
3664
+ }
3665
+ const SESSION_SCHEMA_VERSION = 1;
3666
+ const toMeta = (record) => ({
3667
+ id: record.id,
3668
+ createdAt: record.createdAt,
3669
+ ...record.title !== void 0 ? { title: record.title } : {},
3670
+ ...record.titleLocked === true ? { titleLocked: true } : {},
3671
+ ...record.archived === true ? { archived: true } : {},
3672
+ messageCount: record.messages.length
3673
+ });
3674
+ const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3675
+ /**
3676
+ * 解析会话文件。缺 `schemaVersion` 视为 0(0.0.1 时代文件,兼容读);
3677
+ * 高于当前版本拒绝读,避免新版写的字段被旧版静默丢弃。
3678
+ */
3679
+ function parseSessionFile(raw, path) {
3680
+ let parsed;
3681
+ try {
3682
+ parsed = JSON.parse(raw);
3683
+ } catch (e) {
3684
+ throw new WebSkillError("VALIDATION_FAILED", `Session file is not valid JSON: ${path}`, e);
3685
+ }
3686
+ if (typeof parsed !== "object" || parsed === null) throw new WebSkillError("VALIDATION_FAILED", `Session file is not an object: ${path}`);
3687
+ const file = parsed;
3688
+ const schemaVersion = typeof file.schemaVersion === "number" ? file.schemaVersion : 0;
3689
+ if (schemaVersion > 1) throw new WebSkillError("SESSION_INCOMPATIBLE", `Session file at ${path} has schemaVersion ${schemaVersion}, but this build understands at most 1`);
3690
+ if (typeof file.id !== "string" || typeof file.createdAt !== "string") throw new WebSkillError("VALIDATION_FAILED", `Session file is missing "id" or "createdAt": ${path}`);
3691
+ return {
3692
+ schemaVersion,
3693
+ id: file.id,
3694
+ createdAt: file.createdAt,
3695
+ ...typeof file.title === "string" ? { title: file.title } : {},
3696
+ ...file.titleLocked === true ? { titleLocked: true } : {},
3697
+ ...file.archived === true ? { archived: true } : {},
3698
+ messages: Array.isArray(file.messages) ? file.messages : [],
3699
+ messageCount: Array.isArray(file.messages) ? file.messages.length : 0
3700
+ };
3701
+ }
3702
+ /**
3703
+ * `FileSystemProvider` 后端的会话存储:`<root>/<id>.json`。
3704
+ *
3705
+ * 所有变更操作按 id 串到一条 promise 链上:会话文件是整读整写的,
3706
+ * 两个并发 `appendMessages` 若都先读后写,后写的会覆盖先写的那条消息。
3707
+ */
3708
+ var FsSessionStore = class {
3709
+ #root;
3710
+ #fs;
3711
+ /** id → 该 id 上最后一次变更的完成时点,用于串行化读改写 */
3712
+ #writes = /* @__PURE__ */ new Map();
3713
+ constructor(deps) {
3714
+ this.#root = deps.root.replace(/\/+$/, "");
3715
+ this.#fs = deps.fs;
3716
+ }
3717
+ #path(id) {
3718
+ assertSafePathSegment(id, "session id");
3719
+ return resolveInsideRoot(this.#root, `${id}.json`);
3720
+ }
3721
+ /** 把 mutation 排到该 id 的队尾;前一个失败不阻塞后一个 */
3722
+ async #serialize(id, work) {
3723
+ const next = (this.#writes.get(id) ?? Promise.resolve()).catch(() => void 0).then(work);
3724
+ this.#writes.set(id, next);
3725
+ try {
3726
+ return await next;
3727
+ } finally {
3728
+ if (this.#writes.get(id) === next) this.#writes.delete(id);
3729
+ }
3730
+ }
3731
+ async #write(record) {
3732
+ const file = {
3733
+ schemaVersion: 1,
3734
+ id: record.id,
3735
+ createdAt: record.createdAt,
3736
+ ...record.title !== void 0 ? { title: record.title } : {},
3737
+ ...record.titleLocked === true ? { titleLocked: true } : {},
3738
+ ...record.archived === true ? { archived: true } : {},
3739
+ messages: record.messages
3740
+ };
3741
+ await this.#fs.writeText(this.#path(record.id), JSON.stringify(file, null, 2));
3742
+ }
3743
+ async #require(id) {
3744
+ const record = await this.get(id);
3745
+ if (!record) throw new WebSkillError("FS_NOT_FOUND", `Session "${id}" not found under ${this.#root}`);
3746
+ return record;
3747
+ }
3748
+ async list(options = {}) {
3749
+ if (!await this.#fs.exists(this.#root)) return [];
3750
+ const metas = [];
3751
+ for (const entry of await this.#fs.list(this.#root)) {
3752
+ if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
3753
+ let record;
3754
+ try {
3755
+ record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
3756
+ } catch (e) {
3757
+ console.warn(`Skipping unreadable session file "${entry.path}": ${e instanceof Error ? e.message : String(e)}`);
3758
+ continue;
3759
+ }
3760
+ if (record.archived === true && options.includeArchived !== true) continue;
3761
+ metas.push(toMeta(record));
3762
+ }
3763
+ return metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3764
+ }
3765
+ async get(id) {
3766
+ const path = this.#path(id);
3767
+ if (!await this.#fs.exists(path)) return void 0;
3768
+ return parseSessionFile(await this.#fs.readText(path), path);
3769
+ }
3770
+ async create(init = {}) {
3771
+ const record = {
3772
+ schemaVersion: 1,
3773
+ id: init.id ?? newSessionId(),
3774
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3775
+ ...init.title !== void 0 ? { title: init.title } : {},
3776
+ messages: [],
3777
+ messageCount: 0
3778
+ };
3779
+ return this.#serialize(record.id, async () => {
3780
+ await this.#write(record);
3781
+ return toMeta(record);
3782
+ });
3783
+ }
3784
+ async appendMessages(id, messages) {
3785
+ if (messages.length === 0) return;
3786
+ await this.#serialize(id, async () => {
3787
+ const record = await this.#require(id);
3788
+ record.messages.push(...messages);
3789
+ await this.#write(record);
3790
+ });
3791
+ }
3792
+ async replaceMessages(id, messages) {
3793
+ await this.#serialize(id, async () => {
3794
+ const record = await this.#require(id);
3795
+ record.messages = [...messages];
3796
+ await this.#write(record);
3797
+ });
3798
+ }
3799
+ async setTitle(id, title, options = {}) {
3800
+ await this.#serialize(id, async () => {
3801
+ const record = await this.#require(id);
3802
+ record.title = title;
3803
+ if (options.lock === true) record.titleLocked = true;
3804
+ await this.#write(record);
3805
+ });
3806
+ }
3807
+ async setArchived(id, archived) {
3808
+ await this.#serialize(id, async () => {
3809
+ const record = await this.#require(id);
3810
+ record.archived = archived;
3811
+ await this.#write(record);
3812
+ });
3813
+ }
3814
+ async delete(id) {
3815
+ await this.#serialize(id, async () => {
3816
+ const path = this.#path(id);
3817
+ if (await this.#fs.exists(path)) await this.#fs.remove(path);
3818
+ });
3819
+ }
3820
+ };
3284
3821
 
3285
3822
  //#endregion
3286
- export { fromVercelStreamPart as A, toLlmToolSpec as B, bridgeError as C, extractChartSpec as D, createWebSkillApi as E, normalizeToolContent as F, validateUiSurface as H, normalizeToolError as I, parseBridgeRequest as L, mergeCatalogEntries as M, networkUrlHost as N, extractUiSurfaceEvents as O, normalizeErrorCode as P, resolveToolName as R, WebSkillRuntime as S, createScriptContext as T, validateUiSurfaceEvent as U, toVercelToolSpecs as V, READ_SKILL_FILE_TOOL as _, AnthropicClient as a, SerializingMemoryStore as b, FsArtifactStore as c, FullDisclosureRouter as d, GoogleGenAiClient as f, READ_SKILL_FILE_INPUT_SCHEMA as g, ProgressiveRouter as h, AgentLoop as i, isNetworkAllowed as j, fromVercelResult as k, FsMemoryStore as l, OpenAiCompatibleClient as m, ASK_USER_TOOL as n, CapabilityApproval as o, HookRunner as p, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsRunSnapshotStore as u, READ_SKILL_FILE_TOOL_NAME as v, buildRenderResult as w, TraceRecorder as x, RUN_SNAPSHOT_SCHEMA_VERSION as y, schemaToForm as z };
3823
+ export { createWebSkillApi as A, normalizeToolContent as B, SESSION_SCHEMA_VERSION as C, bridgeError as D, WebSkillRuntime as E, isNetworkAllowed as F, summarizeToolCalls as G, parseBridgeRequest as H, mergeCatalogEntries as I, validateUiSurface as J, toLlmToolSpec as K, networkPolicyLibSource as L, extractUiSurfaceEvents as M, fromVercelResult as N, buildRenderResult as O, fromVercelStreamPart as P, networkUrlHost as R, RUN_TRACE_SCHEMA_VERSION as S, TraceRecorder as T, resolveToolName as U, normalizeToolError as V, schemaToForm as W, validateUiSurfaceEvent as Y, ProgressiveRouter as _, AnthropicClient as a, READ_SKILL_FILE_TOOL_NAME as b, FsArtifactStore as c, FsRunTraceStore as d, FsSessionStore as f, OpenAiCompatibleClient as g, HookRunner as h, AgentLoop as i, extractChartSpec as j, createScriptContext as k, FsMemoryStore as l, GoogleGenAiClient as m, ASK_USER_TOOL as n, CapabilityApproval as o, FullDisclosureRouter as p, toVercelToolSpecs as q, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsRunSnapshotStore as u, READ_SKILL_FILE_INPUT_SCHEMA as v, SerializingMemoryStore as w, RUN_SNAPSHOT_SCHEMA_VERSION as x, READ_SKILL_FILE_TOOL as y, normalizeErrorCode as z };