@webskill/sdk 0.10.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 (34) 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 +758 -46
  5. package/dist/{catalogComponents-DTcYfpLQ-CcoOaz-Z.js → catalogComponents-BFoqpT1v-CjUBZ3bc.js} +253 -14
  6. package/dist/{dist-BViUeszk.js → dist-B-cOu08W.js} +526 -76
  7. package/dist/{dist-1OFC-zax.js → dist-DTHZS2k1.js} +520 -28
  8. package/dist/dist-qnlI2Iup.js +1280 -0
  9. package/dist/{eventTypes-s2uwAcLG-Go3l_dUe.js → eventTypes-FllCrX-Z-DNDeHWoG.js} +6 -2
  10. package/dist/governance.d.ts +7 -3
  11. package/dist/governance.js +1 -1
  12. package/dist/{index-DtFdMKBX.d.ts → index-D3mONFHD.d.ts} +220 -7
  13. package/dist/{index-B0QPLWPZ.d.ts → index-DACk2_XZ.d.ts} +110 -7
  14. package/dist/{index-BF4E1a9j.d.ts → index-DWbs58LF.d.ts} +227 -14
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +3 -3
  17. package/dist/mcp.d.ts +35 -7
  18. package/dist/mcp.js +88 -21
  19. package/dist/node.d.ts +3 -3
  20. package/dist/node.js +52 -2
  21. package/dist/{openUiLibrary-CIrV--Ad-B8zG_91e.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-D-qHk9ZE-DBsYYCWn.d.ts → skillVersionStore-D-qHk9ZE-BcmFLykd.d.ts} +1 -1
  25. package/dist/testing.d.ts +1 -1
  26. package/dist/{types-CrRcT-LM-DZAp8sWv.d.ts → types-C26b05fW-CdrRCRDb.d.ts} +20 -4
  27. package/dist/ui-react.d.ts +2 -2
  28. package/dist/ui-react.js +28 -10
  29. package/dist/ui-vue.d.ts +1 -1
  30. package/dist/ui-vue.js +2 -2
  31. package/dist/ui.d.ts +4 -4
  32. package/dist/ui.js +3 -3
  33. package/dist/{webskillLitCatalog-BJrphK0y-SGmRXaaO.js → webskillLitCatalog-DME6PBkV-CmYNLlIT.js} +135 -16
  34. 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
@@ -1125,10 +1125,16 @@ function toGenAiContents(messages) {
1125
1125
  }
1126
1126
  if (msg.role === "assistant") {
1127
1127
  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
- } });
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
+ }
1132
1138
  out.push({
1133
1139
  role: "model",
1134
1140
  parts: parts.length > 0 ? parts : [{ text: "" }]
@@ -1207,11 +1213,15 @@ var GoogleGenAiClient = class {
1207
1213
  continue;
1208
1214
  }
1209
1215
  const call = part["functionCall"];
1210
- if (call) toolCalls.push({
1211
- id: `call-${toolCalls.length}`,
1212
- name: call.name ?? "",
1213
- arguments: call.args ?? {}
1214
- });
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
+ }
1215
1225
  }
1216
1226
  };
1217
1227
  try {
@@ -1283,11 +1293,15 @@ var GoogleGenAiClient = class {
1283
1293
  const out = [];
1284
1294
  for (const part of parts) {
1285
1295
  const call = part["functionCall"];
1286
- if (call) out.push({
1287
- id: `call-${out.length}`,
1288
- name: call.name ?? "",
1289
- arguments: call.args ?? {}
1290
- });
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
+ }
1291
1305
  }
1292
1306
  return out;
1293
1307
  }
@@ -1426,6 +1440,45 @@ var FullDisclosureRouter = class {
1426
1440
  }
1427
1441
  };
