@webskill/sdk 0.9.0 → 0.11.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 (36) hide show
  1. package/dist/agent.d.ts +3 -2
  2. package/dist/agent.js +3 -1098
  3. package/dist/browser.d.ts +260 -11
  4. package/dist/browser.js +772 -52
  5. package/dist/{catalogComponents-DfxxfUvn-T7Ic8QFV.js → catalogComponents-BFoqpT1v-CjUBZ3bc.js} +1604 -426
  6. package/dist/{dist-CFmkV45C.js → dist-B-cOu08W.js} +874 -107
  7. package/dist/{dist-BQe1uglQ.js → dist-DTHZS2k1.js} +524 -30
  8. package/dist/dist-qnlI2Iup.js +1280 -0
  9. package/dist/{eventTypes-DbOpAECr-BjcjZVms.js → eventTypes-FllCrX-Z-DNDeHWoG.js} +9 -2
  10. package/dist/governance.d.ts +33 -6
  11. package/dist/governance.js +45 -7
  12. package/dist/{index-znZjobkr.d.ts → index-D3mONFHD.d.ts} +281 -11
  13. package/dist/{index-DXNTIa-6.d.ts → index-DACk2_XZ.d.ts} +114 -7
  14. package/dist/{index-lLcCpHE-.d.ts → index-DWbs58LF.d.ts} +234 -14
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +3 -3
  17. package/dist/mcp.d.ts +49 -6
  18. package/dist/mcp.js +146 -38
  19. package/dist/node.d.ts +3 -3
  20. package/dist/node.js +52 -2
  21. package/dist/{openUiLibrary-DURlAxjk-Do_yqg3u.js → openUiLibrary-D5u8oIvx-BLOAQCho.js} +3 -3
  22. package/dist/processSandboxEntry.js +6 -0
  23. package/dist/sandboxWorkerEntry.js +6 -0
  24. package/dist/{skillVersionStore-Bl-ElD45-dMJ8Ybhb.d.ts → skillVersionStore-D-qHk9ZE-BcmFLykd.d.ts} +13 -5
  25. package/dist/{testing-Csg5ljNm.js → testing-WPTyXQYt.js} +26 -3
  26. package/dist/testing.d.ts +1 -1
  27. package/dist/testing.js +1 -1
  28. package/dist/{types-B3n0cMZu-BDhheIhX.d.ts → types-C26b05fW-CdrRCRDb.d.ts} +55 -6
  29. package/dist/ui-react.d.ts +11 -2
  30. package/dist/ui-react.js +141 -42
  31. package/dist/ui-vue.d.ts +1 -1
  32. package/dist/ui-vue.js +2 -2
  33. package/dist/ui.d.ts +4 -4
  34. package/dist/ui.js +3 -3
  35. package/dist/{webskillLitCatalog-DwTwSBFt-BG7kL-Es.js → webskillLitCatalog-DME6PBkV-CmYNLlIT.js} +135 -16
  36. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { K as validateSkills, M as messageOf, P as parseSkillMarkdown, R as renderAvailableSkillsXml, V as resolveInsideRoot, _ as assertSafePathSegment, h as WebSkillError, m as SkillReader, p as SkillDiscovery, v as atomicWriteText, y as buildCatalog } from "./dist-Bev6i6Ip.js";
1
+ import { K as validateSkills, M as messageOf, P as parseSkillMarkdown, R as renderAvailableSkillsXml, V as resolveInsideRoot, _ as assertSafePathSegment, g as assertRemoteUrlAllowed, h as WebSkillError, m as SkillReader, p as SkillDiscovery, v as atomicWriteText, y as buildCatalog } from "./dist-Bev6i6Ip.js";
2
2
  import { a as textParts, i as rejectUnsupportedPart, n as partsToText, o as validateLlmMessages, r as promptText, t as MemoryArtifactStore } from "./memoryArtifactStore-52Zn9npI-LbCQaqyx.js";
3
3
 
4
4
  //#region ../runtime/dist/index.js
@@ -103,11 +103,11 @@ const SCHEMA_KEYS = /* @__PURE__ */ new Set([
103
103
  "then",
104
104
  "else"
105
105
  ]);
106
- function isRecord$6(value) {
106
+ function isRecord$7(value) {
107
107
  return typeof value === "object" && value !== null && !Array.isArray(value);
108
108
  }
109
109
  function isSelfRef(value) {
110
- return isRecord$6(value) && value["$ref"] === SELF_REF;
110
+ return isRecord$7(value) && value["$ref"] === SELF_REF;
111
111
  }
112
112
  /**
113
113
  * 按 JSON Schema 关键字下降到子 schema。
@@ -117,7 +117,7 @@ function isSelfRef(value) {
117
117
  function forEachSubSchema(node, visit) {
118
118
  for (const [key, value] of Object.entries(node)) {
119
119
  if (SCHEMA_MAP_KEYS.has(key)) {
120
- if (isRecord$6(value)) for (const name of Object.keys(value)) visit(value[name], [key, name]);
120
+ if (isRecord$7(value)) for (const name of Object.keys(value)) visit(value[name], [key, name]);
121
121
  continue;
122
122
  }
123
123
  if (SCHEMA_LIST_KEYS.has(key)) {
@@ -136,7 +136,7 @@ function pathKey(path) {
136
136
  * `#` 解析到最近的带 `$id` 的祖先;没有则是文档根。
137
137
  */
138
138
  function collectSelfRefRoots(node, path, resourceRoot, out) {
139
- if (!isRecord$6(node)) return;
139
+ if (!isRecord$7(node)) return;
140
140
  if (isSelfRef(node)) {
141
141
  out.set(pathKey(resourceRoot), resourceRoot);
142
142
  return;
@@ -149,7 +149,7 @@ function collectSelfRefRoots(node, path, resourceRoot, out) {
149
149
  function getAt(schema, path) {
150
150
  let current = schema;
151
151
  for (const segment of path) if (Array.isArray(current) && typeof segment === "number") current = current[segment];
152
- else if (isRecord$6(current) && typeof segment === "string") current = current[segment];
152
+ else if (isRecord$7(current) && typeof segment === "string") current = current[segment];
153
153
  else return void 0;
154
154
  return current;
155
155
  }
@@ -175,7 +175,7 @@ function setAt(schema, path, replacement) {
175
175
  * 正是严格解析器无法解析 `#` 的直接原因。
176
176
  */
177
177
  function rewriteSelfRefs(node, target, depth) {
178
- if (!isRecord$6(node)) return node;
178
+ if (!isRecord$7(node)) return node;
179
179
  if (depth > 8) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool schema nesting exceeds 8 levels; cannot produce a portable form`);
180
180
  if (isSelfRef(node)) {
181
181
  const rest = {};
@@ -188,7 +188,7 @@ function rewriteSelfRefs(node, target, depth) {
188
188
  const result = {};
189
189
  for (const [key, value] of Object.entries(node)) {
190
190
  if (key === "$id" || key === "$schema") continue;
191
- if (SCHEMA_MAP_KEYS.has(key) && isRecord$6(value)) {
191
+ if (SCHEMA_MAP_KEYS.has(key) && isRecord$7(value)) {
192
192
  const mapped = {};
193
193
  for (const [name, child] of Object.entries(value)) mapped[name] = rewriteSelfRefs(child, target, depth + 1);
194
194
  result[key] = mapped;
@@ -226,8 +226,8 @@ function toPortableToolSchema(schema) {
226
226
  if (roots.size > 1) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", "Tool schema contains self references in more than one schema resource; cannot produce a portable form");
227
227
  const rootPath = [...roots.values()][0] ?? [];
228
228
  const resource = getAt(schema, rootPath);
229
- if (!isRecord$6(resource)) return schema;
230
- const existingDefs = isRecord$6(schema[DEFS_KEY]) ? schema[DEFS_KEY] : void 0;
229
+ if (!isRecord$7(resource)) return schema;
230
+ const existingDefs = isRecord$7(schema[DEFS_KEY]) ? schema[DEFS_KEY] : void 0;
231
231
  const taken = new Set(Object.keys(existingDefs ?? {}));
232
232
  const nodeName = uniqueName(NODE_NAME, taken);
233
233
  taken.add(nodeName);
@@ -258,6 +258,63 @@ function toPortableToolSchema(schema) {
258
258
  }
259
259
  };
260
260
  }
261
+ function isRecord$6(value) {
262
+ return typeof value === "object" && value !== null && !Array.isArray(value);
263
+ }
264
+ /**
265
+ * 与 `interaction/schemaToForm` 的 file 判定保持一致:JSON Schema 里二进制内容的
266
+ * 两种标准写法(`format: binary` / `contentEncoding: base64`)。
267
+ */
268
+ function isModelUnfillable(node) {
269
+ return isRecord$6(node) && node["type"] === "string" && (node["format"] === "binary" || node["contentEncoding"] === "base64");
270
+ }
271
+ function stripNode(node) {
272
+ if (Array.isArray(node)) return node.map(stripNode);
273
+ if (!isRecord$6(node)) return node;
274
+ const out = {};
275
+ for (const [key, value] of Object.entries(node)) {
276
+ if (SCHEMA_MAP_KEYS.has(key) && isRecord$6(value)) {
277
+ const mapped = {};
278
+ for (const [name, child] of Object.entries(value)) {
279
+ if (key === "properties" && isModelUnfillable(child)) continue;
280
+ mapped[name] = stripNode(child);
281
+ }
282
+ out[key] = mapped;
283
+ continue;
284
+ }
285
+ if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
286
+ out[key] = value.map(stripNode);
287
+ continue;
288
+ }
289
+ if (SCHEMA_KEYS.has(key)) {
290
+ out[key] = stripNode(value);
291
+ continue;
292
+ }
293
+ out[key] = value;
294
+ }
295
+ const required = out["required"];
296
+ const properties = out["properties"];
297
+ if (Array.isArray(required) && isRecord$6(properties)) out["required"] = required.filter((name) => typeof name !== "string" || name in properties);
298
+ return out;
299
+ }
300
+ /**
301
+ * 把模型不可能自己产出的字段(本地文件二进制)从**出站**工具 schema 里剥离。
302
+ *
303
+ * 模型拿不到用户磁盘上的文件,留着这个字段只有两种下场:模型拒绝调用工具
304
+ * (实测 DeepSeek 直接回文本「请上传文件」,file-pick 永远触发不了),或编造一个
305
+ * 占位字符串把技能跑挂。剥掉后模型以缺参形式调用,引擎的 missing-params 链路
306
+ * 会弹 file-pick 让用户用系统文件选择器补这个字段。
307
+ *
308
+ * 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop——
309
+ * 引擎侧的 missing 校验看的必须是技能作者写的完整 inputSchema。
310
+ *
311
+ * 已知边界(0.10.0 复核注记):只剥 `properties` 里的 inline 二进制字段;
312
+ * 通过 `$ref` 指向 `$defs` 中二进制定义的字段仍会下发(剥掉会留悬空 $ref),
313
+ * 这类技能依旧可能拿不到 file-pick——需要完整修复时再补「剥 $def + 剥指向它的 $ref」。
314
+ */
315
+ function stripModelUnfillableParams(schema) {
316
+ return stripNode(schema);
317
+ }
261
318
  /** 各家都认的基础关键字。Google `FunctionDeclaration.parameters` 是其中最窄的一家。 */
262
319
  const BASE_KEYWORDS = /* @__PURE__ */ new Set([
263
320
  "type",
@@ -499,7 +556,7 @@ const toOpenAiTools = (tools) => tools.map((tool) => ({
499
556
  function: {
500
557
  name: tool.name,
501
558
  ...tool.description ? { description: tool.description } : {},
502
- parameters: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), OPENAI_SCHEMA_PROFILE, tool.name)
559
+ parameters: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), OPENAI_SCHEMA_PROFILE, tool.name)
503
560
  }
504
561
  }));
505
562
  const errorMessage$2 = (e) => e instanceof Error ? e.message : String(e);
@@ -539,6 +596,7 @@ var OpenAiCompatibleClient = class {
539
596
  const reader = res.body.getReader();
540
597
  const frames = createSseFrameReader();
541
598
  let done = false;
599
+ let usage;
542
600
  const handleFrame = function* (data) {
543
601
  if (data === "[DONE]") {
544
602
  done = true;
@@ -550,8 +608,17 @@ var OpenAiCompatibleClient = class {
550
608
  } catch (e) {
551
609
  throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage$2(e)}`, data);
552
610
  }
611
+ const usageRaw = chunk.usage;
612
+ if (usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number")) usage = {
613
+ inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
614
+ outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
615
+ };
553
616
  const delta = chunk.choices?.[0]?.delta;
554
617
  if (!delta) return;
618
+ if (typeof delta["reasoning_content"] === "string" && delta["reasoning_content"] !== "") yield {
619
+ type: "thinking-delta",
620
+ delta: delta["reasoning_content"]
621
+ };
555
622
  if (typeof delta["content"] === "string" && delta["content"] !== "") yield {
556
623
  type: "text-delta",
557
624
  delta: delta["content"]
@@ -601,7 +668,10 @@ var OpenAiCompatibleClient = class {
601
668
  };
602
669
  })
603
670
  };
604
- yield { type: "done" };
671
+ yield {
672
+ type: "done",
673
+ ...usage ? { usage } : {}
674
+ };
605
675
  }
606
676
  async #postChat(input, stream) {
607
677
  const { baseUrl, apiKey, model } = this.#requireConfig();
@@ -615,7 +685,10 @@ var OpenAiCompatibleClient = class {
615
685
  body["tool_choice"] = "auto";
616
686
  }
617
687
  if (input.temperature !== void 0) body["temperature"] = input.temperature;
618
- if (stream) body["stream"] = true;
688
+ if (stream) {
689
+ body["stream"] = true;
690
+ body["stream_options"] = { include_usage: true };
691
+ }
619
692
  let res;
620
693
  try {
621
694
  res = await this.#fetch(`${baseUrl}/chat/completions`, {
@@ -665,9 +738,17 @@ var OpenAiCompatibleClient = class {
665
738
  };
666
739
  });
