@webskill/sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agent.d.ts +2 -2
  2. package/dist/agent.js +57 -15
  3. package/dist/browser.d.ts +101 -5
  4. package/dist/browser.js +421 -11
  5. package/dist/{catalogComponents-DV7cPpUm-C77AEEx9.js → catalogComponents-Dr5dFMAb-Dacibl1e.js} +126 -44
  6. package/dist/{dist-bewtXYlO.js → dist-DnYG2-eY.js} +53 -14
  7. package/dist/{dist-6C03DShK.js → dist-DusANsrn.js} +981 -185
  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 +3 -3
  11. package/dist/governance.js +1 -1
  12. package/dist/{index-Bsqg4ftU.d.ts → index-BMocOEi0.d.ts} +13 -7
  13. package/dist/{index-D_7ZZjkl.d.ts → index-BuTpBMzr.d.ts} +69 -6
  14. package/dist/{index-vBz_FC9w.d.ts → index-C-KFAZoF.d.ts} +298 -52
  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-W3Ce896k-ClFTRZFs.js → openUiLibrary-Bdrji9qK-DzAxRlTY.js} +3 -3
  23. package/dist/{skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts → skillVersionStore-BzLbzFOL-CxdAFWO2.d.ts} +1 -1
  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-D_hoCri8-BnNPiZCi.d.ts → types-4pg-qp_I-Gq63X8Oa.d.ts} +33 -8
  28. package/dist/ui-react.d.ts +2 -2
  29. package/dist/ui-react.js +66 -23
  30. package/dist/ui-vue.d.ts +1 -1
  31. package/dist/ui-vue.js +13 -8
  32. package/dist/ui.d.ts +3 -3
  33. package/dist/ui.js +2 -2
  34. package/dist/{webskillLitCatalog-_mugzRHx-DiuJpCuf.js → webskillLitCatalog-_mugzRHx-B_54vxum.js} +1 -1
  35. package/package.json +1 -1
@@ -1,7 +1,201 @@
1
1
  import { A as parseSkillMarkdown, L as resolveInsideRoot, O as messageOf, P as renderAvailableSkillsXml, V as validateSkills, f as SkillDiscovery, g as assertSafePathSegment, m as WebSkillError, p as SkillReader, v as buildCatalog } from "./dist-8oQRa8Xz.js";
2
- import { a as 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
  };
@@ -1183,70 +1452,12 @@ function extractUiSpecEvents(data) {
1183
1452
  const raw = data["$surface"];
1184
1453
  return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
1185
1454
  }
1186
- /** 跨会话表单填写值在 `user:{userId}` scope 下的 key(FR-5.7) @experimental */
1187
- const FORM_VALUES_KEY = "formValues";
1188
- /**
1189
- * 跨会话稳定的字段标识(FR-5.6)。必须带技能名:
1190
- * 只有字段名时,两个技能各自的 `email` 会互相串号。
1191
- * @experimental
1192
- */
1193
- function formFieldKey(skillName, fieldName) {
1194
- return `${skillName}#${fieldName}`;
1195
- }
1196
- /** memory 里的原始值形状不受控(宿主可能手改文件),逐条过滤而不是整体信任 @experimental */
1197
- function readFormValues(raw) {
1198
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
1199
- const out = {};
1200
- for (const [key, entry] of Object.entries(raw)) {
1201
- if (typeof entry !== "object" || entry === null) continue;
1202
- const record = entry;
1203
- if (typeof record.ts !== "number" || !("value" in record)) continue;
1204
- out[key] = {
1205
- value: record.value,
1206
- ts: record.ts
1207
- };
1208
- }
1209
- return out;
1210
- }
1211
- /** 合并本次提交并按上限裁剪最旧(FR-5.12) @experimental */
1212
- function putFormValues(current, updates, limit) {
1213
- const merged = {
1214
- ...current,
1215
- ...updates
1216
- };
1217
- const keys = Object.keys(merged);
1218
- if (limit <= 0) return {};
1219
- if (keys.length <= limit) return merged;
1220
- const kept = keys.sort((a, b) => merged[a].ts - merged[b].ts).slice(keys.length - limit);
1221
- return Object.fromEntries(kept.map((key) => [key, merged[key]]));
1222
- }
1223
- /** 清除单个字段;不传 fieldKey 即全部清除(FR-5.11) @experimental */
1224
- function clearFormValues(current, fieldKey) {
1225
- if (fieldKey === void 0) return {};
1226
- const { [fieldKey]: _removed, ...rest } = current;
1227
- return rest;
1228
- }
1229
- /**
1230
- * 宿主侧的清除入口(FR-5.11):设置面板的「清除填写历史」直接调它,
1231
- * 不必自己知道 scope 与 key 的约定。
1232
- * @experimental
1233
- */
1234
- async function clearStoredFormValues(memory, userId, fieldKey) {
1235
- const scope = `user:${userId}`;
1236
- if (fieldKey === void 0) {
1237
- await memory.delete(scope, FORM_VALUES_KEY);
1238
- return;
1239
- }
1240
- const next = clearFormValues(readFormValues(await memory.get(scope, FORM_VALUES_KEY)), fieldKey);
1241
- await memory.set(scope, FORM_VALUES_KEY, next);
1242
- }
1243
1455
  /**
1244
1456
  * JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
1245
1457
  * providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
1246
1458
  * type 映射:string→text、number/integer→number、boolean→boolean、enum→select、其余→textarea。
1247
- * 传入 skillName 时给每个字段带上跨会话稳定的 `fieldKey`(FR-5.6)。
1248
1459
  */
