@webskill/sdk 0.0.2 → 0.0.4

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.
@@ -12,6 +12,41 @@ var WebSkillError = class extends Error {
12
12
  this.details = details;
13
13
  }
14
14
  };
15
+ /** 技能目录内的安装清单文件名(不参与 files 列表与 digest) */
16
+ const SKILL_MANIFEST_FILE = "webskill.skill-manifest.json";
17
+ /** managed root 下的锁定文件名(不参与 files 列表与 digest) */
18
+ const SKILLS_LOCKFILE = "skills.lock.json";
19
+ /** 整体 digest:逐文件 "<path>:<sha256>\n" 拼接串(文件按 path 排序)的 sha256(hash 由调用方提供,可为 WebCrypto 异步实现) */
20
+ async function computeDigest(files, sha256) {
21
+ return sha256([...files].sort((a, b) => a.path.localeCompare(b.path)).map((f) => `${f.path}:${f.sha256}\n`).join(""));
22
+ }
23
+ /** 由逐文件清单构建 manifest(digest 由 computeDigest 计算) */
24
+ async function buildManifest(input) {
25
+ return {
26
+ schemaVersion: 1,
27
+ name: input.name,
28
+ ...input.version !== void 0 ? { version: input.version } : {},
29
+ source: input.source,
30
+ installedAt: input.installedAt,
31
+ integrity: {
32
+ algorithm: "sha256",
33
+ digest: await computeDigest(input.files, input.sha256)
34
+ },
35
+ files: input.files
36
+ };
37
+ }
38
+ /** 按 manifest.files 比对实际 hash(调用方负责计算 actualHashes) */
39
+ function verifyManifest(manifest, actualHashes) {
40
+ const mismatches = [];
41
+ for (const file of manifest.files) {
42
+ const actual = actualHashes.get(file.path);
43
+ if (actual === void 0 || actual !== file.sha256) mismatches.push(file.path);
44
+ }
45
+ return {
46
+ ok: mismatches.length === 0,
47
+ mismatches
48
+ };
49
+ }
15
50
  const ROOT = "/";
16
51
  /** 内存文件系统:路径一律规范化为 `/` 分隔,写入自动创建父目录 */
17
52
  var MemoryFS = class {
@@ -1349,34 +1384,35 @@ var AgentLoop = class {
1349
1384
  };
1350
1385
  }
1351
1386
  async run(input) {
1352
- const llm = this.#deps.llm;
1353
- const artifactStore = this.#deps.artifactStore;
1387
+ this.#deps.llm;
1388
+ this.#deps.artifactStore;
1354
1389
  const now = this.#deps.clock?.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1355
1390
  const runId = input.runId ?? `run-${Math.random().toString(36).slice(2, 10)}`;
1356
1391
  const trace = new TraceRecorder(runId, this.#deps.clock);
1357
1392
  const startedAt = now();
1358
1393
  const startMs = Date.parse(startedAt);
1359
- const run = {
1360
- id: runId,
1361
- sessionId: input.sessionId,
1362
- status: "running",
1363
- phase: "route",
1364
- userPrompt: input.userPrompt,
1365
- startedAt,
1366
- activeSkillNames: [],
1367
- artifacts: [],
1368
- trace: []
1369
- };
1370
1394
  const state = {
1371
1395
  runId,
1372
- run,
1396
+ run: {
1397
+ id: runId,
1398
+ sessionId: input.sessionId,
1399
+ status: "running",
1400
+ phase: "route",
1401
+ userPrompt: input.userPrompt,
1402
+ startedAt,
1403
+ activeSkillNames: [],
1404
+ artifacts: [],
1405
+ trace: []
1406
+ },
1373
1407
  trace,
1374
1408
  reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
1375
1409
  activated: /* @__PURE__ */ new Set(),
1376
1410
  activatedTools: /* @__PURE__ */ new Map(),
1377
1411
  toolTimeoutMs: this.#config.toolTimeoutMs,
1378
1412
  now,
1379
- interactionSeq: 0
1413
+ interactionSeq: 0,
1414
+ messages: [],
1415
+ turn: 0
1380
1416
  };
1381
1417
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1382
1418
  const route = this.#deps.catalogFilter ? {
@@ -1396,113 +1432,272 @@ var AgentLoop = class {
1396
1432
  return [];
1397
1433
  }
1398
1434
  }))).flat();
