@cydm/pie 1.0.29 → 1.0.31

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.
@@ -3968,11 +3968,12 @@ var AgentSessionQueue = class {
3968
3968
  clearEditor: options.clearEditor ?? true,
3969
3969
  visibility: options.visibility ?? "user",
3970
3970
  deliveredToAgent: false,
3971
+ deliverWhileStreaming: options.deliverWhileStreaming ?? true,
3971
3972
  messages: [message],
3972
3973
  createdAt: Date.now()
3973
3974
  };
3974
3975
  this.queuedInputs.push(queuedInput);
3975
- if (this.getIsStreaming()) {
3976
+ if (this.getIsStreaming() && queuedInput.deliverWhileStreaming) {
3976
3977
  this.deliverToAgent(queuedInput);
3977
3978
  }
3978
3979
  }
@@ -4004,11 +4005,12 @@ var AgentSessionQueue = class {
4004
4005
  this.emitQueueChanged();
4005
4006
  return;
4006
4007
  }
4007
- const delivered = this.queuedInputs.shift();
4008
- if (delivered) {
4008
+ const delivered = this.queuedInputs[0];
4009
+ if (delivered?.deliveredToAgent) {
4010
+ this.queuedInputs.shift();
4009
4011
  this.emit({ type: "queued_turn_dispatched", queuedInput: cloneQueuedInput(delivered) });
4012
+ this.emitQueueChanged();
4010
4013
  }
4011
- this.emitQueueChanged();
4012
4014
  }
4013
4015
  clear() {
4014
4016
  if (this.queuedInputs.length === 0 && !this.agent.hasQueuedMessages()) {
@@ -4170,6 +4172,9 @@ var AgentSessionQueueDispatcher = class {
4170
4172
  });
4171
4173
  }
