@webskill/sdk 0.12.0 → 0.13.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.
@@ -607,6 +607,76 @@ function createSseFrameReader() {
607
607
  }
608
608
  };
609
609
  }
610
+ /**
611
+ * 内联 `<think>` 段落切分器(内部模块,不进公开导出)。
612
+ *
613
+ * 本地部署的思考型模型(Qwen 等经 OpenAI 兼容端点)不下发 `reasoning_content`,
614
+ * 而是把推理直接写进 `content`,用 `<think>` … `</think>` 包起来。不切分的话
615
+ * 思考正文会当成回答正文流进界面、进消息历史、进落盘,既污染上下文也没法折叠。
616
+ */
617
+ const OPEN = "<think>";
618
+ const CLOSE = "</think>";
619
+ /** buffer 是否为 tag 的真前缀(还不能判定,得再等) */
620
+ const isPartialPrefix = (buffer, tag) => buffer.length < tag.length && tag.startsWith(buffer);
621
+ function createThinkTagSplitter() {
622
+ let buffer = "";
623
+ let inThink = false;
624
+ const consume = () => {
625
+ let text = "";
626
+ let thinking = "";
627
+ for (;;) {
628
+ const tag = inThink ? CLOSE : OPEN;
629
+ const hit = buffer.indexOf(tag);
630
+ if (hit >= 0) {
631
+ const before = buffer.slice(0, hit);
632
+ if (inThink) thinking += before;
633
+ else text += before;
634
+ buffer = buffer.slice(hit + tag.length);
635
+ inThink = !inThink;
636
+ continue;
637
+ }
638
+ const start = buffer.lastIndexOf("<");
639
+ const keep = start >= 0 && isPartialPrefix(buffer.slice(start), tag) ? start : buffer.length;
640
+ const emit = buffer.slice(0, keep);
641
+ if (inThink) thinking += emit;
642
+ else text += emit;
643
+ buffer = buffer.slice(keep);
644
+ return {
645
+ text,
646
+ thinking
647
+ };
648
+ }
649
+ };
650
+ return {
651
+ push(delta) {
652
+ buffer += delta;
653
+ return consume();
654
+ },
655
+ flush() {
656
+ const rest = buffer;
657
+ buffer = "";
658
+ return inThink ? {
659
+ text: "",
660
+ thinking: rest
661
+ } : {
662
+ text: rest,
663
+ thinking: ""
664
+ };
665
+ }
666
+ };
667
+ }
668
+ /** 非流式整段切分:返回正文与思考正文(无 `<think>` 时 thinking 为 undefined) */
669
+ function splitThinkTags(content) {
670
+ if (!content.includes(OPEN)) return { text: content };
671
+ const splitter = createThinkTagSplitter();
672
+ const first = splitter.push(content);
673
+ const last = splitter.flush();
674
+ const thinking = first.thinking + last.thinking;
675
+ return {
676
+ text: first.text + last.text,
677
+ ...thinking !== "" ? { thinking } : {}
678
+ };
679
+ }
610
680
  const dataUrl = (part) => `data:${part.mimeType};base64,${part.data}`;
611
681
  /** parts → OpenAI content;纯文本折叠成字符串(兼容端点对数组形态支持不一) */
