@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.
Files changed (36) hide show
  1. package/dist/agent.d.ts +2 -2
  2. package/dist/agent.js +274 -42
  3. package/dist/browser.d.ts +127 -5
  4. package/dist/browser.js +611 -17
  5. package/dist/{catalogComponents-DV7cPpUm-C77AEEx9.js → catalogComponents-Dr5dFMAb-DKH_7VPI.js} +848 -974
  6. package/dist/{dist-6C03DShK.js → dist-D0qW6e40.js} +1213 -210
  7. package/dist/{dist-bewtXYlO.js → dist-DnYG2-eY.js} +53 -14
  8. package/dist/{env--jJB-TSX-04klhTYi.js → env-8cY40DXB-CGnEVZby.js} +7 -6
  9. package/dist/{env-BPUBZCwJ-4jat_SVG.d.ts → env-AK3cSMEA-Dli6QU5E.d.ts} +4 -3
  10. package/dist/eventTypes-DjIQpt8Y-Bj3vghj4.js +32 -0
  11. package/dist/governance.d.ts +78 -11
  12. package/dist/governance.js +173 -50
  13. package/dist/{index-D_7ZZjkl.d.ts → index-Ba3xFtfz.d.ts} +222 -8
  14. package/dist/{index-Bsqg4ftU.d.ts → index-BwsK9lGk.d.ts} +13 -7
  15. package/dist/{index-vBz_FC9w.d.ts → index-DkbABR43.d.ts} +336 -52
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +3 -3
  18. package/dist/mcp.d.ts +159 -11
  19. package/dist/mcp.js +276 -45
  20. package/dist/{memoryArtifactStore-BtOeB_hm-tj3fC5ip.js → memoryArtifactStore-52Zn9npI-BMPYwvoy.js} +10 -2
  21. package/dist/node.d.ts +4 -4
  22. package/dist/node.js +11 -3
  23. package/dist/{openUiLibrary-W3Ce896k-ClFTRZFs.js → openUiLibrary-Bdrji9qK-D2LxmM-a.js} +3 -3
  24. package/dist/{skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts → skillVersionStore-Bl-ElD45-CWPvGvoq.d.ts} +8 -2
  25. package/dist/{testing-DDCJWvgA.js → testing-CYTFqkDm.js} +1 -1
  26. package/dist/testing.d.ts +2 -2
  27. package/dist/testing.js +3 -3
  28. package/dist/{types-D_hoCri8-BnNPiZCi.d.ts → types-CcxRLdJG-DCXyw1US.d.ts} +62 -10
  29. package/dist/ui-react.d.ts +5 -3
  30. package/dist/ui-react.js +72 -47
  31. package/dist/ui-vue.d.ts +1 -1
  32. package/dist/ui-vue.js +13 -8
  33. package/dist/ui.d.ts +3 -3
  34. package/dist/ui.js +2 -2
  35. package/dist/{webskillLitCatalog-_mugzRHx-DiuJpCuf.js → webskillLitCatalog-_mugzRHx-B_54vxum.js} +1 -1
  36. 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 validateLlmMessages, i as textParts, n as partsToText, r as rejectUnsupportedPart, t as MemoryArtifactStore } from "./memoryArtifactStore-BtOeB_hm-tj3fC5ip.js";
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
- const rawArgs = tc.function?.arguments ?? "{}";
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: JSON.parse(rawArgs)
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}". Activate the owning skill first by reading its SKILL.md`
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; call with no arguments or a simple object.";
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: { question: {
950
- type: "string",
951
- description: "The question to ask the user"
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$3 = (v) => typeof v === "object" && v !== null;
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$3(data)) return void 0;
1399
+ if (!isRecord$4(data)) return void 0;
1001
1400
  const raw = data["$chart"];
1002
- if (!isRecord$3(raw)) return void 0;
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$3(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
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
- if (output.trim() !== "") blocks.push({
1031
- type: "markdown",
1032
- text: output
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$2(value) {
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$2(value)) return false;
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$2(value)) reject(`${path} must be an object`);
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$2(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
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$2(action)) reject("A surface action must be an object");
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$2(value)) reject("A surface patch operation must be an object");
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$2(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
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$2(data) || data["$surface"] === void 0) return [];
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, options) {
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$1 = (v) => typeof v === "object" && v !== null;
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$1(data) || data["$todo"] === void 0) return [];
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$1(entry)) continue;
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 baseName = (p) => p.split("/").pop() ?? p;
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 ?? 10,
1607
- totalTimeoutMs: config.totalTimeoutMs ?? 12e4,
1608
- toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
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
- formValueLimit: config.formValueLimit ?? 100,
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
- * 取消在下一个中断点生效(LLM complete/stream 调用;交互等待不强制中断)。
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([route.systemPrompt, ...externalSystemPrompts].join("\n\n"))
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: textParts(await this.#serializeToolResult(call, result, state))
2431
+ content: await this.#toolResultParts(call, result, state)
1853
2432
  });