667
740
  const content = choice["content"];
741
+ const thinking = choice["reasoning_content"];
742
+ const usageRaw = data.usage;
743
+ const usage = usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number") ? {
744
+ inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
745
+ outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
746
+ } : void 0;
668
747
  return {
669
748
  content: typeof content === "string" && content !== "" ? textParts(content) : void 0,
670
749
  toolCalls: toolCalls?.length ? toolCalls : void 0,
750
+ ...typeof thinking === "string" && thinking !== "" ? { thinking } : {},
751
+ ...usage ? { usage } : {},
671
752
  raw: data
672
753
  };
673
754
  } catch (e) {
@@ -756,7 +837,7 @@ function toAnthropicMessages(messages) {
756
837
  const toAnthropicTools = (tools) => tools.map((tool) => ({
757
838
  name: tool.name,
758
839
  ...tool.description ? { description: tool.description } : {},
759
- input_schema: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), ANTHROPIC_SCHEMA_PROFILE, tool.name)
840
+ input_schema: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), ANTHROPIC_SCHEMA_PROFILE, tool.name)
760
841
  }));
761
842
  /** Anthropic Messages API 客户端(零依赖 fetch;Node/浏览器通用) */
762
843
  var AnthropicClient = class {
@@ -795,6 +876,8 @@ var AnthropicClient = class {
795
876
  const decoder = new TextDecoder();
796
877
  const reader = res.body.getReader();
797
878
  const frames = createSseFrameReader();
879
+ let inputTokens;
880
+ let outputTokens;
798
881
  const handleFrame = function* (data) {
799
882
  let chunk;
800
883
  try {
@@ -804,6 +887,17 @@ var AnthropicClient = class {
804
887
  }
805
888
  const type = chunk["type"];
806
889
  if (type === "error") throw new WebSkillError("LLM_REQUEST_FAILED", `LLM stream error: ${chunk["error"]?.message ?? data}`);
890
+ if (type === "message_start") {
891
+ const usage = chunk["message"]?.["usage"];
892
+ if (typeof usage?.["input_tokens"] === "number") inputTokens = usage["input_tokens"];
893
+ if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
894
+ return;
895
+ }
896
+ if (type === "message_delta") {
897
+ const usage = chunk["usage"];
898
+ if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
899
+ return;
900
+ }
807
901
  if (type === "content_block_start") {
808
902
  const block = chunk["content_block"];
809
903
  if (block?.["type"] === "tool_use") {
@@ -826,6 +920,13 @@ var AnthropicClient = class {
826
920
  };
827
921
  return;
828
922
  }
923
+ if (delta?.["type"] === "thinking_delta" && typeof delta["thinking"] === "string" && delta["thinking"] !== "") {
924
+ yield {
925
+ type: "thinking-delta",
926
+ delta: delta["thinking"]
927
+ };
928
+ return;
929
+ }
829
930
  if (delta?.["type"] === "input_json_delta" && typeof delta["partial_json"] === "string") {
830
931
  const acc = toolCallsByIndex.get(index) ?? {
831
932
  id: "",
@@ -866,7 +967,14 @@ var AnthropicClient = class {
866
967
  };
867
968
  })
868
969
  };
869
- yield { type: "done" };
970
+ const usage = inputTokens !== void 0 || outputTokens !== void 0 ? {
971
+ inputTokens: inputTokens ?? 0,
972
+ outputTokens: outputTokens ?? 0
973
+ } : void 0;
974
+ yield {
975
+ type: "done",
976
+ ...usage ? { usage } : {}
977
+ };
870
978
  }
871
979
  async #post(input, stream) {
872
980
  validateLlmMessages(input.messages);
@@ -878,7 +986,12 @@ var AnthropicClient = class {
878
986
  };
879
987
  if (system !== void 0) body["system"] = system;
880
988
  if (input.tools?.length) body["tools"] = toAnthropicTools(input.tools);
881
- if (input.temperature !== void 0) body["temperature"] = input.temperature;
989
+ const thinkingBudget = this.#config.thinkingBudgetTokens;
990
+ if (thinkingBudget !== void 0 && Number.isInteger(thinkingBudget) && thinkingBudget > 0) body["thinking"] = {
991
+ type: "enabled",
992
+ budget_tokens: thinkingBudget
993
+ };
994
+ else if (input.temperature !== void 0) body["temperature"] = input.temperature;
882
995
  if (stream) body["stream"] = true;
883
996
  let res;
884
997
  try {
@@ -926,14 +1039,22 @@ var AnthropicClient = class {
926
1039
  const blocks = data.content;
927
1040
  if (!Array.isArray(blocks)) throw new Error("response has no content blocks");
928
1041
  const text = blocks.filter((b) => b["type"] === "text").map((b) => String(b["text"] ?? "")).join("");
1042
+ const thinking = blocks.filter((b) => b["type"] === "thinking").map((b) => String(b["thinking"] ?? "")).join("");
929
1043
  const toolCalls = blocks.filter((b) => b["type"] === "tool_use").map((b) => ({
930
1044
  id: typeof b["id"] === "string" ? b["id"] : "",
931
1045
  name: typeof b["name"] === "string" ? b["name"] : "",
932
1046
  arguments: typeof b["input"] === "object" && b["input"] !== null ? b["input"] : {}
933
1047
  }));
1048
+ const usageRaw = data.usage;
1049
+ const usage = typeof usageRaw?.["input_tokens"] === "number" || typeof usageRaw?.["output_tokens"] === "number" ? {
1050
+ inputTokens: typeof usageRaw["input_tokens"] === "number" ? usageRaw["input_tokens"] : 0,
1051
+ outputTokens: typeof usageRaw["output_tokens"] === "number" ? usageRaw["output_tokens"] : 0
1052
+ } : void 0;
934
1053
  return {
935
1054
  content: text === "" ? void 0 : textParts(text),
936
1055
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
1056
+ ...thinking === "" ? {} : { thinking },
1057
+ ...usage ? { usage } : {},
937
1058
  raw: data
938
1059
  };
939
1060
  } catch (e) {
@@ -942,6 +1063,18 @@ var AnthropicClient = class {
942
1063
  }
943
1064
  };
944
1065
  const errorMessage = (e) => e instanceof Error ? e.message : String(e);
1066
+ /** usageMetadata → LlmTokenUsage(promptTokenCount / candidatesTokenCount;无字段返回 undefined) */
1067
+ function parseGenAiUsage(data) {
1068
+ const usage = data?.usageMetadata;
1069
+ if (!usage) return void 0;
1070
+ const input = usage["promptTokenCount"];
1071
+ const output = usage["candidatesTokenCount"];
1072
+ if (typeof input !== "number" && typeof output !== "number") return void 0;
1073
+ return {
1074
+ inputTokens: typeof input === "number" ? input : 0,
1075
+ outputTokens: typeof output === "number" ? output : 0
1076
+ };
1077
+ }
945
1078
  /** parts → GenAI parts(二进制统一走 inlineData) */
946
1079
  const toGenAiParts = (parts, where) => parts.map((part) => {
947
1080
  switch (part.type) {
@@ -992,10 +1125,16 @@ function toGenAiContents(messages) {
992
1125
  }
993
1126
  if (msg.role === "assistant") {
994
1127
  const parts = toGenAiParts(msg.content, "assistant");
995
- for (const call of msg.toolCalls ?? []) parts.push({ functionCall: {
996
- name: call.name,
997
- args: call.arguments
998
- } });
1128
+ for (const call of msg.toolCalls ?? []) {
1129
+ const signature = call.vendor?.["thoughtSignature"];
1130
+ parts.push({
1131
+ functionCall: {
1132
+ name: call.name,
1133
+ args: call.arguments
1134
+ },
1135
+ ...typeof signature === "string" ? { thoughtSignature: signature } : {}
1136
+ });
1137
+ }
999
1138
  out.push({
1000
1139
  role: "model",
1001
1140
  parts: parts.length > 0 ? parts : [{ text: "" }]
@@ -1015,7 +1154,7 @@ function toGenAiContents(messages) {
1015
1154
  const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => ({
1016
1155
  name: tool.name,
1017
1156
  ...tool.description ? { description: tool.description } : {},
1018
- parameters: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), GOOGLE_SCHEMA_PROFILE, tool.name)
1157
+ parameters: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), GOOGLE_SCHEMA_PROFILE, tool.name)
1019
1158
  })) }];
1020
1159
  /** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
1021
1160
  var GoogleGenAiClient = class {
@@ -1039,9 +1178,11 @@ var GoogleGenAiClient = class {
1039
1178
  const parts = this.#responseParts(data);
1040
1179
  const text = parts.filter((p) => typeof p["text"] === "string").map((p) => String(p["text"])).join("");
1041
1180
  const toolCalls = this.#functionCalls(parts);
1181
+ const usage = parseGenAiUsage(data);
1042
1182
  return {
1043
1183
  content: text === "" ? void 0 : textParts(text),
1044
1184
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
1185
+ ...usage ? { usage } : {},
1045
1186
  raw: data
1046
1187
  };
1047
1188
  }
@@ -1053,6 +1194,7 @@ var GoogleGenAiClient = class {
1053
1194
  const decoder = new TextDecoder();
1054
1195
  const reader = res.body.getReader();
1055
1196
  const frames = createSseFrameReader();
1197
+ let usage;
1056
1198
  const handleFrame = function* (data) {
1057
1199
  let chunk;
1058
1200
  try {
@@ -1060,6 +1202,7 @@ var GoogleGenAiClient = class {
1060
1202
  } catch (e) {
1061
1203
  throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage(e)}`, data);
1062
1204
  }
1205
+ usage = parseGenAiUsage(chunk) ?? usage;
1063
1206
  const parts = chunk.candidates?.[0]?.content?.parts;
1064
1207
  for (const part of parts ?? []) {
1065
1208
  if (typeof part["text"] === "string" && part["text"] !== "") {
@@ -1070,11 +1213,15 @@ var GoogleGenAiClient = class {
1070
1213
  continue;
1071
1214
  }
1072
1215
  const call = part["functionCall"];
1073
- if (call) toolCalls.push({
1074
- id: `call-${toolCalls.length}`,
1075
- name: call.name ?? "",
1076
- arguments: call.args ?? {}
1077
- });
1216
+ if (call) {
1217
+ const signature = part["thoughtSignature"];
1218
+ toolCalls.push({
1219
+ id: `call-${toolCalls.length}`,
1220
+ name: call.name ?? "",
1221
+ arguments: call.args ?? {},
1222
+ ...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
1223
+ });
1224
+ }
1078
1225
  }
1079
1226
  };
1080
1227
  try {
@@ -1091,7 +1238,10 @@ var GoogleGenAiClient = class {
1091
1238
  type: "tool-calls",
1092
1239
  toolCalls
1093
1240
  };
1094
- yield { type: "done" };
1241
+ yield {
1242
+ type: "done",
1243
+ ...usage ? { usage } : {}
1244
+ };
1095
1245
  }
1096
1246
  async #post(input, stream) {
1097
1247
  const model = input.model ?? this.#config.model;
@@ -1143,15 +1293,30 @@ var GoogleGenAiClient = class {
1143
1293
  const out = [];
1144
1294
  for (const part of parts) {
1145
1295
  const call = part["functionCall"];
1146
- if (call) out.push({
1147
- id: `call-${out.length}`,
1148
- name: call.name ?? "",
1149
- arguments: call.args ?? {}
1150
- });
1296
+ if (call) {
1297
+ const signature = part["thoughtSignature"];
1298
+ out.push({
1299
+ id: `call-${out.length}`,
1300
+ name: call.name ?? "",
1301
+ arguments: call.args ?? {},
1302
+ ...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
1303
+ });
1304
+ }
1151
1305
  }
1152
1306
  return out;
1153
1307
  }
1154
1308
  };
1309
+ /** v5 { inputTokens, outputTokens } 与 v4 { promptTokens, completionTokens } 两种形状都接 */
1310
+ function fromVercelUsage(raw) {
1311
+ const u = raw ?? {};
1312
+ const input = u["inputTokens"] ?? u["promptTokens"];
1313
+ const output = u["outputTokens"] ?? u["completionTokens"];
1314
+ if (typeof input !== "number" && typeof output !== "number") return void 0;
1315
+ return {
1316
+ inputTokens: typeof input === "number" ? input : 0,
1317
+ outputTokens: typeof output === "number" ? output : 0
1318
+ };
1319
+ }
1155
1320
  function toVercelToolSpecs(tools) {
1156
1321
  const out = {};
1157
1322
  for (const tool of tools) out[tool.name] = {
@@ -1160,6 +1325,16 @@ function toVercelToolSpecs(tools) {
1160
1325
  };
1161
1326
  return out;
1162
1327
  }
1328
+ /**
1329
+ * 归一化供应商思考内容:v5 的 `reasoningText`(字符串)与 v4 的 `reasoning`
1330
+ * (Array<{ text; type }>)都能落到同一条 `LlmResponse.thinking`。
1331
+ */
1332
+ function normalizeThinking(value) {
1333
+ if (typeof value === "string") return value !== "" ? value : void 0;
1334
+ if (!Array.isArray(value)) return void 0;
1335
+ const text = value.map((part) => typeof part?.text === "string" ? part.text : "").join("");
1336
+ return text !== "" ? text : void 0;
1337
+ }
1163
1338
  /** 接受 generateText 的返回值形状:{ text?, toolCalls?: [{ toolCallId, toolName, input|args }] } */
1164
1339
  function fromVercelResult(result) {
1165
1340
  const r = result;
@@ -1171,9 +1346,13 @@ function fromVercelResult(result) {
1171
1346
  arguments: typeof args === "object" && args !== null ? args : {}
1172
1347
  };
1173
1348
  });
1349
+ const thinking = normalizeThinking(r.reasoningText ?? r.reasoning);
1350
+ const usage = fromVercelUsage(r.usage);
1174
1351
  return {
1175
1352
  content: typeof r.text === "string" && r.text !== "" ? textParts(r.text) : void 0,
1176
1353
  toolCalls: toolCalls.length ? toolCalls : void 0,
1354
+ ...thinking !== void 0 ? { thinking } : {},
1355
+ ...usage ? { usage } : {},
1177
1356
  raw: result
1178
1357
  };
1179
1358
  }
@@ -1191,6 +1370,13 @@ function fromVercelStreamPart(part) {
1191
1370
  delta
1192
1371
  } : void 0;
1193
1372
  }
1373
+ case "reasoning-delta": {
1374
+ const delta = p["text"] ?? p["delta"];
1375
+ return typeof delta === "string" && delta !== "" ? {
1376
+ type: "thinking-delta",
1377
+ delta
1378
+ } : void 0;
1379
+ }
1194
1380
  case "tool-call": return {
1195
1381
  type: "tool-calls",
1196
1382
  toolCalls: [{
@@ -1199,7 +1385,13 @@ function fromVercelStreamPart(part) {
1199
1385
  arguments: typeof p["input"] === "object" && p["input"] !== null ? p["input"] : {}
1200
1386
  }]
1201
1387
  };
1202
- case "finish": return { type: "done" };
1388
+ case "finish": {
1389
+ const usage = fromVercelUsage(p["totalUsage"] ?? p["usage"]);
1390
+ return {
1391
+ type: "done",
1392
+ ...usage ? { usage } : {}
1393
+ };
1394
+ }
1203
1395
  default: return;
1204
1396
  }
1205
1397
  }
@@ -1248,6 +1440,45 @@ var FullDisclosureRouter = class {
1248
1440
  }
1249
1441
  };
