@webskill/sdk 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/agent.d.ts +3 -3
  2. package/dist/agent.js +2442 -3
  3. package/dist/{dist-Bfga1TmS.js → approval-BnLLCOG0.js} +235 -2932
  4. package/dist/browser.d.ts +106 -5
  5. package/dist/browser.js +364 -10
  6. package/dist/{openUiLibrary-BKXW7Iwx-DjvEnMlJ.js → dist-CTsxblSD.js} +496 -28
  7. package/dist/{echarts-De78wXqV.js → echarts-BE7oV_Dl.js} +1 -1
  8. package/dist/{env-8cY40DXB-CGnEVZby.js → env-Bj1MI2Ww.js} +1 -1
  9. package/dist/errors-BDZNpC13.js +22 -0
  10. package/dist/{eventTypes-FllCrX-Z-DNDeHWoG.js → eventTypes-Y07N8IAC.js} +14 -2
  11. package/dist/external-_ZRQe-V9.js +53 -0
  12. package/dist/governance.d.ts +15 -3
  13. package/dist/governance.js +86 -4
  14. package/dist/{index-DQwGvHAI.d.ts → index-CQYo4tcT.d.ts} +85 -6
  15. package/dist/{index-sd-gVKey.d.ts → index-CU2md8R1.d.ts} +26 -18
  16. package/dist/{index-CWoKOFBP.d.ts → index-Oc3H89TJ.d.ts} +159 -3
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.js +1001 -5
  19. package/dist/kind-DxgS8LM-.js +77 -0
  20. package/dist/linkedDocument-xNXF-z8n.js +94 -0
  21. package/dist/llm-B7lLH0ZI.js +1767 -0
  22. package/dist/mcp.d.ts +2 -2
  23. package/dist/mcp.js +41 -4
  24. package/dist/memoryArtifactStore-D6dLeY55.js +48 -0
  25. package/dist/miniChart-owdCdaw-.js +533 -0
  26. package/dist/node.d.ts +3 -3
  27. package/dist/node.js +106 -9
  28. package/dist/openUiLibrary-CY_ADoe0.js +30 -0
  29. package/dist/openUiSpecLang-DuOCdw9G.js +1849 -0
  30. package/dist/pathSecurity-B1owvJAF.js +65 -0
  31. package/dist/processSandboxEntry.js +5 -1
  32. package/dist/renderResult-D9Q-Vu2x.js +143 -0
  33. package/dist/{rolldown-runtime-BOF7iYI8.js → rolldown-runtime-BHkdefai.js} +1 -2
  34. package/dist/sandboxWorkerEntry.js +5 -1
  35. package/dist/{dist-D5YhlCdY.js → skill-CAJMsLod.js} +44 -372
  36. package/dist/{skillVersionStore-D-qHk9ZE-C0OFDTQY.d.ts → skillVersionStore-B24Jjjry-C4zomhMZ.d.ts} +14 -2
  37. package/dist/surface-DVGiCmwq.js +145 -0
  38. package/dist/testing.d.ts +1 -1
  39. package/dist/testing.js +64 -3
  40. package/dist/{types-C3V6lAeO-BgeOvc1Y.d.ts → types-C3V6lAeO-BIf7UB2e.d.ts} +17 -1
  41. package/dist/types-TUydnxaj.js +78 -0
  42. package/dist/ui-react.d.ts +12 -5
  43. package/dist/ui-react.js +128548 -4729
  44. package/dist/ui-vue.d.ts +1 -1
  45. package/dist/ui-vue.js +11 -2
  46. package/dist/ui.d.ts +3 -3
  47. package/dist/ui.js +1254 -3
  48. package/dist/urlSafety-CiSuCJvX.js +65 -0
  49. package/dist/webSkillApi-DOOyz2G5.js +106 -0
  50. package/dist/{webskillLitCatalog-D_zCqeQF-swXsWoAe.js → webskillLitCatalog-DSqVFQjK.js} +11 -3
  51. package/package.json +1 -1
  52. package/dist/catalogComponents-BgAJN0p8-Cr55ouOg.js +0 -124384
  53. package/dist/client-BCM6Z3yq-qUQNkKrK.js +0 -7787
  54. package/dist/dist-BnqAkSS6.js +0 -2384
  55. package/dist/dist-CiaDIqkV.js +0 -3509
  56. package/dist/dist-DxyxmXPU-DGC-dpXf.js +0 -5621
  57. package/dist/memoryArtifactStore-52Zn9npI-BxtUkrgV.js +0 -86
  58. package/dist/stdio-CFMoANJJ-BxrTeXh7.js +0 -31
  59. package/dist/testing-Cm8MseLF.js +0 -142
  60. package/dist/types-WovEf4ED-CZSDiiBU.js +0 -6215
