@webskill/sdk 0.2.6 → 0.2.7

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.
@@ -875,22 +875,22 @@ function createScriptContext(deps) {
875
875
  ...onWarning ? { onWarning } : {}
876
876
  };
877
877
  }
878
- const isRecord$1 = (v) => typeof v === "object" && v !== null;
878
+ const isRecord$2 = (v) => typeof v === "object" && v !== null;
879
879
  /**
880
880
  * $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
881
881
  * 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
882
882
  */
883
883
  function extractChartSpec(data) {
884
- if (!isRecord$1(data)) return void 0;
884
+ if (!isRecord$2(data)) return void 0;
885
885
  const raw = data["$chart"];
886
- if (!isRecord$1(raw)) return void 0;
886
+ if (!isRecord$2(raw)) return void 0;
887
887
  const { kind, labels, series } = raw;
888
888
  if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
889
889
  if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
890
890
  if (!Array.isArray(series)) return void 0;
891
891
  const validSeries = [];
892
892
  for (const item of series) {
893
- if (!isRecord$1(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
893
+ if (!isRecord$2(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
894
894
  const name = item["name"];
895
895
  validSeries.push({
896
896
  ...typeof name === "string" ? { name } : {},
@@ -928,6 +928,212 @@ function buildRenderResult(run, output, renderBlocks = []) {
928
928
  artifacts: run.artifacts
929
929
  };
930
930
  }
931
+ const MAX_SURFACE_BYTES = 256 * 1024;
932
+ const MAX_ACTIONS = 32;
933
+ const MAX_FORM_FIELDS = 64;
934
+ const MAX_OPTIONS = 256;
935
+ const MAX_TABLE_COLUMNS = 128;
936
+ const MAX_TABLE_ROWS = 1e4;
937
+ const MAX_CHART_POINTS = 2e4;
938
+ const MAX_JSON_DEPTH = 16;
939
+ const MAX_PATCH_OPERATIONS = 128;
940
+ const actionIntents = /* @__PURE__ */ new Set([
941
+ "submit",
942
+ "cancel",
943
+ "select",
944
+ "download",
945
+ "refresh"
946
+ ]);
947
+ const fieldTypes = /* @__PURE__ */ new Set([
948
+ "text",
949
+ "number",
950
+ "date",
951
+ "textarea",
952
+ "select",
953
+ "multi-select",
954
+ "toggle",
955
+ "file"
956
+ ]);
957
+ function isRecord$1(value) {
958
+ return typeof value === "object" && value !== null && !Array.isArray(value);
959
+ }
960
+ function reject(message) {
961
+ throw new WebSkillError("VALIDATION_FAILED", message);
962
+ }
963
+ function requireString(value, name) {
964
+ if (typeof value !== "string" || value.trim() === "") reject(`${name} must be a non-empty string`);
965
+ }
966
+ function requireNonNegativeInteger(value, name) {
967
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) reject(`${name} must be a non-negative integer`);
968
+ }
969
+ function isJsonValue(value, depth = 0) {
970
+ if (depth > MAX_JSON_DEPTH || value === void 0 || typeof value === "function" || typeof value === "symbol") return false;
971
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
972
+ if (typeof value === "number") return Number.isFinite(value);
973
+ if (Array.isArray(value)) return value.every((item) => isJsonValue(item, depth + 1));
974
+ if (!isRecord$1(value)) return false;
975
+ return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
976
+ }
977
+ function assertActions(value) {
978
+ if (value === void 0) return;
979
+ if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
980
+ for (const action of value) {
981
+ if (!isRecord$1(action)) reject("A surface action must be an object");
982
+ requireString(action["id"], "Surface action ID");
983
+ requireString(action["label"], "Surface action label");
984
+ if (typeof action["intent"] !== "string" || !actionIntents.has(action["intent"])) reject("Surface action intent is invalid");
985
+ if (action["disabled"] !== void 0 && typeof action["disabled"] !== "boolean") reject("Surface action disabled must be a boolean");
986
+ if (action["awaitResponse"] !== void 0 && typeof action["awaitResponse"] !== "boolean") reject("Surface action awaitResponse must be a boolean");
987
+ if (action["nonce"] !== void 0 && (typeof action["nonce"] !== "string" || action["nonce"] === "")) reject("Surface action nonce must be a non-empty string");
988
+ }
989
+ }
990
+ function assertForm(surface) {
991
+ if (!Array.isArray(surface["fields"]) || surface["fields"].length > MAX_FORM_FIELDS) reject(`A form surface must contain at most ${MAX_FORM_FIELDS} fields`);
992
+ for (const field of surface["fields"]) {
993
+ if (!isRecord$1(field)) reject("A form field must be an object");
994
+ requireString(field["name"], "Form field name");
995
+ requireString(field["label"], "Form field label");
996
+ if (typeof field["type"] !== "string" || !fieldTypes.has(field["type"])) reject("Form field type is invalid");
997
+ if (field["required"] !== void 0 && typeof field["required"] !== "boolean") reject("Form field required must be a boolean");
998
+ if (field["description"] !== void 0 && typeof field["description"] !== "string") reject("Form field description must be a string");
999
+ if (field["defaultValue"] !== void 0 && !isJsonValue(field["defaultValue"])) reject("Form field defaultValue must be JSON data");
1000
+ if (field["options"] !== void 0) {
1001
+ if (!Array.isArray(field["options"]) || field["options"].length > MAX_OPTIONS) reject(`Form field options must contain at most ${MAX_OPTIONS} items`);
1002
+ for (const option of field["options"]) {
1003
+ if (!isRecord$1(option)) reject("A form option must be an object");
1004
+ requireString(option["label"], "Form option label");
1005
+ if (!isJsonValue(option["value"])) reject("Form option value must be JSON data");
1006
+ }
1007
+ }
1008
+ }
1009
+ assertActions(surface["actions"]);
1010
+ }
1011
+ function assertChart(surface) {
1012
+ const chart = surface["chart"];
1013
+ if (!isRecord$1(chart)) reject("A chart surface requires a chart object");
1014
+ if (chart["kind"] !== "bar" && chart["kind"] !== "line" && chart["kind"] !== "pie") reject("Chart kind is invalid");
1015
+ if (!Array.isArray(chart["labels"]) || !chart["labels"].every((label) => typeof label === "string")) reject("Chart labels must be an array of strings");
1016
+ if (!Array.isArray(chart["series"])) reject("Chart series must be an array");
1017
+ let points = 0;
1018
+ for (const series of chart["series"]) {
1019
+ 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");
1020
+ if (series["name"] !== void 0 && typeof series["name"] !== "string") reject("Chart series name must be a string");
1021
+ points += series["data"].length;
1022
+ }
1023
+ if (points > MAX_CHART_POINTS) reject(`Chart data exceeds the ${MAX_CHART_POINTS}-point limit`);
1024
+ assertActions(surface["actions"]);
1025
+ }
1026
+ function assertTable(surface) {
1027
+ 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`);
1028
+ if (!Array.isArray(surface["rows"]) || surface["rows"].length > MAX_TABLE_ROWS) reject(`Table rows must contain at most ${MAX_TABLE_ROWS} items`);
1029
+ 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");
1030
+ assertActions(surface["actions"]);
1031
+ }
1032
+ /** Validates the allowlisted, data-only shape accepted by a UI surface renderer. @experimental */
1033
+ function validateUiSurface(value) {
1034
+ if (!isRecord$1(value)) reject("A UI surface must be an object");
1035
+ requireString(value["id"], "UI surface ID");
1036
+ if (value["title"] !== void 0 && typeof value["title"] !== "string") reject("UI surface title must be a string");
1037
+ switch (value["kind"]) {
1038
+ case "form":
1039
+ assertForm(value);
1040
+ break;
1041
+ case "chart":
1042
+ assertChart(value);
1043
+ break;
1044
+ case "table":
1045
+ assertTable(value);
1046
+ break;
1047
+ case "metric":
1048
+ requireString(value["label"], "Metric label");
1049
+ if (typeof value["value"] !== "string" && (typeof value["value"] !== "number" || !Number.isFinite(value["value"]))) reject("Metric value must be a string or finite number");
1050
+ if (value["trend"] !== void 0 && value["trend"] !== "up" && value["trend"] !== "down" && value["trend"] !== "neutral") reject("Metric trend is invalid");
1051
+ break;
1052
+ case "file": {
1053
+ requireString(value["path"], "File path");
1054
+ if (value["mimeType"] !== void 0 && typeof value["mimeType"] !== "string") reject("File mimeType must be a string");
1055
+ const fileSize = value["size"];
1056
+ if (fileSize !== void 0 && (typeof fileSize !== "number" || !Number.isSafeInteger(fileSize) || fileSize < 0)) reject("File size must be a non-negative integer");
1057
+ assertActions(value["actions"]);
1058
+ break;
1059
+ }
1060
+ case "custom":
1061
+ requireString(value["component"], "Custom surface component");
1062
+ if (!isJsonValue(value["props"])) reject("Custom surface props must be JSON data");
1063
+ assertActions(value["actions"]);
1064
+ break;
1065
+ default: reject("UI surface kind is invalid");
1066
+ }
1067
+ if (!isJsonValue(value)) reject("A UI surface must contain JSON data only");
1068
+ if (JSON.stringify(value).length > MAX_SURFACE_BYTES) reject(`A UI surface exceeds the ${MAX_SURFACE_BYTES}-byte limit`);
1069
+ return structuredClone(value);
1070
+ }
1071
+ function assertPatch(value) {
1072
+ if (!isRecord$1(value)) reject("A surface patch operation must be an object");
1073
+ if (value["op"] !== "replace" && value["op"] !== "merge" && value["op"] !== "append") reject("Surface patch operation is invalid");
1074
+ requireString(value["path"], "Surface patch path");
1075
+ if (!value["path"].startsWith("/")) reject("Surface patch path must be a JSON pointer");
1076
+ if (!isJsonValue(value["value"])) reject("Surface patch value must be JSON data");
1077
+ }
1078
+ /** Validates an individual event in the framework-neutral surface stream. @experimental */
1079
+ function validateUiSurfaceEvent(value) {
1080
+ if (!isRecord$1(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
1081
+ if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
1082
+ switch (value["type"]) {
1083
+ case "open": return {
1084
+ type: "open",
1085
+ ...value["runId"] ? { runId: value["runId"] } : {},
1086
+ surface: validateUiSurface(value["surface"])
1087
+ };
1088
+ case "patch":
1089
+ requireString(value["id"], "Surface patch ID");
1090
+ requireNonNegativeInteger(value["revision"], "Surface patch revision");
1091
+ if (!Array.isArray(value["operations"]) || value["operations"].length === 0) reject("A surface patch requires operations");
1092
+ if (value["operations"].length > MAX_PATCH_OPERATIONS) reject(`A surface patch must contain at most ${MAX_PATCH_OPERATIONS} operations`);
1093
+ for (const operation of value["operations"]) assertPatch(operation);
1094
+ return {
1095
+ type: "patch",
1096
+ ...value["runId"] ? { runId: value["runId"] } : {},
1097
+ id: value["id"],
1098
+ revision: value["revision"],
1099
+ operations: structuredClone(value["operations"])
1100
+ };
1101
+ case "complete":
1102
+ requireString(value["id"], "Surface completion ID");
1103
+ requireNonNegativeInteger(value["revision"], "Surface completion revision");
1104
+ return {
1105
+ type: "complete",
1106
+ ...value["runId"] ? { runId: value["runId"] } : {},
1107
+ id: value["id"],
1108
+ revision: value["revision"]
1109
+ };
1110
+ case "error":
1111
+ requireString(value["id"], "Surface error ID");
1112
+ requireString(value["code"], "Surface error code");
1113
+ requireString(value["message"], "Surface error message");
1114
+ return {
1115
+ type: "error",
1116
+ ...value["runId"] ? { runId: value["runId"] } : {},
1117
+ id: value["id"],
1118
+ code: value["code"],
1119
+ message: value["message"]
1120
+ };
1121
+ case "cancel":
1122
+ requireString(value["id"], "Surface cancellation ID");
1123
+ return {
1124
+ type: "cancel",
1125
+ ...value["runId"] ? { runId: value["runId"] } : {},
1126
+ id: value["id"]
1127
+ };
1128
+ default: return reject("UI surface event type is invalid");
1129
+ }
1130
+ }
1131
+ /** Extracts validated surface stream events from structured tool output. @experimental */
1132
+ function extractUiSurfaceEvents(data) {
1133
+ if (!isRecord$1(data) || data["$surface"] === void 0) return [];
1134
+ const raw = data["$surface"];
1135
+ return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSurfaceEvent(event));
1136
+ }
931
1137
  /**
932
1138
  * JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
933
1139
  * providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
@@ -1073,6 +1279,7 @@ var TraceRecorder = class {
1073
1279
  return [...this.#events];
1074
1280
  }
1075
1281
  };
1282
+ const MAX_SURFACE_PATCHES_PER_SECOND = 240;
1076
1283
  /** 交互终态(取消/超时):从工具执行深处直接终止 run */
1077
1284
  var RunTerminated = class extends Error {
1078
1285
  outcome;
@@ -1097,6 +1304,7 @@ const summarizeArgs = (args) => {
1097
1304
  const json = JSON.stringify(args);
1098
1305
  return json.length > 100 ? `${json.slice(0, 100)}…` : json;
1099
1306
  };
1307
+ const surfaceActions = (surface) => "actions" in surface ? surface.actions ?? [] : [];
1100
1308
  /**
1101
1309
  * 多轮 Agent 循环。
1102
1310
  * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
@@ -1168,9 +1376,14 @@ var AgentLoop = class {
1168
1376
  toolTimeoutMs: this.#config.toolTimeoutMs,
1169
1377
  now,
1170
1378
  interactionSeq: 0,
1379
+ surfaceActionSeq: 0,
1171
1380
  messages: [],
1172
1381
  turn: 0,
1173
1382
  renderBlocks: [],
1383
+ surfaceEvents: [],
1384
+ surfacePatchWindowStartedAt: Date.now(),
1385
+ surfacePatchCount: 0,
1386
+ processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
1174
1387
  startMs,
1175
1388
  pausedMs: 0,
1176
1389
  maxTurns: this.#config.maxTurns,
@@ -1310,6 +1523,7 @@ var AgentLoop = class {
1310
1523
  toolCallId: call.id,
1311
1524
  content: await this.#serializeToolResult(call, result, state)
1312
1525
  });
1526
+ await this.#drainSurfaceAction(state);
1313
1527
  }
1314
1528
  }
1315
1529
  } finally {
@@ -1365,7 +1579,7 @@ var AgentLoop = class {
1365
1579
  };
1366
1580
  }
1367
1581
  /** D3 快照写入(含 pendingInteraction 与过期时间;写失败降级 run.warning) */
1368
- async #saveSnapshot(state, request) {
1582
+ async #saveSnapshot(state, pending) {
1369
1583
  const store = this.#deps.snapshotStore;
1370
1584
  if (!store) return;
1371
1585
  const snapshot = {
@@ -1378,10 +1592,14 @@ var AgentLoop = class {
1378
1592
  turn: state.turn,
1379
1593
  activeSkillNames: [...state.activated].sort(),
1380
1594
  activatedTools: [...state.activatedTools.values()],
1381
- pendingInteraction: request,
1595
+ ...pending.interaction ? { pendingInteraction: pending.interaction } : {},
1596
+ ...pending.surfaceAction ? { pendingSurfaceAction: pending.surfaceAction } : {},
1382
1597
  interactionExpiresAt: state.run.interruptExpiresAt,
1383
1598
  renderBlocks: [...state.renderBlocks],
1599
+ surfaceEvents: state.surfaceEvents.map((event) => structuredClone(event)),
1384
1600
  interactionSeq: state.interactionSeq,
1601
+ surfaceActionSeq: state.surfaceActionSeq,
1602
+ processedSurfaceActionNonces: [...state.processedSurfaceActionNonces],
1385
1603
  pausedMs: state.pausedMs,
1386
1604
  config: {
1387
1605
  maxTurns: state.maxTurns,
@@ -1426,9 +1644,14 @@ var AgentLoop = class {
1426
1644
  toolTimeoutMs: snapshot.config.toolTimeoutMs,
1427
1645
  now,
1428
1646
  interactionSeq: snapshot.interactionSeq ?? 0,
1647
+ surfaceActionSeq: snapshot.surfaceActionSeq ?? 0,
1429
1648
  messages: snapshot.messages.map((m) => ({ ...m })),
1430
1649
  turn: snapshot.turn,
1431
1650
  renderBlocks: (snapshot.renderBlocks ?? []).map((b) => ({ ...b })),
1651
+ surfaceEvents: (snapshot.surfaceEvents ?? []).map((event) => structuredClone(event)),
1652
+ surfacePatchWindowStartedAt: Date.now(),
1653
+ surfacePatchCount: 0,
1654
+ processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
1432
1655
  startMs,
1433
1656
  pausedMs: snapshot.pausedMs ?? 0,
1434
1657
  maxTurns: snapshot.config.maxTurns,
@@ -1437,15 +1660,19 @@ var AgentLoop = class {
1437
1660
  };
1438
1661
  this.#controllers.set(runId, state.controller);
1439
1662
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1663
+ const pendingInteraction = snapshot.pendingInteraction;
1664
+ const pendingSurfaceAction = snapshot.pendingSurfaceAction;
1440
1665
  trace.record("run.resumed", { data: {
1441
1666
  snapshotAt: snapshot.snapshotAt,
1442
1667
  turn: snapshot.turn,
1443
- interactionType: snapshot.pendingInteraction.type
1668
+ interactionType: pendingSurfaceAction ? "surface-action" : pendingInteraction?.type
1444
1669
  } });
1445
- const pending = snapshot.pendingInteraction;
1670
+ const pending = pendingInteraction;
1446
1671
  const pendingCall = this.#findPendingToolCall(state.messages);
1447
1672
  try {
1448
- if ((pending.type === "form" || pending.type === "select") && pendingCall) {
1673
+ await this.#replaySurfaceEvents(state);
1674
+ if (pendingSurfaceAction) await this.#resumeSurfaceAction(state, pendingSurfaceAction);
1675
+ else if ((pending?.type === "form" || pending?.type === "select") && pendingCall) {
1449
1676
  const value = await this.#interact(state, pending, {
1450
1677
  tool: pendingCall.name,
1451
1678
  resumed: true
@@ -1463,7 +1690,8 @@ var AgentLoop = class {
1463
1690
  toolCallId: pendingCall.id,
1464
1691
  content: await this.#serializeToolResult(pendingCall, result, state)
1465
1692
  });
1466
- } else if (pending.type === "ask" && pendingCall) {
1693
+ await this.#drainSurfaceAction(state);
1694
+ } else if (pending?.type === "ask" && pendingCall) {
1467
1695
  const value = await this.#interact(state, pending, {
1468
1696
  tool: pendingCall.name,
1469
1697
  resumed: true
@@ -1484,6 +1712,7 @@ var AgentLoop = class {
1484
1712
  toolCallId: pendingCall.id,
1485
1713
  content: await this.#serializeToolResult(pendingCall, result, state)
1486
1714
  });
1715
+ await this.#drainSurfaceAction(state);
1487
1716
  } else if (pendingCall) {
1488
1717
  const result = await this.#executeCall(pendingCall, state);
1489
1718
  state.messages.push({
@@ -1491,6 +1720,7 @@ var AgentLoop = class {
1491
1720
  toolCallId: pendingCall.id,
1492
1721
  content: await this.#serializeToolResult(pendingCall, result, state)
1493
1722
  });
1723
+ await this.#drainSurfaceAction(state);
1494
1724
  }
1495
1725
  for (;;) {
1496
1726
  const next = this.#findPendingToolCall(state.messages);
@@ -1501,6 +1731,7 @@ var AgentLoop = class {
1501
1731
  toolCallId: next.id,
1502
1732
  content: await this.#serializeToolResult(next, result, state)
1503
1733
  });
1734
+ await this.#drainSurfaceAction(state);
1504
1735
  }
1505
1736
  } catch (e) {
1506
1737
  if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
@@ -1597,7 +1828,7 @@ var AgentLoop = class {
1597
1828
  const { run } = state;
1598
1829
  run.status = "interrupted";
1599
1830
  run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
1600
- await this.#saveSnapshot(state, request);
1831
+ await this.#saveSnapshot(state, { interaction: request });
1601
1832
  await this.#lifecycle("interact", state, {
1602
1833
  interactionId: request.id,
1603
1834
  type: request.type
@@ -1691,6 +1922,14 @@ var AgentLoop = class {
1691
1922
  type: "chart",
1692
1923
  chart
1693
1924
  });
1925
+ try {
1926
+ for (const event of extractUiSurfaceEvents(item.data)) await this.#renderSurface(state, event);
1927
+ } catch (e) {
1928
+ state.trace.record("run.warning", {
1929
+ message: `UI surface rejected: ${messageOf(e)}`,
1930
+ data: e instanceof WebSkillError ? { code: e.code } : void 0
1931
+ });
1932
+ }
1694
1933
  }
1695
1934
  } else {
1696
1935
  state.trace.record("tool.failed", {
@@ -1711,10 +1950,155 @@ var AgentLoop = class {
1711
1950
  } });
1712
1951
  return result;
1713
1952
  }
1953
+ /** Attaches trusted run provenance, then records only events accepted by the configured bridge. */
1954
+ async #renderSurface(state, event) {
1955
+ const bridge = this.#deps.uiBridge;
1956
+ if (!bridge?.renderSurface) {
1957
+ state.trace.record("run.warning", { message: "UiBridge does not support renderSurface; UI surface was not rendered" });
1958
+ return;
1959
+ }
1960
+ const attributed = this.#attributeSurfaceEvent(state, event);
1961
+ if (attributed.type === "patch") this.#consumeSurfacePatchBudget(state);
1962
+ await bridge.renderSurface(attributed);
1963
+ state.surfaceEvents.push(structuredClone(attributed));
1964
+ if (attributed.type === "open") {
1965
+ const waiting = surfaceActions(attributed.surface).filter((action) => action.awaitResponse);
1966
+ if (waiting.length > 1) throw new WebSkillError("VALIDATION_FAILED", "A UI surface can wait for only one action");
1967
+ const action = waiting[0];
1968
+ 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" });
1969
+ else if (state.pendingSurfaceAction) throw new WebSkillError("VALIDATION_FAILED", "Only one UI surface action can be pending at a time");
1970
+ else state.pendingSurfaceAction = {
1971
+ runId: state.runId,
1972
+ surfaceId: attributed.surface.id,
1973
+ actionId: action.id,
1974
+ intent: action.intent,
1975
+ nonce: action.nonce
1976
+ };
1977
+ }
1978
+ }
1979
+ #consumeSurfacePatchBudget(state) {
1980
+ const now = Date.now();
1981
+ if (now - state.surfacePatchWindowStartedAt >= 1e3) {
1982
+ state.surfacePatchWindowStartedAt = now;
1983
+ state.surfacePatchCount = 0;
1984
+ }
1985
+ state.surfacePatchCount += 1;
1986
+ 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`);
1987
+ }
1988
+ /** Assigns unforgeable action nonces after model output has passed structural validation. */
1989
+ #attributeSurfaceEvent(state, event) {
1990
+ const actions = event.type === "open" ? surfaceActions(event.surface) : [];
1991
+ if (event.type !== "open" || actions.length === 0) return {
1992
+ ...event,
1993
+ runId: state.runId
1994
+ };
1995
+ return {
1996
+ type: "open",
1997
+ runId: state.runId,
1998
+ surface: {
1999
+ ...event.surface,
2000
+ actions: actions.map((action) => ({
2001
+ ...action,
2002
+ nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
2003
+ }))
2004
+ }
2005
+ };
2006
+ }
2007
+ /** Awaits the single action emitted with the most recently persisted tool result. */
2008
+ async #drainSurfaceAction(state) {
2009
+ const request = state.pendingSurfaceAction;
2010
+ if (!request) return;
2011
+ state.pendingSurfaceAction = void 0;
2012
+ await this.#acceptSurfaceAction(state, request);
2013
+ }
2014
+ /** Restores a checkpointed action wait without re-running the tool that emitted its surface. */
2015
+ async #resumeSurfaceAction(state, request) {
2016
+ await this.#acceptSurfaceAction(state, request, true);
2017
+ }
2018
+ async #acceptSurfaceAction(state, request, resumed = false) {
2019
+ const response = await this.#interactSurfaceAction(state, request, resumed);
2020
+ if (response.cancelled) throw new RunTerminated({
2021
+ status: "cancelled",
2022
+ reason: "user-cancelled",
2023
+ message: "Surface action cancelled by user",
2024
+ code: "RUN_CANCELLED"
2025
+ });
2026
+ 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");
2027
+ if (state.processedSurfaceActionNonces.has(response.nonce)) throw new BridgeRequestError("UiBridge returned a surface action response with an already consumed capability nonce");
2028
+ state.processedSurfaceActionNonces.add(response.nonce);
2029
+ state.messages.push({
2030
+ role: "user",
2031
+ content: JSON.stringify({
2032
+ type: "webskill_surface_action",
2033
+ surfaceId: response.surfaceId,
2034
+ actionId: response.actionId,
2035
+ intent: response.intent,
2036
+ value: response.value ?? null
2037
+ })
2038
+ });
2039
+ }
2040
+ async #interactSurfaceAction(state, request, resumed) {
2041
+ const bridge = this.#deps.uiBridge;
2042
+ if (!bridge?.requestSurfaceAction) throw new BridgeRequestError("UiBridge does not support requestSurfaceAction");
2043
+ this.#disarmDeadline(state);
2044
+ const waitStartMs = Date.parse(state.now());
2045
+ try {
2046
+ const { run } = state;
2047
+ run.status = "interrupted";
2048
+ run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
2049
+ await this.#saveSnapshot(state, { surfaceAction: request });
2050
+ await this.#lifecycle("interact", state, {
2051
+ surfaceId: request.surfaceId,
2052
+ actionId: request.actionId,
2053
+ nonce: request.nonce,
2054
+ ...resumed ? { resumed: true } : {}
2055
+ });
2056
+ state.trace.record("ui.surface-action.requested", { data: {
2057
+ surfaceId: request.surfaceId,
2058
+ actionId: request.actionId,
2059
+ nonce: request.nonce,
2060
+ ...resumed ? { resumed: true } : {}
2061
+ } });
2062
+ const response = await this.#withInteractionTimeout(bridge.requestSurfaceAction(request), this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
2063
+ run.status = "running";
2064
+ run.interruptExpiresAt = void 0;
2065
+ state.trace.record("ui.surface-action.resolved", { data: {
2066
+ surfaceId: request.surfaceId,
2067
+ actionId: request.actionId,
2068
+ nonce: request.nonce
2069
+ } });
2070
+ await this.#lifecycle("execute", state, { surfaceAction: request.actionId });
2071
+ return response;
2072
+ } catch (e) {
2073
+ state.run.status = "running";
2074
+ state.run.interruptExpiresAt = void 0;
2075
+ if (e instanceof WebSkillError && e.code === "RUN_INTERACTION_TIMEOUT") throw new RunTerminated({
2076
+ status: "failed",
2077
+ reason: "interaction-timeout",
2078
+ message: e.message,
2079
+ code: "RUN_INTERACTION_TIMEOUT"
2080
+ });
2081
+ if (e instanceof RunTerminated || e instanceof BridgeRequestError) throw e;
2082
+ throw new BridgeRequestError(messageOf(e));
2083
+ } finally {
2084
+ state.pausedMs += Date.parse(state.now()) - waitStartMs;
2085
+ this.#armDeadline(state);
2086
+ }
2087
+ }
2088
+ /** Replays the persisted stream before an interrupted interaction is rendered again. */
2089
+ async #replaySurfaceEvents(state) {
2090
+ const bridge = this.#deps.uiBridge;
2091
+ if (!bridge?.renderSurface || state.surfaceEvents.length === 0) return;
2092
+ try {
2093
+ for (const event of state.surfaceEvents) await bridge.renderSurface(structuredClone(event));
2094
+ } catch (e) {
2095
+ state.trace.record("run.warning", { message: `Failed to replay UI surfaces: ${messageOf(e)}` });
2096
+ }
2097
+ }
1714
2098
  /**
1715
- * 逐工具实时事件(execute 相位,data.type='tool'):chatbot ToolCallCard 等的 live 状态源;
2099
+ * 逐工具实时事件(execute 相位,data.type='tool'):chatbot 思维链工具行等的 live 状态源;
1716
2100
  * 与既有 execute 相位事件({turn})并存,监听方按 data.type 区分。
1717
- * data.args 为参数摘要(JSON 截断 100 字符,卡片展开详情用)。
2101
+ * data.args 为参数摘要(JSON 截断 100 字符,展开详情用)。
1718
2102
  */