1428
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
+ /**
1429
1482
  * 工具名解析规则(单一实现,Agent 循环使用):
1430
1483
  * 1. `<skillName>__<scriptName>` 且前缀是已激活技能 → 本地脚本
1431
1484
  * 2. `endpoint:` 前缀 → TOOL_UNSUPPORTED(MCP 阶段实现)
@@ -1584,17 +1637,75 @@ const READ_SKILL_FILE_TOOL = {
1584
1637
  source: "builtin"
1585
1638
  };
1586
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
+ ];
1587
1653
  const ASK_USER_INPUT_SCHEMA = {
1588
1654
  type: "object",
1589
1655
  properties: {
1590
1656
  question: {
1591
1657
  type: "string",
1592
- 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
+ }
1593
1704
  },
1594
1705
  choices: {
1595
1706
  type: "array",
1596
1707
  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."
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."
1598
1709
  },
1599
1710
  suggestion: {
1600
1711
  type: "string",
@@ -1604,20 +1715,19 @@ const ASK_USER_INPUT_SCHEMA = {
1604
1715
  type: "string",
1605
1716
  description: "Short reason for the suggestion, shown next to it so the user can judge whether to accept it."
1606
1717
  }
1607
- },
1608
- required: ["question"]
1718
+ }
1609
1719
  };
1610
1720
  /** 内建工具:LLM 信息不足时主动向用户提问;仅当配置了 UiBridge 时注册 */
1611
1721
  const ASK_USER_TOOL = {
1612
1722
  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\".",
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).",
1614
1724
  inputSchema: ASK_USER_INPUT_SCHEMA,
1615
1725
  source: "builtin"
1616
1726
  };
1617
1727
  /**
1618
- * 越权 / 绝对路径 / 父级遍历的引用读取:策略拒绝(FS_PERMISSION_DENIED,
1728
+ * 越权 / 绝对路径 / 父级遍历的技能目录读取:策略拒绝(FS_PERMISSION_DENIED,
1619
1729
  * 计入 POLICY_DENIAL_CODES → 不计入隔离失败计数),
1620
- * 与「references/ 内路径确实不存在 → FS_NOT_FOUND」区分(0.9.0 分册 14,UX-04)。
1730
+ * 与「目录内路径确实不存在 → FS_NOT_FOUND」区分(0.9.0 分册 14,UX-04)。
1621
1731
  */
1622
1732
  function assertReferencePath(relativePath) {
1623
1733
  const trimmed = relativePath.trim();
@@ -1625,18 +1735,31 @@ function assertReferencePath(relativePath) {
1625
1735
  if (trimmed.replace(/\\/g, "/").split("/").includes("..")) throw new WebSkillError("FS_PERMISSION_DENIED", `Reference access denied: parent traversal (path: ${JSON.stringify(relativePath)})`);
1626
1736
  }
1627
1737
  /**
1628
- * 脚本执行上下文:只暴露 readReference / writeArtifact 两个显式能力,
1629
- * 不暴露 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)。
1630
1748
  */
1631
1749
  function createScriptContext(deps) {
1632
- const { fs, artifactStore, skillName, skillRoot, runId, confirm, onWarning, onArtifactCreated } = deps;
1750
+ const { fs, artifactStore, skillName, skillRoot, runId, confirm, fetchData, onWarning, onArtifactCreated } = deps;
1633
1751
  const readFs = fs.withRoot?.(skillRoot) ?? fs;
1634
1752
  return {
1635
1753
  skillName,
1636
1754
  runId,
1637
1755
  async readReference(relativePath) {
1638
- assertReferencePath(relativePath);
1639
- 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));
1640
1763
  },
1641
1764
  async writeArtifact(path, content, options) {
1642
1765
  const artifact = typeof content === "string" ? await artifactStore.createTextArtifact({
@@ -1654,6 +1777,7 @@ function createScriptContext(deps) {
1654
1777
  return artifact;
1655
1778
  },
1656
1779
  ...confirm ? { confirm } : {},
1780
+ ...fetchData ? { fetchData } : {},
1657
1781
  ...onWarning ? { onWarning } : {}
1658
1782
  };
1659
1783
  }
@@ -2146,6 +2270,43 @@ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
2146
2270
  * 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
2147
2271
  */
2148
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
+ }
2149
2310
  /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
2150
2311
  var TraceRecorder = class {
2151
2312
  #runId;
@@ -2377,6 +2538,16 @@ function evaluateToolAccess(state, llmToolName) {
2377
2538
  };
2378
2539
  }
2379
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
+ ]);
2380
2551
  /** 交互终态(取消/超时):从工具执行深处直接终止 run */