@@ -1,1614 +1,41 @@
1
- import { B as messageOf, C as assertSafePathSegment, H as parseSkillMarkdown, K as renderAvailableSkillsXml, S as assertRemoteUrlAllowed, T as buildCatalog, Y as resolveInsideRoot, _ as SkillDiscovery, b as WebSkillError, et as validateSkills, v as SkillReader, w as atomicWriteText } from "./dist-D5YhlCdY.js";
2
- import { a as textParts, i as rejectUnsupportedPart, n as partsToText, o as validateLlmMessages, r as promptText, t as MemoryArtifactStore } from "./memoryArtifactStore-52Zn9npI-BxtUkrgV.js";
1
+ import { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
2
+ import { b as parseSkillMarkdown, i as buildCatalog, n as SkillDiscovery, r as SkillReader } from "./skill-CAJMsLod.js";
3
+ import { a as atomicWriteText, r as resolveInsideRoot, t as assertSafePathSegment } from "./pathSecurity-B1owvJAF.js";
4
+ import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
5
+ import { c as findUnpairedToolCalls, d as partsToText, f as promptText, l as interruptedToolResult, p as textParts, u as sealToolCallPairs } from "./llm-B7lLH0ZI.js";
6
+ import { a as fromBase64, c as DEFAULT_MAX_DOCUMENT_BYTES, d as TEXT_BUDGETED_CONTENT_TYPES, i as UNSUPPORTED_DOCUMENT_MESSAGE, l as DEFAULT_MAX_DOCUMENT_TEXT_BYTES, n as READ_LINKED_DOCUMENT_TOOL_NAME, o as toBase64, r as SUPPORTED_DOCUMENT_MIME, t as READ_LINKED_DOCUMENT_TOOL } from "./linkedDocument-xNXF-z8n.js";
7
+ import { t as mergeCatalogEntries } from "./external-_ZRQe-V9.js";
8
+ import { i as isArtifactCardHidden, n as extractChartSpec, r as admitArtifactMetadata, t as buildRenderResult } from "./renderResult-D9Q-Vu2x.js";
9
+ import { t as extractUiSpecEvents } from "./surface-DVGiCmwq.js";
10
+ import { t as MemoryArtifactStore } from "./memoryArtifactStore-D6dLeY55.js";
3
11
 
4
- //#region ../runtime/dist/index.js
5
- /** 与正常工具结果同构:模型读到的是「这次调用被中断了」,而不是一个凭空消失的调用 */
6
- function interruptedToolResult(call, options = {}) {
7
- const code = options.code ?? "RUN_INTERRUPTED";
8
- const because = options.reason === void 0 ? "" : ` (${options.reason})`;
9
- return {
10
- ok: false,
11
- content: [],
12
- error: {
13
- code,
14
- message: `Tool call "${call.name}" produced no result: the run ended before it finished${because}.`
15
- }
16
- };
17
- }
18
- /** 尚无 `role:'tool'` 应答的工具调用,按消息顺序 */
19
- function findUnpairedToolCalls(messages) {
20
- const answered = new Set(messages.filter((m) => m.role === "tool").map((m) => m.toolCallId));
21
- return messages.filter((m) => m.role === "assistant" && m.toolCalls?.length).flatMap((m) => m.toolCalls ?? []).filter((call) => !answered.has(call.id));
22
- }
23
- /**
24
- * 为每个未应答的工具调用补一条结构化的中断说明。
25
- *
26
- * 补齐消息紧跟在该 assistant 已有的 tool 兄弟之后——供应商装配按相邻块分组,
27
- * 插到序列末尾会让它归属到别的 assistant 消息上。
28
- * 本函数**不改写入参**,也不写盘(AC-11.8)。
29
- */
30
- function sealToolCallPairs(messages, options = {}) {
31
- const answered = new Set(messages.filter((m) => m.role === "tool").map((m) => m.toolCallId));
32
- const code = options.code ?? "RUN_INTERRUPTED";
33
- const sealed = [];
34
- const out = [];
35
- for (let i = 0; i < messages.length; i += 1) {
36
- const message = messages[i];
37
- out.push(message);
38
- if (message.role !== "assistant" || !message.toolCalls?.length) continue;
39
- const missing = message.toolCalls.filter((call) => !answered.has(call.id));
40
- if (missing.length === 0) continue;
41
- while (i + 1 < messages.length && messages[i + 1].role === "tool") {
42
- out.push(messages[i + 1]);
43
- i += 1;
44
- }
45
- for (const call of missing) {
46
- out.push({
47
- role: "tool",
48
- toolCallId: call.id,
49
- content: textParts(JSON.stringify(interruptedToolResult(call, {
50
- ...options,
51
- code
52
- })))
53
- });
54
- answered.add(call.id);
55
- sealed.push({
56
- callId: call.id,
57
- toolName: call.name,
58
- code
59
- });
60
- }
61
- }
62
- return {
63
- messages: sealed.length === 0 ? [...messages] : out,
64
- sealed
65
- };
66
- }
67
- /**
68
- * $defs 必须与 type / properties 同级,位于每个工具 inputSchema 的根。
69
- * 已验证:放进 properties.spec 内层会得到同样的 "Unsupported ref: #" ——
70
- * Qwen / LM Studio 按当前 schema 上下文解析 $defs,不回溯根文档。
71
- * 因此本模块只在**根**写入 $defs,不得改成就近放置。
72
- */
73
- const DEFS_KEY = "$defs";
74
- const SELF_REF = "#";
75
- const NODE_NAME = "Node";
76
- const COMPONENT_PREFIX = "Component_";
77
- /** 值为「名称 → 子 schema」的映射。包内导出:vendorSchema 要用同一套下钻规则(两套会漂移)。 */
78
- const SCHEMA_MAP_KEYS = /* @__PURE__ */ new Set([
79
- "properties",
80
- "patternProperties",
81
- "dependentSchemas",
82
- "$defs",
83
- "definitions"
84
- ]);
85
- /** 值为「子 schema 数组」。 */
86
- const SCHEMA_LIST_KEYS = /* @__PURE__ */ new Set([
87
- "oneOf",
88
- "anyOf",
89
- "allOf",
90
- "prefixItems"
91
- ]);
92
- /** 值为单个子 schema(items 在 draft-07 遗留写法里也可能是数组)。 */
93
- const SCHEMA_KEYS = /* @__PURE__ */ new Set([
94
- "items",
95
- "additionalItems",
96
- "additionalProperties",
97
- "unevaluatedItems",
98
- "unevaluatedProperties",
99
- "contains",
100
- "propertyNames",
101
- "not",
102
- "if",
103
- "then",
104
- "else"
105
- ]);
106
- function isRecord$7(value) {
107
- return typeof value === "object" && value !== null && !Array.isArray(value);
108
- }
109
- function isSelfRef(value) {
110
- return isRecord$7(value) && value["$ref"] === SELF_REF;
111
- }
112
- /**
113
- * 按 JSON Schema 关键字下降到子 schema。
114
- * 只走 schema 位置,`enum` / `const` / `default` 等数据位置一律不进——
115
- * 那里出现的 `{ $ref: '#' }` 是字面数据而非引用,改写它会破坏语义。
116
- */
117
- function forEachSubSchema(node, visit) {
118
- for (const [key, value] of Object.entries(node)) {
119
- if (SCHEMA_MAP_KEYS.has(key)) {
120
- if (isRecord$7(value)) for (const name of Object.keys(value)) visit(value[name], [key, name]);
121
- continue;
122
- }
123
- if (SCHEMA_LIST_KEYS.has(key)) {
124
- if (Array.isArray(value)) value.forEach((item, index) => visit(item, [key, index]));
125
- continue;
126
- }
127
- if (SCHEMA_KEYS.has(key)) if (Array.isArray(value)) value.forEach((item, index) => visit(item, [key, index]));
128
- else visit(value, [key]);
129
- }
130
- }
131
- function pathKey(path) {
132
- return path.join("\0");
133
- }
134
- /**
135
- * 找出所有 `$ref: '#'` 所属的 schema resource 根。
136
- * `#` 解析到最近的带 `$id` 的祖先;没有则是文档根。
137
- */
138
- function collectSelfRefRoots(node, path, resourceRoot, out) {
139
- if (!isRecord$7(node)) return;
140
- if (isSelfRef(node)) {
141
- out.set(pathKey(resourceRoot), resourceRoot);
142
- return;
143
- }
144
- const nextRoot = typeof node["$id"] === "string" && path.length > 0 ? path : resourceRoot;
145
- forEachSubSchema(node, (child, childPath) => {
146
- collectSelfRefRoots(child, [...path, ...childPath], nextRoot, out);
147
- });
148
- }
149
- function getAt(schema, path) {
150
- let current = schema;
151
- for (const segment of path) if (Array.isArray(current) && typeof segment === "number") current = current[segment];
152
- else if (isRecord$7(current) && typeof segment === "string") current = current[segment];
153
- else return void 0;
154
- return current;
155
- }
156
- /** 返回把 `path` 处替换为 `replacement` 后的新对象;沿途节点浅拷贝,其余共享。 */
157
- function setAt(schema, path, replacement) {
158
- if (path.length === 0) return replacement;
159
- const [head, ...rest] = path;
160
- const child = getAt(schema, [head]);
161
- const nextChild = rest.length === 0 ? replacement : setAt(child, rest, replacement);
162
- if (Array.isArray(schema) && typeof head === "number") {
163
- const copy = [...schema];
164
- copy[head] = nextChild;
165
- return copy;
166
- }
167
- return {
168
- ...schema,
169
- [head]: nextChild
170
- };
171
- }
172
- /**
173
- * 深拷贝并把 `{ $ref: '#' }` 改写为指向根 `$defs` 中的具名节点。
174
- * 同时剥掉非根位置的 `$id` / `$schema`:它们会重设 base URI,
175
- * 正是严格解析器无法解析 `#` 的直接原因。
176
- */
177
- function rewriteSelfRefs(node, target, depth) {
178
- if (!isRecord$7(node)) return node;
179
- if (depth > 8) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool schema nesting exceeds 8 levels; cannot produce a portable form`);
180
- if (isSelfRef(node)) {
181
- const rest = {};
182
- for (const [key, value] of Object.entries(node)) if (key !== "$ref") rest[key] = value;
183
- return {
184
- $ref: `#/${DEFS_KEY}/${target}`,
185
- ...rest
186
- };
187
- }
188
- const result = {};
189
- for (const [key, value] of Object.entries(node)) {
190
- if (key === "$id" || key === "$schema") continue;
191
- if (SCHEMA_MAP_KEYS.has(key) && isRecord$7(value)) {
192
- const mapped = {};
193
- for (const [name, child] of Object.entries(value)) mapped[name] = rewriteSelfRefs(child, target, depth + 1);
194
- result[key] = mapped;
195
- continue;
196
- }
197
- if (SCHEMA_LIST_KEYS.has(key) || SCHEMA_KEYS.has(key)) {
198
- result[key] = Array.isArray(value) ? value.map((child) => rewriteSelfRefs(child, target, depth + 1)) : rewriteSelfRefs(value, target, depth + 1);
199
- continue;
200
- }
201
- result[key] = value;
202
- }
203
- return result;
204
- }
205
- function uniqueName(base, taken) {
206
- if (!taken.has(base)) return base;
207
- const prefixed = `Portable${base}`;
208
- if (!taken.has(prefixed)) return prefixed;
209
- let index = 2;
210
- while (taken.has(`${prefixed}_${index}`)) index += 1;
211
- return `${prefixed}_${index}`;
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
- }
284
- /**
285
- * 把工具 inputSchema 重写为严格解析器可接受的 JSON Schema 2020-12 形态:
286
- * 非根 `$defs` 提到根,自引用的 schema resource 提到根 `$defs`,
287
- * 所有 `$ref: '#'` 改写为 `$ref: '#/$defs/<Node>'`。
288
- *
289
- * 幂等:不含自引用也不含嵌套 `$defs` 的 schema 原样返回。
290
- *
291
- * 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop,
292
- * 否则 trace / eventBus 观测到的 schema 会与技能作者写的不一致。
293
- */
294
- function toPortableToolSchema(input) {
295
- const schema = hoistNestedDefs(input);
296
- const roots = /* @__PURE__ */ new Map();
297
- collectSelfRefRoots(schema, [], [], roots);
298
- if (roots.size === 0) return schema;
299
- 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");
300
- const rootPath = [...roots.values()][0] ?? [];
301
- const resource = getAt(schema, rootPath);
302
- if (!isRecord$7(resource)) return schema;
303
- const existingDefs = isRecord$7(schema[DEFS_KEY]) ? schema[DEFS_KEY] : void 0;
304
- const taken = new Set(Object.keys(existingDefs ?? {}));
305
- const nodeName = uniqueName(NODE_NAME, taken);
306
- taken.add(nodeName);
307
- const added = {};
308
- const branches = resource["oneOf"];
309
- if (Array.isArray(branches)) {
310
- const refs = branches.map((branch, index) => {
311
- const name = uniqueName(`${COMPONENT_PREFIX}${index}`, taken);
312
- taken.add(name);
313
- added[name] = rewriteSelfRefs(branch, nodeName, 1);
314
- return { $ref: `#/${DEFS_KEY}/${name}` };
315
- });
316
- const description = resource["description"];
317
- added[nodeName] = typeof description === "string" ? {
318
- description,
319
- oneOf: refs
320
- } : { oneOf: refs };
321
- } else {
322
- const body = {};
323
- for (const [key, value] of Object.entries(resource)) if (key !== DEFS_KEY) body[key] = value;
324
- added[nodeName] = rewriteSelfRefs(body, nodeName, 1);
325
- }
326
- return {
327
- ...setAt(schema, rootPath, { $ref: `#/${DEFS_KEY}/${nodeName}` }),
328
- [DEFS_KEY]: {
329
- ...existingDefs ?? {},
330
- ...added
331
- }
332
- };
333
- }
334
- function isRecord$6(value) {
335
- return typeof value === "object" && value !== null && !Array.isArray(value);
336
- }
337
- /**
338
- * 与 `interaction/schemaToForm` 的 file 判定保持一致:JSON Schema 里二进制内容的
339
- * 两种标准写法(`format: binary` / `contentEncoding: base64`)。
340
- */
341
- function isModelUnfillable(node) {
342
- return isRecord$6(node) && node["type"] === "string" && (node["format"] === "binary" || node["contentEncoding"] === "base64");
343
- }
344
- function stripNode(node) {
345
- if (Array.isArray(node)) return node.map(stripNode);
346
- if (!isRecord$6(node)) return node;
347
- const out = {};
348
- for (const [key, value] of Object.entries(node)) {
349
- if (SCHEMA_MAP_KEYS.has(key) && isRecord$6(value)) {
350
- const mapped = {};
351
- for (const [name, child] of Object.entries(value)) {
352
- if (key === "properties" && isModelUnfillable(child)) continue;
353
- mapped[name] = stripNode(child);
354
- }
355
- out[key] = mapped;
356
- continue;
357
- }
358
- if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
359
- out[key] = value.map(stripNode);
360
- continue;
361
- }
362
- if (SCHEMA_KEYS.has(key)) {
363
- out[key] = stripNode(value);
364
- continue;
365
- }
366
- out[key] = value;
367
- }
368
- const required = out["required"];
369
- const properties = out["properties"];
370
- if (Array.isArray(required) && isRecord$6(properties)) out["required"] = required.filter((name) => typeof name !== "string" || name in properties);
371
- return out;
372
- }
373
- /**
374
- * 把模型不可能自己产出的字段(本地文件二进制)从**出站**工具 schema 里剥离。
375
- *
376
- * 模型拿不到用户磁盘上的文件,留着这个字段只有两种下场:模型拒绝调用工具
377
- * (实测 DeepSeek 直接回文本「请上传文件」,file-pick 永远触发不了),或编造一个
378
- * 占位字符串把技能跑挂。剥掉后模型以缺参形式调用,引擎的 missing-params 链路
379
- * 会弹 file-pick 让用户用系统文件选择器补这个字段。
380
- *
381
- * 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop——
382
- * 引擎侧的 missing 校验看的必须是技能作者写的完整 inputSchema。
383
- *
384
- * 已知边界(0.10.0 复核注记):只剥 `properties` 里的 inline 二进制字段;
385
- * 通过 `$ref` 指向 `$defs` 中二进制定义的字段仍会下发(剥掉会留悬空 $ref),
386
- * 这类技能依旧可能拿不到 file-pick——需要完整修复时再补「剥 $def + 剥指向它的 $ref」。
387
- */
388
- function stripModelUnfillableParams(schema) {
389
- return stripNode(schema);
390
- }
391
- /** 各家都认的基础关键字。Google `FunctionDeclaration.parameters` 是其中最窄的一家。 */
392
- const BASE_KEYWORDS = /* @__PURE__ */ new Set([
393
- "type",
394
- "description",
395
- "title",
396
- "default",
397
- "enum",
398
- "const",
399
- "properties",
400
- "required",
401
- "items",
402
- "anyOf",
403
- "oneOf",
404
- "minimum",
405
- "maximum",
406
- "minItems",
407
- "maxItems",
408
- "nullable",
409
- "format"
410
- ]);
411
- /** 宽松侧:保留 0.6.0 的透传行为,本版不主动收紧(没有实证之前收紧只会制造新的破坏) */
412
- const PERMISSIVE_KEYWORDS = /* @__PURE__ */ new Set([
413
- ...BASE_KEYWORDS,
414
- "additionalProperties",
415
- "patternProperties",
416
- "dependentSchemas",
417
- "definitions",
418
- "$defs",
419
- "$ref",
420
- "$schema",
421
- "$id",
422
- "allOf",
423
- "not",
424
- "if",
425
- "then",
426
- "else",
427
- "prefixItems",
428
- "additionalItems",
429
- "unevaluatedItems",
430
- "unevaluatedProperties",
431
- "contains",
432
- "propertyNames",
433
- "exclusiveMinimum",
434
- "exclusiveMaximum",
435
- "multipleOf",
436
- "minLength",
437
- "maxLength",
438
- "pattern",
439
- "uniqueItems",
440
- "examples",
441
- "readOnly",
442
- "writeOnly",
443
- "deprecated"
444
- ]);
445
- const OPENAI_SCHEMA_PROFILE = {
446
- id: "openai-compatible",
447
- allowedKeywords: PERMISSIVE_KEYWORDS,
448
- supportsRefs: true
449
- };
450
- const ANTHROPIC_SCHEMA_PROFILE = {
451
- id: "anthropic",
452
- allowedKeywords: PERMISSIVE_KEYWORDS,
453
- supportsRefs: true
454
- };
455
- /**
456
- * Google Gen AI 的 `FunctionDeclaration.parameters` 收到 `additionalProperties`
457
- * 直接回 400 `Unknown name "additionalProperties"`,且不解析 `$ref` / `$defs`。
458
- *
459
- * 本 profile 描述的是 **`parameters` 这一字段**的能力,不是 Google 的能力上限:
460
- * 完整 JSON Schema(含 `$ref` / `$defs`)走另一个字段 `parametersJsonSchema`。
461
- * 把 `supportsRefs: false` 读成「Google 不支持引用」是错的,分流见 `googleGenAiClient.toGenAiTools`。
462
- */
463
- const GOOGLE_SCHEMA_PROFILE = {
464
- id: "google",
465
- allowedKeywords: BASE_KEYWORDS,
466
- supportsRefs: false
467
- };
468
- function isRecord$5(value) {
469
- return typeof value === "object" && value !== null && !Array.isArray(value);
470
- }
471
- /** 只在 schema 位置上找 `$ref`:enum / const / default 里的同名键是字面数据,不是引用 */
472
- function containsRef(node) {
473
- if (Array.isArray(node)) return node.some(containsRef);
474
- if (!isRecord$5(node)) return false;
475
- if (typeof node["$ref"] === "string") return true;
476
- for (const [key, value] of Object.entries(node)) {
477
- if (SCHEMA_MAP_KEYS.has(key)) {
478
- if (isRecord$5(value) && Object.values(value).some(containsRef)) return true;
479
- continue;
480
- }
481
- if (SCHEMA_LIST_KEYS.has(key)) {
482
- if (Array.isArray(value) && value.some(containsRef)) return true;
483
- continue;
484
- }
485
- if (SCHEMA_KEYS.has(key) && containsRef(value)) return true;
486
- }
487
- return false;
488
- }
489
- function sanitizeNode(node, profile) {
490
- if (Array.isArray(node)) return node.map((item) => sanitizeNode(item, profile));
491
- if (!isRecord$5(node)) return node;
492
- const out = {};
493
- for (const [key, value] of Object.entries(node)) {
494
- if (!profile.allowedKeywords.has(key)) continue;
495
- if (SCHEMA_MAP_KEYS.has(key) && isRecord$5(value)) {
496
- const mapped = {};
497
- for (const [name, child] of Object.entries(value)) mapped[name] = sanitizeNode(child, profile);
498
- out[key] = mapped;
499
- continue;
500
- }
501
- if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
502
- out[key] = value.map((child) => sanitizeNode(child, profile));
503
- continue;
504
- }
505
- if (SCHEMA_KEYS.has(key)) {
506
- out[key] = sanitizeNode(value, profile);
507
- continue;
508
- }
509
- out[key] = value;
510
- }
511
- return out;
512
- }
513
- /**
514
- * 按供应商 profile 清洗出站 schema。产出新对象,不修改入参(工具源可能复用同一个实例)。
515
- *
516
- * 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop,
517
- * 否则 trace / eventBus 观测到的 schema 会与技能作者写的不一致。
518
- *
519
- * @throws WebSkillError `TOOL_SCHEMA_UNAVAILABLE` — schema 依赖该供应商不支持的引用
520
- */
521
- function sanitizeForVendor(schema, profile, toolName) {
522
- if (!profile.supportsRefs && containsRef(schema)) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool "${toolName}" requires $ref support that provider "${profile.id}" does not accept`);
523
- return sanitizeNode(schema, profile);
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
- }
565
- function createSseFrameReader() {
566
- let buffer = "";
567
- let data = [];
568
- const dispatch = (out) => {
569
- if (data.length === 0) return;
570
- out.push(data.join("\n"));
571
- data = [];
572
- };
573
- const consumeLine = (line, out) => {
574
- if (line === "") {
575
- dispatch(out);
576
- return;
577
- }
578
- if (line.startsWith(":")) return;
579
- const colon = line.indexOf(":");
580
- if (colon < 0) return;
581
- if (line.slice(0, colon) !== "data") return;
582
- const value = line.slice(colon + 1);
583
- data.push(value.startsWith(" ") ? value.slice(1) : value);
584
- };
585
- return {
586
- push(chunk) {
587
- const out = [];
588
- buffer += chunk;
589
- buffer = buffer.replace(/\r\n/g, "\n");
590
- let newline;
591
- while ((newline = buffer.indexOf("\n")) >= 0) {
592
- const line = buffer.slice(0, newline);
593
- buffer = buffer.slice(newline + 1);
594
- consumeLine(line, out);
595
- }
596
- return out;
597
- },
598
- flush() {
599
- const out = [];
600
- if (buffer !== "") {
601
- const line = buffer;
602
- buffer = "";
603
- consumeLine(line, out);
604
- }
605
- dispatch(out);
606
- return out;
607
- }
608
- };
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
- }
680
- const dataUrl = (part) => `data:${part.mimeType};base64,${part.data}`;
681
- /** parts → OpenAI content;纯文本折叠成字符串(兼容端点对数组形态支持不一) */
682
- const toOpenAiContent = (parts, where) => {
683
- if (parts.every((p) => p.type === "text")) return partsToText(parts);
684
- return parts.map((part) => {
685
- switch (part.type) {
686
- case "text": return {
687
- type: "text",
688
- text: part.text
689
- };
690
- case "image": return {
691
- type: "image_url",
692
- image_url: { url: dataUrl(part) }
693
- };
694
- case "file": return {
695
- type: "file",
696
- file: {
697
- ...part.name !== void 0 ? { filename: part.name } : {},
698
- file_data: dataUrl(part)
699
- }
700
- };
701
- default: return rejectUnsupportedPart(part, "OpenAI", where);
702
- }
703
- });
704
- };
705
- /** system / tool 消息只接受文本:这两类在 OpenAI 协议里没有多模态形态 */
706
- const toOpenAiText = (parts, where) => {
707
- const nonText = parts.find((p) => p.type !== "text");
708
- if (nonText) rejectUnsupportedPart(nonText, "OpenAI", where);
709
- return partsToText(parts);
710
- };
711
- const toOpenAiMessage = (msg) => {
712
- if (msg.role === "tool") return {
713
- role: "tool",
714
- tool_call_id: msg.toolCallId,
715
- content: toOpenAiText(msg.content, "tool")
716
- };
717
- if (msg.role === "system") return {
718
- role: "system",
719
- content: toOpenAiText(msg.content, "system")
720
- };
721
- if (msg.role === "assistant" && msg.toolCalls?.length) {
722
- const content = toOpenAiContent(msg.content, "assistant");
723
- return {
724
- role: "assistant",
725
- content: content === "" ? null : content,
726
- tool_calls: msg.toolCalls.map((call) => ({
727
- id: call.id,
728
- type: "function",
729
- function: {
730
- name: call.name,
731
- arguments: JSON.stringify(call.arguments)
732
- }
733
- }))
734
- };
735
- }
736
- return {
737
- role: msg.role,
738
- content: toOpenAiContent(msg.content, msg.role)
739
- };
740
- };
741
- const toOpenAiTools = (tools) => tools.map((tool) => ({
742
- type: "function",
743
- function: {
744
- name: tool.name,
745
- ...tool.description ? { description: tool.description } : {},
746
- parameters: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), OPENAI_SCHEMA_PROFILE, tool.name)
747
- }
748
- }));
749
- const errorMessage$2 = (e) => e instanceof Error ? e.message : String(e);
750
- /** OpenAI chat completions 协议客户端(兼容端点通用) */
751
- var OpenAiCompatibleClient = class {
752
- #config;
753
- #fetch;
754
- constructor(config) {
755
- this.#config = config;
756
- this.#fetch = config.fetchImpl ?? ((input, init) => fetch(input, init));
757
- }
758
- #requireConfig() {
759
- const { baseUrl, apiKey, model } = this.#config;
760
- if (!baseUrl || !apiKey || !model) throw new WebSkillError("LLM_UNAVAILABLE", "LLM client is not configured: baseUrl, apiKey and model are all required");
761
- return {
762
- baseUrl: baseUrl.replace(/\/+$/, ""),
763
- apiKey,
764
- model
765
- };
766
- }
767
- async complete(input) {
768
- const res = await this.#postChat(input, false);
769
- let data;
770
- try {
771
- data = await res.json();
772
- } catch (e) {
773
- throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM response: ${errorMessage$2(e)}`, e);
774
- }
775
- return this.#parseResponse(data);
776
- }
777
- /** SSE 流式:fetch + ReadableStream 按行解析 data: 帧(Node/浏览器通用) */
778
- async *stream(input) {
779
- const res = await this.#postChat(input, true);
780
- if (!res.body) throw new WebSkillError("LLM_REQUEST_FAILED", "LLM streaming response has no body");
781
- const toolCallsByIndex = /* @__PURE__ */ new Map();
782
- const decoder = new TextDecoder();
783
- const reader = res.body.getReader();
784
- const frames = createSseFrameReader();
785
- const think = createThinkTagSplitter();
786
- let done = false;
787
- let usage;
788
- const handleFrame = function* (data) {
789
- if (data === "[DONE]") {
790
- done = true;
791
- return;
792
- }
793
- let chunk;
794
- try {
795
- chunk = JSON.parse(data);
796
- } catch (e) {
797
- throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage$2(e)}`, data);
798
- }
799
- const usageRaw = chunk.usage;
800
- if (usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number")) usage = {
801
- inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
802
- outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
803
- };
804
- const delta = chunk.choices?.[0]?.delta;
805
- if (!delta) return;
806
- if (typeof delta["reasoning_content"] === "string" && delta["reasoning_content"] !== "") yield {
807
- type: "thinking-delta",
808
- delta: delta["reasoning_content"]
809
- };
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
- }
821
- const toolDeltas = delta["tool_calls"];
822
- for (const td of toolDeltas ?? []) {
823
- const index = td.index ?? 0;
824
- const acc = toolCallsByIndex.get(index) ?? {
825
- id: "",
826
- name: "",
827
- arguments: ""
828
- };
829
- if (td.id) acc.id = td.id;
830
- if (td.function?.name) acc.name = td.function.name;
831
- if (td.function?.arguments) acc.arguments += td.function.arguments;
832
- toolCallsByIndex.set(index, acc);
833
- }
834
- };
835
- try {
836
- for (;;) {
837
- const { value, done: readerDone } = await reader.read();
838
- const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
839
- for (const payload of payloads) {
840
- yield* handleFrame(payload);
841
- if (done) break;
842
- }
843
- if (done || readerDone) break;
844
- }
845
- } finally {
846
- reader.releaseLock();
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
- };
857
- if (toolCallsByIndex.size > 0) yield {
858
- type: "tool-calls",
859
- toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
860
- let args = {};
861
- let parseError;
862
- try {
863
- args = JSON.parse(acc.arguments || "{}");
864
- } catch (e) {
865
- parseError = e instanceof Error ? e.message : String(e);
866
- }
867
- return {
868
- id: acc.id || `call-${index}`,
869
- name: acc.name,
870
- arguments: args,
871
- ...parseError ? { argumentsParseError: parseError } : {}
872
- };
873
- })
874
- };
875
- yield {
876
- type: "done",
877
- ...usage ? { usage } : {}
878
- };
879
- }
880
- async #postChat(input, stream) {
881
- const { baseUrl, apiKey, model } = this.#requireConfig();
882
- validateLlmMessages(input.messages);
883
- const body = {
884
- model: input.model ?? model,
885
- messages: input.messages.map(toOpenAiMessage)
886
- };
887
- if (input.tools?.length) {
888
- body["tools"] = toOpenAiTools(input.tools);
889
- body["tool_choice"] = "auto";
890
- }
891
- if (input.temperature !== void 0) body["temperature"] = input.temperature;
892
- if (stream) {
893
- body["stream"] = true;
894
- body["stream_options"] = { include_usage: true };
895
- }
896
- let res;
897
- try {
898
- res = await this.#fetch(`${baseUrl}/chat/completions`, {
899
- method: "POST",
900
- headers: {
901
- "content-type": "application/json",
902
- authorization: `Bearer ${apiKey}`
903
- },
904
- body: JSON.stringify(body),
905
- signal: input.signal ?? (this.#config.requestTimeoutMs ? AbortSignal.timeout(this.#config.requestTimeoutMs) : null)
906
- });
907
- } catch (e) {
908
- throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed: ${errorMessage$2(e)}`, e);
909
- }
910
- if (!res.ok) {
911
- const detail = await res.text().catch(() => "");
912
- throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
913
- }
914
- return res;
915
- }
916
- /** 轻量探测(GET /models),集成测试据此决定 skip */
917
- async checkAvailability() {
918
- try {
919
- const { baseUrl, apiKey } = this.#requireConfig();
920
- return (await this.#fetch(`${baseUrl}/models`, { headers: { authorization: `Bearer ${apiKey}` } })).ok;
921
- } catch {
922
- return false;
923
- }
924
- }
925
- #parseResponse(data) {
926
- try {
927
- const choice = data.choices?.[0]?.message;
928
- if (!choice) throw new Error("response has no choices[0].message");
929
- const toolCalls = choice["tool_calls"]?.map((tc) => {
930
- let args = {};
931
- let parseError;
932
- try {
933
- args = JSON.parse(tc.function?.arguments ?? "{}");
934
- } catch (e) {
935
- parseError = e instanceof Error ? e.message : String(e);
936
- }
937
- return {
938
- id: tc.id,
939
- name: tc.function?.name ?? "",
940
- arguments: args,
941
- ...parseError ? { argumentsParseError: parseError } : {}
942
- };
943
- });
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;
952
- const usageRaw = data.usage;
953
- const usage = usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number") ? {
954
- inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
955
- outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
956
- } : void 0;
957
- return {
958
- content: content !== "" ? textParts(content) : void 0,
959
- toolCalls: toolCalls?.length ? toolCalls : void 0,
960
- ...typeof thinking === "string" && thinking !== "" ? { thinking } : {},
961
- ...usage ? { usage } : {},
962
- raw: data
963
- };
964
- } catch (e) {
965
- throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to interpret LLM response: ${errorMessage$2(e)}`, data);
966
- }
967
- }
968
- };
969
- const ANTHROPIC_VERSION = "2023-06-01";
970
- const errorMessage$1 = (e) => e instanceof Error ? e.message : String(e);
971
- /** parts → Anthropic content blocks(image/document 均为 base64 source) */
972
- const toAnthropicBlocks = (parts, where) => parts.map((part) => {
973
- switch (part.type) {
974
- case "text": return {
975
- type: "text",
976
- text: part.text
977
- };
978
- case "image": return {
979
- type: "image",
980
- source: {
981
- type: "base64",
982
- media_type: part.mimeType,
983
- data: part.data
984
- }
985
- };
986
- case "file": return {
987
- type: "document",
988
- source: {
989
- type: "base64",
990
- media_type: part.mimeType,
991
- data: part.data
992
- }
993
- };
994
- default: return rejectUnsupportedPart(part, "Anthropic", where);
995
- }
996
- });
997
- /** system 是独立的字符串参数,无多模态形态 */
998
- const toAnthropicSystemText = (parts) => {
999
- const nonText = parts.find((p) => p.type !== "text");
1000
- if (nonText) rejectUnsupportedPart(nonText, "Anthropic", "system");
1001
- return partsToText(parts);
1002
- };
1003
- /** LlmMessage 序列 → system 独立参数 + user/assistant 消息(tool 结果合并进 user 消息 tool_result blocks) */
1004
- function toAnthropicMessages(messages) {
1005
- const system = messages.filter((m) => m.role === "system").map((m) => toAnthropicSystemText(m.content)).join("\n");
1006
- const out = [];
1007
- for (const msg of messages) {
1008
- if (msg.role === "system") continue;
1009
- if (msg.role === "tool") {
1010
- const block = {
1011
- type: "tool_result",
1012
- tool_use_id: msg.toolCallId ?? "",
1013
- content: toAnthropicBlocks(msg.content, "tool")
1014
- };
1015
- const last = out.at(-1);
1016
- if (last && last.role === "user" && Array.isArray(last.content)) last.content.push(block);
1017
- else out.push({
1018
- role: "user",
1019
- content: [block]
1020
- });
1021
- continue;
1022
- }
1023
- if (msg.role === "assistant" && msg.toolCalls?.length) {
1024
- const content = toAnthropicBlocks(msg.content, "assistant");
1025
- for (const call of msg.toolCalls) content.push({
1026
- type: "tool_use",
1027
- id: call.id,
1028
- name: call.name,
1029
- input: call.arguments
1030
- });
1031
- out.push({
1032
- role: "assistant",
1033
- content
1034
- });
1035
- continue;
1036
- }
1037
- out.push({
1038
- role: msg.role,
1039
- content: toAnthropicBlocks(msg.content, msg.role)
1040
- });
1041
- }
1042
- return system === "" ? { messages: out } : {
1043
- system,
1044
- messages: out
1045
- };
1046
- }
1047
- const toAnthropicTools = (tools) => tools.map((tool) => ({
1048
- name: tool.name,
1049
- ...tool.description ? { description: tool.description } : {},
1050
- input_schema: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), ANTHROPIC_SCHEMA_PROFILE, tool.name)
1051
- }));
1052
- /** Anthropic Messages API 客户端(零依赖 fetch;Node/浏览器通用) */
1053
- var AnthropicClient = class {
1054
- #config;
1055
- #fetch;
1056
- constructor(config) {
1057
- this.#config = config;
1058
- this.#fetch = config.fetchImpl ?? ((input, init) => fetch(input, init));
1059
- }
1060
- #baseUrl() {
1061
- return (this.#config.baseUrl ?? "https://api.anthropic.com").replace(/\/+$/, "");
1062
- }
1063
- #headers() {
1064
- return {
1065
- "content-type": "application/json",
1066
- "x-api-key": this.#config.apiKey,
1067
- "anthropic-version": ANTHROPIC_VERSION,
1068
- ...this.#config.dangerouslyAllowDirectBrowserAccess ? { "anthropic-dangerous-direct-browser-access": "true" } : {}
1069
- };
1070
- }
1071
- async complete(input) {
1072
- const res = await this.#post(input, false);
1073
- let data;
1074
- try {
1075
- data = await res.json();
1076
- } catch (e) {
1077
- throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM response: ${errorMessage$1(e)}`, e);
1078
- }
1079
- return this.#parseResponse(data);
1080
- }
1081
- /** SSE 流式:text_delta → text-delta;input_json_delta 按 block index 聚合 → tool-calls */
1082
- async *stream(input) {
1083
- const res = await this.#post(input, true);
1084
- if (!res.body) throw new WebSkillError("LLM_REQUEST_FAILED", "LLM streaming response has no body");
1085
- const toolCallsByIndex = /* @__PURE__ */ new Map();
1086
- const decoder = new TextDecoder();
1087
- const reader = res.body.getReader();
1088
- const frames = createSseFrameReader();
1089
- let inputTokens;
1090
- let outputTokens;
1091
- const handleFrame = function* (data) {
1092
- let chunk;
1093
- try {
1094
- chunk = JSON.parse(data);
1095
- } catch (e) {
1096
- throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage$1(e)}`, data);
1097
- }
1098
- const type = chunk["type"];
1099
- if (type === "error") throw new WebSkillError("LLM_REQUEST_FAILED", `LLM stream error: ${chunk["error"]?.message ?? data}`);
1100
- if (type === "message_start") {
1101
- const usage = chunk["message"]?.["usage"];
1102
- if (typeof usage?.["input_tokens"] === "number") inputTokens = usage["input_tokens"];
1103
- if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
1104
- return;
1105
- }
1106
- if (type === "message_delta") {
1107
- const usage = chunk["usage"];
1108
- if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
1109
- return;
1110
- }
1111
- if (type === "content_block_start") {
1112
- const block = chunk["content_block"];
1113
- if (block?.["type"] === "tool_use") {
1114
- const index = typeof chunk["index"] === "number" ? chunk["index"] : 0;
1115
- toolCallsByIndex.set(index, {
1116
- id: typeof block["id"] === "string" ? block["id"] : "",
1117
- name: typeof block["name"] === "string" ? block["name"] : "",
1118
- arguments: ""
1119
- });
1120
- }
1121
- return;
1122
- }
1123
- if (type === "content_block_delta") {
1124
- const index = typeof chunk["index"] === "number" ? chunk["index"] : 0;
1125
- const delta = chunk["delta"];
1126
- if (delta?.["type"] === "text_delta" && typeof delta["text"] === "string" && delta["text"] !== "") {
1127
- yield {
1128
- type: "text-delta",
1129
- delta: delta["text"]
1130
- };
1131
- return;
1132
- }
1133
- if (delta?.["type"] === "thinking_delta" && typeof delta["thinking"] === "string" && delta["thinking"] !== "") {
1134
- yield {
1135
- type: "thinking-delta",
1136
- delta: delta["thinking"]
1137
- };
1138
- return;
1139
- }
1140
- if (delta?.["type"] === "input_json_delta" && typeof delta["partial_json"] === "string") {
1141
- const acc = toolCallsByIndex.get(index) ?? {
1142
- id: "",
1143
- name: "",
1144
- arguments: ""
1145
- };
1146
- acc.arguments += delta["partial_json"];
1147
- toolCallsByIndex.set(index, acc);
1148
- }
1149
- return;
1150
- }
1151
- };
1152
- try {
1153
- for (;;) {
1154
- const { value, done: readerDone } = await reader.read();
1155
- const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
1156
- for (const payload of payloads) yield* handleFrame(payload);
1157
- if (readerDone) break;
1158
- }
1159
- } finally {
1160
- reader.releaseLock();
1161
- }
1162
- if (toolCallsByIndex.size > 0) yield {
1163
- type: "tool-calls",
1164
- toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
1165
- let args = {};
1166
- let parseError;
1167
- try {
1168
- args = JSON.parse(acc.arguments || "{}");
1169
- } catch (e) {
1170
- parseError = e instanceof Error ? e.message : String(e);
1171
- }
1172
- return {
1173
- id: acc.id || `call-${index}`,
1174
- name: acc.name,
1175
- arguments: args,
1176
- ...parseError ? { argumentsParseError: parseError } : {}
1177
- };
1178
- })
1179
- };
1180
- const usage = inputTokens !== void 0 || outputTokens !== void 0 ? {
1181
- inputTokens: inputTokens ?? 0,
1182
- outputTokens: outputTokens ?? 0
1183
- } : void 0;
1184
- yield {
1185
- type: "done",
1186
- ...usage ? { usage } : {}
1187
- };
1188
- }
1189
- async #post(input, stream) {
1190
- validateLlmMessages(input.messages);
1191
- const { system, messages } = toAnthropicMessages(input.messages);
1192
- const body = {
1193
- model: input.model ?? this.#config.model,
1194
- max_tokens: this.#config.maxTokens ?? 4096,
1195
- messages
1196
- };
1197
- if (system !== void 0) body["system"] = system;
1198
- if (input.tools?.length) body["tools"] = toAnthropicTools(input.tools);
1199
- const thinkingBudget = this.#config.thinkingBudgetTokens;
1200
- if (thinkingBudget !== void 0 && Number.isInteger(thinkingBudget) && thinkingBudget > 0) body["thinking"] = {
1201
- type: "enabled",
1202
- budget_tokens: thinkingBudget
1203
- };
1204
- else if (input.temperature !== void 0) body["temperature"] = input.temperature;
1205
- if (stream) body["stream"] = true;
1206
- let res;
1207
- try {
1208
- res = await this.#fetch(`${this.#baseUrl()}/v1/messages`, {
1209
- method: "POST",
1210
- headers: this.#headers(),
1211
- body: JSON.stringify(body),
1212
- signal: input.signal ?? (this.#config.requestTimeoutMs ? AbortSignal.timeout(this.#config.requestTimeoutMs) : null)
1213
- });
1214
- } catch (e) {
1215
- throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed: ${errorMessage$1(e)}`, e);
1216
- }
1217
- if (!res.ok) {
1218
- const detail = await res.text().catch(() => "");
1219
- throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
1220
- }
1221
- return res;
1222
- }
1223
- /**
1224
- * 真实调用路径探测(FR-27.1):`POST /v1/messages` 最小请求(max_tokens=1),200 即可达。
1225
- * Anthropic 兼容端点(如 LM Studio 的 /anthropic 层)不实现 `GET /v1/models`,
1226
- * 用 /models 探测会把「能对话」误判为「不可达」——checkAvailability 的语义从
1227
- * 「/models 可达」改为「能完成一次真实调用」。
1228
- */
1229
- async checkAvailability() {
1230
- try {
1231
- return (await this.#fetch(`${this.#baseUrl()}/v1/messages`, {
1232
- method: "POST",
1233
- headers: this.#headers(),
1234
- body: JSON.stringify({
1235
- model: this.#config.model,
1236
- max_tokens: 1,
1237
- messages: [{
1238
- role: "user",
1239
- content: "ping"
1240
- }]
1241
- })
1242
- })).ok;
1243
- } catch {
1244
- return false;
1245
- }
1246
- }
1247
- #parseResponse(data) {
1248
- try {
1249
- const blocks = data.content;
1250
- if (!Array.isArray(blocks)) throw new Error("response has no content blocks");
1251
- const text = blocks.filter((b) => b["type"] === "text").map((b) => String(b["text"] ?? "")).join("");
1252
- const thinking = blocks.filter((b) => b["type"] === "thinking").map((b) => String(b["thinking"] ?? "")).join("");
1253
- const toolCalls = blocks.filter((b) => b["type"] === "tool_use").map((b) => ({
1254
- id: typeof b["id"] === "string" ? b["id"] : "",
1255
- name: typeof b["name"] === "string" ? b["name"] : "",
1256
- arguments: typeof b["input"] === "object" && b["input"] !== null ? b["input"] : {}
1257
- }));
1258
- const usageRaw = data.usage;
1259
- const usage = typeof usageRaw?.["input_tokens"] === "number" || typeof usageRaw?.["output_tokens"] === "number" ? {
1260
- inputTokens: typeof usageRaw["input_tokens"] === "number" ? usageRaw["input_tokens"] : 0,
1261
- outputTokens: typeof usageRaw["output_tokens"] === "number" ? usageRaw["output_tokens"] : 0
1262
- } : void 0;
1263
- return {
1264
- content: text === "" ? void 0 : textParts(text),
1265
- toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
1266
- ...thinking === "" ? {} : { thinking },
1267
- ...usage ? { usage } : {},
1268
- raw: data
1269
- };
1270
- } catch (e) {
1271
- throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to interpret LLM response: ${errorMessage$1(e)}`, data);
1272
- }
1273
- }
1274
- };
1275
- const errorMessage = (e) => e instanceof Error ? e.message : String(e);
1276
- /** usageMetadata → LlmTokenUsage(promptTokenCount / candidatesTokenCount;无字段返回 undefined) */
1277
- function parseGenAiUsage(data) {
1278
- const usage = data?.usageMetadata;
1279
- if (!usage) return void 0;
1280
- const input = usage["promptTokenCount"];
1281
- const output = usage["candidatesTokenCount"];
1282
- if (typeof input !== "number" && typeof output !== "number") return void 0;
1283
- return {
1284
- inputTokens: typeof input === "number" ? input : 0,
1285
- outputTokens: typeof output === "number" ? output : 0
1286
- };
1287
- }
1288
- /** parts → GenAI parts(二进制统一走 inlineData) */
1289
- const toGenAiParts = (parts, where) => parts.map((part) => {
1290
- switch (part.type) {
1291
- case "text": return { text: part.text };
1292
- case "image":
1293
- case "file": return { inlineData: {
1294
- mimeType: part.mimeType,
1295
- data: part.data
1296
- } };
1297
- default: return rejectUnsupportedPart(part, "Google GenAI", where);
1298
- }
1299
- });
1300
- /** systemInstruction 与 functionResponse 只接受文本 */
1301
- const toGenAiText = (parts, where) => {
1302
- const nonText = parts.find((p) => p.type !== "text");
1303
- if (nonText) rejectUnsupportedPart(nonText, "Google GenAI", where);
1304
- return partsToText(parts);
1305
- };
1306
- /** tool 结果文本包装为 functionResponse.response 对象 */
1307
- function responseObject(content) {
1308
- try {
1309
- const parsed = JSON.parse(content);
1310
- if (typeof parsed === "object" && parsed !== null) return parsed;
1311
- } catch {}
1312
- return { result: content };
1313
- }
1314
- /** LlmMessage 序列 → systemInstruction + contents(tool 结果合并进 user 消息 functionResponse parts) */
1315
- function toGenAiContents(messages) {
1316
- const system = messages.filter((m) => m.role === "system").map((m) => toGenAiText(m.content, "system")).join("\n");
1317
- const nameByCallId = /* @__PURE__ */ new Map();
1318
- for (const msg of messages) if (msg.role === "assistant") for (const call of msg.toolCalls ?? []) nameByCallId.set(call.id, call.name);
1319
- const out = [];
1320
- for (const msg of messages) {
1321
- if (msg.role === "system") continue;
1322
- if (msg.role === "tool") {
1323
- const callId = msg.toolCallId ?? "";
1324
- const part = { functionResponse: {
1325
- name: nameByCallId.get(callId) ?? callId,
1326
- response: responseObject(toGenAiText(msg.content, "tool"))
1327
- } };
1328
- const last = out.at(-1);
1329
- if (last && last.role === "user") last.parts.push(part);
1330
- else out.push({
1331
- role: "user",
1332
- parts: [part]
1333
- });
1334
- continue;
1335
- }
1336
- if (msg.role === "assistant") {
1337
- const parts = toGenAiParts(msg.content, "assistant");
1338
- for (const call of msg.toolCalls ?? []) {
1339
- const signature = call.vendor?.["thoughtSignature"];
1340
- parts.push({
1341
- functionCall: {
1342
- name: call.name,
1343
- args: call.arguments
1344
- },
1345
- ...typeof signature === "string" ? { thoughtSignature: signature } : {}
1346
- });
1347
- }
1348
- out.push({
1349
- role: "model",
1350
- parts: parts.length > 0 ? parts : [{ text: "" }]
1351
- });
1352
- continue;
1353
- }
1354
- out.push({
1355
- role: "user",
1356
- parts: toGenAiParts(msg.content, msg.role)
1357
- });
1358
- }
1359
- return system === "" ? { contents: out } : {
1360
- systemInstruction: { parts: [{ text: system }] },
1361
- contents: out
1362
- };
1363
- }
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
- }) }];
1372
- /** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
1373
- var GoogleGenAiClient = class {
1374
- #config;
1375
- #fetch;
1376
- constructor(config) {
1377
- this.#config = config;
1378
- this.#fetch = config.fetchImpl ?? ((input, init) => fetch(input, init));
1379
- }
1380
- #baseUrl() {
1381
- return (this.#config.baseUrl ?? "https://generativelanguage.googleapis.com").replace(/\/+$/, "");
1382
- }
1383
- async complete(input) {
1384
- const res = await this.#post(input, false);
1385
- let data;
1386
- try {
1387
- data = await res.json();
1388
- } catch (e) {
1389
- throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM response: ${errorMessage(e)}`, e);
1390
- }
1391
- const parts = this.#responseParts(data);
1392
- const text = parts.filter((p) => typeof p["text"] === "string").map((p) => String(p["text"])).join("");
1393
- const toolCalls = this.#functionCalls(parts);
1394
- const usage = parseGenAiUsage(data);
1395
- return {
1396
- content: text === "" ? void 0 : textParts(text),
1397
- toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
1398
- ...usage ? { usage } : {},
1399
- raw: data
1400
- };
1401
- }
1402
- /** SSE 流式:chunk.parts text → text-delta;functionCall 聚合 → tool-calls */
1403
- async *stream(input) {
1404
- const res = await this.#post(input, true);
1405
- if (!res.body) throw new WebSkillError("LLM_REQUEST_FAILED", "LLM streaming response has no body");
1406
- const toolCalls = [];
1407
- const decoder = new TextDecoder();
1408
- const reader = res.body.getReader();
1409
- const frames = createSseFrameReader();
1410
- let usage;
1411
- const handleFrame = function* (data) {
1412
- let chunk;
1413
- try {
1414
- chunk = JSON.parse(data);
1415
- } catch (e) {
1416
- throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage(e)}`, data);
1417
- }
1418
- usage = parseGenAiUsage(chunk) ?? usage;
1419
- const parts = chunk.candidates?.[0]?.content?.parts;
1420
- for (const part of parts ?? []) {
1421
- if (typeof part["text"] === "string" && part["text"] !== "") {
1422
- yield {
1423
- type: "text-delta",
1424
- delta: part["text"]
1425
- };
1426
- continue;
1427
- }
1428
- const call = part["functionCall"];
1429
- if (call) {
1430
- const signature = part["thoughtSignature"];
1431
- toolCalls.push({
1432
- id: `call-${toolCalls.length}`,
1433
- name: call.name ?? "",
1434
- arguments: call.args ?? {},
1435
- ...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
1436
- });
1437
- }
1438
- }
1439
- };
1440
- try {
1441
- for (;;) {
1442
- const { value, done: readerDone } = await reader.read();
1443
- const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
1444
- for (const payload of payloads) yield* handleFrame(payload);
1445
- if (readerDone) break;
1446
- }
1447
- } finally {
1448
- reader.releaseLock();
1449
- }
1450
- if (toolCalls.length > 0) yield {
1451
- type: "tool-calls",
1452
- toolCalls
1453
- };
1454
- yield {
1455
- type: "done",
1456
- ...usage ? { usage } : {}
1457
- };
1458
- }
1459
- async #post(input, stream) {
1460
- const model = input.model ?? this.#config.model;
1461
- const action = stream ? ":streamGenerateContent?alt=sse" : ":generateContent";
1462
- validateLlmMessages(input.messages);
1463
- const { systemInstruction, contents } = toGenAiContents(input.messages);
1464
- const body = { contents };
1465
- if (systemInstruction) body["systemInstruction"] = systemInstruction;
1466
- if (input.tools?.length) body["tools"] = toGenAiTools(input.tools);
1467
- if (input.temperature !== void 0) body["generationConfig"] = { temperature: input.temperature };
1468
- let res;
1469
- try {
1470
- res = await this.#fetch(`${this.#baseUrl()}/v1beta/models/${encodeURIComponent(model)}${action}`, {
1471
- method: "POST",
1472
- headers: {
1473
- "content-type": "application/json",
1474
- "x-goog-api-key": this.#config.apiKey
1475
- },
1476
- body: JSON.stringify(body),
1477
- signal: input.signal ?? (this.#config.requestTimeoutMs ? AbortSignal.timeout(this.#config.requestTimeoutMs) : null)
1478
- });
1479
- } catch (e) {
1480
- throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed: ${errorMessage(e)}`, e);
1481
- }
1482
- if (!res.ok) {
1483
- const detail = await res.text().catch(() => "");
1484
- throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
1485
- }
1486
- return res;
1487
- }
1488
- /** 轻量探测(GET /v1beta/models),集成测试据此决定 skip */
1489
- async checkAvailability() {
1490
- try {
1491
- return (await this.#fetch(`${this.#baseUrl()}/v1beta/models`, { headers: { "x-goog-api-key": this.#config.apiKey } })).ok;
1492
- } catch {
1493
- return false;
1494
- }
1495
- }
1496
- #responseParts(data) {
1497
- try {
1498
- const parts = data.candidates?.[0]?.content?.parts;
1499
- if (!Array.isArray(parts)) throw new Error("response has no candidates[0].content.parts");
1500
- return parts;
1501
- } catch (e) {
1502
- throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to interpret LLM response: ${errorMessage(e)}`, data);
1503
- }
1504
- }
1505
- #functionCalls(parts) {
1506
- const out = [];
1507
- for (const part of parts) {
1508
- const call = part["functionCall"];
1509
- if (call) {
1510
- const signature = part["thoughtSignature"];
1511
- out.push({
1512
- id: `call-${out.length}`,
1513
- name: call.name ?? "",
1514
- arguments: call.args ?? {},
1515
- ...typeof signature === "string" ? { vendor: { thoughtSignature: signature } } : {}
1516
- });
1517
- }
1518
- }
1519
- return out;
1520
- }
1521
- };
1522
- /** v5 { inputTokens, outputTokens } 与 v4 { promptTokens, completionTokens } 两种形状都接 */
1523
- function fromVercelUsage(raw) {
1524
- const u = raw ?? {};
1525
- const input = u["inputTokens"] ?? u["promptTokens"];
1526
- const output = u["outputTokens"] ?? u["completionTokens"];
1527
- if (typeof input !== "number" && typeof output !== "number") return void 0;
1528
- return {
1529
- inputTokens: typeof input === "number" ? input : 0,
1530
- outputTokens: typeof output === "number" ? output : 0
1531
- };
1532
- }
1533
- function toVercelToolSpecs(tools) {
1534
- const out = {};
1535
- for (const tool of tools) out[tool.name] = {
1536
- ...tool.description ? { description: tool.description } : {},
1537
- inputSchema: tool.inputSchema
1538
- };
1539
- return out;
1540
- }
1541
- /**
1542
- * 归一化供应商思考内容:v5 的 `reasoningText`(字符串)与 v4 的 `reasoning`
1543
- * (Array<{ text; type }>)都能落到同一条 `LlmResponse.thinking`。
1544
- */
1545
- function normalizeThinking(value) {
1546
- if (typeof value === "string") return value !== "" ? value : void 0;
1547
- if (!Array.isArray(value)) return void 0;
1548
- const text = value.map((part) => typeof part?.text === "string" ? part.text : "").join("");
1549
- return text !== "" ? text : void 0;
1550
- }
1551
- /** 接受 generateText 的返回值形状:{ text?, toolCalls?: [{ toolCallId, toolName, input|args }] } */
1552
- function fromVercelResult(result) {
1553
- const r = result;
1554
- const toolCalls = (r.toolCalls ?? []).map((tc, i) => {
1555
- const args = tc.input ?? tc.args ?? {};
1556
- return {
1557
- id: typeof tc.toolCallId === "string" ? tc.toolCallId : `call-${i}`,
1558
- name: typeof tc.toolName === "string" ? tc.toolName : "",
1559
- arguments: typeof args === "object" && args !== null ? args : {}
1560
- };
1561
- });
1562
- const thinking = normalizeThinking(r.reasoningText ?? r.reasoning);
1563
- const usage = fromVercelUsage(r.usage);
1564
- return {
1565
- content: typeof r.text === "string" && r.text !== "" ? textParts(r.text) : void 0,
1566
- toolCalls: toolCalls.length ? toolCalls : void 0,
1567
- ...thinking !== void 0 ? { thinking } : {},
1568
- ...usage ? { usage } : {},
1569
- raw: result
1570
- };
12
+ //#region ../core/src/render/xmlRenderer.ts
13
+ /** 转义 XML 五个特殊字符:& < > " ' */
14
+ function escapeXml(text) {
15
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1571
16
  }
1572
- /**
1573
- * Vercel streamText fullStream 片段 → LlmStreamEvent 映射。
1574
- * 未知片段类型返回 undefined(调用方跳过)。
1575
- */
1576
- function fromVercelStreamPart(part) {
1577
- const p = part ?? {};
1578
- switch (p["type"]) {
1579
- case "text-delta": {
1580
- const delta = p["text"] ?? p["delta"];
1581
- return typeof delta === "string" && delta !== "" ? {
1582
- type: "text-delta",
1583
- delta
1584
- } : void 0;
1585
- }
1586
- case "reasoning-delta": {
1587
- const delta = p["text"] ?? p["delta"];
1588
- return typeof delta === "string" && delta !== "" ? {
1589
- type: "thinking-delta",
1590
- delta
1591
- } : void 0;
1592
- }
1593
- case "tool-call": return {
1594
- type: "tool-calls",
1595
- toolCalls: [{
1596
- id: typeof p["toolCallId"] === "string" ? p["toolCallId"] : "call-0",
1597
- name: typeof p["toolName"] === "string" ? p["toolName"] : "",
1598
- arguments: typeof p["input"] === "object" && p["input"] !== null ? p["input"] : {}
1599
- }]
1600
- };
1601
- case "finish": {
1602
- const usage = fromVercelUsage(p["totalUsage"] ?? p["usage"]);
1603
- return {
1604
- type: "done",
1605
- ...usage ? { usage } : {}
1606
- };
1607
- }
1608
- default: return;
1609
- }
17
+ function renderAvailableSkillsXml(catalog) {
18
+ const lines = ["<available_skills>"];
19
+ for (const skill of catalog.entries) {
20
+ lines.push(" <skill>");
21
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
22
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
23
+ if (skill.version !== void 0) lines.push(` <version>${escapeXml(skill.version)}</version>`);
24
+ if (skill.license !== void 0) lines.push(` <license>${escapeXml(skill.license)}</license>`);
25
+ lines.push(` <source>${escapeXml(skill.source)}</source>`);
26
+ lines.push(" </skill>");
27
+ }
28
+ lines.push("</available_skills>");
29
+ return lines.join("\n");
1610
30
  }
1611
- const INSTRUCTIONS$1 = `You are an agent that completes tasks by using skills.
31
+ const xmlRenderer = {
32
+ format: "xml",
33
+ render: renderAvailableSkillsXml
34
+ };
35
+
36
+ //#endregion
37
+ //#region ../runtime/src/routing/progressiveRouter.ts
38
+ const INSTRUCTIONS = `You are an agent that completes tasks by using skills.
1612
39
 
1613
40
  Skills are disclosed progressively: the catalog below only shows metadata. To use a skill:
1614
41
  1. Call the "read_skill_file" tool to load the skill's SKILL.md instructions. Reading SKILL.md activates the skill's script tools, which are named "<skillName>__<scriptName>".
@@ -1623,74 +50,13 @@ var ProgressiveRouter = class {
1623
50
  return {
1624
51
  strategy: "progressive",
1625
52
  catalog,
1626
- systemPrompt: `${INSTRUCTIONS$1}\n\n${renderAvailableSkillsXml(catalog)}`
53
+ systemPrompt: `${INSTRUCTIONS}\n\n${renderAvailableSkillsXml(catalog)}`
1627
54
  };
1628
55
  }
1629
56
  };
1630
- const INSTRUCTIONS = `You are an agent that completes tasks by using skills.
1631
- The full instructions of every available skill are injected below. Follow them directly.
1632
- Script tools are named "<skillName>__<scriptName>".
1633
- When the task is complete, answer in plain text without calling any tool.
1634
57
 
1635
- Never claim to have created, saved, or installed a skill unless a skill tool call actually succeeded.`;
1636
- /** 全量披露路由(对照用):注入每个技能的完整 SKILL.md 正文 */
1637
- var FullDisclosureRouter = class {
1638
- #discovery;
1639
- constructor(discovery) {
1640
- this.#discovery = discovery;
1641
- }
1642
- async route(catalog) {
1643
- const sections = [];
1644
- for (const entry of catalog.entries) {
1645
- const doc = await this.#discovery.loadDocument(entry.name);
1646
- sections.push(`<skill name="${entry.name}">\n${doc.body}\n</skill>`);
1647
- }
1648
- return {
1649
- strategy: "full-disclosure",
1650
- catalog,
1651
- systemPrompt: `${INSTRUCTIONS}\n\n${sections.join("\n\n")}`
1652
- };
1653
- }
1654
- };
1655
- /**
1656
- * 参与 `toolResultMaxBytes` 截断的分片类型(FR-23.3)。**判据的单一来源。**
1657
- *
1658
- * 白名单而不是黑名单:黑名单式下新加的分片默认「参与截断」,
1659
- * 漏改一处就会把一份 PDF 截成坏文件、把 docx 文本腰斩,而且没有任何报错。
1660
- * 白名单式下新分片默认**不参与**,要受文本预算约束必须显式加进来。
1661
- * @experimental
1662
- */
1663
- const TEXT_BUDGETED_CONTENT_TYPES = /* @__PURE__ */ new Set(["text", "json"]);
1664
- /**
1665
- * `file` 分片的上限(FR-23.4),量的是**实际上线的 base64 长度**而不是解码后的字节——
1666
- * provider 的限额算的是请求载荷。
1667
- *
1668
- * 2026-08-14 查证的三家上限:
1669
- * | provider | 上限 | 出处 |
1670
- * | --------- | --------------------------------------- | ----------------------- |
1671
- * | Anthropic | **32 MB**(**整个请求载荷**)、600 页 | PDF support / 请求大小 |
1672
- * | OpenAI | 单文件 50 MB,全部文件合计 50 MB | File inputs / 使用须知 |
1673
- * | Gemini | 50 MB 或 1000 页(内联与 Files API 同) | 文档理解 / 技术详情 |
1674
- *
1675
- * 取最小值 Anthropic 的 32 MB。它是**整个请求**的额度,不是文档单独的额度,
1676
- * 所以再留出余量给系统提示词、catalog(约 34 KB)、历史消息与其余分片。
1677
- * 25 000 KB base64 ≈ 18.75 MB 原始 PDF。取 1024 的整数倍:设置界面按 KB 展示,
1678
- * 十进制的 24 000 000 会显示成 23437.5 这种读不出来的数。
1679
- */
1680
- const DEFAULT_MAX_DOCUMENT_BYTES = 256e5;
1681
- /**
1682
- * `document-text` 分片的上限(FR-23.4)。
1683
- *
1684
- * 约束来自**上下文窗口**而不是请求大小:抽出来的文本要整段进上下文。
1685
- * 按英文约 4 字节/token 折算,500 KB ≈ 128K token,占 200K 窗口的多半,
1686
- * 给历史消息与模型的回答留下其余。
1687
- */
1688
- const DEFAULT_MAX_DOCUMENT_TEXT_BYTES = 512e3;
1689
- /**
1690
- * 单次 fetchData 结果的字节上限(分册 16,FR-16.6):1000 KB。
1691
- * 结构化数据不是文档,量级差一个数量级;超出**只拒不截**——截断的 JSON 解不出来。
1692
- */
1693
- const DEFAULT_MAX_DATA_SOURCE_BYTES = 1024e3;
58
+ //#endregion
59
+ //#region ../runtime/src/tools/toolResolver.ts
1694
60
  /**
1695
61
  * 工具名解析规则(单一实现,Agent 循环使用):
1696
62
  * 1. `<skillName>__<scriptName>` 且前缀是已激活技能 → 本地脚本
@@ -1744,6 +110,9 @@ function toLlmToolSpec(tool) {
1744
110
  }
1745
111
  };
1746
112
  }
113
+
114
+ //#endregion
115
+ //#region ../runtime/src/tools/skillScripts.ts
1747
116
  /** 工具名规则:技能**目录名**加双下划线加脚本名,与 frontmatter 的 name 无关 */
1748
117
  function scriptToolName(skillName, scriptName) {
1749
118
  return `${skillName}__${scriptName}`;
@@ -1790,43 +159,9 @@ function formatSkillScriptManifest(descriptors) {
1790
159
  if (descriptors.length === 0) return "";
1791
160
  return `\n\nAvailable scripts in this skill (from directory listing, authoritative):\n${descriptors.map((item) => item.toolName === void 0 ? `- ${item.path} → not exposed as a tool (${item.unexposedReason ?? "no loadable definition"})` : `- ${item.path} → tool "${item.toolName}" (input schema: ${SCHEMA_SOURCE_TEXT[item.schemaSource]})`).join("\n")}`;
1792
161
  }
1793
- const MCP_CONTENT_RE = /^(text|json|image|resource|audio)$/;
1794
- /**
1795
- * 脚本返回值归一到 MCP content 格式:数组沿用(逐项归一),
1796
- * 字符串包装为 text,其他值序列化包装。Node/浏览器执行器共享。
1797
- * 产出只有文本型分片:脚本侧没有下发图像的通道(图像只来自页面取像)。
1798
- */
1799
- function normalizeToolContent(value) {
1800
- if (Array.isArray(value)) return value.map((item) => {
1801
- if (typeof item === "object" && item !== null && typeof item.type === "string" && MCP_CONTENT_RE.test(item.type)) {
1802
- const { type, ...rest } = item;
1803
- if (type === "json") return {
1804
- type: "json",
1805
- data: rest["data"] ?? rest
1806
- };
1807
- return {
1808
- type: "text",
1809
- text: typeof rest["text"] === "string" ? rest["text"] : JSON.stringify(rest)
1810
- };
1811
- }
1812
- return {
1813
- type: "text",
1814
- text: typeof item === "string" ? item : JSON.stringify(item)
1815
- };
1816
- });
1817
- if (typeof value === "string") return [{
1818
- type: "text",
1819
- text: value
1820
- }];
1821
- if (value === void 0 || value === null) return [{
1822
- type: "text",
1823
- text: ""
1824
- }];
1825
- return [{
1826
- type: "text",
1827
- text: typeof value === "object" ? JSON.stringify(value) : String(value)
1828
- }];
1829
- }
162
+
163
+ //#endregion
164
+ //#region ../runtime/src/tools/readSkillFileTool.ts
1830
165
  const READ_SKILL_FILE_TOOL_NAME = "read_skill_file";
1831
166
  const READ_SKILL_FILE_INPUT_SCHEMA = {
1832
167
  type: "object",
@@ -1849,6 +184,9 @@ const READ_SKILL_FILE_TOOL = {
1849
184
  inputSchema: READ_SKILL_FILE_INPUT_SCHEMA,
1850
185
  source: "builtin"
1851
186
  };
187
+
188
+ //#endregion
189
+ //#region ../runtime/src/tools/askUserTool.ts
1852
190
  const ASK_USER_TOOL_NAME = "ask_user";
1853
191
  /** 一次 `ask_user` 能收的字段数上限(FR-11.3 裁决值),可由 `AgentLoopConfig` 覆盖 */
1854
192
  const ASK_USER_MAX_FIELDS = 20;
@@ -1938,78 +276,9 @@ const ASK_USER_TOOL = {
1938
276
  inputSchema: ASK_USER_INPUT_SCHEMA,
1939
277
  source: "builtin"
1940
278
  };
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
- }
279
+
280
+ //#endregion
281
+ //#region ../runtime/src/tools/scriptContext.ts
2013
282
  /**
2014
283
  * 越权 / 绝对路径 / 父级遍历的技能目录读取:策略拒绝(FS_PERMISSION_DENIED,
2015
284
  * 计入 POLICY_DENIAL_CODES → 不计入隔离失败计数),
@@ -2033,7 +302,7 @@ function resolveSkillDir(skillRoot, dir, relativePath) {
2033
302
  * 四个显式能力,不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
2034
303
  */
2035
304
  function createScriptContext(deps) {
2036
- const { fs, artifactStore, skillName, skillRoot, runId, confirm, fetchData, onWarning, onArtifactCreated } = deps;
305
+ const { fs, artifactStore, skillName, skillRoot, runId, confirm, fetchData, uploadFiles, onWarning, onArtifactCreated } = deps;
2037
306
  const readFs = fs.withRoot?.(skillRoot) ?? fs;
2038
307
  return {
2039
308
  skillName,
@@ -2067,210 +336,17 @@ function createScriptContext(deps) {
2067
336
  },
2068
337
  ...confirm ? { confirm } : {},
2069
338
  ...fetchData ? { fetchData } : {},
339
+ ...deps.documentSurface ? { documentSurface: true } : {},
340
+ ...uploadFiles ? {
341
+ listUploadFiles: async () => [...await uploadFiles.list()],
342
+ readUploadFile: (id) => uploadFiles.read(id)
343
+ } : {},
2070
344
  ...onWarning ? { onWarning } : {}
2071
345
  };
2072
346
  }
2073
- const isRecord$4 = (v) => typeof v === "object" && v !== null;
2074
- /**
2075
- * $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
2076
- * 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
2077
- */
2078
- function extractChartSpec(data) {
2079
- if (!isRecord$4(data)) return void 0;
2080
- const raw = data["$chart"];
2081
- if (!isRecord$4(raw)) return void 0;
2082
- const { kind, labels, series } = raw;
2083
- if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
2084
- if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
2085
- if (!Array.isArray(series)) return void 0;
2086
- const validSeries = [];
2087
- for (const item of series) {
2088
- if (!isRecord$4(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
2089
- const name = item["name"];
2090
- validSeries.push({
2091
- ...typeof name === "string" ? { name } : {},
2092
- data: item["data"]
2093
- });
2094
- }
2095
- const title = raw["title"];
2096
- return {
2097
- kind,
2098
- labels,
2099
- series: validSeries,
2100
- ...typeof title === "string" ? { title } : {}
2101
- };
2102
- }
2103
- /**
2104
- * 默认的结果渲染构造:run 内收集的 renderBlocks(chart 等)在前,
2105
- * LLM 最终输出 → markdown block,run.artifacts → file blocks 在后;summary 取 terminationReason。
2106
- * 注入 output block 时一并记下它的下标(S7),供消费方按来源去重。
2107
- */
2108
- function buildRenderResult(run, output, renderBlocks = []) {
2109
- const blocks = [...renderBlocks];
2110
- let outputBlockIndex;
2111
- if (output.trim() !== "") {
2112
- outputBlockIndex = blocks.length;
2113
- blocks.push({
2114
- type: "markdown",
2115
- text: output
2116
- });
2117
- }
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
- }
2127
- return {
2128
- runId: run.id,
2129
- summary: run.terminationReason,
2130
- blocks,
2131
- artifacts: run.artifacts,
2132
- ...outputBlockIndex === void 0 ? {} : { outputBlockIndex }
2133
- };
2134
- }
2135
- const MAX_SURFACE_BYTES = 256 * 1024;
2136
- const MAX_ACTIONS = 32;
2137
- const MAX_NODES = 2e3;
2138
- const MAX_NODE_DEPTH = 32;
2139
- const MAX_JSON_DEPTH = 16;
2140
- const MAX_PATCH_OPERATIONS = 128;
2141
- const actionIntents = /* @__PURE__ */ new Set([
2142
- "submit",
2143
- "cancel",
2144
- "select",
2145
- "download",
2146
- "refresh"
2147
- ]);
2148
- function isRecord$3(value) {
2149
- return typeof value === "object" && value !== null && !Array.isArray(value);
2150
- }
2151
- function reject(message) {
2152
- throw new WebSkillError("VALIDATION_FAILED", message);
2153
- }
2154
- function requireString(value, name) {
2155
- if (typeof value !== "string" || value.trim() === "") reject(`${name} must be a non-empty string`);
2156
- }
2157
- function requireNonNegativeInteger(value, name) {
2158
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) reject(`${name} must be a non-negative integer`);
2159
- }
2160
- function isJsonValue(value, depth = 0) {
2161
- if (depth > MAX_JSON_DEPTH || value === void 0 || typeof value === "function" || typeof value === "symbol") return false;
2162
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
2163
- if (typeof value === "number") return Number.isFinite(value);
2164
- if (Array.isArray(value)) return value.every((item) => isJsonValue(item, depth + 1));
2165
- if (!isRecord$3(value)) return false;
2166
- return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
2167
- }
2168
- /**
2169
- * Runtime 只做结构校验;组件名白名单与 props schema 属于 catalog 语义,
2170
- * 留在 `@webskill/ui`——runtime 不得为了校验反向依赖 ui。
2171
- */
2172
- function assertNode(value, path, depth, counter) {
2173
- if (depth > MAX_NODE_DEPTH) reject(`A UI spec tree must not nest deeper than ${MAX_NODE_DEPTH} levels`);
2174
- if (++counter.nodes > MAX_NODES) reject(`A UI spec tree must contain at most ${MAX_NODES} nodes`);
2175
- if (!isRecord$3(value)) reject(`${path} must be an object`);
2176
- requireString(value["component"], `${path}.component`);
2177
- if (value["id"] !== void 0) requireString(value["id"], `${path}.id`);
2178
- if (value["props"] !== void 0 && (!isRecord$3(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
2179
- const children = value["children"];
2180
- if (children === void 0) return;
2181
- if (!Array.isArray(children)) reject(`${path}.children must be an array`);
2182
- children.forEach((child, index) => assertNode(child, `${path}.children[${index}]`, depth + 1, counter));
2183
- }
2184
- function assertActions(value) {
2185
- if (value === void 0) return;
2186
- if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
2187
- for (const action of value) {
2188
- if (!isRecord$3(action)) reject("A surface action must be an object");
2189
- requireString(action["id"], "Surface action ID");
2190
- const intent = action["intent"];
2191
- if (typeof intent !== "string" || !actionIntents.has(intent)) reject("Surface action intent is invalid");
2192
- if (action["awaitResponse"] !== void 0 && typeof action["awaitResponse"] !== "boolean") reject("Surface action awaitResponse must be a boolean");
2193
- if (action["nonce"] !== void 0 && (typeof action["nonce"] !== "string" || action["nonce"] === "")) reject("Surface action nonce must be a non-empty string");
2194
- }
2195
- }
2196
- /** Validates the allowlisted, data-only node tree accepted by a UI surface renderer. @experimental */
2197
- function validateUiSpecNode(value) {
2198
- assertNode(value, "root", 0, { nodes: 0 });
2199
- if (!isJsonValue(value)) reject("A UI spec tree must contain JSON data only");
2200
- if (JSON.stringify(value).length > MAX_SURFACE_BYTES) reject(`A UI spec tree exceeds the ${MAX_SURFACE_BYTES}-byte limit`);
2201
- return structuredClone(value);
2202
- }
2203
- function assertPatch(value) {
2204
- if (!isRecord$3(value)) reject("A surface patch operation must be an object");
2205
- if (value["op"] !== "replace" && value["op"] !== "merge" && value["op"] !== "append") reject("Surface patch operation is invalid");
2206
- requireString(value["path"], "Surface patch path");
2207
- if (!value["path"].startsWith("/")) reject("Surface patch path must be a JSON pointer");
2208
- if (!isJsonValue(value["value"])) reject("Surface patch value must be JSON data");
2209
- }
2210
- /** Validates an individual event in the framework-neutral surface stream. @experimental */
2211
- function validateUiSpecEvent(value) {
2212
- if (!isRecord$3(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
2213
- if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
2214
- switch (value["type"]) {
2215
- case "open":
2216
- requireString(value["id"], "UI surface ID");
2217
- assertActions(value["actions"]);
2218
- return {
2219
- type: "open",
2220
- ...value["runId"] ? { runId: value["runId"] } : {},
2221
- id: value["id"],
2222
- node: validateUiSpecNode(value["node"]),
2223
- ...value["actions"] ? { actions: structuredClone(value["actions"]) } : {}
2224
- };
2225
- case "patch":
2226
- requireString(value["id"], "Surface patch ID");
2227
- requireNonNegativeInteger(value["revision"], "Surface patch revision");
2228
- if (!Array.isArray(value["operations"]) || value["operations"].length === 0) reject("A surface patch requires operations");
2229
- if (value["operations"].length > MAX_PATCH_OPERATIONS) reject(`A surface patch must contain at most ${MAX_PATCH_OPERATIONS} operations`);
2230
- for (const operation of value["operations"]) assertPatch(operation);
2231
- return {
2232
- type: "patch",
2233
- ...value["runId"] ? { runId: value["runId"] } : {},
2234
- id: value["id"],
2235
- revision: value["revision"],
2236
- operations: structuredClone(value["operations"])
2237
- };
2238
- case "complete":
2239
- requireString(value["id"], "Surface completion ID");
2240
- requireNonNegativeInteger(value["revision"], "Surface completion revision");
2241
- return {
2242
- type: "complete",
2243
- ...value["runId"] ? { runId: value["runId"] } : {},
2244
- id: value["id"],
2245
- revision: value["revision"]
2246
- };
2247
- case "error":
2248
- requireString(value["id"], "Surface error ID");
2249
- requireString(value["code"], "Surface error code");
2250
- requireString(value["message"], "Surface error message");
2251
- return {
2252
- type: "error",
2253
- ...value["runId"] ? { runId: value["runId"] } : {},
2254
- id: value["id"],
2255
- code: value["code"],
2256
- message: value["message"]
2257
- };
2258
- case "cancel":
2259
- requireString(value["id"], "Surface cancellation ID");
2260
- return {
2261
- type: "cancel",
2262
- ...value["runId"] ? { runId: value["runId"] } : {},
2263
- id: value["id"]
2264
- };
2265
- default: return reject("UI surface event type is invalid");
2266
- }
2267
- }
2268
- /** Extracts validated surface stream events from structured tool output. @experimental */
2269
- function extractUiSpecEvents(data) {
2270
- if (!isRecord$3(data) || data["$surface"] === void 0) return [];
2271
- const raw = data["$surface"];
2272
- return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
2273
- }
347
+
348
+ //#endregion
349
+ //#region ../runtime/src/interaction/schemaToForm.ts
2274
350
  /**
2275
351
  * JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
2276
352
  * providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
@@ -2309,6 +385,9 @@ function mapFieldType(prop) {
2309
385
  default: return "textarea";
2310
386
  }
2311
387
  }
388
+
389
+ //#endregion
390
+ //#region ../runtime/src/lifecycle/eventBus.ts
2312
391
  /** 生命周期事件总线:只读观测,支持按阶段或通配订阅 */
2313
392
  var EventBus = class {
2314
393
  #listeners = /* @__PURE__ */ new Map();
@@ -2349,6 +428,9 @@ var EventBus = class {
2349
428
  }
2350
429
  }
2351
430
  };
431
+
432
+ //#endregion
433
+ //#region ../runtime/src/memory/serializingMemoryStore.ts
2352
434
  /**
2353
435
  * per-scope 串行化 MemoryStore 装饰器:同一 scope 的 get/set/delete/list
2354
436
  * 按 promise 链串行(并发 run 的 read-modify-write 不丢计数)。
@@ -2392,6 +474,9 @@ var SerializingMemoryStore = class {
2392
474
  return this.#serialize(scope, () => fn(this.#inner));
2393
475
  }
2394
476
  };
477
+
478
+ //#endregion
479
+ //#region ../runtime/src/profile/types.ts
2395
480
  /** @experimental */
2396
481
  const DEFAULT_USER_PROFILE_LIMITS = {
2397
482
  recordLimit: 500,
@@ -2406,6 +491,9 @@ const EMPTY_USER_PROFILE = {
2406
491
  entries: [],
2407
492
  refinedAt: 0
2408
493
  };
494
+
495
+ //#endregion
496
+ //#region ../runtime/src/profile/inject.ts
2409
497
  /** 注入段的标题。宿主/测试要判断「有没有注入画像」时对它取子串,不要自己再拼一份 @experimental */
2410
498
  const USER_PROFILE_PROMPT_HEADER = "## About this user (inferred from past behavior; may be outdated)";
2411
499
  /**
@@ -2436,6 +524,9 @@ function renderUserProfileContext(profile = EMPTY_USER_PROFILE, maxBytes = DEFAU
2436
524
  while (kept > 0 && byteLength(render(kept)) > maxBytes) kept -= 1;
2437
525
  return kept === 0 ? "" : render(kept);
2438
526
  }
527
+
528
+ //#endregion
529
+ //#region ../runtime/src/profile/records.ts
2439
530
  /** `user:{id}` scope 下的行为记录 key @experimental */
2440
531
  const BEHAVIOR_RECORDS_KEY = "behaviorRecords";
2441
532
  /** `user:{id}` scope 下的画像 key @experimental */
@@ -2557,59 +648,26 @@ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
2557
648
  for (const record of [...records].reverse()) take(record);
2558
649
  return [...picked.values()].sort((a, b) => a.at - b.at);
2559
650
  }
651
+
652
+ //#endregion
653
+ //#region ../runtime/src/routing/plainChatPrompt.ts
2560
654
  /**
2561
655
  * 无工具模型的系统提示(0.6.0 FR-14.3)。
2562
656
  * 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
2563
657
  */
2564
658
  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.";
659
+
660
+ //#endregion
661
+ //#region ../runtime/src/routing/onDemandToolsHint.ts
2565
662
  /**
2566
663
  * 按需披露的系统提示(分册 19 / FR-19.7)。
2567
664
  * 只在本 run 确实存在按需工具时追加——没有按需工具却说「有些工具没列出来」,
2568
665
  * 是在教模型怀疑一个完整的工具表。
2569
666
  */
2570
667
  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.";
2571
- const READ_LINKED_DOCUMENT_TOOL_NAME = "read_linked_document";
2572
- /**
2573
- * 能直接给模型用的 MIME。其余一律拒绝并列出这份清单——
2574
- * 「尽力而为地猜格式」会让模型收到乱码却以为读成功了。
2575
- */
2576
- const SUPPORTED_DOCUMENT_MIME = {
2577
- pdf: "application/pdf",
2578
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2579
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
2580
- };
2581
- /** 引擎侧固定流程(§2.3)用到的错误文案,集中一处便于判据引用 */
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.`;
2583
- /**
2584
- * 内建工具:读页面链接指向的文档。
2585
- *
2586
- * **始终注册**(只要宿主装配了 reader):docx / xlsx 与纯文本这条路对所有模型成立。
2587
- * PDF 目标在模型不支持文档时**取数后拒绝**,而不是入口就拦 ——
2588
- * 入口拦会误伤 docx,它抽成文本后根本不需要文档能力。
2589
- */
2590
- const READ_LINKED_DOCUMENT_TOOL = {
2591
- name: READ_LINKED_DOCUMENT_TOOL_NAME,
2592
- 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.",
2593
- inputSchema: {
2594
- type: "object",
2595
- properties: { url: {
2596
- type: "string",
2597
- description: "The href of a link that appeared in a perceive_page result."
2598
- } },
2599
- required: ["url"]
2600
- },
2601
- source: "builtin"
2602
- };
2603
- /** 二进制转 base64;`btoa` 只吃 latin1,必须逐字节喂而不是先 decode 成字符串 */
2604
- function toBase64(bytes) {
2605
- let binary = "";
2606
- for (const byte of bytes) binary += String.fromCharCode(byte);
2607
- return btoa(binary);
2608
- }
2609
- /** `toBase64` 的逆;分片里的文档要交回抽取器时用(0.13.0 FR-22.3) */
2610
- function fromBase64(data) {
2611
- return Uint8Array.from(atob(data), (char) => char.charCodeAt(0));
2612
- }
668
+
669
+ //#endregion
670
+ //#region ../runtime/src/trace/traceRecorder.ts
2613
671
  /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
2614
672
  var TraceRecorder = class {
2615
673
  #runId;
@@ -2636,6 +694,9 @@ var TraceRecorder = class {
2636
694
  return [...this.#events];
2637
695
  }
2638
696
  };
697
+
698
+ //#endregion
699
+ //#region ../runtime/src/trace/todoMarker.ts
2639
700
  const isRecord$2 = (v) => typeof v === "object" && v !== null;
2640
701
  /** 工具结果可经 `$todo` 标记记入 trace 的事件类型;其余类型不接受,防止工具源伪造 run.* */
2641
702
  const TODO_TRACE_TYPES = /* @__PURE__ */ new Set([
@@ -2666,6 +727,9 @@ function extractTodoTraceEvents(data) {
2666
727
  }
2667
728
  return events;
2668
729
  }
730
+
731
+ //#endregion
732
+ //#region ../runtime/src/skillGeneration/candidateMarker.ts
2669
733
  const isRecord$1 = (v) => typeof v === "object" && v !== null;
2670
734
  /**
2671
735
  * `$skillCandidate` 约定的形状校验:JSON content 的 data 含合法 `$skillCandidate` 键 → 候选载荷。
@@ -2685,6 +749,9 @@ function extractSkillCandidate(data) {
2685
749
  name
2686
750
  };
2687
751
  }
752
+
753
+ //#endregion
754
+ //#region ../runtime/src/engine/limits.ts
2688
755
  /**
2689
756
  * Agent loop 的运行上限默认值。**全仓唯一来源**(S8)。
2690
757
  *
@@ -2706,6 +773,9 @@ const DEFAULT_LOOP_LIMITS = {
2706
773
  * @stable
2707
774
  */
2708
775
  const MAX_TOOL_STEP_ARG_BYTES = 32768;
776
+
777
+ //#endregion
778
+ //#region ../runtime/src/engine/snapshot.ts
2709
779
  const RUN_SNAPSHOT_SCHEMA_VERSION = 3;
2710
780
  /** @experimental */
2711
781
  function isUnsupportedRunSnapshot(entry) {
@@ -2751,7 +821,7 @@ var FsRunSnapshotStore = class {
2751
821
  throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
2752
822
  }
2753
823
  const schemaVersion = typeof snapshot.schemaVersion === "number" ? snapshot.schemaVersion : 0;
2754
- if (schemaVersion !== 3) throw new WebSkillError("RUN_SNAPSHOT_SCHEMA_UNSUPPORTED", `Snapshot for run "${runId}" uses schema version ${schemaVersion}; this runtime reads version 3. The file was kept for read-only inspection.`);
824
+ if (schemaVersion !== 3) throw new WebSkillError("RUN_SNAPSHOT_SCHEMA_UNSUPPORTED", `Snapshot for run "${runId}" uses schema version ${schemaVersion}; this runtime reads version ${3}. The file was kept for read-only inspection.`);
2755
825
  if (snapshot.runId !== runId) {
2756
826
  await this.#fs.remove(path).catch(() => void 0);
2757
827
  throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
@@ -2803,6 +873,9 @@ var FsRunSnapshotStore = class {
2803
873
  } catch {}
2804
874
  }
2805
875
  };
876
+
877
+ //#endregion
878
+ //#region ../runtime/src/engine/toolAccess.ts
2806
879
  /**
2807
880
  * LLM 可见名 → 作者在 SKILL.md 里写的形态。
2808
881
  * `<已激活技能>__<脚本>` 保持原样(裸标识符按技能作用域单独匹配),
@@ -2872,6 +945,9 @@ function isNamedByActivatedSkill(state, llmToolName) {
2872
945
  }
2873
946
  return false;
2874
947
  }
948
+
949
+ //#endregion
950
+ //#region ../runtime/src/engine/toolStepRedaction.ts
2875
951
  /**
2876
952
  * 参数敏感标注关键字(分册 30 / FR-30.3)。
2877
953
  *
@@ -2935,6 +1011,9 @@ function redactToolStepArgs(args, schema, trust) {
2935
1011
  redacted
2936
1012
  };
2937
1013
  }
1014
+
1015
+ //#endregion
1016
+ //#region ../runtime/src/engine/agentLoop.ts
2938
1017
  const MAX_SURFACE_PATCHES_PER_SECOND = 240;
2939
1018
  /**
2940
1019
  * 一轮 LLM 调用可能抛出、且**值得原样上报**的结构化码。
@@ -5047,6 +3126,8 @@ var AgentLoop = class {
5047
3126
  runId: state.runId,
5048
3127
  confirm: (message) => this.#confirm(message, state),
5049
3128
  ...this.#deps.fetchData ? { fetchData: this.#deps.fetchData } : {},
3129
+ ...this.#deps.documentSurface ? { documentSurface: true } : {},
3130
+ ...this.#deps.uploadFiles ? { uploadFiles: this.#uploadFilePort(this.#deps.uploadFiles, skillName, state) } : {},
5050
3131
  onWarning: (message) => state.trace.record("run.warning", { message }),
5051
3132
  onArtifactCreated: (artifact) => createdIds.add(artifact.id)
5052
3133
  });
@@ -5144,6 +3225,71 @@ var AgentLoop = class {
5144
3225
  #nextInteractionId(state) {
5145
3226
  return `int-${++state.interactionSeq}`;
5146
3227
  }
3228
+ /**
3229
+ * 把宿主端口包成「每次读都要人点头」的形态再交给脚本上下文。
3230
+ * 四个执行器共用这一份,避免各自实现一套把关而出现口径差异。
3231
+ */
3232
+ #uploadFilePort(source, skillName, state) {
3233
+ return {
3234
+ list: () => source.list(),
3235
+ read: async (id) => {
3236
+ const file = (await source.list()).find((f) => f.id === id);
3237
+ if (!file) throw new WebSkillError("UPLOAD_FILE_NOT_FOUND", `Upload file "${id}" is not part of the message that started this run`);
3238
+ const budget = this.#deps.maxUploadFileBytes;
3239
+ if (budget !== void 0 && file.size > budget) throw new WebSkillError("UPLOAD_FILE_TOO_LARGE", `Upload file "${file.name}" is ${file.size} bytes, over the ${budget} byte limit; it was not read`);
3240
+ const approved = await this.#consentUploadFile(file, skillName, state);
3241
+ await this.#auditUploadFile(file, skillName, approved, state);
3242
+ if (!approved) throw new WebSkillError("TOOL_DENIED", `The user declined to share "${file.name}" with skill "${skillName}"`);
3243
+ return source.read(id);
3244
+ }
3245
+ };
3246
+ }
3247
+ /**
3248
+ * 与 `#confirm` 分开是刻意的:那条通路在没接 UI 桥、桥故障或 confirmations=auto-approve
3249
+ * 时都会自动放行,而本能力的「需批准」是恒定的(分册 17 D-17-3),只能失败即拒。
3250
+ */
3251
+ async #consentUploadFile(file, skillName, state) {
3252
+ if (!this.#deps.uiBridge) {
3253
+ state.trace.record("run.warning", { message: "No UiBridge configured; upload file read denied" });
3254
+ return false;
3255
+ }
3256
+ try {
3257
+ return await this.#interact(state, {
3258
+ type: "confirm",
3259
+ id: this.#nextInteractionId(state),
3260
+ message: `Let skill "${skillName}" read the file you uploaded: ${file.name} (${file.contentType}, ${file.size} bytes)?`,
3261
+ defaultValue: false
3262
+ }, {
3263
+ kind: "confirm",
3264
+ uploadFile: file.id
3265
+ }) === true;
3266
+ } catch (e) {
3267
+ if (e instanceof BridgeRequestError) {
3268
+ state.trace.record("run.warning", { message: `Upload file consent bridge failed, denied: ${e.message}` });
3269
+ return false;
3270
+ }
3271
+ throw e;
3272
+ }
3273
+ }
3274
+ /** 只写文件名 / 类型 / 字节数与是否被拒,**不写内容**(FR-17.7) */
3275
+ async #auditUploadFile(file, skillName, approved, state) {
3276
+ try {
3277
+ await this.#deps.uploadFileAudit?.append({
3278
+ type: "upload.file.read",
3279
+ target: skillName,
3280
+ data: {
3281
+ runId: state.runId,
3282
+ fileId: file.id,
3283
+ fileName: file.name,
3284
+ contentType: file.contentType,
3285
+ bytes: file.size,
3286
+ denied: !approved
3287
+ }
3288
+ });
3289
+ } catch (e) {
3290
+ state.trace.record("run.warning", { message: `Upload file audit failed: ${messageOf(e)}` });
3291
+ }
3292
+ }
5147
3293
  async #memoryGet(scope, key, state) {
