@webskill/sdk 0.2.6 → 0.2.8

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.
@@ -125,13 +125,17 @@ var OpenAiCompatibleClient = class {
125
125
  type: "tool-calls",
126
126
  toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
127
127
  let args = {};
128
+ let parseError;
128
129
  try {
129
130
  args = JSON.parse(acc.arguments || "{}");
130
- } catch {}
131
+ } catch (e) {
132
+ parseError = e instanceof Error ? e.message : String(e);
133
+ }
131
134
  return {
132
135
  id: acc.id || `call-${index}`,
133
136
  name: acc.name,
134
- arguments: args
137
+ arguments: args,
138
+ ...parseError ? { argumentsParseError: parseError } : {}
135
139
  };
136
140
  })
137
141
  };
@@ -358,13 +362,17 @@ var AnthropicClient = class {
358
362
  type: "tool-calls",
359
363
  toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
360
364
  let args = {};
365
+ let parseError;
361
366
  try {
362
367
  args = JSON.parse(acc.arguments || "{}");
363
- } catch {}
368
+ } catch (e) {
369
+ parseError = e instanceof Error ? e.message : String(e);
370
+ }
364
371
  return {
365
372
  id: acc.id || `call-${index}`,
366
373
  name: acc.name,
367
- arguments: args
374
+ arguments: args,
375
+ ...parseError ? { argumentsParseError: parseError } : {}
368
376
  };
369
377
  })
370
378
  };
@@ -875,22 +883,22 @@ function createScriptContext(deps) {
875
883
  ...onWarning ? { onWarning } : {}
876
884
  };
877
885
  }
878
- const isRecord$1 = (v) => typeof v === "object" && v !== null;
886
+ const isRecord$2 = (v) => typeof v === "object" && v !== null;
879
887
  /**
880
888
  * $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
881
889
  * 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
882
890
  */
