@webskill/sdk 0.11.0 → 0.13.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 -3
- package/dist/agent.js +3 -3
- package/dist/browser.d.ts +29 -6
- package/dist/browser.js +629 -75
- package/dist/{catalogComponents-BFoqpT1v-CjUBZ3bc.js → catalogComponents-BgAJN0p8-CYEXSk45.js} +606 -926
- package/dist/{dist-DTHZS2k1.js → dist-DqcL6jKO.js} +155 -39
- package/dist/{dist-B-cOu08W.js → dist-ExSQky4C.js} +754 -69
- package/dist/{dist-qnlI2Iup.js → dist-GK6dtjRv.js} +222 -9
- package/dist/{echarts-DhNm2ene.js → echarts-De78wXqV.js} +599 -61
- package/dist/governance.d.ts +3 -3
- package/dist/{index-DACk2_XZ.d.ts → index-C3XdItd_.d.ts} +92 -8
- package/dist/{index-D3mONFHD.d.ts → index-DkvBhJQy.d.ts} +187 -6
- package/dist/{index-DWbs58LF.d.ts → index-iBm9tJL_.d.ts} +122 -23
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +47 -3
- package/dist/mcp.js +79 -2
- package/dist/node.d.ts +3 -3
- package/dist/node.js +9 -3
- package/dist/{openUiLibrary-D5u8oIvx-BLOAQCho.js → openUiLibrary-BKXW7Iwx-CWWBVOkE.js} +3 -3
- package/dist/processSandboxEntry.js +2 -1
- package/dist/sandboxWorkerEntry.js +2 -1
- package/dist/{skillVersionStore-D-qHk9ZE-BcmFLykd.d.ts → skillVersionStore-D-qHk9ZE-DheTIwAB.d.ts} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{types-C26b05fW-CdrRCRDb.d.ts → types-DLctJep_-B5G4uk2u.d.ts} +3 -2
- package/dist/ui-react.d.ts +13 -4
- package/dist/ui-react.js +77 -259
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +4 -4
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-DME6PBkV-CmYNLlIT.js → webskillLitCatalog-D_zCqeQF-C9lrvvMr.js} +2 -2
- package/package.json +2 -1
|
@@ -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 = [];
|
|
@@ -490,6 +607,76 @@ function createSseFrameReader() {
|
|
|
490
607
|
}
|
|
491
608
|
};
|
|
492
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* 内联 `<think>` 段落切分器(内部模块,不进公开导出)。
|
|
612
|
+
*
|
|
613
|
+
* 本地部署的思考型模型(Qwen 等经 OpenAI 兼容端点)不下发 `reasoning_content`,
|
|
614
|
+
* 而是把推理直接写进 `content`,用 `<think>` … `</think>` 包起来。不切分的话
|
|
615
|
+
* 思考正文会当成回答正文流进界面、进消息历史、进落盘,既污染上下文也没法折叠。
|
|
616
|
+
*/
|
|
617
|
+
const OPEN = "<think>";
|
|
618
|
+
const CLOSE = "</think>";
|
|
619
|
+
/** buffer 是否为 tag 的真前缀(还不能判定,得再等) */
|
|
620
|
+
const isPartialPrefix = (buffer, tag) => buffer.length < tag.length && tag.startsWith(buffer);
|
|
621
|
+
function createThinkTagSplitter() {
|
|
622
|
+
let buffer = "";
|
|
623
|
+
let inThink = false;
|
|
624
|
+
const consume = () => {
|
|
625
|
+
let text = "";
|
|
626
|
+
let thinking = "";
|
|
627
|
+
for (;;) {
|
|
628
|
+
const tag = inThink ? CLOSE : OPEN;
|
|
629
|
+
const hit = buffer.indexOf(tag);
|
|
630
|
+
if (hit >= 0) {
|
|
631
|
+
const before = buffer.slice(0, hit);
|
|
632
|
+
if (inThink) thinking += before;
|
|
633
|
+
else text += before;
|
|
634
|
+
buffer = buffer.slice(hit + tag.length);
|
|
635
|
+
inThink = !inThink;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
const start = buffer.lastIndexOf("<");
|
|
639
|
+
const keep = start >= 0 && isPartialPrefix(buffer.slice(start), tag) ? start : buffer.length;
|
|
640
|
+
const emit = buffer.slice(0, keep);
|
|
641
|
+
if (inThink) thinking += emit;
|
|
642
|
+
else text += emit;
|
|
643
|
+
buffer = buffer.slice(keep);
|
|
644
|
+
return {
|
|
645
|
+
text,
|
|
646
|
+
thinking
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
return {
|
|
651
|
+
push(delta) {
|
|
652
|
+
buffer += delta;
|
|
653
|
+
return consume();
|
|
654
|
+
},
|
|
655
|
+
flush() {
|
|
656
|
+
const rest = buffer;
|
|
657
|
+
buffer = "";
|
|
658
|
+
return inThink ? {
|
|
659
|
+
text: "",
|
|
660
|
+
thinking: rest
|
|
661
|
+
} : {
|
|
662
|
+
text: rest,
|
|
663
|
+
thinking: ""
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/** 非流式整段切分:返回正文与思考正文(无 `<think>` 时 thinking 为 undefined) */
|
|
669
|
+
function splitThinkTags(content) {
|
|
670
|
+
if (!content.includes(OPEN)) return { text: content };
|
|
671
|
+
const splitter = createThinkTagSplitter();
|
|
672
|
+
const first = splitter.push(content);
|
|
673
|
+
const last = splitter.flush();
|
|
674
|
+
const thinking = first.thinking + last.thinking;
|
|
675
|
+
return {
|
|
676
|
+
text: first.text + last.text,
|
|
677
|
+
...thinking !== "" ? { thinking } : {}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
493
680
|
const dataUrl = (part) => `data:${part.mimeType};base64,${part.data}`;
|
|
494
681
|
/** parts → OpenAI content;纯文本折叠成字符串(兼容端点对数组形态支持不一) */
|
|
495
682
|
const toOpenAiContent = (parts, where) => {
|
|
@@ -595,6 +782,7 @@ var OpenAiCompatibleClient = class {
|
|
|
595
782
|
const decoder = new TextDecoder();
|
|
596
783
|
const reader = res.body.getReader();
|
|
597
784
|
const frames = createSseFrameReader();
|
|
785
|
+
const think = createThinkTagSplitter();
|
|
598
786
|
let done = false;
|
|
599
787
|
let usage;
|
|
600
788
|
const handleFrame = function* (data) {
|
|
@@ -619,10 +807,17 @@ var OpenAiCompatibleClient = class {
|
|
|
619
807
|
type: "thinking-delta",
|
|
620
808
|
delta: delta["reasoning_content"]
|
|
621
809
|
};
|
|
622
|
-
if (typeof delta["content"] === "string" && delta["content"] !== "")
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
810
|
+
if (typeof delta["content"] === "string" && delta["content"] !== "") {
|
|
811
|
+
const split = think.push(delta["content"]);
|
|
812
|
+
if (split.thinking !== "") yield {
|
|
813
|
+
type: "thinking-delta",
|
|
814
|
+
delta: split.thinking
|
|
815
|
+
};
|
|
816
|
+
if (split.text !== "") yield {
|
|
817
|
+
type: "text-delta",
|
|
818
|
+
delta: split.text
|
|
819
|
+
};
|
|
820
|
+
}
|
|
626
821
|
const toolDeltas = delta["tool_calls"];
|
|
627
822
|
for (const td of toolDeltas ?? []) {
|
|
628
823
|
const index = td.index ?? 0;
|
|
@@ -650,6 +845,15 @@ var OpenAiCompatibleClient = class {
|
|
|
650
845
|
} finally {
|
|
651
846
|
reader.releaseLock();
|
|
652
847
|
}
|
|
848
|
+
const tail = think.flush();
|
|
849
|
+
if (tail.thinking !== "") yield {
|
|
850
|
+
type: "thinking-delta",
|
|
851
|
+
delta: tail.thinking
|
|
852
|
+
};
|
|
853
|
+
if (tail.text !== "") yield {
|
|
854
|
+
type: "text-delta",
|
|
855
|
+
delta: tail.text
|
|
856
|
+
};
|
|
653
857
|
if (toolCallsByIndex.size > 0) yield {
|
|
654
858
|
type: "tool-calls",
|
|
655
859
|
toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
|
|
@@ -705,7 +909,7 @@ var OpenAiCompatibleClient = class {
|
|
|
705
909
|
}
|
|
706
910
|
if (!res.ok) {
|
|
707
911
|
const detail = await res.text().catch(() => "");
|
|
708
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}
|
|
912
|
+
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
|
|
709
913
|
}
|
|
710
914
|
return res;
|
|
711
915
|
}
|
|
@@ -737,15 +941,21 @@ var OpenAiCompatibleClient = class {
|
|
|
737
941
|
...parseError ? { argumentsParseError: parseError } : {}
|
|
738
942
|
};
|
|
739
943
|
});
|
|
740
|
-
const
|
|
741
|
-
const
|
|
944
|
+
const raw = choice["content"];
|
|
945
|
+
const split = typeof raw === "string" ? splitThinkTags(raw) : {
|
|
946
|
+
text: "",
|
|
947
|
+
thinking: void 0
|
|
948
|
+
};
|
|
949
|
+
const content = split.text;
|
|
950
|
+
const declared = choice["reasoning_content"];
|
|
951
|
+
const thinking = typeof declared === "string" && declared !== "" ? declared : split.thinking;
|
|
742
952
|
const usageRaw = data.usage;
|
|
743
953
|
const usage = usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number") ? {
|
|
744
954
|
inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
|
|
745
955
|
outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
|
|
746
956
|
} : void 0;
|
|
747
957
|
return {
|
|
748
|
-
content:
|
|
958
|
+
content: content !== "" ? textParts(content) : void 0,
|
|
749
959
|
toolCalls: toolCalls?.length ? toolCalls : void 0,
|
|
750
960
|
...typeof thinking === "string" && thinking !== "" ? { thinking } : {},
|
|
751
961
|
...usage ? { usage } : {},
|
|
@@ -1006,7 +1216,7 @@ var AnthropicClient = class {
|
|
|
1006
1216
|
}
|
|
1007
1217
|
if (!res.ok) {
|
|
1008
1218
|
const detail = await res.text().catch(() => "");
|
|
1009
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}
|
|
1219
|
+
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
|
|
1010
1220
|
}
|
|
1011
1221
|
return res;
|
|
1012
1222
|
}
|
|
@@ -1151,11 +1361,14 @@ function toGenAiContents(messages) {
|
|
|
1151
1361
|
contents: out
|
|
1152
1362
|
};
|
|
1153
1363
|
}
|
|
1154
|
-
const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) =>
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1364
|
+
const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => {
|
|
1365
|
+
const portable = toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema));
|
|
1366
|
+
return {
|
|
1367
|
+
name: tool.name,
|
|
1368
|
+
...tool.description ? { description: tool.description } : {},
|
|
1369
|
+
...isExpressibleForVendor(portable, GOOGLE_SCHEMA_PROFILE) ? { parameters: sanitizeForVendor(portable, GOOGLE_SCHEMA_PROFILE, tool.name) } : { parametersJsonSchema: relaxRefLoops(portable) }
|
|
1370
|
+
};
|
|
1371
|
+
}) }];
|
|
1159
1372
|
/** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
|
|
1160
1373
|
var GoogleGenAiClient = class {
|
|
1161
1374
|
#config;
|
|
@@ -1268,7 +1481,7 @@ var GoogleGenAiClient = class {
|
|
|
1268
1481
|
}
|
|
1269
1482
|
if (!res.ok) {
|
|
1270
1483
|
const detail = await res.text().catch(() => "");
|
|
1271
|
-
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}
|
|
1484
|
+
throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
|
|
1272
1485
|
}
|
|
1273
1486
|
return res;
|
|
1274
1487
|
}
|
|
@@ -1682,6 +1895,7 @@ const ASK_USER_INPUT_SCHEMA = {
|
|
|
1682
1895
|
type: "string",
|
|
1683
1896
|
description: "Help text shown under the input."
|
|
1684
1897
|
},
|
|
1898
|
+
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." },
|
|
1685
1899
|
options: {
|
|
1686
1900
|
type: "array",
|
|
1687
1901
|
items: {
|
|
@@ -1725,6 +1939,78 @@ const ASK_USER_TOOL = {
|
|
|
1725
1939
|
source: "builtin"
|
|
1726
1940
|
};
|
|
1727
1941
|
/**
|
|
1942
|
+
* 产物 metadata 的准入与判读(分册 18)。
|
|
1943
|
+
*
|
|
1944
|
+
* metadata 是能力桥上第一个**开放结构**的入参,且来自沙箱内的技能脚本。
|
|
1945
|
+
* 校验落在 `createScriptContext.writeArtifact`——它是 in-process 与三个沙箱执行器
|
|
1946
|
+
* 唯一的汇聚点,在此抛错才能带上正确的桥请求 id 回给脚本(DV-10)。
|
|
1947
|
+
*/
|
|
1948
|
+
/** 序列化后的字节上限;`index.json` 是整覆盖原子写,无上限等于廉价的放大面 */
|
|
1949
|
+
const ARTIFACT_METADATA_MAX_BYTES = 4096;
|
|
1950
|
+
/** 原型污染键;JSON 解析产生的同名键是**自有键**,可被 getOwnPropertyNames 查出 */
|
|
1951
|
+
const FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
|
|
1952
|
+
"__proto__",
|
|
1953
|
+
"constructor",
|
|
1954
|
+
"prototype"
|
|
1955
|
+
]);
|
|
1956
|
+
function isPlainObject$1(value) {
|
|
1957
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1958
|
+
const proto = Object.getPrototypeOf(value);
|
|
1959
|
+
return proto === Object.prototype || proto === null;
|
|
1960
|
+
}
|
|
1961
|
+
/** 函数声明而非箭头函数赋值:只有前者能让 TS 的控制流分析认到 never 终止 */
|
|
1962
|
+
function reject$1(message) {
|
|
1963
|
+
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* 递归拒绝原型污染键与不可序列化值。
|
|
1967
|
+
* `JSON.stringify` 会**静默丢弃**函数与 symbol,而静默正是本需求明令禁止的形态,
|
|
1968
|
+
* 所以必须在序列化之前显式查一遍。
|
|
1969
|
+
*/
|
|
1970
|
+
function assertSerializableShape(value, path, seen) {
|
|
1971
|
+
const kind = typeof value;
|
|
1972
|
+
if (kind === "function" || kind === "symbol" || kind === "bigint") reject$1(`Artifact metadata value at "${path}" is not JSON-serializable (${kind})`);
|
|
1973
|
+
if (kind !== "object" || value === null) return;
|
|
1974
|
+
const container = value;
|
|
1975
|
+
if (seen.has(container)) reject$1(`Artifact metadata contains a circular reference at "${path}"`);
|
|
1976
|
+
seen.add(container);
|
|
1977
|
+
if (Array.isArray(value)) value.forEach((item, index) => assertSerializableShape(item, `${path}[${index}]`, seen));
|
|
1978
|
+
else {
|
|
1979
|
+
if (!isPlainObject$1(value)) reject$1(`Artifact metadata value at "${path}" must be a plain object or array`);
|
|
1980
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
1981
|
+
if (FORBIDDEN_KEYS.has(key)) reject$1(`Artifact metadata must not contain the reserved key "${key}" (at "${path}")`);
|
|
1982
|
+
assertSerializableShape(value[key], `${path}.${key}`, seen);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
seen.delete(container);
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* 准入校验。通过后返回 `JSON.parse(JSON.stringify(...))` 的副本:
|
|
1989
|
+
* in-process 路径由此获得与 JSON stdio 沙箱路径完全相同的语义,
|
|
1990
|
+
* 而不是依赖某条传输链路碰巧做了序列化。
|
|
1991
|
+
*/
|
|
1992
|
+
function admitArtifactMetadata(value) {
|
|
1993
|
+
if (!isPlainObject$1(value)) reject$1("Artifact metadata must be a plain object");
|
|
1994
|
+
assertSerializableShape(value, "metadata", /* @__PURE__ */ new Set());
|
|
1995
|
+
let json;
|
|
1996
|
+
try {
|
|
1997
|
+
json = JSON.stringify(value);
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
reject$1(`Artifact metadata is not JSON-serializable: ${error.message}`);
|
|
2000
|
+
}
|
|
2001
|
+
const bytes = new TextEncoder().encode(json).length;
|
|
2002
|
+
if (bytes > 4096) reject$1(`Artifact metadata is too large: ${bytes} bytes exceeds the ${ARTIFACT_METADATA_MAX_BYTES} byte limit`);
|
|
2003
|
+
return JSON.parse(json);
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* 「这份产物不出自动下载卡」的判读。**严格 `=== false`**:
|
|
2007
|
+
* 隐式真值转换会让写错的键值静默生效,而隐藏是不可见的失败。
|
|
2008
|
+
* `buildRenderResult` 与 agentLoop 的去重集合共用本函数,两处判据不得分叉。
|
|
2009
|
+
*/
|
|
2010
|
+
function isArtifactCardHidden(artifact) {
|
|
2011
|
+
return artifact.metadata?.["resultCard"] === false;
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
1728
2014
|
* 越权 / 绝对路径 / 父级遍历的技能目录读取:策略拒绝(FS_PERMISSION_DENIED,
|
|
1729
2015
|
* 计入 POLICY_DENIAL_CODES → 不计入隔离失败计数),
|
|
1730
2016
|
* 与「目录内路径确实不存在 → FS_NOT_FOUND」区分(0.9.0 分册 14,UX-04)。
|
|
@@ -1762,16 +2048,19 @@ function createScriptContext(deps) {
|
|
|
1762
2048
|
return readFs.readBinary(resolveSkillDir(skillRoot, "assets", relativePath));
|
|
1763
2049
|
},
|
|
1764
2050
|
async writeArtifact(path, content, options) {
|
|
2051
|
+
const metadata = options?.metadata === void 0 ? void 0 : admitArtifactMetadata(options.metadata);
|
|
1765
2052
|
const artifact = typeof content === "string" ? await artifactStore.createTextArtifact({
|
|
1766
2053
|
runId,
|
|
1767
2054
|
path,
|
|
1768
2055
|
content,
|
|
1769
|
-
mimeType: options?.mimeType
|
|
2056
|
+
mimeType: options?.mimeType,
|
|
2057
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1770
2058
|
}) : await artifactStore.createBinaryArtifact({
|
|
1771
2059
|
runId,
|
|
1772
2060
|
path,
|
|
1773
2061
|
content,
|
|
1774
|
-
mimeType: options?.mimeType
|
|
2062
|
+
mimeType: options?.mimeType,
|
|
2063
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1775
2064
|
});
|
|
1776
2065
|
onArtifactCreated?.(artifact);
|
|
1777
2066
|
return artifact;
|
|
@@ -1826,12 +2115,15 @@ function buildRenderResult(run, output, renderBlocks = []) {
|
|
|
1826
2115
|
text: output
|
|
1827
2116
|
});
|
|
1828
2117
|
}
|
|
1829
|
-
for (const artifact of run.artifacts)
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2118
|
+
for (const artifact of run.artifacts) {
|
|
2119
|
+
if (isArtifactCardHidden(artifact)) continue;
|
|
2120
|
+
blocks.push({
|
|
2121
|
+
type: "file",
|
|
2122
|
+
path: artifact.path,
|
|
2123
|
+
...artifact.mimeType ? { mimeType: artifact.mimeType } : {},
|
|
2124
|
+
size: artifact.size
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
1835
2127
|
return {
|
|
1836
2128
|
runId: run.id,
|
|
1837
2129
|
summary: run.terminationReason,
|
|
@@ -2270,6 +2562,12 @@ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
|
|
|
2270
2562
|
* 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
|
|
2271
2563
|
*/
|
|
2272
2564
|
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.";
|
|
2565
|
+
/**
|
|
2566
|
+
* 按需披露的系统提示(分册 19 / FR-19.7)。
|
|
2567
|
+
* 只在本 run 确实存在按需工具时追加——没有按需工具却说「有些工具没列出来」,
|
|
2568
|
+
* 是在教模型怀疑一个完整的工具表。
|
|
2569
|
+
*/
|
|
2570
|
+
const ON_DEMAND_TOOLS_HINT = "Some tools are disclosed on demand and are not listed above. If a capability you need seems missing, look for a related skill in the catalog and read its SKILL.md first — activating it may reveal additional tools. Do not conclude the capability does not exist.";
|
|
2273
2571
|
const READ_LINKED_DOCUMENT_TOOL_NAME = "read_linked_document";
|
|
2274
2572
|
/**
|
|
2275
2573
|
* 能直接给模型用的 MIME。其余一律拒绝并列出这份清单——
|
|
@@ -2277,14 +2575,15 @@ const READ_LINKED_DOCUMENT_TOOL_NAME = "read_linked_document";
|
|
|
2277
2575
|
*/
|
|
2278
2576
|
const SUPPORTED_DOCUMENT_MIME = {
|
|
2279
2577
|
pdf: "application/pdf",
|
|
2280
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
2578
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2579
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
2281
2580
|
};
|
|
2282
2581
|
/** 引擎侧固定流程(§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.`;
|
|
2582
|
+
const UNSUPPORTED_DOCUMENT_MESSAGE = `Only PDF (${SUPPORTED_DOCUMENT_MIME.pdf}), Word (${SUPPORTED_DOCUMENT_MIME.docx}), Excel (${SUPPORTED_DOCUMENT_MIME.xlsx}) and plain text documents can be read.`;
|
|
2284
2583
|
/**
|
|
2285
2584
|
* 内建工具:读页面链接指向的文档。
|
|
2286
2585
|
*
|
|
2287
|
-
* **始终注册**(只要宿主装配了 reader):docx 与纯文本这条路对所有模型成立。
|
|
2586
|
+
* **始终注册**(只要宿主装配了 reader):docx / xlsx 与纯文本这条路对所有模型成立。
|
|
2288
2587
|
* PDF 目标在模型不支持文档时**取数后拒绝**,而不是入口就拦 ——
|
|
2289
2588
|
* 入口拦会误伤 docx,它抽成文本后根本不需要文档能力。
|
|
2290
2589
|
*/
|
|
@@ -2307,6 +2606,10 @@ function toBase64(bytes) {
|
|
|
2307
2606
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
2308
2607
|
return btoa(binary);
|
|
2309
2608
|
}
|
|
2609
|
+
/** `toBase64` 的逆;分片里的文档要交回抽取器时用(0.13.0 FR-22.3) */
|
|
2610
|
+
function fromBase64(data) {
|
|
2611
|
+
return Uint8Array.from(atob(data), (char) => char.charCodeAt(0));
|
|
2612
|
+
}
|
|
2310
2613
|
/** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
|
|
2311
2614
|
var TraceRecorder = class {
|
|
2312
2615
|
#runId;
|
|
@@ -2391,10 +2694,18 @@ function extractSkillCandidate(data) {
|
|
|
2391
2694
|
* @stable
|
|
2392
2695
|
*/
|
|
2393
2696
|
const DEFAULT_LOOP_LIMITS = {
|
|
2394
|
-
maxTurns:
|
|
2697
|
+
maxTurns: 1e3,
|
|
2395
2698
|
totalTimeoutMs: 36e5,
|
|
2396
|
-
toolTimeoutMs: 6e5
|
|
2699
|
+
toolTimeoutMs: 6e5,
|
|
2700
|
+
maxHistoryMessages: 1e3
|
|
2397
2701
|
};
|
|
2702
|
+
/**
|
|
2703
|
+
* 工具参数留存的单次体积上限(分册 30 / FR-30.2)。**全仓唯一来源**(S8)。
|
|
2704
|
+
*
|
|
2705
|
+
* 超限时不留半截参数,整条标记为 `truncated`。
|
|
2706
|
+
* @stable
|
|
2707
|
+
*/
|
|
2708
|
+
const MAX_TOOL_STEP_ARG_BYTES = 32768;
|
|
2398
2709
|
const RUN_SNAPSHOT_SCHEMA_VERSION = 3;
|
|
2399
2710
|
/** @experimental */
|
|
2400
2711
|
function isUnsupportedRunSnapshot(entry) {
|
|
@@ -2537,6 +2848,93 @@ function evaluateToolAccess(state, llmToolName) {
|
|
|
2537
2848
|
reason: denialReason(state, canonical)
|
|
2538
2849
|
};
|
|
2539
2850
|
}
|
|
2851
|
+
/**
|
|
2852
|
+
* 分册 19 诊断(FR-19.6):某条模式在给定工具名集合里有没有命中过。
|
|
2853
|
+
* 与点名判定分开,是因为这里问的是「作者写的这条规则有没有意义」,不是「某个工具能不能进上下文」。
|
|
2854
|
+
*/
|
|
2855
|
+
function patternMatchesAnyTool(pattern, declaringSkill, llmToolNames, activated) {
|
|
2856
|
+
for (const name of llmToolNames) if (matchesPattern(pattern, declaringSkill, name, canonicalToolName(name, activated))) return true;
|
|
2857
|
+
return false;
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* 分册 19 的点名判定:**有没有某个已激活技能显式声明了这个工具**。
|
|
2861
|
+
*
|
|
2862
|
+
* 与 `evaluateToolAccess` 回答的不是同一个问题(那个回答「允不允许调用」,
|
|
2863
|
+
* 这个回答「要不要写进上下文」),因此故意不复用它,两处关键差异:
|
|
2864
|
+
* 1. 无人声明 ⇒ 这里返回 **false**(没人点名),那里返回 true(不受限);
|
|
2865
|
+
* 2. 不含一票否决——某个技能没写清单,不影响另一个技能点名成功。
|
|
2866
|
+
*/
|
|
2867
|
+
function isNamedByActivatedSkill(state, llmToolName) {
|
|
2868
|
+
const canonical = canonicalToolName(llmToolName, state.activated);
|
|
2869
|
+
for (const [skill, patterns] of state.skillAllowedTools) {
|
|
2870
|
+
if (!state.activated.has(skill)) continue;
|
|
2871
|
+
for (const pattern of patterns) if (matchesPattern(pattern, skill, llmToolName, canonical)) return true;
|
|
2872
|
+
}
|
|
2873
|
+
return false;
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* 参数敏感标注关键字(分册 30 / FR-30.3)。
|
|
2877
|
+
*
|
|
2878
|
+
* 不用 `format: 'password'`:`format` 是 JSON Schema 的规范关键字,provider 侧
|
|
2879
|
+
* 可能据其做转换或校验,而且它只对 `type: 'string'` 有意义,对象型凭据标不了。
|
|
2880
|
+
* `x-` 前缀走 `JsonSchema` 的索引签名,不与任何规范语义抢占。
|
|
2881
|
+
*/
|
|
2882
|
+
const SENSITIVE_ANNOTATION = "x-webskill-sensitive";
|
|
2883
|
+
const isSensitive = (schema) => schema?.[SENSITIVE_ANNOTATION] === true;
|
|
2884
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2885
|
+
const join = (prefix, key) => prefix === "" ? key : `${prefix}.${key}`;
|
|
2886
|
+
/**
|
|
2887
|
+
* 按 schema 标注逐字段脱敏。
|
|
2888
|
+
*
|
|
2889
|
+
* 脱敏值取 `null` 而不是删键,也不是 `'***'`:删键会让生成器以为这个步骤
|
|
2890
|
+
* 本来就没有这个参数,从而写出漏参的技能;掩码则仍然泄露长度与存在性。
|
|
2891
|
+
*/
|
|
2892
|
+
function redactAnnotated(value, schema, path, redacted) {
|
|
2893
|
+
if (isSensitive(schema)) {
|
|
2894
|
+
redacted.push(path);
|
|
2895
|
+
return null;
|
|
2896
|
+
}
|
|
2897
|
+
if (Array.isArray(value)) return value.map((item, i) => redactAnnotated(item, schema?.items, `${path}[${i}]`, redacted));
|
|
2898
|
+
if (isPlainObject(value)) {
|
|
2899
|
+
const out = {};
|
|
2900
|
+
for (const [key, entry] of Object.entries(value)) out[key] = redactAnnotated(entry, schema?.properties?.[key], join(path, key), redacted);
|
|
2901
|
+
return out;
|
|
2902
|
+
}
|
|
2903
|
+
return value;
|
|
2904
|
+
}
|
|
2905
|
+
/** 不可信来源:所有叶子脱敏,键与结构保留 */
|
|
2906
|
+
function redactAll(value, path, redacted) {
|
|
2907
|
+
if (Array.isArray(value)) return value.map((item, i) => redactAll(item, `${path}[${i}]`, redacted));
|
|
2908
|
+
if (isPlainObject(value)) {
|
|
2909
|
+
const out = {};
|
|
2910
|
+
for (const [key, entry] of Object.entries(value)) out[key] = redactAll(entry, join(path, key), redacted);
|
|
2911
|
+
return out;
|
|
2912
|
+
}
|
|
2913
|
+
redacted.push(path);
|
|
2914
|
+
return null;
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* 分层脱敏(FR-30.3)——**全仓唯一实现**。
|
|
2918
|
+
*
|
|
2919
|
+
* 1. `untrusted` → 所有字段脱敏;
|
|
2920
|
+
* 2. `reviewed` / `host-trusted` → 逐字段走 schema,标注为敏感的脱敏。
|
|
2921
|
+
*
|
|
2922
|
+
* 标注为敏感的字段在**任何**层级都脱敏:信任声明说的是「未标注的可以留」,
|
|
2923
|
+
* 不是「标注了也能留」。
|
|
2924
|
+
*/
|
|
2925
|
+
function redactToolStepArgs(args, schema, trust) {
|
|
2926
|
+
const redacted = [];
|
|
2927
|
+
const redactedArgs = trust.tier === "untrusted" ? redactAll(args, "", redacted) : redactAnnotated(args, schema, "", redacted);
|
|
2928
|
+
if (JSON.stringify(redactedArgs).length > 32768) return {
|
|
2929
|
+
args: {},
|
|
2930
|
+
redacted: [],
|
|
2931
|
+
truncated: true
|
|
2932
|
+
};
|
|
2933
|
+
return {
|
|
2934
|
+
args: redactedArgs,
|
|
2935
|
+
redacted
|
|
2936
|
+
};
|
|
2937
|
+
}
|
|
2540
2938
|
const MAX_SURFACE_PATCHES_PER_SECOND = 240;
|
|
2541
2939
|
/**
|
|
2542
2940
|
* 一轮 LLM 调用可能抛出、且**值得原样上报**的结构化码。
|
|
@@ -2548,6 +2946,30 @@ const LLM_TURN_CODES = /* @__PURE__ */ new Set([
|
|
|
2548
2946
|
"LLM_REQUEST_FAILED",
|
|
2549
2947
|
"TOOL_SCHEMA_UNAVAILABLE"
|
|
2550
2948
|
]);
|
|
2949
|
+
/**
|
|
2950
|
+
* 模型侧图片通道收得下的位图格式(三家 provider 的交集)。
|
|
2951
|
+
* SVG 是文本、`application/octet-stream` 是未知字节——两者编成 data URL 发出去,
|
|
2952
|
+
* 端点当场 400(LM Studio:`'url' field must be a base64 encoded image.`),
|
|
2953
|
+
* 页面里一个内联图标就能把整次 run 打死。这不是「预判模型能不能读」,
|
|
2954
|
+
* 是这些字节根本进不了图片分片。
|
|
2955
|
+
*/
|
|
2956
|
+
const MODEL_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
2957
|
+
"image/png",
|
|
2958
|
+
"image/jpeg",
|
|
2959
|
+
"image/webp",
|
|
2960
|
+
"image/gif"
|
|
2961
|
+
]);
|
|
2962
|
+
/** 分片是不是 PDF;mime 允许带参数与大小写差异,与 FR-11.1 的判定同口径 */
|
|
2963
|
+
const isPdfPart = (part) => part.type === "file" && part.mimeType.split(";")[0]?.trim().toLowerCase() === SUPPORTED_DOCUMENT_MIME.pdf;
|
|
2964
|
+
/**
|
|
2965
|
+
* LLM 客户端在 HTTP 失败时放进 `details` 的状态码(FR-22.2)。
|
|
2966
|
+
* 判不出来返回 undefined——网络层异常没有状态码,回退对它没有意义。
|
|
2967
|
+
*/
|
|
2968
|
+
const httpStatusOf = (e) => {
|
|
2969
|
+
if (!(e instanceof WebSkillError) || typeof e.details !== "object" || e.details === null) return void 0;
|
|
2970
|
+
const status = e.details.status;
|
|
2971
|
+
return typeof status === "number" ? status : void 0;
|
|
2972
|
+
};
|
|
2551
2973
|
/** 交互终态(取消/超时):从工具执行深处直接终止 run */
|
|
2552
2974
|
var RunTerminated = class extends Error {
|
|
2553
2975
|
outcome;
|
|
@@ -2573,6 +2995,21 @@ const summarizeArgs = (args) => {
|
|
|
2573
2995
|
return json.length > 100 ? `${json.slice(0, 100)}…` : json;
|
|
2574
2996
|
};
|
|
2575
2997
|
/**
|
|
2998
|
+
* 图片分片投不出去时换成一条说明(与超预算文档同一处置),多张合并成一条——
|
|
2999
|
+
* 一个页面上十几个内联图标各报一遍会把工具结果淹掉。
|
|
3000
|
+
* 静默丢掉会让模型以为自己看过了;直接发出去则整次请求 400,一张图连累全程。
|
|
3001
|
+
*/
|
|
3002
|
+
const undeliverableImageNote = (parts, toolName) => {
|
|
3003
|
+
const formats = [...new Set(parts.map((p) => p.type === "image" ? p.mimeType : ""))].join(", ");
|
|
3004
|
+
return `${parts.length === 1 ? "1 image" : `${parts.length} images`} from "${toolName}" ${parts.length === 1 ? "was" : "were"} not attached: ${formats} cannot be sent as model image input, which accepts only ${[...MODEL_IMAGE_MIME_TYPES].join(", ")}. Rely on the surrounding text, or ask for a raster version.`;
|
|
3005
|
+
};
|
|
3006
|
+
const isDeliverableImage = (part) => part.type !== "image" || MODEL_IMAGE_MIME_TYPES.has(part.mimeType.split(";")[0]?.trim().toLowerCase() ?? "");
|
|
3007
|
+
/** 外部工具的 inputSchema 索引(分册 30);run 开始与 resume 各重建一次 */
|
|
3008
|
+
function indexExternalToolSchemas(state, specs) {
|
|
3009
|
+
state.externalToolSchemas.clear();
|
|
3010
|
+
for (const spec of specs) state.externalToolSchemas.set(spec.name, spec.inputSchema);
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
2576
3013
|
* file-pick 的回填值带整个文件的 base64,落进 `paramHistory` 会把用户选的文件
|
|
2577
3014
|
* 原样长期留在记忆里(体积与隐私都不可接受)。只留可辨识的描述,正文不留。
|
|
2578
3015
|
*/
|
|
@@ -2697,6 +3134,9 @@ var AgentLoop = class {
|
|
|
2697
3134
|
activatedTools: /* @__PURE__ */ new Map(),
|
|
2698
3135
|
skillAllowedTools: /* @__PURE__ */ new Map(),
|
|
2699
3136
|
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
3137
|
+
externalToolSources: /* @__PURE__ */ new Map(),
|
|
3138
|
+
withheldTools: /* @__PURE__ */ new Set(),
|
|
3139
|
+
pdfFallbackDone: false,
|
|
2700
3140
|
integrityVerdicts: /* @__PURE__ */ new Map(),
|
|
2701
3141
|
toolTimeoutMs: this.#config.toolTimeoutMs,
|
|
2702
3142
|
now,
|
|
@@ -2717,6 +3157,7 @@ var AgentLoop = class {
|
|
|
2717
3157
|
pausedMs: 0,
|
|
2718
3158
|
maxTurns: this.#config.maxTurns,
|
|
2719
3159
|
totalTimeoutMs: this.#config.totalTimeoutMs,
|
|
3160
|
+
externalToolSchemas: /* @__PURE__ */ new Map(),
|
|
2720
3161
|
controller: new AbortController()
|
|
2721
3162
|
};
|
|
2722
3163
|
this.#controllers.set(runId, state.controller);
|
|
@@ -2736,14 +3177,8 @@ var AgentLoop = class {
|
|
|
2736
3177
|
candidates: route.catalog.entries.map((e) => e.name)
|
|
2737
3178
|
}
|
|
2738
3179
|
}, state);
|
|
2739
|
-
const externalSpecs = this.#config.toolCallingDisabled ? [] :
|
|
2740
|
-
|
|
2741
|
-
return await source.listToolSpecs();
|
|
2742
|
-
} catch (e) {
|
|
2743
|
-
trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
|
|
2744
|
-
return [];
|
|
2745
|
-
}
|
|
2746
|
-
}))).flat();
|
|
3180
|
+
const externalSpecs = this.#config.toolCallingDisabled ? [] : await this.#collectExternalSpecs(state);
|
|
3181
|
+
indexExternalToolSchemas(state, externalSpecs);
|
|
2747
3182
|
const externalSystemPrompts = [];
|
|
2748
3183
|
if (!this.#config.toolCallingDisabled) for (const source of this.#deps.externalTools ?? []) {
|
|
2749
3184
|
if (source.systemPrompt === void 0) continue;
|
|
@@ -2754,7 +3189,19 @@ var AgentLoop = class {
|
|
|
2754
3189
|
trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
|
|
2755
3190
|
}
|
|
2756
3191
|
}
|
|
2757
|
-
const
|
|
3192
|
+
const hasOnDemand = !this.#config.toolCallingDisabled && externalSpecs.some((spec) => {
|
|
3193
|
+
const source = state.externalToolSources.get(spec.name);
|
|
3194
|
+
try {
|
|
3195
|
+
return source?.disclosure?.(spec.name) === "on-demand";
|
|
3196
|
+
} catch {
|
|
3197
|
+
return false;
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3200
|
+
const systemPrompt = this.#config.toolCallingDisabled ? PLAIN_CHAT_SYSTEM_PROMPT : [
|
|
3201
|
+
route.systemPrompt,
|
|
3202
|
+
...externalSystemPrompts,
|
|
3203
|
+
...hasOnDemand ? [ON_DEMAND_TOOLS_HINT] : []
|
|
3204
|
+
].join("\n\n");
|
|
2758
3205
|
const profileMessage = await this.#userProfileMessage(state);
|
|
2759
3206
|
state.messages = [
|
|
2760
3207
|
{
|
|
@@ -2813,6 +3260,44 @@ var AgentLoop = class {
|
|
|
2813
3260
|
}
|
|
2814
3261
|
return false;
|
|
2815
3262
|
}
|
|
3263
|
+
/**
|
|
3264
|
+
* 分册 19 暴露点:外部工具本轮是否披露。
|
|
3265
|
+
*
|
|
3266
|
+
* 与 `#checkToolAccess` 是两个问题:那个回答「允不允许调用」,这个只回答「要不要写进上下文」。
|
|
3267
|
+
* 未披露**不影响可执行性**——分发点不看它,模型硬调照样执行(FR-19.5)。
|
|
3268
|
+
*/
|
|
3269
|
+
#disclosed(state, llmToolName) {
|
|
3270
|
+
const source = state.externalToolSources.get(llmToolName);
|
|
3271
|
+
if (source?.disclosure === void 0) return true;
|
|
3272
|
+
let level;
|
|
3273
|
+
try {
|
|
3274
|
+
level = source.disclosure(llmToolName);
|
|
3275
|
+
} catch (e) {
|
|
3276
|
+
state.trace.record("run.warning", { message: `External tool source "${source.kind}" failed to report a disclosure level for "${llmToolName}"; treating it as always disclosed: ${messageOf(e)}` });
|
|
3277
|
+
return true;
|
|
3278
|
+
}
|
|
3279
|
+
if (level !== "on-demand") return true;
|
|
3280
|
+
if (isNamedByActivatedSkill(state, llmToolName)) return true;
|
|
3281
|
+
if (!state.withheldTools.has(llmToolName)) {
|
|
3282
|
+
state.withheldTools.add(llmToolName);
|
|
3283
|
+
state.trace.record("tool.withheld", {
|
|
3284
|
+
message: `Tool "${llmToolName}" is disclosed on demand and was not requested by any activated skill`,
|
|
3285
|
+
data: { name: llmToolName }
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
return false;
|
|
3289
|
+
}
|
|
3290
|
+
/**
|
|
3291
|
+
* FR-19.6:技能声明的外部工具模式一个已知工具都没命中时告警。
|
|
3292
|
+
* 端点名拼错是最常见的失误,而它的表现是「工具静静地不出现」——不告警就无从排查。
|
|
3293
|
+
*/
|
|
3294
|
+
#warnUnmatchedToolPatterns(state, skillName, patterns) {
|
|
3295
|
+
for (const pattern of patterns) {
|
|
3296
|
+
if (!pattern.includes(":") && !pattern.includes("#")) continue;
|
|
3297
|
+
if (patternMatchesAnyTool(pattern, skillName, state.externalToolSources.keys(), state.activated)) continue;
|
|
3298
|
+
state.trace.record("run.warning", { message: `Skill "${skillName}" declares allowed-tools pattern "${pattern}", but it matches no known tool.` });
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
2816
3301
|
/** 分发点被拒时回喂给模型的结构化错误(不抛异常:模型造名字是常态,抛异常会终止整个 run) */
|
|
2817
3302
|
#deniedToolError(state, toolName) {
|
|
2818
3303
|
return toolError("TOOL_NOT_ALLOWED", evaluateToolAccess(state, toolName).reason ?? `Tool "${toolName}" is not allowed`);
|
|
@@ -2828,7 +3313,9 @@ var AgentLoop = class {
|
|
|
2828
3313
|
state.turn = turn;
|
|
2829
3314
|
if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
|
|
2830
3315
|
if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
2831
|
-
const
|
|
3316
|
+
const scriptToolSpecs = [...state.activatedTools.values()].map(toLlmToolSpec).filter((spec) => this.#checkToolAccess(state, spec.name));
|
|
3317
|
+
const externalToolSpecs = externalSpecs.filter((spec) => this.#disclosed(state, spec.name));
|
|
3318
|
+
const skillToolSpecs = [...scriptToolSpecs, ...externalToolSpecs];
|
|
2832
3319
|
const toolSpecs = this.#config.toolCallingDisabled ? [] : [
|
|
2833
3320
|
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
2834
3321
|
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
@@ -2860,8 +3347,12 @@ var AgentLoop = class {
|
|
|
2860
3347
|
if (this.#cancelled.has(state.runId)) return finish("cancelled", "user-cancelled", "Run cancelled by user", "RUN_CANCELLED");
|
|
2861
3348
|
return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
2862
3349
|
}
|
|
3350
|
+
if (await this.#fallbackPdfToText(state, e)) {
|
|
3351
|
+
turn--;
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
2863
3354
|
const code = e instanceof WebSkillError && LLM_TURN_CODES.has(e.code) ? e.code : "LLM_REQUEST_FAILED";
|
|
2864
|
-
const hint = messages.some((message) =>
|
|
3355
|
+
const hint = state.pdfFallbackDone ? " The attached PDF was re-sent as extracted text after the endpoint rejected the original file, and that attempt failed as well." : messages.some((message) => message.content.some(isPdfPart)) ? " 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
3356
|
return finish("failed", "llm-error", `${messageOf(e)}${hint}`, code);
|
|
2866
3357
|
}
|
|
2867
3358
|
if (response.thinking !== void 0 && response.thinking !== "") {
|
|
@@ -3094,6 +3585,9 @@ var AgentLoop = class {
|
|
|
3094
3585
|
activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
|
|
3095
3586
|
skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
|
|
3096
3587
|
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
3588
|
+
externalToolSources: /* @__PURE__ */ new Map(),
|
|
3589
|
+
withheldTools: /* @__PURE__ */ new Set(),
|
|
3590
|
+
pdfFallbackDone: false,
|
|
3097
3591
|
integrityVerdicts: /* @__PURE__ */ new Map(),
|
|
3098
3592
|
toolTimeoutMs: snapshot.config.toolTimeoutMs,
|
|
3099
3593
|
now,
|
|
@@ -3113,6 +3607,7 @@ var AgentLoop = class {
|
|
|
3113
3607
|
pausedMs: snapshot.pausedMs ?? 0,
|
|
3114
3608
|
maxTurns: snapshot.config.maxTurns,
|
|
3115
3609
|
totalTimeoutMs: snapshot.config.totalTimeoutMs,
|
|
3610
|
+
externalToolSchemas: /* @__PURE__ */ new Map(),
|
|
3116
3611
|
controller: new AbortController()
|
|
3117
3612
|
};
|
|
3118
3613
|
this.#controllers.set(runId, state.controller);
|
|
@@ -3126,6 +3621,8 @@ var AgentLoop = class {
|
|
|
3126
3621
|
} });
|
|
3127
3622
|
const pending = pendingInteraction;
|
|
3128
3623
|
const pendingCall = this.#findPendingToolCall(state.messages);
|
|
3624
|
+
const externalSpecs = await this.#collectExternalSpecs(state);
|
|
3625
|
+
indexExternalToolSchemas(state, externalSpecs);
|
|
3129
3626
|
const carried = [];
|
|
3130
3627
|
try {
|
|
3131
3628
|
await this.#replaySurfaceEvents(state);
|
|
@@ -3183,14 +3680,6 @@ var AgentLoop = class {
|
|
|
3183
3680
|
if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
|
|
3184
3681
|
throw e;
|
|
3185
3682
|
}
|
|
3186
|
-
const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
3187
|
-
try {
|
|
3188
|
-
return await source.listToolSpecs();
|
|
3189
|
-
} catch (e) {
|
|
3190
|
-
trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
|
|
3191
|
-
return [];
|
|
3192
|
-
}
|
|
3193
|
-
}))).flat();
|
|
3194
3683
|
try {
|
|
3195
3684
|
return await this.#turnLoop(state, snapshot.turn + 1, externalSpecs);
|
|
3196
3685
|
} catch (e) {
|
|
@@ -3419,6 +3908,84 @@ var AgentLoop = class {
|
|
|
3419
3908
|
});
|
|
3420
3909
|
});
|
|
3421
3910
|
}
|
|
3911
|
+
/** 外部工具 specs:单个来源失败跳过并记 warning(run 开始与 resume 共用) */
|
|
3912
|
+
async #collectExternalSpecs(state) {
|
|
3913
|
+
state.externalToolSources.clear();
|
|
3914
|
+
return (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
3915
|
+
try {
|
|
3916
|
+
const specs = await source.listToolSpecs();
|
|
3917
|
+
for (const spec of specs) state.externalToolSources.set(spec.name, source);
|
|
3918
|
+
return specs;
|
|
3919
|
+
} catch (e) {
|
|
3920
|
+
state.trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
|
|
3921
|
+
return [];
|
|
3922
|
+
}
|
|
3923
|
+
}))).flat();
|
|
3924
|
+
}
|
|
3925
|
+
/**
|
|
3926
|
+
* 一次工具调用的信任层级与 inputSchema(分册 30)。
|
|
3927
|
+
*
|
|
3928
|
+
* 技能脚本工具按 `untrusted`:它们的 schema 来自各技能的 frontmatter,
|
|
3929
|
+
* 是第三方内容,不是本仓审查过的。
|
|
3930
|
+
*/
|
|
3931
|
+
#toolCaptureContext(call, state) {
|
|
3932
|
+
if (call.name === "read_skill_file") return {
|
|
3933
|
+
trust: { tier: "reviewed" },
|
|
3934
|
+
schema: READ_SKILL_FILE_TOOL.inputSchema,
|
|
3935
|
+
args: call.arguments
|
|
3936
|
+
};
|
|
3937
|
+
if (call.name === "ask_user") return {
|
|
3938
|
+
trust: { tier: "reviewed" },
|
|
3939
|
+
schema: ASK_USER_TOOL.inputSchema,
|
|
3940
|
+
args: call.arguments
|
|
3941
|
+
};
|
|
3942
|
+
if (call.name === "read_linked_document") return {
|
|
3943
|
+
trust: { tier: "reviewed" },
|
|
3944
|
+
schema: READ_LINKED_DOCUMENT_TOOL.inputSchema,
|
|
3945
|
+
args: call.arguments
|
|
3946
|
+
};
|
|
3947
|
+
const source = (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
|
|
3948
|
+
if (source === void 0) return {
|
|
3949
|
+
trust: { tier: "untrusted" },
|
|
3950
|
+
args: call.arguments
|
|
3951
|
+
};
|
|
3952
|
+
const schema = state.externalToolSchemas.get(call.name);
|
|
3953
|
+
const trust = source.argCaptureTrust?.(call.name, call.arguments) ?? { tier: "untrusted" };
|
|
3954
|
+
const args = source.captureArgs?.(call.name, call.arguments) ?? call.arguments;
|
|
3955
|
+
return schema === void 0 ? {
|
|
3956
|
+
trust,
|
|
3957
|
+
args
|
|
3958
|
+
} : {
|
|
3959
|
+
trust,
|
|
3960
|
+
schema,
|
|
3961
|
+
args
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
/**
|
|
3965
|
+
* 成功调用的完整参数留存(FR-30.2)。失败的调用不留存:它什么也没做成,
|
|
3966
|
+
* 让模型引用它只会生成一个跑不通的步骤。
|
|
3967
|
+
*/
|
|
3968
|
+
async #captureToolStep(call, state) {
|
|
3969
|
+
const store = this.#deps.toolSteps;
|
|
3970
|
+
if (store === void 0) return;
|
|
3971
|
+
const { trust, schema, args: captured } = this.#toolCaptureContext(call, state);
|
|
3972
|
+
const { args, redacted, truncated } = redactToolStepArgs(captured, schema, trust);
|
|
3973
|
+
try {
|
|
3974
|
+
await store.append({
|
|
3975
|
+
runId: state.runId,
|
|
3976
|
+
sessionId: state.run.sessionId,
|
|
3977
|
+
callId: call.id,
|
|
3978
|
+
tool: call.name,
|
|
3979
|
+
at: state.now(),
|
|
3980
|
+
args,
|
|
3981
|
+
redacted,
|
|
3982
|
+
...truncated !== void 0 ? { truncated } : {},
|
|
3983
|
+
trust
|
|
3984
|
+
});
|
|
3985
|
+
} catch (e) {
|
|
3986
|
+
state.trace.record("run.warning", { message: `Failed to capture tool step arguments: ${messageOf(e)}` });
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3422
3989
|
async #executeCall(call, state) {
|
|
3423
3990
|
const argsSummary = summarizeArgs(call.arguments);
|
|
3424
3991
|
const owner = this.#skillOf(call, state);
|
|
@@ -3436,17 +4003,15 @@ var AgentLoop = class {
|
|
|
3436
4003
|
else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
|
|
3437
4004
|
else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
|
|
3438
4005
|
else if (call.name === "read_linked_document" && this.#deps.linkedDocuments !== void 0) result = await this.#handleReadLinkedDocument(call, state);
|
|
3439
|
-
else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
|
|
3440
4006
|
else {
|
|
3441
4007
|
const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
|
|
3442
|
-
|
|
4008
|
+
const source = resolution.kind === "script" ? void 0 : (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
|
|
4009
|
+
if (source === void 0 && !this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
|
|
4010
|
+
else if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
|
|
4011
|
+
else if (source) result = await source.call(call.name, call.arguments);
|
|
3443
4012
|
else {
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
else {
|
|
3447
|
-
if (resolution.kind === "not-found") state.unknownToolCalls += 1;
|
|
3448
|
-
result = toolError(resolution.code, resolution.message, resolution.kind === "not-found" ? resolution.data : void 0);
|
|
3449
|
-
}
|
|
4013
|
+
if (resolution.kind === "not-found") state.unknownToolCalls += 1;
|
|
4014
|
+
result = toolError(resolution.code, resolution.message, resolution.kind === "not-found" ? resolution.data : void 0);
|
|
3450
4015
|
}
|
|
3451
4016
|
}
|
|
3452
4017
|
const durationMs = Date.parse(state.now()) - callStartMs;
|
|
@@ -3458,6 +4023,7 @@ var AgentLoop = class {
|
|
|
3458
4023
|
durationMs,
|
|
3459
4024
|
...attribution
|
|
3460
4025
|
} });
|
|
4026
|
+
await this.#captureToolStep(call, state);
|
|
3461
4027
|
this.#emitTool(state, "completed", call);
|
|
3462
4028
|
for (const item of result.content) {
|
|
3463
4029
|
if (item.type !== "json") continue;
|
|
@@ -3497,7 +4063,7 @@ var AgentLoop = class {
|
|
|
3497
4063
|
this.#emitTool(state, "failed", call, result.error?.code);
|
|
3498
4064
|
}
|
|
3499
4065
|
for (const artifact of result.artifacts ?? []) {
|
|
3500
|
-
state.artifactPaths.add(artifact.path);
|
|
4066
|
+
if (!isArtifactCardHidden(artifact)) state.artifactPaths.add(artifact.path);
|
|
3501
4067
|
state.trace.record("artifact.created", { data: {
|
|
3502
4068
|
artifactId: artifact.id,
|
|
3503
4069
|
path: artifact.path
|
|
@@ -3575,7 +4141,7 @@ var AgentLoop = class {
|
|
|
3575
4141
|
async #stripDuplicateFileLinks(state, event) {
|
|
3576
4142
|
if (state.artifactPaths.size === 0) {
|
|
3577
4143
|
const listed = await this.#deps.artifactStore.listArtifacts(state.runId).catch(() => []);
|
|
3578
|
-
for (const artifact of listed) state.artifactPaths.add(artifact.path);
|
|
4144
|
+
for (const artifact of listed) if (!isArtifactCardHidden(artifact)) state.artifactPaths.add(artifact.path);
|
|
3579
4145
|
}
|
|
3580
4146
|
if (state.artifactPaths.size === 0) return event;
|
|
3581
4147
|
const paths = state.artifactPaths;
|
|
@@ -3836,17 +4402,20 @@ var AgentLoop = class {
|
|
|
3836
4402
|
};
|
|
3837
4403
|
}
|
|
3838
4404
|
let text;
|
|
3839
|
-
if (mime === SUPPORTED_DOCUMENT_MIME.docx) {
|
|
3840
|
-
|
|
4405
|
+
if (mime === SUPPORTED_DOCUMENT_MIME.docx || mime === SUPPORTED_DOCUMENT_MIME.xlsx) {
|
|
4406
|
+
const docx = mime === SUPPORTED_DOCUMENT_MIME.docx;
|
|
4407
|
+
const kind = docx ? "docx" : "xlsx";
|
|
4408
|
+
const extractor = docx ? this.#deps.docxExtractor : this.#deps.xlsxExtractor;
|
|
4409
|
+
if (extractor === void 0) {
|
|
3841
4410
|
await audit({
|
|
3842
4411
|
...record,
|
|
3843
4412
|
ok: false,
|
|
3844
|
-
reason:
|
|
4413
|
+
reason: `no ${kind} extractor`
|
|
3845
4414
|
});
|
|
3846
|
-
return toolError("TOOL_UNSUPPORTED", "Word documents cannot be read in this environment: no
|
|
4415
|
+
return toolError("TOOL_UNSUPPORTED", `${docx ? "Word documents" : "Excel workbooks"} cannot be read in this environment: no ${kind} text extractor is configured.`);
|
|
3847
4416
|
}
|
|
3848
4417
|
try {
|
|
3849
|
-
text = await
|
|
4418
|
+
text = await extractor(fetched.bytes);
|
|
3850
4419
|
} catch (e) {
|
|
3851
4420
|
const message = messageOf(e);
|
|
3852
4421
|
await audit({
|
|
@@ -3956,6 +4525,7 @@ var AgentLoop = class {
|
|
|
3956
4525
|
type,
|
|
3957
4526
|
...field["required"] === true ? { required: true } : {},
|
|
3958
4527
|
...typeof field["description"] === "string" ? { description: field["description"] } : {},
|
|
4528
|
+
...field["defaultValue"] !== void 0 ? { defaultValue: field["defaultValue"] } : {},
|
|
3959
4529
|
...Array.isArray(options) ? { options: options.filter((o) => typeof o === "object" && o !== null).map((o) => ({
|
|
3960
4530
|
label: String(o["label"] ?? o["value"]),
|
|
3961
4531
|
value: o["value"]
|
|
@@ -4056,6 +4626,57 @@ var AgentLoop = class {
|
|
|
4056
4626
|
return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
|
|
4057
4627
|
}
|
|
4058
4628
|
/**
|
|
4629
|
+
* 端点拒收 PDF 后,把消息里的 PDF 分片换成抽取文本,让本轮可以重发(FR-22.2 / FR-22.3)。
|
|
4630
|
+
* 返回 true 表示已改写、调用方应重试本轮;返回 false 表示走原有失败路径。
|
|
4631
|
+
*/
|
|
4632
|
+
async #fallbackPdfToText(state, cause) {
|
|
4633
|
+
const extract = this.#deps.pdfExtractor;
|
|
4634
|
+
if (extract === void 0 || state.pdfFallbackDone) return false;
|
|
4635
|
+
const status = httpStatusOf(cause);
|
|
4636
|
+
if (status === void 0 || status < 400 || status >= 500) return false;
|
|
4637
|
+
if (!state.messages.some((message) => message.content.some(isPdfPart))) return false;
|
|
4638
|
+
state.pdfFallbackDone = true;
|
|
4639
|
+
let converted = 0;
|
|
4640
|
+
for (const message of state.messages) for (let i = 0; i < message.content.length; i++) {
|
|
4641
|
+
const part = message.content[i];
|
|
4642
|
+
if (part === void 0 || !isPdfPart(part)) continue;
|
|
4643
|
+
message.content[i] = {
|
|
4644
|
+
type: "text",
|
|
4645
|
+
text: await this.#pdfAsText(state, part, extract)
|
|
4646
|
+
};
|
|
4647
|
+
converted++;
|
|
4648
|
+
}
|
|
4649
|
+
state.trace.record("run.warning", {
|
|
4650
|
+
message: `The endpoint rejected ${converted} attached PDF file(s) with HTTP ${status}; they were replaced with extracted text and the turn was retried. Original error: ${messageOf(cause)}`,
|
|
4651
|
+
data: {
|
|
4652
|
+
converted,
|
|
4653
|
+
status
|
|
4654
|
+
}
|
|
4655
|
+
});
|
|
4656
|
+
return converted > 0;
|
|
4657
|
+
}
|
|
4658
|
+
/** 单个 PDF 分片 → 文本;包装形态与 chatEngine 的附件标签同口径(FR-22.3 / FR-22.4) */
|
|
4659
|
+
async #pdfAsText(state, part, extract) {
|
|
4660
|
+
const name = part.name ?? "document.pdf";
|
|
4661
|
+
const label = (note) => `--- Attachment: ${name} (${SUPPORTED_DOCUMENT_MIME.pdf}, ${note}) ---\n`;
|
|
4662
|
+
let text;
|
|
4663
|
+
try {
|
|
4664
|
+
text = await extract(fromBase64(part.data));
|
|
4665
|
+
} catch (e) {
|
|
4666
|
+
return `${label("text extraction failed")}${messageOf(e)}`;
|
|
4667
|
+
}
|
|
4668
|
+
if (text.trim() === "") return `${label("no extractable text")}This PDF has no text layer, so its contents could not be extracted. It is most likely a scan or an image-only export. Tell the user this instead of guessing what it contains.`;
|
|
4669
|
+
const id = `doc-${state.runId}-${++state.documentSeq}`;
|
|
4670
|
+
const tooLarge = this.#documentTooLarge({
|
|
4671
|
+
type: "document-text",
|
|
4672
|
+
text,
|
|
4673
|
+
name,
|
|
4674
|
+
id
|
|
4675
|
+
});
|
|
4676
|
+
if (tooLarge !== void 0) return `${label("not attached")}${tooLarge}`;
|
|
4677
|
+
return `${label("extracted text")}${text}`;
|
|
4678
|
+
}
|
|
4679
|
+
/**
|
|
4059
4680
|
* 超预算的文档分片换成一条说明(FR-23.4)。**不截断**:
|
|
4060
4681
|
* 半份 PDF 是坏文件,半份抽取文本会让模型以为自己读全了。
|
|
4061
4682
|
*/
|
|
@@ -4099,6 +4720,7 @@ var AgentLoop = class {
|
|
|
4099
4720
|
};
|
|
4100
4721
|
const notes = [];
|
|
4101
4722
|
const carried = [];
|
|
4723
|
+
const undeliverable = [];
|
|
4102
4724
|
for (const part of passthrough) {
|
|
4103
4725
|
const tooLarge = this.#documentTooLarge(part);
|
|
4104
4726
|
if (tooLarge) {
|
|
@@ -4106,6 +4728,10 @@ var AgentLoop = class {
|
|
|
4106
4728
|
notes.push(tooLarge);
|
|
4107
4729
|
continue;
|
|
4108
4730
|
}
|
|
4731
|
+
if (!isDeliverableImage(part)) {
|
|
4732
|
+
undeliverable.push(part);
|
|
4733
|
+
continue;
|
|
4734
|
+
}
|
|
4109
4735
|
if (part.type === "image") carried.push({
|
|
4110
4736
|
type: "image",
|
|
4111
4737
|
mimeType: part.mimeType,
|
|
@@ -4119,6 +4745,11 @@ var AgentLoop = class {
|
|
|
4119
4745
|
});
|
|
4120
4746
|
else if (part.type === "document-text") notes.push(part.text);
|
|
4121
4747
|
}
|
|
4748
|
+
if (undeliverable.length > 0) {
|
|
4749
|
+
const note = undeliverableImageNote(undeliverable, call.name);
|
|
4750
|
+
state.trace.record("run.warning", { message: note });
|
|
4751
|
+
notes.push(note);
|
|
4752
|
+
}
|
|
4122
4753
|
return {
|
|
4123
4754
|
tool: [
|
|
4124
4755
|
{
|
|
@@ -4235,6 +4866,7 @@ var AgentLoop = class {
|
|
|
4235
4866
|
if (rawAllowed !== void 0) if (Array.isArray(rawAllowed)) {
|
|
4236
4867
|
allowedTools = rawAllowed.filter((e) => typeof e === "string");
|
|
4237
4868
|
state.skillAllowedTools.set(skillName, allowedTools);
|
|
4869
|
+
this.#warnUnmatchedToolPatterns(state, skillName, allowedTools);
|
|
4238
4870
|
} else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
|
|
4239
4871
|
} catch (e) {
|
|
4240
4872
|
state.trace.record("run.warning", { message: `Failed to read or parse SKILL.md of skill "${skillName}": ${messageOf(e)}` });
|
|
@@ -4719,12 +5351,12 @@ var WebSkillRuntime = class {
|
|
|
4719
5351
|
* 多会话:session 对象的 run(prompt) 跨 run 延续消息历史(同一 session 对话上下文连续)。
|
|
4720
5352
|
* 既有 runtime.run(prompt) 保持无状态单次语义不变。
|
|
4721
5353
|
* 同 handle 并发 run 经 per-handle 队列串行化(防历史 last-writer-wins 丢轮次);
|
|
4722
|
-
* maxHistoryMessages
|
|
5354
|
+
* maxHistoryMessages(默认取 `DEFAULT_LOOP_LIMITS`)超出时滚动裁剪中段(保留首尾;边界对齐 tool 契约)。
|
|
4723
5355
|
*/
|
|
4724
5356
|
createSession(options = {}) {
|
|
4725
5357
|
const sessionId = options.sessionId ?? `session-${Math.random().toString(36).slice(2, 10)}`;
|
|
4726
5358
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4727
|
-
const maxHistory = options.maxHistoryMessages ??
|
|
5359
|
+
const maxHistory = options.maxHistoryMessages ?? DEFAULT_LOOP_LIMITS.maxHistoryMessages;
|
|
4728
5360
|
let history = [];
|
|
4729
5361
|
let queue = Promise.resolve();
|
|
4730
5362
|
return {
|
|
@@ -4791,12 +5423,15 @@ var WebSkillRuntime = class {
|
|
|
4791
5423
|
skillProviders: this.#deps.skillProviders,
|
|
4792
5424
|
catalogFilter: this.#deps.catalogFilter,
|
|
4793
5425
|
snapshotStore: this.#deps.snapshotStore,
|
|
5426
|
+
toolSteps: this.#deps.toolSteps,
|
|
4794
5427
|
skillStateGuard: this.#deps.skillStateGuard,
|
|
4795
5428
|
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
4796
5429
|
skillOutcomeReporter: this.#deps.skillOutcomeReporter,
|
|
4797
5430
|
fetchData: this.#deps.fetchData,
|
|
4798
5431
|
linkedDocuments: this.#deps.linkedDocuments,
|
|
4799
5432
|
docxExtractor: this.#deps.docxExtractor,
|
|
5433
|
+
xlsxExtractor: this.#deps.xlsxExtractor,
|
|
5434
|
+
pdfExtractor: this.#deps.pdfExtractor,
|
|
4800
5435
|
documentAudit: this.#deps.documentAudit
|
|
4801
5436
|
}, this.#deps.config);
|
|
4802
5437
|
const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -4916,12 +5551,15 @@ var WebSkillRuntime = class {
|
|
|
4916
5551
|
skillProviders: this.#deps.skillProviders,
|
|
4917
5552
|
catalogFilter: this.#deps.catalogFilter,
|
|
4918
5553
|
snapshotStore: store,
|
|
5554
|
+
toolSteps: this.#deps.toolSteps,
|
|
4919
5555
|
skillStateGuard: this.#deps.skillStateGuard,
|
|
4920
5556
|
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
4921
5557
|
skillOutcomeReporter: this.#deps.skillOutcomeReporter,
|
|
4922
5558
|
fetchData: this.#deps.fetchData,
|
|
4923
5559
|
linkedDocuments: this.#deps.linkedDocuments,
|
|
4924
5560
|
docxExtractor: this.#deps.docxExtractor,
|
|
5561
|
+
xlsxExtractor: this.#deps.xlsxExtractor,
|
|
5562
|
+
pdfExtractor: this.#deps.pdfExtractor,
|
|
4925
5563
|
documentAudit: this.#deps.documentAudit
|
|
4926
5564
|
}, this.#deps.config);
|
|
4927
5565
|
this.#loops.set(runId, loop);
|
|
@@ -5449,7 +6087,8 @@ function parseBridgeRequest(data) {
|
|
|
5449
6087
|
id,
|
|
5450
6088
|
path,
|
|
5451
6089
|
content,
|
|
5452
|
-
...isNonEmptyString(mimeType) ? { mimeType } : {}
|
|
6090
|
+
...isNonEmptyString(mimeType) ? { mimeType } : {},
|
|
6091
|
+
..."metadata" in data ? { metadata: data["metadata"] } : {}
|
|
5453
6092
|
};
|
|
5454
6093
|
}
|
|
5455
6094
|
case "confirm": return isNonEmptyString(data["message"]) ? {
|
|
@@ -5892,6 +6531,7 @@ function sortByStartedAtDesc(summaries) {
|
|
|
5892
6531
|
function applyFilter(summaries, filter) {
|
|
5893
6532
|
let runs = summaries;
|
|
5894
6533
|
if (filter.status !== void 0 && filter.status !== "") runs = runs.filter((r) => r.status === filter.status);
|
|
6534
|
+
if (filter.sessionId !== void 0 && filter.sessionId !== "") runs = runs.filter((r) => r.sessionId === filter.sessionId);
|
|
5895
6535
|
const q = filter.search?.trim().toLowerCase();
|
|
5896
6536
|
if (q !== void 0 && q !== "") runs = runs.filter((r) => r.runId.toLowerCase().includes(q) || r.activeSkills.some((s) => s.toLowerCase().includes(q)));
|
|
5897
6537
|
return runs;
|
|
@@ -6151,6 +6791,51 @@ var FsSessionStore = class {
|
|
|
6151
6791
|
});
|
|
6152
6792
|
}
|
|
6153
6793
|
};
|
|
6794
|
+
/**
|
|
6795
|
+
* 按会话分文件的 append-only JSONL 留存。
|
|
6796
|
+
*
|
|
6797
|
+
* 分文件维度取会话而非 run:读取侧唯一的查询就是「这个会话的全部步骤」,
|
|
6798
|
+
* 按 run 分会让一次生成要开 N 个文件。
|
|
6799
|
+
*/
|
|
6800
|
+
var FsToolStepStore = class {
|
|
6801
|
+
#root;
|
|
6802
|
+
#fs;
|
|
6803
|
+
#onError;
|
|
6804
|
+
constructor(deps) {
|
|
6805
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
6806
|
+
this.#fs = deps.fs;
|
|
6807
|
+
this.#onError = deps.onError ?? ((error, record) => {
|
|
6808
|
+
console.warn(`Failed to persist tool step for call "${record.callId}": ${messageOf(error)}`);
|
|
6809
|
+
});
|
|
6810
|
+
}
|
|
6811
|
+
#path(sessionId) {
|
|
6812
|
+
assertSafePathSegment(sessionId, "session id");
|
|
6813
|
+
return resolveInsideRoot(this.#root, `${sessionId}.jsonl`);
|
|
6814
|
+
}
|
|
6815
|
+
async append(record) {
|
|
6816
|
+
const path = this.#path(record.sessionId);
|
|
6817
|
+
try {
|
|
6818
|
+
await this.#fs.appendText(path, `${JSON.stringify(record)}\n`);
|
|
6819
|
+
} catch (e) {
|
|
6820
|
+
this.#onError(e, record);
|
|
6821
|
+
}
|
|
6822
|
+
}
|
|
6823
|
+
async listBySession(sessionId) {
|
|
6824
|
+
const path = this.#path(sessionId);
|
|
6825
|
+
if (!await this.#fs.exists(path)) return [];
|
|
6826
|
+
const raw = await this.#fs.readText(path);
|
|
6827
|
+
const records = [];
|
|
6828
|
+
for (const line of raw.split("\n")) {
|
|
6829
|
+
if (line.trim() === "") continue;
|
|
6830
|
+
try {
|
|
6831
|
+
records.push(JSON.parse(line));
|
|
6832
|
+
} catch {
|
|
6833
|
+
continue;
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
return records;
|
|
6837
|
+
}
|
|
6838
|
+
};
|
|
6154
6839
|
|
|
6155
6840
|
//#endregion
|
|
6156
|
-
export {
|
|
6841
|
+
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 };
|