@webskill/sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/browser.d.ts +2 -2
  2. package/dist/browser.js +2 -6
  3. package/dist/{catalogComponents-C_V39rbF-BOHveMWa.js → catalogComponents-KsujmL4b-Clx1kCnU.js} +278 -122
  4. package/dist/{dist-rorEJsNi.js → dist-C-Sh0MDU.js} +498 -282
  5. package/dist/{dist-ZKaM8j06.js → dist-D9Lcn5Pp.js} +527 -837
  6. package/dist/governance.d.ts +45 -10
  7. package/dist/governance.js +151 -24
  8. package/dist/{index-wiV5X8Rz.d.ts → index-CHXxDccV.d.ts} +62 -144
  9. package/dist/{index-8d-oEDww.d.ts → index-DLfR2Y6I.d.ts} +215 -23
  10. package/dist/index.d.ts +3 -3
  11. package/dist/index.js +3 -2
  12. package/dist/mcp.d.ts +2 -2
  13. package/dist/mcp.js +1 -1
  14. package/dist/memoryArtifactStore-BtOeB_hm-tj3fC5ip.js +78 -0
  15. package/dist/node.d.ts +8 -4
  16. package/dist/node.js +1 -1
  17. package/dist/{openUiLibrary-B8-Cvou9-BbpNTXS3.js → openUiLibrary-YLS-cxyT-C96jWDQq.js} +6 -5
  18. package/dist/{skillVersionStore-DOEI9ptb-BxbYL70B.d.ts → skillVersionStore-uyefLPR1-DXOzbksv.d.ts} +41 -10
  19. package/dist/{testing-CsrG3XLz.js → testing-DDCJWvgA.js} +7 -5
  20. package/dist/testing.d.ts +1 -1
  21. package/dist/testing.js +2 -2
  22. package/dist/{types-AmKCKJn_-VGabeXK4.d.ts → types-7Wcg--Vh-1YlQ4jF9.d.ts} +85 -71
  23. package/dist/ui-react.d.ts +329 -18
  24. package/dist/ui-react.js +3714 -3463
  25. package/dist/ui-vue.d.ts +1 -1
  26. package/dist/ui-vue.js +1 -1
  27. package/dist/ui.d.ts +4 -3
  28. package/dist/ui.js +3 -3
  29. package/dist/{webskillLitCatalog-CNaUpasU-BslMcxRZ.js → webskillLitCatalog-CSTbhBe_-CYIs5BX8.js} +312 -122
  30. package/package.json +1 -1
  31. package/dist/jsonRenderRegistry-9GrWP_hE-U6Do3Kid.js +0 -2468
  32. package/dist/memoryArtifactStore-C9lFVqPF-yFz6yJj0.js +0 -48
@@ -1,5 +1,5 @@
1
1
  import { A as parseSkillMarkdown, L as resolveInsideRoot, O as messageOf, P as renderAvailableSkillsXml, V as validateSkills, f as SkillDiscovery, g as assertSafePathSegment, m as WebSkillError, p as SkillReader, v as buildCatalog } from "./dist-8oQRa8Xz.js";
2
- import { t as MemoryArtifactStore } from "./memoryArtifactStore-C9lFVqPF-yFz6yJj0.js";
2
+ import { a as validateLlmMessages, i as textParts, n as partsToText, r as rejectUnsupportedPart, t as MemoryArtifactStore } from "./memoryArtifactStore-BtOeB_hm-tj3fC5ip.js";
3
3
 
4
4
  //#region ../runtime/dist/index.js
5
5
  function createSseFrameReader() {
@@ -47,27 +47,65 @@ function createSseFrameReader() {
47
47
  }
48
48
  };
49
49
  }