883
891
  function extractChartSpec(data) {
884
- if (!isRecord$1(data)) return void 0;
892
+ if (!isRecord$2(data)) return void 0;
885
893
  const raw = data["$chart"];
886
- if (!isRecord$1(raw)) return void 0;
894
+ if (!isRecord$2(raw)) return void 0;
887
895
  const { kind, labels, series } = raw;
888
896
  if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
889
897
  if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
890
898
  if (!Array.isArray(series)) return void 0;
891
899
  const validSeries = [];
892
900
  for (const item of series) {
893
- if (!isRecord$1(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
901
+ if (!isRecord$2(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
894
902
  const name = item["name"];
895
903
  validSeries.push({
896
904
  ...typeof name === "string" ? { name } : {},
@@ -928,6 +936,212 @@ function buildRenderResult(run, output, renderBlocks = []) {
928
936
  artifacts: run.artifacts
929
937
  };
930
938
  }
939
+ const MAX_SURFACE_BYTES = 256 * 1024;
940
+ const MAX_ACTIONS = 32;
941
+ const MAX_FORM_FIELDS = 64;
942
+ const MAX_OPTIONS = 256;
943
+ const MAX_TABLE_COLUMNS = 128;
944
+ const MAX_TABLE_ROWS = 1e4;
945
+ const MAX_CHART_POINTS = 2e4;
946
+ const MAX_JSON_DEPTH = 16;
947
+ const MAX_PATCH_OPERATIONS = 128;
948
+ const actionIntents = /* @__PURE__ */ new Set([
949
+ "submit",
950
+ "cancel",
951
+ "select",
952
+ "download",
953
+ "refresh"
954
+ ]);
955
+ const fieldTypes = /* @__PURE__ */ new Set([
956
+ "text",
957
+ "number",
958
+ "date",
959
+ "textarea",
960
+ "select",
961
+ "multi-select",
962
+ "toggle",
963
+ "file"
964
+ ]);
965
+ function isRecord$1(value) {
966
+ return typeof value === "object" && value !== null && !Array.isArray(value);
967
+ }
968
+ function reject(message) {
969
+ throw new WebSkillError("VALIDATION_FAILED", message);
970
+ }
971
+ function requireString(value, name) {
972
+ if (typeof value !== "string" || value.trim() === "") reject(`${name} must be a non-empty string`);
973
+ }
974
+ function requireNonNegativeInteger(value, name) {
975
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) reject(`${name} must be a non-negative integer`);
976
+ }
977
+ function isJsonValue(value, depth = 0) {
978
+ if (depth > MAX_JSON_DEPTH || value === void 0 || typeof value === "function" || typeof value === "symbol") return false;
979
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
980
+ if (typeof value === "number") return Number.isFinite(value);
981
+ if (Array.isArray(value)) return value.every((item) => isJsonValue(item, depth + 1));
982
+ if (!isRecord$1(value)) return false;
983
+ return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
984
+ }
985
+ function assertActions(value) {
986
+ if (value === void 0) return;
987
+ if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
988
+ for (const action of value) {
989
+ if (!isRecord$1(action)) reject("A surface action must be an object");
990
+ requireString(action["id"], "Surface action ID");
991
+ requireString(action["label"], "Surface action label");
992
+ if (typeof action["intent"] !== "string" || !actionIntents.has(action["intent"])) reject("Surface action intent is invalid");
993
+ if (action["disabled"] !== void 0 && typeof action["disabled"] !== "boolean") reject("Surface action disabled must be a boolean");
994
+ if (action["awaitResponse"] !== void 0 && typeof action["awaitResponse"] !== "boolean") reject("Surface action awaitResponse must be a boolean");
995
+ if (action["nonce"] !== void 0 && (typeof action["nonce"] !== "string" || action["nonce"] === "")) reject("Surface action nonce must be a non-empty string");
996
+ }
997
+ }
998
+ function assertForm(surface) {
999
+ if (!Array.isArray(surface["fields"]) || surface["fields"].length > MAX_FORM_FIELDS) reject(`A form surface must contain at most ${MAX_FORM_FIELDS} fields`);
1000
+ for (const field of surface["fields"]) {
1001
+ if (!isRecord$1(field)) reject("A form field must be an object");
1002
+ requireString(field["name"], "Form field name");
1003
+ requireString(field["label"], "Form field label");
1004
+ if (typeof field["type"] !== "string" || !fieldTypes.has(field["type"])) reject("Form field type is invalid");
1005
+ if (field["required"] !== void 0 && typeof field["required"] !== "boolean") reject("Form field required must be a boolean");
1006
+ if (field["description"] !== void 0 && typeof field["description"] !== "string") reject("Form field description must be a string");
1007
+ if (field["defaultValue"] !== void 0 && !isJsonValue(field["defaultValue"])) reject("Form field defaultValue must be JSON data");
1008
+ if (field["options"] !== void 0) {
1009
+ if (!Array.isArray(field["options"]) || field["options"].length > MAX_OPTIONS) reject(`Form field options must contain at most ${MAX_OPTIONS} items`);
1010
+ for (const option of field["options"]) {
1011
+ if (!isRecord$1(option)) reject("A form option must be an object");
1012
+ requireString(option["label"], "Form option label");
1013
+ if (!isJsonValue(option["value"])) reject("Form option value must be JSON data");
1014
+ }
1015
+ }
1016
+ }
1017
+ assertActions(surface["actions"]);
1018
+ }
1019
+ function assertChart(surface) {
1020
+ const chart = surface["chart"];
1021
+ if (!isRecord$1(chart)) reject("A chart surface requires a chart object");
1022
+ if (chart["kind"] !== "bar" && chart["kind"] !== "line" && chart["kind"] !== "pie") reject("Chart kind is invalid");
1023
+ if (!Array.isArray(chart["labels"]) || !chart["labels"].every((label) => typeof label === "string")) reject("Chart labels must be an array of strings");
1024
+ if (!Array.isArray(chart["series"])) reject("Chart series must be an array");
1025
+ let points = 0;
1026
+ for (const series of chart["series"]) {
1027
+ if (!isRecord$1(series) || !Array.isArray(series["data"]) || !series["data"].every((point) => typeof point === "number" && Number.isFinite(point))) reject("Chart series data must be finite numbers");
1028
+ if (series["name"] !== void 0 && typeof series["name"] !== "string") reject("Chart series name must be a string");
1029
+ points += series["data"].length;
1030
+ }
1031
+ if (points > MAX_CHART_POINTS) reject(`Chart data exceeds the ${MAX_CHART_POINTS}-point limit`);
1032
+ assertActions(surface["actions"]);
1033
+ }
1034
+ function assertTable(surface) {
1035
+ if (!Array.isArray(surface["columns"]) || surface["columns"].length > MAX_TABLE_COLUMNS || !surface["columns"].every((column) => typeof column === "string")) reject(`Table columns must be strings and contain at most ${MAX_TABLE_COLUMNS} items`);
1036
+ if (!Array.isArray(surface["rows"]) || surface["rows"].length > MAX_TABLE_ROWS) reject(`Table rows must contain at most ${MAX_TABLE_ROWS} items`);
1037
+ for (const row of surface["rows"]) if (!Array.isArray(row) || row.length > surface["columns"].length || !row.every((cell) => isJsonValue(cell))) reject("Table rows must contain JSON cells within the declared column count");
1038
+ assertActions(surface["actions"]);
1039
+ }
1040
+ /** Validates the allowlisted, data-only shape accepted by a UI surface renderer. @experimental */
1041
+ function validateUiSurface(value) {
1042
+ if (!isRecord$1(value)) reject("A UI surface must be an object");
1043
+ requireString(value["id"], "UI surface ID");
1044
+ if (value["title"] !== void 0 && typeof value["title"] !== "string") reject("UI surface title must be a string");
1045
+ switch (value["kind"]) {
1046
+ case "form":
1047
+ assertForm(value);
1048
+ break;
1049
+ case "chart":
1050
+ assertChart(value);
1051
+ break;
1052
+ case "table":
1053
+ assertTable(value);
1054
+ break;
1055
+ case "metric":
1056
+ requireString(value["label"], "Metric label");
1057
+ if (typeof value["value"] !== "string" && (typeof value["value"] !== "number" || !Number.isFinite(value["value"]))) reject("Metric value must be a string or finite number");
1058
+ if (value["trend"] !== void 0 && value["trend"] !== "up" && value["trend"] !== "down" && value["trend"] !== "neutral") reject("Metric trend is invalid");
1059
+ break;
1060
+ case "file": {
1061
+ requireString(value["path"], "File path");
1062
+ if (value["mimeType"] !== void 0 && typeof value["mimeType"] !== "string") reject("File mimeType must be a string");
1063
+ const fileSize = value["size"];
1064
+ if (fileSize !== void 0 && (typeof fileSize !== "number" || !Number.isSafeInteger(fileSize) || fileSize < 0)) reject("File size must be a non-negative integer");
1065
+ assertActions(value["actions"]);
1066
+ break;
1067
+ }
1068
+ case "custom":
1069
+ requireString(value["component"], "Custom surface component");
1070
+ if (!isJsonValue(value["props"])) reject("Custom surface props must be JSON data");
1071
+ assertActions(value["actions"]);
1072
+ break;
1073
+ default: reject("UI surface kind is invalid");
1074
+ }
1075
+ if (!isJsonValue(value)) reject("A UI surface must contain JSON data only");
1076
+ if (JSON.stringify(value).length > MAX_SURFACE_BYTES) reject(`A UI surface exceeds the ${MAX_SURFACE_BYTES}-byte limit`);
1077
+ return structuredClone(value);
1078
+ }
1079
+ function assertPatch(value) {
1080
+ if (!isRecord$1(value)) reject("A surface patch operation must be an object");
1081
+ if (value["op"] !== "replace" && value["op"] !== "merge" && value["op"] !== "append") reject("Surface patch operation is invalid");
1082
+ requireString(value["path"], "Surface patch path");
1083
+ if (!value["path"].startsWith("/")) reject("Surface patch path must be a JSON pointer");
1084
+ if (!isJsonValue(value["value"])) reject("Surface patch value must be JSON data");
1085
+ }
1086
+ /** Validates an individual event in the framework-neutral surface stream. @experimental */
1087
+ function validateUiSurfaceEvent(value) {
1088
+ if (!isRecord$1(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
1089
+ if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
1090
+ switch (value["type"]) {
1091
+ case "open": return {
1092
+ type: "open",
1093
+ ...value["runId"] ? { runId: value["runId"] } : {},
1094
+ surface: validateUiSurface(value["surface"])
1095
+ };
1096
+ case "patch":
1097
+ requireString(value["id"], "Surface patch ID");
1098
+ requireNonNegativeInteger(value["revision"], "Surface patch revision");
1099
+ if (!Array.isArray(value["operations"]) || value["operations"].length === 0) reject("A surface patch requires operations");
1100
+ if (value["operations"].length > MAX_PATCH_OPERATIONS) reject(`A surface patch must contain at most ${MAX_PATCH_OPERATIONS} operations`);
1101
+ for (const operation of value["operations"]) assertPatch(operation);
1102
+ return {
1103
+ type: "patch",
1104
+ ...value["runId"] ? { runId: value["runId"] } : {},
1105
+ id: value["id"],
1106
+ revision: value["revision"],
1107
+ operations: structuredClone(value["operations"])
1108
+ };
1109
+ case "complete":
1110
+ requireString(value["id"], "Surface completion ID");
1111
+ requireNonNegativeInteger(value["revision"], "Surface completion revision");
1112
+ return {
1113
+ type: "complete",
1114
+ ...value["runId"] ? { runId: value["runId"] } : {},
1115
+ id: value["id"],
1116
+ revision: value["revision"]
1117
+ };
1118
+ case "error":
1119
+ requireString(value["id"], "Surface error ID");
1120
+ requireString(value["code"], "Surface error code");
1121
+ requireString(value["message"], "Surface error message");
1122
+ return {
1123
+ type: "error",
1124
+ ...value["runId"] ? { runId: value["runId"] } : {},
1125
+ id: value["id"],
1126
+ code: value["code"],
1127
+ message: value["message"]
1128
+ };
1129
+ case "cancel":
1130
+ requireString(value["id"], "Surface cancellation ID");
1131
+ return {
1132
+ type: "cancel",
1133
+ ...value["runId"] ? { runId: value["runId"] } : {},
1134
+ id: value["id"]
1135
+ };
1136
+ default: return reject("UI surface event type is invalid");
1137
+ }
1138
+ }
1139
+ /** Extracts validated surface stream events from structured tool output. @experimental */
1140
+ function extractUiSurfaceEvents(data) {
1141
+ if (!isRecord$1(data) || data["$surface"] === void 0) return [];
1142
+ const raw = data["$surface"];
1143
+ return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSurfaceEvent(event));
1144
+ }
931
1145
  /**
932
1146
  * JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
933
1147
  * providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
@@ -1073,6 +1287,7 @@ var TraceRecorder = class {
1073
1287
  return [...this.#events];
1074
1288
  }
1075
1289
  };
1290
+ const MAX_SURFACE_PATCHES_PER_SECOND = 240;
1076
1291
  /** 交互终态(取消/超时):从工具执行深处直接终止 run */
1077
1292
  var RunTerminated = class extends Error {
1078
1293
  outcome;
@@ -1097,6 +1312,7 @@ const summarizeArgs = (args) => {
1097
1312
  const json = JSON.stringify(args);
1098
1313
  return json.length > 100 ? `${json.slice(0, 100)}…` : json;
1099
1314
  };
1315
+ const surfaceActions = (surface) => "actions" in surface ? surface.actions ?? [] : [];
1100
1316
  /**
1101
1317
  * 多轮 Agent 循环。
1102
1318
  * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
@@ -1168,9 +1384,14 @@ var AgentLoop = class {
1168
1384
  toolTimeoutMs: this.#config.toolTimeoutMs,
1169
1385
  now,
1170
1386
  interactionSeq: 0,
1387
+ surfaceActionSeq: 0,
1171
1388
  messages: [],
1172
1389
  turn: 0,
1173
1390
  renderBlocks: [],
1391
+ surfaceEvents: [],
1392
+ surfacePatchWindowStartedAt: Date.now(),
1393
+ surfacePatchCount: 0,
1394
+ processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
1174
1395
  startMs,
1175
1396
  pausedMs: 0,
1176
1397
  maxTurns: this.#config.maxTurns,
@@ -1310,6 +1531,7 @@ var AgentLoop = class {
1310
1531
  toolCallId: call.id,
1311
1532
  content: await this.#serializeToolResult(call, result, state)
1312
1533
  });
1534
+ await this.#drainSurfaceAction(state);
1313
1535
  }
1314
1536
  }
1315
1537
  } finally {
@@ -1365,7 +1587,7 @@ var AgentLoop = class {
1365
1587
  };
1366
1588
  }
1367
1589
  /** D3 快照写入(含 pendingInteraction 与过期时间;写失败降级 run.warning) */
1368
- async #saveSnapshot(state, request) {
1590
+ async #saveSnapshot(state, pending) {
1369
1591
  const store = this.#deps.snapshotStore;
1370
1592
  if (!store) return;
1371
1593
  const snapshot = {
@@ -1378,10 +1600,14 @@ var AgentLoop = class {
1378
1600
  turn: state.turn,
1379
1601
  activeSkillNames: [...state.activated].sort(),
1380
1602
  activatedTools: [...state.activatedTools.values()],
1381
- pendingInteraction: request,
1603
+ ...pending.interaction ? { pendingInteraction: pending.interaction } : {},
1604
+ ...pending.surfaceAction ? { pendingSurfaceAction: pending.surfaceAction } : {},
1382
1605
  interactionExpiresAt: state.run.interruptExpiresAt,
1383
1606
  renderBlocks: [...state.renderBlocks],
1607
+ surfaceEvents: state.surfaceEvents.map((event) => structuredClone(event)),
1384
1608
  interactionSeq: state.interactionSeq,
1609
+ surfaceActionSeq: state.surfaceActionSeq,
1610
+ processedSurfaceActionNonces: [...state.processedSurfaceActionNonces],
1385
1611
  pausedMs: state.pausedMs,
1386
1612
  config: {
1387
1613
  maxTurns: state.maxTurns,
@@ -1426,9 +1652,14 @@ var AgentLoop = class {
1426
1652
  toolTimeoutMs: snapshot.config.toolTimeoutMs,
1427
1653
  now,
1428
1654
  interactionSeq: snapshot.interactionSeq ?? 0,
1655
+ surfaceActionSeq: snapshot.surfaceActionSeq ?? 0,
1429
1656
  messages: snapshot.messages.map((m) => ({ ...m })),
1430
1657
  turn: snapshot.turn,
1431
1658
  renderBlocks: (snapshot.renderBlocks ?? []).map((b) => ({ ...b })),
1659
+ surfaceEvents: (snapshot.surfaceEvents ?? []).map((event) => structuredClone(event)),
1660
+ surfacePatchWindowStartedAt: Date.now(),
1661
+ surfacePatchCount: 0,
1662
+ processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
1432
1663
  startMs,
1433
1664
  pausedMs: snapshot.pausedMs ?? 0,
1434
1665
  maxTurns: snapshot.config.maxTurns,
@@ -1437,15 +1668,19 @@ var AgentLoop = class {
1437
1668
  };
1438
1669
  this.#controllers.set(runId, state.controller);
1439
1670
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1671
+ const pendingInteraction = snapshot.pendingInteraction;
1672
+ const pendingSurfaceAction = snapshot.pendingSurfaceAction;
1440
1673
  trace.record("run.resumed", { data: {
1441
1674
  snapshotAt: snapshot.snapshotAt,
1442
1675
  turn: snapshot.turn,
1443
- interactionType: snapshot.pendingInteraction.type
1676
+ interactionType: pendingSurfaceAction ? "surface-action" : pendingInteraction?.type
1444
1677
  } });
1445
- const pending = snapshot.pendingInteraction;
1678
+ const pending = pendingInteraction;
1446
1679
  const pendingCall = this.#findPendingToolCall(state.messages);
1447
1680
  try {
1448
- if ((pending.type === "form" || pending.type === "select") && pendingCall) {
1681
+ await this.#replaySurfaceEvents(state);
1682
+ if (pendingSurfaceAction) await this.#resumeSurfaceAction(state, pendingSurfaceAction);
1683
+ else if ((pending?.type === "form" || pending?.type === "select") && pendingCall) {
1449
1684
  const value = await this.#interact(state, pending, {
1450
1685
  tool: pendingCall.name,
1451
1686
  resumed: true
@@ -1463,7 +1698,8 @@ var AgentLoop = class {
1463
1698
  toolCallId: pendingCall.id,
1464
1699
  content: await this.#serializeToolResult(pendingCall, result, state)
1465
1700
  });
1466
- } else if (pending.type === "ask" && pendingCall) {
1701
+ await this.#drainSurfaceAction(state);
1702
+ } else if (pending?.type === "ask" && pendingCall) {
1467
1703
  const value = await this.#interact(state, pending, {
1468
1704
  tool: pendingCall.name,
1469
1705
  resumed: true
@@ -1484,6 +1720,7 @@ var AgentLoop = class {
1484
1720
  toolCallId: pendingCall.id,
1485
1721
  content: await this.#serializeToolResult(pendingCall, result, state)
1486
1722
  });
1723
+ await this.#drainSurfaceAction(state);
1487
1724
  } else if (pendingCall) {
1488
1725
  const result = await this.#executeCall(pendingCall, state);
1489
1726
  state.messages.push({
@@ -1491,6 +1728,7 @@ var AgentLoop = class {
1491
1728
  toolCallId: pendingCall.id,
1492
1729
  content: await this.#serializeToolResult(pendingCall, result, state)
1493
1730
  });
1731
+ await this.#drainSurfaceAction(state);
1494
1732
  }
1495
1733
  for (;;) {
1496
1734
  const next = this.#findPendingToolCall(state.messages);
@@ -1501,6 +1739,7 @@ var AgentLoop = class {
1501
1739
  toolCallId: next.id,
1502
1740
  content: await this.#serializeToolResult(next, result, state)
1503
1741
  });
1742
+ await this.#drainSurfaceAction(state);
1504
1743
  }
1505
1744
  } catch (e) {
1506
1745
  if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
@@ -1597,7 +1836,7 @@ var AgentLoop = class {
1597
1836
  const { run } = state;
1598
1837
  run.status = "interrupted";
1599
1838
  run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
1600
- await this.#saveSnapshot(state, request);
1839
+ await this.#saveSnapshot(state, { interaction: request });
1601
1840
  await this.#lifecycle("interact", state, {
1602
1841
  interactionId: request.id,
1603
1842
  type: request.type
@@ -1665,7 +1904,8 @@ var AgentLoop = class {
1665
1904
  } });
1666
1905
  this.#emitTool(state, "started", call);
1667
1906
  let result;
1668
- if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
1907
+ if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
1908
+ else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
1669
1909
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
1670
1910
  else {
1671
1911
  const resolution = resolveToolName(call.name, state.activated);
@@ -1691,6 +1931,14 @@ var AgentLoop = class {
1691
1931
  type: "chart",
1692
1932
  chart
1693
1933
  });
1934
+ try {
1935
+ for (const event of extractUiSurfaceEvents(item.data)) await this.#renderSurface(state, event);
1936
+ } catch (e) {
1937
+ state.trace.record("run.warning", {
1938
+ message: `UI surface rejected: ${messageOf(e)}`,
1939
+ data: e instanceof WebSkillError ? { code: e.code } : void 0
1940
+ });
1941
+ }
1694
1942
  }
1695
1943
  } else {
1696
1944
  state.trace.record("tool.failed", {
@@ -1711,10 +1959,155 @@ var AgentLoop = class {
1711
1959
  } });
1712
1960
  return result;
1713
1961
  }
1962
+ /** Attaches trusted run provenance, then records only events accepted by the configured bridge. */
1963
+ async #renderSurface(state, event) {
1964
+ const bridge = this.#deps.uiBridge;
1965
+ if (!bridge?.renderSurface) {
1966
+ state.trace.record("run.warning", { message: "UiBridge does not support renderSurface; UI surface was not rendered" });
1967
+ return;
1968
+ }
1969
+ const attributed = this.#attributeSurfaceEvent(state, event);
1970
+ if (attributed.type === "patch") this.#consumeSurfacePatchBudget(state);
1971
+ await bridge.renderSurface(attributed);
1972
+ state.surfaceEvents.push(structuredClone(attributed));
1973
+ if (attributed.type === "open") {
1974
+ const waiting = surfaceActions(attributed.surface).filter((action) => action.awaitResponse);
1975
+ if (waiting.length > 1) throw new WebSkillError("VALIDATION_FAILED", "A UI surface can wait for only one action");
1976
+ const action = waiting[0];
1977
+ if (action?.nonce) if (!bridge.requestSurfaceAction) state.trace.record("run.warning", { message: "UiBridge does not support requestSurfaceAction; UI surface action will not pause the run" });
1978
+ else if (state.pendingSurfaceAction) throw new WebSkillError("VALIDATION_FAILED", "Only one UI surface action can be pending at a time");
1979
+ else state.pendingSurfaceAction = {
1980
+ runId: state.runId,
1981
+ surfaceId: attributed.surface.id,
1982
+ actionId: action.id,
1983
+ intent: action.intent,
1984
+ nonce: action.nonce
1985
+ };
1986
+ }
1987
+ }
1988
+ #consumeSurfacePatchBudget(state) {
1989
+ const now = Date.now();
1990
+ if (now - state.surfacePatchWindowStartedAt >= 1e3) {
1991
+ state.surfacePatchWindowStartedAt = now;
1992
+ state.surfacePatchCount = 0;
1993
+ }
1994
+ state.surfacePatchCount += 1;
1995
+ if (state.surfacePatchCount > MAX_SURFACE_PATCHES_PER_SECOND) throw new WebSkillError("VALIDATION_FAILED", `UI surface patch rate exceeds ${MAX_SURFACE_PATCHES_PER_SECOND} events per second`);
1996
+ }
1997
+ /** Assigns unforgeable action nonces after model output has passed structural validation. */
1998
+ #attributeSurfaceEvent(state, event) {
1999
+ const actions = event.type === "open" ? surfaceActions(event.surface) : [];
2000
+ if (event.type !== "open" || actions.length === 0) return {
2001
+ ...event,
2002
+ runId: state.runId
2003
+ };
2004
+ return {
2005
+ type: "open",
2006
+ runId: state.runId,
2007
+ surface: {
2008
+ ...event.surface,
2009
+ actions: actions.map((action) => ({
2010
+ ...action,
2011
+ nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
2012
+ }))
2013
+ }
2014
+ };
2015
+ }
2016
+ /** Awaits the single action emitted with the most recently persisted tool result. */
2017
+ async #drainSurfaceAction(state) {
2018
+ const request = state.pendingSurfaceAction;
2019
+ if (!request) return;
2020
+ state.pendingSurfaceAction = void 0;
2021
+ await this.#acceptSurfaceAction(state, request);
2022
+ }
2023
+ /** Restores a checkpointed action wait without re-running the tool that emitted its surface. */
2024
+ async #resumeSurfaceAction(state, request) {
2025
+ await this.#acceptSurfaceAction(state, request, true);
2026
+ }
2027
+ async #acceptSurfaceAction(state, request, resumed = false) {
2028
+ const response = await this.#interactSurfaceAction(state, request, resumed);
2029
+ if (response.cancelled) throw new RunTerminated({
2030
+ status: "cancelled",
2031
+ reason: "user-cancelled",
2032
+ message: "Surface action cancelled by user",
2033
+ code: "RUN_CANCELLED"
2034
+ });
2035
+ if (response.runId !== request.runId || response.surfaceId !== request.surfaceId || response.actionId !== request.actionId || response.nonce !== request.nonce) throw new BridgeRequestError("UiBridge returned a surface action response with an invalid capability nonce");
2036
+ if (state.processedSurfaceActionNonces.has(response.nonce)) throw new BridgeRequestError("UiBridge returned a surface action response with an already consumed capability nonce");
2037
+ state.processedSurfaceActionNonces.add(response.nonce);
2038
+ state.messages.push({
2039
+ role: "user",
2040
+ content: JSON.stringify({
2041
+ type: "webskill_surface_action",
2042
+ surfaceId: response.surfaceId,
2043
+ actionId: response.actionId,
2044
+ intent: response.intent,
2045
+ value: response.value ?? null
2046
+ })
2047
+ });
2048
+ }
2049
+ async #interactSurfaceAction(state, request, resumed) {
2050
+ const bridge = this.#deps.uiBridge;
2051
+ if (!bridge?.requestSurfaceAction) throw new BridgeRequestError("UiBridge does not support requestSurfaceAction");
2052
+ this.#disarmDeadline(state);
2053
+ const waitStartMs = Date.parse(state.now());
2054
+ try {
2055
+ const { run } = state;
2056
+ run.status = "interrupted";
2057
+ run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
2058
+ await this.#saveSnapshot(state, { surfaceAction: request });
2059
+ await this.#lifecycle("interact", state, {
2060
+ surfaceId: request.surfaceId,
2061
+ actionId: request.actionId,
2062
+ nonce: request.nonce,
2063
+ ...resumed ? { resumed: true } : {}
2064
+ });
2065
+ state.trace.record("ui.surface-action.requested", { data: {
2066
+ surfaceId: request.surfaceId,
2067
+ actionId: request.actionId,
2068
+ nonce: request.nonce,
2069
+ ...resumed ? { resumed: true } : {}
2070
+ } });
2071
+ const response = await this.#withInteractionTimeout(bridge.requestSurfaceAction(request), this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
2072
+ run.status = "running";
2073
+ run.interruptExpiresAt = void 0;
2074
+ state.trace.record("ui.surface-action.resolved", { data: {
2075
+ surfaceId: request.surfaceId,
2076
+ actionId: request.actionId,
2077
+ nonce: request.nonce
2078
+ } });
2079
+ await this.#lifecycle("execute", state, { surfaceAction: request.actionId });
2080
+ return response;
2081
+ } catch (e) {
2082
+ state.run.status = "running";
2083
+ state.run.interruptExpiresAt = void 0;
2084
+ if (e instanceof WebSkillError && e.code === "RUN_INTERACTION_TIMEOUT") throw new RunTerminated({
2085
+ status: "failed",
2086
+ reason: "interaction-timeout",
2087
+ message: e.message,
2088
+ code: "RUN_INTERACTION_TIMEOUT"
2089
+ });
2090
+ if (e instanceof RunTerminated || e instanceof BridgeRequestError) throw e;
2091
+ throw new BridgeRequestError(messageOf(e));
2092
+ } finally {
2093
+ state.pausedMs += Date.parse(state.now()) - waitStartMs;
2094
+ this.#armDeadline(state);
2095
+ }
2096
+ }
2097
+ /** Replays the persisted stream before an interrupted interaction is rendered again. */
2098
+ async #replaySurfaceEvents(state) {
2099
+ const bridge = this.#deps.uiBridge;
2100
+ if (!bridge?.renderSurface || state.surfaceEvents.length === 0) return;
2101
+ try {
2102
+ for (const event of state.surfaceEvents) await bridge.renderSurface(structuredClone(event));
2103
+ } catch (e) {
2104
+ state.trace.record("run.warning", { message: `Failed to replay UI surfaces: ${messageOf(e)}` });
2105
+ }
2106
+ }
1714
2107
  /**
1715
- * 逐工具实时事件(execute 相位,data.type='tool'):chatbot ToolCallCard 等的 live 状态源;
2108
+ * 逐工具实时事件(execute 相位,data.type='tool'):chatbot 思维链工具行等的 live 状态源;
1716
2109
  * 与既有 execute 相位事件({turn})并存,监听方按 data.type 区分。
1717
- * data.args 为参数摘要(JSON 截断 100 字符,卡片展开详情用)。
2110
+ * data.args 为参数摘要(JSON 截断 100 字符,展开详情用)。
1718
2111
  */