612
682
  const toOpenAiContent = (parts, where) => {
@@ -712,6 +782,7 @@ var OpenAiCompatibleClient = class {
712
782
  const decoder = new TextDecoder();
713
783
  const reader = res.body.getReader();
714
784
  const frames = createSseFrameReader();
785
+ const think = createThinkTagSplitter();
715
786
  let done = false;
716
787
  let usage;
717
788
  const handleFrame = function* (data) {
@@ -736,10 +807,17 @@ var OpenAiCompatibleClient = class {
736
807
  type: "thinking-delta",
737
808
  delta: delta["reasoning_content"]
738
809
  };
739
- if (typeof delta["content"] === "string" && delta["content"] !== "") yield {
740
- type: "text-delta",
741
- delta: delta["content"]
742
- };
810
+ if (typeof delta["content"] === "string" && delta["content"] !== "") {
811
+ const split = think.push(delta["content"]);
812
+ if (split.thinking !== "") yield {
813
+ type: "thinking-delta",
814
+ delta: split.thinking
815
+ };
816
+ if (split.text !== "") yield {
817
+ type: "text-delta",
818
+ delta: split.text
819
+ };
820
+ }
743
821
  const toolDeltas = delta["tool_calls"];
744
822
  for (const td of toolDeltas ?? []) {
745
823
  const index = td.index ?? 0;
@@ -767,6 +845,15 @@ var OpenAiCompatibleClient = class {
767
845
  } finally {
768
846
  reader.releaseLock();
769
847
  }
848
+ const tail = think.flush();
849
+ if (tail.thinking !== "") yield {
850
+ type: "thinking-delta",
851
+ delta: tail.thinking
852
+ };
853
+ if (tail.text !== "") yield {
854
+ type: "text-delta",
855
+ delta: tail.text
856
+ };
770
857
  if (toolCallsByIndex.size > 0) yield {
771
858
  type: "tool-calls",
772
859
  toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
@@ -822,7 +909,7 @@ var OpenAiCompatibleClient = class {
822
909
  }
823
910
  if (!res.ok) {
824
911
  const detail = await res.text().catch(() => "");
825
- throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`);
912
+ throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
826
913
  }
827
914
  return res;
828
915
  }
@@ -854,15 +941,21 @@ var OpenAiCompatibleClient = class {
854
941
  ...parseError ? { argumentsParseError: parseError } : {}
855
942
  };
856
943
  });
857
- const content = choice["content"];
858
- const thinking = choice["reasoning_content"];
944
+ const raw = choice["content"];
945
+ const split = typeof raw === "string" ? splitThinkTags(raw) : {
946
+ text: "",
947
+ thinking: void 0
948
+ };
949
+ const content = split.text;
950
+ const declared = choice["reasoning_content"];
951
+ const thinking = typeof declared === "string" && declared !== "" ? declared : split.thinking;
859
952
  const usageRaw = data.usage;
860
953
  const usage = usageRaw && (typeof usageRaw["prompt_tokens"] === "number" || typeof usageRaw["completion_tokens"] === "number") ? {
861
954
  inputTokens: typeof usageRaw["prompt_tokens"] === "number" ? usageRaw["prompt_tokens"] : 0,
862
955
  outputTokens: typeof usageRaw["completion_tokens"] === "number" ? usageRaw["completion_tokens"] : 0
863
956
  } : void 0;
864
957
  return {
865
- content: typeof content === "string" && content !== "" ? textParts(content) : void 0,
958
+ content: content !== "" ? textParts(content) : void 0,
866
959
  toolCalls: toolCalls?.length ? toolCalls : void 0,
867
960
  ...typeof thinking === "string" && thinking !== "" ? { thinking } : {},
868
961
  ...usage ? { usage } : {},
@@ -1123,7 +1216,7 @@ var AnthropicClient = class {
1123
1216
  }
1124
1217
  if (!res.ok) {
1125
1218
  const detail = await res.text().catch(() => "");
1126
- throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`);
1219
+ throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
1127
1220
  }
1128
1221
  return res;
1129
1222
  }
@@ -1388,7 +1481,7 @@ var GoogleGenAiClient = class {
1388
1481
  }
1389
1482
  if (!res.ok) {
1390
1483
  const detail = await res.text().catch(() => "");
1391
- throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`);
1484
+ throw new WebSkillError("LLM_REQUEST_FAILED", `LLM request failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 500)}` : ""}`, { status: res.status });
1392
1485
  }
1393
1486
  return res;
1394
1487
  }
