@webskill/sdk 0.2.5 → 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.
@@ -40,7 +40,7 @@ var OpenAiCompatibleClient = class {
40
40
  #fetch;
41
41
  constructor(config) {
42
42
  this.#config = config;
43
- this.#fetch = config.fetchImpl ?? fetch;
43
+ this.#fetch = config.fetchImpl ?? ((input, init) => fetch(input, init));
44
44
  }
45
45
  #requireConfig() {
46
46
  const { baseUrl, apiKey, model } = this.#config;
@@ -262,7 +262,7 @@ var AnthropicClient = class {
262
262
  #fetch;
263
263
  constructor(config) {
264
264
  this.#config = config;
265
- this.#fetch = config.fetchImpl ?? fetch;
265
+ this.#fetch = config.fetchImpl ?? ((input, init) => fetch(input, init));
266
266
  }
267
267
  #baseUrl() {
268
268
  return (this.#config.baseUrl ?? "https://api.anthropic.com").replace(/\/+$/, "");
@@ -491,7 +491,7 @@ var GoogleGenAiClient = class {
491
491
  #fetch;
492
492
  constructor(config) {
493
493
  this.#config = config;
494
- this.#fetch = config.fetchImpl ?? fetch;
494
+ this.#fetch = config.fetchImpl ?? ((input, init) => fetch(input, init));
495
495
  }
496
496
  #baseUrl() {
497
497
  return (this.#config.baseUrl ?? "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
@@ -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;
@@ -1092,6 +1299,12 @@ const toolError = (code, message) => ({
1092
1299
  message
1093
1300
  }
1094
1301
  });
1302
+ /** 工具参数摘要(JSON 截断 100 字符;trace 与实时事件同一口径,ToolCallCard 展开详情用) */
1303
+ const summarizeArgs = (args) => {
1304
+ const json = JSON.stringify(args);
1305
+ return json.length > 100 ? `${json.slice(0, 100)}…` : json;
1306
+ };
1307
+ const surfaceActions = (surface) => "actions" in surface ? surface.actions ?? [] : [];
1095
1308
  /**
1096
1309
  * 多轮 Agent 循环。
1097
1310
  * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
@@ -1101,6 +1314,10 @@ var AgentLoop = class {
1101
1314
  #deps;
1102
1315
  #config;
1103
1316
  #policy;
1317
+ /** 活跃 run 的 AbortController(cancel(runId) 触发;终态清理) */
1318
+ #controllers = /* @__PURE__ */ new Map();
1319
+ /** 经 cancel() 主动取消的 run(aborted 分支据此区分 cancelled 与 timeout) */
1320
+ #cancelled = /* @__PURE__ */ new Set();
1104
1321
  constructor(deps, config = {}) {
1105
1322
  this.#deps = {
1106
1323
  ...deps,
@@ -1121,6 +1338,18 @@ var AgentLoop = class {
1121
1338
  interactionTimeoutMs: deps.interaction?.interactionTimeoutMs ?? 3e5
1122
1339
  };
1123
1340
  }
1341
+ /**
1342
+ * 取消进行中的 run:触发该 run 的 AbortController(与 totalTimeout 硬期限同一通道),
1343
+ * run 以 cancelled(RUN_CANCELLED)终止;未找到(已终态或不属于本实例)返回 false。
1344
+ * 取消在下一个中断点生效(LLM complete/stream 调用;交互等待不强制中断)。
1345
+ */
1346
+ cancel(runId) {
1347
+ const controller = this.#controllers.get(runId);
1348
+ if (!controller) return false;
1349
+ this.#cancelled.add(runId);
1350
+ controller.abort();
1351
+ return true;
1352
+ }
1124
1353
  async run(input) {
1125
1354
  const now = this.#deps.clock?.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1126
1355
  const runId = input.runId ?? `run-${Math.random().toString(36).slice(2, 10)}`;
@@ -1147,15 +1376,21 @@ var AgentLoop = class {
1147
1376
  toolTimeoutMs: this.#config.toolTimeoutMs,
1148
1377
  now,
1149
1378
  interactionSeq: 0,
1379
+ surfaceActionSeq: 0,
1150
1380
  messages: [],
1151
1381
  turn: 0,
1152
1382
  renderBlocks: [],
1383
+ surfaceEvents: [],
1384
+ surfacePatchWindowStartedAt: Date.now(),
1385
+ surfacePatchCount: 0,
1386
+ processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
1153
1387
  startMs,
1154
1388
  pausedMs: 0,
1155
1389
  maxTurns: this.#config.maxTurns,
1156
1390
  totalTimeoutMs: this.#config.totalTimeoutMs,
1157
1391
  controller: new AbortController()
1158
1392
  };
1393
+ this.#controllers.set(runId, state.controller);
1159
1394
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1160
1395
  const route = this.#deps.catalogFilter ? {
1161
1396
  ...input.route,
@@ -1250,7 +1485,10 @@ var AgentLoop = class {
1250
1485
  signal: state.controller.signal
1251
1486
  });
1252
1487
  } catch (e) {
1253
- if (state.controller.signal.aborted) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1488
+ if (state.controller.signal.aborted) {
1489
+ if (this.#cancelled.has(state.runId)) return finish("cancelled", "user-cancelled", "Run cancelled by user", "RUN_CANCELLED");
1490
+ return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1491
+ }
1254
1492
  const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
1255
1493
  return finish("failed", "llm-error", messageOf(e), code);
1256
1494
  }
@@ -1285,6 +1523,7 @@ var AgentLoop = class {
1285
1523
  toolCallId: call.id,
1286
1524
  content: await this.#serializeToolResult(call, result, state)
1287
1525
  });
1526
+ await this.#drainSurfaceAction(state);
1288
1527
  }
1289
1528
  }
1290
1529
  } finally {
@@ -1294,6 +1533,8 @@ var AgentLoop = class {
1294
1533
  /** 统一终态处理:completed/failed/cancelled;终态即删快照(快照是续命机制,审计归治理) */
1295
1534
  async #finish(state, status, reason, output, errorCode) {
1296
1535
  this.#disarmDeadline(state);
1536
+ this.#controllers.delete(state.runId);
1537
+ this.#cancelled.delete(state.runId);
1297
1538
  const { run, trace } = state;
1298
1539
  run.status = status;
1299
1540
  run.terminationReason = reason;
@@ -1338,7 +1579,7 @@ var AgentLoop = class {
1338
1579
  };
1339
1580
  }
1340
1581
  /** D3 快照写入(含 pendingInteraction 与过期时间;写失败降级 run.warning) */
1341
- async #saveSnapshot(state, request) {
1582
+ async #saveSnapshot(state, pending) {
1342
1583
  const store = this.#deps.snapshotStore;
1343
1584
  if (!store) return;
1344
1585
  const snapshot = {
@@ -1351,10 +1592,14 @@ var AgentLoop = class {
1351
1592
  turn: state.turn,
1352
1593
  activeSkillNames: [...state.activated].sort(),
1353
1594
  activatedTools: [...state.activatedTools.values()],
1354
- pendingInteraction: request,
1595
+ ...pending.interaction ? { pendingInteraction: pending.interaction } : {},
1596
+ ...pending.surfaceAction ? { pendingSurfaceAction: pending.surfaceAction } : {},
1355
1597
  interactionExpiresAt: state.run.interruptExpiresAt,
1356
1598
  renderBlocks: [...state.renderBlocks],
1599
+ surfaceEvents: state.surfaceEvents.map((event) => structuredClone(event)),
1357
1600
  interactionSeq: state.interactionSeq,
1601
+ surfaceActionSeq: state.surfaceActionSeq,
1602
+ processedSurfaceActionNonces: [...state.processedSurfaceActionNonces],
1358
1603
  pausedMs: state.pausedMs,
1359
1604
  config: {
1360
1605
  maxTurns: state.maxTurns,
@@ -1399,25 +1644,35 @@ var AgentLoop = class {
1399
1644
  toolTimeoutMs: snapshot.config.toolTimeoutMs,
1400
1645
  now,
1401
1646
  interactionSeq: snapshot.interactionSeq ?? 0,
1647
+ surfaceActionSeq: snapshot.surfaceActionSeq ?? 0,
1402
1648
  messages: snapshot.messages.map((m) => ({ ...m })),
1403
1649
  turn: snapshot.turn,
1404
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 ?? []),
1405
1655
  startMs,
1406
1656
  pausedMs: snapshot.pausedMs ?? 0,
1407
1657
  maxTurns: snapshot.config.maxTurns,
1408
1658
  totalTimeoutMs: snapshot.config.totalTimeoutMs,
1409
1659
  controller: new AbortController()
1410
1660
  };
1661
+ this.#controllers.set(runId, state.controller);
1411
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;
1412
1665
  trace.record("run.resumed", { data: {
1413
1666
  snapshotAt: snapshot.snapshotAt,
1414
1667
  turn: snapshot.turn,
1415
- interactionType: snapshot.pendingInteraction.type
1668
+ interactionType: pendingSurfaceAction ? "surface-action" : pendingInteraction?.type
1416
1669
  } });
1417
- const pending = snapshot.pendingInteraction;
1670
+ const pending = pendingInteraction;
1418
1671
  const pendingCall = this.#findPendingToolCall(state.messages);
1419
1672
  try {
1420
- 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) {
1421
1676
  const value = await this.#interact(state, pending, {
1422
1677
  tool: pendingCall.name,
1423
1678
  resumed: true
@@ -1435,7 +1690,8 @@ var AgentLoop = class {
1435
1690
  toolCallId: pendingCall.id,
1436
1691
  content: await this.#serializeToolResult(pendingCall, result, state)
1437
1692
  });
1438
- } else if (pending.type === "ask" && pendingCall) {
1693
+ await this.#drainSurfaceAction(state);
1694
+ } else if (pending?.type === "ask" && pendingCall) {
1439
1695
  const value = await this.#interact(state, pending, {
1440
1696
  tool: pendingCall.name,
1441
1697
  resumed: true
@@ -1456,6 +1712,7 @@ var AgentLoop = class {
1456
1712
  toolCallId: pendingCall.id,
1457
1713
  content: await this.#serializeToolResult(pendingCall, result, state)
1458
1714
  });
1715
+ await this.#drainSurfaceAction(state);
1459
1716
  } else if (pendingCall) {
1460
1717
  const result = await this.#executeCall(pendingCall, state);
1461
1718
  state.messages.push({
@@ -1463,6 +1720,7 @@ var AgentLoop = class {
1463
1720
  toolCallId: pendingCall.id,
1464
1721
  content: await this.#serializeToolResult(pendingCall, result, state)
1465
1722
  });
1723
+ await this.#drainSurfaceAction(state);
1466
1724
  }
1467
1725
  for (;;) {
1468
1726
  const next = this.#findPendingToolCall(state.messages);
@@ -1473,6 +1731,7 @@ var AgentLoop = class {
1473
1731
  toolCallId: next.id,
1474
1732
  content: await this.#serializeToolResult(next, result, state)
1475
1733
  });
1734
+ await this.#drainSurfaceAction(state);
1476
1735
  }
1477
1736
  } catch (e) {
1478
1737
  if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
@@ -1569,7 +1828,7 @@ var AgentLoop = class {
1569
1828
  const { run } = state;
1570
1829
  run.status = "interrupted";
1571
1830
  run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
1572
- await this.#saveSnapshot(state, request);
1831
+ await this.#saveSnapshot(state, { interaction: request });
1573
1832
  await this.#lifecycle("interact", state, {
1574
1833
  interactionId: request.id,
1575
1834
  type: request.type
@@ -1628,10 +1887,14 @@ var AgentLoop = class {
1628
1887
  });
1629
1888
  }