1249
- function schemaToForm(schema, providedArgs, options) {
1460
+ function schemaToForm(schema, providedArgs) {
1250
1461
  const required = new Set(Array.isArray(schema.required) ? schema.required : []);
1251
1462
  const fields = [];
1252
1463
  for (const [name, prop] of Object.entries(schema.properties ?? {})) {
@@ -1257,7 +1468,6 @@ function schemaToForm(schema, providedArgs, options) {
1257
1468
  type: mapFieldType(prop),
1258
1469
  ...required.has(name) ? { required: true } : {},
1259
1470
  ...typeof prop.description === "string" ? { description: prop.description } : {},
1260
- ...options?.skillName !== void 0 ? { fieldKey: formFieldKey(options.skillName, name) } : {},
1261
1471
  ...provided !== void 0 ? { defaultValue: provided } : prop.default !== void 0 ? { defaultValue: prop.default } : {}
1262
1472
  };
1263
1473
  if (Array.isArray(prop.enum)) field.options = prop.enum.map((v) => ({
@@ -1271,7 +1481,7 @@ function schemaToForm(schema, providedArgs, options) {
1271
1481
  function mapFieldType(prop) {
1272
1482
  if (Array.isArray(prop.enum)) return "select";
1273
1483
  switch (prop.type) {
1274
- case "string": return "text";
1484
+ case "string": return prop["format"] === "binary" || prop["contentEncoding"] === "base64" ? "file" : "text";
1275
1485
  case "number":
1276
1486
  case "integer": return "number";
1277
1487
  case "boolean": return "boolean";
@@ -1361,6 +1571,176 @@ var SerializingMemoryStore = class {
1361
1571
  return this.#serialize(scope, () => fn(this.#inner));
1362
1572
  }
1363
1573
  };
1574
+ /** @experimental */
1575
+ const DEFAULT_USER_PROFILE_LIMITS = {
1576
+ recordLimit: 500,
1577
+ valueMaxChars: 2e3,
1578
+ sampleLimit: 80,
1579
+ injectMaxBytes: 2e3,
1580
+ entryLimit: 40
1581
+ };
1582
+ /** @experimental */
1583
+ const EMPTY_USER_PROFILE = {
1584
+ version: 1,
1585
+ entries: [],
1586
+ refinedAt: 0
1587
+ };
1588
+ /** 注入段的标题。宿主/测试要判断「有没有注入画像」时对它取子串,不要自己再拼一份 @experimental */
1589
+ const USER_PROFILE_PROMPT_HEADER = "## About this user (inferred from past behavior; may be outdated)";
1590
+ /**
1591
+ * 无画像时也要下发的约束(AC-19.11):没有依据就把字段留空,
1592
+ * 不写这一句时模型会把「建议值」当成必填项去编。
1593
+ * @experimental
1594
+ */
1595
+ const USER_PROFILE_NO_INVENTION_RULE = "Only offer a suggested value for a question when the profile above actually supports it. With no relevant profile entry, omit the suggestion and leave the field empty; never invent one.";
1596
+ const byPriority = (a, b) => a.confidence === b.confidence ? b.updatedAt - a.updatedAt : a.confidence === "high" ? -1 : 1;
1597
+ const byteLength = (text) => new TextEncoder().encode(text).length;
1598
+ /**
1599
+ * 画像 → 系统提示片段,按 confidence + updatedAt 优先级排序后逐条累加,
1600
+ * 超出字节上限的部分丢弃并标注(FR-19.4 / AC-19.9)。
1601
+ * 空画像返回空串:只有标题的空段落白占 token,还会诱导模型编造。
1602
+ * @experimental
1603
+ */
1604
+ function renderUserProfileContext(profile = EMPTY_USER_PROFILE, maxBytes = DEFAULT_USER_PROFILE_LIMITS.injectMaxBytes) {
1605
+ const entries = [...profile.entries].sort(byPriority);
1606
+ if (entries.length === 0 || maxBytes <= 0) return "";
1607
+ const lines = entries.map((entry) => `- ${entry.text}`);
1608
+ const render = (keptCount) => {
1609
+ const omitted = lines.length - keptCount;
1610
+ const body = [USER_PROFILE_PROMPT_HEADER, ...lines.slice(0, keptCount)];
1611
+ if (omitted > 0) body.push(`(truncated: ${omitted} more entries omitted)`);
1612
+ return body.join("\n");
1613
+ };
1614
+ let kept = lines.length;
1615
+ while (kept > 0 && byteLength(render(kept)) > maxBytes) kept -= 1;
1616
+ return kept === 0 ? "" : render(kept);
1617
+ }
1618
+ /** `user:{id}` scope 下的行为记录 key @experimental */
1619
+ const BEHAVIOR_RECORDS_KEY = "behaviorRecords";
1620
+ /** `user:{id}` scope 下的画像 key @experimental */
1621
+ const USER_PROFILE_KEY = "userProfile";
1622
+ const KINDS = [
1623
+ "question",
1624
+ "choice",
1625
+ "text-input",
1626
+ "stated-preference"
1627
+ ];
1628
+ const isKind = (value) => KINDS.includes(value);
1629
+ const stringList = (raw) => Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : void 0;
1630
+ /** 存储里的形状不受控(宿主可能手改文件),逐条筛而不是整体信任 @experimental */
1631
+ function readBehaviorRecords(raw) {
1632
+ if (!Array.isArray(raw)) return [];
1633
+ const out = [];
1634
+ for (const item of raw) {
1635
+ if (typeof item !== "object" || item === null) continue;
1636
+ const record = item;
1637
+ const scene = record.scene;
1638
+ if (typeof record.id !== "string" || !isKind(record.kind)) continue;
1639
+ if (typeof record.at !== "number" || typeof record.value !== "string") continue;
1640
+ if (typeof scene !== "object" || scene === null || typeof scene.sessionId !== "string") continue;
1641
+ const options = stringList(scene.options);
1642
+ out.push({
1643
+ id: record.id,
1644
+ kind: record.kind,
1645
+ at: record.at,
1646
+ value: record.value,
1647
+ ...record.confirmed === true ? { confirmed: true } : {},
1648
+ scene: {
1649
+ sessionId: scene.sessionId,
1650
+ ...typeof scene.skillName === "string" ? { skillName: scene.skillName } : {},
1651
+ ...typeof scene.interactionId === "string" ? { interactionId: scene.interactionId } : {},
1652
+ ...typeof scene.fieldName === "string" ? { fieldName: scene.fieldName } : {},
1653
+ ...typeof scene.fieldSemantics === "string" ? { fieldSemantics: scene.fieldSemantics } : {},
1654
+ ...options && options.length > 0 ? { options } : {}
1655
+ }
1656
+ });
1657
+ }
1658
+ return out;
1659
+ }
1660
+ /**
1661
+ * 追加并按容量淘汰最旧(FR-19.2)。`value` 同时截断:
1662
+ * 一条被粘贴进来的长文本能把整个 JSON 撑爆,而提炼只需要语义。
1663
+ * @experimental
1664
+ */
1665
+ function appendBehaviorRecords(current, incoming, limits = DEFAULT_USER_PROFILE_LIMITS) {
1666
+ if (limits.recordLimit <= 0) return [];
1667
+ const truncated = incoming.map((record) => record.value.length > limits.valueMaxChars ? {
1668
+ ...record,
1669
+ value: record.value.slice(0, limits.valueMaxChars)
1670
+ } : record);
1671
+ const merged = [...current, ...truncated];
1672
+ return merged.slice(Math.max(0, merged.length - limits.recordLimit));
1673
+ }
1674
+ /** @experimental */
1675
+ function readUserProfile(raw) {
1676
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return EMPTY_USER_PROFILE;
1677
+ const value = raw;
1678
+ return {
1679
+ version: 1,
1680
+ entries: readProfileEntries(value.entries),
1681
+ refinedAt: typeof value.refinedAt === "number" ? value.refinedAt : 0
1682
+ };
1683
+ }
1684
+ /** @experimental */
1685
+ function readProfileEntries(raw) {
1686
+ if (!Array.isArray(raw)) return [];
1687
+ const out = [];
1688
+ for (const item of raw) {
1689
+ if (typeof item !== "object" || item === null) continue;
1690
+ const entry = item;
1691
+ if (typeof entry.id !== "string" || entry.id === "") continue;
1692
+ if (typeof entry.text !== "string" || entry.text.trim() === "") continue;
1693
+ out.push({
1694
+ id: entry.id,
1695
+ text: entry.text,
1696
+ confidence: entry.confidence === "high" ? "high" : "low",
1697
+ updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : 0,
1698
+ sourceKinds: (stringList(entry.sourceKinds) ?? []).filter(isKind)
1699
+ });
1700
+ }
1701
+ return out;
1702
+ }
1703
+ /**
1704
+ * 合并提炼结果:同 id 更新,新 id 追加,超上限按 `updatedAt` 淘汰最旧(设计 09 §1.4)。
1705
+ * @experimental
1706
+ */
1707
+ function mergeProfileEntries(current, incoming, options) {
1708
+ const limits = options.limits ?? DEFAULT_USER_PROFILE_LIMITS;
1709
+ const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
1710
+ for (const entry of incoming) byId.set(entry.id, entry);
1711
+ return {
1712
+ version: 1,
1713
+ entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, Math.max(0, limits.entryLimit)),
1714
+ refinedAt: options.now
1715
+ };
1716
+ }
1717
+ /**
1718
+ * 提炼采样(FR-19.3):最近的 + 高频的 + 被用户显式确认过的,去重后截到上限。
1719
+ * 三个维度缺一不可——只取最近会让长期偏好被一次突发淹没,只取高频则学不到新变化。
1720
+ * @experimental
1721
+ */
1722
+ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
1723
+ const limit = Math.max(0, limits.sampleLimit);
1724
+ if (limit === 0 || records.length === 0) return [];
1725
+ if (records.length <= limit) return [...records].sort((a, b) => a.at - b.at);
1726
+ const picked = /* @__PURE__ */ new Map();
1727
+ const take = (record) => {
1728
+ if (picked.size < limit) picked.set(record.id, record);
1729
+ };
1730
+ for (const record of records) if (record.confirmed === true) take(record);
1731
+ const frequency = /* @__PURE__ */ new Map();
1732
+ const bucketOf = (record) => `${record.scene.skillName ?? ""}#${record.scene.fieldName ?? record.kind}`;
1733
+ for (const record of records) frequency.set(bucketOf(record), (frequency.get(bucketOf(record)) ?? 0) + 1);
1734
+ const byFrequency = [...records].sort((a, b) => (frequency.get(bucketOf(b)) ?? 0) - (frequency.get(bucketOf(a)) ?? 0));
1735
+ for (const record of byFrequency.slice(0, Math.ceil(limit / 2))) take(record);
1736
+ for (const record of [...records].reverse()) take(record);
1737
+ return [...picked.values()].sort((a, b) => a.at - b.at);
1738
+ }
1739
+ /**
1740
+ * 无工具模型的系统提示(0.6.0 FR-14.3)。
1741
+ * 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
1742
+ */
1743
+ const PLAIN_CHAT_SYSTEM_PROMPT = "You are a helpful assistant. Answer the user's question directly and concisely. You have no tools available; do not describe or simulate tool calls.";
1364
1744
  /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
1365
1745
  var TraceRecorder = class {
1366
1746
  #runId;
@@ -1570,13 +1950,13 @@ var RunTerminated = class extends Error {
1570
1950
  };
1571
1951
  /** bridge.request 自身异常(非超时):恢复 running 后转为工具错误回喂 */
1572
1952
  var BridgeRequestError = class extends Error {};
1573
- const baseName = (p) => p.split("/").pop() ?? p;
1574
- const toolError = (code, message) => ({
1953
+ const toolError = (code, message, data) => ({
1575
1954
  ok: false,
1576
1955
  content: [],
1577
1956
  error: {
1578
1957
  code,
1579
- message
1958
+ message,
1959
+ ...data !== void 0 ? { data } : {}
1580
1960
  }
1581
1961
  });
1582
1962
  /** 工具参数摘要(JSON 截断 100 字符;trace 与实时事件同一口径,ToolCallCard 展开详情用) */
@@ -1585,6 +1965,16 @@ const summarizeArgs = (args) => {
1585
1965
  return json.length > 100 ? `${json.slice(0, 100)}…` : json;
1586
1966
  };
1587
1967
  /**
1968
+ * file-pick 的回填值带整个文件的 base64,落进 `paramHistory` 会把用户选的文件
1969
+ * 原样长期留在记忆里(体积与隐私都不可接受)。只留可辨识的描述,正文不留。
1970
+ */
1971
+ const redactFileValue = (value) => {
1972
+ if (Array.isArray(value)) return value.map(redactFileValue);
1973
+ if (typeof value !== "object" || value === null) return typeof value === "string" ? "[file]" : value;
1974
+ const { data: _data, ...rest } = value;
1975
+ return rest;
1976
+ };
1977
+ /**
1588
1978
  * 多轮 Agent 循环。
1589
1979
  * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
1590
1980
  * 缺参/确认/提问经 UiBridge 行内 await 暂停恢复;取消/超时/LLM 异常/护栏超限才终止 run。
@@ -1608,7 +1998,8 @@ var AgentLoop = class {
1608
1998
  toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
1609
1999
  toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
1610
2000
  paramHistoryLimit: config.paramHistoryLimit ?? 50,
1611
- formValueLimit: config.formValueLimit ?? 100,
2001
+ toolCallingDisabled: config.toolCallingDisabled ?? false,
2002
+ maxUnknownToolRetries: config.maxUnknownToolRetries ?? 2,
1612
2003
  temperature: config.temperature,
1613
2004
  renderResult: config.renderResult
1614
2005
  };
@@ -1621,7 +2012,7 @@ var AgentLoop = class {
1621
2012
  /**
1622
2013
  * 取消进行中的 run:触发该 run 的 AbortController(与 totalTimeout 硬期限同一通道),
1623
2014
  * run 以 cancelled(RUN_CANCELLED)终止;未找到(已终态或不属于本实例)返回 false。
1624
- * 取消在下一个中断点生效(LLM complete/stream 调用;交互等待不强制中断)。
2015
+ * 取消在下一个中断点生效;LLM 调用与交互等待都监听同一个 signal,因此等表单时也能立即停。
1625
2016
  */
1626
2017
  cancel(runId) {
1627
2018
  const controller = this.#controllers.get(runId);
@@ -1643,7 +2034,7 @@ var AgentLoop = class {
1643
2034
  sessionId: input.sessionId,
1644
2035
  status: "running",
1645
2036
  phase: "route",
1646
- userPrompt: input.userPrompt,
2037
+ userPrompt: promptText(input.userPrompt),
1647
2038
  startedAt,
1648
2039
  activeSkillNames: [],
1649
2040
  artifacts: [],
@@ -1668,6 +2059,7 @@ var AgentLoop = class {
1668
2059
  surfacePatchCount: 0,
1669
2060
  processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
1670
2061
  emittedToolEvents: /* @__PURE__ */ new Set(),
2062
+ unknownToolCalls: 0,
1671
2063
  startMs,
1672
2064
  pausedMs: 0,
1673
2065
  maxTurns: this.#config.maxTurns,
@@ -1691,7 +2083,7 @@ var AgentLoop = class {
1691
2083
  candidates: route.catalog.entries.map((e) => e.name)
1692
2084
  }
1693
2085
  }, state);
1694
- const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
2086
+ const externalSpecs = this.#config.toolCallingDisabled ? [] : (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
1695
2087
  try {
1696
2088
  return await source.listToolSpecs();
1697
2089
  } catch (e) {
@@ -1700,7 +2092,7 @@ var AgentLoop = class {
1700
2092
  }
1701
2093
  }))).flat();
1702
2094
  const externalSystemPrompts = [];
1703
- for (const source of this.#deps.externalTools ?? []) {
2095
+ if (!this.#config.toolCallingDisabled) for (const source of this.#deps.externalTools ?? []) {
1704
2096
  if (source.systemPrompt === void 0) continue;
1705
2097
  try {
1706
2098
  const text = (await source.systemPrompt())?.trim();
@@ -1709,17 +2101,21 @@ var AgentLoop = class {
1709
2101
  trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
1710
2102
  }
1711
2103
  }
2104
+ const systemPrompt = this.#config.toolCallingDisabled ? PLAIN_CHAT_SYSTEM_PROMPT : [route.systemPrompt, ...externalSystemPrompts].join("\n\n");
2105
+ const profileMessage = await this.#userProfileMessage(state);
1712
2106
  state.messages = [
1713
2107
  {
1714
2108
  role: "system",
1715
- content: textParts([route.systemPrompt, ...externalSystemPrompts].join("\n\n"))
2109
+ content: textParts(systemPrompt)
1716
2110
  },
2111
+ ...profileMessage ? [profileMessage] : [],
1717
2112
  ...(input.history ?? []).map((m) => ({ ...m })),
1718
2113
  {
1719
2114
  role: "user",
1720
- content: textParts(input.userPrompt)
2115
+ content: typeof input.userPrompt === "string" ? textParts(input.userPrompt) : [...input.userPrompt]
1721
2116
  }
1722
2117
  ];
2118
+ await this.#recordUserPrompt(state);
1723
2119
  try {
1724
2120
  return await this.#turnLoop(state, 1, externalSpecs);
1725
2121
  } catch (e) {
@@ -1780,7 +2176,7 @@ var AgentLoop = class {
1780
2176
  if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1781
2177
  if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
1782
2178
  const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
1783
- const toolSpecs = [
2179
+ const toolSpecs = this.#config.toolCallingDisabled ? [] : [
1784
2180
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
1785
2181
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
1786
2182
  ...skillToolSpecs
@@ -1849,9 +2245,14 @@ var AgentLoop = class {
1849
2245
  messages.push({
1850
2246
  role: "tool",
1851
2247
  toolCallId: call.id,
1852
- content: textParts(await this.#serializeToolResult(call, result, state))
2248
+ content: await this.#toolResultParts(call, result, state)
1853
2249
  });
1854
- 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
+ }
1855
2256
  }
1856
2257
  }
1857
2258
  } finally {
@@ -1988,6 +2389,7 @@ var AgentLoop = class {
1988
2389
  surfacePatchCount: 0,
1989
2390
  processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
1990
2391
  emittedToolEvents: /* @__PURE__ */ new Set(),
2392
+ unknownToolCalls: 0,
1991
2393
  startMs,
1992
2394
  pausedMs: snapshot.pausedMs ?? 0,
1993
2395
  maxTurns: snapshot.config.maxTurns,
@@ -2024,7 +2426,7 @@ var AgentLoop = class {
2024
2426
  state.messages.push({
2025
2427
  role: "tool",
2026
2428
  toolCallId: pendingCall.id,
2027
- content: textParts(await this.#serializeToolResult(pendingCall, result, state))
2429
+ content: await this.#toolResultParts(pendingCall, result, state)
2028
2430
  });
2029
2431
  await this.#drainSurfaceAction(state);
2030
2432
  } else if (pending?.type === "ask" && pendingCall) {
@@ -2046,7 +2448,7 @@ var AgentLoop = class {
2046
2448
  state.messages.push({
2047
2449
  role: "tool",
2048
2450
  toolCallId: pendingCall.id,
2049
- content: textParts(await this.#serializeToolResult(pendingCall, result, state))
2451
+ content: await this.#toolResultParts(pendingCall, result, state)
2050
2452
  });
2051
2453
  await this.#drainSurfaceAction(state);
2052
2454
  } else if (pendingCall) {
@@ -2054,7 +2456,7 @@ var AgentLoop = class {
2054
2456
  state.messages.push({
2055
2457
  role: "tool",
2056
2458
  toolCallId: pendingCall.id,
2057
- content: textParts(await this.#serializeToolResult(pendingCall, result, state))
2459
+ content: await this.#toolResultParts(pendingCall, result, state)
2058
2460
  });
2059
2461
  await this.#drainSurfaceAction(state);
2060
2462
  }
@@ -2065,7 +2467,7 @@ var AgentLoop = class {
2065
2467
  state.messages.push({
2066
2468
  role: "tool",
2067
2469
  toolCallId: next.id,
2068
- content: textParts(await this.#serializeToolResult(next, result, state))
2470
+ content: await this.#toolResultParts(next, result, state)
2069
2471
  });
2070
2472
  await this.#drainSurfaceAction(state);
2071
2473
  }
@@ -2180,7 +2582,7 @@ var AgentLoop = class {
2180
2582
  } });
2181
2583
  let response;
2182
2584
  try {
2183
- response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs, () => bridge.cancel?.(request.id));
2585
+ response = await this.#withInteractionTimeout(bridge.request(request), this.#policy.interactionTimeoutMs, state, () => bridge.cancel?.(request.id));
2184
2586
  } catch (e) {
2185
2587
  run.status = "running";
2186
2588
  run.interruptExpiresAt = void 0;
@@ -2190,6 +2592,7 @@ var AgentLoop = class {
2190
2592
  message: e.message,
2191
2593
  code: "RUN_INTERACTION_TIMEOUT"
2192
2594
  });
2595
+ if (e instanceof WebSkillError && e.code === "RUN_CANCELLED") throw this.#abortedInteraction(state);
2193
2596
  state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf(e)}` });
2194
2597
  throw new BridgeRequestError(messageOf(e));
2195
2598
  }
@@ -2215,19 +2618,62 @@ var AgentLoop = class {
2215
2618
  await this.#appendParamHistory(state, request, response.value);
2216
2619
  return response.value;
2217
2620
  }
2218
- #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) {
2219
2640
  return new Promise((resolve, reject) => {
2220
- const timer = setTimeout(() => {
2641
+ const signal = state.controller.signal;
2642
+ let settled = false;
2643
+ const abandon = () => {
2221
2644
  try {
2222
- onTimeout?.();
2645
+ onAbandon?.();
2223
2646
  } catch {}
2647
+ };
2648
+ const cleanup = () => {
2649
+ settled = true;
2650
+ clearTimeout(timer);
2651
+ signal.removeEventListener("abort", onAbort);
2652
+ };
2653
+ const onAbort = () => {
2654
+ if (settled) return;
2655
+ cleanup();
2656
+ abandon();
2657
+ reject(new WebSkillError("RUN_CANCELLED", "Interaction aborted because the run was stopped"));
2658
+ };
2659
+ const timer = setTimeout(() => {
2660
+ if (settled) return;
2661
+ cleanup();
2662
+ abandon();
2224
2663
  reject(new WebSkillError("RUN_INTERACTION_TIMEOUT", `Interaction timed out after ${timeoutMs}ms`));
2225
2664
  }, timeoutMs);
2665
+ if (signal.aborted) {
2666
+ onAbort();
2667
+ return;
2668
+ }
2669
+ signal.addEventListener("abort", onAbort);
2226
2670
  promise.then((v) => {
2227
- clearTimeout(timer);
2671
+ if (settled) return;
2672
+ cleanup();
2228
2673
  resolve(v);
2229
2674
  }, (e) => {
2230
- clearTimeout(timer);
2675
+ if (settled) return;
2676
+ cleanup();
2231
2677
  reject(e instanceof Error ? e : new Error(String(e)));
2232
2678
  });
2233
2679
  });
@@ -2247,11 +2693,15 @@ var AgentLoop = class {
2247
2693
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
2248
2694
  else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
2249
2695
  else {
2250
- const resolution = resolveToolName(call.name, state.activated);
2696
+ const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
2251
2697
  if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
2252
2698
  else {
2253
2699
  const source = (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
2254
- result = 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
+ }
2255
2705
  }
2256
2706
  }
2257
2707
  const durationMs = Date.parse(state.now()) - callStartMs;
@@ -2297,6 +2747,12 @@ var AgentLoop = class {
2297
2747
  artifactId: artifact.id,
2298
2748
  path: artifact.path
2299
2749
  } });
2750
+ if (state.unknownToolCalls > this.#config.maxUnknownToolRetries) throw new RunTerminated({
2751
+ status: "failed",
2752
+ reason: "tool-resolution-exhausted",
2753
+ message: "Repeated calls to non-existent tools. Stopping to avoid consuming the remaining turn budget.",
2754
+ code: "TOOL_RESOLUTION_EXHAUSTED"
2755
+ });
2300
2756
  return result;
2301
2757
  }
2302
2758
  /** Attaches trusted run provenance, then records only events accepted by the configured bridge. */
@@ -2410,7 +2866,7 @@ var AgentLoop = class {
2410
2866
  nonce: request.nonce,
2411
2867
  ...resumed ? { resumed: true } : {}
2412
2868
  } });
2413
- const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
2869
+ const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, state, () => bridge.cancelSurfaceAction?.(request.nonce));
2414
2870
  run.status = "running";
2415
2871
  run.interruptExpiresAt = void 0;
2416
2872
  state.trace.record("ui.surface-action.resolved", { data: {
@@ -2437,6 +2893,7 @@ var AgentLoop = class {
2437
2893
  message: e.message,
2438
2894
  code: "RUN_INTERACTION_TIMEOUT"
2439
2895
  });
2896
+ if (e instanceof WebSkillError && e.code === "RUN_CANCELLED") throw this.#abortedInteraction(state);
2440
2897
  if (e instanceof RunTerminated || e instanceof BridgeRequestError) throw e;
2441
2898
  throw new BridgeRequestError(messageOf(e));
2442
2899
  } finally {
@@ -2488,12 +2945,32 @@ var AgentLoop = class {
2488
2945
  async #handleAskUser(call, state) {
2489
2946
  const question = call.arguments["question"];
2490
2947
  if (typeof question !== "string" || question === "") return toolError("TOOL_EXECUTION_FAILED", "ask_user requires a non-empty \"question\" string argument");
2948
+ const rawChoices = call.arguments["choices"];
2949
+ const choices = Array.isArray(rawChoices) ? rawChoices.filter((choice) => typeof choice === "string" && choice !== "") : [];
2950
+ const id = this.#nextInteractionId(state);
2951
+ const rawSuggestion = call.arguments["suggestion"];
2952
+ const rawReason = call.arguments["suggestionReason"];
2953
+ const suggestion = this.#deps.userProfile && typeof rawSuggestion === "string" && rawSuggestion !== "" ? { suggestion: {
2954
+ value: rawSuggestion,
2955
+ ...typeof rawReason === "string" && rawReason !== "" ? { reason: rawReason } : {}
2956
+ } } : {};
2957
+ const request = choices.length > 0 ? {
2958
+ type: "select",
2959
+ id,
2960
+ message: question,
2961
+ options: choices.map((label) => ({
2962
+ label,
2963
+ value: label
2964
+ })),
2965
+ ...suggestion
2966
+ } : {
2967
+ type: "ask",
2968
+ id,
2969
+ message: question,
2970
+ ...suggestion
2971
+ };
2491
2972
  try {
2492
- const value = await this.#interact(state, {
2493
- type: "ask",
2494
- id: this.#nextInteractionId(state),
2495
- message: question
2496
- }, { tool: call.name });
2973
+ const value = await this.#interact(state, request, { tool: call.name });
2497
2974
  return {
2498
2975
  ok: true,
2499
2976
  content: [{
@@ -2579,6 +3056,26 @@ var AgentLoop = class {
2579
3056
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
2580
3057
  }
2581
3058
  /**
3059
+ * 工具结果 → tool 消息内容。image 分片单独提出,不进 JSON 也不参与截断计算(0.6.0 §1.1):
3060
+ * 100 KB 的文本预算遇到一张 200 KB 的图会把整个结果截成垃圾。
3061
+ */
3062
+ async #toolResultParts(call, result, state) {
3063
+ const images = result.content.filter((part) => part.type === "image");
3064
+ if (images.length === 0) return textParts(await this.#serializeToolResult(call, result, state));
3065
+ const textual = {
3066
+ ...result,
3067
+ content: result.content.filter((part) => part.type !== "image")
3068
+ };
3069
+ return [{
3070
+ type: "text",
3071
+ text: await this.#serializeToolResult(call, textual, state)
3072
+ }, ...images.map((image) => ({
3073
+ type: "image",
3074
+ mimeType: image.mimeType,
3075
+ data: image.data
3076
+ }))];
3077
+ }
3078
+ /**
2582
3079
  * 工具结果回喂序列化:超过 toolResultMaxBytes(默认 100KB)时头尾保留截断,
2583
3080
  * 完整内容经 artifactStore 落 artifact,回喂摘要含 artifact id。
2584
3081
  */
@@ -2682,32 +3179,36 @@ var AgentLoop = class {
2682
3179
  }
2683
3180
  const executor = this.#deps.executor;
2684
3181
  const loaded = [];
3182
+ let scripts = [];
2685
3183
  if (executor) {
2686
3184
  let scriptFiles;
2687
3185
  try {
2688
- scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => 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}`);
2689
3187
  } catch (e) {
2690
3188
  scriptFiles = [];
2691
3189
  if (!(e instanceof WebSkillError && e.code === "FS_NOT_FOUND")) state.trace.record("run.warning", { message: `Failed to list scripts of skill "${skillName}": ${messageOf(e)}` });
2692
3190
  }
2693
- 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.` });
3191
+ scripts = listSkillScripts(skillName, scriptFiles, allowedTools);
3192
+ for (const script of scripts) {
3193
+ if (script.toolName === void 0) {
3194
+ state.trace.record("run.warning", { message: `Skill "${skillName}" declares allowed-tools, so its script "${script.scriptName}" is not registered as a tool. Add "${script.scriptName}" to allowed-tools if that was not intended.` });
2698
3195
  continue;
2699
3196
  }
2700
3197
  try {
2701
- const def = await executor.loadDefinition(root, match[1]);
2702
- 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;
2703
3201
  state.activatedTools.set(def.name, def);
2704
3202
  loaded.push(def.name);
2705
3203
  } catch (e) {
2706
- state.trace.record("run.warning", { message: `Failed to load definition of script "${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";
2707
3208
  }
2708
3209
  }
2709
3210
  }
2710
- let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
3211
+ let note = (loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "") + formatSkillScriptManifest(scripts);
2711
3212
  for (const dep of dependencies) {
2712
3213
  if (state.activated.has(dep)) continue;
2713
3214
  if (!this.#deps.skillIndex.has(dep)) {
@@ -2721,53 +3222,86 @@ var AgentLoop = class {
2721
3222
  /**
2722
3223
  * D2 Schema 兜底链(同一 run 内激活时只算一次):
2723
3224
  * 显式 inputSchema > sidecar scripts/<name>.schema.json > schemaInferer 推导 > schemaUnavailable
3225
+ * 返回值即最终来源,进技能清单告诉模型该信谁(FR-18.1)。
2724
3226
  */
2725
3227
  async #enrichDefinition(skillRoot, scriptName, def, state) {
2726
- if (def.inputSchema) return;
3228
+ if (def.inputSchema) return "module-export";
2727
3229
  const sidecar = `${skillRoot}/scripts/${scriptName}.schema.json`;
2728
3230
  if (await this.#deps.fs.exists(sidecar)) try {
2729
3231
  def.inputSchema = JSON.parse(await this.#deps.fs.readText(sidecar));
2730
- return;
3232
+ return "sidecar";
2731
3233
  } catch (e) {
2732
3234
  state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf(e)}` });
2733
3235
  }
2734
- if (!this.#deps.schemaInferer) return;
3236
+ if (!this.#deps.schemaInferer) return "unavailable";
2735
3237
  for (const ext of ["ts", "js"]) {
2736
3238
  const scriptPath = `${skillRoot}/scripts/${scriptName}.${ext}`;
2737
3239
  if (!await this.#deps.fs.exists(scriptPath)) continue;
2738
3240
  try {
2739
3241
  const inferred = this.#deps.schemaInferer.inferSchemaFromSource(await this.#deps.fs.readText(scriptPath), { fileName: `${scriptName}.${ext}` });
2740
- if (inferred) def.inputSchema = inferred;
3242
+ if (inferred) {
3243
+ def.inputSchema = inferred;
3244
+ return "inferred";
3245
+ }
2741
3246
  } catch (e) {
2742
3247
  state.trace.record("run.warning", { message: `Schema inference failed for ${scriptPath}: ${messageOf(e)}` });
2743
3248
  }
2744
- return;
3249
+ return "unavailable";
2745
3250
  }
3251
+ return "unavailable";
2746
3252
  }
2747
3253
  async #handleScriptTool(call, skillName, scriptName, state) {
2748
3254
  const root = this.#deps.skillIndex.get(skillName);
2749
3255
  if (!root) return toolError("TOOL_NOT_FOUND", `Skill "${skillName}" is not in the catalog`);
2750
3256
  if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
2751
3257
  const def = state.activatedTools.get(call.name);
2752
- if (!def) 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
+ }
2753
3267
  let args = call.arguments;
2754
3268
  const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
2755
3269
  if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
2756
- const fields = schemaToForm(def.inputSchema, args, { 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
- };
3270
+ const fields = schemaToForm(def.inputSchema, args);
3271
+ const declined = /* @__PURE__ */ new Set();
3272
+ for (const field of fields) {
3273
+ if (field.type !== "file" || !missing.includes(field.name)) continue;
3274
+ const value = await this.#interact(state, {
3275
+ type: "file-pick",
3276
+ id: this.#nextInteractionId(state),
3277
+ message: `Open local file picker to fill field "${field.name}"?`,
3278
+ field: field.name
3279
+ }, {
3280
+ tool: call.name,
3281
+ missing: [field.name]
3282
+ });
3283
+ if (value === void 0 || value === null) declined.add(field.name);
3284
+ else args = {
3285
+ ...args,
3286
+ [field.name]: value
3287
+ };
3288
+ }
3289
+ const stillMissing = missing.filter((key) => args[key] === void 0 && !declined.has(key));
3290
+ if (stillMissing.length > 0) {
3291
+ const value = await this.#interact(state, {
3292
+ type: "form",
3293
+ id: this.#nextInteractionId(state),
3294
+ title: `Missing parameters for ${call.name}`,
3295
+ fields: fields.filter((f) => f.type !== "file" && !declined.has(f.name))
3296
+ }, {
3297
+ tool: call.name,
3298
+ missing: stillMissing
3299
+ });
3300
+ if (typeof value === "object" && value !== null) args = {
3301
+ ...args,
3302
+ ...value
3303
+ };
3304
+ }
2771
3305
  } catch (e) {
2772
3306
  if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `Parameter form failed: ${e.message}`);
2773
3307
  throw e;
@@ -2921,57 +3455,111 @@ var AgentLoop = class {
2921
3455
  if (!this.#deps.memory) return;
2922
3456
  const scope = `session:${state.run.sessionId}`;
2923
3457
  const limit = this.#config.paramHistoryLimit;
3458
+ const recorded = request.type === "file-pick" ? redactFileValue(value) : value;
2924
3459
  await this.#memoryMutate(scope, "paramHistory", state, (current) => {
2925
3460
  const history = current ?? [];
2926
3461
  history.push({
2927
3462
  ts: state.now(),
2928
3463
  interactionId: request.id,
2929
3464
  type: request.type,
2930
- value
3465
+ value: recorded
2931
3466
  });
2932
3467
  return history.slice(-limit);
2933
3468
  });
2934
- await this.#rememberFormValues(state, request, value);
2935
- }
2936
- /**
2937
- * 跨会话字段值的写入(FR-5.6/5.12)。与 `paramHistory` 双写而不是合并:
2938
- * 那边是按时间的追加序列(运行观测),这边是按字段的最新值(召回),形态不同。
2939
- */
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));
3469
+ await this.#recordInteractionBehavior(state, request, value);
3470
+ }
3471
+ /** 用户提问 → 行为记录(FR-19.2)。记的是「问了什么」,不是模型答了什么 */
3472
+ async #recordUserPrompt(state) {
3473
+ const prompt = state.run.userPrompt.trim();
3474
+ if (prompt === "") return;
3475
+ await this.#appendBehaviorRecords(state, [{
3476
+ id: `${state.runId}#prompt`,
3477
+ kind: "question",
3478
+ at: Date.parse(state.now()),
3479
+ value: prompt,
3480
+ scene: { sessionId: state.run.sessionId }
3481
+ }]);
2958
3482
  }
2959
3483
  /**
2960
- * 召回(FR-5.8):命中的历史值挂在 `suggestion` 上,**绝不写进 `defaultValue`**——
2961
- * 后者会被渲染器直接填进控件,等于静默预填(AC-5.6 禁止)。
3484
+ * 交互提交 → 行为记录(FR-19.2)。写在这里而不是 UiBridge:
3485
+ * 只有 AgentLoop 同时握有场景(技能、字段语义、候选集)与提交值。
2962
3486
  */
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
- };
3487
+ async #recordInteractionBehavior(state, request, value) {
3488
+ const records = [];
3489
+ const at = Date.parse(state.now());
3490
+ const base = {
3491
+ sessionId: state.run.sessionId,
3492
+ interactionId: request.id
3493
+ };
3494
+ const skill = [...state.activated].at(-1);
3495
+ const scene = skill !== void 0 ? {
3496
+ ...base,
3497
+ skillName: skill
3498
+ } : base;
3499
+ if (request.type === "select") records.push({
3500
+ id: `${request.id}#selected`,
3501
+ kind: "choice",
3502
+ at,
3503
+ confirmed: true,
3504
+ value: String(value),
3505
+ scene: {
3506
+ ...scene,
3507
+ fieldSemantics: request.message,
3508
+ options: request.options.map((o) => String(o.label))
3509
+ }
3510
+ });
3511
+ else if (request.type === "ask" && typeof value === "string" && value !== "") records.push({
3512
+ id: `${request.id}#answer`,
3513
+ kind: "text-input",
3514
+ at,
3515
+ confirmed: true,
3516
+ value,
3517
+ scene: {
3518
+ ...scene,
3519
+ fieldSemantics: request.message
3520
+ }
3521
+ });
3522
+ else if (request.type === "form" && typeof value === "object" && value !== null) {
3523
+ const submitted = value;
3524
+ for (const field of request.fields) {
3525
+ const next = submitted[field.name];
3526
+ if (typeof next !== "string" && typeof next !== "number" && typeof next !== "boolean") continue;
3527
+ if (next === "") continue;
3528
+ records.push({
3529
+ id: `${request.id}#${field.name}`,
3530
+ kind: field.options ? "choice" : "text-input",
3531
+ at,
3532
+ confirmed: true,
3533
+ value: String(next),
3534
+ scene: {
3535
+ ...scene,
3536
+ fieldName: field.name,
3537
+ fieldSemantics: field.description ?? field.label,
3538
+ ...field.options ? { options: field.options.map((o) => String(o.label)) } : {}
3539
+ }
3540
+ });
3541
+ }
2974
3542
  }
3543
+ await this.#appendBehaviorRecords(state, records);
3544
+ }
3545
+ /** 行为记录追加(FR-19.2)。默认关闭:未注入 `userProfile` 时零持久化写入 */
3546
+ async #appendBehaviorRecords(state, records) {
3547
+ const profile = this.#deps.userProfile;
3548
+ if (!profile || !this.#deps.memory || records.length === 0) return;
3549
+ const limits = profile.limits ?? DEFAULT_USER_PROFILE_LIMITS;
3550
+ await this.#memoryMutate(`user:${profile.userId}`, BEHAVIOR_RECORDS_KEY, state, (current) => appendBehaviorRecords(readBehaviorRecords(current), records, limits));
3551
+ }
3552
+ /** 画像 → 系统消息(FR-19.4)。读不到或为空时不注入任何内容 */
3553
+ async #userProfileMessage(state) {
3554
+ const profile = this.#deps.userProfile;
3555
+ if (!profile || !this.#deps.memory) return void 0;
3556
+ const limits = profile.limits ?? DEFAULT_USER_PROFILE_LIMITS;
3557
+ const context = renderUserProfileContext(readUserProfile(await this.#memoryGet(`user:${profile.userId}`, USER_PROFILE_KEY, state)), limits.injectMaxBytes);
3558
+ if (context === "") return void 0;
3559
+ return {
3560
+ role: "system",
3561
+ content: textParts(`${context}\n\n${USER_PROFILE_NO_INVENTION_RULE}`)
3562
+ };
2975
3563
  }
2976
3564
  };
2977
3565
  /**
@@ -3083,6 +3671,7 @@ var WebSkillRuntime = class {
3083
3671
  return result;
3084
3672
  }
3085
3673
  async run(userPrompt, options = {}) {
3674
+ if (options.skillName !== void 0 && this.#deps.config?.toolCallingDisabled === true) throw new WebSkillError("MODEL_TOOLS_UNSUPPORTED", "The selected model does not support skill invocation. Switch to a model with tool calling.");
3086
3675
  if (!this.#catalogCache) await this.discover();
3087
3676
  const cache = this.#catalogCache;
3088
3677
  if (!cache) throw new Error("discover() did not populate the catalog cache");
@@ -3097,7 +3686,9 @@ var WebSkillRuntime = class {
3097
3686
  }))).flat();
3098
3687
  const catalog = providerEntries.length > 0 ? { entries: mergeCatalogEntries(cache.catalog.entries, providerEntries) } : cache.catalog;
3099
3688
  const filteredCatalog = this.#deps.catalogFilter ? { entries: await this.#deps.catalogFilter(catalog.entries) } : catalog;
3100
- const 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);
3101
3692
  const loop = new AgentLoop({
3102
3693
  llm: this.#deps.llm,
3103
3694
  executor: this.#deps.executor,
@@ -3112,7 +3703,7 @@ var WebSkillRuntime = class {
3112
3703
  hooks: this.#deps.hooks,
3113
3704
  eventBus: this.#events,
3114
3705
  longTerm: this.#deps.longTerm,
3115
- formAutofill: this.#deps.formAutofill,
3706
+ userProfile: this.#deps.userProfile,
3116
3707
  externalTools: this.#deps.externalTools,
3117
3708
  skillProviders: this.#deps.skillProviders,
3118
3709
  catalogFilter: this.#deps.catalogFilter,
@@ -3143,7 +3734,7 @@ var WebSkillRuntime = class {
3143
3734
  message: `Skill provider listSkills() failed: ${failure}`
3144
3735
  });
3145
3736
  if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
3146
- prompt: userPrompt,
3737
+ prompt: promptText(userPrompt),
3147
3738
  run: result.run
3148
3739
  }).catch((e) => {
3149
3740
  result.run.trace.push({
@@ -3219,7 +3810,7 @@ var WebSkillRuntime = class {
3219
3810
  hooks: this.#deps.hooks,
3220
3811
  eventBus: this.#events,
3221
3812
  longTerm: this.#deps.longTerm,
3222
- formAutofill: this.#deps.formAutofill,
3813
+ userProfile: this.#deps.userProfile,
3223
3814
  externalTools: this.#deps.externalTools,
3224
3815
  skillProviders: this.#deps.skillProviders,
3225
3816
  catalogFilter: this.#deps.catalogFilter,
@@ -3474,6 +4065,191 @@ var FsMemoryStore = class {
3474
4065
  for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
3475
4066
  }
3476
4067
  };
4068
+ /**
4069
+ * 提炼提示词。要求返回结论列表而不是散文:条目化是「可见、可编辑、可删除」的前提(FR-19.3)。
4070
+ * `id` 由模型给出稳定 slug,才能在下一次提炼时更新同一条而不是无限追加。
4071
+ */
4072
+ const USER_PROFILE_REFINE_PROMPT = `You summarize what a single user is like, based on records of what they did in a chat application.
4073
+
4074
+ Return ONLY a JSON object with this shape, and nothing else:
4075
+
4076
+ {
4077
+ "entries": [
4078
+ { "id": "slug-case-stable-id", "text": "One sentence about the user.", "confidence": "high" | "low" }
4079
+ ]
4080
+ }
4081
+
4082
+ Rules:
4083
+ - Each entry is one durable, reusable fact or preference about the user — how they like answers, what they work on, values they keep entering, languages they use.
4084
+ - Reuse the same "id" across runs for the same conclusion so it can be updated instead of duplicated. Use lowercase words joined by hyphens.
4085
+ - Use "high" only when several records agree. A single record is "low".
4086
+ - Do not restate individual records. Do not include secrets, credentials, or full personal identifiers.
4087
+ - Do not invent anything the records do not support. If the records say nothing useful, return {"entries": []}.`;
4088
+ /** 采样后的记录 → 送模型的摘要。导出供测试与宿主复核实际请求内容(AC-19.6) @experimental */
4089
+ function toRecordDigests(records) {
4090
+ return records.map((record) => ({
4091
+ kind: record.kind,
4092
+ at: new Date(record.at).toISOString(),
4093
+ value: record.value,
4094
+ ...record.scene.skillName !== void 0 ? { skill: record.scene.skillName } : {},
4095
+ ...record.scene.fieldName !== void 0 ? { field: record.scene.fieldName } : {},
4096
+ ...record.scene.fieldSemantics !== void 0 ? { meaning: record.scene.fieldSemantics } : {},
4097
+ ...record.scene.options !== void 0 ? { options: record.scene.options } : {},
4098
+ ...record.confirmed === true ? { confirmed: true } : {}
4099
+ }));
4100
+ }
4101
+ /** 模型可能把 JSON 包在 ```json 里;只取第一个对象,解析不出就当没结论 */
4102
+ function parseEntries(content, now) {
4103
+ const text = promptText(content ?? []);
4104
+ const start = text.indexOf("{");
4105
+ const end = text.lastIndexOf("}");
4106
+ if (start < 0 || end <= start) return [];
4107
+ let parsed;
4108
+ try {
4109
+ parsed = JSON.parse(text.slice(start, end + 1));
4110
+ } catch {
4111
+ return [];
4112
+ }
4113
+ const raw = parsed?.entries;
4114
+ return readProfileEntries(Array.isArray(raw) ? raw.map((item) => ({
4115
+ updatedAt: now,
4116
+ ...item
4117
+ })) : []);
4118
+ }
4119
+ /**
4120
+ * 一次提炼(FR-19.3)。调用方负责持久化返回值;本函数不碰存储,
4121
+ * 也不吞异常——「失败不影响会话结束」由调用点的 fire-and-forget 保证,
4122
+ * 在这里吞掉会让手动提炼入口无法报错。
4123
+ * @experimental
4124
+ */
4125
+ async function refineUserProfile(input) {
4126
+ const limits = input.limits ?? DEFAULT_USER_PROFILE_LIMITS;
4127
+ const now = (input.now ?? Date.now)();
4128
+ const current = input.profile ?? EMPTY_USER_PROFILE;
4129
+ const sampled = sampleBehaviorRecords(input.records, limits);
4130
+ if (sampled.length === 0) return current;
4131
+ const existing = current.entries.map((entry) => ({
4132
+ id: entry.id,
4133
+ text: entry.text
4134
+ }));
4135
+ const entries = parseEntries((await input.llm.complete({
4136
+ ...input.model !== void 0 ? { model: input.model } : {},
4137
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
4138
+ messages: [{
4139
+ role: "system",
4140
+ content: [{
4141
+ type: "text",
4142
+ text: USER_PROFILE_REFINE_PROMPT
4143
+ }]
4144
+ }, {
4145
+ role: "user",
4146
+ content: [{
4147
+ type: "text",
4148
+ text: `Existing conclusions:\n${JSON.stringify(existing)}\n\nRecords:\n${JSON.stringify(toRecordDigests(sampled))}`
4149
+ }]
4150
+ }]
4151
+ })).content, now);
4152
+ if (entries.length === 0) return current;
4153
+ return mergeProfileEntries(current, entries, {
4154
+ now,
4155
+ limits
4156
+ });
4157
+ }
4158
+ /** 导出文件的版本;导入时逐字校验,不做向前兼容猜测 @experimental */
4159
+ const USER_PROFILE_EXPORT_VERSION = 1;
4160
+ /** 与偏好导出同一套判据:归一化后按子串匹配,命中即拒 */
4161
+ const CREDENTIAL_MARKERS = [
4162
+ "apikey",
4163
+ "secret",
4164
+ "token",
4165
+ "password",
4166
+ "passphrase",
4167
+ "credential",
4168
+ "privatekey"
4169
+ ];
4170
+ const isCredentialKey = (key) => {
4171
+ const normalized = key.toLowerCase().replace(/[-_\s]/g, "");
4172
+ return CREDENTIAL_MARKERS.some((marker) => normalized.includes(marker));
4173
+ };
4174
+ function findCredentialKey(value) {
4175
+ if (Array.isArray(value)) {
4176
+ for (const item of value) {
4177
+ const hit = findCredentialKey(item);
4178
+ if (hit !== void 0) return hit;
4179
+ }
4180
+ return;
4181
+ }
4182
+ if (typeof value !== "object" || value === null) return void 0;
4183
+ for (const [key, child] of Object.entries(value)) {
4184
+ if (isCredentialKey(key)) return key;
4185
+ const hit = findCredentialKey(child);
4186
+ if (hit !== void 0) return hit;
4187
+ }
4188
+ }
4189
+ /**
4190
+ * 导出画像(FR-19.7)。`excludeIds` 来自导出前的审阅界面:用户勾掉的条目不进文件。
4191
+ * @experimental
4192
+ */
4193
+ function exportUserProfile(profile, options) {
4194
+ const excluded = new Set(options.excludeIds ?? []);
4195
+ const result = {
4196
+ kind: "webskill.user-profile",
4197
+ version: 1,
4198
+ origin: options.origin,
4199
+ exportedAt: (options.now ?? Date.now)(),
4200
+ entries: profile.entries.filter((entry) => !excluded.has(entry.id))
4201
+ };
4202
+ const leaked = findCredentialKey(result);
4203
+ if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `A user profile export must not contain credentials, but "${leaked}" was present`);
4204
+ return result;
4205
+ }
4206
+ /** 解析并校验导入内容(FR-19.7)。含凭据是**拒绝**而不是忽略 @experimental */
4207
+ function parseUserProfileExport(raw) {
4208
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must be a JSON object");
4209
+ const record = raw;
4210
+ if (record["kind"] !== "webskill.user-profile") throw new WebSkillError("PROFILE_IMPORT_INVALID", "A user profile file must have \"kind\": \"webskill.user-profile\"");
4211
+ if (record["version"] !== 1) throw new WebSkillError("PROFILE_IMPORT_VERSION_UNSUPPORTED", `Unsupported user profile file version ${JSON.stringify(record["version"])}; expected 1`);
4212
+ const leaked = findCredentialKey(record);
4213
+ if (leaked !== void 0) throw new WebSkillError("PROFILE_IMPORT_CREDENTIAL_REJECTED", `The user profile file contains a credential field "${leaked}"; the import was rejected`);
4214
+ const entries = readProfileEntries(record["entries"]);
4215
+ if (entries.length === 0) throw new WebSkillError("PROFILE_IMPORT_INVALID", "The user profile file has no usable entries");
4216
+ return {
4217
+ kind: "webskill.user-profile",
4218
+ version: 1,
4219
+ origin: typeof record["origin"] === "string" ? record["origin"] : "unknown",
4220
+ exportedAt: typeof record["exportedAt"] === "number" ? record["exportedAt"] : 0,
4221
+ entries
4222
+ };
4223
+ }
4224
+ /** 导入前给用户看的差异(FR-19.7):新增哪些、覆盖哪些 @experimental */
4225
+ function diffUserProfile(current, incoming) {
4226
+ const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
4227
+ const added = [];
4228
+ const updated = [];
4229
+ for (const entry of incoming.entries) {
4230
+ const existing = byId.get(entry.id);
4231
+ if (existing === void 0) added.push(entry);
4232
+ else if (existing.text !== entry.text) updated.push(entry);
4233
+ }
4234
+ return {
4235
+ added,
4236
+ updated
4237
+ };
4238
+ }
4239
+ /** 确认后落库:同 id 覆盖,其余保留;不触碰模型配置与凭据 @experimental */
4240
+ function applyUserProfileImport(current, incoming, options = {}) {
4241
+ const now = (options.now ?? Date.now)();
4242
+ const byId = new Map(current.entries.map((entry) => [entry.id, entry]));
4243
+ for (const entry of incoming.entries) byId.set(entry.id, {
4244
+ ...entry,
4245
+ updatedAt: now
4246
+ });
4247
+ return {
4248
+ version: 1,
4249
+ entries: [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, DEFAULT_USER_PROFILE_LIMITS.entryLimit),
4250
+ refinedAt: current.refinedAt
4251
+ };
4252
+ }
3477
4253
  const INDEX_FILE$1 = "index.json";
3478
4254
  /**
3479
4255
  * 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
@@ -3825,12 +4601,16 @@ var FsRunTraceStore = class {
3825
4601
  #root;
3826
4602
  #fs;
3827
4603
  #onError;
4604
+ #onIndexError;
3828
4605
  constructor(deps) {
3829
4606
  this.#root = deps.root.replace(/\/+$/, "");
3830
4607
  this.#fs = deps.fs;
3831
4608
  this.#onError = deps.onError ?? ((error, run) => {
3832
4609
  console.warn(`Failed to persist run trace "${run.id}": ${messageOf(error)}`);
3833
4610
  });
4611
+ this.#onIndexError = deps.onIndexError ?? ((error, run) => {
4612
+ console.warn(`Run trace "${run.id}" was saved but its index entry could not be appended: ${messageOf(error)}. The index will be rebuilt on the next read.`);
4613
+ });
3834
4614
  }
3835
4615
  #path(runId) {
3836
4616
  return resolveInsideRoot(this.#root, `${runId}.json`);
@@ -3848,9 +4628,14 @@ var FsRunTraceStore = class {
3848
4628
  };
3849
4629
  try {
3850
4630
  await this.#fs.writeText(this.#path(run.id), JSON.stringify(trace, null, 2));
3851
- await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
3852
4631
  } catch (e) {
3853
4632
  this.#onError(e, run);
4633
+ return;
4634
+ }
4635
+ try {
4636
+ await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
4637
+ } catch (e) {
4638
+ this.#onIndexError(e, run);
3854
4639
  }
3855
4640
  }
3856
4641
  async get(runId) {
@@ -4001,6 +4786,7 @@ const toMeta = (record) => ({
4001
4786
  messageCount: record.messages.length
4002
4787
  });
4003
4788
  const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4789
+ const baseName = (path) => path.split("/").pop() ?? path;
4004
4790
  /**
4005
4791
  * 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
4006
4792
  * 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
@@ -4094,8 +4880,13 @@ var FsSessionStore = class {
4094
4880
  return record;
4095
4881
  }
4096
4882
  async list(options = {}) {
4097
- if (!await this.#fs.exists(this.#root)) return { items: [] };
4883
+ if (!await this.#fs.exists(this.#root)) return {
4884
+ items: [],
4885
+ source: "missing-root",
4886
+ skipped: []
4887
+ };
4098
4888
  const metas = [];
4889
+ const skipped = [];
4099
4890
  for (const entry of await this.#fs.list(this.#root)) {
4100
4891
  if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
4101
4892
  let record;
@@ -4103,13 +4894,18 @@ var FsSessionStore = class {
4103
4894
  record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
4104
4895
  } catch (e) {
4105
4896
  console.warn(`Skipping unreadable session file "${entry.path}": ${e instanceof Error ? e.message : String(e)}`);
4897
+ skipped.push(baseName(entry.path));
4106
4898
  continue;
4107
4899
  }
4108
4900
  if (record.archived === true && options.includeArchived !== true) continue;
4109
4901
  metas.push(toMeta(record));
4110
4902
  }
4111
4903
  metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
4112
- return takeTailPage(metas, options, "session");
4904
+ return {
4905
+ ...takeTailPage(metas, options, "session"),
4906
+ source: "ok",
4907
+ skipped
4908
+ };
4113
4909
  }
4114
4910
  /**
4115
4911
  * 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
@@ -4176,4 +4972,4 @@ var FsSessionStore = class {
4176
4972
  };
4177
4973
 
4178
4974
  //#endregion
4179
- export { 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 };
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 };