@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.
- package/dist/agent.d.ts +3 -2
- package/dist/agent.js +3 -1102
- package/dist/browser.d.ts +258 -11
- package/dist/browser.js +758 -46
- package/dist/{catalogComponents-DTcYfpLQ-CcoOaz-Z.js → catalogComponents-BFoqpT1v-CjUBZ3bc.js} +253 -14
- package/dist/{dist-BViUeszk.js → dist-B-cOu08W.js} +526 -76
- package/dist/{dist-1OFC-zax.js → dist-DTHZS2k1.js} +520 -28
- package/dist/dist-qnlI2Iup.js +1280 -0
- package/dist/{eventTypes-s2uwAcLG-Go3l_dUe.js → eventTypes-FllCrX-Z-DNDeHWoG.js} +6 -2
- package/dist/governance.d.ts +7 -3
- package/dist/governance.js +1 -1
- package/dist/{index-DtFdMKBX.d.ts → index-D3mONFHD.d.ts} +220 -7
- package/dist/{index-B0QPLWPZ.d.ts → index-DACk2_XZ.d.ts} +110 -7
- package/dist/{index-BF4E1a9j.d.ts → index-DWbs58LF.d.ts} +227 -14
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +35 -7
- package/dist/mcp.js +88 -21
- package/dist/node.d.ts +3 -3
- package/dist/node.js +52 -2
- package/dist/{openUiLibrary-CIrV--Ad-B8zG_91e.js → openUiLibrary-D5u8oIvx-BLOAQCho.js} +3 -3
- package/dist/processSandboxEntry.js +6 -0
- package/dist/sandboxWorkerEntry.js +6 -0
- package/dist/{skillVersionStore-D-qHk9ZE-DBsYYCWn.d.ts → skillVersionStore-D-qHk9ZE-BcmFLykd.d.ts} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{types-CrRcT-LM-DZAp8sWv.d.ts → types-C26b05fW-CdrRCRDb.d.ts} +20 -4
- package/dist/ui-react.d.ts +2 -2
- package/dist/ui-react.js +28 -10
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +2 -2
- package/dist/ui.d.ts +4 -4
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-BJrphK0y-SGmRXaaO.js → webskillLitCatalog-DME6PBkV-CmYNLlIT.js} +135 -16
- 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 ?? [])
|
|
1129
|
-
|
|
1130
|
-
|
|
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)
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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)
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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: "
|
|
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
|
|
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
|
-
* 越权 / 绝对路径 /
|
|
1728
|
+
* 越权 / 绝对路径 / 父级遍历的技能目录读取:策略拒绝(FS_PERMISSION_DENIED,
|
|
1619
1729
|
* 计入 POLICY_DENIAL_CODES → 不计入隔离失败计数),
|
|
1620
|
-
*
|
|
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
|
-
*
|
|
1629
|
-
*
|
|
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
|
-
|
|
1639
|
-
|
|
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
|
|
2687
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
3693
|
-
*
|
|
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
|
|
3697
|
-
|
|
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:
|
|
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":
|
|
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 {
|
|
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 };
|