@@ -2469,6 +2562,12 @@ function sampleBehaviorRecords(records, limits = DEFAULT_USER_PROFILE_LIMITS) {
2469
2562
  * 最后一句是重点:不明说「没有工具」,模型会凭训练记忆自己编造 tool_call 标记。
2470
2563
  */
2471
2564
  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.";
2565
+ /**
2566
+ * 按需披露的系统提示(分册 19 / FR-19.7)。
2567
+ * 只在本 run 确实存在按需工具时追加——没有按需工具却说「有些工具没列出来」,
2568
+ * 是在教模型怀疑一个完整的工具表。
2569
+ */
2570
+ const ON_DEMAND_TOOLS_HINT = "Some tools are disclosed on demand and are not listed above. If a capability you need seems missing, look for a related skill in the catalog and read its SKILL.md first — activating it may reveal additional tools. Do not conclude the capability does not exist.";
2472
2571
  const READ_LINKED_DOCUMENT_TOOL_NAME = "read_linked_document";
2473
2572
  /**
2474
2573
  * 能直接给模型用的 MIME。其余一律拒绝并列出这份清单——
@@ -2476,14 +2575,15 @@ const READ_LINKED_DOCUMENT_TOOL_NAME = "read_linked_document";
2476
2575
  */
2477
2576
  const SUPPORTED_DOCUMENT_MIME = {
2478
2577
  pdf: "application/pdf",
2479
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
2578
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2579
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
2480
2580
  };
2481
2581
  /** 引擎侧固定流程(§2.3)用到的错误文案,集中一处便于判据引用 */
2482
- const UNSUPPORTED_DOCUMENT_MESSAGE = `Only PDF (${SUPPORTED_DOCUMENT_MIME.pdf}), Word (${SUPPORTED_DOCUMENT_MIME.docx}) and plain text documents can be read.`;
2582
+ const UNSUPPORTED_DOCUMENT_MESSAGE = `Only PDF (${SUPPORTED_DOCUMENT_MIME.pdf}), Word (${SUPPORTED_DOCUMENT_MIME.docx}), Excel (${SUPPORTED_DOCUMENT_MIME.xlsx}) and plain text documents can be read.`;
2483
2583
  /**
2484
2584
  * 内建工具:读页面链接指向的文档。
2485
2585
  *
2486
- * **始终注册**(只要宿主装配了 reader):docx 与纯文本这条路对所有模型成立。
2586
+ * **始终注册**(只要宿主装配了 reader):docx / xlsx 与纯文本这条路对所有模型成立。
2487
2587
  * PDF 目标在模型不支持文档时**取数后拒绝**,而不是入口就拦 ——
2488
2588
  * 入口拦会误伤 docx,它抽成文本后根本不需要文档能力。
2489
2589
  */
@@ -2506,6 +2606,10 @@ function toBase64(bytes) {
2506
2606
  for (const byte of bytes) binary += String.fromCharCode(byte);
2507
2607
  return btoa(binary);
2508
2608
  }
2609
+ /** `toBase64` 的逆;分片里的文档要交回抽取器时用(0.13.0 FR-22.3) */
2610
+ function fromBase64(data) {
2611
+ return Uint8Array.from(atob(data), (char) => char.charCodeAt(0));
2612
+ }
2509
2613
  /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
2510
2614
  var TraceRecorder = class {
2511
2615
  #runId;
@@ -2745,6 +2849,30 @@ function evaluateToolAccess(state, llmToolName) {
2745
2849
  };
2746
2850
  }
2747
2851
  /**
2852
+ * 分册 19 诊断(FR-19.6):某条模式在给定工具名集合里有没有命中过。
2853
+ * 与点名判定分开,是因为这里问的是「作者写的这条规则有没有意义」,不是「某个工具能不能进上下文」。
2854
+ */
2855
+ function patternMatchesAnyTool(pattern, declaringSkill, llmToolNames, activated) {
2856
+ for (const name of llmToolNames) if (matchesPattern(pattern, declaringSkill, name, canonicalToolName(name, activated))) return true;
2857
+ return false;
2858
+ }
2859
+ /**
2860
+ * 分册 19 的点名判定:**有没有某个已激活技能显式声明了这个工具**。
2861
+ *
2862
+ * 与 `evaluateToolAccess` 回答的不是同一个问题(那个回答「允不允许调用」,
2863
+ * 这个回答「要不要写进上下文」),因此故意不复用它,两处关键差异:
2864
+ * 1. 无人声明 ⇒ 这里返回 **false**(没人点名),那里返回 true(不受限);
2865
+ * 2. 不含一票否决——某个技能没写清单,不影响另一个技能点名成功。
2866
+ */
2867
+ function isNamedByActivatedSkill(state, llmToolName) {
2868
+ const canonical = canonicalToolName(llmToolName, state.activated);
2869
+ for (const [skill, patterns] of state.skillAllowedTools) {
2870
+ if (!state.activated.has(skill)) continue;
2871
+ for (const pattern of patterns) if (matchesPattern(pattern, skill, llmToolName, canonical)) return true;
2872
+ }
2873
+ return false;
2874
+ }
2875
+ /**
2748
2876
  * 参数敏感标注关键字(分册 30 / FR-30.3)。
2749
2877
  *
2750
2878
  * 不用 `format: 'password'`:`format` 是 JSON Schema 的规范关键字,provider 侧
@@ -2818,6 +2946,30 @@ const LLM_TURN_CODES = /* @__PURE__ */ new Set([
2818
2946
  "LLM_REQUEST_FAILED",
2819
2947
  "TOOL_SCHEMA_UNAVAILABLE"
2820
2948
  ]);
2949
+ /**
2950
+ * 模型侧图片通道收得下的位图格式(三家 provider 的交集)。
2951
+ * SVG 是文本、`application/octet-stream` 是未知字节——两者编成 data URL 发出去,
2952
+ * 端点当场 400(LM Studio:`'url' field must be a base64 encoded image.`),
2953
+ * 页面里一个内联图标就能把整次 run 打死。这不是「预判模型能不能读」,
2954
+ * 是这些字节根本进不了图片分片。
2955
+ */
2956
+ const MODEL_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
2957
+ "image/png",
2958
+ "image/jpeg",
2959
+ "image/webp",
2960
+ "image/gif"
2961
+ ]);
2962
+ /** 分片是不是 PDF;mime 允许带参数与大小写差异,与 FR-11.1 的判定同口径 */
2963
+ const isPdfPart = (part) => part.type === "file" && part.mimeType.split(";")[0]?.trim().toLowerCase() === SUPPORTED_DOCUMENT_MIME.pdf;
2964
+ /**
2965
+ * LLM 客户端在 HTTP 失败时放进 `details` 的状态码(FR-22.2)。
2966
+ * 判不出来返回 undefined——网络层异常没有状态码,回退对它没有意义。
2967
+ */
2968
+ const httpStatusOf = (e) => {
2969
+ if (!(e instanceof WebSkillError) || typeof e.details !== "object" || e.details === null) return void 0;
2970
+ const status = e.details.status;
2971
+ return typeof status === "number" ? status : void 0;
2972
+ };
2821
2973
  /** 交互终态(取消/超时):从工具执行深处直接终止 run */