1719
2103
  #emitTool(state, status, call) {
1720
2104
  this.#deps.eventBus?.emit({
@@ -2899,4 +3283,4 @@ var CapabilityApproval = class CapabilityApproval {
2899
3283
  };
2900
3284
 
2901
3285
  //#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 };
3286
+ export { fromVercelStreamPart as A, toLlmToolSpec as B, bridgeError as C, extractChartSpec as D, createWebSkillApi as E, normalizeToolContent as F, validateUiSurface as H, normalizeToolError as I, parseBridgeRequest as L, mergeCatalogEntries as M, networkUrlHost as N, extractUiSurfaceEvents as O, normalizeErrorCode as P, resolveToolName as R, WebSkillRuntime as S, createScriptContext as T, validateUiSurfaceEvent as U, toVercelToolSpecs as V, READ_SKILL_FILE_TOOL as _, AnthropicClient as a, SerializingMemoryStore as b, FsArtifactStore as c, FullDisclosureRouter as d, GoogleGenAiClient as f, READ_SKILL_FILE_INPUT_SCHEMA as g, ProgressiveRouter as h, AgentLoop as i, isNetworkAllowed as j, fromVercelResult as k, FsMemoryStore as l, OpenAiCompatibleClient as m, ASK_USER_TOOL as n, CapabilityApproval as o, HookRunner as p, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsRunSnapshotStore as u, READ_SKILL_FILE_TOOL_NAME as v, buildRenderResult as w, TraceRecorder as x, RUN_SNAPSHOT_SCHEMA_VERSION as y, schemaToForm as z };
@@ -1,5 +1,5 @@
1
1
  import { A as resolveArchiveLimits, C as messageOf, D as readResponseWithLimit, E as parseSkillPackManifest, M as unzipWithLimits, N as validateSkills, P as verifyManifest, T as parseSkillMarkdown, b as exportSkills, d as assertRemoteUrlAllowed, h as buildManifest, i as SKILL_MANIFEST_FILE, j as resolveInsideRoot, p as atomicWriteText, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, x as isValidSkillName } from "./dist-BQzncxXg.js";
2
- import { A as isNetworkAllowed, C as bridgeError, F as normalizeToolError, I as parseBridgeRequest, M as networkUrlHost, P as normalizeToolContent, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-BXpDDZpR.js";
2
+ import { C as bridgeError, F as normalizeToolContent, I as normalizeToolError, L as parseBridgeRequest, N as networkUrlHost, c as FsArtifactStore, j as isNetworkAllowed, l as FsMemoryStore, o as CapabilityApproval } from "./dist-BdOW8N4V.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { unzipSync, zipSync } from "fflate";
5
5
  import { existsSync, promises, realpathSync } from "node:fs";