@webskill/sdk 0.5.0 → 0.6.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 +57 -15
- package/dist/browser.d.ts +101 -5
- package/dist/browser.js +421 -11
- package/dist/{catalogComponents-DV7cPpUm-C77AEEx9.js → catalogComponents-Dr5dFMAb-Dacibl1e.js} +126 -44
- package/dist/{dist-bewtXYlO.js → dist-DnYG2-eY.js} +53 -14
- package/dist/{dist-6C03DShK.js → dist-DusANsrn.js} +981 -185
- 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/governance.d.ts +3 -3
- package/dist/governance.js +1 -1
- package/dist/{index-Bsqg4ftU.d.ts → index-BMocOEi0.d.ts} +13 -7
- package/dist/{index-D_7ZZjkl.d.ts → index-BuTpBMzr.d.ts} +69 -6
- package/dist/{index-vBz_FC9w.d.ts → index-C-KFAZoF.d.ts} +298 -52
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +39 -8
- package/dist/mcp.js +53 -19
- package/dist/{memoryArtifactStore-BtOeB_hm-tj3fC5ip.js → memoryArtifactStore-52Zn9npI-BMPYwvoy.js} +10 -2
- package/dist/node.d.ts +4 -4
- package/dist/node.js +9 -2
- package/dist/{openUiLibrary-W3Ce896k-ClFTRZFs.js → openUiLibrary-Bdrji9qK-DzAxRlTY.js} +3 -3
- package/dist/{skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts → skillVersionStore-BzLbzFOL-CxdAFWO2.d.ts} +1 -1
- 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-4pg-qp_I-Gq63X8Oa.d.ts} +33 -8
- package/dist/ui-react.d.ts +2 -2
- package/dist/ui-react.js +66 -23
- 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,201 @@
|
|
|
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」的映射。 */
|
|
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$4(value) {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
function isSelfRef(value) {
|
|
48
|
+
return isRecord$4(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$4(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$4(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$4(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$4(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$4(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$4(resource)) return schema;
|
|
168
|
+
const existingDefs = isRecord$4(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
|
+
}
|
|
5
199
|
function createSseFrameReader() {
|
|
6
200
|
let buffer = "";
|
|
7
201
|
let data = [];
|
|
@@ -113,7 +307,7 @@ const toOpenAiTools = (tools) => tools.map((tool) => ({
|
|
|
113
307
|
function: {
|
|
114
308
|
name: tool.name,
|
|
115
309
|
...tool.description ? { description: tool.description } : {},
|
|
116
|
-
parameters: tool.inputSchema
|
|
310
|
+
parameters: toPortableToolSchema(tool.inputSchema)
|
|
117
311
|
}
|
|
118
312
|
}));
|
|
119
313
|
const errorMessage$2 = (e) => e instanceof Error ? e.message : String(e);
|
|
@@ -264,11 +458,18 @@ var OpenAiCompatibleClient = class {
|
|
|
264
458
|
const choice = data.choices?.[0]?.message;
|
|
265
459
|
if (!choice) throw new Error("response has no choices[0].message");
|
|
266
460
|
const toolCalls = choice["tool_calls"]?.map((tc) => {
|
|
267
|
-
|
|
461
|
+
let args = {};
|
|
462
|
+
let parseError;
|
|
463
|
+
try {
|
|
464
|
+
args = JSON.parse(tc.function?.arguments ?? "{}");
|
|
465
|
+
} catch (e) {
|
|
466
|
+
parseError = e instanceof Error ? e.message : String(e);
|
|
467
|
+
}
|
|
268
468
|
return {
|
|
269
469
|
id: tc.id,
|
|
270
470
|
name: tc.function?.name ?? "",
|
|
271
|
-
arguments:
|
|
471
|
+
arguments: args,
|
|
472
|
+
...parseError ? { argumentsParseError: parseError } : {}
|
|
272
473
|
};
|
|
273
474
|
});
|
|
274
475
|
const content = choice["content"];
|
|
@@ -363,7 +564,7 @@ function toAnthropicMessages(messages) {
|
|
|
363
564
|
const toAnthropicTools = (tools) => tools.map((tool) => ({
|
|
364
565
|
name: tool.name,
|
|
365
566
|
...tool.description ? { description: tool.description } : {},
|
|
366
|
-
input_schema: tool.inputSchema
|
|
567
|
+
input_schema: toPortableToolSchema(tool.inputSchema)
|
|
367
568
|
}));
|
|
368
569
|
/** Anthropic Messages API 客户端(零依赖 fetch;Node/浏览器通用) */
|
|
369
570
|
var AnthropicClient = class {
|
|
@@ -606,7 +807,7 @@ function toGenAiContents(messages) {
|
|
|
606
807
|
const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => ({
|
|
607
808
|
name: tool.name,
|
|
608
809
|
...tool.description ? { description: tool.description } : {},
|
|
609
|
-
parameters: tool.inputSchema
|
|
810
|
+
parameters: toPortableToolSchema(tool.inputSchema)
|
|
610
811
|
})) }];
|
|
611
812
|
/** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
|
|
612
813
|
var GoogleGenAiClient = class {
|
|
@@ -800,7 +1001,9 @@ Skills are disclosed progressively: the catalog below only shows metadata. To us
|
|
|
800
1001
|
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
1002
|
2. Call the activated script tools to do the work.
|
|
802
1003
|
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
|
|
1004
|
+
4. When the task is complete, answer in plain text without calling any tool.
|
|
1005
|
+
|
|
1006
|
+
Never claim to have created, saved, or installed a skill unless a skill tool call actually succeeded.`;
|
|
804
1007
|
/** 渐进披露路由(默认):system prompt 只注入 Catalog 元数据 + 工具使用说明 */
|
|
805
1008
|
var ProgressiveRouter = class {
|
|
806
1009
|
async route(catalog) {
|
|
@@ -814,7 +1017,9 @@ var ProgressiveRouter = class {
|
|
|
814
1017
|
const INSTRUCTIONS = `You are an agent that completes tasks by using skills.
|
|
815
1018
|
The full instructions of every available skill are injected below. Follow them directly.
|
|
816
1019
|
Script tools are named "<skillName>__<scriptName>".
|
|
817
|
-
When the task is complete, answer in plain text without calling any tool
|
|
1020
|
+
When the task is complete, answer in plain text without calling any tool.
|
|
1021
|
+
|
|
1022
|
+
Never claim to have created, saved, or installed a skill unless a skill tool call actually succeeded.`;
|
|
818
1023
|
/** 全量披露路由(对照用):注入每个技能的完整 SKILL.md 正文 */
|
|
819
1024
|
var FullDisclosureRouter = class {
|
|
820
1025
|
#discovery;
|
|
@@ -841,7 +1046,7 @@ var FullDisclosureRouter = class {
|
|
|
841
1046
|
* 3. `mcp#` 前缀 → TOOL_UNSUPPORTED(WebMCP 阶段实现)
|
|
842
1047
|
* 4. 其余 → TOOL_NOT_FOUND
|
|
843
1048
|
*/
|
|
844
|
-
function resolveToolName(name, activatedSkills) {
|
|
1049
|
+
function resolveToolName(name, activatedSkills, activatedTools = []) {
|
|
845
1050
|
const sep = name.indexOf("__");
|
|
846
1051
|
if (sep > 0) {
|
|
847
1052
|
const skillName = name.slice(0, sep);
|
|
@@ -862,10 +1067,12 @@ function resolveToolName(name, activatedSkills) {
|
|
|
862
1067
|
code: "TOOL_UNSUPPORTED",
|
|
863
1068
|
message: `Tool "${name}" is a WebMCP tool, which is not supported yet`
|
|
864
1069
|
};
|
|
1070
|
+
const availableTools = [...activatedTools].sort();
|
|
865
1071
|
return {
|
|
866
1072
|
kind: "not-found",
|
|
867
1073
|
code: "TOOL_NOT_FOUND",
|
|
868
|
-
message: `Unknown tool "${name}".
|
|
1074
|
+
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."}`,
|
|
1075
|
+
data: { availableTools }
|
|
869
1076
|
};
|
|
870
1077
|
}
|
|
871
1078
|
/** ToolDefinition → LLM 工具 spec;缺 schema 时用宽松 object 兜底并在描述中标记 */
|
|
@@ -875,7 +1082,7 @@ function toLlmToolSpec(tool) {
|
|
|
875
1082
|
...tool.description ? { description: tool.description } : {},
|
|
876
1083
|
inputSchema: tool.inputSchema
|
|
877
1084
|
};
|
|
878
|
-
const note = "Input schema unavailable;
|
|
1085
|
+
const note = "Input schema unavailable for this script. Inspect the skill documentation before calling; do not assume it takes no arguments.";
|
|
879
1086
|
return {
|
|
880
1087
|
name: tool.name,
|
|
881
1088
|
description: tool.description ? `${tool.description} (${note})` : note,
|
|
@@ -885,10 +1092,57 @@ function toLlmToolSpec(tool) {
|
|
|
885
1092
|
}
|
|
886
1093
|
};
|
|
887
1094
|
}
|
|
1095
|
+
/** 工具名规则:技能**目录名**加双下划线加脚本名,与 frontmatter 的 name 无关 */
|
|
1096
|
+
function scriptToolName(skillName, scriptName) {
|
|
1097
|
+
return `${skillName}__${scriptName}`;
|
|
1098
|
+
}
|
|
1099
|
+
const SCHEMA_SOURCE_TEXT = {
|
|
1100
|
+
"module-export": "from module export",
|
|
1101
|
+
sidecar: "from the .schema.json sidecar",
|
|
1102
|
+
inferred: "inferred from source",
|
|
1103
|
+
unavailable: "unavailable",
|
|
1104
|
+
unknown: "not loaded yet"
|
|
1105
|
+
};
|
|
1106
|
+
/** 展示用的 schema 来源文案(console 与模型清单共用一套措辞) */
|
|
1107
|
+
function schemaSourceLabel(source) {
|
|
1108
|
+
return SCHEMA_SOURCE_TEXT[source];
|
|
1109
|
+
}
|
|
1110
|
+
/** allowed-tools 排除的脚本对模型不可见;清单里必须写明原因,否则作者只会看到工具凭空消失 */
|
|
1111
|
+
const ALLOWED_TOOLS_EXCLUSION_REASON = "excluded by the allowed-tools list in SKILL.md";
|
|
1112
|
+
/**
|
|
1113
|
+
* 从技能文件清单中挑出脚本并给出工具名映射。
|
|
1114
|
+
* @param files 技能根目录下的相对路径(如 `scripts/parse.ts`、`SKILL.md`)
|
|
1115
|
+
* @param allowedTools SKILL.md 声明的 allowed-tools;未声明传 undefined(全部暴露)
|
|
1116
|
+
*/
|
|
1117
|
+
function listSkillScripts(skillName, files, allowedTools) {
|
|
1118
|
+
const sidecars = new Set(files.filter((path) => path.startsWith("scripts/") && path.endsWith(".schema.json")));
|
|
1119
|
+
const out = [];
|
|
1120
|
+
for (const path of [...files].sort()) {
|
|
1121
|
+
const scriptName = /^scripts\/([^/]+)\.(?:ts|js)$/.exec(path)?.[1];
|
|
1122
|
+
if (scriptName === void 0 || scriptName === "") continue;
|
|
1123
|
+
const excluded = allowedTools !== void 0 && !allowedTools.includes(scriptName);
|
|
1124
|
+
out.push({
|
|
1125
|
+
path,
|
|
1126
|
+
scriptName,
|
|
1127
|
+
...excluded ? { unexposedReason: ALLOWED_TOOLS_EXCLUSION_REASON } : { toolName: scriptToolName(skillName, scriptName) },
|
|
1128
|
+
schemaSource: excluded ? "unavailable" : sidecars.has(`scripts/${scriptName}.schema.json`) ? "sidecar" : "unknown"
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
return out;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* 清单 → 喂给模型的文本(FR-18.1)。
|
|
1135
|
+
* 「authoritative」这句是给模型的信号:以目录为准,别照 SKILL.md 的描述猜文件名。
|
|
1136
|
+
*/
|
|
1137
|
+
function formatSkillScriptManifest(descriptors) {
|
|
1138
|
+
if (descriptors.length === 0) return "";
|
|
1139
|
+
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")}`;
|
|
1140
|
+
}
|
|
888
1141
|
const MCP_CONTENT_RE = /^(text|json|image|resource|audio)$/;
|
|
889
1142
|
/**
|
|
890
1143
|
* 脚本返回值归一到 MCP content 格式:数组沿用(逐项归一),
|
|
891
1144
|
* 字符串包装为 text,其他值序列化包装。Node/浏览器执行器共享。
|
|
1145
|
+
* 产出只有文本型分片:脚本侧没有下发图像的通道(图像只来自页面取像)。
|
|
892
1146
|
*/
|
|
893
1147
|
function normalizeToolContent(value) {
|
|
894
1148
|
if (Array.isArray(value)) return value.map((item) => {
|
|
@@ -946,16 +1200,31 @@ const READ_SKILL_FILE_TOOL = {
|
|
|
946
1200
|
const ASK_USER_TOOL_NAME = "ask_user";
|
|
947
1201
|
const ASK_USER_INPUT_SCHEMA = {
|
|
948
1202
|
type: "object",
|
|
949
|
-
properties: {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1203
|
+
properties: {
|
|
1204
|
+
question: {
|
|
1205
|
+
type: "string",
|
|
1206
|
+
description: "The question to ask the user"
|
|
1207
|
+
},
|
|
1208
|
+
choices: {
|
|
1209
|
+
type: "array",
|
|
1210
|
+
items: { type: "string" },
|
|
1211
|
+
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."
|
|
1212
|
+
},
|
|
1213
|
+
suggestion: {
|
|
1214
|
+
type: "string",
|
|
1215
|
+
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."
|
|
1216
|
+
},
|
|
1217
|
+
suggestionReason: {
|
|
1218
|
+
type: "string",
|
|
1219
|
+
description: "Short reason for the suggestion, shown next to it so the user can judge whether to accept it."
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
953
1222
|
required: ["question"]
|
|
954
1223
|
};
|
|
955
1224
|
/** 内建工具:LLM 信息不足时主动向用户提问;仅当配置了 UiBridge 时注册 */
|
|
956
1225
|
const ASK_USER_TOOL = {
|
|
957
1226
|
name: ASK_USER_TOOL_NAME,
|
|
958
|
-
description: "Ask the user a question when information is missing and continue with their answer.",
|
|
1227
|
+
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
1228
|
inputSchema: ASK_USER_INPUT_SCHEMA,
|
|
960
1229
|
source: "builtin"
|
|
961
1230
|
};
|
|
@@ -1183,70 +1452,12 @@ function extractUiSpecEvents(data) {
|
|
|
1183
1452
|
const raw = data["$surface"];
|
|
1184
1453
|
return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
|
|
1185
1454
|
}
|
|
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
1455
|
/**
|
|
1244
1456
|
* JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
|
|
1245
1457
|
* providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
|
|
1246
1458
|
* type 映射:string→text、number/integer→number、boolean→boolean、enum→select、其余→textarea。
|
|
1247
|
-
* 传入 skillName 时给每个字段带上跨会话稳定的 `fieldKey`(FR-5.6)。
|
|
1248
1459
|
*/
|
|
1249
|
-
function schemaToForm(schema, providedArgs
|
|
1460
|
+
function schemaToForm(schema, providedArgs) {
|
|
1250
1461
|
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
1251
1462
|
const fields = [];
|
|
1252
1463
|
for (const [name, prop] of Object.entries(schema.properties ?? {})) {
|
|
@@ -1257,7 +1468,6 @@ function schemaToForm(schema, providedArgs, options) {
|
|
|
1257
1468
|
type: mapFieldType(prop),
|
|
1258
1469
|
...required.has(name) ? { required: true } : {},
|
|
1259
1470
|
...typeof prop.description === "string" ? { description: prop.description } : {},
|
|
1260
|
-
...options?.skillName !== void 0 ? { fieldKey: formFieldKey(options.skillName, name) } : {},
|
|
1261
1471
|
...provided !== void 0 ? { defaultValue: provided } : prop.default !== void 0 ? { defaultValue: prop.default } : {}
|
|
1262
1472
|
};
|
|
1263
1473
|
if (Array.isArray(prop.enum)) field.options = prop.enum.map((v) => ({
|
|
@@ -1271,7 +1481,7 @@ function schemaToForm(schema, providedArgs, options) {
|
|
|
1271
1481
|
function mapFieldType(prop) {
|
|
1272
1482
|
if (Array.isArray(prop.enum)) return "select";
|
|
1273
1483
|
switch (prop.type) {
|
|
1274
|
-
case "string": return "text";
|
|
1484
|
+
case "string": return prop["format"] === "binary" || prop["contentEncoding"] === "base64" ? "file" : "text";
|
|
1275
1485
|
case "number":
|
|
1276
1486
|
case "integer": return "number";
|
|
1277
1487
|
case "boolean": return "boolean";
|
|
@@ -1361,6 +1571,176 @@ var SerializingMemoryStore = class {
|
|
|
1361
1571
|
return this.#serialize(scope, () => fn(this.#inner));
|
|
1362
1572
|
}
|
|
1363
1573
|
};
|
|
1574
|
+
/** @experimental */
|
|
1575
|
+
const DEFAULT_USER_PROFILE_LIMITS = {
|
|
1576
|
+
recordLimit: 500,
|
|
1577
|
+
valueMaxChars: 2e3,
|
|
1578
|
+
sampleLimit: 80,
|
|
1579
|
+
injectMaxBytes: 2e3,
|
|
1580
|
+
entryLimit: 40
|
|
1581
|
+
};
|
|
1582
|
+
/** @experimental */
|
|
1583
|
+
const EMPTY_USER_PROFILE = {
|
|
1584
|
+
version: 1,
|
|
1585
|
+
entries: [],
|
|
1586
|
+
refinedAt: 0
|
|
1587
|
+
};
|
|
1588
|
+
/** 注入段的标题。宿主/测试要判断「有没有注入画像」时对它取子串,不要自己再拼一份 @experimental */
|
|
1589
|
+
const USER_PROFILE_PROMPT_HEADER = "## About this user (inferred from past behavior; may be outdated)";
|
|
1590
|
+
/**
|
|
1591
|
+
* 无画像时也要下发的约束(AC-19.11):没有依据就把字段留空,
|
|
1592
|
+
* 不写这一句时模型会把「建议值」当成必填项去编。
|
|
1593
|
+
* @experimental
|
|
1594
|
+
*/
|
|
1595
|
+
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.";
|
|
1596
|
+
const byPriority = (a, b) => a.confidence === b.confidence ? b.updatedAt - a.updatedAt : a.confidence === "high" ? -1 : 1;
|
|
1597
|
+
const byteLength = (text) => new TextEncoder().encode(text).length;
|
|
1598
|
+
/**
|
|
1599
|
+
* 画像 → 系统提示片段,按 confidence + updatedAt 优先级排序后逐条累加,
|
|
1600
|
+
* 超出字节上限的部分丢弃并标注(FR-19.4 / AC-19.9)。
|
|
1601
|
+
* 空画像返回空串:只有标题的空段落白占 token,还会诱导模型编造。
|
|
1602
|
+
* @experimental
|
|
1603
|
+
*/
|
|
1604
|
+
function renderUserProfileContext(profile = EMPTY_USER_PROFILE, maxBytes = DEFAULT_USER_PROFILE_LIMITS.injectMaxBytes) {
|
|
1605
|
+
const entries = [...profile.entries].sort(byPriority);
|
|
1606
|
+
if (entries.length === 0 || maxBytes <= 0) return "";
|
|
1607
|
+
const lines = entries.map((entry) => `- ${entry.text}`);
|
|
1608
|
+
const render = (keptCount) => {
|
|
1609
|
+
const omitted = lines.length - keptCount;
|
|
1610
|
+
const body = [USER_PROFILE_PROMPT_HEADER, ...lines.slice(0, keptCount)];
|
|
1611
|
+
if (omitted > 0) body.push(`(truncated: ${omitted} more entries omitted)`);
|
|
1612
|
+
return body.join("\n");
|
|
1613
|
+
};
|
|
1614
|
+
let kept = lines.length;
|
|
1615
|
+
while (kept > 0 && byteLength(render(kept)) > maxBytes) kept -= 1;
|
|
1616
|
+
return kept === 0 ? "" : render(kept);
|
|
1617
|
+
}
|
|
1618
|
+
/** `user:{id}` scope 下的行为记录 key @experimental */
|
|
1619
|
+
const BEHAVIOR_RECORDS_KEY = "behaviorRecords";
|
|
1620
|
+
/** `user:{id}` scope 下的画像 key @experimental */
|
|
1621
|
+
const USER_PROFILE_KEY = "userProfile";
|
|
1622
|
+
const KINDS = [
|
|
1623
|
+
"question",
|
|
1624
|
+
"choice",
|
|
1625
|
+
"text-input",
|
|
1626
|
+
"stated-preference"
|
|
1627
|
+
];
|
|
1628
|
+
const isKind = (value) => KINDS.includes(value);
|
|
1629
|
+
const stringList = (raw) => Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : void 0;
|
|
1630
|
+
/** 存储里的形状不受控(宿主可能手改文件),逐条筛而不是整体信任 @experimental */
|
|
1631
|
+
function readBehaviorRecords(raw) {
|
|
1632
|
+
if (!Array.isArray(raw)) return [];
|
|
1633
|
+
const out = [];
|
|
1634
|
+
for (const item of raw) {
|
|
1635
|
+
if (typeof item !== "object" || item === null) continue;
|
|
1636
|
+
const record = item;
|
|
1637
|
+
const scene = record.scene;
|
|
1638
|
+
if (typeof record.id !== "string" || !isKind(record.kind)) continue;
|
|
1639
|
+
if (typeof record.at !== "number" || typeof record.value !== "string") continue;
|
|
1640
|
+
if (typeof scene !== "object" || scene === null || typeof scene.sessionId !== "string") continue;
|
|
1641
|
+
const options = stringList(scene.options);
|
|
1642
|
+
out.push({
|
|
1643
|
+
id: record.id,
|
|
1644
|
+
kind: record.kind,
|
|
1645
|
+
at: record.at,
|
|
1646
|
+
value: record.value,
|
|
1647
|
+
...record.confirmed === true ? { confirmed: true } : {},
|
|
1648
|
+
scene: {
|
|
1649
|
+
sessionId: scene.sessionId,
|
|
1650
|
+
...typeof scene.skillName === "string" ? { skillName: scene.skillName } : {},
|
|
1651
|
+
...typeof scene.interactionId === "string" ? { interactionId: scene.interactionId } : {},
|
|
1652
|
+
...typeof scene.fieldName === "string" ? { fieldName: scene.fieldName } : {},
|
|
1653
|
+
...typeof scene.fieldSemantics === "string" ? { fieldSemantics: scene.fieldSemantics } : {},
|
|
1654
|
+
...options && options.length > 0 ? { options } : {}
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
return out;
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* 追加并按容量淘汰最旧(FR-19.2)。`value` 同时截断:
|
|
1662
|
+
* 一条被粘贴进来的长文本能把整个 JSON 撑爆,而提炼只需要语义。
|
|
1663
|
+
* @experimental
|
|
1664
|
+
*/
|
|
1665
|
+
function appendBehaviorRecords(current, incoming, limits = DEFAULT_USER_PROFILE_LIMITS) {
|
|
1666
|
+
if (limits.recordLimit <= 0) return [];
|
|
1667
|
+
const truncated = incoming.map((record) => record.value.length > limits.valueMaxChars ? {
|
|
1668
|
+
...record,
|
|
1669
|
+
value: record.value.slice(0, limits.valueMaxChars)
|
|
1670
|
+
} : record);
|
|
1671
|
+
const merged = [...current, ...truncated];
|
|
1672
|
+
return merged.slice(Math.max(0, merged.length - limits.recordLimit));
|
|
1673
|
+
}
|
|
1674
|
+
/** @experimental */
|
|
1675
|
+
function readUserProfile(raw) {
|
|
1676
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return EMPTY_USER_PROFILE;
|
|
1677
|
+
const value = raw;
|
|
1678
|
+
return {
|
|
1679
|
+
version: 1,
|
|
1680
|
+
entries: readProfileEntries(value.entries),
|
|
1681
|
+
refinedAt: typeof value.refinedAt === "number" ? value.refinedAt : 0
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
/** @experimental */
|
|
1685
|
+
function readProfileEntries(raw) {
|
|
1686
|
+
if (!Array.isArray(raw)) return [];
|
|
1687
|
+
const out = [];
|
|
1688
|
+
for (const item of raw) {
|
|
1689
|
+
if (typeof item !== "object" || item === null) continue;
|
|
1690
|
+
const entry = item;
|
|
1691
|
+
if (typeof entry.id !== "string" || entry.id === "") continue;
|
|
1692
|
+
if (typeof entry.text !== "string" || entry.text.trim() === "") continue;
|
|
1693
|
+
out.push({
|
|
1694
|
+
id: entry.id,
|
|
1695
|
+
text: entry.text,
|
|
1696
|
+
confidence: entry.confidence === "high" ? "high" : "low",
|
|
1697
|
+
updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : 0,
|
|
1698
|
+
sourceKinds: (stringList(entry.sourceKinds) ?? []).filter(isKind)
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
return out;
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* 合并提炼结果:同 id 更新,新 id 追加,超上限按 `updatedAt` 淘汰最旧(设计 09 §1.4)。
|
|
1705
|
+
* @experimental
|
|
1706
|
+
*/
|
|
1707
|
+
function mergeProfileEntries(current, incoming, options) {
|
|
1708
|
+
const limits = options.limits ?? DEFAULT_USER_PROFILE_LIMITS;
|
|
1709
|
+
const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
|
|
1710
|
+
for (const entry of incoming) byId.set(entry.id, entry);
|
|
1711
|
+
return {
|
|
1712
|
+
version: 1,
|
|
1713
|
+
entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, Math.max(0, limits.entryLimit)),
|
|
1714
|
+
refinedAt: options.now
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* 提炼采样(FR-19.3):最近的 + 高频的 + 被用户显式确认过的,去重后截到上限。
|
|
1719
|
+
* 三个维度缺一不可——只取最近会让长期偏好被一次突发淹没,只取高频则学不到新变化。
|
|
1720
|
+
* @experimental
|
|
1721
|
+
*/
|
|
1722
|
+
function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
|
|
1723
|
+
const limit = Math.max(0, limits.sampleLimit);
|
|
1724
|
+
if (limit === 0 || records.length === 0) return [];
|
|
1725
|
+
if (records.length <= limit) return [...records].sort((a, b) => a.at - b.at);
|
|
1726
|
+
const picked = /* @__PURE__ */ new Map();
|
|
1727
|
+
const take = (record) => {
|
|
1728
|
+
if (picked.size < limit) picked.set(record.id, record);
|
|
1729
|
+
};
|
|
1730
|
+
for (const record of records) if (record.confirmed === true) take(record);
|
|
1731
|
+
const frequency = /* @__PURE__ */ new Map();
|
|
1732
|
+
const bucketOf = (record) => `${record.scene.skillName ?? ""}#${record.scene.fieldName ?? record.kind}`;
|
|
1733
|
+
for (const record of records) frequency.set(bucketOf(record), (frequency.get(bucketOf(record)) ?? 0) + 1);
|
|
1734
|
+
const byFrequency = [...records].sort((a, b) => (frequency.get(bucketOf(b)) ?? 0) - (frequency.get(bucketOf(a)) ?? 0));
|
|
1735
|
+
for (const record of byFrequency.slice(0, Math.ceil(limit / 2))) take(record);
|
|
1736
|
+
for (const record of [...records].reverse()) take(record);
|
|
1737
|
+
return [...picked.values()].sort((a, b) => a.at - b.at);
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* 无工具模型的系统提示(0.6.0 FR-14.3)。
|
|
1741
|
+
* 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
|
|
1742
|
+
*/
|
|
1743
|
+
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
1744
|
/** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
|
|
1365
1745
|
var TraceRecorder = class {
|
|
1366
1746
|
#runId;
|
|
@@ -1570,13 +1950,13 @@ var RunTerminated = class extends Error {
|
|
|
1570
1950
|
};
|
|
1571
1951
|
/** bridge.request 自身异常(非超时):恢复 running 后转为工具错误回喂 */
|
|
1572
1952
|
var BridgeRequestError = class extends Error {};
|
|
1573
|
-
const
|
|
1574
|
-
const toolError = (code, message) => ({
|
|
1953
|
+
const toolError = (code, message, data) => ({
|
|
1575
1954
|
ok: false,
|
|
1576
1955
|
content: [],
|
|
1577
1956
|
error: {
|
|
1578
1957
|
code,
|
|
1579
|
-
message
|
|
1958
|
+
message,
|
|
1959
|
+
...data !== void 0 ? { data } : {}
|
|
1580
1960
|
}
|
|
1581
1961
|
});
|
|
1582
1962
|
/** 工具参数摘要(JSON 截断 100 字符;trace 与实时事件同一口径,ToolCallCard 展开详情用) */
|
|
@@ -1585,6 +1965,16 @@ const summarizeArgs = (args) => {
|
|
|
1585
1965
|
return json.length > 100 ? `${json.slice(0, 100)}…` : json;
|
|
1586
1966
|
};
|
|
1587
1967
|
/**
|
|
1968
|
+
* file-pick 的回填值带整个文件的 base64,落进 `paramHistory` 会把用户选的文件
|
|
1969
|
+
* 原样长期留在记忆里(体积与隐私都不可接受)。只留可辨识的描述,正文不留。
|
|
1970
|
+
*/
|
|
1971
|
+
const redactFileValue = (value) => {
|
|
1972
|
+
if (Array.isArray(value)) return value.map(redactFileValue);
|
|
1973
|
+
if (typeof value !== "object" || value === null) return typeof value === "string" ? "[file]" : value;
|
|
1974
|
+
const { data: _data, ...rest } = value;
|
|
1975
|
+
return rest;
|
|
1976
|
+
};
|
|
1977
|
+
/**
|
|
1588
1978
|
* 多轮 Agent 循环。
|
|
1589
1979
|
* 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
|
|
1590
1980
|
* 缺参/确认/提问经 UiBridge 行内 await 暂停恢复;取消/超时/LLM 异常/护栏超限才终止 run。
|
|
@@ -1608,7 +1998,8 @@ var AgentLoop = class {
|
|
|
1608
1998
|
toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
|
|
1609
1999
|
toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
|
|
1610
2000
|
paramHistoryLimit: config.paramHistoryLimit ?? 50,
|
|
1611
|
-
|
|
2001
|
+
toolCallingDisabled: config.toolCallingDisabled ?? false,
|
|
2002
|
+
maxUnknownToolRetries: config.maxUnknownToolRetries ?? 2,
|
|
1612
2003
|
temperature: config.temperature,
|
|
1613
2004
|
renderResult: config.renderResult
|
|
1614
2005
|
};
|
|
@@ -1621,7 +2012,7 @@ var AgentLoop = class {
|
|
|
1621
2012
|
/**
|
|
1622
2013
|
* 取消进行中的 run:触发该 run 的 AbortController(与 totalTimeout 硬期限同一通道),
|
|
1623
2014
|
* run 以 cancelled(RUN_CANCELLED)终止;未找到(已终态或不属于本实例)返回 false。
|
|
1624
|
-
*
|
|
2015
|
+
* 取消在下一个中断点生效;LLM 调用与交互等待都监听同一个 signal,因此等表单时也能立即停。
|
|
1625
2016
|
*/
|
|
1626
2017
|
cancel(runId) {
|
|
1627
2018
|
const controller = this.#controllers.get(runId);
|
|
@@ -1643,7 +2034,7 @@ var AgentLoop = class {
|
|
|
1643
2034
|
sessionId: input.sessionId,
|
|
1644
2035
|
status: "running",
|
|
1645
2036
|
phase: "route",
|
|
1646
|
-
userPrompt: input.userPrompt,
|
|
2037
|
+
userPrompt: promptText(input.userPrompt),
|
|
1647
2038
|
startedAt,
|
|
1648
2039
|
activeSkillNames: [],
|
|
1649
2040
|
artifacts: [],
|
|
@@ -1668,6 +2059,7 @@ var AgentLoop = class {
|
|
|
1668
2059
|
surfacePatchCount: 0,
|
|
1669
2060
|
processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
|
|
1670
2061
|
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
2062
|
+
unknownToolCalls: 0,
|
|
1671
2063
|
startMs,
|
|
1672
2064
|
pausedMs: 0,
|
|
1673
2065
|
maxTurns: this.#config.maxTurns,
|
|
@@ -1691,7 +2083,7 @@ var AgentLoop = class {
|
|
|
1691
2083
|
candidates: route.catalog.entries.map((e) => e.name)
|
|
1692
2084
|
}
|
|
1693
2085
|
}, state);
|
|
1694
|
-
const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
2086
|
+
const externalSpecs = this.#config.toolCallingDisabled ? [] : (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
1695
2087
|
try {
|
|
1696
2088
|
return await source.listToolSpecs();
|
|
1697
2089
|
} catch (e) {
|
|
@@ -1700,7 +2092,7 @@ var AgentLoop = class {
|
|
|
1700
2092
|
}
|
|
1701
2093
|
}))).flat();
|
|
1702
2094
|
const externalSystemPrompts = [];
|
|
1703
|
-
for (const source of this.#deps.externalTools ?? []) {
|
|
2095
|
+
if (!this.#config.toolCallingDisabled) for (const source of this.#deps.externalTools ?? []) {
|
|
1704
2096
|
if (source.systemPrompt === void 0) continue;
|
|
1705
2097
|
try {
|
|
1706
2098
|
const text = (await source.systemPrompt())?.trim();
|
|
@@ -1709,17 +2101,21 @@ var AgentLoop = class {
|
|
|
1709
2101
|
trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
|
|
1710
2102
|
}
|
|
1711
2103
|
}
|
|
2104
|
+
const systemPrompt = this.#config.toolCallingDisabled ? PLAIN_CHAT_SYSTEM_PROMPT : [route.systemPrompt, ...externalSystemPrompts].join("\n\n");
|
|
2105
|
+
const profileMessage = await this.#userProfileMessage(state);
|
|
1712
2106
|
state.messages = [
|
|
1713
2107
|
{
|
|
1714
2108
|
role: "system",
|
|
1715
|
-
content: textParts(
|
|
2109
|
+
content: textParts(systemPrompt)
|
|
1716
2110
|
},
|
|
2111
|
+
...profileMessage ? [profileMessage] : [],
|
|
1717
2112
|
...(input.history ?? []).map((m) => ({ ...m })),
|
|
1718
2113
|
{
|
|
1719
2114
|
role: "user",
|
|
1720
|
-
content: textParts(input.userPrompt)
|
|
2115
|
+
content: typeof input.userPrompt === "string" ? textParts(input.userPrompt) : [...input.userPrompt]
|
|
1721
2116
|
}
|
|
1722
2117
|
];
|
|
2118
|
+
await this.#recordUserPrompt(state);
|
|
1723
2119
|
try {
|
|
1724
2120
|
return await this.#turnLoop(state, 1, externalSpecs);
|
|
1725
2121
|
} catch (e) {
|
|
@@ -1780,7 +2176,7 @@ var AgentLoop = class {
|
|
|
1780
2176
|
if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
|
|
1781
2177
|
if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
1782
2178
|
const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
|
|
1783
|
-
const toolSpecs = [
|
|
2179
|
+
const toolSpecs = this.#config.toolCallingDisabled ? [] : [
|
|
1784
2180
|
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
1785
2181
|
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
1786
2182
|
...skillToolSpecs
|
|
@@ -1849,9 +2245,14 @@ var AgentLoop = class {
|
|
|
1849
2245
|
messages.push({
|
|
1850
2246
|
role: "tool",
|
|
1851
2247
|
toolCallId: call.id,
|
|
1852
|
-
content:
|
|
2248
|
+
content: await this.#toolResultParts(call, result, state)
|
|
1853
2249
|
});
|
|
1854
|
-
|
|
2250
|
+
try {
|
|
2251
|
+
await this.#drainSurfaceAction(state);
|
|
2252
|
+
} catch (e) {
|
|
2253
|
+
if (e instanceof RunTerminated) return finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
|
|
2254
|
+
throw e;
|
|
2255
|
+
}
|
|
1855
2256
|
}
|
|
1856
2257
|
}
|
|
1857
2258
|
} finally {
|
|
@@ -1988,6 +2389,7 @@ var AgentLoop = class {
|
|
|
1988
2389
|
surfacePatchCount: 0,
|
|
1989
2390
|
processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
|
|
1990
2391
|
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
2392
|
+
unknownToolCalls: 0,
|
|
1991
2393
|
startMs,
|
|
1992
2394
|
pausedMs: snapshot.pausedMs ?? 0,
|
|
1993
2395
|
maxTurns: snapshot.config.maxTurns,
|
|
@@ -2024,7 +2426,7 @@ var AgentLoop = class {
|
|
|
2024
2426
|
state.messages.push({
|
|
2025
2427
|
role: "tool",
|
|
2026
2428
|
toolCallId: pendingCall.id,
|
|
2027
|
-
content:
|
|
2429
|
+
content: await this.#toolResultParts(pendingCall, result, state)
|
|
2028
2430
|
});
|
|
2029
2431
|
await this.#drainSurfaceAction(state);
|
|
2030
2432
|
} else if (pending?.type === "ask" && pendingCall) {
|
|
@@ -2046,7 +2448,7 @@ var AgentLoop = class {
|
|
|
2046
2448
|
state.messages.push({
|
|
2047
2449
|
role: "tool",
|
|
2048
2450
|
toolCallId: pendingCall.id,
|
|
2049
|
-
content:
|
|
2451
|
+
content: await this.#toolResultParts(pendingCall, result, state)
|
|
2050
2452
|
});
|
|
2051
2453
|
await this.#drainSurfaceAction(state);
|
|
2052
2454
|
} else if (pendingCall) {
|
|
@@ -2054,7 +2456,7 @@ var AgentLoop = class {
|
|
|
2054
2456
|
state.messages.push({
|
|
2055
2457
|
role: "tool",
|
|
2056
2458
|
toolCallId: pendingCall.id,
|
|
2057
|
-
content:
|
|
2459
|
+
content: await this.#toolResultParts(pendingCall, result, state)
|
|
2058
2460
|
});
|
|
2059
2461
|
await this.#drainSurfaceAction(state);
|
|
2060
2462
|
}
|
|
@@ -2065,7 +2467,7 @@ var AgentLoop = class {
|
|
|
2065
2467
|
state.messages.push({
|
|
2066
2468
|
role: "tool",
|
|
2067
2469
|
toolCallId: next.id,
|
|
2068
|
-
content:
|
|
2470
|
+
content: await this.#toolResultParts(next, result, state)
|
|
2069
2471
|
});
|
|
2070
2472
|
await this.#drainSurfaceAction(state);
|
|
2071
2473
|
}
|
|
@@ -2180,7 +2582,7 @@ var AgentLoop = class {
|
|
|
2180
2582
|
} });
|
|
2181
2583
|
let response;
|
|
2182
2584
|
try {
|
|
2183
|
-
response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs, () => bridge.cancel?.(request.id));
|
|
2585
|
+
response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs, state, () => bridge.cancel?.(request.id));
|
|
2184
2586
|
} catch (e) {
|
|
2185
2587
|
run.status = "running";
|
|
2186
2588
|
run.interruptExpiresAt = void 0;
|
|
@@ -2190,6 +2592,7 @@ var AgentLoop = class {
|
|
|
2190
2592
|
message: e.message,
|
|
2191
2593
|
code: "RUN_INTERACTION_TIMEOUT"
|
|
2192
2594
|
});
|
|
2595
|
+
if (e instanceof WebSkillError && e.code === "RUN_CANCELLED") throw this.#abortedInteraction(state);
|
|
2193
2596
|
state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf(e)}` });
|
|
2194
2597
|
throw new BridgeRequestError(messageOf(e));
|
|
2195
2598
|
}
|
|
@@ -2215,19 +2618,62 @@ var AgentLoop = class {
|
|
|
2215
2618
|
await this.#appendParamHistory(state, request, response.value);
|
|
2216
2619
|
return response.value;
|
|
2217
2620
|
}
|
|
2218
|
-
|
|
2621
|
+
/**
|
|
2622
|
+
* 交互等待被 abort:cancel() 与 totalTimeout 硬期限共用同一个 controller,
|
|
2623
|
+
* 按 `#cancelled` 区分二者,与 LLM 调用被 abort 时的归类保持一致。
|
|
2624
|
+
*/
|
|
2625
|
+
#abortedInteraction(state) {
|
|
2626
|
+
if (this.#cancelled.has(state.runId)) return new RunTerminated({
|
|
2627
|
+
status: "cancelled",
|
|
2628
|
+
reason: "user-cancelled",
|
|
2629
|
+
message: "Run cancelled by user",
|
|
2630
|
+
code: "RUN_CANCELLED"
|
|
2631
|
+
});
|
|
2632
|
+
return new RunTerminated({
|
|
2633
|
+
status: "failed",
|
|
2634
|
+
reason: "timeout",
|
|
2635
|
+
message: `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`,
|
|
2636
|
+
code: "RUN_TIMEOUT"
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
#withInteractionTimeout(promise, timeoutMs, state, onAbandon) {
|
|
2219
2640
|
return new Promise((resolve, reject) => {
|
|
2220
|
-
const
|
|
2641
|
+
const signal = state.controller.signal;
|
|
2642
|
+
let settled = false;
|
|
2643
|
+
const abandon = () => {
|
|
2221
2644
|
try {
|
|
2222
|
-
|
|
2645
|
+
onAbandon?.();
|
|
2223
2646
|
} catch {}
|
|
2647
|
+
};
|
|
2648
|
+
const cleanup = () => {
|
|
2649
|
+
settled = true;
|
|
2650
|
+
clearTimeout(timer);
|
|
2651
|
+
signal.removeEventListener("abort", onAbort);
|
|
2652
|
+
};
|
|
2653
|
+
const onAbort = () => {
|
|
2654
|
+
if (settled) return;
|
|
2655
|
+
cleanup();
|
|
2656
|
+
abandon();
|
|
2657
|
+
reject(new WebSkillError("RUN_CANCELLED", "Interaction aborted because the run was stopped"));
|
|
2658
|
+
};
|
|
2659
|
+
const timer = setTimeout(() => {
|
|
2660
|
+
if (settled) return;
|
|
2661
|
+
cleanup();
|
|
2662
|
+
abandon();
|
|
2224
2663
|
reject(new WebSkillError("RUN_INTERACTION_TIMEOUT", `Interaction timed out after ${timeoutMs}ms`));
|
|
2225
2664
|
}, timeoutMs);
|
|
2665
|
+
if (signal.aborted) {
|
|
2666
|
+
onAbort();
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
signal.addEventListener("abort", onAbort);
|
|
2226
2670
|
promise.then((v) => {
|
|
2227
|
-
|
|
2671
|
+
if (settled) return;
|
|
2672
|
+
cleanup();
|
|
2228
2673
|
resolve(v);
|
|
2229
2674
|
}, (e) => {
|
|
2230
|
-
|
|
2675
|
+
if (settled) return;
|
|
2676
|
+
cleanup();
|
|
2231
2677
|
reject(e instanceof Error ? e : new Error(String(e)));
|
|
2232
2678
|
});
|
|
2233
2679
|
});
|
|
@@ -2247,11 +2693,15 @@ var AgentLoop = class {
|
|
|
2247
2693
|
else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
|
|
2248
2694
|
else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
|
|
2249
2695
|
else {
|
|
2250
|
-
const resolution = resolveToolName(call.name, state.activated);
|
|
2696
|
+
const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
|
|
2251
2697
|
if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
|
|
2252
2698
|
else {
|
|
2253
2699
|
const source = (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
|
|
2254
|
-
result =
|
|
2700
|
+
if (source) result = await source.call(call.name, call.arguments);
|
|
2701
|
+
else {
|
|
2702
|
+
if (resolution.kind === "not-found") state.unknownToolCalls += 1;
|
|
2703
|
+
result = toolError(resolution.code, resolution.message, resolution.kind === "not-found" ? resolution.data : void 0);
|
|
2704
|
+
}
|
|
2255
2705
|
}
|
|
2256
2706
|
}
|
|
2257
2707
|
const durationMs = Date.parse(state.now()) - callStartMs;
|
|
@@ -2297,6 +2747,12 @@ var AgentLoop = class {
|
|
|
2297
2747
|
artifactId: artifact.id,
|
|
2298
2748
|
path: artifact.path
|
|
2299
2749
|
} });
|
|
2750
|
+
if (state.unknownToolCalls > this.#config.maxUnknownToolRetries) throw new RunTerminated({
|
|
2751
|
+
status: "failed",
|
|
2752
|
+
reason: "tool-resolution-exhausted",
|
|
2753
|
+
message: "Repeated calls to non-existent tools. Stopping to avoid consuming the remaining turn budget.",
|
|
2754
|
+
code: "TOOL_RESOLUTION_EXHAUSTED"
|
|
2755
|
+
});
|
|
2300
2756
|
return result;
|
|
2301
2757
|
}
|
|
2302
2758
|
/** Attaches trusted run provenance, then records only events accepted by the configured bridge. */
|
|
@@ -2410,7 +2866,7 @@ var AgentLoop = class {
|
|
|
2410
2866
|
nonce: request.nonce,
|
|
2411
2867
|
...resumed ? { resumed: true } : {}
|
|
2412
2868
|
} });
|
|
2413
|
-
const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
|
|
2869
|
+
const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, state, () => bridge.cancelSurfaceAction?.(request.nonce));
|
|
2414
2870
|
run.status = "running";
|
|
2415
2871
|
run.interruptExpiresAt = void 0;
|
|
2416
2872
|
state.trace.record("ui.surface-action.resolved", { data: {
|
|
@@ -2437,6 +2893,7 @@ var AgentLoop = class {
|
|
|
2437
2893
|
message: e.message,
|
|
2438
2894
|
code: "RUN_INTERACTION_TIMEOUT"
|
|
2439
2895
|
});
|
|
2896
|
+
if (e instanceof WebSkillError && e.code === "RUN_CANCELLED") throw this.#abortedInteraction(state);
|
|
2440
2897
|
if (e instanceof RunTerminated || e instanceof BridgeRequestError) throw e;
|
|
2441
2898
|
throw new BridgeRequestError(messageOf(e));
|
|
2442
2899
|
} finally {
|
|
@@ -2488,12 +2945,32 @@ var AgentLoop = class {
|
|
|
2488
2945
|
async #handleAskUser(call, state) {
|
|
2489
2946
|
const question = call.arguments["question"];
|
|
2490
2947
|
if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires a non-empty \"question\" string argument");
|
|
2948
|
+
const rawChoices = call.arguments["choices"];
|
|
2949
|
+
const choices = Array.isArray(rawChoices) ? rawChoices.filter((choice) => typeof choice === "string" && choice !== "") : [];
|
|
2950
|
+
const id = this.#nextInteractionId(state);
|
|
2951
|
+
const rawSuggestion = call.arguments["suggestion"];
|
|
2952
|
+
const rawReason = call.arguments["suggestionReason"];
|
|
2953
|
+
const suggestion = this.#deps.userProfile && typeof rawSuggestion === "string" && rawSuggestion !== "" ? { suggestion: {
|
|
2954
|
+
value: rawSuggestion,
|
|
2955
|
+
...typeof rawReason === "string" && rawReason !== "" ? { reason: rawReason } : {}
|
|
2956
|
+
} } : {};
|
|
2957
|
+
const request = choices.length > 0 ? {
|
|
2958
|
+
type: "select",
|
|
2959
|
+
id,
|
|
2960
|
+
message: question,
|
|
2961
|
+
options: choices.map((label) => ({
|
|
2962
|
+
label,
|
|
2963
|
+
value: label
|
|
2964
|
+
})),
|
|
2965
|
+
...suggestion
|
|
2966
|
+
} : {
|
|
2967
|
+
type: "ask",
|
|
2968
|
+
id,
|
|
2969
|
+
message: question,
|
|
2970
|
+
...suggestion
|
|
2971
|
+
};
|
|
2491
2972
|
try {
|
|
2492
|
-
const value = await this.#interact(state, {
|
|
2493
|
-
type: "ask",
|
|
2494
|
-
id: this.#nextInteractionId(state),
|
|
2495
|
-
message: question
|
|
2496
|
-
}, { tool: call.name });
|
|
2973
|
+
const value = await this.#interact(state, request, { tool: call.name });
|
|
2497
2974
|
return {
|
|
2498
2975
|
ok: true,
|
|
2499
2976
|
content: [{
|
|
@@ -2579,6 +3056,26 @@ var AgentLoop = class {
|
|
|
2579
3056
|
return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
|
|
2580
3057
|
}
|
|
2581
3058
|
/**
|
|
3059
|
+
* 工具结果 → tool 消息内容。image 分片单独提出,不进 JSON 也不参与截断计算(0.6.0 §1.1):
|
|
3060
|
+
* 100 KB 的文本预算遇到一张 200 KB 的图会把整个结果截成垃圾。
|
|
3061
|
+
*/
|
|
3062
|
+
async #toolResultParts(call, result, state) {
|
|
3063
|
+
const images = result.content.filter((part) => part.type === "image");
|
|
3064
|
+
if (images.length === 0) return textParts(await this.#serializeToolResult(call, result, state));
|
|
3065
|
+
const textual = {
|
|
3066
|
+
...result,
|
|
3067
|
+
content: result.content.filter((part) => part.type !== "image")
|
|
3068
|
+
};
|
|
3069
|
+
return [{
|
|
3070
|
+
type: "text",
|
|
3071
|
+
text: await this.#serializeToolResult(call, textual, state)
|
|
3072
|
+
}, ...images.map((image) => ({
|
|
3073
|
+
type: "image",
|
|
3074
|
+
mimeType: image.mimeType,
|
|
3075
|
+
data: image.data
|
|
3076
|
+
}))];
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
2582
3079
|
* 工具结果回喂序列化:超过 toolResultMaxBytes(默认 100KB)时头尾保留截断,
|
|
2583
3080
|
* 完整内容经 artifactStore 落 artifact,回喂摘要含 artifact id。
|
|
2584
3081
|
*/
|
|
@@ -2682,32 +3179,36 @@ var AgentLoop = class {
|
|
|
2682
3179
|
}
|
|
2683
3180
|
const executor = this.#deps.executor;
|
|
2684
3181
|
const loaded = [];
|
|
3182
|
+
let scripts = [];
|
|
2685
3183
|
if (executor) {
|
|
2686
3184
|
let scriptFiles;
|
|
2687
3185
|
try {
|
|
2688
|
-
scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) =>
|
|
3186
|
+
scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => `scripts/${s.path.split("/").pop() ?? s.path}`);
|
|
2689
3187
|
} catch (e) {
|
|
2690
3188
|
scriptFiles = [];
|
|
2691
3189
|
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
3190
|
}
|
|
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.` });
|
|
3191
|
+
scripts = listSkillScripts(skillName, scriptFiles, allowedTools);
|
|
3192
|
+
for (const script of scripts) {
|
|
3193
|
+
if (script.toolName === void 0) {
|
|
3194
|
+
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
3195
|
continue;
|
|
2699
3196
|
}
|
|
2700
3197
|
try {
|
|
2701
|
-
const def = await executor.loadDefinition(root,
|
|
2702
|
-
await this.#enrichDefinition(root,
|
|
3198
|
+
const def = await executor.loadDefinition(root, script.scriptName);
|
|
3199
|
+
script.schemaSource = await this.#enrichDefinition(root, script.scriptName, def, state);
|
|
3200
|
+
script.toolName = def.name;
|
|
2703
3201
|
state.activatedTools.set(def.name, def);
|
|
2704
3202
|
loaded.push(def.name);
|
|
2705
3203
|
} catch (e) {
|
|
2706
|
-
state.trace.record("run.warning", { message: `Failed to load definition of script "${
|
|
3204
|
+
state.trace.record("run.warning", { message: `Failed to load definition of script "${script.scriptName}" for skill "${skillName}": ${messageOf(e)}` });
|
|
3205
|
+
delete script.toolName;
|
|
3206
|
+
script.schemaSource = "unavailable";
|
|
3207
|
+
script.unexposedReason = "its definition could not be loaded";
|
|
2707
3208
|
}
|
|
2708
3209
|
}
|
|
2709
3210
|
}
|
|
2710
|
-
let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
|
|
3211
|
+
let note = (loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "") + formatSkillScriptManifest(scripts);
|
|
2711
3212
|
for (const dep of dependencies) {
|
|
2712
3213
|
if (state.activated.has(dep)) continue;
|
|
2713
3214
|
if (!this.#deps.skillIndex.has(dep)) {
|
|
@@ -2721,53 +3222,86 @@ var AgentLoop = class {
|
|
|
2721
3222
|
/**
|
|
2722
3223
|
* D2 Schema 兜底链(同一 run 内激活时只算一次):
|
|
2723
3224
|
* 显式 inputSchema > sidecar scripts/<name>.schema.json > schemaInferer 推导 > schemaUnavailable
|
|
3225
|
+
* 返回值即最终来源,进技能清单告诉模型该信谁(FR-18.1)。
|
|
2724
3226
|
*/
|
|
2725
3227
|
async #enrichDefinition(skillRoot, scriptName, def, state) {
|
|
2726
|
-
if (def.inputSchema) return;
|
|
3228
|
+
if (def.inputSchema) return "module-export";
|
|
2727
3229
|
const sidecar = `${skillRoot}/scripts/${scriptName}.schema.json`;
|
|
2728
3230
|
if (await this.#deps.fs.exists(sidecar)) try {
|
|
2729
3231
|
def.inputSchema = JSON.parse(await this.#deps.fs.readText(sidecar));
|
|
2730
|
-
return;
|
|
3232
|
+
return "sidecar";
|
|
2731
3233
|
} catch (e) {
|
|
2732
3234
|
state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf(e)}` });
|
|
2733
3235
|
}
|
|
2734
|
-
if (!this.#deps.schemaInferer) return;
|
|
3236
|
+
if (!this.#deps.schemaInferer) return "unavailable";
|
|
2735
3237
|
for (const ext of ["ts", "js"]) {
|
|
2736
3238
|
const scriptPath = `${skillRoot}/scripts/${scriptName}.${ext}`;
|
|
2737
3239
|
if (!await this.#deps.fs.exists(scriptPath)) continue;
|
|
2738
3240
|
try {
|
|
2739
3241
|
const inferred = this.#deps.schemaInferer.inferSchemaFromSource(await this.#deps.fs.readText(scriptPath), { fileName: `${scriptName}.${ext}` });
|
|
2740
|
-
if (inferred)
|
|
3242
|
+
if (inferred) {
|
|
3243
|
+
def.inputSchema = inferred;
|
|
3244
|
+
return "inferred";
|
|
3245
|
+
}
|
|
2741
3246
|
} catch (e) {
|
|
2742
3247
|
state.trace.record("run.warning", { message: `Schema inference failed for ${scriptPath}: ${messageOf(e)}` });
|
|
2743
3248
|
}
|
|
2744
|
-
return;
|
|
3249
|
+
return "unavailable";
|
|
2745
3250
|
}
|
|
3251
|
+
return "unavailable";
|
|
2746
3252
|
}
|
|
2747
3253
|
async #handleScriptTool(call, skillName, scriptName, state) {
|
|
2748
3254
|
const root = this.#deps.skillIndex.get(skillName);
|
|
2749
3255
|
if (!root) return toolError("TOOL_NOT_FOUND", `Skill "${skillName}" is not in the catalog`);
|
|
2750
3256
|
if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
|
|
2751
3257
|
const def = state.activatedTools.get(call.name);
|
|
2752
|
-
if (!def)
|
|
3258
|
+
if (!def) {
|
|
3259
|
+
state.unknownToolCalls += 1;
|
|
3260
|
+
const availableTools = [...state.activatedTools.keys()].filter((tool) => tool.startsWith(`${skillName}__`)).sort();
|
|
3261
|
+
const detail = availableTools.length > 0 ? ` Available tools for skill "${skillName}": ${availableTools.join(", ")}.` : ` Skill "${skillName}" exposes no script tools.`;
|
|
3262
|
+
return toolError("TOOL_NOT_FOUND", `Tool "${call.name}" is not available.${detail}`, {
|
|
3263
|
+
skillName,
|
|
3264
|
+
availableTools
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
2753
3267
|
let args = call.arguments;
|
|
2754
3268
|
const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
|
|
2755
3269
|
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
|
-
|
|
3270
|
+
const fields = schemaToForm(def.inputSchema, args);
|
|
3271
|
+
const declined = /* @__PURE__ */ new Set();
|
|
3272
|
+
for (const field of fields) {
|
|
3273
|
+
if (field.type !== "file" || !missing.includes(field.name)) continue;
|
|
3274
|
+
const value = await this.#interact(state, {
|
|
3275
|
+
type: "file-pick",
|
|
3276
|
+
id: this.#nextInteractionId(state),
|
|
3277
|
+
message: `Open local file picker to fill field "${field.name}"?`,
|
|
3278
|
+
field: field.name
|
|
3279
|
+
}, {
|
|
3280
|
+
tool: call.name,
|
|
3281
|
+
missing: [field.name]
|
|
3282
|
+
});
|
|
3283
|
+
if (value === void 0 || value === null) declined.add(field.name);
|
|
3284
|
+
else args = {
|
|
3285
|
+
...args,
|
|
3286
|
+
[field.name]: value
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
const stillMissing = missing.filter((key) => args[key] === void 0 && !declined.has(key));
|
|
3290
|
+
if (stillMissing.length > 0) {
|
|
3291
|
+
const value = await this.#interact(state, {
|
|
3292
|
+
type: "form",
|
|
3293
|
+
id: this.#nextInteractionId(state),
|
|
3294
|
+
title: `Missing parameters for ${call.name}`,
|
|
3295
|
+
fields: fields.filter((f) => f.type !== "file" && !declined.has(f.name))
|
|
3296
|
+
}, {
|
|
3297
|
+
tool: call.name,
|
|
3298
|
+
missing: stillMissing
|
|
3299
|
+
});
|
|
3300
|
+
if (typeof value === "object" && value !== null) args = {
|
|
3301
|
+
...args,
|
|
3302
|
+
...value
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
2771
3305
|
} catch (e) {
|
|
2772
3306
|
if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `Parameter form failed: ${e.message}`);
|
|
2773
3307
|
throw e;
|
|
@@ -2921,57 +3455,111 @@ var AgentLoop = class {
|
|
|
2921
3455
|
if (!this.#deps.memory) return;
|
|
2922
3456
|
const scope = `session:${state.run.sessionId}`;
|
|
2923
3457
|
const limit = this.#config.paramHistoryLimit;
|
|
3458
|
+
const recorded = request.type === "file-pick" ? redactFileValue(value) : value;
|
|
2924
3459
|
await this.#memoryMutate(scope, "paramHistory", state, (current) => {
|
|
2925
3460
|
const history = current ?? [];
|
|
2926
3461
|
history.push({
|
|
2927
3462
|
ts: state.now(),
|
|
2928
3463
|
interactionId: request.id,
|
|
2929
3464
|
type: request.type,
|
|
2930
|
-
value
|
|
3465
|
+
value: recorded
|
|
2931
3466
|
});
|
|
2932
3467
|
return history.slice(-limit);
|
|
2933
3468
|
});
|
|
2934
|
-
await this.#
|
|
2935
|
-
}
|
|
2936
|
-
/**
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
for (const field of request.fields) {
|
|
2948
|
-
if (field.fieldKey === void 0) continue;
|
|
2949
|
-
const next = submitted[field.name];
|
|
2950
|
-
if (next === void 0 || next === "") continue;
|
|
2951
|
-
updates[field.fieldKey] = {
|
|
2952
|
-
value: next,
|
|
2953
|
-
ts
|
|
2954
|
-
};
|
|
2955
|
-
}
|
|
2956
|
-
if (Object.keys(updates).length === 0) return;
|
|
2957
|
-
await this.#memoryMutate(`user:${autofill.userId}`, FORM_VALUES_KEY, state, (current) => putFormValues(readFormValues(current), updates, this.#config.formValueLimit));
|
|
3469
|
+
await this.#recordInteractionBehavior(state, request, value);
|
|
3470
|
+
}
|
|
3471
|
+
/** 用户提问 → 行为记录(FR-19.2)。记的是「问了什么」,不是模型答了什么 */
|
|
3472
|
+
async #recordUserPrompt(state) {
|
|
3473
|
+
const prompt = state.run.userPrompt.trim();
|
|
3474
|
+
if (prompt === "") return;
|
|
3475
|
+
await this.#appendBehaviorRecords(state, [{
|
|
3476
|
+
id: `${state.runId}#prompt`,
|
|
3477
|
+
kind: "question",
|
|
3478
|
+
at: Date.parse(state.now()),
|
|
3479
|
+
value: prompt,
|
|
3480
|
+
scene: { sessionId: state.run.sessionId }
|
|
3481
|
+
}]);
|
|
2958
3482
|
}
|
|
2959
3483
|
/**
|
|
2960
|
-
*
|
|
2961
|
-
*
|
|
3484
|
+
* 交互提交 → 行为记录(FR-19.2)。写在这里而不是 UiBridge:
|
|
3485
|
+
* 只有 AgentLoop 同时握有场景(技能、字段语义、候选集)与提交值。
|
|
2962
3486
|
*/
|
|
2963
|
-
async #
|
|
2964
|
-
const
|
|
2965
|
-
|
|
2966
|
-
const
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3487
|
+
async #recordInteractionBehavior(state, request, value) {
|
|
3488
|
+
const records = [];
|
|
3489
|
+
const at = Date.parse(state.now());
|
|
3490
|
+
const base = {
|
|
3491
|
+
sessionId: state.run.sessionId,
|
|
3492
|
+
interactionId: request.id
|
|
3493
|
+
};
|
|
3494
|
+
const skill = [...state.activated].at(-1);
|
|
3495
|
+
const scene = skill !== void 0 ? {
|
|
3496
|
+
...base,
|
|
3497
|
+
skillName: skill
|
|
3498
|
+
} : base;
|
|
3499
|
+
if (request.type === "select") records.push({
|
|
3500
|
+
id: `${request.id}#selected`,
|
|
3501
|
+
kind: "choice",
|
|
3502
|
+
at,
|
|
3503
|
+
confirmed: true,
|
|
3504
|
+
value: String(value),
|
|
3505
|
+
scene: {
|
|
3506
|
+
...scene,
|
|
3507
|
+
fieldSemantics: request.message,
|
|
3508
|
+
options: request.options.map((o) => String(o.label))
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3511
|
+
else if (request.type === "ask" && typeof value === "string" && value !== "") records.push({
|
|
3512
|
+
id: `${request.id}#answer`,
|
|
3513
|
+
kind: "text-input",
|
|
3514
|
+
at,
|
|
3515
|
+
confirmed: true,
|
|
3516
|
+
value,
|
|
3517
|
+
scene: {
|
|
3518
|
+
...scene,
|
|
3519
|
+
fieldSemantics: request.message
|
|
3520
|
+
}
|
|
3521
|
+
});
|
|
3522
|
+
else if (request.type === "form" && typeof value === "object" && value !== null) {
|
|
3523
|
+
const submitted = value;
|
|
3524
|
+
for (const field of request.fields) {
|
|
3525
|
+
const next = submitted[field.name];
|
|
3526
|
+
if (typeof next !== "string" && typeof next !== "number" && typeof next !== "boolean") continue;
|
|
3527
|
+
if (next === "") continue;
|
|
3528
|
+
records.push({
|
|
3529
|
+
id: `${request.id}#${field.name}`,
|
|
3530
|
+
kind: field.options ? "choice" : "text-input",
|
|
3531
|
+
at,
|
|
3532
|
+
confirmed: true,
|
|
3533
|
+
value: String(next),
|
|
3534
|
+
scene: {
|
|
3535
|
+
...scene,
|
|
3536
|
+
fieldName: field.name,
|
|
3537
|
+
fieldSemantics: field.description ?? field.label,
|
|
3538
|
+
...field.options ? { options: field.options.map((o) => String(o.label)) } : {}
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
2974
3542
|
}
|
|
3543
|
+
await this.#appendBehaviorRecords(state, records);
|
|
3544
|
+
}
|
|
3545
|
+
/** 行为记录追加(FR-19.2)。默认关闭:未注入 `userProfile` 时零持久化写入 */
|
|
3546
|
+
async #appendBehaviorRecords(state, records) {
|
|
3547
|
+
const profile = this.#deps.userProfile;
|
|
3548
|
+
if (!profile || !this.#deps.memory || records.length === 0) return;
|
|
3549
|
+
const limits = profile.limits ?? DEFAULT_USER_PROFILE_LIMITS;
|
|
3550
|
+
await this.#memoryMutate(`user:${profile.userId}`, BEHAVIOR_RECORDS_KEY, state, (current) => appendBehaviorRecords(readBehaviorRecords(current), records, limits));
|
|
3551
|
+
}
|
|
3552
|
+
/** 画像 → 系统消息(FR-19.4)。读不到或为空时不注入任何内容 */
|
|
3553
|
+
async #userProfileMessage(state) {
|
|
3554
|
+
const profile = this.#deps.userProfile;
|
|
3555
|
+
if (!profile || !this.#deps.memory) return void 0;
|
|
3556
|
+
const limits = profile.limits ?? DEFAULT_USER_PROFILE_LIMITS;
|
|
3557
|
+
const context = renderUserProfileContext(readUserProfile(await this.#memoryGet(`user:${profile.userId}`, USER_PROFILE_KEY, state)), limits.injectMaxBytes);
|
|
3558
|
+
if (context === "") return void 0;
|
|
3559
|
+
return {
|
|
3560
|
+
role: "system",
|
|
3561
|
+
content: textParts(`${context}\n\n${USER_PROFILE_NO_INVENTION_RULE}`)
|
|
3562
|
+
};
|
|
2975
3563
|
}
|
|
2976
3564
|
};
|
|
2977
3565
|
/**
|
|
@@ -3083,6 +3671,7 @@ var WebSkillRuntime = class {
|
|
|
3083
3671
|
return result;
|
|
3084
3672
|
}
|
|
3085
3673
|
async run(userPrompt, options = {}) {
|
|
3674
|
+
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
3675
|
if (!this.#catalogCache) await this.discover();
|
|
3087
3676
|
const cache = this.#catalogCache;
|
|
3088
3677
|
if (!cache) throw new Error("discover() did not populate the catalog cache");
|
|
@@ -3097,7 +3686,9 @@ var WebSkillRuntime = class {
|
|
|
3097
3686
|
}))).flat();
|
|
3098
3687
|
const catalog = providerEntries.length > 0 ? { entries: mergeCatalogEntries(cache.catalog.entries, providerEntries) } : cache.catalog;
|
|
3099
3688
|
const filteredCatalog = this.#deps.catalogFilter ? { entries: await this.#deps.catalogFilter(catalog.entries) } : catalog;
|
|
3100
|
-
const
|
|
3689
|
+
const routedCatalog = options.skillName === void 0 ? filteredCatalog : { entries: filteredCatalog.entries.filter((entry) => entry.name === options.skillName) };
|
|
3690
|
+
if (options.skillName !== void 0 && routedCatalog.entries.length === 0) throw new WebSkillError("SKILL_NOT_FOUND", `Skill "${options.skillName}" is not available in this catalog`);
|
|
3691
|
+
const route = await this.#router.route(routedCatalog);
|
|
3101
3692
|
const loop = new AgentLoop({
|
|
3102
3693
|
llm: this.#deps.llm,
|
|
3103
3694
|
executor: this.#deps.executor,
|
|
@@ -3112,7 +3703,7 @@ var WebSkillRuntime = class {
|
|
|
3112
3703
|
hooks: this.#deps.hooks,
|
|
3113
3704
|
eventBus: this.#events,
|
|
3114
3705
|
longTerm: this.#deps.longTerm,
|
|
3115
|
-
|
|
3706
|
+
userProfile: this.#deps.userProfile,
|
|
3116
3707
|
externalTools: this.#deps.externalTools,
|
|
3117
3708
|
skillProviders: this.#deps.skillProviders,
|
|
3118
3709
|
catalogFilter: this.#deps.catalogFilter,
|
|
@@ -3143,7 +3734,7 @@ var WebSkillRuntime = class {
|
|
|
3143
3734
|
message: `Skill provider listSkills() failed: ${failure}`
|
|
3144
3735
|
});
|
|
3145
3736
|
if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
|
|
3146
|
-
prompt: userPrompt,
|
|
3737
|
+
prompt: promptText(userPrompt),
|
|
3147
3738
|
run: result.run
|
|
3148
3739
|
}).catch((e) => {
|
|
3149
3740
|
result.run.trace.push({
|
|
@@ -3219,7 +3810,7 @@ var WebSkillRuntime = class {
|
|
|
3219
3810
|
hooks: this.#deps.hooks,
|
|
3220
3811
|
eventBus: this.#events,
|
|
3221
3812
|
longTerm: this.#deps.longTerm,
|
|
3222
|
-
|
|
3813
|
+
userProfile: this.#deps.userProfile,
|
|
3223
3814
|
externalTools: this.#deps.externalTools,
|
|
3224
3815
|
skillProviders: this.#deps.skillProviders,
|
|
3225
3816
|
catalogFilter: this.#deps.catalogFilter,
|
|
@@ -3474,6 +4065,191 @@ var FsMemoryStore = class {
|
|
|
3474
4065
|
for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
|
|
3475
4066
|
}
|
|
3476
4067
|
};
|
|
4068
|
+
/**
|
|
4069
|
+
* 提炼提示词。要求返回结论列表而不是散文:条目化是「可见、可编辑、可删除」的前提(FR-19.3)。
|
|
4070
|
+
* `id` 由模型给出稳定 slug,才能在下一次提炼时更新同一条而不是无限追加。
|
|
4071
|
+
*/
|
|
4072
|
+
const USER_PROFILE_REFINE_PROMPT = `You summarize what a single user is like, based on records of what they did in a chat application.
|
|
4073
|
+
|
|
4074
|
+
Return ONLY a JSON object with this shape, and nothing else:
|
|
4075
|
+
|
|
4076
|
+
{
|
|
4077
|
+
"entries": [
|
|
4078
|
+
{ "id": "slug-case-stable-id", "text": "One sentence about the user.", "confidence": "high" | "low" }
|
|
4079
|
+
]
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
Rules:
|
|
4083
|
+
- 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.
|
|
4084
|
+
- Reuse the same "id" across runs for the same conclusion so it can be updated instead of duplicated. Use lowercase words joined by hyphens.
|
|
4085
|
+
- Use "high" only when several records agree. A single record is "low".
|
|
4086
|
+
- Do not restate individual records. Do not include secrets, credentials, or full personal identifiers.
|
|
4087
|
+
- Do not invent anything the records do not support. If the records say nothing useful, return {"entries": []}.`;
|
|
4088
|
+
/** 采样后的记录 → 送模型的摘要。导出供测试与宿主复核实际请求内容(AC-19.6) @experimental */
|
|
4089
|
+
function toRecordDigests(records) {
|
|
4090
|
+
return records.map((record) => ({
|
|
4091
|
+
kind: record.kind,
|
|
4092
|
+
at: new Date(record.at).toISOString(),
|
|
4093
|
+
value: record.value,
|
|
4094
|
+
...record.scene.skillName !== void 0 ? { skill: record.scene.skillName } : {},
|
|
4095
|
+
...record.scene.fieldName !== void 0 ? { field: record.scene.fieldName } : {},
|
|
4096
|
+
...record.scene.fieldSemantics !== void 0 ? { meaning: record.scene.fieldSemantics } : {},
|
|
4097
|
+
...record.scene.options !== void 0 ? { options: record.scene.options } : {},
|
|
4098
|
+
...record.confirmed === true ? { confirmed: true } : {}
|
|
4099
|
+
}));
|
|
4100
|
+
}
|
|
4101
|
+
/** 模型可能把 JSON 包在 ```json 里;只取第一个对象,解析不出就当没结论 */
|
|
4102
|
+
function parseEntries(content, now) {
|
|
4103
|
+
const text = promptText(content ?? []);
|
|
4104
|
+
const start = text.indexOf("{");
|
|
4105
|
+
const end = text.lastIndexOf("}");
|
|
4106
|
+
if (start < 0 || end <= start) return [];
|
|
4107
|
+
let parsed;
|
|
4108
|
+
try {
|
|
4109
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
4110
|
+
} catch {
|
|
4111
|
+
return [];
|
|
4112
|
+
}
|
|
4113
|
+
const raw = parsed?.entries;
|
|
4114
|
+
return readProfileEntries(Array.isArray(raw) ? raw.map((item) => ({
|
|
4115
|
+
updatedAt: now,
|
|
4116
|
+
...item
|
|
4117
|
+
})) : []);
|
|
4118
|
+
}
|
|
4119
|
+
/**
|
|
4120
|
+
* 一次提炼(FR-19.3)。调用方负责持久化返回值;本函数不碰存储,
|
|
4121
|
+
* 也不吞异常——「失败不影响会话结束」由调用点的 fire-and-forget 保证,
|
|
4122
|
+
* 在这里吞掉会让手动提炼入口无法报错。
|
|
4123
|
+
* @experimental
|
|
4124
|
+
*/
|
|
4125
|
+
async function refineUserProfile(input) {
|
|
4126
|
+
const limits = input.limits ?? DEFAULT_USER_PROFILE_LIMITS;
|
|
4127
|
+
const now = (input.now ?? Date.now)();
|
|
4128
|
+
const current = input.profile ?? EMPTY_USER_PROFILE;
|
|
4129
|
+
const sampled = sampleBehaviorRecords(input.records, limits);
|
|
4130
|
+
if (sampled.length === 0) return current;
|
|
4131
|
+
const existing = current.entries.map((entry) => ({
|
|
4132
|
+
id: entry.id,
|
|
4133
|
+
text: entry.text
|
|
4134
|
+
}));
|
|
4135
|
+
const entries = parseEntries((await input.llm.complete({
|
|
4136
|
+
...input.model !== void 0 ? { model: input.model } : {},
|
|
4137
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
4138
|
+
messages: [{
|
|
4139
|
+
role: "system",
|
|
4140
|
+
content: [{
|
|
4141
|
+
type: "text",
|
|
4142
|
+
text: USER_PROFILE_REFINE_PROMPT
|
|
4143
|
+
}]
|
|
4144
|
+
}, {
|
|
4145
|
+
role: "user",
|
|
4146
|
+
content: [{
|
|
4147
|
+
type: "text",
|
|
4148
|
+
text: `Existing conclusions:\n${JSON.stringify(existing)}\n\nRecords:\n${JSON.stringify(toRecordDigests(sampled))}`
|
|
4149
|
+
}]
|
|
4150
|
+
}]
|
|
4151
|
+
})).content, now);
|
|
4152
|
+
if (entries.length === 0) return current;
|
|
4153
|
+
return mergeProfileEntries(current, entries, {
|
|
4154
|
+
now,
|
|
4155
|
+
limits
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
/** 导出文件的版本;导入时逐字校验,不做向前兼容猜测 @experimental */
|
|
4159
|
+
const USER_PROFILE_EXPORT_VERSION = 1;
|
|
4160
|
+
/** 与偏好导出同一套判据:归一化后按子串匹配,命中即拒 */
|
|
4161
|
+
const CREDENTIAL_MARKERS = [
|
|
4162
|
+
"apikey",
|
|
4163
|
+
"secret",
|
|
4164
|
+
"token",
|
|
4165
|
+
"password",
|
|
4166
|
+
"passphrase",
|
|
4167
|
+
"credential",
|
|
4168
|
+
"privatekey"
|
|
4169
|
+
];
|
|
4170
|
+
const isCredentialKey = (key) => {
|
|
4171
|
+
const normalized = key.toLowerCase().replace(/[-_\s]/g, "");
|
|
4172
|
+
return CREDENTIAL_MARKERS.some((marker) => normalized.includes(marker));
|
|
4173
|
+
};
|
|
4174
|
+
function findCredentialKey(value) {
|
|
4175
|
+
if (Array.isArray(value)) {
|
|
4176
|
+
for (const item of value) {
|
|
4177
|
+
const hit = findCredentialKey(item);
|
|
4178
|
+
if (hit !== void 0) return hit;
|
|
4179
|
+
}
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
4183
|
+
for (const [key, child] of Object.entries(value)) {
|
|
4184
|
+
if (isCredentialKey(key)) return key;
|
|
4185
|
+
const hit = findCredentialKey(child);
|
|
4186
|
+
if (hit !== void 0) return hit;
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
/**
|
|
4190
|
+
* 导出画像(FR-19.7)。`excludeIds` 来自导出前的审阅界面:用户勾掉的条目不进文件。
|
|
4191
|
+
* @experimental
|
|
4192
|
+
*/
|
|
4193
|
+
function exportUserProfile(profile, options) {
|
|
4194
|
+
const excluded = new Set(options.excludeIds ?? []);
|
|
4195
|
+
const result = {
|
|
4196
|
+
kind: "webskill.user-profile",
|
|
4197
|
+
version: 1,
|
|
4198
|
+
origin: options.origin,
|
|
4199
|
+
exportedAt: (options.now ?? Date.now)(),
|
|
4200
|
+
entries: profile.entries.filter((entry) => !excluded.has(entry.id))
|
|
4201
|
+
};
|
|
4202
|
+
const leaked = findCredentialKey(result);
|
|
4203
|
+
if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `A user profile export must not contain credentials, but "${leaked}" was present`);
|
|
4204
|
+
return result;
|
|
4205
|
+
}
|
|
4206
|
+
/** 解析并校验导入内容(FR-19.7)。含凭据是**拒绝**而不是忽略 @experimental */
|
|
4207
|
+
function parseUserProfileExport(raw) {
|
|
4208
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must be a JSON object");
|
|
4209
|
+
const record = raw;
|
|
4210
|
+
if (record["kind"] !== "webskill.user-profile") throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must have \"kind\": \"webskill.user-profile\"");
|
|
4211
|
+
if (record["version"] !== 1) throw new WebSkillError("PROFILE_IMPORT_VERSION_UNSUPPORTED", `Unsupported user profile file version ${JSON.stringify(record["version"])}; expected 1`);
|
|
4212
|
+
const leaked = findCredentialKey(record);
|
|
4213
|
+
if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `The user profile file contains a credential field "${leaked}"; the import was rejected`);
|
|
4214
|
+
const entries = readProfileEntries(record["entries"]);
|
|
4215
|
+
if (entries.length === 0) throw new WebSkillError("PROFILE_IMPORT_INVALID", "The user profile file has no usable entries");
|
|
4216
|
+
return {
|
|
4217
|
+
kind: "webskill.user-profile",
|
|
4218
|
+
version: 1,
|
|
4219
|
+
origin: typeof record["origin"] === "string" ? record["origin"] : "unknown",
|
|
4220
|
+
exportedAt: typeof record["exportedAt"] === "number" ? record["exportedAt"] : 0,
|
|
4221
|
+
entries
|
|
4222
|
+
};
|
|
4223
|
+
}
|
|
4224
|
+
/** 导入前给用户看的差异(FR-19.7):新增哪些、覆盖哪些 @experimental */
|
|
4225
|
+
function diffUserProfile(current, incoming) {
|
|
4226
|
+
const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
|
|
4227
|
+
const added = [];
|
|
4228
|
+
const updated = [];
|
|
4229
|
+
for (const entry of incoming.entries) {
|
|
4230
|
+
const existing = byId.get(entry.id);
|
|
4231
|
+
if (existing === void 0) added.push(entry);
|
|
4232
|
+
else if (existing.text !== entry.text) updated.push(entry);
|
|
4233
|
+
}
|
|
4234
|
+
return {
|
|
4235
|
+
added,
|
|
4236
|
+
updated
|
|
4237
|
+
};
|
|
4238
|
+
}
|
|
4239
|
+
/** 确认后落库:同 id 覆盖,其余保留;不触碰模型配置与凭据 @experimental */
|
|
4240
|
+
function applyUserProfileImport(current, incoming, options = {}) {
|
|
4241
|
+
const now = (options.now ?? Date.now)();
|
|
4242
|
+
const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
|
|
4243
|
+
for (const entry of incoming.entries) byId.set(entry.id, {
|
|
4244
|
+
...entry,
|
|
4245
|
+
updatedAt: now
|
|
4246
|
+
});
|
|
4247
|
+
return {
|
|
4248
|
+
version: 1,
|
|
4249
|
+
entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, DEFAULT_USER_PROFILE_LIMITS.entryLimit),
|
|
4250
|
+
refinedAt: current.refinedAt
|
|
4251
|
+
};
|
|
4252
|
+
}
|
|
3477
4253
|
const INDEX_FILE$1 = "index.json";
|
|
3478
4254
|
/**
|
|
3479
4255
|
* 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
|
|
@@ -3825,12 +4601,16 @@ var FsRunTraceStore = class {
|
|
|
3825
4601
|
#root;
|
|
3826
4602
|
#fs;
|
|
3827
4603
|
#onError;
|
|
4604
|
+
#onIndexError;
|
|
3828
4605
|
constructor(deps) {
|
|
3829
4606
|
this.#root = deps.root.replace(/\/+$/, "");
|
|
3830
4607
|
this.#fs = deps.fs;
|
|
3831
4608
|
this.#onError = deps.onError ?? ((error, run) => {
|
|
3832
4609
|
console.warn(`Failed to persist run trace "${run.id}": ${messageOf(error)}`);
|
|
3833
4610
|
});
|
|
4611
|
+
this.#onIndexError = deps.onIndexError ?? ((error, run) => {
|
|
4612
|
+
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.`);
|
|
4613
|
+
});
|
|
3834
4614
|
}
|
|
3835
4615
|
#path(runId) {
|
|
3836
4616
|
return resolveInsideRoot(this.#root, `${runId}.json`);
|
|
@@ -3848,9 +4628,14 @@ var FsRunTraceStore = class {
|
|
|
3848
4628
|
};
|
|
3849
4629
|
try {
|
|
3850
4630
|
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
4631
|
} catch (e) {
|
|
3853
4632
|
this.#onError(e, run);
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
try {
|
|
4636
|
+
await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
|
|
4637
|
+
} catch (e) {
|
|
4638
|
+
this.#onIndexError(e, run);
|
|
3854
4639
|
}
|
|
3855
4640
|
}
|
|
3856
4641
|
async get(runId) {
|
|
@@ -4001,6 +4786,7 @@ const toMeta = (record) => ({
|
|
|
4001
4786
|
messageCount: record.messages.length
|
|
4002
4787
|
});
|
|
4003
4788
|
const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4789
|
+
const baseName = (path) => path.split("/").pop() ?? path;
|
|
4004
4790
|
/**
|
|
4005
4791
|
* 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
|
|
4006
4792
|
* 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
|
|
@@ -4094,8 +4880,13 @@ var FsSessionStore = class {
|
|
|
4094
4880
|
return record;
|
|
4095
4881
|
}
|
|
4096
4882
|
async list(options = {}) {
|
|
4097
|
-
if (!await this.#fs.exists(this.#root)) return {
|
|
4883
|
+
if (!await this.#fs.exists(this.#root)) return {
|
|
4884
|
+
items: [],
|
|
4885
|
+
source: "missing-root",
|
|
4886
|
+
skipped: []
|
|
4887
|
+
};
|
|
4098
4888
|
const metas = [];
|
|
4889
|
+
const skipped = [];
|
|
4099
4890
|
for (const entry of await this.#fs.list(this.#root)) {
|
|
4100
4891
|
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
4101
4892
|
let record;
|
|
@@ -4103,13 +4894,18 @@ var FsSessionStore = class {
|
|
|
4103
4894
|
record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
|
|
4104
4895
|
} catch (e) {
|
|
4105
4896
|
console.warn(`Skipping unreadable session file "${entry.path}": ${e instanceof Error ? e.message : String(e)}`);
|
|
4897
|
+
skipped.push(baseName(entry.path));
|
|
4106
4898
|
continue;
|
|
4107
4899
|
}
|
|
4108
4900
|
if (record.archived === true && options.includeArchived !== true) continue;
|
|
4109
4901
|
metas.push(toMeta(record));
|
|
4110
4902
|
}
|
|
4111
4903
|
metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
4112
|
-
return
|
|
4904
|
+
return {
|
|
4905
|
+
...takeTailPage(metas, options, "session"),
|
|
4906
|
+
source: "ok",
|
|
4907
|
+
skipped
|
|
4908
|
+
};
|
|
4113
4909
|
}
|
|
4114
4910
|
/**
|
|
4115
4911
|
* 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
|
|
@@ -4176,4 +4972,4 @@ var FsSessionStore = class {
|
|
|
4176
4972
|
};
|
|
4177
4973
|
|
|
4178
4974
|
//#endregion
|
|
4179
|
-
export {
|
|
4975
|
+
export { listSkillScripts as $, TraceRecorder as A, buildRenderResult as B, READ_SKILL_FILE_INPUT_SCHEMA as C, validateUiSpecEvent as Ct, RUN_TRACE_SCHEMA_VERSION as D, RUN_SNAPSHOT_SCHEMA_VERSION as E, USER_PROFILE_REFINE_PROMPT as F, extractChartSpec as G, createWebSkillApi as H, WebSkillRuntime as I, formatSkillScriptManifest as J, extractTodoTraceEvents as K, appendBehaviorRecords as L, USER_PROFILE_KEY as M, USER_PROFILE_NO_INVENTION_RULE as N, SESSION_SCHEMA_VERSION as O, USER_PROFILE_PROMPT_HEADER as P, isUnsupportedRunSnapshot as Q, applyUserProfileImport as R, ProgressiveRouter as S, toVercelToolSpecs as St, READ_SKILL_FILE_TOOL_NAME as T, diffUserProfile as U, createScriptContext as V, exportUserProfile as W, fromVercelStreamPart as X, fromVercelResult as Y, isNetworkAllowed as Z, FsSessionStore as _, schemaToForm as _t, AgentLoop as a, normalizeToolContent as at, HookRunner as b, toLlmToolSpec as bt, CapabilityApproval as c, parseUserProfileExport as ct, EventBus as d, readUserProfile as dt, mergeCatalogEntries as et, FS_SESSION_PAGE_SIZE as f, refineUserProfile as ft, FsRunTraceStore as g, schemaSourceLabel as gt, FsRunSnapshotStore as h, sampleBehaviorRecords as ht, ASK_USER_TOOL_NAME as i, normalizeErrorCode as it, USER_PROFILE_EXPORT_VERSION as j, SerializingMemoryStore as k, DEFAULT_USER_PROFILE_LIMITS as l, readBehaviorRecords as lt, FsMemoryStore as m, resolveToolName as mt, ASK_USER_INPUT_SCHEMA as n, networkPolicyLibSource as nt, AnthropicClient as o, normalizeToolError as ot, FsArtifactStore as p, renderUserProfileContext as pt, extractUiSpecEvents as q, ASK_USER_TOOL as r, networkUrlHost as rt, BEHAVIOR_RECORDS_KEY as s, parseBridgeRequest as st, ALLOWED_TOOLS_EXCLUSION_REASON as t, mergeProfileEntries as tt, EMPTY_USER_PROFILE as u, readProfileEntries as ut, FullDisclosureRouter as v, scriptToolName as vt, READ_SKILL_FILE_TOOL as w, validateUiSpecNode as wt, OpenAiCompatibleClient as x, toRecordDigests as xt, GoogleGenAiClient as y, summarizeToolCalls as yt, bridgeError as z };
|