1719
2112
  #emitTool(state, status, call) {
1720
2113
  this.#deps.eventBus?.emit({
@@ -1880,8 +2273,9 @@ var AgentLoop = class {
1880
2273
  let scriptFiles;
1881
2274
  try {
1882
2275
  scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
1883
- } catch {
2276
+ } catch (e) {
1884
2277
  scriptFiles = [];
2278
+ if (!(e instanceof WebSkillError && e.code === "FS_NOT_FOUND")) state.trace.record("run.warning", { message: `Failed to list scripts of skill "${skillName}": ${messageOf(e)}` });
1885
2279
  }
1886
2280
  for (const file of scriptFiles) {
1887
2281
  const match = /^(.*)\.(ts|js)$/.exec(file);
@@ -2018,10 +2412,11 @@ var AgentLoop = class {
2018
2412
  #nextInteractionId(state) {
2019
2413
  return `int-${++state.interactionSeq}`;
2020
2414
  }
2021
- async #memoryGet(scope, key) {
2415
+ async #memoryGet(scope, key, state) {
2022
2416
  try {
2023
2417
  return await this.#deps.memory?.get(scope, key);
2024
- } catch {
2418
+ } catch (e) {
2419
+ state.trace.record("run.warning", { message: `Memory read failed: ${messageOf(e)}` });
2025
2420
  return;
2026
2421
  }
2027
2422
  }
@@ -2046,7 +2441,7 @@ var AgentLoop = class {
2046
2441
  }
2047
2442
  return;
2048
2443
  }
