@webskill/sdk 0.3.0 → 0.5.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 (35) hide show
  1. package/dist/agent.d.ts +2 -0
  2. package/dist/agent.js +867 -0
  3. package/dist/browser.d.ts +137 -4
  4. package/dist/browser.js +458 -22
  5. package/dist/{catalogComponents-C_V39rbF-BOHveMWa.js → catalogComponents-DV7cPpUm-C77AEEx9.js} +477 -157
  6. package/dist/{dist-rorEJsNi.js → dist-6C03DShK.js} +654 -298
  7. package/dist/{dist-ZKaM8j06.js → dist-bewtXYlO.js} +1061 -807
  8. package/dist/governance.d.ts +87 -10
  9. package/dist/governance.js +194 -24
  10. package/dist/{index-wiV5X8Rz.d.ts → index-Bsqg4ftU.d.ts} +151 -143
  11. package/dist/index-D_7ZZjkl.d.ts +411 -0
  12. package/dist/{index-8d-oEDww.d.ts → index-vBz_FC9w.d.ts} +289 -24
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +3 -2
  15. package/dist/mcp.d.ts +2 -2
  16. package/dist/mcp.js +1 -1
  17. package/dist/memoryArtifactStore-BtOeB_hm-tj3fC5ip.js +78 -0
  18. package/dist/node.d.ts +8 -4
  19. package/dist/node.js +1 -1
  20. package/dist/{openUiLibrary-B8-Cvou9-BbpNTXS3.js → openUiLibrary-W3Ce896k-ClFTRZFs.js} +6 -5
  21. package/dist/{skillVersionStore-DOEI9ptb-BxbYL70B.d.ts → skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts} +43 -11
  22. package/dist/{testing-CsrG3XLz.js → testing-DDCJWvgA.js} +7 -5
  23. package/dist/testing.d.ts +1 -1
  24. package/dist/testing.js +2 -2
  25. package/dist/{types-AmKCKJn_-VGabeXK4.d.ts → types-D_hoCri8-BnNPiZCi.d.ts} +111 -72
  26. package/dist/ui-react.d.ts +352 -20
  27. package/dist/ui-react.js +3804 -3478
  28. package/dist/ui-vue.d.ts +1 -1
  29. package/dist/ui-vue.js +25 -6
  30. package/dist/ui.d.ts +4 -3
  31. package/dist/ui.js +3 -3
  32. package/dist/{webskillLitCatalog-CNaUpasU-BslMcxRZ.js → webskillLitCatalog-_mugzRHx-DiuJpCuf.js} +398 -122
  33. package/package.json +6 -1
  34. package/dist/jsonRenderRegistry-9GrWP_hE-U6Do3Kid.js +0 -2468
  35. 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
  };
@@ -905,22 +991,22 @@ function createScriptContext(deps) {
905
991
  ...onWarning ? { onWarning } : {}
906
992
  };
907
993
  }
908
- const isRecord$2 = (v) => typeof v === "object" && v !== null;
994
+ const isRecord$3 = (v) => typeof v === "object" && v !== null;
909
995
  /**
910
996
  * $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
911
997
  * 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
912
998
  */