5148
3294
  try {
5149
3295
  return await this.#deps.memory?.get(scope, key);
@@ -5314,15 +3460,9 @@ var AgentLoop = class {
5314
3460
  };
5315
3461
  }
5316
3462
  };
5317
- /**
5318
- * Catalog 合并:同名本地优先;外部条目的 mcp:// 根 URI 保留,
5319
- * 调用方可凭 URI 强制选择外部技能(绕过本地优先)。
5320
- */
5321
- function mergeCatalogEntries(localEntries, providerEntries) {
5322
- const byName = new Map(localEntries.map((e) => [e.name, e]));
5323
- for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
5324
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
5325
- }
3463
+
3464
+ //#endregion
3465
+ //#region ../runtime/src/engine/runtime.ts
5326
3466
  /**
5327
3467
  * session history 滚动裁剪:超出预算时裁掉中段,保留首尾。
5328
3468
  * 边界对齐 LLM tool 契约:head 不以未应答的 assistant toolCalls 结尾,
@@ -5400,11 +3540,12 @@ var WebSkillRuntime = class {
5400
3540
  return {
5401
3541
  id: sessionId,
5402
3542
  createdAt,
5403
- run: (prompt) => {
3543
+ run: (prompt, runOptions = {}) => {
5404
3544
  const result = queue.then(async () => {
5405
3545
  const r = await this.run(prompt, {
5406
3546
  sessionId,
5407
- history
3547
+ history,
3548
+ ...runOptions
5408
3549
  });
5409
3550
  history = trimSessionHistory(r.messages.slice(1), maxHistory);
5410
3551
  return r;
@@ -5471,7 +3612,11 @@ var WebSkillRuntime = class {
5471
3612
  docxExtractor: this.#deps.docxExtractor,
5472
3613
  xlsxExtractor: this.#deps.xlsxExtractor,
5473
3614
  pdfExtractor: this.#deps.pdfExtractor,
5474
- documentAudit: this.#deps.documentAudit
3615
+ documentAudit: this.#deps.documentAudit,
3616
+ documentSurface: this.#deps.documentSurface,
3617
+ uploadFileAudit: this.#deps.uploadFileAudit,
3618
+ maxUploadFileBytes: this.#deps.maxUploadFileBytes,
3619
+ uploadFiles: options.uploadFiles
5475
3620
  }, this.#deps.config);
5476
3621
  const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
5477
3622
  this.#loops.set(runId, loop);
@@ -5599,173 +3744,31 @@ var WebSkillRuntime = class {
5599
3744
  linkedDocuments: this.#deps.linkedDocuments,
5600
3745
  docxExtractor: this.#deps.docxExtractor,
5601
3746
  xlsxExtractor: this.#deps.xlsxExtractor,
5602
- pdfExtractor: this.#deps.pdfExtractor,
5603
- documentAudit: this.#deps.documentAudit
5604
- }, this.#deps.config);
5605
- this.#loops.set(runId, loop);
5606
- try {
5607
- return await loop.resume(snapshot);
5608
- } finally {
5609
- this.#loops.delete(runId);
5610
- }
5611
- }
5612
- /**
5613
- * 取消进行中的 run(chatbot Stop 按钮等):触发该 run 的 AbortController,
5614
- * run 以 cancelled(RUN_CANCELLED)终止;未找到活跃 run 返回 false。
5615
- * 取消在下一个中断点生效(LLM complete/stream;交互等待不强制中断)。
5616
- */
5617
- cancel(runId) {
5618
- return this.#loops.get(runId)?.cancel(runId) ?? false;
5619
- }
5620
- };
5621
- /**
5622
- * navigator.webskill 门面装配层:零新业务逻辑,全部直转
5623
- * SkillDiscovery / validateSkills / WebSkillRuntime / SkillManager。
5624
- * @stable
5625
- */
5626
- function createWebSkillApi(deps) {
5627
- const { fs, roots } = deps;
5628
- let runtime;
5629
- const discoveries = /* @__PURE__ */ new Map();
5630
- const discoveryFor = (paths) => {
5631
- const key = paths.join("");
5632
- let discovery = discoveries.get(key);
5633
- if (!discovery) {
5634
- discovery = new SkillDiscovery(fs, paths);
5635
- discoveries.set(key, discovery);
5636
- }
5637
- return discovery;
5638
- };
5639
- const lazyRuntime = () => {
5640
- runtime ??= new WebSkillRuntime({
5641
- fs,
5642
- roots,
5643
- llm: deps.llm,
5644
- ...deps.executor ? { executor: deps.executor } : {},
5645
- ...deps.artifactStore ? { artifactStore: deps.artifactStore } : {},
5646
- ...deps.loopConfig ? { config: deps.loopConfig } : {}
5647
- });
5648
- return runtime;
5649
- };
5650
- const requireManager = () => {
5651
- if (!deps.skillManager) throw new WebSkillError("TOOL_UNSUPPORTED", "No skill manager is configured; install/uninstall is unavailable");
5652
- return deps.skillManager;
5653
- };
5654
- return {
5655
- async discover(path) {
5656
- const { entries } = await discoveryFor([path]).discover();
5657
- return buildCatalog(entries);
5658
- },
5659
- async read(name) {
5660
- const { entries } = await discoveryFor(roots).discover();
5661
- const entry = entries.find((e) => e.name === name);
5662
- if (!entry) throw new WebSkillError("SKILL_NOT_FOUND", `Unknown skill: ${name}`);
5663
- const skillMdPath = `${entry.root}/SKILL.md`;
5664
- const { metadata, body } = parseSkillMarkdown(await fs.readText(skillMdPath));
5665
- return {
5666
- metadata,
5667
- body,
5668
- location: {
5669
- skillRoot: entry.root,
5670
- skillMdPath,
5671
- source: entry.source
5672
- }
5673
- };
5674
- },
5675
- validate(path) {
5676
- return validateSkills(fs, [path]);
5677
- },
5678
- async run(prompt) {
5679
- return (await lazyRuntime().run(prompt)).run;
5680
- },
5681
- async install(url) {
5682
- const manifest = await requireManager().install(sourceFromUrl(url));
5683
- return {
5684
- name: manifest.name,
5685
- ...manifest.version !== void 0 ? { version: manifest.version } : {},
5686
- installedAt: manifest.installedAt,
5687
- integrity: manifest.integrity
5688
- };
5689
- },
5690
- async uninstall(name) {
5691
- await requireManager().uninstall(name);
5692
- }
5693
- };
5694
- }
5695
- /** install URL 分派:http(s) .zip → http;.git → git(无 git 能力的 manager 自行 TOOL_UNSUPPORTED);
5696
- * 其余 http(s) URL → INSTALL_FAILED(不静默当 local);非 URL → local */
5697
- function sourceFromUrl(url) {
5698
- const bare = url.split("?")[0] ?? url;
5699
- if (/^https?:\/\//.test(url)) {
5700
- if (bare.endsWith(".zip")) return {
5701
- type: "http",
5702
- url
5703
- };
5704
- if (bare.endsWith(".git")) return {
5705
- type: "git",
5706
- url
5707
- };
5708
- throw new WebSkillError("INSTALL_FAILED", `Unsupported install URL "${url}" (supported: http(s) URLs ending in .zip or .git, or local paths)`);
5709
- }
5710
- if (bare.endsWith(".git")) return {
5711
- type: "git",
5712
- url
5713
- };
5714
- return {
5715
- type: "local",
5716
- path: url
5717
- };
5718
- }
5719
- /** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
5720
- var HookRunner = class {
5721
- #hooks = /* @__PURE__ */ new Map();
5722
- /** 宿主可在装配后按 RuntimeConfig 调整(console Settings › Agent Runtime 即经此生效) */
5723
- timeoutMs;
5724
- failOnHookError;
5725
- onWarning;
5726
- constructor(options = {}) {
5727
- this.timeoutMs = options.timeoutMs ?? 5e3;
5728
- this.failOnHookError = options.failOnHookError ?? false;
5729
- this.onWarning = options.onWarning;
5730
- }
5731
- register(phase, hook) {
5732
- const key = phase;
5733
- const list = this.#hooks.get(key) ?? [];
5734
- list.push(hook);
5735
- this.#hooks.set(key, list);
5736
- return this;
3747
+ pdfExtractor: this.#deps.pdfExtractor,
3748
+ documentAudit: this.#deps.documentAudit,
3749
+ documentSurface: this.#deps.documentSurface,
3750
+ uploadFileAudit: this.#deps.uploadFileAudit,
3751
+ maxUploadFileBytes: this.#deps.maxUploadFileBytes
3752
+ }, this.#deps.config);
3753
+ this.#loops.set(runId, loop);
3754
+ try {
3755
+ return await loop.resume(snapshot);
3756
+ } finally {
3757
+ this.#loops.delete(runId);
3758
+ }
5737
3759
  }
