@webskill/sdk 0.1.4 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import { C as renderAvailableSkillsXml, E as validateSkills, T as resolveInsideRoot, c as SkillReader, d as buildCatalog, l as WebSkillError, s as SkillDiscovery, u as assertSafePathSegment, x as parseSkillMarkdown } from "./dist-DvoBsChX.js";
1
+ import { C as parseSkillMarkdown, E as renderAvailableSkillsXml, c as SkillDiscovery, d as assertSafePathSegment, j as validateSkills, k as resolveInsideRoot, l as SkillReader, p as buildCatalog, u as WebSkillError } from "./dist-D0saNPi_.js";
2
2
  import { t as MemoryArtifactStore } from "./memoryArtifactStore-C9lFVqPF-yFz6yJj0.js";
3
3
 
4
4
  //#region ../runtime/dist/index.js
@@ -986,6 +986,41 @@ var EventBus = class {
986
986
  for (const listener of this.#listeners.get("*") ?? []) listener(frozen);
987
987
  }
988
988
  };
989
+ /**
990
+ * per-scope 串行化 MemoryStore 装饰器:同一 scope 的 get/set/delete/list
991
+ * 按 promise 链串行(并发 run 的 read-modify-write 不丢计数)。
992
+ */
993
+ var SerializingMemoryStore = class {
994
+ #inner;
995
+ #chains = /* @__PURE__ */ new Map();
996
+ constructor(inner) {
997
+ this.#inner = inner;
998
+ }
999
+ #serialize(scope, fn) {
1000
+ const next = (this.#chains.get(scope) ?? Promise.resolve()).then(fn, fn);
1001
+ this.#chains.set(scope, next.catch(() => void 0));
1002
+ return next;
1003
+ }
1004
+ get(scope, key) {
1005
+ return this.#serialize(scope, () => this.#inner.get(scope, key));
1006
+ }
1007
+ set(scope, key, value) {
1008
+ return this.#serialize(scope, () => this.#inner.set(scope, key, value));
1009
+ }
1010
+ delete(scope, key) {
1011
+ return this.#serialize(scope, () => this.#inner.delete(scope, key));
1012
+ }
1013
+ list(scope) {
1014
+ return this.#serialize(scope, () => this.#inner.list(scope));
1015
+ }
1016
+ clear(scope) {
1017
+ return this.#serialize(scope ?? "", () => this.#inner.clear(scope));
1018
+ }
1019
+ /** scope 级原子段:read-modify-write 全段在 promise 链内串行执行(fn 收到底层 store) */
1020
+ transaction(scope, fn) {
1021
+ return this.#serialize(scope, () => fn(this.#inner));
1022
+ }
1023
+ };
989
1024
  /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
990
1025
  var TraceRecorder = class {
991
1026
  #runId;
@@ -1050,6 +1085,7 @@ var AgentLoop = class {
1050
1085
  maxTurns: config.maxTurns ?? 10,
1051
1086
  totalTimeoutMs: config.totalTimeoutMs ?? 12e4,
1052
1087
  toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
1088
+ toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
1053
1089
  temperature: config.temperature,
1054
1090
  renderResult: config.renderResult
1055
1091
  };
@@ -1060,8 +1096,6 @@ var AgentLoop = class {
1060
1096
  };
1061
1097
  }
1062
1098
  async run(input) {
1063
- this.#deps.llm;
1064
- this.#deps.artifactStore;
1065
1099
  const now = this.#deps.clock?.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1066
1100
  const runId = input.runId ?? `run-${Math.random().toString(36).slice(2, 10)}`;
1067
1101
  const trace = new TraceRecorder(runId, this.#deps.clock);
@@ -1089,7 +1123,12 @@ var AgentLoop = class {
1089
1123
  interactionSeq: 0,
1090
1124
  messages: [],
1091
1125
  turn: 0,
1092
- renderBlocks: []
1126
+ renderBlocks: [],
1127
+ startMs,
1128
+ pausedMs: 0,
1129
+ maxTurns: this.#config.maxTurns,
1130
+ totalTimeoutMs: this.#config.totalTimeoutMs,
1131
+ controller: new AbortController()
1093
1132
  };
1094
1133
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1095
1134
  const route = this.#deps.catalogFilter ? {
@@ -1109,29 +1148,54 @@ var AgentLoop = class {
1109
1148
  return [];
1110
1149
  }
1111
1150
  }))).flat();
