@webskill/sdk 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agent.d.ts +2 -0
  2. package/dist/agent.js +909 -0
  3. package/dist/browser.d.ts +233 -4
  4. package/dist/browser.js +869 -19
  5. package/dist/{catalogComponents-KsujmL4b-Clx1kCnU.js → catalogComponents-Dr5dFMAb-Dacibl1e.js} +372 -126
  6. package/dist/{dist-D9Lcn5Pp.js → dist-DnYG2-eY.js} +642 -39
  7. package/dist/{dist-C-Sh0MDU.js → dist-DusANsrn.js} +1035 -99
  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/governance.d.ts +46 -4
  11. package/dist/governance.js +45 -2
  12. package/dist/{index-CHXxDccV.d.ts → index-BMocOEi0.d.ts} +106 -10
  13. package/dist/index-BuTpBMzr.d.ts +474 -0
  14. package/dist/{index-DLfR2Y6I.d.ts → index-C-KFAZoF.d.ts} +337 -18
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +3 -3
  17. package/dist/mcp.d.ts +39 -8
  18. package/dist/mcp.js +53 -19
  19. package/dist/{memoryArtifactStore-BtOeB_hm-tj3fC5ip.js → memoryArtifactStore-52Zn9npI-BMPYwvoy.js} +10 -2
  20. package/dist/node.d.ts +4 -4
  21. package/dist/node.js +9 -2
  22. package/dist/{openUiLibrary-YLS-cxyT-C96jWDQq.js → openUiLibrary-Bdrji9qK-DzAxRlTY.js} +3 -3
  23. package/dist/{skillVersionStore-uyefLPR1-DXOzbksv.d.ts → skillVersionStore-BzLbzFOL-CxdAFWO2.d.ts} +4 -3
  24. package/dist/{testing-DDCJWvgA.js → testing-CYTFqkDm.js} +1 -1
  25. package/dist/testing.d.ts +2 -2
  26. package/dist/testing.js +3 -3
  27. package/dist/{types-7Wcg--Vh-1YlQ4jF9.d.ts → types-4pg-qp_I-Gq63X8Oa.d.ts} +55 -5
  28. package/dist/ui-react.d.ts +26 -5
  29. package/dist/ui-react.js +147 -29
  30. package/dist/ui-vue.d.ts +1 -1
  31. package/dist/ui-vue.js +30 -6
  32. package/dist/ui.d.ts +4 -4
  33. package/dist/ui.js +3 -3
  34. package/dist/{webskillLitCatalog-CSTbhBe_-CYIs5BX8.js → webskillLitCatalog-_mugzRHx-B_54vxum.js} +88 -2
  35. package/package.json +6 -1
@@ -1,7 +1,201 @@
1
1
  import { A as parseSkillMarkdown, L as resolveInsideRoot, O as messageOf, P as renderAvailableSkillsXml, V as validateSkills, f as SkillDiscovery, g as assertSafePathSegment, m as WebSkillError, p as SkillReader, v as buildCatalog } from "./dist-8oQRa8Xz.js";