1250
1442
  /**
1443
+ * 参与 `toolResultMaxBytes` 截断的分片类型(FR-23.3)。**判据的单一来源。**
1444
+ *
1445
+ * 白名单而不是黑名单:黑名单式下新加的分片默认「参与截断」,
1446
+ * 漏改一处就会把一份 PDF 截成坏文件、把 docx 文本腰斩,而且没有任何报错。
1447
+ * 白名单式下新分片默认**不参与**,要受文本预算约束必须显式加进来。
1448
+ * @experimental
1449
+ */
1450
+ const TEXT_BUDGETED_CONTENT_TYPES = /* @__PURE__ */ new Set(["text", "json"]);
1451
+ /**
1452
+ * `file` 分片的上限(FR-23.4),量的是**实际上线的 base64 长度**而不是解码后的字节——
1453
+ * provider 的限额算的是请求载荷。
1454
+ *
1455
+ * 2026-08-14 查证的三家上限:
1456
+ * | provider | 上限 | 出处 |
1457
+ * | --------- | --------------------------------------- | ----------------------- |
1458
+ * | Anthropic | **32 MB**(**整个请求载荷**)、600 页 | PDF support / 请求大小 |
1459
+ * | OpenAI | 单文件 50 MB,全部文件合计 50 MB | File inputs / 使用须知 |
1460
+ * | Gemini | 50 MB 或 1000 页(内联与 Files API 同) | 文档理解 / 技术详情 |
1461
+ *
1462
+ * 取最小值 Anthropic 的 32 MB。它是**整个请求**的额度,不是文档单独的额度,
1463
+ * 所以再留出余量给系统提示词、catalog(约 34 KB)、历史消息与其余分片。
1464
+ * 25 000 KB base64 ≈ 18.75 MB 原始 PDF。取 1024 的整数倍:设置界面按 KB 展示,
1465
+ * 十进制的 24 000 000 会显示成 23437.5 这种读不出来的数。
1466
+ */
1467
+ const DEFAULT_MAX_DOCUMENT_BYTES = 256e5;
1468
+ /**
1469
+ * `document-text` 分片的上限(FR-23.4)。
1470
+ *
1471
+ * 约束来自**上下文窗口**而不是请求大小:抽出来的文本要整段进上下文。
1472
+ * 按英文约 4 字节/token 折算,500 KB ≈ 128K token,占 200K 窗口的多半,
1473
+ * 给历史消息与模型的回答留下其余。
1474
+ */
1475
+ const DEFAULT_MAX_DOCUMENT_TEXT_BYTES = 512e3;
1476
+ /**
1477
+ * 单次 fetchData 结果的字节上限(分册 16,FR-16.6):1000 KB。
1478
+ * 结构化数据不是文档,量级差一个数量级;超出**只拒不截**——截断的 JSON 解不出来。
1479
+ */
1480
+ const DEFAULT_MAX_DATA_SOURCE_BYTES = 1024e3;
1481
+ /**
1251
1482
  * 工具名解析规则(单一实现,Agent 循环使用):
1252
1483
  * 1. `<skillName>__<scriptName>` 且前缀是已激活技能 → 本地脚本
1253
1484
  * 2. `endpoint:` 前缀 → TOOL_UNSUPPORTED(MCP 阶段实现)
@@ -1406,17 +1637,75 @@ const READ_SKILL_FILE_TOOL = {
1406
1637
  source: "builtin"
1407
1638
  };
1408
1639
  const ASK_USER_TOOL_NAME = "ask_user";
1640
+ /** 一次 `ask_user` 能收的字段数上限(FR-11.3 裁决值),可由 `AgentLoopConfig` 覆盖 */
1641
+ const ASK_USER_MAX_FIELDS = 20;
1642
+ /** `fields[].type`:`FormField['type']` 的八种。`multi-select` 不在其中——它的值是数组(FR-11.1b) */
1643
+ const ASK_USER_FIELD_TYPES = [
1644
+ "text",
1645
+ "number",
1646
+ "boolean",
1647
+ "select",
1648
+ "textarea",
1649
+ "file",
1650
+ "password",
1651
+ "date"
1652
+ ];
1409
1653
  const ASK_USER_INPUT_SCHEMA = {
1410
1654
  type: "object",
1411
1655
  properties: {
1412
1656
  question: {
1413
1657
  type: "string",
1414
- description: "The question to ask the user"
1658
+ description: "A single question. Use it only when one answer is genuinely all you need."
1659
+ },
1660
+ fields: {
1661
+ type: "array",
1662
+ maxItems: 20,
1663
+ description: "Collect several answers in one form. Use this whenever you need more than one piece of information, so the user fills everything in once instead of answering a chain of questions.",
1664
+ items: {
1665
+ type: "object",
1666
+ properties: {
1667
+ name: {
1668
+ type: "string",
1669
+ description: "Key this answer is returned under."
1670
+ },
1671
+ label: {
1672
+ type: "string",
1673
+ description: "Short label shown next to the input."
1674
+ },
1675
+ type: {
1676
+ type: "string",
1677
+ enum: [...ASK_USER_FIELD_TYPES],
1678
+ description: "Input kind. Use \"date\" for dates; the value comes back as a YYYY-MM-DD string."
1679
+ },
1680
+ required: { type: "boolean" },
1681
+ description: {
1682
+ type: "string",
1683
+ description: "Help text shown under the input."
1684
+ },
1685
+ options: {
1686
+ type: "array",
1687
+ items: {
1688
+ type: "object",
1689
+ properties: {
1690
+ label: { type: "string" },
1691
+ value: {}
1692
+ },
1693
+ required: ["label", "value"]
1694
+ },
1695
+ description: "Choices for a \"select\" field. Required when type is \"select\"."
1696
+ }
1697
+ },
1698
+ required: [
1699
+ "name",
1700
+ "label",
1701
+ "type"
1702
+ ]
1703
+ }
1415
1704
  },
1416
1705
  choices: {
1417
1706
  type: "array",
1418
1707
  items: { type: "string" },
1419
- description: "Closed set of acceptable answers. Provide it whenever the answer must be one of a known finite set, for example when asking which installed skill to use. The user then picks from a list instead of typing free text."
1708
+ description: "Closed set of acceptable answers for the single-question form. Provide it whenever the answer must be one of a known finite set, for example when asking which installed skill to use. The user then picks from a list instead of typing free text."
1420
1709
  },
1421
1710
  suggestion: {
1422
1711
  type: "string",
@@ -1426,20 +1715,19 @@ const ASK_USER_INPUT_SCHEMA = {
1426
1715
  type: "string",
1427
1716
  description: "Short reason for the suggestion, shown next to it so the user can judge whether to accept it."
1428
1717
  }
1429
- },
1430
- required: ["question"]
1718
+ }
1431
1719
  };