1112
- state.messages = [{
1113
- role: "system",
1114
- content: route.systemPrompt
1115
- }, {
1116
- role: "user",
1117
- content: input.userPrompt
1118
- }];
1151
+ state.messages = [
1152
+ {
1153
+ role: "system",
1154
+ content: route.systemPrompt
1155
+ },
1156
+ ...(input.history ?? []).map((m) => ({ ...m })),
1157
+ {
1158
+ role: "user",
1159
+ content: input.userPrompt
1160
+ }
1161
+ ];
1119
1162
  try {
1120
- return await this.#turnLoop(state, startMs, 1, externalSpecs);
1163
+ return await this.#turnLoop(state, 1, externalSpecs);
1121
1164
  } catch (e) {
1122
1165
  if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
1123
1166
  throw e;
1124
1167
  }
1125
1168
  }
1169
+ /** 总时长消耗(wall - 交互等待);交互期间不计入 totalTimeout */
1170
+ #elapsed(state) {
1171
+ return Date.parse(state.now()) - state.startMs - state.pausedMs;
1172
+ }
1173
+ /** 重设硬期限定时器(剩余 = totalTimeoutMs - 已消耗;耗尽立即 abort) */
1174
+ #armDeadline(state) {
1175
+ if (state.timer) clearTimeout(state.timer);
1176
+ const remaining = state.totalTimeoutMs - this.#elapsed(state);
1177
+ if (remaining <= 0) {
1178
+ state.controller.abort();
1179
+ return;
1180
+ }
1181
+ state.timer = setTimeout(() => state.controller.abort(), remaining);
1182
+ }
1183
+ #disarmDeadline(state) {
1184
+ if (state.timer) {
1185
+ clearTimeout(state.timer);
1186
+ state.timer = void 0;
1187
+ }
1188
+ }
1126
1189
  /** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
1127
- async #turnLoop(state, startMs, startTurn, externalSpecs) {
1190
+ async #turnLoop(state, startTurn, externalSpecs) {
1128
1191
  const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
1129
1192
  const { llm } = this.#deps;
1130
1193
  const { trace, messages } = state;
1194
+ this.#armDeadline(state);
1131
1195
  for (let turn = startTurn;; turn++) {
1132
1196
  state.turn = turn;
1133
- if (turn > this.#config.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${this.#config.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1134
- 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");
1197
+ if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1198
+ if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1135
1199
  const toolSpecs = [
1136
1200
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
1137
1201
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
@@ -1149,14 +1213,17 @@ var AgentLoop = class {
1149
1213
  model: this.#deps.model,
1150
1214
  messages,
1151
1215
  tools: toolSpecs,
1152
- temperature: this.#config.temperature
1216
+ temperature: this.#config.temperature,
1217
+ signal: state.controller.signal
1153
1218
  }) : await llm.complete({
1154
1219
  model: this.#deps.model,
1155
1220
  messages,
1156
1221
  tools: toolSpecs,
1157
- temperature: this.#config.temperature
1222
+ temperature: this.#config.temperature,
1223
+ signal: state.controller.signal
1158
1224
  });
1159
1225
  } catch (e) {
1226
+ if (state.controller.signal.aborted) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1160
1227
  const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
1161
1228
  return finish("failed", "llm-error", messageOf$1(e), code);
1162
1229
  }
@@ -1165,7 +1232,13 @@ var AgentLoop = class {
1165
1232
  hasToolCalls: Boolean(response.toolCalls?.length),
1166
1233
  contentLength: response.content?.length ?? 0
1167
1234
  } });
1168
- if (!response.toolCalls?.length) return finish("completed", "final-answer", response.content ?? "");
1235
+ if (!response.toolCalls?.length) {
1236
+ messages.push({
1237
+ role: "assistant",
1238
+ content: response.content ?? ""
1239
+ });
1240
+ return finish("completed", "final-answer", response.content ?? "");
1241
+ }
1169
1242
  await this.#lifecycle("execute", state, { turn });
1170
1243
  messages.push({
1171
1244
  role: "assistant",
@@ -1183,13 +1256,14 @@ var AgentLoop = class {
1183
1256
  messages.push({
1184
1257
  role: "tool",
1185
1258
  toolCallId: call.id,
1186
- content: JSON.stringify(result)
1259
+ content: await this.#serializeToolResult(call, result, state)
1187
1260
  });
1188
1261
  }
1189
1262
  }
1190
1263
  }
1191
1264
  /** 统一终态处理:completed/failed/cancelled;终态即删快照(快照是续命机制,审计归治理) */