1854
- await this.#drainSurfaceAction(state);
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: textParts(await this.#serializeToolResult(pendingCall, result, state))
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: textParts(await this.#serializeToolResult(pendingCall, result, state))
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: textParts(await this.#serializeToolResult(pendingCall, result, state))
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: textParts(await this.#serializeToolResult(next, result, state))
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
- #withInteractionTimeout(promise, timeoutMs, onTimeout) {
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 timer = setTimeout(() => {
2825
+ const signal = state.controller.signal;
2826
+ let settled = false;
2827
+ const abandon = () => {
2221
2828
  try {
2222
- onTimeout?.();
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
- clearTimeout(timer);
2855
+ if (settled) return;
2856
+ cleanup();
2228
2857
  resolve(v);
2229
2858
  }, (e) => {
2230
- clearTimeout(timer);
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 = source ? await source.call(call.name, call.arguments) : toolError(resolution.code, resolution.message);
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) => baseName(s.path));
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
- for (const file of scriptFiles) {
2694
- const match = /^(.*)\.(ts|js)$/.exec(file);
2695
- if (!match?.[1]) continue;
2696
- if (allowedTools && !allowedTools.includes(match[1])) {
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, match[1]);
2702
- await this.#enrichDefinition(root, match[1], def, state);
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 "${match[1]}" for skill "${skillName}": ${messageOf(e)}` });
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) def.inputSchema = 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) return toolError("TOOL_NOT_FOUND", `Tool "${call.name}" is not available for skill "${skillName}"`);
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, { skillName });
2757
- await this.#attachFormSuggestions(fields, state);
2758
- const value = await this.#interact(state, {
2759
- type: "form",
2760
- id: this.#nextInteractionId(state),
2761
- title: `Missing parameters for ${call.name}`,
2762
- fields
2763
- }, {
2764
- tool: call.name,
2765
- missing
2766
- });
2767
- if (typeof value === "object" && value !== null) args = {
2768
- ...args,
2769
- ...value
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 (!result.ok) await this.#reportSkillFailure(skillName, state, {
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.#rememberFormValues(state, request, value);
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
- * 跨会话字段值的写入(FR-5.6/5.12)。与 `paramHistory` 双写而不是合并:
2938
- * 那边是按时间的追加序列(运行观测),这边是按字段的最新值(召回),形态不同。
3691
+ * 交互提交 → 行为记录(FR-19.2)。写在这里而不是 UiBridge:
3692
+ * 只有 AgentLoop 同时握有场景(技能、字段语义、候选集)与提交值。
2939
3693
  */
2940
- async #rememberFormValues(state, request, value) {
2941
- const autofill = this.#deps.formAutofill;
2942
- if (!autofill || request.type !== "form") return;
2943
- if (typeof value !== "object" || value === null) return;
2944
- const submitted = value;
2945
- const ts = Date.parse(state.now());
2946
- const updates = {};
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));
2958
- }
2959
- /**
2960
- * 召回(FR-5.8):命中的历史值挂在 `suggestion` 上,**绝不写进 `defaultValue`**——
2961
- * 后者会被渲染器直接填进控件,等于静默预填(AC-5.6 禁止)。
2962
- */
2963
- async #attachFormSuggestions(fields, state) {
2964
- const autofill = this.#deps.formAutofill;
2965
- if (!autofill) return;
2966
- const stored = readFormValues(await this.#memoryGet(`user:${autofill.userId}`, FORM_VALUES_KEY, state));
2967
- for (const field of fields) {
2968
- if (field.fieldKey === void 0 || field.defaultValue !== void 0) continue;
2969
- const hit = stored[field.fieldKey];
2970
- if (hit !== void 0) field.suggestion = {
2971
- value: hit.value,
2972
- ts: hit.ts
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 route = await this.#router.route(filteredCatalog);
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
- formAutofill: this.#deps.formAutofill,
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
- formAutofill: this.#deps.formAutofill,
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 { items: [] };
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 takeTailPage(metas, options, "session");
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 { schemaToForm as $, buildRenderResult as A, fromVercelStreamPart as B, RUN_SNAPSHOT_SCHEMA_VERSION as C, TraceRecorder as D, SerializingMemoryStore as E, extractChartSpec as F, networkUrlHost as G, isUnsupportedRunSnapshot as H, extractTodoTraceEvents as I, normalizeToolError as J, normalizeErrorCode as K, extractUiSpecEvents as L, clearStoredFormValues as M, createScriptContext as N, WebSkillRuntime as O, createWebSkillApi as P, resolveToolName as Q, formFieldKey as R, READ_SKILL_FILE_TOOL_NAME as S, SESSION_SCHEMA_VERSION as T, mergeCatalogEntries as U, isNetworkAllowed as V, networkPolicyLibSource as W, putFormValues as X, parseBridgeRequest as Y, readFormValues as Z, HookRunner as _, AnthropicClient as a, READ_SKILL_FILE_INPUT_SCHEMA as b, FORM_VALUES_KEY as c, FsMemoryStore as d, summarizeToolCalls as et, FsRunSnapshotStore as f, GoogleGenAiClient as g, FullDisclosureRouter as h, AgentLoop as i, validateUiSpecNode as it, clearFormValues as j, bridgeError as k, FS_SESSION_PAGE_SIZE as l, FsSessionStore as m, ASK_USER_TOOL as n, toVercelToolSpecs as nt, CapabilityApproval as o, FsRunTraceStore as p, normalizeToolContent as q, ASK_USER_TOOL_NAME as r, validateUiSpecEvent as rt, EventBus as s, ASK_USER_INPUT_SCHEMA as t, toLlmToolSpec as tt, FsArtifactStore as u, OpenAiCompatibleClient as v, RUN_TRACE_SCHEMA_VERSION as w, READ_SKILL_FILE_TOOL as x, ProgressiveRouter as y, fromVercelResult as z };
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 };