1432
1720
  /** 内建工具:LLM 信息不足时主动向用户提问;仅当配置了 UiBridge 时注册 */
1433
1721
  const ASK_USER_TOOL = {
1434
1722
  name: ASK_USER_TOOL_NAME,
1435
- description: "Ask the user a question when information is missing and continue with their answer. When the answer belongs to a known finite set, you must pass \"choices\".",
1723
+ description: "Ask the user for information you are missing. Pass \"fields\" to collect everything you need in a single form; do not ask one question per turn when several answers are needed. Pass \"question\" only when a single answer is all you need. When an answer belongs to a known finite set, you must pass \"choices\" (single question) or \"options\" (field).",
1436
1724
  inputSchema: ASK_USER_INPUT_SCHEMA,
1437
1725
  source: "builtin"
1438
1726
  };
1439
1727
  /**
1440
- * 越权 / 绝对路径 / 父级遍历的引用读取:策略拒绝(FS_PERMISSION_DENIED,
1728
+ * 越权 / 绝对路径 / 父级遍历的技能目录读取:策略拒绝(FS_PERMISSION_DENIED,
1441
1729
  * 计入 POLICY_DENIAL_CODES → 不计入隔离失败计数),
1442
- * 与「references/ 内路径确实不存在 → FS_NOT_FOUND」区分(0.9.0 分册 14,UX-04)。
1730
+ * 与「目录内路径确实不存在 → FS_NOT_FOUND」区分(0.9.0 分册 14,UX-04)。
1443
1731
  */