2381
2552
  var RunTerminated = class extends Error {
2382
2553
  outcome;
@@ -2476,8 +2647,12 @@ var AgentLoop = class {
2476
2647
  paramHistoryLimit: config.paramHistoryLimit ?? 50,
2477
2648
  toolCallingDisabled: config.toolCallingDisabled ?? false,
2478
2649
  maxUnknownToolRetries: config.maxUnknownToolRetries ?? 2,
2650
+ askUserMaxFields: config.askUserMaxFields ?? 20,
2651
+ maxDocumentBytes: config.maxDocumentBytes ?? 256e5,
2652
+ maxDocumentTextBytes: config.maxDocumentTextBytes ?? 512e3,
2479
2653
  temperature: config.temperature,
2480
- renderResult: config.renderResult
2654
+ renderResult: config.renderResult,
2655
+ remoteUrl: config.remoteUrl
2481
2656
  };
2482
2657
  this.#policy = {
2483
2658
  missingParams: deps.interaction?.missingParams ?? "user",
@@ -2527,6 +2702,7 @@ var AgentLoop = class {
2527
2702
  now,
2528
2703
  interactionSeq: 0,
2529
2704
  surfaceActionSeq: 0,
2705
+ documentSeq: 0,
2530
2706
  messages: [],
2531
2707
  turn: 0,
2532
2708
  renderBlocks: [],
@@ -2656,6 +2832,7 @@ var AgentLoop = class {
2656
2832
  const toolSpecs = this.#config.toolCallingDisabled ? [] : [
2657
2833
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
2658
2834
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
2835
+ ...this.#deps.linkedDocuments ? [toLlmToolSpec(READ_LINKED_DOCUMENT_TOOL)] : [],
2659
2836
  ...skillToolSpecs
2660
2837
  ];
2661
2838
  trace.record("llm.request", { data: {
@@ -2683,8 +2860,9 @@ var AgentLoop = class {
2683
2860
  if (this.#cancelled.has(state.runId)) return finish("cancelled", "user-cancelled", "Run cancelled by user", "RUN_CANCELLED");
2684
2861
  return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
2685
2862
  }
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);
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);
2688
2866
  }
2689
2867
  if (response.thinking !== void 0 && response.thinking !== "") {
2690
2868
  state.trace.record("llm.thinking", { data: {
@@ -2734,6 +2912,7 @@ var AgentLoop = class {
2734
2912
  content: response.content ?? [],
2735
2913
  toolCalls: response.toolCalls
2736
2914
  });
2915
+ const carried = [];
2737
2916
  for (const call of response.toolCalls) {
2738
2917
  let result;
2739
2918
  try {
@@ -2742,11 +2921,7 @@ var AgentLoop = class {
2742
2921
  if (e instanceof RunTerminated) return finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
2743
2922
  throw e;
2744
2923
  }
2745
- messages.push({
2746
- role: "tool",
2747
- toolCallId: call.id,
2748
- content: await this.#toolResultParts(call, result, state)
2749
- });
2924
+ carried.push(...await this.#pushToolResult(state, call, result));
2750
2925
  try {
2751
2926
  await this.#drainSurfaceAction(state);
2752
2927
  } catch (e) {
@@ -2754,6 +2929,10 @@ var AgentLoop = class {
2754
2929
  throw e;
2755
2930
  }
2756
2931
  }
2932
+ if (carried.length > 0) messages.push({
2933
+ role: "user",
2934
+ content: carried
2935
+ });
2757
2936
  }
2758
2937
  } finally {
2759
2938
  this.#disarmDeadline(state);
@@ -2799,7 +2978,10 @@ var AgentLoop = class {
2799
2978
  try {
2800
2979
  await this.#lifecycle({
2801
2980
  phase: status === "completed" ? "complete" : "fail",
2802
- data: { reason }
2981
+ data: {
2982
+ reason,
2983
+ ...status === "failed" ? { detail: output } : {}
2984
+ }
2803
2985
  }, state);
2804
2986
  } catch (e) {
2805
2987
  trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
@@ -2830,7 +3012,7 @@ var AgentLoop = class {
2830
3012
  state.messages.push({
2831
3013
  role: "tool",
2832
3014
  toolCallId: call.id,
2833
- content: await this.#toolResultParts(call, result, state)
3015
+ content: (await this.#toolResultParts(call, result, state)).tool
2834
3016
  });
2835
3017
  state.trace.record("run.warning", {
2836
3018
  message: `Sealed unanswered tool call "${call.name}": the run ended before it produced a result.`,
@@ -2907,6 +3089,7 @@ var AgentLoop = class {
2907
3089
  },
2908
3090
  trace,
2909
3091
  reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
3092
+ documentSeq: 0,
2910
3093
  activated: new Set(snapshot.activeSkillNames),
2911
3094
  activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
2912
3095
  skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
@@ -2943,6 +3126,7 @@ var AgentLoop = class {
2943
3126
  } });
2944
3127
  const pending = pendingInteraction;
2945
3128
  const pendingCall = this.#findPendingToolCall(state.messages);
3129
+ const carried = [];
2946
3130
  try {
2947
3131
  await this.#replaySurfaceEvents(state);
2948
3132
  if (pendingSurfaceAction) await this.#resumeSurfaceAction(state, pendingSurfaceAction);
@@ -2959,11 +3143,7 @@ var AgentLoop = class {
2959
3143
  ...pendingCall,
2960
3144
  arguments: args
2961
3145
  }, state);
2962
- state.messages.push({
2963
- role: "tool",
2964
- toolCallId: pendingCall.id,
2965
- content: await this.#toolResultParts(pendingCall, result, state)
2966
- });
3146
+ carried.push(...await this.#pushToolResult(state, pendingCall, result));
2967
3147
  await this.#drainSurfaceAction(state);
2968
3148
  } else if (pending?.type === "ask" && pendingCall) {
2969
3149
  const value = await this.#interact(state, pending, {
@@ -2981,32 +3161,24 @@ var AgentLoop = class {
2981
3161
  name: pendingCall.name,
2982
3162
  callId: pendingCall.id
2983
3163
  } });
2984
- state.messages.push({
2985
- role: "tool",
2986
- toolCallId: pendingCall.id,
2987
- content: await this.#toolResultParts(pendingCall, result, state)
2988
- });
3164
+ carried.push(...await this.#pushToolResult(state, pendingCall, result));
2989
3165
  await this.#drainSurfaceAction(state);
2990
3166
  } else if (pendingCall) {
2991
3167
  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
- });
3168
+ carried.push(...await this.#pushToolResult(state, pendingCall, result));
2997
3169
  await this.#drainSurfaceAction(state);
2998
3170
  }
2999
3171
  for (;;) {
3000
3172
  const next = this.#findPendingToolCall(state.messages);
3001
3173
  if (!next) break;
3002
3174
  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
- });
3175
+ carried.push(...await this.#pushToolResult(state, next, result));
3008
3176
  await this.#drainSurfaceAction(state);
3009
3177
  }
3178
+ if (carried.length > 0) state.messages.push({
3179
+ role: "user",
3180
+ content: carried
3181
+ });
3010
3182
  } catch (e) {
3011
3183
  if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
3012
3184
  throw e;
@@ -3031,7 +3203,7 @@ var AgentLoop = class {
3031
3203
  * 拿 `activated[0]` 顶替会把内置工具的失败栽给一个无关技能,比不归因更糟(设计 26 §2.3)。
3032
3204
  */
3033
3205
  #skillOf(call, state) {
3034
- 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;
3035
3207
  const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
3036
3208
  return resolution.kind === "script" ? resolution.skillName : void 0;
3037
3209
  }
@@ -3263,6 +3435,7 @@ var AgentLoop = class {
3263
3435
  if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
3264
3436
  else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
3265
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);
3266
3439
  else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
3267
3440
  else {
3268
3441
  const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
@@ -3575,9 +3748,149 @@ var AgentLoop = class {
3575
3748
  }
3576
3749
  });
3577
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
+ }
3578
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);
3579
3892
  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");
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");
3581
3894
  const rawChoices = call.arguments["choices"];
3582
3895
  const choices = Array.isArray(rawChoices) ? rawChoices.filter((choice) => typeof choice === "string" && choice !== "") : [];
3583
3896
  const id = this.#nextInteractionId(state);
@@ -3616,6 +3929,60 @@ var AgentLoop = class {
3616
3929
  throw e;
3617
3930
  }
3618
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
+ }
3619
3986
  async #handleReadSkillFile(call, state) {
3620
3987
  const skillName = call.arguments["skillName"];
3621
3988
  if (typeof skillName !== "string" || skillName === "") return toolError("TOOL_EXECUTION_FAILED", "read_skill_file requires a non-empty \"skillName\" string argument");
@@ -3689,24 +4056,86 @@ var AgentLoop = class {
3689
4056
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
3690
4057
  }
3691
4058
  /**
3692
- * 工具结果 → tool 消息内容。image 分片单独提出,不进 JSON 也不参与截断计算(0.6.0 §1.1):
3693
- * 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 消息。
3694
4088
  */
3695
4089
  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));
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
+ };
3698
4096
  const textual = {
3699
4097
  ...result,
3700
- 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
3701
4138
  };
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
4139
  }