2049
- await this.#memorySet(scope, key, mutate(await this.#memoryGet(scope, key)), state);
2444
+ await this.#memorySet(scope, key, mutate(await this.#memoryGet(scope, key, state)), state);
2050
2445
  }
2051
2446
  async #writeActivationMemory(skillName, state) {
2052
2447
  if (!this.#deps.memory) return;
@@ -2578,9 +2973,26 @@ const encode = (s) => encodeURIComponent(s);
2578
2973
  var FsMemoryStore = class {
2579
2974
  #root;
2580
2975
  #fs;
2976
+ #onWarning;
2581
2977
  constructor(deps) {
2582
2978
  this.#root = deps.root.replace(/\/+$/, "");
2583
2979
  this.#fs = deps.fs;
2980
+ this.#onWarning = deps.onWarning ?? ((message) => console.warn(message));
2981
+ }
2982
+ /**
2983
+ * 0.2.8 G2:损坏条目此前直接 remove——静默销毁用户数据,且对调用方伪装成「没有这条记忆」。
2984
+ * 改为隔离到 .corrupt 并告警:run 照常继续,但数据留存、故障可见。
2985
+ */
2986
+ async #quarantine(path, error) {
2987
+ const reason = error instanceof Error ? error.message : String(error);
2988
+ const target = `${path}.corrupt`;
2989
+ try {
2990
+ await this.#fs.rename(path, target);
2991
+ this.#onWarning(`[webskill] Corrupted memory entry quarantined to ${target}: ${reason}`);
2992
+ } catch (renameError) {
2993
+ const detail = renameError instanceof Error ? renameError.message : String(renameError);
2994
+ this.#onWarning(`[webskill] Corrupted memory entry at ${path} could not be quarantined (${detail}): ${reason}`);
2995
+ }
2584
2996
  }
2585
2997
  #scopeDir(scope) {
2586
2998
  return `${this.#root}/${encode(scope)}`;
@@ -2593,8 +3005,8 @@ var FsMemoryStore = class {
2593
3005
  if (!await this.#fs.exists(path)) return void 0;
2594
3006
  try {
2595
3007
  return JSON.parse(await this.#fs.readText(path));
2596
- } catch {
2597
- await this.#fs.remove(path);
3008
+ } catch (e) {
3009
+ await this.#quarantine(path, e);
2598
3010
  return;
2599
3011
  }
2600
3012
  }
@@ -2618,8 +3030,8 @@ var FsMemoryStore = class {
2618
3030
  key,
2619
3031
  value: JSON.parse(await this.#fs.readText(entry.path))
2620
3032
  });
2621
- } catch {
2622
- await this.#fs.remove(entry.path);
3033
+ } catch (e) {
3034
+ await this.#quarantine(entry.path, e);
2623
3035
  }
2624
3036
  }
2625
3037
  return out.sort((a, b) => a.key.localeCompare(b.key));
@@ -2643,9 +3055,11 @@ const INDEX_FILE = "index.json";
2643
3055
  var FsArtifactStore = class {
2644
3056
  #root;
2645
3057
  #fs;
3058
+ #onWarning;
2646
3059
  constructor(deps) {
2647
3060
  this.#root = deps.root.replace(/\/+$/, "");
2648
3061
  this.#fs = deps.fs;
3062
+ this.#onWarning = deps.onWarning ?? ((message) => console.warn(message));
2649
3063
  }
2650
3064
  async createTextArtifact(input) {
2651
3065
  const size = new TextEncoder().encode(input.content).length;
@@ -2671,8 +3085,15 @@ var FsArtifactStore = class {
2671
3085
  const raw = await this.#fs.readText(indexPath);
2672
3086
  try {
2673
3087
  return JSON.parse(raw).artifacts ?? [];
2674
- } catch {
2675
- await this.#fs.remove(indexPath);
3088
+ } catch (e) {
3089
+ const reason = e instanceof Error ? e.message : String(e);
3090
+ try {
3091
+ await this.#fs.rename(indexPath, `${indexPath}.corrupt`);
3092
+ this.#onWarning(`[webskill] Corrupted artifact index for run ${runId} quarantined to ${indexPath}.corrupt: ${reason}`);
3093
+ } catch (renameError) {
3094
+ const detail = renameError instanceof Error ? renameError.message : String(renameError);
3095
+ this.#onWarning(`[webskill] Corrupted artifact index for run ${runId} could not be quarantined (${detail}): ${reason}`);
3096
+ }
2676
3097
  return [];
2677
3098
  }
2678
3099
  }
@@ -2786,6 +3207,20 @@ function networkUrlHost(url) {
2786
3207
  return "(unparseable-url)";
2787
3208
  }
2788
3209
  }
3210
+ /**
3211
+ * 网络策略判定逻辑的可注入源码(单一来源)。
3212
+ *
3213
+ * 0.2.8 C4:此前各注入点直接拼 `isNetworkAllowed.toString()`,依赖**函数名在产物里保持不变**。
3214
+ * SDK 自身不压缩,但消费方一旦跑生产构建,打包器会把导出函数改名(`function Ke(...)`),
3215
+ * 注入后的沙箱里 `isNetworkAllowed` 就是 undefined —— 沙箱内任何 fetch 直接
3216
+ * TOOL_EXECUTION_FAILED,网络白名单形同虚设。dev server 不压缩,所以只在真实产物上暴露。
3217
+ *
3218
+ * 因此改为把函数源码绑定到**固定的变量名**上:`var isNetworkAllowed = function Ke(...) {…};`
3219
+ * ——名字随便压缩,绑定名恒定。两个函数都自包含(不引用模块内其它符号),故可独立绑定。
3220
+ */
3221
+ function networkPolicyLibSource() {
3222
+ return `var isNetworkAllowed = ${isNetworkAllowed.toString()};\nvar networkUrlHost = ${networkUrlHost.toString()};`;
3223
+ }
2789
3224
  /** WebSkillErrorCode 全量白名单(错误码归一用;与 core errors.ts 保持同步) */
2790
3225
  const WHITELIST = /* @__PURE__ */ new Set([
2791
3226
  "FS_NOT_FOUND",
@@ -2899,4 +3334,4 @@ var CapabilityApproval = class CapabilityApproval {
2899
3334
  };
2900
3335
 
2901
3336
  //#endregion
2902
- export { isNetworkAllowed as A, toVercelToolSpecs as B, bridgeError as C, extractChartSpec as D, createWebSkillApi as E, normalizeToolError as F, parseBridgeRequest as I, resolveToolName as L, networkUrlHost as M, normalizeErrorCode as N, fromVercelResult as O, normalizeToolContent as P, schemaToForm 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, toLlmToolSpec as z };
3337
+ export { fromVercelStreamPart as A, schemaToForm as B, bridgeError as C, extractChartSpec as D, createWebSkillApi as E, normalizeErrorCode as F, toVercelToolSpecs as H, normalizeToolContent as I, normalizeToolError as L, mergeCatalogEntries as M, networkPolicyLibSource as N, extractUiSurfaceEvents as O, networkUrlHost as P, parseBridgeRequest as R, WebSkillRuntime as S, createScriptContext as T, validateUiSurface as U, toLlmToolSpec as V, validateUiSurfaceEvent as W, 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, resolveToolName as z };