1444
1732
  function assertReferencePath(relativePath) {
1445
1733
  const trimmed = relativePath.trim();
@@ -1447,18 +1735,31 @@ function assertReferencePath(relativePath) {
1447
1735
  if (trimmed.replace(/\\/g, "/").split("/").includes("..")) throw new WebSkillError("FS_PERMISSION_DENIED", `Reference access denied: parent traversal (path: ${JSON.stringify(relativePath)})`);
1448
1736
  }
1449
1737
  /**
1450
- * 脚本执行上下文:只暴露 readReference / writeArtifact 两个显式能力,
1451
- * 不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
1738
+ * 越权判定必须在拼接前缀**之前**做:`/managed/...` 这类绝对路径
1739
+ * 若直接拼接会退化成目录内的相对路径 误报 FS_NOT_FOUND(UX-04)。
1740
+ */
1741
+ function resolveSkillDir(skillRoot, dir, relativePath) {
1742
+ assertReferencePath(relativePath);
1743
+ return resolveInsideRoot(skillRoot, `${dir}/${relativePath}`);
1744
+ }
1745
+ /**
1746
+ * 脚本执行上下文:只暴露 readReference / readAsset / readAssetBinary / writeArtifact
1747
+ * 四个显式能力,不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
1452
1748
  */
1453
1749
  function createScriptContext(deps) {
1454
- const { fs, artifactStore, skillName, skillRoot, runId, confirm, onWarning, onArtifactCreated } = deps;
1750
+ const { fs, artifactStore, skillName, skillRoot, runId, confirm, fetchData, onWarning, onArtifactCreated } = deps;
1455
1751
  const readFs = fs.withRoot?.(skillRoot) ?? fs;
1456
1752
  return {
1457
1753
  skillName,
1458
1754
  runId,
1459
1755
  async readReference(relativePath) {
1460
- assertReferencePath(relativePath);
1461
- return readFs.readText(resolveInsideRoot(skillRoot, `references/${relativePath}`));
1756
+ return readFs.readText(resolveSkillDir(skillRoot, "references", relativePath));
1757
+ },
1758
+ async readAsset(relativePath) {
1759
+ return readFs.readText(resolveSkillDir(skillRoot, "assets", relativePath));
1760
+ },
1761
+ async readAssetBinary(relativePath) {
1762
+ return readFs.readBinary(resolveSkillDir(skillRoot, "assets", relativePath));
1462
1763
  },
1463
1764
  async writeArtifact(path, content, options) {
1464
1765
  const artifact = typeof content === "string" ? await artifactStore.createTextArtifact({
@@ -1476,6 +1777,7 @@ function createScriptContext(deps) {
1476
1777
  return artifact;
1477
1778
  },
1478
1779
  ...confirm ? { confirm } : {},
1780
+ ...fetchData ? { fetchData } : {},
1479
1781
  ...onWarning ? { onWarning } : {}
1480
1782
  };
1481
1783
  }
@@ -1803,7 +2105,7 @@ const DEFAULT_USER_PROFILE_LIMITS = {
1803
2105
  recordLimit: 500,
1804
2106
  valueMaxChars: 2e3,
1805
2107
  sampleLimit: 80,
1806
- injectMaxBytes: 2e3,
2108
+ injectMaxBytes: 4e3,
1807
2109
  entryLimit: 40
1808
2110
  };
1809
2111
  /** @experimental */
@@ -1968,6 +2270,43 @@ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
1968
2270
  * 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
1969
2271
  */
1970
2272
  const PLAIN_CHAT_SYSTEM_PROMPT = "You are a helpful assistant. Answer the user's question directly and concisely. You have no tools available; do not describe or simulate tool calls.";
2273
+ const READ_LINKED_DOCUMENT_TOOL_NAME = "read_linked_document";
2274
+ /**
2275
+ * 能直接给模型用的 MIME。其余一律拒绝并列出这份清单——
2276
+ * 「尽力而为地猜格式」会让模型收到乱码却以为读成功了。
2277
+ */
2278
+ const SUPPORTED_DOCUMENT_MIME = {
2279
+ pdf: "application/pdf",
2280
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
2281
+ };
2282
+ /** 引擎侧固定流程(§2.3)用到的错误文案,集中一处便于判据引用 */
2283
+ const UNSUPPORTED_DOCUMENT_MESSAGE = `Only PDF (${SUPPORTED_DOCUMENT_MIME.pdf}), Word (${SUPPORTED_DOCUMENT_MIME.docx}) and plain text documents can be read.`;
2284
+ /**
2285
+ * 内建工具:读页面链接指向的文档。
2286
+ *
2287
+ * **始终注册**(只要宿主装配了 reader):docx 与纯文本这条路对所有模型成立。
2288
+ * PDF 目标在模型不支持文档时**取数后拒绝**,而不是入口就拦 ——
2289
+ * 入口拦会误伤 docx,它抽成文本后根本不需要文档能力。
2290
+ */
2291
+ const READ_LINKED_DOCUMENT_TOOL = {
2292
+ name: READ_LINKED_DOCUMENT_TOOL_NAME,
2293
+ description: "Read a document linked from the current page. Pass the exact \"href\" from a perceive_page result. Cross-origin documents need the user to confirm each time.",
2294
+ inputSchema: {
2295
+ type: "object",
2296
+ properties: { url: {
2297
+ type: "string",
2298
+ description: "The href of a link that appeared in a perceive_page result."
2299
+ } },
2300
+ required: ["url"]
2301
+ },
2302
+ source: "builtin"
2303
+ };
2304
+ /** 二进制转 base64;`btoa` 只吃 latin1,必须逐字节喂而不是先 decode 成字符串 */
2305
+ function toBase64(bytes) {
2306
+ let binary = "";
2307
+ for (const byte of bytes) binary += String.fromCharCode(byte);
2308
+ return btoa(binary);
2309
+ }
1971
2310
  /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
1972
2311
  var TraceRecorder = class {
1973
2312
  #runId;
@@ -2053,8 +2392,8 @@ function extractSkillCandidate(data) {
2053
2392
  */
2054
2393
  const DEFAULT_LOOP_LIMITS = {
2055
2394
  maxTurns: 10,
2056
- totalTimeoutMs: 12e4,
2057
- toolTimeoutMs: 3e4
2395
+ totalTimeoutMs: 36e5,
2396
+ toolTimeoutMs: 6e5
2058
2397
  };
2059
2398
  const RUN_SNAPSHOT_SCHEMA_VERSION = 3;
2060
2399
  /** @experimental */
@@ -2199,6 +2538,16 @@ function evaluateToolAccess(state, llmToolName) {
2199
2538
  };
2200
2539
  }
2201
2540
  const MAX_SURFACE_PATCHES_PER_SECOND = 240;
2541
+ /**
2542
+ * 一轮 LLM 调用可能抛出、且**值得原样上报**的结构化码。
2543
+ * 不在表里的(含未知异常)统一归 `LLM_REQUEST_FAILED`——那是「这次请求没成」的兜底,
2544
+ * 但把 schema 不兼容这类可定位的原因也压进去,排障就只剩看文案(UI-UX8 D4)。
2545
+ */
2546
+ const LLM_TURN_CODES = /* @__PURE__ */ new Set([
2547
+ "LLM_UNAVAILABLE",
2548
+ "LLM_REQUEST_FAILED",
2549
+ "TOOL_SCHEMA_UNAVAILABLE"
2550
+ ]);
2202
2551
  /** 交互终态(取消/超时):从工具执行深处直接终止 run */
2203
2552
  var RunTerminated = class extends Error {
2204
2553
  outcome;
@@ -2294,12 +2643,16 @@ var AgentLoop = class {
2294
2643
  maxTurns: config.maxTurns ?? DEFAULT_LOOP_LIMITS.maxTurns,
2295
2644
  totalTimeoutMs: config.totalTimeoutMs ?? DEFAULT_LOOP_LIMITS.totalTimeoutMs,
2296
2645
  toolTimeoutMs: config.toolTimeoutMs ?? DEFAULT_LOOP_LIMITS.toolTimeoutMs,
2297
- toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
2646
+ toolResultMaxBytes: config.toolResultMaxBytes ?? 256e3,
2298
2647
  paramHistoryLimit: config.paramHistoryLimit ?? 50,
2299
2648
  toolCallingDisabled: config.toolCallingDisabled ?? false,
2300
2649
  maxUnknownToolRetries: config.maxUnknownToolRetries ?? 2,
2650
+ askUserMaxFields: config.askUserMaxFields ?? 20,
2651
+ maxDocumentBytes: config.maxDocumentBytes ?? 256e5,
2652
+ maxDocumentTextBytes: config.maxDocumentTextBytes ?? 512e3,
2301
2653
  temperature: config.temperature,
2302
- renderResult: config.renderResult
2654
+ renderResult: config.renderResult,
2655
+ remoteUrl: config.remoteUrl
2303
2656
  };
2304
2657
  this.#policy = {
2305
2658
  missingParams: deps.interaction?.missingParams ?? "user",
@@ -2349,6 +2702,7 @@ var AgentLoop = class {
2349
2702
  now,
2350
2703
  interactionSeq: 0,
2351
2704
  surfaceActionSeq: 0,
2705
+ documentSeq: 0,
2352
2706
  messages: [],
2353
2707
  turn: 0,
2354
2708
  renderBlocks: [],
@@ -2357,6 +2711,7 @@ var AgentLoop = class {
2357
2711
  surfacePatchCount: 0,
2358
2712
  processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
2359
2713
  emittedToolEvents: /* @__PURE__ */ new Set(),
2714
+ artifactPaths: /* @__PURE__ */ new Set(),
2360
2715
  unknownToolCalls: 0,
2361
2716
  startMs,
2362
2717
  pausedMs: 0,
@@ -2477,6 +2832,7 @@ var AgentLoop = class {
2477
2832
  const toolSpecs = this.#config.toolCallingDisabled ? [] : [
2478
2833
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
2479
2834
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
2835
+ ...this.#deps.linkedDocuments ? [toLlmToolSpec(READ_LINKED_DOCUMENT_TOOL)] : [],
2480
2836
  ...skillToolSpecs
2481
2837
  ];
2482
2838
  trace.record("llm.request", { data: {
@@ -2504,8 +2860,26 @@ var AgentLoop = class {
2504
2860
  if (this.#cancelled.has(state.runId)) return finish("cancelled", "user-cancelled", "Run cancelled by user", "RUN_CANCELLED");
2505
2861
  return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
2506
2862
  }
2507
- const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
2508
- return finish("failed", "llm-error", messageOf(e), code);
2863
+ const code = e instanceof WebSkillError && LLM_TURN_CODES.has(e.code) ? e.code : "LLM_REQUEST_FAILED";
2864
+ const hint = messages.some((message) => Array.isArray(message.content) && message.content.some((part) => part.type === "file")) ? " This turn attached a document; the selected model may not accept document input. Try another model, or link a Word or text file instead." : "";
2865
+ return finish("failed", "llm-error", `${messageOf(e)}${hint}`, code);
2866
+ }
2867
+ if (response.thinking !== void 0 && response.thinking !== "") {
2868
+ state.trace.record("llm.thinking", { data: {
2869
+ turn,
2870
+ text: response.thinking
2871
+ } });
2872
+ const bus = this.#deps.eventBus;
2873
+ if (!llm.stream && bus && bus.listenerCount() > 0) bus.emit({
2874
+ phase: "execute",
2875
+ runId: state.runId,
2876
+ sessionId: state.run.sessionId,
2877
+ ts: state.now(),
2878
+ data: {
2879
+ kind: "thinking-delta",
2880
+ delta: response.thinking
2881
+ }
2882
+ });
2509
2883
  }
2510
2884
  const responseText = partsToText(response.content);
2511
2885
  if (!llm.stream && responseText !== "") await this.#deps.uiBridge?.onTextDelta?.(state.runId, responseText);
@@ -2514,6 +2888,11 @@ var AgentLoop = class {
2514
2888
  hasToolCalls: Boolean(response.toolCalls?.length),
2515
2889
  contentLength: responseText.length
2516
2890
  } });
2891
+ if (response.usage) trace.record("llm.usage", { data: {
2892
+ turn,
2893
+ inputTokens: response.usage.inputTokens,
2894
+ outputTokens: response.usage.outputTokens
2895
+ } });
2517
2896
  if (!response.toolCalls?.length) {
2518
2897
  messages.push({
2519
2898
  role: "assistant",
@@ -2533,6 +2912,7 @@ var AgentLoop = class {
2533
2912
  content: response.content ?? [],
2534
2913
  toolCalls: response.toolCalls
2535
2914
  });
2915
+ const carried = [];
2536
2916
  for (const call of response.toolCalls) {
2537
2917
  let result;
2538
2918
  try {
@@ -2541,11 +2921,7 @@ var AgentLoop = class {
2541
2921
  if (e instanceof RunTerminated) return finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
2542
2922
  throw e;
2543
2923
  }
2544
- messages.push({
2545
- role: "tool",
2546
- toolCallId: call.id,
2547
- content: await this.#toolResultParts(call, result, state)
2548
- });
2924
+ carried.push(...await this.#pushToolResult(state, call, result));
2549
2925
  try {
2550
2926
  await this.#drainSurfaceAction(state);
2551
2927
  } catch (e) {
@@ -2553,6 +2929,10 @@ var AgentLoop = class {
2553
2929
  throw e;
2554
2930
  }
2555
2931
  }
2932
+ if (carried.length > 0) messages.push({
2933
+ role: "user",
2934
+ content: carried
2935
+ });
2556
2936
  }
2557
2937
  } finally {
2558
2938
  this.#disarmDeadline(state);
@@ -2598,7 +2978,10 @@ var AgentLoop = class {
2598
2978
  try {
2599
2979
  await this.#lifecycle({
2600
2980
  phase: status === "completed" ? "complete" : "fail",
2601
- data: { reason }
2981
+ data: {
2982
+ reason,
2983
+ ...status === "failed" ? { detail: output } : {}
2984
+ }
2602
2985
  }, state);
2603
2986
  } catch (e) {
2604
2987
  trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
@@ -2629,7 +3012,7 @@ var AgentLoop = class {
2629
3012
  state.messages.push({
2630
3013
  role: "tool",
2631
3014
  toolCallId: call.id,
2632
- content: await this.#toolResultParts(call, result, state)
3015
+ content: (await this.#toolResultParts(call, result, state)).tool
2633
3016
  });
2634
3017
  state.trace.record("run.warning", {
2635
3018
  message: `Sealed unanswered tool call "${call.name}": the run ended before it produced a result.`,
@@ -2706,6 +3089,7 @@ var AgentLoop = class {
2706
3089
  },
2707
3090
  trace,
2708
3091
  reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
3092
+ documentSeq: 0,
2709
3093
  activated: new Set(snapshot.activeSkillNames),
2710
3094
  activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
2711
3095
  skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
@@ -2723,6 +3107,7 @@ var AgentLoop = class {
2723
3107
  surfacePatchCount: 0,
2724
3108
  processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
2725
3109
  emittedToolEvents: /* @__PURE__ */ new Set(),
3110
+ artifactPaths: /* @__PURE__ */ new Set(),
2726
3111
  unknownToolCalls: 0,
2727
3112
  startMs,
2728
3113
  pausedMs: snapshot.pausedMs ?? 0,
@@ -2741,6 +3126,7 @@ var AgentLoop = class {
2741
3126
  } });
2742
3127
  const pending = pendingInteraction;
2743
3128
  const pendingCall = this.#findPendingToolCall(state.messages);
3129
+ const carried = [];
2744
3130
  try {
2745
3131
  await this.#replaySurfaceEvents(state);
2746
3132
  if (pendingSurfaceAction) await this.#resumeSurfaceAction(state, pendingSurfaceAction);
@@ -2757,11 +3143,7 @@ var AgentLoop = class {
2757
3143
  ...pendingCall,
2758
3144
  arguments: args
2759
3145
  }, state);
2760
- state.messages.push({
2761
- role: "tool",
2762
- toolCallId: pendingCall.id,
2763
- content: await this.#toolResultParts(pendingCall, result, state)
2764
- });
3146
+ carried.push(...await this.#pushToolResult(state, pendingCall, result));
2765
3147
  await this.#drainSurfaceAction(state);
2766
3148
  } else if (pending?.type === "ask" && pendingCall) {
2767
3149
  const value = await this.#interact(state, pending, {
@@ -2779,32 +3161,24 @@ var AgentLoop = class {
2779
3161
  name: pendingCall.name,
2780
3162
  callId: pendingCall.id
2781
3163
  } });
2782
- state.messages.push({
2783
- role: "tool",
2784
- toolCallId: pendingCall.id,
2785
- content: await this.#toolResultParts(pendingCall, result, state)
2786
- });
3164
+ carried.push(...await this.#pushToolResult(state, pendingCall, result));
2787
3165
  await this.#drainSurfaceAction(state);
2788
3166
  } else if (pendingCall) {
2789
3167
  const result = await this.#executeCall(pendingCall, state);
2790
- state.messages.push({
2791
- role: "tool",
2792
- toolCallId: pendingCall.id,
2793
- content: await this.#toolResultParts(pendingCall, result, state)
2794
- });
3168
+ carried.push(...await this.#pushToolResult(state, pendingCall, result));
2795
3169
  await this.#drainSurfaceAction(state);
2796
3170
  }
2797
3171
  for (;;) {
2798
3172
  const next = this.#findPendingToolCall(state.messages);
2799
3173
  if (!next) break;
2800
3174
  const result = await this.#executeCall(next, state);
2801
- state.messages.push({
2802
- role: "tool",
2803
- toolCallId: next.id,
2804
- content: await this.#toolResultParts(next, result, state)
2805
- });
3175
+ carried.push(...await this.#pushToolResult(state, next, result));
2806
3176
  await this.#drainSurfaceAction(state);
2807
3177
  }
3178
+ if (carried.length > 0) state.messages.push({
3179
+ role: "user",
3180
+ content: carried
3181
+ });
2808
3182
  } catch (e) {
2809
3183
  if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
2810
3184
  throw e;
@@ -2829,7 +3203,7 @@ var AgentLoop = class {
2829
3203
  * 拿 `activated[0]` 顶替会把内置工具的失败栽给一个无关技能,比不归因更糟(设计 26 §2.3)。
2830
3204
  */
2831
3205
  #skillOf(call, state) {
2832
- if (call.name === "read_skill_file" || call.name === "ask_user") return void 0;
3206
+ if (call.name === "read_skill_file" || call.name === "ask_user" || call.name === "read_linked_document") return;
2833
3207
  const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
2834
3208
  return resolution.kind === "script" ? resolution.skillName : void 0;
2835
3209
  }
@@ -2843,6 +3217,7 @@ var AgentLoop = class {
2843
3217
  /**
2844
3218
  * 流式 LLM 调用:text-delta 仅在 EventBus 有订阅者时发射 llm.delta 事件,
2845
3219
  * 并同步转发 uiBridge.onTextDelta;tool-calls/done 组装为 LlmResponse 走原有分支。
3220
+ * thinking-delta 同理累积成 `response.thinking`(每轮一段,UI-UX5 #32)。
2846
3221
  */
2847
3222
  async #completeStreaming(llm, state, input) {
2848
3223
  const emitDelta = (delta) => {
@@ -2859,18 +3234,41 @@ var AgentLoop = class {
2859
3234
  });
2860
3235
  this.#deps.uiBridge?.onTextDelta?.(state.runId, delta);
2861
3236
  };
3237
+ const emitThinkingDelta = (delta) => {
3238
+ const bus = this.#deps.eventBus;
3239
+ if (bus && bus.listenerCount() > 0) bus.emit({
3240
+ phase: "execute",
3241
+ runId: state.runId,
3242
+ sessionId: state.run.sessionId,
3243
+ ts: state.now(),
3244
+ data: {
3245
+ kind: "thinking-delta",
3246
+ delta
3247
+ }
3248
+ });
3249
+ };
2862
3250
  let content = "";
3251
+ let thinking = "";
2863
3252
  let doneContent;
3253
+ let usage;
2864
3254
  const toolCalls = [];
2865
3255
  for await (const event of llm.stream(input)) if (event.type === "text-delta") {
2866
3256
  content += event.delta;
2867
3257
  emitDelta(event.delta);
3258
+ } else if (event.type === "thinking-delta") {
3259
+ thinking += event.delta;
3260
+ emitThinkingDelta(event.delta);
2868
3261
  } else if (event.type === "tool-calls") toolCalls.push(...event.toolCalls);
2869
- else if (event.type === "done") doneContent = event.content;
3262
+ else if (event.type === "done") {
3263
+ doneContent = event.content;
3264
+ usage = event.usage ?? usage;
3265
+ }
2870
3266
  const text = doneContent ?? content;
2871
3267
  return {
2872
3268
  content: text === "" ? void 0 : textParts(text),
2873
- toolCalls: toolCalls.length > 0 ? toolCalls : void 0
3269
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
3270
+ ...thinking === "" ? {} : { thinking },
3271
+ ...usage ? { usage } : {}
2874
3272
  };
2875
3273
  }
2876
3274
  /** 生命周期接线:更新 phase、发事件、跑钩子 */
@@ -3037,6 +3435,7 @@ var AgentLoop = class {
3037
3435
  if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
3038
3436
  else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
3039
3437
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
3438
+ else if (call.name === "read_linked_document" && this.#deps.linkedDocuments !== void 0) result = await this.#handleReadLinkedDocument(call, state);
3040
3439
  else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
3041
3440
  else {
3042
3441
  const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
@@ -3097,10 +3496,13 @@ var AgentLoop = class {
3097
3496
  });
3098
3497
  this.#emitTool(state, "failed", call, result.error?.code);
3099
3498
  }
3100
- for (const artifact of result.artifacts ?? []) state.trace.record("artifact.created", { data: {
3101
- artifactId: artifact.id,
3102
- path: artifact.path
3103
- } });
3499
+ for (const artifact of result.artifacts ?? []) {
3500
+ state.artifactPaths.add(artifact.path);
3501
+ state.trace.record("artifact.created", { data: {
3502
+ artifactId: artifact.id,
3503
+ path: artifact.path
3504
+ } });
3505
+ }
3104
3506
  if (state.unknownToolCalls > this.#config.maxUnknownToolRetries) throw new RunTerminated({
3105
3507
  status: "failed",
3106
3508
  reason: "tool-resolution-exhausted",
@@ -3116,7 +3518,12 @@ var AgentLoop = class {
3116
3518
  state.trace.record("run.warning", { message: "UiBridge does not support renderSurface; UI surface was not rendered" });
3117
3519
  return;
3118
3520
  }
3119
- const attributed = this.#attributeSurfaceEvent(state, event);
3521
+ let attributed = this.#attributeSurfaceEvent(state, event);
3522
+ if (attributed.type === "open") {
3523
+ const kept = await this.#stripDuplicateFileLinks(state, attributed);
3524
+ if (kept === void 0) return;
3525
+ attributed = kept;
3526
+ }
3120
3527
  if (attributed.type === "patch") this.#consumeSurfacePatchBudget(state);
3121
3528
  await bridge.renderSurface(attributed);
3122
3529
  state.surfaceEvents.push(structuredClone(attributed));
@@ -3159,6 +3566,44 @@ var AgentLoop = class {
3159
3566
  }))
3160
3567
  };
3161
3568
  }
3569
+ /**
3570
+ * 剥掉指向「本 run 已写出 artifact」的 FileLink 节点(UI-UX5 #5):
3571
+ * 宿主在 run 完成时会把每个 artifact 自动渲染成下载卡(`buildRenderResult`),
3572
+ * 模型/技能再建一张就是重复。整棵树剥空返回 undefined(放弃这次 open);
3573
+ * 有剥离就记一条 run.warning,让「少了一张卡」在轨迹里可查。
3574
+ */
3575
+ async #stripDuplicateFileLinks(state, event) {
3576
+ if (state.artifactPaths.size === 0) {
3577
+ const listed = await this.#deps.artifactStore.listArtifacts(state.runId).catch(() => []);
3578
+ for (const artifact of listed) state.artifactPaths.add(artifact.path);
3579
+ }
3580
+ if (state.artifactPaths.size === 0) return event;
3581
+ const paths = state.artifactPaths;
3582
+ let stripped = 0;
3583
+ const walk = (node) => {
3584
+ if (node.component === "FileLink") {
3585
+ const path = node.props?.["path"];
3586
+ if (typeof path === "string" && paths.has(path)) {
3587
+ stripped += 1;
3588
+ return;
3589
+ }
3590
+ }
3591
+ if (!node.children || node.children.length === 0) return node;
3592
+ const children = node.children.map(walk).filter((child) => child !== void 0);
3593
+ if (children.length === node.children.length) return node;
3594
+ return {
3595
+ ...node,
3596
+ children
3597
+ };
3598
+ };
3599
+ const node = walk(event.node);
3600
+ if (stripped === 0) return event;
3601
+ state.trace.record("run.warning", { message: `Dropped ${stripped} FileLink node(s) duplicating auto-rendered artifact download card(s)` });
3602
+ return node === void 0 ? void 0 : {
3603
+ ...event,
3604
+ node
3605
+ };
3606
+ }
3162
3607
  /** Awaits the single action emitted with the most recently persisted tool result. */
3163
3608
  async #drainSurfaceAction(state) {
3164
3609
  const request = state.pendingSurfaceAction;
@@ -3303,9 +3748,149 @@ var AgentLoop = class {
3303
3748
  }
3304
3749
  });
3305
3750
  }
3751
+ /**
3752
+ * 读取页面链接指向的文档(分册 22 §2.3 的六步)。
3753
+ *
3754
+ * 顺序不能调换:准入 → 同源判定 → 跨源确认 → 取数 → 格式分派 → 留痕。
3755
+ * 把确认放到取数之后,就等于「先下载了再问用户要不要下载」。
3756
+ */
3757
+ async #handleReadLinkedDocument(call, state) {
3758
+ const reader = this.#deps.linkedDocuments;
3759
+ const url = call.arguments.url;
3760
+ if (typeof url !== "string" || url.trim() === "") return toolError("VALIDATION_FAILED", "read_linked_document needs a \"url\" string.");
3761
+ const audit = async (data) => {
3762
+ state.trace.record("run.warning", { message: `read_linked_document: ${JSON.stringify(data)}` });
3763
+ await this.#deps.documentAudit?.append({
3764
+ type: "document.read",
3765
+ target: url,
3766
+ data
3767
+ });
3768
+ };
3769
+ let target;
3770
+ try {
3771
+ target = assertRemoteUrlAllowed(url, this.#config.remoteUrl ?? {});
3772
+ } catch (e) {
3773
+ const message = messageOf(e);
3774
+ await audit({
3775
+ ok: false,
3776
+ reason: message
3777
+ });
3778
+ return toolError("NETWORK_BLOCKED", message);
3779
+ }
3780
+ const sameOrigin = reader.origin !== void 0 && reader.origin === target.origin;
3781
+ if (!sameOrigin) {
3782
+ if (!await this.#confirm(`Read the linked document at ${target.href}?`, state)) {
3783
+ await audit({
3784
+ ok: false,
3785
+ crossOrigin: true,
3786
+ approved: false,
3787
+ reason: "declined"
3788
+ });
3789
+ return toolError("TOOL_DENIED", `Reading ${target.href} was declined by the user.`);
3790
+ }
3791
+ }
3792
+ let fetched;
3793
+ try {
3794
+ fetched = await reader.read(target.href);
3795
+ } catch (e) {
3796
+ const message = messageOf(e);
3797
+ await audit({
3798
+ ok: false,
3799
+ crossOrigin: !sameOrigin,
3800
+ approved: !sameOrigin,
3801
+ reason: message
3802
+ });
3803
+ return toolError("TOOL_EXECUTION_FAILED", message);
3804
+ }
3805
+ const mime = fetched.mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
3806
+ const name = target.pathname.split("/").pop() || "document";
3807
+ const id = `doc-${state.runId}-${++state.documentSeq}`;
3808
+ const record = {
3809
+ ok: true,
3810
+ crossOrigin: !sameOrigin,
3811
+ approved: !sameOrigin,
3812
+ bytes: fetched.bytes.length,
3813
+ mimeType: mime
3814
+ };
3815
+ if (mime === SUPPORTED_DOCUMENT_MIME.pdf) {
3816
+ const part = {
3817
+ type: "file",
3818
+ mimeType: mime,
3819
+ data: toBase64(fetched.bytes),
3820
+ name,
3821
+ id
3822
+ };
3823
+ const tooLarge = this.#documentTooLarge(part);
3824
+ if (tooLarge !== void 0) {
3825
+ await audit({
3826
+ ...record,
3827
+ ok: false,
3828
+ reason: tooLarge
3829
+ });
3830
+ return toolError("TOOL_RESULT_TOO_LARGE", tooLarge);
3831
+ }
3832
+ await audit(record);
3833
+ return {
3834
+ ok: true,
3835
+ content: [part]
3836
+ };
3837
+ }
3838
+ let text;
3839
+ if (mime === SUPPORTED_DOCUMENT_MIME.docx) {
3840
+ if (this.#deps.docxExtractor === void 0) {
3841
+ await audit({
3842
+ ...record,
3843
+ ok: false,
3844
+ reason: "no docx extractor"
3845
+ });
3846
+ return toolError("TOOL_UNSUPPORTED", "Word documents cannot be read in this environment: no docx text extractor is configured.");
3847
+ }
3848
+ try {
3849
+ text = await this.#deps.docxExtractor(fetched.bytes);
3850
+ } catch (e) {
3851
+ const message = messageOf(e);
3852
+ await audit({
3853
+ ...record,
3854
+ ok: false,
3855
+ reason: message
3856
+ });
3857
+ return toolError("TOOL_EXECUTION_FAILED", message);
3858
+ }
3859
+ } else if (mime.startsWith("text/")) text = new TextDecoder().decode(fetched.bytes);
3860
+ else {
3861
+ await audit({
3862
+ ...record,
3863
+ ok: false,
3864
+ reason: `unsupported mime ${mime}`
3865
+ });
3866
+ return toolError("TOOL_UNSUPPORTED", `${UNSUPPORTED_DOCUMENT_MESSAGE} This link served "${mime}".`);
3867
+ }
3868
+ const part = {
3869
+ type: "document-text",
3870
+ text,
3871
+ name,
3872
+ id
3873
+ };
3874
+ const tooLarge = this.#documentTooLarge(part);
3875
+ if (tooLarge !== void 0) {
3876
+ await audit({
3877
+ ...record,
3878
+ ok: false,
3879
+ reason: tooLarge
3880
+ });
3881
+ return toolError("TOOL_RESULT_TOO_LARGE", tooLarge);
3882
+ }
3883
+ await audit(record);
3884
+ return {
3885
+ ok: true,
3886
+ content: [part]
3887
+ };
3888
+ }
3306
3889
  async #handleAskUser(call, state) {
3890
+ const rawFields = call.arguments["fields"];
3891
+ if (Array.isArray(rawFields) && rawFields.length > 0) return this.#handleAskUserFields(call, state, rawFields);
3307
3892
  const question = call.arguments["question"];
3308
- if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires a non-empty \"question\" string argument");
3893
+ if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires either a non-empty \"question\" string or a non-empty \"fields\" array");
3309
3894
  const rawChoices = call.arguments["choices"];
3310
3895
  const choices = Array.isArray(rawChoices) ? rawChoices.filter((choice) => typeof choice === "string" && choice !== "") : [];
3311
3896
  const id = this.#nextInteractionId(state);
@@ -3344,6 +3929,60 @@ var AgentLoop = class {
3344
3929
  throw e;
3345
3930
  }
3346
3931
  }
3932
+ /**
3933
+ * 多字段 `ask_user`(FR-11.1)。构造的是与「技能缺参」同一个 `{ type: 'form' }`
3934
+ * 交互,因此渲染、回传、快照都走既有那一条路径——不允许出现第二套表单实现。
3935
+ */
3936
+ async #handleAskUserFields(call, state, rawFields) {
3937
+ const limit = this.#config.askUserMaxFields;
3938
+ if (rawFields.length > limit) return toolError("TOOL_EXECUTION_FAILED", `ask_user accepts at most ${limit} fields, received ${rawFields.length}. Split the collection into several steps, or group related fields and ask for the rest afterwards.`);
3939
+ const fields = [];
3940
+ for (const [index, raw] of rawFields.entries()) {
3941
+ const field = raw;
3942
+ const at = `fields[${index}]`;
3943
+ if (typeof field !== "object" || field === null) return toolError("VALIDATION_FAILED", `ask_user ${at} must be an object`);
3944
+ const name = field["name"];
3945
+ const label = field["label"];
3946
+ const type = field["type"];
3947
+ if (typeof name !== "string" || name === "") return toolError("VALIDATION_FAILED", `ask_user ${at} requires a non-empty "name"`);
3948
+ if (typeof label !== "string" || label === "") return toolError("VALIDATION_FAILED", `ask_user ${at} requires a non-empty "label"`);
3949
+ if (type === "multi-select") return toolError("VALIDATION_FAILED", `ask_user ${at} does not support "multi-select". Use render_ui with a MultiSelect field instead.`);
3950
+ if (typeof type !== "string" || !ASK_USER_FIELD_TYPES.includes(type)) return toolError("VALIDATION_FAILED", `ask_user ${at} has unsupported type "${String(type)}". Supported types: ${ASK_USER_FIELD_TYPES.join(", ")}.`);
3951
+ const options = field["options"];
3952
+ if (type === "select" && !Array.isArray(options)) return toolError("VALIDATION_FAILED", `ask_user ${at} is a select and requires "options"`);
3953
+ fields.push({
3954
+ name,
3955
+ label,
3956
+ type,
3957
+ ...field["required"] === true ? { required: true } : {},
3958
+ ...typeof field["description"] === "string" ? { description: field["description"] } : {},
3959
+ ...Array.isArray(options) ? { options: options.filter((o) => typeof o === "object" && o !== null).map((o) => ({
3960
+ label: String(o["label"] ?? o["value"]),
3961
+ value: o["value"]
3962
+ })) } : {}
3963
+ });
3964
+ }
3965
+ const question = call.arguments["question"];
3966
+ if (typeof question === "string" && question !== "") state.trace.record("run.warning", { message: "ask_user received both \"question\" and \"fields\"; the form was used and the single question was ignored." });
3967
+ try {
3968
+ const value = await this.#interact(state, {
3969
+ type: "form",
3970
+ id: this.#nextInteractionId(state),
3971
+ ...typeof question === "string" && question !== "" ? { title: question } : {},
3972
+ fields
3973
+ }, { tool: call.name });
3974
+ return {
3975
+ ok: true,
3976
+ content: [{
3977
+ type: "text",
3978
+ text: typeof value === "string" ? value : JSON.stringify(value ?? {})
3979
+ }]
3980
+ };
3981
+ } catch (e) {
3982
+ if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `ask_user failed: ${e.message}`);
3983
+ throw e;
3984
+ }
3985
+ }
3347
3986
  async #handleReadSkillFile(call, state) {
3348
3987
  const skillName = call.arguments["skillName"];
3349
3988
  if (typeof skillName !== "string" || skillName === "") return toolError("TOOL_EXECUTION_FAILED", "read_skill_file requires a non-empty \"skillName\" string argument");
@@ -3417,27 +4056,89 @@ var AgentLoop = class {
3417
4056
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
3418
4057
  }
3419
4058
  /**
3420
- * 工具结果 → tool 消息内容。image 分片单独提出,不进 JSON 也不参与截断计算(0.6.0 §1.1):
3421
- * 100 KB 的文本预算遇到一张 200 KB 的图会把整个结果截成垃圾。
4059
+ * 超预算的文档分片换成一条说明(FR-23.4)。**不截断**:
4060
+ * 半份 PDF 是坏文件,半份抽取文本会让模型以为自己读全了。
4061
+ */
4062
+ #documentTooLarge(part) {
4063
+ if (part.type !== "file" && part.type !== "document-text") return void 0;
4064
+ const isFile = part.type === "file";
4065
+ const size = isFile ? part.data.length : new TextEncoder().encode(part.text).length;
4066
+ const max = isFile ? this.#config.maxDocumentBytes : this.#config.maxDocumentTextBytes;
4067
+ if (size <= max) return void 0;
4068
+ return `The ${isFile ? "document" : "extracted document text"}${part.name ? ` "${part.name}"` : ""} was not attached: it is ${size} bytes, over the ${max} byte limit. It was not truncated, because a partial document would be unusable. Ask for a smaller file, a specific page range, or a summary produced elsewhere.`;
4069
+ }
4070
+ /** 推一条 tool 消息,返回需要另投 user 消息的二进制分片 */
4071
+ async #pushToolResult(state, call, result) {
4072
+ const split = await this.#toolResultParts(call, result, state);
4073
+ state.messages.push({
4074
+ role: "tool",
4075
+ toolCallId: call.id,
4076
+ content: split.tool
4077
+ });
4078
+ return split.carried;
4079
+ }
4080
+ /**
4081
+ * 工具结果 → tool 消息内容 + 另投的二进制分片。只有白名单内的分片进 JSON 并参与截断计算(FR-23.3):
4082
+ * 100 KB 的文本预算遇到一张 200 KB 的图会把整个结果截成垃圾,
4083
+ * 一份被截断的 PDF 则直接是坏文件。
4084
+ *
4085
+ * `carried` 单独返回而不是并进 tool 消息:OpenAI 协议的 tool 消息只有 `content: string`,
4086
+ * Google 的 `functionResponse`、Anthropic 的 `tool_result` 同样只收文本——
4087
+ * 三家客户端都会当场抛 `VALIDATION_FAILED`。二进制只能走 user 消息。
3422
4088
  */