3711
4140
  /**
3712
4141
  * 工具结果回喂序列化:超过 toolResultMaxBytes(默认 256KB)时头尾保留截断,
@@ -3947,6 +4376,7 @@ var AgentLoop = class {
3947
4376
  skillRoot: root,
3948
4377
  runId: state.runId,
3949
4378
  confirm: (message) => this.#confirm(message, state),
4379
+ ...this.#deps.fetchData ? { fetchData: this.#deps.fetchData } : {},
3950
4380
  onWarning: (message) => state.trace.record("run.warning", { message }),
3951
4381
  onArtifactCreated: (artifact) => createdIds.add(artifact.id)
3952
4382
  });
@@ -4363,7 +4793,11 @@ var WebSkillRuntime = class {
4363
4793
  snapshotStore: this.#deps.snapshotStore,
4364
4794
  skillStateGuard: this.#deps.skillStateGuard,
4365
4795
  skillIntegrityGuard: this.#deps.skillIntegrityGuard,
4366
- 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
4367
4801
  }, this.#deps.config);
4368
4802
  const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
4369
4803
  this.#loops.set(runId, loop);
@@ -4484,7 +4918,11 @@ var WebSkillRuntime = class {
4484
4918
  snapshotStore: store,
4485
4919
  skillStateGuard: this.#deps.skillStateGuard,
4486
4920
  skillIntegrityGuard: this.#deps.skillIntegrityGuard,
4487
- 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
4488
4926
  }, this.#deps.config);
4489
4927
  this.#loops.set(runId, loop);
4490
4928
  try {
@@ -4995,7 +5433,9 @@ function parseBridgeRequest(data) {
4995
5433
  const { kind, id } = data;
4996
5434
  if (!isNonEmptyString(id)) return void 0;
4997
5435
  switch (kind) {
4998
- case "readReference": return isNonEmptyString(data["path"]) ? {
5436
+ case "readReference":
5437
+ case "readAsset":
5438
+ case "readAssetBinary": return isNonEmptyString(data["path"]) ? {
4999
5439
  kind,
5000
5440
  id,
5001
5441
  path: data["path"]
@@ -5017,6 +5457,16 @@ function parseBridgeRequest(data) {
5017
5457
  id,
5018
5458
  message: data["message"]
5019
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
+ }
5020
5470
  default: return;
5021
5471
  }
5022
5472
  }
@@ -5703,4 +6153,4 @@ var FsSessionStore = class {
5703
6153
  };
5704
6154
 
5705
6155
  //#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 };
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 };