5738
3760
  /**
5739
- * 已注册钩子的**计数**,按注册相位分组(`'*'` 为全相位钩子)。
5740
- * 刻意不返回函数引用:那会给 UI 一条调用宿主钩子的执行路径,而面板只需要「装没装上」。
3761
+ * 取消进行中的 run(chatbot Stop 按钮等):触发该 run 的 AbortController,
3762
+ * run cancelled(RUN_CANCELLED)终止;未找到活跃 run 返回 false。
3763
+ * 取消在下一个中断点生效(LLM complete/stream;交互等待不强制中断)。
5741
3764
  */
5742
- listRegisteredHooks() {
5743
- const counts = /* @__PURE__ */ new Map();
5744
- for (const [phase, hooks] of this.#hooks) counts.set(phase, hooks.length);
5745
- return counts;
5746
- }
5747
- async run(phase, ctx) {
5748
- const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
5749
- for (const hook of hooks) try {
5750
- await this.#withTimeout(Promise.resolve().then(() => hook(ctx)), phase);
5751
- } catch (e) {
5752
- if (this.failOnHookError) throw e instanceof WebSkillError ? e : new WebSkillError("RUN_FAILED", messageOf(e), e);
5753
- this.onWarning?.(messageOf(e));
5754
- }
5755
- }
5756
- #withTimeout(promise, phase) {
5757
- return new Promise((resolve, reject) => {
5758
- const timer = setTimeout(() => reject(new WebSkillError("RUN_TIMEOUT", `Lifecycle hook for phase "${phase}" timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
5759
- promise.then((v) => {
5760
- clearTimeout(timer);
5761
- resolve(v);
5762
- }, (e) => {
5763
- clearTimeout(timer);
5764
- reject(e instanceof Error ? e : new Error(String(e)));
5765
- });
5766
- });
3765
+ cancel(runId) {
3766
+ return this.#loops.get(runId)?.cancel(runId) ?? false;
5767
3767
  }
5768
3768
  };
3769
+
3770
+ //#endregion
3771
+ //#region ../runtime/src/memory/fsMemoryStore.ts
5769
3772
  const encode = (s) => encodeURIComponent(s);
5770
3773
  /**
5771
3774
  * 基于 FileSystemProvider 的 MemoryStore:
@@ -5848,191 +3851,10 @@ var FsMemoryStore = class {
5848
3851
  for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
5849
3852
  }
5850
3853
  };
5851
- /**
5852
- * 提炼提示词。要求返回结论列表而不是散文:条目化是「可见、可编辑、可删除」的前提(FR-19.3)。
5853
- * `id` 由模型给出稳定 slug,才能在下一次提炼时更新同一条而不是无限追加。
5854
- */
5855
- const USER_PROFILE_REFINE_PROMPT = `You summarize what a single user is like, based on records of what they did in a chat application.
5856
-
5857
- Return ONLY a JSON object with this shape, and nothing else:
5858
-
5859
- {
5860
- "entries": [
5861
- { "id": "slug-case-stable-id", "text": "One sentence about the user.", "confidence": "high" | "low" }
5862
- ]
5863
- }
5864
3854
 
5865
- Rules:
5866
- - Each entry is one durable, reusable fact or preference about the user — how they like answers, what they work on, values they keep entering, languages they use.
5867
- - Reuse the same "id" across runs for the same conclusion so it can be updated instead of duplicated. Use lowercase words joined by hyphens.
5868
- - Use "high" only when several records agree. A single record is "low".
5869
- - Do not restate individual records. Do not include secrets, credentials, or full personal identifiers.
5870
- - Do not invent anything the records do not support. If the records say nothing useful, return {"entries": []}.`;
5871
- /** 采样后的记录 → 送模型的摘要。导出供测试与宿主复核实际请求内容(AC-19.6) @experimental */
5872
- function toRecordDigests(records) {
5873
- return records.map((record) => ({
5874
- kind: record.kind,
5875
- at: new Date(record.at).toISOString(),
5876
- value: record.value,
5877
- ...record.scene.skillName !== void 0 ? { skill: record.scene.skillName } : {},
5878
- ...record.scene.fieldName !== void 0 ? { field: record.scene.fieldName } : {},
5879
- ...record.scene.fieldSemantics !== void 0 ? { meaning: record.scene.fieldSemantics } : {},
5880
- ...record.scene.options !== void 0 ? { options: record.scene.options } : {},
5881
- ...record.confirmed === true ? { confirmed: true } : {}
5882
- }));
5883
- }
5884
- /** 模型可能把 JSON 包在 ```json 里;只取第一个对象,解析不出就当没结论 */
5885
- function parseEntries(content, now) {
5886
- const text = promptText(content ?? []);
5887
- const start = text.indexOf("{");
5888
- const end = text.lastIndexOf("}");
5889
- if (start < 0 || end <= start) return [];
5890
- let parsed;
5891
- try {
5892
- parsed = JSON.parse(text.slice(start, end + 1));
5893
- } catch {
5894
- return [];
5895
- }
5896
- const raw = parsed?.entries;
5897
- return readProfileEntries(Array.isArray(raw) ? raw.map((item) => ({
5898
- updatedAt: now,
5899
- ...item
5900
- })) : []);
5901
- }
5902
- /**
5903
- * 一次提炼(FR-19.3)。调用方负责持久化返回值;本函数不碰存储,
5904
- * 也不吞异常——「失败不影响会话结束」由调用点的 fire-and-forget 保证,
5905
- * 在这里吞掉会让手动提炼入口无法报错。
5906
- * @experimental
5907
- */
5908
- async function refineUserProfile(input) {
5909
- const limits = input.limits ?? DEFAULT_USER_PROFILE_LIMITS;
5910
- const now = (input.now ?? Date.now)();
5911
- const current = input.profile ?? EMPTY_USER_PROFILE;
5912
- const sampled = sampleBehaviorRecords(input.records, limits);
5913
- if (sampled.length === 0) return current;
5914
- const existing = current.entries.map((entry) => ({
5915
- id: entry.id,
5916
- text: entry.text
5917
- }));
5918
- const entries = parseEntries((await input.llm.complete({
5919
- ...input.model !== void 0 ? { model: input.model } : {},
5920
- ...input.signal !== void 0 ? { signal: input.signal } : {},
5921
- messages: [{
5922
- role: "system",
5923
- content: [{
5924
- type: "text",
5925
- text: USER_PROFILE_REFINE_PROMPT
5926
- }]
5927
- }, {
5928
- role: "user",
5929
- content: [{
5930
- type: "text",
5931
- text: `Existing conclusions:\n${JSON.stringify(existing)}\n\nRecords:\n${JSON.stringify(toRecordDigests(sampled))}`
5932
- }]
5933
- }]
5934
- })).content, now);
5935
- if (entries.length === 0) return current;
5936
- return mergeProfileEntries(current, entries, {
5937
- now,
5938
- limits
5939
- });
5940
- }
5941
- /** 导出文件的版本;导入时逐字校验,不做向前兼容猜测 @experimental */
5942
- const USER_PROFILE_EXPORT_VERSION = 1;
5943
- /** 与偏好导出同一套判据:归一化后按子串匹配,命中即拒 */
5944
- const CREDENTIAL_MARKERS = [
5945
- "apikey",
5946
- "secret",
5947
- "token",
5948
- "password",
5949
- "passphrase",
5950
- "credential",
5951
- "privatekey"
5952
- ];
5953
- const isCredentialKey = (key) => {
5954
- const normalized = key.toLowerCase().replace(/[-_\s]/g, "");
5955
- return CREDENTIAL_MARKERS.some((marker) => normalized.includes(marker));
5956
- };
5957
- function findCredentialKey(value) {
5958
- if (Array.isArray(value)) {
5959
- for (const item of value) {
5960
- const hit = findCredentialKey(item);
5961
- if (hit !== void 0) return hit;
5962
- }
5963
- return;
5964
- }
5965
- if (typeof value !== "object" || value === null) return void 0;
5966
- for (const [key, child] of Object.entries(value)) {
5967
- if (isCredentialKey(key)) return key;
5968
- const hit = findCredentialKey(child);
5969
- if (hit !== void 0) return hit;
5970
- }
5971
- }
5972
- /**
5973
- * 导出画像(FR-19.7)。`excludeIds` 来自导出前的审阅界面:用户勾掉的条目不进文件。
5974
- * @experimental
5975
- */
5976
- function exportUserProfile(profile, options) {
5977
- const excluded = new Set(options.excludeIds ?? []);
5978
- const result = {
5979
- kind: "webskill.user-profile",
5980
- version: 1,
5981
- origin: options.origin,
5982
- exportedAt: (options.now ?? Date.now)(),
5983
- entries: profile.entries.filter((entry) => !excluded.has(entry.id))
5984
- };
5985
- const leaked = findCredentialKey(result);
5986
- if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `A user profile export must not contain credentials, but "${leaked}" was present`);
5987
- return result;
5988
- }
5989
- /** 解析并校验导入内容(FR-19.7)。含凭据是**拒绝**而不是忽略 @experimental */
5990
- function parseUserProfileExport(raw) {
5991
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must be a JSON object");
5992
- const record = raw;
5993
- if (record["kind"] !== "webskill.user-profile") throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must have \"kind\": \"webskill.user-profile\"");
5994
- if (record["version"] !== 1) throw new WebSkillError("PROFILE_IMPORT_VERSION_UNSUPPORTED", `Unsupported user profile file version ${JSON.stringify(record["version"])}; expected 1`);
5995
- const leaked = findCredentialKey(record);
5996
- if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `The user profile file contains a credential field "${leaked}"; the import was rejected`);
5997
- const entries = readProfileEntries(record["entries"]);
5998
- if (entries.length === 0) throw new WebSkillError("PROFILE_IMPORT_INVALID", "The user profile file has no usable entries");
5999
- return {
6000
- kind: "webskill.user-profile",
6001
- version: 1,
6002
- origin: typeof record["origin"] === "string" ? record["origin"] : "unknown",
6003
- exportedAt: typeof record["exportedAt"] === "number" ? record["exportedAt"] : 0,
6004
- entries
6005
- };
6006
- }
6007
- function diffUserProfile(current, incoming) {
6008
- const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
6009
- const added = [];
6010
- const updated = [];
6011
- for (const entry of incoming.entries) {
6012
- const existing = byId.get(entry.id);
6013
- if (existing === void 0) added.push(entry);
6014
- else if (existing.text !== entry.text) updated.push(entry);
6015
- }
6016
- return {
6017
- added,
6018
- updated
6019
- };
6020
- }
6021
- /** 确认后落库:同 id 覆盖,其余保留;不触碰模型配置与凭据 @experimental */
6022
- function applyUserProfileImport(current, incoming, options = {}) {
6023
- const now = (options.now ?? Date.now)();
6024
- const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
6025
- for (const entry of incoming.entries) byId.set(entry.id, {
6026
- ...entry,
6027
- updatedAt: now
6028
- });
6029
- return {
6030
- version: 1,
6031
- entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, DEFAULT_USER_PROFILE_LIMITS.entryLimit),
6032
- refinedAt: current.refinedAt
6033
- };
6034
- }
6035
- const INDEX_FILE$1 = "index.json";
3855
+ //#endregion
3856
+ //#region ../runtime/src/artifacts/fsArtifactStore.ts
3857
+ const INDEX_FILE = "index.json";
6036
3858
  /**
6037
3859
  * 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
6038
3860
  * 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
@@ -6066,7 +3888,7 @@ var FsArtifactStore = class {
6066
3888
  }
6067
3889
  async listArtifacts(runId) {
6068
3890
  assertSafePathSegment(runId, "runId");
6069
- const indexPath = `${this.#root}/${runId}/${INDEX_FILE$1}`;
3891
+ const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
6070
3892
  if (!await this.#fs.exists(indexPath)) return [];
6071
3893
  const raw = await this.#fs.readText(indexPath);
6072
3894
  try {
@@ -6099,10 +3921,13 @@ var FsArtifactStore = class {
6099
3921
  metadata: input.metadata
6100
3922
  };
6101
3923
  const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
6102
- await atomicWriteText(this.#fs, `${this.#root}/${input.runId}/${INDEX_FILE$1}`, JSON.stringify({ artifacts: next }, null, 2));
3924
+ await atomicWriteText(this.#fs, `${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
6103
3925
  return artifact;
6104
3926
  }
6105
3927
  };
3928
+
3929
+ //#endregion
3930
+ //#region ../runtime/src/sandbox/bridgeProtocol.ts
6106
3931
  const isRecord = (v) => typeof v === "object" && v !== null;
6107
3932
  const isNonEmptyString = (v) => typeof v === "string" && v !== "";
6108
3933
  /** 入站校验:非法 kind / 缺 id / 缺字段 → undefined(调用方忽略并记 warning) */
@@ -6136,6 +3961,15 @@ function parseBridgeRequest(data) {
6136
3961
  id,
6137
3962
  message: data["message"]
6138
3963
  } : void 0;
3964
+ case "listUploadFiles": return {
3965
+ kind,
3966
+ id
3967
+ };
3968
+ case "readUploadFile": return isNonEmptyString(data["fileId"]) ? {
3969
+ kind,
3970
+ id,
3971
+ fileId: data["fileId"]
3972
+ } : void 0;
6139
3973
  case "fetchData": {
6140
3974
  if (!isNonEmptyString(data["sourceId"])) return void 0;
6141
3975
  const params = data["params"];
@@ -6160,6 +3994,14 @@ function bridgeError(id, code, message) {
6160
3994
  };
6161
3995
  }
6162
3996
  /**
3997
+ * 上传文件能力缺席时四个执行器共用的说辞(分册 17 FR-17.6):
3998
+ * 讲清「宿主没接 / 开关没开」,与「用户拒了这一次」(TOOL_DENIED)分得开。
3999
+ */
4000
+ const UPLOAD_FILES_UNAVAILABLE = "Capability \"uploadFiles\" is unavailable: this host did not wire access to the files you uploaded, or the sandbox setting for it is off. This is not the same as the read being declined.";
4001
+
4002
+ //#endregion
4003
+ //#region ../runtime/src/sandbox/networkPolicy.ts
4004
+ /**
6163
4005
  * 判定 URL 是否被策略放行:
6164
4006
  * - 'deny-all'(默认)全拒;'allow-all' 全放
6165
4007
  * - { allow } 条目支持三种写法:
@@ -6220,6 +4062,9 @@ function networkUrlHost(url) {
6220
4062
  function networkPolicyLibSource() {
6221
4063
  return `var isNetworkAllowed = ${isNetworkAllowed.toString()};\nvar networkUrlHost = ${networkUrlHost.toString()};`;
6222
4064
  }
4065
+
4066
+ //#endregion
4067
+ //#region ../runtime/src/sandbox/errorCodes.ts
6223
4068
  /** WebSkillErrorCode 全量白名单(错误码归一用;与 core errors.ts 保持同步) */
6224
4069
  const WHITELIST = /* @__PURE__ */ new Set([
6225
4070
  "FS_NOT_FOUND",
@@ -6281,6 +4126,9 @@ function normalizeToolError(code, message) {
6281
4126
  message: `${message} [original code: ${String(code)}]`
6282
4127
  };
6283
4128
  }
4129
+
4130
+ //#endregion
4131
+ //#region ../runtime/src/sandbox/approval.ts
6284
4132
  /**
6285
4133
  * Per-capability 强制授权判定(宿主侧,browser/node 执行器共用单一来源)。
6286
4134
  * 'require-approval' 模式下经注入的 UiBridge 发 authorize 交互;
@@ -6331,551 +4179,6 @@ var CapabilityApproval = class CapabilityApproval {
6331
4179
  return "allowed";
6332
4180
  }
6333
4181
  };
6334
- /**
6335
- * 从 run 的 trace 聚合大模型用量(请求次数 + 输入/输出 token)。
6336
- * 与 `summarizeToolCalls` 同型:消费方不再各自遍历 trace。
6337
- * @stable
6338
- */
6339
- function summarizeRunUsage(trace) {
6340
- let llmCalls = 0;
6341
- let inputTokens = 0;
6342
- let outputTokens = 0;
6343
- let hasUsage = false;
6344
- for (const event of trace) {
6345
- if (event.type === "llm.request") {
6346
- llmCalls += 1;
6347
- continue;
6348
- }
6349
- if (event.type !== "llm.usage") continue;
6350
- const input = event.data?.["inputTokens"];
6351
- const output = event.data?.["outputTokens"];
6352
- const hasInput = typeof input === "number" && Number.isFinite(input);
6353
- const hasOutput = typeof output === "number" && Number.isFinite(output);
6354
- if (hasInput) inputTokens += input;
6355
- if (hasOutput) outputTokens += output;
6356
- if (hasInput || hasOutput) hasUsage = true;
6357
- }
6358
- return {
6359
- llmCalls,
6360
- ...hasUsage ? {
6361
- inputTokens,
6362
- outputTokens
6363
- } : {}
6364
- };
6365
- }
6366
- const RUN_TRACE_SCHEMA_VERSION = 1;
6367
- /** 终止原因:取最后一条 run.completed/cancelled/failed 事件的 data.reason */
6368
- function extractEndReason(events) {
6369
- for (let i = events.length - 1; i >= 0; i--) {
6370
- const event = events[i];
6371
- if (event.type === "run.completed" || event.type === "run.cancelled" || event.type === "run.failed") {
6372
- const reason = event.data?.["reason"];
6373
- return typeof reason === "string" ? reason : void 0;
6374
- }
6375
- }
6376
- }
6377
- function summarize(trace) {
6378
- const endReason = extractEndReason(trace.events);
6379
- const durationMs = trace.endedAt === void 0 ? void 0 : Date.parse(trace.endedAt) - Date.parse(trace.startedAt);
6380
- const runUsage = summarizeRunUsage(trace.events);
6381
- return {
6382
- runId: trace.runId,
6383
- startedAt: trace.startedAt,
6384
- status: trace.status,
6385
- activeSkills: trace.activeSkills,
6386
- eventCount: trace.events.length,
6387
- turnCount: trace.events.filter((e) => e.type === "llm.request").length,
6388
- ...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
6389
- ...trace.endedAt !== void 0 ? { endedAt: trace.endedAt } : {},
6390
- ...endReason !== void 0 ? { endReason } : {},
6391
- ...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {},
6392
- ...runUsage.inputTokens !== void 0 ? { usage: {
6393
- inputTokens: runUsage.inputTokens,
6394
- outputTokens: runUsage.outputTokens ?? 0
6395
- } } : {}
6396
- };
6397
- }
6398
- function parseTraceFile(raw, path) {
6399
- let data;
6400
- try {
6401
- data = JSON.parse(raw);
6402
- } catch {
6403
- return;
6404
- }
6405
- if (typeof data !== "object" || data === null) return void 0;
6406
- if (typeof data.runId !== "string" || typeof data.startedAt !== "string") return void 0;
6407
- const schemaVersion = typeof data.schemaVersion === "number" ? data.schemaVersion : 0;
6408
- if (schemaVersion > 1) throw new WebSkillError("RUN_TRACE_INCOMPATIBLE", `Run trace "${path}" declares schemaVersion ${schemaVersion}, which is newer than the supported 1`);
6409
- return {
6410
- schemaVersion,
6411
- runId: data.runId,
6412
- ...typeof data.sessionId === "string" ? { sessionId: data.sessionId } : {},
6413
- startedAt: data.startedAt,
6414
- ...typeof data.endedAt === "string" ? { endedAt: data.endedAt } : {},
6415
- status: typeof data.status === "string" ? data.status : "unknown",
6416
- activeSkills: Array.isArray(data.activeSkills) ? data.activeSkills.filter((s) => typeof s === "string") : [],
6417
- events: Array.isArray(data.events) ? data.events : []
6418
- };
6419
- }
6420
- const INDEX_FILE = "index.jsonl";
6421
- /**
6422
- * `FileSystemProvider` 后端:`<root>/<runId>.json` 存全量 trace,
6423
- * `<root>/index.jsonl` 是 append-only 的摘要旁路索引。
6424
- *
6425
- * 索引存在的唯一理由是**读次数**:0.0.1 的实现为了拼一份列表要整读每个 trace
6426
- * 文件,点一次指标再来一遍。有了索引,`list()` 与 `metrics()` 各只读一个文件。
6427
- *
6428
- * 索引与 trace 目录发散(写完 trace 崩在追加前)的检测不引入新的读放大:
6429
- * 目录列举本来就要做,比较 `*.json` 个数与索引行数即可;不等则整体重建。
6430
- * @stable
6431
- */
6432
- var FsRunTraceStore = class {
6433
- #root;
6434
- #fs;
6435
- #onError;
6436
- #onIndexError;
6437
- constructor(deps) {
6438
- this.#root = deps.root.replace(/\/+$/, "");
6439
- this.#fs = deps.fs;
6440
- this.#onError = deps.onError ?? ((error, run) => {
6441
- console.warn(`Failed to persist run trace "${run.id}": ${messageOf(error)}`);
6442
- });
6443
- this.#onIndexError = deps.onIndexError ?? ((error, run) => {
6444
- console.warn(`Run trace "${run.id}" was saved but its index entry could not be appended: ${messageOf(error)}. The index will be rebuilt on the next read.`);
6445
- });
6446
- }
6447
- #path(runId) {
6448
- return resolveInsideRoot(this.#root, `${runId}.json`);
6449
- }
6450
- async put(run) {
6451
- const trace = {
6452
- schemaVersion: 1,
6453
- runId: run.id,
6454
- sessionId: run.sessionId,
6455
- startedAt: run.startedAt,
6456
- ...run.endedAt !== void 0 ? { endedAt: run.endedAt } : {},
6457
- status: run.status === "completed" || run.status === "cancelled" ? run.status : "failed",
6458
- activeSkills: run.activeSkillNames,
6459
- events: run.trace
6460
- };
6461
- try {
6462
- await atomicWriteText(this.#fs, this.#path(run.id), JSON.stringify(trace, null, 2));
6463
- } catch (e) {
6464
- this.#onError(e, run);
6465
- return;
6466
- }
6467
- try {
6468
- await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
6469
- } catch (e) {
6470
- this.#onIndexError(e, run);
6471
- }
6472
- }
6473
- async get(runId) {
6474
- assertSafePathSegment(runId, "run id");
6475
- const path = this.#path(runId);
6476
- let raw;
6477
- try {
6478
- raw = await this.#fs.readText(path);
6479
- } catch (e) {
6480
- if (e instanceof WebSkillError) throw e;
6481
- throw new WebSkillError("FS_NOT_FOUND", `Trace not found for run ${JSON.stringify(runId)}: ${path}`);
6482
- }
6483
- const trace = parseTraceFile(raw, path);
6484
- if (!trace) throw new WebSkillError("VALIDATION_FAILED", `Trace file is corrupted or malformed: ${path}`);
6485
- return trace;
6486
- }
6487
- async list(filter = {}) {
6488
- const { summaries } = await this.#readIndex();
6489
- const matched = applyFilter(summaries, filter);
6490
- const offset = Math.max(0, filter.offset ?? 0);
6491
- const sliced = matched.slice(offset);
6492
- return filter.limit !== void 0 ? sliced.slice(0, Math.max(0, filter.limit)) : sliced;
6493
- }
6494
- async metrics(filter = {}) {
6495
- const { summaries, skipped } = await this.#readIndex();
6496
- const runs = applyFilter(summaries, filter);
6497
- const succeeded = runs.filter((r) => r.status === "completed").length;
6498
- const failedRuns = runs.filter((r) => r.status === "failed");
6499
- const durations = runs.map((r) => r.durationMs).filter((ms) => ms !== void 0);
6500
- return {
6501
- totalRuns: runs.length,
6502
- succeeded,
6503
- failed: failedRuns.length,
6504
- successRate: runs.length === 0 ? 0 : succeeded / runs.length,
6505
- avgTurns: runs.length === 0 ? 0 : runs.reduce((sum, r) => sum + r.turnCount, 0) / runs.length,
6506
- skippedRuns: skipped,
6507
- avgDurationMs: durations.length === 0 ? 0 : durations.reduce((a, b) => a + b, 0) / durations.length,
6508
- recentFailures: failedRuns.slice(0, 5)
6509
- };
6510
- }
6511
- /** 健康路径恒为「一次目录列举 + 一次索引读」,与 run 数无关。 */
6512
- async #readIndex() {
6513
- if (!await this.#fs.exists(this.#root)) return {
6514
- summaries: [],
6515
- skipped: 0
6516
- };
6517
- const traceCount = (await this.#fs.list(this.#root)).filter((f) => f.type === "file" && f.path.endsWith(".json")).length;
6518
- const indexPath = `${this.#root}/${INDEX_FILE}`;
6519
- let lines = [];
6520
- if (await this.#fs.exists(indexPath)) lines = (await this.#fs.readText(indexPath)).split("\n").filter((line) => line.trim() !== "");
6521
- if (lines.length !== traceCount) return {
6522
- summaries: await this.#rebuildIndex(),
6523
- skipped: 0
6524
- };
6525
- const summaries = [];
6526
- let skipped = 0;
6527
- for (const line of lines) try {
6528
- summaries.push(JSON.parse(line));
6529
- } catch {
6530
- skipped += 1;
6531
- }
6532
- if (skipped > 0) return {
6533
- summaries: await this.#rebuildIndex(),
6534
- skipped
6535
- };
6536
- return {
6537
- summaries: sortByStartedAtDesc(summaries),
6538
- skipped: 0
6539
- };
6540
- }
6541
- /** 异常路径,允许 O(N):整读全部 trace 文件后用 temp + rename 原子替换索引 */
6542
- async #rebuildIndex() {
6543
- const summaries = [];
6544
- for (const entry of await this.#fs.list(this.#root)) {
6545
- if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
6546
- let trace;
6547
- try {
6548
- trace = parseTraceFile(await this.#fs.readText(entry.path), entry.path);
6549
- } catch {
6550
- continue;
6551
- }
6552
- if (!trace) continue;
6553
- summaries.push(summarize(trace));
6554
- }
6555
- const sorted = sortByStartedAtDesc(summaries);
6556
- const indexPath = `${this.#root}/${INDEX_FILE}`;
6557
- const tempPath = `${this.#root}/${INDEX_FILE}.rebuilding`;
6558
- try {
6559
- await this.#fs.writeText(tempPath, sorted.map((s) => `${JSON.stringify(s)}\n`).join(""));
6560
- await this.#fs.rename(tempPath, indexPath);
6561
- } catch {
6562
- await this.#fs.remove(tempPath).catch(() => void 0);
6563
- }
6564
- return sorted;
6565
- }
6566
- };
6567
- /** ISO 时间戳可按字典序比较 */
6568
- function sortByStartedAtDesc(summaries) {
6569
- return [...summaries].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
6570
- }
6571
- function applyFilter(summaries, filter) {
6572
- let runs = summaries;
6573
- if (filter.status !== void 0 && filter.status !== "") runs = runs.filter((r) => r.status === filter.status);
6574
- if (filter.sessionId !== void 0 && filter.sessionId !== "") runs = runs.filter((r) => r.sessionId === filter.sessionId);
6575
- const q = filter.search?.trim().toLowerCase();
6576
- if (q !== void 0 && q !== "") runs = runs.filter((r) => r.runId.toLowerCase().includes(q) || r.activeSkills.some((s) => s.toLowerCase().includes(q)));
6577
- return runs;
6578
- }
6579
- /**
6580
- * 从 run 的 trace 推导终态工具调用列表。
6581
- *
6582
- * 消费者过去要靠自己遍历 trace 才能拿到这份列表,而遍历 trace 的同一段代码
6583
- * 又顺手承担了「补发漏掉的 live 事件」的职责——两件事绑在一起,谁也删不掉。
6584
- * 幂等归 runtime(见 `AgentLoop` 的 per-run 已发集合)之后,
6585
- * 推导终态列表就是纯函数,独立导出。
6586
- * @stable
6587
- */
6588
- function summarizeToolCalls(run) {
6589
- const startedAtByCallId = /* @__PURE__ */ new Map();
6590
- for (const event of run.trace) {
6591
- if (event.type !== "tool.started") continue;
6592
- const callId = event.data?.["callId"];
6593
- if (typeof callId === "string" && !startedAtByCallId.has(callId)) startedAtByCallId.set(callId, event.ts);
6594
- }
6595
- const calls = [];
6596
- for (const event of run.trace) {
6597
- if (event.type !== "tool.completed" && event.type !== "tool.failed") continue;
6598
- const name = event.data?.["name"];
6599
- const callId = event.data?.["callId"];
6600
- if (typeof name !== "string" || typeof callId !== "string") continue;
6601
- const args = event.data?.["args"];
6602
- const durationMs = event.data?.["durationMs"];
6603
- const errorCode = event.data?.["code"];
6604
- const startedAt = startedAtByCallId.get(callId);
6605
- calls.push({
6606
- callId,
6607
- name,
6608
- status: event.type === "tool.completed" ? "completed" : "failed",
6609
- ...typeof args === "string" ? { args } : {},
6610
- ...typeof durationMs === "number" ? { durationMs } : {},
6611
- ...typeof errorCode === "string" ? { errorCode } : {},
6612
- ...startedAt !== void 0 ? { startedAt } : {},
6613
- ...event.type === "tool.failed" && typeof event.message === "string" ? { errorMessage: event.message } : {}
6614
- });
6615
- }
6616
- return calls;
6617
- }
6618
- const SESSION_SCHEMA_VERSION = 1;
6619
- /** `FsSessionStore` 的缺省页长。缺省值属于实现,不属于调用方——否则「下推」只推了一半 */
6620
- const FS_SESSION_PAGE_SIZE = 50;
6621
- const toMeta = (record) => ({
6622
- id: record.id,
6623
- createdAt: record.createdAt,
6624
- ...record.title !== void 0 ? { title: record.title } : {},
6625
- ...record.titleLocked === true ? { titleLocked: true } : {},
6626
- ...record.archived === true ? { archived: true } : {},
6627
- ...record.modelId !== void 0 ? { modelId: record.modelId } : {},
6628
- messageCount: record.messages.length
6629
- });
6630
- const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
6631
- const baseName = (path) => path.split("/").pop() ?? path;
6632
- /**
6633
- * 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
6634
- * 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
6635
- * 但它是不透明串,调用方不得自己构造(构造出来的越界值一律按 `VALIDATION_FAILED` 拒绝)。
6636
- */
6637
- function takeTailPage(all, options, what) {
6638
- const limit = options.limit ?? 50;
6639
- if (!Number.isInteger(limit) || limit <= 0) throw new WebSkillError("VALIDATION_FAILED", `Page limit must be a positive integer, received ${String(limit)}`);
6640
- let end = all.length;
6641
- if (options.cursor !== void 0) {
6642
- end = Number(options.cursor);
6643
- if (!Number.isInteger(end) || end < 0 || end > all.length) throw new WebSkillError("VALIDATION_FAILED", `Invalid ${what} cursor: ${JSON.stringify(options.cursor)}`);
6644
- }
6645
- const start = Math.max(0, end - limit);
6646
- return {
6647
- items: all.slice(start, end),
6648
- ...start > 0 ? { nextCursor: String(start) } : {}
6649
- };
6650
- }
6651
- /**
6652
- * 解析会话文件。缺 `schemaVersion` 视为 0(0.0.1 时代文件,兼容读);
6653
- * 高于当前版本拒绝读,避免新版写的字段被旧版静默丢弃。
6654
- */
6655
- function parseSessionFile(raw, path) {
6656
- let parsed;
6657
- try {
6658
- parsed = JSON.parse(raw);
6659
- } catch (e) {
6660
- throw new WebSkillError("VALIDATION_FAILED", `Session file is not valid JSON: ${path}`, e);
6661
- }
6662
- if (typeof parsed !== "object" || parsed === null) throw new WebSkillError("VALIDATION_FAILED", `Session file is not an object: ${path}`);
6663
- const file = parsed;
6664
- const schemaVersion = typeof file.schemaVersion === "number" ? file.schemaVersion : 0;
6665
- if (schemaVersion > 1) throw new WebSkillError("SESSION_INCOMPATIBLE", `Session file at ${path} has schemaVersion ${schemaVersion}, but this build understands at most 1`);
6666
- if (typeof file.id !== "string" || typeof file.createdAt !== "string") throw new WebSkillError("VALIDATION_FAILED", `Session file is missing "id" or "createdAt": ${path}`);
6667
- return {
6668
- schemaVersion,
6669
- id: file.id,
6670
- createdAt: file.createdAt,
6671
- ...typeof file.title === "string" ? { title: file.title } : {},
6672
- ...file.titleLocked === true ? { titleLocked: true } : {},
6673
- ...file.archived === true ? { archived: true } : {},
6674
- ...typeof file.modelId === "string" ? { modelId: file.modelId } : {},
6675
- messages: Array.isArray(file.messages) ? file.messages : [],
6676
- messageCount: Array.isArray(file.messages) ? file.messages.length : 0
6677
- };
6678
- }
6679
- /**
6680
- * `FileSystemProvider` 后端的会话存储:`<root>/<id>.json`。
6681
- *
6682
- * 所有变更操作按 id 串到一条 promise 链上:会话文件是整读整写的,
6683
- * 两个并发 `appendMessages` 若都先读后写,后写的会覆盖先写的那条消息。
6684
- */
6685
- var FsSessionStore = class {
6686
- #root;
6687
- #fs;
6688
- /** id → 该 id 上最后一次变更的完成时点,用于串行化读改写 */
6689
- #writes = /* @__PURE__ */ new Map();
6690
- /** FR-11.4:损坏文件诊断只报一次,避免 list 轮询刷屏 */
6691
- #warnedDataLoss = false;
6692
- constructor(deps) {
6693
- this.#root = deps.root.replace(/\/+$/, "");
6694
- this.#fs = deps.fs;
6695
- }
6696
- #path(id) {
6697
- assertSafePathSegment(id, "session id");
6698
- return resolveInsideRoot(this.#root, `${id}.json`);
6699
- }
6700
- /** 把 mutation 排到该 id 的队尾;前一个失败不阻塞后一个 */
6701
- async #serialize(id, work) {
6702
- const next = (this.#writes.get(id) ?? Promise.resolve()).catch(() => void 0).then(work);
6703
- this.#writes.set(id, next);
6704
- try {
6705
- return await next;
6706
- } finally {
6707
- if (this.#writes.get(id) === next) this.#writes.delete(id);
6708
- }
6709
- }
6710
- async #write(record) {
6711
- const file = {
6712
- schemaVersion: 1,
6713
- id: record.id,
6714
- createdAt: record.createdAt,
6715
- ...record.title !== void 0 ? { title: record.title } : {},
6716
- ...record.titleLocked === true ? { titleLocked: true } : {},
6717
- ...record.archived === true ? { archived: true } : {},
6718
- ...record.modelId !== void 0 ? { modelId: record.modelId } : {},
6719
- messages: record.messages
6720
- };
6721
- await atomicWriteText(this.#fs, this.#path(record.id), JSON.stringify(file, null, 2));
6722
- }
6723
- async #require(id) {
6724
- const record = await this.get(id);
6725
- if (!record) throw new WebSkillError("FS_NOT_FOUND", `Session "${id}" not found under ${this.#root}`);
6726
- return record;
6727
- }
6728
- async list(options = {}) {
6729
- if (!await this.#fs.exists(this.#root)) return {
6730
- items: [],
6731
- source: "missing-root",
6732
- skipped: []
6733
- };
6734
- const metas = [];
6735
- const skipped = [];
6736
- for (const entry of await this.#fs.list(this.#root)) {
6737
- if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
6738
- let record;
6739
- try {
6740
- record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
6741
- } catch {
6742
- skipped.push(baseName(entry.path));
6743
- continue;
6744
- }
6745
- if (record.archived === true && options.includeArchived !== true) continue;
6746
- metas.push(toMeta(record));
6747
- }
6748
- if (skipped.length > 0 && !this.#warnedDataLoss) {
6749
- this.#warnedDataLoss = true;
6750
- console.warn(`[webskill] ${skipped.length} session file(s) under ${this.#root} are unreadable (possible data loss from an interrupted write): ${skipped.join(", ")}`);
6751
- }
6752
- metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
6753
- return {
6754
- ...takeTailPage(metas, options, "session"),
6755
- source: "ok",
6756
- skipped
6757
- };
6758
- }
6759
- /**
6760
- * 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
6761
- * 但**跨出端口的记录数**已是常量——这正是分页对上层的意义。
6762
- */
6763
- async listMessages(id, options = {}) {
6764
- return takeTailPage((await this.#require(id)).messages, options, "message");
6765
- }
6766
- async get(id) {
6767
- const path = this.#path(id);
6768
- if (!await this.#fs.exists(path)) return void 0;
6769
- return parseSessionFile(await this.#fs.readText(path), path);
6770
- }
6771
- async create(init = {}) {
6772
- const record = {
6773
- schemaVersion: 1,
6774
- id: init.id ?? newSessionId(),
6775
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6776
- ...init.title !== void 0 ? { title: init.title } : {},
6777
- messages: [],
6778
- messageCount: 0
6779
- };
6780
- return this.#serialize(record.id, async () => {
6781
- await this.#write(record);
6782
- return toMeta(record);
6783
- });
6784
- }
6785
- async appendMessages(id, messages) {
6786
- if (messages.length === 0) return;
6787
- await this.#serialize(id, async () => {
6788
- const record = await this.#require(id);
6789
- record.messages.push(...messages);
6790
- await this.#write(record);
6791
- });
6792
- }
6793
- async replaceMessages(id, messages) {
6794
- await this.#serialize(id, async () => {
6795
- const record = await this.#require(id);
6796
- record.messages = [...messages];
6797
- await this.#write(record);
6798
- });
6799
- }
6800
- async setTitle(id, title, options = {}) {
6801
- await this.#serialize(id, async () => {
6802
- const record = await this.#require(id);
6803
- record.title = title;
6804
- if (options.lock === true) record.titleLocked = true;
6805
- await this.#write(record);
6806
- });
6807
- }
6808
- async setArchived(id, archived) {
6809
- await this.#serialize(id, async () => {
6810
- const record = await this.#require(id);
6811
- record.archived = archived;
6812
- await this.#write(record);
6813
- });
6814
- }
6815
- async setModel(id, modelId) {
6816
- await this.#serialize(id, async () => {
6817
- const record = await this.#require(id);
6818
- if (modelId === void 0) delete record.modelId;
6819
- else record.modelId = modelId;
6820
- await this.#write(record);
6821
- });
6822
- }
6823
- async getMeta(id) {
6824
- const record = await this.get(id);
6825
- return record === void 0 ? void 0 : toMeta(record);
6826
- }
6827
- async delete(id) {
6828
- await this.#serialize(id, async () => {
6829
- const path = this.#path(id);
6830
- if (await this.#fs.exists(path)) await this.#fs.remove(path);
6831
- });
6832
- }
6833
- };
6834
- /**
6835
- * 按会话分文件的 append-only JSONL 留存。
6836
- *
6837
- * 分文件维度取会话而非 run:读取侧唯一的查询就是「这个会话的全部步骤」,
6838
- * 按 run 分会让一次生成要开 N 个文件。
6839
- */
6840
- var FsToolStepStore = class {
6841
- #root;
6842
- #fs;
6843
- #onError;
6844
- constructor(deps) {
6845
- this.#root = deps.root.replace(/\/+$/, "");
6846
- this.#fs = deps.fs;
6847
- this.#onError = deps.onError ?? ((error, record) => {
6848
- console.warn(`Failed to persist tool step for call "${record.callId}": ${messageOf(error)}`);
6849
- });
6850
- }
6851
- #path(sessionId) {
6852
- assertSafePathSegment(sessionId, "session id");
6853
- return resolveInsideRoot(this.#root, `${sessionId}.jsonl`);
6854
- }
6855
- async append(record) {
6856
- const path = this.#path(record.sessionId);
6857
- try {
6858
- await this.#fs.appendText(path, `${JSON.stringify(record)}\n`);
6859
- } catch (e) {
6860
- this.#onError(e, record);
6861
- }
6862
- }
6863
- async listBySession(sessionId) {
6864
- const path = this.#path(sessionId);
6865
- if (!await this.#fs.exists(path)) return [];
6866
- const raw = await this.#fs.readText(path);
6867
- const records = [];
6868
- for (const line of raw.split("\n")) {
6869
- if (line.trim() === "") continue;
6870
- try {
6871
- records.push(JSON.parse(line));
6872
- } catch {
6873
- continue;
6874
- }
6875
- }
6876
- return records;
6877
- }
6878
- };
6879
4182
 
