@webskill/sdk 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +274 -42
- package/dist/browser.d.ts +127 -5
- package/dist/browser.js +611 -17
- package/dist/{catalogComponents-DV7cPpUm-C77AEEx9.js → catalogComponents-Dr5dFMAb-DKH_7VPI.js} +848 -974
- package/dist/{dist-6C03DShK.js → dist-D0qW6e40.js} +1213 -210
- package/dist/{dist-bewtXYlO.js → dist-DnYG2-eY.js} +53 -14
- package/dist/{env--jJB-TSX-04klhTYi.js → env-8cY40DXB-CGnEVZby.js} +7 -6
- package/dist/{env-BPUBZCwJ-4jat_SVG.d.ts → env-AK3cSMEA-Dli6QU5E.d.ts} +4 -3
- package/dist/eventTypes-DjIQpt8Y-Bj3vghj4.js +32 -0
- package/dist/governance.d.ts +78 -11
- package/dist/governance.js +173 -50
- package/dist/{index-D_7ZZjkl.d.ts → index-Ba3xFtfz.d.ts} +222 -8
- package/dist/{index-Bsqg4ftU.d.ts → index-BwsK9lGk.d.ts} +13 -7
- package/dist/{index-vBz_FC9w.d.ts → index-DkbABR43.d.ts} +336 -52
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +159 -11
- package/dist/mcp.js +276 -45
- package/dist/{memoryArtifactStore-BtOeB_hm-tj3fC5ip.js → memoryArtifactStore-52Zn9npI-BMPYwvoy.js} +10 -2
- package/dist/node.d.ts +4 -4
- package/dist/node.js +11 -3
- package/dist/{openUiLibrary-W3Ce896k-ClFTRZFs.js → openUiLibrary-Bdrji9qK-D2LxmM-a.js} +3 -3
- package/dist/{skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts → skillVersionStore-Bl-ElD45-CWPvGvoq.d.ts} +8 -2
- package/dist/{testing-DDCJWvgA.js → testing-CYTFqkDm.js} +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/dist/{types-D_hoCri8-BnNPiZCi.d.ts → types-CcxRLdJG-DCXyw1US.d.ts} +62 -10
- package/dist/ui-react.d.ts +5 -3
- package/dist/ui-react.js +72 -47
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +13 -8
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +2 -2
- package/dist/{webskillLitCatalog-_mugzRHx-DiuJpCuf.js → webskillLitCatalog-_mugzRHx-B_54vxum.js} +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,331 @@
|
|
|
1
1
|
import { A as parseSkillMarkdown, L as resolveInsideRoot, O as messageOf, P as renderAvailableSkillsXml, V as validateSkills, f as SkillDiscovery, g as assertSafePathSegment, m as WebSkillError, p as SkillReader, v as buildCatalog } from "./dist-8oQRa8Xz.js";
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as textParts, i as rejectUnsupportedPart, n as partsToText, o as validateLlmMessages, r as promptText, t as MemoryArtifactStore } from "./memoryArtifactStore-52Zn9npI-BMPYwvoy.js";
|
|
3
3
|
|
|
4
4
|
//#region ../runtime/dist/index.js
|
|
5
|
+
/**
|
|
6
|
+
* $defs 必须与 type / properties 同级,位于每个工具 inputSchema 的根。
|
|
7
|
+
* 已验证:放进 properties.spec 内层会得到同样的 "Unsupported ref: #" ——
|
|
8
|
+
* Qwen / LM Studio 按当前 schema 上下文解析 $defs,不回溯根文档。
|
|
9
|
+
* 因此本模块只在**根**写入 $defs,不得改成就近放置。
|
|
10
|
+
*/
|
|
11
|
+
const DEFS_KEY = "$defs";
|
|
12
|
+
const SELF_REF = "#";
|
|
13
|
+
const NODE_NAME = "Node";
|
|
14
|
+
const COMPONENT_PREFIX = "Component_";
|
|
15
|
+
/** 值为「名称 → 子 schema」的映射。包内导出:vendorSchema 要用同一套下钻规则(两套会漂移)。 */
|
|
16
|
+
const SCHEMA_MAP_KEYS = /* @__PURE__ */ new Set([
|
|
17
|
+
"properties",
|
|
18
|
+
"patternProperties",
|
|
19
|
+
"dependentSchemas",
|
|
20
|
+
"$defs",
|
|
21
|
+
"definitions"
|
|
22
|
+
]);
|
|
23
|
+
/** 值为「子 schema 数组」。 */
|
|
24
|
+
const SCHEMA_LIST_KEYS = /* @__PURE__ */ new Set([
|
|
25
|
+
"oneOf",
|
|
26
|
+
"anyOf",
|
|
27
|
+
"allOf",
|
|
28
|
+
"prefixItems"
|
|
29
|
+
]);
|
|
30
|
+
/** 值为单个子 schema(items 在 draft-07 遗留写法里也可能是数组)。 */
|
|
31
|
+
const SCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
32
|
+
"items",
|
|
33
|
+
"additionalItems",
|
|
34
|
+
"additionalProperties",
|
|
35
|
+
"unevaluatedItems",
|
|
36
|
+
"unevaluatedProperties",
|
|
37
|
+
"contains",
|
|
38
|
+
"propertyNames",
|
|
39
|
+
"not",
|
|
40
|
+
"if",
|
|
41
|
+
"then",
|
|
42
|
+
"else"
|
|
43
|
+
]);
|
|
44
|
+
function isRecord$6(value) {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
function isSelfRef(value) {
|
|
48
|
+
return isRecord$6(value) && value["$ref"] === SELF_REF;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 按 JSON Schema 关键字下降到子 schema。
|
|
52
|
+
* 只走 schema 位置,`enum` / `const` / `default` 等数据位置一律不进——
|
|
53
|
+
* 那里出现的 `{ $ref: '#' }` 是字面数据而非引用,改写它会破坏语义。
|
|
54
|
+
*/
|
|
55
|
+
function forEachSubSchema(node, visit) {
|
|
56
|
+
for (const [key, value] of Object.entries(node)) {
|
|
57
|
+
if (SCHEMA_MAP_KEYS.has(key)) {
|
|
58
|
+
if (isRecord$6(value)) for (const name of Object.keys(value)) visit(value[name], [key, name]);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (SCHEMA_LIST_KEYS.has(key)) {
|
|
62
|
+
if (Array.isArray(value)) value.forEach((item, index) => visit(item, [key, index]));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (SCHEMA_KEYS.has(key)) if (Array.isArray(value)) value.forEach((item, index) => visit(item, [key, index]));
|
|
66
|
+
else visit(value, [key]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function pathKey(path) {
|
|
70
|
+
return path.join("\0");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 找出所有 `$ref: '#'` 所属的 schema resource 根。
|
|
74
|
+
* `#` 解析到最近的带 `$id` 的祖先;没有则是文档根。
|
|
75
|
+
*/
|
|
76
|
+
function collectSelfRefRoots(node, path, resourceRoot, out) {
|
|
77
|
+
if (!isRecord$6(node)) return;
|
|
78
|
+
if (isSelfRef(node)) {
|
|
79
|
+
out.set(pathKey(resourceRoot), resourceRoot);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const nextRoot = typeof node["$id"] === "string" && path.length > 0 ? path : resourceRoot;
|
|
83
|
+
forEachSubSchema(node, (child, childPath) => {
|
|
84
|
+
collectSelfRefRoots(child, [...path, ...childPath], nextRoot, out);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function getAt(schema, path) {
|
|
88
|
+
let current = schema;
|
|
89
|
+
for (const segment of path) if (Array.isArray(current) && typeof segment === "number") current = current[segment];
|
|
90
|
+
else if (isRecord$6(current) && typeof segment === "string") current = current[segment];
|
|
91
|
+
else return void 0;
|
|
92
|
+
return current;
|
|
93
|
+
}
|
|
94
|
+
/** 返回把 `path` 处替换为 `replacement` 后的新对象;沿途节点浅拷贝,其余共享。 */
|
|
95
|
+
function setAt(schema, path, replacement) {
|
|
96
|
+
if (path.length === 0) return replacement;
|
|
97
|
+
const [head, ...rest] = path;
|
|
98
|
+
const child = getAt(schema, [head]);
|
|
99
|
+
const nextChild = rest.length === 0 ? replacement : setAt(child, rest, replacement);
|
|
100
|
+
if (Array.isArray(schema) && typeof head === "number") {
|
|
101
|
+
const copy = [...schema];
|
|
102
|
+
copy[head] = nextChild;
|
|
103
|
+
return copy;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
...schema,
|
|
107
|
+
[head]: nextChild
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 深拷贝并把 `{ $ref: '#' }` 改写为指向根 `$defs` 中的具名节点。
|
|
112
|
+
* 同时剥掉非根位置的 `$id` / `$schema`:它们会重设 base URI,
|
|
113
|
+
* 正是严格解析器无法解析 `#` 的直接原因。
|
|
114
|
+
*/
|
|
115
|
+
function rewriteSelfRefs(node, target, depth) {
|
|
116
|
+
if (!isRecord$6(node)) return node;
|
|
117
|
+
if (depth > 8) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool schema nesting exceeds 8 levels; cannot produce a portable form`);
|
|
118
|
+
if (isSelfRef(node)) {
|
|
119
|
+
const rest = {};
|
|
120
|
+
for (const [key, value] of Object.entries(node)) if (key !== "$ref") rest[key] = value;
|
|
121
|
+
return {
|
|
122
|
+
$ref: `#/${DEFS_KEY}/${target}`,
|
|
123
|
+
...rest
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const result = {};
|
|
127
|
+
for (const [key, value] of Object.entries(node)) {
|
|
128
|
+
if (key === "$id" || key === "$schema") continue;
|
|
129
|
+
if (SCHEMA_MAP_KEYS.has(key) && isRecord$6(value)) {
|
|
130
|
+
const mapped = {};
|
|
131
|
+
for (const [name, child] of Object.entries(value)) mapped[name] = rewriteSelfRefs(child, target, depth + 1);
|
|
132
|
+
result[key] = mapped;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (SCHEMA_LIST_KEYS.has(key) || SCHEMA_KEYS.has(key)) {
|
|
136
|
+
result[key] = Array.isArray(value) ? value.map((child) => rewriteSelfRefs(child, target, depth + 1)) : rewriteSelfRefs(value, target, depth + 1);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
result[key] = value;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
function uniqueName(base, taken) {
|
|
144
|
+
if (!taken.has(base)) return base;
|
|
145
|
+
const prefixed = `Portable${base}`;
|
|
146
|
+
if (!taken.has(prefixed)) return prefixed;
|
|
147
|
+
let index = 2;
|
|
148
|
+
while (taken.has(`${prefixed}_${index}`)) index += 1;
|
|
149
|
+
return `${prefixed}_${index}`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 把工具 inputSchema 重写为严格解析器可接受的 JSON Schema 2020-12 形态:
|
|
153
|
+
* 自引用的 schema resource 提到根 `$defs`,所有 `$ref: '#'` 改写为 `$ref: '#/$defs/<Node>'`。
|
|
154
|
+
*
|
|
155
|
+
* 幂等:不含自引用的 schema 原样返回。
|
|
156
|
+
*
|
|
157
|
+
* 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop,
|
|
158
|
+
* 否则 trace / eventBus 观测到的 schema 会与技能作者写的不一致。
|
|
159
|
+
*/
|
|
160
|
+
function toPortableToolSchema(schema) {
|
|
161
|
+
const roots = /* @__PURE__ */ new Map();
|
|
162
|
+
collectSelfRefRoots(schema, [], [], roots);
|
|
163
|
+
if (roots.size === 0) return schema;
|
|
164
|
+
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");
|
|
165
|
+
const rootPath = [...roots.values()][0] ?? [];
|
|
166
|
+
const resource = getAt(schema, rootPath);
|
|
167
|
+
if (!isRecord$6(resource)) return schema;
|
|
168
|
+
const existingDefs = isRecord$6(schema[DEFS_KEY]) ? schema[DEFS_KEY] : void 0;
|
|
169
|
+
const taken = new Set(Object.keys(existingDefs ?? {}));
|
|
170
|
+
const nodeName = uniqueName(NODE_NAME, taken);
|
|
171
|
+
taken.add(nodeName);
|
|
172
|
+
const added = {};
|
|
173
|
+
const branches = resource["oneOf"];
|
|
174
|
+
if (Array.isArray(branches)) {
|
|
175
|
+
const refs = branches.map((branch, index) => {
|
|
176
|
+
const name = uniqueName(`${COMPONENT_PREFIX}${index}`, taken);
|
|
177
|
+
taken.add(name);
|
|
178
|
+
added[name] = rewriteSelfRefs(branch, nodeName, 1);
|
|
179
|
+
return { $ref: `#/${DEFS_KEY}/${name}` };
|
|
180
|
+
});
|
|
181
|
+
const description = resource["description"];
|
|
182
|
+
added[nodeName] = typeof description === "string" ? {
|
|
183
|
+
description,
|
|
184
|
+
oneOf: refs
|
|
185
|
+
} : { oneOf: refs };
|
|
186
|
+
} else {
|
|
187
|
+
const body = {};
|
|
188
|
+
for (const [key, value] of Object.entries(resource)) if (key !== DEFS_KEY) body[key] = value;
|
|
189
|
+
added[nodeName] = rewriteSelfRefs(body, nodeName, 1);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
...setAt(schema, rootPath, { $ref: `#/${DEFS_KEY}/${nodeName}` }),
|
|
193
|
+
[DEFS_KEY]: {
|
|
194
|
+
...existingDefs ?? {},
|
|
195
|
+
...added
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** 各家都认的基础关键字。Google `FunctionDeclaration.parameters` 是其中最窄的一家。 */
|
|
200
|
+
const BASE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
201
|
+
"type",
|
|
202
|
+
"description",
|
|
203
|
+
"title",
|
|
204
|
+
"default",
|
|
205
|
+
"enum",
|
|
206
|
+
"const",
|
|
207
|
+
"properties",
|
|
208
|
+
"required",
|
|
209
|
+
"items",
|
|
210
|
+
"anyOf",
|
|
211
|
+
"oneOf",
|
|
212
|
+
"minimum",
|
|
213
|
+
"maximum",
|
|
214
|
+
"minItems",
|
|
215
|
+
"maxItems",
|
|
216
|
+
"nullable",
|
|
217
|
+
"format"
|
|
218
|
+
]);
|
|
219
|
+
/** 宽松侧:保留 0.6.0 的透传行为,本版不主动收紧(没有实证之前收紧只会制造新的破坏) */
|
|
220
|
+
const PERMISSIVE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
221
|
+
...BASE_KEYWORDS,
|
|
222
|
+
"additionalProperties",
|
|
223
|
+
"patternProperties",
|
|
224
|
+
"dependentSchemas",
|
|
225
|
+
"definitions",
|
|
226
|
+
"$defs",
|
|
227
|
+
"$ref",
|
|
228
|
+
"$schema",
|
|
229
|
+
"$id",
|
|
230
|
+
"allOf",
|
|
231
|
+
"not",
|
|
232
|
+
"if",
|
|
233
|
+
"then",
|
|
234
|
+
"else",
|
|
235
|
+
"prefixItems",
|
|
236
|
+
"additionalItems",
|
|
237
|
+
"unevaluatedItems",
|
|
238
|
+
"unevaluatedProperties",
|
|
239
|
+
"contains",
|
|
240
|
+
"propertyNames",
|
|
241
|
+
"exclusiveMinimum",
|
|
242
|
+
"exclusiveMaximum",
|
|
243
|
+
"multipleOf",
|
|
244
|
+
"minLength",
|
|
245
|
+
"maxLength",
|
|
246
|
+
"pattern",
|
|
247
|
+
"uniqueItems",
|
|
248
|
+
"examples",
|
|
249
|
+
"readOnly",
|
|
250
|
+
"writeOnly",
|
|
251
|
+
"deprecated"
|
|
252
|
+
]);
|
|
253
|
+
const OPENAI_SCHEMA_PROFILE = {
|
|
254
|
+
id: "openai-compatible",
|
|
255
|
+
allowedKeywords: PERMISSIVE_KEYWORDS,
|
|
256
|
+
supportsRefs: true
|
|
257
|
+
};
|
|
258
|
+
const ANTHROPIC_SCHEMA_PROFILE = {
|
|
259
|
+
id: "anthropic",
|
|
260
|
+
allowedKeywords: PERMISSIVE_KEYWORDS,
|
|
261
|
+
supportsRefs: true
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Google Gen AI 的 `FunctionDeclaration.parameters` 收到 `additionalProperties`
|
|
265
|
+
* 直接回 400 `Unknown name "additionalProperties"`,且不解析 `$ref` / `$defs`。
|
|
266
|
+
*/
|
|
267
|
+
const GOOGLE_SCHEMA_PROFILE = {
|
|
268
|
+
id: "google",
|
|
269
|
+
allowedKeywords: BASE_KEYWORDS,
|
|
270
|
+
supportsRefs: false
|
|
271
|
+
};
|
|
272
|
+
function isRecord$5(value) {
|
|
273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
274
|
+
}
|
|
275
|
+
/** 只在 schema 位置上找 `$ref`:enum / const / default 里的同名键是字面数据,不是引用 */
|
|
276
|
+
function containsRef(node) {
|
|
277
|
+
if (Array.isArray(node)) return node.some(containsRef);
|
|
278
|
+
if (!isRecord$5(node)) return false;
|
|
279
|
+
if (typeof node["$ref"] === "string") return true;
|
|
280
|
+
for (const [key, value] of Object.entries(node)) {
|
|
281
|
+
if (SCHEMA_MAP_KEYS.has(key)) {
|
|
282
|
+
if (isRecord$5(value) && Object.values(value).some(containsRef)) return true;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (SCHEMA_LIST_KEYS.has(key)) {
|
|
286
|
+
if (Array.isArray(value) && value.some(containsRef)) return true;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (SCHEMA_KEYS.has(key) && containsRef(value)) return true;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
function sanitizeNode(node, profile) {
|
|
294
|
+
if (Array.isArray(node)) return node.map((item) => sanitizeNode(item, profile));
|
|
295
|
+
if (!isRecord$5(node)) return node;
|
|
296
|
+
const out = {};
|
|
297
|
+
for (const [key, value] of Object.entries(node)) {
|
|
298
|
+
if (!profile.allowedKeywords.has(key)) continue;
|
|
299
|
+
if (SCHEMA_MAP_KEYS.has(key) && isRecord$5(value)) {
|
|
300
|
+
const mapped = {};
|
|
301
|
+
for (const [name, child] of Object.entries(value)) mapped[name] = sanitizeNode(child, profile);
|
|
302
|
+
out[key] = mapped;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
306
|
+
out[key] = value.map((child) => sanitizeNode(child, profile));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (SCHEMA_KEYS.has(key)) {
|
|
310
|
+
out[key] = sanitizeNode(value, profile);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
out[key] = value;
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 按供应商 profile 清洗出站 schema。产出新对象,不修改入参(工具源可能复用同一个实例)。
|
|
319
|
+
*
|
|
320
|
+
* 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop,
|
|
321
|
+
* 否则 trace / eventBus 观测到的 schema 会与技能作者写的不一致。
|
|
322
|
+
*
|
|
323
|
+
* @throws WebSkillError `TOOL_SCHEMA_UNAVAILABLE` — schema 依赖该供应商不支持的引用
|
|
324
|
+
*/
|
|
325
|
+
function sanitizeForVendor(schema, profile, toolName) {
|
|
326
|
+
if (!profile.supportsRefs && containsRef(schema)) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool "${toolName}" requires $ref support that provider "${profile.id}" does not accept`);
|
|
327
|
+
return sanitizeNode(schema, profile);
|
|
328
|
+
}
|
|
5
329
|
function createSseFrameReader() {
|
|
6
330
|
let buffer = "";
|
|
7
331
|
let data = [];
|
|
@@ -113,7 +437,7 @@ const toOpenAiTools = (tools) => tools.map((tool) => ({
|
|
|
113
437
|
function: {
|
|
114
438
|
name: tool.name,
|
|
115
439
|
...tool.description ? { description: tool.description } : {},
|
|
116
|
-
parameters: tool.inputSchema
|
|
440
|
+
parameters: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), OPENAI_SCHEMA_PROFILE, tool.name)
|
|
117
441
|
}
|
|
118
442
|
}));
|
|
119
443
|
const errorMessage$2 = (e) => e instanceof Error ? e.message : String(e);
|
|
@@ -264,11 +588,18 @@ var OpenAiCompatibleClient = class {
|
|
|
264
588
|
const choice = data.choices?.[0]?.message;
|
|
265
589
|
if (!choice) throw new Error("response has no choices[0].message");
|
|
266
590
|
const toolCalls = choice["tool_calls"]?.map((tc) => {
|
|
267
|
-
|
|
591
|
+
let args = {};
|
|
592
|
+
let parseError;
|
|
593
|
+
try {
|
|
594
|
+
args = JSON.parse(tc.function?.arguments ?? "{}");
|
|
595
|
+
} catch (e) {
|
|
596
|
+
parseError = e instanceof Error ? e.message : String(e);
|
|
597
|
+
}
|
|
268
598
|
return {
|
|
269
599
|
id: tc.id,
|
|
270
600
|
name: tc.function?.name ?? "",
|
|
271
|
-
arguments:
|
|
601
|
+
arguments: args,
|
|
602
|
+
...parseError ? { argumentsParseError: parseError } : {}
|
|
272
603
|
};
|
|
273
604
|
});
|
|
274
605
|
const content = choice["content"];
|
|
@@ -363,7 +694,7 @@ function toAnthropicMessages(messages) {
|
|
|
363
694
|
const toAnthropicTools = (tools) => tools.map((tool) => ({
|
|
364
695
|
name: tool.name,
|
|
365
696
|
...tool.description ? { description: tool.description } : {},
|
|
366
|
-
input_schema: tool.inputSchema
|
|
697
|
+
input_schema: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), ANTHROPIC_SCHEMA_PROFILE, tool.name)
|
|
367
698
|
}));
|
|
368
699
|
/** Anthropic Messages API 客户端(零依赖 fetch;Node/浏览器通用) */
|
|
369
700
|
var AnthropicClient = class {
|
|
@@ -606,7 +937,7 @@ function toGenAiContents(messages) {
|
|
|
606
937
|
const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => ({
|
|
607
938
|
name: tool.name,
|
|
608
939
|
...tool.description ? { description: tool.description } : {},
|
|
609
|
-
parameters: tool.inputSchema
|
|
940
|
+
parameters: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), GOOGLE_SCHEMA_PROFILE, tool.name)
|
|
610
941
|
})) }];
|
|
611
942
|
/** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
|
|
612
943
|
var GoogleGenAiClient = class {
|
|
@@ -800,7 +1131,9 @@ Skills are disclosed progressively: the catalog below only shows metadata. To us
|
|
|
800
1131
|
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>".
|
|
801
1132
|
2. Call the activated script tools to do the work.
|
|
802
1133
|
3. If a tool call fails, read the error, adjust your approach and retry, or continue without it.
|
|
803
|
-
4. When the task is complete, answer in plain text without calling any tool
|
|
1134
|
+
4. When the task is complete, answer in plain text without calling any tool.
|
|
1135
|
+
|
|
1136
|
+
Never claim to have created, saved, or installed a skill unless a skill tool call actually succeeded.`;
|
|
804
1137
|
/** 渐进披露路由(默认):system prompt 只注入 Catalog 元数据 + 工具使用说明 */
|
|
805
1138
|
var ProgressiveRouter = class {
|
|
806
1139
|
async route(catalog) {
|
|
@@ -814,7 +1147,9 @@ var ProgressiveRouter = class {
|
|
|
814
1147
|
const INSTRUCTIONS = `You are an agent that completes tasks by using skills.
|
|
815
1148
|
The full instructions of every available skill are injected below. Follow them directly.
|
|
816
1149
|
Script tools are named "<skillName>__<scriptName>".
|
|
817
|
-
When the task is complete, answer in plain text without calling any tool
|
|
1150
|
+
When the task is complete, answer in plain text without calling any tool.
|
|
1151
|
+
|
|
1152
|
+
Never claim to have created, saved, or installed a skill unless a skill tool call actually succeeded.`;
|
|
818
1153
|
/** 全量披露路由(对照用):注入每个技能的完整 SKILL.md 正文 */
|
|
819
1154
|
var FullDisclosureRouter = class {
|
|
820
1155
|
#discovery;
|
|
@@ -841,7 +1176,7 @@ var FullDisclosureRouter = class {
|
|
|
841
1176
|
* 3. `mcp#` 前缀 → TOOL_UNSUPPORTED(WebMCP 阶段实现)
|
|
842
1177
|
* 4. 其余 → TOOL_NOT_FOUND
|
|
843
1178
|
*/
|
|
844
|
-
function resolveToolName(name, activatedSkills) {
|
|
1179
|
+
function resolveToolName(name, activatedSkills, activatedTools = []) {
|
|
845
1180
|
const sep = name.indexOf("__");
|
|
846
1181
|
if (sep > 0) {
|
|
847
1182
|
const skillName = name.slice(0, sep);
|
|
@@ -862,10 +1197,12 @@ function resolveToolName(name, activatedSkills) {
|
|
|
862
1197
|
code: "TOOL_UNSUPPORTED",
|
|
863
1198
|
message: `Tool "${name}" is a WebMCP tool, which is not supported yet`
|
|
864
1199
|
};
|
|
1200
|
+
const availableTools = [...activatedTools].sort();
|
|
865
1201
|
return {
|
|
866
1202
|
kind: "not-found",
|
|
867
1203
|
code: "TOOL_NOT_FOUND",
|
|
868
|
-
message: `Unknown tool "${name}".
|
|
1204
|
+
message: `Unknown tool "${name}".${availableTools.length > 0 ? ` Available tools right now: ${availableTools.join(", ")}.` : " No skill tools are activated yet; read the SKILL.md of the owning skill first."}`,
|
|
1205
|
+
data: { availableTools }
|
|
869
1206
|
};
|
|
870
1207
|
}
|
|
871
1208
|
/** ToolDefinition → LLM 工具 spec;缺 schema 时用宽松 object 兜底并在描述中标记 */
|
|
@@ -875,7 +1212,7 @@ function toLlmToolSpec(tool) {
|
|
|
875
1212
|
...tool.description ? { description: tool.description } : {},
|
|
876
1213
|
inputSchema: tool.inputSchema
|
|
877
1214
|
};
|
|
878
|
-
const note = "Input schema unavailable;
|
|
1215
|
+
const note = "Input schema unavailable for this script. Inspect the skill documentation before calling; do not assume it takes no arguments.";
|
|
879
1216
|
return {
|
|
880
1217
|
name: tool.name,
|
|
881
1218
|
description: tool.description ? `${tool.description} (${note})` : note,
|
|
@@ -885,10 +1222,57 @@ function toLlmToolSpec(tool) {
|
|
|
885
1222
|
}
|
|
886
1223
|
};
|
|
887
1224
|
}
|
|
1225
|
+
/** 工具名规则:技能**目录名**加双下划线加脚本名,与 frontmatter 的 name 无关 */
|
|
1226
|
+
function scriptToolName(skillName, scriptName) {
|
|
1227
|
+
return `${skillName}__${scriptName}`;
|
|
1228
|
+
}
|
|
1229
|
+
const SCHEMA_SOURCE_TEXT = {
|
|
1230
|
+
"module-export": "from module export",
|
|
1231
|
+
sidecar: "from the .schema.json sidecar",
|
|
1232
|
+
inferred: "inferred from source",
|
|
1233
|
+
unavailable: "unavailable",
|
|
1234
|
+
unknown: "not loaded yet"
|
|
1235
|
+
};
|
|
1236
|
+
/** 展示用的 schema 来源文案(console 与模型清单共用一套措辞) */
|
|
1237
|
+
function schemaSourceLabel(source) {
|
|
1238
|
+
return SCHEMA_SOURCE_TEXT[source];
|
|
1239
|
+
}
|
|
1240
|
+
/** allowed-tools 排除的脚本对模型不可见;清单里必须写明原因,否则作者只会看到工具凭空消失 */
|
|
1241
|
+
const ALLOWED_TOOLS_EXCLUSION_REASON = "excluded by the allowed-tools list in SKILL.md";
|
|
1242
|
+
/**
|
|
1243
|
+
* 从技能文件清单中挑出脚本并给出工具名映射。
|
|
1244
|
+
* @param files 技能根目录下的相对路径(如 `scripts/parse.ts`、`SKILL.md`)
|
|
1245
|
+
* @param allowedTools SKILL.md 声明的 allowed-tools;未声明传 undefined(全部暴露)
|
|
1246
|
+
*/
|
|
1247
|
+
function listSkillScripts(skillName, files, allowedTools) {
|
|
1248
|
+
const sidecars = new Set(files.filter((path) => path.startsWith("scripts/") && path.endsWith(".schema.json")));
|
|
1249
|
+
const out = [];
|
|
1250
|
+
for (const path of [...files].sort()) {
|
|
1251
|
+
const scriptName = /^scripts\/([^/]+)\.(?:ts|js)$/.exec(path)?.[1];
|
|
1252
|
+
if (scriptName === void 0 || scriptName === "") continue;
|
|
1253
|
+
const excluded = allowedTools !== void 0 && !allowedTools.includes(scriptName);
|
|
1254
|
+
out.push({
|
|
1255
|
+
path,
|
|
1256
|
+
scriptName,
|
|
1257
|
+
...excluded ? { unexposedReason: ALLOWED_TOOLS_EXCLUSION_REASON } : { toolName: scriptToolName(skillName, scriptName) },
|
|
1258
|
+
schemaSource: excluded ? "unavailable" : sidecars.has(`scripts/${scriptName}.schema.json`) ? "sidecar" : "unknown"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
return out;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* 清单 → 喂给模型的文本(FR-18.1)。
|
|
1265
|
+
* 「authoritative」这句是给模型的信号:以目录为准,别照 SKILL.md 的描述猜文件名。
|
|
1266
|
+
*/
|
|
1267
|
+
function formatSkillScriptManifest(descriptors) {
|
|
1268
|
+
if (descriptors.length === 0) return "";
|
|
1269
|
+
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")}`;
|
|
1270
|
+
}
|
|
888
1271
|
const MCP_CONTENT_RE = /^(text|json|image|resource|audio)$/;
|
|
889
1272
|
/**
|
|
890
1273
|
* 脚本返回值归一到 MCP content 格式:数组沿用(逐项归一),
|
|
891
1274
|
* 字符串包装为 text,其他值序列化包装。Node/浏览器执行器共享。
|
|
1275
|
+
* 产出只有文本型分片:脚本侧没有下发图像的通道(图像只来自页面取像)。
|
|
892
1276
|
*/
|
|
893
1277
|
function normalizeToolContent(value) {
|
|
894
1278
|
if (Array.isArray(value)) return value.map((item) => {
|
|
@@ -946,16 +1330,31 @@ const READ_SKILL_FILE_TOOL = {
|
|
|
946
1330
|
const ASK_USER_TOOL_NAME = "ask_user";
|
|
947
1331
|
const ASK_USER_INPUT_SCHEMA = {
|
|
948
1332
|
type: "object",
|
|
949
|
-
properties: {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1333
|
+
properties: {
|
|
1334
|
+
question: {
|
|
1335
|
+
type: "string",
|
|
1336
|
+
description: "The question to ask the user"
|
|
1337
|
+
},
|
|
1338
|
+
choices: {
|
|
1339
|
+
type: "array",
|
|
1340
|
+
items: { type: "string" },
|
|
1341
|
+
description: "Closed set of acceptable answers. Provide it whenever the answer must be one of a known finite set, for example when asking which installed skill to use. The user then picks from a list instead of typing free text."
|
|
1342
|
+
},
|
|
1343
|
+
suggestion: {
|
|
1344
|
+
type: "string",
|
|
1345
|
+
description: "A value you believe the user is likely to answer, based only on the profile in the system prompt. It is shown as a suggestion the user may accept; it is never filled in for them. Omit it when nothing in the profile supports a value."
|
|
1346
|
+
},
|
|
1347
|
+
suggestionReason: {
|
|
1348
|
+
type: "string",
|
|
1349
|
+
description: "Short reason for the suggestion, shown next to it so the user can judge whether to accept it."
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
953
1352
|
required: ["question"]
|
|
954
1353
|
};
|
|
955
1354
|
/** 内建工具:LLM 信息不足时主动向用户提问;仅当配置了 UiBridge 时注册 */
|
|
956
1355
|
const ASK_USER_TOOL = {
|
|
957
1356
|
name: ASK_USER_TOOL_NAME,
|
|
958
|
-
description: "Ask the user a question when information is missing and continue with their answer.",
|
|
1357
|
+
description: "Ask the user a question when information is missing and continue with their answer. When the answer belongs to a known finite set, you must pass \"choices\".",
|
|
959
1358
|
inputSchema: ASK_USER_INPUT_SCHEMA,
|
|
960
1359
|
source: "builtin"
|
|
961
1360
|
};
|
|
@@ -991,22 +1390,22 @@ function createScriptContext(deps) {
|
|
|
991
1390
|
...onWarning ? { onWarning } : {}
|
|
992
1391
|
};
|
|
993
1392
|
}
|
|
994
|
-
const isRecord$
|
|
1393
|
+
const isRecord$4 = (v) => typeof v === "object" && v !== null;
|
|
995
1394
|
/**
|
|
996
1395
|
* $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
|
|
997
1396
|
* 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
|
|
998
1397
|
*/
|
|
999
1398
|
function extractChartSpec(data) {
|
|
1000
|
-
if (!isRecord$
|
|
1399
|
+
if (!isRecord$4(data)) return void 0;
|
|
1001
1400
|
const raw = data["$chart"];
|
|
1002
|
-
if (!isRecord$
|
|
1401
|
+
if (!isRecord$4(raw)) return void 0;
|
|
1003
1402
|
const { kind, labels, series } = raw;
|
|
1004
1403
|
if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
|
|
1005
1404
|
if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
|
|
1006
1405
|
if (!Array.isArray(series)) return void 0;
|
|
1007
1406
|
const validSeries = [];
|
|
1008
1407
|
for (const item of series) {
|
|
1009
|
-
if (!isRecord$
|
|
1408
|
+
if (!isRecord$4(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
|
|
1010
1409
|
const name = item["name"];
|
|
1011
1410
|
validSeries.push({
|
|
1012
1411
|
...typeof name === "string" ? { name } : {},
|
|
@@ -1024,13 +1423,18 @@ function extractChartSpec(data) {
|
|
|
1024
1423
|
/**
|
|
1025
1424
|
* 默认的结果渲染构造:run 内收集的 renderBlocks(chart 等)在前,
|
|
1026
1425
|
* LLM 最终输出 → markdown block,run.artifacts → file blocks 在后;summary 取 terminationReason。
|
|
1426
|
+
* 注入 output block 时一并记下它的下标(S7),供消费方按来源去重。
|
|
1027
1427
|
*/
|
|
1028
1428
|
function buildRenderResult(run, output, renderBlocks = []) {
|
|
1029
1429
|
const blocks = [...renderBlocks];
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1430
|
+
let outputBlockIndex;
|
|
1431
|
+
if (output.trim() !== "") {
|
|
1432
|
+
outputBlockIndex = blocks.length;
|
|
1433
|
+
blocks.push({
|
|
1434
|
+
type: "markdown",
|
|
1435
|
+
text: output
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1034
1438
|
for (const artifact of run.artifacts) blocks.push({
|
|
1035
1439
|
type: "file",
|
|
1036
1440
|
path: artifact.path,
|
|
@@ -1041,7 +1445,8 @@ function buildRenderResult(run, output, renderBlocks = []) {
|
|
|
1041
1445
|
runId: run.id,
|
|
1042
1446
|
summary: run.terminationReason,
|
|
1043
1447
|
blocks,
|
|
1044
|
-
artifacts: run.artifacts
|
|
1448
|
+
artifacts: run.artifacts,
|
|
1449
|
+
...outputBlockIndex === void 0 ? {} : { outputBlockIndex }
|
|
1045
1450
|
};
|
|
1046
1451
|
}
|
|
1047
1452
|
const MAX_SURFACE_BYTES = 256 * 1024;
|
|
@@ -1057,7 +1462,7 @@ const actionIntents = /* @__PURE__ */ new Set([
|
|
|
1057
1462
|
"download",
|
|
1058
1463
|
"refresh"
|
|
1059
1464
|
]);
|
|
1060
|
-
function isRecord$
|
|
1465
|
+
function isRecord$3(value) {
|
|
1061
1466
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1062
1467
|
}
|
|
1063
1468
|
function reject(message) {
|
|
@@ -1074,7 +1479,7 @@ function isJsonValue(value, depth = 0) {
|
|
|
1074
1479
|
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
1075
1480
|
if (typeof value === "number") return Number.isFinite(value);
|
|
1076
1481
|
if (Array.isArray(value)) return value.every((item) => isJsonValue(item, depth + 1));
|
|
1077
|
-
if (!isRecord$
|
|
1482
|
+
if (!isRecord$3(value)) return false;
|
|
1078
1483
|
return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
|
|
1079
1484
|
}
|
|
1080
1485
|
/**
|
|
@@ -1084,10 +1489,10 @@ function isJsonValue(value, depth = 0) {
|
|
|
1084
1489
|
function assertNode(value, path, depth, counter) {
|
|
1085
1490
|
if (depth > MAX_NODE_DEPTH) reject(`A UI spec tree must not nest deeper than ${MAX_NODE_DEPTH} levels`);
|
|
1086
1491
|
if (++counter.nodes > MAX_NODES) reject(`A UI spec tree must contain at most ${MAX_NODES} nodes`);
|
|
1087
|
-
if (!isRecord$
|
|
1492
|
+
if (!isRecord$3(value)) reject(`${path} must be an object`);
|
|
1088
1493
|
requireString(value["component"], `${path}.component`);
|
|
1089
1494
|
if (value["id"] !== void 0) requireString(value["id"], `${path}.id`);
|
|
1090
|
-
if (value["props"] !== void 0 && (!isRecord$
|
|
1495
|
+
if (value["props"] !== void 0 && (!isRecord$3(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
|
|
1091
1496
|
const children = value["children"];
|
|
1092
1497
|
if (children === void 0) return;
|
|
1093
1498
|
if (!Array.isArray(children)) reject(`${path}.children must be an array`);
|
|
@@ -1097,7 +1502,7 @@ function assertActions(value) {
|
|
|
1097
1502
|
if (value === void 0) return;
|
|
1098
1503
|
if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
|
|
1099
1504
|
for (const action of value) {
|
|
1100
|
-
if (!isRecord$
|
|
1505
|
+
if (!isRecord$3(action)) reject("A surface action must be an object");
|
|
1101
1506
|
requireString(action["id"], "Surface action ID");
|
|
1102
1507
|
const intent = action["intent"];
|
|
1103
1508
|
if (typeof intent !== "string" || !actionIntents.has(intent)) reject("Surface action intent is invalid");
|
|
@@ -1113,7 +1518,7 @@ function validateUiSpecNode(value) {
|
|
|
1113
1518
|
return structuredClone(value);
|
|
1114
1519
|
}
|
|
1115
1520
|
function assertPatch(value) {
|
|
1116
|
-
if (!isRecord$
|
|
1521
|
+
if (!isRecord$3(value)) reject("A surface patch operation must be an object");
|
|
1117
1522
|
if (value["op"] !== "replace" && value["op"] !== "merge" && value["op"] !== "append") reject("Surface patch operation is invalid");
|
|
1118
1523
|
requireString(value["path"], "Surface patch path");
|
|
1119
1524
|
if (!value["path"].startsWith("/")) reject("Surface patch path must be a JSON pointer");
|
|
@@ -1121,7 +1526,7 @@ function assertPatch(value) {
|
|
|
1121
1526
|
}
|
|
1122
1527
|
/** Validates an individual event in the framework-neutral surface stream. @experimental */
|
|
1123
1528
|
function validateUiSpecEvent(value) {
|
|
1124
|
-
if (!isRecord$
|
|
1529
|
+
if (!isRecord$3(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
|
|
1125
1530
|
if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
|
|
1126
1531
|
switch (value["type"]) {
|
|
1127
1532
|
case "open":
|
|
@@ -1179,74 +1584,16 @@ function validateUiSpecEvent(value) {
|
|
|
1179
1584
|
}
|
|
1180
1585
|
/** Extracts validated surface stream events from structured tool output. @experimental */
|
|
1181
1586
|
function extractUiSpecEvents(data) {
|
|
1182
|
-
if (!isRecord$
|
|
1587
|
+
if (!isRecord$3(data) || data["$surface"] === void 0) return [];
|
|
1183
1588
|
const raw = data["$surface"];
|
|
1184
1589
|
return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
|
|
1185
1590
|
}
|
|
1186
|
-
/** 跨会话表单填写值在 `user:{userId}` scope 下的 key(FR-5.7) @experimental */
|
|
1187
|
-
const FORM_VALUES_KEY = "formValues";
|
|
1188
|
-
/**
|
|
1189
|
-
* 跨会话稳定的字段标识(FR-5.6)。必须带技能名:
|
|
1190
|
-
* 只有字段名时,两个技能各自的 `email` 会互相串号。
|
|
1191
|
-
* @experimental
|
|
1192
|
-
*/
|
|
1193
|
-
function formFieldKey(skillName, fieldName) {
|
|
1194
|
-
return `${skillName}#${fieldName}`;
|
|
1195
|
-
}
|
|
1196
|
-
/** memory 里的原始值形状不受控(宿主可能手改文件),逐条过滤而不是整体信任 @experimental */
|
|
1197
|
-
function readFormValues(raw) {
|
|
1198
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
1199
|
-
const out = {};
|
|
1200
|
-
for (const [key, entry] of Object.entries(raw)) {
|
|
1201
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
1202
|
-
const record = entry;
|
|
1203
|
-
if (typeof record.ts !== "number" || !("value" in record)) continue;
|
|
1204
|
-
out[key] = {
|
|
1205
|
-
value: record.value,
|
|
1206
|
-
ts: record.ts
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
return out;
|
|
1210
|
-
}
|
|
1211
|
-
/** 合并本次提交并按上限裁剪最旧(FR-5.12) @experimental */
|
|
1212
|
-
function putFormValues(current, updates, limit) {
|
|
1213
|
-
const merged = {
|
|
1214
|
-
...current,
|
|
1215
|
-
...updates
|
|
1216
|
-
};
|
|
1217
|
-
const keys = Object.keys(merged);
|
|
1218
|
-
if (limit <= 0) return {};
|
|
1219
|
-
if (keys.length <= limit) return merged;
|
|
1220
|
-
const kept = keys.sort((a, b) => merged[a].ts - merged[b].ts).slice(keys.length - limit);
|
|
1221
|
-
return Object.fromEntries(kept.map((key) => [key, merged[key]]));
|
|
1222
|
-
}
|
|
1223
|
-
/** 清除单个字段;不传 fieldKey 即全部清除(FR-5.11) @experimental */
|
|
1224
|
-
function clearFormValues(current, fieldKey) {
|
|
1225
|
-
if (fieldKey === void 0) return {};
|
|
1226
|
-
const { [fieldKey]: _removed, ...rest } = current;
|
|
1227
|
-
return rest;
|
|
1228
|
-
}
|
|
1229
|
-
/**
|
|
1230
|
-
* 宿主侧的清除入口(FR-5.11):设置面板的「清除填写历史」直接调它,
|
|
1231
|
-
* 不必自己知道 scope 与 key 的约定。
|
|
1232
|
-
* @experimental
|
|
1233
|
-
*/
|
|
1234
|
-
async function clearStoredFormValues(memory, userId, fieldKey) {
|
|
1235
|
-
const scope = `user:${userId}`;
|
|
1236
|
-
if (fieldKey === void 0) {
|
|
1237
|
-
await memory.delete(scope, FORM_VALUES_KEY);
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
const next = clearFormValues(readFormValues(await memory.get(scope, FORM_VALUES_KEY)), fieldKey);
|
|
1241
|
-
await memory.set(scope, FORM_VALUES_KEY, next);
|
|
1242
|
-
}
|
|
1243
1591
|
/**
|
|
1244
1592
|
* JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
|
|
1245
1593
|
* providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
|
|
1246
1594
|
* type 映射:string→text、number/integer→number、boolean→boolean、enum→select、其余→textarea。
|
|
1247
|
-
* 传入 skillName 时给每个字段带上跨会话稳定的 `fieldKey`(FR-5.6)。
|
|
1248
1595
|
*/
|
|
1249
|
-
function schemaToForm(schema, providedArgs
|
|
1596
|
+
function schemaToForm(schema, providedArgs) {
|
|
1250
1597
|
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
1251
1598
|
const fields = [];
|
|
1252
1599
|
for (const [name, prop] of Object.entries(schema.properties ?? {})) {
|
|
@@ -1257,7 +1604,6 @@ function schemaToForm(schema, providedArgs, options) {
|
|
|
1257
1604
|
type: mapFieldType(prop),
|
|
1258
1605
|
...required.has(name) ? { required: true } : {},
|
|
1259
1606
|
...typeof prop.description === "string" ? { description: prop.description } : {},
|
|
1260
|
-
...options?.skillName !== void 0 ? { fieldKey: formFieldKey(options.skillName, name) } : {},
|
|
1261
1607
|
...provided !== void 0 ? { defaultValue: provided } : prop.default !== void 0 ? { defaultValue: prop.default } : {}
|
|
1262
1608
|
};
|
|
1263
1609
|
if (Array.isArray(prop.enum)) field.options = prop.enum.map((v) => ({
|
|
@@ -1271,7 +1617,7 @@ function schemaToForm(schema, providedArgs, options) {
|
|
|
1271
1617
|
function mapFieldType(prop) {
|
|
1272
1618
|
if (Array.isArray(prop.enum)) return "select";
|
|
1273
1619
|
switch (prop.type) {
|
|
1274
|
-
case "string": return "text";
|
|
1620
|
+
case "string": return prop["format"] === "binary" || prop["contentEncoding"] === "base64" ? "file" : "text";
|
|
1275
1621
|
case "number":
|
|
1276
1622
|
case "integer": return "number";
|
|
1277
1623
|
case "boolean": return "boolean";
|
|
@@ -1361,6 +1707,176 @@ var SerializingMemoryStore = class {
|
|
|
1361
1707
|
return this.#serialize(scope, () => fn(this.#inner));
|
|
1362
1708
|
}
|
|
1363
1709
|
};
|
|
1710
|
+
/** @experimental */
|
|
1711
|
+
const DEFAULT_USER_PROFILE_LIMITS = {
|
|
1712
|
+
recordLimit: 500,
|
|
1713
|
+
valueMaxChars: 2e3,
|
|
1714
|
+
sampleLimit: 80,
|
|
1715
|
+
injectMaxBytes: 2e3,
|
|
1716
|
+
entryLimit: 40
|
|
1717
|
+
};
|
|
1718
|
+
/** @experimental */
|
|
1719
|
+
const EMPTY_USER_PROFILE = {
|
|
1720
|
+
version: 1,
|
|
1721
|
+
entries: [],
|
|
1722
|
+
refinedAt: 0
|
|
1723
|
+
};
|
|
1724
|
+
/** 注入段的标题。宿主/测试要判断「有没有注入画像」时对它取子串,不要自己再拼一份 @experimental */
|
|
1725
|
+
const USER_PROFILE_PROMPT_HEADER = "## About this user (inferred from past behavior; may be outdated)";
|
|
1726
|
+
/**
|
|
1727
|
+
* 无画像时也要下发的约束(AC-19.11):没有依据就把字段留空,
|
|
1728
|
+
* 不写这一句时模型会把「建议值」当成必填项去编。
|
|
1729
|
+
* @experimental
|
|
1730
|
+
*/
|
|
1731
|
+
const USER_PROFILE_NO_INVENTION_RULE = "Only offer a suggested value for a question when the profile above actually supports it. With no relevant profile entry, omit the suggestion and leave the field empty; never invent one.";
|
|
1732
|
+
const byPriority = (a, b) => a.confidence === b.confidence ? b.updatedAt - a.updatedAt : a.confidence === "high" ? -1 : 1;
|
|
1733
|
+
const byteLength = (text) => new TextEncoder().encode(text).length;
|
|
1734
|
+
/**
|
|
1735
|
+
* 画像 → 系统提示片段,按 confidence + updatedAt 优先级排序后逐条累加,
|
|
1736
|
+
* 超出字节上限的部分丢弃并标注(FR-19.4 / AC-19.9)。
|
|
1737
|
+
* 空画像返回空串:只有标题的空段落白占 token,还会诱导模型编造。
|
|
1738
|
+
* @experimental
|
|
1739
|
+
*/
|
|
1740
|
+
function renderUserProfileContext(profile = EMPTY_USER_PROFILE, maxBytes = DEFAULT_USER_PROFILE_LIMITS.injectMaxBytes) {
|
|
1741
|
+
const entries = [...profile.entries].sort(byPriority);
|
|
1742
|
+
if (entries.length === 0 || maxBytes <= 0) return "";
|
|
1743
|
+
const lines = entries.map((entry) => `- ${entry.text}`);
|
|
1744
|
+
const render = (keptCount) => {
|
|
1745
|
+
const omitted = lines.length - keptCount;
|
|
1746
|
+
const body = [USER_PROFILE_PROMPT_HEADER, ...lines.slice(0, keptCount)];
|
|
1747
|
+
if (omitted > 0) body.push(`(truncated: ${omitted} more entries omitted)`);
|
|
1748
|
+
return body.join("\n");
|
|
1749
|
+
};
|
|
1750
|
+
let kept = lines.length;
|
|
1751
|
+
while (kept > 0 && byteLength(render(kept)) > maxBytes) kept -= 1;
|
|
1752
|
+
return kept === 0 ? "" : render(kept);
|
|
1753
|
+
}
|
|
1754
|
+
/** `user:{id}` scope 下的行为记录 key @experimental */
|
|
1755
|
+
const BEHAVIOR_RECORDS_KEY = "behaviorRecords";
|
|
1756
|
+
/** `user:{id}` scope 下的画像 key @experimental */
|
|
1757
|
+
const USER_PROFILE_KEY = "userProfile";
|
|
1758
|
+
const KINDS = [
|
|
1759
|
+
"question",
|
|
1760
|
+
"choice",
|
|
1761
|
+
"text-input",
|
|
1762
|
+
"stated-preference"
|
|
1763
|
+
];
|
|
1764
|
+
const isKind = (value) => KINDS.includes(value);
|
|
1765
|
+
const stringList = (raw) => Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : void 0;
|
|
1766
|
+
/** 存储里的形状不受控(宿主可能手改文件),逐条筛而不是整体信任 @experimental */
|
|
1767
|
+
function readBehaviorRecords(raw) {
|
|
1768
|
+
if (!Array.isArray(raw)) return [];
|
|
1769
|
+
const out = [];
|
|
1770
|
+
for (const item of raw) {
|
|
1771
|
+
if (typeof item !== "object" || item === null) continue;
|
|
1772
|
+
const record = item;
|
|
1773
|
+
const scene = record.scene;
|
|
1774
|
+
if (typeof record.id !== "string" || !isKind(record.kind)) continue;
|
|
1775
|
+
if (typeof record.at !== "number" || typeof record.value !== "string") continue;
|
|
1776
|
+
if (typeof scene !== "object" || scene === null || typeof scene.sessionId !== "string") continue;
|
|
1777
|
+
const options = stringList(scene.options);
|
|
1778
|
+
out.push({
|
|
1779
|
+
id: record.id,
|
|
1780
|
+
kind: record.kind,
|
|
1781
|
+
at: record.at,
|
|
1782
|
+
value: record.value,
|
|
1783
|
+
...record.confirmed === true ? { confirmed: true } : {},
|
|
1784
|
+
scene: {
|
|
1785
|
+
sessionId: scene.sessionId,
|
|
1786
|
+
...typeof scene.skillName === "string" ? { skillName: scene.skillName } : {},
|
|
1787
|
+
...typeof scene.interactionId === "string" ? { interactionId: scene.interactionId } : {},
|
|
1788
|
+
...typeof scene.fieldName === "string" ? { fieldName: scene.fieldName } : {},
|
|
1789
|
+
...typeof scene.fieldSemantics === "string" ? { fieldSemantics: scene.fieldSemantics } : {},
|
|
1790
|
+
...options && options.length > 0 ? { options } : {}
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
return out;
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* 追加并按容量淘汰最旧(FR-19.2)。`value` 同时截断:
|
|
1798
|
+
* 一条被粘贴进来的长文本能把整个 JSON 撑爆,而提炼只需要语义。
|
|
1799
|
+
* @experimental
|
|
1800
|
+
*/
|
|
1801
|
+
function appendBehaviorRecords(current, incoming, limits = DEFAULT_USER_PROFILE_LIMITS) {
|
|
1802
|
+
if (limits.recordLimit <= 0) return [];
|
|
1803
|
+
const truncated = incoming.map((record) => record.value.length > limits.valueMaxChars ? {
|
|
1804
|
+
...record,
|
|
1805
|
+
value: record.value.slice(0, limits.valueMaxChars)
|
|
1806
|
+
} : record);
|
|
1807
|
+
const merged = [...current, ...truncated];
|
|
1808
|
+
return merged.slice(Math.max(0, merged.length - limits.recordLimit));
|
|
1809
|
+
}
|
|
1810
|
+
/** @experimental */
|
|
1811
|
+
function readUserProfile(raw) {
|
|
1812
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return EMPTY_USER_PROFILE;
|
|
1813
|
+
const value = raw;
|
|
1814
|
+
return {
|
|
1815
|
+
version: 1,
|
|
1816
|
+
entries: readProfileEntries(value.entries),
|
|
1817
|
+
refinedAt: typeof value.refinedAt === "number" ? value.refinedAt : 0
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
/** @experimental */
|
|
1821
|
+
function readProfileEntries(raw) {
|
|
1822
|
+
if (!Array.isArray(raw)) return [];
|
|
1823
|
+
const out = [];
|
|
1824
|
+
for (const item of raw) {
|
|
1825
|
+
if (typeof item !== "object" || item === null) continue;
|
|
1826
|
+
const entry = item;
|
|
1827
|
+
if (typeof entry.id !== "string" || entry.id === "") continue;
|
|
1828
|
+
if (typeof entry.text !== "string" || entry.text.trim() === "") continue;
|
|
1829
|
+
out.push({
|
|
1830
|
+
id: entry.id,
|
|
1831
|
+
text: entry.text,
|
|
1832
|
+
confidence: entry.confidence === "high" ? "high" : "low",
|
|
1833
|
+
updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : 0,
|
|
1834
|
+
sourceKinds: (stringList(entry.sourceKinds) ?? []).filter(isKind)
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
return out;
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* 合并提炼结果:同 id 更新,新 id 追加,超上限按 `updatedAt` 淘汰最旧(设计 09 §1.4)。
|
|
1841
|
+
* @experimental
|
|
1842
|
+
*/
|
|
1843
|
+
function mergeProfileEntries(current, incoming, options) {
|
|
1844
|
+
const limits = options.limits ?? DEFAULT_USER_PROFILE_LIMITS;
|
|
1845
|
+
const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
|
|
1846
|
+
for (const entry of incoming) byId.set(entry.id, entry);
|
|
1847
|
+
return {
|
|
1848
|
+
version: 1,
|
|
1849
|
+
entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, Math.max(0, limits.entryLimit)),
|
|
1850
|
+
refinedAt: options.now
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* 提炼采样(FR-19.3):最近的 + 高频的 + 被用户显式确认过的,去重后截到上限。
|
|
1855
|
+
* 三个维度缺一不可——只取最近会让长期偏好被一次突发淹没,只取高频则学不到新变化。
|
|
1856
|
+
* @experimental
|
|
1857
|
+
*/
|
|
1858
|
+
function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
|
|
1859
|
+
const limit = Math.max(0, limits.sampleLimit);
|
|
1860
|
+
if (limit === 0 || records.length === 0) return [];
|
|
1861
|
+
if (records.length <= limit) return [...records].sort((a, b) => a.at - b.at);
|
|
1862
|
+
const picked = /* @__PURE__ */ new Map();
|
|
1863
|
+
const take = (record) => {
|
|
1864
|
+
if (picked.size < limit) picked.set(record.id, record);
|
|
1865
|
+
};
|
|
1866
|
+
for (const record of records) if (record.confirmed === true) take(record);
|
|
1867
|
+
const frequency = /* @__PURE__ */ new Map();
|
|
1868
|
+
const bucketOf = (record) => `${record.scene.skillName ?? ""}#${record.scene.fieldName ?? record.kind}`;
|
|
1869
|
+
for (const record of records) frequency.set(bucketOf(record), (frequency.get(bucketOf(record)) ?? 0) + 1);
|
|
1870
|
+
const byFrequency = [...records].sort((a, b) => (frequency.get(bucketOf(b)) ?? 0) - (frequency.get(bucketOf(a)) ?? 0));
|
|
1871
|
+
for (const record of byFrequency.slice(0, Math.ceil(limit / 2))) take(record);
|
|
1872
|
+
for (const record of [...records].reverse()) take(record);
|
|
1873
|
+
return [...picked.values()].sort((a, b) => a.at - b.at);
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* 无工具模型的系统提示(0.6.0 FR-14.3)。
|
|
1877
|
+
* 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
|
|
1878
|
+
*/
|
|
1879
|
+
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.";
|
|
1364
1880
|
/** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
|
|
1365
1881
|
var TraceRecorder = class {
|
|
1366
1882
|
#runId;
|
|
@@ -1387,7 +1903,7 @@ var TraceRecorder = class {
|
|
|
1387
1903
|
return [...this.#events];
|
|
1388
1904
|
}
|
|
1389
1905
|
};
|
|
1390
|
-
const isRecord$
|
|
1906
|
+
const isRecord$2 = (v) => typeof v === "object" && v !== null;
|
|
1391
1907
|
/** 工具结果可经 `$todo` 标记记入 trace 的事件类型;其余类型不接受,防止工具源伪造 run.* */
|
|
1392
1908
|
const TODO_TRACE_TYPES = /* @__PURE__ */ new Set([
|
|
1393
1909
|
"todo.created",
|
|
@@ -1402,12 +1918,12 @@ const TODO_TRACE_TYPES = /* @__PURE__ */ new Set([
|
|
|
1402
1918
|
* @experimental
|
|
1403
1919
|
*/
|
|
1404
1920
|
function extractTodoTraceEvents(data) {
|
|
1405
|
-
if (!isRecord$
|
|
1921
|
+
if (!isRecord$2(data) || data["$todo"] === void 0) return [];
|
|
1406
1922
|
const raw = data["$todo"];
|
|
1407
1923
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
1408
1924
|
const events = [];
|
|
1409
1925
|
for (const entry of entries) {
|
|
1410
|
-
if (!isRecord$
|
|
1926
|
+
if (!isRecord$2(entry)) continue;
|
|
1411
1927
|
const { type, ...rest } = entry;
|
|
1412
1928
|
if (typeof type !== "string" || !TODO_TRACE_TYPES.has(type)) continue;
|
|
1413
1929
|
events.push({
|
|
@@ -1417,6 +1933,38 @@ function extractTodoTraceEvents(data) {
|
|
|
1417
1933
|
}
|
|
1418
1934
|
return events;
|
|
1419
1935
|
}
|
|
1936
|
+
const isRecord$1 = (v) => typeof v === "object" && v !== null;
|
|
1937
|
+
/**
|
|
1938
|
+
* `$skillCandidate` 约定的形状校验:JSON content 的 data 含合法 `$skillCandidate` 键 → 候选载荷。
|
|
1939
|
+
*
|
|
1940
|
+
* 与 `$chart` / `$todo` / `$surface` 同一条既有通道,是第四个。
|
|
1941
|
+
* 候选的生成与存储全部在 `@webskill/agent`,runtime 只认这个形状,畸形忽略不炸。
|
|
1942
|
+
* @experimental
|
|
1943
|
+
*/
|
|
1944
|
+
function extractSkillCandidate(data) {
|
|
1945
|
+
if (!isRecord$1(data)) return void 0;
|
|
1946
|
+
const raw = data["$skillCandidate"];
|
|
1947
|
+
if (!isRecord$1(raw)) return void 0;
|
|
1948
|
+
const { id, name } = raw;
|
|
1949
|
+
if (typeof id !== "string" || id === "" || typeof name !== "string" || name === "") return void 0;
|
|
1950
|
+
return {
|
|
1951
|
+
id,
|
|
1952
|
+
name
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Agent loop 的运行上限默认值。**全仓唯一来源**(S8)。
|
|
1957
|
+
*
|
|
1958
|
+
* 在 0.7.0 之前这三个数字在 `AgentLoop` 的构造与 ui-kit 的 `defaultRuntimeConfig()`
|
|
1959
|
+
* 里各写了一遍。两份值今天恰好相等,所以不出事;一旦其中一处被改,
|
|
1960
|
+
* 「恢复默认值」会恢复到 agent loop 根本不用的值——验收绿、行为错。
|
|
1961
|
+
* @stable
|
|
1962
|
+
*/
|
|
1963
|
+
const DEFAULT_LOOP_LIMITS = {
|
|
1964
|
+
maxTurns: 10,
|
|
1965
|
+
totalTimeoutMs: 12e4,
|
|
1966
|
+
toolTimeoutMs: 3e4
|
|
1967
|
+
};
|
|
1420
1968
|
const RUN_SNAPSHOT_SCHEMA_VERSION = 2;
|
|
1421
1969
|
/** @experimental */
|
|
1422
1970
|
function isUnsupportedRunSnapshot(entry) {
|
|
@@ -1570,13 +2118,13 @@ var RunTerminated = class extends Error {
|
|
|
1570
2118
|
};
|
|
1571
2119
|
/** bridge.request 自身异常(非超时):恢复 running 后转为工具错误回喂 */
|
|
1572
2120
|
var BridgeRequestError = class extends Error {};
|
|
1573
|
-
const
|
|
1574
|
-
const toolError = (code, message) => ({
|
|
2121
|
+
const toolError = (code, message, data) => ({
|
|
1575
2122
|
ok: false,
|
|
1576
2123
|
content: [],
|
|
1577
2124
|
error: {
|
|
1578
2125
|
code,
|
|
1579
|
-
message
|
|
2126
|
+
message,
|
|
2127
|
+
...data !== void 0 ? { data } : {}
|
|
1580
2128
|
}
|
|
1581
2129
|
});
|
|
1582
2130
|
/** 工具参数摘要(JSON 截断 100 字符;trace 与实时事件同一口径,ToolCallCard 展开详情用) */
|
|
@@ -1585,6 +2133,30 @@ const summarizeArgs = (args) => {
|
|
|
1585
2133
|
return json.length > 100 ? `${json.slice(0, 100)}…` : json;
|
|
1586
2134
|
};
|
|
1587
2135
|
/**
|
|
2136
|
+
* file-pick 的回填值带整个文件的 base64,落进 `paramHistory` 会把用户选的文件
|
|
2137
|
+
* 原样长期留在记忆里(体积与隐私都不可接受)。只留可辨识的描述,正文不留。
|
|
2138
|
+
*/
|
|
2139
|
+
const redactFileValue = (value) => {
|
|
2140
|
+
if (Array.isArray(value)) return value.map(redactFileValue);
|
|
2141
|
+
if (typeof value !== "object" || value === null) return typeof value === "string" ? "[file]" : value;
|
|
2142
|
+
const { data: _data, ...rest } = value;
|
|
2143
|
+
return rest;
|
|
2144
|
+
};
|
|
2145
|
+
/**
|
|
2146
|
+
* 触顶失败的结构化细节(FR-21.2)。由终止原因推出上限字段,而不是在每个终止点各写一遍——
|
|
2147
|
+
* 终止点有四个,写四遍就迟早有一处对不上。
|
|
2148
|
+
*/
|
|
2149
|
+
const limitDetails = (state, reason) => {
|
|
2150
|
+
if (reason === "timeout") return {
|
|
2151
|
+
limit: "totalTimeoutMs",
|
|
2152
|
+
value: state.totalTimeoutMs
|
|
2153
|
+
};
|
|
2154
|
+
if (reason === "max-turns") return {
|
|
2155
|
+
limit: "maxTurns",
|
|
2156
|
+
value: state.maxTurns
|
|
2157
|
+
};
|
|
2158
|
+
};
|
|
2159
|
+
/**
|
|
1588
2160
|
* 多轮 Agent 循环。
|
|
1589
2161
|
* 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
|
|
1590
2162
|
* 缺参/确认/提问经 UiBridge 行内 await 暂停恢复;取消/超时/LLM 异常/护栏超限才终止 run。
|
|
@@ -1603,12 +2175,13 @@ var AgentLoop = class {
|
|
|
1603
2175
|
artifactStore: deps.artifactStore ?? new MemoryArtifactStore()
|
|
1604
2176
|
};
|
|
1605
2177
|
this.#config = {
|
|
1606
|
-
maxTurns: config.maxTurns ??
|
|
1607
|
-
totalTimeoutMs: config.totalTimeoutMs ??
|
|
1608
|
-
toolTimeoutMs: config.toolTimeoutMs ??
|
|
2178
|
+
maxTurns: config.maxTurns ?? DEFAULT_LOOP_LIMITS.maxTurns,
|
|
2179
|
+
totalTimeoutMs: config.totalTimeoutMs ?? DEFAULT_LOOP_LIMITS.totalTimeoutMs,
|
|
2180
|
+
toolTimeoutMs: config.toolTimeoutMs ?? DEFAULT_LOOP_LIMITS.toolTimeoutMs,
|
|
1609
2181
|
toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
|
|
1610
2182
|
paramHistoryLimit: config.paramHistoryLimit ?? 50,
|
|
1611
|
-
|
|
2183
|
+
toolCallingDisabled: config.toolCallingDisabled ?? false,
|
|
2184
|
+
maxUnknownToolRetries: config.maxUnknownToolRetries ?? 2,
|
|
1612
2185
|
temperature: config.temperature,
|
|
1613
2186
|
renderResult: config.renderResult
|
|
1614
2187
|
};
|
|
@@ -1621,7 +2194,7 @@ var AgentLoop = class {
|
|
|
1621
2194
|
/**
|
|
1622
2195
|
* 取消进行中的 run:触发该 run 的 AbortController(与 totalTimeout 硬期限同一通道),
|
|
1623
2196
|
* run 以 cancelled(RUN_CANCELLED)终止;未找到(已终态或不属于本实例)返回 false。
|
|
1624
|
-
*
|
|
2197
|
+
* 取消在下一个中断点生效;LLM 调用与交互等待都监听同一个 signal,因此等表单时也能立即停。
|
|
1625
2198
|
*/
|
|
1626
2199
|
cancel(runId) {
|
|
1627
2200
|
const controller = this.#controllers.get(runId);
|
|
@@ -1643,7 +2216,7 @@ var AgentLoop = class {
|
|
|
1643
2216
|
sessionId: input.sessionId,
|
|
1644
2217
|
status: "running",
|
|
1645
2218
|
phase: "route",
|
|
1646
|
-
userPrompt: input.userPrompt,
|
|
2219
|
+
userPrompt: promptText(input.userPrompt),
|
|
1647
2220
|
startedAt,
|
|
1648
2221
|
activeSkillNames: [],
|
|
1649
2222
|
artifacts: [],
|
|
@@ -1668,6 +2241,7 @@ var AgentLoop = class {
|
|
|
1668
2241
|
surfacePatchCount: 0,
|
|
1669
2242
|
processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
|
|
1670
2243
|
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
2244
|
+
unknownToolCalls: 0,
|
|
1671
2245
|
startMs,
|
|
1672
2246
|
pausedMs: 0,
|
|
1673
2247
|
maxTurns: this.#config.maxTurns,
|
|
@@ -1691,7 +2265,7 @@ var AgentLoop = class {
|
|
|
1691
2265
|
candidates: route.catalog.entries.map((e) => e.name)
|
|
1692
2266
|
}
|
|
1693
2267
|
}, state);
|
|
1694
|
-
const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
2268
|
+
const externalSpecs = this.#config.toolCallingDisabled ? [] : (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
1695
2269
|
try {
|
|
1696
2270
|
return await source.listToolSpecs();
|
|
1697
2271
|
} catch (e) {
|
|
@@ -1700,7 +2274,7 @@ var AgentLoop = class {
|
|
|
1700
2274
|
}
|
|
1701
2275
|
}))).flat();
|
|
1702
2276
|
const externalSystemPrompts = [];
|
|
1703
|
-
for (const source of this.#deps.externalTools ?? []) {
|
|
2277
|
+
if (!this.#config.toolCallingDisabled) for (const source of this.#deps.externalTools ?? []) {
|
|
1704
2278
|
if (source.systemPrompt === void 0) continue;
|
|
1705
2279
|
try {
|
|
1706
2280
|
const text = (await source.systemPrompt())?.trim();
|
|
@@ -1709,17 +2283,21 @@ var AgentLoop = class {
|
|
|
1709
2283
|
trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
|
|
1710
2284
|
}
|
|
1711
2285
|
}
|
|
2286
|
+
const systemPrompt = this.#config.toolCallingDisabled ? PLAIN_CHAT_SYSTEM_PROMPT : [route.systemPrompt, ...externalSystemPrompts].join("\n\n");
|
|
2287
|
+
const profileMessage = await this.#userProfileMessage(state);
|
|
1712
2288
|
state.messages = [
|
|
1713
2289
|
{
|
|
1714
2290
|
role: "system",
|
|
1715
|
-
content: textParts(
|
|
2291
|
+
content: textParts(systemPrompt)
|
|
1716
2292
|
},
|
|
2293
|
+
...profileMessage ? [profileMessage] : [],
|
|
1717
2294
|
...(input.history ?? []).map((m) => ({ ...m })),
|
|
1718
2295
|
{
|
|
1719
2296
|
role: "user",
|
|
1720
|
-
content: textParts(input.userPrompt)
|
|
2297
|
+
content: typeof input.userPrompt === "string" ? textParts(input.userPrompt) : [...input.userPrompt]
|
|
1721
2298
|
}
|
|
1722
2299
|
];
|
|
2300
|
+
await this.#recordUserPrompt(state);
|
|
1723
2301
|
try {
|
|
1724
2302
|
return await this.#turnLoop(state, 1, externalSpecs);
|
|
1725
2303
|
} catch (e) {
|
|
@@ -1780,7 +2358,7 @@ var AgentLoop = class {
|
|
|
1780
2358
|
if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
|
|
1781
2359
|
if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
1782
2360
|
const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
|
|
1783
|
-
const toolSpecs = [
|
|
2361
|
+
const toolSpecs = this.#config.toolCallingDisabled ? [] : [
|
|
1784
2362
|
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
1785
2363
|
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
1786
2364
|
...skillToolSpecs
|
|
@@ -1814,6 +2392,7 @@ var AgentLoop = class {
|
|
|
1814
2392
|
return finish("failed", "llm-error", messageOf(e), code);
|
|
1815
2393
|
}
|
|
1816
2394
|
const responseText = partsToText(response.content);
|
|
2395
|
+
if (!llm.stream && responseText !== "") await this.#deps.uiBridge?.onTextDelta?.(state.runId, responseText);
|
|
1817
2396
|
trace.record("llm.response", { data: {
|
|
1818
2397
|
turn,
|
|
1819
2398
|
hasToolCalls: Boolean(response.toolCalls?.length),
|
|
@@ -1849,9 +2428,14 @@ var AgentLoop = class {
|
|
|
1849
2428
|
messages.push({
|
|
1850
2429
|
role: "tool",
|
|
1851
2430
|
toolCallId: call.id,
|
|
1852
|
-
content:
|
|
2431
|
+
content: await this.#toolResultParts(call, result, state)
|
|
1853
2432
|
});
|
|
1854
|
-
|
|
2433
|
+
try {
|
|
2434
|
+
await this.#drainSurfaceAction(state);
|
|
2435
|
+
} catch (e) {
|
|
2436
|
+
if (e instanceof RunTerminated) return finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
|
|
2437
|
+
throw e;
|
|
2438
|
+
}
|
|
1855
2439
|
}
|
|
1856
2440
|
}
|
|
1857
2441
|
} finally {
|
|
@@ -1878,7 +2462,8 @@ var AgentLoop = class {
|
|
|
1878
2462
|
message: output,
|
|
1879
2463
|
data: {
|
|
1880
2464
|
reason,
|
|
1881
|
-
...errorCode ? { code: errorCode } : {}
|
|
2465
|
+
...errorCode ? { code: errorCode } : {},
|
|
2466
|
+
...limitDetails(state, reason) ?? {}
|
|
1882
2467
|
}
|
|
1883
2468
|
});
|
|
1884
2469
|
const bridge = this.#deps.uiBridge;
|
|
@@ -1988,6 +2573,7 @@ var AgentLoop = class {
|
|
|
1988
2573
|
surfacePatchCount: 0,
|
|
1989
2574
|
processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
|
|
1990
2575
|
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
2576
|
+
unknownToolCalls: 0,
|
|
1991
2577
|
startMs,
|
|
1992
2578
|
pausedMs: snapshot.pausedMs ?? 0,
|
|
1993
2579
|
maxTurns: snapshot.config.maxTurns,
|
|
@@ -2024,7 +2610,7 @@ var AgentLoop = class {
|
|
|
2024
2610
|
state.messages.push({
|
|
2025
2611
|
role: "tool",
|
|
2026
2612
|
toolCallId: pendingCall.id,
|
|
2027
|
-
content:
|
|
2613
|
+
content: await this.#toolResultParts(pendingCall, result, state)
|
|
2028
2614
|
});
|
|
2029
2615
|
await this.#drainSurfaceAction(state);
|
|
2030
2616
|
} else if (pending?.type === "ask" && pendingCall) {
|
|
@@ -2046,7 +2632,7 @@ var AgentLoop = class {
|
|
|
2046
2632
|
state.messages.push({
|
|
2047
2633
|
role: "tool",
|
|
2048
2634
|
toolCallId: pendingCall.id,
|
|
2049
|
-
content:
|
|
2635
|
+
content: await this.#toolResultParts(pendingCall, result, state)
|
|
2050
2636
|
});
|
|
2051
2637
|
await this.#drainSurfaceAction(state);
|
|
2052
2638
|
} else if (pendingCall) {
|
|
@@ -2054,7 +2640,7 @@ var AgentLoop = class {
|
|
|
2054
2640
|
state.messages.push({
|
|
2055
2641
|
role: "tool",
|
|
2056
2642
|
toolCallId: pendingCall.id,
|
|
2057
|
-
content:
|
|
2643
|
+
content: await this.#toolResultParts(pendingCall, result, state)
|
|
2058
2644
|
});
|
|
2059
2645
|
await this.#drainSurfaceAction(state);
|
|
2060
2646
|
}
|
|
@@ -2065,7 +2651,7 @@ var AgentLoop = class {
|
|
|
2065
2651
|
state.messages.push({
|
|
2066
2652
|
role: "tool",
|
|
2067
2653
|
toolCallId: next.id,
|
|
2068
|
-
content:
|
|
2654
|
+
content: await this.#toolResultParts(next, result, state)
|
|
2069
2655
|
});
|
|
2070
2656
|
await this.#drainSurfaceAction(state);
|
|
2071
2657
|
}
|
|
@@ -2180,7 +2766,7 @@ var AgentLoop = class {
|
|
|
2180
2766
|
} });
|
|
2181
2767
|
let response;
|
|
2182
2768
|
try {
|
|
2183
|
-
response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs, () => bridge.cancel?.(request.id));
|
|
2769
|
+
response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs, state, () => bridge.cancel?.(request.id));
|
|
2184
2770
|
} catch (e) {
|
|
2185
2771
|
run.status = "running";
|
|
2186
2772
|
run.interruptExpiresAt = void 0;
|
|
@@ -2190,6 +2776,7 @@ var AgentLoop = class {
|
|
|
2190
2776
|
message: e.message,
|
|
2191
2777
|
code: "RUN_INTERACTION_TIMEOUT"
|
|
2192
2778
|
});
|
|
2779
|
+
if (e instanceof WebSkillError && e.code === "RUN_CANCELLED") throw this.#abortedInteraction(state);
|
|
2193
2780
|
state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf(e)}` });
|
|
2194
2781
|
throw new BridgeRequestError(messageOf(e));
|
|
2195
2782
|
}
|
|
@@ -2215,19 +2802,62 @@ var AgentLoop = class {
|
|
|
2215
2802
|
await this.#appendParamHistory(state, request, response.value);
|
|
2216
2803
|
return response.value;
|
|
2217
2804
|
}
|
|
2218
|
-
|
|
2805
|
+
/**
|
|
2806
|
+
* 交互等待被 abort:cancel() 与 totalTimeout 硬期限共用同一个 controller,
|
|
2807
|
+
* 按 `#cancelled` 区分二者,与 LLM 调用被 abort 时的归类保持一致。
|
|
2808
|
+
*/
|
|
2809
|
+
#abortedInteraction(state) {
|
|
2810
|
+
if (this.#cancelled.has(state.runId)) return new RunTerminated({
|
|
2811
|
+
status: "cancelled",
|
|
2812
|
+
reason: "user-cancelled",
|
|
2813
|
+
message: "Run cancelled by user",
|
|
2814
|
+
code: "RUN_CANCELLED"
|
|
2815
|
+
});
|
|
2816
|
+
return new RunTerminated({
|
|
2817
|
+
status: "failed",
|
|
2818
|
+
reason: "timeout",
|
|
2819
|
+
message: `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`,
|
|
2820
|
+
code: "RUN_TIMEOUT"
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
#withInteractionTimeout(promise, timeoutMs, state, onAbandon) {
|
|
2219
2824
|
return new Promise((resolve, reject) => {
|
|
2220
|
-
const
|
|
2825
|
+
const signal = state.controller.signal;
|
|
2826
|
+
let settled = false;
|
|
2827
|
+
const abandon = () => {
|
|
2221
2828
|
try {
|
|
2222
|
-
|
|
2829
|
+
onAbandon?.();
|
|
2223
2830
|
} catch {}
|
|
2831
|
+
};
|
|
2832
|
+
const cleanup = () => {
|
|
2833
|
+
settled = true;
|
|
2834
|
+
clearTimeout(timer);
|
|
2835
|
+
signal.removeEventListener("abort", onAbort);
|
|
2836
|
+
};
|
|
2837
|
+
const onAbort = () => {
|
|
2838
|
+
if (settled) return;
|
|
2839
|
+
cleanup();
|
|
2840
|
+
abandon();
|
|
2841
|
+
reject(new WebSkillError("RUN_CANCELLED", "Interaction aborted because the run was stopped"));
|
|
2842
|
+
};
|
|
2843
|
+
const timer = setTimeout(() => {
|
|
2844
|
+
if (settled) return;
|
|
2845
|
+
cleanup();
|
|
2846
|
+
abandon();
|
|
2224
2847
|
reject(new WebSkillError("RUN_INTERACTION_TIMEOUT", `Interaction timed out after ${timeoutMs}ms`));
|
|
2225
2848
|
}, timeoutMs);
|
|
2849
|
+
if (signal.aborted) {
|
|
2850
|
+
onAbort();
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
signal.addEventListener("abort", onAbort);
|
|
2226
2854
|
promise.then((v) => {
|
|
2227
|
-
|
|
2855
|
+
if (settled) return;
|
|
2856
|
+
cleanup();
|
|
2228
2857
|
resolve(v);
|
|
2229
2858
|
}, (e) => {
|
|
2230
|
-
|
|
2859
|
+
if (settled) return;
|
|
2860
|
+
cleanup();
|
|
2231
2861
|
reject(e instanceof Error ? e : new Error(String(e)));
|
|
2232
2862
|
});
|
|
2233
2863
|
});
|
|
@@ -2247,11 +2877,15 @@ var AgentLoop = class {
|
|
|
2247
2877
|
else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
|
|
2248
2878
|
else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
|
|
2249
2879
|
else {
|
|
2250
|
-
const resolution = resolveToolName(call.name, state.activated);
|
|
2880
|
+
const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
|
|
2251
2881
|
if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
|
|
2252
2882
|
else {
|
|
2253
2883
|
const source = (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
|
|
2254
|
-
result =
|
|
2884
|
+
if (source) result = await source.call(call.name, call.arguments);
|
|
2885
|
+
else {
|
|
2886
|
+
if (resolution.kind === "not-found") state.unknownToolCalls += 1;
|
|
2887
|
+
result = toolError(resolution.code, resolution.message, resolution.kind === "not-found" ? resolution.data : void 0);
|
|
2888
|
+
}
|
|
2255
2889
|
}
|
|
2256
2890
|
}
|
|
2257
2891
|
const durationMs = Date.parse(state.now()) - callStartMs;
|
|
@@ -2271,6 +2905,12 @@ var AgentLoop = class {
|
|
|
2271
2905
|
chart
|
|
2272
2906
|
});
|
|
2273
2907
|
for (const todo of extractTodoTraceEvents(item.data)) state.trace.record(todo.type, { data: todo.data });
|
|
2908
|
+
const candidate = extractSkillCandidate(item.data);
|
|
2909
|
+
if (candidate) try {
|
|
2910
|
+
await this.#deps.uiBridge?.onSkillCandidate?.(state.runId, candidate);
|
|
2911
|
+
} catch (e) {
|
|
2912
|
+
state.trace.record("run.warning", { message: `Skill candidate was not handed off to the UI: ${messageOf(e)}` });
|
|
2913
|
+
}
|
|
2274
2914
|
try {
|
|
2275
2915
|
for (const event of extractUiSpecEvents(item.data)) await this.#renderSurface(state, event);
|
|
2276
2916
|
} catch (e) {
|
|
@@ -2297,6 +2937,12 @@ var AgentLoop = class {
|
|
|
2297
2937
|
artifactId: artifact.id,
|
|
2298
2938
|
path: artifact.path
|
|
2299
2939
|
} });
|
|
2940
|
+
if (state.unknownToolCalls > this.#config.maxUnknownToolRetries) throw new RunTerminated({
|
|
2941
|
+
status: "failed",
|
|
2942
|
+
reason: "tool-resolution-exhausted",
|
|
2943
|
+
message: "Repeated calls to non-existent tools. Stopping to avoid consuming the remaining turn budget.",
|
|
2944
|
+
code: "TOOL_RESOLUTION_EXHAUSTED"
|
|
2945
|
+
});
|
|
2300
2946
|
return result;
|
|
2301
2947
|
}
|
|
2302
2948
|
/** Attaches trusted run provenance, then records only events accepted by the configured bridge. */
|
|
@@ -2410,7 +3056,7 @@ var AgentLoop = class {
|
|
|
2410
3056
|
nonce: request.nonce,
|
|
2411
3057
|
...resumed ? { resumed: true } : {}
|
|
2412
3058
|
} });
|
|
2413
|
-
const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
|
|
3059
|
+
const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, state, () => bridge.cancelSurfaceAction?.(request.nonce));
|
|
2414
3060
|
run.status = "running";
|
|
2415
3061
|
run.interruptExpiresAt = void 0;
|
|
2416
3062
|
state.trace.record("ui.surface-action.resolved", { data: {
|
|
@@ -2437,6 +3083,7 @@ var AgentLoop = class {
|
|
|
2437
3083
|
message: e.message,
|
|
2438
3084
|
code: "RUN_INTERACTION_TIMEOUT"
|
|
2439
3085
|
});
|
|
3086
|
+
if (e instanceof WebSkillError && e.code === "RUN_CANCELLED") throw this.#abortedInteraction(state);
|
|
2440
3087
|
if (e instanceof RunTerminated || e instanceof BridgeRequestError) throw e;
|
|
2441
3088
|
throw new BridgeRequestError(messageOf(e));
|
|
2442
3089
|
} finally {
|
|
@@ -2488,12 +3135,32 @@ var AgentLoop = class {
|
|
|
2488
3135
|
async #handleAskUser(call, state) {
|
|
2489
3136
|
const question = call.arguments["question"];
|
|
2490
3137
|
if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires a non-empty \"question\" string argument");
|
|
3138
|
+
const rawChoices = call.arguments["choices"];
|
|
3139
|
+
const choices = Array.isArray(rawChoices) ? rawChoices.filter((choice) => typeof choice === "string" && choice !== "") : [];
|
|
3140
|
+
const id = this.#nextInteractionId(state);
|
|
3141
|
+
const rawSuggestion = call.arguments["suggestion"];
|
|
3142
|
+
const rawReason = call.arguments["suggestionReason"];
|
|
3143
|
+
const suggestion = this.#deps.userProfile && typeof rawSuggestion === "string" && rawSuggestion !== "" ? { suggestion: {
|
|
3144
|
+
value: rawSuggestion,
|
|
3145
|
+
...typeof rawReason === "string" && rawReason !== "" ? { reason: rawReason } : {}
|
|
3146
|
+
} } : {};
|
|
3147
|
+
const request = choices.length > 0 ? {
|
|
3148
|
+
type: "select",
|
|
3149
|
+
id,
|
|
3150
|
+
message: question,
|
|
3151
|
+
options: choices.map((label) => ({
|
|
3152
|
+
label,
|
|
3153
|
+
value: label
|
|
3154
|
+
})),
|
|
3155
|
+
...suggestion
|
|
3156
|
+
} : {
|
|
3157
|
+
type: "ask",
|
|
3158
|
+
id,
|
|
3159
|
+
message: question,
|
|
3160
|
+
...suggestion
|
|
3161
|
+
};
|
|
2491
3162
|
try {
|
|
2492
|
-
const value = await this.#interact(state, {
|
|
2493
|
-
type: "ask",
|
|
2494
|
-
id: this.#nextInteractionId(state),
|
|
2495
|
-
message: question
|
|
2496
|
-
}, { tool: call.name });
|
|
3163
|
+
const value = await this.#interact(state, request, { tool: call.name });
|
|
2497
3164
|
return {
|
|
2498
3165
|
ok: true,
|
|
2499
3166
|
content: [{
|
|
@@ -2579,6 +3246,26 @@ var AgentLoop = class {
|
|
|
2579
3246
|
return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
|
|
2580
3247
|
}
|
|
2581
3248
|
/**
|
|
3249
|
+
* 工具结果 → tool 消息内容。image 分片单独提出,不进 JSON 也不参与截断计算(0.6.0 §1.1):
|
|
3250
|
+
* 100 KB 的文本预算遇到一张 200 KB 的图会把整个结果截成垃圾。
|
|
3251
|
+
*/
|
|
3252
|
+
async #toolResultParts(call, result, state) {
|
|
3253
|
+
const images = result.content.filter((part) => part.type === "image");
|
|
3254
|
+
if (images.length === 0) return textParts(await this.#serializeToolResult(call, result, state));
|
|
3255
|
+
const textual = {
|
|
3256
|
+
...result,
|
|
3257
|
+
content: result.content.filter((part) => part.type !== "image")
|
|
3258
|
+
};
|
|
3259
|
+
return [{
|
|
3260
|
+
type: "text",
|
|
3261
|
+
text: await this.#serializeToolResult(call, textual, state)
|
|
3262
|
+
}, ...images.map((image) => ({
|
|
3263
|
+
type: "image",
|
|
3264
|
+
mimeType: image.mimeType,
|
|
3265
|
+
data: image.data
|
|
3266
|
+
}))];
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
2582
3269
|
* 工具结果回喂序列化:超过 toolResultMaxBytes(默认 100KB)时头尾保留截断,
|
|
2583
3270
|
* 完整内容经 artifactStore 落 artifact,回喂摘要含 artifact id。
|
|
2584
3271
|
*/
|
|
@@ -2682,32 +3369,36 @@ var AgentLoop = class {
|
|
|
2682
3369
|
}
|
|
2683
3370
|
const executor = this.#deps.executor;
|
|
2684
3371
|
const loaded = [];
|
|
3372
|
+
let scripts = [];
|
|
2685
3373
|
if (executor) {
|
|
2686
3374
|
let scriptFiles;
|
|
2687
3375
|
try {
|
|
2688
|
-
scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) =>
|
|
3376
|
+
scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => `scripts/${s.path.split("/").pop() ?? s.path}`);
|
|
2689
3377
|
} catch (e) {
|
|
2690
3378
|
scriptFiles = [];
|
|
2691
3379
|
if (!(e instanceof WebSkillError && e.code === "FS_NOT_FOUND")) state.trace.record("run.warning", { message: `Failed to list scripts of skill "${skillName}": ${messageOf(e)}` });
|
|
2692
3380
|
}
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
if (
|
|
2696
|
-
|
|
2697
|
-
state.trace.record("run.warning", { message: `Skill "${skillName}" declares allowed-tools, so its script "${match[1]}" is not registered as a tool. Add "${match[1]}" to allowed-tools if that was not intended.` });
|
|
3381
|
+
scripts = listSkillScripts(skillName, scriptFiles, allowedTools);
|
|
3382
|
+
for (const script of scripts) {
|
|
3383
|
+
if (script.toolName === void 0) {
|
|
3384
|
+
state.trace.record("run.warning", { message: `Skill "${skillName}" declares allowed-tools, so its script "${script.scriptName}" is not registered as a tool. Add "${script.scriptName}" to allowed-tools if that was not intended.` });
|
|
2698
3385
|
continue;
|
|
2699
3386
|
}
|
|
2700
3387
|
try {
|
|
2701
|
-
const def = await executor.loadDefinition(root,
|
|
2702
|
-
await this.#enrichDefinition(root,
|
|
3388
|
+
const def = await executor.loadDefinition(root, script.scriptName);
|
|
3389
|
+
script.schemaSource = await this.#enrichDefinition(root, script.scriptName, def, state);
|
|
3390
|
+
script.toolName = def.name;
|
|
2703
3391
|
state.activatedTools.set(def.name, def);
|
|
2704
3392
|
loaded.push(def.name);
|
|
2705
3393
|
} catch (e) {
|
|
2706
|
-
state.trace.record("run.warning", { message: `Failed to load definition of script "${
|
|
3394
|
+
state.trace.record("run.warning", { message: `Failed to load definition of script "${script.scriptName}" for skill "${skillName}": ${messageOf(e)}` });
|
|
3395
|
+
delete script.toolName;
|
|
3396
|
+
script.schemaSource = "unavailable";
|
|
3397
|
+
script.unexposedReason = "its definition could not be loaded";
|
|
2707
3398
|
}
|
|
2708
3399
|
}
|
|
2709
3400
|
}
|
|
2710
|
-
let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
|
|
3401
|
+
let note = (loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "") + formatSkillScriptManifest(scripts);
|
|
2711
3402
|
for (const dep of dependencies) {
|
|
2712
3403
|
if (state.activated.has(dep)) continue;
|
|
2713
3404
|
if (!this.#deps.skillIndex.has(dep)) {
|
|
@@ -2721,53 +3412,86 @@ var AgentLoop = class {
|
|
|
2721
3412
|
/**
|
|
2722
3413
|
* D2 Schema 兜底链(同一 run 内激活时只算一次):
|
|
2723
3414
|
* 显式 inputSchema > sidecar scripts/<name>.schema.json > schemaInferer 推导 > schemaUnavailable
|
|
3415
|
+
* 返回值即最终来源,进技能清单告诉模型该信谁(FR-18.1)。
|
|
2724
3416
|
*/
|
|
2725
3417
|
async #enrichDefinition(skillRoot, scriptName, def, state) {
|
|
2726
|
-
if (def.inputSchema) return;
|
|
3418
|
+
if (def.inputSchema) return "module-export";
|
|
2727
3419
|
const sidecar = `${skillRoot}/scripts/${scriptName}.schema.json`;
|
|
2728
3420
|
if (await this.#deps.fs.exists(sidecar)) try {
|
|
2729
3421
|
def.inputSchema = JSON.parse(await this.#deps.fs.readText(sidecar));
|
|
2730
|
-
return;
|
|
3422
|
+
return "sidecar";
|
|
2731
3423
|
} catch (e) {
|
|
2732
3424
|
state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf(e)}` });
|
|
2733
3425
|
}
|
|
2734
|
-
if (!this.#deps.schemaInferer) return;
|
|
3426
|
+
if (!this.#deps.schemaInferer) return "unavailable";
|
|
2735
3427
|
for (const ext of ["ts", "js"]) {
|
|
2736
3428
|
const scriptPath = `${skillRoot}/scripts/${scriptName}.${ext}`;
|
|
2737
3429
|
if (!await this.#deps.fs.exists(scriptPath)) continue;
|
|
2738
3430
|
try {
|
|
2739
3431
|
const inferred = this.#deps.schemaInferer.inferSchemaFromSource(await this.#deps.fs.readText(scriptPath), { fileName: `${scriptName}.${ext}` });
|
|
2740
|
-
if (inferred)
|
|
3432
|
+
if (inferred) {
|
|
3433
|
+
def.inputSchema = inferred;
|
|
3434
|
+
return "inferred";
|
|
3435
|
+
}
|
|
2741
3436
|
} catch (e) {
|
|
2742
3437
|
state.trace.record("run.warning", { message: `Schema inference failed for ${scriptPath}: ${messageOf(e)}` });
|
|
2743
3438
|
}
|
|
2744
|
-
return;
|
|
3439
|
+
return "unavailable";
|
|
2745
3440
|
}
|
|
3441
|
+
return "unavailable";
|
|
2746
3442
|
}
|
|
2747
3443
|
async #handleScriptTool(call, skillName, scriptName, state) {
|
|
2748
3444
|
const root = this.#deps.skillIndex.get(skillName);
|
|
2749
3445
|
if (!root) return toolError("TOOL_NOT_FOUND", `Skill "${skillName}" is not in the catalog`);
|
|
2750
3446
|
if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
|
|
2751
3447
|
const def = state.activatedTools.get(call.name);
|
|
2752
|
-
if (!def)
|
|
3448
|
+
if (!def) {
|
|
3449
|
+
state.unknownToolCalls += 1;
|
|
3450
|
+
const availableTools = [...state.activatedTools.keys()].filter((tool) => tool.startsWith(`${skillName}__`)).sort();
|
|
3451
|
+
const detail = availableTools.length > 0 ? ` Available tools for skill "${skillName}": ${availableTools.join(", ")}.` : ` Skill "${skillName}" exposes no script tools.`;
|
|
3452
|
+
return toolError("TOOL_NOT_FOUND", `Tool "${call.name}" is not available.${detail}`, {
|
|
3453
|
+
skillName,
|
|
3454
|
+
availableTools
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
2753
3457
|
let args = call.arguments;
|
|
2754
3458
|
const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
|
|
2755
3459
|
if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
|
|
2756
|
-
const fields = schemaToForm(def.inputSchema, args
|
|
2757
|
-
|
|
2758
|
-
const
|
|
2759
|
-
type
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
3460
|
+
const fields = schemaToForm(def.inputSchema, args);
|
|
3461
|
+
const declined = /* @__PURE__ */ new Set();
|
|
3462
|
+
for (const field of fields) {
|
|
3463
|
+
if (field.type !== "file" || !missing.includes(field.name)) continue;
|
|
3464
|
+
const value = await this.#interact(state, {
|
|
3465
|
+
type: "file-pick",
|
|
3466
|
+
id: this.#nextInteractionId(state),
|
|
3467
|
+
message: `Open local file picker to fill field "${field.name}"?`,
|
|
3468
|
+
field: field.name
|
|
3469
|
+
}, {
|
|
3470
|
+
tool: call.name,
|
|
3471
|
+
missing: [field.name]
|
|
3472
|
+
});
|
|
3473
|
+
if (value === void 0 || value === null) declined.add(field.name);
|
|
3474
|
+
else args = {
|
|
3475
|
+
...args,
|
|
3476
|
+
[field.name]: value
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
const stillMissing = missing.filter((key) => args[key] === void 0 && !declined.has(key));
|
|
3480
|
+
if (stillMissing.length > 0) {
|
|
3481
|
+
const value = await this.#interact(state, {
|
|
3482
|
+
type: "form",
|
|
3483
|
+
id: this.#nextInteractionId(state),
|
|
3484
|
+
title: `Missing parameters for ${call.name}`,
|
|
3485
|
+
fields: fields.filter((f) => f.type !== "file" && !declined.has(f.name))
|
|
3486
|
+
}, {
|
|
3487
|
+
tool: call.name,
|
|
3488
|
+
missing: stillMissing
|
|
3489
|
+
});
|
|
3490
|
+
if (typeof value === "object" && value !== null) args = {
|
|
3491
|
+
...args,
|
|
3492
|
+
...value
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
2771
3495
|
} catch (e) {
|
|
2772
3496
|
if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `Parameter form failed: ${e.message}`);
|
|
2773
3497
|
throw e;
|
|
@@ -2798,7 +3522,8 @@ var AgentLoop = class {
|
|
|
2798
3522
|
if (fresh.length > 0) result.artifacts = [...result.artifacts ?? [], ...fresh];
|
|
2799
3523
|
}
|
|
2800
3524
|
await this.#bumpSkillStat(skillName, result.ok ? "successes" : "failures", state);
|
|
2801
|
-
if (
|
|
3525
|
+
if (result.ok) await this.#reportSkillSuccess(skillName, state);
|
|
3526
|
+
else await this.#reportSkillFailure(skillName, state, {
|
|
2802
3527
|
code: result.error?.code ?? "TOOL_EXECUTION_FAILED",
|
|
2803
3528
|
message: result.error?.message ?? `Tool "${call.name}" returned a failure result`
|
|
2804
3529
|
});
|
|
@@ -2835,6 +3560,22 @@ var AgentLoop = class {
|
|
|
2835
3560
|
state.trace.record("run.warning", { message: `Skill failure report for "${skillName}" was not recorded: ${messageOf(e)}` });
|
|
2836
3561
|
}
|
|
2837
3562
|
}
|
|
3563
|
+
/**
|
|
3564
|
+
* D-5 技能成功上报:把失败计数从「累计」变成「连续」的那半边信号。
|
|
3565
|
+
* 与失败上报同样是可选 port,未注入即整段跳过。
|
|
3566
|
+
*/
|
|
3567
|
+
async #reportSkillSuccess(skillName, state) {
|
|
3568
|
+
const report = this.#deps.skillOutcomeReporter?.onSkillSucceeded;
|
|
3569
|
+
if (report === void 0) return;
|
|
3570
|
+
try {
|
|
3571
|
+
await report.call(this.#deps.skillOutcomeReporter, {
|
|
3572
|
+
skillName,
|
|
3573
|
+
runId: state.runId
|
|
3574
|
+
});
|
|
3575
|
+
} catch (e) {
|
|
3576
|
+
state.trace.record("run.warning", { message: `Skill success report for "${skillName}" was not recorded: ${messageOf(e)}` });
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
2838
3579
|
/** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
|
|
2839
3580
|
async #confirm(message, state) {
|
|
2840
3581
|
if (this.#policy.confirmations === "auto-approve") return true;
|
|
@@ -2921,57 +3662,111 @@ var AgentLoop = class {
|
|
|
2921
3662
|
if (!this.#deps.memory) return;
|
|
2922
3663
|
const scope = `session:${state.run.sessionId}`;
|
|
2923
3664
|
const limit = this.#config.paramHistoryLimit;
|
|
3665
|
+
const recorded = request.type === "file-pick" ? redactFileValue(value) : value;
|
|
2924
3666
|
await this.#memoryMutate(scope, "paramHistory", state, (current) => {
|
|
2925
3667
|
const history = current ?? [];
|
|
2926
3668
|
history.push({
|
|
2927
3669
|
ts: state.now(),
|
|
2928
3670
|
interactionId: request.id,
|
|
2929
3671
|
type: request.type,
|
|
2930
|
-
value
|
|
3672
|
+
value: recorded
|
|
2931
3673
|
});
|
|
2932
3674
|
return history.slice(-limit);
|
|
2933
3675
|
});
|
|
2934
|
-
await this.#
|
|
3676
|
+
await this.#recordInteractionBehavior(state, request, value);
|
|
3677
|
+
}
|
|
3678
|
+
/** 用户提问 → 行为记录(FR-19.2)。记的是「问了什么」,不是模型答了什么 */
|
|
3679
|
+
async #recordUserPrompt(state) {
|
|
3680
|
+
const prompt = state.run.userPrompt.trim();
|
|
3681
|
+
if (prompt === "") return;
|
|
3682
|
+
await this.#appendBehaviorRecords(state, [{
|
|
3683
|
+
id: `${state.runId}#prompt`,
|
|
3684
|
+
kind: "question",
|
|
3685
|
+
at: Date.parse(state.now()),
|
|
3686
|
+
value: prompt,
|
|
3687
|
+
scene: { sessionId: state.run.sessionId }
|
|
3688
|
+
}]);
|
|
2935
3689
|
}
|
|
2936
3690
|
/**
|
|
2937
|
-
*
|
|
2938
|
-
*
|
|
3691
|
+
* 交互提交 → 行为记录(FR-19.2)。写在这里而不是 UiBridge:
|
|
3692
|
+
* 只有 AgentLoop 同时握有场景(技能、字段语义、候选集)与提交值。
|
|
2939
3693
|
*/
|
|
2940
|
-
async #
|
|
2941
|
-
const
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
}
|
|
3694
|
+
async #recordInteractionBehavior(state, request, value) {
|
|
3695
|
+
const records = [];
|
|
3696
|
+
const at = Date.parse(state.now());
|
|
3697
|
+
const base = {
|
|
3698
|
+
sessionId: state.run.sessionId,
|
|
3699
|
+
interactionId: request.id
|
|
3700
|
+
};
|
|
3701
|
+
const skill = [...state.activated].at(-1);
|
|
3702
|
+
const scene = skill !== void 0 ? {
|
|
3703
|
+
...base,
|
|
3704
|
+
skillName: skill
|
|
3705
|
+
} : base;
|
|
3706
|
+
if (request.type === "select") records.push({
|
|
3707
|
+
id: `${request.id}#selected`,
|
|
3708
|
+
kind: "choice",
|
|
3709
|
+
at,
|
|
3710
|
+
confirmed: true,
|
|
3711
|
+
value: String(value),
|
|
3712
|
+
scene: {
|
|
3713
|
+
...scene,
|
|
3714
|
+
fieldSemantics: request.message,
|
|
3715
|
+
options: request.options.map((o) => String(o.label))
|
|
3716
|
+
}
|
|
3717
|
+
});
|
|
3718
|
+
else if (request.type === "ask" && typeof value === "string" && value !== "") records.push({
|
|
3719
|
+
id: `${request.id}#answer`,
|
|
3720
|
+
kind: "text-input",
|
|
3721
|
+
at,
|
|
3722
|
+
confirmed: true,
|
|
3723
|
+
value,
|
|
3724
|
+
scene: {
|
|
3725
|
+
...scene,
|
|
3726
|
+
fieldSemantics: request.message
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3729
|
+
else if (request.type === "form" && typeof value === "object" && value !== null) {
|
|
3730
|
+
const submitted = value;
|
|
3731
|
+
for (const field of request.fields) {
|
|
3732
|
+
const next = submitted[field.name];
|
|
3733
|
+
if (typeof next !== "string" && typeof next !== "number" && typeof next !== "boolean") continue;
|
|
3734
|
+
if (next === "") continue;
|
|
3735
|
+
records.push({
|
|
3736
|
+
id: `${request.id}#${field.name}`,
|
|
3737
|
+
kind: field.options ? "choice" : "text-input",
|
|
3738
|
+
at,
|
|
3739
|
+
confirmed: true,
|
|
3740
|
+
value: String(next),
|
|
3741
|
+
scene: {
|
|
3742
|
+
...scene,
|
|
3743
|
+
fieldName: field.name,
|
|
3744
|
+
fieldSemantics: field.description ?? field.label,
|
|
3745
|
+
...field.options ? { options: field.options.map((o) => String(o.label)) } : {}
|
|
3746
|
+
}
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
2974
3749
|
}
|
|
3750
|
+
await this.#appendBehaviorRecords(state, records);
|
|
3751
|
+
}
|
|
3752
|
+
/** 行为记录追加(FR-19.2)。默认关闭:未注入 `userProfile` 时零持久化写入 */
|
|
3753
|
+
async #appendBehaviorRecords(state, records) {
|
|
3754
|
+
const profile = this.#deps.userProfile;
|
|
3755
|
+
if (!profile || !this.#deps.memory || records.length === 0) return;
|
|
3756
|
+
const limits = profile.limits ?? DEFAULT_USER_PROFILE_LIMITS;
|
|
3757
|
+
await this.#memoryMutate(`user:${profile.userId}`, BEHAVIOR_RECORDS_KEY, state, (current) => appendBehaviorRecords(readBehaviorRecords(current), records, limits));
|
|
3758
|
+
}
|
|
3759
|
+
/** 画像 → 系统消息(FR-19.4)。读不到或为空时不注入任何内容 */
|
|
3760
|
+
async #userProfileMessage(state) {
|
|
3761
|
+
const profile = this.#deps.userProfile;
|
|
3762
|
+
if (!profile || !this.#deps.memory) return void 0;
|
|
3763
|
+
const limits = profile.limits ?? DEFAULT_USER_PROFILE_LIMITS;
|
|
3764
|
+
const context = renderUserProfileContext(readUserProfile(await this.#memoryGet(`user:${profile.userId}`, USER_PROFILE_KEY, state)), limits.injectMaxBytes);
|
|
3765
|
+
if (context === "") return void 0;
|
|
3766
|
+
return {
|
|
3767
|
+
role: "system",
|
|
3768
|
+
content: textParts(`${context}\n\n${USER_PROFILE_NO_INVENTION_RULE}`)
|
|
3769
|
+
};
|
|
2975
3770
|
}
|
|
2976
3771
|
};
|
|
2977
3772
|
/**
|
|
@@ -3083,6 +3878,7 @@ var WebSkillRuntime = class {
|
|
|
3083
3878
|
return result;
|
|
3084
3879
|
}
|
|
3085
3880
|
async run(userPrompt, options = {}) {
|
|
3881
|
+
if (options.skillName !== void 0 && this.#deps.config?.toolCallingDisabled === true) throw new WebSkillError("MODEL_TOOLS_UNSUPPORTED", "The selected model does not support skill invocation. Switch to a model with tool calling.");
|
|
3086
3882
|
if (!this.#catalogCache) await this.discover();
|
|
3087
3883
|
const cache = this.#catalogCache;
|
|
3088
3884
|
if (!cache) throw new Error("discover() did not populate the catalog cache");
|
|
@@ -3097,7 +3893,9 @@ var WebSkillRuntime = class {
|
|
|
3097
3893
|
}))).flat();
|
|
3098
3894
|
const catalog = providerEntries.length > 0 ? { entries: mergeCatalogEntries(cache.catalog.entries, providerEntries) } : cache.catalog;
|
|
3099
3895
|
const filteredCatalog = this.#deps.catalogFilter ? { entries: await this.#deps.catalogFilter(catalog.entries) } : catalog;
|
|
3100
|
-
const
|
|
3896
|
+
const routedCatalog = options.skillName === void 0 ? filteredCatalog : { entries: filteredCatalog.entries.filter((entry) => entry.name === options.skillName) };
|
|
3897
|
+
if (options.skillName !== void 0 && routedCatalog.entries.length === 0) throw new WebSkillError("SKILL_NOT_FOUND", `Skill "${options.skillName}" is not available in this catalog`);
|
|
3898
|
+
const route = await this.#router.route(routedCatalog);
|
|
3101
3899
|
const loop = new AgentLoop({
|
|
3102
3900
|
llm: this.#deps.llm,
|
|
3103
3901
|
executor: this.#deps.executor,
|
|
@@ -3112,7 +3910,7 @@ var WebSkillRuntime = class {
|
|
|
3112
3910
|
hooks: this.#deps.hooks,
|
|
3113
3911
|
eventBus: this.#events,
|
|
3114
3912
|
longTerm: this.#deps.longTerm,
|
|
3115
|
-
|
|
3913
|
+
userProfile: this.#deps.userProfile,
|
|
3116
3914
|
externalTools: this.#deps.externalTools,
|
|
3117
3915
|
skillProviders: this.#deps.skillProviders,
|
|
3118
3916
|
catalogFilter: this.#deps.catalogFilter,
|
|
@@ -3143,7 +3941,7 @@ var WebSkillRuntime = class {
|
|
|
3143
3941
|
message: `Skill provider listSkills() failed: ${failure}`
|
|
3144
3942
|
});
|
|
3145
3943
|
if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
|
|
3146
|
-
prompt: userPrompt,
|
|
3944
|
+
prompt: promptText(userPrompt),
|
|
3147
3945
|
run: result.run
|
|
3148
3946
|
}).catch((e) => {
|
|
3149
3947
|
result.run.trace.push({
|
|
@@ -3219,7 +4017,7 @@ var WebSkillRuntime = class {
|
|
|
3219
4017
|
hooks: this.#deps.hooks,
|
|
3220
4018
|
eventBus: this.#events,
|
|
3221
4019
|
longTerm: this.#deps.longTerm,
|
|
3222
|
-
|
|
4020
|
+
userProfile: this.#deps.userProfile,
|
|
3223
4021
|
externalTools: this.#deps.externalTools,
|
|
3224
4022
|
skillProviders: this.#deps.skillProviders,
|
|
3225
4023
|
catalogFilter: this.#deps.catalogFilter,
|
|
@@ -3474,6 +4272,191 @@ var FsMemoryStore = class {
|
|
|
3474
4272
|
for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
|
|
3475
4273
|
}
|
|
3476
4274
|
};
|
|
4275
|
+
/**
|
|
4276
|
+
* 提炼提示词。要求返回结论列表而不是散文:条目化是「可见、可编辑、可删除」的前提(FR-19.3)。
|
|
4277
|
+
* `id` 由模型给出稳定 slug,才能在下一次提炼时更新同一条而不是无限追加。
|
|
4278
|
+
*/
|
|
4279
|
+
const USER_PROFILE_REFINE_PROMPT = `You summarize what a single user is like, based on records of what they did in a chat application.
|
|
4280
|
+
|
|
4281
|
+
Return ONLY a JSON object with this shape, and nothing else:
|
|
4282
|
+
|
|
4283
|
+
{
|
|
4284
|
+
"entries": [
|
|
4285
|
+
{ "id": "slug-case-stable-id", "text": "One sentence about the user.", "confidence": "high" | "low" }
|
|
4286
|
+
]
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
Rules:
|
|
4290
|
+
- 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.
|
|
4291
|
+
- Reuse the same "id" across runs for the same conclusion so it can be updated instead of duplicated. Use lowercase words joined by hyphens.
|
|
4292
|
+
- Use "high" only when several records agree. A single record is "low".
|
|
4293
|
+
- Do not restate individual records. Do not include secrets, credentials, or full personal identifiers.
|
|
4294
|
+
- Do not invent anything the records do not support. If the records say nothing useful, return {"entries": []}.`;
|
|
4295
|
+
/** 采样后的记录 → 送模型的摘要。导出供测试与宿主复核实际请求内容(AC-19.6) @experimental */
|
|
4296
|
+
function toRecordDigests(records) {
|
|
4297
|
+
return records.map((record) => ({
|
|
4298
|
+
kind: record.kind,
|
|
4299
|
+
at: new Date(record.at).toISOString(),
|
|
4300
|
+
value: record.value,
|
|
4301
|
+
...record.scene.skillName !== void 0 ? { skill: record.scene.skillName } : {},
|
|
4302
|
+
...record.scene.fieldName !== void 0 ? { field: record.scene.fieldName } : {},
|
|
4303
|
+
...record.scene.fieldSemantics !== void 0 ? { meaning: record.scene.fieldSemantics } : {},
|
|
4304
|
+
...record.scene.options !== void 0 ? { options: record.scene.options } : {},
|
|
4305
|
+
...record.confirmed === true ? { confirmed: true } : {}
|
|
4306
|
+
}));
|
|
4307
|
+
}
|
|
4308
|
+
/** 模型可能把 JSON 包在 ```json 里;只取第一个对象,解析不出就当没结论 */
|
|
4309
|
+
function parseEntries(content, now) {
|
|
4310
|
+
const text = promptText(content ?? []);
|
|
4311
|
+
const start = text.indexOf("{");
|
|
4312
|
+
const end = text.lastIndexOf("}");
|
|
4313
|
+
if (start < 0 || end <= start) return [];
|
|
4314
|
+
let parsed;
|
|
4315
|
+
try {
|
|
4316
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
4317
|
+
} catch {
|
|
4318
|
+
return [];
|
|
4319
|
+
}
|
|
4320
|
+
const raw = parsed?.entries;
|
|
4321
|
+
return readProfileEntries(Array.isArray(raw) ? raw.map((item) => ({
|
|
4322
|
+
updatedAt: now,
|
|
4323
|
+
...item
|
|
4324
|
+
})) : []);
|
|
4325
|
+
}
|
|
4326
|
+
/**
|
|
4327
|
+
* 一次提炼(FR-19.3)。调用方负责持久化返回值;本函数不碰存储,
|
|
4328
|
+
* 也不吞异常——「失败不影响会话结束」由调用点的 fire-and-forget 保证,
|
|
4329
|
+
* 在这里吞掉会让手动提炼入口无法报错。
|
|
4330
|
+
* @experimental
|
|
4331
|
+
*/
|
|
4332
|
+
async function refineUserProfile(input) {
|
|
4333
|
+
const limits = input.limits ?? DEFAULT_USER_PROFILE_LIMITS;
|
|
4334
|
+
const now = (input.now ?? Date.now)();
|
|
4335
|
+
const current = input.profile ?? EMPTY_USER_PROFILE;
|
|
4336
|
+
const sampled = sampleBehaviorRecords(input.records, limits);
|
|
4337
|
+
if (sampled.length === 0) return current;
|
|
4338
|
+
const existing = current.entries.map((entry) => ({
|
|
4339
|
+
id: entry.id,
|
|
4340
|
+
text: entry.text
|
|
4341
|
+
}));
|
|
4342
|
+
const entries = parseEntries((await input.llm.complete({
|
|
4343
|
+
...input.model !== void 0 ? { model: input.model } : {},
|
|
4344
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
4345
|
+
messages: [{
|
|
4346
|
+
role: "system",
|
|
4347
|
+
content: [{
|
|
4348
|
+
type: "text",
|
|
4349
|
+
text: USER_PROFILE_REFINE_PROMPT
|
|
4350
|
+
}]
|
|
4351
|
+
}, {
|
|
4352
|
+
role: "user",
|
|
4353
|
+
content: [{
|
|
4354
|
+
type: "text",
|
|
4355
|
+
text: `Existing conclusions:\n${JSON.stringify(existing)}\n\nRecords:\n${JSON.stringify(toRecordDigests(sampled))}`
|
|
4356
|
+
}]
|
|
4357
|
+
}]
|
|
4358
|
+
})).content, now);
|
|
4359
|
+
if (entries.length === 0) return current;
|
|
4360
|
+
return mergeProfileEntries(current, entries, {
|
|
4361
|
+
now,
|
|
4362
|
+
limits
|
|
4363
|
+
});
|
|
4364
|
+
}
|
|
4365
|
+
/** 导出文件的版本;导入时逐字校验,不做向前兼容猜测 @experimental */
|
|
4366
|
+
const USER_PROFILE_EXPORT_VERSION = 1;
|
|
4367
|
+
/** 与偏好导出同一套判据:归一化后按子串匹配,命中即拒 */
|
|
4368
|
+
const CREDENTIAL_MARKERS = [
|
|
4369
|
+
"apikey",
|
|
4370
|
+
"secret",
|
|
4371
|
+
"token",
|
|
4372
|
+
"password",
|
|
4373
|
+
"passphrase",
|
|
4374
|
+
"credential",
|
|
4375
|
+
"privatekey"
|
|
4376
|
+
];
|
|
4377
|
+
const isCredentialKey = (key) => {
|
|
4378
|
+
const normalized = key.toLowerCase().replace(/[-_\s]/g, "");
|
|
4379
|
+
return CREDENTIAL_MARKERS.some((marker) => normalized.includes(marker));
|
|
4380
|
+
};
|
|
4381
|
+
function findCredentialKey(value) {
|
|
4382
|
+
if (Array.isArray(value)) {
|
|
4383
|
+
for (const item of value) {
|
|
4384
|
+
const hit = findCredentialKey(item);
|
|
4385
|
+
if (hit !== void 0) return hit;
|
|
4386
|
+
}
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
4390
|
+
for (const [key, child] of Object.entries(value)) {
|
|
4391
|
+
if (isCredentialKey(key)) return key;
|
|
4392
|
+
const hit = findCredentialKey(child);
|
|
4393
|
+
if (hit !== void 0) return hit;
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
/**
|
|
4397
|
+
* 导出画像(FR-19.7)。`excludeIds` 来自导出前的审阅界面:用户勾掉的条目不进文件。
|
|
4398
|
+
* @experimental
|
|
4399
|
+
*/
|
|
4400
|
+
function exportUserProfile(profile, options) {
|
|
4401
|
+
const excluded = new Set(options.excludeIds ?? []);
|
|
4402
|
+
const result = {
|
|
4403
|
+
kind: "webskill.user-profile",
|
|
4404
|
+
version: 1,
|
|
4405
|
+
origin: options.origin,
|
|
4406
|
+
exportedAt: (options.now ?? Date.now)(),
|
|
4407
|
+
entries: profile.entries.filter((entry) => !excluded.has(entry.id))
|
|
4408
|
+
};
|
|
4409
|
+
const leaked = findCredentialKey(result);
|
|
4410
|
+
if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `A user profile export must not contain credentials, but "${leaked}" was present`);
|
|
4411
|
+
return result;
|
|
4412
|
+
}
|
|
4413
|
+
/** 解析并校验导入内容(FR-19.7)。含凭据是**拒绝**而不是忽略 @experimental */
|
|
4414
|
+
function parseUserProfileExport(raw) {
|
|
4415
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must be a JSON object");
|
|
4416
|
+
const record = raw;
|
|
4417
|
+
if (record["kind"] !== "webskill.user-profile") throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must have \"kind\": \"webskill.user-profile\"");
|
|
4418
|
+
if (record["version"] !== 1) throw new WebSkillError("PROFILE_IMPORT_VERSION_UNSUPPORTED", `Unsupported user profile file version ${JSON.stringify(record["version"])}; expected 1`);
|
|
4419
|
+
const leaked = findCredentialKey(record);
|
|
4420
|
+
if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `The user profile file contains a credential field "${leaked}"; the import was rejected`);
|
|
4421
|
+
const entries = readProfileEntries(record["entries"]);
|
|
4422
|
+
if (entries.length === 0) throw new WebSkillError("PROFILE_IMPORT_INVALID", "The user profile file has no usable entries");
|
|
4423
|
+
return {
|
|
4424
|
+
kind: "webskill.user-profile",
|
|
4425
|
+
version: 1,
|
|
4426
|
+
origin: typeof record["origin"] === "string" ? record["origin"] : "unknown",
|
|
4427
|
+
exportedAt: typeof record["exportedAt"] === "number" ? record["exportedAt"] : 0,
|
|
4428
|
+
entries
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
/** 导入前给用户看的差异(FR-19.7):新增哪些、覆盖哪些 @experimental */
|
|
4432
|
+
function diffUserProfile(current, incoming) {
|
|
4433
|
+
const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
|
|
4434
|
+
const added = [];
|
|
4435
|
+
const updated = [];
|
|
4436
|
+
for (const entry of incoming.entries) {
|
|
4437
|
+
const existing = byId.get(entry.id);
|
|
4438
|
+
if (existing === void 0) added.push(entry);
|
|
4439
|
+
else if (existing.text !== entry.text) updated.push(entry);
|
|
4440
|
+
}
|
|
4441
|
+
return {
|
|
4442
|
+
added,
|
|
4443
|
+
updated
|
|
4444
|
+
};
|
|
4445
|
+
}
|
|
4446
|
+
/** 确认后落库:同 id 覆盖,其余保留;不触碰模型配置与凭据 @experimental */
|
|
4447
|
+
function applyUserProfileImport(current, incoming, options = {}) {
|
|
4448
|
+
const now = (options.now ?? Date.now)();
|
|
4449
|
+
const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
|
|
4450
|
+
for (const entry of incoming.entries) byId.set(entry.id, {
|
|
4451
|
+
...entry,
|
|
4452
|
+
updatedAt: now
|
|
4453
|
+
});
|
|
4454
|
+
return {
|
|
4455
|
+
version: 1,
|
|
4456
|
+
entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, DEFAULT_USER_PROFILE_LIMITS.entryLimit),
|
|
4457
|
+
refinedAt: current.refinedAt
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
3477
4460
|
const INDEX_FILE$1 = "index.json";
|
|
3478
4461
|
/**
|
|
3479
4462
|
* 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
|
|
@@ -3825,12 +4808,16 @@ var FsRunTraceStore = class {
|
|
|
3825
4808
|
#root;
|
|
3826
4809
|
#fs;
|
|
3827
4810
|
#onError;
|
|
4811
|
+
#onIndexError;
|
|
3828
4812
|
constructor(deps) {
|
|
3829
4813
|
this.#root = deps.root.replace(/\/+$/, "");
|
|
3830
4814
|
this.#fs = deps.fs;
|
|
3831
4815
|
this.#onError = deps.onError ?? ((error, run) => {
|
|
3832
4816
|
console.warn(`Failed to persist run trace "${run.id}": ${messageOf(error)}`);
|
|
3833
4817
|
});
|
|
4818
|
+
this.#onIndexError = deps.onIndexError ?? ((error, run) => {
|
|
4819
|
+
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.`);
|
|
4820
|
+
});
|
|
3834
4821
|
}
|
|
3835
4822
|
#path(runId) {
|
|
3836
4823
|
return resolveInsideRoot(this.#root, `${runId}.json`);
|
|
@@ -3848,9 +4835,14 @@ var FsRunTraceStore = class {
|
|
|
3848
4835
|
};
|
|
3849
4836
|
try {
|
|
3850
4837
|
await this.#fs.writeText(this.#path(run.id), JSON.stringify(trace, null, 2));
|
|
3851
|
-
await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
|
|
3852
4838
|
} catch (e) {
|
|
3853
4839
|
this.#onError(e, run);
|
|
4840
|
+
return;
|
|
4841
|
+
}
|
|
4842
|
+
try {
|
|
4843
|
+
await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
|
|
4844
|
+
} catch (e) {
|
|
4845
|
+
this.#onIndexError(e, run);
|
|
3854
4846
|
}
|
|
3855
4847
|
}
|
|
3856
4848
|
async get(runId) {
|
|
@@ -4001,6 +4993,7 @@ const toMeta = (record) => ({
|
|
|
4001
4993
|
messageCount: record.messages.length
|
|
4002
4994
|
});
|
|
4003
4995
|
const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4996
|
+
const baseName = (path) => path.split("/").pop() ?? path;
|
|
4004
4997
|
/**
|
|
4005
4998
|
* 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
|
|
4006
4999
|
* 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
|
|
@@ -4094,8 +5087,13 @@ var FsSessionStore = class {
|
|
|
4094
5087
|
return record;
|
|
4095
5088
|
}
|
|
4096
5089
|
async list(options = {}) {
|
|
4097
|
-
if (!await this.#fs.exists(this.#root)) return {
|
|
5090
|
+
if (!await this.#fs.exists(this.#root)) return {
|
|
5091
|
+
items: [],
|
|
5092
|
+
source: "missing-root",
|
|
5093
|
+
skipped: []
|
|
5094
|
+
};
|
|
4098
5095
|
const metas = [];
|
|
5096
|
+
const skipped = [];
|
|
4099
5097
|
for (const entry of await this.#fs.list(this.#root)) {
|
|
4100
5098
|
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
4101
5099
|
let record;
|
|
@@ -4103,13 +5101,18 @@ var FsSessionStore = class {
|
|
|
4103
5101
|
record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
|
|
4104
5102
|
} catch (e) {
|
|
4105
5103
|
console.warn(`Skipping unreadable session file "${entry.path}": ${e instanceof Error ? e.message : String(e)}`);
|
|
5104
|
+
skipped.push(baseName(entry.path));
|
|
4106
5105
|
continue;
|
|
4107
5106
|
}
|
|
4108
5107
|
if (record.archived === true && options.includeArchived !== true) continue;
|
|
4109
5108
|
metas.push(toMeta(record));
|
|
4110
5109
|
}
|
|
4111
5110
|
metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
4112
|
-
return
|
|
5111
|
+
return {
|
|
5112
|
+
...takeTailPage(metas, options, "session"),
|
|
5113
|
+
source: "ok",
|
|
5114
|
+
skipped
|
|
5115
|
+
};
|
|
4113
5116
|
}
|
|
4114
5117
|
/**
|
|
4115
5118
|
* 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
|
|
@@ -4176,4 +5179,4 @@ var FsSessionStore = class {
|
|
|
4176
5179
|
};
|
|
4177
5180
|
|
|
4178
5181
|
//#endregion
|
|
4179
|
-
export {
|
|
5182
|
+
export { isNetworkAllowed as $, SerializingMemoryStore as A, bridgeError as B, ProgressiveRouter as C, toRecordDigests as Ct, RUN_SNAPSHOT_SCHEMA_VERSION as D, READ_SKILL_FILE_TOOL_NAME as E, validateUiSpecNode as Et, USER_PROFILE_PROMPT_HEADER as F, exportUserProfile as G, createScriptContext as H, USER_PROFILE_REFINE_PROMPT as I, extractTodoTraceEvents as J, extractChartSpec as K, WebSkillRuntime as L, USER_PROFILE_EXPORT_VERSION as M, USER_PROFILE_KEY as N, RUN_TRACE_SCHEMA_VERSION as O, USER_PROFILE_NO_INVENTION_RULE as P, fromVercelStreamPart as Q, appendBehaviorRecords as R, OpenAiCompatibleClient as S, toLlmToolSpec as St, READ_SKILL_FILE_TOOL as T, validateUiSpecEvent as Tt, createWebSkillApi as U, buildRenderResult as V, diffUserProfile as W, formatSkillScriptManifest as X, extractUiSpecEvents as Y, fromVercelResult as Z, FsRunTraceStore as _, sampleBehaviorRecords as _t, AgentLoop as a, networkUrlHost as at, GoogleGenAiClient as b, scriptToolName as bt, CapabilityApproval as c, normalizeToolError as ct, EMPTY_USER_PROFILE as d, readBehaviorRecords as dt, isUnsupportedRunSnapshot as et, EventBus as f, readProfileEntries as ft, FsRunSnapshotStore as g, resolveToolName as gt, FsMemoryStore as h, renderUserProfileContext as ht, ASK_USER_TOOL_NAME as i, networkPolicyLibSource as it, TraceRecorder as j, SESSION_SCHEMA_VERSION as k, DEFAULT_LOOP_LIMITS as l, parseBridgeRequest as lt, FsArtifactStore as m, refineUserProfile as mt, ASK_USER_INPUT_SCHEMA as n, mergeCatalogEntries as nt, AnthropicClient as o, normalizeErrorCode as ot, FS_SESSION_PAGE_SIZE as p, readUserProfile as pt, extractSkillCandidate as q, ASK_USER_TOOL as r, mergeProfileEntries as rt, BEHAVIOR_RECORDS_KEY as s, normalizeToolContent as st, ALLOWED_TOOLS_EXCLUSION_REASON as t, listSkillScripts as tt, DEFAULT_USER_PROFILE_LIMITS as u, parseUserProfileExport as ut, FsSessionStore as v, schemaSourceLabel as vt, READ_SKILL_FILE_INPUT_SCHEMA as w, toVercelToolSpecs as wt, HookRunner as x, summarizeToolCalls as xt, FullDisclosureRouter as y, schemaToForm as yt, applyUserProfileImport as z };
|