4172
4174
  resolvePending() {
4175
+ if (this.dispatchTimer !== void 0) {
4176
+ return;
4177
+ }
4173
4178
  const resolve3 = this.resolvePendingDispatch;
4174
4179
  this.pendingDispatch = void 0;
4175
4180
  this.resolvePendingDispatch = void 0;
@@ -5662,7 +5667,7 @@ function createAskUserCapability() {
5662
5667
  {
5663
5668
  name: "ask_user_multi",
5664
5669
  label: "Ask User",
5665
- description: `Ask one or more questions with review and back navigation. Use this for all user questioning flows, including demonstrations that should actually ask the user instead of only describing the questionnaire. When you decide to use this tool, invoke it immediately. Do not print explanatory preambles, numbered plans, or example JSON in chat before calling it. For a generic demo such as '\u6F14\u793A\u4E00\u4E0B ask \u5DE5\u5177', '\u6F14\u793A\u4E00\u4E0B\u63D0\u95EE\u5DE5\u5177', or 'ask me', ask exactly one simple single-choice question with 3-5 explicit options so the user sees a selector instead of a free-text editor. Do not switch to an open-ended or multi-question flow unless the user explicitly asks for it. Prefer the shortest valid shape. For a single question, use {"question":"Choose a color","options":["Red","Blue","Green"]}. You may also use {"prompt":"Choose a color","options":[...]} for compatibility. For multiple questions, use \`questions\`. Question ids are optional and will be auto-generated.`,
5670
+ description: 'Ask the user one or more structured questions with review/back navigation. Invoke it directly when you need user input; do not print example JSON first. For a simple demo, ask one single-choice question with 3-5 options. Prefer the shortest shape: {"question":"Choose a color","options":["Red","Blue","Green"]}. For multiple questions, use `questions`; ids are optional.',
5666
5671
  parameters: AskUserMultiParamsSchema,
5667
5672
  async execute(toolCallIdOrArgs, maybeArgs) {
5668
5673
  try {
@@ -5805,9 +5810,7 @@ function createAskUserCapability() {
5805
5810
  requiredInteractions: ["select_one", "confirm", "text_input", "multiline_input"],
5806
5811
  systemPromptAdditions: [
5807
5812
  "When the user asks you to ask questions, collect choices, or gather structured input, call `ask_user_multi` instead of describing the questions in plain text.",
5808
- "When demonstrating `ask_user_multi`, call it immediately instead of first printing explanations, plans, or example JSON.",
5809
- "When the user asks for a generic ask-tool demo, such as '\u6F14\u793A\u4E00\u4E0B ask \u5DE5\u5177', '\u6F14\u793A\u4E00\u4E0B\u63D0\u95EE\u5DE5\u5177', or 'ask me', ask exactly one simple single-choice question with explicit options so the interaction opens as a selector.",
5810
- "For a single question, prefer the shortest valid argument shape such as `question` plus optional `options`."
5813
+ "When demonstrating `ask_user_multi`, call it immediately. For a generic demo, ask one simple single-choice question with explicit options."
5811
5814
  ]
5812
5815
  }),
5813
5816
  tools
@@ -6305,6 +6308,13 @@ var BINARY_MAGIC_PREFIX_BYTES = 8 * 1024;
6305
6308
  var MAX_INLINE_IMAGE_BYTES = 20 * 1024 * 1024;
6306
6309
  var MAX_CROSS_HOST_LINE_NUMBER = 2147483647;
6307
6310
  var INLINE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
6311
+ function createReadErrorResult(message, details) {
6312
+ return {
6313
+ content: [{ type: "text", text: `read_file failed: ${message}` }],
6314
+ details,
6315
+ isError: true
6316
+ };
6317
+ }
6308
6318
  function createReadTool(cwd, options) {
6309
6319
  const fs2 = getFileSystem();
6310
6320
  const fileObservationState = options?.fileObservationState ?? createFileObservationState();
@@ -6325,13 +6335,13 @@ function createReadTool(cwd, options) {
6325
6335
  execute: async (_toolCallId, params, signal) => {
6326
6336
  const { path: path4, root, offset, limit, question } = params;
6327
6337
  if (offset !== void 0 && (!Number.isSafeInteger(offset) || offset < 1 || offset > MAX_CROSS_HOST_LINE_NUMBER)) {
6328
- throw new Error(`Invalid read_file arguments: offset must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
6338
+ return createReadErrorResult(`Invalid arguments: offset must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
6329
6339
  }
6330
6340
  if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CROSS_HOST_LINE_NUMBER)) {
6331
- throw new Error(`Invalid read_file arguments: limit must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
6341
+ return createReadErrorResult(`Invalid arguments: limit must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
6332
6342
  }
6333
6343
  if (offset !== void 0 && limit !== void 0 && offset - 1 + limit > MAX_CROSS_HOST_LINE_NUMBER) {
6334
- throw new Error(`Invalid read_file arguments: offset and limit exceed the ${MAX_CROSS_HOST_LINE_NUMBER}-line cross-host range.`);
6344
+ return createReadErrorResult(`Invalid arguments: offset and limit exceed the ${MAX_CROSS_HOST_LINE_NUMBER}-line cross-host range.`);
6335
6345
  }
6336
6346
  let absolutePath;
6337
6347
  try {
@@ -6339,11 +6349,11 @@ function createReadTool(cwd, options) {
6339
6349
  } catch (error) {
6340
6350
  const message = error instanceof Error ? error.message : String(error);
6341
6351
  if (/outside sandbox|outside root|escapes sandbox/i.test(message)) {
6342
- throw new Error(
6352
+ return createReadErrorResult(
6343
6353
  `${message}. read_file is workspace-scoped; for intentional runtime logs or temporary files outside the workspace, use bash with a bounded command such as head, tail, sed -n, or cat on that path.`
6344
6354
  );
6345
6355
  }
6346
- throw error;
6356
+ return createReadErrorResult(message);
6347
6357
  }
6348
6358
  return new Promise(
6349
6359
  (resolve3, reject) => {
@@ -6474,6 +6484,24 @@ function createReadTool(cwd, options) {
6474
6484
  });
6475
6485
  return;
6476
6486
  }
6487
+ const textBinaryInfo = detectTextOnlyBinary(prefix, absolutePath, stat.size > prefix.length);
6488
+ if (textBinaryInfo) {
6489
+ resolve3({
6490
+ content: [{
6491
+ type: "text",
6492
+ text: `Cannot read binary file ${path4}: ${textBinaryInfo.mimeType}. Use a file-understanding handler for supported media/documents, or inspect metadata with a bounded shell command.`
6493
+ }],
6494
+ details: {
6495
+ understanding: {
6496
+ kind: textBinaryInfo.kind,
6497
+ mimeType: textBinaryInfo.mimeType,
6498
+ fallbackReason: "binary content is not safe to display as text"
6499
+ }
6500
+ },
6501
+ isError: true
6502
+ });
6503
+ return;
6504
+ }
6477
6505
  const startLine = offset === void 0 ? 0 : offset - 1;
6478
6506
  const startLineDisplay = startLine + 1;
6479
6507
  const range2 = await fs2.readTextRange(absolutePath, {
@@ -6595,7 +6623,12 @@ Base directory: ${nodePath.dirname(absolutePath)}
6595
6623
  });
6596
6624
  } catch (error) {
6597
6625
  if (!aborted) {
6598
- reject(error);
6626
+ const message = error instanceof Error ? error.message : String(error);
6627
+ if (/aborted/i.test(message)) {
6628
+ reject(error);
6629
+ return;
6630
+ }
6631
+ resolve3(createReadErrorResult(message, { path: absolutePath }));
6599
6632
  }
6600
6633
  } finally {
6601
6634
  signal?.removeEventListener("abort", onAbort);
@@ -6725,7 +6758,10 @@ function looksLikeBinaryText(input) {
6725
6758
  return characters > 0 && suspiciousControls / characters > 0.02;
6726
6759
  }
6727
6760
  function detectTextOnlyBinary(input, filePath, prefixMayBeTruncated) {
6728
- return detectBinaryMagic(input) ?? detectBinaryKind(filePath) ?? (looksLikeBinary(input, prefixMayBeTruncated) ? { kind: "document", mimeType: "application/octet-stream" } : void 0);
6761
+ const magicInfo = detectBinaryMagic(input);
6762
+ if (magicInfo) return magicInfo;
6763
+ if (!looksLikeBinary(input, prefixMayBeTruncated)) return void 0;
6764
+ return detectBinaryKind(filePath) ?? { kind: "document", mimeType: "application/octet-stream" };
6729
6765
  }
6730
6766
  async function readImageInfo(path4, fs2) {
6731
6767
  if (typeof Buffer === "undefined") {
@@ -7176,6 +7212,25 @@ function createEditErrorResult(params) {
7176
7212
  isError: true
7177
7213
  };
7178
7214
  }
7215
+ function createEditThrownErrorResult(message, path4) {
7216
+ return {
7217
+ content: [{ type: "text", text: `edit_file failed: ${message}` }],
7218
+ details: {
7219
+ status: "error",
7220
+ matchKind: "none",
7221
+ changed: false,
7222
+ path: path4,
7223
+ diff: "",
7224
+ oldTextOccurrences: 0,
7225
+ newTextOccurrences: 0,
7226
+ replaceAll: false,
7227
+ replacedOccurrences: 0,
7228
+ errorReason: "old_text_not_found",
7229
+ recoveryHint: "Check the path and read the current file before retrying."
7230
+ },
7231
+ isError: true
7232
+ };
7233
+ }
7179
7234
  function createEditTool(cwd, options) {
7180
7235
  const fs2 = getFileSystem();
7181
7236
  const fileObservationState = options?.fileObservationState ?? createFileObservationState();
@@ -7201,8 +7256,15 @@ function createEditTool(cwd, options) {
7201
7256
  parameters: editSchema,
7202
7257
  execute: async (_toolCallId, params, signal) => {
7203
7258
  const { path: path4, root } = params;
7204
- const replacements = validateEditArgs(params);
7205
- const absolutePath = resolveToCwd(path4, cwd, options, root);
7259
+ let replacements;
7260
+ let absolutePath;
7261
+ try {
7262
+ replacements = validateEditArgs(params);
7263
+ absolutePath = resolveToCwd(path4, cwd, options, root);
7264
+ } catch (error) {
7265
+ const message = error instanceof Error ? error.message : String(error);
7266
+ return createEditThrownErrorResult(message, typeof path4 === "string" ? path4 : void 0);
7267
+ }
7206
7268
  return new Promise((resolve3, reject) => {
7207
7269
  if (signal?.aborted) {
7208
7270
  reject(new Error("Operation aborted"));
@@ -7225,7 +7287,7 @@ function createEditTool(cwd, options) {
7225
7287
  await fs2.access(absolutePath);
7226
7288
  } catch {
7227
7289
  if (signal) signal.removeEventListener("abort", onAbort);
7228
- reject(new Error(`File not found: ${path4}`));
7290
+ resolve3(createEditThrownErrorResult(`File not found: ${path4}`, path4));
7229
7291
  return;
7230
7292
  }
7231
7293
  throwIfAborted();
@@ -7377,7 +7439,12 @@ function createEditTool(cwd, options) {
7377
7439
  signal.removeEventListener("abort", onAbort);
7378
7440
  }
7379
7441
  if (!aborted) {
7380
- reject(error);
7442
+ const message = error instanceof Error ? error.message : String(error);
7443
+ if (/aborted/i.test(message)) {
7444
+ reject(error);
7445
+ return;
7446
+ }
7447
+ resolve3(createEditThrownErrorResult(message, path4));
7381
7448
  }
7382
7449
  }
7383
7450
  })();
@@ -9527,6 +9594,13 @@ var MAX_TEXT_GREP_TOTAL_BYTES = 64 * 1024 * 1024;
9527
9594
  var MAX_GREP_PATTERN_LENGTH = 512;
9528
9595
  var MAX_GREP_CONTEXT_LINES = 20;
9529
9596
  var MAX_GREP_RESULT_LIMIT = 1e3;
9597
+ function boundedErrorMessage(prefix, error, hint) {
9598
+ const message = error instanceof Error ? error.message : String(error);
9599
+ const text = `${prefix}: ${message}
9600
+ ${hint}`;
9601
+ return text.length > 1e3 ? `${text.slice(0, 1e3)}
9602
+ [error truncated]` : text;
9603
+ }
9530
9604
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "__pycache__", ".svn", ".hg"]);
9531
9605
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
9532
9606
  ".png",
@@ -9873,7 +9947,7 @@ function createGrepTextTool(cwd, options) {
9873
9947
  return {
9874
9948
  name: "grep_text",
9875
9949
  label: "grep_text",
9876
- description: `Search file contents for a bounded regex or literal pattern. ${pathResolutionDescription} Regex evaluation is isolated by hosts that support general expressions; other hosts require a repetition-free regex or literal=true. outputMode='content' returns matching lines with file paths and line numbers, outputMode='files_with_matches' returns matching file paths, and outputMode='count' returns per-file match counts. Output is truncated to ${DEFAULT_LIMIT2} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
9950
+ description: `Search file contents with a bounded regex or literal pattern. ${pathResolutionDescription} Use literal=true for plain text or unsafe regexes. outputMode: content (matching lines), files_with_matches, or count. Output is capped at ${DEFAULT_LIMIT2} results or ${DEFAULT_MAX_BYTES / 1024}KB; long lines cap at ${GREP_MAX_LINE_LENGTH} chars.`,
9877
9951
  parameters: grepSchema,
9878
9952
  execute: async (_toolCallId, params, signal) => {
9879
9953
  const {
@@ -9888,8 +9962,17 @@ function createGrepTextTool(cwd, options) {
9888
9962
  offset: rawOffset,
9889
9963
  limit
9890
9964
  } = params;
9891
- assertGrepPatternLength(pattern);
9892
- const fileGlobMatcher = createGrepGlobMatcher(globPattern);
9965
+ let fileGlobMatcher;
9966
+ try {
9967
+ assertGrepPatternLength(pattern);
9968
+ fileGlobMatcher = createGrepGlobMatcher(globPattern);
9969
+ } catch (error) {
9970
+ return {
9971
+ content: [{ type: "text", text: boundedErrorMessage("grep_text failed", error, "Use literal=true for plain text, shorten the pattern, or narrow the glob.") }],
9972
+ details: { searchIncomplete: true, safetyLimit: "io_error" },
9973
+ isError: true
9974
+ };
9975
+ }
9893
9976
  return new Promise((resolve3, reject) => {
9894
9977
  if (signal?.aborted) {
9895
9978
  reject(new Error("Operation aborted"));
@@ -9912,7 +9995,12 @@ function createGrepTextTool(cwd, options) {
9912
9995
  const stat = await fs2.stat(searchPath);
9913
9996
  isDir = stat.isDirectory;
9914
9997
  } catch {
9915
- reject(new Error(`Path not found: ${searchPath}`));
9998
+ resolve3({
9999
+ content: [{ type: "text", text: `grep_text failed: Path not found: ${searchPath}
10000
+ Check the path or search from the workspace root.` }],
10001
+ details: { searchIncomplete: true, safetyLimit: "io_error" },
10002
+ isError: true
10003
+ });
9916
10004
  return;
9917
10005
  }
9918
10006
  const isGitignored = await loadGitignoreMatcher(searchPath, fs2, {
@@ -9938,7 +10026,11 @@ function createGrepTextTool(cwd, options) {
9938
10026
  } else {
9939
10027
  }
9940
10028
  } catch (e) {
9941
- reject(new Error(`Invalid regex pattern: ${e.message}`));
10029
+ resolve3({
10030
+ content: [{ type: "text", text: boundedErrorMessage("grep_text failed: invalid regex pattern", e, "Use literal=true for plain text, or simplify the regex.") }],
10031
+ details: { searchIncomplete: true, safetyLimit: "io_error" },
10032
+ isError: true
10033
+ });
9942
10034
  return;
9943
10035
  }
9944
10036
  let outputLines = [];
@@ -10174,7 +10266,16 @@ function createGrepTextTool(cwd, options) {
10174
10266
  details: Object.keys(details).length > 0 ? details : void 0
10175
10267
  });
10176
10268
  } catch (err) {
10177
- reject(err);
10269
+ const message = err instanceof Error ? err.message : String(err);
10270
+ if (/aborted/i.test(message)) {
10271
+ reject(err);
10272
+ return;
10273
+ }
10274
+ resolve3({
10275
+ content: [{ type: "text", text: boundedErrorMessage("grep_text failed", err, "Check the path/pattern, use literal=true for plain text, or narrow the search.") }],
10276
+ details: { searchIncomplete: true, safetyLimit: "io_error" },
10277
+ isError: true
10278
+ });
10178
10279
  } finally {
10179
10280
  await regexSession?.dispose();
10180
10281
  signal?.removeEventListener("abort", onAbort);
@@ -10191,6 +10292,13 @@ var MAX_RESULT_LIMIT2 = 1e4;
10191
10292
  var MAX_SCANNED_DIRECTORIES = 1e4;
10192
10293
  var MAX_SCANNED_FILES = 1e4;
10193
10294
  var MAX_DIRECTORY_ENTRIES = 1e4;
10295
+ function boundedFindError(error) {
10296
+ const message = error instanceof Error ? error.message : String(error);
10297
+ const text = `find_files failed: ${message}
10298
+ Shorten the pattern, remove unsupported glob syntax, or narrow the search path.`;
10299
+ return text.length > 1e3 ? `${text.slice(0, 1e3)}
10300
+ [error truncated]` : text;
10301
+ }
10194
10302
  var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__", ".svn", ".hg"]);
10195
10303
  function createFindFilesMatcher(pattern) {
10196
10304
  assertBoundedGlobPattern(pattern, "find_files pattern");
@@ -10358,7 +10466,16 @@ function createFindFilesTool(cwd, options) {
10358
10466
  parameters: findSchema,
10359
10467
  execute: async (_toolCallId, params, signal) => {
10360
10468
  const { pattern, path: searchDir, root, limit } = params;
10361
- const matchesPattern = createFindFilesMatcher(pattern);
10469
+ let matchesPattern;
10470
+ try {
10471
+ matchesPattern = createFindFilesMatcher(pattern);
10472
+ } catch (error) {
10473
+ return {
10474
+ content: [{ type: "text", text: boundedFindError(error) }],
10475
+ details: { searchIncomplete: true },
10476
+ isError: true
10477
+ };
10478
+ }
10362
10479
  return new Promise((resolve3, reject) => {
10363
10480
  if (signal?.aborted) {
10364
10481
  reject(new Error("Operation aborted"));
@@ -10380,7 +10497,12 @@ function createFindFilesTool(cwd, options) {
10380
10497
  throw new Error(`Invalid find_files arguments: limit must be between 1 and ${MAX_RESULT_LIMIT2}.`);
10381
10498
  }
10382
10499
  if (!await fs2.exists(searchPath)) {
10383
- reject(new Error(`Path not found: ${searchPath}`));
10500
+ resolve3({
10501
+ content: [{ type: "text", text: `find_files failed: Path not found: ${searchPath}
10502
+ Check the path or search from the workspace root.` }],
10503
+ details: { searchIncomplete: true },
10504
+ isError: true
10505
+ });
10384
10506
  return;
10385
10507
  }
10386
10508
  let results;
@@ -10447,7 +10569,16 @@ function createFindFilesTool(cwd, options) {
10447
10569
  });
10448
10570
  } catch (e) {
10449
10571
  signal?.removeEventListener("abort", onAbort);
10450
- reject(e);
10572
+ const message = e instanceof Error ? e.message : String(e);
10573
+ if (/aborted/i.test(message)) {
10574
+ reject(e);
10575
+ return;
10576
+ }
10577
+ resolve3({
10578
+ content: [{ type: "text", text: boundedFindError(e) }],
10579
+ details: { searchIncomplete: true },
10580
+ isError: true
10581
+ });
10451
10582
  }
10452
10583
  })();
10453
10584
  });
@@ -12645,10 +12776,19 @@ function createSharedWebFetchTool(deps = {}) {
12645
12776
 
12646
12777
  // ../../packages/shared-headless-capabilities/src/web-research.ts
12647
12778
  var webResearchSchema = Type.Object({
12648
- query: Type.String({
12779
+ query: Type.Optional(Type.String({
12649
12780
  minLength: 2,
12650
- description: "The research question or topic. Use for end-to-end public web research."
12651
- }),
12781
+ description: "Research/search query. For url fetch mode, use as an optional extraction prompt."
12782
+ })),
12783
+ url: Type.Optional(Type.String({
12784
+ description: "Specific http(s) URL to fetch and read."
12785
+ })),
12786
+ mode: Type.Optional(Type.Union([
12787
+ Type.Literal("research"),
12788
+ Type.Literal("search")
12789
+ ], {
12790
+ description: "Default research searches and fetches cited sources. Use search for low-level provider search results."
12791
+ })),
12652
12792
  allowed_domains: Type.Optional(Type.Array(Type.String({
12653
12793
  description: "Only include sources from these domains. Use only when the user explicitly asks to limit sources."
12654
12794
  }))),
@@ -12670,12 +12810,70 @@ function isRecord5(value) {
12670
12810
  function textFromResult(result) {
12671
12811
  return result.content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n").trim();
12672
12812
  }
12813
+ function makeEmptyResearchDetails(params) {
12814
+ return {
12815
+ query: params.query,
12816
+ actualQueries: params.actualQueries ?? [],
12817
+ sources: params.sources ?? [],
12818
+ fetchedSources: params.fetchedSources ?? [],
12819
+ providerAttempts: params.providerAttempts ?? [],
12820
+ attempts: params.attempts ?? [],
12821
+ qualityScore: params.qualityScore ?? 0,
12822
+ warnings: params.warnings ?? [],
12823
+ browserRequiredSources: params.browserRequiredSources ?? [],
12824
+ routeRankReason: params.routeRankReason,
12825
+ latencyMetrics: {
12826
+ totalDurationMs: Date.now() - params.totalStartedAt,
12827
+ searchDurationMs: params.searchDurationMs ?? 0,
12828
+ fetchDurationMs: params.fetchDurationMs ?? 0,
12829
+ browserDurationMs: params.browserDurationMs ?? 0
12830
+ },
12831
+ costEstimate: {
12832
+ status: "unknown",
12833
+ reason: "Provider usage is not yet normalized across web research search/fetch/browser routes."
12834
+ }
12835
+ };
12836
+ }
12673
12837
  function clamp(value, min, max) {
12674
12838
  return Math.min(max, Math.max(min, value));
12675
12839
  }
12676
12840
  function normalizeHost(hostname) {
12677
12841
  return hostname.toLowerCase().replace(/^www\./, "");
12678
12842
  }
12843
+ function normalizeDomainFilter(value) {
12844
+ let domain = value.trim().toLowerCase();
12845
+ if (!domain) return void 0;
12846
+ domain = domain.replace(/^https?:\/\//i, "");
12847
+ domain = domain.replace(/^[^@/]+@/, "");
12848
+ domain = domain.split(/[/?#]/)[0] || "";
12849
+ if (domain.startsWith("[") && domain.includes("]")) {
12850
+ domain = domain.slice(1, domain.indexOf("]"));
12851
+ } else {
12852
+ domain = domain.split(":")[0] || "";
12853
+ }
12854
+ domain = domain.replace(/^\*\./, "").replace(/^www\./, "").replace(/\.$/, "");
12855
+ return domain || void 0;
12856
+ }
12857
+ function hostMatchesDomainFilter(hostname, filter2) {
12858
+ const host = normalizeHost(hostname);
12859
+ const domain = normalizeDomainFilter(filter2);
12860
+ if (!domain) return false;
12861
+ return host === domain || host.endsWith(`.${domain}`);
12862
+ }
12863
+ function directUrlFilterFailure(url, input) {
12864
+ const allowedDomains = input.allowed_domains?.length ? input.allowed_domains : void 0;
12865
+ const blockedDomains = allowedDomains ? void 0 : input.blocked_domains?.length ? input.blocked_domains : void 0;
12866
+ if (!allowedDomains && !blockedDomains) return void 0;
12867
+ const parsed = parseResearchUrl(url);
12868
+ if (!parsed) return `web_research URL mode only supports http(s) URLs: ${url}`;
12869
+ if (allowedDomains && !allowedDomains.some((domain) => hostMatchesDomainFilter(parsed.hostname, domain))) {
12870
+ return `web_research blocked ${url}: URL host is not in allowed_domains.`;
12871
+ }
12872
+ if (blockedDomains?.some((domain) => hostMatchesDomainFilter(parsed.hostname, domain))) {
12873
+ return `web_research blocked ${url}: URL host matches blocked_domains.`;
12874
+ }
12875
+ return void 0;
12876
+ }
12679
12877
  function parseResearchUrl(value) {
12680
12878
  const trimmed = value.trim();
12681
12879
  const match2 = /^(https?):\/\/([^/?#]+)([^?#]*)?(\?[^#]*)?(#.*)?$/i.exec(trimmed);
@@ -12967,7 +13165,7 @@ function createSharedWebResearchTool(deps) {
12967
13165
  return {
12968
13166
  name: "web_research",
12969
13167
  label: "web_research",
12970
- description: "Run end-to-end public web research: plan search queries, search provider-native sources, fetch credible pages, and return a cited research pack. Use for general online research, current information, official docs, news, releases, issues, and webpage summaries. Use web_search/web_fetch directly only when you need precise low-level control. Do not use bash/curl/Python as a web research fallback.",
13168
+ description: "Canonical web tool. For normal research, pass query. To read a known page, pass url. For low-level search results only, pass query with mode='search'. Use for current information, official docs, news, releases, issues, and webpage summaries. Do not use bash/curl/Python as a web fallback.",
12971
13169
  parameters: webResearchSchema,
12972
13170
  capabilityMetadata: {
12973
13171
  riskClass: "network",
@@ -12986,6 +13184,8 @@ function createSharedWebResearchTool(deps) {
12986
13184
  const rankedCandidates = sortCandidatesByHealth(activeCandidates, currentHealth);
12987
13185
  const routeRankReason = describeRouteRanking(rankedCandidates.length ? rankedCandidates : activeCandidates, currentHealth);
12988
13186
  const query = String(input.query || "").trim();
13187
+ const directUrl = typeof input.url === "string" ? String(input.url).trim() : "";
13188
+ const mode = input.mode === "search" ? "search" : "research";
12989
13189
  const maxSources = clamp(Math.floor(input.max_sources ?? 3), 1, 8);
12990
13190
  const searchAttemptTimeoutMs = Math.max(1e3, deps.searchAttemptTimeoutMs ?? deps.timeoutMs ?? DEFAULT_WEB_RESEARCH_SEARCH_ATTEMPT_TIMEOUT_MS);
12991
13191
  const fetchAttemptTimeoutMs = Math.max(1e3, deps.fetchAttemptTimeoutMs ?? DEFAULT_WEB_RESEARCH_FETCH_ATTEMPT_TIMEOUT_MS);
@@ -13003,6 +13203,242 @@ function createSharedWebResearchTool(deps) {
13003
13203
  let searchDurationMs = 0;
13004
13204
  let fetchDurationMs = 0;
13005
13205
  let browserDurationMs = 0;
13206
+ if (directUrl) {
13207
+ if (input.allowed_domains?.length && input.blocked_domains?.length) {
13208
+ warnings.push("Both allowed_domains and blocked_domains were provided; web_research used allowed_domains and ignored blocked_domains for provider compatibility.");
13209
+ }
13210
+ const filterFailure = directUrlFilterFailure(directUrl, input);
13211
+ if (filterFailure) {
13212
+ return {
13213
+ content: [{ type: "text", text: filterFailure }],
13214
+ details: makeEmptyResearchDetails({
13215
+ query: query || directUrl,
13216
+ actualQueries: query ? [query] : [],
13217
+ warnings,
13218
+ routeRankReason,
13219
+ totalStartedAt
13220
+ }),
13221
+ isError: true
13222
+ };
13223
+ }
13224
+ onUpdate?.({
13225
+ content: [{ type: "text", text: `Fetching URL: ${directUrl}` }],
13226
+ details: makeEmptyResearchDetails({ query: query || directUrl, attempts, providerAttempts, totalStartedAt, routeRankReason })
13227
+ });
13228
+ const startedAt = Date.now();
13229
+ const fetchResult = await runWithDeadline(
13230
+ (childSignal) => webFetchTool.execute("web_research_fetch_direct", { url: directUrl, prompt: query || void 0 }, childSignal),
13231
+ fetchAttemptTimeoutMs,
13232
+ `web_research fetch attempt timed out after ${Math.ceil(fetchAttemptTimeoutMs / 1e3)} seconds`,
13233
+ signal
13234
+ ).catch((error) => ({
13235
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
13236
+ details: {
13237
+ url: directUrl,
13238
+ sourceUrl: directUrl,
13239
+ durationMs: Date.now() - startedAt,
13240
+ failureCategory: "timeout"
13241
+ },
13242
+ isError: true
13243
+ }));
13244
+ fetchDurationMs = Date.now() - startedAt;
13245
+ const fetchDetails = contentDetails(fetchResult);
13246
+ const text = textFromResult(fetchResult);
13247
+ const fetchedSource = fetchResult.isError ? void 0 : {
13248
+ url: directUrl,
13249
+ finalUrl: fetchDetails.finalUrl || directUrl,
13250
+ title: fetchDetails.title || directUrl,
13251
+ textExcerpt: excerpt(text),
13252
+ score: 100,
13253
+ scoreReasons: ["direct_url"],
13254
+ extractionMethod: fetchDetails.extractionMethod,
13255
+ citationAnchors: fetchDetails.citationAnchors,
13256
+ requiresBrowser: fetchDetails.requiresBrowser,
13257
+ failureCategory: fetchDetails.failureCategory
13258
+ };
13259
+ const attemptsForFetch = [{
13260
+ phase: "fetch",
13261
+ url: directUrl,
13262
+ status: fetchResult.isError ? "failed" : "completed",
13263
+ failureCategory: fetchDetails.failureCategory,
13264
+ durationMs: fetchDurationMs
13265
+ }];
13266
+ if (fetchDetails.requiresBrowser) {
13267
+ const browserSource = {
13268
+ url: directUrl,
13269
+ finalUrl: typeof fetchDetails.finalUrl === "string" ? fetchDetails.finalUrl : directUrl,
13270
+ title: typeof fetchDetails.title === "string" ? fetchDetails.title : directUrl,
13271
+ score: 100,
13272
+ scoreReasons: ["direct_url"],
13273
+ requiresBrowser: true,
13274
+ failureCategory: fetchDetails.failureCategory
13275
+ };
13276
+ if (deps.renderedPageReader) {
13277
+ const browserStartedAt = Date.now();
13278
+ try {
13279
+ onUpdate?.({
13280
+ content: [{ type: "text", text: `Reading browser-rendered URL: ${fetchDetails.finalUrl || directUrl}` }],
13281
+ details: makeEmptyResearchDetails({
13282
+ query: query || directUrl,
13283
+ actualQueries: query ? [query] : [],
13284
+ sources: [{ title: browserSource.title || directUrl, url: browserSource.finalUrl || directUrl }],
13285
+ attempts: attemptsForFetch,
13286
+ warnings,
13287
+ browserRequiredSources: [browserSource],
13288
+ routeRankReason,
13289
+ totalStartedAt,
13290
+ fetchDurationMs
13291
+ })
13292
+ });
13293
+ const rendered = await runWithDeadline(
13294
+ (childSignal) => deps.renderedPageReader({ url: fetchDetails.finalUrl || directUrl, query }, childSignal),
13295
+ browserAttemptTimeoutMs,
13296
+ `web_research browser reader timed out after ${Math.ceil(browserAttemptTimeoutMs / 1e3)} seconds`,
13297
+ signal
13298
+ );
13299
+ browserDurationMs = Date.now() - browserStartedAt;
13300
+ const renderedSource = {
13301
+ url: directUrl,
13302
+ finalUrl: rendered.finalUrl || rendered.url,
13303
+ title: rendered.title || browserSource.title || directUrl,
13304
+ textExcerpt: excerpt(rendered.text),
13305
+ score: 100,
13306
+ scoreReasons: ["direct_url"],
13307
+ extractionMethod: "browser_rendered",
13308
+ citationAnchors: rendered.citationAnchors,
13309
+ requiresBrowser: false,
13310
+ browserReadSummary: {
13311
+ screenshotPath: rendered.screenshotPath,
13312
+ consoleMessageCount: rendered.consoleMessages?.length,
13313
+ networkRequestCount: rendered.networkRequests?.length
13314
+ }
13315
+ };
13316
+ const attemptsWithBrowser = [
13317
+ ...attemptsForFetch,
13318
+ { phase: "browser", url: rendered.finalUrl || rendered.url, status: "completed", durationMs: browserDurationMs }
13319
+ ];
13320
+ return {
13321
+ content: [{ type: "text", text: formatResearchPack(query || directUrl, [renderedSource], warnings, []) }],
13322
+ details: makeEmptyResearchDetails({
13323
+ query: query || directUrl,
13324
+ actualQueries: query ? [query] : [],
13325
+ sources: [{ title: renderedSource.title || directUrl, url: renderedSource.finalUrl || directUrl }],
13326
+ fetchedSources: [renderedSource],
13327
+ attempts: attemptsWithBrowser,
13328
+ warnings,
13329
+ qualityScore: 100,
13330
+ routeRankReason,
13331
+ totalStartedAt,
13332
+ fetchDurationMs,
13333
+ browserDurationMs
13334
+ })
13335
+ };
13336
+ } catch (error) {
13337
+ browserDurationMs = Date.now() - browserStartedAt;
13338
+ attemptsForFetch.push({
13339
+ phase: "browser",
13340
+ url: fetchDetails.finalUrl || directUrl,
13341
+ status: "failed",
13342
+ failureCategory: "requires_browser",
13343
+ warnings: [error instanceof Error ? error.message : String(error)],
13344
+ durationMs: browserDurationMs
13345
+ });
13346
+ warnings.push(`Browser reader failed for ${fetchDetails.finalUrl || directUrl}: ${error instanceof Error ? error.message : String(error)}`);
13347
+ }
13348
+ }
13349
+ return {
13350
+ content: [{ type: "text", text: `web_research could not read ${directUrl}: the page requires browser-rendered reading and no host browser reader completed.` }],
13351
+ details: makeEmptyResearchDetails({
13352
+ query: query || directUrl,
13353
+ actualQueries: query ? [query] : [],
13354
+ sources: [{ title: browserSource.title || directUrl, url: browserSource.finalUrl || directUrl }],
13355
+ attempts: attemptsForFetch,
13356
+ warnings,
13357
+ browserRequiredSources: [browserSource],
13358
+ routeRankReason,
13359
+ totalStartedAt,
13360
+ fetchDurationMs,
13361
+ browserDurationMs
13362
+ }),
13363
+ isError: true
13364
+ };
13365
+ }
13366
+ return {
13367
+ content: fetchResult.content,
13368
+ details: makeEmptyResearchDetails({
13369
+ query: query || directUrl,
13370
+ actualQueries: query ? [query] : [],
13371
+ sources: [{ title: fetchDetails.title || directUrl, url: fetchDetails.finalUrl || directUrl }],
13372
+ fetchedSources: fetchedSource ? [fetchedSource] : [],
13373
+ attempts: attemptsForFetch,
13374
+ warnings,
13375
+ qualityScore: fetchedSource ? 100 : 0,
13376
+ routeRankReason,
13377
+ totalStartedAt,
13378
+ fetchDurationMs
13379
+ }),
13380
+ isError: fetchResult.isError
13381
+ };
13382
+ }
13383
+ if (mode === "search") {
13384
+ if (!query) {
13385
+ return {
13386
+ content: [{ type: "text", text: "web_research search mode requires a non-empty query." }],
13387
+ details: makeEmptyResearchDetails({ query, warnings: ["missing_query"], routeRankReason, totalStartedAt }),
13388
+ isError: true
13389
+ };
13390
+ }
13391
+ const startedAt = Date.now();
13392
+ const allowedDomains = input.allowed_domains?.length ? input.allowed_domains : void 0;
13393
+ const blockedDomains = allowedDomains ? void 0 : input.blocked_domains;
13394
+ const result = await runWithDeadline(
13395
+ (childSignal) => webSearchTool.execute("web_research_search_direct", {
13396
+ query,
13397
+ allowed_domains: allowedDomains,
13398
+ blocked_domains: blockedDomains
13399
+ }, childSignal),
13400
+ searchAttemptTimeoutMs,
13401
+ `web_research search attempt timed out after ${Math.ceil(searchAttemptTimeoutMs / 1e3)} seconds`,
13402
+ signal
13403
+ ).catch((error) => ({
13404
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
13405
+ details: {
13406
+ query,
13407
+ requestedMode: deps.getMode?.() ?? "auto",
13408
+ providerMode: "unavailable",
13409
+ durationSeconds: (Date.now() - startedAt) / 1e3,
13410
+ sourceCount: 0,
13411
+ sources: [],
13412
+ failureCategory: "timeout"
13413
+ },
13414
+ isError: true
13415
+ }));
13416
+ searchDurationMs = Date.now() - startedAt;
13417
+ const searchDetails = contentDetails(result);
13418
+ return {
13419
+ content: result.content,
13420
+ details: makeEmptyResearchDetails({
13421
+ query,
13422
+ actualQueries: searchDetails.actualQueries ?? [query],
13423
+ sources: searchDetails.sources ?? [],
13424
+ providerAttempts: searchDetails.attempts ?? [],
13425
+ attempts: [{
13426
+ phase: "search",
13427
+ query,
13428
+ status: result.isError ? "failed" : "completed",
13429
+ failureCategory: searchDetails.failureCategory,
13430
+ warnings: searchDetails.qualityWarnings,
13431
+ durationMs: searchDurationMs
13432
+ }],
13433
+ warnings: searchDetails.qualityWarnings ?? [],
13434
+ qualityScore: (searchDetails.sources?.length ?? 0) > 0 ? 50 : 0,
13435
+ routeRankReason,
13436
+ totalStartedAt,
13437
+ searchDurationMs
13438
+ }),
13439
+ isError: result.isError
13440
+ };
13441
+ }
13006
13442
  if (!query) {
13007
13443
  return {
13008
13444
  content: [{ type: "text", text: "web_research requires a non-empty query." }],
@@ -13586,9 +14022,16 @@ function createCodeIntelTool(options) {
13586
14022
  isError: result.isError
13587
14023
  };
13588
14024
  } catch (error) {
14025
+ const message = error instanceof Error ? error.message : String(error);
13589
14026
  return {
13590
- content: [{ type: "text", text: `code_intel failed: ${error instanceof Error ? error.message : String(error)}` }],
13591
- details: { operation: input.operation, projectRoot, path: input.path, provider: "lightweight" },
14027
+ content: [{ type: "text", text: `code_intel failed: ${message}` }],
14028
+ details: {
14029
+ operation: input.operation,
14030
+ projectRoot,
14031
+ path: input.path,
14032
+ provider: "unavailable",
14033
+ fallbackReason: message
14034
+ },
13592
14035
  isError: true
13593
14036
  };
13594
14037
  }
@@ -13659,5 +14102,6 @@ export {
13659
14102
  createSharedWebSearchTool,
13660
14103
  createSharedWebFetchTool,
13661
14104
  createSharedWebResearchTool,
14105
+ createLightweightCodeIntelProvider,
13662
14106
  createCodeIntelTool
13663
14107
  };