50
+ const dataUrl = (part) => `data:${part.mimeType};base64,${part.data}`;
51
+ /** parts → OpenAI content;纯文本折叠成字符串(兼容端点对数组形态支持不一) */
52
+ const toOpenAiContent = (parts, where) => {
53
+ if (parts.every((p) => p.type === "text")) return partsToText(parts);
54
+ return parts.map((part) => {
55
+ switch (part.type) {
56
+ case "text": return {
57
+ type: "text",
58
+ text: part.text
59
+ };
60
+ case "image": return {
61
+ type: "image_url",
62
+ image_url: { url: dataUrl(part) }
63
+ };
64
+ case "file": return {
65
+ type: "file",
66
+ file: {
67
+ ...part.name !== void 0 ? { filename: part.name } : {},
68
+ file_data: dataUrl(part)
69
+ }
70
+ };
71
+ default: return rejectUnsupportedPart(part, "OpenAI", where);
72
+ }
73
+ });
74
+ };
75
+ /** system / tool 消息只接受文本:这两类在 OpenAI 协议里没有多模态形态 */
76
+ const toOpenAiText = (parts, where) => {
77
+ const nonText = parts.find((p) => p.type !== "text");
78
+ if (nonText) rejectUnsupportedPart(nonText, "OpenAI", where);
79
+ return partsToText(parts);
80
+ };
50
81
  const toOpenAiMessage = (msg) => {
51
82
  if (msg.role === "tool") return {
52
83
  role: "tool",
53
84
  tool_call_id: msg.toolCallId,
54
- content: msg.content
85
+ content: toOpenAiText(msg.content, "tool")
55
86
  };
56
- if (msg.role === "assistant" && msg.toolCalls?.length) return {
57
- role: "assistant",
58
- content: msg.content === "" ? null : msg.content,
59
- tool_calls: msg.toolCalls.map((call) => ({
60
- id: call.id,
61
- type: "function",
62
- function: {
63
- name: call.name,
64
- arguments: JSON.stringify(call.arguments)
65
- }
66
- }))
87
+ if (msg.role === "system") return {
88
+ role: "system",
89
+ content: toOpenAiText(msg.content, "system")
67
90
  };
91
+ if (msg.role === "assistant" && msg.toolCalls?.length) {
92
+ const content = toOpenAiContent(msg.content, "assistant");
93
+ return {
94
+ role: "assistant",
95
+ content: content === "" ? null : content,
96
+ tool_calls: msg.toolCalls.map((call) => ({
97
+ id: call.id,
98
+ type: "function",
99
+ function: {
100
+ name: call.name,
101
+ arguments: JSON.stringify(call.arguments)
102
+ }
103
+ }))
104
+ };
105
+ }
68
106
  return {
69
107
  role: msg.role,
70
- content: msg.content
108
+ content: toOpenAiContent(msg.content, msg.role)
71
109
  };
72
110
  };
73
111
  const toOpenAiTools = (tools) => tools.map((tool) => ({
@@ -181,6 +219,7 @@ var OpenAiCompatibleClient = class {
181
219
  }
182
220
  async #postChat(input, stream) {
183
221
  const { baseUrl, apiKey, model } = this.#requireConfig();
222
+ validateLlmMessages(input.messages);
184
223
  const body = {
185
224
  model: input.model ?? model,
186
225
  messages: input.messages.map(toOpenAiMessage)
@@ -234,7 +273,7 @@ var OpenAiCompatibleClient = class {
234
273
  });
235
274
  const content = choice["content"];
236
275
  return {
237
- content: typeof content === "string" ? content : void 0,
276
+ content: typeof content === "string" && content !== "" ? textParts(content) : void 0,
238
277
  toolCalls: toolCalls?.length ? toolCalls : void 0,
239
278
  raw: data
240
279
  };
@@ -245,9 +284,41 @@ var OpenAiCompatibleClient = class {
245
284
  };
246
285
  const ANTHROPIC_VERSION = "2023-06-01";
247
286
  const errorMessage$1 = (e) => e instanceof Error ? e.message : String(e);
287
+ /** parts → Anthropic content blocks(image/document 均为 base64 source) */
288
+ const toAnthropicBlocks = (parts, where) => parts.map((part) => {
289
+ switch (part.type) {
290
+ case "text": return {
291
+ type: "text",
292
+ text: part.text
293
+ };
294
+ case "image": return {
295
+ type: "image",
296
+ source: {
297
+ type: "base64",
298
+ media_type: part.mimeType,
299
+ data: part.data
300
+ }
301
+ };
302
+ case "file": return {
303
+ type: "document",
304
+ source: {
305
+ type: "base64",
306
+ media_type: part.mimeType,
307
+ data: part.data
308
+ }
309
+ };
310
+ default: return rejectUnsupportedPart(part, "Anthropic", where);
311
+ }
312
+ });
313
+ /** system 是独立的字符串参数,无多模态形态 */
314
+ const toAnthropicSystemText = (parts) => {
315
+ const nonText = parts.find((p) => p.type !== "text");
316
+ if (nonText) rejectUnsupportedPart(nonText, "Anthropic", "system");
317
+ return partsToText(parts);
318
+ };
248
319
  /** LlmMessage 序列 → system 独立参数 + user/assistant 消息(tool 结果合并进 user 消息 tool_result blocks) */
249
320
  function toAnthropicMessages(messages) {
250
- const system = messages.filter((m) => m.role === "system").map((m) => m.content).join("\n");
321
+ const system = messages.filter((m) => m.role === "system").map((m) => toAnthropicSystemText(m.content)).join("\n");
251
322
  const out = [];
252
323
  for (const msg of messages) {
253
324
  if (msg.role === "system") continue;
@@ -255,7 +326,7 @@ function toAnthropicMessages(messages) {
255
326
  const block = {
256
327
  type: "tool_result",
257
328
  tool_use_id: msg.toolCallId ?? "",
258
- content: msg.content
329
+ content: toAnthropicBlocks(msg.content, "tool")
259
330
  };
260
331
  const last = out.at(-1);
261
332
  if (last && last.role === "user" && Array.isArray(last.content)) last.content.push(block);
@@ -266,11 +337,7 @@ function toAnthropicMessages(messages) {
266
337
  continue;
267
338
  }
268
339
  if (msg.role === "assistant" && msg.toolCalls?.length) {
269
- const content = [];
270
- if (msg.content !== "") content.push({
271
- type: "text",
272
- text: msg.content
273
- });
340
+ const content = toAnthropicBlocks(msg.content, "assistant");
274
341
  for (const call of msg.toolCalls) content.push({
275
342
  type: "tool_use",
276
343
  id: call.id,
@@ -285,7 +352,7 @@ function toAnthropicMessages(messages) {
285
352
  }
286
353
  out.push({
287
354
  role: msg.role,
288
- content: msg.content
355
+ content: toAnthropicBlocks(msg.content, msg.role)
289
356
  });
290
357
  }
291
358
  return system === "" ? { messages: out } : {
@@ -409,6 +476,7 @@ var AnthropicClient = class {
409
476
  yield { type: "done" };
410
477
  }
411
478
  async #post(input, stream) {
479
+ validateLlmMessages(input.messages);
412
480
  const { system, messages } = toAnthropicMessages(input.messages);
413
481
  const body = {
414
482
  model: input.model ?? this.#config.model,
@@ -455,7 +523,7 @@ var AnthropicClient = class {
455
523
  arguments: typeof b["input"] === "object" && b["input"] !== null ? b["input"] : {}
456
524
  }));
457
525
  return {
458
- content: text === "" ? void 0 : text,
526
+ content: text === "" ? void 0 : textParts(text),
459
527
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
460
528
  raw: data
461
529
  };
@@ -465,6 +533,24 @@ var AnthropicClient = class {
465
533
  }
466
534
  };
467
535
  const errorMessage = (e) => e instanceof Error ? e.message : String(e);
536
+ /** parts → GenAI parts(二进制统一走 inlineData) */
537
+ const toGenAiParts = (parts, where) => parts.map((part) => {
538
+ switch (part.type) {
539
+ case "text": return { text: part.text };
540
+ case "image":
541
+ case "file": return { inlineData: {
542
+ mimeType: part.mimeType,
543
+ data: part.data
544
+ } };
545
+ default: return rejectUnsupportedPart(part, "Google GenAI", where);
546
+ }
547
+ });
548
+ /** systemInstruction 与 functionResponse 只接受文本 */
549
+ const toGenAiText = (parts, where) => {
550
+ const nonText = parts.find((p) => p.type !== "text");
551
+ if (nonText) rejectUnsupportedPart(nonText, "Google GenAI", where);
552
+ return partsToText(parts);
553
+ };
468
554
  /** tool 结果文本包装为 functionResponse.response 对象 */
469
555
  function responseObject(content) {
470
556
  try {
@@ -475,7 +561,7 @@ function responseObject(content) {
475
561
  }
476
562
  /** LlmMessage 序列 → systemInstruction + contents(tool 结果合并进 user 消息 functionResponse parts) */
477
563
  function toGenAiContents(messages) {
478
- const system = messages.filter((m) => m.role === "system").map((m) => m.content).join("\n");
564
+ const system = messages.filter((m) => m.role === "system").map((m) => toGenAiText(m.content, "system")).join("\n");
479
565
  const nameByCallId = /* @__PURE__ */ new Map();
480
566
  for (const msg of messages) if (msg.role === "assistant") for (const call of msg.toolCalls ?? []) nameByCallId.set(call.id, call.name);
481
567
  const out = [];
@@ -485,7 +571,7 @@ function toGenAiContents(messages) {
485
571
  const callId = msg.toolCallId ?? "";
486
572
  const part = { functionResponse: {
487
573
  name: nameByCallId.get(callId) ?? callId,
488
- response: responseObject(msg.content)
574
+ response: responseObject(toGenAiText(msg.content, "tool"))
489
575
  } };
490
576
  const last = out.at(-1);
491
577
  if (last && last.role === "user") last.parts.push(part);
@@ -496,8 +582,7 @@ function toGenAiContents(messages) {
496
582
  continue;
497
583
  }
498
584
  if (msg.role === "assistant") {
499
- const parts = [];
500
- if (msg.content !== "") parts.push({ text: msg.content });
585
+ const parts = toGenAiParts(msg.content, "assistant");
501
586
  for (const call of msg.toolCalls ?? []) parts.push({ functionCall: {
502
587
  name: call.name,
503
588
  args: call.arguments
@@ -510,7 +595,7 @@ function toGenAiContents(messages) {
510
595
  }
511
596
  out.push({
512
597
  role: "user",
513
- parts: [{ text: msg.content }]
598
+ parts: toGenAiParts(msg.content, msg.role)
514
599
  });
515
600
  }
516
601
  return system === "" ? { contents: out } : {
@@ -546,7 +631,7 @@ var GoogleGenAiClient = class {
546
631
  const text = parts.filter((p) => typeof p["text"] === "string").map((p) => String(p["text"])).join("");
547
632
  const toolCalls = this.#functionCalls(parts);
548
633
  return {
549
- content: text === "" ? void 0 : text,
634
+ content: text === "" ? void 0 : textParts(text),
550
635
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
551
636
  raw: data
552
637
  };
@@ -602,6 +687,7 @@ var GoogleGenAiClient = class {
602
687
  async #post(input, stream) {
603
688
  const model = input.model ?? this.#config.model;
604
689
  const action = stream ? ":streamGenerateContent?alt=sse" : ":generateContent";
690
+ validateLlmMessages(input.messages);
605
691
  const { systemInstruction, contents } = toGenAiContents(input.messages);
606
692
  const body = { contents };
607
693
  if (systemInstruction) body["systemInstruction"] = systemInstruction;
@@ -677,7 +763,7 @@ function fromVercelResult(result) {
677
763
  };
678
764
  });
679
765
  return {
680
- content: typeof r.text === "string" && r.text !== "" ? r.text : void 0,
766
+ content: typeof r.text === "string" && r.text !== "" ? textParts(r.text) : void 0,
681
767
  toolCalls: toolCalls.length ? toolCalls : void 0,
682
768
  raw: result
683
769
  };
@@ -960,11 +1046,8 @@ function buildRenderResult(run, output, renderBlocks = []) {
960
1046
  }
961
1047
  const MAX_SURFACE_BYTES = 256 * 1024;
962
1048
  const MAX_ACTIONS = 32;
963
- const MAX_FORM_FIELDS = 64;
964
- const MAX_OPTIONS = 256;
965
- const MAX_TABLE_COLUMNS = 128;
966
- const MAX_TABLE_ROWS = 1e4;
967
- const MAX_CHART_POINTS = 2e4;
1049
+ const MAX_NODES = 2e3;
1050
+ const MAX_NODE_DEPTH = 32;
968
1051
  const MAX_JSON_DEPTH = 16;
969
1052
  const MAX_PATCH_OPERATIONS = 128;
970
1053
  const actionIntents = /* @__PURE__ */ new Set([
@@ -974,16 +1057,6 @@ const actionIntents = /* @__PURE__ */ new Set([
974
1057
  "download",
975
1058
  "refresh"
976
1059
  ]);
977
- const fieldTypes = /* @__PURE__ */ new Set([
978
- "text",
979
- "number",
980
- "date",
981
- "textarea",
982
- "select",
983
- "multi-select",
984
- "toggle",
985
- "file"
986
- ]);
987
1060
  function isRecord$1(value) {
988
1061
  return typeof value === "object" && value !== null && !Array.isArray(value);
989
1062
  }
@@ -1004,98 +1077,39 @@ function isJsonValue(value, depth = 0) {
1004
1077
  if (!isRecord$1(value)) return false;
1005
1078
  return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
1006
1079
  }
1080
+ /**
1081
+ * Runtime 只做结构校验;组件名白名单与 props schema 属于 catalog 语义,
1082
+ * 留在 `@webskill/ui`——runtime 不得为了校验反向依赖 ui。
1083
+ */
1084
+ function assertNode(value, path, depth, counter) {
1085
+ if (depth > MAX_NODE_DEPTH) reject(`A UI spec tree must not nest deeper than ${MAX_NODE_DEPTH} levels`);
1086
+ if (++counter.nodes > MAX_NODES) reject(`A UI spec tree must contain at most ${MAX_NODES} nodes`);
1087
+ if (!isRecord$1(value)) reject(`${path} must be an object`);
1088
+ requireString(value["component"], `${path}.component`);
1089
+ if (value["id"] !== void 0) requireString(value["id"], `${path}.id`);
1090
+ if (value["props"] !== void 0 && (!isRecord$1(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
1091
+ const children = value["children"];
1092
+ if (children === void 0) return;
1093
+ if (!Array.isArray(children)) reject(`${path}.children must be an array`);
1094
+ children.forEach((child, index) => assertNode(child, `${path}.children[${index}]`, depth + 1, counter));
1095
+ }
1007
1096
  function assertActions(value) {
1008
1097
  if (value === void 0) return;
1009
1098
  if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
1010
1099
  for (const action of value) {
1011
1100
  if (!isRecord$1(action)) reject("A surface action must be an object");
1012
1101
  requireString(action["id"], "Surface action ID");
1013
- requireString(action["label"], "Surface action label");
1014
- if (typeof action["intent"] !== "string" || !actionIntents.has(action["intent"])) reject("Surface action intent is invalid");
1015
- if (action["disabled"] !== void 0 && typeof action["disabled"] !== "boolean") reject("Surface action disabled must be a boolean");
1102
+ const intent = action["intent"];
1103
+ if (typeof intent !== "string" || !actionIntents.has(intent)) reject("Surface action intent is invalid");
1016
1104
  if (action["awaitResponse"] !== void 0 && typeof action["awaitResponse"] !== "boolean") reject("Surface action awaitResponse must be a boolean");
1017
1105
  if (action["nonce"] !== void 0 && (typeof action["nonce"] !== "string" || action["nonce"] === "")) reject("Surface action nonce must be a non-empty string");
1018
1106
  }
1019
1107
  }
1020
- function assertForm(surface) {
1021
- if (!Array.isArray(surface["fields"]) || surface["fields"].length > MAX_FORM_FIELDS) reject(`A form surface must contain at most ${MAX_FORM_FIELDS} fields`);
1022
- for (const field of surface["fields"]) {
1023
- if (!isRecord$1(field)) reject("A form field must be an object");
1024
- requireString(field["name"], "Form field name");
1025
- requireString(field["label"], "Form field label");
1026
- if (typeof field["type"] !== "string" || !fieldTypes.has(field["type"])) reject("Form field type is invalid");
1027
- if (field["required"] !== void 0 && typeof field["required"] !== "boolean") reject("Form field required must be a boolean");
1028
- if (field["description"] !== void 0 && typeof field["description"] !== "string") reject("Form field description must be a string");
1029
- if (field["defaultValue"] !== void 0 && !isJsonValue(field["defaultValue"])) reject("Form field defaultValue must be JSON data");
1030
- if (field["options"] !== void 0) {
1031
- if (!Array.isArray(field["options"]) || field["options"].length > MAX_OPTIONS) reject(`Form field options must contain at most ${MAX_OPTIONS} items`);
1032
- for (const option of field["options"]) {
1033
- if (!isRecord$1(option)) reject("A form option must be an object");
1034
- requireString(option["label"], "Form option label");
1035
- if (!isJsonValue(option["value"])) reject("Form option value must be JSON data");
1036
- }
1037
- }
1038
- }
1039
- assertActions(surface["actions"]);
1040
- }
1041
- function assertChart(surface) {
1042
- const chart = surface["chart"];
1043
- if (!isRecord$1(chart)) reject("A chart surface requires a chart object");
1044
- if (chart["kind"] !== "bar" && chart["kind"] !== "line" && chart["kind"] !== "pie") reject("Chart kind is invalid");
1045
- if (!Array.isArray(chart["labels"]) || !chart["labels"].every((label) => typeof label === "string")) reject("Chart labels must be an array of strings");
1046
- if (!Array.isArray(chart["series"])) reject("Chart series must be an array");
1047
- let points = 0;
1048
- for (const series of chart["series"]) {
1049
- 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");
1050
- if (series["name"] !== void 0 && typeof series["name"] !== "string") reject("Chart series name must be a string");
1051
- points += series["data"].length;
1052
- }
1053
- if (points > MAX_CHART_POINTS) reject(`Chart data exceeds the ${MAX_CHART_POINTS}-point limit`);
1054
- assertActions(surface["actions"]);
1055
- }
1056
- function assertTable(surface) {
1057
- 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`);
1058
- if (!Array.isArray(surface["rows"]) || surface["rows"].length > MAX_TABLE_ROWS) reject(`Table rows must contain at most ${MAX_TABLE_ROWS} items`);
1059
- 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");
1060
- assertActions(surface["actions"]);
1061
- }
1062
- /** Validates the allowlisted, data-only shape accepted by a UI surface renderer. @experimental */
1063
- function validateUiSurface(value) {
1064
- if (!isRecord$1(value)) reject("A UI surface must be an object");
1065
- requireString(value["id"], "UI surface ID");
1066
- if (value["title"] !== void 0 && typeof value["title"] !== "string") reject("UI surface title must be a string");
1067
- switch (value["kind"]) {
1068
- case "form":
1069
- assertForm(value);
1070
- break;
1071
- case "chart":
1072
- assertChart(value);
1073
- break;
1074
- case "table":
1075
- assertTable(value);
1076
- break;
1077
- case "metric":
1078
- requireString(value["label"], "Metric label");
1079
- if (typeof value["value"] !== "string" && (typeof value["value"] !== "number" || !Number.isFinite(value["value"]))) reject("Metric value must be a string or finite number");
1080
- if (value["trend"] !== void 0 && value["trend"] !== "up" && value["trend"] !== "down" && value["trend"] !== "neutral") reject("Metric trend is invalid");
1081
- break;
1082
- case "file": {
1083
- requireString(value["path"], "File path");
1084
- if (value["mimeType"] !== void 0 && typeof value["mimeType"] !== "string") reject("File mimeType must be a string");
1085
- const fileSize = value["size"];
1086
- if (fileSize !== void 0 && (typeof fileSize !== "number" || !Number.isSafeInteger(fileSize) || fileSize < 0)) reject("File size must be a non-negative integer");
1087
- assertActions(value["actions"]);
1088
- break;
1089
- }
1090
- case "custom":
1091
- requireString(value["component"], "Custom surface component");
1092
- if (!isJsonValue(value["props"])) reject("Custom surface props must be JSON data");
1093
- assertActions(value["actions"]);
1094
- break;
1095
- default: reject("UI surface kind is invalid");
1096
- }
1097
- if (!isJsonValue(value)) reject("A UI surface must contain JSON data only");
1098
- if (JSON.stringify(value).length > MAX_SURFACE_BYTES) reject(`A UI surface exceeds the ${MAX_SURFACE_BYTES}-byte limit`);
1108
+ /** Validates the allowlisted, data-only node tree accepted by a UI surface renderer. @experimental */
1109
+ function validateUiSpecNode(value) {
1110
+ assertNode(value, "root", 0, { nodes: 0 });
1111
+ if (!isJsonValue(value)) reject("A UI spec tree must contain JSON data only");
1112
+ if (JSON.stringify(value).length > MAX_SURFACE_BYTES) reject(`A UI spec tree exceeds the ${MAX_SURFACE_BYTES}-byte limit`);
1099
1113
  return structuredClone(value);
1100
1114
  }
1101
1115
  function assertPatch(value) {
@@ -1106,15 +1120,20 @@ function assertPatch(value) {
1106
1120
  if (!isJsonValue(value["value"])) reject("Surface patch value must be JSON data");
1107
1121
  }
1108
1122
  /** Validates an individual event in the framework-neutral surface stream. @experimental */
1109
- function validateUiSurfaceEvent(value) {
1123
+ function validateUiSpecEvent(value) {
1110
1124
  if (!isRecord$1(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
1111
1125
  if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
1112
1126
  switch (value["type"]) {
1113
- case "open": return {
1114
- type: "open",
1115
- ...value["runId"] ? { runId: value["runId"] } : {},
1116
- surface: validateUiSurface(value["surface"])
1117
- };
1127
+ case "open":
1128
+ requireString(value["id"], "UI surface ID");
1129
+ assertActions(value["actions"]);
1130
+ return {
1131
+ type: "open",
1132
+ ...value["runId"] ? { runId: value["runId"] } : {},
1133
+ id: value["id"],
1134
+ node: validateUiSpecNode(value["node"]),
1135
+ ...value["actions"] ? { actions: structuredClone(value["actions"]) } : {}
1136
+ };
1118
1137
  case "patch":
1119
1138
  requireString(value["id"], "Surface patch ID");
1120
1139
  requireNonNegativeInteger(value["revision"], "Surface patch revision");
@@ -1159,10 +1178,10 @@ function validateUiSurfaceEvent(value) {
1159
1178
  }
1160
1179
  }
1161
1180
  /** Extracts validated surface stream events from structured tool output. @experimental */
1162
- function extractUiSurfaceEvents(data) {
1181
+ function extractUiSpecEvents(data) {
1163
1182
  if (!isRecord$1(data) || data["$surface"] === void 0) return [];
1164
1183
  const raw = data["$surface"];
1165
- return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSurfaceEvent(event));
1184
+ return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
1166
1185
  }
1167
1186
  /**
1168
1187
  * JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
@@ -1309,6 +1328,103 @@ var TraceRecorder = class {
1309
1328
  return [...this.#events];
1310
1329
  }
1311
1330
  };
1331
+ const RUN_SNAPSHOT_SCHEMA_VERSION = 2;
1332
+ /** @experimental */
1333
+ function isUnsupportedRunSnapshot(entry) {
1334
+ return entry.unsupported === true;
1335
+ }
1336
+ const SNAPSHOT_SUFFIX = ".snapshot.json";
1337
+ /**
1338
+ * FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
1339
+ * 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
1340
+ * save/list 时顺带清理已过 interactionExpiresAt 的过期快照。
1341
+ *
1342
+ * 数据敏感性说明:快照含完整对话历史(用户输入、工具结果、可能的凭据片段),
1343
+ * 以明文 JSON 落盘于宿主提供的 fs;宿主应将其视为会话数据同等保护。
1344
+ * @experimental
1345
+ */
1346
+ var FsRunSnapshotStore = class {
1347
+ #root;
1348
+ #fs;
1349
+ constructor(deps) {
1350
+ this.#root = deps.root.replace(/\/+$/, "");
1351
+ this.#fs = deps.fs;
1352
+ }
1353
+ #path(runId) {
1354
+ return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
1355
+ }
1356
+ async save(snapshot) {
1357
+ await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
1358
+ await this.#pruneExpired();
1359
+ }
1360
+ async load(runId) {
1361
+ const path = this.#path(runId);
1362
+ if (!await this.#fs.exists(path)) return void 0;
1363
+ let parsed;
1364
+ try {
1365
+ parsed = JSON.parse(await this.#fs.readText(path));
1366
+ } catch (e) {
1367
+ await this.#fs.remove(path).catch(() => void 0);
1368
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
1369
+ }
1370
+ const snapshot = parsed;
1371
+ if (typeof snapshot !== "object" || snapshot === null) {
1372
+ await this.#fs.remove(path).catch(() => void 0);
1373
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
1374
+ }
1375
+ const schemaVersion = typeof snapshot.schemaVersion === "number" ? snapshot.schemaVersion : 0;
1376
+ if (schemaVersion !== 2) throw new WebSkillError("RUN_SNAPSHOT_SCHEMA_UNSUPPORTED", `Snapshot for run "${runId}" uses schema version ${schemaVersion}; this runtime reads version 2. The file was kept for read-only inspection.`);
1377
+ if (snapshot.runId !== runId) {
1378
+ await this.#fs.remove(path).catch(() => void 0);
1379
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
1380
+ }
1381
+ return snapshot;
1382
+ }
1383
+ async delete(runId) {
1384
+ const path = this.#path(runId);
1385
+ if (await this.#fs.exists(path)) await this.#fs.remove(path);
1386
+ }
1387
+ async list() {
1388
+ await this.#pruneExpired();
1389
+ if (!await this.#fs.exists(this.#root)) return [];
1390
+ const out = [];
1391
+ for (const entry of await this.#fs.list(this.#root)) {
1392
+ if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
1393
+ try {
1394
+ const parsed = JSON.parse(await this.#fs.readText(entry.path));
1395
+ const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 0;
1396
+ if (schemaVersion === 2) {
1397
+ out.push(parsed);
1398
+ continue;
1399
+ }
1400
+ out.push({
1401
+ unsupported: true,
1402
+ schemaVersion,
1403
+ runId: String(parsed.runId ?? entry.path),
1404
+ snapshotAt: typeof parsed.snapshotAt === "string" ? parsed.snapshotAt : "",
1405
+ ...typeof parsed.sessionId === "string" ? { sessionId: parsed.sessionId } : {},
1406
+ ...typeof parsed.userPrompt === "string" ? { userPrompt: parsed.userPrompt } : {},
1407
+ ...typeof parsed.interactionExpiresAt === "string" ? { interactionExpiresAt: parsed.interactionExpiresAt } : {}
1408
+ });
1409
+ } catch {}
1410
+ }
1411
+ return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
1412
+ }
1413
+ /** 过期快照清理(save/list 时顺带;失败静默不阻断主流程) */
1414
+ async #pruneExpired() {
1415
+ try {
1416
+ if (!await this.#fs.exists(this.#root)) return;
1417
+ const now = Date.now();
1418
+ for (const entry of await this.#fs.list(this.#root)) {
1419
+ if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
1420
+ try {
1421
+ const parsed = JSON.parse(await this.#fs.readText(entry.path));
1422
+ if (parsed.interactionExpiresAt !== void 0 && Date.parse(parsed.interactionExpiresAt) < now) await this.#fs.remove(entry.path);
1423
+ } catch {}
1424
+ }
1425
+ } catch {}
1426
+ }
1427
+ };
1312
1428
  /**
1313
1429
  * LLM 可见名 → 作者在 SKILL.md 里写的形态。
1314
1430
  * `<已激活技能>__<脚本>` 保持原样(裸标识符按技能作用域单独匹配),
@@ -1338,7 +1454,7 @@ function denialReason(state, canonical) {
1338
1454
  const skills = [...state.skillAllowedTools.keys()].sort().map((s) => `"${s}"`);
1339
1455
  const subject = skills.length === 1 ? `Skill ${skills[0]} declares` : `Skills ${skills.join(", ")} declare`;
1340
1456
  const slash = canonical.lastIndexOf("/");
1341
- return `${subject} allowed-tools, but tool "${canonical}" is not listed. It is still available in 0.3.0 but WILL BE REJECTED in 0.4.0. Add "${canonical}"${slash > 0 ? ` or "${canonical.slice(0, slash)}/*"` : ""} to allowed-tools.`;
1457
+ return `${subject} allowed-tools, but tool "${canonical}" is not listed and was rejected. Add "${canonical}"${slash > 0 ? ` or "${canonical.slice(0, slash)}/*"` : ""} to allowed-tools.`;
1342
1458
  }
1343
1459
  /**
1344
1460
  * 多技能语义:无人声明 → 不受限;有人声明 → 命中任一清单,
@@ -1379,7 +1495,6 @@ const summarizeArgs = (args) => {
1379
1495
  const json = JSON.stringify(args);
1380
1496
  return json.length > 100 ? `${json.slice(0, 100)}…` : json;
1381
1497
  };
1382
- const surfaceActions = (surface) => "actions" in surface ? surface.actions ?? [] : [];
1383
1498
  /**
1384
1499
  * 多轮 Agent 循环。
1385
1500
  * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
@@ -1450,6 +1565,7 @@ var AgentLoop = class {
1450
1565
  activatedTools: /* @__PURE__ */ new Map(),
1451
1566
  skillAllowedTools: /* @__PURE__ */ new Map(),
1452
1567
  warnedDeniedTools: /* @__PURE__ */ new Set(),
1568
+ integrityVerdicts: /* @__PURE__ */ new Map(),
1453
1569
  toolTimeoutMs: this.#config.toolTimeoutMs,
1454
1570
  now,
1455
1571
  interactionSeq: 0,
@@ -1478,7 +1594,13 @@ var AgentLoop = class {
1478
1594
  strategy: route.strategy,
1479
1595
  skillCount: route.catalog.entries.length
1480
1596
  } });
1481
- await this.#lifecycle("route", state, { strategy: route.strategy });
1597
+ await this.#lifecycle({
1598
+ phase: "route",
1599
+ data: {
1600
+ strategy: route.strategy,
1601
+ candidates: route.catalog.entries.map((e) => e.name)
1602
+ }
1603
+ }, state);
1482
1604
  const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
1483
1605
  try {
1484
1606
  return await source.listToolSpecs();
@@ -1487,15 +1609,25 @@ var AgentLoop = class {
1487
1609
  return [];
1488
1610
  }
1489
1611
  }))).flat();
1612
+ const externalSystemPrompts = [];
1613
+ for (const source of this.#deps.externalTools ?? []) {
1614
+ if (source.systemPrompt === void 0) continue;
1615
+ try {
1616
+ const text = (await source.systemPrompt())?.trim();
1617
+ if (text !== void 0 && text !== "") externalSystemPrompts.push(text);
1618
+ } catch (e) {
1619
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
1620
+ }
1621
+ }
1490
1622
  state.messages = [
1491
1623
  {
1492
1624
  role: "system",
1493
- content: route.systemPrompt
1625
+ content: textParts([route.systemPrompt, ...externalSystemPrompts].join("\n\n"))
1494
1626
  },
1495
1627
  ...(input.history ?? []).map((m) => ({ ...m })),
1496
1628
  {
1497
1629
  role: "user",
1498
- content: input.userPrompt
1630
+ content: textParts(input.userPrompt)
1499
1631
  }
1500
1632
  ];
1501
1633
  try {
@@ -1527,17 +1659,25 @@ var AgentLoop = class {
1527
1659
  }
1528
1660
  /**
1529
1661
  * D10 判定的唯一出口(暴露点 + 分发点共用)。
1530
- * 0.3.0 只记警告并返回 false,调用点照常放行;0.4.0 收紧时改的是调用点对返回值的处理。
1662
+ * 0.4.0(D1)起两个调用点都看返回值:暴露点过滤、分发点回喂 TOOL_NOT_ALLOWED。
1663
+ * 判定语义与 0.3.0 一致,改的只是调用点与 trace 事件类型(warning → denied)。
1531
1664
  */
1532
1665
  #checkToolAccess(state, toolName) {
1533
1666
  const verdict = evaluateToolAccess(state, toolName);
1534
1667
  if (verdict.allowed) return true;
1535
1668
  if (!state.warnedDeniedTools.has(toolName)) {
1536
1669
  state.warnedDeniedTools.add(toolName);
1537
- state.trace.record("run.warning", { message: verdict.reason ?? `Tool "${toolName}" is not allowed` });
1670
+ state.trace.record("tool.denied", {
1671
+ message: verdict.reason ?? `Tool "${toolName}" is not allowed`,
1672
+ data: { name: toolName }
1673
+ });
1538
1674
  }
1539
1675
  return false;
1540
1676
  }
1677
+ /** 分发点被拒时回喂给模型的结构化错误(不抛异常:模型造名字是常态,抛异常会终止整个 run) */
1678
+ #deniedToolError(state, toolName) {
1679
+ return toolError("TOOL_NOT_ALLOWED", evaluateToolAccess(state, toolName).reason ?? `Tool "${toolName}" is not allowed`);
1680
+ }
1541
1681
  /** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
1542
1682
  async #turnLoop(state, startTurn, externalSpecs) {
1543
1683
  const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
@@ -1549,8 +1689,7 @@ var AgentLoop = class {
1549
1689
  state.turn = turn;
1550
1690
  if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1551
1691
  if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1552
- const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs];
1553
- for (const spec of skillToolSpecs) this.#checkToolAccess(state, spec.name);
1692
+ const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
1554
1693
  const toolSpecs = [
1555
1694
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
1556
1695
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
@@ -1584,22 +1723,29 @@ var AgentLoop = class {
1584
1723
  const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
1585
1724
  return finish("failed", "llm-error", messageOf(e), code);
1586
1725
  }
1726
+ const responseText = partsToText(response.content);
1587
1727
  trace.record("llm.response", { data: {
1588
1728
  turn,
1589
1729
  hasToolCalls: Boolean(response.toolCalls?.length),
1590
- contentLength: response.content?.length ?? 0
1730
+ contentLength: responseText.length
1591
1731
  } });
1592
1732
  if (!response.toolCalls?.length) {
1593
1733
  messages.push({
1594
1734
  role: "assistant",
1595
- content: response.content ?? ""
1735
+ content: response.content ?? []
1596
1736
  });
1597
- return finish("completed", "final-answer", response.content ?? "");
1737
+ return finish("completed", "final-answer", responseText);
1598
1738
  }
1599
- await this.#lifecycle("execute", state, { turn });
1739
+ await this.#lifecycle({
1740
+ phase: "execute",
1741
+ data: {
1742
+ kind: "turn",
1743
+ turn
1744
+ }
1745
+ }, state);
1600
1746
  messages.push({
1601
1747
  role: "assistant",
1602
- content: response.content ?? "",
1748
+ content: response.content ?? [],
1603
1749
  toolCalls: response.toolCalls
1604
1750
  });
1605
1751
  for (const call of response.toolCalls) {
@@ -1613,7 +1759,7 @@ var AgentLoop = class {
1613
1759
  messages.push({
1614
1760
  role: "tool",
1615
1761
  toolCallId: call.id,
1616
- content: await this.#serializeToolResult(call, result, state)
1762
+ content: textParts(await this.#serializeToolResult(call, result, state))
1617
1763
  });
1618
1764
  await this.#drainSurfaceAction(state);
1619
1765
  }
@@ -1659,7 +1805,10 @@ var AgentLoop = class {
1659
1805
  trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
1660
1806
  }
1661
1807
  try {
1662
- await this.#lifecycle(status === "completed" ? "complete" : "fail", state, { reason });
1808
+ await this.#lifecycle({
1809
+ phase: status === "completed" ? "complete" : "fail",
1810
+ data: { reason }
1811
+ }, state);
1663
1812
  } catch (e) {
1664
1813
  trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
1665
1814
  }
@@ -1675,7 +1824,7 @@ var AgentLoop = class {
1675
1824
  const store = this.#deps.snapshotStore;
1676
1825
  if (!store) return;
1677
1826
  const snapshot = {
1678
- schemaVersion: 1,
1827
+ schemaVersion: 2,
1679
1828
  runId: state.runId,
1680
1829
  sessionId: state.run.sessionId,
1681
1830
  userPrompt: state.run.userPrompt,
@@ -1736,6 +1885,7 @@ var AgentLoop = class {
1736
1885
  activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
1737
1886
  skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
1738
1887
  warnedDeniedTools: /* @__PURE__ */ new Set(),
1888
+ integrityVerdicts: /* @__PURE__ */ new Map(),
1739
1889
  toolTimeoutMs: snapshot.config.toolTimeoutMs,
1740
1890
  now,
1741
1891
  interactionSeq: snapshot.interactionSeq ?? 0,
@@ -1784,7 +1934,7 @@ var AgentLoop = class {
1784
1934
  state.messages.push({
1785
1935
  role: "tool",
1786
1936
  toolCallId: pendingCall.id,
1787
- content: await this.#serializeToolResult(pendingCall, result, state)
1937
+ content: textParts(await this.#serializeToolResult(pendingCall, result, state))
1788
1938
  });
1789
1939
  await this.#drainSurfaceAction(state);
1790
1940
  } else if (pending?.type === "ask" && pendingCall) {
@@ -1806,7 +1956,7 @@ var AgentLoop = class {
1806
1956
  state.messages.push({
1807
1957
  role: "tool",
1808
1958
  toolCallId: pendingCall.id,
1809
- content: await this.#serializeToolResult(pendingCall, result, state)
1959
+ content: textParts(await this.#serializeToolResult(pendingCall, result, state))
1810
1960
  });
1811
1961
  await this.#drainSurfaceAction(state);
1812
1962
  } else if (pendingCall) {
@@ -1814,7 +1964,7 @@ var AgentLoop = class {
1814
1964
  state.messages.push({
1815
1965
  role: "tool",
1816
1966
  toolCallId: pendingCall.id,
1817
- content: await this.#serializeToolResult(pendingCall, result, state)
1967
+ content: textParts(await this.#serializeToolResult(pendingCall, result, state))
1818
1968
  });
1819
1969
  await this.#drainSurfaceAction(state);
1820
1970
  }
@@ -1825,7 +1975,7 @@ var AgentLoop = class {
1825
1975
  state.messages.push({
1826
1976
  role: "tool",
1827
1977
  toolCallId: next.id,
1828
- content: await this.#serializeToolResult(next, result, state)
1978
+ content: textParts(await this.#serializeToolResult(next, result, state))
1829
1979
  });
1830
1980
  await this.#drainSurfaceAction(state);
1831
1981
  }
@@ -1868,7 +2018,7 @@ var AgentLoop = class {
1868
2018
  sessionId: state.run.sessionId,
1869
2019
  ts: state.now(),
1870
2020
  data: {
1871
- type: "llm.delta",
2021
+ kind: "llm-delta",
1872
2022
  delta
1873
2023
  }
1874
2024
  });
@@ -1882,23 +2032,23 @@ var AgentLoop = class {
1882
2032
  emitDelta(event.delta);
1883
2033
  } else if (event.type === "tool-calls") toolCalls.push(...event.toolCalls);
1884
2034
  else if (event.type === "done") doneContent = event.content;
2035
+ const text = doneContent ?? content;
1885
2036
  return {
1886
- content: doneContent ?? (content === "" ? void 0 : content),
2037
+ content: text === "" ? void 0 : textParts(text),
1887
2038
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0
1888
2039
  };
1889
2040
  }
1890
2041
  /** 生命周期接线:更新 phase、发事件、跑钩子 */
1891
- async #lifecycle(phase, state, data) {
1892
- state.run.phase = phase;
2042
+ async #lifecycle(init, state) {
2043
+ state.run.phase = init.phase;
1893
2044
  const event = {
1894
- phase,
2045
+ ...init,
1895
2046
  runId: state.runId,
1896
2047
  sessionId: state.run.sessionId,
1897
- ts: state.now(),
1898
- ...data ? { data } : {}
2048
+ ts: state.now()
1899
2049
  };
1900
2050
  this.#deps.eventBus?.emit(event);
1901
- if (this.#deps.hooks) await this.#deps.hooks.run(phase, {
2051
+ if (this.#deps.hooks) await this.#deps.hooks.run(init.phase, {
1902
2052
  event,
1903
2053
  run: state.run
1904
2054
  });
@@ -1925,10 +2075,14 @@ var AgentLoop = class {
1925
2075
  run.status = "interrupted";
1926
2076
  run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
1927
2077
  await this.#saveSnapshot(state, { interaction: request });
1928
- await this.#lifecycle("interact", state, {
1929
- interactionId: request.id,
1930
- type: request.type
1931
- });
2078
+ await this.#lifecycle({
2079
+ phase: "interact",
2080
+ data: {
2081
+ kind: "interaction",
2082
+ interactionId: request.id,
2083
+ interactionType: request.type
2084
+ }
2085
+ }, state);
1932
2086
  state.trace.record("ui.requested", { data: {
1933
2087
  interactionId: request.id,
1934
2088
  type: request.type,
@@ -1961,7 +2115,13 @@ var AgentLoop = class {
1961
2115
  interactionId: request.id,
1962
2116
  type: request.type
1963
2117
  } });
1964
- await this.#lifecycle("execute", state, { resumed: request.id });
2118
+ await this.#lifecycle({
2119
+ phase: "execute",
2120
+ data: {
2121
+ kind: "interaction-resumed",
2122
+ interactionId: request.id
2123
+ }
2124
+ }, state);
1965
2125
  await this.#appendParamHistory(state, request, response.value);
1966
2126
  return response.value;
1967
2127
  }
@@ -1995,8 +2155,8 @@ var AgentLoop = class {
1995
2155
  if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
1996
2156
  else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
1997
2157
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
2158
+ else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
1998
2159
  else {
1999
- this.#checkToolAccess(state, call.name);
2000
2160
  const resolution = resolveToolName(call.name, state.activated);
2001
2161
  if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
2002
2162
  else {
@@ -2021,7 +2181,7 @@ var AgentLoop = class {
2021
2181
  chart
2022
2182
  });
2023
2183
  try {
2024
- for (const event of extractUiSurfaceEvents(item.data)) await this.#renderSurface(state, event);
2184
+ for (const event of extractUiSpecEvents(item.data)) await this.#renderSurface(state, event);
2025
2185
  } catch (e) {
2026
2186
  state.trace.record("run.warning", {
2027
2187
  message: `UI surface rejected: ${messageOf(e)}`,
@@ -2060,14 +2220,14 @@ var AgentLoop = class {
2060
2220
  await bridge.renderSurface(attributed);
2061
2221
  state.surfaceEvents.push(structuredClone(attributed));
2062
2222
  if (attributed.type === "open") {
2063
- const waiting = surfaceActions(attributed.surface).filter((action) => action.awaitResponse);
2223
+ const waiting = (attributed.actions ?? []).filter((action) => action.awaitResponse);
2064
2224
  if (waiting.length > 1) throw new WebSkillError("VALIDATION_FAILED", "A UI surface can wait for only one action");
2065
2225
  const action = waiting[0];
2066
2226
  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" });
2067
2227
  else if (state.pendingSurfaceAction) throw new WebSkillError("VALIDATION_FAILED", "Only one UI surface action can be pending at a time");
2068
2228
  else state.pendingSurfaceAction = {
2069
2229
  runId: state.runId,
2070
- surfaceId: attributed.surface.id,
2230
+ surfaceId: attributed.id,
2071
2231
  actionId: action.id,
2072
2232
  intent: action.intent,
2073
2233
  nonce: action.nonce
@@ -2085,21 +2245,17 @@ var AgentLoop = class {
2085
2245
  }
2086
2246
  /** Assigns unforgeable action nonces after model output has passed structural validation. */
2087
2247
  #attributeSurfaceEvent(state, event) {
2088
- const actions = event.type === "open" ? surfaceActions(event.surface) : [];
2089
- if (event.type !== "open" || actions.length === 0) return {
2248
+ if (event.type !== "open" || (event.actions ?? []).length === 0) return {
2090
2249
  ...event,
2091
2250
  runId: state.runId
2092
2251
  };
2093
2252
  return {
2094
- type: "open",
2253
+ ...event,
2095
2254
  runId: state.runId,
2096
- surface: {
2097
- ...event.surface,
2098
- actions: actions.map((action) => ({
2099
- ...action,
2100
- nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
2101
- }))
2102
- }
2255
+ actions: (event.actions ?? []).map((action) => ({
2256
+ ...action,
2257
+ nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
2258
+ }))
2103
2259
  };
2104
2260
  }
2105
2261
  /** Awaits the single action emitted with the most recently persisted tool result. */
@@ -2126,13 +2282,13 @@ var AgentLoop = class {
2126
2282
  state.processedSurfaceActionNonces.add(response.nonce);
2127
2283
  state.messages.push({
2128
2284
  role: "user",
2129
- content: JSON.stringify({
2285
+ content: textParts(JSON.stringify({
2130
2286
  type: "webskill_surface_action",
2131
2287
  surfaceId: response.surfaceId,
2132
2288
  actionId: response.actionId,
2133
2289
  intent: response.intent,
2134
2290
  value: response.value ?? null
2135
- })
2291
+ }))
2136
2292
  });
2137
2293
  }
2138
2294
  async #interactSurfaceAction(state, request, resumed) {
@@ -2144,20 +2300,26 @@ var AgentLoop = class {
2144
2300
  const { run } = state;
2145
2301
  run.status = "interrupted";
2146
2302
  run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
2303
+ const pendingResponse = bridge.requestSurfaceAction(request);
2304
+ pendingResponse.catch(() => void 0);
2147
2305
  await this.#saveSnapshot(state, { surfaceAction: request });
2148
- await this.#lifecycle("interact", state, {
2149
- surfaceId: request.surfaceId,
2150
- actionId: request.actionId,
2151
- nonce: request.nonce,
2152
- ...resumed ? { resumed: true } : {}
2153
- });
2306
+ await this.#lifecycle({
2307
+ phase: "interact",
2308
+ data: {
2309
+ kind: "surface-action",
2310
+ surfaceId: request.surfaceId,
2311
+ actionId: request.actionId,
2312
+ nonce: request.nonce,
2313
+ resumed
2314
+ }
2315
+ }, state);
2154
2316
  state.trace.record("ui.surface-action.requested", { data: {
2155
2317
  surfaceId: request.surfaceId,
2156
2318
  actionId: request.actionId,
2157
2319
  nonce: request.nonce,
2158
2320
  ...resumed ? { resumed: true } : {}
2159
2321
  } });
2160
- const response = await this.#withInteractionTimeout(bridge.requestSurfaceAction(request), this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
2322
+ const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
2161
2323
  run.status = "running";
2162
2324
  run.interruptExpiresAt = void 0;
2163
2325
  state.trace.record("ui.surface-action.resolved", { data: {
@@ -2165,9 +2327,17 @@ var AgentLoop = class {
2165
2327
  actionId: request.actionId,
2166
2328
  nonce: request.nonce
2167
2329
  } });
2168
- await this.#lifecycle("execute", state, { surfaceAction: request.actionId });
2330
+ await this.#lifecycle({
2331
+ phase: "execute",
2332
+ data: {
2333
+ kind: "surface-action-resumed",
2334
+ surfaceId: request.surfaceId,
2335
+ actionId: request.actionId
2336
+ }
2337
+ }, state);
2169
2338
  return response;
2170
2339
  } catch (e) {
2340
+ bridge.cancelSurfaceAction?.(request.nonce);
2171
2341
  state.run.status = "running";
2172
2342
  state.run.interruptExpiresAt = void 0;
2173
2343
  if (e instanceof WebSkillError && e.code === "RUN_INTERACTION_TIMEOUT") throw new RunTerminated({
@@ -2215,7 +2385,7 @@ var AgentLoop = class {
2215
2385
  sessionId: state.run.sessionId,
2216
2386
  ts: state.now(),
2217
2387
  data: {
2218
- type: "tool",
2388
+ kind: "tool",
2219
2389
  status,
2220
2390
  name: call.name,
2221
2391
  callId: call.id,
@@ -2285,7 +2455,13 @@ var AgentLoop = class {
2285
2455
  skillName: name,
2286
2456
  source: "external"
2287
2457
  } });
2288
- await this.#lifecycle("activate", state, { skillName: name });
2458
+ await this.#lifecycle({
2459
+ phase: "activate",
2460
+ data: {
2461
+ skillName: name,
2462
+ source: "external"
2463
+ }
2464
+ }, state);
2289
2465
  await this.#writeActivationMemory(name, state);
2290
2466
  }
2291
2467
  return {
@@ -2339,18 +2515,62 @@ var AgentLoop = class {
2339
2515
  if (!check) return false;
2340
2516
  return await check(skillName) === false;
2341
2517
  }
2518
+ /**
2519
+ * D3 激活期完整性校验。结论按技能名在 run 内缓存,**失败也缓存**:
2520
+ * 校验失败的技能不会进 `activated` 集合,不缓存的话同一个坏技能每尝试激活一次
2521
+ * 就要把它的文件全扫一遍——成本随文件数线性放大,正是需求 §3 验收 3 要防的。
2522
+ *
2523
+ * 设计 §3.5 写的缓存键是 `(skillName, manifest.integrity.digest)`,但 digest 只有
2524
+ * **调用之后**才知道;runtime 读不到 manifest,两元组键在这一层无法实现。
2525
+ * 实际键是技能名,digest 作为结论的一部分留在 trace 里供事后对账。
2526
+ *
2527
+ * 失败不抛错:抛错会终止整个 run,而「某个技能被改过」不该让其余技能一起停摆。
2528
+ */
2529
+ async #integrityOk(skillName, state) {
2530
+ const guard = this.#deps.skillIntegrityGuard;
2531
+ if (!guard?.verifyOnActivate) return true;
2532
+ let verdict = state.integrityVerdicts.get(skillName);
2533
+ if (verdict === void 0) {
2534
+ try {
2535
+ verdict = await guard.verifyOnActivate(skillName);
2536
+ } catch (e) {
2537
+ verdict = {
2538
+ ok: false,
2539
+ reason: `the integrity guard threw: ${messageOf(e)}`
2540
+ };
2541
+ }
2542
+ state.integrityVerdicts.set(skillName, verdict);
2543
+ }
2544
+ if (verdict.ok) return true;
2545
+ state.trace.record("skill.integrity-failed", {
2546
+ message: `Skill "${skillName}" failed integrity verification on activation: ${verdict.reason ?? "no reason given"}`,
2547
+ data: {
2548
+ skillName,
2549
+ ...verdict.digest !== void 0 ? { digest: verdict.digest } : {}
2550
+ }
2551
+ });
2552
+ return false;
2553
+ }
2342
2554
  /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
2343
2555
  async #activateSkill(skillName, state, via, skillMdText) {
2344
2556
  if (await this.#guardDenied("canActivate", skillName)) {
2345
2557
  state.trace.record("run.warning", { message: `Skill "${skillName}" is blocked by the skill state guard (activate)` });
2346
2558
  return "";
2347
2559
  }
2560
+ if (!await this.#integrityOk(skillName, state)) return "";
2348
2561
  state.activated.add(skillName);
2349
2562
  state.trace.record("skill.activated", { data: {
2350
2563
  skillName,
2351
2564
  ...via ? { via } : {}
2352
2565
  } });
2353
- await this.#lifecycle("activate", state, { skillName });
2566
+ await this.#lifecycle({
2567
+ phase: "activate",
2568
+ data: {
2569
+ skillName,
2570
+ source: "local",
2571
+ ...via ? { via } : {}
2572
+ }
2573
+ }, state);
2354
2574
  await this.#writeActivationMemory(skillName, state);
2355
2575
  const root = this.#deps.skillIndex.get(skillName);
2356
2576
  if (!root) return "";
@@ -2484,11 +2704,41 @@ var AgentLoop = class {
2484
2704
  if (fresh.length > 0) result.artifacts = [...result.artifacts ?? [], ...fresh];
2485
2705
  }
2486
2706
  await this.#bumpSkillStat(skillName, result.ok ? "successes" : "failures", state);
2707
+ if (!result.ok) await this.#reportSkillFailure(skillName, state, {
2708
+ code: result.error?.code ?? "TOOL_EXECUTION_FAILED",
2709
+ message: result.error?.message ?? `Tool "${call.name}" returned a failure result`
2710
+ });
2487
2711
  return result;
2488
2712
  } catch (e) {
2489
2713
  if (e instanceof RunTerminated) throw e;
2490
2714
  await this.#bumpSkillStat(skillName, "failures", state);
2491
- return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `Tool "${call.name}" failed: ${messageOf(e)}`);
2715
+ const code = e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED";
2716
+ const message = `Tool "${call.name}" failed: ${messageOf(e)}`;
2717
+ await this.#reportSkillFailure(skillName, state, {
2718
+ code,
2719
+ message
2720
+ });
2721
+ return toolError(code, message);
2722
+ }
2723
+ }
2724
+ /**
2725
+ * F1 技能失败上报:喂给治理的失败计数器(`SkillStatePolicy.recordFailure`),
2726
+ * 达阈值即自动隔离。无注入即整段跳过(默认关闭)。
2727
+ *
2728
+ * 上报方抛错降级为 `run.warning`:治理写盘失败不该把一次「工具出错但已回喂给 LLM」
2729
+ * 的 run 变成崩溃——那会让引入治理反而降低可用性。
2730
+ */
2731
+ async #reportSkillFailure(skillName, state, detail) {
2732
+ const report = this.#deps.skillOutcomeReporter?.onSkillFailed;
2733
+ if (report === void 0) return;
2734
+ try {
2735
+ await report.call(this.#deps.skillOutcomeReporter, {
2736
+ skillName,
2737
+ runId: state.runId,
2738
+ ...detail
2739
+ });
2740
+ } catch (e) {
2741
+ state.trace.record("run.warning", { message: `Skill failure report for "${skillName}" was not recorded: ${messageOf(e)}` });
2492
2742
  }
2493
2743
  }
2494
2744
  /** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
@@ -2598,79 +2848,6 @@ function mergeCatalogEntries(localEntries, providerEntries) {
2598
2848
  for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
2599
2849
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2600
2850
  }
2601
- const RUN_SNAPSHOT_SCHEMA_VERSION = 1;
2602
- const SNAPSHOT_SUFFIX = ".snapshot.json";
2603
- /**
2604
- * FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
2605
- * 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
2606
- * save/list 时顺带清理已过 interactionExpiresAt 的过期快照。
2607
- *
2608
- * 数据敏感性说明:快照含完整对话历史(用户输入、工具结果、可能的凭据片段),
2609
- * 以明文 JSON 落盘于宿主提供的 fs;宿主应将其视为会话数据同等保护。
2610
- * @experimental
2611
- */
2612
- var FsRunSnapshotStore = class {
2613
- #root;
2614
- #fs;
2615
- constructor(deps) {
2616
- this.#root = deps.root.replace(/\/+$/, "");
2617
- this.#fs = deps.fs;
2618
- }
2619
- #path(runId) {
2620
- return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
2621
- }
2622
- async save(snapshot) {
2623
- await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
2624
- await this.#pruneExpired();
2625
- }
2626
- async load(runId) {
2627
- const path = this.#path(runId);
2628
- if (!await this.#fs.exists(path)) return void 0;
2629
- let parsed;
2630
- try {
2631
- parsed = JSON.parse(await this.#fs.readText(path));
2632
- } catch (e) {
2633
- await this.#fs.remove(path).catch(() => void 0);
2634
- throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
2635
- }
2636
- const snapshot = parsed;
2637
- if (typeof snapshot !== "object" || snapshot === null || snapshot.runId !== runId) {
2638
- await this.#fs.remove(path).catch(() => void 0);
2639
- throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
2640
- }
2641
- return snapshot;
2642
- }
2643
- async delete(runId) {
2644
- const path = this.#path(runId);
2645
- if (await this.#fs.exists(path)) await this.#fs.remove(path);
2646
- }
2647
- async list() {
2648
- await this.#pruneExpired();
2649
- if (!await this.#fs.exists(this.#root)) return [];
2650
- const out = [];
2651
- for (const entry of await this.#fs.list(this.#root)) {
2652
- if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
2653
- try {
2654
- out.push(JSON.parse(await this.#fs.readText(entry.path)));
2655
- } catch {}
2656
- }
2657
- return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
2658
- }
2659
- /** 过期快照清理(save/list 时顺带;失败静默不阻断主流程) */
2660
- async #pruneExpired() {
2661
- try {
2662
- if (!await this.#fs.exists(this.#root)) return;
2663
- const now = Date.now();
2664
- for (const entry of await this.#fs.list(this.#root)) {
2665
- if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
2666
- try {
2667
- const parsed = JSON.parse(await this.#fs.readText(entry.path));
2668
- if (parsed.interactionExpiresAt !== void 0 && Date.parse(parsed.interactionExpiresAt) < now) await this.#fs.remove(entry.path);
2669
- } catch {}
2670
- }
2671
- } catch {}
2672
- }
2673
- };
2674
2851
  /**
2675
2852
  * session history 滚动裁剪:超出预算时裁掉中段,保留首尾。
2676
2853
  * 边界对齐 LLM tool 契约:head 不以未应答的 assistant toolCalls 结尾,
@@ -2804,7 +2981,9 @@ var WebSkillRuntime = class {
2804
2981
  skillProviders: this.#deps.skillProviders,
2805
2982
  catalogFilter: this.#deps.catalogFilter,
2806
2983
  snapshotStore: this.#deps.snapshotStore,
2807
- skillStateGuard: this.#deps.skillStateGuard
2984
+ skillStateGuard: this.#deps.skillStateGuard,
2985
+ skillIntegrityGuard: this.#deps.skillIntegrityGuard,
2986
+ skillOutcomeReporter: this.#deps.skillOutcomeReporter
2808
2987
  }, this.#deps.config);
2809
2988
  const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
2810
2989
  this.#loops.set(runId, loop);
@@ -2841,14 +3020,14 @@ var WebSkillRuntime = class {
2841
3020
  });
2842
3021
  return result;
2843
3022
  }
2844
- /** D3:列出可恢复的 interrupted run(供 UI 展示"未完成任务" */
3023
+ /** D3:列出 interrupted run(供 UI 展示"未完成任务");版本不受支持的项带 unsupported 标记 */
2845
3024
  async listInterruptedRuns() {
2846
3025
  if (!this.#deps.snapshotStore) return [];
2847
3026
  return this.#deps.snapshotStore.list();
2848
3027
  }
2849
3028
  /**
2850
3029
  * D3 恢复 interrupted run:
2851
- * 不存在 → RUN_SNAPSHOT_NOT_FOUND;schemaVersion 不匹配删除 + RUN_SNAPSHOT_INCOMPATIBLE;
3030
+ * 不存在 → RUN_SNAPSHOT_NOT_FOUND;schema 版本不受支持RUN_SNAPSHOT_SCHEMA_UNSUPPORTED(由 store 抛出,不删文件);
2852
3031
  * 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
2853
3032
  * @experimental
2854
3033
  */
@@ -2856,10 +3035,6 @@ var WebSkillRuntime = class {
2856
3035
  const store = this.#deps.snapshotStore;
2857
3036
  const snapshot = store ? await store.load(runId) : void 0;
2858
3037
  if (!snapshot) throw new WebSkillError("RUN_SNAPSHOT_NOT_FOUND", `No snapshot found for run "${runId}"`);
2859
- if (snapshot.schemaVersion !== 1) {
2860
- await store.delete(runId);
2861
- throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has incompatible schemaVersion ${String(snapshot.schemaVersion)}`);
2862
- }
2863
3038
  if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
2864
3039
  await store.delete(runId);
2865
3040
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -2912,7 +3087,9 @@ var WebSkillRuntime = class {
2912
3087
  skillProviders: this.#deps.skillProviders,
2913
3088
  catalogFilter: this.#deps.catalogFilter,
2914
3089
  snapshotStore: store,
2915
- skillStateGuard: this.#deps.skillStateGuard
3090
+ skillStateGuard: this.#deps.skillStateGuard,
3091
+ skillIntegrityGuard: this.#deps.skillIntegrityGuard,
3092
+ skillOutcomeReporter: this.#deps.skillOutcomeReporter
2916
3093
  }, this.#deps.config);
2917
3094
  this.#loops.set(runId, loop);
2918
3095
  try {
@@ -3031,6 +3208,7 @@ function sourceFromUrl(url) {
3031
3208
  /** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
3032
3209
  var HookRunner = class {
3033
3210
  #hooks = /* @__PURE__ */ new Map();
3211
+ /** 宿主可在装配后按 RuntimeConfig 调整(console Settings › Agent Runtime 即经此生效) */
3034
3212
  timeoutMs;
3035
3213
  failOnHookError;
3036
3214
  onWarning;
@@ -3046,6 +3224,15 @@ var HookRunner = class {
3046
3224
  this.#hooks.set(key, list);
3047
3225
  return this;
3048
3226
  }
3227
+ /**
3228
+ * 已注册钩子的**计数**,按注册相位分组(`'*'` 为全相位钩子)。
3229
+ * 刻意不返回函数引用:那会给 UI 一条调用宿主钩子的执行路径,而面板只需要「装没装上」。
3230
+ */
3231
+ listRegisteredHooks() {
3232
+ const counts = /* @__PURE__ */ new Map();
3233
+ for (const [phase, hooks] of this.#hooks) counts.set(phase, hooks.length);
3234
+ return counts;
3235
+ }
3049
3236
  async run(phase, ctx) {
3050
3237
  const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
3051
3238
  for (const hook of hooks) try {
@@ -3663,6 +3850,8 @@ function summarizeToolCalls(run) {
3663
3850
  return calls;
3664
3851
  }
3665
3852
  const SESSION_SCHEMA_VERSION = 1;
3853
+ /** `FsSessionStore` 的缺省页长。缺省值属于实现,不属于调用方——否则「下推」只推了一半 */
3854
+ const FS_SESSION_PAGE_SIZE = 50;
3666
3855
  const toMeta = (record) => ({
3667
3856
  id: record.id,
3668
3857
  createdAt: record.createdAt,
@@ -3673,6 +3862,25 @@ const toMeta = (record) => ({
3673
3862
  });
3674
3863
  const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3675
3864
  /**
3865
+ * 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
3866
+ * 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
3867
+ * 但它是不透明串,调用方不得自己构造(构造出来的越界值一律按 `VALIDATION_FAILED` 拒绝)。
3868
+ */
3869
+ function takeTailPage(all, options, what) {
3870
+ const limit = options.limit ?? 50;
3871
+ if (!Number.isInteger(limit) || limit <= 0) throw new WebSkillError("VALIDATION_FAILED", `Page limit must be a positive integer, received ${String(limit)}`);
3872
+ let end = all.length;
3873
+ if (options.cursor !== void 0) {
3874
+ end = Number(options.cursor);
3875
+ if (!Number.isInteger(end) || end < 0 || end > all.length) throw new WebSkillError("VALIDATION_FAILED", `Invalid ${what} cursor: ${JSON.stringify(options.cursor)}`);
3876
+ }
3877
+ const start = Math.max(0, end - limit);
3878
+ return {
3879
+ items: all.slice(start, end),
3880
+ ...start > 0 ? { nextCursor: String(start) } : {}
3881
+ };
3882
+ }
3883
+ /**
3676
3884
  * 解析会话文件。缺 `schemaVersion` 视为 0(0.0.1 时代文件,兼容读);
3677
3885
  * 高于当前版本拒绝读,避免新版写的字段被旧版静默丢弃。
3678
3886
  */
@@ -3746,7 +3954,7 @@ var FsSessionStore = class {
3746
3954
  return record;
3747
3955
  }
3748
3956
  async list(options = {}) {
3749
- if (!await this.#fs.exists(this.#root)) return [];
3957
+ if (!await this.#fs.exists(this.#root)) return { items: [] };
3750
3958
  const metas = [];
3751
3959
  for (const entry of await this.#fs.list(this.#root)) {
3752
3960
  if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
@@ -3760,7 +3968,15 @@ var FsSessionStore = class {
3760
3968
  if (record.archived === true && options.includeArchived !== true) continue;
3761
3969
  metas.push(toMeta(record));
3762
3970
  }
3763
- return metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3971
+ metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3972
+ return takeTailPage(metas, options, "session");
3973
+ }
3974
+ /**
3975
+ * 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
3976
+ * 但**跨出端口的记录数**已是常量——这正是分页对上层的意义。
3977
+ */
3978
+ async listMessages(id, options = {}) {
3979
+ return takeTailPage((await this.#require(id)).messages, options, "message");
3764
3980
  }
3765
3981
  async get(id) {
3766
3982
  const path = this.#path(id);
@@ -3820,4 +4036,4 @@ var FsSessionStore = class {
3820
4036
  };
3821
4037
 
3822
4038
  //#endregion
3823
- export { createWebSkillApi as A, normalizeToolContent as B, SESSION_SCHEMA_VERSION as C, bridgeError as D, WebSkillRuntime as E, isNetworkAllowed as F, summarizeToolCalls as G, parseBridgeRequest as H, mergeCatalogEntries as I, validateUiSurface as J, toLlmToolSpec as K, networkPolicyLibSource as L, extractUiSurfaceEvents as M, fromVercelResult as N, buildRenderResult as O, fromVercelStreamPart as P, networkUrlHost as R, RUN_TRACE_SCHEMA_VERSION as S, TraceRecorder as T, resolveToolName as U, normalizeToolError as V, schemaToForm as W, validateUiSurfaceEvent as Y, ProgressiveRouter as _, AnthropicClient as a, READ_SKILL_FILE_TOOL_NAME as b, FsArtifactStore as c, FsRunTraceStore as d, FsSessionStore as f, OpenAiCompatibleClient as g, HookRunner as h, AgentLoop as i, extractChartSpec as j, createScriptContext as k, FsMemoryStore as l, GoogleGenAiClient as m, ASK_USER_TOOL as n, CapabilityApproval as o, FullDisclosureRouter as p, toVercelToolSpecs as q, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsRunSnapshotStore as u, READ_SKILL_FILE_INPUT_SCHEMA as v, SerializingMemoryStore as w, RUN_SNAPSHOT_SCHEMA_VERSION as x, READ_SKILL_FILE_TOOL as y, normalizeErrorCode as z };
4039
+ export { createScriptContext as A, networkUrlHost as B, RUN_TRACE_SCHEMA_VERSION as C, WebSkillRuntime as D, TraceRecorder as E, fromVercelStreamPart as F, resolveToolName as G, normalizeToolContent as H, isNetworkAllowed as I, toLlmToolSpec as J, schemaToForm as K, isUnsupportedRunSnapshot as L, extractChartSpec as M, extractUiSpecEvents as N, bridgeError as O, fromVercelResult as P, mergeCatalogEntries as R, RUN_SNAPSHOT_SCHEMA_VERSION as S, SerializingMemoryStore as T, normalizeToolError as U, normalizeErrorCode as V, parseBridgeRequest as W, validateUiSpecEvent as X, toVercelToolSpecs as Y, validateUiSpecNode as Z, OpenAiCompatibleClient as _, AnthropicClient as a, READ_SKILL_FILE_TOOL as b, FS_SESSION_PAGE_SIZE as c, FsRunSnapshotStore as d, FsRunTraceStore as f, HookRunner as g, GoogleGenAiClient as h, AgentLoop as i, createWebSkillApi as j, buildRenderResult as k, FsArtifactStore as l, FullDisclosureRouter as m, ASK_USER_TOOL as n, CapabilityApproval as o, FsSessionStore as p, summarizeToolCalls as q, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsMemoryStore as u, ProgressiveRouter as v, SESSION_SCHEMA_VERSION as w, READ_SKILL_FILE_TOOL_NAME as x, READ_SKILL_FILE_INPUT_SCHEMA as y, networkPolicyLibSource as z };