@webskill/sdk 0.8.0 → 0.10.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 +6 -2
  3. package/dist/browser.d.ts +5 -3
  4. package/dist/browser.js +35 -13
  5. package/dist/{catalogComponents-DfxxfUvn-D55Gbb2l.js → catalogComponents-DTcYfpLQ-CcoOaz-Z.js} +1946 -976
  6. package/dist/{dist-CJqQsIm9.js → dist-1OFC-zax.js} +7 -5
  7. package/dist/{dist-DmI5SBBF.js → dist-BViUeszk.js} +393 -44
  8. package/dist/{dist-59XlqDuv.js → dist-Bev6i6Ip.js} +10 -2
  9. package/dist/{eventTypes-g1BXL6x5-CibcOftR.js → eventTypes-s2uwAcLG-Go3l_dUe.js} +13 -2
  10. package/dist/governance.d.ts +37 -6
  11. package/dist/governance.js +53 -15
  12. package/dist/{index-P9J2LTfU.d.ts → index-B0QPLWPZ.d.ts} +6 -2
  13. package/dist/{index-fLskQfAS.d.ts → index-BF4E1a9j.d.ts} +10 -3
  14. package/dist/{index-K-eewlGL.d.ts → index-DtFdMKBX.d.ts} +69 -7
  15. package/dist/index.d.ts +11 -3
  16. package/dist/index.js +13 -4
  17. package/dist/mcp.d.ts +23 -3
  18. package/dist/mcp.js +70 -25
  19. package/dist/{memoryArtifactStore-52Zn9npI-upv5OWYf.js → memoryArtifactStore-52Zn9npI-LbCQaqyx.js} +1 -1
  20. package/dist/node.d.ts +3 -3
  21. package/dist/node.js +6 -6
  22. package/dist/{openUiLibrary-DURlAxjk-CU6AzfSW.js → openUiLibrary-CIrV--Ad-B8zG_91e.js} +3 -3
  23. package/dist/{skillVersionStore-Bl-ElD45-gRfSaAby.d.ts → skillVersionStore-D-qHk9ZE-DBsYYCWn.d.ts} +13 -5
  24. package/dist/{testing-BCUO5gZR.js → testing-WPTyXQYt.js} +28 -5
  25. package/dist/testing.d.ts +1 -1
  26. package/dist/testing.js +2 -2
  27. package/dist/{types-B3n0cMZu-BdcqQ35O.d.ts → types-CrRcT-LM-DZAp8sWv.d.ts} +44 -3
  28. package/dist/ui-react.d.ts +17 -2
  29. package/dist/ui-react.js +123 -39
  30. package/dist/ui-vue.d.ts +1 -1
  31. package/dist/ui-vue.js +1 -1
  32. package/dist/ui.d.ts +3 -3
  33. package/dist/ui.js +2 -2
  34. package/dist/{webskillLitCatalog-DwTwSBFt-DiXXpNZA.js → webskillLitCatalog-BJrphK0y-SGmRXaaO.js} +2 -2
  35. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
- import { A as messageOf, I as renderAvailableSkillsXml, M as parseSkillMarkdown, W as validateSkills, f as SkillDiscovery, g as assertSafePathSegment, m as WebSkillError, p as SkillReader, v as buildCatalog, z as resolveInsideRoot } from "./dist-59XlqDuv.js";
2
- import { a as textParts, i as rejectUnsupportedPart, n as partsToText, o as validateLlmMessages, r as promptText, t as MemoryArtifactStore } from "./memoryArtifactStore-52Zn9npI-upv5OWYf.js";
1
+ import { K as validateSkills, M as messageOf, P as parseSkillMarkdown, R as renderAvailableSkillsXml, V as resolveInsideRoot, _ as assertSafePathSegment, h as WebSkillError, m as SkillReader, p as SkillDiscovery, v as atomicWriteText, y as buildCatalog } from "./dist-Bev6i6Ip.js";
2
+ import { a as textParts, i as rejectUnsupportedPart, n as partsToText, o as validateLlmMessages, r as promptText, t as MemoryArtifactStore } from "./memoryArtifactStore-52Zn9npI-LbCQaqyx.js";
3
3
 
4
4
  //#region ../runtime/dist/index.js
5
5
  /** 与正常工具结果同构:模型读到的是「这次调用被中断了」,而不是一个凭空消失的调用 */
@@ -103,11 +103,11 @@ const SCHEMA_KEYS = /* @__PURE__ */ new Set([
103
103
  "then",
104
104
  "else"
105
105
  ]);