3423
4089
  async #toolResultParts(call, result, state) {
3424
- const images = result.content.filter((part) => part.type === "image");
3425
- if (images.length === 0) return textParts(await this.#serializeToolResult(call, result, state));
4090
+ const budgeted = result.content.filter((part) => TEXT_BUDGETED_CONTENT_TYPES.has(part.type));
4091
+ const passthrough = result.content.filter((part) => !TEXT_BUDGETED_CONTENT_TYPES.has(part.type));
4092
+ if (passthrough.length === 0) return {
4093
+ tool: textParts(await this.#serializeToolResult(call, result, state)),
4094
+ carried: []
4095
+ };
3426
4096
  const textual = {
3427
4097
  ...result,
3428
- content: result.content.filter((part) => part.type !== "image")
4098
+ content: budgeted
4099
+ };
4100
+ const notes = [];
4101
+ const carried = [];
4102
+ for (const part of passthrough) {
4103
+ const tooLarge = this.#documentTooLarge(part);
4104
+ if (tooLarge) {
4105
+ state.trace.record("run.warning", { message: tooLarge });
4106
+ notes.push(tooLarge);
4107
+ continue;
4108
+ }
4109
+ if (part.type === "image") carried.push({
4110
+ type: "image",
4111
+ mimeType: part.mimeType,
4112
+ data: part.data
4113
+ });
4114
+ else if (part.type === "file") carried.push({
4115
+ type: "file",
4116
+ mimeType: part.mimeType,
4117
+ data: part.data,
4118
+ ...part.name ? { name: part.name } : {}
4119
+ });
4120
+ else if (part.type === "document-text") notes.push(part.text);
4121
+ }
4122
+ return {
4123
+ tool: [
4124
+ {
4125
+ type: "text",
4126
+ text: await this.#serializeToolResult(call, textual, state)
4127
+ },
4128
+ ...notes.map((text) => ({
4129
+ type: "text",
4130
+ text
4131
+ })),
4132
+ ...carried.length > 0 ? [{
4133
+ type: "text",
4134
+ text: `The ${carried.length === 1 ? "attachment" : `${carried.length} attachments`} from "${call.name}" ${carried.length === 1 ? "is" : "are"} in the next message.`
4135
+ }] : []
4136
+ ],
4137
+ carried
3429
4138
  };
3430
- return [{
3431
- type: "text",
3432
- text: await this.#serializeToolResult(call, textual, state)
3433
- }, ...images.map((image) => ({
3434
- type: "image",
3435
- mimeType: image.mimeType,
3436
- data: image.data
3437
- }))];
3438
4139
  }
3439
4140
  /**
3440
- * 工具结果回喂序列化:超过 toolResultMaxBytes(默认 100KB)时头尾保留截断,
4141
+ * 工具结果回喂序列化:超过 toolResultMaxBytes(默认 256KB)时头尾保留截断,
3441
4142
  * 完整内容经 artifactStore 落 artifact,回喂摘要含 artifact id。
3442
4143
  */
3443
4144
  async #serializeToolResult(call, result, state) {
@@ -3675,6 +4376,7 @@ var AgentLoop = class {
3675
4376
  skillRoot: root,
3676
4377
  runId: state.runId,
3677
4378
  confirm: (message) => this.#confirm(message, state),
4379
+ ...this.#deps.fetchData ? { fetchData: this.#deps.fetchData } : {},
3678
4380
  onWarning: (message) => state.trace.record("run.warning", { message }),
3679
4381
  onArtifactCreated: (artifact) => createdIds.add(artifact.id)
3680
4382
  });