2
- import { a as 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」的映射。 */
16
+ const SCHEMA_MAP_KEYS = /* @__PURE__ */ new Set([
17
+ "properties",
18
+ "patternProperties",
19
+ "dependentSchemas",
20
+ "$defs",
21
+ "definitions"
22
+ ]);
23
+ /** 值为「子 schema 数组」。 */
24
+ const SCHEMA_LIST_KEYS = /* @__PURE__ */ new Set([
25
+ "oneOf",
26
+ "anyOf",
27
+ "allOf",
28
+ "prefixItems"
29
+ ]);
30
+ /** 值为单个子 schema(items 在 draft-07 遗留写法里也可能是数组)。 */
31
+ const SCHEMA_KEYS = /* @__PURE__ */ new Set([
32
+ "items",
33
+ "additionalItems",
34
+ "additionalProperties",
35
+ "unevaluatedItems",
36
+ "unevaluatedProperties",
37
+ "contains",
38
+ "propertyNames",
39
+ "not",
40
+ "if",
41
+ "then",
42
+ "else"
43
+ ]);
44
+ function isRecord$4(value) {
45
+ return typeof value === "object" && value !== null && !Array.isArray(value);
46
+ }
47
+ function isSelfRef(value) {
48
+ return isRecord$4(value) && value["$ref"] === SELF_REF;
49
+ }
50
+ /**
51
+ * 按 JSON Schema 关键字下降到子 schema。
52
+ * 只走 schema 位置,`enum` / `const` / `default` 等数据位置一律不进——
53
+ * 那里出现的 `{ $ref: '#' }` 是字面数据而非引用,改写它会破坏语义。
54
+ */
55
+ function forEachSubSchema(node, visit) {
56
+ for (const [key, value] of Object.entries(node)) {
57
+ if (SCHEMA_MAP_KEYS.has(key)) {
58
+ if (isRecord$4(value)) for (const name of Object.keys(value)) visit(value[name], [key, name]);
59
+ continue;
60
+ }
61
+ if (SCHEMA_LIST_KEYS.has(key)) {
62
+ if (Array.isArray(value)) value.forEach((item, index) => visit(item, [key, index]));
63
+ continue;
64
+ }
65
+ if (SCHEMA_KEYS.has(key)) if (Array.isArray(value)) value.forEach((item, index) => visit(item, [key, index]));
66
+ else visit(value, [key]);
67
+ }
68
+ }
69
+ function pathKey(path) {
70
+ return path.join("\0");
71
+ }
72
+ /**
73
+ * 找出所有 `$ref: '#'` 所属的 schema resource 根。
74
+ * `#` 解析到最近的带 `$id` 的祖先;没有则是文档根。
75
+ */
76
+ function collectSelfRefRoots(node, path, resourceRoot, out) {
77
+ if (!isRecord$4(node)) return;
78
+ if (isSelfRef(node)) {
79
+ out.set(pathKey(resourceRoot), resourceRoot);
80
+ return;
81
+ }
82
+ const nextRoot = typeof node["$id"] === "string" && path.length > 0 ? path : resourceRoot;
83
+ forEachSubSchema(node, (child, childPath) => {
84
+ collectSelfRefRoots(child, [...path, ...childPath], nextRoot, out);
85
+ });
86
+ }
87
+ function getAt(schema, path) {
88
+ let current = schema;
89
+ for (const segment of path) if (Array.isArray(current) && typeof segment === "number") current = current[segment];
90
+ else if (isRecord$4(current) && typeof segment === "string") current = current[segment];
91
+ else return void 0;
92
+ return current;
93
+ }
94
+ /** 返回把 `path` 处替换为 `replacement` 后的新对象;沿途节点浅拷贝,其余共享。 */
95
+ function setAt(schema, path, replacement) {
96
+ if (path.length === 0) return replacement;
97
+ const [head, ...rest] = path;
98
+ const child = getAt(schema, [head]);
99
+ const nextChild = rest.length === 0 ? replacement : setAt(child, rest, replacement);
100
+ if (Array.isArray(schema) && typeof head === "number") {
101
+ const copy = [...schema];
102
+ copy[head] = nextChild;
103
+ return copy;
104
+ }
105
+ return {
106
+ ...schema,
107
+ [head]: nextChild
108
+ };
109
+ }
110
+ /**
111
+ * 深拷贝并把 `{ $ref: '#' }` 改写为指向根 `$defs` 中的具名节点。
112
+ * 同时剥掉非根位置的 `$id` / `$schema`:它们会重设 base URI,
113
+ * 正是严格解析器无法解析 `#` 的直接原因。
114
+ */
115
+ function rewriteSelfRefs(node, target, depth) {
116
+ if (!isRecord$4(node)) return node;
117
+ if (depth > 8) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool schema nesting exceeds 8 levels; cannot produce a portable form`);
118
+ if (isSelfRef(node)) {
119
+ const rest = {};
120
+ for (const [key, value] of Object.entries(node)) if (key !== "$ref") rest[key] = value;
121
+ return {
122
+ $ref: `#/${DEFS_KEY}/${target}`,
123
+ ...rest
124
+ };
125
+ }
126
+ const result = {};
127
+ for (const [key, value] of Object.entries(node)) {
128
+ if (key === "$id" || key === "$schema") continue;
129
+ if (SCHEMA_MAP_KEYS.has(key) && isRecord$4(value)) {
130
+ const mapped = {};
131
+ for (const [name, child] of Object.entries(value)) mapped[name] = rewriteSelfRefs(child, target, depth + 1);
132
+ result[key] = mapped;
133
+ continue;
134
+ }
135
+ if (SCHEMA_LIST_KEYS.has(key) || SCHEMA_KEYS.has(key)) {
136
+ result[key] = Array.isArray(value) ? value.map((child) => rewriteSelfRefs(child, target, depth + 1)) : rewriteSelfRefs(value, target, depth + 1);
137
+ continue;
138
+ }
139
+ result[key] = value;
140
+ }
141
+ return result;
142
+ }
143
+ function uniqueName(base, taken) {
144
+ if (!taken.has(base)) return base;
145
+ const prefixed = `Portable${base}`;
146
+ if (!taken.has(prefixed)) return prefixed;
147
+ let index = 2;
148
+ while (taken.has(`${prefixed}_${index}`)) index += 1;
149
+ return `${prefixed}_${index}`;
150
+ }
151
+ /**
152
+ * 把工具 inputSchema 重写为严格解析器可接受的 JSON Schema 2020-12 形态:
153
+ * 自引用的 schema resource 提到根 `$defs`,所有 `$ref: '#'` 改写为 `$ref: '#/$defs/<Node>'`。
154
+ *
155
+ * 幂等:不含自引用的 schema 原样返回。
156
+ *
157
+ * 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop,
158
+ * 否则 trace / eventBus 观测到的 schema 会与技能作者写的不一致。
159
+ */
160
+ function toPortableToolSchema(schema) {
161
+ const roots = /* @__PURE__ */ new Map();
162
+ collectSelfRefRoots(schema, [], [], roots);
163
+ if (roots.size === 0) return schema;
164
+ if (roots.size > 1) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", "Tool schema contains self references in more than one schema resource; cannot produce a portable form");
165
+ const rootPath = [...roots.values()][0] ?? [];
166
+ const resource = getAt(schema, rootPath);
167
+ if (!isRecord$4(resource)) return schema;
168
+ const existingDefs = isRecord$4(schema[DEFS_KEY]) ? schema[DEFS_KEY] : void 0;
169
+ const taken = new Set(Object.keys(existingDefs ?? {}));
170
+ const nodeName = uniqueName(NODE_NAME, taken);
171
+ taken.add(nodeName);
172
+ const added = {};
173
+ const branches = resource["oneOf"];
174
+ if (Array.isArray(branches)) {
175
+ const refs = branches.map((branch, index) => {
176
+ const name = uniqueName(`${COMPONENT_PREFIX}${index}`, taken);
177
+ taken.add(name);
178
+ added[name] = rewriteSelfRefs(branch, nodeName, 1);
179
+ return { $ref: `#/${DEFS_KEY}/${name}` };
180
+ });
181
+ const description = resource["description"];
182
+ added[nodeName] = typeof description === "string" ? {
183
+ description,
184
+ oneOf: refs
185
+ } : { oneOf: refs };
186
+ } else {
187
+ const body = {};
188
+ for (const [key, value] of Object.entries(resource)) if (key !== DEFS_KEY) body[key] = value;
189
+ added[nodeName] = rewriteSelfRefs(body, nodeName, 1);
190
+ }
191
+ return {
192
+ ...setAt(schema, rootPath, { $ref: `#/${DEFS_KEY}/${nodeName}` }),
193
+ [DEFS_KEY]: {
194
+ ...existingDefs ?? {},
195
+ ...added
196
+ }
197
+ };
198
+ }
5
199
  function createSseFrameReader() {
6
200
  let buffer = "";
7
201
  let data = [];
@@ -113,7 +307,7 @@ const toOpenAiTools = (tools) => tools.map((tool) => ({
113
307
  function: {
114
308
  name: tool.name,
115
309
  ...tool.description ? { description: tool.description } : {},
116
- parameters: tool.inputSchema
310
+ parameters: toPortableToolSchema(tool.inputSchema)
117
311
  }
118
312
  }));
119
313
  const errorMessage$2 = (e) => e instanceof Error ? e.message : String(e);
@@ -264,11 +458,18 @@ var OpenAiCompatibleClient = class {
264
458
  const choice = data.choices?.[0]?.message;
265
459
  if (!choice) throw new Error("response has no choices[0].message");
266
460
  const toolCalls = choice["tool_calls"]?.map((tc) => {
267
- const rawArgs = tc.function?.arguments ?? "{}";
461
+ let args = {};
462
+ let parseError;
463
+ try {
464
+ args = JSON.parse(tc.function?.arguments ?? "{}");
465
+ } catch (e) {
466
+ parseError = e instanceof Error ? e.message : String(e);
467
+ }
268
468
  return {
269
469
  id: tc.id,
270
470
  name: tc.function?.name ?? "",
271
- arguments: JSON.parse(rawArgs)
471
+ arguments: args,
472
+ ...parseError ? { argumentsParseError: parseError } : {}
272
473
  };
273
474
  });
274
475
  const content = choice["content"];
@@ -363,7 +564,7 @@ function toAnthropicMessages(messages) {
363
564
  const toAnthropicTools = (tools) => tools.map((tool) => ({
364
565
  name: tool.name,
365
566
  ...tool.description ? { description: tool.description } : {},
366
- input_schema: tool.inputSchema
567
+ input_schema: toPortableToolSchema(tool.inputSchema)
367
568
  }));
368
569
  /** Anthropic Messages API 客户端(零依赖 fetch;Node/浏览器通用) */
369
570
  var AnthropicClient = class {
@@ -606,7 +807,7 @@ function toGenAiContents(messages) {
606
807
  const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => ({
607
808
  name: tool.name,
608
809
  ...tool.description ? { description: tool.description } : {},
609
- parameters: tool.inputSchema
810
+ parameters: toPortableToolSchema(tool.inputSchema)
610
811
  })) }];
611
812
  /** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
612
813
  var GoogleGenAiClient = class {
@@ -800,7 +1001,9 @@ Skills are disclosed progressively: the catalog below only shows metadata. To us
800
1001
  1. Call the "read_skill_file" tool to load the skill's SKILL.md instructions. Reading SKILL.md activates the skill's script tools, which are named "<skillName>__<scriptName>".
801
1002
  2. Call the activated script tools to do the work.
802
1003
  3. If a tool call fails, read the error, adjust your approach and retry, or continue without it.
803
- 4. When the task is complete, answer in plain text without calling any tool.`;
1004
+ 4. When the task is complete, answer in plain text without calling any tool.
1005
+
1006
+ Never claim to have created, saved, or installed a skill unless a skill tool call actually succeeded.`;
804
1007
  /** 渐进披露路由(默认):system prompt 只注入 Catalog 元数据 + 工具使用说明 */
805
1008
  var ProgressiveRouter = class {
806
1009
  async route(catalog) {
@@ -814,7 +1017,9 @@ var ProgressiveRouter = class {
814
1017
  const INSTRUCTIONS = `You are an agent that completes tasks by using skills.
815
1018
  The full instructions of every available skill are injected below. Follow them directly.
816
1019
  Script tools are named "<skillName>__<scriptName>".
817
- When the task is complete, answer in plain text without calling any tool.`;
1020
+ When the task is complete, answer in plain text without calling any tool.
1021
+
1022
+ Never claim to have created, saved, or installed a skill unless a skill tool call actually succeeded.`;
818
1023
  /** 全量披露路由(对照用):注入每个技能的完整 SKILL.md 正文 */
819
1024
  var FullDisclosureRouter = class {
820
1025
  #discovery;
@@ -841,7 +1046,7 @@ var FullDisclosureRouter = class {
841
1046
  * 3. `mcp#` 前缀 → TOOL_UNSUPPORTED(WebMCP 阶段实现)
842
1047
  * 4. 其余 → TOOL_NOT_FOUND
843
1048
  */
844
- function resolveToolName(name, activatedSkills) {
1049
+ function resolveToolName(name, activatedSkills, activatedTools = []) {
845
1050
  const sep = name.indexOf("__");
846
1051
  if (sep > 0) {
847
1052
  const skillName = name.slice(0, sep);
@@ -862,10 +1067,12 @@ function resolveToolName(name, activatedSkills) {
862
1067
  code: "TOOL_UNSUPPORTED",
863
1068
  message: `Tool "${name}" is a WebMCP tool, which is not supported yet`
864
1069
  };
1070
+ const availableTools = [...activatedTools].sort();
865
1071
  return {
866
1072
  kind: "not-found",
867
1073
  code: "TOOL_NOT_FOUND",
868
- message: `Unknown tool "${name}". Activate the owning skill first by reading its SKILL.md`
1074
+ message: `Unknown tool "${name}".${availableTools.length > 0 ? ` Available tools right now: ${availableTools.join(", ")}.` : " No skill tools are activated yet; read the SKILL.md of the owning skill first."}`,
1075
+ data: { availableTools }
869
1076
  };
870
1077
  }
871
1078
  /** ToolDefinition → LLM 工具 spec;缺 schema 时用宽松 object 兜底并在描述中标记 */
@@ -875,7 +1082,7 @@ function toLlmToolSpec(tool) {
875
1082
  ...tool.description ? { description: tool.description } : {},
876
1083
  inputSchema: tool.inputSchema
877
1084
  };
878
- const note = "Input schema unavailable; call with no arguments or a simple object.";
1085
+ const note = "Input schema unavailable for this script. Inspect the skill documentation before calling; do not assume it takes no arguments.";
879
1086
  return {
880
1087
  name: tool.name,
881
1088
  description: tool.description ? `${tool.description} (${note})` : note,
@@ -885,10 +1092,57 @@ function toLlmToolSpec(tool) {
885
1092
  }
886
1093
  };
887
1094
  }
1095
+ /** 工具名规则:技能**目录名**加双下划线加脚本名,与 frontmatter 的 name 无关 */
1096
+ function scriptToolName(skillName, scriptName) {
1097
+ return `${skillName}__${scriptName}`;
1098
+ }
1099
+ const SCHEMA_SOURCE_TEXT = {
1100
+ "module-export": "from module export",
1101
+ sidecar: "from the .schema.json sidecar",
1102
+ inferred: "inferred from source",
1103
+ unavailable: "unavailable",
1104
+ unknown: "not loaded yet"
1105
+ };
1106
+ /** 展示用的 schema 来源文案(console 与模型清单共用一套措辞) */
1107
+ function schemaSourceLabel(source) {
1108
+ return SCHEMA_SOURCE_TEXT[source];
1109
+ }
1110
+ /** allowed-tools 排除的脚本对模型不可见;清单里必须写明原因,否则作者只会看到工具凭空消失 */
1111
+ const ALLOWED_TOOLS_EXCLUSION_REASON = "excluded by the allowed-tools list in SKILL.md";
1112
+ /**
1113
+ * 从技能文件清单中挑出脚本并给出工具名映射。
1114
+ * @param files 技能根目录下的相对路径(如 `scripts/parse.ts`、`SKILL.md`)
1115
+ * @param allowedTools SKILL.md 声明的 allowed-tools;未声明传 undefined(全部暴露)
1116
+ */
1117
+ function listSkillScripts(skillName, files, allowedTools) {
1118
+ const sidecars = new Set(files.filter((path) => path.startsWith("scripts/") && path.endsWith(".schema.json")));
1119
+ const out = [];
1120
+ for (const path of [...files].sort()) {
1121
+ const scriptName = /^scripts\/([^/]+)\.(?:ts|js)$/.exec(path)?.[1];
1122
+ if (scriptName === void 0 || scriptName === "") continue;
1123
+ const excluded = allowedTools !== void 0 && !allowedTools.includes(scriptName);
1124
+ out.push({
1125
+ path,
1126
+ scriptName,
1127
+ ...excluded ? { unexposedReason: ALLOWED_TOOLS_EXCLUSION_REASON } : { toolName: scriptToolName(skillName, scriptName) },
1128
+ schemaSource: excluded ? "unavailable" : sidecars.has(`scripts/${scriptName}.schema.json`) ? "sidecar" : "unknown"
1129
+ });
1130
+ }
1131
+ return out;
1132
+ }
1133
+ /**
1134
+ * 清单 → 喂给模型的文本(FR-18.1)。
1135
+ * 「authoritative」这句是给模型的信号:以目录为准,别照 SKILL.md 的描述猜文件名。
1136
+ */
1137
+ function formatSkillScriptManifest(descriptors) {
1138
+ if (descriptors.length === 0) return "";
1139
+ return `\n\nAvailable scripts in this skill (from directory listing, authoritative):\n${descriptors.map((item) => item.toolName === void 0 ? `- ${item.path} → not exposed as a tool (${item.unexposedReason ?? "no loadable definition"})` : `- ${item.path} → tool "${item.toolName}" (input schema: ${SCHEMA_SOURCE_TEXT[item.schemaSource]})`).join("\n")}`;
1140
+ }
888
1141
  const MCP_CONTENT_RE = /^(text|json|image|resource|audio)$/;
889
1142
  /**
890
1143
  * 脚本返回值归一到 MCP content 格式:数组沿用(逐项归一),
891
1144
  * 字符串包装为 text,其他值序列化包装。Node/浏览器执行器共享。
1145
+ * 产出只有文本型分片:脚本侧没有下发图像的通道(图像只来自页面取像)。
892
1146
  */
893
1147
  function normalizeToolContent(value) {
894
1148
  if (Array.isArray(value)) return value.map((item) => {
@@ -946,16 +1200,31 @@ const READ_SKILL_FILE_TOOL = {
946
1200
  const ASK_USER_TOOL_NAME = "ask_user";
947
1201
  const ASK_USER_INPUT_SCHEMA = {
948
1202
  type: "object",
949
- properties: { question: {
950
- type: "string",
951
- description: "The question to ask the user"
952
- } },
1203
+ properties: {
1204
+ question: {
1205
+ type: "string",
1206
+ description: "The question to ask the user"
1207
+ },
1208
+ choices: {
1209
+ type: "array",
1210
+ items: { type: "string" },
1211
+ description: "Closed set of acceptable answers. Provide it whenever the answer must be one of a known finite set, for example when asking which installed skill to use. The user then picks from a list instead of typing free text."
1212
+ },
1213
+ suggestion: {
1214
+ type: "string",
1215
+ description: "A value you believe the user is likely to answer, based only on the profile in the system prompt. It is shown as a suggestion the user may accept; it is never filled in for them. Omit it when nothing in the profile supports a value."
1216
+ },
1217
+ suggestionReason: {
1218
+ type: "string",
1219
+ description: "Short reason for the suggestion, shown next to it so the user can judge whether to accept it."
1220
+ }
1221
+ },
953
1222
  required: ["question"]
954
1223
  };
955
1224
  /** 内建工具:LLM 信息不足时主动向用户提问;仅当配置了 UiBridge 时注册 */
956
1225
  const ASK_USER_TOOL = {
957
1226
  name: ASK_USER_TOOL_NAME,
958
- description: "Ask the user a question when information is missing and continue with their answer.",
1227
+ description: "Ask the user a question when information is missing and continue with their answer. When the answer belongs to a known finite set, you must pass \"choices\".",
959
1228
  inputSchema: ASK_USER_INPUT_SCHEMA,
960
1229
  source: "builtin"
961
1230
  };
@@ -991,22 +1260,22 @@ function createScriptContext(deps) {
991
1260
  ...onWarning ? { onWarning } : {}
992
1261
  };
993
1262
  }
994
- const isRecord$2 = (v) => typeof v === "object" && v !== null;
1263
+ const isRecord$3 = (v) => typeof v === "object" && v !== null;
995
1264
  /**
996
1265
  * $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
997
1266
  * 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
998
1267
  */
999
1268
  function extractChartSpec(data) {
1000
- if (!isRecord$2(data)) return void 0;
1269
+ if (!isRecord$3(data)) return void 0;
1001
1270
  const raw = data["$chart"];
1002
- if (!isRecord$2(raw)) return void 0;
1271
+ if (!isRecord$3(raw)) return void 0;
1003
1272
  const { kind, labels, series } = raw;
1004
1273
  if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
1005
1274
  if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
1006
1275
  if (!Array.isArray(series)) return void 0;
1007
1276
  const validSeries = [];
1008
1277
  for (const item of series) {
1009
- if (!isRecord$2(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
1278
+ if (!isRecord$3(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
1010
1279
  const name = item["name"];
1011
1280
  validSeries.push({
1012
1281
  ...typeof name === "string" ? { name } : {},
@@ -1057,7 +1326,7 @@ const actionIntents = /* @__PURE__ */ new Set([
1057
1326
  "download",
1058
1327
  "refresh"
1059
1328
  ]);
1060
- function isRecord$1(value) {
1329
+ function isRecord$2(value) {
1061
1330
  return typeof value === "object" && value !== null && !Array.isArray(value);
1062
1331
  }
1063
1332
  function reject(message) {
@@ -1074,7 +1343,7 @@ function isJsonValue(value, depth = 0) {
1074
1343
  if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1075
1344
  if (typeof value === "number") return Number.isFinite(value);
1076
1345
  if (Array.isArray(value)) return value.every((item) => isJsonValue(item, depth + 1));
1077
- if (!isRecord$1(value)) return false;
1346
+ if (!isRecord$2(value)) return false;
1078
1347
  return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
1079
1348
  }
1080
1349
  /**
@@ -1084,10 +1353,10 @@ function isJsonValue(value, depth = 0) {
1084
1353
  function assertNode(value, path, depth, counter) {
1085
1354
  if (depth > MAX_NODE_DEPTH) reject(`A UI spec tree must not nest deeper than ${MAX_NODE_DEPTH} levels`);
1086
1355
  if (++counter.nodes > MAX_NODES) reject(`A UI spec tree must contain at most ${MAX_NODES} nodes`);
1087
- if (!isRecord$1(value)) reject(`${path} must be an object`);
1356
+ if (!isRecord$2(value)) reject(`${path} must be an object`);
1088
1357
  requireString(value["component"], `${path}.component`);
1089
1358
  if (value["id"] !== void 0) requireString(value["id"], `${path}.id`);
1090
- if (value["props"] !== void 0 && (!isRecord$1(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
1359
+ if (value["props"] !== void 0 && (!isRecord$2(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
1091
1360
  const children = value["children"];
1092
1361
  if (children === void 0) return;
1093
1362
  if (!Array.isArray(children)) reject(`${path}.children must be an array`);
@@ -1097,7 +1366,7 @@ function assertActions(value) {
1097
1366
  if (value === void 0) return;
1098
1367
  if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
1099
1368
  for (const action of value) {
1100
- if (!isRecord$1(action)) reject("A surface action must be an object");
1369
+ if (!isRecord$2(action)) reject("A surface action must be an object");
1101
1370
  requireString(action["id"], "Surface action ID");
1102
1371
  const intent = action["intent"];
1103
1372
  if (typeof intent !== "string" || !actionIntents.has(intent)) reject("Surface action intent is invalid");
@@ -1113,7 +1382,7 @@ function validateUiSpecNode(value) {
1113
1382
  return structuredClone(value);
1114
1383
  }
1115
1384
  function assertPatch(value) {
1116
- if (!isRecord$1(value)) reject("A surface patch operation must be an object");
1385
+ if (!isRecord$2(value)) reject("A surface patch operation must be an object");
1117
1386
  if (value["op"] !== "replace" && value["op"] !== "merge" && value["op"] !== "append") reject("Surface patch operation is invalid");
1118
1387
  requireString(value["path"], "Surface patch path");
1119
1388
  if (!value["path"].startsWith("/")) reject("Surface patch path must be a JSON pointer");
@@ -1121,7 +1390,7 @@ function assertPatch(value) {
1121
1390
  }
1122
1391
  /** Validates an individual event in the framework-neutral surface stream. @experimental */
1123
1392
  function validateUiSpecEvent(value) {
1124
- if (!isRecord$1(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
1393
+ if (!isRecord$2(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
1125
1394
  if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
1126
1395
  switch (value["type"]) {
1127
1396
  case "open":
@@ -1179,7 +1448,7 @@ function validateUiSpecEvent(value) {
1179
1448
  }
1180
1449
  /** Extracts validated surface stream events from structured tool output. @experimental */
1181
1450
  function extractUiSpecEvents(data) {
1182
- if (!isRecord$1(data) || data["$surface"] === void 0) return [];
1451
+ if (!isRecord$2(data) || data["$surface"] === void 0) return [];
1183
1452
  const raw = data["$surface"];
1184
1453
  return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
1185
1454
  }
@@ -1212,7 +1481,7 @@ function schemaToForm(schema, providedArgs) {
1212
1481
  function mapFieldType(prop) {
1213
1482
  if (Array.isArray(prop.enum)) return "select";
1214
1483
  switch (prop.type) {
1215
- case "string": return "text";
1484
+ case "string": return prop["format"] === "binary" || prop["contentEncoding"] === "base64" ? "file" : "text";
1216
1485
  case "number":
1217
1486
  case "integer": return "number";
1218
1487
  case "boolean": return "boolean";
@@ -1302,6 +1571,176 @@ var SerializingMemoryStore = class {
1302
1571
  return this.#serialize(scope, () => fn(this.#inner));
1303
1572
  }
1304
1573
  };
1574
+ /** @experimental */
1575
+ const DEFAULT_USER_PROFILE_LIMITS = {
1576
+ recordLimit: 500,
1577
+ valueMaxChars: 2e3,
1578
+ sampleLimit: 80,
1579
+ injectMaxBytes: 2e3,
1580
+ entryLimit: 40
1581
+ };
1582
+ /** @experimental */
1583
+ const EMPTY_USER_PROFILE = {
1584
+ version: 1,
1585
+ entries: [],
1586
+ refinedAt: 0
1587
+ };
1588
+ /** 注入段的标题。宿主/测试要判断「有没有注入画像」时对它取子串,不要自己再拼一份 @experimental */
1589
+ const USER_PROFILE_PROMPT_HEADER = "## About this user (inferred from past behavior; may be outdated)";
1590
+ /**
1591
+ * 无画像时也要下发的约束(AC-19.11):没有依据就把字段留空,
1592
+ * 不写这一句时模型会把「建议值」当成必填项去编。
1593
+ * @experimental
1594
+ */
1595
+ const USER_PROFILE_NO_INVENTION_RULE = "Only offer a suggested value for a question when the profile above actually supports it. With no relevant profile entry, omit the suggestion and leave the field empty; never invent one.";
1596
+ const byPriority = (a, b) => a.confidence === b.confidence ? b.updatedAt - a.updatedAt : a.confidence === "high" ? -1 : 1;
1597
+ const byteLength = (text) => new TextEncoder().encode(text).length;
1598
+ /**
1599
+ * 画像 → 系统提示片段,按 confidence + updatedAt 优先级排序后逐条累加,
1600
+ * 超出字节上限的部分丢弃并标注(FR-19.4 / AC-19.9)。
1601
+ * 空画像返回空串:只有标题的空段落白占 token,还会诱导模型编造。
1602
+ * @experimental
1603
+ */
1604
+ function renderUserProfileContext(profile = EMPTY_USER_PROFILE, maxBytes = DEFAULT_USER_PROFILE_LIMITS.injectMaxBytes) {
1605
+ const entries = [...profile.entries].sort(byPriority);
1606
+ if (entries.length === 0 || maxBytes <= 0) return "";
1607
+ const lines = entries.map((entry) => `- ${entry.text}`);
1608
+ const render = (keptCount) => {
1609
+ const omitted = lines.length - keptCount;
1610
+ const body = [USER_PROFILE_PROMPT_HEADER, ...lines.slice(0, keptCount)];
1611
+ if (omitted > 0) body.push(`(truncated: ${omitted} more entries omitted)`);
1612
+ return body.join("\n");
1613
+ };
1614
+ let kept = lines.length;
1615
+ while (kept > 0 && byteLength(render(kept)) > maxBytes) kept -= 1;
1616
+ return kept === 0 ? "" : render(kept);
1617
+ }
1618
+ /** `user:{id}` scope 下的行为记录 key @experimental */
1619
+ const BEHAVIOR_RECORDS_KEY = "behaviorRecords";
1620
+ /** `user:{id}` scope 下的画像 key @experimental */
1621
+ const USER_PROFILE_KEY = "userProfile";
1622
+ const KINDS = [
1623
+ "question",
1624
+ "choice",
1625
+ "text-input",
1626
+ "stated-preference"
1627
+ ];
1628
+ const isKind = (value) => KINDS.includes(value);
1629
+ const stringList = (raw) => Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : void 0;
1630
+ /** 存储里的形状不受控(宿主可能手改文件),逐条筛而不是整体信任 @experimental */
1631
+ function readBehaviorRecords(raw) {
1632
+ if (!Array.isArray(raw)) return [];
1633
+ const out = [];
1634
+ for (const item of raw) {
1635
+ if (typeof item !== "object" || item === null) continue;
1636
+ const record = item;
1637
+ const scene = record.scene;
1638
+ if (typeof record.id !== "string" || !isKind(record.kind)) continue;
1639
+ if (typeof record.at !== "number" || typeof record.value !== "string") continue;
1640
+ if (typeof scene !== "object" || scene === null || typeof scene.sessionId !== "string") continue;
1641
+ const options = stringList(scene.options);
1642
+ out.push({
1643
+ id: record.id,
1644
+ kind: record.kind,
1645
+ at: record.at,
1646
+ value: record.value,
1647
+ ...record.confirmed === true ? { confirmed: true } : {},
1648
+ scene: {
1649
+ sessionId: scene.sessionId,
1650
+ ...typeof scene.skillName === "string" ? { skillName: scene.skillName } : {},
1651
+ ...typeof scene.interactionId === "string" ? { interactionId: scene.interactionId } : {},
1652
+ ...typeof scene.fieldName === "string" ? { fieldName: scene.fieldName } : {},
1653
+ ...typeof scene.fieldSemantics === "string" ? { fieldSemantics: scene.fieldSemantics } : {},
1654
+ ...options && options.length > 0 ? { options } : {}
1655
+ }
1656
+ });
1657
+ }
1658
+ return out;
1659
+ }
1660
+ /**
1661
+ * 追加并按容量淘汰最旧(FR-19.2)。`value` 同时截断:
1662
+ * 一条被粘贴进来的长文本能把整个 JSON 撑爆,而提炼只需要语义。
1663
+ * @experimental
1664
+ */
1665
+ function appendBehaviorRecords(current, incoming, limits = DEFAULT_USER_PROFILE_LIMITS) {
1666
+ if (limits.recordLimit <= 0) return [];
1667
+ const truncated = incoming.map((record) => record.value.length > limits.valueMaxChars ? {
1668
+ ...record,
1669
+ value: record.value.slice(0, limits.valueMaxChars)
1670
+ } : record);
1671
+ const merged = [...current, ...truncated];
1672
+ return merged.slice(Math.max(0, merged.length - limits.recordLimit));
1673
+ }
1674
+ /** @experimental */
1675
+ function readUserProfile(raw) {
1676
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return EMPTY_USER_PROFILE;
1677
+ const value = raw;
1678
+ return {
1679
+ version: 1,
1680
+ entries: readProfileEntries(value.entries),
1681
+ refinedAt: typeof value.refinedAt === "number" ? value.refinedAt : 0
1682
+ };
1683
+ }
1684
+ /** @experimental */
1685
+ function readProfileEntries(raw) {
1686
+ if (!Array.isArray(raw)) return [];
1687
+ const out = [];
1688
+ for (const item of raw) {
1689
+ if (typeof item !== "object" || item === null) continue;
1690
+ const entry = item;
1691
+ if (typeof entry.id !== "string" || entry.id === "") continue;
1692
+ if (typeof entry.text !== "string" || entry.text.trim() === "") continue;
1693
+ out.push({
1694
+ id: entry.id,
1695
+ text: entry.text,
1696
+ confidence: entry.confidence === "high" ? "high" : "low",
1697
+ updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : 0,
1698
+ sourceKinds: (stringList(entry.sourceKinds) ?? []).filter(isKind)
1699
+ });
1700
+ }
1701
+ return out;
1702
+ }
1703
+ /**
1704
+ * 合并提炼结果:同 id 更新,新 id 追加,超上限按 `updatedAt` 淘汰最旧(设计 09 §1.4)。
1705
+ * @experimental
1706
+ */
1707
+ function mergeProfileEntries(current, incoming, options) {
1708
+ const limits = options.limits ?? DEFAULT_USER_PROFILE_LIMITS;
1709
+ const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
1710
+ for (const entry of incoming) byId.set(entry.id, entry);
1711
+ return {
1712
+ version: 1,
1713
+ entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, Math.max(0, limits.entryLimit)),
1714
+ refinedAt: options.now
1715
+ };
1716
+ }
1717
+ /**
1718
+ * 提炼采样(FR-19.3):最近的 + 高频的 + 被用户显式确认过的,去重后截到上限。
1719
+ * 三个维度缺一不可——只取最近会让长期偏好被一次突发淹没,只取高频则学不到新变化。
1720
+ * @experimental
1721
+ */
1722
+ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
1723
+ const limit = Math.max(0, limits.sampleLimit);
1724
+ if (limit === 0 || records.length === 0) return [];
1725
+ if (records.length <= limit) return [...records].sort((a, b) => a.at - b.at);
1726
+ const picked = /* @__PURE__ */ new Map();
1727
+ const take = (record) => {
1728
+ if (picked.size < limit) picked.set(record.id, record);
1729
+ };
1730
+ for (const record of records) if (record.confirmed === true) take(record);
1731
+ const frequency = /* @__PURE__ */ new Map();
1732
+ const bucketOf = (record) => `${record.scene.skillName ?? ""}#${record.scene.fieldName ?? record.kind}`;
1733
+ for (const record of records) frequency.set(bucketOf(record), (frequency.get(bucketOf(record)) ?? 0) + 1);
1734
+ const byFrequency = [...records].sort((a, b) => (frequency.get(bucketOf(b)) ?? 0) - (frequency.get(bucketOf(a)) ?? 0));
1735
+ for (const record of byFrequency.slice(0, Math.ceil(limit / 2))) take(record);
1736
+ for (const record of [...records].reverse()) take(record);
1737
+ return [...picked.values()].sort((a, b) => a.at - b.at);
1738
+ }
1739
+ /**
1740
+ * 无工具模型的系统提示(0.6.0 FR-14.3)。
1741
+ * 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
1742
+ */
1743
+ const PLAIN_CHAT_SYSTEM_PROMPT = "You are a helpful assistant. Answer the user's question directly and concisely. You have no tools available; do not describe or simulate tool calls.";
1305
1744
  /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
1306
1745
  var TraceRecorder = class {
1307
1746
  #runId;
@@ -1328,6 +1767,36 @@ var TraceRecorder = class {
1328
1767
  return [...this.#events];
1329
1768
  }
1330
1769
  };
1770
+ const isRecord$1 = (v) => typeof v === "object" && v !== null;
1771
+ /** 工具结果可经 `$todo` 标记记入 trace 的事件类型;其余类型不接受,防止工具源伪造 run.* */
1772
+ const TODO_TRACE_TYPES = /* @__PURE__ */ new Set([
1773
+ "todo.created",
1774
+ "todo.updated",
1775
+ "todo.cleared"
1776
+ ]);
1777
+ /**
1778
+ * `$todo` 约定的形状校验:JSON content 的 data 含 `$todo` 键(单条或数组)→ trace 事件。
1779
+ *
1780
+ * 与 `$chart` / `$surface` 同一条既有通道——待办清单的状态机全部在 `@webskill/agent`,
1781
+ * runtime 只认这三个事件类型名,不含任何计划态逻辑。畸形条目忽略不炸。
1782
+ * @experimental
1783
+ */
1784
+ function extractTodoTraceEvents(data) {
1785
+ if (!isRecord$1(data) || data["$todo"] === void 0) return [];
1786
+ const raw = data["$todo"];
1787
+ const entries = Array.isArray(raw) ? raw : [raw];
1788
+ const events = [];
1789
+ for (const entry of entries) {
1790
+ if (!isRecord$1(entry)) continue;
1791
+ const { type, ...rest } = entry;
1792
+ if (typeof type !== "string" || !TODO_TRACE_TYPES.has(type)) continue;
1793
+ events.push({
1794
+ type,
1795
+ data: rest
1796
+ });
1797
+ }
1798
+ return events;
1799
+ }
1331
1800
  const RUN_SNAPSHOT_SCHEMA_VERSION = 2;
1332
1801
  /** @experimental */
1333
1802
  function isUnsupportedRunSnapshot(entry) {
@@ -1481,13 +1950,13 @@ var RunTerminated = class extends Error {
1481
1950
  };
1482
1951
  /** bridge.request 自身异常(非超时):恢复 running 后转为工具错误回喂 */
1483
1952
  var BridgeRequestError = class extends Error {};
1484
- const baseName = (p) => p.split("/").pop() ?? p;
1485
- const toolError = (code, message) => ({
1953
+ const toolError = (code, message, data) => ({
1486
1954
  ok: false,
1487
1955
  content: [],
1488
1956
  error: {
1489
1957
  code,
1490
- message
1958
+ message,
1959
+ ...data !== void 0 ? { data } : {}
1491
1960
  }
1492
1961
  });
1493
1962
  /** 工具参数摘要(JSON 截断 100 字符;trace 与实时事件同一口径,ToolCallCard 展开详情用) */
@@ -1496,6 +1965,16 @@ const summarizeArgs = (args) => {
1496
1965
  return json.length > 100 ? `${json.slice(0, 100)}…` : json;
1497
1966
  };
1498
1967
  /**
1968
+ * file-pick 的回填值带整个文件的 base64,落进 `paramHistory` 会把用户选的文件
1969
+ * 原样长期留在记忆里(体积与隐私都不可接受)。只留可辨识的描述,正文不留。
1970
+ */
1971
+ const redactFileValue = (value) => {
1972
+ if (Array.isArray(value)) return value.map(redactFileValue);
1973
+ if (typeof value !== "object" || value === null) return typeof value === "string" ? "[file]" : value;
1974
+ const { data: _data, ...rest } = value;
1975
+ return rest;
1976
+ };
1977
+ /**
1499
1978
  * 多轮 Agent 循环。
1500
1979
  * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
1501
1980
  * 缺参/确认/提问经 UiBridge 行内 await 暂停恢复;取消/超时/LLM 异常/护栏超限才终止 run。
@@ -1519,6 +1998,8 @@ var AgentLoop = class {
1519
1998
  toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
1520
1999
  toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
1521
2000
  paramHistoryLimit: config.paramHistoryLimit ?? 50,
2001
+ toolCallingDisabled: config.toolCallingDisabled ?? false,
2002
+ maxUnknownToolRetries: config.maxUnknownToolRetries ?? 2,
1522
2003
  temperature: config.temperature,
1523
2004
  renderResult: config.renderResult
1524
2005
  };
@@ -1531,7 +2012,7 @@ var AgentLoop = class {
1531
2012
  /**
1532
2013
  * 取消进行中的 run:触发该 run 的 AbortController(与 totalTimeout 硬期限同一通道),
1533
2014
  * run 以 cancelled(RUN_CANCELLED)终止;未找到(已终态或不属于本实例)返回 false。
1534
- * 取消在下一个中断点生效(LLM complete/stream 调用;交互等待不强制中断)。
2015
+ * 取消在下一个中断点生效;LLM 调用与交互等待都监听同一个 signal,因此等表单时也能立即停。
1535
2016
  */
1536
2017
  cancel(runId) {
1537
2018
  const controller = this.#controllers.get(runId);
@@ -1553,7 +2034,7 @@ var AgentLoop = class {
1553
2034
  sessionId: input.sessionId,
1554
2035
  status: "running",
1555
2036
  phase: "route",
1556
- userPrompt: input.userPrompt,
2037
+ userPrompt: promptText(input.userPrompt),
1557
2038
  startedAt,
1558
2039
  activeSkillNames: [],
1559
2040
  artifacts: [],
@@ -1578,6 +2059,7 @@ var AgentLoop = class {
1578
2059
  surfacePatchCount: 0,
1579
2060
  processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
1580
2061
  emittedToolEvents: /* @__PURE__ */ new Set(),
2062
+ unknownToolCalls: 0,
1581
2063
  startMs,
1582
2064
  pausedMs: 0,
1583
2065
  maxTurns: this.#config.maxTurns,
@@ -1601,7 +2083,7 @@ var AgentLoop = class {
1601
2083
  candidates: route.catalog.entries.map((e) => e.name)
1602
2084
  }
1603
2085
  }, state);
1604
- const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
2086
+ const externalSpecs = this.#config.toolCallingDisabled ? [] : (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
1605
2087
  try {
1606
2088
  return await source.listToolSpecs();
1607
2089
  } catch (e) {
@@ -1610,7 +2092,7 @@ var AgentLoop = class {
1610
2092
  }
1611
2093
  }))).flat();
1612
2094
  const externalSystemPrompts = [];
1613
- for (const source of this.#deps.externalTools ?? []) {
2095
+ if (!this.#config.toolCallingDisabled) for (const source of this.#deps.externalTools ?? []) {
1614
2096
  if (source.systemPrompt === void 0) continue;
1615
2097
  try {
1616
2098
  const text = (await source.systemPrompt())?.trim();
@@ -1619,17 +2101,21 @@ var AgentLoop = class {
1619
2101
  trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
1620
2102
  }
1621
2103
  }
2104
+ const systemPrompt = this.#config.toolCallingDisabled ? PLAIN_CHAT_SYSTEM_PROMPT : [route.systemPrompt, ...externalSystemPrompts].join("\n\n");
2105
+ const profileMessage = await this.#userProfileMessage(state);
1622
2106
  state.messages = [
1623
2107
  {
1624
2108
  role: "system",
1625
- content: textParts([route.systemPrompt, ...externalSystemPrompts].join("\n\n"))
2109
+ content: textParts(systemPrompt)
1626
2110
  },
2111
+ ...profileMessage ? [profileMessage] : [],
1627
2112
  ...(input.history ?? []).map((m) => ({ ...m })),
1628
2113
  {
1629
2114
  role: "user",
1630
- content: textParts(input.userPrompt)
2115
+ content: typeof input.userPrompt === "string" ? textParts(input.userPrompt) : [...input.userPrompt]
1631
2116
  }
1632
2117
  ];
2118
+ await this.#recordUserPrompt(state);
1633
2119
  try {
1634
2120
  return await this.#turnLoop(state, 1, externalSpecs);
1635
2121
  } catch (e) {
@@ -1690,7 +2176,7 @@ var AgentLoop = class {
1690
2176
  if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1691
2177
  if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1692
2178
  const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
1693
- const toolSpecs = [
2179
+ const toolSpecs = this.#config.toolCallingDisabled ? [] : [
1694
2180
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
1695
2181
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
1696
2182
  ...skillToolSpecs
@@ -1759,9 +2245,14 @@ var AgentLoop = class {
1759
2245
  messages.push({
1760
2246
  role: "tool",
1761
2247
  toolCallId: call.id,
1762
- content: textParts(await this.#serializeToolResult(call, result, state))
2248
+ content: await this.#toolResultParts(call, result, state)
1763
2249
  });
1764
- await this.#drainSurfaceAction(state);
2250
+ try {
2251
+ await this.#drainSurfaceAction(state);
2252
+ } catch (e) {
2253
+ if (e instanceof RunTerminated) return finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
2254
+ throw e;
2255
+ }
1765
2256
  }
1766
2257
  }
1767
2258
  } finally {
@@ -1898,6 +2389,7 @@ var AgentLoop = class {
1898
2389
  surfacePatchCount: 0,
1899
2390
  processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
1900
2391
  emittedToolEvents: /* @__PURE__ */ new Set(),
2392
+ unknownToolCalls: 0,
1901
2393
  startMs,
1902
2394
  pausedMs: snapshot.pausedMs ?? 0,
1903
2395
  maxTurns: snapshot.config.maxTurns,
@@ -1934,7 +2426,7 @@ var AgentLoop = class {
1934
2426
  state.messages.push({
1935
2427
  role: "tool",
1936
2428
  toolCallId: pendingCall.id,
1937
- content: textParts(await this.#serializeToolResult(pendingCall, result, state))
2429
+ content: await this.#toolResultParts(pendingCall, result, state)
1938
2430
  });
1939
2431
  await this.#drainSurfaceAction(state);
1940
2432
  } else if (pending?.type === "ask" && pendingCall) {
@@ -1956,7 +2448,7 @@ var AgentLoop = class {
1956
2448
  state.messages.push({
1957
2449
  role: "tool",
1958
2450
  toolCallId: pendingCall.id,
1959
- content: textParts(await this.#serializeToolResult(pendingCall, result, state))
2451
+ content: await this.#toolResultParts(pendingCall, result, state)
1960
2452
  });
1961
2453
  await this.#drainSurfaceAction(state);
1962
2454
  } else if (pendingCall) {
@@ -1964,7 +2456,7 @@ var AgentLoop = class {
1964
2456
  state.messages.push({
1965
2457
  role: "tool",
1966
2458
  toolCallId: pendingCall.id,
1967
- content: textParts(await this.#serializeToolResult(pendingCall, result, state))
2459
+ content: await this.#toolResultParts(pendingCall, result, state)
1968
2460
  });
1969
2461
  await this.#drainSurfaceAction(state);
1970
2462
  }
@@ -1975,7 +2467,7 @@ var AgentLoop = class {
1975
2467
  state.messages.push({
1976
2468
  role: "tool",
1977
2469
  toolCallId: next.id,
1978
- content: textParts(await this.#serializeToolResult(next, result, state))
2470
+ content: await this.#toolResultParts(next, result, state)
1979
2471
  });
1980
2472
  await this.#drainSurfaceAction(state);
1981
2473
  }
@@ -2090,7 +2582,7 @@ var AgentLoop = class {
2090
2582
  } });
2091
2583
  let response;
2092
2584
  try {
2093
- response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs, () => bridge.cancel?.(request.id));
2585
+ response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs, state, () => bridge.cancel?.(request.id));
2094
2586
  } catch (e) {
2095
2587
  run.status = "running";
2096
2588
  run.interruptExpiresAt = void 0;
@@ -2100,6 +2592,7 @@ var AgentLoop = class {
2100
2592
  message: e.message,
2101
2593
  code: "RUN_INTERACTION_TIMEOUT"
2102
2594
  });
2595
+ if (e instanceof WebSkillError && e.code === "RUN_CANCELLED") throw this.#abortedInteraction(state);
2103
2596
  state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf(e)}` });
2104
2597
  throw new BridgeRequestError(messageOf(e));
2105
2598
  }
@@ -2125,19 +2618,62 @@ var AgentLoop = class {
2125
2618
  await this.#appendParamHistory(state, request, response.value);
2126
2619
  return response.value;
2127
2620
  }
2128
- #withInteractionTimeout(promise, timeoutMs, onTimeout) {
2621
+ /**
2622
+ * 交互等待被 abort:cancel() 与 totalTimeout 硬期限共用同一个 controller,
2623
+ * 按 `#cancelled` 区分二者,与 LLM 调用被 abort 时的归类保持一致。
2624
+ */
2625
+ #abortedInteraction(state) {
2626
+ if (this.#cancelled.has(state.runId)) return new RunTerminated({
2627
+ status: "cancelled",
2628
+ reason: "user-cancelled",
2629
+ message: "Run cancelled by user",
2630
+ code: "RUN_CANCELLED"
2631
+ });
2632
+ return new RunTerminated({
2633
+ status: "failed",
2634
+ reason: "timeout",
2635
+ message: `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`,
2636
+ code: "RUN_TIMEOUT"
2637
+ });
2638
+ }
2639
+ #withInteractionTimeout(promise, timeoutMs, state, onAbandon) {
2129
2640
  return new Promise((resolve, reject) => {
2130
- const timer = setTimeout(() => {
2641
+ const signal = state.controller.signal;
2642
+ let settled = false;
2643
+ const abandon = () => {
2131
2644
  try {
2132
- onTimeout?.();
2645
+ onAbandon?.();
2133
2646
  } catch {}
2647
+ };
2648
+ const cleanup = () => {
2649
+ settled = true;
2650
+ clearTimeout(timer);
2651
+ signal.removeEventListener("abort", onAbort);
2652
+ };
2653
+ const onAbort = () => {
2654
+ if (settled) return;
2655
+ cleanup();
2656
+ abandon();
2657
+ reject(new WebSkillError("RUN_CANCELLED", "Interaction aborted because the run was stopped"));
2658
+ };
2659
+ const timer = setTimeout(() => {
2660
+ if (settled) return;
2661
+ cleanup();
2662
+ abandon();
2134
2663
  reject(new WebSkillError("RUN_INTERACTION_TIMEOUT", `Interaction timed out after ${timeoutMs}ms`));
2135
2664
  }, timeoutMs);
2665
+ if (signal.aborted) {
2666
+ onAbort();
2667
+ return;
2668
+ }
2669
+ signal.addEventListener("abort", onAbort);
2136
2670
  promise.then((v) => {
2137
- clearTimeout(timer);
2671
+ if (settled) return;
2672
+ cleanup();
2138
2673
  resolve(v);
2139
2674
  }, (e) => {
2140
- clearTimeout(timer);
2675
+ if (settled) return;
2676
+ cleanup();
2141
2677
  reject(e instanceof Error ? e : new Error(String(e)));
2142
2678
  });
2143
2679
  });
@@ -2157,11 +2693,15 @@ var AgentLoop = class {
2157
2693
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
2158
2694
  else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
2159
2695
  else {
2160
- const resolution = resolveToolName(call.name, state.activated);
2696
+ const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
2161
2697
  if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
2162
2698
  else {
2163
2699
  const source = (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
2164
- result = source ? await source.call(call.name, call.arguments) : toolError(resolution.code, resolution.message);
2700
+ if (source) result = await source.call(call.name, call.arguments);
2701
+ else {
2702
+ if (resolution.kind === "not-found") state.unknownToolCalls += 1;
2703
+ result = toolError(resolution.code, resolution.message, resolution.kind === "not-found" ? resolution.data : void 0);
2704
+ }
2165
2705
  }
2166
2706
  }
2167
2707
  const durationMs = Date.parse(state.now()) - callStartMs;
@@ -2180,6 +2720,7 @@ var AgentLoop = class {
2180
2720
  type: "chart",
2181
2721
  chart
2182
2722
  });
2723
+ for (const todo of extractTodoTraceEvents(item.data)) state.trace.record(todo.type, { data: todo.data });
2183
2724
  try {
2184
2725
  for (const event of extractUiSpecEvents(item.data)) await this.#renderSurface(state, event);
2185
2726
  } catch (e) {
@@ -2200,12 +2741,18 @@ var AgentLoop = class {
2200
2741
  durationMs
2201
2742
  }
2202
2743
  });
2203
- this.#emitTool(state, "failed", call);
2744
+ this.#emitTool(state, "failed", call, result.error?.code);
2204
2745
  }
2205
2746
  for (const artifact of result.artifacts ?? []) state.trace.record("artifact.created", { data: {
2206
2747
  artifactId: artifact.id,
2207
2748
  path: artifact.path
2208
2749
  } });
2750
+ if (state.unknownToolCalls > this.#config.maxUnknownToolRetries) throw new RunTerminated({
2751
+ status: "failed",
2752
+ reason: "tool-resolution-exhausted",
2753
+ message: "Repeated calls to non-existent tools. Stopping to avoid consuming the remaining turn budget.",
2754
+ code: "TOOL_RESOLUTION_EXHAUSTED"
2755
+ });
2209
2756
  return result;
2210
2757
  }
2211
2758
  /** Attaches trusted run provenance, then records only events accepted by the configured bridge. */
@@ -2319,7 +2866,7 @@ var AgentLoop = class {
2319
2866
  nonce: request.nonce,
2320
2867
  ...resumed ? { resumed: true } : {}
2321
2868
  } });
2322
- const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
2869
+ const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, state, () => bridge.cancelSurfaceAction?.(request.nonce));
2323
2870
  run.status = "running";
2324
2871
  run.interruptExpiresAt = void 0;
2325
2872
  state.trace.record("ui.surface-action.resolved", { data: {
@@ -2346,6 +2893,7 @@ var AgentLoop = class {
2346
2893
  message: e.message,
2347
2894
  code: "RUN_INTERACTION_TIMEOUT"
2348
2895
  });
2896
+ if (e instanceof WebSkillError && e.code === "RUN_CANCELLED") throw this.#abortedInteraction(state);
2349
2897
  if (e instanceof RunTerminated || e instanceof BridgeRequestError) throw e;
2350
2898
  throw new BridgeRequestError(messageOf(e));
2351
2899
  } finally {
@@ -2375,7 +2923,7 @@ var AgentLoop = class {
2375
2923
  * 集合挂在 LoopState 上、**不写进快照**:写进快照会让跨进程恢复的
2376
2924
  * 消费者永远收不到它本来就没见过的事件。
2377
2925
  */
2378
- #emitTool(state, status, call) {
2926
+ #emitTool(state, status, call, errorCode) {
2379
2927
  const key = `${call.id}:${status}`;
2380
2928
  if (state.emittedToolEvents.has(key)) return;
2381
2929
  state.emittedToolEvents.add(key);
@@ -2389,19 +2937,40 @@ var AgentLoop = class {
2389
2937
  status,
2390
2938
  name: call.name,
2391
2939
  callId: call.id,
2392
- args: summarizeArgs(call.arguments)
2940
+ args: summarizeArgs(call.arguments),
2941
+ ...errorCode !== void 0 ? { errorCode } : {}
2393
2942
  }
2394
2943
  });
2395
2944
  }
2396
2945
  async #handleAskUser(call, state) {
2397
2946
  const question = call.arguments["question"];
2398
2947
  if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires a non-empty \"question\" string argument");
2948
+ const rawChoices = call.arguments["choices"];
2949
+ const choices = Array.isArray(rawChoices) ? rawChoices.filter((choice) => typeof choice === "string" && choice !== "") : [];
2950
+ const id = this.#nextInteractionId(state);
2951
+ const rawSuggestion = call.arguments["suggestion"];
2952
+ const rawReason = call.arguments["suggestionReason"];
2953
+ const suggestion = this.#deps.userProfile && typeof rawSuggestion === "string" && rawSuggestion !== "" ? { suggestion: {
2954
+ value: rawSuggestion,
2955
+ ...typeof rawReason === "string" && rawReason !== "" ? { reason: rawReason } : {}
2956
+ } } : {};
2957
+ const request = choices.length > 0 ? {
2958
+ type: "select",
2959
+ id,
2960
+ message: question,
2961
+ options: choices.map((label) => ({
2962
+ label,
2963
+ value: label
2964
+ })),
2965
+ ...suggestion
2966
+ } : {
2967
+ type: "ask",
2968
+ id,
2969
+ message: question,
2970
+ ...suggestion
2971
+ };
2399
2972
  try {
2400
- const value = await this.#interact(state, {
2401
- type: "ask",
2402
- id: this.#nextInteractionId(state),
2403
- message: question
2404
- }, { tool: call.name });
2973
+ const value = await this.#interact(state, request, { tool: call.name });
2405
2974
  return {
2406
2975
  ok: true,
2407
2976
  content: [{
@@ -2487,6 +3056,26 @@ var AgentLoop = class {
2487
3056
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
2488
3057
  }
2489
3058
  /**
3059
+ * 工具结果 → tool 消息内容。image 分片单独提出,不进 JSON 也不参与截断计算(0.6.0 §1.1):
3060
+ * 100 KB 的文本预算遇到一张 200 KB 的图会把整个结果截成垃圾。
3061
+ */
3062
+ async #toolResultParts(call, result, state) {
3063
+ const images = result.content.filter((part) => part.type === "image");
3064
+ if (images.length === 0) return textParts(await this.#serializeToolResult(call, result, state));
3065
+ const textual = {
3066
+ ...result,
3067
+ content: result.content.filter((part) => part.type !== "image")
3068
+ };
3069
+ return [{
3070
+ type: "text",
3071
+ text: await this.#serializeToolResult(call, textual, state)
3072
+ }, ...images.map((image) => ({
3073
+ type: "image",
3074
+ mimeType: image.mimeType,
3075
+ data: image.data
3076
+ }))];
3077
+ }
3078
+ /**
2490
3079
  * 工具结果回喂序列化:超过 toolResultMaxBytes(默认 100KB)时头尾保留截断,
2491
3080
  * 完整内容经 artifactStore 落 artifact,回喂摘要含 artifact id。
2492
3081
  */
@@ -2590,32 +3179,36 @@ var AgentLoop = class {
2590
3179
  }
2591
3180
  const executor = this.#deps.executor;
2592
3181
  const loaded = [];
3182
+ let scripts = [];
2593
3183
  if (executor) {
2594
3184
  let scriptFiles;
2595
3185
  try {
2596
- scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
3186
+ scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => `scripts/${s.path.split("/").pop() ?? s.path}`);
2597
3187
  } catch (e) {
2598
3188
  scriptFiles = [];
2599
3189
  if (!(e instanceof WebSkillError && e.code === "FS_NOT_FOUND")) state.trace.record("run.warning", { message: `Failed to list scripts of skill "${skillName}": ${messageOf(e)}` });
2600
3190
  }
2601
- for (const file of scriptFiles) {
2602
- const match = /^(.*)\.(ts|js)$/.exec(file);
2603
- if (!match?.[1]) continue;
2604
- if (allowedTools && !allowedTools.includes(match[1])) {
2605
- state.trace.record("run.warning", { message: `Skill "${skillName}" declares allowed-tools, so its script "${match[1]}" is not registered as a tool. Add "${match[1]}" to allowed-tools if that was not intended.` });
3191
+ scripts = listSkillScripts(skillName, scriptFiles, allowedTools);
3192
+ for (const script of scripts) {
3193
+ if (script.toolName === void 0) {
3194
+ state.trace.record("run.warning", { message: `Skill "${skillName}" declares allowed-tools, so its script "${script.scriptName}" is not registered as a tool. Add "${script.scriptName}" to allowed-tools if that was not intended.` });
2606
3195
  continue;
2607
3196
  }
2608
3197
  try {
2609
- const def = await executor.loadDefinition(root, match[1]);
2610
- await this.#enrichDefinition(root, match[1], def, state);
3198
+ const def = await executor.loadDefinition(root, script.scriptName);
3199
+ script.schemaSource = await this.#enrichDefinition(root, script.scriptName, def, state);
3200
+ script.toolName = def.name;
2611
3201
  state.activatedTools.set(def.name, def);
2612
3202
  loaded.push(def.name);
2613
3203
  } catch (e) {
2614
- state.trace.record("run.warning", { message: `Failed to load definition of script "${match[1]}" for skill "${skillName}": ${messageOf(e)}` });
3204
+ state.trace.record("run.warning", { message: `Failed to load definition of script "${script.scriptName}" for skill "${skillName}": ${messageOf(e)}` });
3205
+ delete script.toolName;
3206
+ script.schemaSource = "unavailable";
3207
+ script.unexposedReason = "its definition could not be loaded";
2615
3208
  }
2616
3209
  }
2617
3210
  }
2618
- let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
3211
+ let note = (loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "") + formatSkillScriptManifest(scripts);
2619
3212
  for (const dep of dependencies) {
2620
3213
  if (state.activated.has(dep)) continue;
2621
3214
  if (!this.#deps.skillIndex.has(dep)) {
@@ -2629,51 +3222,86 @@ var AgentLoop = class {
2629
3222
  /**
2630
3223
  * D2 Schema 兜底链(同一 run 内激活时只算一次):
2631
3224
  * 显式 inputSchema > sidecar scripts/<name>.schema.json > schemaInferer 推导 > schemaUnavailable
3225
+ * 返回值即最终来源,进技能清单告诉模型该信谁(FR-18.1)。
2632
3226
  */
2633
3227
  async #enrichDefinition(skillRoot, scriptName, def, state) {
2634
- if (def.inputSchema) return;
3228
+ if (def.inputSchema) return "module-export";
2635
3229
  const sidecar = `${skillRoot}/scripts/${scriptName}.schema.json`;
2636
3230
  if (await this.#deps.fs.exists(sidecar)) try {
2637
3231
  def.inputSchema = JSON.parse(await this.#deps.fs.readText(sidecar));
2638
- return;
3232
+ return "sidecar";
2639
3233
  } catch (e) {
2640
3234
  state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf(e)}` });
2641
3235
  }
2642
- if (!this.#deps.schemaInferer) return;
3236
+ if (!this.#deps.schemaInferer) return "unavailable";
2643
3237
  for (const ext of ["ts", "js"]) {
2644
3238
  const scriptPath = `${skillRoot}/scripts/${scriptName}.${ext}`;
2645
3239
  if (!await this.#deps.fs.exists(scriptPath)) continue;
2646
3240
  try {
2647
3241
  const inferred = this.#deps.schemaInferer.inferSchemaFromSource(await this.#deps.fs.readText(scriptPath), { fileName: `${scriptName}.${ext}` });
2648
- if (inferred) def.inputSchema = inferred;
3242
+ if (inferred) {
3243
+ def.inputSchema = inferred;
3244
+ return "inferred";
3245
+ }
2649
3246
  } catch (e) {
2650
3247
  state.trace.record("run.warning", { message: `Schema inference failed for ${scriptPath}: ${messageOf(e)}` });
2651
3248
  }
2652
- return;
3249
+ return "unavailable";
2653
3250
  }
3251
+ return "unavailable";
2654
3252
  }
2655
3253
  async #handleScriptTool(call, skillName, scriptName, state) {
2656
3254
  const root = this.#deps.skillIndex.get(skillName);
2657
3255
  if (!root) return toolError("TOOL_NOT_FOUND", `Skill "${skillName}" is not in the catalog`);
2658
3256
  if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
2659
3257
  const def = state.activatedTools.get(call.name);
2660
- if (!def) return toolError("TOOL_NOT_FOUND", `Tool "${call.name}" is not available for skill "${skillName}"`);
3258
+ if (!def) {
3259
+ state.unknownToolCalls += 1;
3260
+ const availableTools = [...state.activatedTools.keys()].filter((tool) => tool.startsWith(`${skillName}__`)).sort();
3261
+ const detail = availableTools.length > 0 ? ` Available tools for skill "${skillName}": ${availableTools.join(", ")}.` : ` Skill "${skillName}" exposes no script tools.`;
3262
+ return toolError("TOOL_NOT_FOUND", `Tool "${call.name}" is not available.${detail}`, {
3263
+ skillName,
3264
+ availableTools
3265
+ });
3266
+ }
2661
3267
  let args = call.arguments;
2662
3268
  const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
2663
3269
  if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
2664
- const value = await this.#interact(state, {
2665
- type: "form",
2666
- id: this.#nextInteractionId(state),
2667
- title: `Missing parameters for ${call.name}`,
2668
- fields: schemaToForm(def.inputSchema, args)
2669
- }, {
2670
- tool: call.name,
2671
- missing
2672
- });
2673
- if (typeof value === "object" && value !== null) args = {
2674
- ...args,
2675
- ...value
2676
- };
3270
+ const fields = schemaToForm(def.inputSchema, args);
3271
+ const declined = /* @__PURE__ */ new Set();
3272
+ for (const field of fields) {
3273
+ if (field.type !== "file" || !missing.includes(field.name)) continue;
3274
+ const value = await this.#interact(state, {
3275
+ type: "file-pick",
3276
+ id: this.#nextInteractionId(state),
3277
+ message: `Open local file picker to fill field "${field.name}"?`,
3278
+ field: field.name
3279
+ }, {
3280
+ tool: call.name,
3281
+ missing: [field.name]
3282
+ });
3283
+ if (value === void 0 || value === null) declined.add(field.name);
3284
+ else args = {
3285
+ ...args,
3286
+ [field.name]: value
3287
+ };
3288
+ }
3289
+ const stillMissing = missing.filter((key) => args[key] === void 0 && !declined.has(key));
3290
+ if (stillMissing.length > 0) {
3291
+ const value = await this.#interact(state, {
3292
+ type: "form",
3293
+ id: this.#nextInteractionId(state),
3294
+ title: `Missing parameters for ${call.name}`,
3295
+ fields: fields.filter((f) => f.type !== "file" && !declined.has(f.name))
3296
+ }, {
3297
+ tool: call.name,
3298
+ missing: stillMissing
3299
+ });
3300
+ if (typeof value === "object" && value !== null) args = {
3301
+ ...args,
3302
+ ...value
3303
+ };
3304
+ }
2677
3305
  } catch (e) {
2678
3306
  if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `Parameter form failed: ${e.message}`);
2679
3307
  throw e;
@@ -2827,16 +3455,111 @@ var AgentLoop = class {
2827
3455
  if (!this.#deps.memory) return;
2828
3456
  const scope = `session:${state.run.sessionId}`;
2829
3457
  const limit = this.#config.paramHistoryLimit;
3458
+ const recorded = request.type === "file-pick" ? redactFileValue(value) : value;
2830
3459
  await this.#memoryMutate(scope, "paramHistory", state, (current) => {
2831
3460
  const history = current ?? [];
2832
3461
  history.push({
2833
3462
  ts: state.now(),
2834
3463
  interactionId: request.id,
2835
3464
  type: request.type,
2836
- value
3465
+ value: recorded
2837
3466
  });
2838
3467
  return history.slice(-limit);
2839
3468
  });
3469
+ await this.#recordInteractionBehavior(state, request, value);
3470
+ }
3471
+ /** 用户提问 → 行为记录(FR-19.2)。记的是「问了什么」,不是模型答了什么 */
3472
+ async #recordUserPrompt(state) {
3473
+ const prompt = state.run.userPrompt.trim();
3474
+ if (prompt === "") return;
3475
+ await this.#appendBehaviorRecords(state, [{
3476
+ id: `${state.runId}#prompt`,
3477
+ kind: "question",
3478
+ at: Date.parse(state.now()),
3479
+ value: prompt,
3480
+ scene: { sessionId: state.run.sessionId }
3481
+ }]);
3482
+ }
3483
+ /**
3484
+ * 交互提交 → 行为记录(FR-19.2)。写在这里而不是 UiBridge:
3485
+ * 只有 AgentLoop 同时握有场景(技能、字段语义、候选集)与提交值。
3486
+ */
3487
+ async #recordInteractionBehavior(state, request, value) {
3488
+ const records = [];
3489
+ const at = Date.parse(state.now());
3490
+ const base = {
3491
+ sessionId: state.run.sessionId,
3492
+ interactionId: request.id
3493
+ };
3494
+ const skill = [...state.activated].at(-1);
3495
+ const scene = skill !== void 0 ? {
3496
+ ...base,
3497
+ skillName: skill
3498
+ } : base;
3499
+ if (request.type === "select") records.push({
3500
+ id: `${request.id}#selected`,
3501
+ kind: "choice",
3502
+ at,
3503
+ confirmed: true,
3504
+ value: String(value),
3505
+ scene: {
3506
+ ...scene,
3507
+ fieldSemantics: request.message,
3508
+ options: request.options.map((o) => String(o.label))
3509
+ }
3510
+ });
3511
+ else if (request.type === "ask" && typeof value === "string" && value !== "") records.push({
3512
+ id: `${request.id}#answer`,
3513
+ kind: "text-input",
3514
+ at,
3515
+ confirmed: true,
3516
+ value,
3517
+ scene: {
3518
+ ...scene,
3519
+ fieldSemantics: request.message
3520
+ }
3521
+ });
3522
+ else if (request.type === "form" && typeof value === "object" && value !== null) {
3523
+ const submitted = value;
3524
+ for (const field of request.fields) {
3525
+ const next = submitted[field.name];
3526
+ if (typeof next !== "string" && typeof next !== "number" && typeof next !== "boolean") continue;
3527
+ if (next === "") continue;
3528
+ records.push({
3529
+ id: `${request.id}#${field.name}`,
3530
+ kind: field.options ? "choice" : "text-input",
3531
+ at,
3532
+ confirmed: true,
3533
+ value: String(next),
3534
+ scene: {
3535
+ ...scene,
3536
+ fieldName: field.name,
3537
+ fieldSemantics: field.description ?? field.label,
3538
+ ...field.options ? { options: field.options.map((o) => String(o.label)) } : {}
3539
+ }
3540
+ });
3541
+ }
3542
+ }
3543
+ await this.#appendBehaviorRecords(state, records);
3544
+ }
3545
+ /** 行为记录追加(FR-19.2)。默认关闭:未注入 `userProfile` 时零持久化写入 */
3546
+ async #appendBehaviorRecords(state, records) {
3547
+ const profile = this.#deps.userProfile;
3548
+ if (!profile || !this.#deps.memory || records.length === 0) return;
3549
+ const limits = profile.limits ?? DEFAULT_USER_PROFILE_LIMITS;
3550
+ await this.#memoryMutate(`user:${profile.userId}`, BEHAVIOR_RECORDS_KEY, state, (current) => appendBehaviorRecords(readBehaviorRecords(current), records, limits));
3551
+ }
3552
+ /** 画像 → 系统消息(FR-19.4)。读不到或为空时不注入任何内容 */
3553
+ async #userProfileMessage(state) {
3554
+ const profile = this.#deps.userProfile;
3555
+ if (!profile || !this.#deps.memory) return void 0;
3556
+ const limits = profile.limits ?? DEFAULT_USER_PROFILE_LIMITS;
3557
+ const context = renderUserProfileContext(readUserProfile(await this.#memoryGet(`user:${profile.userId}`, USER_PROFILE_KEY, state)), limits.injectMaxBytes);
3558
+ if (context === "") return void 0;
3559
+ return {
3560
+ role: "system",
3561
+ content: textParts(`${context}\n\n${USER_PROFILE_NO_INVENTION_RULE}`)
3562
+ };
2840
3563
  }
2841
3564
  };
2842
3565
  /**
@@ -2948,6 +3671,7 @@ var WebSkillRuntime = class {
2948
3671
  return result;
2949
3672
  }
2950
3673
  async run(userPrompt, options = {}) {
3674
+ if (options.skillName !== void 0 && this.#deps.config?.toolCallingDisabled === true) throw new WebSkillError("MODEL_TOOLS_UNSUPPORTED", "The selected model does not support skill invocation. Switch to a model with tool calling.");
2951
3675
  if (!this.#catalogCache) await this.discover();
2952
3676
  const cache = this.#catalogCache;
2953
3677
  if (!cache) throw new Error("discover() did not populate the catalog cache");
@@ -2962,7 +3686,9 @@ var WebSkillRuntime = class {
2962
3686
  }))).flat();
2963
3687
  const catalog = providerEntries.length > 0 ? { entries: mergeCatalogEntries(cache.catalog.entries, providerEntries) } : cache.catalog;
2964
3688
  const filteredCatalog = this.#deps.catalogFilter ? { entries: await this.#deps.catalogFilter(catalog.entries) } : catalog;
2965
- const route = await this.#router.route(filteredCatalog);
3689
+ const routedCatalog = options.skillName === void 0 ? filteredCatalog : { entries: filteredCatalog.entries.filter((entry) => entry.name === options.skillName) };
3690
+ if (options.skillName !== void 0 && routedCatalog.entries.length === 0) throw new WebSkillError("SKILL_NOT_FOUND", `Skill "${options.skillName}" is not available in this catalog`);
3691
+ const route = await this.#router.route(routedCatalog);
2966
3692
  const loop = new AgentLoop({
2967
3693
  llm: this.#deps.llm,
2968
3694
  executor: this.#deps.executor,
@@ -2977,6 +3703,7 @@ var WebSkillRuntime = class {
2977
3703
  hooks: this.#deps.hooks,
2978
3704
  eventBus: this.#events,
2979
3705
  longTerm: this.#deps.longTerm,
3706
+ userProfile: this.#deps.userProfile,
2980
3707
  externalTools: this.#deps.externalTools,
2981
3708
  skillProviders: this.#deps.skillProviders,
2982
3709
  catalogFilter: this.#deps.catalogFilter,
@@ -3007,7 +3734,7 @@ var WebSkillRuntime = class {
3007
3734
  message: `Skill provider listSkills() failed: ${failure}`
3008
3735
  });
3009
3736
  if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
3010
- prompt: userPrompt,
3737
+ prompt: promptText(userPrompt),
3011
3738
  run: result.run
3012
3739
  }).catch((e) => {
3013
3740
  result.run.trace.push({
@@ -3083,6 +3810,7 @@ var WebSkillRuntime = class {
3083
3810
  hooks: this.#deps.hooks,
3084
3811
  eventBus: this.#events,
3085
3812
  longTerm: this.#deps.longTerm,
3813
+ userProfile: this.#deps.userProfile,
3086
3814
  externalTools: this.#deps.externalTools,
3087
3815
  skillProviders: this.#deps.skillProviders,
3088
3816
  catalogFilter: this.#deps.catalogFilter,
@@ -3337,6 +4065,191 @@ var FsMemoryStore = class {
3337
4065
  for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
3338
4066
  }
3339
4067
  };
4068
+ /**
4069
+ * 提炼提示词。要求返回结论列表而不是散文:条目化是「可见、可编辑、可删除」的前提(FR-19.3)。
4070
+ * `id` 由模型给出稳定 slug,才能在下一次提炼时更新同一条而不是无限追加。
4071
+ */
4072
+ const USER_PROFILE_REFINE_PROMPT = `You summarize what a single user is like, based on records of what they did in a chat application.
4073
+
4074
+ Return ONLY a JSON object with this shape, and nothing else:
4075
+
4076
+ {
4077
+ "entries": [
4078
+ { "id": "slug-case-stable-id", "text": "One sentence about the user.", "confidence": "high" | "low" }
4079
+ ]
4080
+ }
4081
+
4082
+ Rules:
4083
+ - Each entry is one durable, reusable fact or preference about the user — how they like answers, what they work on, values they keep entering, languages they use.
4084
+ - Reuse the same "id" across runs for the same conclusion so it can be updated instead of duplicated. Use lowercase words joined by hyphens.
4085
+ - Use "high" only when several records agree. A single record is "low".
4086
+ - Do not restate individual records. Do not include secrets, credentials, or full personal identifiers.
4087
+ - Do not invent anything the records do not support. If the records say nothing useful, return {"entries": []}.`;
4088
+ /** 采样后的记录 → 送模型的摘要。导出供测试与宿主复核实际请求内容(AC-19.6) @experimental */
4089
+ function toRecordDigests(records) {
4090
+ return records.map((record) => ({
4091
+ kind: record.kind,
4092
+ at: new Date(record.at).toISOString(),
4093
+ value: record.value,
4094
+ ...record.scene.skillName !== void 0 ? { skill: record.scene.skillName } : {},
4095
+ ...record.scene.fieldName !== void 0 ? { field: record.scene.fieldName } : {},
4096
+ ...record.scene.fieldSemantics !== void 0 ? { meaning: record.scene.fieldSemantics } : {},
4097
+ ...record.scene.options !== void 0 ? { options: record.scene.options } : {},
4098
+ ...record.confirmed === true ? { confirmed: true } : {}
4099
+ }));
4100
+ }
4101
+ /** 模型可能把 JSON 包在 ```json 里;只取第一个对象,解析不出就当没结论 */
4102
+ function parseEntries(content, now) {
4103
+ const text = promptText(content ?? []);
4104
+ const start = text.indexOf("{");
4105
+ const end = text.lastIndexOf("}");
4106
+ if (start < 0 || end <= start) return [];
4107
+ let parsed;
4108
+ try {
4109
+ parsed = JSON.parse(text.slice(start, end + 1));
4110
+ } catch {
4111
+ return [];
4112
+ }
4113
+ const raw = parsed?.entries;
4114
+ return readProfileEntries(Array.isArray(raw) ? raw.map((item) => ({
4115
+ updatedAt: now,
4116
+ ...item
4117
+ })) : []);
4118
+ }
4119
+ /**
4120
+ * 一次提炼(FR-19.3)。调用方负责持久化返回值;本函数不碰存储,
4121
+ * 也不吞异常——「失败不影响会话结束」由调用点的 fire-and-forget 保证,
4122
+ * 在这里吞掉会让手动提炼入口无法报错。
4123
+ * @experimental
4124
+ */
4125
+ async function refineUserProfile(input) {
4126
+ const limits = input.limits ?? DEFAULT_USER_PROFILE_LIMITS;
4127
+ const now = (input.now ?? Date.now)();
4128
+ const current = input.profile ?? EMPTY_USER_PROFILE;
4129
+ const sampled = sampleBehaviorRecords(input.records, limits);
4130
+ if (sampled.length === 0) return current;
4131
+ const existing = current.entries.map((entry) => ({
4132
+ id: entry.id,
4133
+ text: entry.text
4134
+ }));
4135
+ const entries = parseEntries((await input.llm.complete({
4136
+ ...input.model !== void 0 ? { model: input.model } : {},
4137
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
4138
+ messages: [{
4139
+ role: "system",
4140
+ content: [{
4141
+ type: "text",
4142
+ text: USER_PROFILE_REFINE_PROMPT
4143
+ }]
4144
+ }, {
4145
+ role: "user",
4146
+ content: [{
4147
+ type: "text",
4148
+ text: `Existing conclusions:\n${JSON.stringify(existing)}\n\nRecords:\n${JSON.stringify(toRecordDigests(sampled))}`
4149
+ }]
4150
+ }]
4151
+ })).content, now);
4152
+ if (entries.length === 0) return current;
4153
+ return mergeProfileEntries(current, entries, {
4154
+ now,
4155
+ limits
4156
+ });
4157
+ }
4158
+ /** 导出文件的版本;导入时逐字校验,不做向前兼容猜测 @experimental */
4159
+ const USER_PROFILE_EXPORT_VERSION = 1;
4160
+ /** 与偏好导出同一套判据:归一化后按子串匹配,命中即拒 */
4161
+ const CREDENTIAL_MARKERS = [
4162
+ "apikey",
4163
+ "secret",
4164
+ "token",
4165
+ "password",
4166
+ "passphrase",
4167
+ "credential",
4168
+ "privatekey"
4169
+ ];
4170
+ const isCredentialKey = (key) => {
4171
+ const normalized = key.toLowerCase().replace(/[-_\s]/g, "");
4172
+ return CREDENTIAL_MARKERS.some((marker) => normalized.includes(marker));
4173
+ };
4174
+ function findCredentialKey(value) {
4175
+ if (Array.isArray(value)) {
4176
+ for (const item of value) {
4177
+ const hit = findCredentialKey(item);
4178
+ if (hit !== void 0) return hit;
4179
+ }
4180
+ return;
4181
+ }
4182
+ if (typeof value !== "object" || value === null) return void 0;
4183
+ for (const [key, child] of Object.entries(value)) {
4184
+ if (isCredentialKey(key)) return key;
4185
+ const hit = findCredentialKey(child);
4186
+ if (hit !== void 0) return hit;
4187
+ }
4188
+ }
4189
+ /**
4190
+ * 导出画像(FR-19.7)。`excludeIds` 来自导出前的审阅界面:用户勾掉的条目不进文件。
4191
+ * @experimental
4192
+ */
4193
+ function exportUserProfile(profile, options) {
4194
+ const excluded = new Set(options.excludeIds ?? []);
4195
+ const result = {
4196
+ kind: "webskill.user-profile",
4197
+ version: 1,
4198
+ origin: options.origin,
4199
+ exportedAt: (options.now ?? Date.now)(),
4200
+ entries: profile.entries.filter((entry) => !excluded.has(entry.id))
4201
+ };
4202
+ const leaked = findCredentialKey(result);
4203
+ if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `A user profile export must not contain credentials, but "${leaked}" was present`);
4204
+ return result;
4205
+ }
4206
+ /** 解析并校验导入内容(FR-19.7)。含凭据是**拒绝**而不是忽略 @experimental */
4207
+ function parseUserProfileExport(raw) {
4208
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must be a JSON object");
4209
+ const record = raw;
4210
+ if (record["kind"] !== "webskill.user-profile") throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must have \"kind\": \"webskill.user-profile\"");
4211
+ if (record["version"] !== 1) throw new WebSkillError("PROFILE_IMPORT_VERSION_UNSUPPORTED", `Unsupported user profile file version ${JSON.stringify(record["version"])}; expected 1`);
4212
+ const leaked = findCredentialKey(record);
4213
+ if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `The user profile file contains a credential field "${leaked}"; the import was rejected`);
4214
+ const entries = readProfileEntries(record["entries"]);
4215
+ if (entries.length === 0) throw new WebSkillError("PROFILE_IMPORT_INVALID", "The user profile file has no usable entries");
4216
+ return {
4217
+ kind: "webskill.user-profile",
4218
+ version: 1,
4219
+ origin: typeof record["origin"] === "string" ? record["origin"] : "unknown",
4220
+ exportedAt: typeof record["exportedAt"] === "number" ? record["exportedAt"] : 0,
4221
+ entries
4222
+ };
4223
+ }
4224
+ /** 导入前给用户看的差异(FR-19.7):新增哪些、覆盖哪些 @experimental */
4225
+ function diffUserProfile(current, incoming) {
4226
+ const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
4227
+ const added = [];
4228
+ const updated = [];
4229
+ for (const entry of incoming.entries) {
4230
+ const existing = byId.get(entry.id);
4231
+ if (existing === void 0) added.push(entry);
4232
+ else if (existing.text !== entry.text) updated.push(entry);
4233
+ }
4234
+ return {
4235
+ added,
4236
+ updated
4237
+ };
4238
+ }
4239
+ /** 确认后落库:同 id 覆盖,其余保留;不触碰模型配置与凭据 @experimental */
4240
+ function applyUserProfileImport(current, incoming, options = {}) {
4241
+ const now = (options.now ?? Date.now)();
4242
+ const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
4243
+ for (const entry of incoming.entries) byId.set(entry.id, {
4244
+ ...entry,
4245
+ updatedAt: now
4246
+ });
4247
+ return {
4248
+ version: 1,
4249
+ entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, DEFAULT_USER_PROFILE_LIMITS.entryLimit),
4250
+ refinedAt: current.refinedAt
4251
+ };
4252
+ }
3340
4253
  const INDEX_FILE$1 = "index.json";
3341
4254
  /**
3342
4255
  * 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
@@ -3688,12 +4601,16 @@ var FsRunTraceStore = class {
3688
4601
  #root;
3689
4602
  #fs;
3690
4603
  #onError;
4604
+ #onIndexError;
3691
4605
  constructor(deps) {
3692
4606
  this.#root = deps.root.replace(/\/+$/, "");
3693
4607
  this.#fs = deps.fs;
3694
4608
  this.#onError = deps.onError ?? ((error, run) => {
3695
4609
  console.warn(`Failed to persist run trace "${run.id}": ${messageOf(error)}`);
3696
4610
  });
4611
+ this.#onIndexError = deps.onIndexError ?? ((error, run) => {
4612
+ console.warn(`Run trace "${run.id}" was saved but its index entry could not be appended: ${messageOf(error)}. The index will be rebuilt on the next read.`);
4613
+ });
3697
4614
  }
3698
4615
  #path(runId) {
3699
4616
  return resolveInsideRoot(this.#root, `${runId}.json`);
@@ -3711,9 +4628,14 @@ var FsRunTraceStore = class {
3711
4628
  };
3712
4629
  try {
3713
4630
  await this.#fs.writeText(this.#path(run.id), JSON.stringify(trace, null, 2));
3714
- await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
3715
4631
  } catch (e) {
3716
4632
  this.#onError(e, run);
4633
+ return;
4634
+ }
4635
+ try {
4636
+ await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
4637
+ } catch (e) {
4638
+ this.#onIndexError(e, run);
3717
4639
  }
3718
4640
  }
3719
4641
  async get(runId) {
@@ -3839,12 +4761,15 @@ function summarizeToolCalls(run) {
3839
4761
  if (typeof name !== "string" || typeof callId !== "string") continue;
3840
4762
  const args = event.data?.["args"];
3841
4763
  const durationMs = event.data?.["durationMs"];
4764
+ const errorCode = event.data?.["code"];
3842
4765
  calls.push({
3843
4766
  callId,
3844
4767
  name,
3845
4768
  status: event.type === "tool.completed" ? "completed" : "failed",
3846
4769
  ...typeof args === "string" ? { args } : {},
3847
- ...typeof durationMs === "number" ? { durationMs } : {}
4770
+ ...typeof durationMs === "number" ? { durationMs } : {},
4771
+ ...typeof errorCode === "string" ? { errorCode } : {},
4772
+ ...event.type === "tool.failed" && typeof event.message === "string" ? { errorMessage: event.message } : {}
3848
4773
  });
3849
4774
  }
3850
4775
  return calls;
@@ -3861,6 +4786,7 @@ const toMeta = (record) => ({
3861
4786
  messageCount: record.messages.length
3862
4787
  });
3863
4788
  const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4789
+ const baseName = (path) => path.split("/").pop() ?? path;
3864
4790
  /**
3865
4791
  * 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
3866
4792
  * 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
@@ -3954,8 +4880,13 @@ var FsSessionStore = class {
3954
4880
  return record;
3955
4881
  }
3956
4882
  async list(options = {}) {
3957
- if (!await this.#fs.exists(this.#root)) return { items: [] };
4883
+ if (!await this.#fs.exists(this.#root)) return {
4884
+ items: [],
4885
+ source: "missing-root",
4886
+ skipped: []
4887
+ };
3958
4888
  const metas = [];
4889
+ const skipped = [];
3959
4890
  for (const entry of await this.#fs.list(this.#root)) {
3960
4891
  if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
3961
4892
  let record;
@@ -3963,13 +4894,18 @@ var FsSessionStore = class {
3963
4894
  record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
3964
4895
  } catch (e) {
3965
4896
  console.warn(`Skipping unreadable session file "${entry.path}": ${e instanceof Error ? e.message : String(e)}`);
4897
+ skipped.push(baseName(entry.path));
3966
4898
  continue;
3967
4899
  }
3968
4900
  if (record.archived === true && options.includeArchived !== true) continue;
3969
4901
  metas.push(toMeta(record));
3970
4902
  }
3971
4903
  metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3972
- return takeTailPage(metas, options, "session");
4904
+ return {
4905
+ ...takeTailPage(metas, options, "session"),
4906
+ source: "ok",
4907
+ skipped
4908
+ };
3973
4909
  }
3974
4910
  /**
3975
4911
  * 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
@@ -4036,4 +4972,4 @@ var FsSessionStore = class {
4036
4972
  };
4037
4973
 
4038
4974
  //#endregion
4039
- export { createScriptContext as A, networkUrlHost as B, RUN_TRACE_SCHEMA_VERSION as C, WebSkillRuntime as D, TraceRecorder as E, fromVercelStreamPart as F, resolveToolName as G, normalizeToolContent as H, isNetworkAllowed as I, toLlmToolSpec as J, schemaToForm as K, isUnsupportedRunSnapshot as L, extractChartSpec as M, extractUiSpecEvents as N, bridgeError as O, fromVercelResult as P, mergeCatalogEntries as R, RUN_SNAPSHOT_SCHEMA_VERSION as S, SerializingMemoryStore as T, normalizeToolError as U, normalizeErrorCode as V, parseBridgeRequest as W, validateUiSpecEvent as X, toVercelToolSpecs as Y, validateUiSpecNode as Z, OpenAiCompatibleClient as _, AnthropicClient as a, READ_SKILL_FILE_TOOL as b, FS_SESSION_PAGE_SIZE as c, FsRunSnapshotStore as d, FsRunTraceStore as f, HookRunner as g, GoogleGenAiClient as h, AgentLoop as i, createWebSkillApi as j, buildRenderResult as k, FsArtifactStore as l, FullDisclosureRouter as m, ASK_USER_TOOL as n, CapabilityApproval as o, FsSessionStore as p, summarizeToolCalls as q, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsMemoryStore as u, ProgressiveRouter as v, SESSION_SCHEMA_VERSION as w, READ_SKILL_FILE_TOOL_NAME as x, READ_SKILL_FILE_INPUT_SCHEMA as y, networkPolicyLibSource as z };
4975
+ export { listSkillScripts as $, TraceRecorder as A, buildRenderResult as B, READ_SKILL_FILE_INPUT_SCHEMA as C, validateUiSpecEvent as Ct, RUN_TRACE_SCHEMA_VERSION as D, RUN_SNAPSHOT_SCHEMA_VERSION as E, USER_PROFILE_REFINE_PROMPT as F, extractChartSpec as G, createWebSkillApi as H, WebSkillRuntime as I, formatSkillScriptManifest as J, extractTodoTraceEvents as K, appendBehaviorRecords as L, USER_PROFILE_KEY as M, USER_PROFILE_NO_INVENTION_RULE as N, SESSION_SCHEMA_VERSION as O, USER_PROFILE_PROMPT_HEADER as P, isUnsupportedRunSnapshot as Q, applyUserProfileImport as R, ProgressiveRouter as S, toVercelToolSpecs as St, READ_SKILL_FILE_TOOL_NAME as T, diffUserProfile as U, createScriptContext as V, exportUserProfile as W, fromVercelStreamPart as X, fromVercelResult as Y, isNetworkAllowed as Z, FsSessionStore as _, schemaToForm as _t, AgentLoop as a, normalizeToolContent as at, HookRunner as b, toLlmToolSpec as bt, CapabilityApproval as c, parseUserProfileExport as ct, EventBus as d, readUserProfile as dt, mergeCatalogEntries as et, FS_SESSION_PAGE_SIZE as f, refineUserProfile as ft, FsRunTraceStore as g, schemaSourceLabel as gt, FsRunSnapshotStore as h, sampleBehaviorRecords as ht, ASK_USER_TOOL_NAME as i, normalizeErrorCode as it, USER_PROFILE_EXPORT_VERSION as j, SerializingMemoryStore as k, DEFAULT_USER_PROFILE_LIMITS as l, readBehaviorRecords as lt, FsMemoryStore as m, resolveToolName as mt, ASK_USER_INPUT_SCHEMA as n, networkPolicyLibSource as nt, AnthropicClient as o, normalizeToolError as ot, FsArtifactStore as p, renderUserProfileContext as pt, extractUiSpecEvents as q, ASK_USER_TOOL as r, networkUrlHost as rt, BEHAVIOR_RECORDS_KEY as s, parseBridgeRequest as st, ALLOWED_TOOLS_EXCLUSION_REASON as t, mergeProfileEntries as tt, EMPTY_USER_PROFILE as u, readProfileEntries as ut, FullDisclosureRouter as v, scriptToolName as vt, READ_SKILL_FILE_TOOL as w, validateUiSpecNode as wt, OpenAiCompatibleClient as x, toRecordDigests as xt, GoogleGenAiClient as y, summarizeToolCalls as yt, bridgeError as z };