106
- function isRecord$6(value) {
106
+ function isRecord$7(value) {
107
107
  return typeof value === "object" && value !== null && !Array.isArray(value);
108
108
  }
109
109
  function isSelfRef(value) {
110
- return isRecord$6(value) && value["$ref"] === SELF_REF;
110
+ return isRecord$7(value) && value["$ref"] === SELF_REF;
111
111
  }
112
112
  /**
113
113
  * 按 JSON Schema 关键字下降到子 schema。
@@ -117,7 +117,7 @@ function isSelfRef(value) {
117
117
  function forEachSubSchema(node, visit) {
118
118
  for (const [key, value] of Object.entries(node)) {
119
119
  if (SCHEMA_MAP_KEYS.has(key)) {
120
- if (isRecord$6(value)) for (const name of Object.keys(value)) visit(value[name], [key, name]);
120
+ if (isRecord$7(value)) for (const name of Object.keys(value)) visit(value[name], [key, name]);
121
121
  continue;
122
122
  }
123
123
  if (SCHEMA_LIST_KEYS.has(key)) {
@@ -136,7 +136,7 @@ function pathKey(path) {
136
136
  * `#` 解析到最近的带 `$id` 的祖先;没有则是文档根。
137
137
  */
138
138
  function collectSelfRefRoots(node, path, resourceRoot, out) {
139
- if (!isRecord$6(node)) return;
139
+ if (!isRecord$7(node)) return;
140
140
  if (isSelfRef(node)) {
141
141
  out.set(pathKey(resourceRoot), resourceRoot);
142
142
  return;
@@ -149,7 +149,7 @@ function collectSelfRefRoots(node, path, resourceRoot, out) {
149
149
  function getAt(schema, path) {
150
150
  let current = schema;
151
151
  for (const segment of path) if (Array.isArray(current) && typeof segment === "number") current = current[segment];
152
- else if (isRecord$6(current) && typeof segment === "string") current = current[segment];
152
+ else if (isRecord$7(current) && typeof segment === "string") current = current[segment];
153
153
  else return void 0;
154
154
  return current;
155
155
  }
@@ -175,7 +175,7 @@ function setAt(schema, path, replacement) {
175
175
  * 正是严格解析器无法解析 `#` 的直接原因。
176
176
  */
177
177
  function rewriteSelfRefs(node, target, depth) {
178
- if (!isRecord$6(node)) return node;
178
+ if (!isRecord$7(node)) return node;
179
179
  if (depth > 8) throw new WebSkillError("TOOL_SCHEMA_UNAVAILABLE", `Tool schema nesting exceeds 8 levels; cannot produce a portable form`);
180
180
  if (isSelfRef(node)) {
181
181
  const rest = {};
@@ -188,7 +188,7 @@ function rewriteSelfRefs(node, target, depth) {
188
188
  const result = {};
189
189
  for (const [key, value] of Object.entries(node)) {
190
190
  if (key === "$id" || key === "$schema") continue;
191
- if (SCHEMA_MAP_KEYS.has(key) && isRecord$6(value)) {
191
+ if (SCHEMA_MAP_KEYS.has(key) && isRecord$7(value)) {
192
192
  const mapped = {};
193
193
  for (const [name, child] of Object.entries(value)) mapped[name] = rewriteSelfRefs(child, target, depth + 1);
194
194
  result[key] = mapped;
@@ -226,8 +226,8 @@ function toPortableToolSchema(schema) {
226
226
  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");
227
227
  const rootPath = [...roots.values()][0] ?? [];
228
228
  const resource = getAt(schema, rootPath);
229
- if (!isRecord$6(resource)) return schema;
230
- const existingDefs = isRecord$6(schema[DEFS_KEY]) ? schema[DEFS_KEY] : void 0;
229
+ if (!isRecord$7(resource)) return schema;
230
+ const existingDefs = isRecord$7(schema[DEFS_KEY]) ? schema[DEFS_KEY] : void 0;
231
231
  const taken = new Set(Object.keys(existingDefs ?? {}));
232
232
  const nodeName = uniqueName(NODE_NAME, taken);
233
233
  taken.add(nodeName);
@@ -258,6 +258,63 @@ function toPortableToolSchema(schema) {
258
258
  }
259
259
  };
260
260
  }
261
+ function isRecord$6(value) {
262
+ return typeof value === "object" && value !== null && !Array.isArray(value);
263
+ }
264
+ /**
265
+ * 与 `interaction/schemaToForm` 的 file 判定保持一致:JSON Schema 里二进制内容的
266
+ * 两种标准写法(`format: binary` / `contentEncoding: base64`)。
267
+ */
268
+ function isModelUnfillable(node) {
269
+ return isRecord$6(node) && node["type"] === "string" && (node["format"] === "binary" || node["contentEncoding"] === "base64");
270
+ }
271
+ function stripNode(node) {
272
+ if (Array.isArray(node)) return node.map(stripNode);
273
+ if (!isRecord$6(node)) return node;
274
+ const out = {};
275
+ for (const [key, value] of Object.entries(node)) {
276
+ if (SCHEMA_MAP_KEYS.has(key) && isRecord$6(value)) {
277
+ const mapped = {};
278
+ for (const [name, child] of Object.entries(value)) {
279
+ if (key === "properties" && isModelUnfillable(child)) continue;
280
+ mapped[name] = stripNode(child);
281
+ }
282
+ out[key] = mapped;
283
+ continue;
284
+ }
285
+ if (SCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
286
+ out[key] = value.map(stripNode);
287
+ continue;
288
+ }
289
+ if (SCHEMA_KEYS.has(key)) {
290
+ out[key] = stripNode(value);
291
+ continue;
292
+ }
293
+ out[key] = value;
294
+ }
295
+ const required = out["required"];
296
+ const properties = out["properties"];
297
+ if (Array.isArray(required) && isRecord$6(properties)) out["required"] = required.filter((name) => typeof name !== "string" || name in properties);
298
+ return out;
299
+ }
300
+ /**
301
+ * 把模型不可能自己产出的字段(本地文件二进制)从**出站**工具 schema 里剥离。
302
+ *
303
+ * 模型拿不到用户磁盘上的文件,留着这个字段只有两种下场:模型拒绝调用工具
304
+ * (实测 DeepSeek 直接回文本「请上传文件」,file-pick 永远触发不了),或编造一个
305
+ * 占位字符串把技能跑挂。剥掉后模型以缺参形式调用,引擎的 missing-params 链路
306
+ * 会弹 file-pick 让用户用系统文件选择器补这个字段。
307
+ *
308
+ * 这是**传输层适配**,只应在出站请求体上调用;不要下沉到 AgentLoop——
309
+ * 引擎侧的 missing 校验看的必须是技能作者写的完整 inputSchema。
310
+ *
311
+ * 已知边界(0.10.0 复核注记):只剥 `properties` 里的 inline 二进制字段;
312
+ * 通过 `$ref` 指向 `$defs` 中二进制定义的字段仍会下发(剥掉会留悬空 $ref),
313
+ * 这类技能依旧可能拿不到 file-pick——需要完整修复时再补「剥 $def + 剥指向它的 $ref」。
314
+ */
315
+ function stripModelUnfillableParams(schema) {
316
+ return stripNode(schema);
317
+ }
261
318
  /** 各家都认的基础关键字。Google `FunctionDeclaration.parameters` 是其中最窄的一家。 */
262
319
  const BASE_KEYWORDS = /* @__PURE__ */ new Set([
263
320
  "type",
@@ -499,7 +556,7 @@ const toOpenAiTools = (tools) => tools.map((tool) => ({
499
556
  function: {
500
557
  name: tool.name,
501
558
  ...tool.description ? { description: tool.description } : {},
502
- parameters: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), OPENAI_SCHEMA_PROFILE, tool.name)
559
+ parameters: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), OPENAI_SCHEMA_PROFILE, tool.name)
503
560
  }
504
561
  }));
505
562
  const errorMessage$2 = (e) => e instanceof Error ? e.message : String(e);
@@ -539,6 +596,7 @@ var OpenAiCompatibleClient = class {
539
596
  const reader = res.body.getReader();
540
597
  const frames = createSseFrameReader();
541
598
  let done = false;
599
+ let usage;
542
600
  const handleFrame = function* (data) {
543
601
  if (data === "[DONE]") {
544
602
  done = true;
@@ -550,8 +608,17 @@ var OpenAiCompatibleClient = class {
550
608
  } catch (e) {
551
609
  throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage$2(e)}`, data);
552
610
  }
611
+ const usageRaw = chunk.usage;
612
+ if (usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number")) usage = {
613
+ inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
614
+ outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
615
+ };
553
616
  const delta = chunk.choices?.[0]?.delta;
554
617
  if (!delta) return;
618
+ if (typeof delta["reasoning_content"] === "string" && delta["reasoning_content"] !== "") yield {
619
+ type: "thinking-delta",
620
+ delta: delta["reasoning_content"]
621
+ };
555
622
  if (typeof delta["content"] === "string" && delta["content"] !== "") yield {
556
623
  type: "text-delta",
557
624
  delta: delta["content"]
@@ -601,7 +668,10 @@ var OpenAiCompatibleClient = class {
601
668
  };
602
669
  })
603
670
  };
604
- yield { type: "done" };
671
+ yield {
672
+ type: "done",
673
+ ...usage ? { usage } : {}
674
+ };
605
675
  }
606
676
  async #postChat(input, stream) {
607
677
  const { baseUrl, apiKey, model } = this.#requireConfig();
@@ -615,7 +685,10 @@ var OpenAiCompatibleClient = class {
615
685
  body["tool_choice"] = "auto";
616
686
  }
617
687
  if (input.temperature !== void 0) body["temperature"] = input.temperature;
618
- if (stream) body["stream"] = true;
688
+ if (stream) {
689
+ body["stream"] = true;
690
+ body["stream_options"] = { include_usage: true };
691
+ }
619
692
  let res;
620
693
  try {
621
694
  res = await this.#fetch(`${baseUrl}/chat/completions`, {
@@ -665,9 +738,17 @@ var OpenAiCompatibleClient = class {
665
738
  };
666
739
  });
667
740
  const content = choice["content"];
741
+ const thinking = choice["reasoning_content"];
742
+ const usageRaw = data.usage;
743
+ const usage = usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number") ? {
744
+ inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
745
+ outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
746
+ } : void 0;
668
747
  return {
669
748
  content: typeof content === "string" && content !== "" ? textParts(content) : void 0,
670
749
  toolCalls: toolCalls?.length ? toolCalls : void 0,
750
+ ...typeof thinking === "string" && thinking !== "" ? { thinking } : {},
751
+ ...usage ? { usage } : {},
671
752
  raw: data
672
753
  };
673
754
  } catch (e) {
@@ -756,7 +837,7 @@ function toAnthropicMessages(messages) {
756
837
  const toAnthropicTools = (tools) => tools.map((tool) => ({
757
838
  name: tool.name,
758
839
  ...tool.description ? { description: tool.description } : {},
759
- input_schema: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), ANTHROPIC_SCHEMA_PROFILE, tool.name)
840
+ input_schema: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), ANTHROPIC_SCHEMA_PROFILE, tool.name)
760
841
  }));
761
842
  /** Anthropic Messages API 客户端(零依赖 fetch;Node/浏览器通用) */
762
843
  var AnthropicClient = class {
@@ -795,6 +876,8 @@ var AnthropicClient = class {
795
876
  const decoder = new TextDecoder();
796
877
  const reader = res.body.getReader();
797
878
  const frames = createSseFrameReader();
879
+ let inputTokens;
880
+ let outputTokens;
798
881
  const handleFrame = function* (data) {
799
882
  let chunk;
800
883
  try {
@@ -804,6 +887,17 @@ var AnthropicClient = class {
804
887
  }
805
888
  const type = chunk["type"];
806
889
  if (type === "error") throw new WebSkillError("LLM_REQUEST_FAILED", `LLM stream error: ${chunk["error"]?.message ?? data}`);
890
+ if (type === "message_start") {
891
+ const usage = chunk["message"]?.["usage"];
892
+ if (typeof usage?.["input_tokens"] === "number") inputTokens = usage["input_tokens"];
893
+ if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
894
+ return;
895
+ }
896
+ if (type === "message_delta") {
897
+ const usage = chunk["usage"];
898
+ if (typeof usage?.["output_tokens"] === "number") outputTokens = usage["output_tokens"];
899
+ return;
900
+ }
807
901
  if (type === "content_block_start") {
808
902
  const block = chunk["content_block"];
809
903
  if (block?.["type"] === "tool_use") {
@@ -826,6 +920,13 @@ var AnthropicClient = class {
826
920
  };
827
921
  return;
828
922
  }
923
+ if (delta?.["type"] === "thinking_delta" && typeof delta["thinking"] === "string" && delta["thinking"] !== "") {
924
+ yield {
925
+ type: "thinking-delta",
926
+ delta: delta["thinking"]
927
+ };
928
+ return;
929
+ }
829
930
  if (delta?.["type"] === "input_json_delta" && typeof delta["partial_json"] === "string") {
830
931
  const acc = toolCallsByIndex.get(index) ?? {
831
932
  id: "",
@@ -866,7 +967,14 @@ var AnthropicClient = class {
866
967
  };
867
968
  })
868
969
  };
869
- yield { type: "done" };
970
+ const usage = inputTokens !== void 0 || outputTokens !== void 0 ? {
971
+ inputTokens: inputTokens ?? 0,
972
+ outputTokens: outputTokens ?? 0
973
+ } : void 0;
974
+ yield {
975
+ type: "done",
976
+ ...usage ? { usage } : {}
977
+ };
870
978
  }
871
979
  async #post(input, stream) {
872
980
  validateLlmMessages(input.messages);
@@ -878,7 +986,12 @@ var AnthropicClient = class {
878
986
  };
879
987
  if (system !== void 0) body["system"] = system;
880
988
  if (input.tools?.length) body["tools"] = toAnthropicTools(input.tools);
881
- if (input.temperature !== void 0) body["temperature"] = input.temperature;
989
+ const thinkingBudget = this.#config.thinkingBudgetTokens;
990
+ if (thinkingBudget !== void 0 && Number.isInteger(thinkingBudget) && thinkingBudget > 0) body["thinking"] = {
991
+ type: "enabled",
992
+ budget_tokens: thinkingBudget
993
+ };
994
+ else if (input.temperature !== void 0) body["temperature"] = input.temperature;
882
995
  if (stream) body["stream"] = true;
883
996
  let res;
884
997
  try {
@@ -897,10 +1010,26 @@ var AnthropicClient = class {
897
1010
  }
898
1011
  return res;
899
1012
  }
900
- /** 轻量探测(GET /v1/models),集成测试据此决定 skip */
1013
+ /**
1014
+ * 真实调用路径探测(FR-27.1):`POST /v1/messages` 最小请求(max_tokens=1),200 即可达。
1015
+ * Anthropic 兼容端点(如 LM Studio 的 /anthropic 层)不实现 `GET /v1/models`,
1016
+ * 用 /models 探测会把「能对话」误判为「不可达」——checkAvailability 的语义从
1017
+ * 「/models 可达」改为「能完成一次真实调用」。
1018
+ */
901
1019
  async checkAvailability() {
902
1020
  try {
903
- return (await this.#fetch(`${this.#baseUrl()}/v1/models`, { headers: this.#headers() })).ok;
1021
+ return (await this.#fetch(`${this.#baseUrl()}/v1/messages`, {
1022
+ method: "POST",
1023
+ headers: this.#headers(),
1024
+ body: JSON.stringify({
1025
+ model: this.#config.model,
1026
+ max_tokens: 1,
1027
+ messages: [{
1028
+ role: "user",
1029
+ content: "ping"
1030
+ }]
1031
+ })
1032
+ })).ok;
904
1033
  } catch {
905
1034
  return false;
906
1035
  }
@@ -910,14 +1039,22 @@ var AnthropicClient = class {
910
1039
  const blocks = data.content;
911
1040
  if (!Array.isArray(blocks)) throw new Error("response has no content blocks");
912
1041
  const text = blocks.filter((b) => b["type"] === "text").map((b) => String(b["text"] ?? "")).join("");
1042
+ const thinking = blocks.filter((b) => b["type"] === "thinking").map((b) => String(b["thinking"] ?? "")).join("");
913
1043
  const toolCalls = blocks.filter((b) => b["type"] === "tool_use").map((b) => ({
914
1044
  id: typeof b["id"] === "string" ? b["id"] : "",
915
1045
  name: typeof b["name"] === "string" ? b["name"] : "",
916
1046
  arguments: typeof b["input"] === "object" && b["input"] !== null ? b["input"] : {}
917
1047
  }));
1048
+ const usageRaw = data.usage;
1049
+ const usage = typeof usageRaw?.["input_tokens"] === "number" || typeof usageRaw?.["output_tokens"] === "number" ? {
1050
+ inputTokens: typeof usageRaw["input_tokens"] === "number" ? usageRaw["input_tokens"] : 0,
1051
+ outputTokens: typeof usageRaw["output_tokens"] === "number" ? usageRaw["output_tokens"] : 0
1052
+ } : void 0;
918
1053
  return {
919
1054
  content: text === "" ? void 0 : textParts(text),
920
1055
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
1056
+ ...thinking === "" ? {} : { thinking },
1057
+ ...usage ? { usage } : {},
921
1058
  raw: data
922
1059
  };
923
1060
  } catch (e) {
@@ -926,6 +1063,18 @@ var AnthropicClient = class {
926
1063
  }
927
1064
  };
928
1065
  const errorMessage = (e) => e instanceof Error ? e.message : String(e);
1066
+ /** usageMetadata → LlmTokenUsage(promptTokenCount / candidatesTokenCount;无字段返回 undefined) */
1067
+ function parseGenAiUsage(data) {
1068
+ const usage = data?.usageMetadata;
1069
+ if (!usage) return void 0;
1070
+ const input = usage["promptTokenCount"];
1071
+ const output = usage["candidatesTokenCount"];
1072
+ if (typeof input !== "number" && typeof output !== "number") return void 0;
1073
+ return {
1074
+ inputTokens: typeof input === "number" ? input : 0,
1075
+ outputTokens: typeof output === "number" ? output : 0
1076
+ };
1077
+ }
929
1078
  /** parts → GenAI parts(二进制统一走 inlineData) */
930
1079
  const toGenAiParts = (parts, where) => parts.map((part) => {
931
1080
  switch (part.type) {
@@ -999,7 +1148,7 @@ function toGenAiContents(messages) {
999
1148
  const toGenAiTools = (tools) => [{ functionDeclarations: tools.map((tool) => ({
1000
1149
  name: tool.name,
1001
1150
  ...tool.description ? { description: tool.description } : {},
1002
- parameters: sanitizeForVendor(toPortableToolSchema(tool.inputSchema), GOOGLE_SCHEMA_PROFILE, tool.name)
1151
+ parameters: sanitizeForVendor(toPortableToolSchema(stripModelUnfillableParams(tool.inputSchema)), GOOGLE_SCHEMA_PROFILE, tool.name)
1003
1152
  })) }];
1004
1153
  /** Google GenAI(generateContent / streamGenerateContent)客户端(零依赖 fetch) */
1005
1154
  var GoogleGenAiClient = class {
@@ -1023,9 +1172,11 @@ var GoogleGenAiClient = class {
1023
1172
  const parts = this.#responseParts(data);
1024
1173
  const text = parts.filter((p) => typeof p["text"] === "string").map((p) => String(p["text"])).join("");
1025
1174
  const toolCalls = this.#functionCalls(parts);
1175
+ const usage = parseGenAiUsage(data);
1026
1176
  return {
1027
1177
  content: text === "" ? void 0 : textParts(text),
1028
1178
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
1179
+ ...usage ? { usage } : {},
1029
1180
  raw: data
1030
1181
  };
1031
1182
  }
@@ -1037,6 +1188,7 @@ var GoogleGenAiClient = class {
1037
1188
  const decoder = new TextDecoder();
1038
1189
  const reader = res.body.getReader();
1039
1190
  const frames = createSseFrameReader();
1191
+ let usage;
1040
1192
  const handleFrame = function* (data) {
1041
1193
  let chunk;
1042
1194
  try {
@@ -1044,6 +1196,7 @@ var GoogleGenAiClient = class {
1044
1196
  } catch (e) {
1045
1197
  throw new WebSkillError("LLM_REQUEST_FAILED", `Failed to parse LLM stream frame: ${errorMessage(e)}`, data);
1046
1198
  }
1199
+ usage = parseGenAiUsage(chunk) ?? usage;
1047
1200
  const parts = chunk.candidates?.[0]?.content?.parts;
1048
1201
  for (const part of parts ?? []) {
1049
1202
  if (typeof part["text"] === "string" && part["text"] !== "") {
@@ -1075,7 +1228,10 @@ var GoogleGenAiClient = class {
1075
1228
  type: "tool-calls",
1076
1229
  toolCalls
1077
1230
  };
1078
- yield { type: "done" };
1231
+ yield {
1232
+ type: "done",
1233
+ ...usage ? { usage } : {}
1234
+ };
1079
1235
  }
1080
1236
  async #post(input, stream) {
1081
1237
  const model = input.model ?? this.#config.model;
@@ -1136,6 +1292,17 @@ var GoogleGenAiClient = class {
1136
1292
  return out;
1137
1293
  }
1138
1294
  };
1295
+ /** v5 { inputTokens, outputTokens } 与 v4 { promptTokens, completionTokens } 两种形状都接 */
1296
+ function fromVercelUsage(raw) {
1297
+ const u = raw ?? {};
1298
+ const input = u["inputTokens"] ?? u["promptTokens"];
1299
+ const output = u["outputTokens"] ?? u["completionTokens"];
1300
+ if (typeof input !== "number" && typeof output !== "number") return void 0;
1301
+ return {
1302
+ inputTokens: typeof input === "number" ? input : 0,
1303
+ outputTokens: typeof output === "number" ? output : 0
1304
+ };
1305
+ }
1139
1306
  function toVercelToolSpecs(tools) {
1140
1307
  const out = {};
1141
1308
  for (const tool of tools) out[tool.name] = {
@@ -1144,6 +1311,16 @@ function toVercelToolSpecs(tools) {
1144
1311
  };
1145
1312
  return out;
1146
1313
  }
1314
+ /**
1315
+ * 归一化供应商思考内容:v5 的 `reasoningText`(字符串)与 v4 的 `reasoning`
1316
+ * (Array<{ text; type }>)都能落到同一条 `LlmResponse.thinking`。
1317
+ */
1318
+ function normalizeThinking(value) {
1319
+ if (typeof value === "string") return value !== "" ? value : void 0;
1320
+ if (!Array.isArray(value)) return void 0;
1321
+ const text = value.map((part) => typeof part?.text === "string" ? part.text : "").join("");
1322
+ return text !== "" ? text : void 0;
1323
+ }
1147
1324
  /** 接受 generateText 的返回值形状:{ text?, toolCalls?: [{ toolCallId, toolName, input|args }] } */
1148
1325
  function fromVercelResult(result) {
1149
1326
  const r = result;
@@ -1155,9 +1332,13 @@ function fromVercelResult(result) {
1155
1332
  arguments: typeof args === "object" && args !== null ? args : {}
1156
1333
  };
1157
1334
  });
1335
+ const thinking = normalizeThinking(r.reasoningText ?? r.reasoning);
1336
+ const usage = fromVercelUsage(r.usage);
1158
1337
  return {
1159
1338
  content: typeof r.text === "string" && r.text !== "" ? textParts(r.text) : void 0,
1160
1339
  toolCalls: toolCalls.length ? toolCalls : void 0,
1340
+ ...thinking !== void 0 ? { thinking } : {},
1341
+ ...usage ? { usage } : {},
1161
1342
  raw: result
1162
1343
  };
1163
1344
  }
@@ -1175,6 +1356,13 @@ function fromVercelStreamPart(part) {
1175
1356
  delta
1176
1357
  } : void 0;
1177
1358
  }
1359
+ case "reasoning-delta": {
1360
+ const delta = p["text"] ?? p["delta"];
1361
+ return typeof delta === "string" && delta !== "" ? {
1362
+ type: "thinking-delta",
1363
+ delta
1364
+ } : void 0;
1365
+ }
1178
1366
  case "tool-call": return {
1179
1367
  type: "tool-calls",
1180
1368
  toolCalls: [{
@@ -1183,7 +1371,13 @@ function fromVercelStreamPart(part) {
1183
1371
  arguments: typeof p["input"] === "object" && p["input"] !== null ? p["input"] : {}
1184
1372
  }]
1185
1373
  };
1186
- case "finish": return { type: "done" };
1374
+ case "finish": {
1375
+ const usage = fromVercelUsage(p["totalUsage"] ?? p["usage"]);
1376
+ return {
1377
+ type: "done",
1378
+ ...usage ? { usage } : {}
1379
+ };
1380
+ }
1187
1381
  default: return;
1188
1382
  }
1189
1383
  }
@@ -1421,6 +1615,16 @@ const ASK_USER_TOOL = {
1421
1615
  source: "builtin"
1422
1616
  };
1423
1617
  /**
1618
+ * 越权 / 绝对路径 / 父级遍历的引用读取:策略拒绝(FS_PERMISSION_DENIED,
1619
+ * 计入 POLICY_DENIAL_CODES → 不计入隔离失败计数),
1620
+ * 与「references/ 内路径确实不存在 → FS_NOT_FOUND」区分(0.9.0 分册 14,UX-04)。
1621
+ */
1622
+ function assertReferencePath(relativePath) {
1623
+ const trimmed = relativePath.trim();
1624
+ if (trimmed === "" || trimmed.startsWith("/") || trimmed.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(trimmed)) throw new WebSkillError("FS_PERMISSION_DENIED", `Reference access denied: absolute or empty path (path: ${JSON.stringify(relativePath)})`);
1625
+ if (trimmed.replace(/\\/g, "/").split("/").includes("..")) throw new WebSkillError("FS_PERMISSION_DENIED", `Reference access denied: parent traversal (path: ${JSON.stringify(relativePath)})`);
1626
+ }
1627
+ /**
1424
1628
  * 脚本执行上下文:只暴露 readReference / writeArtifact 两个显式能力,
1425
1629
  * 不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
1426
1630
  */
@@ -1431,6 +1635,7 @@ function createScriptContext(deps) {
1431
1635
  skillName,
1432
1636
  runId,
1433
1637
  async readReference(relativePath) {
1638
+ assertReferencePath(relativePath);
1434
1639
  return readFs.readText(resolveInsideRoot(skillRoot, `references/${relativePath}`));
1435
1640
  },
1436
1641
  async writeArtifact(path, content, options) {
@@ -1776,7 +1981,7 @@ const DEFAULT_USER_PROFILE_LIMITS = {
1776
1981
  recordLimit: 500,
1777
1982
  valueMaxChars: 2e3,
1778
1983
  sampleLimit: 80,
1779
- injectMaxBytes: 2e3,
1984
+ injectMaxBytes: 4e3,
1780
1985
  entryLimit: 40
1781
1986
  };
1782
1987
  /** @experimental */
@@ -2026,8 +2231,8 @@ function extractSkillCandidate(data) {
2026
2231
  */
2027
2232
  const DEFAULT_LOOP_LIMITS = {
2028
2233
  maxTurns: 10,
2029
- totalTimeoutMs: 12e4,
2030
- toolTimeoutMs: 3e4
2234
+ totalTimeoutMs: 36e5,
2235
+ toolTimeoutMs: 6e5
2031
2236
  };
2032
2237
  const RUN_SNAPSHOT_SCHEMA_VERSION = 3;
2033
2238
  /** @experimental */
@@ -2055,7 +2260,7 @@ var FsRunSnapshotStore = class {
2055
2260
  return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
2056
2261
  }
2057
2262
  async save(snapshot) {
2058
- await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
2263
+ await atomicWriteText(this.#fs, this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
2059
2264
  await this.#pruneExpired();
2060
2265
  }
2061
2266
  async load(runId) {
@@ -2267,7 +2472,7 @@ var AgentLoop = class {
2267
2472
  maxTurns: config.maxTurns ?? DEFAULT_LOOP_LIMITS.maxTurns,
2268
2473
  totalTimeoutMs: config.totalTimeoutMs ?? DEFAULT_LOOP_LIMITS.totalTimeoutMs,
2269
2474
  toolTimeoutMs: config.toolTimeoutMs ?? DEFAULT_LOOP_LIMITS.toolTimeoutMs,
2270
- toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
2475
+ toolResultMaxBytes: config.toolResultMaxBytes ?? 256e3,
2271
2476
  paramHistoryLimit: config.paramHistoryLimit ?? 50,
2272
2477
  toolCallingDisabled: config.toolCallingDisabled ?? false,
2273
2478
  maxUnknownToolRetries: config.maxUnknownToolRetries ?? 2,
@@ -2330,6 +2535,7 @@ var AgentLoop = class {
2330
2535
  surfacePatchCount: 0,
2331
2536
  processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
2332
2537
  emittedToolEvents: /* @__PURE__ */ new Set(),
2538
+ artifactPaths: /* @__PURE__ */ new Set(),
2333
2539
  unknownToolCalls: 0,
2334
2540
  startMs,
2335
2541
  pausedMs: 0,
@@ -2480,6 +2686,23 @@ var AgentLoop = class {
2480
2686
  const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
2481
2687
  return finish("failed", "llm-error", messageOf(e), code);
2482
2688
  }
2689
+ if (response.thinking !== void 0 && response.thinking !== "") {
2690
+ state.trace.record("llm.thinking", { data: {
2691
+ turn,
2692
+ text: response.thinking
2693
+ } });
2694
+ const bus = this.#deps.eventBus;
2695
+ if (!llm.stream && bus && bus.listenerCount() > 0) bus.emit({
2696
+ phase: "execute",
2697
+ runId: state.runId,
2698
+ sessionId: state.run.sessionId,
2699
+ ts: state.now(),
2700
+ data: {
2701
+ kind: "thinking-delta",
2702
+ delta: response.thinking
2703
+ }
2704
+ });
2705
+ }
2483
2706
  const responseText = partsToText(response.content);
2484
2707
  if (!llm.stream && responseText !== "") await this.#deps.uiBridge?.onTextDelta?.(state.runId, responseText);
2485
2708
  trace.record("llm.response", { data: {
@@ -2487,6 +2710,11 @@ var AgentLoop = class {
2487
2710
  hasToolCalls: Boolean(response.toolCalls?.length),
2488
2711
  contentLength: responseText.length
2489
2712
  } });
2713
+ if (response.usage) trace.record("llm.usage", { data: {
2714
+ turn,
2715
+ inputTokens: response.usage.inputTokens,
2716
+ outputTokens: response.usage.outputTokens
2717
+ } });
2490
2718
  if (!response.toolCalls?.length) {
2491
2719
  messages.push({
2492
2720
  role: "assistant",
@@ -2696,6 +2924,7 @@ var AgentLoop = class {
2696
2924
  surfacePatchCount: 0,
2697
2925
  processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
2698
2926
  emittedToolEvents: /* @__PURE__ */ new Set(),
2927
+ artifactPaths: /* @__PURE__ */ new Set(),
2699
2928
  unknownToolCalls: 0,
2700
2929
  startMs,
2701
2930
  pausedMs: snapshot.pausedMs ?? 0,
@@ -2816,6 +3045,7 @@ var AgentLoop = class {
2816
3045
  /**
2817
3046
  * 流式 LLM 调用:text-delta 仅在 EventBus 有订阅者时发射 llm.delta 事件,
2818
3047
  * 并同步转发 uiBridge.onTextDelta;tool-calls/done 组装为 LlmResponse 走原有分支。
3048
+ * thinking-delta 同理累积成 `response.thinking`(每轮一段,UI-UX5 #32)。
2819
3049
  */
2820
3050
  async #completeStreaming(llm, state, input) {
2821
3051
  const emitDelta = (delta) => {
@@ -2832,18 +3062,41 @@ var AgentLoop = class {
2832
3062
  });
2833
3063
  this.#deps.uiBridge?.onTextDelta?.(state.runId, delta);
2834
3064
  };
3065
+ const emitThinkingDelta = (delta) => {
3066
+ const bus = this.#deps.eventBus;
3067
+ if (bus && bus.listenerCount() > 0) bus.emit({
3068
+ phase: "execute",
3069
+ runId: state.runId,
3070
+ sessionId: state.run.sessionId,
3071
+ ts: state.now(),
3072
+ data: {
3073
+ kind: "thinking-delta",
3074
+ delta
3075
+ }
3076
+ });
3077
+ };
2835
3078
  let content = "";
3079
+ let thinking = "";
2836
3080
  let doneContent;
3081
+ let usage;
2837
3082
  const toolCalls = [];
2838
3083
  for await (const event of llm.stream(input)) if (event.type === "text-delta") {
2839
3084
  content += event.delta;
2840
3085
  emitDelta(event.delta);
3086
+ } else if (event.type === "thinking-delta") {
3087
+ thinking += event.delta;
3088
+ emitThinkingDelta(event.delta);
2841
3089
  } else if (event.type === "tool-calls") toolCalls.push(...event.toolCalls);
2842
- else if (event.type === "done") doneContent = event.content;
3090
+ else if (event.type === "done") {
3091
+ doneContent = event.content;
3092
+ usage = event.usage ?? usage;
3093
+ }
2843
3094
  const text = doneContent ?? content;
2844
3095
  return {
2845
3096
  content: text === "" ? void 0 : textParts(text),
2846
- toolCalls: toolCalls.length > 0 ? toolCalls : void 0
3097
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
3098
+ ...thinking === "" ? {} : { thinking },
3099
+ ...usage ? { usage } : {}
2847
3100
  };
2848
3101
  }
2849
3102
  /** 生命周期接线:更新 phase、发事件、跑钩子 */
@@ -3070,10 +3323,13 @@ var AgentLoop = class {
3070
3323
  });
3071
3324
  this.#emitTool(state, "failed", call, result.error?.code);
3072
3325
  }
3073
- for (const artifact of result.artifacts ?? []) state.trace.record("artifact.created", { data: {
3074
- artifactId: artifact.id,
3075
- path: artifact.path
3076
- } });
3326
+ for (const artifact of result.artifacts ?? []) {
3327
+ state.artifactPaths.add(artifact.path);
3328
+ state.trace.record("artifact.created", { data: {
3329
+ artifactId: artifact.id,
3330
+ path: artifact.path
3331
+ } });
3332
+ }
3077
3333
  if (state.unknownToolCalls > this.#config.maxUnknownToolRetries) throw new RunTerminated({
3078
3334
  status: "failed",
3079
3335
  reason: "tool-resolution-exhausted",
@@ -3089,7 +3345,12 @@ var AgentLoop = class {
3089
3345
  state.trace.record("run.warning", { message: "UiBridge does not support renderSurface; UI surface was not rendered" });
3090
3346
  return;
3091
3347
  }
3092
- const attributed = this.#attributeSurfaceEvent(state, event);
3348
+ let attributed = this.#attributeSurfaceEvent(state, event);
3349
+ if (attributed.type === "open") {
3350
+ const kept = await this.#stripDuplicateFileLinks(state, attributed);
3351
+ if (kept === void 0) return;
3352
+ attributed = kept;
3353
+ }
3093
3354
  if (attributed.type === "patch") this.#consumeSurfacePatchBudget(state);
3094
3355
  await bridge.renderSurface(attributed);
3095
3356
  state.surfaceEvents.push(structuredClone(attributed));
@@ -3132,6 +3393,44 @@ var AgentLoop = class {
3132
3393
  }))
3133
3394
  };
3134
3395
  }
3396
+ /**
3397
+ * 剥掉指向「本 run 已写出 artifact」的 FileLink 节点(UI-UX5 #5):
3398
+ * 宿主在 run 完成时会把每个 artifact 自动渲染成下载卡(`buildRenderResult`),
3399
+ * 模型/技能再建一张就是重复。整棵树剥空返回 undefined(放弃这次 open);
3400
+ * 有剥离就记一条 run.warning,让「少了一张卡」在轨迹里可查。
3401
+ */
3402
+ async #stripDuplicateFileLinks(state, event) {
3403
+ if (state.artifactPaths.size === 0) {
3404
+ const listed = await this.#deps.artifactStore.listArtifacts(state.runId).catch(() => []);
3405
+ for (const artifact of listed) state.artifactPaths.add(artifact.path);
3406
+ }
3407
+ if (state.artifactPaths.size === 0) return event;
3408
+ const paths = state.artifactPaths;
3409
+ let stripped = 0;
3410
+ const walk = (node) => {
3411
+ if (node.component === "FileLink") {
3412
+ const path = node.props?.["path"];
3413
+ if (typeof path === "string" && paths.has(path)) {
3414
+ stripped += 1;
3415
+ return;
3416
+ }
3417
+ }
3418
+ if (!node.children || node.children.length === 0) return node;
3419
+ const children = node.children.map(walk).filter((child) => child !== void 0);
3420
+ if (children.length === node.children.length) return node;
3421
+ return {
3422
+ ...node,
3423
+ children
3424
+ };
3425
+ };
3426
+ const node = walk(event.node);
3427
+ if (stripped === 0) return event;
3428
+ state.trace.record("run.warning", { message: `Dropped ${stripped} FileLink node(s) duplicating auto-rendered artifact download card(s)` });
3429
+ return node === void 0 ? void 0 : {
3430
+ ...event,
3431
+ node
3432
+ };
3433
+ }
3135
3434
  /** Awaits the single action emitted with the most recently persisted tool result. */
3136
3435
  async #drainSurfaceAction(state) {
3137
3436
  const request = state.pendingSurfaceAction;
@@ -3410,7 +3709,7 @@ var AgentLoop = class {
3410
3709
  }))];
3411
3710
  }
3412
3711
  /**
3413
- * 工具结果回喂序列化:超过 toolResultMaxBytes(默认 100KB)时头尾保留截断,
3712
+ * 工具结果回喂序列化:超过 toolResultMaxBytes(默认 256KB)时头尾保留截断,
3414
3713
  * 完整内容经 artifactStore 落 artifact,回喂摘要含 artifact id。
3415
3714
  */
3416
3715
  async #serializeToolResult(call, result, state) {
@@ -4398,7 +4697,7 @@ var FsMemoryStore = class {
4398
4697
  }
4399
4698
  }
4400
4699
  async set(scope, key, value) {
4401
- await this.#fs.writeText(this.#keyPath(scope, key), JSON.stringify(value, null, 2));
4700
+ await atomicWriteText(this.#fs, this.#keyPath(scope, key), JSON.stringify(value, null, 2));
4402
4701
  }
4403
4702
  async delete(scope, key) {
4404
4703
  const path = this.#keyPath(scope, key);
@@ -4634,7 +4933,7 @@ var FsArtifactStore = class {
4634
4933
  }
4635
4934
  async createTextArtifact(input) {
4636
4935
  const size = new TextEncoder().encode(input.content).length;
4637
- await this.#fs.writeText(this.#artifactPath(input.runId, input.path), input.content);
4936
+ await atomicWriteText(this.#fs, this.#artifactPath(input.runId, input.path), input.content);
4638
4937
  return this.#persist({
4639
4938
  ...input,
4640
4939
  type: "text",
@@ -4684,7 +4983,7 @@ var FsArtifactStore = class {
4684
4983
  metadata: input.metadata
4685
4984
  };
4686
4985
  const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
4687
- await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE$1}`, JSON.stringify({ artifacts: next }, null, 2));
4986
+ await atomicWriteText(this.#fs, `${this.#root}/${input.runId}/${INDEX_FILE$1}`, JSON.stringify({ artifacts: next }, null, 2));
4688
4987
  return artifact;
4689
4988
  }
4690
4989
  };
@@ -4903,6 +5202,38 @@ var CapabilityApproval = class CapabilityApproval {
4903
5202
  return "allowed";
4904
5203
  }
4905
5204
  };
5205
+ /**
5206
+ * 从 run 的 trace 聚合大模型用量(请求次数 + 输入/输出 token)。
5207
+ * 与 `summarizeToolCalls` 同型:消费方不再各自遍历 trace。
5208
+ * @stable
5209
+ */
5210
+ function summarizeRunUsage(trace) {
5211
+ let llmCalls = 0;
5212
+ let inputTokens = 0;
5213
+ let outputTokens = 0;
5214
+ let hasUsage = false;
5215
+ for (const event of trace) {
5216
+ if (event.type === "llm.request") {
5217
+ llmCalls += 1;
5218
+ continue;
5219
+ }
5220
+ if (event.type !== "llm.usage") continue;
5221
+ const input = event.data?.["inputTokens"];
5222
+ const output = event.data?.["outputTokens"];
5223
+ const hasInput = typeof input === "number" && Number.isFinite(input);
5224
+ const hasOutput = typeof output === "number" && Number.isFinite(output);
5225
+ if (hasInput) inputTokens += input;
5226
+ if (hasOutput) outputTokens += output;
5227
+ if (hasInput || hasOutput) hasUsage = true;
5228
+ }
5229
+ return {
5230
+ llmCalls,
5231
+ ...hasUsage ? {
5232
+ inputTokens,
5233
+ outputTokens
5234
+ } : {}
5235
+ };
5236
+ }
4906
5237
  const RUN_TRACE_SCHEMA_VERSION = 1;
4907
5238
  /** 终止原因:取最后一条 run.completed/cancelled/failed 事件的 data.reason */
4908
5239
  function extractEndReason(events) {
@@ -4917,6 +5248,7 @@ function extractEndReason(events) {
4917
5248
  function summarize(trace) {
4918
5249
  const endReason = extractEndReason(trace.events);
4919
5250
  const durationMs = trace.endedAt === void 0 ? void 0 : Date.parse(trace.endedAt) - Date.parse(trace.startedAt);
5251
+ const runUsage = summarizeRunUsage(trace.events);
4920
5252
  return {
4921
5253
  runId: trace.runId,
4922
5254
  startedAt: trace.startedAt,
@@ -4927,7 +5259,11 @@ function summarize(trace) {
4927
5259
  ...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
4928
5260
  ...trace.endedAt !== void 0 ? { endedAt: trace.endedAt } : {},
4929
5261
  ...endReason !== void 0 ? { endReason } : {},
4930
- ...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {}
5262
+ ...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {},
5263
+ ...runUsage.inputTokens !== void 0 ? { usage: {
5264
+ inputTokens: runUsage.inputTokens,
5265
+ outputTokens: runUsage.outputTokens ?? 0
5266
+ } } : {}
4931
5267
  };
4932
5268
  }
4933
5269
  function parseTraceFile(raw, path) {
@@ -4994,7 +5330,7 @@ var FsRunTraceStore = class {
4994
5330
  events: run.trace
4995
5331
  };
4996
5332
  try {
4997
- await this.#fs.writeText(this.#path(run.id), JSON.stringify(trace, null, 2));
5333
+ await atomicWriteText(this.#fs, this.#path(run.id), JSON.stringify(trace, null, 2));
4998
5334
  } catch (e) {
4999
5335
  this.#onError(e, run);
5000
5336
  return;
@@ -5120,6 +5456,12 @@ function applyFilter(summaries, filter) {
5120
5456
  * @stable
5121
5457
  */
5122
5458
  function summarizeToolCalls(run) {
5459
+ const startedAtByCallId = /* @__PURE__ */ new Map();
5460
+ for (const event of run.trace) {
5461
+ if (event.type !== "tool.started") continue;
5462
+ const callId = event.data?.["callId"];
5463
+ if (typeof callId === "string" && !startedAtByCallId.has(callId)) startedAtByCallId.set(callId, event.ts);
5464
+ }
5123
5465
  const calls = [];
5124
5466
  for (const event of run.trace) {
5125
5467
  if (event.type !== "tool.completed" && event.type !== "tool.failed") continue;
@@ -5129,6 +5471,7 @@ function summarizeToolCalls(run) {
5129
5471
  const args = event.data?.["args"];
5130
5472
  const durationMs = event.data?.["durationMs"];
5131
5473
  const errorCode = event.data?.["code"];
5474
+ const startedAt = startedAtByCallId.get(callId);
5132
5475
  calls.push({
5133
5476
  callId,
5134
5477
  name,
@@ -5136,6 +5479,7 @@ function summarizeToolCalls(run) {
5136
5479
  ...typeof args === "string" ? { args } : {},
5137
5480
  ...typeof durationMs === "number" ? { durationMs } : {},
5138
5481
  ...typeof errorCode === "string" ? { errorCode } : {},
5482
+ ...startedAt !== void 0 ? { startedAt } : {},
5139
5483
  ...event.type === "tool.failed" && typeof event.message === "string" ? { errorMessage: event.message } : {}
5140
5484
  });
5141
5485
  }
@@ -5213,6 +5557,8 @@ var FsSessionStore = class {
5213
5557
  #fs;
5214
5558
  /** id → 该 id 上最后一次变更的完成时点,用于串行化读改写 */
5215
5559
  #writes = /* @__PURE__ */ new Map();
5560
+ /** FR-11.4:损坏文件诊断只报一次,避免 list 轮询刷屏 */
5561
+ #warnedDataLoss = false;
5216
5562
  constructor(deps) {
5217
5563
  this.#root = deps.root.replace(/\/+$/, "");
5218
5564
  this.#fs = deps.fs;
@@ -5242,7 +5588,7 @@ var FsSessionStore = class {
5242
5588
  ...record.modelId !== void 0 ? { modelId: record.modelId } : {},
5243
5589
  messages: record.messages
5244
5590
  };
5245
- await this.#fs.writeText(this.#path(record.id), JSON.stringify(file, null, 2));
5591
+ await atomicWriteText(this.#fs, this.#path(record.id), JSON.stringify(file, null, 2));
5246
5592
  }
5247
5593
  async #require(id) {
5248
5594
  const record = await this.get(id);
@@ -5262,14 +5608,17 @@ var FsSessionStore = class {
5262
5608
  let record;
5263
5609
  try {
5264
5610
  record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
5265
- } catch (e) {
5266
- console.warn(`Skipping unreadable session file "${entry.path}": ${e instanceof Error ? e.message : String(e)}`);
5611
+ } catch {
5267
5612
  skipped.push(baseName(entry.path));
5268
5613
  continue;
5269
5614
  }
5270
5615
  if (record.archived === true && options.includeArchived !== true) continue;
5271
5616
  metas.push(toMeta(record));
5272
5617
  }
5618
+ if (skipped.length > 0 && !this.#warnedDataLoss) {
5619
+ this.#warnedDataLoss = true;
5620
+ console.warn(`[webskill] ${skipped.length} session file(s) under ${this.#root} are unreadable (possible data loss from an interrupted write): ${skipped.join(", ")}`);
5621
+ }
5273
5622
  metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
5274
5623
  return {
5275
5624
  ...takeTailPage(metas, options, "session"),
@@ -5354,4 +5703,4 @@ var FsSessionStore = class {
5354
5703
  };
5355
5704
 
5356
5705
  //#endregion
5357
- export { fromVercelStreamPart as $, SerializingMemoryStore as A, bridgeError as B, ProgressiveRouter as C, sealToolCallPairs as Ct, RUN_SNAPSHOT_SCHEMA_VERSION as D, toVercelToolSpecs as Dt, READ_SKILL_FILE_TOOL_NAME as E, toRecordDigests as Et, USER_PROFILE_PROMPT_HEADER as F, exportUserProfile as G, createScriptContext as H, USER_PROFILE_REFINE_PROMPT as I, extractTodoTraceEvents as J, extractChartSpec as K, WebSkillRuntime as L, USER_PROFILE_EXPORT_VERSION as M, USER_PROFILE_KEY as N, RUN_TRACE_SCHEMA_VERSION as O, validateUiSpecEvent as Ot, USER_PROFILE_NO_INVENTION_RULE as P, fromVercelResult as Q, appendBehaviorRecords as R, OpenAiCompatibleClient as S, scriptToolName as St, READ_SKILL_FILE_TOOL as T, toLlmToolSpec as Tt, createWebSkillApi as U, buildRenderResult as V, diffUserProfile as W, findUnpairedToolCalls as X, extractUiSpecEvents as Y, formatSkillScriptManifest as Z, FsRunTraceStore as _, renderUserProfileContext as _t, AgentLoop as a, mergeProfileEntries as at, GoogleGenAiClient as b, schemaSourceLabel as bt, CapabilityApproval as c, normalizeErrorCode as ct, EMPTY_USER_PROFILE as d, parseBridgeRequest as dt, interruptedToolResult as et, EventBus as f, parseUserProfileExport as ft, FsRunSnapshotStore as g, refineUserProfile as gt, FsMemoryStore as h, readUserProfile as ht, ASK_USER_TOOL_NAME as i, mergeCatalogEntries as it, TraceRecorder as j, SESSION_SCHEMA_VERSION as k, validateUiSpecNode as kt, DEFAULT_LOOP_LIMITS as l, normalizeToolContent as lt, FsArtifactStore as m, readProfileEntries as mt, ASK_USER_INPUT_SCHEMA as n, isUnsupportedRunSnapshot as nt, AnthropicClient as o, networkPolicyLibSource as ot, FS_SESSION_PAGE_SIZE as p, readBehaviorRecords as pt, extractSkillCandidate as q, ASK_USER_TOOL as r, listSkillScripts as rt, BEHAVIOR_RECORDS_KEY as s, networkUrlHost as st, ALLOWED_TOOLS_EXCLUSION_REASON as t, isNetworkAllowed as tt, DEFAULT_USER_PROFILE_LIMITS as u, normalizeToolError as ut, FsSessionStore as v, resolveToolName as vt, READ_SKILL_FILE_INPUT_SCHEMA as w, summarizeToolCalls as wt, HookRunner as x, schemaToForm as xt, FullDisclosureRouter as y, sampleBehaviorRecords as yt, applyUserProfileImport as z };
5706
+ export { fromVercelStreamPart as $, SerializingMemoryStore as A, validateUiSpecNode as At, bridgeError as B, ProgressiveRouter as C, sealToolCallPairs as Ct, RUN_SNAPSHOT_SCHEMA_VERSION as D, toRecordDigests as Dt, READ_SKILL_FILE_TOOL_NAME as E, toLlmToolSpec as Et, USER_PROFILE_PROMPT_HEADER as F, exportUserProfile as G, createScriptContext as H, USER_PROFILE_REFINE_PROMPT as I, extractTodoTraceEvents as J, extractChartSpec as K, WebSkillRuntime as L, USER_PROFILE_EXPORT_VERSION as M, USER_PROFILE_KEY as N, RUN_TRACE_SCHEMA_VERSION as O, toVercelToolSpecs as Ot, USER_PROFILE_NO_INVENTION_RULE as P, fromVercelResult as Q, appendBehaviorRecords as R, OpenAiCompatibleClient as S, scriptToolName as St, READ_SKILL_FILE_TOOL as T, summarizeToolCalls as Tt, createWebSkillApi as U, buildRenderResult as V, diffUserProfile as W, findUnpairedToolCalls as X, extractUiSpecEvents as Y, formatSkillScriptManifest as Z, FsRunTraceStore as _, renderUserProfileContext as _t, AgentLoop as a, mergeProfileEntries as at, GoogleGenAiClient as b, schemaSourceLabel as bt, CapabilityApproval as c, normalizeErrorCode as ct, EMPTY_USER_PROFILE as d, parseBridgeRequest as dt, interruptedToolResult as et, EventBus as f, parseUserProfileExport as ft, FsRunSnapshotStore as g, refineUserProfile as gt, FsMemoryStore as h, readUserProfile as ht, ASK_USER_TOOL_NAME as i, mergeCatalogEntries as it, TraceRecorder as j, SESSION_SCHEMA_VERSION as k, validateUiSpecEvent as kt, DEFAULT_LOOP_LIMITS as l, normalizeToolContent as lt, FsArtifactStore as m, readProfileEntries as mt, ASK_USER_INPUT_SCHEMA as n, isUnsupportedRunSnapshot as nt, AnthropicClient as o, networkPolicyLibSource as ot, FS_SESSION_PAGE_SIZE as p, readBehaviorRecords as pt, extractSkillCandidate as q, ASK_USER_TOOL as r, listSkillScripts as rt, BEHAVIOR_RECORDS_KEY as s, networkUrlHost as st, ALLOWED_TOOLS_EXCLUSION_REASON as t, isNetworkAllowed as tt, DEFAULT_USER_PROFILE_LIMITS as u, normalizeToolError as ut, FsSessionStore as v, resolveToolName as vt, READ_SKILL_FILE_INPUT_SCHEMA as w, summarizeRunUsage as wt, HookRunner as x, schemaToForm as xt, FullDisclosureRouter as y, sampleBehaviorRecords as yt, applyUserProfileImport as z };