@@ -4091,7 +4793,11 @@ var WebSkillRuntime = class {
4091
4793
  snapshotStore: this.#deps.snapshotStore,
4092
4794
  skillStateGuard: this.#deps.skillStateGuard,
4093
4795
  skillIntegrityGuard: this.#deps.skillIntegrityGuard,
4094
- skillOutcomeReporter: this.#deps.skillOutcomeReporter
4796
+ skillOutcomeReporter: this.#deps.skillOutcomeReporter,
4797
+ fetchData: this.#deps.fetchData,
4798
+ linkedDocuments: this.#deps.linkedDocuments,
4799
+ docxExtractor: this.#deps.docxExtractor,
4800
+ documentAudit: this.#deps.documentAudit
4095
4801
  }, this.#deps.config);
4096
4802
  const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
4097
4803
  this.#loops.set(runId, loop);
@@ -4212,7 +4918,11 @@ var WebSkillRuntime = class {
4212
4918
  snapshotStore: store,
4213
4919
  skillStateGuard: this.#deps.skillStateGuard,
4214
4920
  skillIntegrityGuard: this.#deps.skillIntegrityGuard,
4215
- skillOutcomeReporter: this.#deps.skillOutcomeReporter
4921
+ skillOutcomeReporter: this.#deps.skillOutcomeReporter,
4922
+ fetchData: this.#deps.fetchData,
4923
+ linkedDocuments: this.#deps.linkedDocuments,
4924
+ docxExtractor: this.#deps.docxExtractor,
4925
+ documentAudit: this.#deps.documentAudit
4216
4926
  }, this.#deps.config);
4217
4927
  this.#loops.set(runId, loop);