1192
1265
  async #finish(state, status, reason, output, errorCode) {
1266
+ this.#disarmDeadline(state);
1193
1267
  const { run, trace } = state;
1194
1268
  run.status = status;
1195
1269
  run.terminationReason = reason;
@@ -1229,7 +1303,8 @@ var AgentLoop = class {
1229
1303
  run.trace = trace.list();
1230
1304
  return {
1231
1305
  output,
1232
- run
1306
+ run,
1307
+ messages: state.messages.map((m) => ({ ...m }))
1233
1308
  };
1234
1309
  }
1235
1310
  /** D3 快照写入(含 pendingInteraction 与过期时间;写失败降级 run.warning) */
@@ -1248,10 +1323,12 @@ var AgentLoop = class {
1248
1323
  activatedTools: [...state.activatedTools.values()],
1249
1324
  pendingInteraction: request,
1250
1325
  interactionExpiresAt: state.run.interruptExpiresAt,
1326
+ renderBlocks: [...state.renderBlocks],
1327
+ interactionSeq: state.interactionSeq,
1251
1328
  config: {
1252
- maxTurns: this.#config.maxTurns,
1253
- totalTimeoutMs: this.#config.totalTimeoutMs,
1254
- toolTimeoutMs: this.#config.toolTimeoutMs,
1329
+ maxTurns: state.maxTurns,
1330
+ totalTimeoutMs: state.totalTimeoutMs,
1331
+ toolTimeoutMs: state.toolTimeoutMs,
1255
1332
  ...this.#config.temperature !== void 0 ? { temperature: this.#config.temperature } : {}
1256
1333
  },
1257
1334
  snapshotAt: state.now()
@@ -1290,10 +1367,15 @@ var AgentLoop = class {
1290
1367
  activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
1291
1368
  toolTimeoutMs: snapshot.config.toolTimeoutMs,
1292
1369
  now,
1293
- interactionSeq: 0,
1370
+ interactionSeq: snapshot.interactionSeq ?? 0,
1294
1371
  messages: snapshot.messages.map((m) => ({ ...m })),
1295
1372
  turn: snapshot.turn,
1296
- renderBlocks: []
1373
+ renderBlocks: (snapshot.renderBlocks ?? []).map((b) => ({ ...b })),
1374
+ startMs,
1375
+ pausedMs: 0,
1376
+ maxTurns: snapshot.config.maxTurns,
1377
+ totalTimeoutMs: snapshot.config.totalTimeoutMs,
1378
+ controller: new AbortController()
1297
1379
  };
1298
1380
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1299
1381
  trace.record("run.resumed", { data: {
@@ -1364,7 +1446,7 @@ var AgentLoop = class {
1364
1446
  }
1365
1447
  }))).flat();
1366
1448
  try {
1367
- return await this.#turnLoop(state, startMs, snapshot.turn + 1, externalSpecs);
1449
+ return await this.#turnLoop(state, snapshot.turn + 1, externalSpecs);
1368
1450
  } catch (e) {
1369
1451
  if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
1370
1452
  throw e;
@@ -1431,8 +1513,18 @@ var AgentLoop = class {
1431
1513
  * 取消 → RunTerminated(cancelled);超时 → RunTerminated(interaction-timeout)。
1432
1514
  */
1433
1515
  async #interact(state, request, traceData) {
1516
+ if (!this.#deps.uiBridge) throw new Error("interact() requires a UiBridge");
1517
+ this.#disarmDeadline(state);
1518
+ const waitStartMs = Date.parse(state.now());
1519
+ try {
1520
+ return await this.#interactInner(state, request, traceData);
1521
+ } finally {
1522
+ state.pausedMs += Date.parse(state.now()) - waitStartMs;
1523
+ this.#armDeadline(state);
1524
+ }
1525
+ }
1526
+ async #interactInner(state, request, traceData) {
1434
1527
  const bridge = this.#deps.uiBridge;
1435
- if (!bridge) throw new Error("interact() requires a UiBridge");
1436
1528
  const { run } = state;
1437
1529
  run.status = "interrupted";
1438
1530
  run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
@@ -1448,7 +1540,7 @@ var AgentLoop = class {
1448
1540
  } });