1399
- const messages = [{
1435
+ state.messages = [{
1400
1436
  role: "system",
1401
1437
  content: route.systemPrompt
1402
1438
  }, {
1403
1439
  role: "user",
1404
1440
  content: input.userPrompt
1405
1441
  }];
1406
- const finish = async (status, reason, output, errorCode) => {
1407
- run.status = status;
1408
- run.terminationReason = reason;
1409
- run.endedAt = now();
1410
- run.activeSkillNames = [...state.activated].sort();
1411
- run.artifacts = await artifactStore.listArtifacts(runId);
1412
- if (status === "completed") trace.record("run.completed", { data: { reason } });
1413
- else if (status === "cancelled") trace.record("run.cancelled", {
1414
- message: output,
1415
- data: { reason }
1416
- });
1417
- else trace.record("run.failed", {
1418
- message: output,
1419
- data: {
1420
- reason,
1421
- ...errorCode ? { code: errorCode } : {}
1422
- }
1423
- });
1424
- const bridge = this.#deps.uiBridge;
1425
- if (status === "completed" && bridge?.renderResult && this.#config.renderResult !== "off") try {
1426
- const request = buildRenderResult(run, output);
1427
- await bridge.renderResult(request);
1428
- trace.record("ui.rendered", { data: { blockCount: request.blocks.length } });
1429
- } catch (e) {
1430
- trace.record("run.warning", { message: `renderResult failed: ${messageOf(e)}` });
1431
- }
1442
+ try {
1443
+ return await this.#turnLoop(state, startMs, 1, externalSpecs);
1444
+ } catch (e) {
1445
+ if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
1446
+ throw e;
1447
+ }
1448
+ }
1449
+ /** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
1450
+ async #turnLoop(state, startMs, startTurn, externalSpecs) {
1451
+ const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
1452
+ const { llm } = this.#deps;
1453
+ const { trace, messages } = state;
1454
+ for (let turn = startTurn;; turn++) {
1455
+ state.turn = turn;
1456
+ if (turn > this.#config.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${this.#config.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1457
+ if (Date.parse(state.now()) - startMs > this.#config.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${this.#config.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1458
+ const toolSpecs = [
1459
+ toLlmToolSpec(READ_SKILL_FILE_TOOL),
1460
+ ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
1461
+ ...[...state.activatedTools.values()].map(toLlmToolSpec),
1462
+ ...externalSpecs
1463
+ ];
1464
+ trace.record("llm.request", { data: {
1465
+ turn,
1466
+ messageCount: messages.length,
1467
+ toolCount: toolSpecs.length
1468
+ } });
1469
+ let response;
1432
1470
  try {
1433
- await this.#lifecycle(status === "completed" ? "complete" : "fail", state, { reason });
1471
+ response = llm.stream ? await this.#completeStreaming(llm, state, {
1472
+ model: this.#deps.model,
1473
+ messages,
1474
+ tools: toolSpecs,
1475
+ temperature: this.#config.temperature
1476
+ }) : await llm.complete({
1477
+ model: this.#deps.model,
1478
+ messages,
1479
+ tools: toolSpecs,
1480
+ temperature: this.#config.temperature
1481
+ });
1434
1482
  } catch (e) {
1435
- trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
1483
+ const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
1484
+ return finish("failed", "llm-error", messageOf(e), code);
1436
1485
  }
1437
- run.trace = trace.list();
1438
- return {
1439
- output,
1440
- run
1441
- };
1442
- };
1443
- try {
1444
- for (let turn = 1;; turn++) {
1445
- 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");
1446
- 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");
1447
- const toolSpecs = [
1448
- toLlmToolSpec(READ_SKILL_FILE_TOOL),
1449
- ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
1450
- ...[...state.activatedTools.values()].map(toLlmToolSpec),
1451
- ...externalSpecs
1452
- ];
1453
- trace.record("llm.request", { data: {
1454
- turn,
1455
- messageCount: messages.length,
1456
- toolCount: toolSpecs.length
1457
- } });
1458
- let response;
1486
+ trace.record("llm.response", { data: {
1487
+ turn,
1488
+ hasToolCalls: Boolean(response.toolCalls?.length),
1489
+ contentLength: response.content?.length ?? 0
1490
+ } });
1491
+ if (!response.toolCalls?.length) return finish("completed", "final-answer", response.content ?? "");
1492
+ await this.#lifecycle("execute", state, { turn });
1493
+ messages.push({
1494
+ role: "assistant",
1495
+ content: response.content ?? "",
1496
+ toolCalls: response.toolCalls
1497
+ });
1498
+ for (const call of response.toolCalls) {
1499
+ let result;
1459
1500
  try {
1460
- response = llm.stream ? await this.#completeStreaming(llm, state, {
1461
- model: this.#deps.model,
1462
- messages,
1463
- tools: toolSpecs,
1464
- temperature: this.#config.temperature
1465
- }) : await llm.complete({
1466
- model: this.#deps.model,
1467
- messages,
1468
- tools: toolSpecs,
1469
- temperature: this.#config.temperature
1470
- });
1501
+ result = await this.#executeCall(call, state);
1471
1502
  } catch (e) {
1472
- const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
1473
- return await finish("failed", "llm-error", messageOf(e), code);
1503
+ if (e instanceof RunTerminated) return finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
1504
+ throw e;
1474
1505
  }
1475
- trace.record("llm.response", { data: {
1476
- turn,
1477
- hasToolCalls: Boolean(response.toolCalls?.length),
1478
- contentLength: response.content?.length ?? 0
1479
- } });
1480
- if (!response.toolCalls?.length) return await finish("completed", "final-answer", response.content ?? "");
1481
- await this.#lifecycle("execute", state, { turn });
1482
1506
  messages.push({
1483
- role: "assistant",
1484
- content: response.content ?? "",
1485
- toolCalls: response.toolCalls
1507
+ role: "tool",
1508
+ toolCallId: call.id,
1509
+ content: JSON.stringify(result)
1510
+ });
1511
+ }
1512
+ }
1513
+ }
1514
+ /** 统一终态处理:completed/failed/cancelled;终态即删快照(快照是续命机制,审计归治理) */
1515
+ async #finish(state, status, reason, output, errorCode) {
1516
+ const { run, trace } = state;
1517
+ run.status = status;
1518
+ run.terminationReason = reason;
1519
+ run.endedAt = state.now();
1520
+ run.activeSkillNames = [...state.activated].sort();
1521
+ run.artifacts = await this.#deps.artifactStore.listArtifacts(state.runId);
1522
+ if (status === "completed") trace.record("run.completed", { data: { reason } });
1523
+ else if (status === "cancelled") trace.record("run.cancelled", {
1524
+ message: output,
1525
+ data: { reason }
1526
+ });
1527
+ else trace.record("run.failed", {
1528
+ message: output,
1529
+ data: {
1530
+ reason,
1531
+ ...errorCode ? { code: errorCode } : {}
1532
+ }
1533
+ });
1534
+ const bridge = this.#deps.uiBridge;
1535
+ if (status === "completed" && bridge?.renderResult && this.#config.renderResult !== "off") try {
1536
+ const request = buildRenderResult(run, output);
1537
+ await bridge.renderResult(request);
1538
+ trace.record("ui.rendered", { data: { blockCount: request.blocks.length } });
1539
+ } catch (e) {
1540
+ trace.record("run.warning", { message: `renderResult failed: ${messageOf(e)}` });
1541
+ }
1542
+ if (this.#deps.snapshotStore) try {
1543
+ await this.#deps.snapshotStore.delete(state.runId);
1544
+ } catch (e) {
1545
+ trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
1546
+ }
1547
+ try {
1548
+ await this.#lifecycle(status === "completed" ? "complete" : "fail", state, { reason });
1549
+ } catch (e) {
1550
+ trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
1551
+ }
1552
+ run.trace = trace.list();
1553
+ return {
1554
+ output,
1555
+ run
1556
+ };
1557
+ }
1558
+ /** D3 快照写入(含 pendingInteraction 与过期时间;写失败降级 run.warning) */
1559
+ async #saveSnapshot(state, request) {
1560
+ const store = this.#deps.snapshotStore;
1561
+ if (!store) return;
1562
+ const snapshot = {
1563
+ schemaVersion: 1,
1564
+ runId: state.runId,
1565
+ sessionId: state.run.sessionId,
1566
+ userPrompt: state.run.userPrompt,
1567
+ startedAt: state.run.startedAt,
1568
+ messages: state.messages.map((m) => ({ ...m })),
1569
+ turn: state.turn,
1570
+ activeSkillNames: [...state.activated].sort(),
1571
+ activatedTools: [...state.activatedTools.values()],
1572
+ pendingInteraction: request,
1573
+ interactionExpiresAt: state.run.interruptExpiresAt,
1574
+ config: {
1575
+ maxTurns: this.#config.maxTurns,
1576
+ totalTimeoutMs: this.#config.totalTimeoutMs,
1577
+ toolTimeoutMs: this.#config.toolTimeoutMs,
1578
+ ...this.#config.temperature !== void 0 ? { temperature: this.#config.temperature } : {}
1579
+ },
1580
+ snapshotAt: state.now()
1581
+ };
1582
+ try {
1583
+ await store.save(snapshot);
1584
+ } catch (e) {
1585
+ state.trace.record("run.warning", { message: `Failed to save run snapshot: ${messageOf(e)}` });
1586
+ }
1587
+ }
1588
+ /**
1589
+ * D3 恢复:以快照重建 LoopState,重新发起等待中的交互(表单重新渲染),
1590
+ * 应答后从快照轮次续跑主循环;totalTimeout 以快照 startedAt 续算。
1591
+ */
1592
+ async resume(snapshot) {
1593
+ const now = this.#deps.clock?.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1594
+ const runId = snapshot.runId;
1595
+ const trace = new TraceRecorder(runId, this.#deps.clock);
1596
+ const startMs = Date.parse(snapshot.startedAt);
1597
+ const state = {
1598
+ runId,
1599
+ run: {
1600
+ id: runId,
1601
+ sessionId: snapshot.sessionId,
1602
+ status: "running",
1603
+ phase: "execute",
1604
+ userPrompt: snapshot.userPrompt,
1605
+ startedAt: snapshot.startedAt,
1606
+ activeSkillNames: snapshot.activeSkillNames,
1607
+ artifacts: [],
1608
+ trace: []
1609
+ },
1610
+ trace,
1611
+ reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
1612
+ activated: new Set(snapshot.activeSkillNames),
1613
+ activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
1614
+ toolTimeoutMs: snapshot.config.toolTimeoutMs,
1615
+ now,
1616
+ interactionSeq: 0,
1617
+ messages: snapshot.messages.map((m) => ({ ...m })),
1618
+ turn: snapshot.turn
1619
+ };
1620
+ if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1621
+ trace.record("run.resumed", { data: {
1622
+ snapshotAt: snapshot.snapshotAt,
1623
+ turn: snapshot.turn,
1624
+ interactionType: snapshot.pendingInteraction.type
1625
+ } });
1626
+ const pending = snapshot.pendingInteraction;
1627
+ const pendingCall = this.#findPendingToolCall(state.messages);
1628
+ try {
1629
+ if ((pending.type === "form" || pending.type === "select") && pendingCall) {
1630
+ const value = await this.#interact(state, pending, {
1631
+ tool: pendingCall.name,
1632
+ resumed: true
1633
+ });
1634
+ const args = {
1635
+ ...pendingCall.arguments,
1636
+ ...typeof value === "object" && value !== null ? value : {}
1637
+ };
1638
+ const result = await this.#executeCall({
1639
+ ...pendingCall,
1640
+ arguments: args
1641
+ }, state);
1642
+ state.messages.push({
1643
+ role: "tool",
1644
+ toolCallId: pendingCall.id,
1645
+ content: JSON.stringify(result)
1646
+ });
1647
+ } else if (pending.type === "ask" && pendingCall) {
1648
+ const value = await this.#interact(state, pending, {
1649
+ tool: pendingCall.name,
1650
+ resumed: true
1651
+ });
1652
+ const result = {
1653
+ ok: true,
1654
+ content: [{
1655
+ type: "text",
1656
+ text: typeof value === "string" ? value : JSON.stringify(value ?? "")
1657
+ }]
1658
+ };
1659
+ state.trace.record("tool.completed", { data: {
1660
+ name: pendingCall.name,
1661
+ callId: pendingCall.id
1662
+ } });
1663
+ state.messages.push({
1664
+ role: "tool",
1665
+ toolCallId: pendingCall.id,
1666
+ content: JSON.stringify(result)
1667
+ });
1668
+ } else if (pendingCall) {
1669
+ const result = await this.#executeCall(pendingCall, state);
1670
+ state.messages.push({
1671
+ role: "tool",
1672
+ toolCallId: pendingCall.id,
1673
+ content: JSON.stringify(result)
1486
1674
  });
1487
- for (const call of response.toolCalls) {
1488
- let result;
1489
- try {
1490
- result = await this.#executeCall(call, state);
1491
- } catch (e) {
1492
- if (e instanceof RunTerminated) return await finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
1493
- throw e;
1494
- }
1495
- messages.push({
1496
- role: "tool",
1497
- toolCallId: call.id,
1498
- content: JSON.stringify(result)
1499
- });
1500
- }
1501
1675
  }