6880
4183
  //#endregion
6881
- export { bridgeError as $, ProgressiveRouter as A, refineUserProfile as At, SUPPORTED_DOCUMENT_MIME as B, toBase64 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, validateUiSpecNode as Gt, TEXT_BUDGETED_CONTENT_TYPES as H, toRecordDigests 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, toVercelToolSpecs as Ut, SerializingMemoryStore as V, toLlmToolSpec as Vt, UNSUPPORTED_DOCUMENT_MESSAGE as W, validateUiSpecEvent 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 };
4184
+ export { scriptToolName as $, readUserProfile as A, createScriptContext as B, TraceRecorder as C, mergeProfileEntries as D, appendBehaviorRecords as E, DEFAULT_USER_PROFILE_LIMITS as F, ASK_USER_TOOL_NAME as G, ASK_USER_INPUT_SCHEMA as H, EMPTY_USER_PROFILE as I, READ_SKILL_FILE_TOOL_NAME as J, READ_SKILL_FILE_INPUT_SCHEMA as K, SerializingMemoryStore as L, USER_PROFILE_NO_INVENTION_RULE as M, USER_PROFILE_PROMPT_HEADER as N, readBehaviorRecords as O, renderUserProfileContext as P, schemaSourceLabel as Q, EventBus as R, extractTodoTraceEvents as S, USER_PROFILE_KEY as T, ASK_USER_MAX_FIELDS as U, ASK_USER_FIELD_TYPES as V, ASK_USER_TOOL as W, formatSkillScriptManifest as X, ALLOWED_TOOLS_EXCLUSION_REASON as Y, listSkillScripts as Z, RUN_SNAPSHOT_SCHEMA_VERSION as _, networkPolicyLibSource as a, xmlRenderer as at, MAX_TOOL_STEP_ARG_BYTES as b, bridgeError as c, FsMemoryStore as d, resolveToolName as et, WebSkillRuntime as f, FsRunSnapshotStore as g, redactToolStepArgs as h, isNetworkAllowed as i, renderAvailableSkillsXml as it, sampleBehaviorRecords as j, readProfileEntries as k, parseBridgeRequest as l, SENSITIVE_ANNOTATION as m, normalizeErrorCode as n, ProgressiveRouter as nt, networkUrlHost as o, AgentLoop as p, READ_SKILL_FILE_TOOL as q, normalizeToolError as r, escapeXml as rt, UPLOAD_FILES_UNAVAILABLE as s, CapabilityApproval as t, toLlmToolSpec as tt, FsArtifactStore as u, isUnsupportedRunSnapshot as v, BEHAVIOR_RECORDS_KEY as w, extractSkillCandidate as x, DEFAULT_LOOP_LIMITS as y, schemaToForm as z };