913
999
  function extractChartSpec(data) {
914
- if (!isRecord$2(data)) return void 0;
1000
+ if (!isRecord$3(data)) return void 0;
915
1001
  const raw = data["$chart"];
916
- if (!isRecord$2(raw)) return void 0;
1002
+ if (!isRecord$3(raw)) return void 0;
917
1003
  const { kind, labels, series } = raw;
918
1004
  if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
919
1005
  if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
920
1006
  if (!Array.isArray(series)) return void 0;
921
1007
  const validSeries = [];
922
1008
  for (const item of series) {
923
- if (!isRecord$2(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
1009
+ if (!isRecord$3(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
924
1010
  const name = item["name"];
925
1011
  validSeries.push({
926
1012
  ...typeof name === "string" ? { name } : {},
@@ -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,17 +1057,7 @@ 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
- function isRecord$1(value) {
1060
+ function isRecord$2(value) {
988
1061
  return typeof value === "object" && value !== null && !Array.isArray(value);
989
1062
  }
990
1063
  function reject(message) {
@@ -1001,120 +1074,66 @@ function isJsonValue(value, depth = 0) {
1001
1074
  if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1002
1075
  if (typeof value === "number") return Number.isFinite(value);
1003
1076
  if (Array.isArray(value)) return value.every((item) => isJsonValue(item, depth + 1));
1004
- if (!isRecord$1(value)) return false;
1077
+ if (!isRecord$2(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$2(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$2(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
- if (!isRecord$1(action)) reject("A surface action must be an object");
1100
+ if (!isRecord$2(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) {
1102
- if (!isRecord$1(value)) reject("A surface patch operation must be an object");
1116
+ if (!isRecord$2(value)) reject("A surface patch operation must be an object");
1103
1117
  if (value["op"] !== "replace" && value["op"] !== "merge" && value["op"] !== "append") reject("Surface patch operation is invalid");
1104
1118
  requireString(value["path"], "Surface patch path");
1105
1119
  if (!value["path"].startsWith("/")) reject("Surface patch path must be a JSON pointer");
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) {
1110
- if (!isRecord$1(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
1123
+ function validateUiSpecEvent(value) {
1124
+ if (!isRecord$2(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,17 +1178,75 @@ function validateUiSurfaceEvent(value) {
1159
1178
  }
1160
1179
  }
1161
1180
  /** Extracts validated surface stream events from structured tool output. @experimental */
1162
- function extractUiSurfaceEvents(data) {
1163
- if (!isRecord$1(data) || data["$surface"] === void 0) return [];
1181
+ function extractUiSpecEvents(data) {
1182
+ if (!isRecord$2(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));
1185
+ }
1186
+ /** 跨会话表单填写值在 `user:{userId}` scope 下的 key(FR-5.7) @experimental */
1187
+ const FORM_VALUES_KEY = "formValues";
1188
+ /**
1189
+ * 跨会话稳定的字段标识(FR-5.6)。必须带技能名:
1190
+ * 只有字段名时,两个技能各自的 `email` 会互相串号。
1191
+ * @experimental
1192
+ */
1193
+ function formFieldKey(skillName, fieldName) {
1194
+ return `${skillName}#${fieldName}`;
1195
+ }
1196
+ /** memory 里的原始值形状不受控(宿主可能手改文件),逐条过滤而不是整体信任 @experimental */
1197
+ function readFormValues(raw) {
1198
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
1199
+ const out = {};
1200
+ for (const [key, entry] of Object.entries(raw)) {
1201
+ if (typeof entry !== "object" || entry === null) continue;
1202
+ const record = entry;
1203
+ if (typeof record.ts !== "number" || !("value" in record)) continue;
1204
+ out[key] = {
1205
+ value: record.value,
1206
+ ts: record.ts
1207
+ };
1208
+ }
1209
+ return out;
1210
+ }
1211
+ /** 合并本次提交并按上限裁剪最旧(FR-5.12) @experimental */
1212
+ function putFormValues(current, updates, limit) {
1213
+ const merged = {
1214
+ ...current,
1215
+ ...updates
1216
+ };
1217
+ const keys = Object.keys(merged);
1218
+ if (limit <= 0) return {};
1219
+ if (keys.length <= limit) return merged;
1220
+ const kept = keys.sort((a, b) => merged[a].ts - merged[b].ts).slice(keys.length - limit);
1221
+ return Object.fromEntries(kept.map((key) => [key, merged[key]]));
1222
+ }
1223
+ /** 清除单个字段;不传 fieldKey 即全部清除(FR-5.11) @experimental */
1224
+ function clearFormValues(current, fieldKey) {
1225
+ if (fieldKey === void 0) return {};
1226
+ const { [fieldKey]: _removed, ...rest } = current;
1227
+ return rest;
1228
+ }
1229
+ /**
1230
+ * 宿主侧的清除入口(FR-5.11):设置面板的「清除填写历史」直接调它,
1231
+ * 不必自己知道 scope 与 key 的约定。
1232
+ * @experimental
1233
+ */
1234
+ async function clearStoredFormValues(memory, userId, fieldKey) {
1235
+ const scope = `user:${userId}`;
1236
+ if (fieldKey === void 0) {
1237
+ await memory.delete(scope, FORM_VALUES_KEY);
1238
+ return;
1239
+ }
1240
+ const next = clearFormValues(readFormValues(await memory.get(scope, FORM_VALUES_KEY)), fieldKey);
1241
+ await memory.set(scope, FORM_VALUES_KEY, next);
1166
1242
  }
1167
1243
  /**
1168
1244
  * JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
1169
1245
  * providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
1170
1246
  * type 映射:string→text、number/integer→number、boolean→boolean、enum→select、其余→textarea。
1247
+ * 传入 skillName 时给每个字段带上跨会话稳定的 `fieldKey`(FR-5.6)。
1171
1248
  */
1172
- function schemaToForm(schema, providedArgs) {
1249
+ function schemaToForm(schema, providedArgs, options) {
1173
1250
  const required = new Set(Array.isArray(schema.required) ? schema.required : []);
1174
1251
  const fields = [];
1175
1252
  for (const [name, prop] of Object.entries(schema.properties ?? {})) {
@@ -1180,6 +1257,7 @@ function schemaToForm(schema, providedArgs) {
1180
1257
  type: mapFieldType(prop),
1181
1258
  ...required.has(name) ? { required: true } : {},
1182
1259
  ...typeof prop.description === "string" ? { description: prop.description } : {},
1260
+ ...options?.skillName !== void 0 ? { fieldKey: formFieldKey(options.skillName, name) } : {},
1183
1261
  ...provided !== void 0 ? { defaultValue: provided } : prop.default !== void 0 ? { defaultValue: prop.default } : {}
1184
1262
  };
1185
1263
  if (Array.isArray(prop.enum)) field.options = prop.enum.map((v) => ({
@@ -1309,6 +1387,133 @@ var TraceRecorder = class {
1309
1387
  return [...this.#events];
1310
1388
  }
1311
1389
  };
1390
+ const isRecord$1 = (v) => typeof v === "object" && v !== null;
1391
+ /** 工具结果可经 `$todo` 标记记入 trace 的事件类型;其余类型不接受,防止工具源伪造 run.* */
1392
+ const TODO_TRACE_TYPES = /* @__PURE__ */ new Set([
1393
+ "todo.created",
1394
+ "todo.updated",
1395
+ "todo.cleared"
1396
+ ]);
1397
+ /**
1398
+ * `$todo` 约定的形状校验:JSON content 的 data 含 `$todo` 键(单条或数组)→ trace 事件。
1399
+ *
1400
+ * 与 `$chart` / `$surface` 同一条既有通道——待办清单的状态机全部在 `@webskill/agent`,
1401
+ * runtime 只认这三个事件类型名,不含任何计划态逻辑。畸形条目忽略不炸。
1402
+ * @experimental
1403
+ */
1404
+ function extractTodoTraceEvents(data) {
1405
+ if (!isRecord$1(data) || data["$todo"] === void 0) return [];
1406
+ const raw = data["$todo"];
1407
+ const entries = Array.isArray(raw) ? raw : [raw];
1408
+ const events = [];
1409
+ for (const entry of entries) {
1410
+ if (!isRecord$1(entry)) continue;
1411
+ const { type, ...rest } = entry;
1412
+ if (typeof type !== "string" || !TODO_TRACE_TYPES.has(type)) continue;
1413
+ events.push({
1414
+ type,
1415
+ data: rest
1416
+ });
1417
+ }
1418
+ return events;
1419
+ }
1420
+ const RUN_SNAPSHOT_SCHEMA_VERSION = 2;
1421
+ /** @experimental */
1422
+ function isUnsupportedRunSnapshot(entry) {
1423
+ return entry.unsupported === true;
1424
+ }
1425
+ const SNAPSHOT_SUFFIX = ".snapshot.json";
1426
+ /**
1427
+ * FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
1428
+ * 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
1429
+ * save/list 时顺带清理已过 interactionExpiresAt 的过期快照。
1430
+ *
1431
+ * 数据敏感性说明:快照含完整对话历史(用户输入、工具结果、可能的凭据片段),
1432
+ * 以明文 JSON 落盘于宿主提供的 fs;宿主应将其视为会话数据同等保护。
1433
+ * @experimental
1434
+ */
1435
+ var FsRunSnapshotStore = class {
1436
+ #root;
1437
+ #fs;
1438
+ constructor(deps) {
1439
+ this.#root = deps.root.replace(/\/+$/, "");
1440
+ this.#fs = deps.fs;
1441
+ }
1442
+ #path(runId) {
1443
+ return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
1444
+ }
1445
+ async save(snapshot) {
1446
+ await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
1447
+ await this.#pruneExpired();
1448
+ }
1449
+ async load(runId) {
1450
+ const path = this.#path(runId);
1451
+ if (!await this.#fs.exists(path)) return void 0;
1452
+ let parsed;
1453
+ try {
1454
+ parsed = JSON.parse(await this.#fs.readText(path));
1455
+ } catch (e) {
1456
+ await this.#fs.remove(path).catch(() => void 0);
1457
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
1458
+ }
1459
+ const snapshot = parsed;
1460
+ if (typeof snapshot !== "object" || snapshot === null) {
1461
+ await this.#fs.remove(path).catch(() => void 0);
1462
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
1463
+ }
1464
+ const schemaVersion = typeof snapshot.schemaVersion === "number" ? snapshot.schemaVersion : 0;
1465
+ 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.`);
1466
+ if (snapshot.runId !== runId) {
1467
+ await this.#fs.remove(path).catch(() => void 0);
1468
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
1469
+ }
1470
+ return snapshot;
1471
+ }
1472
+ async delete(runId) {
1473
+ const path = this.#path(runId);
1474
+ if (await this.#fs.exists(path)) await this.#fs.remove(path);
1475
+ }
1476
+ async list() {
1477
+ await this.#pruneExpired();
1478
+ if (!await this.#fs.exists(this.#root)) return [];
1479
+ const out = [];
1480
+ for (const entry of await this.#fs.list(this.#root)) {
1481
+ if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
1482
+ try {
1483
+ const parsed = JSON.parse(await this.#fs.readText(entry.path));
1484
+ const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 0;
1485
+ if (schemaVersion === 2) {
1486
+ out.push(parsed);
1487
+ continue;
1488
+ }
1489
+ out.push({
1490
+ unsupported: true,
1491
+ schemaVersion,
1492
+ runId: String(parsed.runId ?? entry.path),
1493
+ snapshotAt: typeof parsed.snapshotAt === "string" ? parsed.snapshotAt : "",
1494
+ ...typeof parsed.sessionId === "string" ? { sessionId: parsed.sessionId } : {},
1495
+ ...typeof parsed.userPrompt === "string" ? { userPrompt: parsed.userPrompt } : {},
1496
+ ...typeof parsed.interactionExpiresAt === "string" ? { interactionExpiresAt: parsed.interactionExpiresAt } : {}
1497
+ });
1498
+ } catch {}
1499
+ }
1500
+ return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
1501
+ }
1502
+ /** 过期快照清理(save/list 时顺带;失败静默不阻断主流程) */
1503
+ async #pruneExpired() {
1504
+ try {
1505
+ if (!await this.#fs.exists(this.#root)) return;
1506
+ const now = Date.now();
1507
+ for (const entry of await this.#fs.list(this.#root)) {
1508
+ if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
1509
+ try {
1510
+ const parsed = JSON.parse(await this.#fs.readText(entry.path));
1511
+ if (parsed.interactionExpiresAt !== void 0 && Date.parse(parsed.interactionExpiresAt) < now) await this.#fs.remove(entry.path);
1512
+ } catch {}
1513
+ }
1514
+ } catch {}
1515
+ }
1516
+ };
1312
1517
  /**
1313
1518
  * LLM 可见名 → 作者在 SKILL.md 里写的形态。
1314
1519
  * `<已激活技能>__<脚本>` 保持原样(裸标识符按技能作用域单独匹配),
@@ -1338,7 +1543,7 @@ function denialReason(state, canonical) {
1338
1543
  const skills = [...state.skillAllowedTools.keys()].sort().map((s) => `"${s}"`);
1339
1544
  const subject = skills.length === 1 ? `Skill ${skills[0]} declares` : `Skills ${skills.join(", ")} declare`;
1340
1545
  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.`;
1546
+ 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
1547
  }
1343
1548
  /**
1344
1549
  * 多技能语义:无人声明 → 不受限;有人声明 → 命中任一清单,
@@ -1379,7 +1584,6 @@ const summarizeArgs = (args) => {
1379
1584
  const json = JSON.stringify(args);
1380
1585
  return json.length > 100 ? `${json.slice(0, 100)}…` : json;
1381
1586
  };
1382
- const surfaceActions = (surface) => "actions" in surface ? surface.actions ?? [] : [];
1383
1587
  /**
1384
1588
  * 多轮 Agent 循环。
1385
1589
  * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
@@ -1404,6 +1608,7 @@ var AgentLoop = class {
1404
1608
  toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
1405
1609
  toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
1406
1610
  paramHistoryLimit: config.paramHistoryLimit ?? 50,
1611
+ formValueLimit: config.formValueLimit ?? 100,
1407
1612
  temperature: config.temperature,
1408
1613
  renderResult: config.renderResult
1409
1614
  };
@@ -1450,6 +1655,7 @@ var AgentLoop = class {
1450
1655
  activatedTools: /* @__PURE__ */ new Map(),
1451
1656
  skillAllowedTools: /* @__PURE__ */ new Map(),
1452
1657
  warnedDeniedTools: /* @__PURE__ */ new Set(),
1658
+ integrityVerdicts: /* @__PURE__ */ new Map(),
1453
1659
  toolTimeoutMs: this.#config.toolTimeoutMs,
1454
1660
  now,
1455
1661
  interactionSeq: 0,
@@ -1478,7 +1684,13 @@ var AgentLoop = class {
1478
1684
  strategy: route.strategy,
1479
1685
  skillCount: route.catalog.entries.length
1480
1686
  } });
1481
- await this.#lifecycle("route", state, { strategy: route.strategy });
1687
+ await this.#lifecycle({
1688
+ phase: "route",
1689
+ data: {
1690
+ strategy: route.strategy,
1691
+ candidates: route.catalog.entries.map((e) => e.name)
1692
+ }
1693
+ }, state);
1482
1694
  const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
1483
1695
  try {
1484
1696
  return await source.listToolSpecs();
@@ -1487,15 +1699,25 @@ var AgentLoop = class {
1487
1699
  return [];
1488
1700
  }
1489
1701
  }))).flat();
1702
+ const externalSystemPrompts = [];
1703
+ for (const source of this.#deps.externalTools ?? []) {
1704
+ if (source.systemPrompt === void 0) continue;
1705
+ try {
1706
+ const text = (await source.systemPrompt())?.trim();
1707
+ if (text !== void 0 && text !== "") externalSystemPrompts.push(text);
1708
+ } catch (e) {
1709
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
1710
+ }
1711
+ }
1490
1712
  state.messages = [
1491
1713
  {
1492
1714
  role: "system",
1493
- content: route.systemPrompt
1715
+ content: textParts([route.systemPrompt, ...externalSystemPrompts].join("\n\n"))
1494
1716
  },
1495
1717
  ...(input.history ?? []).map((m) => ({ ...m })),
1496
1718
  {
1497
1719
  role: "user",
1498
- content: input.userPrompt
1720
+ content: textParts(input.userPrompt)
1499
1721
  }
1500
1722
  ];
1501
1723
  try {
@@ -1527,17 +1749,25 @@ var AgentLoop = class {
1527
1749
  }
1528
1750
  /**
1529
1751
  * D10 判定的唯一出口(暴露点 + 分发点共用)。
1530
- * 0.3.0 只记警告并返回 false,调用点照常放行;0.4.0 收紧时改的是调用点对返回值的处理。
1752
+ * 0.4.0(D1)起两个调用点都看返回值:暴露点过滤、分发点回喂 TOOL_NOT_ALLOWED。
1753
+ * 判定语义与 0.3.0 一致,改的只是调用点与 trace 事件类型(warning → denied)。
1531
1754
  */
1532
1755
  #checkToolAccess(state, toolName) {
1533
1756
  const verdict = evaluateToolAccess(state, toolName);
1534
1757
  if (verdict.allowed) return true;
1535
1758
  if (!state.warnedDeniedTools.has(toolName)) {
1536
1759
  state.warnedDeniedTools.add(toolName);
1537
- state.trace.record("run.warning", { message: verdict.reason ?? `Tool "${toolName}" is not allowed` });
1760
+ state.trace.record("tool.denied", {
1761
+ message: verdict.reason ?? `Tool "${toolName}" is not allowed`,
1762
+ data: { name: toolName }
1763
+ });
1538
1764
  }
1539
1765
  return false;
1540
1766
  }
1767
+ /** 分发点被拒时回喂给模型的结构化错误(不抛异常:模型造名字是常态,抛异常会终止整个 run) */
1768
+ #deniedToolError(state, toolName) {
1769
+ return toolError("TOOL_NOT_ALLOWED", evaluateToolAccess(state, toolName).reason ?? `Tool "${toolName}" is not allowed`);
1770
+ }
1541
1771
  /** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
1542
1772
  async #turnLoop(state, startTurn, externalSpecs) {
1543
1773
  const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
@@ -1549,8 +1779,7 @@ var AgentLoop = class {
1549
1779
  state.turn = turn;
1550
1780
  if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1551
1781
  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);
1782
+ const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
1554
1783
  const toolSpecs = [
1555
1784
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
1556
1785
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
@@ -1584,22 +1813,29 @@ var AgentLoop = class {
1584
1813
  const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
1585
1814
  return finish("failed", "llm-error", messageOf(e), code);
1586
1815
  }
1816
+ const responseText = partsToText(response.content);
1587
1817
  trace.record("llm.response", { data: {
1588
1818
  turn,
1589
1819
  hasToolCalls: Boolean(response.toolCalls?.length),
1590
- contentLength: response.content?.length ?? 0
1820
+ contentLength: responseText.length
1591
1821
  } });
1592
1822
  if (!response.toolCalls?.length) {
1593
1823
  messages.push({
1594
1824
  role: "assistant",
1595
- content: response.content ?? ""
1825
+ content: response.content ?? []
1596
1826
  });
1597
- return finish("completed", "final-answer", response.content ?? "");
1827
+ return finish("completed", "final-answer", responseText);
1598
1828
  }
1599
- await this.#lifecycle("execute", state, { turn });
1829
+ await this.#lifecycle({
1830
+ phase: "execute",
1831
+ data: {
1832
+ kind: "turn",
1833
+ turn
1834
+ }
1835
+ }, state);
1600
1836
  messages.push({
1601
1837
  role: "assistant",
1602
- content: response.content ?? "",
1838
+ content: response.content ?? [],
1603
1839
  toolCalls: response.toolCalls
1604
1840
  });
1605
1841
  for (const call of response.toolCalls) {
@@ -1613,7 +1849,7 @@ var AgentLoop = class {
1613
1849
  messages.push({
1614
1850
  role: "tool",
1615
1851
  toolCallId: call.id,
1616
- content: await this.#serializeToolResult(call, result, state)
1852
+ content: textParts(await this.#serializeToolResult(call, result, state))
1617
1853
  });
1618
1854
  await this.#drainSurfaceAction(state);
1619
1855
  }
@@ -1659,7 +1895,10 @@ var AgentLoop = class {
1659
1895
  trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
1660
1896
  }
1661
1897
  try {
1662
- await this.#lifecycle(status === "completed" ? "complete" : "fail", state, { reason });
1898
+ await this.#lifecycle({
1899
+ phase: status === "completed" ? "complete" : "fail",
1900
+ data: { reason }
1901
+ }, state);
1663
1902
  } catch (e) {
1664
1903
  trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
1665
1904
  }
@@ -1675,7 +1914,7 @@ var AgentLoop = class {
1675
1914
  const store = this.#deps.snapshotStore;
1676
1915
  if (!store) return;
1677
1916
  const snapshot = {
1678
- schemaVersion: 1,
1917
+ schemaVersion: 2,
1679
1918
  runId: state.runId,
1680
1919
  sessionId: state.run.sessionId,
1681
1920
  userPrompt: state.run.userPrompt,
@@ -1736,6 +1975,7 @@ var AgentLoop = class {
1736
1975
  activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
1737
1976
  skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
1738
1977
  warnedDeniedTools: /* @__PURE__ */ new Set(),
1978
+ integrityVerdicts: /* @__PURE__ */ new Map(),
1739
1979
  toolTimeoutMs: snapshot.config.toolTimeoutMs,
1740
1980
  now,
1741
1981
  interactionSeq: snapshot.interactionSeq ?? 0,
@@ -1784,7 +2024,7 @@ var AgentLoop = class {
1784
2024
  state.messages.push({
1785
2025
  role: "tool",
1786
2026
  toolCallId: pendingCall.id,
1787
- content: await this.#serializeToolResult(pendingCall, result, state)
2027
+ content: textParts(await this.#serializeToolResult(pendingCall, result, state))
1788
2028
  });
1789
2029
  await this.#drainSurfaceAction(state);
1790
2030
  } else if (pending?.type === "ask" && pendingCall) {
@@ -1806,7 +2046,7 @@ var AgentLoop = class {
1806
2046
  state.messages.push({
1807
2047
  role: "tool",
1808
2048
  toolCallId: pendingCall.id,
1809
- content: await this.#serializeToolResult(pendingCall, result, state)
2049
+ content: textParts(await this.#serializeToolResult(pendingCall, result, state))
1810
2050
  });
1811
2051
  await this.#drainSurfaceAction(state);
1812
2052
  } else if (pendingCall) {
@@ -1814,7 +2054,7 @@ var AgentLoop = class {
1814
2054
  state.messages.push({
1815
2055
  role: "tool",
1816
2056
  toolCallId: pendingCall.id,
1817
- content: await this.#serializeToolResult(pendingCall, result, state)
2057
+ content: textParts(await this.#serializeToolResult(pendingCall, result, state))
1818
2058
  });
1819
2059
  await this.#drainSurfaceAction(state);
1820
2060
  }
@@ -1825,7 +2065,7 @@ var AgentLoop = class {
1825
2065
  state.messages.push({
1826
2066
  role: "tool",
1827
2067
  toolCallId: next.id,
1828
- content: await this.#serializeToolResult(next, result, state)
2068
+ content: textParts(await this.#serializeToolResult(next, result, state))
1829
2069
  });
1830
2070
  await this.#drainSurfaceAction(state);
1831
2071
  }
@@ -1868,7 +2108,7 @@ var AgentLoop = class {
1868
2108
  sessionId: state.run.sessionId,
1869
2109
  ts: state.now(),
1870
2110
  data: {
1871
- type: "llm.delta",
2111
+ kind: "llm-delta",
1872
2112
  delta
1873
2113
  }
1874
2114
  });
@@ -1882,23 +2122,23 @@ var AgentLoop = class {
1882
2122
  emitDelta(event.delta);
1883
2123
  } else if (event.type === "tool-calls") toolCalls.push(...event.toolCalls);
1884
2124
  else if (event.type === "done") doneContent = event.content;
2125
+ const text = doneContent ?? content;
1885
2126
  return {
1886
- content: doneContent ?? (content === "" ? void 0 : content),
2127
+ content: text === "" ? void 0 : textParts(text),
1887
2128
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0
1888
2129
  };
1889
2130
  }
1890
2131
  /** 生命周期接线:更新 phase、发事件、跑钩子 */
1891
- async #lifecycle(phase, state, data) {
1892
- state.run.phase = phase;
2132
+ async #lifecycle(init, state) {
2133
+ state.run.phase = init.phase;
1893
2134
  const event = {
1894
- phase,
2135
+ ...init,
1895
2136
  runId: state.runId,
1896
2137
  sessionId: state.run.sessionId,
1897
- ts: state.now(),
1898
- ...data ? { data } : {}
2138
+ ts: state.now()
1899
2139
  };
1900
2140
  this.#deps.eventBus?.emit(event);
1901
- if (this.#deps.hooks) await this.#deps.hooks.run(phase, {
2141
+ if (this.#deps.hooks) await this.#deps.hooks.run(init.phase, {
1902
2142
  event,
1903
2143
  run: state.run
1904
2144
  });
@@ -1925,10 +2165,14 @@ var AgentLoop = class {
1925
2165
  run.status = "interrupted";
1926
2166
  run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
1927
2167
  await this.#saveSnapshot(state, { interaction: request });
1928
- await this.#lifecycle("interact", state, {
1929
- interactionId: request.id,
1930
- type: request.type
1931
- });
2168
+ await this.#lifecycle({
2169
+ phase: "interact",
2170
+ data: {
2171
+ kind: "interaction",
2172
+ interactionId: request.id,
2173
+ interactionType: request.type
2174
+ }
2175
+ }, state);
1932
2176
  state.trace.record("ui.requested", { data: {
1933
2177
  interactionId: request.id,
1934
2178
  type: request.type,
@@ -1961,7 +2205,13 @@ var AgentLoop = class {
1961
2205
  interactionId: request.id,
1962
2206
  type: request.type
1963
2207
  } });
1964
- await this.#lifecycle("execute", state, { resumed: request.id });
2208
+ await this.#lifecycle({
2209
+ phase: "execute",
2210
+ data: {
2211
+ kind: "interaction-resumed",
2212
+ interactionId: request.id
2213
+ }
2214
+ }, state);
1965
2215
  await this.#appendParamHistory(state, request, response.value);
1966
2216
  return response.value;
1967
2217
  }
@@ -1995,8 +2245,8 @@ var AgentLoop = class {
1995
2245
  if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
1996
2246
  else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
1997
2247
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
2248
+ else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
1998
2249
  else {
1999
- this.#checkToolAccess(state, call.name);
2000
2250
  const resolution = resolveToolName(call.name, state.activated);
2001
2251
  if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
2002
2252
  else {
@@ -2020,8 +2270,9 @@ var AgentLoop = class {
2020
2270
  type: "chart",
2021
2271
  chart
2022
2272
  });
2273
+ for (const todo of extractTodoTraceEvents(item.data)) state.trace.record(todo.type, { data: todo.data });
2023
2274
  try {
2024
- for (const event of extractUiSurfaceEvents(item.data)) await this.#renderSurface(state, event);
2275
+ for (const event of extractUiSpecEvents(item.data)) await this.#renderSurface(state, event);
2025
2276
  } catch (e) {
2026
2277
  state.trace.record("run.warning", {
2027
2278
  message: `UI surface rejected: ${messageOf(e)}`,
@@ -2040,7 +2291,7 @@ var AgentLoop = class {
2040
2291
  durationMs
2041
2292
  }
2042
2293
  });
2043
- this.#emitTool(state, "failed", call);
2294
+ this.#emitTool(state, "failed", call, result.error?.code);
2044
2295
  }
2045
2296
  for (const artifact of result.artifacts ?? []) state.trace.record("artifact.created", { data: {
2046
2297
  artifactId: artifact.id,
@@ -2060,14 +2311,14 @@ var AgentLoop = class {
2060
2311
  await bridge.renderSurface(attributed);
2061
2312
  state.surfaceEvents.push(structuredClone(attributed));
2062
2313
  if (attributed.type === "open") {
2063
- const waiting = surfaceActions(attributed.surface).filter((action) => action.awaitResponse);
2314
+ const waiting = (attributed.actions ?? []).filter((action) => action.awaitResponse);
2064
2315
  if (waiting.length > 1) throw new WebSkillError("VALIDATION_FAILED", "A UI surface can wait for only one action");
2065
2316
  const action = waiting[0];
2066
2317
  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
2318
  else if (state.pendingSurfaceAction) throw new WebSkillError("VALIDATION_FAILED", "Only one UI surface action can be pending at a time");
2068
2319
  else state.pendingSurfaceAction = {
2069
2320
  runId: state.runId,
2070
- surfaceId: attributed.surface.id,
2321
+ surfaceId: attributed.id,
2071
2322
  actionId: action.id,
2072
2323
  intent: action.intent,
2073
2324
  nonce: action.nonce
@@ -2085,21 +2336,17 @@ var AgentLoop = class {
2085
2336
  }
2086
2337
  /** Assigns unforgeable action nonces after model output has passed structural validation. */
2087
2338
  #attributeSurfaceEvent(state, event) {
2088
- const actions = event.type === "open" ? surfaceActions(event.surface) : [];
2089
- if (event.type !== "open" || actions.length === 0) return {
2339
+ if (event.type !== "open" || (event.actions ?? []).length === 0) return {
2090
2340
  ...event,
2091
2341
  runId: state.runId
2092
2342
  };
2093
2343
  return {
2094
- type: "open",
2344
+ ...event,
2095
2345
  runId: state.runId,
2096
- surface: {
2097
- ...event.surface,
2098
- actions: actions.map((action) => ({
2099
- ...action,
2100
- nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
2101
- }))
2102
- }
2346
+ actions: (event.actions ?? []).map((action) => ({
2347
+ ...action,
2348
+ nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
2349
+ }))
2103
2350
  };
2104
2351
  }
2105
2352
  /** Awaits the single action emitted with the most recently persisted tool result. */
@@ -2126,13 +2373,13 @@ var AgentLoop = class {
2126
2373
  state.processedSurfaceActionNonces.add(response.nonce);
2127
2374
  state.messages.push({
2128
2375
  role: "user",
2129
- content: JSON.stringify({
2376
+ content: textParts(JSON.stringify({
2130
2377
  type: "webskill_surface_action",
2131
2378
  surfaceId: response.surfaceId,
2132
2379
  actionId: response.actionId,
2133
2380
  intent: response.intent,
2134
2381
  value: response.value ?? null
2135
- })
2382
+ }))
2136
2383
  });
2137
2384
  }
2138
2385
  async #interactSurfaceAction(state, request, resumed) {
@@ -2144,20 +2391,26 @@ var AgentLoop = class {
2144
2391
  const { run } = state;
2145
2392
  run.status = "interrupted";
2146
2393
  run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
2394
+ const pendingResponse = bridge.requestSurfaceAction(request);
2395
+ pendingResponse.catch(() => void 0);
2147
2396
  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
- });
2397
+ await this.#lifecycle({
2398
+ phase: "interact",
2399
+ data: {
2400
+ kind: "surface-action",
2401
+ surfaceId: request.surfaceId,
2402
+ actionId: request.actionId,
2403
+ nonce: request.nonce,
2404
+ resumed
2405
+ }
2406
+ }, state);
2154
2407
  state.trace.record("ui.surface-action.requested", { data: {
2155
2408
  surfaceId: request.surfaceId,
2156
2409
  actionId: request.actionId,
2157
2410
  nonce: request.nonce,
2158
2411
  ...resumed ? { resumed: true } : {}
2159
2412
  } });
2160
- const response = await this.#withInteractionTimeout(bridge.requestSurfaceAction(request), this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
2413
+ const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
2161
2414
  run.status = "running";
2162
2415
  run.interruptExpiresAt = void 0;
2163
2416
  state.trace.record("ui.surface-action.resolved", { data: {
@@ -2165,9 +2418,17 @@ var AgentLoop = class {
2165
2418
  actionId: request.actionId,
2166
2419
  nonce: request.nonce
2167
2420
  } });
2168
- await this.#lifecycle("execute", state, { surfaceAction: request.actionId });
2421
+ await this.#lifecycle({
2422
+ phase: "execute",
2423
+ data: {
2424
+ kind: "surface-action-resumed",
2425
+ surfaceId: request.surfaceId,
2426
+ actionId: request.actionId
2427
+ }
2428
+ }, state);
2169
2429
  return response;
2170
2430
  } catch (e) {
2431
+ bridge.cancelSurfaceAction?.(request.nonce);
2171
2432
  state.run.status = "running";
2172
2433
  state.run.interruptExpiresAt = void 0;
2173
2434
  if (e instanceof WebSkillError && e.code === "RUN_INTERACTION_TIMEOUT") throw new RunTerminated({
@@ -2205,7 +2466,7 @@ var AgentLoop = class {
2205
2466
  * 集合挂在 LoopState 上、**不写进快照**:写进快照会让跨进程恢复的
2206
2467
  * 消费者永远收不到它本来就没见过的事件。
2207
2468
  */
2208
- #emitTool(state, status, call) {
2469
+ #emitTool(state, status, call, errorCode) {
2209
2470
  const key = `${call.id}:${status}`;
2210
2471
  if (state.emittedToolEvents.has(key)) return;
2211
2472
  state.emittedToolEvents.add(key);
@@ -2215,11 +2476,12 @@ var AgentLoop = class {
2215
2476
  sessionId: state.run.sessionId,
2216
2477
  ts: state.now(),
2217
2478
  data: {
2218
- type: "tool",
2479
+ kind: "tool",
2219
2480
  status,
2220
2481
  name: call.name,
2221
2482
  callId: call.id,
2222
- args: summarizeArgs(call.arguments)
2483
+ args: summarizeArgs(call.arguments),
2484
+ ...errorCode !== void 0 ? { errorCode } : {}
2223
2485
  }
2224
2486
  });
2225
2487
  }
@@ -2285,7 +2547,13 @@ var AgentLoop = class {
2285
2547
  skillName: name,
2286
2548
  source: "external"
2287
2549
  } });
2288
- await this.#lifecycle("activate", state, { skillName: name });
2550
+ await this.#lifecycle({
2551
+ phase: "activate",
2552
+ data: {
2553
+ skillName: name,
2554
+ source: "external"
2555
+ }
2556
+ }, state);
2289
2557
  await this.#writeActivationMemory(name, state);
2290
2558
  }
2291
2559
  return {
@@ -2339,18 +2607,62 @@ var AgentLoop = class {
2339
2607
  if (!check) return false;
2340
2608
  return await check(skillName) === false;
2341
2609
  }
2610
+ /**
2611
+ * D3 激活期完整性校验。结论按技能名在 run 内缓存,**失败也缓存**:
2612
+ * 校验失败的技能不会进 `activated` 集合,不缓存的话同一个坏技能每尝试激活一次
2613
+ * 就要把它的文件全扫一遍——成本随文件数线性放大,正是需求 §3 验收 3 要防的。
2614
+ *
2615
+ * 设计 §3.5 写的缓存键是 `(skillName, manifest.integrity.digest)`,但 digest 只有
2616
+ * **调用之后**才知道;runtime 读不到 manifest,两元组键在这一层无法实现。
2617
+ * 实际键是技能名,digest 作为结论的一部分留在 trace 里供事后对账。
2618
+ *
2619
+ * 失败不抛错:抛错会终止整个 run,而「某个技能被改过」不该让其余技能一起停摆。
2620
+ */
2621
+ async #integrityOk(skillName, state) {
2622
+ const guard = this.#deps.skillIntegrityGuard;
2623
+ if (!guard?.verifyOnActivate) return true;
2624
+ let verdict = state.integrityVerdicts.get(skillName);
2625
+ if (verdict === void 0) {
2626
+ try {
2627
+ verdict = await guard.verifyOnActivate(skillName);
2628
+ } catch (e) {
2629
+ verdict = {
2630
+ ok: false,
2631
+ reason: `the integrity guard threw: ${messageOf(e)}`
2632
+ };
2633
+ }
2634
+ state.integrityVerdicts.set(skillName, verdict);
2635
+ }
2636
+ if (verdict.ok) return true;
2637
+ state.trace.record("skill.integrity-failed", {
2638
+ message: `Skill "${skillName}" failed integrity verification on activation: ${verdict.reason ?? "no reason given"}`,
2639
+ data: {
2640
+ skillName,
2641
+ ...verdict.digest !== void 0 ? { digest: verdict.digest } : {}
2642
+ }
2643
+ });
2644
+ return false;
2645
+ }
2342
2646
  /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
2343
2647
  async #activateSkill(skillName, state, via, skillMdText) {
2344
2648
  if (await this.#guardDenied("canActivate", skillName)) {
2345
2649
  state.trace.record("run.warning", { message: `Skill "${skillName}" is blocked by the skill state guard (activate)` });
2346
2650
  return "";
2347
2651
  }
2652
+ if (!await this.#integrityOk(skillName, state)) return "";
2348
2653
  state.activated.add(skillName);
2349
2654
  state.trace.record("skill.activated", { data: {
2350
2655
  skillName,
2351
2656
  ...via ? { via } : {}
2352
2657
  } });
2353
- await this.#lifecycle("activate", state, { skillName });
2658
+ await this.#lifecycle({
2659
+ phase: "activate",
2660
+ data: {
2661
+ skillName,
2662
+ source: "local",
2663
+ ...via ? { via } : {}
2664
+ }
2665
+ }, state);
2354
2666
  await this.#writeActivationMemory(skillName, state);
2355
2667
  const root = this.#deps.skillIndex.get(skillName);
2356
2668
  if (!root) return "";
@@ -2441,11 +2753,13 @@ var AgentLoop = class {
2441
2753
  let args = call.arguments;
2442
2754
  const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
2443
2755
  if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
2756
+ const fields = schemaToForm(def.inputSchema, args, { skillName });
2757
+ await this.#attachFormSuggestions(fields, state);
2444
2758
  const value = await this.#interact(state, {
2445
2759
  type: "form",
2446
2760
  id: this.#nextInteractionId(state),
2447
2761
  title: `Missing parameters for ${call.name}`,
2448
- fields: schemaToForm(def.inputSchema, args)
2762
+ fields
2449
2763
  }, {
2450
2764
  tool: call.name,
2451
2765
  missing
@@ -2484,11 +2798,41 @@ var AgentLoop = class {
2484
2798
  if (fresh.length > 0) result.artifacts = [...result.artifacts ?? [], ...fresh];
2485
2799
  }
2486
2800
  await this.#bumpSkillStat(skillName, result.ok ? "successes" : "failures", state);
2801
+ if (!result.ok) await this.#reportSkillFailure(skillName, state, {
2802
+ code: result.error?.code ?? "TOOL_EXECUTION_FAILED",
2803
+ message: result.error?.message ?? `Tool "${call.name}" returned a failure result`
2804
+ });
2487
2805
  return result;
2488
2806
  } catch (e) {
2489
2807
  if (e instanceof RunTerminated) throw e;
2490
2808
  await this.#bumpSkillStat(skillName, "failures", state);
2491
- return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `Tool "${call.name}" failed: ${messageOf(e)}`);
2809
+ const code = e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED";
2810
+ const message = `Tool "${call.name}" failed: ${messageOf(e)}`;
2811
+ await this.#reportSkillFailure(skillName, state, {
2812
+ code,
2813
+ message
2814
+ });
2815
+ return toolError(code, message);
2816
+ }
2817
+ }
2818
+ /**
2819
+ * F1 技能失败上报:喂给治理的失败计数器(`SkillStatePolicy.recordFailure`),
2820
+ * 达阈值即自动隔离。无注入即整段跳过(默认关闭)。
2821
+ *
2822
+ * 上报方抛错降级为 `run.warning`:治理写盘失败不该把一次「工具出错但已回喂给 LLM」
2823
+ * 的 run 变成崩溃——那会让引入治理反而降低可用性。
2824
+ */
2825
+ async #reportSkillFailure(skillName, state, detail) {
2826
+ const report = this.#deps.skillOutcomeReporter?.onSkillFailed;
2827
+ if (report === void 0) return;
2828
+ try {
2829
+ await report.call(this.#deps.skillOutcomeReporter, {
2830
+ skillName,
2831
+ runId: state.runId,
2832
+ ...detail
2833
+ });
2834
+ } catch (e) {
2835
+ state.trace.record("run.warning", { message: `Skill failure report for "${skillName}" was not recorded: ${messageOf(e)}` });
2492
2836
  }
2493
2837
  }
2494
2838
  /** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
@@ -2587,6 +2931,47 @@ var AgentLoop = class {
2587
2931
  });
2588
2932
  return history.slice(-limit);
2589
2933
  });
2934
+ await this.#rememberFormValues(state, request, value);
2935
+ }
2936
+ /**
2937
+ * 跨会话字段值的写入(FR-5.6/5.12)。与 `paramHistory` 双写而不是合并:
2938
+ * 那边是按时间的追加序列(运行观测),这边是按字段的最新值(召回),形态不同。
2939
+ */
2940
+ async #rememberFormValues(state, request, value) {
2941
+ const autofill = this.#deps.formAutofill;
2942
+ if (!autofill || request.type !== "form") return;
2943
+ if (typeof value !== "object" || value === null) return;
2944
+ const submitted = value;
2945
+ const ts = Date.parse(state.now());
2946
+ const updates = {};
2947
+ for (const field of request.fields) {
2948
+ if (field.fieldKey === void 0) continue;
2949
+ const next = submitted[field.name];
2950
+ if (next === void 0 || next === "") continue;
2951
+ updates[field.fieldKey] = {
2952
+ value: next,
2953
+ ts
2954
+ };
2955
+ }
2956
+ if (Object.keys(updates).length === 0) return;
2957
+ await this.#memoryMutate(`user:${autofill.userId}`, FORM_VALUES_KEY, state, (current) => putFormValues(readFormValues(current), updates, this.#config.formValueLimit));
2958
+ }
2959
+ /**
2960
+ * 召回(FR-5.8):命中的历史值挂在 `suggestion` 上,**绝不写进 `defaultValue`**——
2961
+ * 后者会被渲染器直接填进控件,等于静默预填(AC-5.6 禁止)。
2962
+ */
2963
+ async #attachFormSuggestions(fields, state) {
2964
+ const autofill = this.#deps.formAutofill;
2965
+ if (!autofill) return;
2966
+ const stored = readFormValues(await this.#memoryGet(`user:${autofill.userId}`, FORM_VALUES_KEY, state));
2967
+ for (const field of fields) {
2968
+ if (field.fieldKey === void 0 || field.defaultValue !== void 0) continue;
2969
+ const hit = stored[field.fieldKey];
2970
+ if (hit !== void 0) field.suggestion = {
2971
+ value: hit.value,
2972
+ ts: hit.ts
2973
+ };
2974
+ }
2590
2975
  }
2591
2976
  };
2592
2977
  /**
@@ -2598,79 +2983,6 @@ function mergeCatalogEntries(localEntries, providerEntries) {
2598
2983
  for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
2599
2984
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2600
2985
  }
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
2986
  /**
2675
2987
  * session history 滚动裁剪:超出预算时裁掉中段,保留首尾。
2676
2988
  * 边界对齐 LLM tool 契约:head 不以未应答的 assistant toolCalls 结尾,
@@ -2800,11 +3112,14 @@ var WebSkillRuntime = class {
2800
3112
  hooks: this.#deps.hooks,
2801
3113
  eventBus: this.#events,
2802
3114
  longTerm: this.#deps.longTerm,
3115
+ formAutofill: this.#deps.formAutofill,
2803
3116
  externalTools: this.#deps.externalTools,
2804
3117
  skillProviders: this.#deps.skillProviders,
2805
3118
  catalogFilter: this.#deps.catalogFilter,
2806
3119
  snapshotStore: this.#deps.snapshotStore,
2807
- skillStateGuard: this.#deps.skillStateGuard
3120
+ skillStateGuard: this.#deps.skillStateGuard,
3121
+ skillIntegrityGuard: this.#deps.skillIntegrityGuard,
3122
+ skillOutcomeReporter: this.#deps.skillOutcomeReporter
2808
3123
  }, this.#deps.config);
2809
3124
  const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
2810
3125
  this.#loops.set(runId, loop);
@@ -2841,14 +3156,14 @@ var WebSkillRuntime = class {
2841
3156
  });
2842
3157
  return result;
2843
3158
  }
2844
- /** D3:列出可恢复的 interrupted run(供 UI 展示"未完成任务" */
3159
+ /** D3:列出 interrupted run(供 UI 展示"未完成任务");版本不受支持的项带 unsupported 标记 */
2845
3160
  async listInterruptedRuns() {
2846
3161
  if (!this.#deps.snapshotStore) return [];
2847
3162
  return this.#deps.snapshotStore.list();
2848
3163
  }
2849
3164
  /**
2850
3165
  * D3 恢复 interrupted run:
2851
- * 不存在 → RUN_SNAPSHOT_NOT_FOUND;schemaVersion 不匹配删除 + RUN_SNAPSHOT_INCOMPATIBLE;
3166
+ * 不存在 → RUN_SNAPSHOT_NOT_FOUND;schema 版本不受支持RUN_SNAPSHOT_SCHEMA_UNSUPPORTED(由 store 抛出,不删文件);
2852
3167
  * 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
2853
3168
  * @experimental
2854
3169
  */
@@ -2856,10 +3171,6 @@ var WebSkillRuntime = class {
2856
3171
  const store = this.#deps.snapshotStore;
2857
3172
  const snapshot = store ? await store.load(runId) : void 0;
2858
3173
  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
3174
  if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
2864
3175
  await store.delete(runId);
2865
3176
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -2908,11 +3219,14 @@ var WebSkillRuntime = class {
2908
3219
  hooks: this.#deps.hooks,
2909
3220
  eventBus: this.#events,
2910
3221
  longTerm: this.#deps.longTerm,
3222
+ formAutofill: this.#deps.formAutofill,
2911
3223
  externalTools: this.#deps.externalTools,
2912
3224
  skillProviders: this.#deps.skillProviders,
2913
3225
  catalogFilter: this.#deps.catalogFilter,
2914
3226
  snapshotStore: store,
2915
- skillStateGuard: this.#deps.skillStateGuard
3227
+ skillStateGuard: this.#deps.skillStateGuard,
3228
+ skillIntegrityGuard: this.#deps.skillIntegrityGuard,
3229
+ skillOutcomeReporter: this.#deps.skillOutcomeReporter
2916
3230
  }, this.#deps.config);
2917
3231
  this.#loops.set(runId, loop);
2918
3232
  try {
@@ -3031,6 +3345,7 @@ function sourceFromUrl(url) {
3031
3345
  /** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
3032
3346
  var HookRunner = class {
3033
3347
  #hooks = /* @__PURE__ */ new Map();
3348
+ /** 宿主可在装配后按 RuntimeConfig 调整(console Settings › Agent Runtime 即经此生效) */
3034
3349
  timeoutMs;
3035
3350
  failOnHookError;
3036
3351
  onWarning;
@@ -3046,6 +3361,15 @@ var HookRunner = class {
3046
3361
  this.#hooks.set(key, list);
3047
3362
  return this;
3048
3363
  }
3364
+ /**
3365
+ * 已注册钩子的**计数**,按注册相位分组(`'*'` 为全相位钩子)。
3366
+ * 刻意不返回函数引用:那会给 UI 一条调用宿主钩子的执行路径,而面板只需要「装没装上」。
3367
+ */
3368
+ listRegisteredHooks() {
3369
+ const counts = /* @__PURE__ */ new Map();
3370
+ for (const [phase, hooks] of this.#hooks) counts.set(phase, hooks.length);
3371
+ return counts;
3372
+ }
3049
3373
  async run(phase, ctx) {
3050
3374
  const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
3051
3375
  for (const hook of hooks) try {
@@ -3652,17 +3976,22 @@ function summarizeToolCalls(run) {
3652
3976
  if (typeof name !== "string" || typeof callId !== "string") continue;
3653
3977
  const args = event.data?.["args"];
3654
3978
  const durationMs = event.data?.["durationMs"];
3979
+ const errorCode = event.data?.["code"];
3655
3980
  calls.push({
3656
3981
  callId,
3657
3982
  name,
3658
3983
  status: event.type === "tool.completed" ? "completed" : "failed",
3659
3984
  ...typeof args === "string" ? { args } : {},
3660
- ...typeof durationMs === "number" ? { durationMs } : {}
3985
+ ...typeof durationMs === "number" ? { durationMs } : {},
3986
+ ...typeof errorCode === "string" ? { errorCode } : {},
3987
+ ...event.type === "tool.failed" && typeof event.message === "string" ? { errorMessage: event.message } : {}
3661
3988
  });
3662
3989
  }
3663
3990
  return calls;
3664
3991
  }
3665
3992
  const SESSION_SCHEMA_VERSION = 1;
3993
+ /** `FsSessionStore` 的缺省页长。缺省值属于实现,不属于调用方——否则「下推」只推了一半 */
3994
+ const FS_SESSION_PAGE_SIZE = 50;
3666
3995
  const toMeta = (record) => ({
3667
3996
  id: record.id,
3668
3997
  createdAt: record.createdAt,
@@ -3673,6 +4002,25 @@ const toMeta = (record) => ({
3673
4002
  });
3674
4003
  const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3675
4004
  /**
4005
+ * 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
4006
+ * 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
4007
+ * 但它是不透明串,调用方不得自己构造(构造出来的越界值一律按 `VALIDATION_FAILED` 拒绝)。
4008
+ */
4009
+ function takeTailPage(all, options, what) {
4010
+ const limit = options.limit ?? 50;
4011
+ if (!Number.isInteger(limit) || limit <= 0) throw new WebSkillError("VALIDATION_FAILED", `Page limit must be a positive integer, received ${String(limit)}`);
4012
+ let end = all.length;
4013
+ if (options.cursor !== void 0) {
4014
+ end = Number(options.cursor);
4015
+ if (!Number.isInteger(end) || end < 0 || end > all.length) throw new WebSkillError("VALIDATION_FAILED", `Invalid ${what} cursor: ${JSON.stringify(options.cursor)}`);
4016
+ }
4017
+ const start = Math.max(0, end - limit);
4018
+ return {
4019
+ items: all.slice(start, end),
4020
+ ...start > 0 ? { nextCursor: String(start) } : {}
4021
+ };
4022
+ }
4023
+ /**
3676
4024
  * 解析会话文件。缺 `schemaVersion` 视为 0(0.0.1 时代文件,兼容读);
3677
4025
  * 高于当前版本拒绝读,避免新版写的字段被旧版静默丢弃。
3678
4026
  */
@@ -3746,7 +4094,7 @@ var FsSessionStore = class {
3746
4094
  return record;
3747
4095
  }
3748
4096
  async list(options = {}) {
3749
- if (!await this.#fs.exists(this.#root)) return [];
4097
+ if (!await this.#fs.exists(this.#root)) return { items: [] };
3750
4098
  const metas = [];
3751
4099
  for (const entry of await this.#fs.list(this.#root)) {
3752
4100
  if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
@@ -3760,7 +4108,15 @@ var FsSessionStore = class {
3760
4108
  if (record.archived === true && options.includeArchived !== true) continue;
3761
4109
  metas.push(toMeta(record));
3762
4110
  }
3763
- return metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
4111
+ metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
4112
+ return takeTailPage(metas, options, "session");
4113
+ }
4114
+ /**
4115
+ * 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
4116
+ * 但**跨出端口的记录数**已是常量——这正是分页对上层的意义。
4117
+ */
4118
+ async listMessages(id, options = {}) {
4119
+ return takeTailPage((await this.#require(id)).messages, options, "message");
3764
4120
  }
3765
4121
  async get(id) {
3766
4122
  const path = this.#path(id);
@@ -3820,4 +4176,4 @@ var FsSessionStore = class {
3820
4176
  };
3821
4177
 
3822
4178
  //#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 };
4179
+ export { schemaToForm as $, buildRenderResult as A, fromVercelStreamPart as B, RUN_SNAPSHOT_SCHEMA_VERSION as C, TraceRecorder as D, SerializingMemoryStore as E, extractChartSpec as F, networkUrlHost as G, isUnsupportedRunSnapshot as H, extractTodoTraceEvents as I, normalizeToolError as J, normalizeErrorCode as K, extractUiSpecEvents as L, clearStoredFormValues as M, createScriptContext as N, WebSkillRuntime as O, createWebSkillApi as P, resolveToolName as Q, formFieldKey as R, READ_SKILL_FILE_TOOL_NAME as S, SESSION_SCHEMA_VERSION as T, mergeCatalogEntries as U, isNetworkAllowed as V, networkPolicyLibSource as W, putFormValues as X, parseBridgeRequest as Y, readFormValues as Z, HookRunner as _, AnthropicClient as a, READ_SKILL_FILE_INPUT_SCHEMA as b, FORM_VALUES_KEY as c, FsMemoryStore as d, summarizeToolCalls as et, FsRunSnapshotStore as f, GoogleGenAiClient as g, FullDisclosureRouter as h, AgentLoop as i, validateUiSpecNode as it, clearFormValues as j, bridgeError as k, FS_SESSION_PAGE_SIZE as l, FsSessionStore as m, ASK_USER_TOOL as n, toVercelToolSpecs as nt, CapabilityApproval as o, FsRunTraceStore as p, normalizeToolContent as q, ASK_USER_TOOL_NAME as r, validateUiSpecEvent as rt, EventBus as s, ASK_USER_INPUT_SCHEMA as t, toLlmToolSpec as tt, FsArtifactStore as u, OpenAiCompatibleClient as v, RUN_TRACE_SCHEMA_VERSION as w, READ_SKILL_FILE_TOOL as x, ProgressiveRouter as y, fromVercelResult as z };