1502
1676
  } catch (e) {
1503
- if (e instanceof WebSkillError && e.code === "RUN_FAILED") return await finish("failed", "hook-error", messageOf(e), "RUN_FAILED");
1677
+ if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
1504
1678
  throw e;
1505
1679
  }
1680
+ const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
1681
+ try {
1682
+ return await source.listToolSpecs();
1683
+ } catch (e) {
1684
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
1685
+ return [];
1686
+ }
1687
+ }))).flat();
1688
+ try {
1689
+ return await this.#turnLoop(state, startMs, snapshot.turn + 1, externalSpecs);
1690
+ } catch (e) {
1691
+ if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
1692
+ throw e;
1693
+ }
1694
+ }
1695
+ /** 最后一条 assistant 消息里第一个尚无 tool 响应的工具调用(即中断时正在处理的那个) */
1696
+ #findPendingToolCall(messages) {
1697
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant" && m.toolCalls?.length);
1698
+ if (!lastAssistant?.toolCalls) return void 0;
1699
+ const answered = new Set(messages.filter((m) => m.role === "tool").map((m) => m.toolCallId));
1700
+ return lastAssistant.toolCalls.find((call) => !answered.has(call.id));
1506
1701
  }
1507
1702
  /**
1508
1703
  * 流式 LLM 调用:text-delta 仅在 EventBus 有订阅者时发射 llm.delta 事件,
@@ -1563,6 +1758,7 @@ var AgentLoop = class {
1563
1758
  const { run } = state;
1564
1759
  run.status = "interrupted";
1565
1760
  run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
1761
+ await this.#saveSnapshot(state, request);
1566
1762
  await this.#lifecycle("interact", state, {
1567
1763
  interactionId: request.id,
1568
1764
  type: request.type
@@ -1919,6 +2115,58 @@ function mergeCatalogEntries(localEntries, providerEntries) {
1919
2115
  for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
1920
2116
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
1921
2117
  }
2118
+ const RUN_SNAPSHOT_SCHEMA_VERSION = 1;
2119
+ const SNAPSHOT_SUFFIX = ".snapshot.json";
2120
+ /**
2121
+ * FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
2122
+ * 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
2123
+ */
2124
+ var FsRunSnapshotStore = class {
2125
+ #root;
2126
+ #fs;
2127
+ constructor(deps) {
2128
+ this.#root = deps.root.replace(/\/+$/, "");
2129
+ this.#fs = deps.fs;
2130
+ }
2131
+ #path(runId) {
2132
+ return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
2133
+ }
2134
+ async save(snapshot) {
2135
+ await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
2136
+ }
2137
+ async load(runId) {
2138
+ const path = this.#path(runId);
2139
+ if (!await this.#fs.exists(path)) return void 0;
2140
+ let parsed;
2141
+ try {
2142
+ parsed = JSON.parse(await this.#fs.readText(path));
2143
+ } catch (e) {
2144
+ await this.#fs.remove(path).catch(() => void 0);
2145
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
2146
+ }
2147
+ const snapshot = parsed;
2148
+ if (typeof snapshot !== "object" || snapshot === null || snapshot.runId !== runId) {
2149
+ await this.#fs.remove(path).catch(() => void 0);
2150
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
2151
+ }
2152
+ return snapshot;
2153
+ }
2154
+ async delete(runId) {
2155
+ const path = this.#path(runId);
2156
+ if (await this.#fs.exists(path)) await this.#fs.remove(path);
2157
+ }
2158
+ async list() {
2159
+ if (!await this.#fs.exists(this.#root)) return [];
2160
+ const out = [];
2161
+ for (const entry of await this.#fs.list(this.#root)) {
2162
+ if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
2163
+ try {
2164
+ out.push(JSON.parse(await this.#fs.readText(entry.path)));
2165
+ } catch {}
2166
+ }
2167
+ return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
2168
+ }
2169
+ };
1922
2170
  /** runtime 门面:组合 discovery / router / agent loop / lifecycle */
1923
2171
  var WebSkillRuntime = class {
1924
2172
  #deps;
@@ -1982,7 +2230,8 @@ var WebSkillRuntime = class {
1982
2230
  longTerm: this.#deps.longTerm,
1983
2231
  externalTools: this.#deps.externalTools,
1984
2232
  skillProviders: this.#deps.skillProviders,
1985
- catalogFilter: this.#deps.catalogFilter
2233
+ catalogFilter: this.#deps.catalogFilter,
2234
+ snapshotStore: this.#deps.snapshotStore
1986
2235
  }, this.#deps.config).run({
1987
2236
  sessionId: this.#session.id,
1988
2237
  userPrompt,
@@ -2002,7 +2251,78 @@ var WebSkillRuntime = class {
2002
2251
  });
2003
2252
  return result;
2004
2253
  }
2254
+ /** D3:列出可恢复的 interrupted run(供 UI 展示"未完成任务") */
2255
+ async listInterruptedRuns() {
2256
+ if (!this.#deps.snapshotStore) return [];
2257
+ return this.#deps.snapshotStore.list();
2258
+ }
2259
+ /**
2260
+ * D3 恢复 interrupted run:
2261
+ * 不存在 → RUN_SNAPSHOT_NOT_FOUND;schemaVersion 不匹配 → 删除 + RUN_SNAPSHOT_INCOMPATIBLE;
2262
+ * 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
2263
+ */
2264
+ async resumeRun(runId) {
2265
+ const store = this.#deps.snapshotStore;
2266
+ const snapshot = store ? await store.load(runId) : void 0;
2267
+ if (!snapshot) throw new WebSkillError("RUN_SNAPSHOT_NOT_FOUND", `No snapshot found for run "${runId}"`);
2268
+ if (snapshot.schemaVersion !== 1) {
2269
+ await store.delete(runId);
2270
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has incompatible schemaVersion ${String(snapshot.schemaVersion)}`);
2271
+ }
2272
+ if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
2273
+ await store.delete(runId);
2274
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2275
+ return {
2276
+ output: "Interaction timed out before the run was resumed",
2277
+ run: {
2278
+ id: runId,
2279
+ sessionId: snapshot.sessionId,
2280
+ status: "failed",
2281
+ phase: "fail",
2282
+ userPrompt: snapshot.userPrompt,
2283
+ startedAt: snapshot.startedAt,
2284
+ endedAt,
2285
+ terminationReason: "interaction-timeout",
2286
+ activeSkillNames: snapshot.activeSkillNames,
2287
+ artifacts: [],
2288
+ trace: [{
2289
+ id: `evt-expired-${runId}`,
2290
+ runId,
2291
+ ts: endedAt,
2292
+ type: "run.failed",
2293
+ message: `Interaction timed out before the run was resumed (expired at ${snapshot.interactionExpiresAt})`,
2294
+ data: {
2295
+ reason: "interaction-timeout",
2296
+ code: "RUN_INTERACTION_TIMEOUT"
2297
+ }
2298
+ }]
2299
+ }
2300
+ };
2301
+ }
2302
+ if (!this.#catalogCache) await this.discover();
2303
+ const cache = this.#catalogCache;
2304
+ if (!cache) throw new WebSkillError("RUN_FAILED", "discover() did not populate the catalog cache");
2305
+ return new AgentLoop({
2306
+ llm: this.#deps.llm,
2307
+ executor: this.#deps.executor,
2308
+ schemaInferer: this.#deps.schemaInferer,
2309
+ artifactStore: this.#deps.artifactStore,
2310
+ fs: this.#deps.fs,
2311
+ skillIndex: cache.index,
2312
+ model: this.#deps.model,
2313
+ uiBridge: this.#deps.uiBridge,
2314
+ interaction: this.#deps.interaction,
2315
+ memory: this.#deps.memory,
2316
+ hooks: this.#deps.hooks,
2317
+ eventBus: this.#events,
2318
+ longTerm: this.#deps.longTerm,
2319
+ externalTools: this.#deps.externalTools,
2320
+ skillProviders: this.#deps.skillProviders,
2321
+ catalogFilter: this.#deps.catalogFilter,
2322
+ snapshotStore: store
2323
+ }, this.#deps.config).resume(snapshot);
2324
+ }
2005
2325
  };
2006
2326
 
2007
2327
  //#endregion
2008
- 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 };
2328
+ export { renderCatalogJson as $, parseBridgeRequest as A, SkillDiscovery as B, bridgeError as C, fromVercelStreamPart as D, fromVercelResult as E, MemoryFS as F, checkSkillRules as G, WebSkillError as H, SKILLS_LOCKFILE as I, isValidSkillName as J, computeDigest as K, SKILL_MANIFEST_FILE as L, schemaToForm as M, toLlmToolSpec as N, mergeCatalogEntries as O, toVercelToolSpecs as P, renderAvailableSkillsXml as Q, SKILL_NAME_MAX_LENGTH as R, WebSkillRuntime as S, createScriptContext as T, buildCatalog as U, SkillReader as V, buildManifest as W, normalizePath as X, jsonRenderer as Y, parseSkillMarkdown as Z, READ_SKILL_FILE_INPUT_SCHEMA as _, EventBus as a, RUN_SNAPSHOT_SCHEMA_VERSION as b, FsRunSnapshotStore as c, InMemoryStore as d, resolveInsideRoot as et, MemoryArtifactStore as f, ProgressiveRouter as g, OpenAiCompatibleClient as h, AgentLoop as i, resolveToolName as j, normalizeToolContent as k, FullDisclosureRouter as l, MockUiBridge as m, ASK_USER_TOOL as n, verifyManifest as nt, FsArtifactStore as o, MockLlmClient as p, escapeXml as q, ASK_USER_TOOL_NAME as r, xmlRenderer as rt, FsMemoryStore as s, ASK_USER_INPUT_SCHEMA as t, validateSkills as tt, HookRunner as u, READ_SKILL_FILE_TOOL as v, buildRenderResult as w, TraceRecorder as x, READ_SKILL_FILE_TOOL_NAME as y, SKILL_NAME_PATTERN as z };
@@ -1,5 +1,5 @@
1
- import { Nt as FileSystemProvider, gt as WebSkillRuntime, j as LlmMessage, k as LlmClient, mt as UiBridge, tt as RuntimeRun, zt as SkillCatalogEntry } from "./index-j3Z3hbDk.js";
2
- import { m as SkillManifest, p as SkillManager } from "./index-DF5ea732.js";
1
+ import { A as LlmClient, Gt as SkillCatalogEntry, Lt as FileSystemProvider, M as LlmMessage, Zt as SkillManifest, at as RuntimeRun, bt as WebSkillRuntime, vt as UiBridge } from "./index-CqDIreSE.js";
2
+ import { d as SkillManager } from "./index-BsbfEiaC.js";
3
3
  //#region ../governance/dist/index.d.ts
4
4
  //#region src/types.d.ts
5
5
  type CandidateStatus = 'draft' | 'pending-review' | 'approved' | 'published' | 'rejected';
@@ -1,5 +1,5 @@
1
- import { H as isValidSkillName, J as resolveInsideRoot, R as WebSkillError, Y as validateSkills } from "./dist-BbyIk4vG.js";
2
- import { i as NodeFS } from "./dist-BMioPPri.js";
1
+ import { H as WebSkillError, J as isValidSkillName, et as resolveInsideRoot, tt as validateSkills } from "./dist-COsE72Ct.js";
2
+ import { i as NodeFS } from "./dist-Bvhbxrrt.js";
3
3
  import path from "node:path";
4
4
  import { mkdtemp } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";