@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.
- package/dist/agent.d.ts +3 -2
- package/dist/agent.js +3 -1102
- package/dist/browser.d.ts +258 -11
- package/dist/browser.js +763 -47
- package/dist/{catalogComponents-DTcYfpLQ-CcoOaz-Z.js → catalogComponents-BgAJN0p8-C3K8klJd.js} +508 -923
- package/dist/dist-DU9KDAuR.js +1468 -0
- package/dist/{dist-1OFC-zax.js → dist-DqcL6jKO.js} +668 -60
- package/dist/{dist-BViUeszk.js → dist-sdKFgERo.js} +956 -115
- package/dist/{echarts-DhNm2ene.js → echarts-De78wXqV.js} +599 -61
- 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-B0QPLWPZ.d.ts → index-3fCHc1mQ.d.ts} +198 -11
- package/dist/{index-DtFdMKBX.d.ts → index-C9pzXLKy.d.ts} +366 -9
- package/dist/{index-BF4E1a9j.d.ts → index-DFhU1uks.d.ts} +286 -19
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +58 -7
- package/dist/mcp.js +124 -21
- package/dist/node.d.ts +3 -3
- package/dist/node.js +60 -4
- package/dist/{openUiLibrary-CIrV--Ad-B8zG_91e.js → openUiLibrary-BKXW7Iwx-DaymVubt.js} +3 -3
- package/dist/processSandboxEntry.js +8 -1
- package/dist/sandboxWorkerEntry.js +8 -1
- package/dist/{skillVersionStore-D-qHk9ZE-DBsYYCWn.d.ts → skillVersionStore-D-qHk9ZE-DheTIwAB.d.ts} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{types-CrRcT-LM-DZAp8sWv.d.ts → types-DLctJep_-B5G4uk2u.d.ts} +22 -5
- package/dist/ui-react.d.ts +13 -4
- package/dist/ui-react.js +93 -259
- 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-D_zCqeQF-C9lrvvMr.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
|
|
@@ -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
|
-
*
|
|
286
|
+
* 非根 `$defs` 提到根,自引用的 schema resource 提到根 `$defs`,
|
|
287
|
+
* 所有 `$ref: '#'` 改写为 `$ref: '#/$defs/<Node>'`。
|
|
216
288
|
*
|
|
217
|
-
*
|
|
289
|
+
* 幂等:不含自引用也不含嵌套 `$defs` 的 schema 原样返回。
|
|
218
290
|
*
|
|
219
291
|
* 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop,
|
|
220
292
|
* 否则 trace / eventBus 观测到的 schema 会与技能作者写的不一致。
|
|
221
293
|
*/
|
|
222
|
-
function toPortableToolSchema(
|
|
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 ?? [])
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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)
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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)
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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: "
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1629
|
-
*
|
|
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
|
-
|
|
1639
|
-
|
|
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)
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
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:
|
|
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 ? [] :
|
|
2564
|
-
|
|
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
|
|
2687
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
3693
|
-
*
|
|
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
|
|
3697
|
-
|
|
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:
|
|
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
|
|
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 ??
|
|
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":
|
|
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 {
|
|
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 };
|