@webskill/sdk 0.10.0 → 0.12.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 +3 -2
  2. package/dist/agent.js +3 -1102
  3. package/dist/browser.d.ts +258 -11
  4. package/dist/browser.js +763 -47
  5. package/dist/{catalogComponents-DTcYfpLQ-CcoOaz-Z.js → catalogComponents-BgAJN0p8-C3K8klJd.js} +508 -923
  6. package/dist/dist-DU9KDAuR.js +1468 -0
  7. package/dist/{dist-1OFC-zax.js → dist-DqcL6jKO.js} +668 -60
  8. package/dist/{dist-BViUeszk.js → dist-sdKFgERo.js} +956 -115
  9. package/dist/{echarts-DhNm2ene.js → echarts-De78wXqV.js} +599 -61
  10. package/dist/{eventTypes-s2uwAcLG-Go3l_dUe.js → eventTypes-FllCrX-Z-DNDeHWoG.js} +6 -2
  11. package/dist/governance.d.ts +7 -3
  12. package/dist/governance.js +1 -1
  13. package/dist/{index-B0QPLWPZ.d.ts → index-3fCHc1mQ.d.ts} +198 -11
  14. package/dist/{index-DtFdMKBX.d.ts → index-C9pzXLKy.d.ts} +366 -9
  15. package/dist/{index-BF4E1a9j.d.ts → index-DFhU1uks.d.ts} +286 -19
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +3 -3
  18. package/dist/mcp.d.ts +58 -7
  19. package/dist/mcp.js +124 -21
  20. package/dist/node.d.ts +3 -3
  21. package/dist/node.js +60 -4
  22. package/dist/{openUiLibrary-CIrV--Ad-B8zG_91e.js → openUiLibrary-BKXW7Iwx-DaymVubt.js} +3 -3
  23. package/dist/processSandboxEntry.js +8 -1
  24. package/dist/sandboxWorkerEntry.js +8 -1
  25. package/dist/{skillVersionStore-D-qHk9ZE-DBsYYCWn.d.ts → skillVersionStore-D-qHk9ZE-DheTIwAB.d.ts} +1 -1
  26. package/dist/testing.d.ts +1 -1
  27. package/dist/{types-CrRcT-LM-DZAp8sWv.d.ts → types-DLctJep_-B5G4uk2u.d.ts} +22 -5
  28. package/dist/ui-react.d.ts +13 -4
  29. package/dist/ui-react.js +93 -259
  30. package/dist/ui-vue.d.ts +1 -1
  31. package/dist/ui-vue.js +2 -2
  32. package/dist/ui.d.ts +4 -4
  33. package/dist/ui.js +3 -3
  34. package/dist/{webskillLitCatalog-BJrphK0y-SGmRXaaO.js → webskillLitCatalog-D_zCqeQF-C9lrvvMr.js} +135 -16
  35. 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
@@ -210,16 +210,89 @@ function uniqueName(base, taken) {
210
210
  while (taken.has(`${prefixed}_${index}`)) index += 1;
211
211
  return `${prefixed}_${index}`;
212
212
  }
213
+ function renameDefRefs(node, renames) {
214
+ if (Array.isArray(node)) return node.map((child) => renameDefRefs(child, renames));
215
+ if (!isRecord$7(node)) return node;
216
+ const out = {};
217
+ const prefix = `#/${DEFS_KEY}/`;
218
+ for (const [key, value] of Object.entries(node)) {
219
+ if (key === "$ref" && typeof value === "string" && value.startsWith(prefix)) {
220
+ const [head, ...tail] = value.slice(prefix.length).split("/");
221
+ const mapped = head === void 0 ? void 0 : renames.get(head);
222
+ out[key] = mapped === void 0 ? value : [`${prefix}${mapped}`, ...tail].join("/");
223
+ continue;
224
+ }
225
+ out[key] = renameDefRefs(value, renames);
226
+ }
227
+ return out;
228
+ }
229
+ /**
230
+ * 把非根位置的 `$defs` 搬到根。zod 每次 `toJSONSchema()` 各自产出 `$defs.__schemaN`,
231
+ * 嵌进大 schema 后其 `#/$defs/...` 从大根解析不到——提升即修复。重名时避让并改写引用。
232
+ */
233
+ function hoistNestedDefs(schema) {
234
+ const rootDefs = isRecord$7(schema[DEFS_KEY]) ? { ...schema[DEFS_KEY] } : {};
235
+ const taken = new Set(Object.keys(rootDefs));
236
+ let hoisted = false;
237
+ function visit(node, isRoot) {
238
+ if (Array.isArray(node)) return node.map((child) => visit(child, false));
239
+ if (!isRecord$7(node)) return node;
240
+ const nested = !isRoot && isRecord$7(node[DEFS_KEY]) ? node[DEFS_KEY] : void 0;
241
+ const body = {};
242
+ for (const [key, value] of Object.entries(node)) {
243
+ if (key === DEFS_KEY && nested !== void 0) continue;
244
+ if (key === DEFS_KEY) {
245
+ body[key] = value;
246
+ continue;
247
+ }
248
+ if (SCHEMA_MAP_KEYS.has(key) && isRecord$7(value)) {
249
+ const mapped = {};
250
+ for (const [name, child] of Object.entries(value)) mapped[name] = visit(child, false);
251
+ body[key] = mapped;
252
+ continue;
253
+ }
254
+ if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
255
+ body[key] = value.map((child) => visit(child, false));
256
+ continue;
257
+ }
258
+ if (SCHEMA_KEYS.has(key)) {
259
+ body[key] = visit(value, false);
260
+ continue;
261
+ }
262
+ body[key] = value;
263
+ }
264
+ if (nested === void 0) return body;
265
+ hoisted = true;
266
+ const renames = /* @__PURE__ */ new Map();
267
+ for (const name of Object.keys(nested)) {
268
+ const target = uniqueName(name, taken);
269
+ taken.add(target);
270
+ if (target !== name) renames.set(name, target);
271
+ }
272
+ for (const [name, def] of Object.entries(nested)) {
273
+ const moved = visit(def, false);
274
+ rootDefs[renames.get(name) ?? name] = renames.size > 0 ? renameDefRefs(moved, renames) : moved;
275
+ }
276
+ return renames.size > 0 ? renameDefRefs(body, renames) : body;
277
+ }
278
+ const out = visit(schema, true);
279
+ return hoisted ? {
280
+ ...out,
281
+ [DEFS_KEY]: rootDefs
282
+ } : schema;
283
+ }
213
284
  /**
214
285
  * 把工具 inputSchema 重写为严格解析器可接受的 JSON Schema 2020-12 形态:
215
- * 自引用的 schema resource 提到根 `$defs`,所有 `$ref: '#'` 改写为 `$ref: '#/$defs/<Node>'`。
286
+ * 非根 `$defs` 提到根,自引用的 schema resource 提到根 `$defs`,
287
+ * 所有 `$ref: '#'` 改写为 `$ref: '#/$defs/<Node>'`。
216
288
  *
217
- * 幂等:不含自引用的 schema 原样返回。
289
+ * 幂等:不含自引用也不含嵌套 `$defs` 的 schema 原样返回。
218
290
  *
219
291
  * 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop,
220
292
  * 否则 trace / eventBus 观测到的 schema 会与技能作者写的不一致。
221
293
  */