2822
2974
  var RunTerminated = class extends Error {
2823
2975
  outcome;
@@ -2842,6 +2994,16 @@ const summarizeArgs = (args) => {
2842
2994
  const json = JSON.stringify(args);
2843
2995
  return json.length > 100 ? `${json.slice(0, 100)}…` : json;
2844
2996
  };
2997
+ /**
2998
+ * 图片分片投不出去时换成一条说明(与超预算文档同一处置),多张合并成一条——
2999
+ * 一个页面上十几个内联图标各报一遍会把工具结果淹掉。
3000
+ * 静默丢掉会让模型以为自己看过了;直接发出去则整次请求 400,一张图连累全程。
3001
+ */
3002
+ const undeliverableImageNote = (parts, toolName) => {
3003
+ const formats = [...new Set(parts.map((p) => p.type === "image" ? p.mimeType : ""))].join(", ");
3004
+ return `${parts.length === 1 ? "1 image" : `${parts.length} images`} from "${toolName}" ${parts.length === 1 ? "was" : "were"} not attached: ${formats} cannot be sent as model image input, which accepts only ${[...MODEL_IMAGE_MIME_TYPES].join(", ")}. Rely on the surrounding text, or ask for a raster version.`;
3005
+ };
3006
+ const isDeliverableImage = (part) => part.type !== "image" || MODEL_IMAGE_MIME_TYPES.has(part.mimeType.split(";")[0]?.trim().toLowerCase() ?? "");
2845
3007
  /** 外部工具的 inputSchema 索引(分册 30);run 开始与 resume 各重建一次 */
2846
3008
  function indexExternalToolSchemas(state, specs) {
2847
3009
  state.externalToolSchemas.clear();
@@ -2972,6 +3134,9 @@ var AgentLoop = class {
2972
3134
  activatedTools: /* @__PURE__ */ new Map(),
2973
3135
  skillAllowedTools: /* @__PURE__ */ new Map(),
2974
3136
  warnedDeniedTools: /* @__PURE__ */ new Set(),
3137
+ externalToolSources: /* @__PURE__ */ new Map(),
3138
+ withheldTools: /* @__PURE__ */ new Set(),
3139
+ pdfFallbackDone: false,
2975
3140
  integrityVerdicts: /* @__PURE__ */ new Map(),
2976
3141
  toolTimeoutMs: this.#config.toolTimeoutMs,
2977
3142
  now,
@@ -3024,7 +3189,19 @@ var AgentLoop = class {
3024
3189
  trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
3025
3190
  }
3026
3191
  }
3027
- const systemPrompt = this.#config.toolCallingDisabled ? PLAIN_CHAT_SYSTEM_PROMPT : [route.systemPrompt, ...externalSystemPrompts].join("\n\n");
3192
+ const hasOnDemand = !this.#config.toolCallingDisabled && externalSpecs.some((spec) => {
3193
+ const source = state.externalToolSources.get(spec.name);
3194
+ try {
3195
+ return source?.disclosure?.(spec.name) === "on-demand";
3196
+ } catch {
3197
+ return false;
3198
+ }
3199
+ });
3200
+ const systemPrompt = this.#config.toolCallingDisabled ? PLAIN_CHAT_SYSTEM_PROMPT : [
3201
+ route.systemPrompt,
3202
+ ...externalSystemPrompts,
3203
+ ...hasOnDemand ? [ON_DEMAND_TOOLS_HINT] : []
3204
+ ].join("\n\n");
3028
3205
  const profileMessage = await this.#userProfileMessage(state);
3029
3206
  state.messages = [
3030
3207
  {
@@ -3083,6 +3260,44 @@ var AgentLoop = class {
3083
3260
  }
3084
3261
  return false;
3085
3262
  }
3263
+ /**
3264
+ * 分册 19 暴露点:外部工具本轮是否披露。
3265
+ *
3266
+ * 与 `#checkToolAccess` 是两个问题:那个回答「允不允许调用」,这个只回答「要不要写进上下文」。
3267
+ * 未披露**不影响可执行性**——分发点不看它,模型硬调照样执行(FR-19.5)。
3268
+ */
3269
+ #disclosed(state, llmToolName) {
3270
+ const source = state.externalToolSources.get(llmToolName);
3271
+ if (source?.disclosure === void 0) return true;
3272
+ let level;
3273
+ try {
3274
+ level = source.disclosure(llmToolName);
3275
+ } catch (e) {
3276
+ state.trace.record("run.warning", { message: `External tool source "${source.kind}" failed to report a disclosure level for "${llmToolName}"; treating it as always disclosed: ${messageOf(e)}` });
3277
+ return true;
3278
+ }
3279
+ if (level !== "on-demand") return true;
3280
+ if (isNamedByActivatedSkill(state, llmToolName)) return true;
3281
+ if (!state.withheldTools.has(llmToolName)) {
3282
+ state.withheldTools.add(llmToolName);
3283
+ state.trace.record("tool.withheld", {
3284
+ message: `Tool "${llmToolName}" is disclosed on demand and was not requested by any activated skill`,
3285
+ data: { name: llmToolName }
3286
+ });
3287
+ }
3288
+ return false;
3289
+ }
3290
+ /**
3291
+ * FR-19.6:技能声明的外部工具模式一个已知工具都没命中时告警。
3292
+ * 端点名拼错是最常见的失误,而它的表现是「工具静静地不出现」——不告警就无从排查。
3293
+ */
3294
+ #warnUnmatchedToolPatterns(state, skillName, patterns) {
3295
+ for (const pattern of patterns) {
3296
+ if (!pattern.includes(":") && !pattern.includes("#")) continue;
3297
+ if (patternMatchesAnyTool(pattern, skillName, state.externalToolSources.keys(), state.activated)) continue;
3298
+ state.trace.record("run.warning", { message: `Skill "${skillName}" declares allowed-tools pattern "${pattern}", but it matches no known tool.` });
3299
+ }
3300
+ }
3086
3301
  /** 分发点被拒时回喂给模型的结构化错误(不抛异常:模型造名字是常态,抛异常会终止整个 run) */