4218
4928
  try {
@@ -4723,7 +5433,9 @@ function parseBridgeRequest(data) {
4723
5433
  const { kind, id } = data;
4724
5434
  if (!isNonEmptyString(id)) return void 0;
4725
5435
  switch (kind) {
4726
- case "readReference": return isNonEmptyString(data["path"]) ? {
5436
+ case "readReference":
5437
+ case "readAsset":
5438
+ case "readAssetBinary": return isNonEmptyString(data["path"]) ? {
4727
5439
  kind,
4728
5440
  id,
4729
5441
  path: data["path"]
@@ -4745,6 +5457,16 @@ function parseBridgeRequest(data) {
4745
5457
  id,
4746
5458
  message: data["message"]
4747
5459
  } : void 0;
5460
+ case "fetchData": {
5461
+ if (!isNonEmptyString(data["sourceId"])) return void 0;
5462
+ const params = data["params"];
5463
+ return {
5464
+ kind,
5465
+ id,
5466
+ sourceId: data["sourceId"],
5467
+ ...isRecord(params) ? { params } : {}
5468
+ };
5469
+ }
4748
5470
  default: return;
4749
5471
  }
4750
5472
  }
@@ -4930,6 +5652,38 @@ var CapabilityApproval = class CapabilityApproval {
4930
5652
  return "allowed";
4931
5653
  }
4932
5654
  };
5655
+ /**
5656
+ * 从 run 的 trace 聚合大模型用量(请求次数 + 输入/输出 token)。
5657
+ * 与 `summarizeToolCalls` 同型:消费方不再各自遍历 trace。
5658
+ * @stable
5659
+ */
5660
+ function summarizeRunUsage(trace) {
5661
+ let llmCalls = 0;
5662
+ let inputTokens = 0;
5663
+ let outputTokens = 0;
5664
+ let hasUsage = false;
5665
+ for (const event of trace) {
5666
+ if (event.type === "llm.request") {
5667
+ llmCalls += 1;
5668
+ continue;
5669
+ }
5670
+ if (event.type !== "llm.usage") continue;
5671
+ const input = event.data?.["inputTokens"];
5672
+ const output = event.data?.["outputTokens"];
5673
+ const hasInput = typeof input === "number" && Number.isFinite(input);
5674
+ const hasOutput = typeof output === "number" && Number.isFinite(output);
5675
+ if (hasInput) inputTokens += input;
5676
+ if (hasOutput) outputTokens += output;
5677
+ if (hasInput || hasOutput) hasUsage = true;
5678
+ }
5679
+ return {
5680
+ llmCalls,
5681
+ ...hasUsage ? {
5682
+ inputTokens,
5683
+ outputTokens
5684
+ } : {}
5685
+ };
5686
+ }
4933
5687
  const RUN_TRACE_SCHEMA_VERSION = 1;
4934
5688
  /** 终止原因:取最后一条 run.completed/cancelled/failed 事件的 data.reason */
4935
5689
  function extractEndReason(events) {
@@ -4944,6 +5698,7 @@ function extractEndReason(events) {
4944
5698
  function summarize(trace) {
4945
5699
  const endReason = extractEndReason(trace.events);
4946
5700
  const durationMs = trace.endedAt === void 0 ? void 0 : Date.parse(trace.endedAt) - Date.parse(trace.startedAt);
5701
+ const runUsage = summarizeRunUsage(trace.events);
4947
5702
  return {
4948
5703
  runId: trace.runId,
4949
5704
  startedAt: trace.startedAt,
@@ -4954,7 +5709,11 @@ function summarize(trace) {
4954
5709
  ...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
4955
5710
  ...trace.endedAt !== void 0 ? { endedAt: trace.endedAt } : {},
4956
5711
  ...endReason !== void 0 ? { endReason } : {},
4957
- ...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {}
5712
+ ...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {},
5713
+ ...runUsage.inputTokens !== void 0 ? { usage: {
5714
+ inputTokens: runUsage.inputTokens,
5715
+ outputTokens: runUsage.outputTokens ?? 0
5716
+ } } : {}
4958
5717
  };
4959
5718
  }
4960
5719
  function parseTraceFile(raw, path) {
@@ -5147,6 +5906,12 @@ function applyFilter(summaries, filter) {
5147
5906
  * @stable
5148
5907
  */
5149
5908
  function summarizeToolCalls(run) {
5909
+ const startedAtByCallId = /* @__PURE__ */ new Map();
5910
+ for (const event of run.trace) {
5911
+ if (event.type !== "tool.started") continue;
5912
+ const callId = event.data?.["callId"];
5913
+ if (typeof callId === "string" && !startedAtByCallId.has(callId)) startedAtByCallId.set(callId, event.ts);
5914
+ }
5150
5915
  const calls = [];
5151
5916
  for (const event of run.trace) {
5152
5917
  if (event.type !== "tool.completed" && event.type !== "tool.failed") continue;
@@ -5156,6 +5921,7 @@ function summarizeToolCalls(run) {
5156
5921
  const args = event.data?.["args"];
5157
5922
  const durationMs = event.data?.["durationMs"];
5158
5923
  const errorCode = event.data?.["code"];
5924
+ const startedAt = startedAtByCallId.get(callId);
5159
5925
  calls.push({
5160
5926
  callId,
5161
5927
  name,
@@ -5163,6 +5929,7 @@ function summarizeToolCalls(run) {
5163
5929
  ...typeof args === "string" ? { args } : {},
5164
5930
  ...typeof durationMs === "number" ? { durationMs } : {},
5165
5931
  ...typeof errorCode === "string" ? { errorCode } : {},
5932
+ ...startedAt !== void 0 ? { startedAt } : {},
5166
5933
  ...event.type === "tool.failed" && typeof event.message === "string" ? { errorMessage: event.message } : {}
5167
5934
  });
5168
5935
  }
@@ -5386,4 +6153,4 @@ var FsSessionStore = class {
5386
6153
  };
5387
6154
 
5388
6155
  //#endregion
5389
- export { fromVercelStreamPart as $, SerializingMemoryStore as A, bridgeError as B, ProgressiveRouter as C, sealToolCallPairs as Ct, RUN_SNAPSHOT_SCHEMA_VERSION as D, toVercelToolSpecs as Dt, READ_SKILL_FILE_TOOL_NAME as E, toRecordDigests as Et, USER_PROFILE_PROMPT_HEADER as F, exportUserProfile as G, createScriptContext as H, USER_PROFILE_REFINE_PROMPT as I, extractTodoTraceEvents as J, extractChartSpec as K, WebSkillRuntime as L, USER_PROFILE_EXPORT_VERSION as M, USER_PROFILE_KEY as N, RUN_TRACE_SCHEMA_VERSION as O, validateUiSpecEvent as Ot, USER_PROFILE_NO_INVENTION_RULE as P, fromVercelResult as Q, appendBehaviorRecords as R, OpenAiCompatibleClient as S, scriptToolName as St, READ_SKILL_FILE_TOOL as T, toLlmToolSpec as Tt, createWebSkillApi as U, buildRenderResult as V, diffUserProfile as W, findUnpairedToolCalls as X, extractUiSpecEvents as Y, formatSkillScriptManifest as Z, FsRunTraceStore as _, renderUserProfileContext as _t, AgentLoop as a, mergeProfileEntries as at, GoogleGenAiClient as b, schemaSourceLabel as bt, CapabilityApproval as c, normalizeErrorCode as ct, EMPTY_USER_PROFILE as d, parseBridgeRequest as dt, interruptedToolResult as et, EventBus as f, parseUserProfileExport as ft, FsRunSnapshotStore as g, refineUserProfile as gt, FsMemoryStore as h, readUserProfile as ht, ASK_USER_TOOL_NAME as i, mergeCatalogEntries as it, TraceRecorder as j, SESSION_SCHEMA_VERSION as k, validateUiSpecNode as kt, DEFAULT_LOOP_LIMITS as l, normalizeToolContent as lt, FsArtifactStore as m, readProfileEntries as mt, ASK_USER_INPUT_SCHEMA as n, isUnsupportedRunSnapshot as nt, AnthropicClient as o, networkPolicyLibSource as ot, FS_SESSION_PAGE_SIZE as p, readBehaviorRecords as pt, extractSkillCandidate as q, ASK_USER_TOOL as r, listSkillScripts as rt, BEHAVIOR_RECORDS_KEY as s, networkUrlHost as st, ALLOWED_TOOLS_EXCLUSION_REASON as t, isNetworkAllowed as tt, DEFAULT_USER_PROFILE_LIMITS as u, normalizeToolError as ut, FsSessionStore as v, resolveToolName as vt, READ_SKILL_FILE_INPUT_SCHEMA as w, summarizeToolCalls as wt, HookRunner as x, schemaToForm as xt, FullDisclosureRouter as y, sampleBehaviorRecords as yt, applyUserProfileImport as z };
6156
+ export { createWebSkillApi as $, READ_LINKED_DOCUMENT_TOOL_NAME as A, schemaSourceLabel as At, TraceRecorder as B, validateUiSpecNode as Bt, FsSessionStore as C, readBehaviorRecords as Ct, OpenAiCompatibleClient as D, renderUserProfileContext as Dt, HookRunner as E, refineUserProfile as Et, RUN_TRACE_SCHEMA_VERSION as F, summarizeToolCalls as Ft, USER_PROFILE_PROMPT_HEADER as G, USER_PROFILE_EXPORT_VERSION as H, SESSION_SCHEMA_VERSION as I, toLlmToolSpec as It, appendBehaviorRecords as J, USER_PROFILE_REFINE_PROMPT as K, SUPPORTED_DOCUMENT_MIME as L, toRecordDigests as Lt, READ_SKILL_FILE_TOOL as M, scriptToolName as Mt, READ_SKILL_FILE_TOOL_NAME as N, sealToolCallPairs as Nt, ProgressiveRouter as O, resolveToolName as Ot, RUN_SNAPSHOT_SCHEMA_VERSION as P, summarizeRunUsage as Pt, createScriptContext as Q, SerializingMemoryStore as R, toVercelToolSpecs as Rt, FsRunTraceStore as S, parseUserProfileExport as St, GoogleGenAiClient as T, readUserProfile as Tt, USER_PROFILE_KEY as U, UNSUPPORTED_DOCUMENT_MESSAGE as V, USER_PROFILE_NO_INVENTION_RULE as W, bridgeError as X, applyUserProfileImport as Y, buildRenderResult as Z, EventBus as _, networkUrlHost as _t, ASK_USER_TOOL as a, extractUiSpecEvents as at, FsMemoryStore as b, normalizeToolError as bt, AnthropicClient as c, fromVercelResult as ct, DEFAULT_LOOP_LIMITS as d, isNetworkAllowed as dt, diffUserProfile as et, DEFAULT_MAX_DATA_SOURCE_BYTES as f, isUnsupportedRunSnapshot as ft, EMPTY_USER_PROFILE as g, networkPolicyLibSource as gt, DEFAULT_USER_PROFILE_LIMITS as h, mergeProfileEntries as ht, ASK_USER_MAX_FIELDS as i, extractTodoTraceEvents as it, READ_SKILL_FILE_INPUT_SCHEMA as j, schemaToForm as jt, READ_LINKED_DOCUMENT_TOOL as k, sampleBehaviorRecords as kt, BEHAVIOR_RECORDS_KEY as l, fromVercelStreamPart as lt, DEFAULT_MAX_DOCUMENT_TEXT_BYTES as m, mergeCatalogEntries as mt, ASK_USER_FIELD_TYPES as n, extractChartSpec as nt, ASK_USER_TOOL_NAME as o, findUnpairedToolCalls as ot, DEFAULT_MAX_DOCUMENT_BYTES as p, listSkillScripts as pt, WebSkillRuntime as q, ASK_USER_INPUT_SCHEMA as r, extractSkillCandidate as rt, AgentLoop as s, formatSkillScriptManifest as st, ALLOWED_TOOLS_EXCLUSION_REASON as t, exportUserProfile as tt, CapabilityApproval as u, interruptedToolResult as ut, FS_SESSION_PAGE_SIZE as v, normalizeErrorCode as vt, FullDisclosureRouter as w, readProfileEntries as wt, FsRunSnapshotStore as x, parseBridgeRequest as xt, FsArtifactStore as y, normalizeToolContent as yt, TEXT_BUDGETED_CONTENT_TYPES as z, validateUiSpecEvent as zt };