1630
1889
  async #executeCall(call, state) {
1890
+ const argsSummary = summarizeArgs(call.arguments);
1891
+ const callStartMs = Date.parse(state.now());
1631
1892
  state.trace.record("tool.started", { data: {
1632
1893
  name: call.name,
1633
- callId: call.id
1894
+ callId: call.id,
1895
+ args: argsSummary
1634
1896
  } });
1897
+ this.#emitTool(state, "started", call);
1635
1898
  let result;
1636
1899
  if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
1637
1900
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
@@ -1643,11 +1906,15 @@ var AgentLoop = class {
1643
1906
  result = source ? await source.call(call.name, call.arguments) : toolError(resolution.code, resolution.message);
1644
1907
  }
1645
1908
  }
1909
+ const durationMs = Date.parse(state.now()) - callStartMs;
1646
1910
  if (result.ok) {
1647
1911
  state.trace.record("tool.completed", { data: {
1648
1912
  name: call.name,
1649
- callId: call.id
1913
+ callId: call.id,
1914
+ args: argsSummary,
1915
+ durationMs
1650
1916
  } });
1917
+ this.#emitTool(state, "completed", call);
1651
1918
  for (const item of result.content) {
1652
1919
  if (item.type !== "json") continue;
1653
1920
  const chart = extractChartSpec(item.data);
@@ -1655,21 +1922,199 @@ var AgentLoop = class {
1655
1922
  type: "chart",
1656
1923
  chart
1657
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
+ }
1658
1933
  }
1659
- } else state.trace.record("tool.failed", {
1660
- message: result.error?.message,
1661
- data: {
1662
- name: call.name,
1663
- callId: call.id,
1664
- code: result.error?.code
1665
- }
1666
- });
1934
+ } else {
1935
+ state.trace.record("tool.failed", {
1936
+ message: result.error?.message,
1937
+ data: {
1938
+ name: call.name,
1939
+ callId: call.id,
1940
+ code: result.error?.code,
1941
+ args: argsSummary,
1942
+ durationMs
1943
+ }
1944
+ });
1945
+ this.#emitTool(state, "failed", call);
1946
+ }
1667
1947
  for (const artifact of result.artifacts ?? []) state.trace.record("artifact.created", { data: {
1668
1948
  artifactId: artifact.id,
1669
1949
  path: artifact.path
1670
1950
  } });
