@webskill/sdk 0.25.0 → 0.26.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.
package/dist/agent.js CHANGED
@@ -135,7 +135,7 @@ const STATUS_ENUM = [
135
135
  "completed"
136
136
  ];
137
137
  const TODO_TOOL_DESCRIPTION = "Maintain the visible task list for a multi-step request. The rules are in the system prompt.";
138
- const INPUT_SCHEMA$6 = {
138
+ const INPUT_SCHEMA$7 = {
139
139
  type: "object",
140
140
  properties: {
141
141
  action: {
@@ -188,7 +188,7 @@ const INPUT_SCHEMA$6 = {
188
188
  required: ["action"],
189
189
  additionalProperties: false
190
190
  };
191
- function toolError$8(code, message) {
191
+ function toolError$9(code, message) {
192
192
  return {
193
193
  ok: false,
194
194
  content: [],
@@ -237,15 +237,15 @@ function createTodoToolSource(options = {}) {
237
237
  try {
238
238
  if (action === "create") {
239
239
  const items = parseItems(args["items"]);
240
- if (typeof items === "string") return Promise.resolve(toolError$8("VALIDATION_FAILED", items));
240
+ if (typeof items === "string") return Promise.resolve(toolError$9("VALIDATION_FAILED", items));
241
241
  const list = store.create(items);
242
242
  return Promise.resolve(ok(events, progressText(list.items)));
243
243
  }
244
244
  if (action === "update") {
245
245
  const id = args["id"];
246
246
  const status = args["status"];
247
- if (typeof id !== "string") return Promise.resolve(toolError$8("VALIDATION_FAILED", "The \"id\" argument is required for action \"update\""));
248
- if (status !== void 0 && typeof status !== "string") return Promise.resolve(toolError$8("VALIDATION_FAILED", "The \"status\" argument must be a string"));
247
+ if (typeof id !== "string") return Promise.resolve(toolError$9("VALIDATION_FAILED", "The \"id\" argument is required for action \"update\""));
248
+ if (status !== void 0 && typeof status !== "string") return Promise.resolve(toolError$9("VALIDATION_FAILED", "The \"status\" argument must be a string"));
249
249
  const title = args["title"];
250
250
  const list = store.update(id, {
251
251
  ...status !== void 0 ? { status } : {},
@@ -257,10 +257,10 @@ function createTodoToolSource(options = {}) {
257
257
  store.clear();
258
258
  return Promise.resolve(ok(events, progressText([])));
259
259
  }
260
- return Promise.resolve(toolError$8("VALIDATION_FAILED", "The \"action\" argument must be one of create, update, clear"));
260
+ return Promise.resolve(toolError$9("VALIDATION_FAILED", "The \"action\" argument must be one of create, update, clear"));
261
261
  } catch (e) {
262
262
  const code = e instanceof Error && "code" in e ? String(e.code) : "VALIDATION_FAILED";
263
- return Promise.resolve(toolError$8(code, e instanceof Error ? e.message : String(e)));
263
+ return Promise.resolve(toolError$9(code, e instanceof Error ? e.message : String(e)));
264
264
  } finally {
265
265
  unsubscribe();
266
266
  }
@@ -272,7 +272,7 @@ function createTodoToolSource(options = {}) {
272
272
  listToolSpecs: () => Promise.resolve([{
273
273
  name: MANAGE_TODO_TOOL,
274
274
  description: TODO_TOOL_DESCRIPTION,
275
- inputSchema: INPUT_SCHEMA$6
275
+ inputSchema: INPUT_SCHEMA$7
276
276
  }]),
277
277
  canHandle: (name) => name === MANAGE_TODO_TOOL,
278
278
  argCaptureTrust: (name) => name === "manage_todo" ? { tier: "reviewed" } : void 0,
@@ -304,7 +304,7 @@ const SCRIPT_EXTENSIONS = [".ts", ".js"];
304
304
  function defaultConfirmSkill(draft) {
305
305
  return `Save this conversation as the skill "${draft.name}"? ${draft.description}\nIt will be submitted for review, not activated. Please check the preview below for anything that must not be stored.`;
306
306
  }
307
- function fail(message, details) {
307
+ function fail$2(message, details) {
308
308
  throw new WebSkillError("SKILL_GENERATION_VALIDATION_FAILED", message, details);
309
309
  }
310
310
  /**
@@ -312,21 +312,21 @@ function fail(message, details) {
312
312
  * 再去打扰用户——不让用户确认一个注定会失败的技能。
313
313
  */
314
314
  function validateDraft(draft) {
315
- if (!isValidSkillName(draft.name)) fail(`"${draft.name}" is not a valid skill name; use lowercase letters, digits and hyphens`);
316
- if (draft.description.trim() === "") fail("The skill description must not be empty");
317
- if (typeof draft.content !== "string" || draft.content.trim() === "") fail("The SKILL.md content must not be empty");
315
+ if (!isValidSkillName(draft.name)) fail$2(`"${draft.name}" is not a valid skill name; use lowercase letters, digits and hyphens`);
316
+ if (draft.description.trim() === "") fail$2("The skill description must not be empty");
317
+ if (typeof draft.content !== "string" || draft.content.trim() === "") fail$2("The SKILL.md content must not be empty");
318
318
  let metadata;
319
319
  try {
320
320
  metadata = parseSkillMarkdown(draft.content).metadata;
321
321
  } catch (e) {
322
- fail(`The SKILL.md content is not a valid skill document: ${e instanceof Error ? e.message : String(e)}`);
322
+ fail$2(`The SKILL.md content is not a valid skill document: ${e instanceof Error ? e.message : String(e)}`);
323
323
  }
324
- if (metadata.name !== draft.name) fail(`The SKILL.md frontmatter declares name "${metadata.name}" but the draft name is "${draft.name}"`);
324
+ if (metadata.name !== draft.name) fail$2(`The SKILL.md frontmatter declares name "${metadata.name}" but the draft name is "${draft.name}"`);
325
325
  for (const file of draft.files ?? []) {
326
326
  const path = file.path;
327
- if (path.trim() === "") fail("Every attached file needs a path");
328
- if (path.startsWith("/") || path.includes("\\") || path.split("/").includes("..")) fail(`The attached file path "${path}" must be relative and must not escape the skill folder`);
329
- if (path.startsWith("scripts/") && !SCRIPT_EXTENSIONS.some((ext) => path.endsWith(ext))) fail(`The script "${path}" must end with .ts or .js`);
327
+ if (path.trim() === "") fail$2("Every attached file needs a path");
328
+ if (path.startsWith("/") || path.includes("\\") || path.split("/").includes("..")) fail$2(`The attached file path "${path}" must be relative and must not escape the skill folder`);
329
+ if (path.startsWith("scripts/") && !SCRIPT_EXTENSIONS.some((ext) => path.endsWith(ext))) fail$2(`The script "${path}" must end with .ts or .js`);
330
330
  }
331
331
  }
332
332
  /** 确认面板正文:完整 SKILL.md + 附带文件,供用户逐字检查(FR-9.4 风险缓解) */
@@ -358,25 +358,25 @@ async function materializeBinaries(draft, options) {
358
358
  const out = /* @__PURE__ */ new Map();
359
359
  if (claimed.length === 0) return out;
360
360
  const resolve = options.resolveAttachment;
361
- if (resolve === void 0) fail("This host cannot put uploaded files into a skill. Remove \"attachmentId\" from the file entries and write their contents instead.");
361
+ if (resolve === void 0) fail$2("This host cannot put uploaded files into a skill. Remove \"attachmentId\" from the file entries and write their contents instead.");
362
362
  const maxOne = options.policy?.maxBinaryBytes ?? DEFAULT_MAX_BINARY_BYTES;
363
363
  const maxTotal = options.policy?.maxTotalBinaryBytes ?? DEFAULT_MAX_TOTAL_BINARY_BYTES;
364
364
  let total = 0;
365
365
  for (const file of claimed) {
366
366
  const id = file.attachmentId;
367
367
  const attachment = await resolve(id);
368
- if (attachment === void 0) fail(`No file with attachment id "${id}" was uploaded in this conversation, so "${file.path}" cannot be added to the skill.`, {
368
+ if (attachment === void 0) fail$2(`No file with attachment id "${id}" was uploaded in this conversation, so "${file.path}" cannot be added to the skill.`, {
369
369
  attachmentId: id,
370
370
  path: file.path
371
371
  });
372
372
  const size = attachment.bytes.byteLength;
373
- if (size > maxOne) fail(`The attached file "${attachment.name}" is ${size} bytes, above the ${maxOne} byte limit for one file.`, {
373
+ if (size > maxOne) fail$2(`The attached file "${attachment.name}" is ${size} bytes, above the ${maxOne} byte limit for one file.`, {
374
374
  path: file.path,
375
375
  size,
376
376
  limit: maxOne
377
377
  });
378
378
  total += size;
379
- if (total > maxTotal) fail(`The attached files add up to ${total} bytes, above the ${maxTotal} byte limit for one skill.`, {
379
+ if (total > maxTotal) fail$2(`The attached files add up to ${total} bytes, above the ${maxTotal} byte limit for one skill.`, {
380
380
  path: file.path,
381
381
  total,
382
382
  limit: maxTotal
@@ -403,7 +403,7 @@ function assertStepsAreBackedByTrace(draft, records) {
403
403
  const executed = new Set(records.map((r) => r.tool));
404
404
  const unknown = [...new Set(claimed.map((s) => s.tool).filter((tool) => !executed.has(tool)))];
405
405
  if (unknown.length === 0) return;
406
- fail(`The skill claims tools that were never called in this session: ${unknown.join(", ")}. Tools actually called: ${executed.size === 0 ? "(none)" : [...executed].sort().join(", ")}. Rewrite the skill using only the tools above.`, {
406
+ fail$2(`The skill claims tools that were never called in this session: ${unknown.join(", ")}. Tools actually called: ${executed.size === 0 ? "(none)" : [...executed].sort().join(", ")}. Rewrite the skill using only the tools above.`, {
407
407
  claimed: unknown,
408
408
  executed: [...executed].sort()
409
409
  });
@@ -421,7 +421,7 @@ function assertStepsAreNotTruncated(draft, records) {
421
421
  const usable = new Set(records.filter((r) => r.truncated !== true).map((r) => r.tool));
422
422
  const truncated = [...new Set(claimed.map((s) => s.tool).filter((tool) => !usable.has(tool)))];
423
423
  if (truncated.length === 0) return;
424
- fail(`The arguments of these tool calls were too large to record, so the skill cannot state them: ${truncated.join(", ")}. Rewrite the skill without these steps.`, { truncated });
424
+ fail$2(`The arguments of these tool calls were too large to record, so the skill cannot state them: ${truncated.join(", ")}. Rewrite the skill without these steps.`, { truncated });
425
425
  }
426
426
  /**
427
427
  * 确认卡证据(AC-30.12 / AC-30.13):逐条列出模型**写进技能的**步骤,
@@ -569,7 +569,7 @@ const SKILL_GENERATION_SYSTEM_PROMPT = [
569
569
  //#region ../agent/src/skillGeneration/toolSource.ts
570
570
  const GENERATE_SKILL_TOOL = "generate_skill";
571
571
  const TOOL_DESCRIPTION = "Turn the procedure just demonstrated in this conversation into a skill candidate. The user must confirm it, and a reviewer must approve it before anyone can use it.";
572
- const INPUT_SCHEMA$5 = {
572
+ const INPUT_SCHEMA$6 = {
573
573
  type: "object",
574
574
  properties: {
575
575
  name: {
@@ -634,7 +634,7 @@ const INPUT_SCHEMA$5 = {
634
634
  ],
635
635
  additionalProperties: false
636
636
  };
637
- function toolError$7(code, message) {
637
+ function toolError$8(code, message) {
638
638
  return {
639
639
  ok: false,
640
640
  content: [],
@@ -715,7 +715,7 @@ function createSkillGenerationToolSource(options) {
715
715
  const generator = new SkillGenerator(options);
716
716
  const call = async (_name, args) => {
717
717
  const draft = readDraft(args);
718
- if (typeof draft === "string") return toolError$7("SKILL_GENERATION_VALIDATION_FAILED", draft);
718
+ if (typeof draft === "string") return toolError$8("SKILL_GENERATION_VALIDATION_FAILED", draft);
719
719
  try {
720
720
  const outcome = await generator.generate(draft);
721
721
  if (outcome.status === "declined") return {
@@ -739,7 +739,7 @@ function createSkillGenerationToolSource(options) {
739
739
  }]
740
740
  };
741
741
  } catch (e) {
742
- return toolError$7(e instanceof Error && "code" in e ? String(e.code) : "SKILL_GENERATION_VALIDATION_FAILED", e instanceof Error ? e.message : String(e));
742
+ return toolError$8(e instanceof Error && "code" in e ? String(e.code) : "SKILL_GENERATION_VALIDATION_FAILED", e instanceof Error ? e.message : String(e));
743
743
  }
744
744
  };
745
745
  return {
@@ -748,7 +748,7 @@ function createSkillGenerationToolSource(options) {
748
748
  listToolSpecs: () => Promise.resolve([{
749
749
  name: GENERATE_SKILL_TOOL,
750
750
  description: TOOL_DESCRIPTION,
751
- inputSchema: INPUT_SCHEMA$5
751
+ inputSchema: INPUT_SCHEMA$6
752
752
  }]),
753
753
  canHandle: (name) => name === GENERATE_SKILL_TOOL,
754
754
  argCaptureTrust: (name) => name === "generate_skill" ? { tier: "reviewed" } : void 0,
@@ -887,7 +887,7 @@ const DELEGATION_SYSTEM_PROMPT = [
887
887
  //#region ../agent/src/delegation/toolSource.ts
888
888
  const DELEGATE_TASK_TOOL = "delegate_task";
889
889
  const DELEGATE_TOOL_DESCRIPTION = "Hand one self-contained sub-task to a sub-agent and wait for its summary. Several delegations may run in parallel, up to a limit the user picks.";
890
- const INPUT_SCHEMA$4 = {
890
+ const INPUT_SCHEMA$5 = {
891
891
  type: "object",
892
892
  properties: {
893
893
  todoId: {
@@ -912,7 +912,7 @@ const INPUT_SCHEMA$4 = {
912
912
  required: ["todoId", "task"],
913
913
  additionalProperties: false
914
914
  };
915
- function toolError$6(code, message) {
915
+ function toolError$7(code, message) {
916
916
  return {
917
917
  ok: false,
918
918
  content: [],
@@ -947,12 +947,12 @@ function createDelegationToolSource(options) {
947
947
  const call = async (_name, args) => {
948
948
  const todoId = args["todoId"];
949
949
  const task = args["task"];
950
- if (typeof todoId !== "string" || todoId === "") return toolError$6("VALIDATION_FAILED", "The \"todoId\" argument is required and must be a string");
951
- if (typeof task !== "string" || task.trim() === "") return toolError$6("VALIDATION_FAILED", "The \"task\" argument is required and must be a non-empty string");
950
+ if (typeof todoId !== "string" || todoId === "") return toolError$7("VALIDATION_FAILED", "The \"todoId\" argument is required and must be a string");
951
+ if (typeof task !== "string" || task.trim() === "") return toolError$7("VALIDATION_FAILED", "The \"task\" argument is required and must be a non-empty string");
952
952
  const rawTools = args["allowedTools"];
953
953
  let allowedTools;
954
954
  if (rawTools !== void 0) {
955
- if (!Array.isArray(rawTools) || rawTools.some((entry) => typeof entry !== "string")) return toolError$6("VALIDATION_FAILED", "The \"allowedTools\" argument must be an array of tool names");
955
+ if (!Array.isArray(rawTools) || rawTools.some((entry) => typeof entry !== "string")) return toolError$7("VALIDATION_FAILED", "The \"allowedTools\" argument must be an array of tool names");
956
956
  allowedTools = rawTools;
957
957
  }
958
958
  try {
@@ -984,7 +984,7 @@ function createDelegationToolSource(options) {
984
984
  }]
985
985
  };
986
986
  } catch (e) {
987
- return toolError$6(e instanceof Error && "code" in e ? String(e.code) : "DELEGATION_FAILED", e instanceof Error ? e.message : String(e));
987
+ return toolError$7(e instanceof Error && "code" in e ? String(e.code) : "DELEGATION_FAILED", e instanceof Error ? e.message : String(e));
988
988
  }
989
989
  };
990
990
  return {
@@ -994,7 +994,7 @@ function createDelegationToolSource(options) {
994
994
  listToolSpecs: () => Promise.resolve([{
995
995
  name: DELEGATE_TASK_TOOL,
996
996
  description: DELEGATE_TOOL_DESCRIPTION,
997
- inputSchema: INPUT_SCHEMA$4
997
+ inputSchema: INPUT_SCHEMA$5
998
998
  }]),
999
999
  canHandle: (name) => name === DELEGATE_TASK_TOOL,
1000
1000
  argCaptureTrust: (name) => name === "delegate_task" ? { tier: "reviewed" } : void 0,
@@ -1397,7 +1397,7 @@ const PERCEPTION_SYSTEM_PROMPT = [
1397
1397
  //#endregion
1398
1398
  //#region ../agent/src/perception/toolSource.ts
1399
1399
  const PERCEIVE_PAGE_TOOL = "perceive_page";
1400
- const DESCRIPTION$4 = "Read the parts of the current page the host made readable, as a structured accessibility outline. Read-only: it cannot click, type, submit or navigate.";
1400
+ const DESCRIPTION$5 = "Read the parts of the current page the host made readable, as a structured accessibility outline. Read-only: it cannot click, type, submit or navigate.";
1401
1401
  /**
1402
1402
  * 两个参数,且它们都扩不出一寸新范围(AC-10.8 的判据由 0.14.0 分册 22 §3 修订)。
1403
1403
  *
@@ -1410,7 +1410,7 @@ const DESCRIPTION$4 = "Read the parts of the current page the host made readable
1410
1410
  * 「模型看到的工具契约」随宿主配置漂移,同一段对话里换个设置就变一次形状,
1411
1411
  * 而模型的历史消息里还留着旧形状。宁可多一个恒定存在、关闭时明确拒绝的参数。
1412
1412
  */
1413
- const INPUT_SCHEMA$3 = {
1413
+ const INPUT_SCHEMA$4 = {
1414
1414
  type: "object",
1415
1415
  properties: {
1416
1416
  cursor: {
@@ -1431,7 +1431,7 @@ const SUBTREE_CODES = /* @__PURE__ */ new Set([
1431
1431
  "PERCEPTION_SUBTREE_OUT_OF_SCOPE",
1432
1432
  "PERCEPTION_SUBTREE_UNKNOWN_REF"
1433
1433
  ]);
1434
- function toolError$5(code, message) {
1434
+ function toolError$6(code, message) {
1435
1435
  return {
1436
1436
  ok: false,
1437
1437
  content: [],
@@ -1523,8 +1523,8 @@ function createPagePerceptionToolSource(options) {
1523
1523
  kind: "page-perception",
1524
1524
  listToolSpecs: () => Promise.resolve(policy.enabled ? [{
1525
1525
  name: PERCEIVE_PAGE_TOOL,
1526
- description: DESCRIPTION$4,
1527
- inputSchema: INPUT_SCHEMA$3
1526
+ description: DESCRIPTION$5,
1527
+ inputSchema: INPUT_SCHEMA$4
1528
1528
  }] : []),
1529
1529
  systemPrompt: () => Promise.resolve(policy.enabled ? PERCEPTION_SYSTEM_PROMPT : void 0),
1530
1530
  canHandle: (name) => name === PERCEIVE_PAGE_TOOL,
@@ -1538,11 +1538,11 @@ function createPagePerceptionToolSource(options) {
1538
1538
  };
1539
1539
  const cursor = args["cursor"];
1540
1540
  const ref = args["ref"];
1541
- if (ref !== void 0 && typeof ref !== "string") return toolError$5("PERCEPTION_REF_INVALID", "perceive_page needs \"ref\" to be a string when it is present.");
1541
+ if (ref !== void 0 && typeof ref !== "string") return toolError$6("PERCEPTION_REF_INVALID", "perceive_page needs \"ref\" to be a string when it is present.");
1542
1542
  if (cursor !== void 0) {
1543
1543
  const index = typeof cursor === "string" ? pending?.cursors.get(cursor) : void 0;
1544
- if (!paged || pending === void 0 || index === void 0 || pending.runId !== context?.runId) return toolError$5("PERCEPTION_CURSOR_INVALID", CURSOR_REJECTED);
1545
- if (ref !== void 0 && ref !== pending.subtreeRef) return toolError$5("PERCEPTION_CURSOR_INVALID", CURSOR_REJECTED);
1544
+ if (!paged || pending === void 0 || index === void 0 || pending.runId !== context?.runId) return toolError$6("PERCEPTION_CURSOR_INVALID", CURSOR_REJECTED);
1545
+ if (ref !== void 0 && ref !== pending.subtreeRef) return toolError$6("PERCEPTION_CURSOR_INVALID", CURSOR_REJECTED);
1546
1546
  return resultOf(pending, index);
1547
1547
  }
1548
1548
  try {
@@ -1604,8 +1604,8 @@ function createPagePerceptionToolSource(options) {
1604
1604
  return resultOf(pending, 0);
1605
1605
  } catch (e) {
1606
1606
  const code = e instanceof WebSkillError ? e.code : void 0;
1607
- if (code !== void 0 && SUBTREE_CODES.has(code)) return toolError$5(code, messageOf(e));
1608
- return toolError$5("PERCEPTION_FAILED", `perceive_page failed: ${messageOf(e)}`);
1607
+ if (code !== void 0 && SUBTREE_CODES.has(code)) return toolError$6(code, messageOf(e));
1608
+ return toolError$6("PERCEPTION_FAILED", `perceive_page failed: ${messageOf(e)}`);
1609
1609
  }
1610
1610
  }
1611
1611
  };
@@ -2233,13 +2233,13 @@ const PAGE_ACTION_SYSTEM_PROMPT = [
2233
2233
  //#endregion
2234
2234
  //#region ../agent/src/pageAction/toolSource.ts
2235
2235
  const PAGE_ACTION_TOOL = "act_on_page";
2236
- const DESCRIPTION$3 = "Act on one element of the current page. The target must be a \"ref\" from a fresh perceive_page result; selectors are rejected. Each call asks the user to confirm before anything happens.";
2236
+ const DESCRIPTION$4 = "Act on one element of the current page. The target must be a \"ref\" from a fresh perceive_page result; selectors are rejected. Each call asks the user to confirm before anything happens.";
2237
2237
  /**
2238
2238
  * 入参里**没有** `scope` / `selector` / `include` / `frame` 一类字段(AC-23.2):
2239
2239
  * 工具一旦接受它们,「能操作哪里」就有了一条从模型输出流进来的路径。
2240
2240
  * 模型能表达的只有「在哪个句柄上做哪个动作」。
2241
2241
  */
2242
- const INPUT_SCHEMA$2 = {
2242
+ const INPUT_SCHEMA$3 = {
2243
2243
  type: "object",
2244
2244
  properties: {
2245
2245
  ref: {
@@ -2258,7 +2258,7 @@ const INPUT_SCHEMA$2 = {
2258
2258
  required: ["action"],
2259
2259
  additionalProperties: false
2260
2260
  };
2261
- function toolError$4(code, message) {
2261
+ function toolError$5(code, message) {
2262
2262
  return {
2263
2263
  ok: false,
2264
2264
  content: [],
@@ -2281,8 +2281,8 @@ function createPageActionToolSource(options) {
2281
2281
  kind: "page-action",
2282
2282
  listToolSpecs: () => Promise.resolve(policy.enabled ? [{
2283
2283
  name: PAGE_ACTION_TOOL,
2284
- description: DESCRIPTION$3,
2285
- inputSchema: INPUT_SCHEMA$2
2284
+ description: DESCRIPTION$4,
2285
+ inputSchema: INPUT_SCHEMA$3
2286
2286
  }] : []),
2287
2287
  systemPrompt: () => Promise.resolve(policy.enabled ? PAGE_ACTION_SYSTEM_PROMPT : void 0),
2288
2288
  canHandle: (name) => name === PAGE_ACTION_TOOL,
@@ -2308,7 +2308,7 @@ function createPageActionToolSource(options) {
2308
2308
  },
2309
2309
  call: async (_name, args, context) => {
2310
2310
  const action = args["action"];
2311
- if (typeof action !== "string" || !PAGE_ACTION_KINDS.includes(action)) return toolError$4("VALIDATION_FAILED", `act_on_page needs one of ${PAGE_ACTION_KINDS.join(" / ")}.`);
2311
+ if (typeof action !== "string" || !PAGE_ACTION_KINDS.includes(action)) return toolError$5("VALIDATION_FAILED", `act_on_page needs one of ${PAGE_ACTION_KINDS.join(" / ")}.`);
2312
2312
  const runContext = { ...context?.runId !== void 0 ? { runId: context.runId } : {} };
2313
2313
  const run = async (request) => {
2314
2314
  try {
@@ -2325,25 +2325,25 @@ function createPageActionToolSource(options) {
2325
2325
  } }
2326
2326
  };
2327
2327
  } catch (e) {
2328
- return toolError$4(e instanceof WebSkillError ? e.code : "PAGE_ACTION_FAILED", messageOf(e));
2328
+ return toolError$5(e instanceof WebSkillError ? e.code : "PAGE_ACTION_FAILED", messageOf(e));
2329
2329
  }
2330
2330
  };
2331
2331
  if (action === "back") {
2332
- if ("ref" in args || "value" in args) return toolError$4("VALIDATION_FAILED", "act_on_page with \"back\" takes no \"ref\" and no \"value\".");
2332
+ if ("ref" in args || "value" in args) return toolError$5("VALIDATION_FAILED", "act_on_page with \"back\" takes no \"ref\" and no \"value\".");
2333
2333
  return await run({
2334
2334
  ref: PAGE_DOCUMENT_REF,
2335
2335
  action: "back"
2336
2336
  });
2337
2337
  }
2338
2338
  const ref = args["ref"];
2339
- if (typeof ref !== "string") return toolError$4("VALIDATION_FAILED", `act_on_page needs a "ref" string when the action is "${action}".`);
2339
+ if (typeof ref !== "string") return toolError$5("VALIDATION_FAILED", `act_on_page needs a "ref" string when the action is "${action}".`);
2340
2340
  const value = args["value"];
2341
2341
  if ([
2342
2342
  "fill",
2343
2343
  "select",
2344
2344
  "set"
2345
- ].includes(action) && typeof value !== "string") return toolError$4("VALIDATION_FAILED", `act_on_page needs a "value" string when the action is "${action}".`);
2346
- if (action === "scroll" && !SCROLL_DIRECTIONS.includes(value)) return toolError$4("VALIDATION_FAILED", `act_on_page with "scroll" needs a "value" of ${SCROLL_DIRECTIONS.map((d) => `"${d}"`).join(" / ")}.`);
2345
+ ].includes(action) && typeof value !== "string") return toolError$5("VALIDATION_FAILED", `act_on_page needs a "value" string when the action is "${action}".`);
2346
+ if (action === "scroll" && !SCROLL_DIRECTIONS.includes(value)) return toolError$5("VALIDATION_FAILED", `act_on_page with "scroll" needs a "value" of ${SCROLL_DIRECTIONS.map((d) => `"${d}"`).join(" / ")}.`);
2347
2347
  return await run({
2348
2348
  ref,
2349
2349
  action,
@@ -2585,7 +2585,7 @@ const SPECS = [
2585
2585
  inputSchema: TAB_SCHEMA
2586
2586
  }
2587
2587
  ];
2588
- function toolError$3(code, message) {
2588
+ function toolError$4(code, message) {
2589
2589
  return {
2590
2590
  ok: false,
2591
2591
  content: [],
@@ -2620,7 +2620,7 @@ function createTabWorksetToolSource(options) {
2620
2620
  }]
2621
2621
  };
2622
2622
  const handle = args["tab"];
2623
- if (typeof handle !== "string" || handle === "") return toolError$3("VALIDATION_FAILED", `${name} needs a "tab" handle from list_page_tabs.`);
2623
+ if (typeof handle !== "string" || handle === "") return toolError$4("VALIDATION_FAILED", `${name} needs a "tab" handle from list_page_tabs.`);
2624
2624
  return {
2625
2625
  ok: true,
2626
2626
  content: [{
@@ -2629,7 +2629,7 @@ function createTabWorksetToolSource(options) {
2629
2629
  }]
2630
2630
  };
2631
2631
  } catch (e) {
2632
- return toolError$3(e instanceof WebSkillError ? e.code : "PAGE_TAB_FAILED", messageOf(e));
2632
+ return toolError$4(e instanceof WebSkillError ? e.code : "PAGE_TAB_FAILED", messageOf(e));
2633
2633
  }
2634
2634
  }
2635
2635
  };
@@ -2788,6 +2788,18 @@ var DownloadedFilePolicy = class {
2788
2788
  }
2789
2789
  async #materialize(entry, contentType, kind, bytes) {
2790
2790
  if (kind === "text" || kind === "document-text") {
2791
+ if (kind === "document-text" && extractedDocumentFormat(contentType, entry.name) === "xlsx") {
2792
+ const routed = await this.#options.sheetRouting?.({
2793
+ entry,
2794
+ bytes
2795
+ });
2796
+ if (routed !== void 0) return {
2797
+ entry,
2798
+ contentType,
2799
+ kind,
2800
+ text: routed
2801
+ };
2802
+ }
2791
2803
  const body = kind === "text" ? new TextDecoder().decode(bytes) : await this.#extract(contentType, entry.name)(bytes);
2792
2804
  return {
2793
2805
  entry,
@@ -2958,8 +2970,8 @@ const READ_INPUT_SCHEMA = {
2958
2970
  required: ["id"],
2959
2971
  additionalProperties: false
2960
2972
  };
2961
- const SYSTEM_PROMPT$3 = "You can list and read files the user downloaded in this browser with list_downloaded_files and read_downloaded_file. They cover the browser download folder and nothing else. A file the user attached to a message is already in this conversation and is reachable by the id given with it, so never go looking for an attachment here. Every call asks the user for permission, so only use them when the task actually needs a local file. Never guess an id: always list first, and never treat the listing as evidence about files you were not shown.";
2962
- function toolError$2(e) {
2973
+ const SYSTEM_PROMPT$4 = "You can list and read files the user downloaded in this browser with list_downloaded_files and read_downloaded_file. They cover the browser download folder and nothing else. A file the user attached to a message is already in this conversation and is reachable by the id given with it, so never go looking for an attachment here. Every call asks the user for permission, so only use them when the task actually needs a local file. Never guess an id: always list first, and never treat the listing as evidence about files you were not shown.";
2974
+ function toolError$3(e) {
2963
2975
  return {
2964
2976
  ok: false,
2965
2977
  content: [],
@@ -2989,7 +3001,7 @@ function createDownloadedFileToolSource(options) {
2989
3001
  description: READ_DESCRIPTION,
2990
3002
  inputSchema: READ_INPUT_SCHEMA
2991
3003
  }] : []),
2992
- systemPrompt: () => Promise.resolve(policy.enabled ? SYSTEM_PROMPT$3 : void 0),
3004
+ systemPrompt: () => Promise.resolve(policy.enabled ? SYSTEM_PROMPT$4 : void 0),
2993
3005
  canHandle: (name) => name === "list_downloaded_files" || name === "read_downloaded_file",
2994
3006
  argCaptureTrust: (name) => name === "list_downloaded_files" || name === "read_downloaded_file" ? { tier: "reviewed" } : void 0,
2995
3007
  call: async (name, args) => {
@@ -3008,7 +3020,7 @@ function createDownloadedFileToolSource(options) {
3008
3020
  content: readContent(await policy.read(id))
3009
3021
  };
3010
3022
  } catch (e) {
3011
- return toolError$2(e);
3023
+ return toolError$3(e);
3012
3024
  }
3013
3025
  }
3014
3026
  };
@@ -3051,7 +3063,7 @@ function readContent(result) {
3051
3063
  //#endregion
3052
3064
  //#region ../agent/src/spreadsheet/toolSource.ts
3053
3065
  const EXPORT_SPREADSHEET_TOOL = "export_spreadsheet";
3054
- const DESCRIPTION$2 = "Write tabular data — and, when they belong in the sheet, pictures — to an Excel (.xlsx) file the user can download.";
3066
+ const DESCRIPTION$3 = "Write tabular data — and, when they belong in the sheet, pictures — to an Excel (.xlsx) file the user can download.";
3055
3067
  const CELL_SCHEMA = {
3056
3068
  anyOf: [
3057
3069
  { type: "string" },
@@ -3168,7 +3180,7 @@ function inputSchema(withImages) {
3168
3180
  function systemPrompt$1(withImages) {
3169
3181
  return "Use export_spreadsheet when the user asks for a downloadable Excel file. The tool returns only a summary; the file itself is delivered to the user as a card. Do not re-read the file you just wrote. Cell values are written literally — the tool never turns text into a number or a date, and it writes no styling, formulas, merged cells or charts." + (withImages ? " Pictures do go in: put them in the sheet's \"images\" with a ref and an anchor cell. A picture the user attached to a message is already in this conversation — reference it as \"upload:<id or file name>\" and do not go looking for it on the page or in the download list first. When the user asks for a workbook of pictures, put the pictures in it — do not deliver the images separately and ask the user to insert them." : "");
3170
3182
  }
3171
- function toolError$1(e) {
3183
+ function toolError$2(e) {
3172
3184
  return {
3173
3185
  ok: false,
3174
3186
  content: [],
@@ -3221,7 +3233,7 @@ function createSpreadsheetToolSource(options) {
3221
3233
  kind: "spreadsheet",
3222
3234
  listToolSpecs: () => Promise.resolve(writeArtifact ? [{
3223
3235
  name: EXPORT_SPREADSHEET_TOOL,
3224
- description: DESCRIPTION$2,
3236
+ description: DESCRIPTION$3,
3225
3237
  inputSchema: schema
3226
3238
  }] : []),
3227
3239
  systemPrompt: () => Promise.resolve(writeArtifact ? prompt : void 0),
@@ -3254,11 +3266,1037 @@ function createSpreadsheetToolSource(options) {
3254
3266
  }
3255
3267
  }]
3256
3268
  };
3269
+ } catch (e) {
3270
+ return toolError$2(e);
3271
+ }
3272
+ }
3273
+ };
3274
+ }
3275
+
3276
+ //#endregion
3277
+ //#region ../agent/src/table/columns.ts
3278
+ const NUMBER_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
3279
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2})?$/;
3280
+ /** 前导零的整数(工号、手机号、邮编)是标识不是数量:当文本,免得被求和 */
3281
+ const LEADING_ZERO_RE = /^[+-]?0\d/;
3282
+ /** 入参是**已去首尾空白**的文本 */
3283
+ function kindOf(trimmed) {
3284
+ if (trimmed === "") return "empty";
3285
+ if (NUMBER_RE.test(trimmed)) return LEADING_ZERO_RE.test(trimmed) ? "text" : "number";
3286
+ if (DATE_RE.test(trimmed)) return "date";
3287
+ return "text";
3288
+ }
3289
+ /** 0 → A,25 → Z,26 → AA */
3290
+ function columnLetter(index) {
3291
+ let n = index + 1;
3292
+ let out = "";
3293
+ while (n > 0) {
3294
+ const rem = (n - 1) % 26;
3295
+ out = String.fromCharCode(65 + rem) + out;
3296
+ n = Math.floor((n - 1) / 26);
3297
+ }
3298
+ return out;
3299
+ }
3300
+ const blank = (cells) => cells.every((cell) => cell.trim() === "");
3301
+ function fail$1(message) {
3302
+ throw new WebSkillError("VALIDATION_FAILED", message);
3303
+ }
3304
+ /** 先按序号(1 基)、再按表名定位;缺省取第一张有内容的表 */
3305
+ function resolveSheet(workbook, ref) {
3306
+ const { sheets } = workbook;
3307
+ if (sheets.length === 0) fail$1("This workbook has no sheets.");
3308
+ const names = () => sheets.map((sheet, i) => `${i + 1}: "${sheet.name}"`).join(", ");
3309
+ if (ref === void 0) {
3310
+ const found = sheets.findIndex((sheet) => sheet.rows.some((row) => !blank(row)));
3311
+ const index = found === -1 ? 0 : found;
3312
+ return {
3313
+ sheet: sheets[index],
3314
+ index
3315
+ };
3316
+ }
3317
+ if (typeof ref === "number") {
3318
+ const sheet = Number.isInteger(ref) ? sheets[ref - 1] : void 0;
3319
+ if (sheet === void 0) fail$1(`There is no sheet number ${String(ref)}. Sheets: ${names()}.`);
3320
+ return {
3321
+ sheet,
3322
+ index: ref - 1
3323
+ };
3324
+ }
3325
+ if (typeof ref === "string") {
3326
+ const index = sheets.findIndex((sheet) => sheet.name === ref);
3327
+ if (index === -1) fail$1(`There is no sheet named "${ref}". Sheets: ${names()}.`);
3328
+ return {
3329
+ sheet: sheets[index],
3330
+ index
3331
+ };
3332
+ }
3333
+ return fail$1("\"sheet\" must be a sheet name or a 1-based sheet number.");
3334
+ }
3335
+ /** 表头行(缺省为第一个有内容的行)+ 其后有内容的行;行号保持工作表的 1 基行号 */
3336
+ function buildView(workbook, sheetRef, headerRowArg) {
3337
+ const { sheet, index } = resolveSheet(workbook, sheetRef);
3338
+ const rows = sheet.rows;
3339
+ let headerIndex;
3340
+ if (headerRowArg === void 0) headerIndex = rows.findIndex((row) => !blank(row));
3341
+ else {
3342
+ if (typeof headerRowArg !== "number" || !Number.isInteger(headerRowArg) || headerRowArg < 1) fail$1("\"headerRow\" must be a positive integer (1-based worksheet row number).");
3343
+ headerIndex = headerRowArg - 1;
3344
+ if (headerIndex >= rows.length) fail$1(`"headerRow" ${headerRowArg} is past the last row (${rows.length}) of the sheet.`);
3345
+ }
3346
+ if (headerIndex === -1) return {
3347
+ sheetName: sheet.name,
3348
+ sheetIndex: index,
3349
+ headerRow: 0,
3350
+ columns: [],
3351
+ data: []
3352
+ };
3353
+ const header = rows[headerIndex] ?? [];
3354
+ const data = [];
3355
+ let width = header.length;
3356
+ for (let i = headerIndex + 1; i < rows.length; i += 1) {
3357
+ const cells = rows[i];
3358
+ if (blank(cells)) continue;
3359
+ if (cells.length > width) width = cells.length;
3360
+ data.push({
3361
+ row: i + 1,
3362
+ cells
3363
+ });
3364
+ }
3365
+ const columns = [];
3366
+ for (let c = 0; c < width; c += 1) {
3367
+ const text = (header[c] ?? "").trim();
3368
+ const letter = columnLetter(c);
3369
+ columns.push({
3370
+ index: c,
3371
+ letter,
3372
+ header: text,
3373
+ label: text === "" ? letter : `${letter} ${text}`,
3374
+ name: text === "" ? letter : text
3375
+ });
3376
+ }
3377
+ return {
3378
+ sheetName: sheet.name,
3379
+ sheetIndex: index,
3380
+ headerRow: headerIndex + 1,
3381
+ columns,
3382
+ data
3383
+ };
3384
+ }
3385
+ const LETTER_RE = /^[A-Za-z]{1,3}$/;
3386
+ function listColumns(view) {
3387
+ return view.columns.map((column) => column.label).join("; ");
3388
+ }
3389
+ /** 表头文字优先,其次列字母;表头重名时拒绝按表头引用,并点出同名各列的列字母 */
3390
+ function resolveColumn(view, ref, what) {
3391
+ if (typeof ref !== "string" || ref.trim() === "") fail$1(`${what} must name a column (header text or column letter).`);
3392
+ const text = ref.trim();
3393
+ const byHeader = view.columns.filter((column) => column.header === text);
3394
+ if (byHeader.length === 1) return byHeader[0];
3395
+ if (byHeader.length > 1) fail$1(`${what}: the header "${text}" is shared by columns ${byHeader.map((c) => c.letter).join(", ")}. Refer to the column by its letter instead.`);
3396
+ if (LETTER_RE.test(text)) {
3397
+ const byLetter = view.columns.find((column) => column.letter === text.toUpperCase());
3398
+ if (byLetter !== void 0) return byLetter;
3399
+ }
3400
+ return fail$1(`${what}: no column "${text}" on sheet "${view.sheetName}". Columns: ${listColumns(view)}.`);
3401
+ }
3402
+ /** Neumaier 补偿求和:几万个小数相加不累积漂移 */
3403
+ var Summer = class {
3404
+ #sum = 0;
3405
+ #comp = 0;
3406
+ add(x) {
3407
+ const t = this.#sum + x;
3408
+ this.#comp += Math.abs(this.#sum) >= Math.abs(x) ? this.#sum - t + x : x - t + this.#sum;
3409
+ this.#sum = t;
3410
+ }
3411
+ get total() {
3412
+ return this.#sum + this.#comp;
3413
+ }
3414
+ };
3415
+ /** 12 位有效数字:消掉 `0.30000000000000004` 一类尾巴,又不损失财务级精度 */
3416
+ const tidy = (x) => Number(x.toPrecision(12));
3417
+
3418
+ //#endregion
3419
+ //#region ../agent/src/table/profile.ts
3420
+ const PROFILE_MAX_CHARS = 8e3;
3421
+ const DISTINCT_CAP = 1e4;
3422
+ const SAMPLE_ROWS = 5;
3423
+ function statsOf(view, column) {
3424
+ let filled = 0;
3425
+ let numbers = 0;
3426
+ let dates = 0;
3427
+ let texts = 0;
3428
+ const summer = new Summer();
3429
+ let min;
3430
+ let max;
3431
+ let earliest;
3432
+ let latest;
3433
+ const freq = /* @__PURE__ */ new Map();
3434
+ let capped = false;
3435
+ for (const { cells } of view.data) {
3436
+ const text = (cells[column.index] ?? "").trim();
3437
+ const kind = kindOf(text);
3438
+ if (kind === "empty") continue;
3439
+ filled += 1;
3440
+ if (kind === "number") {
3441
+ numbers += 1;
3442
+ const x = Number(text);
3443
+ summer.add(x);
3444
+ if (min === void 0 || x < min) min = x;
3445
+ if (max === void 0 || x > max) max = x;
3446
+ } else if (kind === "date") {
3447
+ dates += 1;
3448
+ if (earliest === void 0 || text < earliest) earliest = text;
3449
+ if (latest === void 0 || text > latest) latest = text;
3450
+ } else texts += 1;
3451
+ const seen = freq.get(text);
3452
+ if (seen !== void 0) freq.set(text, seen + 1);
3453
+ else if (freq.size < DISTINCT_CAP) freq.set(text, 1);
3454
+ else capped = true;
3455
+ }
3456
+ let type;
3457
+ if (filled === 0) type = "empty";
3458
+ else if (numbers >= filled * .95) type = "number";
3459
+ else if (dates >= filled * .95) type = "date";
3460
+ else if (numbers + dates <= filled * .05) type = "text";
3461
+ else type = "mixed";
3462
+ const top = capped || type !== "text" && type !== "mixed" ? [] : [...freq.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 3);
3463
+ return {
3464
+ filled,
3465
+ numbers,
3466
+ dates,
3467
+ texts,
3468
+ type,
3469
+ distinct: capped ? void 0 : freq.size,
3470
+ sum: summer.total,
3471
+ min,
3472
+ max,
3473
+ earliest,
3474
+ latest,
3475
+ top
3476
+ };
3477
+ }
3478
+ const clip$2 = (text, max) => {
3479
+ const chars = [...text.replace(/[\r\n\t]+/g, " ")];
3480
+ return chars.length <= max ? chars.join("") : `${chars.slice(0, max - 1).join("")}…`;
3481
+ };
3482
+ /** 超出字符预算时的收缩阶梯:先少表、再少列、再收样本、最后去掉 top */
3483
+ const LADDER = [
3484
+ {
3485
+ maxSheets: 8,
3486
+ maxColumns: 40,
3487
+ sampleRows: SAMPLE_ROWS,
3488
+ sampleCell: 40,
3489
+ withTop: true,
3490
+ note: void 0
3491
+ },
3492
+ {
3493
+ maxSheets: 3,
3494
+ maxColumns: 40,
3495
+ sampleRows: SAMPLE_ROWS,
3496
+ sampleCell: 40,
3497
+ withTop: true,
3498
+ note: "sheets"
3499
+ },
3500
+ {
3501
+ maxSheets: 3,
3502
+ maxColumns: 20,
3503
+ sampleRows: SAMPLE_ROWS,
3504
+ sampleCell: 40,
3505
+ withTop: true,
3506
+ note: "sheets, columns"
3507
+ },
3508
+ {
3509
+ maxSheets: 3,
3510
+ maxColumns: 20,
3511
+ sampleRows: 3,
3512
+ sampleCell: 16,
3513
+ withTop: true,
3514
+ note: "sheets, columns, sample"
3515
+ },
3516
+ {
3517
+ maxSheets: 2,
3518
+ maxColumns: 12,
3519
+ sampleRows: 2,
3520
+ sampleCell: 12,
3521
+ withTop: false,
3522
+ note: "sheets, columns, sample, top values"
3523
+ }
3524
+ ];
3525
+ function columnLine(column, stats, withTop) {
3526
+ const parts = [
3527
+ `${column.letter} ${clip$2(column.header, 40)}`.trimEnd(),
3528
+ stats.type,
3529
+ `${stats.filled} filled`
3530
+ ];
3531
+ parts.push(stats.distinct === void 0 ? `>${DISTINCT_CAP} distinct` : `${stats.distinct} distinct`);
3532
+ if (stats.min !== void 0 && stats.max !== void 0) parts.push(`min ${tidy(stats.min)}`, `max ${tidy(stats.max)}`, `sum ${tidy(stats.sum)}`);
3533
+ if (stats.earliest !== void 0 && stats.latest !== void 0) parts.push(`${stats.earliest} … ${stats.latest}`);
3534
+ if (withTop && stats.top.length > 0) parts.push(`top: ${stats.top.map(([value, count]) => `${clip$2(value, 24)} ×${count}`).join(", ")}`);
3535
+ return parts.join(" | ");
3536
+ }
3537
+ function sheetSection(view, rung, expanded, statsFor) {
3538
+ const head = `Sheet ${view.sheetIndex + 1} "${view.sheetName}": ${view.data.length} data rows × ${view.columns.length} columns` + (view.headerRow > 0 ? `, header at row ${view.headerRow}` : ", empty");
3539
+ if (!expanded || view.columns.length === 0) return [head];
3540
+ const lines = [head, "Columns:"];
3541
+ const shown = view.columns.slice(0, rung.maxColumns);
3542
+ for (const column of shown) lines.push(columnLine(column, statsFor(column), rung.withTop));
3543
+ if (view.columns.length > shown.length) lines.push(`+${view.columns.length - shown.length} more columns — call query_table with op "describe" and this sheet to list them`);
3544
+ const sample = view.data.slice(0, rung.sampleRows);
3545
+ if (sample.length > 0) {
3546
+ lines.push(`Sample (sheet rows ${sample[0].row}–${sample[sample.length - 1].row}):`);
3547
+ for (const { cells } of sample) lines.push(shown.map((column) => clip$2((cells[column.index] ?? "").trim(), rung.sampleCell)).join(" "));
3548
+ }
3549
+ return lines;
3550
+ }
3551
+ /**
3552
+ * 表格画像:每张表的行列数、表头位置,每列的类型 / 非空数 / 去重数 / 数值与日期范围 / 高频取值,
3553
+ * 以及前几行样本(0.26.0 分册 10 · FR-10.3)。
3554
+ *
3555
+ * 分流进消息与 `query_table` 的 `describe` 调同一个函数,模型看到的是同一份口径。
3556
+ * 整份不超过 8000 字符;超出时按阶梯收缩,并在末尾写明收缩了什么。
3557
+ * @experimental
3558
+ */
3559
+ function profileTable(input) {
3560
+ const { workbook } = input;
3561
+ const total = workbook.sheets.length;
3562
+ const only = input.sheet === void 0 ? void 0 : buildView(workbook, input.sheet, input.headerRow).sheetIndex;
3563
+ const header = `--- Spreadsheet profile: ${input.name} (id: ${input.id}) · ${total} sheet${total === 1 ? "" : "s"} ---`;
3564
+ const views = workbook.sheets.map((_, index) => buildView(workbook, index + 1, only === index ? input.headerRow : void 0));
3565
+ const memo = /* @__PURE__ */ new Map();
3566
+ const statsFor = (view) => (column) => {
3567
+ let perView = memo.get(view);
3568
+ if (perView === void 0) memo.set(view, perView = /* @__PURE__ */ new Map());
3569
+ let stats = perView.get(column.index);
3570
+ if (stats === void 0) perView.set(column.index, stats = statsOf(view, column));
3571
+ return stats;
3572
+ };
3573
+ let result = "";
3574
+ for (const rung of LADDER) {
3575
+ const lines = [header];
3576
+ let expandedCount = 0;
3577
+ for (const [index, view] of views.entries()) {
3578
+ const expanded = only === void 0 ? expandedCount < rung.maxSheets : index === only;
3579
+ if (expanded && view.data.length > 0) expandedCount += 1;
3580
+ lines.push(...sheetSection(view, rung, expanded && (only !== void 0 || view.data.length > 0), statsFor(view)));
3581
+ }
3582
+ if (rung.note !== void 0) lines.push(`Profile shortened to fit: ${rung.note}. Call query_table with op "describe" for the rest.`);
3583
+ result = lines.join("\n");
3584
+ if (result.length <= PROFILE_MAX_CHARS) return result;
3585
+ }
3586
+ return `${result.slice(0, PROFILE_MAX_CHARS - 80)}\nProfile cut at ${PROFILE_MAX_CHARS} characters.`;
3587
+ }
3588
+
3589
+ //#endregion
3590
+ //#region ../agent/src/table/query.ts
3591
+ const QUERY_LIMITS = {
3592
+ defaultRows: 50,
3593
+ maxRows: 500,
3594
+ maxFilters: 20,
3595
+ maxGroupBy: 3,
3596
+ maxMetrics: 8,
3597
+ maxOrderBy: 3,
3598
+ maxIn: 200,
3599
+ /** 返回体的硬上限,远低于 `loop.toolResultMaxBytes` 的 256 KB(FR-10.12) */
3600
+ maxBytes: 64 * 1024,
3601
+ cellChars: 200
3602
+ };
3603
+ const METRIC_FNS = [
3604
+ "count",
3605
+ "countDistinct",
3606
+ "sum",
3607
+ "avg",
3608
+ "min",
3609
+ "max",
3610
+ "median"
3611
+ ];
3612
+ const FILTER_OPS = [
3613
+ "eq",
3614
+ "ne",
3615
+ "gt",
3616
+ "gte",
3617
+ "lt",
3618
+ "lte",
3619
+ "contains",
3620
+ "in",
3621
+ "empty",
3622
+ "notEmpty"
3623
+ ];
3624
+ const BUCKETS = [
3625
+ "value",
3626
+ "year",
3627
+ "month",
3628
+ "day"
3629
+ ];
3630
+ function fail(message) {
3631
+ throw new WebSkillError("VALIDATION_FAILED", message);
3632
+ }
3633
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3634
+ function list(v, what, max) {
3635
+ if (v === void 0) return [];
3636
+ if (!Array.isArray(v)) fail(`"${what}" must be an array.`);
3637
+ if (v.length > max) fail(`"${what}" has ${v.length} entries; at most ${max} are allowed.`);
3638
+ return v;
3639
+ }
3640
+ function oneOf(v, allowed, what) {
3641
+ if (typeof v === "string" && allowed.includes(v)) return v;
3642
+ return fail(`${what} must be one of: ${allowed.join(", ")}.`);
3643
+ }
3644
+ function noOtherKeys(obj, allowed, what) {
3645
+ const extra = Object.keys(obj).filter((key) => !allowed.includes(key));
3646
+ if (extra.length > 0) fail(`${what} has unknown field${extra.length > 1 ? "s" : ""}: ${extra.join(", ")}.`);
3647
+ }
3648
+ function scalar(v, what) {
3649
+ if (typeof v === "string") return v;
3650
+ if (typeof v === "number" && Number.isFinite(v)) return v;
3651
+ return fail(`${what} must be a string or a finite number.`);
3652
+ }
3653
+ function compileFilter(view, raw, index, uncomparable) {
3654
+ const what = `where[${index}]`;
3655
+ if (!isObject(raw)) fail(`${what} must be an object.`);
3656
+ noOtherKeys(raw, [
3657
+ "column",
3658
+ "op",
3659
+ "value"
3660
+ ], what);
3661
+ const column = resolveColumn(view, raw["column"], `${what}.column`);
3662
+ const op = oneOf(raw["op"], FILTER_OPS, `${what}.op`);
3663
+ const at = column.index;
3664
+ const cellOf = (cells) => (cells[at] ?? "").trim();
3665
+ if (op === "empty") return {
3666
+ column,
3667
+ test: (cells) => cellOf(cells) === ""
3668
+ };
3669
+ if (op === "notEmpty") return {
3670
+ column,
3671
+ test: (cells) => cellOf(cells) !== ""
3672
+ };
3673
+ if (op === "in") {
3674
+ const values = raw["value"];
3675
+ if (!Array.isArray(values) || values.length === 0 || values.length > QUERY_LIMITS.maxIn) fail(`${what}.value must be a non-empty array of at most ${QUERY_LIMITS.maxIn} strings or numbers for op "in".`);
3676
+ const texts = /* @__PURE__ */ new Set();
3677
+ const numbers = /* @__PURE__ */ new Set();
3678
+ for (const item of values) {
3679
+ const text = String(scalar(item, `${what}.value[]`)).trim();
3680
+ texts.add(text);
3681
+ if (kindOf(text) === "number") numbers.add(Number(text));
3682
+ }
3683
+ return {
3684
+ column,
3685
+ test: (cells) => {
3686
+ const text = cellOf(cells);
3687
+ return texts.has(text) || numbers.size > 0 && kindOf(text) === "number" && numbers.has(Number(text));
3688
+ }
3689
+ };
3690
+ }
3691
+ if (raw["value"] === void 0) fail(`${what}.value is required for op "${op}".`);
3692
+ const value = String(scalar(raw["value"], `${what}.value`)).trim();
3693
+ const valueKind = kindOf(value);
3694
+ if (op === "contains") {
3695
+ const needle = value.toLowerCase();
3696
+ return {
3697
+ column,
3698
+ test: (cells) => cellOf(cells).toLowerCase().includes(needle)
3699
+ };
3700
+ }
3701
+ if (op === "eq" || op === "ne") {
3702
+ const numeric = valueKind === "number" ? Number(value) : void 0;
3703
+ const equal = (cells) => {
3704
+ const text = cellOf(cells);
3705
+ if (text === value) return true;
3706
+ return numeric !== void 0 && kindOf(text) === "number" && Number(text) === numeric;
3707
+ };
3708
+ return {
3709
+ column,
3710
+ test: op === "eq" ? equal : (cells) => !equal(cells)
3711
+ };
3712
+ }
3713
+ if (valueKind !== "number" && valueKind !== "date") fail(`${what}.value must be a number or an ISO date (YYYY-MM-DD) for op "${op}", received "${value}".`);
3714
+ const numericValue = valueKind === "number" ? Number(value) : 0;
3715
+ const satisfied = (order) => op === "gt" ? order > 0 : op === "gte" ? order >= 0 : op === "lt" ? order < 0 : order <= 0;
3716
+ return {
3717
+ column,
3718
+ test: (cells) => {
3719
+ const text = cellOf(cells);
3720
+ if (text === "") return false;
3721
+ const kind = kindOf(text);
3722
+ if (kind === valueKind) {
3723
+ const order = kind === "number" ? Number(text) - numericValue : text < value ? -1 : text > value ? 1 : 0;
3724
+ return satisfied(order);
3725
+ }
3726
+ uncomparable.set(column.label, (uncomparable.get(column.label) ?? 0) + 1);
3727
+ return false;
3728
+ }
3729
+ };
3730
+ }
3731
+ function parseLimit(v) {
3732
+ if (v === void 0) return {
3733
+ limit: QUERY_LIMITS.defaultRows,
3734
+ clamped: false
3735
+ };
3736
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 1) fail("\"limit\" must be a positive integer.");
3737
+ return v > QUERY_LIMITS.maxRows ? {
3738
+ limit: QUERY_LIMITS.maxRows,
3739
+ clamped: true
3740
+ } : {
3741
+ limit: v,
3742
+ clamped: false
3743
+ };
3744
+ }
3745
+ function parseQuery(args, view) {
3746
+ const op = oneOf(args["op"], ["aggregate", "rows"], "\"op\"");
3747
+ const uncomparable = /* @__PURE__ */ new Map();
3748
+ const filters = list(args["where"], "where", QUERY_LIMITS.maxFilters).map((raw, i) => compileFilter(view, raw, i, uncomparable));
3749
+ const { limit, clamped } = parseLimit(args["limit"]);
3750
+ const orderRaw = list(args["orderBy"], "orderBy", QUERY_LIMITS.maxOrderBy).map((raw, i) => {
3751
+ if (!isObject(raw)) fail(`orderBy[${i}] must be an object.`);
3752
+ noOtherKeys(raw, ["key", "desc"], `orderBy[${i}]`);
3753
+ if (typeof raw["key"] !== "string" || raw["key"].trim() === "") fail(`orderBy[${i}].key must be a string.`);
3754
+ if (raw["desc"] !== void 0 && typeof raw["desc"] !== "boolean") fail(`orderBy[${i}].desc must be a boolean.`);
3755
+ return {
3756
+ key: raw["key"].trim(),
3757
+ desc: raw["desc"] === true
3758
+ };
3759
+ });
3760
+ if (op === "rows") {
3761
+ if (args["groupBy"] !== void 0 || args["metrics"] !== void 0) fail("\"groupBy\" and \"metrics\" belong to op \"aggregate\". Use op \"rows\" with \"columns\" to list rows.");
3762
+ const columns = list(args["columns"], "columns", view.columns.length || 1).map((ref, i) => resolveColumn(view, ref, `columns[${i}]`));
3763
+ for (const entry of orderRaw) resolveColumn(view, entry.key, "orderBy.key");
3764
+ const offset = args["offset"];
3765
+ if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) fail("\"offset\" must be a non-negative integer.");
3766
+ return {
3767
+ op,
3768
+ filters,
3769
+ uncomparable,
3770
+ groups: [],
3771
+ metrics: [],
3772
+ columns: columns.length > 0 ? columns : [...view.columns],
3773
+ order: orderRaw,
3774
+ limit,
3775
+ limitClamped: clamped,
3776
+ offset: typeof offset === "number" ? offset : 0
3777
+ };
3778
+ }
3779
+ if (args["columns"] !== void 0 || args["offset"] !== void 0) fail("\"columns\" and \"offset\" belong to op \"rows\". Use op \"aggregate\" with \"groupBy\" and \"metrics\".");
3780
+ const groups = list(args["groupBy"], "groupBy", QUERY_LIMITS.maxGroupBy).map((raw, i) => {
3781
+ if (!isObject(raw)) fail(`groupBy[${i}] must be an object.`);
3782
+ noOtherKeys(raw, ["column", "bucket"], `groupBy[${i}]`);
3783
+ const column = resolveColumn(view, raw["column"], `groupBy[${i}].column`);
3784
+ return {
3785
+ column,
3786
+ bucket: raw["bucket"] === void 0 ? "value" : oneOf(raw["bucket"], BUCKETS, `groupBy[${i}].bucket`),
3787
+ name: column.name
3788
+ };
3789
+ });
3790
+ if (new Set(groups.map((g) => g.column.index)).size !== groups.length) fail("\"groupBy\" names the same column twice.");
3791
+ if (new Set(groups.map((g) => g.name)).size !== groups.length) for (const g of groups) g.name = g.column.label;
3792
+ const metricsRaw = list(args["metrics"], "metrics", QUERY_LIMITS.maxMetrics);
3793
+ const metrics = (metricsRaw.length > 0 ? metricsRaw : [{ fn: "count" }]).map((raw, i) => {
3794
+ if (!isObject(raw)) fail(`metrics[${i}] must be an object.`);
3795
+ noOtherKeys(raw, [
3796
+ "fn",
3797
+ "column",
3798
+ "as"
3799
+ ], `metrics[${i}]`);
3800
+ const fn = oneOf(raw["fn"], METRIC_FNS, `metrics[${i}].fn`);
3801
+ if (raw["column"] === void 0 && fn !== "count") fail(`metrics[${i}].column is required for "${fn}".`);
3802
+ const column = raw["column"] === void 0 ? void 0 : resolveColumn(view, raw["column"], `metrics[${i}].column`);
3803
+ if (raw["as"] !== void 0 && (typeof raw["as"] !== "string" || raw["as"].trim() === "")) fail(`metrics[${i}].as must be a non-empty string.`);
3804
+ const alias = typeof raw["as"] === "string" ? raw["as"].trim() : column === void 0 ? fn : `${fn}(${column.name})`;
3805
+ return column === void 0 ? {
3806
+ fn,
3807
+ alias
3808
+ } : {
3809
+ fn,
3810
+ column,
3811
+ alias
3812
+ };
3813
+ });
3814
+ if (new Set(metrics.map((m) => m.alias)).size !== metrics.length) fail("Two metrics have the same name; give one of them a distinct \"as\".");
3815
+ const available = () => [...metrics.map((m) => m.alias), ...groups.map((g) => g.name)].join(", ");
3816
+ for (const entry of orderRaw) {
3817
+ if (metrics.some((m) => m.alias === entry.key)) continue;
3818
+ const column = resolveColumn(view, entry.key, "orderBy.key");
3819
+ if (!groups.some((g) => g.column.index === column.index)) fail(`orderBy.key "${entry.key}" is neither a metric nor a grouped column. Available: ${available()}.`);
3820
+ }
3821
+ return {
3822
+ op,
3823
+ filters,
3824
+ uncomparable,
3825
+ groups,
3826
+ metrics,
3827
+ columns: [],
3828
+ order: orderRaw,
3829
+ limit,
3830
+ limitClamped: clamped,
3831
+ offset: 0
3832
+ };
3833
+ }
3834
+ function compareValues(a, b) {
3835
+ if (typeof a === "number" && typeof b === "number") return a - b;
3836
+ const sa = String(a);
3837
+ const sb = String(b);
3838
+ if (kindOf(sa) === "number" && kindOf(sb) === "number") return Number(sa) - Number(sb);
3839
+ return sa < sb ? -1 : sa > sb ? 1 : 0;
3840
+ }
3841
+ /** 空值永远排在最后,与升降序无关 */
3842
+ function compareNullable(a, b, desc) {
3843
+ if (a === null && b === null) return 0;
3844
+ if (a === null) return 1;
3845
+ if (b === null) return -1;
3846
+ const order = compareValues(a, b);
3847
+ return desc ? -order : order;
3848
+ }
3849
+ const clip$1 = (text) => {
3850
+ const chars = [...text];
3851
+ return chars.length <= QUERY_LIMITS.cellChars ? {
3852
+ text,
3853
+ clipped: false
3854
+ } : {
3855
+ text: `${chars.slice(0, QUERY_LIMITS.cellChars).join("")}…`,
3856
+ clipped: true
3857
+ };
3858
+ };
3859
+ function newAcc(metric) {
3860
+ return {
3861
+ rows: 0,
3862
+ numbers: 0,
3863
+ dates: 0,
3864
+ sum: new Summer(),
3865
+ ...metric.fn === "countDistinct" ? { distinct: /* @__PURE__ */ new Set() } : {},
3866
+ ...metric.fn === "median" ? { values: [] } : {}
3867
+ };
3868
+ }
3869
+ function finish(metric, acc) {
3870
+ switch (metric.fn) {
3871
+ case "count": return acc.rows;
3872
+ case "countDistinct": return acc.distinct?.size ?? 0;
3873
+ case "sum": return acc.numbers > 0 ? tidy(acc.sum.total) : null;
3874
+ case "avg": return acc.numbers > 0 ? tidy(acc.sum.total / acc.numbers) : null;
3875
+ case "median": {
3876
+ const values = acc.values ?? [];
3877
+ if (values.length === 0) return null;
3878
+ const sorted = Float64Array.from(values).sort();
3879
+ const mid = Math.floor(sorted.length / 2);
3880
+ return tidy(sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2);
3881
+ }
3882
+ case "min":
3883
+ case "max":
3884
+ if (acc.numbers === 0 && acc.dates === 0) return null;
3885
+ if (acc.numbers >= acc.dates) return (metric.fn === "min" ? acc.minN : acc.maxN) ?? null;
3886
+ return (metric.fn === "min" ? acc.minD : acc.maxD) ?? null;
3887
+ }
3888
+ }
3889
+ function runAggregate(view, query) {
3890
+ const { groups: groupSpecs, metrics } = query;
3891
+ const skipped = new Map(metrics.map((m) => [m.alias, {
3892
+ empty: 0,
3893
+ nonNumeric: 0
3894
+ }]));
3895
+ const unbucketed = /* @__PURE__ */ new Map();
3896
+ const table = /* @__PURE__ */ new Map();
3897
+ let matched = 0;
3898
+ const groupOf = (key) => {
3899
+ const id = key.map((part) => part === null ? "\0" : part).join("");
3900
+ let group = table.get(id);
3901
+ if (group === void 0) {
3902
+ group = {
3903
+ key,
3904
+ accs: metrics.map(newAcc)
3905
+ };
3906
+ table.set(id, group);
3907
+ }
3908
+ return group;
3909
+ };
3910
+ if (groupSpecs.length === 0) groupOf([]);
3911
+ for (const { cells } of view.data) {
3912
+ if (!query.filters.every((filter) => filter.test(cells))) continue;
3913
+ matched += 1;
3914
+ const group = groupOf(groupSpecs.map((spec) => {
3915
+ const text = (cells[spec.column.index] ?? "").trim();
3916
+ if (text === "") return null;
3917
+ if (spec.bucket === "value") return text;
3918
+ if (kindOf(text) === "date") return text.slice(0, spec.bucket === "year" ? 4 : spec.bucket === "month" ? 7 : 10);
3919
+ unbucketed.set(spec.name, (unbucketed.get(spec.name) ?? 0) + 1);
3920
+ return null;
3921
+ }));
3922
+ for (const [i, metric] of metrics.entries()) {
3923
+ const acc = group.accs[i];
3924
+ if (metric.column === void 0) {
3925
+ acc.rows += 1;
3926
+ continue;
3927
+ }
3928
+ const text = (cells[metric.column.index] ?? "").trim();
3929
+ if (metric.fn === "count") {
3930
+ if (text !== "") acc.rows += 1;
3931
+ continue;
3932
+ }
3933
+ if (metric.fn === "countDistinct") {
3934
+ if (text !== "") acc.distinct.add(text);
3935
+ continue;
3936
+ }
3937
+ const skip = skipped.get(metric.alias);
3938
+ const kind = kindOf(text);
3939
+ if (kind === "empty") skip.empty += 1;
3940
+ else if (kind === "number") {
3941
+ const x = Number(text);
3942
+ acc.numbers += 1;
3943
+ if (metric.fn === "sum" || metric.fn === "avg") acc.sum.add(x);
3944
+ else if (metric.fn === "median") acc.values.push(x);
3945
+ else if (metric.fn === "min") acc.minN = acc.minN === void 0 || x < acc.minN ? x : acc.minN;
3946
+ else acc.maxN = acc.maxN === void 0 || x > acc.maxN ? x : acc.maxN;
3947
+ } else if (kind === "date" && (metric.fn === "min" || metric.fn === "max")) {
3948
+ acc.dates += 1;
3949
+ if (metric.fn === "min") acc.minD = acc.minD === void 0 || text < acc.minD ? text : acc.minD;
3950
+ else acc.maxD = acc.maxD === void 0 || text > acc.maxD ? text : acc.maxD;
3951
+ } else skip.nonNumeric += 1;
3952
+ }
3953
+ }
3954
+ const rows = [...table.values()].map((group) => ({
3955
+ key: group.key,
3956
+ values: metrics.map((metric, i) => finish(metric, group.accs[i]))
3957
+ }));
3958
+ const keyIndexOf = (key) => {
3959
+ const column = metrics.some((m) => m.alias === key) ? void 0 : resolveColumn(view, key, "orderBy.key");
3960
+ return column === void 0 ? -1 : groupSpecs.findIndex((g) => g.column.index === column.index);
3961
+ };
3962
+ const comparators = [];
3963
+ for (const entry of query.order) {
3964
+ const metricIndex = metrics.findIndex((m) => m.alias === entry.key);
3965
+ const groupIndex = metricIndex === -1 ? keyIndexOf(entry.key) : -1;
3966
+ comparators.push((a, b) => metricIndex !== -1 ? compareNullable(a.values[metricIndex] ?? null, b.values[metricIndex] ?? null, entry.desc) : compareNullable(a.key[groupIndex] ?? null, b.key[groupIndex] ?? null, entry.desc));
3967
+ }
3968
+ if (comparators.length === 0 && groupSpecs.length > 0) {
3969
+ comparators.push((a, b) => compareNullable(a.values[0] ?? null, b.values[0] ?? null, true));
3970
+ for (let i = 0; i < groupSpecs.length; i += 1) comparators.push((a, b) => compareNullable(a.key[i] ?? null, b.key[i] ?? null, false));
3971
+ }
3972
+ rows.sort((a, b) => {
3973
+ for (const compare of comparators) {
3974
+ const order = compare(a, b);
3975
+ if (order !== 0) return order;
3976
+ }
3977
+ return 0;
3978
+ });
3979
+ let cellsTruncated = false;
3980
+ const names = groupSpecs.map((g) => g.name);
3981
+ const out = rows.slice(0, query.limit).map((row) => ({
3982
+ key: Object.fromEntries(row.key.map((part, i) => {
3983
+ if (part === null) return [names[i], null];
3984
+ const cut = clip$1(part);
3985
+ if (cut.clipped) cellsTruncated = true;
3986
+ return [names[i], cut.text];
3987
+ })),
3988
+ values: Object.fromEntries(metrics.map((metric, i) => [metric.alias, row.values[i] ?? null]))
3989
+ }));
3990
+ const skippedOut = {};
3991
+ for (const [alias, { empty, nonNumeric }] of skipped) {
3992
+ if (empty === 0 && nonNumeric === 0) continue;
3993
+ skippedOut[alias] = {
3994
+ ...empty > 0 ? { empty } : {},
3995
+ ...nonNumeric > 0 ? { nonNumeric } : {}
3996
+ };
3997
+ }
3998
+ return {
3999
+ matchedRows: matched,
4000
+ uncomparable: Object.fromEntries(query.uncomparable),
4001
+ groups: out,
4002
+ totalGroups: rows.length,
4003
+ ...Object.keys(skippedOut).length > 0 ? { skipped: skippedOut } : {},
4004
+ ...unbucketed.size > 0 ? { unbucketed: Object.fromEntries(unbucketed) } : {},
4005
+ cellsTruncated
4006
+ };
4007
+ }
4008
+ function runRows(view, query) {
4009
+ const matched = view.data.filter((entry) => query.filters.every((filter) => filter.test(entry.cells)));
4010
+ if (query.order.length > 0) {
4011
+ const keys = query.order.map((entry) => ({
4012
+ index: resolveColumn(view, entry.key, "orderBy.key").index,
4013
+ desc: entry.desc
4014
+ }));
4015
+ const valueOf = (cells, index) => {
4016
+ const text = (cells[index] ?? "").trim();
4017
+ return text === "" ? null : text;
4018
+ };
4019
+ matched.sort((a, b) => {
4020
+ for (const { index, desc } of keys) {
4021
+ const order = compareNullable(valueOf(a.cells, index), valueOf(b.cells, index), desc);
4022
+ if (order !== 0) return order;
4023
+ }
4024
+ return a.row - b.row;
4025
+ });
4026
+ }
4027
+ let cellsTruncated = false;
4028
+ const rows = matched.slice(query.offset, query.offset + query.limit).map((entry) => ({
4029
+ row: entry.row,
4030
+ cells: query.columns.map((column) => {
4031
+ const cut = clip$1((entry.cells[column.index] ?? "").trim());
4032
+ if (cut.clipped) cellsTruncated = true;
4033
+ return cut.text;
4034
+ })
4035
+ }));
4036
+ return {
4037
+ matchedRows: matched.length,
4038
+ uncomparable: Object.fromEntries(query.uncomparable),
4039
+ rows,
4040
+ columns: query.columns.map((column) => column.label),
4041
+ cellsTruncated
4042
+ };
4043
+ }
4044
+
4045
+ //#endregion
4046
+ //#region ../agent/src/table/toolSource.ts
4047
+ /** @experimental */
4048
+ const QUERY_TABLE_TOOL = "query_table";
4049
+ const DESCRIPTION$2 = "Run exact statistics over an Excel (.xlsx) workbook the user attached, without reading it row by row: describe its sheets and columns, aggregate (filter, group, count, distinct count, sum, average, min, max, median), or list matching rows. Every query covers all rows and is computed on the user’s device.";
4050
+ const DESCRIPTION_WITH_DOWNLOADS = DESCRIPTION$2.replace("the user attached,", "the user attached or downloaded,");
4051
+ const SYSTEM_PROMPT$3 = "query_table computes over every row of an attached .xlsx workbook. Use it for any count, total, average, extreme, median, ranking or breakdown, and quote its numbers as returned — they are exact. Do not estimate statistics from sample rows, and do not page through a whole sheet with read_document to compute them. When a result reports skipped or uncomparable cells, tell the user how many and why. Attachment ids from earlier messages of this conversation stay valid for this tool.";
4052
+ const SYSTEM_PROMPT_WITH_DOWNLOADS = `${SYSTEM_PROMPT$3} A downloaded workbook\u2019s id from list_downloaded_files works too.`;
4053
+ const ID_DESCRIPTION = "The attachment id from the message that carried the workbook.";
4054
+ const ID_DESCRIPTION_WITH_DOWNLOADS = `${ID_DESCRIPTION} For a downloaded file, use the id from list_downloaded_files; the user is asked for permission to read it.`;
4055
+ const COLUMN = {
4056
+ type: "string",
4057
+ description: "A column, by its header text or its column letter (A, B, …). Use the letter when headers repeat."
4058
+ };
4059
+ const INPUT_SCHEMA$2 = {
4060
+ type: "object",
4061
+ properties: {
4062
+ id: {
4063
+ type: "string",
4064
+ description: ID_DESCRIPTION
4065
+ },
4066
+ op: {
4067
+ type: "string",
4068
+ enum: [
4069
+ "describe",
4070
+ "aggregate",
4071
+ "rows"
4072
+ ],
4073
+ description: "\"describe\": sheets, columns, types and a sample. \"aggregate\": filter, group and compute metrics. \"rows\": list matching rows."
4074
+ },
4075
+ sheet: {
4076
+ anyOf: [{ type: "string" }, {
4077
+ type: "integer",
4078
+ minimum: 1
4079
+ }],
4080
+ description: "Sheet name or 1-based sheet number. Defaults to the first sheet that has data."
4081
+ },
4082
+ headerRow: {
4083
+ type: "integer",
4084
+ minimum: 1,
4085
+ description: "1-based worksheet row that holds the column headers. Defaults to the first non-empty row."
4086
+ },
4087
+ where: {
4088
+ type: "array",
4089
+ maxItems: QUERY_LIMITS.maxFilters,
4090
+ description: "Row filters; a row must satisfy all of them.",
4091
+ items: {
4092
+ type: "object",
4093
+ properties: {
4094
+ column: COLUMN,
4095
+ op: {
4096
+ type: "string",
4097
+ enum: [...FILTER_OPS]
4098
+ },
4099
+ value: {
4100
+ anyOf: [
4101
+ { type: "string" },
4102
+ { type: "number" },
4103
+ {
4104
+ type: "array",
4105
+ items: { anyOf: [{ type: "string" }, { type: "number" }] }
4106
+ }
4107
+ ],
4108
+ description: "Comparison value. Use an array for \"in\". Ignored by \"empty\" and \"notEmpty\". gt/gte/lt/lte compare numbers with numbers and ISO dates (YYYY-MM-DD) with dates."
4109
+ }
4110
+ },
4111
+ required: ["column", "op"],
4112
+ additionalProperties: false
4113
+ }
4114
+ },
4115
+ groupBy: {
4116
+ type: "array",
4117
+ maxItems: QUERY_LIMITS.maxGroupBy,
4118
+ description: "aggregate only. Group rows by these columns. Omit for a single overall result.",
4119
+ items: {
4120
+ type: "object",
4121
+ properties: {
4122
+ column: COLUMN,
4123
+ bucket: {
4124
+ type: "string",
4125
+ enum: [...BUCKETS],
4126
+ description: "\"value\" (default) groups by the cell text; year/month/day bucket ISO date cells."
4127
+ }
4128
+ },
4129
+ required: ["column"],
4130
+ additionalProperties: false
4131
+ }
4132
+ },
4133
+ metrics: {
4134
+ type: "array",
4135
+ maxItems: QUERY_LIMITS.maxMetrics,
4136
+ description: "aggregate only. Defaults to a single row count. \"count\" without a column counts rows; with a column it counts non-empty cells.",
4137
+ items: {
4138
+ type: "object",
4139
+ properties: {
4140
+ fn: {
4141
+ type: "string",
4142
+ enum: [...METRIC_FNS]
4143
+ },
4144
+ column: COLUMN,
4145
+ as: {
4146
+ type: "string",
4147
+ description: "Name for this metric in the result."
4148
+ }
4149
+ },
4150
+ required: ["fn"],
4151
+ additionalProperties: false
4152
+ }
4153
+ },
4154
+ columns: {
4155
+ type: "array",
4156
+ items: COLUMN,
4157
+ description: "rows only. Which columns to return. Defaults to all."
4158
+ },
4159
+ orderBy: {
4160
+ type: "array",
4161
+ maxItems: QUERY_LIMITS.maxOrderBy,
4162
+ description: "aggregate: a metric name or a grouped column; default is the first metric, largest first. rows: any column; default is sheet order.",
4163
+ items: {
4164
+ type: "object",
4165
+ properties: {
4166
+ key: { type: "string" },
4167
+ desc: { type: "boolean" }
4168
+ },
4169
+ required: ["key"],
4170
+ additionalProperties: false
4171
+ }
4172
+ },
4173
+ limit: {
4174
+ type: "integer",
4175
+ minimum: 1,
4176
+ description: `Groups or rows to return. Default ${QUERY_LIMITS.defaultRows}, at most ${QUERY_LIMITS.maxRows}.`
4177
+ },
4178
+ offset: {
4179
+ type: "integer",
4180
+ minimum: 0,
4181
+ description: "rows only. Skip this many matching rows."
4182
+ }
4183
+ },
4184
+ required: ["id", "op"],
4185
+ additionalProperties: false
4186
+ };
4187
+ function toolError$1(e) {
4188
+ return {
4189
+ ok: false,
4190
+ content: [],
4191
+ error: {
4192
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
4193
+ message: messageOf(e)
4194
+ }
4195
+ };
4196
+ }
4197
+ const byteLength = (text) => new TextEncoder().encode(text).length;
4198
+ /** 返回体超过上限时从尾部减条数。`more` = 条件匹配的比应给的多(limit/offset 已经截过一次),减过也如实标 `truncated` */
4199
+ function fit(head, field, items, more) {
4200
+ let count = items.length;
4201
+ for (;;) {
4202
+ const truncated = more || count < items.length;
4203
+ const json = JSON.stringify({
4204
+ ...head,
4205
+ [field]: items.slice(0, count),
4206
+ returned: count,
4207
+ ...truncated ? { truncated: true } : {}
4208
+ });
4209
+ if (count <= 1 || byteLength(json) <= QUERY_LIMITS.maxBytes) return json;
4210
+ count = Math.max(1, Math.floor(count * .7));
4211
+ }
4212
+ }
4213
+ /**
4214
+ * 在本机对整张表精确计算(0.26.0 分册 10)。
4215
+ *
4216
+ * 结构化入参,**不执行**任何模型给的表达式或代码;工具只认 `TableSource` 一个取件出口,
4217
+ * 不认识附件、下载与会话。
4218
+ * @experimental
4219
+ */
4220
+ function createTableQueryToolSource(options) {
4221
+ const schemaWithDownloads = {
4222
+ ...INPUT_SCHEMA$2,
4223
+ properties: {
4224
+ ...INPUT_SCHEMA$2.properties,
4225
+ id: {
4226
+ type: "string",
4227
+ description: ID_DESCRIPTION_WITH_DOWNLOADS
4228
+ }
4229
+ }
4230
+ };
4231
+ const downloads = () => options.downloadedFiles?.() === true;
4232
+ return {
4233
+ kind: "table-query",
4234
+ listToolSpecs: () => Promise.resolve([downloads() ? {
4235
+ name: QUERY_TABLE_TOOL,
4236
+ description: DESCRIPTION_WITH_DOWNLOADS,
4237
+ inputSchema: schemaWithDownloads
4238
+ } : {
4239
+ name: QUERY_TABLE_TOOL,
4240
+ description: DESCRIPTION$2,
4241
+ inputSchema: INPUT_SCHEMA$2
4242
+ }]),
4243
+ systemPrompt: () => Promise.resolve(downloads() ? SYSTEM_PROMPT_WITH_DOWNLOADS : SYSTEM_PROMPT$3),
4244
+ canHandle: (name) => name === QUERY_TABLE_TOOL,
4245
+ argCaptureTrust: (name) => name === "query_table" ? { tier: "reviewed" } : void 0,
4246
+ call: async (name, args, context) => {
4247
+ try {
4248
+ if (name !== "query_table") throw new WebSkillError("TOOL_NOT_FOUND", `Unknown tool ${name}.`);
4249
+ return await run(args, context);
3257
4250
  } catch (e) {
3258
4251
  return toolError$1(e);
3259
4252
  }
3260
4253
  }
3261
4254
  };
4255
+ async function run(args, context) {
4256
+ const id = args["id"];
4257
+ if (typeof id !== "string" || id === "") throw new WebSkillError("TOOL_UNSUPPORTED", "query_table requires the \"id\" of an attached Excel workbook.");
4258
+ const loaded = await options.tables.load(id, context);
4259
+ if (loaded === void 0) throw new WebSkillError("TOOL_UNSUPPORTED", `No readable workbook with id "${id}". Use the id of an Excel file attached in this conversation; files from other conversations are not available.`);
4260
+ const { workbook, name } = loaded;
4261
+ if (args["op"] === "describe") return {
4262
+ ok: true,
4263
+ content: [{
4264
+ type: "text",
4265
+ text: profileTable({
4266
+ name,
4267
+ id,
4268
+ workbook,
4269
+ ...args["sheet"] !== void 0 ? { sheet: args["sheet"] } : {},
4270
+ ...args["headerRow"] !== void 0 ? { headerRow: args["headerRow"] } : {}
4271
+ })
4272
+ }]
4273
+ };
4274
+ const view = buildView(workbook, args["sheet"], args["headerRow"]);
4275
+ const query = parseQuery(args, view);
4276
+ const output = query.op === "aggregate" ? runAggregate(view, query) : runRows(view, query);
4277
+ const head = {
4278
+ sheet: view.sheetName,
4279
+ headerRow: view.headerRow,
4280
+ dataRows: view.data.length,
4281
+ matchedRows: output.matchedRows,
4282
+ ...output.totalGroups !== void 0 ? { totalGroups: output.totalGroups } : {},
4283
+ ...output.columns !== void 0 ? { columns: output.columns } : {},
4284
+ ...output.skipped !== void 0 ? { skipped: output.skipped } : {},
4285
+ ...output.unbucketed !== void 0 ? { unbucketed: output.unbucketed } : {},
4286
+ ...Object.keys(output.uncomparable).length > 0 ? { uncomparable: output.uncomparable } : {},
4287
+ ...output.cellsTruncated ? { cellsTruncated: true } : {},
4288
+ ...query.limitClamped ? { limitClamped: QUERY_LIMITS.maxRows } : {}
4289
+ };
4290
+ const items = output.groups ?? output.rows ?? [];
4291
+ const more = output.groups !== void 0 ? (output.totalGroups ?? 0) > output.groups.length : query.offset + items.length < output.matchedRows;
4292
+ return {
4293
+ ok: true,
4294
+ content: [{
4295
+ type: "text",
4296
+ text: fit(head, output.groups !== void 0 ? "groups" : "rows", items, more)
4297
+ }]
4298
+ };
4299
+ }
3262
4300
  }
3263
4301
 
3264
4302
  //#endregion
@@ -4091,4 +5129,4 @@ function createDwgToolSource(options) {
4091
5129
  }
4092
5130
 
4093
5131
  //#endregion
4094
- export { CAPTURE_PAGE_IMAGE_TOOL, CLOSE_PAGE_TAB_TOOL, DEFAULT_MAX_DATA_SOURCE_BYTES, DELEGATE_TASK_TOOL, DELEGATION_SYSTEM_PROMPT, DOCUMENT_READ_BUDGET, DOWNLOAD_WAIT_DEFAULT_MS, DOWNLOAD_WAIT_MAX_MS, DOWNLOAD_WAIT_MIN_MS, DWG_MODEL_VIEW_ID, DataSourceCandidatePolicy, DataSourcePolicy, DelegationOrchestrator, DownloadedFilePolicy, EMU_PER_PIXEL, EMU_PER_POINT, EMU_PER_TWIP, EXPORT_SPREADSHEET_TOOL, FETCH_REMOTE_IMAGE_TOOL, FILL_TEMPLATE_TOOL, FOCUS_PAGE_TAB_TOOL, GENERATE_SKILL_TOOL, INSPECT_TEMPLATE_TOOL, LIST_DOWNLOADED_FILES_TOOL, LIST_PAGE_TABS_TOOL, MANAGE_TODO_TOOL, MAX_DIGIT_WIDTH, PAGE_ACTION_KINDS, PAGE_ACTION_SYSTEM_PROMPT, PAGE_ACTION_TOOL, PAGE_DOCUMENT_REF, PDF_GARBLED_RATIO, PDF_LARGE_IMAGE_RATIO, PERCEIVE_PAGE_TOOL, PERCEPTION_PAGING_DEFAULTS, PERCEPTION_SYSTEM_PROMPT, PageActionPolicy, PagePerceptionPolicy, READ_DOCUMENT_TOOL, READ_DOWNLOADED_FILE_TOOL, SCROLL_DIRECTIONS, SKILL_GENERATION_SYSTEM_PROMPT, SkillGenerator, TAB_WORKSET_DEFAULT_MAX, TAB_WORKSET_MAX_MAX, TAB_WORKSET_MIN_MAX, TAB_WORKSET_SYSTEM_PROMPT, TEMPLATE_INSPECT_BUDGET, TODO_SYSTEM_PROMPT, TabWorksetPolicy, TodoStore, VIEW_DWG_TOOL, asReadMode, clampDownloadWaitMs, clampWorksetMax, columnWidthToPixels, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createDwgToolSource, createHttpDataSourceTransport, createPageActionToolSource, createPageImageToolSource, createPagePerceptionToolSource, createRemoteImageToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTemplateToolSource, createTodoToolSource, defersImage, dwgPickView, dwgViewsOf, fitWithin, formatCellAddress, frameLabel, frameSteps, isPdfTextTrustworthy, isRangeTopLeft, measurePdfTextQuality, parseCellAddress, parseCellRange, rangeContains, readImageSize, sniffDocumentFormat, takesImage, toActionFrameScopes, toFrameScopes, withDelegationOrigin };
5132
+ export { CAPTURE_PAGE_IMAGE_TOOL, CLOSE_PAGE_TAB_TOOL, DEFAULT_MAX_DATA_SOURCE_BYTES, DELEGATE_TASK_TOOL, DELEGATION_SYSTEM_PROMPT, DOCUMENT_READ_BUDGET, DOWNLOAD_WAIT_DEFAULT_MS, DOWNLOAD_WAIT_MAX_MS, DOWNLOAD_WAIT_MIN_MS, DWG_MODEL_VIEW_ID, DataSourceCandidatePolicy, DataSourcePolicy, DelegationOrchestrator, DownloadedFilePolicy, EMU_PER_PIXEL, EMU_PER_POINT, EMU_PER_TWIP, EXPORT_SPREADSHEET_TOOL, FETCH_REMOTE_IMAGE_TOOL, FILL_TEMPLATE_TOOL, FOCUS_PAGE_TAB_TOOL, GENERATE_SKILL_TOOL, INSPECT_TEMPLATE_TOOL, LIST_DOWNLOADED_FILES_TOOL, LIST_PAGE_TABS_TOOL, MANAGE_TODO_TOOL, MAX_DIGIT_WIDTH, PAGE_ACTION_KINDS, PAGE_ACTION_SYSTEM_PROMPT, PAGE_ACTION_TOOL, PAGE_DOCUMENT_REF, PDF_GARBLED_RATIO, PDF_LARGE_IMAGE_RATIO, PERCEIVE_PAGE_TOOL, PERCEPTION_PAGING_DEFAULTS, PERCEPTION_SYSTEM_PROMPT, PageActionPolicy, PagePerceptionPolicy, QUERY_TABLE_TOOL, READ_DOCUMENT_TOOL, READ_DOWNLOADED_FILE_TOOL, SCROLL_DIRECTIONS, SKILL_GENERATION_SYSTEM_PROMPT, SkillGenerator, TAB_WORKSET_DEFAULT_MAX, TAB_WORKSET_MAX_MAX, TAB_WORKSET_MIN_MAX, TAB_WORKSET_SYSTEM_PROMPT, TEMPLATE_INSPECT_BUDGET, TODO_SYSTEM_PROMPT, TabWorksetPolicy, TodoStore, VIEW_DWG_TOOL, asReadMode, clampDownloadWaitMs, clampWorksetMax, columnWidthToPixels, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createDwgToolSource, createHttpDataSourceTransport, createPageActionToolSource, createPageImageToolSource, createPagePerceptionToolSource, createRemoteImageToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTableQueryToolSource, createTemplateToolSource, createTodoToolSource, defersImage, dwgPickView, dwgViewsOf, fitWithin, formatCellAddress, frameLabel, frameSteps, isPdfTextTrustworthy, isRangeTopLeft, measurePdfTextQuality, parseCellAddress, parseCellRange, profileTable, rangeContains, readImageSize, sniffDocumentFormat, takesImage, toActionFrameScopes, toFrameScopes, withDelegationOrigin };