@webskill/sdk 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +3 -2
- package/dist/agent.js +3 -1098
- package/dist/browser.d.ts +260 -11
- package/dist/browser.js +772 -52
- package/dist/{catalogComponents-DfxxfUvn-T7Ic8QFV.js → catalogComponents-BFoqpT1v-CjUBZ3bc.js} +1604 -426
- package/dist/{dist-CFmkV45C.js → dist-B-cOu08W.js} +874 -107
- package/dist/{dist-BQe1uglQ.js → dist-DTHZS2k1.js} +524 -30
- package/dist/dist-qnlI2Iup.js +1280 -0
- package/dist/{eventTypes-DbOpAECr-BjcjZVms.js → eventTypes-FllCrX-Z-DNDeHWoG.js} +9 -2
- package/dist/governance.d.ts +33 -6
- package/dist/governance.js +45 -7
- package/dist/{index-znZjobkr.d.ts → index-D3mONFHD.d.ts} +281 -11
- package/dist/{index-DXNTIa-6.d.ts → index-DACk2_XZ.d.ts} +114 -7
- package/dist/{index-lLcCpHE-.d.ts → index-DWbs58LF.d.ts} +234 -14
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +49 -6
- package/dist/mcp.js +146 -38
- package/dist/node.d.ts +3 -3
- package/dist/node.js +52 -2
- package/dist/{openUiLibrary-DURlAxjk-Do_yqg3u.js → openUiLibrary-D5u8oIvx-BLOAQCho.js} +3 -3
- package/dist/processSandboxEntry.js +6 -0
- package/dist/sandboxWorkerEntry.js +6 -0
- package/dist/{skillVersionStore-Bl-ElD45-dMJ8Ybhb.d.ts → skillVersionStore-D-qHk9ZE-BcmFLykd.d.ts} +13 -5
- package/dist/{testing-Csg5ljNm.js → testing-WPTyXQYt.js} +26 -3
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-B3n0cMZu-BDhheIhX.d.ts → types-C26b05fW-CdrRCRDb.d.ts} +55 -6
- package/dist/ui-react.d.ts +11 -2
- package/dist/ui-react.js +141 -42
- 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-DwTwSBFt-BG7kL-Es.js → webskillLitCatalog-DME6PBkV-CmYNLlIT.js} +135 -16
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { K as validateSkills, M as messageOf, P as parseSkillMarkdown, R as renderAvailableSkillsXml, V as resolveInsideRoot, _ as assertSafePathSegment, h as WebSkillError, m as SkillReader, p as SkillDiscovery, v as atomicWriteText, y as buildCatalog } from "./dist-Bev6i6Ip.js";
|
|
1
|
+
import { K as validateSkills, M as messageOf, P as parseSkillMarkdown, R as renderAvailableSkillsXml, V as resolveInsideRoot, _ as assertSafePathSegment, g as assertRemoteUrlAllowed, h as WebSkillError, m as SkillReader, p as SkillDiscovery, v as atomicWriteText, y as buildCatalog } from "./dist-Bev6i6Ip.js";
|
|
2
2
|
import { a as textParts, i as rejectUnsupportedPart, n as partsToText, o as validateLlmMessages, r as promptText, t as MemoryArtifactStore } from "./memoryArtifactStore-52Zn9npI-LbCQaqyx.js";
|
|
3
3
|
|
|
4
4
|
//#region ../runtime/dist/index.js
|
|
@@ -103,11 +103,11 @@ const SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
|
103
103
|
"then",
|
|
104
104
|
"else"
|
|
105
105
|
]);
|
|
106
|
-
function isRecord$
|
|
106
|
+
function isRecord$7(value) {
|
|
107
107
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
108
108
|
}
|
|
109
109
|
function isSelfRef(value) {
|
|
110
|
-
return isRecord$
|
|
110
|
+
return isRecord$7(value) && value["$ref"] === SELF_REF;
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
113
|
* 按 JSON Schema 关键字下降到子 schema。
|
|
@@ -117,7 +117,7 @@ function isSelfRef(value) {
|
|
|
117
117
|
function forEachSubSchema(node, visit) {
|
|
118
118
|
for (const [key, value] of Object.entries(node)) {
|
|
119
119
|
if (SCHEMA_MAP_KEYS.has(key)) {
|
|
120
|
-
if (isRecord$
|
|
120
|
+
if (isRecord$7(value)) for (const name of Object.keys(value)) visit(value[name], [key, name]);
|
|
121
121
|
continue;
|
|
122
122
|
}
|
|
123
123
|
if (SCHEMA_LIST_KEYS.has(key)) {
|
|
@@ -136,7 +136,7 @@ function pathKey(path) {
|
|
|
136
136
|
* `#` 解析到最近的带 `$id` 的祖先;没有则是文档根。
|
|
137
137
|
*/
|
|
138
138
|
function collectSelfRefRoots(node, path, resourceRoot, out) {
|
|
139
|
-
if (!isRecord$
|
|
139
|
+
if (!isRecord$7(node)) return;
|
|
140
140
|
if (isSelfRef(node)) {
|
|
141
141
|
out.set(pathKey(resourceRoot), resourceRoot);
|
|
142
142
|
return;
|
|
@@ -149,7 +149,7 @@ function collectSelfRefRoots(node, path, resourceRoot, out) {
|
|
|
149
149
|
function getAt(schema, path) {
|
|
150
150
|
let current = schema;
|
|
151
151
|
for (const segment of path) if (Array.isArray(current) && typeof segment === "number") current = current[segment];
|
|
152
|
-
else if (isRecord$
|
|
152
|
+
else if (isRecord$7(current) && typeof segment === "string") current = current[segment];
|
|
153
153
|
else return void 0;
|
|
154
154
|
return current;
|
|
155
155
|
}
|
|
@@ -175,7 +175,7 @@ function setAt(schema, path, replacement) {
|
|
|
175
175
|
* 正是严格解析器无法解析 `#` 的直接原因。
|
|
176
176
|
*/
|
|
177
177
|
function rewriteSelfRefs(node, target, depth) {
|
|
178
|
-
if (!isRecord$
|
|
178
|
+
if (!isRecord$7(node)) return node;
|
|
179
179
|
if (depth > 8) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool schema nesting exceeds 8 levels; cannot produce a portable form`);
|
|
180
180
|
if (isSelfRef(node)) {
|
|
181
181
|
const rest = {};
|
|
@@ -188,7 +188,7 @@ function rewriteSelfRefs(node, target, depth) {
|
|
|
188
188
|
const result = {};
|
|
189
189
|
for (const [key, value] of Object.entries(node)) {
|
|
190
190
|
if (key === "$id" || key === "$schema") continue;
|
|
191
|
-
if (SCHEMA_MAP_KEYS.has(key) && isRecord$
|
|
191
|
+
if (SCHEMA_MAP_KEYS.has(key) && isRecord$7(value)) {
|
|
192
192
|
const mapped = {};
|
|
193
193
|
for (const [name, child] of Object.entries(value)) mapped[name] = rewriteSelfRefs(child, target, depth + 1);
|
|
194
194
|
result[key] = mapped;
|
|
@@ -226,8 +226,8 @@ function toPortableToolSchema(schema) {
|
|
|
226
226
|
if (roots.size > 1) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", "Tool schema contains self references in more than one schema resource; cannot produce a portable form");
|
|
227
227
|
const rootPath = [...roots.values()][0] ?? [];
|
|
228
228
|
const resource = getAt(schema, rootPath);
|
|
229
|
-
if (!isRecord$
|
|
230
|
-
const existingDefs = isRecord$
|
|
229
|
+
if (!isRecord$7(resource)) return schema;
|
|
230
|
+
const existingDefs = isRecord$7(schema[DEFS_KEY]) ? schema[DEFS_KEY] : void 0;
|
|
231
231
|
const taken = new Set(Object.keys(existingDefs ?? {}));
|
|
232
232
|
const nodeName = uniqueName(NODE_NAME, taken);
|
|
233
233
|
taken.add(nodeName);
|
|
@@ -258,6 +258,63 @@ function toPortableToolSchema(schema) {
|
|
|
258
258
|
}
|
|
259
259
|
};
|
|
260
260
|
}
|
|
261
|
+
function isRecord$6(value) {
|
|
262
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* 与 `interaction/schemaToForm` 的 file 判定保持一致:JSON Schema 里二进制内容的
|
|
266
|
+
* 两种标准写法(`format: binary` / `contentEncoding: base64`)。
|
|
267
|
+
*/
|
|
268
|
+
function isModelUnfillable(node) {
|
|
269
|
+
return isRecord$6(node) && node["type"] === "string" && (node["format"] === "binary" || node["contentEncoding"] === "base64");
|
|
270
|
+
}
|
|
271
|
+
function stripNode(node) {
|
|
272
|
+
if (Array.isArray(node)) return node.map(stripNode);
|
|
273
|
+
if (!isRecord$6(node)) return node;
|
|
274
|
+
const out = {};
|
|
275
|
+
for (const [key, value] of Object.entries(node)) {
|
|
276
|
+
if (SCHEMA_MAP_KEYS.has(key) && isRecord$6(value)) {
|
|
277
|
+
const mapped = {};
|
|
278
|
+
for (const [name, child] of Object.entries(value)) {
|
|
279
|
+
if (key === "properties" && isModelUnfillable(child)) continue;
|
|
280
|
+
mapped[name] = stripNode(child);
|
|
281
|
+
}
|
|
282
|
+
out[key] = mapped;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
286
|
+
out[key] = value.map(stripNode);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (SCHEMA_KEYS.has(key)) {
|
|
290
|
+
out[key] = stripNode(value);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
out[key] = value;
|
|
294
|
+
}
|
|
295
|
+
const required = out["required"];
|
|
296
|
+
const properties = out["properties"];
|
|
297
|
+
if (Array.isArray(required) && isRecord$6(properties)) out["required"] = required.filter((name) => typeof name !== "string" || name in properties);
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* 把模型不可能自己产出的字段(本地文件二进制)从**出站**工具 schema 里剥离。
|
|
302
|
+
*
|
|
303
|
+
* 模型拿不到用户磁盘上的文件,留着这个字段只有两种下场:模型拒绝调用工具
|
|
304
|
+
* (实测 DeepSeek 直接回文本「请上传文件」,file-pick 永远触发不了),或编造一个
|
|
305
|
+
* 占位字符串把技能跑挂。剥掉后模型以缺参形式调用,引擎的 missing-params 链路
|
|
306
|
+
* 会弹 file-pick 让用户用系统文件选择器补这个字段。
|
|
307
|
+
*
|
|
308
|
+
* 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop——
|
|
309
|
+
* 引擎侧的 missing 校验看的必须是技能作者写的完整 inputSchema。
|
|
310
|
+
*
|
|
311
|
+
* 已知边界(0.10.0 复核注记):只剥 `properties` 里的 inline 二进制字段;
|
|
312
|
+
* 通过 `$ref` 指向 `$defs` 中二进制定义的字段仍会下发(剥掉会留悬空 $ref),
|
|
313
|
+
* 这类技能依旧可能拿不到 file-pick——需要完整修复时再补「剥 $def + 剥指向它的 $ref」。
|
|
314
|
+
*/
|
|
315
|
+
function stripModelUnfillableParams(schema) {
|
|
316
|
+
return stripNode(schema);
|
|
317
|
+
}
|
|
261
318
|
/** 各家都认的基础关键字。Google `FunctionDeclaration.parameters` 是其中最窄的一家。 */
|
|
262
319
|
const BASE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
263
320
|
"type",
|
|
@@ -499,7 +556,7 @@ const toOpenAiTools = (tools) => tools.map((tool) => ({
|
|
|
499
556
|
function: {
|
|
500
557
|
name: tool.name,
|
|
501
558
|
...tool.description ? { description: tool.description } : {},
|
|
502
|
-
parameters: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), OPENAI_SCHEMA_PROFILE, tool.name)
|
|
559
|
+
parameters: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), OPENAI_SCHEMA_PROFILE, tool.name)
|
|
503
560
|
}
|
|
504
561
|
}));
|
|
505
562
|
const errorMessage$2 = (e) => e instanceof Error ? e.message : String(e);
|
|
@@ -539,6 +596,7 @@ var OpenAiCompatibleClient = class {
|
|
|
539
596
|
const reader = res.body.getReader();
|
|
540
597
|
const frames = createSseFrameReader();
|
|
541
598
|
let done = false;
|
|
599
|
+
let usage;
|
|
542
600
|
const handleFrame = function* (data) {
|
|
543
601
|
if (data === "[DONE]") {
|
|
544
602
|
done = true;
|
|
@@ -550,8 +608,17 @@ var OpenAiCompatibleClient = class {
|
|
|
550
608
|
} catch (e) {
|
|
551
609
|
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage$2(e)}`, data);
|
|
552
610
|
}
|
|
611
|
+
const usageRaw = chunk.usage;
|
|
612
|
+
if (usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number")) usage = {
|
|
613
|
+
inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
|
|
614
|
+
outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
|
|
615
|
+
};
|
|
553
616
|
const delta = chunk.choices?.[0]?.delta;
|
|
554
617
|
if (!delta) return;
|
|
618
|
+
if (typeof delta["reasoning_content"] === "string" && delta["reasoning_content"] !== "") yield {
|
|
619
|
+
type: "thinking-delta",
|
|
620
|
+
delta: delta["reasoning_content"]
|
|
621
|
+
};
|
|
555
622
|
if (typeof delta["content"] === "string" && delta["content"] !== "") yield {
|
|
556
623
|
type: "text-delta",
|
|
557
624
|
delta: delta["content"]
|
|
@@ -601,7 +668,10 @@ var OpenAiCompatibleClient = class {
|
|
|
601
668
|
};
|
|
602
669
|
})
|
|
603
670
|
};
|
|
604
|
-
yield {
|
|
671
|
+
yield {
|
|
672
|
+
type: "done",
|
|
673
|
+
...usage ? { usage } : {}
|
|
674
|
+
};
|
|
605
675
|
}
|
|
606
676
|
async #postChat(input, stream) {
|
|
607
677
|
const { baseUrl, apiKey, model } = this.#requireConfig();
|
|
@@ -615,7 +685,10 @@ var OpenAiCompatibleClient = class {
|
|
|
615
685
|
body["tool_choice"] = "auto";
|
|
616
686
|
}
|
|
617
687
|
if (input.temperature !== void 0) body["temperature"] = input.temperature;
|
|
618
|
-
if (stream)
|
|
688
|
+
if (stream) {
|
|
689
|
+
body["stream"] = true;
|
|
690
|
+
body["stream_options"] = { include_usage: true };
|
|
691
|
+
}
|
|
619
692
|
let res;
|
|
620
693
|
try {
|
|
621
694
|
res = await this.#fetch(`${baseUrl}/chat/completions`, {
|
|
@@ -665,9 +738,17 @@ var OpenAiCompatibleClient = class {
|
|
|
665
738
|
};
|
|
666
739
|
});
|
|
667
740
|
const content = choice["content"];
|
|
741
|
+
const thinking = choice["reasoning_content"];
|
|
742
|
+
const usageRaw = data.usage;
|
|
743
|
+
const usage = usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number") ? {
|
|
744
|
+
inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
|
|
745
|
+
outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
|
|
746
|
+
} : void 0;
|
|
668
747
|
return {
|
|
669
748
|
content: typeof content === "string" && content !== "" ? textParts(content) : void 0,
|
|
670
749
|
toolCalls: toolCalls?.length ? toolCalls : void 0,
|
|
750
|
+
...typeof thinking === "string" && thinking !== "" ? { thinking } : {},
|
|
751
|
+
...usage ? { usage } : {},
|
|
671
752
|
raw: data
|
|
672
753
|
};
|
|
673
754
|
} catch (e) {
|
|
@@ -756,7 +837,7 @@ function toAnthropicMessages(messages) {
|
|
|
756
837
|
const toAnthropicTools = (tools) => tools.map((tool) => ({
|
|
757
838
|
name: tool.name,
|
|
758
839
|
...tool.description ? { description: tool.description } : {},
|
|
759
|
-
input_schema: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), ANTHROPIC_SCHEMA_PROFILE, tool.name)
|
|
840
|
+
input_schema: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), ANTHROPIC_SCHEMA_PROFILE, tool.name)
|
|
760
841
|
}));
|
|
761
842
|
/** Anthropic Messages API 客户端(零依赖 fetch;Node/浏览器通用) */
|
|
762
843
|
var AnthropicClient = class {
|
|
@@ -795,6 +876,8 @@ var AnthropicClient = class {
|
|
|
795
876
|
const decoder = new TextDecoder();
|
|
796
877
|
const reader = res.body.getReader();
|
|
797
878
|
const frames = createSseFrameReader();
|
|
879
|
+
let inputTokens;
|
|
880
|
+
let outputTokens;
|
|
798
881
|
const handleFrame = function* (data) {
|
|
799
882
|
let chunk;
|
|
800
883
|
try {
|
|
@@ -804,6 +887,17 @@ var AnthropicClient = class {
|
|
|
804
887
|
}
|
|
805
888
|
const type = chunk["type"];
|
|
806
889
|
if (type === "error") throw new WebSkillError("LLM_REQUEST_FAILED", `LLM stream error: ${chunk["error"]?.message ?? data}`);
|
|
890
|
+
if (type === "message_start") {
|
|
891
|
+
const usage = chunk["message"]?.["usage"];
|
|
892
|
+
if (typeof usage?.["input_tokens"] === "number") inputTokens = usage["input_tokens"];
|
|
893
|
+
if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (type === "message_delta") {
|
|
897
|
+
const usage = chunk["usage"];
|
|
898
|
+
if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
807
901
|
if (type === "content_block_start") {
|
|
808
902
|
const block = chunk["content_block"];
|
|
809
903
|
if (block?.["type"] === "tool_use") {
|
|
@@ -826,6 +920,13 @@ var AnthropicClient = class {
|
|
|
826
920
|
};
|
|
827
921
|
return;
|
|
828
922
|
}
|
|
923
|
+
if (delta?.["type"] === "thinking_delta" && typeof delta["thinking"] === "string" && delta["thinking"] !== "") {
|
|
924
|
+
yield {
|
|
925
|
+
type: "thinking-delta",
|
|
926
|
+
delta: delta["thinking"]
|
|
927
|
+
};
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
829
930
|
if (delta?.["type"] === "input_json_delta" && typeof delta["partial_json"] === "string") {
|
|
830
931
|
const acc = toolCallsByIndex.get(index) ?? {
|
|
831
932
|
id: "",
|
|
@@ -866,7 +967,14 @@ var AnthropicClient = class {
|
|
|
866
967
|
};
|
|
867
968
|
})
|
|
868
969
|
};
|
|
869
|
-
|
|
970
|
+
const usage = inputTokens !== void 0 || outputTokens !== void 0 ? {
|
|
971
|
+
inputTokens: inputTokens ?? 0,
|
|
972
|
+
outputTokens: outputTokens ?? 0
|
|
973
|
+
} : void 0;
|
|
974
|
+
yield {
|
|
975
|
+
type: "done",
|
|
976
|
+
...usage ? { usage } : {}
|
|
977
|
+
};
|
|
870
978
|
}
|
|
871
979
|
async #post(input, stream) {
|
|
872
980
|
validateLlmMessages(input.messages);
|
|
@@ -878,7 +986,12 @@ var AnthropicClient = class {
|
|
|
878
986
|
};
|
|
879
987
|
if (system !== void 0) body["system"] = system;
|
|
880
988
|
if (input.tools?.length) body["tools"] = toAnthropicTools(input.tools);
|
|
881
|
-
|
|
989
|
+
const thinkingBudget = this.#config.thinkingBudgetTokens;
|
|
990
|
+
if (thinkingBudget !== void 0 && Number.isInteger(thinkingBudget) && thinkingBudget > 0) body["thinking"] = {
|
|
991
|
+
type: "enabled",
|
|
992
|
+
budget_tokens: thinkingBudget
|
|
993
|
+
};
|
|
994
|
+
else if (input.temperature !== void 0) body["temperature"] = input.temperature;
|
|
882
995
|
if (stream) body["stream"] = true;
|
|
883
996
|
let res;
|
|
884
997
|
try {
|
|
@@ -926,14 +1039,22 @@ var AnthropicClient = class {
|
|
|
926
1039
|
const blocks = data.content;
|
|
927
1040
|
if (!Array.isArray(blocks)) throw new Error("response has no content blocks");
|
|
928
1041
|
const text = blocks.filter((b) => b["type"] === "text").map((b) => String(b["text"] ?? "")).join("");
|
|
1042
|
+
const thinking = blocks.filter((b) => b["type"] === "thinking").map((b) => String(b["thinking"] ?? "")).join("");
|
|
929
1043
|
const toolCalls = blocks.filter((b) => b["type"] === "tool_use").map((b) => ({
|
|
930
1044
|
id: typeof b["id"] === "string" ? b["id"] : "",
|
|
931
1045
|
name: typeof b["name"] === "string" ? b["name"] : "",
|
|
932
1046
|
arguments: typeof b["input"] === "object" && b["input"] !== null ? b["input"] : {}
|
|
933
1047
|
}));
|
|
1048
|
+
const usageRaw = data.usage;
|
|
1049
|
+
const usage = typeof usageRaw?.["input_tokens"] === "number" || typeof usageRaw?.["output_tokens"] === "number" ? {
|
|
1050
|
+
inputTokens: typeof usageRaw["input_tokens"] === "number" ? usageRaw["input_tokens"] : 0,
|
|
1051
|
+
outputTokens: typeof usageRaw["output_tokens"] === "number" ? usageRaw["output_tokens"] : 0
|
|
1052
|
+
} : void 0;
|
|
934
1053
|
return {
|
|
935
1054
|
content: text === "" ? void 0 : textParts(text),
|
|
936
1055
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
1056
|
+
...thinking === "" ? {} : { thinking },
|
|
1057
|
+
...usage ? { usage } : {},
|
|
937
1058
|
raw: data
|
|
938
1059
|
};
|
|
939
1060
|
} catch (e) {
|
|
@@ -942,6 +1063,18 @@ var AnthropicClient = class {
|
|
|
942
1063
|
}
|
|
943
1064
|
};
|
|
944
1065
|
const errorMessage = (e) => e instanceof Error ? e.message : String(e);
|
|
1066
|
+
/** usageMetadata → LlmTokenUsage(promptTokenCount / candidatesTokenCount;无字段返回 undefined) */
|
|
1067
|
+
function parseGenAiUsage(data) {
|
|
1068
|
+
const usage = data?.usageMetadata;
|
|
1069
|
+
if (!usage) return void 0;
|
|
1070
|
+
const input = usage["promptTokenCount"];
|
|
1071
|
+
const output = usage["candidatesTokenCount"];
|
|
1072
|
+
if (typeof input !== "number" && typeof output !== "number") return void 0;
|
|
1073
|
+
return {
|
|
1074
|
+
inputTokens: typeof input === "number" ? input : 0,
|
|
1075
|
+
outputTokens: typeof output === "number" ? output : 0
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
945
1078
|
/** parts → GenAI parts(二进制统一走 inlineData) */
|
|
946
1079
|
const toGenAiParts = (parts, where) => parts.map((part) => {
|
|
947
1080
|
switch (part.type) {
|
|
@@ -992,10 +1125,16 @@ function toGenAiContents(messages) {
|
|
|
992
1125
|
}
|
|
993
1126
|
if (msg.role === "assistant") {
|
|
994
1127
|
const parts = toGenAiParts(msg.content, "assistant");
|
|
995
|
-
for (const call of msg.toolCalls ?? [])
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1128
|
+
for (const call of msg.toolCalls ?? []) {
|
|
1129
|
+
const signature = call.vendor?.["thoughtSignature"];
|
|
1130
|
+
parts.push({
|
|
1131
|
+
functionCall: {
|
|
1132
|
+
name: call.name,
|
|
1133
|
+
args: call.arguments
|
|
1134
|
+
},
|
|
1135
|
+
...typeof signature === "string" ? { thoughtSignature: signature } : {}
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
999
1138
|
out.push({
|
|
1000
1139
|
role: "model",
|
|
1001
1140
|
parts: parts.length > 0 ? parts : [{ text: "" }]
|
|
@@ -1015,7 +1154,7 @@ function toGenAiContents(messages) {
|
|
|
1015
1154
|
const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => ({
|
|
1016
1155
|
name: tool.name,
|
|
1017
1156
|
...tool.description ? { description: tool.description } : {},
|
|
1018
|
-
parameters: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), GOOGLE_SCHEMA_PROFILE, tool.name)
|
|
1157
|
+
parameters: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), GOOGLE_SCHEMA_PROFILE, tool.name)
|
|
1019
1158
|
})) }];
|
|
1020
1159
|
/** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
|
|
1021
1160
|
var GoogleGenAiClient = class {
|
|
@@ -1039,9 +1178,11 @@ var GoogleGenAiClient = class {
|
|
|
1039
1178
|
const parts = this.#responseParts(data);
|
|
1040
1179
|
const text = parts.filter((p) => typeof p["text"] === "string").map((p) => String(p["text"])).join("");
|
|
1041
1180
|
const toolCalls = this.#functionCalls(parts);
|
|
1181
|
+
const usage = parseGenAiUsage(data);
|
|
1042
1182
|
return {
|
|
1043
1183
|
content: text === "" ? void 0 : textParts(text),
|
|
1044
1184
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
1185
|
+
...usage ? { usage } : {},
|
|
1045
1186
|
raw: data
|
|
1046
1187
|
};
|
|
1047
1188
|
}
|
|
@@ -1053,6 +1194,7 @@ var GoogleGenAiClient = class {
|
|
|
1053
1194
|
const decoder = new TextDecoder();
|
|
1054
1195
|
const reader = res.body.getReader();
|
|
1055
1196
|
const frames = createSseFrameReader();
|
|
1197
|
+
let usage;
|
|
1056
1198
|
const handleFrame = function* (data) {
|
|
1057
1199
|
let chunk;
|
|
1058
1200
|
try {
|
|
@@ -1060,6 +1202,7 @@ var GoogleGenAiClient = class {
|
|
|
1060
1202
|
} catch (e) {
|
|
1061
1203
|
throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage(e)}`, data);
|
|
1062
1204
|
}
|
|
1205
|
+
usage = parseGenAiUsage(chunk) ?? usage;
|
|
1063
1206
|
const parts = chunk.candidates?.[0]?.content?.parts;
|
|
1064
1207
|
for (const part of parts ?? []) {
|
|
1065
1208
|
if (typeof part["text"] === "string" && part["text"] !== "") {
|
|
@@ -1070,11 +1213,15 @@ var GoogleGenAiClient = class {
|
|
|
1070
1213
|
continue;
|
|
1071
1214
|
}
|
|
1072
1215
|
const call = part["functionCall"];
|
|
1073
|
-
if (call)
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1216
|
+
if (call) {
|
|
1217
|
+
const signature = part["thoughtSignature"];
|
|
1218
|
+
toolCalls.push({
|
|
1219
|
+
id: `call-${toolCalls.length}`,
|
|
1220
|
+
name: call.name ?? "",
|
|
1221
|
+
arguments: call.args ?? {},
|
|
1222
|
+
...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1078
1225
|
}
|
|
1079
1226
|
};
|
|
1080
1227
|
try {
|
|
@@ -1091,7 +1238,10 @@ var GoogleGenAiClient = class {
|
|
|
1091
1238
|
type: "tool-calls",
|
|
1092
1239
|
toolCalls
|
|
1093
1240
|
};
|
|
1094
|
-
yield {
|
|
1241
|
+
yield {
|
|
1242
|
+
type: "done",
|
|
1243
|
+
...usage ? { usage } : {}
|
|
1244
|
+
};
|
|
1095
1245
|
}
|
|
1096
1246
|
async #post(input, stream) {
|
|
1097
1247
|
const model = input.model ?? this.#config.model;
|
|
@@ -1143,15 +1293,30 @@ var GoogleGenAiClient = class {
|
|
|
1143
1293
|
const out = [];
|
|
1144
1294
|
for (const part of parts) {
|
|
1145
1295
|
const call = part["functionCall"];
|
|
1146
|
-
if (call)
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1296
|
+
if (call) {
|
|
1297
|
+
const signature = part["thoughtSignature"];
|
|
1298
|
+
out.push({
|
|
1299
|
+
id: `call-${out.length}`,
|
|
1300
|
+
name: call.name ?? "",
|
|
1301
|
+
arguments: call.args ?? {},
|
|
1302
|
+
...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1151
1305
|
}
|
|
1152
1306
|
return out;
|
|
1153
1307
|
}
|
|
1154
1308
|
};
|
|
1309
|
+
/** v5 { inputTokens, outputTokens } 与 v4 { promptTokens, completionTokens } 两种形状都接 */
|
|
1310
|
+
function fromVercelUsage(raw) {
|
|
1311
|
+
const u = raw ?? {};
|
|
1312
|
+
const input = u["inputTokens"] ?? u["promptTokens"];
|
|
1313
|
+
const output = u["outputTokens"] ?? u["completionTokens"];
|
|
1314
|
+
if (typeof input !== "number" && typeof output !== "number") return void 0;
|
|
1315
|
+
return {
|
|
1316
|
+
inputTokens: typeof input === "number" ? input : 0,
|
|
1317
|
+
outputTokens: typeof output === "number" ? output : 0
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1155
1320
|
function toVercelToolSpecs(tools) {
|
|
1156
1321
|
const out = {};
|
|
1157
1322
|
for (const tool of tools) out[tool.name] = {
|
|
@@ -1160,6 +1325,16 @@ function toVercelToolSpecs(tools) {
|
|
|
1160
1325
|
};
|
|
1161
1326
|
return out;
|
|
1162
1327
|
}
|
|
1328
|
+
/**
|
|
1329
|
+
* 归一化供应商思考内容:v5 的 `reasoningText`(字符串)与 v4 的 `reasoning`
|
|
1330
|
+
* (Array<{ text; type }>)都能落到同一条 `LlmResponse.thinking`。
|
|
1331
|
+
*/
|
|
1332
|
+
function normalizeThinking(value) {
|
|
1333
|
+
if (typeof value === "string") return value !== "" ? value : void 0;
|
|
1334
|
+
if (!Array.isArray(value)) return void 0;
|
|
1335
|
+
const text = value.map((part) => typeof part?.text === "string" ? part.text : "").join("");
|
|
1336
|
+
return text !== "" ? text : void 0;
|
|
1337
|
+
}
|
|
1163
1338
|
/** 接受 generateText 的返回值形状:{ text?, toolCalls?: [{ toolCallId, toolName, input|args }] } */
|
|
1164
1339
|
function fromVercelResult(result) {
|
|
1165
1340
|
const r = result;
|
|
@@ -1171,9 +1346,13 @@ function fromVercelResult(result) {
|
|
|
1171
1346
|
arguments: typeof args === "object" && args !== null ? args : {}
|
|
1172
1347
|
};
|
|
1173
1348
|
});
|
|
1349
|
+
const thinking = normalizeThinking(r.reasoningText ?? r.reasoning);
|
|
1350
|
+
const usage = fromVercelUsage(r.usage);
|
|
1174
1351
|
return {
|
|
1175
1352
|
content: typeof r.text === "string" && r.text !== "" ? textParts(r.text) : void 0,
|
|
1176
1353
|
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
1354
|
+
...thinking !== void 0 ? { thinking } : {},
|
|
1355
|
+
...usage ? { usage } : {},
|
|
1177
1356
|
raw: result
|
|
1178
1357
|
};
|
|
1179
1358
|
}
|
|
@@ -1191,6 +1370,13 @@ function fromVercelStreamPart(part) {
|
|
|
1191
1370
|
delta
|
|
1192
1371
|
} : void 0;
|
|
1193
1372
|
}
|
|
1373
|
+
case "reasoning-delta": {
|
|
1374
|
+
const delta = p["text"] ?? p["delta"];
|
|
1375
|
+
return typeof delta === "string" && delta !== "" ? {
|
|
1376
|
+
type: "thinking-delta",
|
|
1377
|
+
delta
|
|
1378
|
+
} : void 0;
|
|
1379
|
+
}
|
|
1194
1380
|
case "tool-call": return {
|
|
1195
1381
|
type: "tool-calls",
|
|
1196
1382
|
toolCalls: [{
|
|
@@ -1199,7 +1385,13 @@ function fromVercelStreamPart(part) {
|
|
|
1199
1385
|
arguments: typeof p["input"] === "object" && p["input"] !== null ? p["input"] : {}
|
|
1200
1386
|
}]
|
|
1201
1387
|
};
|
|
1202
|
-
case "finish":
|
|
1388
|
+
case "finish": {
|
|
1389
|
+
const usage = fromVercelUsage(p["totalUsage"] ?? p["usage"]);
|
|
1390
|
+
return {
|
|
1391
|
+
type: "done",
|
|
1392
|
+
...usage ? { usage } : {}
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1203
1395
|
default: return;
|
|
1204
1396
|
}
|
|
1205
1397
|
}
|
|
@@ -1248,6 +1440,45 @@ var FullDisclosureRouter = class {
|
|
|
1248
1440
|
}
|
|
1249
1441
|
};
|
|
1250
1442
|
/**
|
|
1443
|
+
* 参与 `toolResultMaxBytes` 截断的分片类型(FR-23.3)。**判据的单一来源。**
|
|
1444
|
+
*
|
|
1445
|
+
* 白名单而不是黑名单:黑名单式下新加的分片默认「参与截断」,
|
|
1446
|
+
* 漏改一处就会把一份 PDF 截成坏文件、把 docx 文本腰斩,而且没有任何报错。
|
|
1447
|
+
* 白名单式下新分片默认**不参与**,要受文本预算约束必须显式加进来。
|
|
1448
|
+
* @experimental
|
|
1449
|
+
*/
|
|
1450
|
+
const TEXT_BUDGETED_CONTENT_TYPES = /* @__PURE__ */ new Set(["text", "json"]);
|
|
1451
|
+
/**
|
|
1452
|
+
* `file` 分片的上限(FR-23.4),量的是**实际上线的 base64 长度**而不是解码后的字节——
|
|
1453
|
+
* provider 的限额算的是请求载荷。
|
|
1454
|
+
*
|
|
1455
|
+
* 2026-08-14 查证的三家上限:
|
|
1456
|
+
* | provider | 上限 | 出处 |
|
|
1457
|
+
* | --------- | --------------------------------------- | ----------------------- |
|
|
1458
|
+
* | Anthropic | **32 MB**(**整个请求载荷**)、600 页 | PDF support / 请求大小 |
|
|
1459
|
+
* | OpenAI | 单文件 50 MB,全部文件合计 50 MB | File inputs / 使用须知 |
|
|
1460
|
+
* | Gemini | 50 MB 或 1000 页(内联与 Files API 同) | 文档理解 / 技术详情 |
|
|
1461
|
+
*
|
|
1462
|
+
* 取最小值 Anthropic 的 32 MB。它是**整个请求**的额度,不是文档单独的额度,
|
|
1463
|
+
* 所以再留出余量给系统提示词、catalog(约 34 KB)、历史消息与其余分片。
|
|
1464
|
+
* 25 000 KB base64 ≈ 18.75 MB 原始 PDF。取 1024 的整数倍:设置界面按 KB 展示,
|
|
1465
|
+
* 十进制的 24 000 000 会显示成 23437.5 这种读不出来的数。
|
|
1466
|
+
*/
|
|
1467
|
+
const DEFAULT_MAX_DOCUMENT_BYTES = 256e5;
|
|
1468
|
+
/**
|
|
1469
|
+
* `document-text` 分片的上限(FR-23.4)。
|
|
1470
|
+
*
|
|
1471
|
+
* 约束来自**上下文窗口**而不是请求大小:抽出来的文本要整段进上下文。
|
|
1472
|
+
* 按英文约 4 字节/token 折算,500 KB ≈ 128K token,占 200K 窗口的多半,
|
|
1473
|
+
* 给历史消息与模型的回答留下其余。
|
|
1474
|
+
*/
|
|
1475
|
+
const DEFAULT_MAX_DOCUMENT_TEXT_BYTES = 512e3;
|
|
1476
|
+
/**
|
|
1477
|
+
* 单次 fetchData 结果的字节上限(分册 16,FR-16.6):1000 KB。
|
|
1478
|
+
* 结构化数据不是文档,量级差一个数量级;超出**只拒不截**——截断的 JSON 解不出来。
|
|
1479
|
+
*/
|
|
1480
|
+
const DEFAULT_MAX_DATA_SOURCE_BYTES = 1024e3;
|
|
1481
|
+
/**
|
|
1251
1482
|
* 工具名解析规则(单一实现,Agent 循环使用):
|
|
1252
1483
|
* 1. `<skillName>__<scriptName>` 且前缀是已激活技能 → 本地脚本
|
|
1253
1484
|
* 2. `endpoint:` 前缀 → TOOL_UNSUPPORTED(MCP 阶段实现)
|
|
@@ -1406,17 +1637,75 @@ const READ_SKILL_FILE_TOOL = {
|
|
|
1406
1637
|
source: "builtin"
|
|
1407
1638
|
};
|
|
1408
1639
|
const ASK_USER_TOOL_NAME = "ask_user";
|
|
1640
|
+
/** 一次 `ask_user` 能收的字段数上限(FR-11.3 裁决值),可由 `AgentLoopConfig` 覆盖 */
|
|
1641
|
+
const ASK_USER_MAX_FIELDS = 20;
|
|
1642
|
+
/** `fields[].type`:`FormField['type']` 的八种。`multi-select` 不在其中——它的值是数组(FR-11.1b) */
|
|
1643
|
+
const ASK_USER_FIELD_TYPES = [
|
|
1644
|
+
"text",
|
|
1645
|
+
"number",
|
|
1646
|
+
"boolean",
|
|
1647
|
+
"select",
|
|
1648
|
+
"textarea",
|
|
1649
|
+
"file",
|
|
1650
|
+
"password",
|
|
1651
|
+
"date"
|
|
1652
|
+
];
|
|
1409
1653
|
const ASK_USER_INPUT_SCHEMA = {
|
|
1410
1654
|
type: "object",
|
|
1411
1655
|
properties: {
|
|
1412
1656
|
question: {
|
|
1413
1657
|
type: "string",
|
|
1414
|
-
description: "
|
|
1658
|
+
description: "A single question. Use it only when one answer is genuinely all you need."
|
|
1659
|
+
},
|
|
1660
|
+
fields: {
|
|
1661
|
+
type: "array",
|
|
1662
|
+
maxItems: 20,
|
|
1663
|
+
description: "Collect several answers in one form. Use this whenever you need more than one piece of information, so the user fills everything in once instead of answering a chain of questions.",
|
|
1664
|
+
items: {
|
|
1665
|
+
type: "object",
|
|
1666
|
+
properties: {
|
|
1667
|
+
name: {
|
|
1668
|
+
type: "string",
|
|
1669
|
+
description: "Key this answer is returned under."
|
|
1670
|
+
},
|
|
1671
|
+
label: {
|
|
1672
|
+
type: "string",
|
|
1673
|
+
description: "Short label shown next to the input."
|
|
1674
|
+
},
|
|
1675
|
+
type: {
|
|
1676
|
+
type: "string",
|
|
1677
|
+
enum: [...ASK_USER_FIELD_TYPES],
|
|
1678
|
+
description: "Input kind. Use \"date\" for dates; the value comes back as a YYYY-MM-DD string."
|
|
1679
|
+
},
|
|
1680
|
+
required: { type: "boolean" },
|
|
1681
|
+
description: {
|
|
1682
|
+
type: "string",
|
|
1683
|
+
description: "Help text shown under the input."
|
|
1684
|
+
},
|
|
1685
|
+
options: {
|
|
1686
|
+
type: "array",
|
|
1687
|
+
items: {
|
|
1688
|
+
type: "object",
|
|
1689
|
+
properties: {
|
|
1690
|
+
label: { type: "string" },
|
|
1691
|
+
value: {}
|
|
1692
|
+
},
|
|
1693
|
+
required: ["label", "value"]
|
|
1694
|
+
},
|
|
1695
|
+
description: "Choices for a \"select\" field. Required when type is \"select\"."
|
|
1696
|
+
}
|
|
1697
|
+
},
|
|
1698
|
+
required: [
|
|
1699
|
+
"name",
|
|
1700
|
+
"label",
|
|
1701
|
+
"type"
|
|
1702
|
+
]
|
|
1703
|
+
}
|
|
1415
1704
|
},
|
|
1416
1705
|
choices: {
|
|
1417
1706
|
type: "array",
|
|
1418
1707
|
items: { type: "string" },
|
|
1419
|
-
description: "Closed set of acceptable answers. Provide it whenever the answer must be one of a known finite set, for example when asking which installed skill to use. The user then picks from a list instead of typing free text."
|
|
1708
|
+
description: "Closed set of acceptable answers for the single-question form. Provide it whenever the answer must be one of a known finite set, for example when asking which installed skill to use. The user then picks from a list instead of typing free text."
|
|
1420
1709
|
},
|
|
1421
1710
|
suggestion: {
|
|
1422
1711
|
type: "string",
|
|
@@ -1426,20 +1715,19 @@ const ASK_USER_INPUT_SCHEMA = {
|
|
|
1426
1715
|
type: "string",
|
|
1427
1716
|
description: "Short reason for the suggestion, shown next to it so the user can judge whether to accept it."
|
|
1428
1717
|
}
|
|
1429
|
-
}
|
|
1430
|
-
required: ["question"]
|
|
1718
|
+
}
|
|
1431
1719
|
};
|
|
1432
1720
|
/** 内建工具:LLM 信息不足时主动向用户提问;仅当配置了 UiBridge 时注册 */
|
|
1433
1721
|
const ASK_USER_TOOL = {
|
|
1434
1722
|
name: ASK_USER_TOOL_NAME,
|
|
1435
|
-
description: "Ask the user a question when
|
|
1723
|
+
description: "Ask the user for information you are missing. Pass \"fields\" to collect everything you need in a single form; do not ask one question per turn when several answers are needed. Pass \"question\" only when a single answer is all you need. When an answer belongs to a known finite set, you must pass \"choices\" (single question) or \"options\" (field).",
|
|
1436
1724
|
inputSchema: ASK_USER_INPUT_SCHEMA,
|
|
1437
1725
|
source: "builtin"
|
|
1438
1726
|
};
|
|
1439
1727
|
/**
|
|
1440
|
-
* 越权 / 绝对路径 /
|
|
1728
|
+
* 越权 / 绝对路径 / 父级遍历的技能目录读取:策略拒绝(FS_PERMISSION_DENIED,
|
|
1441
1729
|
* 计入 POLICY_DENIAL_CODES → 不计入隔离失败计数),
|
|
1442
|
-
*
|
|
1730
|
+
* 与「目录内路径确实不存在 → FS_NOT_FOUND」区分(0.9.0 分册 14,UX-04)。
|
|
1443
1731
|
*/
|
|
1444
1732
|
function assertReferencePath(relativePath) {
|
|
1445
1733
|
const trimmed = relativePath.trim();
|
|
@@ -1447,18 +1735,31 @@ function assertReferencePath(relativePath) {
|
|
|
1447
1735
|
if (trimmed.replace(/\\/g, "/").split("/").includes("..")) throw new WebSkillError("FS_PERMISSION_DENIED", `Reference access denied: parent traversal (path: ${JSON.stringify(relativePath)})`);
|
|
1448
1736
|
}
|
|
1449
1737
|
/**
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1738
|
+
* 越权判定必须在拼接前缀**之前**做:`/managed/...` 这类绝对路径
|
|
1739
|
+
* 若直接拼接会退化成目录内的相对路径 → 误报 FS_NOT_FOUND(UX-04)。
|
|
1740
|
+
*/
|
|
1741
|
+
function resolveSkillDir(skillRoot, dir, relativePath) {
|
|
1742
|
+
assertReferencePath(relativePath);
|
|
1743
|
+
return resolveInsideRoot(skillRoot, `${dir}/${relativePath}`);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* 脚本执行上下文:只暴露 readReference / readAsset / readAssetBinary / writeArtifact
|
|
1747
|
+
* 四个显式能力,不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
|
|
1452
1748
|
*/
|
|
1453
1749
|
function createScriptContext(deps) {
|
|
1454
|
-
const { fs, artifactStore, skillName, skillRoot, runId, confirm, onWarning, onArtifactCreated } = deps;
|
|
1750
|
+
const { fs, artifactStore, skillName, skillRoot, runId, confirm, fetchData, onWarning, onArtifactCreated } = deps;
|
|
1455
1751
|
const readFs = fs.withRoot?.(skillRoot) ?? fs;
|
|
1456
1752
|
return {
|
|
1457
1753
|
skillName,
|
|
1458
1754
|
runId,
|
|
1459
1755
|
async readReference(relativePath) {
|
|
1460
|
-
|
|
1461
|
-
|
|
1756
|
+
return readFs.readText(resolveSkillDir(skillRoot, "references", relativePath));
|
|
1757
|
+
},
|
|
1758
|
+
async readAsset(relativePath) {
|
|
1759
|
+
return readFs.readText(resolveSkillDir(skillRoot, "assets", relativePath));
|
|
1760
|
+
},
|
|
1761
|
+
async readAssetBinary(relativePath) {
|
|
1762
|
+
return readFs.readBinary(resolveSkillDir(skillRoot, "assets", relativePath));
|
|
1462
1763
|
},
|
|
1463
1764
|
async writeArtifact(path, content, options) {
|
|
1464
1765
|
const artifact = typeof content === "string" ? await artifactStore.createTextArtifact({
|
|
@@ -1476,6 +1777,7 @@ function createScriptContext(deps) {
|
|
|
1476
1777
|
return artifact;
|
|
1477
1778
|
},
|
|
1478
1779
|
...confirm ? { confirm } : {},
|
|
1780
|
+
...fetchData ? { fetchData } : {},
|
|
1479
1781
|
...onWarning ? { onWarning } : {}
|
|
1480
1782
|
};
|
|
1481
1783
|
}
|
|
@@ -1803,7 +2105,7 @@ const DEFAULT_USER_PROFILE_LIMITS = {
|
|
|
1803
2105
|
recordLimit: 500,
|
|
1804
2106
|
valueMaxChars: 2e3,
|
|
1805
2107
|
sampleLimit: 80,
|
|
1806
|
-
injectMaxBytes:
|
|
2108
|
+
injectMaxBytes: 4e3,
|
|
1807
2109
|
entryLimit: 40
|
|
1808
2110
|
};
|
|
1809
2111
|
/** @experimental */
|
|
@@ -1968,6 +2270,43 @@ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
|
|
|
1968
2270
|
* 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
|
|
1969
2271
|
*/
|
|
1970
2272
|
const PLAIN_CHAT_SYSTEM_PROMPT = "You are a helpful assistant. Answer the user's question directly and concisely. You have no tools available; do not describe or simulate tool calls.";
|
|
2273
|
+
const READ_LINKED_DOCUMENT_TOOL_NAME = "read_linked_document";
|
|
2274
|
+
/**
|
|
2275
|
+
* 能直接给模型用的 MIME。其余一律拒绝并列出这份清单——
|
|
2276
|
+
* 「尽力而为地猜格式」会让模型收到乱码却以为读成功了。
|
|
2277
|
+
*/
|
|
2278
|
+
const SUPPORTED_DOCUMENT_MIME = {
|
|
2279
|
+
pdf: "application/pdf",
|
|
2280
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
2281
|
+
};
|
|
2282
|
+
/** 引擎侧固定流程(§2.3)用到的错误文案,集中一处便于判据引用 */
|
|
2283
|
+
const UNSUPPORTED_DOCUMENT_MESSAGE = `Only PDF (${SUPPORTED_DOCUMENT_MIME.pdf}), Word (${SUPPORTED_DOCUMENT_MIME.docx}) and plain text documents can be read.`;
|
|
2284
|
+
/**
|
|
2285
|
+
* 内建工具:读页面链接指向的文档。
|
|
2286
|
+
*
|
|
2287
|
+
* **始终注册**(只要宿主装配了 reader):docx 与纯文本这条路对所有模型成立。
|
|
2288
|
+
* PDF 目标在模型不支持文档时**取数后拒绝**,而不是入口就拦 ——
|
|
2289
|
+
* 入口拦会误伤 docx,它抽成文本后根本不需要文档能力。
|
|
2290
|
+
*/
|
|
2291
|
+
const READ_LINKED_DOCUMENT_TOOL = {
|
|
2292
|
+
name: READ_LINKED_DOCUMENT_TOOL_NAME,
|
|
2293
|
+
description: "Read a document linked from the current page. Pass the exact \"href\" from a perceive_page result. Cross-origin documents need the user to confirm each time.",
|
|
2294
|
+
inputSchema: {
|
|
2295
|
+
type: "object",
|
|
2296
|
+
properties: { url: {
|
|
2297
|
+
type: "string",
|
|
2298
|
+
description: "The href of a link that appeared in a perceive_page result."
|
|
2299
|
+
} },
|
|
2300
|
+
required: ["url"]
|
|
2301
|
+
},
|
|
2302
|
+
source: "builtin"
|
|
2303
|
+
};
|
|
2304
|
+
/** 二进制转 base64;`btoa` 只吃 latin1,必须逐字节喂而不是先 decode 成字符串 */
|
|
2305
|
+
function toBase64(bytes) {
|
|
2306
|
+
let binary = "";
|
|
2307
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
2308
|
+
return btoa(binary);
|
|
2309
|
+
}
|
|
1971
2310
|
/** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
|
|
1972
2311
|
var TraceRecorder = class {
|
|
1973
2312
|
#runId;
|
|
@@ -2053,8 +2392,8 @@ function extractSkillCandidate(data) {
|
|
|
2053
2392
|
*/
|
|
2054
2393
|
const DEFAULT_LOOP_LIMITS = {
|
|
2055
2394
|
maxTurns: 10,
|
|
2056
|
-
totalTimeoutMs:
|
|
2057
|
-
toolTimeoutMs:
|
|
2395
|
+
totalTimeoutMs: 36e5,
|
|
2396
|
+
toolTimeoutMs: 6e5
|
|
2058
2397
|
};
|
|
2059
2398
|
const RUN_SNAPSHOT_SCHEMA_VERSION = 3;
|
|
2060
2399
|
/** @experimental */
|
|
@@ -2199,6 +2538,16 @@ function evaluateToolAccess(state, llmToolName) {
|
|
|
2199
2538
|
};
|
|
2200
2539
|
}
|
|
2201
2540
|
const MAX_SURFACE_PATCHES_PER_SECOND = 240;
|
|
2541
|
+
/**
|
|
2542
|
+
* 一轮 LLM 调用可能抛出、且**值得原样上报**的结构化码。
|
|
2543
|
+
* 不在表里的(含未知异常)统一归 `LLM_REQUEST_FAILED`——那是「这次请求没成」的兜底,
|
|
2544
|
+
* 但把 schema 不兼容这类可定位的原因也压进去,排障就只剩看文案(UI-UX8 D4)。
|
|
2545
|
+
*/
|
|
2546
|
+
const LLM_TURN_CODES = /* @__PURE__ */ new Set([
|
|
2547
|
+
"LLM_UNAVAILABLE",
|
|
2548
|
+
"LLM_REQUEST_FAILED",
|
|
2549
|
+
"TOOL_SCHEMA_UNAVAILABLE"
|
|
2550
|
+
]);
|
|
2202
2551
|
/** 交互终态(取消/超时):从工具执行深处直接终止 run */
|
|
2203
2552
|
var RunTerminated = class extends Error {
|
|
2204
2553
|
outcome;
|
|
@@ -2294,12 +2643,16 @@ var AgentLoop = class {
|
|
|
2294
2643
|
maxTurns: config.maxTurns ?? DEFAULT_LOOP_LIMITS.maxTurns,
|
|
2295
2644
|
totalTimeoutMs: config.totalTimeoutMs ?? DEFAULT_LOOP_LIMITS.totalTimeoutMs,
|
|
2296
2645
|
toolTimeoutMs: config.toolTimeoutMs ?? DEFAULT_LOOP_LIMITS.toolTimeoutMs,
|
|
2297
|
-
toolResultMaxBytes: config.toolResultMaxBytes ??
|
|
2646
|
+
toolResultMaxBytes: config.toolResultMaxBytes ?? 256e3,
|
|
2298
2647
|
paramHistoryLimit: config.paramHistoryLimit ?? 50,
|
|
2299
2648
|
toolCallingDisabled: config.toolCallingDisabled ?? false,
|
|
2300
2649
|
maxUnknownToolRetries: config.maxUnknownToolRetries ?? 2,
|
|
2650
|
+
askUserMaxFields: config.askUserMaxFields ?? 20,
|
|
2651
|
+
maxDocumentBytes: config.maxDocumentBytes ?? 256e5,
|
|
2652
|
+
maxDocumentTextBytes: config.maxDocumentTextBytes ?? 512e3,
|
|
2301
2653
|
temperature: config.temperature,
|
|
2302
|
-
renderResult: config.renderResult
|
|
2654
|
+
renderResult: config.renderResult,
|
|
2655
|
+
remoteUrl: config.remoteUrl
|
|
2303
2656
|
};
|
|
2304
2657
|
this.#policy = {
|
|
2305
2658
|
missingParams: deps.interaction?.missingParams ?? "user",
|
|
@@ -2349,6 +2702,7 @@ var AgentLoop = class {
|
|
|
2349
2702
|
now,
|
|
2350
2703
|
interactionSeq: 0,
|
|
2351
2704
|
surfaceActionSeq: 0,
|
|
2705
|
+
documentSeq: 0,
|
|
2352
2706
|
messages: [],
|
|
2353
2707
|
turn: 0,
|
|
2354
2708
|
renderBlocks: [],
|
|
@@ -2357,6 +2711,7 @@ var AgentLoop = class {
|
|
|
2357
2711
|
surfacePatchCount: 0,
|
|
2358
2712
|
processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
|
|
2359
2713
|
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
2714
|
+
artifactPaths: /* @__PURE__ */ new Set(),
|
|
2360
2715
|
unknownToolCalls: 0,
|
|
2361
2716
|
startMs,
|
|
2362
2717
|
pausedMs: 0,
|
|
@@ -2477,6 +2832,7 @@ var AgentLoop = class {
|
|
|
2477
2832
|
const toolSpecs = this.#config.toolCallingDisabled ? [] : [
|
|
2478
2833
|
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
2479
2834
|
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
2835
|
+
...this.#deps.linkedDocuments ? [toLlmToolSpec(READ_LINKED_DOCUMENT_TOOL)] : [],
|
|
2480
2836
|
...skillToolSpecs
|
|
2481
2837
|
];
|
|
2482
2838
|
trace.record("llm.request", { data: {
|
|
@@ -2504,8 +2860,26 @@ var AgentLoop = class {
|
|
|
2504
2860
|
if (this.#cancelled.has(state.runId)) return finish("cancelled", "user-cancelled", "Run cancelled by user", "RUN_CANCELLED");
|
|
2505
2861
|
return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
2506
2862
|
}
|
|
2507
|
-
const code = e instanceof WebSkillError && (e.code
|
|
2508
|
-
|
|
2863
|
+
const code = e instanceof WebSkillError && LLM_TURN_CODES.has(e.code) ? e.code : "LLM_REQUEST_FAILED";
|
|
2864
|
+
const hint = messages.some((message) => Array.isArray(message.content) && message.content.some((part) => part.type === "file")) ? " This turn attached a document; the selected model may not accept document input. Try another model, or link a Word or text file instead." : "";
|
|
2865
|
+
return finish("failed", "llm-error", `${messageOf(e)}${hint}`, code);
|
|
2866
|
+
}
|
|
2867
|
+
if (response.thinking !== void 0 && response.thinking !== "") {
|
|
2868
|
+
state.trace.record("llm.thinking", { data: {
|
|
2869
|
+
turn,
|
|
2870
|
+
text: response.thinking
|
|
2871
|
+
} });
|
|
2872
|
+
const bus = this.#deps.eventBus;
|
|
2873
|
+
if (!llm.stream && bus && bus.listenerCount() > 0) bus.emit({
|
|
2874
|
+
phase: "execute",
|
|
2875
|
+
runId: state.runId,
|
|
2876
|
+
sessionId: state.run.sessionId,
|
|
2877
|
+
ts: state.now(),
|
|
2878
|
+
data: {
|
|
2879
|
+
kind: "thinking-delta",
|
|
2880
|
+
delta: response.thinking
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2509
2883
|
}
|
|
2510
2884
|
const responseText = partsToText(response.content);
|
|
2511
2885
|
if (!llm.stream && responseText !== "") await this.#deps.uiBridge?.onTextDelta?.(state.runId, responseText);
|
|
@@ -2514,6 +2888,11 @@ var AgentLoop = class {
|
|
|
2514
2888
|
hasToolCalls: Boolean(response.toolCalls?.length),
|
|
2515
2889
|
contentLength: responseText.length
|
|
2516
2890
|
} });
|
|
2891
|
+
if (response.usage) trace.record("llm.usage", { data: {
|
|
2892
|
+
turn,
|
|
2893
|
+
inputTokens: response.usage.inputTokens,
|
|
2894
|
+
outputTokens: response.usage.outputTokens
|
|
2895
|
+
} });
|
|
2517
2896
|
if (!response.toolCalls?.length) {
|
|
2518
2897
|
messages.push({
|
|
2519
2898
|
role: "assistant",
|
|
@@ -2533,6 +2912,7 @@ var AgentLoop = class {
|
|
|
2533
2912
|
content: response.content ?? [],
|
|
2534
2913
|
toolCalls: response.toolCalls
|
|
2535
2914
|
});
|
|
2915
|
+
const carried = [];
|
|
2536
2916
|
for (const call of response.toolCalls) {
|
|
2537
2917
|
let result;
|
|
2538
2918
|
try {
|
|
@@ -2541,11 +2921,7 @@ var AgentLoop = class {
|
|
|
2541
2921
|
if (e instanceof RunTerminated) return finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
|
|
2542
2922
|
throw e;
|
|
2543
2923
|
}
|
|
2544
|
-
|
|
2545
|
-
role: "tool",
|
|
2546
|
-
toolCallId: call.id,
|
|
2547
|
-
content: await this.#toolResultParts(call, result, state)
|
|
2548
|
-
});
|
|
2924
|
+
carried.push(...await this.#pushToolResult(state, call, result));
|
|
2549
2925
|
try {
|
|
2550
2926
|
await this.#drainSurfaceAction(state);
|
|
2551
2927
|
} catch (e) {
|
|
@@ -2553,6 +2929,10 @@ var AgentLoop = class {
|
|
|
2553
2929
|
throw e;
|
|
2554
2930
|
}
|
|
2555
2931
|
}
|
|
2932
|
+
if (carried.length > 0) messages.push({
|
|
2933
|
+
role: "user",
|
|
2934
|
+
content: carried
|
|
2935
|
+
});
|
|
2556
2936
|
}
|
|
2557
2937
|
} finally {
|
|
2558
2938
|
this.#disarmDeadline(state);
|
|
@@ -2598,7 +2978,10 @@ var AgentLoop = class {
|
|
|
2598
2978
|
try {
|
|
2599
2979
|
await this.#lifecycle({
|
|
2600
2980
|
phase: status === "completed" ? "complete" : "fail",
|
|
2601
|
-
data: {
|
|
2981
|
+
data: {
|
|
2982
|
+
reason,
|
|
2983
|
+
...status === "failed" ? { detail: output } : {}
|
|
2984
|
+
}
|
|
2602
2985
|
}, state);
|
|
2603
2986
|
} catch (e) {
|
|
2604
2987
|
trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
|
|
@@ -2629,7 +3012,7 @@ var AgentLoop = class {
|
|
|
2629
3012
|
state.messages.push({
|
|
2630
3013
|
role: "tool",
|
|
2631
3014
|
toolCallId: call.id,
|
|
2632
|
-
content: await this.#toolResultParts(call, result, state)
|
|
3015
|
+
content: (await this.#toolResultParts(call, result, state)).tool
|
|
2633
3016
|
});
|
|
2634
3017
|
state.trace.record("run.warning", {
|
|
2635
3018
|
message: `Sealed unanswered tool call "${call.name}": the run ended before it produced a result.`,
|
|
@@ -2706,6 +3089,7 @@ var AgentLoop = class {
|
|
|
2706
3089
|
},
|
|
2707
3090
|
trace,
|
|
2708
3091
|
reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
|
|
3092
|
+
documentSeq: 0,
|
|
2709
3093
|
activated: new Set(snapshot.activeSkillNames),
|
|
2710
3094
|
activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
|
|
2711
3095
|
skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
|
|
@@ -2723,6 +3107,7 @@ var AgentLoop = class {
|
|
|
2723
3107
|
surfacePatchCount: 0,
|
|
2724
3108
|
processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
|
|
2725
3109
|
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
3110
|
+
artifactPaths: /* @__PURE__ */ new Set(),
|
|
2726
3111
|
unknownToolCalls: 0,
|
|
2727
3112
|
startMs,
|
|
2728
3113
|
pausedMs: snapshot.pausedMs ?? 0,
|
|
@@ -2741,6 +3126,7 @@ var AgentLoop = class {
|
|
|
2741
3126
|
} });
|
|
2742
3127
|
const pending = pendingInteraction;
|
|
2743
3128
|
const pendingCall = this.#findPendingToolCall(state.messages);
|
|
3129
|
+
const carried = [];
|
|
2744
3130
|
try {
|
|
2745
3131
|
await this.#replaySurfaceEvents(state);
|
|
2746
3132
|
if (pendingSurfaceAction) await this.#resumeSurfaceAction(state, pendingSurfaceAction);
|
|
@@ -2757,11 +3143,7 @@ var AgentLoop = class {
|
|
|
2757
3143
|
...pendingCall,
|
|
2758
3144
|
arguments: args
|
|
2759
3145
|
}, state);
|
|
2760
|
-
|
|
2761
|
-
role: "tool",
|
|
2762
|
-
toolCallId: pendingCall.id,
|
|
2763
|
-
content: await this.#toolResultParts(pendingCall, result, state)
|
|
2764
|
-
});
|
|
3146
|
+
carried.push(...await this.#pushToolResult(state, pendingCall, result));
|
|
2765
3147
|
await this.#drainSurfaceAction(state);
|
|
2766
3148
|
} else if (pending?.type === "ask" && pendingCall) {
|
|
2767
3149
|
const value = await this.#interact(state, pending, {
|
|
@@ -2779,32 +3161,24 @@ var AgentLoop = class {
|
|
|
2779
3161
|
name: pendingCall.name,
|
|
2780
3162
|
callId: pendingCall.id
|
|
2781
3163
|
} });
|
|
2782
|
-
|
|
2783
|
-
role: "tool",
|
|
2784
|
-
toolCallId: pendingCall.id,
|
|
2785
|
-
content: await this.#toolResultParts(pendingCall, result, state)
|
|
2786
|
-
});
|
|
3164
|
+
carried.push(...await this.#pushToolResult(state, pendingCall, result));
|
|
2787
3165
|
await this.#drainSurfaceAction(state);
|
|
2788
3166
|
} else if (pendingCall) {
|
|
2789
3167
|
const result = await this.#executeCall(pendingCall, state);
|
|
2790
|
-
|
|
2791
|
-
role: "tool",
|
|
2792
|
-
toolCallId: pendingCall.id,
|
|
2793
|
-
content: await this.#toolResultParts(pendingCall, result, state)
|
|
2794
|
-
});
|
|
3168
|
+
carried.push(...await this.#pushToolResult(state, pendingCall, result));
|
|
2795
3169
|
await this.#drainSurfaceAction(state);
|
|
2796
3170
|
}
|
|
2797
3171
|
for (;;) {
|
|
2798
3172
|
const next = this.#findPendingToolCall(state.messages);
|
|
2799
3173
|
if (!next) break;
|
|
2800
3174
|
const result = await this.#executeCall(next, state);
|
|
2801
|
-
|
|
2802
|
-
role: "tool",
|
|
2803
|
-
toolCallId: next.id,
|
|
2804
|
-
content: await this.#toolResultParts(next, result, state)
|
|
2805
|
-
});
|
|
3175
|
+
carried.push(...await this.#pushToolResult(state, next, result));
|
|
2806
3176
|
await this.#drainSurfaceAction(state);
|
|
2807
3177
|
}
|
|
3178
|
+
if (carried.length > 0) state.messages.push({
|
|
3179
|
+
role: "user",
|
|
3180
|
+
content: carried
|
|
3181
|
+
});
|
|
2808
3182
|
} catch (e) {
|
|
2809
3183
|
if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
|
|
2810
3184
|
throw e;
|
|
@@ -2829,7 +3203,7 @@ var AgentLoop = class {
|
|
|
2829
3203
|
* 拿 `activated[0]` 顶替会把内置工具的失败栽给一个无关技能,比不归因更糟(设计 26 §2.3)。
|
|
2830
3204
|
*/
|
|
2831
3205
|
#skillOf(call, state) {
|
|
2832
|
-
if (call.name === "read_skill_file" || call.name === "ask_user") return
|
|
3206
|
+
if (call.name === "read_skill_file" || call.name === "ask_user" || call.name === "read_linked_document") return;
|
|
2833
3207
|
const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
|
|
2834
3208
|
return resolution.kind === "script" ? resolution.skillName : void 0;
|
|
2835
3209
|
}
|
|
@@ -2843,6 +3217,7 @@ var AgentLoop = class {
|
|
|
2843
3217
|
/**
|
|
2844
3218
|
* 流式 LLM 调用:text-delta 仅在 EventBus 有订阅者时发射 llm.delta 事件,
|
|
2845
3219
|
* 并同步转发 uiBridge.onTextDelta;tool-calls/done 组装为 LlmResponse 走原有分支。
|
|
3220
|
+
* thinking-delta 同理累积成 `response.thinking`(每轮一段,UI-UX5 #32)。
|
|
2846
3221
|
*/
|
|
2847
3222
|
async #completeStreaming(llm, state, input) {
|
|
2848
3223
|
const emitDelta = (delta) => {
|
|
@@ -2859,18 +3234,41 @@ var AgentLoop = class {
|
|
|
2859
3234
|
});
|
|
2860
3235
|
this.#deps.uiBridge?.onTextDelta?.(state.runId, delta);
|
|
2861
3236
|
};
|
|
3237
|
+
const emitThinkingDelta = (delta) => {
|
|
3238
|
+
const bus = this.#deps.eventBus;
|
|
3239
|
+
if (bus && bus.listenerCount() > 0) bus.emit({
|
|
3240
|
+
phase: "execute",
|
|
3241
|
+
runId: state.runId,
|
|
3242
|
+
sessionId: state.run.sessionId,
|
|
3243
|
+
ts: state.now(),
|
|
3244
|
+
data: {
|
|
3245
|
+
kind: "thinking-delta",
|
|
3246
|
+
delta
|
|
3247
|
+
}
|
|
3248
|
+
});
|
|
3249
|
+
};
|
|
2862
3250
|
let content = "";
|
|
3251
|
+
let thinking = "";
|
|
2863
3252
|
let doneContent;
|
|
3253
|
+
let usage;
|
|
2864
3254
|
const toolCalls = [];
|
|
2865
3255
|
for await (const event of llm.stream(input)) if (event.type === "text-delta") {
|
|
2866
3256
|
content += event.delta;
|
|
2867
3257
|
emitDelta(event.delta);
|
|
3258
|
+
} else if (event.type === "thinking-delta") {
|
|
3259
|
+
thinking += event.delta;
|
|
3260
|
+
emitThinkingDelta(event.delta);
|
|
2868
3261
|
} else if (event.type === "tool-calls") toolCalls.push(...event.toolCalls);
|
|
2869
|
-
else if (event.type === "done")
|
|
3262
|
+
else if (event.type === "done") {
|
|
3263
|
+
doneContent = event.content;
|
|
3264
|
+
usage = event.usage ?? usage;
|
|
3265
|
+
}
|
|
2870
3266
|
const text = doneContent ?? content;
|
|
2871
3267
|
return {
|
|
2872
3268
|
content: text === "" ? void 0 : textParts(text),
|
|
2873
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : void 0
|
|
3269
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
3270
|
+
...thinking === "" ? {} : { thinking },
|
|
3271
|
+
...usage ? { usage } : {}
|
|
2874
3272
|
};
|
|
2875
3273
|
}
|
|
2876
3274
|
/** 生命周期接线:更新 phase、发事件、跑钩子 */
|
|
@@ -3037,6 +3435,7 @@ var AgentLoop = class {
|
|
|
3037
3435
|
if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
|
|
3038
3436
|
else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
|
|
3039
3437
|
else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
|
|
3438
|
+
else if (call.name === "read_linked_document" && this.#deps.linkedDocuments !== void 0) result = await this.#handleReadLinkedDocument(call, state);
|
|
3040
3439
|
else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
|
|
3041
3440
|
else {
|
|
3042
3441
|
const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
|
|
@@ -3097,10 +3496,13 @@ var AgentLoop = class {
|
|
|
3097
3496
|
});
|
|
3098
3497
|
this.#emitTool(state, "failed", call, result.error?.code);
|
|
3099
3498
|
}
|
|
3100
|
-
for (const artifact of result.artifacts ?? [])
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3499
|
+
for (const artifact of result.artifacts ?? []) {
|
|
3500
|
+
state.artifactPaths.add(artifact.path);
|
|
3501
|
+
state.trace.record("artifact.created", { data: {
|
|
3502
|
+
artifactId: artifact.id,
|
|
3503
|
+
path: artifact.path
|
|
3504
|
+
} });
|
|
3505
|
+
}
|
|
3104
3506
|
if (state.unknownToolCalls > this.#config.maxUnknownToolRetries) throw new RunTerminated({
|
|
3105
3507
|
status: "failed",
|
|
3106
3508
|
reason: "tool-resolution-exhausted",
|
|
@@ -3116,7 +3518,12 @@ var AgentLoop = class {
|
|
|
3116
3518
|
state.trace.record("run.warning", { message: "UiBridge does not support renderSurface; UI surface was not rendered" });
|
|
3117
3519
|
return;
|
|
3118
3520
|
}
|
|
3119
|
-
|
|
3521
|
+
let attributed = this.#attributeSurfaceEvent(state, event);
|
|
3522
|
+
if (attributed.type === "open") {
|
|
3523
|
+
const kept = await this.#stripDuplicateFileLinks(state, attributed);
|
|
3524
|
+
if (kept === void 0) return;
|
|
3525
|
+
attributed = kept;
|
|
3526
|
+
}
|
|
3120
3527
|
if (attributed.type === "patch") this.#consumeSurfacePatchBudget(state);
|
|
3121
3528
|
await bridge.renderSurface(attributed);
|
|
3122
3529
|
state.surfaceEvents.push(structuredClone(attributed));
|
|
@@ -3159,6 +3566,44 @@ var AgentLoop = class {
|
|
|
3159
3566
|
}))
|
|
3160
3567
|
};
|
|
3161
3568
|
}
|
|
3569
|
+
/**
|
|
3570
|
+
* 剥掉指向「本 run 已写出 artifact」的 FileLink 节点(UI-UX5 #5):
|
|
3571
|
+
* 宿主在 run 完成时会把每个 artifact 自动渲染成下载卡(`buildRenderResult`),
|
|
3572
|
+
* 模型/技能再建一张就是重复。整棵树剥空返回 undefined(放弃这次 open);
|
|
3573
|
+
* 有剥离就记一条 run.warning,让「少了一张卡」在轨迹里可查。
|
|
3574
|
+
*/
|
|
3575
|
+
async #stripDuplicateFileLinks(state, event) {
|
|
3576
|
+
if (state.artifactPaths.size === 0) {
|
|
3577
|
+
const listed = await this.#deps.artifactStore.listArtifacts(state.runId).catch(() => []);
|
|
3578
|
+
for (const artifact of listed) state.artifactPaths.add(artifact.path);
|
|
3579
|
+
}
|
|
3580
|
+
if (state.artifactPaths.size === 0) return event;
|
|
3581
|
+
const paths = state.artifactPaths;
|
|
3582
|
+
let stripped = 0;
|
|
3583
|
+
const walk = (node) => {
|
|
3584
|
+
if (node.component === "FileLink") {
|
|
3585
|
+
const path = node.props?.["path"];
|
|
3586
|
+
if (typeof path === "string" && paths.has(path)) {
|
|
3587
|
+
stripped += 1;
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
if (!node.children || node.children.length === 0) return node;
|
|
3592
|
+
const children = node.children.map(walk).filter((child) => child !== void 0);
|
|
3593
|
+
if (children.length === node.children.length) return node;
|
|
3594
|
+
return {
|
|
3595
|
+
...node,
|
|
3596
|
+
children
|
|
3597
|
+
};
|
|
3598
|
+
};
|
|
3599
|
+
const node = walk(event.node);
|
|
3600
|
+
if (stripped === 0) return event;
|
|
3601
|
+
state.trace.record("run.warning", { message: `Dropped ${stripped} FileLink node(s) duplicating auto-rendered artifact download card(s)` });
|
|
3602
|
+
return node === void 0 ? void 0 : {
|
|
3603
|
+
...event,
|
|
3604
|
+
node
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3162
3607
|
/** Awaits the single action emitted with the most recently persisted tool result. */
|
|
3163
3608
|
async #drainSurfaceAction(state) {
|
|
3164
3609
|
const request = state.pendingSurfaceAction;
|
|
@@ -3303,9 +3748,149 @@ var AgentLoop = class {
|
|
|
3303
3748
|
}
|
|
3304
3749
|
});
|
|
3305
3750
|
}
|
|
3751
|
+
/**
|
|
3752
|
+
* 读取页面链接指向的文档(分册 22 §2.3 的六步)。
|
|
3753
|
+
*
|
|
3754
|
+
* 顺序不能调换:准入 → 同源判定 → 跨源确认 → 取数 → 格式分派 → 留痕。
|
|
3755
|
+
* 把确认放到取数之后,就等于「先下载了再问用户要不要下载」。
|
|
3756
|
+
*/
|
|
3757
|
+
async #handleReadLinkedDocument(call, state) {
|
|
3758
|
+
const reader = this.#deps.linkedDocuments;
|
|
3759
|
+
const url = call.arguments.url;
|
|
3760
|
+
if (typeof url !== "string" || url.trim() === "") return toolError("VALIDATION_FAILED", "read_linked_document needs a \"url\" string.");
|
|
3761
|
+
const audit = async (data) => {
|
|
3762
|
+
state.trace.record("run.warning", { message: `read_linked_document: ${JSON.stringify(data)}` });
|
|
3763
|
+
await this.#deps.documentAudit?.append({
|
|
3764
|
+
type: "document.read",
|
|
3765
|
+
target: url,
|
|
3766
|
+
data
|
|
3767
|
+
});
|
|
3768
|
+
};
|
|
3769
|
+
let target;
|
|
3770
|
+
try {
|
|
3771
|
+
target = assertRemoteUrlAllowed(url, this.#config.remoteUrl ?? {});
|
|
3772
|
+
} catch (e) {
|
|
3773
|
+
const message = messageOf(e);
|
|
3774
|
+
await audit({
|
|
3775
|
+
ok: false,
|
|
3776
|
+
reason: message
|
|
3777
|
+
});
|
|
3778
|
+
return toolError("NETWORK_BLOCKED", message);
|
|
3779
|
+
}
|
|
3780
|
+
const sameOrigin = reader.origin !== void 0 && reader.origin === target.origin;
|
|
3781
|
+
if (!sameOrigin) {
|
|
3782
|
+
if (!await this.#confirm(`Read the linked document at ${target.href}?`, state)) {
|
|
3783
|
+
await audit({
|
|
3784
|
+
ok: false,
|
|
3785
|
+
crossOrigin: true,
|
|
3786
|
+
approved: false,
|
|
3787
|
+
reason: "declined"
|
|
3788
|
+
});
|
|
3789
|
+
return toolError("TOOL_DENIED", `Reading ${target.href} was declined by the user.`);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
let fetched;
|
|
3793
|
+
try {
|
|
3794
|
+
fetched = await reader.read(target.href);
|
|
3795
|
+
} catch (e) {
|
|
3796
|
+
const message = messageOf(e);
|
|
3797
|
+
await audit({
|
|
3798
|
+
ok: false,
|
|
3799
|
+
crossOrigin: !sameOrigin,
|
|
3800
|
+
approved: !sameOrigin,
|
|
3801
|
+
reason: message
|
|
3802
|
+
});
|
|
3803
|
+
return toolError("TOOL_EXECUTION_FAILED", message);
|
|
3804
|
+
}
|
|
3805
|
+
const mime = fetched.mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
3806
|
+
const name = target.pathname.split("/").pop() || "document";
|
|
3807
|
+
const id = `doc-${state.runId}-${++state.documentSeq}`;
|
|
3808
|
+
const record = {
|
|
3809
|
+
ok: true,
|
|
3810
|
+
crossOrigin: !sameOrigin,
|
|
3811
|
+
approved: !sameOrigin,
|
|
3812
|
+
bytes: fetched.bytes.length,
|
|
3813
|
+
mimeType: mime
|
|
3814
|
+
};
|
|
3815
|
+
if (mime === SUPPORTED_DOCUMENT_MIME.pdf) {
|
|
3816
|
+
const part = {
|
|
3817
|
+
type: "file",
|
|
3818
|
+
mimeType: mime,
|
|
3819
|
+
data: toBase64(fetched.bytes),
|
|
3820
|
+
name,
|
|
3821
|
+
id
|
|
3822
|
+
};
|
|
3823
|
+
const tooLarge = this.#documentTooLarge(part);
|
|
3824
|
+
if (tooLarge !== void 0) {
|
|
3825
|
+
await audit({
|
|
3826
|
+
...record,
|
|
3827
|
+
ok: false,
|
|
3828
|
+
reason: tooLarge
|
|
3829
|
+
});
|
|
3830
|
+
return toolError("TOOL_RESULT_TOO_LARGE", tooLarge);
|
|
3831
|
+
}
|
|
3832
|
+
await audit(record);
|
|
3833
|
+
return {
|
|
3834
|
+
ok: true,
|
|
3835
|
+
content: [part]
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
let text;
|
|
3839
|
+
if (mime === SUPPORTED_DOCUMENT_MIME.docx) {
|
|
3840
|
+
if (this.#deps.docxExtractor === void 0) {
|
|
3841
|
+
await audit({
|
|
3842
|
+
...record,
|
|
3843
|
+
ok: false,
|
|
3844
|
+
reason: "no docx extractor"
|
|
3845
|
+
});
|
|
3846
|
+
return toolError("TOOL_UNSUPPORTED", "Word documents cannot be read in this environment: no docx text extractor is configured.");
|
|
3847
|
+
}
|
|
3848
|
+
try {
|
|
3849
|
+
text = await this.#deps.docxExtractor(fetched.bytes);
|
|
3850
|
+
} catch (e) {
|
|
3851
|
+
const message = messageOf(e);
|
|
3852
|
+
await audit({
|
|
3853
|
+
...record,
|
|
3854
|
+
ok: false,
|
|
3855
|
+
reason: message
|
|
3856
|
+
});
|
|
3857
|
+
return toolError("TOOL_EXECUTION_FAILED", message);
|
|
3858
|
+
}
|
|
3859
|
+
} else if (mime.startsWith("text/")) text = new TextDecoder().decode(fetched.bytes);
|
|
3860
|
+
else {
|
|
3861
|
+
await audit({
|
|
3862
|
+
...record,
|
|
3863
|
+
ok: false,
|
|
3864
|
+
reason: `unsupported mime ${mime}`
|
|
3865
|
+
});
|
|
3866
|
+
return toolError("TOOL_UNSUPPORTED", `${UNSUPPORTED_DOCUMENT_MESSAGE} This link served "${mime}".`);
|
|
3867
|
+
}
|
|
3868
|
+
const part = {
|
|
3869
|
+
type: "document-text",
|
|
3870
|
+
text,
|
|
3871
|
+
name,
|
|
3872
|
+
id
|
|
3873
|
+
};
|
|
3874
|
+
const tooLarge = this.#documentTooLarge(part);
|
|
3875
|
+
if (tooLarge !== void 0) {
|
|
3876
|
+
await audit({
|
|
3877
|
+
...record,
|
|
3878
|
+
ok: false,
|
|
3879
|
+
reason: tooLarge
|
|
3880
|
+
});
|
|
3881
|
+
return toolError("TOOL_RESULT_TOO_LARGE", tooLarge);
|
|
3882
|
+
}
|
|
3883
|
+
await audit(record);
|
|
3884
|
+
return {
|
|
3885
|
+
ok: true,
|
|
3886
|
+
content: [part]
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3306
3889
|
async #handleAskUser(call, state) {
|
|
3890
|
+
const rawFields = call.arguments["fields"];
|
|
3891
|
+
if (Array.isArray(rawFields) && rawFields.length > 0) return this.#handleAskUserFields(call, state, rawFields);
|
|
3307
3892
|
const question = call.arguments["question"];
|
|
3308
|
-
if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires a non-empty \"question\" string
|
|
3893
|
+
if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires either a non-empty \"question\" string or a non-empty \"fields\" array");
|
|
3309
3894
|
const rawChoices = call.arguments["choices"];
|
|
3310
3895
|
const choices = Array.isArray(rawChoices) ? rawChoices.filter((choice) => typeof choice === "string" && choice !== "") : [];
|
|
3311
3896
|
const id = this.#nextInteractionId(state);
|
|
@@ -3344,6 +3929,60 @@ var AgentLoop = class {
|
|
|
3344
3929
|
throw e;
|
|
3345
3930
|
}
|
|
3346
3931
|
}
|
|
3932
|
+
/**
|
|
3933
|
+
* 多字段 `ask_user`(FR-11.1)。构造的是与「技能缺参」同一个 `{ type: 'form' }`
|
|
3934
|
+
* 交互,因此渲染、回传、快照都走既有那一条路径——不允许出现第二套表单实现。
|
|
3935
|
+
*/
|
|
3936
|
+
async #handleAskUserFields(call, state, rawFields) {
|
|
3937
|
+
const limit = this.#config.askUserMaxFields;
|
|
3938
|
+
if (rawFields.length > limit) return toolError("TOOL_EXECUTION_FAILED", `ask_user accepts at most ${limit} fields, received ${rawFields.length}. Split the collection into several steps, or group related fields and ask for the rest afterwards.`);
|
|
3939
|
+
const fields = [];
|
|
3940
|
+
for (const [index, raw] of rawFields.entries()) {
|
|
3941
|
+
const field = raw;
|
|
3942
|
+
const at = `fields[${index}]`;
|
|
3943
|
+
if (typeof field !== "object" || field === null) return toolError("VALIDATION_FAILED", `ask_user ${at} must be an object`);
|
|
3944
|
+
const name = field["name"];
|
|
3945
|
+
const label = field["label"];
|
|
3946
|
+
const type = field["type"];
|
|
3947
|
+
if (typeof name !== "string" || name === "") return toolError("VALIDATION_FAILED", `ask_user ${at} requires a non-empty "name"`);
|
|
3948
|
+
if (typeof label !== "string" || label === "") return toolError("VALIDATION_FAILED", `ask_user ${at} requires a non-empty "label"`);
|
|
3949
|
+
if (type === "multi-select") return toolError("VALIDATION_FAILED", `ask_user ${at} does not support "multi-select". Use render_ui with a MultiSelect field instead.`);
|
|
3950
|
+
if (typeof type !== "string" || !ASK_USER_FIELD_TYPES.includes(type)) return toolError("VALIDATION_FAILED", `ask_user ${at} has unsupported type "${String(type)}". Supported types: ${ASK_USER_FIELD_TYPES.join(", ")}.`);
|
|
3951
|
+
const options = field["options"];
|
|
3952
|
+
if (type === "select" && !Array.isArray(options)) return toolError("VALIDATION_FAILED", `ask_user ${at} is a select and requires "options"`);
|
|
3953
|
+
fields.push({
|
|
3954
|
+
name,
|
|
3955
|
+
label,
|
|
3956
|
+
type,
|
|
3957
|
+
...field["required"] === true ? { required: true } : {},
|
|
3958
|
+
...typeof field["description"] === "string" ? { description: field["description"] } : {},
|
|
3959
|
+
...Array.isArray(options) ? { options: options.filter((o) => typeof o === "object" && o !== null).map((o) => ({
|
|
3960
|
+
label: String(o["label"] ?? o["value"]),
|
|
3961
|
+
value: o["value"]
|
|
3962
|
+
})) } : {}
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
const question = call.arguments["question"];
|
|
3966
|
+
if (typeof question === "string" && question !== "") state.trace.record("run.warning", { message: "ask_user received both \"question\" and \"fields\"; the form was used and the single question was ignored." });
|
|
3967
|
+
try {
|
|
3968
|
+
const value = await this.#interact(state, {
|
|
3969
|
+
type: "form",
|
|
3970
|
+
id: this.#nextInteractionId(state),
|
|
3971
|
+
...typeof question === "string" && question !== "" ? { title: question } : {},
|
|
3972
|
+
fields
|
|
3973
|
+
}, { tool: call.name });
|
|
3974
|
+
return {
|
|
3975
|
+
ok: true,
|
|
3976
|
+
content: [{
|
|
3977
|
+
type: "text",
|
|
3978
|
+
text: typeof value === "string" ? value : JSON.stringify(value ?? {})
|
|
3979
|
+
}]
|
|
3980
|
+
};
|
|
3981
|
+
} catch (e) {
|
|
3982
|
+
if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `ask_user failed: ${e.message}`);
|
|
3983
|
+
throw e;
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3347
3986
|
async #handleReadSkillFile(call, state) {
|
|
3348
3987
|
const skillName = call.arguments["skillName"];
|
|
3349
3988
|
if (typeof skillName !== "string" || skillName === "") return toolError("TOOL_EXECUTION_FAILED", "read_skill_file requires a non-empty \"skillName\" string argument");
|
|
@@ -3417,27 +4056,89 @@ var AgentLoop = class {
|
|
|
3417
4056
|
return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
|
|
3418
4057
|
}
|
|
3419
4058
|
/**
|
|
3420
|
-
*
|
|
3421
|
-
*
|
|
4059
|
+
* 超预算的文档分片换成一条说明(FR-23.4)。**不截断**:
|
|
4060
|
+
* 半份 PDF 是坏文件,半份抽取文本会让模型以为自己读全了。
|
|
4061
|
+
*/
|
|
4062
|
+
#documentTooLarge(part) {
|
|
4063
|
+
if (part.type !== "file" && part.type !== "document-text") return void 0;
|
|
4064
|
+
const isFile = part.type === "file";
|
|
4065
|
+
const size = isFile ? part.data.length : new TextEncoder().encode(part.text).length;
|
|
4066
|
+
const max = isFile ? this.#config.maxDocumentBytes : this.#config.maxDocumentTextBytes;
|
|
4067
|
+
if (size <= max) return void 0;
|
|
4068
|
+
return `The ${isFile ? "document" : "extracted document text"}${part.name ? ` "${part.name}"` : ""} was not attached: it is ${size} bytes, over the ${max} byte limit. It was not truncated, because a partial document would be unusable. Ask for a smaller file, a specific page range, or a summary produced elsewhere.`;
|
|
4069
|
+
}
|
|
4070
|
+
/** 推一条 tool 消息,返回需要另投 user 消息的二进制分片 */
|
|
4071
|
+
async #pushToolResult(state, call, result) {
|
|
4072
|
+
const split = await this.#toolResultParts(call, result, state);
|
|
4073
|
+
state.messages.push({
|
|
4074
|
+
role: "tool",
|
|
4075
|
+
toolCallId: call.id,
|
|
4076
|
+
content: split.tool
|
|
4077
|
+
});
|
|
4078
|
+
return split.carried;
|
|
4079
|
+
}
|
|
4080
|
+
/**
|
|
4081
|
+
* 工具结果 → tool 消息内容 + 另投的二进制分片。只有白名单内的分片进 JSON 并参与截断计算(FR-23.3):
|
|
4082
|
+
* 100 KB 的文本预算遇到一张 200 KB 的图会把整个结果截成垃圾,
|
|
4083
|
+
* 一份被截断的 PDF 则直接是坏文件。
|
|
4084
|
+
*
|
|
4085
|
+
* `carried` 单独返回而不是并进 tool 消息:OpenAI 协议的 tool 消息只有 `content: string`,
|
|
4086
|
+
* Google 的 `functionResponse`、Anthropic 的 `tool_result` 同样只收文本——
|
|
4087
|
+
* 三家客户端都会当场抛 `VALIDATION_FAILED`。二进制只能走 user 消息。
|
|
3422
4088
|
*/
|
|
3423
4089
|
async #toolResultParts(call, result, state) {
|
|
3424
|
-
const
|
|
3425
|
-
|
|
4090
|
+
const budgeted = result.content.filter((part) => TEXT_BUDGETED_CONTENT_TYPES.has(part.type));
|
|
4091
|
+
const passthrough = result.content.filter((part) => !TEXT_BUDGETED_CONTENT_TYPES.has(part.type));
|
|
4092
|
+
if (passthrough.length === 0) return {
|
|
4093
|
+
tool: textParts(await this.#serializeToolResult(call, result, state)),
|
|
4094
|
+
carried: []
|
|
4095
|
+
};
|
|
3426
4096
|
const textual = {
|
|
3427
4097
|
...result,
|
|
3428
|
-
content:
|
|
4098
|
+
content: budgeted
|
|
4099
|
+
};
|
|
4100
|
+
const notes = [];
|
|
4101
|
+
const carried = [];
|
|
4102
|
+
for (const part of passthrough) {
|
|
4103
|
+
const tooLarge = this.#documentTooLarge(part);
|
|
4104
|
+
if (tooLarge) {
|
|
4105
|
+
state.trace.record("run.warning", { message: tooLarge });
|
|
4106
|
+
notes.push(tooLarge);
|
|
4107
|
+
continue;
|
|
4108
|
+
}
|
|
4109
|
+
if (part.type === "image") carried.push({
|
|
4110
|
+
type: "image",
|
|
4111
|
+
mimeType: part.mimeType,
|
|
4112
|
+
data: part.data
|
|
4113
|
+
});
|
|
4114
|
+
else if (part.type === "file") carried.push({
|
|
4115
|
+
type: "file",
|
|
4116
|
+
mimeType: part.mimeType,
|
|
4117
|
+
data: part.data,
|
|
4118
|
+
...part.name ? { name: part.name } : {}
|
|
4119
|
+
});
|
|
4120
|
+
else if (part.type === "document-text") notes.push(part.text);
|
|
4121
|
+
}
|
|
4122
|
+
return {
|
|
4123
|
+
tool: [
|
|
4124
|
+
{
|
|
4125
|
+
type: "text",
|
|
4126
|
+
text: await this.#serializeToolResult(call, textual, state)
|
|
4127
|
+
},
|
|
4128
|
+
...notes.map((text) => ({
|
|
4129
|
+
type: "text",
|
|
4130
|
+
text
|
|
4131
|
+
})),
|
|
4132
|
+
...carried.length > 0 ? [{
|
|
4133
|
+
type: "text",
|
|
4134
|
+
text: `The ${carried.length === 1 ? "attachment" : `${carried.length} attachments`} from "${call.name}" ${carried.length === 1 ? "is" : "are"} in the next message.`
|
|
4135
|
+
}] : []
|
|
4136
|
+
],
|
|
4137
|
+
carried
|
|
3429
4138
|
};
|
|
3430
|
-
return [{
|
|
3431
|
-
type: "text",
|
|
3432
|
-
text: await this.#serializeToolResult(call, textual, state)
|
|
3433
|
-
}, ...images.map((image) => ({
|
|
3434
|
-
type: "image",
|
|
3435
|
-
mimeType: image.mimeType,
|
|
3436
|
-
data: image.data
|
|
3437
|
-
}))];
|
|
3438
4139
|
}
|
|
3439
4140
|
/**
|
|
3440
|
-
* 工具结果回喂序列化:超过 toolResultMaxBytes(默认
|
|
4141
|
+
* 工具结果回喂序列化:超过 toolResultMaxBytes(默认 256KB)时头尾保留截断,
|
|
3441
4142
|
* 完整内容经 artifactStore 落 artifact,回喂摘要含 artifact id。
|
|
3442
4143
|
*/
|
|
3443
4144
|
async #serializeToolResult(call, result, state) {
|
|
@@ -3675,6 +4376,7 @@ var AgentLoop = class {
|
|
|
3675
4376
|
skillRoot: root,
|
|
3676
4377
|
runId: state.runId,
|
|
3677
4378
|
confirm: (message) => this.#confirm(message, state),
|
|
4379
|
+
...this.#deps.fetchData ? { fetchData: this.#deps.fetchData } : {},
|
|
3678
4380
|
onWarning: (message) => state.trace.record("run.warning", { message }),
|
|
3679
4381
|
onArtifactCreated: (artifact) => createdIds.add(artifact.id)
|
|
3680
4382
|
});
|
|
@@ -4091,7 +4793,11 @@ var WebSkillRuntime = class {
|
|
|
4091
4793
|
snapshotStore: this.#deps.snapshotStore,
|
|
4092
4794
|
skillStateGuard: this.#deps.skillStateGuard,
|
|
4093
4795
|
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
4094
|
-
skillOutcomeReporter: this.#deps.skillOutcomeReporter
|
|
4796
|
+
skillOutcomeReporter: this.#deps.skillOutcomeReporter,
|
|
4797
|
+
fetchData: this.#deps.fetchData,
|
|
4798
|
+
linkedDocuments: this.#deps.linkedDocuments,
|
|
4799
|
+
docxExtractor: this.#deps.docxExtractor,
|
|
4800
|
+
documentAudit: this.#deps.documentAudit
|
|
4095
4801
|
}, this.#deps.config);
|
|
4096
4802
|
const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
|
|
4097
4803
|
this.#loops.set(runId, loop);
|
|
@@ -4212,7 +4918,11 @@ var WebSkillRuntime = class {
|
|
|
4212
4918
|
snapshotStore: store,
|
|
4213
4919
|
skillStateGuard: this.#deps.skillStateGuard,
|
|
4214
4920
|
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
4215
|
-
skillOutcomeReporter: this.#deps.skillOutcomeReporter
|
|
4921
|
+
skillOutcomeReporter: this.#deps.skillOutcomeReporter,
|
|
4922
|
+
fetchData: this.#deps.fetchData,
|
|
4923
|
+
linkedDocuments: this.#deps.linkedDocuments,
|
|
4924
|
+
docxExtractor: this.#deps.docxExtractor,
|
|
4925
|
+
documentAudit: this.#deps.documentAudit
|
|
4216
4926
|
}, this.#deps.config);
|
|
4217
4927
|
this.#loops.set(runId, loop);
|
|
4218
4928
|
try {
|
|
@@ -4723,7 +5433,9 @@ function parseBridgeRequest(data) {
|
|
|
4723
5433
|
const { kind, id } = data;
|
|
4724
5434
|
if (!isNonEmptyString(id)) return void 0;
|
|
4725
5435
|
switch (kind) {
|
|
4726
|
-
case "readReference":
|
|
5436
|
+
case "readReference":
|
|
5437
|
+
case "readAsset":
|
|
5438
|
+
case "readAssetBinary": return isNonEmptyString(data["path"]) ? {
|
|
4727
5439
|
kind,
|
|
4728
5440
|
id,
|
|
4729
5441
|
path: data["path"]
|
|
@@ -4745,6 +5457,16 @@ function parseBridgeRequest(data) {
|
|
|
4745
5457
|
id,
|
|
4746
5458
|
message: data["message"]
|
|
4747
5459
|
} : void 0;
|
|
5460
|
+
case "fetchData": {
|
|
5461
|
+
if (!isNonEmptyString(data["sourceId"])) return void 0;
|
|
5462
|
+
const params = data["params"];
|
|
5463
|
+
return {
|
|
5464
|
+
kind,
|
|
5465
|
+
id,
|
|
5466
|
+
sourceId: data["sourceId"],
|
|
5467
|
+
...isRecord(params) ? { params } : {}
|
|
5468
|
+
};
|
|
5469
|
+
}
|
|
4748
5470
|
default: return;
|
|
4749
5471
|
}
|
|
4750
5472
|
}
|
|
@@ -4930,6 +5652,38 @@ var CapabilityApproval = class CapabilityApproval {
|
|
|
4930
5652
|
return "allowed";
|
|
4931
5653
|
}
|
|
4932
5654
|
};
|
|
5655
|
+
/**
|
|
5656
|
+
* 从 run 的 trace 聚合大模型用量(请求次数 + 输入/输出 token)。
|
|
5657
|
+
* 与 `summarizeToolCalls` 同型:消费方不再各自遍历 trace。
|
|
5658
|
+
* @stable
|
|
5659
|
+
*/
|
|
5660
|
+
function summarizeRunUsage(trace) {
|
|
5661
|
+
let llmCalls = 0;
|
|
5662
|
+
let inputTokens = 0;
|
|
5663
|
+
let outputTokens = 0;
|
|
5664
|
+
let hasUsage = false;
|
|
5665
|
+
for (const event of trace) {
|
|
5666
|
+
if (event.type === "llm.request") {
|
|
5667
|
+
llmCalls += 1;
|
|
5668
|
+
continue;
|
|
5669
|
+
}
|
|
5670
|
+
if (event.type !== "llm.usage") continue;
|
|
5671
|
+
const input = event.data?.["inputTokens"];
|
|
5672
|
+
const output = event.data?.["outputTokens"];
|
|
5673
|
+
const hasInput = typeof input === "number" && Number.isFinite(input);
|
|
5674
|
+
const hasOutput = typeof output === "number" && Number.isFinite(output);
|
|
5675
|
+
if (hasInput) inputTokens += input;
|
|
5676
|
+
if (hasOutput) outputTokens += output;
|
|
5677
|
+
if (hasInput || hasOutput) hasUsage = true;
|
|
5678
|
+
}
|
|
5679
|
+
return {
|
|
5680
|
+
llmCalls,
|
|
5681
|
+
...hasUsage ? {
|
|
5682
|
+
inputTokens,
|
|
5683
|
+
outputTokens
|
|
5684
|
+
} : {}
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
4933
5687
|
const RUN_TRACE_SCHEMA_VERSION = 1;
|
|
4934
5688
|
/** 终止原因:取最后一条 run.completed/cancelled/failed 事件的 data.reason */
|
|
4935
5689
|
function extractEndReason(events) {
|
|
@@ -4944,6 +5698,7 @@ function extractEndReason(events) {
|
|
|
4944
5698
|
function summarize(trace) {
|
|
4945
5699
|
const endReason = extractEndReason(trace.events);
|
|
4946
5700
|
const durationMs = trace.endedAt === void 0 ? void 0 : Date.parse(trace.endedAt) - Date.parse(trace.startedAt);
|
|
5701
|
+
const runUsage = summarizeRunUsage(trace.events);
|
|
4947
5702
|
return {
|
|
4948
5703
|
runId: trace.runId,
|
|
4949
5704
|
startedAt: trace.startedAt,
|
|
@@ -4954,7 +5709,11 @@ function summarize(trace) {
|
|
|
4954
5709
|
...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
|
|
4955
5710
|
...trace.endedAt !== void 0 ? { endedAt: trace.endedAt } : {},
|
|
4956
5711
|
...endReason !== void 0 ? { endReason } : {},
|
|
4957
|
-
...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {}
|
|
5712
|
+
...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {},
|
|
5713
|
+
...runUsage.inputTokens !== void 0 ? { usage: {
|
|
5714
|
+
inputTokens: runUsage.inputTokens,
|
|
5715
|
+
outputTokens: runUsage.outputTokens ?? 0
|
|
5716
|
+
} } : {}
|
|
4958
5717
|
};
|
|
4959
5718
|
}
|
|
4960
5719
|
function parseTraceFile(raw, path) {
|
|
@@ -5147,6 +5906,12 @@ function applyFilter(summaries, filter) {
|
|
|
5147
5906
|
* @stable
|
|
5148
5907
|
*/
|
|
5149
5908
|
function summarizeToolCalls(run) {
|
|
5909
|
+
const startedAtByCallId = /* @__PURE__ */ new Map();
|
|
5910
|
+
for (const event of run.trace) {
|
|
5911
|
+
if (event.type !== "tool.started") continue;
|
|
5912
|
+
const callId = event.data?.["callId"];
|
|
5913
|
+
if (typeof callId === "string" && !startedAtByCallId.has(callId)) startedAtByCallId.set(callId, event.ts);
|
|
5914
|
+
}
|
|
5150
5915
|
const calls = [];
|
|
5151
5916
|
for (const event of run.trace) {
|
|
5152
5917
|
if (event.type !== "tool.completed" && event.type !== "tool.failed") continue;
|
|
@@ -5156,6 +5921,7 @@ function summarizeToolCalls(run) {
|
|
|
5156
5921
|
const args = event.data?.["args"];
|
|
5157
5922
|
const durationMs = event.data?.["durationMs"];
|
|
5158
5923
|
const errorCode = event.data?.["code"];
|
|
5924
|
+
const startedAt = startedAtByCallId.get(callId);
|
|
5159
5925
|
calls.push({
|
|
5160
5926
|
callId,
|
|
5161
5927
|
name,
|
|
@@ -5163,6 +5929,7 @@ function summarizeToolCalls(run) {
|
|
|
5163
5929
|
...typeof args === "string" ? { args } : {},
|
|
5164
5930
|
...typeof durationMs === "number" ? { durationMs } : {},
|
|
5165
5931
|
...typeof errorCode === "string" ? { errorCode } : {},
|
|
5932
|
+
...startedAt !== void 0 ? { startedAt } : {},
|
|
5166
5933
|
...event.type === "tool.failed" && typeof event.message === "string" ? { errorMessage: event.message } : {}
|
|
5167
5934
|
});
|
|
5168
5935
|
}
|
|
@@ -5386,4 +6153,4 @@ var FsSessionStore = class {
|
|
|
5386
6153
|
};
|
|
5387
6154
|
|
|
5388
6155
|
//#endregion
|
|
5389
|
-
export {
|
|
6156
|
+
export { createWebSkillApi as $, READ_LINKED_DOCUMENT_TOOL_NAME as A, schemaSourceLabel as At, TraceRecorder as B, validateUiSpecNode as Bt, FsSessionStore as C, readBehaviorRecords as Ct, OpenAiCompatibleClient as D, renderUserProfileContext as Dt, HookRunner as E, refineUserProfile as Et, RUN_TRACE_SCHEMA_VERSION as F, summarizeToolCalls as Ft, USER_PROFILE_PROMPT_HEADER as G, USER_PROFILE_EXPORT_VERSION as H, SESSION_SCHEMA_VERSION as I, toLlmToolSpec as It, appendBehaviorRecords as J, USER_PROFILE_REFINE_PROMPT as K, SUPPORTED_DOCUMENT_MIME as L, toRecordDigests as Lt, READ_SKILL_FILE_TOOL as M, scriptToolName as Mt, READ_SKILL_FILE_TOOL_NAME as N, sealToolCallPairs as Nt, ProgressiveRouter as O, resolveToolName as Ot, RUN_SNAPSHOT_SCHEMA_VERSION as P, summarizeRunUsage as Pt, createScriptContext as Q, SerializingMemoryStore as R, toVercelToolSpecs as Rt, FsRunTraceStore as S, parseUserProfileExport as St, GoogleGenAiClient as T, readUserProfile as Tt, USER_PROFILE_KEY as U, UNSUPPORTED_DOCUMENT_MESSAGE as V, USER_PROFILE_NO_INVENTION_RULE as W, bridgeError as X, applyUserProfileImport as Y, buildRenderResult as Z, EventBus as _, networkUrlHost as _t, ASK_USER_TOOL as a, extractUiSpecEvents as at, FsMemoryStore as b, normalizeToolError as bt, AnthropicClient as c, fromVercelResult as ct, DEFAULT_LOOP_LIMITS as d, isNetworkAllowed as dt, diffUserProfile as et, DEFAULT_MAX_DATA_SOURCE_BYTES as f, isUnsupportedRunSnapshot as ft, EMPTY_USER_PROFILE as g, networkPolicyLibSource as gt, DEFAULT_USER_PROFILE_LIMITS as h, mergeProfileEntries as ht, ASK_USER_MAX_FIELDS as i, extractTodoTraceEvents as it, READ_SKILL_FILE_INPUT_SCHEMA as j, schemaToForm as jt, READ_LINKED_DOCUMENT_TOOL as k, sampleBehaviorRecords as kt, BEHAVIOR_RECORDS_KEY as l, fromVercelStreamPart as lt, DEFAULT_MAX_DOCUMENT_TEXT_BYTES as m, mergeCatalogEntries as mt, ASK_USER_FIELD_TYPES as n, extractChartSpec as nt, ASK_USER_TOOL_NAME as o, findUnpairedToolCalls as ot, DEFAULT_MAX_DOCUMENT_BYTES as p, listSkillScripts as pt, WebSkillRuntime as q, ASK_USER_INPUT_SCHEMA as r, extractSkillCandidate as rt, AgentLoop as s, formatSkillScriptManifest as st, ALLOWED_TOOLS_EXCLUSION_REASON as t, exportUserProfile as tt, CapabilityApproval as u, interruptedToolResult as ut, FS_SESSION_PAGE_SIZE as v, normalizeErrorCode as vt, FullDisclosureRouter as w, readProfileEntries as wt, FsRunSnapshotStore as x, parseBridgeRequest as xt, FsArtifactStore as y, normalizeToolContent as yt, TEXT_BUDGETED_CONTENT_TYPES as z, validateUiSpecEvent as zt };
|