222
- function toPortableToolSchema(schema) {
294
+ function toPortableToolSchema(input) {
295
+ const schema = hoistNestedDefs(input);
223
296
  const roots = /* @__PURE__ */ new Map();
224
297
  collectSelfRefRoots(schema, [], [], roots);
225
298
  if (roots.size === 0) return schema;
@@ -382,6 +455,10 @@ const ANTHROPIC_SCHEMA_PROFILE = {
382
455
  /**
383
456
  * Google Gen AI 的 `FunctionDeclaration.parameters` 收到 `additionalProperties`
384
457
  * 直接回 400 `Unknown name "additionalProperties"`,且不解析 `$ref` / `$defs`。
458
+ *
459
+ * 本 profile 描述的是 **`parameters` 这一字段**的能力,不是 Google 的能力上限:
460
+ * 完整 JSON Schema(含 `$ref` / `$defs`)走另一个字段 `parametersJsonSchema`。
461
+ * 把 `supportsRefs: false` 读成「Google 不支持引用」是错的,分流见 `googleGenAiClient.toGenAiTools`。
385
462
  */
386
463
  const GOOGLE_SCHEMA_PROFILE = {
387
464
  id: "google",
@@ -445,6 +522,46 @@ function sanitizeForVendor(schema, profile, toolName) {
445
522
  if (!profile.supportsRefs && containsRef(schema)) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool "${toolName}" requires $ref support that provider "${profile.id}" does not accept`);
446
523
  return sanitizeNode(schema, profile);
447
524
  }
525
+ /**
526
+ * schema 能否走 profile 描述的受限通道。与 `sanitizeForVendor` 同判据,但不抛错——
527
+ * 调用方需要的是「选哪条通道」而不是「失败」。
528
+ */
529
+ function isExpressibleForVendor(schema, profile) {
530
+ return profile.supportsRefs || !containsRef(schema);
531
+ }
532
+ /**
533
+ * 放宽不可终止的递归环:Gemini 只接受「可选 / 可为 null / 可零长数组」的引用环。
534
+ * 对 `items` 直接是 `$ref` 的数组节点删去 `minItems`。
535
+ *
536
+ * **只作用于出站副本**:catalog 本体的校验判据不变。
537
+ */
538
+ function relaxRefLoops(schema) {
539
+ return relaxNode(schema);
540
+ }
541
+ function relaxNode(node) {
542
+ if (Array.isArray(node)) return node.map(relaxNode);
543
+ if (!isRecord$5(node)) return node;
544
+ const out = {};
545
+ for (const [key, value] of Object.entries(node)) {
546
+ if (key === "minItems" && isRecord$5(node["items"]) && typeof node["items"]["$ref"] === "string") continue;
547
+ if (SCHEMA_MAP_KEYS.has(key) && isRecord$5(value)) {
548
+ const mapped = {};
549
+ for (const [name, child] of Object.entries(value)) mapped[name] = relaxNode(child);
550
+ out[key] = mapped;
551
+ continue;
552
+ }
553
+ if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
554
+ out[key] = value.map(relaxNode);
555
+ continue;
556
+ }
557
+ if (SCHEMA_KEYS.has(key)) {
558
+ out[key] = relaxNode(value);
559
+ continue;
560
+ }
561
+ out[key] = value;
562
+ }
563
+ return out;
564
+ }
448
565
  function createSseFrameReader() {
449
566
  let buffer = "";
450
567
  let data = [];
@@ -1125,10 +1242,16 @@ function toGenAiContents(messages) {
1125
1242
  }
1126
1243
  if (msg.role === "assistant") {
1127
1244
  const parts = toGenAiParts(msg.content, "assistant");
1128
- for (const call of msg.toolCalls ?? []) parts.push({ functionCall: {
1129
- name: call.name,
1130
- args: call.arguments
1131
- } });
1245
+ for (const call of msg.toolCalls ?? []) {
1246
+ const signature = call.vendor?.["thoughtSignature"];
1247
+ parts.push({
1248
+ functionCall: {
1249
+ name: call.name,
1250
+ args: call.arguments
1251
+ },
1252
+ ...typeof signature === "string" ? { thoughtSignature: signature } : {}
1253
+ });
1254
+ }
1132
1255
  out.push({
1133
1256
  role: "model",
1134
1257
  parts: parts.length > 0 ? parts : [{ text: "" }]
@@ -1145,11 +1268,14 @@ function toGenAiContents(messages) {
1145
1268
  contents: out
1146
1269
  };
1147
1270
  }
1148
- const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => ({
1149
- name: tool.name,
1150
- ...tool.description ? { description: tool.description } : {},
1151
- parameters: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), GOOGLE_SCHEMA_PROFILE, tool.name)
1152
- })) }];
1271
+ const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => {
1272
+ const portable = toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema));
1273
+ return {
1274
+ name: tool.name,
1275
+ ...tool.description ? { description: tool.description } : {},
1276
+ ...isExpressibleForVendor(portable, GOOGLE_SCHEMA_PROFILE) ? { parameters: sanitizeForVendor(portable, GOOGLE_SCHEMA_PROFILE, tool.name) } : { parametersJsonSchema: relaxRefLoops(portable) }
1277
+ };
1278
+ }) }];
1153
1279
  /** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
1154
1280
  var GoogleGenAiClient = class {
1155
1281
  #config;
@@ -1207,11 +1333,15 @@ var GoogleGenAiClient = class {
1207
1333
  continue;
1208
1334
  }
1209
1335
  const call = part["functionCall"];
1210
- if (call) toolCalls.push({
1211
- id: `call-${toolCalls.length}`,
1212
- name: call.name ?? "",
1213
- arguments: call.args ?? {}
1214
- });
1336
+ if (call) {
1337
+ const signature = part["thoughtSignature"];
1338
+ toolCalls.push({
1339
+ id: `call-${toolCalls.length}`,
1340
+ name: call.name ?? "",
1341
+ arguments: call.args ?? {},
1342
+ ...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
1343
+ });
1344
+ }
1215
1345
  }
1216
1346
  };
1217
1347
  try {
@@ -1283,11 +1413,15 @@ var GoogleGenAiClient = class {
1283
1413
  const out = [];
1284
1414
  for (const part of parts) {
1285
1415
  const call = part["functionCall"];
1286
- if (call) out.push({
1287
- id: `call-${out.length}`,
1288
- name: call.name ?? "",
1289
- arguments: call.args ?? {}
1290
- });
1416
+ if (call) {
1417
+ const signature = part["thoughtSignature"];
1418
+ out.push({
1419
+ id: `call-${out.length}`,
1420
+ name: call.name ?? "",
1421
+ arguments: call.args ?? {},
1422
+ ...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
1423
+ });
1424
+ }
1291
1425
  }
1292
1426
  return out;
1293
1427
  }
@@ -1426,6 +1560,45 @@ var FullDisclosureRouter = class {
1426
1560
  }
1427
1561
  };
1428
1562
  /**
1563
+ * 参与 `toolResultMaxBytes` 截断的分片类型(FR-23.3)。**判据的单一来源。**
1564
+ *
1565
+ * 白名单而不是黑名单:黑名单式下新加的分片默认「参与截断」,
1566
+ * 漏改一处就会把一份 PDF 截成坏文件、把 docx 文本腰斩,而且没有任何报错。
1567
+ * 白名单式下新分片默认**不参与**,要受文本预算约束必须显式加进来。
1568
+ * @experimental
1569
+ */
1570
+ const TEXT_BUDGETED_CONTENT_TYPES = /* @__PURE__ */ new Set(["text", "json"]);
1571
+ /**
1572
+ * `file` 分片的上限(FR-23.4),量的是**实际上线的 base64 长度**而不是解码后的字节——
1573
+ * provider 的限额算的是请求载荷。
1574
+ *
1575
+ * 2026-08-14 查证的三家上限:
1576
+ * | provider | 上限 | 出处 |
1577
+ * | --------- | --------------------------------------- | ----------------------- |
1578
+ * | Anthropic | **32 MB**(**整个请求载荷**)、600 页 | PDF support / 请求大小 |
1579
+ * | OpenAI | 单文件 50 MB,全部文件合计 50 MB | File inputs / 使用须知 |
1580
+ * | Gemini | 50 MB 或 1000 页(内联与 Files API 同) | 文档理解 / 技术详情 |
1581
+ *
1582
+ * 取最小值 Anthropic 的 32 MB。它是**整个请求**的额度,不是文档单独的额度,
1583
+ * 所以再留出余量给系统提示词、catalog(约 34 KB)、历史消息与其余分片。
1584
+ * 25 000 KB base64 ≈ 18.75 MB 原始 PDF。取 1024 的整数倍:设置界面按 KB 展示,
1585
+ * 十进制的 24 000 000 会显示成 23437.5 这种读不出来的数。
1586
+ */
1587
+ const DEFAULT_MAX_DOCUMENT_BYTES = 256e5;
1588
+ /**
1589
+ * `document-text` 分片的上限(FR-23.4)。
1590
+ *
1591
+ * 约束来自**上下文窗口**而不是请求大小:抽出来的文本要整段进上下文。
1592
+ * 按英文约 4 字节/token 折算,500 KB ≈ 128K token,占 200K 窗口的多半,
1593
+ * 给历史消息与模型的回答留下其余。
1594
+ */
1595
+ const DEFAULT_MAX_DOCUMENT_TEXT_BYTES = 512e3;
1596
+ /**
1597
+ * 单次 fetchData 结果的字节上限(分册 16,FR-16.6):1000 KB。
1598
+ * 结构化数据不是文档,量级差一个数量级;超出**只拒不截**——截断的 JSON 解不出来。
1599
+ */
1600
+ const DEFAULT_MAX_DATA_SOURCE_BYTES = 1024e3;
1601
+ /**
1429
1602
  * 工具名解析规则(单一实现,Agent 循环使用):
1430
1603
  * 1. `<skillName>__<scriptName>` 且前缀是已激活技能 → 本地脚本
1431
1604
  * 2. `endpoint:` 前缀 → TOOL_UNSUPPORTED(MCP 阶段实现)
@@ -1584,17 +1757,76 @@ const READ_SKILL_FILE_TOOL = {
1584
1757
  source: "builtin"
1585
1758
  };
1586
1759
  const ASK_USER_TOOL_NAME = "ask_user";
1760
+ /** 一次 `ask_user` 能收的字段数上限(FR-11.3 裁决值),可由 `AgentLoopConfig` 覆盖 */
1761
+ const ASK_USER_MAX_FIELDS = 20;
1762
+ /** `fields[].type`:`FormField['type']` 的八种。`multi-select` 不在其中——它的值是数组(FR-11.1b) */
1763
+ const ASK_USER_FIELD_TYPES = [
1764
+ "text",
1765
+ "number",
1766
+ "boolean",
1767
+ "select",
1768
+ "textarea",
1769
+ "file",
1770
+ "password",
1771
+ "date"
1772
+ ];
1587
1773
  const ASK_USER_INPUT_SCHEMA = {
1588
1774
  type: "object",
1589
1775
  properties: {
1590
1776
  question: {
1591
1777
  type: "string",
1592
- description: "The question to ask the user"
1778
+ description: "A single question. Use it only when one answer is genuinely all you need."
1779
+ },
1780
+ fields: {
1781
+ type: "array",
1782
+ maxItems: 20,
1783
+ 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.",
1784
+ items: {
1785
+ type: "object",
1786
+ properties: {
1787
+ name: {
1788
+ type: "string",
1789
+ description: "Key this answer is returned under."
1790
+ },
1791
+ label: {
1792
+ type: "string",
1793
+ description: "Short label shown next to the input."
1794
+ },
1795
+ type: {
1796
+ type: "string",
1797
+ enum: [...ASK_USER_FIELD_TYPES],
1798
+ description: "Input kind. Use \"date\" for dates; the value comes back as a YYYY-MM-DD string."
1799
+ },
1800
+ required: { type: "boolean" },
1801
+ description: {
1802
+ type: "string",
1803
+ description: "Help text shown under the input."
1804
+ },
1805
+ defaultValue: { description: "A value the user already stated in this conversation; it is filled into the input for them. Only pass it when the user actually said it — do not guess." },
1806
+ options: {
1807
+ type: "array",
1808
+ items: {
1809
+ type: "object",
1810
+ properties: {
1811
+ label: { type: "string" },
1812
+ value: {}
1813
+ },
1814
+ required: ["label", "value"]
1815
+ },
1816
+ description: "Choices for a \"select\" field. Required when type is \"select\"."
1817
+ }
1818
+ },
1819
+ required: [
1820
+ "name",
1821
+ "label",
1822
+ "type"
1823
+ ]
1824
+ }
1593
1825
  },
1594
1826
  choices: {
1595
1827
  type: "array",
1596
1828
  items: { type: "string" },
1597
- 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."
1829
+ 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."
1598
1830
  },
1599
1831
  suggestion: {
1600
1832
  type: "string",
@@ -1604,20 +1836,91 @@ const ASK_USER_INPUT_SCHEMA = {
1604
1836
  type: "string",
1605
1837
  description: "Short reason for the suggestion, shown next to it so the user can judge whether to accept it."
1606
1838
  }
1607
- },
1608
- required: ["question"]
1839
+ }
1609
1840
  };
1610
1841
  /** 内建工具:LLM 信息不足时主动向用户提问;仅当配置了 UiBridge 时注册 */
1611
1842
  const ASK_USER_TOOL = {
1612
1843
  name: ASK_USER_TOOL_NAME,
1613
- 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\".",
1844
+ 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).",
1614
1845
  inputSchema: ASK_USER_INPUT_SCHEMA,
1615
1846
  source: "builtin"
1616
1847
  };
1617
1848
  /**
1618
- * 越权 / 绝对路径 / 父级遍历的引用读取:策略拒绝(FS_PERMISSION_DENIED,
1849
+ * 产物 metadata 的准入与判读(分册 18)。
1850
+ *
1851
+ * metadata 是能力桥上第一个**开放结构**的入参,且来自沙箱内的技能脚本。
1852
+ * 校验落在 `createScriptContext.writeArtifact`——它是 in-process 与三个沙箱执行器
1853
+ * 唯一的汇聚点,在此抛错才能带上正确的桥请求 id 回给脚本(DV-10)。
1854
+ */
1855
+ /** 序列化后的字节上限;`index.json` 是整覆盖原子写,无上限等于廉价的放大面 */
1856
+ const ARTIFACT_METADATA_MAX_BYTES = 4096;
1857
+ /** 原型污染键;JSON 解析产生的同名键是**自有键**,可被 getOwnPropertyNames 查出 */
1858
+ const FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
1859
+ "__proto__",
1860
+ "constructor",
1861
+ "prototype"
1862
+ ]);
1863
+ function isPlainObject$1(value) {
1864
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1865
+ const proto = Object.getPrototypeOf(value);
1866
+ return proto === Object.prototype || proto === null;
1867
+ }
1868
+ /** 函数声明而非箭头函数赋值:只有前者能让 TS 的控制流分析认到 never 终止 */
1869
+ function reject$1(message) {
1870
+ throw new WebSkillError("VALIDATION_FAILED", message);
1871
+ }
1872
+ /**
1873
+ * 递归拒绝原型污染键与不可序列化值。
1874
+ * `JSON.stringify` 会**静默丢弃**函数与 symbol,而静默正是本需求明令禁止的形态,
1875
+ * 所以必须在序列化之前显式查一遍。
1876
+ */
1877
+ function assertSerializableShape(value, path, seen) {
1878
+ const kind = typeof value;
1879
+ if (kind === "function" || kind === "symbol" || kind === "bigint") reject$1(`Artifact metadata value at "${path}" is not JSON-serializable (${kind})`);
1880
+ if (kind !== "object" || value === null) return;
1881
+ const container = value;
1882
+ if (seen.has(container)) reject$1(`Artifact metadata contains a circular reference at "${path}"`);
1883
+ seen.add(container);
1884
+ if (Array.isArray(value)) value.forEach((item, index) => assertSerializableShape(item, `${path}[${index}]`, seen));
1885
+ else {
1886
+ if (!isPlainObject$1(value)) reject$1(`Artifact metadata value at "${path}" must be a plain object or array`);
1887
+ for (const key of Object.getOwnPropertyNames(value)) {
1888
+ if (FORBIDDEN_KEYS.has(key)) reject$1(`Artifact metadata must not contain the reserved key "${key}" (at "${path}")`);
1889
+ assertSerializableShape(value[key], `${path}.${key}`, seen);
1890
+ }
1891
+ }
1892
+ seen.delete(container);
1893
+ }
1894
+ /**
1895
+ * 准入校验。通过后返回 `JSON.parse(JSON.stringify(...))` 的副本:
1896
+ * in-process 路径由此获得与 JSON stdio 沙箱路径完全相同的语义,
1897
+ * 而不是依赖某条传输链路碰巧做了序列化。
1898
+ */
1899
+ function admitArtifactMetadata(value) {
1900
+ if (!isPlainObject$1(value)) reject$1("Artifact metadata must be a plain object");
1901
+ assertSerializableShape(value, "metadata", /* @__PURE__ */ new Set());
1902
+ let json;
1903
+ try {
1904
+ json = JSON.stringify(value);
1905
+ } catch (error) {
1906
+ reject$1(`Artifact metadata is not JSON-serializable: ${error.message}`);
1907
+ }
1908
+ const bytes = new TextEncoder().encode(json).length;
1909
+ if (bytes > 4096) reject$1(`Artifact metadata is too large: ${bytes} bytes exceeds the ${ARTIFACT_METADATA_MAX_BYTES} byte limit`);
1910
+ return JSON.parse(json);
1911
+ }
1912
+ /**
1913
+ * 「这份产物不出自动下载卡」的判读。**严格 `=== false`**:
1914
+ * 隐式真值转换会让写错的键值静默生效,而隐藏是不可见的失败。
1915
+ * `buildRenderResult` 与 agentLoop 的去重集合共用本函数,两处判据不得分叉。
1916
+ */
1917
+ function isArtifactCardHidden(artifact) {
1918
+ return artifact.metadata?.["resultCard"] === false;
1919
+ }
1920
+ /**
1921
+ * 越权 / 绝对路径 / 父级遍历的技能目录读取:策略拒绝(FS_PERMISSION_DENIED,
1619
1922
  * 计入 POLICY_DENIAL_CODES → 不计入隔离失败计数),
1620
- * 与「references/ 内路径确实不存在 → FS_NOT_FOUND」区分(0.9.0 分册 14,UX-04)。
1923
+ * 与「目录内路径确实不存在 → FS_NOT_FOUND」区分(0.9.0 分册 14,UX-04)。
1621
1924
  */
1622
1925
  function assertReferencePath(relativePath) {
1623
1926
  const trimmed = relativePath.trim();
@@ -1625,35 +1928,52 @@ function assertReferencePath(relativePath) {
1625
1928
  if (trimmed.replace(/\\/g, "/").split("/").includes("..")) throw new WebSkillError("FS_PERMISSION_DENIED", `Reference access denied: parent traversal (path: ${JSON.stringify(relativePath)})`);
1626
1929
  }
1627
1930
  /**
1628
- * 脚本执行上下文:只暴露 readReference / writeArtifact 两个显式能力,
1629
- * 不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
1931
+ * 越权判定必须在拼接前缀**之前**做:`/managed/...` 这类绝对路径
1932
+ * 若直接拼接会退化成目录内的相对路径 误报 FS_NOT_FOUND(UX-04)。
1933
+ */
1934
+ function resolveSkillDir(skillRoot, dir, relativePath) {
1935
+ assertReferencePath(relativePath);
1936
+ return resolveInsideRoot(skillRoot, `${dir}/${relativePath}`);
1937
+ }
1938
+ /**
1939
+ * 脚本执行上下文:只暴露 readReference / readAsset / readAssetBinary / writeArtifact
1940
+ * 四个显式能力,不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
1630
1941
  */
1631
1942
  function createScriptContext(deps) {
1632
- const { fs, artifactStore, skillName, skillRoot, runId, confirm, onWarning, onArtifactCreated } = deps;
1943
+ const { fs, artifactStore, skillName, skillRoot, runId, confirm, fetchData, onWarning, onArtifactCreated } = deps;
1633
1944
  const readFs = fs.withRoot?.(skillRoot) ?? fs;
1634
1945
  return {
1635
1946
  skillName,
1636
1947
  runId,
1637
1948
  async readReference(relativePath) {
1638
- assertReferencePath(relativePath);
1639
- return readFs.readText(resolveInsideRoot(skillRoot, `references/${relativePath}`));
1949
+ return readFs.readText(resolveSkillDir(skillRoot, "references", relativePath));
1950
+ },
1951
+ async readAsset(relativePath) {
1952
+ return readFs.readText(resolveSkillDir(skillRoot, "assets", relativePath));
1953
+ },
1954
+ async readAssetBinary(relativePath) {
1955
+ return readFs.readBinary(resolveSkillDir(skillRoot, "assets", relativePath));
1640
1956
  },
1641
1957
  async writeArtifact(path, content, options) {
1958
+ const metadata = options?.metadata === void 0 ? void 0 : admitArtifactMetadata(options.metadata);
1642
1959
  const artifact = typeof content === "string" ? await artifactStore.createTextArtifact({
1643
1960
  runId,
1644
1961
  path,
1645
1962
  content,
1646
- mimeType: options?.mimeType
1963
+ mimeType: options?.mimeType,
1964
+ ...metadata === void 0 ? {} : { metadata }
1647
1965
  }) : await artifactStore.createBinaryArtifact({
1648
1966
  runId,
1649
1967
  path,
1650
1968
  content,
1651
- mimeType: options?.mimeType
1969
+ mimeType: options?.mimeType,
1970
+ ...metadata === void 0 ? {} : { metadata }
1652
1971
  });
1653
1972
  onArtifactCreated?.(artifact);
1654
1973
  return artifact;
1655
1974
  },
1656
1975
  ...confirm ? { confirm } : {},
1976
+ ...fetchData ? { fetchData } : {},
1657
1977
  ...onWarning ? { onWarning } : {}
1658
1978
  };
1659
1979
  }
@@ -1702,12 +2022,15 @@ function buildRenderResult(run, output, renderBlocks = []) {
1702
2022
  text: output
1703
2023
  });
1704
2024
  }
1705
- for (const artifact of run.artifacts) blocks.push({
1706
- type: "file",
1707
- path: artifact.path,
1708
- ...artifact.mimeType ? { mimeType: artifact.mimeType } : {},
1709
- size: artifact.size
1710
- });
2025
+ for (const artifact of run.artifacts) {
2026
+ if (isArtifactCardHidden(artifact)) continue;
2027
+ blocks.push({
2028
+ type: "file",
2029
+ path: artifact.path,
2030
+ ...artifact.mimeType ? { mimeType: artifact.mimeType } : {},
2031
+ size: artifact.size
2032
+ });
2033
+ }
1711
2034
  return {
1712
2035
  runId: run.id,
1713
2036
  summary: run.terminationReason,
@@ -2146,6 +2469,43 @@ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
2146
2469
  * 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
2147
2470
  */
2148
2471
  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.";
2472
+ const READ_LINKED_DOCUMENT_TOOL_NAME = "read_linked_document";
2473
+ /**
2474
+ * 能直接给模型用的 MIME。其余一律拒绝并列出这份清单——
2475
+ * 「尽力而为地猜格式」会让模型收到乱码却以为读成功了。
2476
+ */
2477
+ const SUPPORTED_DOCUMENT_MIME = {
2478
+ pdf: "application/pdf",
2479
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
2480
+ };
2481
+ /** 引擎侧固定流程(§2.3)用到的错误文案,集中一处便于判据引用 */
2482
+ const UNSUPPORTED_DOCUMENT_MESSAGE = `Only PDF (${SUPPORTED_DOCUMENT_MIME.pdf}), Word (${SUPPORTED_DOCUMENT_MIME.docx}) and plain text documents can be read.`;
2483
+ /**
2484
+ * 内建工具:读页面链接指向的文档。
2485
+ *
2486
+ * **始终注册**(只要宿主装配了 reader):docx 与纯文本这条路对所有模型成立。
2487
+ * PDF 目标在模型不支持文档时**取数后拒绝**,而不是入口就拦 ——
2488
+ * 入口拦会误伤 docx,它抽成文本后根本不需要文档能力。
2489
+ */
2490
+ const READ_LINKED_DOCUMENT_TOOL = {
2491
+ name: READ_LINKED_DOCUMENT_TOOL_NAME,
2492
+ 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.",
2493
+ inputSchema: {
2494
+ type: "object",
2495
+ properties: { url: {
2496
+ type: "string",
2497
+ description: "The href of a link that appeared in a perceive_page result."
2498
+ } },
2499
+ required: ["url"]
2500
+ },
2501
+ source: "builtin"
2502
+ };
2503
+ /** 二进制转 base64;`btoa` 只吃 latin1,必须逐字节喂而不是先 decode 成字符串 */
2504
+ function toBase64(bytes) {
2505
+ let binary = "";
2506
+ for (const byte of bytes) binary += String.fromCharCode(byte);
2507
+ return btoa(binary);
2508
+ }
2149
2509
  /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
2150
2510
  var TraceRecorder = class {
2151
2511
  #runId;
@@ -2230,10 +2590,18 @@ function extractSkillCandidate(data) {
2230
2590
  * @stable
2231
2591
  */
2232
2592
  const DEFAULT_LOOP_LIMITS = {
2233
- maxTurns: 10,
2593
+ maxTurns: 1e3,
2234
2594
  totalTimeoutMs: 36e5,
2235
- toolTimeoutMs: 6e5
2595
+ toolTimeoutMs: 6e5,
2596
+ maxHistoryMessages: 1e3
2236
2597
  };
2598
+ /**
2599
+ * 工具参数留存的单次体积上限(分册 30 / FR-30.2)。**全仓唯一来源**(S8)。
2600
+ *
2601
+ * 超限时不留半截参数,整条标记为 `truncated`。
2602
+ * @stable
2603
+ */
2604
+ const MAX_TOOL_STEP_ARG_BYTES = 32768;
2237
2605
  const RUN_SNAPSHOT_SCHEMA_VERSION = 3;
2238
2606
  /** @experimental */
2239
2607
  function isUnsupportedRunSnapshot(entry) {
@@ -2376,7 +2744,80 @@ function evaluateToolAccess(state, llmToolName) {
2376
2744
  reason: denialReason(state, canonical)
2377
2745
  };
2378
2746
  }
2747
+ /**
2748
+ * 参数敏感标注关键字(分册 30 / FR-30.3)。
2749
+ *
2750
+ * 不用 `format: 'password'`:`format` 是 JSON Schema 的规范关键字,provider 侧
2751
+ * 可能据其做转换或校验,而且它只对 `type: 'string'` 有意义,对象型凭据标不了。
2752
+ * `x-` 前缀走 `JsonSchema` 的索引签名,不与任何规范语义抢占。
2753
+ */
2754
+ const SENSITIVE_ANNOTATION = "x-webskill-sensitive";
2755
+ const isSensitive = (schema) => schema?.[SENSITIVE_ANNOTATION] === true;
2756
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2757
+ const join = (prefix, key) => prefix === "" ? key : `${prefix}.${key}`;
2758
+ /**
2759
+ * 按 schema 标注逐字段脱敏。
2760
+ *
2761
+ * 脱敏值取 `null` 而不是删键,也不是 `'***'`:删键会让生成器以为这个步骤
2762
+ * 本来就没有这个参数,从而写出漏参的技能;掩码则仍然泄露长度与存在性。
2763
+ */
2764
+ function redactAnnotated(value, schema, path, redacted) {
2765
+ if (isSensitive(schema)) {
2766
+ redacted.push(path);
2767
+ return null;
2768
+ }
2769
+ if (Array.isArray(value)) return value.map((item, i) => redactAnnotated(item, schema?.items, `${path}[${i}]`, redacted));
2770
+ if (isPlainObject(value)) {
2771
+ const out = {};
2772
+ for (const [key, entry] of Object.entries(value)) out[key] = redactAnnotated(entry, schema?.properties?.[key], join(path, key), redacted);
2773
+ return out;
2774
+ }
2775
+ return value;
2776
+ }
2777
+ /** 不可信来源:所有叶子脱敏,键与结构保留 */
2778
+ function redactAll(value, path, redacted) {
2779
+ if (Array.isArray(value)) return value.map((item, i) => redactAll(item, `${path}[${i}]`, redacted));
2780
+ if (isPlainObject(value)) {
2781
+ const out = {};
2782
+ for (const [key, entry] of Object.entries(value)) out[key] = redactAll(entry, join(path, key), redacted);
2783
+ return out;
2784
+ }
2785
+ redacted.push(path);
2786
+ return null;
2787
+ }
2788
+ /**
2789
+ * 分层脱敏(FR-30.3)——**全仓唯一实现**。
2790
+ *
2791
+ * 1. `untrusted` → 所有字段脱敏;
2792
+ * 2. `reviewed` / `host-trusted` → 逐字段走 schema,标注为敏感的脱敏。
2793
+ *
2794
+ * 标注为敏感的字段在**任何**层级都脱敏:信任声明说的是「未标注的可以留」,
2795
+ * 不是「标注了也能留」。
2796
+ */
2797
+ function redactToolStepArgs(args, schema, trust) {
2798
+ const redacted = [];
2799
+ const redactedArgs = trust.tier === "untrusted" ? redactAll(args, "", redacted) : redactAnnotated(args, schema, "", redacted);
2800
+ if (JSON.stringify(redactedArgs).length > 32768) return {
2801
+ args: {},
2802
+ redacted: [],
2803
+ truncated: true
2804
+ };
2805
+ return {
2806
+ args: redactedArgs,
2807
+ redacted
2808
+ };
2809
+ }
2379
2810
  const MAX_SURFACE_PATCHES_PER_SECOND = 240;
2811
+ /**
2812
+ * 一轮 LLM 调用可能抛出、且**值得原样上报**的结构化码。
2813
+ * 不在表里的(含未知异常)统一归 `LLM_REQUEST_FAILED`——那是「这次请求没成」的兜底,
2814
+ * 但把 schema 不兼容这类可定位的原因也压进去,排障就只剩看文案(UI-UX8 D4)。
2815
+ */
2816
+ const LLM_TURN_CODES = /* @__PURE__ */ new Set([
2817
+ "LLM_UNAVAILABLE",
2818
+ "LLM_REQUEST_FAILED",
2819
+ "TOOL_SCHEMA_UNAVAILABLE"
2820
+ ]);
2380
2821
  /** 交互终态(取消/超时):从工具执行深处直接终止 run */
2381
2822
  var RunTerminated = class extends Error {
2382
2823
  outcome;
@@ -2401,6 +2842,11 @@ const summarizeArgs = (args) => {
2401
2842
  const json = JSON.stringify(args);
2402
2843
  return json.length > 100 ? `${json.slice(0, 100)}…` : json;
2403
2844
  };
2845
+ /** 外部工具的 inputSchema 索引(分册 30);run 开始与 resume 各重建一次 */
2846
+ function indexExternalToolSchemas(state, specs) {
2847
+ state.externalToolSchemas.clear();
2848
+ for (const spec of specs) state.externalToolSchemas.set(spec.name, spec.inputSchema);
2849
+ }
2404
2850
  /**
2405
2851
  * file-pick 的回填值带整个文件的 base64,落进 `paramHistory` 会把用户选的文件
2406
2852
  * 原样长期留在记忆里(体积与隐私都不可接受)。只留可辨识的描述,正文不留。
@@ -2476,8 +2922,12 @@ var AgentLoop = class {
2476
2922
  paramHistoryLimit: config.paramHistoryLimit ?? 50,
2477
2923
  toolCallingDisabled: config.toolCallingDisabled ?? false,
2478
2924
  maxUnknownToolRetries: config.maxUnknownToolRetries ?? 2,
2925
+ askUserMaxFields: config.askUserMaxFields ?? 20,
2926
+ maxDocumentBytes: config.maxDocumentBytes ?? 256e5,
2927
+ maxDocumentTextBytes: config.maxDocumentTextBytes ?? 512e3,
2479
2928
  temperature: config.temperature,
2480
- renderResult: config.renderResult
2929
+ renderResult: config.renderResult,
2930
+ remoteUrl: config.remoteUrl
2481
2931
  };
2482
2932
  this.#policy = {
2483
2933
  missingParams: deps.interaction?.missingParams ?? "user",
@@ -2527,6 +2977,7 @@ var AgentLoop = class {
2527
2977
  now,
2528
2978
  interactionSeq: 0,
2529
2979
  surfaceActionSeq: 0,
2980
+ documentSeq: 0,
2530
2981
  messages: [],
2531
2982
  turn: 0,
2532
2983
  renderBlocks: [],
@@ -2541,6 +2992,7 @@ var AgentLoop = class {
2541
2992
  pausedMs: 0,
2542
2993
  maxTurns: this.#config.maxTurns,
2543
2994
  totalTimeoutMs: this.#config.totalTimeoutMs,
2995
+ externalToolSchemas: /* @__PURE__ */ new Map(),
2544
2996
  controller: new AbortController()
2545
2997
  };
2546
2998
  this.#controllers.set(runId, state.controller);
@@ -2560,14 +3012,8 @@ var AgentLoop = class {
2560
3012
  candidates: route.catalog.entries.map((e) => e.name)
2561
3013
  }
2562
3014
  }, state);
2563
- const externalSpecs = this.#config.toolCallingDisabled ? [] : (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
2564
- try {
2565
- return await source.listToolSpecs();
2566
- } catch (e) {
2567
- trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
2568
- return [];
2569
- }
2570
- }))).flat();
3015
+ const externalSpecs = this.#config.toolCallingDisabled ? [] : await this.#collectExternalSpecs(state);
3016
+ indexExternalToolSchemas(state, externalSpecs);
2571
3017
  const externalSystemPrompts = [];
2572
3018
  if (!this.#config.toolCallingDisabled) for (const source of this.#deps.externalTools ?? []) {
2573
3019
  if (source.systemPrompt === void 0) continue;
@@ -2656,6 +3102,7 @@ var AgentLoop = class {
2656
3102
  const toolSpecs = this.#config.toolCallingDisabled ? [] : [
2657
3103
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
2658
3104
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
3105
+ ...this.#deps.linkedDocuments ? [toLlmToolSpec(READ_LINKED_DOCUMENT_TOOL)] : [],
2659
3106
  ...skillToolSpecs
2660
3107
  ];
2661
3108
  trace.record("llm.request", { data: {
@@ -2683,8 +3130,9 @@ var AgentLoop = class {
2683
3130
  if (this.#cancelled.has(state.runId)) return finish("cancelled", "user-cancelled", "Run cancelled by user", "RUN_CANCELLED");
2684
3131
  return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
2685
3132
  }
2686
- const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
2687
- return finish("failed", "llm-error", messageOf(e), code);
3133
+ const code = e instanceof WebSkillError && LLM_TURN_CODES.has(e.code) ? e.code : "LLM_REQUEST_FAILED";
3134
+ 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." : "";
3135
+ return finish("failed", "llm-error", `${messageOf(e)}${hint}`, code);
2688
3136
  }
2689
3137
  if (response.thinking !== void 0 && response.thinking !== "") {
2690
3138
  state.trace.record("llm.thinking", { data: {
@@ -2734,6 +3182,7 @@ var AgentLoop = class {
2734
3182
  content: response.content ?? [],
2735
3183
  toolCalls: response.toolCalls
2736
3184
  });
3185
+ const carried = [];
2737
3186
  for (const call of response.toolCalls) {
2738
3187
  let result;
2739
3188
  try {
@@ -2742,11 +3191,7 @@ var AgentLoop = class {
2742
3191
  if (e instanceof RunTerminated) return finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
2743
3192
  throw e;
2744
3193
  }
2745
- messages.push({
2746
- role: "tool",
2747
- toolCallId: call.id,
2748
- content: await this.#toolResultParts(call, result, state)
2749
- });
3194
+ carried.push(...await this.#pushToolResult(state, call, result));
2750
3195
  try {
2751
3196
  await this.#drainSurfaceAction(state);
2752
3197
  } catch (e) {
@@ -2754,6 +3199,10 @@ var AgentLoop = class {
2754
3199
  throw e;
2755
3200
  }
2756
3201
  }
3202
+ if (carried.length > 0) messages.push({
3203
+ role: "user",
3204
+ content: carried
3205
+ });
2757
3206
  }
2758
3207
  } finally {
2759
3208
  this.#disarmDeadline(state);
@@ -2799,7 +3248,10 @@ var AgentLoop = class {
2799
3248
  try {
2800
3249
  await this.#lifecycle({
2801
3250
  phase: status === "completed" ? "complete" : "fail",
2802
- data: { reason }
3251
+ data: {
3252
+ reason,
3253
+ ...status === "failed" ? { detail: output } : {}
3254
+ }
2803
3255
  }, state);
2804
3256
  } catch (e) {
2805
3257
  trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
@@ -2830,7 +3282,7 @@ var AgentLoop = class {
2830
3282
  state.messages.push({
2831
3283
  role: "tool",
2832
3284
  toolCallId: call.id,
2833
- content: await this.#toolResultParts(call, result, state)
3285
+ content: (await this.#toolResultParts(call, result, state)).tool
2834
3286
  });
2835
3287
  state.trace.record("run.warning", {
2836
3288
  message: `Sealed unanswered tool call "${call.name}": the run ended before it produced a result.`,
@@ -2907,6 +3359,7 @@ var AgentLoop = class {
2907
3359
  },
2908
3360
  trace,
2909
3361
  reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
3362
+ documentSeq: 0,
2910
3363
  activated: new Set(snapshot.activeSkillNames),
2911
3364
  activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
2912
3365
  skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
@@ -2930,6 +3383,7 @@ var AgentLoop = class {
2930
3383
  pausedMs: snapshot.pausedMs ?? 0,
2931
3384
  maxTurns: snapshot.config.maxTurns,
2932
3385
  totalTimeoutMs: snapshot.config.totalTimeoutMs,
3386
+ externalToolSchemas: /* @__PURE__ */ new Map(),
2933
3387
  controller: new AbortController()
2934
3388
  };
2935
3389
  this.#controllers.set(runId, state.controller);
@@ -2943,6 +3397,9 @@ var AgentLoop = class {
2943
3397
  } });
2944
3398
  const pending = pendingInteraction;
2945
3399
  const pendingCall = this.#findPendingToolCall(state.messages);
3400
+ const externalSpecs = await this.#collectExternalSpecs(state);
3401
+ indexExternalToolSchemas(state, externalSpecs);
3402
+ const carried = [];
2946
3403
  try {
2947
3404
  await this.#replaySurfaceEvents(state);
2948
3405
  if (pendingSurfaceAction) await this.#resumeSurfaceAction(state, pendingSurfaceAction);
@@ -2959,11 +3416,7 @@ var AgentLoop = class {
2959
3416
  ...pendingCall,
2960
3417
  arguments: args
2961
3418
  }, state);
2962
- state.messages.push({
2963
- role: "tool",
2964
- toolCallId: pendingCall.id,
2965
- content: await this.#toolResultParts(pendingCall, result, state)
2966
- });
3419
+ carried.push(...await this.#pushToolResult(state, pendingCall, result));
2967
3420
  await this.#drainSurfaceAction(state);
2968
3421
  } else if (pending?.type === "ask" && pendingCall) {
2969
3422
  const value = await this.#interact(state, pending, {
@@ -2981,44 +3434,28 @@ var AgentLoop = class {
2981
3434
  name: pendingCall.name,
2982
3435
  callId: pendingCall.id
2983
3436
  } });
2984
- state.messages.push({
2985
- role: "tool",
2986
- toolCallId: pendingCall.id,
2987
- content: await this.#toolResultParts(pendingCall, result, state)
2988
- });
3437
+ carried.push(...await this.#pushToolResult(state, pendingCall, result));
2989
3438
  await this.#drainSurfaceAction(state);
2990
3439
  } else if (pendingCall) {
2991
3440
  const result = await this.#executeCall(pendingCall, state);
2992
- state.messages.push({
2993
- role: "tool",
2994
- toolCallId: pendingCall.id,
2995
- content: await this.#toolResultParts(pendingCall, result, state)
2996
- });
3441
+ carried.push(...await this.#pushToolResult(state, pendingCall, result));
2997
3442
  await this.#drainSurfaceAction(state);
2998
3443
  }
2999
3444
  for (;;) {
3000
3445
  const next = this.#findPendingToolCall(state.messages);
3001
3446
  if (!next) break;
3002
3447
  const result = await this.#executeCall(next, state);
3003
- state.messages.push({
3004
- role: "tool",
3005
- toolCallId: next.id,
3006
- content: await this.#toolResultParts(next, result, state)
3007
- });
3448
+ carried.push(...await this.#pushToolResult(state, next, result));
3008
3449
  await this.#drainSurfaceAction(state);
3009
3450
  }
3451
+ if (carried.length > 0) state.messages.push({
3452
+ role: "user",
3453
+ content: carried
3454
+ });
3010
3455
  } catch (e) {
3011
3456
  if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
3012
3457
  throw e;
3013
3458
  }
3014
- const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
3015
- try {
3016
- return await source.listToolSpecs();
3017
- } catch (e) {
3018
- trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
3019
- return [];
3020
- }
3021
- }))).flat();
3022
3459
  try {
3023
3460
  return await this.#turnLoop(state, snapshot.turn + 1, externalSpecs);
3024
3461
  } catch (e) {
@@ -3031,7 +3468,7 @@ var AgentLoop = class {
3031
3468
  * 拿 `activated[0]` 顶替会把内置工具的失败栽给一个无关技能,比不归因更糟(设计 26 §2.3)。
3032
3469
  */
3033
3470
  #skillOf(call, state) {
3034
- if (call.name === "read_skill_file" || call.name === "ask_user") return void 0;
3471
+ if (call.name === "read_skill_file" || call.name === "ask_user" || call.name === "read_linked_document") return;
3035
3472
  const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
3036
3473
  return resolution.kind === "script" ? resolution.skillName : void 0;
3037
3474
  }
@@ -3247,6 +3684,81 @@ var AgentLoop = class {
3247
3684
  });
3248
3685
  });
3249
3686
  }
3687
+ /** 外部工具 specs:单个来源失败跳过并记 warning(run 开始与 resume 共用) */
3688
+ async #collectExternalSpecs(state) {
3689
+ return (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
3690
+ try {
3691
+ return await source.listToolSpecs();
3692
+ } catch (e) {
3693
+ state.trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
3694
+ return [];
3695
+ }
3696
+ }))).flat();
3697
+ }
3698
+ /**
3699
+ * 一次工具调用的信任层级与 inputSchema(分册 30)。
3700
+ *
3701
+ * 技能脚本工具按 `untrusted`:它们的 schema 来自各技能的 frontmatter,
3702
+ * 是第三方内容,不是本仓审查过的。
3703
+ */
3704
+ #toolCaptureContext(call, state) {
3705
+ if (call.name === "read_skill_file") return {
3706
+ trust: { tier: "reviewed" },
3707
+ schema: READ_SKILL_FILE_TOOL.inputSchema,
3708
+ args: call.arguments
3709
+ };
3710
+ if (call.name === "ask_user") return {
3711
+ trust: { tier: "reviewed" },
3712
+ schema: ASK_USER_TOOL.inputSchema,
3713
+ args: call.arguments
3714
+ };
3715
+ if (call.name === "read_linked_document") return {
3716
+ trust: { tier: "reviewed" },
3717
+ schema: READ_LINKED_DOCUMENT_TOOL.inputSchema,
3718
+ args: call.arguments
3719
+ };
3720
+ const source = (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
3721
+ if (source === void 0) return {
3722
+ trust: { tier: "untrusted" },
3723
+ args: call.arguments
3724
+ };
3725
+ const schema = state.externalToolSchemas.get(call.name);
3726
+ const trust = source.argCaptureTrust?.(call.name, call.arguments) ?? { tier: "untrusted" };
3727
+ const args = source.captureArgs?.(call.name, call.arguments) ?? call.arguments;
3728
+ return schema === void 0 ? {
3729
+ trust,
3730
+ args
3731
+ } : {
3732
+ trust,
3733
+ schema,
3734
+ args
3735
+ };
3736
+ }
3737
+ /**
3738
+ * 成功调用的完整参数留存(FR-30.2)。失败的调用不留存:它什么也没做成,
3739
+ * 让模型引用它只会生成一个跑不通的步骤。
3740
+ */
3741
+ async #captureToolStep(call, state) {
3742
+ const store = this.#deps.toolSteps;
3743
+ if (store === void 0) return;
3744
+ const { trust, schema, args: captured } = this.#toolCaptureContext(call, state);
3745
+ const { args, redacted, truncated } = redactToolStepArgs(captured, schema, trust);
3746
+ try {
3747
+ await store.append({
3748
+ runId: state.runId,
3749
+ sessionId: state.run.sessionId,
3750
+ callId: call.id,
3751
+ tool: call.name,
3752
+ at: state.now(),
3753
+ args,
3754
+ redacted,
3755
+ ...truncated !== void 0 ? { truncated } : {},
3756
+ trust
3757
+ });
3758
+ } catch (e) {
3759
+ state.trace.record("run.warning", { message: `Failed to capture tool step arguments: ${messageOf(e)}` });
3760
+ }
3761
+ }
3250
3762
  async #executeCall(call, state) {
3251
3763
  const argsSummary = summarizeArgs(call.arguments);
3252
3764
  const owner = this.#skillOf(call, state);
@@ -3263,6 +3775,7 @@ var AgentLoop = class {
3263
3775
  if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
3264
3776
  else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
3265
3777
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
3778
+ else if (call.name === "read_linked_document" && this.#deps.linkedDocuments !== void 0) result = await this.#handleReadLinkedDocument(call, state);
3266
3779
  else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
3267
3780
  else {
3268
3781
  const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
@@ -3285,6 +3798,7 @@ var AgentLoop = class {
3285
3798
  durationMs,
3286
3799
  ...attribution
3287
3800
  } });
3801
+ await this.#captureToolStep(call, state);
3288
3802
  this.#emitTool(state, "completed", call);
3289
3803
  for (const item of result.content) {
3290
3804
  if (item.type !== "json") continue;
@@ -3324,7 +3838,7 @@ var AgentLoop = class {
3324
3838
  this.#emitTool(state, "failed", call, result.error?.code);
3325
3839
  }
3326
3840
  for (const artifact of result.artifacts ?? []) {
3327
- state.artifactPaths.add(artifact.path);
3841
+ if (!isArtifactCardHidden(artifact)) state.artifactPaths.add(artifact.path);
3328
3842
  state.trace.record("artifact.created", { data: {
3329
3843
  artifactId: artifact.id,
3330
3844
  path: artifact.path
@@ -3402,7 +3916,7 @@ var AgentLoop = class {
3402
3916
  async #stripDuplicateFileLinks(state, event) {
3403
3917
  if (state.artifactPaths.size === 0) {
3404
3918
  const listed = await this.#deps.artifactStore.listArtifacts(state.runId).catch(() => []);
3405
- for (const artifact of listed) state.artifactPaths.add(artifact.path);
3919
+ for (const artifact of listed) if (!isArtifactCardHidden(artifact)) state.artifactPaths.add(artifact.path);
3406
3920
  }
3407
3921
  if (state.artifactPaths.size === 0) return event;
3408
3922
  const paths = state.artifactPaths;
@@ -3575,9 +4089,149 @@ var AgentLoop = class {
3575
4089
  }
3576
4090
  });
3577
4091
  }
4092
+ /**
4093
+ * 读取页面链接指向的文档(分册 22 §2.3 的六步)。
4094
+ *
4095
+ * 顺序不能调换:准入 → 同源判定 → 跨源确认 → 取数 → 格式分派 → 留痕。
4096
+ * 把确认放到取数之后,就等于「先下载了再问用户要不要下载」。
4097
+ */
4098
+ async #handleReadLinkedDocument(call, state) {
4099
+ const reader = this.#deps.linkedDocuments;
4100
+ const url = call.arguments.url;
4101
+ if (typeof url !== "string" || url.trim() === "") return toolError("VALIDATION_FAILED", "read_linked_document needs a \"url\" string.");
4102
+ const audit = async (data) => {
4103
+ state.trace.record("run.warning", { message: `read_linked_document: ${JSON.stringify(data)}` });
4104
+ await this.#deps.documentAudit?.append({
4105
+ type: "document.read",
4106
+ target: url,
4107
+ data
4108
+ });
4109
+ };
4110
+ let target;
4111
+ try {
4112
+ target = assertRemoteUrlAllowed(url, this.#config.remoteUrl ?? {});
4113
+ } catch (e) {
4114
+ const message = messageOf(e);
4115
+ await audit({
4116
+ ok: false,
4117
+ reason: message
4118
+ });
4119
+ return toolError("NETWORK_BLOCKED", message);
4120
+ }
4121
+ const sameOrigin = reader.origin !== void 0 && reader.origin === target.origin;
4122
+ if (!sameOrigin) {
4123
+ if (!await this.#confirm(`Read the linked document at ${target.href}?`, state)) {
4124
+ await audit({
4125
+ ok: false,
4126
+ crossOrigin: true,
4127
+ approved: false,
4128
+ reason: "declined"
4129
+ });
4130
+ return toolError("TOOL_DENIED", `Reading ${target.href} was declined by the user.`);
4131
+ }
4132
+ }
4133
+ let fetched;
4134
+ try {
4135
+ fetched = await reader.read(target.href);
4136
+ } catch (e) {
4137
+ const message = messageOf(e);
4138
+ await audit({
4139
+ ok: false,
4140
+ crossOrigin: !sameOrigin,
4141
+ approved: !sameOrigin,
4142
+ reason: message
4143
+ });
4144
+ return toolError("TOOL_EXECUTION_FAILED", message);
4145
+ }
4146
+ const mime = fetched.mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
4147
+ const name = target.pathname.split("/").pop() || "document";
4148
+ const id = `doc-${state.runId}-${++state.documentSeq}`;
4149
+ const record = {
4150
+ ok: true,
4151
+ crossOrigin: !sameOrigin,
4152
+ approved: !sameOrigin,
4153
+ bytes: fetched.bytes.length,
4154
+ mimeType: mime
4155
+ };
4156
+ if (mime === SUPPORTED_DOCUMENT_MIME.pdf) {
4157
+ const part = {
4158
+ type: "file",
4159
+ mimeType: mime,
4160
+ data: toBase64(fetched.bytes),
4161
+ name,
4162
+ id
4163
+ };
4164
+ const tooLarge = this.#documentTooLarge(part);
4165
+ if (tooLarge !== void 0) {
4166
+ await audit({
4167
+ ...record,
4168
+ ok: false,
4169
+ reason: tooLarge
4170
+ });
4171
+ return toolError("TOOL_RESULT_TOO_LARGE", tooLarge);
4172
+ }
4173
+ await audit(record);
4174
+ return {
4175
+ ok: true,
4176
+ content: [part]
4177
+ };
4178
+ }
4179
+ let text;
4180
+ if (mime === SUPPORTED_DOCUMENT_MIME.docx) {
4181
+ if (this.#deps.docxExtractor === void 0) {
4182
+ await audit({
4183
+ ...record,
4184
+ ok: false,
4185
+ reason: "no docx extractor"
4186
+ });
4187
+ return toolError("TOOL_UNSUPPORTED", "Word documents cannot be read in this environment: no docx text extractor is configured.");
4188
+ }
4189
+ try {
4190
+ text = await this.#deps.docxExtractor(fetched.bytes);
4191
+ } catch (e) {
4192
+ const message = messageOf(e);
4193
+ await audit({
4194
+ ...record,
4195
+ ok: false,
4196
+ reason: message
4197
+ });
4198
+ return toolError("TOOL_EXECUTION_FAILED", message);
4199
+ }
4200
+ } else if (mime.startsWith("text/")) text = new TextDecoder().decode(fetched.bytes);
4201
+ else {
4202
+ await audit({
4203
+ ...record,
4204
+ ok: false,
4205
+ reason: `unsupported mime ${mime}`
4206
+ });
4207
+ return toolError("TOOL_UNSUPPORTED", `${UNSUPPORTED_DOCUMENT_MESSAGE} This link served "${mime}".`);
4208
+ }
4209
+ const part = {
4210
+ type: "document-text",
4211
+ text,
4212
+ name,
4213
+ id
4214
+ };
4215
+ const tooLarge = this.#documentTooLarge(part);
4216
+ if (tooLarge !== void 0) {
4217
+ await audit({
4218
+ ...record,
4219
+ ok: false,
4220
+ reason: tooLarge
4221
+ });
4222
+ return toolError("TOOL_RESULT_TOO_LARGE", tooLarge);
4223
+ }
4224
+ await audit(record);
4225
+ return {
4226
+ ok: true,
4227
+ content: [part]
4228
+ };
4229
+ }
3578
4230
  async #handleAskUser(call, state) {
4231
+ const rawFields = call.arguments["fields"];
4232
+ if (Array.isArray(rawFields) && rawFields.length > 0) return this.#handleAskUserFields(call, state, rawFields);
3579
4233
  const question = call.arguments["question"];
3580
- if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires a non-empty \"question\" string argument");
4234
+ 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");
3581
4235
  const rawChoices = call.arguments["choices"];
3582
4236
  const choices = Array.isArray(rawChoices) ? rawChoices.filter((choice) => typeof choice === "string" && choice !== "") : [];
3583
4237
  const id = this.#nextInteractionId(state);
@@ -3616,6 +4270,61 @@ var AgentLoop = class {
3616
4270
  throw e;
3617
4271
  }
3618
4272
  }
4273
+ /**
4274
+ * 多字段 `ask_user`(FR-11.1)。构造的是与「技能缺参」同一个 `{ type: 'form' }`
4275
+ * 交互,因此渲染、回传、快照都走既有那一条路径——不允许出现第二套表单实现。
4276
+ */
4277
+ async #handleAskUserFields(call, state, rawFields) {
4278
+ const limit = this.#config.askUserMaxFields;
4279
+ 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.`);
4280
+ const fields = [];
4281
+ for (const [index, raw] of rawFields.entries()) {
4282
+ const field = raw;
4283
+ const at = `fields[${index}]`;
4284
+ if (typeof field !== "object" || field === null) return toolError("VALIDATION_FAILED", `ask_user ${at} must be an object`);
4285
+ const name = field["name"];
4286
+ const label = field["label"];
4287
+ const type = field["type"];
4288
+ if (typeof name !== "string" || name === "") return toolError("VALIDATION_FAILED", `ask_user ${at} requires a non-empty "name"`);
4289
+ if (typeof label !== "string" || label === "") return toolError("VALIDATION_FAILED", `ask_user ${at} requires a non-empty "label"`);
4290
+ if (type === "multi-select") return toolError("VALIDATION_FAILED", `ask_user ${at} does not support "multi-select". Use render_ui with a MultiSelect field instead.`);
4291
+ 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(", ")}.`);
4292
+ const options = field["options"];
4293
+ if (type === "select" && !Array.isArray(options)) return toolError("VALIDATION_FAILED", `ask_user ${at} is a select and requires "options"`);
4294
+ fields.push({
4295
+ name,
4296
+ label,
4297
+ type,
4298
+ ...field["required"] === true ? { required: true } : {},
4299
+ ...typeof field["description"] === "string" ? { description: field["description"] } : {},
4300
+ ...field["defaultValue"] !== void 0 ? { defaultValue: field["defaultValue"] } : {},
4301
+ ...Array.isArray(options) ? { options: options.filter((o) => typeof o === "object" && o !== null).map((o) => ({
4302
+ label: String(o["label"] ?? o["value"]),
4303
+ value: o["value"]
4304
+ })) } : {}
4305
+ });
4306
+ }
4307
+ const question = call.arguments["question"];
4308
+ 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." });
4309
+ try {
4310
+ const value = await this.#interact(state, {
4311
+ type: "form",
4312
+ id: this.#nextInteractionId(state),
4313
+ ...typeof question === "string" && question !== "" ? { title: question } : {},
4314
+ fields
4315
+ }, { tool: call.name });
4316
+ return {
4317
+ ok: true,
4318
+ content: [{
4319
+ type: "text",
4320
+ text: typeof value === "string" ? value : JSON.stringify(value ?? {})
4321
+ }]
4322
+ };
4323
+ } catch (e) {
4324
+ if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `ask_user failed: ${e.message}`);
4325
+ throw e;
4326
+ }
4327
+ }
3619
4328
  async #handleReadSkillFile(call, state) {
3620
4329
  const skillName = call.arguments["skillName"];
3621
4330
  if (typeof skillName !== "string" || skillName === "") return toolError("TOOL_EXECUTION_FAILED", "read_skill_file requires a non-empty \"skillName\" string argument");
@@ -3689,24 +4398,86 @@ var AgentLoop = class {
3689
4398
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
3690
4399
  }
3691
4400
  /**
3692
- * 工具结果 → tool 消息内容。image 分片单独提出,不进 JSON 也不参与截断计算(0.6.0 §1.1):
3693
- * 100 KB 的文本预算遇到一张 200 KB 的图会把整个结果截成垃圾。
4401
+ * 超预算的文档分片换成一条说明(FR-23.4)。**不截断**:
4402
+ * 半份 PDF 是坏文件,半份抽取文本会让模型以为自己读全了。
4403
+ */
4404
+ #documentTooLarge(part) {
4405
+ if (part.type !== "file" && part.type !== "document-text") return void 0;
4406
+ const isFile = part.type === "file";
4407
+ const size = isFile ? part.data.length : new TextEncoder().encode(part.text).length;
4408
+ const max = isFile ? this.#config.maxDocumentBytes : this.#config.maxDocumentTextBytes;
4409
+ if (size <= max) return void 0;
4410
+ 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.`;
4411
+ }
4412
+ /** 推一条 tool 消息,返回需要另投 user 消息的二进制分片 */
4413
+ async #pushToolResult(state, call, result) {
4414
+ const split = await this.#toolResultParts(call, result, state);
4415
+ state.messages.push({
4416
+ role: "tool",
4417
+ toolCallId: call.id,
4418
+ content: split.tool
4419
+ });
4420
+ return split.carried;
4421
+ }
4422
+ /**
4423
+ * 工具结果 → tool 消息内容 + 另投的二进制分片。只有白名单内的分片进 JSON 并参与截断计算(FR-23.3):
4424
+ * 100 KB 的文本预算遇到一张 200 KB 的图会把整个结果截成垃圾,
4425
+ * 一份被截断的 PDF 则直接是坏文件。
4426
+ *
4427
+ * `carried` 单独返回而不是并进 tool 消息:OpenAI 协议的 tool 消息只有 `content: string`,
4428
+ * Google 的 `functionResponse`、Anthropic 的 `tool_result` 同样只收文本——
4429
+ * 三家客户端都会当场抛 `VALIDATION_FAILED`。二进制只能走 user 消息。
3694
4430
  */
3695
4431
  async #toolResultParts(call, result, state) {
3696
- const images = result.content.filter((part) => part.type === "image");
3697
- if (images.length === 0) return textParts(await this.#serializeToolResult(call, result, state));
4432
+ const budgeted = result.content.filter((part) => TEXT_BUDGETED_CONTENT_TYPES.has(part.type));
4433
+ const passthrough = result.content.filter((part) => !TEXT_BUDGETED_CONTENT_TYPES.has(part.type));
4434
+ if (passthrough.length === 0) return {
4435
+ tool: textParts(await this.#serializeToolResult(call, result, state)),
4436
+ carried: []
4437
+ };
3698
4438
  const textual = {
3699
4439
  ...result,
3700
- content: result.content.filter((part) => part.type !== "image")
4440
+ content: budgeted
4441
+ };
4442
+ const notes = [];
4443
+ const carried = [];
4444
+ for (const part of passthrough) {
4445
+ const tooLarge = this.#documentTooLarge(part);
4446
+ if (tooLarge) {
4447
+ state.trace.record("run.warning", { message: tooLarge });
4448
+ notes.push(tooLarge);
4449
+ continue;
4450
+ }
4451
+ if (part.type === "image") carried.push({
4452
+ type: "image",
4453
+ mimeType: part.mimeType,
4454
+ data: part.data
4455
+ });
4456
+ else if (part.type === "file") carried.push({
4457
+ type: "file",
4458
+ mimeType: part.mimeType,
4459
+ data: part.data,
4460
+ ...part.name ? { name: part.name } : {}
4461
+ });
4462
+ else if (part.type === "document-text") notes.push(part.text);
4463
+ }
4464
+ return {
4465
+ tool: [
4466
+ {
4467
+ type: "text",
4468
+ text: await this.#serializeToolResult(call, textual, state)
4469
+ },
4470
+ ...notes.map((text) => ({
4471
+ type: "text",
4472
+ text
4473
+ })),
4474
+ ...carried.length > 0 ? [{
4475
+ type: "text",
4476
+ text: `The ${carried.length === 1 ? "attachment" : `${carried.length} attachments`} from "${call.name}" ${carried.length === 1 ? "is" : "are"} in the next message.`
4477
+ }] : []
4478
+ ],
4479
+ carried
3701
4480
  };
3702
- return [{
3703
- type: "text",
3704
- text: await this.#serializeToolResult(call, textual, state)
3705
- }, ...images.map((image) => ({
3706
- type: "image",
3707
- mimeType: image.mimeType,
3708
- data: image.data
3709
- }))];
3710
4481
  }
3711
4482
  /**
3712
4483
  * 工具结果回喂序列化:超过 toolResultMaxBytes(默认 256KB)时头尾保留截断,
@@ -3947,6 +4718,7 @@ var AgentLoop = class {
3947
4718
  skillRoot: root,
3948
4719
  runId: state.runId,
3949
4720
  confirm: (message) => this.#confirm(message, state),
4721
+ ...this.#deps.fetchData ? { fetchData: this.#deps.fetchData } : {},
3950
4722
  onWarning: (message) => state.trace.record("run.warning", { message }),
3951
4723
  onArtifactCreated: (artifact) => createdIds.add(artifact.id)
3952
4724
  });
@@ -4289,12 +5061,12 @@ var WebSkillRuntime = class {
4289
5061
  * 多会话:session 对象的 run(prompt) 跨 run 延续消息历史(同一 session 对话上下文连续)。
4290
5062
  * 既有 runtime.run(prompt) 保持无状态单次语义不变。
4291
5063
  * 同 handle 并发 run 经 per-handle 队列串行化(防历史 last-writer-wins 丢轮次);
4292
- * maxHistoryMessages(默认 100)超出时滚动裁剪中段(保留首尾;边界对齐 tool 契约)。
5064
+ * maxHistoryMessages(默认取 `DEFAULT_LOOP_LIMITS`)超出时滚动裁剪中段(保留首尾;边界对齐 tool 契约)。
4293
5065
  */
4294
5066
  createSession(options = {}) {
4295
5067
  const sessionId = options.sessionId ?? `session-${Math.random().toString(36).slice(2, 10)}`;
4296
5068
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
4297
- const maxHistory = options.maxHistoryMessages ?? 100;
5069
+ const maxHistory = options.maxHistoryMessages ?? DEFAULT_LOOP_LIMITS.maxHistoryMessages;
4298
5070
  let history = [];
4299
5071
  let queue = Promise.resolve();
4300
5072
  return {
@@ -4361,9 +5133,14 @@ var WebSkillRuntime = class {
4361
5133
  skillProviders: this.#deps.skillProviders,
4362
5134
  catalogFilter: this.#deps.catalogFilter,
4363
5135
  snapshotStore: this.#deps.snapshotStore,
5136
+ toolSteps: this.#deps.toolSteps,
4364
5137
  skillStateGuard: this.#deps.skillStateGuard,
4365
5138
  skillIntegrityGuard: this.#deps.skillIntegrityGuard,
4366
- skillOutcomeReporter: this.#deps.skillOutcomeReporter
5139
+ skillOutcomeReporter: this.#deps.skillOutcomeReporter,
5140
+ fetchData: this.#deps.fetchData,
5141
+ linkedDocuments: this.#deps.linkedDocuments,
5142
+ docxExtractor: this.#deps.docxExtractor,
5143
+ documentAudit: this.#deps.documentAudit
4367
5144
  }, this.#deps.config);
4368
5145
  const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
4369
5146
  this.#loops.set(runId, loop);
@@ -4482,9 +5259,14 @@ var WebSkillRuntime = class {
4482
5259
  skillProviders: this.#deps.skillProviders,
4483
5260
  catalogFilter: this.#deps.catalogFilter,
4484
5261
  snapshotStore: store,
5262
+ toolSteps: this.#deps.toolSteps,
4485
5263
  skillStateGuard: this.#deps.skillStateGuard,
4486
5264
  skillIntegrityGuard: this.#deps.skillIntegrityGuard,
4487
- skillOutcomeReporter: this.#deps.skillOutcomeReporter
5265
+ skillOutcomeReporter: this.#deps.skillOutcomeReporter,
5266
+ fetchData: this.#deps.fetchData,
5267
+ linkedDocuments: this.#deps.linkedDocuments,
5268
+ docxExtractor: this.#deps.docxExtractor,
5269
+ documentAudit: this.#deps.documentAudit
4488
5270
  }, this.#deps.config);
4489
5271
  this.#loops.set(runId, loop);
4490
5272
  try {
@@ -4995,7 +5777,9 @@ function parseBridgeRequest(data) {
4995
5777
  const { kind, id } = data;
4996
5778
  if (!isNonEmptyString(id)) return void 0;
4997
5779
  switch (kind) {
4998
- case "readReference": return isNonEmptyString(data["path"]) ? {
5780
+ case "readReference":
5781
+ case "readAsset":
5782
+ case "readAssetBinary": return isNonEmptyString(data["path"]) ? {
4999
5783
  kind,
5000
5784
  id,
5001
5785
  path: data["path"]
@@ -5009,7 +5793,8 @@ function parseBridgeRequest(data) {
5009
5793
  id,
5010
5794
  path,
5011
5795
  content,
5012
- ...isNonEmptyString(mimeType) ? { mimeType } : {}
5796
+ ...isNonEmptyString(mimeType) ? { mimeType } : {},
5797
+ ..."metadata" in data ? { metadata: data["metadata"] } : {}
5013
5798
  };
5014
5799
  }
5015
5800
  case "confirm": return isNonEmptyString(data["message"]) ? {
@@ -5017,6 +5802,16 @@ function parseBridgeRequest(data) {
5017
5802
  id,
5018
5803
  message: data["message"]
5019
5804
  } : void 0;
5805
+ case "fetchData": {
5806
+ if (!isNonEmptyString(data["sourceId"])) return void 0;
5807
+ const params = data["params"];
5808
+ return {
5809
+ kind,
5810
+ id,
5811
+ sourceId: data["sourceId"],
5812
+ ...isRecord(params) ? { params } : {}
5813
+ };
5814
+ }
5020
5815
  default: return;
5021
5816
  }
5022
5817
  }
@@ -5442,6 +6237,7 @@ function sortByStartedAtDesc(summaries) {
5442
6237
  function applyFilter(summaries, filter) {
5443
6238
  let runs = summaries;
5444
6239
  if (filter.status !== void 0 && filter.status !== "") runs = runs.filter((r) => r.status === filter.status);
6240
+ if (filter.sessionId !== void 0 && filter.sessionId !== "") runs = runs.filter((r) => r.sessionId === filter.sessionId);
5445
6241
  const q = filter.search?.trim().toLowerCase();
5446
6242
  if (q !== void 0 && q !== "") runs = runs.filter((r) => r.runId.toLowerCase().includes(q) || r.activeSkills.some((s) => s.toLowerCase().includes(q)));
5447
6243
  return runs;
@@ -5701,6 +6497,51 @@ var FsSessionStore = class {
5701
6497
  });
5702
6498
  }
5703
6499
  };
6500
+ /**
6501
+ * 按会话分文件的 append-only JSONL 留存。
6502
+ *
6503
+ * 分文件维度取会话而非 run:读取侧唯一的查询就是「这个会话的全部步骤」,
6504
+ * 按 run 分会让一次生成要开 N 个文件。
6505
+ */
6506
+ var FsToolStepStore = class {
6507
+ #root;
6508
+ #fs;
6509
+ #onError;
6510
+ constructor(deps) {
6511
+ this.#root = deps.root.replace(/\/+$/, "");
6512
+ this.#fs = deps.fs;
6513
+ this.#onError = deps.onError ?? ((error, record) => {
6514
+ console.warn(`Failed to persist tool step for call "${record.callId}": ${messageOf(error)}`);
6515
+ });
6516
+ }
6517
+ #path(sessionId) {
6518
+ assertSafePathSegment(sessionId, "session id");
6519
+ return resolveInsideRoot(this.#root, `${sessionId}.jsonl`);
6520
+ }
6521
+ async append(record) {
6522
+ const path = this.#path(record.sessionId);
6523
+ try {
6524
+ await this.#fs.appendText(path, `${JSON.stringify(record)}\n`);
6525
+ } catch (e) {
6526
+ this.#onError(e, record);
6527
+ }
6528
+ }
6529
+ async listBySession(sessionId) {
6530
+ const path = this.#path(sessionId);
6531
+ if (!await this.#fs.exists(path)) return [];
6532
+ const raw = await this.#fs.readText(path);
6533
+ const records = [];
6534
+ for (const line of raw.split("\n")) {
6535
+ if (line.trim() === "") continue;
6536
+ try {
6537
+ records.push(JSON.parse(line));
6538
+ } catch {
6539
+ continue;
6540
+ }
6541
+ }
6542
+ return records;
6543
+ }
6544
+ };
5704
6545
 
5705
6546
  //#endregion
5706
- export { fromVercelStreamPart as $, SerializingMemoryStore as A, validateUiSpecNode as At, bridgeError as B, ProgressiveRouter as C, sealToolCallPairs as Ct, RUN_SNAPSHOT_SCHEMA_VERSION as D, toRecordDigests as Dt, READ_SKILL_FILE_TOOL_NAME as E, toLlmToolSpec 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, toVercelToolSpecs 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, summarizeToolCalls 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, validateUiSpecEvent 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, summarizeRunUsage as wt, HookRunner as x, schemaToForm as xt, FullDisclosureRouter as y, sampleBehaviorRecords as yt, applyUserProfileImport as z };
6547
+ export { bridgeError as $, ProgressiveRouter as A, refineUserProfile as At, SUPPORTED_DOCUMENT_MIME as B, toLlmToolSpec as Bt, FsSessionStore as C, normalizeToolError as Ct, HookRunner as D, readProfileEntries as Dt, GoogleGenAiClient as E, readBehaviorRecords as Et, READ_SKILL_FILE_TOOL_NAME as F, schemaToForm as Ft, USER_PROFILE_EXPORT_VERSION as G, TEXT_BUDGETED_CONTENT_TYPES as H, toVercelToolSpecs as Ht, RUN_SNAPSHOT_SCHEMA_VERSION as I, scriptToolName as It, USER_PROFILE_PROMPT_HEADER as J, USER_PROFILE_KEY as K, RUN_TRACE_SCHEMA_VERSION as L, sealToolCallPairs as Lt, READ_LINKED_DOCUMENT_TOOL_NAME as M, resolveToolName as Mt, READ_SKILL_FILE_INPUT_SCHEMA as N, sampleBehaviorRecords as Nt, MAX_TOOL_STEP_ARG_BYTES as O, readUserProfile as Ot, READ_SKILL_FILE_TOOL as P, schemaSourceLabel as Pt, applyUserProfileImport as Q, SENSITIVE_ANNOTATION as R, summarizeRunUsage as Rt, FsRunTraceStore as S, normalizeToolContent as St, FullDisclosureRouter as T, parseUserProfileExport as Tt, TraceRecorder as U, validateUiSpecEvent as Ut, SerializingMemoryStore as V, toRecordDigests as Vt, UNSUPPORTED_DOCUMENT_MESSAGE as W, validateUiSpecNode as Wt, WebSkillRuntime as X, USER_PROFILE_REFINE_PROMPT as Y, appendBehaviorRecords as Z, EventBus as _, mergeCatalogEntries as _t, ASK_USER_TOOL as a, extractChartSpec as at, FsMemoryStore as b, networkUrlHost as bt, AnthropicClient as c, extractUiSpecEvents as ct, DEFAULT_LOOP_LIMITS as d, fromVercelResult as dt, buildRenderResult as et, DEFAULT_MAX_DATA_SOURCE_BYTES as f, fromVercelStreamPart as ft, EMPTY_USER_PROFILE as g, listSkillScripts as gt, DEFAULT_USER_PROFILE_LIMITS as h, isUnsupportedRunSnapshot as ht, ASK_USER_MAX_FIELDS as i, exportUserProfile as it, READ_LINKED_DOCUMENT_TOOL as j, renderUserProfileContext as jt, OpenAiCompatibleClient as k, redactToolStepArgs as kt, BEHAVIOR_RECORDS_KEY as l, findUnpairedToolCalls as lt, DEFAULT_MAX_DOCUMENT_TEXT_BYTES as m, isNetworkAllowed as mt, ASK_USER_FIELD_TYPES as n, createWebSkillApi as nt, ASK_USER_TOOL_NAME as o, extractSkillCandidate as ot, DEFAULT_MAX_DOCUMENT_BYTES as p, interruptedToolResult as pt, USER_PROFILE_NO_INVENTION_RULE as q, ASK_USER_INPUT_SCHEMA as r, diffUserProfile as rt, AgentLoop as s, extractTodoTraceEvents as st, ALLOWED_TOOLS_EXCLUSION_REASON as t, createScriptContext as tt, CapabilityApproval as u, formatSkillScriptManifest as ut, FS_SESSION_PAGE_SIZE as v, mergeProfileEntries as vt, FsToolStepStore as w, parseBridgeRequest as wt, FsRunSnapshotStore as x, normalizeErrorCode as xt, FsArtifactStore as y, networkPolicyLibSource as yt, SESSION_SCHEMA_VERSION as z, summarizeToolCalls as zt };