1671
1951
  return result;
1672
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
+ }
2098
+ /**
2099
+ * 逐工具实时事件(execute 相位,data.type='tool'):chatbot 思维链工具行等的 live 状态源;
2100
+ * 与既有 execute 相位事件({turn})并存,监听方按 data.type 区分。
2101
+ * data.args 为参数摘要(JSON 截断 100 字符,展开详情用)。
2102
+ */
2103
+ #emitTool(state, status, call) {
2104
+ this.#deps.eventBus?.emit({
2105
+ phase: "execute",
2106
+ runId: state.runId,
2107
+ sessionId: state.run.sessionId,
2108
+ ts: state.now(),
2109
+ data: {
2110
+ type: "tool",
2111
+ status,
2112
+ name: call.name,
2113
+ callId: call.id,
2114
+ args: summarizeArgs(call.arguments)
2115
+ }
2116
+ });
2117
+ }
1673
2118
  async #handleAskUser(call, state) {
1674
2119
  const question = call.arguments["question"];
1675
2120
  if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires a non-empty \"question\" string argument");
@@ -2140,6 +2585,8 @@ var WebSkillRuntime = class {
2140
2585
  #session;
2141
2586
  #events;
2142
2587
  #catalogCache;
2588
+ /** 活跃 run 的 loop 实例(cancel(runId) 路由;终态清理) */
2589
+ #loops = /* @__PURE__ */ new Map();
2143
2590
  constructor(deps) {
2144
2591
  this.#deps = {
2145
2592
  ...deps,
@@ -2224,7 +2671,7 @@ var WebSkillRuntime = class {
2224
2671
  const catalog = providerEntries.length > 0 ? { entries: mergeCatalogEntries(cache.catalog.entries, providerEntries) } : cache.catalog;
2225
2672
  const filteredCatalog = this.#deps.catalogFilter ? { entries: await this.#deps.catalogFilter(catalog.entries) } : catalog;
2226
2673
  const route = await this.#router.route(filteredCatalog);
2227
- const result = await new AgentLoop({
2674
+ const loop = new AgentLoop({
2228
2675
  llm: this.#deps.llm,
2229
2676
  executor: this.#deps.executor,
2230
2677
  schemaInferer: this.#deps.schemaInferer,
@@ -2243,12 +2690,21 @@ var WebSkillRuntime = class {
2243
2690
  catalogFilter: this.#deps.catalogFilter,
2244
2691
  snapshotStore: this.#deps.snapshotStore,
2245
2692
  skillStateGuard: this.#deps.skillStateGuard
2246
- }, this.#deps.config).run({
2247
- sessionId: options.sessionId ?? this.#session.id,
2248
- userPrompt,
2249
- route,
2250
- ...options.history ? { history: options.history } : {}
2251
- });
2693
+ }, this.#deps.config);
2694
+ const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
2695
+ this.#loops.set(runId, loop);
2696
+ let result;
2697
+ try {
2698
+ result = await loop.run({
2699
+ sessionId: options.sessionId ?? this.#session.id,
2700
+ userPrompt,
2701
+ route,
2702
+ runId,
2703
+ ...options.history ? { history: options.history } : {}
2704
+ });
2705
+ } finally {
2706
+ this.#loops.delete(runId);
2707
+ }
2252
2708
  for (const failure of providerFailures) result.run.trace.push({
2253
2709
  id: `evt-provider-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2254
2710
  runId: result.run.id,
@@ -2323,7 +2779,7 @@ var WebSkillRuntime = class {
2323
2779
  if (!this.#catalogCache) await this.discover();
2324
2780
  const cache = this.#catalogCache;
2325
2781
  if (!cache) throw new WebSkillError("RUN_FAILED", "discover() did not populate the catalog cache");
2326
- return new AgentLoop({
2782
+ const loop = new AgentLoop({
2327
2783
  llm: this.#deps.llm,
2328
2784
  executor: this.#deps.executor,
2329
2785
  schemaInferer: this.#deps.schemaInferer,
@@ -2342,7 +2798,21 @@ var WebSkillRuntime = class {
2342
2798
  catalogFilter: this.#deps.catalogFilter,
2343
2799
  snapshotStore: store,
2344
2800
  skillStateGuard: this.#deps.skillStateGuard
2345
- }, this.#deps.config).resume(snapshot);
2801
+ }, this.#deps.config);
2802
+ this.#loops.set(runId, loop);
2803
+ try {
2804
+ return await loop.resume(snapshot);
2805
+ } finally {
2806
+ this.#loops.delete(runId);
2807
+ }
2808
+ }
2809
+ /**
2810
+ * 取消进行中的 run(chatbot Stop 按钮等):触发该 run 的 AbortController,
2811
+ * run 以 cancelled(RUN_CANCELLED)终止;未找到活跃 run 返回 false。
2812
+ * 取消在下一个中断点生效(LLM complete/stream;交互等待不强制中断)。
2813
+ */
2814
+ cancel(runId) {
2815
+ return this.#loops.get(runId)?.cancel(runId) ?? false;
2346
2816
  }
2347
2817
  };
2348
2818
  /**
@@ -2813,4 +3283,4 @@ var CapabilityApproval = class CapabilityApproval {
2813
3283
  };
2814
3284
 
2815
3285
  //#endregion
2816
- 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 };