1449
1541
  let response;
1450
1542
  try {
1451
- response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs);
1543
+ response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs, () => bridge.cancel?.(request.id));
1452
1544
  } catch (e) {
1453
1545
  run.status = "running";
1454
1546
  run.interruptExpiresAt = void 0;
@@ -1477,9 +1569,14 @@ var AgentLoop = class {
1477
1569
  await this.#appendParamHistory(state, request, response.value);
1478
1570
  return response.value;
1479
1571
  }
1480
- #withInteractionTimeout(promise, timeoutMs) {
1572
+ #withInteractionTimeout(promise, timeoutMs, onTimeout) {
1481
1573
  return new Promise((resolve, reject) => {
1482
- const timer = setTimeout(() => reject(new WebSkillError("RUN_INTERACTION_TIMEOUT", `Interaction timed out after ${timeoutMs}ms`)), timeoutMs);
1574
+ const timer = setTimeout(() => {
1575
+ try {
1576
+ onTimeout?.();
1577
+ } catch {}
1578
+ reject(new WebSkillError("RUN_INTERACTION_TIMEOUT", `Interaction timed out after ${timeoutMs}ms`));
1579
+ }, timeoutMs);
1483
1580
  promise.then((v) => {
1484
1581
  clearTimeout(timer);
1485
1582
  resolve(v);
@@ -1560,6 +1657,7 @@ var AgentLoop = class {
1560
1657
  if (typeof pathArg !== "string") return toolError("TOOL_EXECUTION_FAILED", "read_skill_file \"path\" must be a string");
1561
1658
  if (skillName.startsWith("mcp://")) return this.#handleReadExternalSkill(skillName, pathArg, state);
1562
1659
  try {
1660
+ if (await this.#guardDenied("canRead", skillName)) return toolError("SKILL_DISABLED", `Skill "${skillName}" is blocked by the skill state guard (read)`);
1563
1661
  const text = await state.reader.readSkillFile(skillName, pathArg);
1564
1662
  let note = "";
1565
1663
  if (pathArg === "SKILL.md" && !state.activated.has(skillName)) note = await this.#activateSkill(skillName, state, void 0, text);
@@ -1618,8 +1716,35 @@ var AgentLoop = class {
1618
1716
  const detail = failures.length > 0 ? ` (provider errors: ${failures.join("; ")})` : "";
1619
1717
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
1620
1718
  }
1719
+ /**
1720
+ * 工具结果回喂序列化:超过 toolResultMaxBytes(默认 100KB)时头尾保留截断,
1721
+ * 完整内容经 artifactStore 落 artifact,回喂摘要含 artifact id。
1722
+ */
1723
+ async #serializeToolResult(call, result, state) {
1724
+ const text = JSON.stringify(result);
1725
+ const max = this.#config.toolResultMaxBytes;
1726
+ if (text.length <= max) return text;
1727
+ const artifact = await this.#deps.artifactStore.createTextArtifact({
1728
+ runId: state.runId,
1729
+ path: `tool-results/${call.id}.json`,
1730
+ content: text
1731
+ });
1732
+ const head = text.slice(0, Math.floor(max * .6));
1733
+ const tail = text.slice(-Math.floor(max * .3));
1734
+ return `${head}\n...[truncated ${text.length - head.length - tail.length} chars; full tool result saved as artifact "${artifact.id}"]...\n${tail}`;
1735
+ }
1736
+ /** SkillStateGuard 判定(无注入默认全放行;仅显式 false 拦截) */
1737
+ async #guardDenied(kind, skillName) {
1738
+ const check = this.#deps.skillStateGuard?.[kind];
1739
+ if (!check) return false;
1740
+ return await check(skillName) === false;
1741
+ }
1621
1742
  /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
1622
1743
  async #activateSkill(skillName, state, via, skillMdText) {
1744
+ if (await this.#guardDenied("canActivate", skillName)) {
1745
+ state.trace.record("run.warning", { message: `Skill "${skillName}" is blocked by the skill state guard (activate)` });
1746
+ return "";
1747
+ }
1623
1748
  state.activated.add(skillName);
1624
1749
  state.trace.record("skill.activated", { data: {
1625
1750
  skillName,
@@ -1644,7 +1769,7 @@ var AgentLoop = class {
1644
1769
  const executor = this.#deps.executor;
1645
1770
  const loaded = [];
1646
1771
  if (executor) {
1647
- let scriptFiles = [];
1772
+ let scriptFiles;
1648
1773
  try {
1649
1774
  scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
1650
1775
  } catch {
@@ -1692,8 +1817,12 @@ var AgentLoop = class {
1692
1817
  for (const ext of ["ts", "js"]) {
1693
1818
  const scriptPath = `${skillRoot}/scripts/${scriptName}.${ext}`;
1694
1819
  if (!await this.#deps.fs.exists(scriptPath)) continue;
1695
- const inferred = this.#deps.schemaInferer.inferSchemaFromSource(await this.#deps.fs.readText(scriptPath), { fileName: `${scriptName}.${ext}` });
1696
- if (inferred) def.inputSchema = inferred;
1820
+ try {
1821
+ const inferred = this.#deps.schemaInferer.inferSchemaFromSource(await this.#deps.fs.readText(scriptPath), { fileName: `${scriptName}.${ext}` });
1822
+ if (inferred) def.inputSchema = inferred;
1823
+ } catch (e) {
1824
+ state.trace.record("run.warning", { message: `Schema inference failed for ${scriptPath}: ${messageOf$1(e)}` });
1825
+ }
1697
1826
  return;
1698
1827
  }
1699
1828
  }
@@ -1735,12 +1864,14 @@ var AgentLoop = class {
1735
1864
  onArtifactCreated: (artifact) => createdIds.add(artifact.id)
1736
1865
  });
1737
1866
  try {
1867
+ if (await this.#guardDenied("canExecute", skillName)) return toolError("SKILL_DISABLED", `Skill "${skillName}" is blocked by the skill state guard (execute)`);
1868
+ const remainingMs = state.totalTimeoutMs - this.#elapsed(state);
1738
1869
  const result = await this.#deps.executor.execute({
1739
1870
  skillRoot: root,
1740
1871
  scriptName,
1741
1872
  args,
1742
1873
  context,
1743
- timeoutMs: state.toolTimeoutMs
1874
+ timeoutMs: Math.max(1, Math.min(state.toolTimeoutMs, remainingMs))
1744
1875
  });
1745
1876
  if (createdIds.size > 0) {
1746
1877
  const fresh = (await this.#deps.artifactStore.listArtifacts(state.runId)).filter((a) => createdIds.has(a.id));
@@ -1793,6 +1924,22 @@ var AgentLoop = class {
1793
1924
  state.trace.record("run.warning", { message: `Memory write failed: ${messageOf$1(e)}` });
1794
1925
  }
1795
1926
  }
1927
+ /** read-modify-write:有 transaction 实现在 scope 原子段内执行(并发不丢计数),否则顺序执行 */
1928
+ async #memoryMutate(scope, key, state, mutate) {
1929
+ const memory = this.#deps.memory;
1930
+ if (!memory) return;
1931
+ if (memory.transaction) {
1932
+ try {
1933
+ await memory.transaction(scope, async (inner) => {
1934
+ await inner.set(scope, key, mutate(await inner.get(scope, key)));
1935
+ });
1936
+ } catch (e) {
1937
+ state.trace.record("run.warning", { message: `Memory write failed: ${messageOf$1(e)}` });
1938
+ }
1939
+ return;
1940
+ }
1941
+ await this.#memorySet(scope, key, mutate(await this.#memoryGet(scope, key)), state);
1942
+ }
1796
1943
  async #writeActivationMemory(skillName, state) {
1797
1944
  if (!this.#deps.memory) return;
1798
1945
  const sessionScope = `session:${state.run.sessionId}`;
@@ -1800,33 +1947,38 @@ var AgentLoop = class {
1800
1947
  await this.#bumpSkillStat(skillName, "activations", state);
1801
1948
  if (this.#deps.longTerm?.enabled) {
1802
1949
  const userScope = `user:${this.#deps.longTerm.userId}`;
1803
- const usage = await this.#memoryGet(userScope, "skillUsage") ?? {};
1804
- usage[skillName] = (usage[skillName] ?? 0) + 1;
1805
- await this.#memorySet(userScope, "skillUsage", usage, state);
1950
+ await this.#memoryMutate(userScope, "skillUsage", state, (current) => {
1951
+ const usage = current ?? {};
1952
+ usage[skillName] = (usage[skillName] ?? 0) + 1;
1953
+ return usage;
1954
+ });
1806
1955
  }
1807
1956
  }
1808
1957
  async #bumpSkillStat(skillName, field, state) {
1809
1958
  if (!this.#deps.memory) return;
1810
- const scope = `skill:${skillName}`;
1811
- const stats = await this.#memoryGet(scope, "stats") ?? {
1812
- activations: 0,
1813
- successes: 0,
1814
- failures: 0
1815
- };
1816
- stats[field] = (stats[field] ?? 0) + 1;
1817
- await this.#memorySet(scope, "stats", stats, state);
1959
+ await this.#memoryMutate(`skill:${skillName}`, "stats", state, (current) => {
1960
+ const stats = current ?? {
1961
+ activations: 0,
1962
+ successes: 0,
1963
+ failures: 0
1964
+ };
1965
+ stats[field] = (stats[field] ?? 0) + 1;
1966
+ return stats;
1967
+ });
1818
1968
  }
1819
1969
  async #appendParamHistory(state, request, value) {
1820
1970
  if (!this.#deps.memory) return;
1821
1971
  const scope = `session:${state.run.sessionId}`;
1822
- const history = await this.#memoryGet(scope, "paramHistory") ?? [];
1823
- history.push({
1824
- ts: state.now(),
1825
- interactionId: request.id,
1826
- type: request.type,
1827
- value
1972
+ await this.#memoryMutate(scope, "paramHistory", state, (current) => {
1973
+ const history = current ?? [];
1974
+ history.push({
1975
+ ts: state.now(),
1976
+ interactionId: request.id,
1977
+ type: request.type,
1978
+ value
1979
+ });
1980
+ return history;
1828
1981
  });
1829
- await this.#memorySet(scope, "paramHistory", history, state);
1830
1982
  }
1831
1983
  };
1832
1984
  /**
@@ -1843,6 +1995,10 @@ const SNAPSHOT_SUFFIX = ".snapshot.json";
1843
1995
  /**
1844
1996
  * FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
1845
1997
  * 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
1998
+ * save/list 时顺带清理已过 interactionExpiresAt 的过期快照。
1999
+ *
2000
+ * 数据敏感性说明:快照含完整对话历史(用户输入、工具结果、可能的凭据片段),
2001
+ * 以明文 JSON 落盘于宿主提供的 fs;宿主应将其视为会话数据同等保护。
1846
2002
  * @experimental
1847
2003
  */
1848
2004
  var FsRunSnapshotStore = class {
@@ -1857,6 +2013,7 @@ var FsRunSnapshotStore = class {
1857
2013
  }
1858
2014
  async save(snapshot) {
1859
2015
  await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
2016
+ await this.#pruneExpired();
1860
2017
  }
1861
2018
  async load(runId) {
1862
2019
  const path = this.#path(runId);
@@ -1880,6 +2037,7 @@ var FsRunSnapshotStore = class {
1880
2037
  if (await this.#fs.exists(path)) await this.#fs.remove(path);
1881
2038
  }
1882
2039
  async list() {
2040
+ await this.#pruneExpired();
1883
2041
  if (!await this.#fs.exists(this.#root)) return [];
1884
2042
  const out = [];
1885
2043
  for (const entry of await this.#fs.list(this.#root)) {
@@ -1890,6 +2048,20 @@ var FsRunSnapshotStore = class {
1890
2048
  }
1891
2049
  return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
1892
2050
  }
2051
+ /** 过期快照清理(save/list 时顺带;失败静默不阻断主流程) */
2052
+ async #pruneExpired() {
2053
+ try {
2054
+ if (!await this.#fs.exists(this.#root)) return;
2055
+ const now = Date.now();
2056
+ for (const entry of await this.#fs.list(this.#root)) {
2057
+ if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
2058
+ try {
2059
+ const parsed = JSON.parse(await this.#fs.readText(entry.path));
2060
+ if (parsed.interactionExpiresAt !== void 0 && Date.parse(parsed.interactionExpiresAt) < now) await this.#fs.remove(entry.path);
2061
+ } catch {}
2062
+ }
2063
+ } catch {}
2064
+ }
1893
2065
  };
1894
2066
  /**
1895
2067
  * runtime 门面:组合 discovery / router / agent loop / lifecycle
@@ -1903,7 +2075,10 @@ var WebSkillRuntime = class {
1903
2075
  #events;
1904
2076
  #catalogCache;
1905
2077
  constructor(deps) {
1906
- this.#deps = deps;
2078
+ this.#deps = {
2079
+ ...deps,
2080
+ ...deps.memory ? { memory: new SerializingMemoryStore(deps.memory) } : {}
2081
+ };
1907
2082
  this.#discovery = new SkillDiscovery(deps.fs, deps.roots);
1908
2083
  this.#router = deps.router ?? new ProgressiveRouter();
1909
2084
  this.#events = deps.eventBus ?? new EventBus();
@@ -1919,6 +2094,34 @@ var WebSkillRuntime = class {
1919
2094
  get events() {
1920
2095
  return this.#events;
1921
2096
  }
2097
+ /**
2098
+ * Catalog 缓存失效:下次 run/discover 重新扫描技能目录。
2099
+ * 与 SkillManager onChanged 接线:`new SkillManager({ ..., onChanged: () => runtime.invalidate() })`。
2100
+ */
2101
+ invalidate() {
2102
+ this.#catalogCache = void 0;
2103
+ }
2104
+ /**
2105
+ * 多会话:session 对象的 run(prompt) 跨 run 延续消息历史(同一 session 对话上下文连续)。
2106
+ * 既有 runtime.run(prompt) 保持无状态单次语义不变。
2107
+ */
2108
+ createSession(options = {}) {
2109
+ const sessionId = options.sessionId ?? `session-${Math.random().toString(36).slice(2, 10)}`;
2110
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
2111
+ let history = [];
2112
+ return {
2113
+ id: sessionId,
2114
+ createdAt,
2115
+ run: async (prompt) => {
2116
+ const result = await this.run(prompt, {
2117
+ sessionId,
2118
+ history
2119
+ });
2120
+ history = result.messages.slice(1);
2121
+ return result;
2122
+ }
2123
+ };
2124
+ }
1922
2125
  async discover() {
1923
2126
  const result = await this.#discovery.discover();
1924
2127
  this.#catalogCache = {
@@ -1927,7 +2130,7 @@ var WebSkillRuntime = class {
1927
2130
  };
1928
2131
  return result;
1929
2132
  }
1930
- async run(userPrompt) {
2133
+ async run(userPrompt, options = {}) {
1931
2134
  if (!this.#catalogCache) await this.discover();
1932
2135
  const cache = this.#catalogCache;
1933
2136
  if (!cache) throw new Error("discover() did not populate the catalog cache");
@@ -1960,11 +2163,13 @@ var WebSkillRuntime = class {
1960
2163
  externalTools: this.#deps.externalTools,
1961
2164
  skillProviders: this.#deps.skillProviders,
1962
2165
  catalogFilter: this.#deps.catalogFilter,
1963
- snapshotStore: this.#deps.snapshotStore
2166
+ snapshotStore: this.#deps.snapshotStore,
2167
+ skillStateGuard: this.#deps.skillStateGuard
1964
2168
  }, this.#deps.config).run({
1965
- sessionId: this.#session.id,
2169
+ sessionId: options.sessionId ?? this.#session.id,
1966
2170
  userPrompt,
1967
- route
2171
+ route,
2172
+ ...options.history ? { history: options.history } : {}
1968
2173
  });
1969
2174
  for (const failure of providerFailures) result.run.trace.push({
1970
2175
  id: `evt-provider-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -2033,7 +2238,8 @@ var WebSkillRuntime = class {
2033
2238
  code: "RUN_INTERACTION_TIMEOUT"
2034
2239
  }
2035
2240
  }]
2036
- }
2241
+ },
2242
+ messages: []
2037
2243
  };
2038
2244
  }
2039
2245
  if (!this.#catalogCache) await this.discover();
@@ -2056,7 +2262,8 @@ var WebSkillRuntime = class {
2056
2262
  externalTools: this.#deps.externalTools,
2057
2263
  skillProviders: this.#deps.skillProviders,
2058
2264
  catalogFilter: this.#deps.catalogFilter,
2059
- snapshotStore: store
2265
+ snapshotStore: store,
2266
+ skillStateGuard: this.#deps.skillStateGuard
2060
2267
  }, this.#deps.config).resume(snapshot);
2061
2268
  }
2062
2269
  };
@@ -2134,7 +2341,7 @@ function createWebSkillApi(deps) {
2134
2341
  }
2135
2342
  };
2136
2343
  }
2137
- /** install URL 分派:http(s)…​.zip → http;…​.git → git(无 git 能力的 manager 自行 TOOL_UNSUPPORTED);
2344
+ /** install URL 分派:http(s) .zip → http;.git → git(无 git 能力的 manager 自行 TOOL_UNSUPPORTED);
2138
2345
  * 其余 http(s) URL → INSTALL_FAILED(不静默当 local);非 URL → local */
2139
2346
  function sourceFromUrl(url) {
2140
2347
  const bare = url.split("?")[0] ?? url;
@@ -2422,16 +2629,21 @@ function networkUrlHost(url) {
2422
2629
  * once-per-run(默认)在 run 内记忆已批准能力,every-call 每次调用都问。
2423
2630
  * @stable
2424
2631
  */
2425
- var CapabilityApproval = class {
2632
+ var CapabilityApproval = class CapabilityApproval {
2426
2633
  #uiBridge;
2427
2634
  #scope;
2428
- /** runId → 已批准能力集合(once-per-run 记忆) */
2635
+ /** runId → 已批准能力集合(once-per-run 记忆;LRU 上限防长跑进程膨胀) */
2429
2636
  #approved = /* @__PURE__ */ new Map();
2430
2637
  #seq = 0;
2638
+ static #MAX_RUNS = 128;
2431
2639
  constructor(deps = {}) {
2432
2640
  this.#uiBridge = deps.uiBridge;
2433
2641
  this.#scope = deps.scope ?? "once-per-run";
2434
2642
  }
2643
+ /** run 终态清理授权记录(宿主在 run 结束后调用;同时有 LRU 上限兜底) */
2644
+ clearRun(runId) {
2645
+ this.#approved.delete(runId);
2646
+ }
2435
2647
  async authorize(input) {
2436
2648
  const { runId, capability, mode } = input;
2437
2649
  if (mode === false) return "disabled";
@@ -2452,10 +2664,14 @@ var CapabilityApproval = class {
2452
2664
  this.#approved.set(runId, set);
2453
2665
  }
2454
2666
  set.add(capability);
2667
+ if (this.#approved.size > CapabilityApproval.#MAX_RUNS) {
2668
+ const oldest = this.#approved.keys().next().value;
2669
+ if (oldest !== void 0) this.#approved.delete(oldest);
2670
+ }
2455
2671
  }
2456
2672
  return "allowed";
2457
2673
  }
2458
2674
  };
2459
2675
 
2460
2676
  //#endregion
2461
- export { mergeCatalogEntries as A, buildRenderResult as C, fromVercelResult as D, extractChartSpec as E, schemaToForm as F, toLlmToolSpec as I, toVercelToolSpecs as L, normalizeToolContent as M, parseBridgeRequest as N, fromVercelStreamPart as O, resolveToolName as P, bridgeError as S, createWebSkillApi as T, READ_SKILL_FILE_TOOL as _, AnthropicClient as a, TraceRecorder as b, FsArtifactStore as c, FullDisclosureRouter as d, GoogleGenAiClient as f, READ_SKILL_FILE_INPUT_SCHEMA as g, ProgressiveRouter as h, AgentLoop as i, networkUrlHost as j, isNetworkAllowed 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, createScriptContext as w, WebSkillRuntime as x, RUN_SNAPSHOT_SCHEMA_VERSION as y };
2677
+ export { isNetworkAllowed as A, bridgeError as C, extractChartSpec as D, createWebSkillApi as E, resolveToolName as F, schemaToForm as I, toLlmToolSpec as L, networkUrlHost as M, normalizeToolContent as N, fromVercelResult as O, parseBridgeRequest as P, toVercelToolSpecs as R, WebSkillRuntime as S, createScriptContext as T, 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, mergeCatalogEntries as j, fromVercelStreamPart 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 };