3087
3302
  #deniedToolError(state, toolName) {
3088
3303
  return toolError("TOOL_NOT_ALLOWED", evaluateToolAccess(state, toolName).reason ?? `Tool "${toolName}" is not allowed`);
@@ -3098,7 +3313,9 @@ var AgentLoop = class {
3098
3313
  state.turn = turn;
3099
3314
  if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
3100
3315
  if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
3101
- const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
3316
+ const scriptToolSpecs = [...state.activatedTools.values()].map(toLlmToolSpec).filter((spec) => this.#checkToolAccess(state, spec.name));
3317
+ const externalToolSpecs = externalSpecs.filter((spec) => this.#disclosed(state, spec.name));
3318
+ const skillToolSpecs = [...scriptToolSpecs, ...externalToolSpecs];
3102
3319
  const toolSpecs = this.#config.toolCallingDisabled ? [] : [
3103
3320
  toLlmToolSpec(READ_SKILL_FILE_TOOL),
3104
3321
  ...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
@@ -3130,8 +3347,12 @@ var AgentLoop = class {
3130
3347
  if (this.#cancelled.has(state.runId)) return finish("cancelled", "user-cancelled", "Run cancelled by user", "RUN_CANCELLED");
3131
3348
  return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
3132
3349
  }
3350
+ if (await this.#fallbackPdfToText(state, e)) {
3351
+ turn--;
3352
+ continue;
3353
+ }
3133
3354
  const code = e instanceof WebSkillError && LLM_TURN_CODES.has(e.code) ? e.code : "LLM_REQUEST_FAILED";
3134
- const hint = messages.some((message) => Array.isArray(message.content) && message.content.some((part) => part.type === "file")) ? " This turn attached a document; the selected model may not accept document input. Try another model, or link a Word or text file instead." : "";
3355
+ const hint = state.pdfFallbackDone ? " The attached PDF was re-sent as extracted text after the endpoint rejected the original file, and that attempt failed as well." : messages.some((message) => message.content.some(isPdfPart)) ? " This turn attached a document; the selected model may not accept document input. Try another model, or link a Word or text file instead." : "";
3135
3356
  return finish("failed", "llm-error", `${messageOf(e)}${hint}`, code);
3136
3357
  }
3137
3358
  if (response.thinking !== void 0 && response.thinking !== "") {
@@ -3364,6 +3585,9 @@ var AgentLoop = class {
3364
3585
  activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
3365
3586
  skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
3366
3587
  warnedDeniedTools: /* @__PURE__ */ new Set(),
3588
+ externalToolSources: /* @__PURE__ */ new Map(),
3589
+ withheldTools: /* @__PURE__ */ new Set(),
3590
+ pdfFallbackDone: false,
3367
3591
  integrityVerdicts: /* @__PURE__ */ new Map(),
3368
3592
  toolTimeoutMs: snapshot.config.toolTimeoutMs,
3369
3593
  now,
@@ -3686,9 +3910,12 @@ var AgentLoop = class {
3686
3910
  }
3687
3911
  /** 外部工具 specs:单个来源失败跳过并记 warning(run 开始与 resume 共用) */
3688
3912
  async #collectExternalSpecs(state) {
3913
+ state.externalToolSources.clear();
3689
3914
  return (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
3690
3915
  try {
3691
- return await source.listToolSpecs();
3916
+ const specs = await source.listToolSpecs();
3917
+ for (const spec of specs) state.externalToolSources.set(spec.name, source);
3918
+ return specs;
3692
3919
  } catch (e) {
3693
3920
  state.trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
3694
3921
  return [];
@@ -3776,17 +4003,15 @@ var AgentLoop = class {
3776
4003
  else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
3777
4004
  else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
3778
4005
  else if (call.name === "read_linked_document" && this.#deps.linkedDocuments !== void 0) result = await this.#handleReadLinkedDocument(call, state);
3779
- else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
3780
4006
  else {
3781
4007
  const resolution = resolveToolName(call.name, state.activated, state.activatedTools.keys());
3782
- if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
4008
+ const source = resolution.kind === "script" ? void 0 : (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
4009
+ if (source === void 0 && !this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
4010
+ else if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
4011
+ else if (source) result = await source.call(call.name, call.arguments);
3783
4012
  else {
3784
- const source = (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
3785
- if (source) result = await source.call(call.name, call.arguments);
3786
- else {
3787
- if (resolution.kind === "not-found") state.unknownToolCalls += 1;
3788
- result = toolError(resolution.code, resolution.message, resolution.kind === "not-found" ? resolution.data : void 0);
3789
- }
4013
+ if (resolution.kind === "not-found") state.unknownToolCalls += 1;
4014
+ result = toolError(resolution.code, resolution.message, resolution.kind === "not-found" ? resolution.data : void 0);
3790
4015
  }
3791
4016
  }
3792
4017
  const durationMs = Date.parse(state.now()) - callStartMs;
@@ -4177,17 +4402,20 @@ var AgentLoop = class {
4177
4402
  };
4178
4403
  }
4179
4404
  let text;
4180
- if (mime === SUPPORTED_DOCUMENT_MIME.docx) {
4181
- if (this.#deps.docxExtractor === void 0) {
4405
+ if (mime === SUPPORTED_DOCUMENT_MIME.docx || mime === SUPPORTED_DOCUMENT_MIME.xlsx) {
4406
+ const docx = mime === SUPPORTED_DOCUMENT_MIME.docx;
4407
+ const kind = docx ? "docx" : "xlsx";
4408
+ const extractor = docx ? this.#deps.docxExtractor : this.#deps.xlsxExtractor;
4409
+ if (extractor === void 0) {
4182
4410
  await audit({
4183
4411
  ...record,
4184
4412
  ok: false,
4185
- reason: "no docx extractor"
4413
+ reason: `no ${kind} extractor`
4186
4414
  });
4187
- return toolError("TOOL_UNSUPPORTED", "Word documents cannot be read in this environment: no docx text extractor is configured.");
4415
+ return toolError("TOOL_UNSUPPORTED", `${docx ? "Word documents" : "Excel workbooks"} cannot be read in this environment: no ${kind} text extractor is configured.`);
4188
4416
  }
4189
4417
  try {
4190
- text = await this.#deps.docxExtractor(fetched.bytes);
4418
+ text = await extractor(fetched.bytes);
4191
4419
  } catch (e) {
4192
4420
  const message = messageOf(e);
4193
4421
  await audit({
@@ -4398,6 +4626,57 @@ var AgentLoop = class {
4398
4626
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
4399
4627
  }
4400
4628
  /**
4629
+ * 端点拒收 PDF 后,把消息里的 PDF 分片换成抽取文本,让本轮可以重发(FR-22.2 / FR-22.3)。
4630
+ * 返回 true 表示已改写、调用方应重试本轮;返回 false 表示走原有失败路径。
4631
+ */
4632
+ async #fallbackPdfToText(state, cause) {
4633
+ const extract = this.#deps.pdfExtractor;
4634
+ if (extract === void 0 || state.pdfFallbackDone) return false;
4635
+ const status = httpStatusOf(cause);
4636
+ if (status === void 0 || status < 400 || status >= 500) return false;
4637
+ if (!state.messages.some((message) => message.content.some(isPdfPart))) return false;
4638
+ state.pdfFallbackDone = true;
4639
+ let converted = 0;
4640
+ for (const message of state.messages) for (let i = 0; i < message.content.length; i++) {
4641
+ const part = message.content[i];
4642
+ if (part === void 0 || !isPdfPart(part)) continue;
4643
+ message.content[i] = {
4644
+ type: "text",
4645
+ text: await this.#pdfAsText(state, part, extract)
4646
+ };
4647
+ converted++;
4648
+ }
4649
+ state.trace.record("run.warning", {
4650
+ message: `The endpoint rejected ${converted} attached PDF file(s) with HTTP ${status}; they were replaced with extracted text and the turn was retried. Original error: ${messageOf(cause)}`,
4651
+ data: {
4652
+ converted,
4653
+ status
4654
+ }
4655
+ });
4656
+ return converted > 0;
4657
+ }
4658
+ /** 单个 PDF 分片 → 文本;包装形态与 chatEngine 的附件标签同口径(FR-22.3 / FR-22.4) */
4659
+ async #pdfAsText(state, part, extract) {
4660
+ const name = part.name ?? "document.pdf";
4661
+ const label = (note) => `--- Attachment: ${name} (${SUPPORTED_DOCUMENT_MIME.pdf}, ${note}) ---\n`;
4662
+ let text;
4663
+ try {
4664
+ text = await extract(fromBase64(part.data));
4665
+ } catch (e) {
4666
+ return `${label("text extraction failed")}${messageOf(e)}`;
4667
+ }
4668
+ if (text.trim() === "") return `${label("no extractable text")}This PDF has no text layer, so its contents could not be extracted. It is most likely a scan or an image-only export. Tell the user this instead of guessing what it contains.`;
4669
+ const id = `doc-${state.runId}-${++state.documentSeq}`;
4670
+ const tooLarge = this.#documentTooLarge({
4671
+ type: "document-text",
4672
+ text,
4673
+ name,
4674
+ id
4675
+ });
4676
+ if (tooLarge !== void 0) return `${label("not attached")}${tooLarge}`;
4677
+ return `${label("extracted text")}${text}`;
4678
+ }
4679
+ /**
4401
4680
  * 超预算的文档分片换成一条说明(FR-23.4)。**不截断**:
4402
4681
  * 半份 PDF 是坏文件,半份抽取文本会让模型以为自己读全了。
4403
4682
  */
@@ -4441,6 +4720,7 @@ var AgentLoop = class {
4441
4720
  };
4442
4721
  const notes = [];
4443
4722
  const carried = [];
4723
+ const undeliverable = [];
4444
4724
  for (const part of passthrough) {
4445
4725
  const tooLarge = this.#documentTooLarge(part);
4446
4726
  if (tooLarge) {
@@ -4448,6 +4728,10 @@ var AgentLoop = class {
4448
4728
  notes.push(tooLarge);
4449
4729
  continue;
4450
4730
  }
4731
+ if (!isDeliverableImage(part)) {
4732
+ undeliverable.push(part);
4733
+ continue;
4734
+ }
4451
4735
  if (part.type === "image") carried.push({
4452
4736
  type: "image",
4453
4737
  mimeType: part.mimeType,
@@ -4461,6 +4745,11 @@ var AgentLoop = class {
4461
4745
  });
4462
4746
  else if (part.type === "document-text") notes.push(part.text);
4463
4747
  }
4748
+ if (undeliverable.length > 0) {
4749
+ const note = undeliverableImageNote(undeliverable, call.name);
4750
+ state.trace.record("run.warning", { message: note });
4751
+ notes.push(note);
4752
+ }
4464
4753
  return {
4465
4754
  tool: [
4466
4755
  {
@@ -4577,6 +4866,7 @@ var AgentLoop = class {
4577
4866
  if (rawAllowed !== void 0) if (Array.isArray(rawAllowed)) {
4578
4867
  allowedTools = rawAllowed.filter((e) => typeof e === "string");
4579
4868
  state.skillAllowedTools.set(skillName, allowedTools);
4869
+ this.#warnUnmatchedToolPatterns(state, skillName, allowedTools);
4580
4870
  } else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
4581
4871
  } catch (e) {
4582
4872
  state.trace.record("run.warning", { message: `Failed to read or parse SKILL.md of skill "${skillName}": ${messageOf(e)}` });
@@ -5140,6 +5430,8 @@ var WebSkillRuntime = class {
5140
5430
  fetchData: this.#deps.fetchData,
5141
5431
  linkedDocuments: this.#deps.linkedDocuments,
5142
5432
  docxExtractor: this.#deps.docxExtractor,
5433
+ xlsxExtractor: this.#deps.xlsxExtractor,
5434
+ pdfExtractor: this.#deps.pdfExtractor,
5143
5435
  documentAudit: this.#deps.documentAudit
5144
5436
  }, this.#deps.config);
5145
5437
  const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
@@ -5266,6 +5558,8 @@ var WebSkillRuntime = class {
5266
5558
  fetchData: this.#deps.fetchData,
5267
5559
  linkedDocuments: this.#deps.linkedDocuments,
5268
5560
  docxExtractor: this.#deps.docxExtractor,
5561
+ xlsxExtractor: this.#deps.xlsxExtractor,
5562
+ pdfExtractor: this.#deps.pdfExtractor,
5269
5563
  documentAudit: this.#deps.documentAudit
5270
5564
  }, this.#deps.config);
5271
5565
  this.#loops.set(runId, loop);
@@ -1,5 +1,5 @@
1
1
  import { M as messageOf, P as parseSkillMarkdown, g as assertRemoteUrlAllowed, h as WebSkillError, k as isValidSkillName } from "./dist-Bev6i6Ip.js";
2
- import { f as DEFAULT_MAX_DATA_SOURCE_BYTES } from "./dist-sdKFgERo.js";
2
+ import { f as DEFAULT_MAX_DATA_SOURCE_BYTES } from "./dist-ExSQky4C.js";
3
3
 
4
4
  //#region ../agent/dist/index.js
5
5
  const STATUSES = [
@@ -875,6 +875,30 @@ function withDelegationOrigin(bridge, origin) {
875
875
  };
876
876
  }
877
877
  /**
878
+ * 帧指称的展示形式(分册 18 FR-18.2)。
879
+ *
880
+ * 感知与操作两侧各自声明自己的 scope 类型(那是刻意的,见各自 types.ts),
881
+ * 但「一条帧路径写成给人看的字符串」只能有一份实现——
882
+ * 两处各写一遍,审计里的帧名和确认卡里的帧名就会慢慢对不上。
883
+ */
884
+ /**
885
+ * 帧路径的稳定展示串:`'self'` / `'#a'` / `'#a >>> #b'`。
886
+ *
887
+ * `>>>` **只是展示分隔符**:CSS 选择器里可以合法出现任意字符,
888
+ * 反向解析这个串取回路径是不成立的,实现层一律传原始形状(D-18-1)。
889
+ * @experimental
890
+ */
891
+ function frameLabel(frame) {
892
+ if (typeof frame === "string") return frame;
893
+ if (frame.length === 0) return "self";
894
+ return frame.join(" >>> ");
895
+ }
896
+ /** 把两种形状归一成逐层选择器数组;`'self'` 与空数组都归一成空数组 @experimental */
897
+ function frameSteps(frame) {
898
+ if (typeof frame === "string") return frame === "self" ? [] : [frame];
899
+ return frame;
900
+ }
901
+ /**
878
902
  * 两种形状归一成帧列表;旧形状等价于一条 `frame:'self'`(FR-24.1)。
879
903
  *
880
904
  * **这是判别的单一来源。** 散在各处写 `'frames' in scope` 会让
@@ -937,7 +961,7 @@ var PagePerceptionPolicy = class {
937
961
  at: this.#options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
938
962
  include: frames.flatMap((frame) => [...frame.include]),
939
963
  exclude: frames.flatMap((frame) => [...frame.exclude ?? []]),
940
- ...frames.length > 1 || frames[0]?.frame !== "self" ? { frames: frames.map((frame) => frame.frame) } : {},
964
+ ...frames.length > 1 || frames[0] !== void 0 && frameLabel(frames[0].frame) !== "self" ? { frames: frames.map((frame) => frameLabel(frame.frame)) } : {},
941
965
  nodeCount: nodes.length,
942
966
  ...capture?.images === true ? {
943
967
  images: {
@@ -1035,7 +1059,8 @@ function createPagePerceptionToolSource(options) {
1035
1059
  const capture = {
1036
1060
  images: budget?.enabled === true,
1037
1061
  maxImageBytes: budget?.maxImageBytes ?? 0,
1038
- maxImages: budget?.maxImages ?? 0
1062
+ maxImages: budget?.maxImages ?? 0,
1063
+ minImageArea: budget?.minImageArea ?? 0
1039
1064
  };
1040
1065
  const { nodes, images, imagesOmitted, record } = await policy.perceive(capture);
1041
1066
  const data = {
@@ -1465,4 +1490,4 @@ function createPageActionToolSource(options) {
1465
1490
  }
1466
1491
 
1467
1492
  //#endregion
1468
- export { createTodoToolSource as C, withDelegationOrigin as E, createSkillGenerationToolSource as S, toFrameScopes as T, TodoStore as _, GENERATE_SKILL_TOOL as a, createPageActionToolSource as b, PAGE_ACTION_SYSTEM_PROMPT as c, PERCEPTION_SYSTEM_PROMPT as d, PageActionPolicy as f, TODO_SYSTEM_PROMPT as g, SkillGenerator as h, DelegationOrchestrator as i, PAGE_ACTION_TOOL as l, SKILL_GENERATION_SYSTEM_PROMPT as m, DELEGATION_SYSTEM_PROMPT as n, MANAGE_TODO_TOOL as o, PagePerceptionPolicy as p, DataSourcePolicy as r, PAGE_ACTION_KINDS as s, DELEGATE_TASK_TOOL as t, PERCEIVE_PAGE_TOOL as u, createDelegationToolSource as v, toActionFrameScopes as w, createPagePerceptionToolSource as x, createHttpDataSourceTransport as y };
1493
+ export { createTodoToolSource as C, toFrameScopes as D, toActionFrameScopes as E, withDelegationOrigin as O, createSkillGenerationToolSource as S, frameSteps as T, TodoStore as _, GENERATE_SKILL_TOOL as a, createPageActionToolSource as b, PAGE_ACTION_SYSTEM_PROMPT as c, PERCEPTION_SYSTEM_PROMPT as d, PageActionPolicy as f, TODO_SYSTEM_PROMPT as g, SkillGenerator as h, DelegationOrchestrator as i, PAGE_ACTION_TOOL as l, SKILL_GENERATION_SYSTEM_PROMPT as m, DELEGATION_SYSTEM_PROMPT as n, MANAGE_TODO_TOOL as o, PagePerceptionPolicy as p, DataSourcePolicy as r, PAGE_ACTION_KINDS as s, DELEGATE_TASK_TOOL as t, PERCEIVE_PAGE_TOOL as u, createDelegationToolSource as v, frameLabel as w, createPagePerceptionToolSource as x, createHttpDataSourceTransport as y };
@@ -1,5 +1,5 @@
1
1
  import { R as FileSystemProvider, S as UiBridge, U as Page, W as PageQuery, f as LlmMessage, it as SkillCatalogEntry, l as LlmClient, ot as SkillDocument, ut as SkillManagerPort } from "./types-DLctJep_-B5G4uk2u.js";
2
- import { Nt as RuntimeRun, Pn as WebSkillRuntime, Qt as SkillOutcomeReporter, Y as IntegrityVerdict, Zt as SkillIntegrityGuard, nn as SkillStateGuard } from "./index-C9pzXLKy.js";
2
+ import { $t as SkillOutcomeReporter, In as WebSkillRuntime, Pt as RuntimeRun, Qt as SkillIntegrityGuard, Y as IntegrityVerdict, rn as SkillStateGuard } from "./index-DkvBhJQy.js";
3
3
  import { _ as SkillState, a as AuditLog, b as SkillVersionStore, c as CandidateFile, d as CandidateSkill, f as CandidateSource, g as SKILL_VERSION_PAGE_SIZE, h as CompositeApprovalPolicy, i as AuditEvent, l as CandidatePage, m as CandidateStore, n as ApprovalDecision, o as AuditQueryFilter, p as CandidateStatus, r as ApprovalPolicy, s as CANDIDATE_PAGE_SIZE, t as AlwaysHumanApprovalPolicy, u as CandidateRisk, v as SkillVersion, x as candidateToCatalogEntry, y as SkillVersionPage } from "./skillVersionStore-D-qHk9ZE-DheTIwAB.js";
4
4
  //#region ../governance/dist/index.d.ts
5
5
  //#region src/candidate/candidateNormalizer.d.ts
@@ -1,5 +1,5 @@
1
1
  import { B as JsonSchema, C as UiSpecActionCapability, D as UiSpecSnapshot, S as UiBridge, b as RenderResultRequest, bt as UiSpecNode, c as InteractionResponse, r as ChartSpec, s as InteractionRequest, y as RenderBlock } from "./types-DLctJep_-B5G4uk2u.js";
2
- import { F as ExternalToolSource } from "./index-C9pzXLKy.js";
2
+ import { F as ExternalToolSource } from "./index-DkvBhJQy.js";
3
3
  import { z } from "zod";
4
4
  import { ComponentType, ReactNode } from "react";
5
5
  //#region ../ui/dist/index.d.ts