@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +162 -40
  2. package/dist/cli.js +4559 -4543
  3. package/dist/types/advisor/config.d.ts +14 -6
  4. package/dist/types/advisor/runtime.d.ts +20 -11
  5. package/dist/types/autolearn/controller.d.ts +1 -0
  6. package/dist/types/config/model-discovery.d.ts +17 -0
  7. package/dist/types/config/model-resolver.d.ts +6 -2
  8. package/dist/types/config/settings-schema.d.ts +39 -10
  9. package/dist/types/config/settings.d.ts +50 -0
  10. package/dist/types/cursor.d.ts +11 -0
  11. package/dist/types/discovery/helpers.d.ts +5 -2
  12. package/dist/types/eval/agent-bridge.d.ts +7 -19
  13. package/dist/types/exec/bash-executor.d.ts +2 -0
  14. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  15. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  17. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
  18. package/dist/types/extensibility/shared-events.d.ts +6 -0
  19. package/dist/types/extensibility/utils.d.ts +2 -2
  20. package/dist/types/lsp/client.d.ts +2 -0
  21. package/dist/types/lsp/types.d.ts +3 -0
  22. package/dist/types/mcp/transports/stdio.d.ts +42 -1
  23. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  24. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  25. package/dist/types/modes/components/hook-editor.d.ts +7 -0
  26. package/dist/types/modes/components/model-hub.d.ts +5 -2
  27. package/dist/types/modes/components/session-selector.d.ts +2 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  30. package/dist/types/modes/print-mode.d.ts +1 -1
  31. package/dist/types/modes/warp-events.d.ts +24 -0
  32. package/dist/types/modes/warp-events.test.d.ts +1 -0
  33. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  34. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  35. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  36. package/dist/types/registry/persisted-agents.d.ts +3 -0
  37. package/dist/types/sdk.d.ts +27 -5
  38. package/dist/types/session/agent-session.d.ts +34 -10
  39. package/dist/types/session/session-entries.d.ts +6 -1
  40. package/dist/types/session/session-history-format.d.ts +10 -0
  41. package/dist/types/session/session-manager.d.ts +19 -0
  42. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  43. package/dist/types/task/executor.d.ts +26 -1
  44. package/dist/types/task/index.d.ts +1 -1
  45. package/dist/types/task/label.d.ts +1 -1
  46. package/dist/types/task/parallel.d.ts +14 -0
  47. package/dist/types/task/structured-subagent.d.ts +111 -0
  48. package/dist/types/task/types.d.ts +51 -0
  49. package/dist/types/telemetry-export.d.ts +34 -9
  50. package/dist/types/tools/approval.d.ts +8 -0
  51. package/dist/types/tools/bash.d.ts +2 -0
  52. package/dist/types/tools/essential-tools.d.ts +29 -0
  53. package/dist/types/tools/fetch.d.ts +4 -5
  54. package/dist/types/tools/hub/messaging.d.ts +5 -1
  55. package/dist/types/tools/image-gen.d.ts +2 -1
  56. package/dist/types/tools/index.d.ts +17 -1
  57. package/dist/types/tools/todo.d.ts +31 -0
  58. package/dist/types/tools/xdev.d.ts +11 -2
  59. package/dist/types/tui/tree-list.d.ts +7 -0
  60. package/dist/types/utils/markit.d.ts +11 -0
  61. package/dist/types/utils/title-generator.d.ts +2 -1
  62. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  63. package/dist/types/web/search/types.d.ts +1 -1
  64. package/package.json +21 -16
  65. package/src/advisor/__tests__/advisor.test.ts +1304 -42
  66. package/src/advisor/__tests__/config.test.ts +58 -2
  67. package/src/advisor/config.ts +76 -24
  68. package/src/advisor/runtime.ts +445 -92
  69. package/src/autolearn/controller.ts +23 -28
  70. package/src/cli/file-processor.ts +1 -2
  71. package/src/cli.ts +5 -1
  72. package/src/config/model-discovery.ts +81 -21
  73. package/src/config/model-registry.ts +25 -6
  74. package/src/config/model-resolver.ts +14 -7
  75. package/src/config/settings-schema.ts +46 -9
  76. package/src/config/settings.ts +405 -25
  77. package/src/cursor.ts +20 -3
  78. package/src/debug/report-bundle.ts +40 -4
  79. package/src/discovery/helpers.ts +28 -5
  80. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  81. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  82. package/src/eval/agent-bridge.ts +104 -477
  83. package/src/eval/jl/prelude.jl +7 -6
  84. package/src/eval/js/shared/prelude.txt +5 -4
  85. package/src/eval/py/prelude.py +11 -39
  86. package/src/eval/rb/prelude.rb +5 -6
  87. package/src/exec/bash-executor.ts +14 -5
  88. package/src/extensibility/custom-tools/loader.ts +3 -3
  89. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  90. package/src/extensibility/extensions/loader.ts +3 -3
  91. package/src/extensibility/extensions/managed-timers.ts +83 -0
  92. package/src/extensibility/extensions/runner.ts +26 -0
  93. package/src/extensibility/extensions/types.ts +18 -0
  94. package/src/extensibility/extensions/wrapper.ts +2 -1
  95. package/src/extensibility/hooks/loader.ts +3 -3
  96. package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
  97. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  98. package/src/extensibility/plugins/manager.ts +2 -2
  99. package/src/extensibility/shared-events.ts +6 -0
  100. package/src/extensibility/utils.ts +91 -25
  101. package/src/irc/bus.ts +22 -3
  102. package/src/launch/broker.ts +3 -2
  103. package/src/launch/client.ts +2 -2
  104. package/src/launch/presence.ts +2 -2
  105. package/src/lsp/client.ts +58 -1
  106. package/src/lsp/index.ts +62 -6
  107. package/src/lsp/types.ts +3 -0
  108. package/src/main.ts +11 -8
  109. package/src/mcp/manager.ts +9 -3
  110. package/src/mcp/oauth-flow.ts +20 -0
  111. package/src/mcp/transports/stdio.test.ts +269 -1
  112. package/src/mcp/transports/stdio.ts +255 -24
  113. package/src/modes/components/advisor-config.ts +65 -3
  114. package/src/modes/components/agent-hub.ts +1 -72
  115. package/src/modes/components/ask-dialog.ts +1 -1
  116. package/src/modes/components/bash-execution.ts +7 -3
  117. package/src/modes/components/eval-execution.ts +3 -1
  118. package/src/modes/components/hook-editor.ts +18 -3
  119. package/src/modes/components/model-hub.ts +138 -42
  120. package/src/modes/components/session-selector.ts +4 -0
  121. package/src/modes/components/status-line/component.test.ts +1 -0
  122. package/src/modes/components/status-line/segments.ts +21 -6
  123. package/src/modes/controllers/command-controller.ts +167 -47
  124. package/src/modes/controllers/event-controller.ts +5 -0
  125. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  126. package/src/modes/controllers/input-controller.ts +12 -12
  127. package/src/modes/controllers/selector-controller.ts +191 -31
  128. package/src/modes/interactive-mode.ts +169 -62
  129. package/src/modes/print-mode.ts +3 -3
  130. package/src/modes/rpc/host-tools.ts +2 -1
  131. package/src/modes/rpc/rpc-mode.ts +19 -4
  132. package/src/modes/warp-events.test.ts +794 -0
  133. package/src/modes/warp-events.ts +232 -0
  134. package/src/plan-mode/model-transition.test.ts +60 -0
  135. package/src/plan-mode/model-transition.ts +51 -0
  136. package/src/prompts/system/system-prompt.md +1 -1
  137. package/src/prompts/tools/eval.md +5 -2
  138. package/src/prompts/tools/read.md +1 -1
  139. package/src/prompts/tools/task.md +12 -8
  140. package/src/prompts/tools/write.md +1 -1
  141. package/src/registry/agent-lifecycle.ts +133 -18
  142. package/src/registry/persisted-agents.ts +74 -0
  143. package/src/sdk.ts +343 -122
  144. package/src/session/agent-session.ts +1359 -368
  145. package/src/session/session-entries.ts +6 -1
  146. package/src/session/session-history-format.ts +20 -5
  147. package/src/session/session-manager.ts +57 -0
  148. package/src/session/streaming-output.ts +41 -1
  149. package/src/slash-commands/builtin-registry.ts +7 -0
  150. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  151. package/src/system-prompt.ts +7 -2
  152. package/src/task/executor.ts +100 -22
  153. package/src/task/index.ts +258 -429
  154. package/src/task/label.ts +2 -0
  155. package/src/task/parallel.ts +43 -0
  156. package/src/task/persisted-revive.ts +19 -7
  157. package/src/task/structured-subagent.ts +642 -0
  158. package/src/task/types.ts +58 -0
  159. package/src/telemetry-export.ts +453 -97
  160. package/src/tools/__tests__/eval-description.test.ts +1 -1
  161. package/src/tools/approval.ts +11 -0
  162. package/src/tools/bash.ts +71 -38
  163. package/src/tools/essential-tools.ts +45 -0
  164. package/src/tools/fetch.ts +28 -105
  165. package/src/tools/gh.ts +169 -2
  166. package/src/tools/hub/messaging.ts +16 -3
  167. package/src/tools/image-gen.ts +69 -7
  168. package/src/tools/index.ts +50 -15
  169. package/src/tools/path-utils.ts +1 -0
  170. package/src/tools/read.ts +62 -29
  171. package/src/tools/todo.ts +126 -13
  172. package/src/tools/write.ts +22 -4
  173. package/src/tools/xdev.ts +14 -3
  174. package/src/tui/tree-list.ts +39 -0
  175. package/src/utils/markit.ts +12 -0
  176. package/src/utils/title-generator.ts +15 -4
  177. package/src/utils/zip.ts +16 -1
  178. package/src/web/search/providers/kimi.ts +18 -12
  179. package/src/web/search/types.ts +6 -1
package/src/tools/bash.ts CHANGED
@@ -177,6 +177,8 @@ export interface BashToolDetails {
177
177
  wallTimeMs?: number;
178
178
  /** Exit code of a command that ran to completion but failed (non-zero). */
179
179
  exitCode?: number;
180
+ /** True when the command was killed by its timeout deadline (not a failure). */
181
+ timedOut?: boolean;
180
182
  terminalId?: string;
181
183
  async?: {
182
184
  state: "running" | "completed" | "failed";
@@ -208,10 +210,6 @@ function normalizeResultOutput(result: BashResult | BashInteractiveResult): stri
208
210
  return result.output || "";
209
211
  }
210
212
 
211
- function isInteractiveResult(result: BashResult | BashInteractiveResult): result is BashInteractiveResult {
212
- return "timedOut" in result;
213
- }
214
-
215
213
  function normalizeBashEnv(env: Record<string, string> | undefined): Record<string, string> | undefined {
216
214
  if (!env || Object.keys(env).length === 0) return undefined;
217
215
  const normalized: Record<string, string> = {};
@@ -439,12 +437,15 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
439
437
  }
440
438
 
441
439
  /**
442
- * Throw for outcomes that are *not* a completed command: user/timeout
443
- * aborts and a missing exit status. The foreground and bridge callers plus
444
- * the async job manager rely on these throwing so cancellations surface as
445
- * aborts and jobs are recorded as failed. A definite non-zero exit is a
446
- * completed command that failed; #buildCompletedResult surfaces it as an
447
- * error *result* (carrying execution details) rather than a throw.
440
+ * Throw for outcomes that are *not* a completed command: user aborts and a
441
+ * missing exit status. Timeouts are handled separately by
442
+ * #buildCompletedResult, which returns a non-throwing error result with
443
+ * details.timedOut=true so the renderer can show a warning border. The
444
+ * foreground and bridge callers plus the async job manager rely on these
445
+ * throwing so cancellations surface as aborts and jobs are recorded as
446
+ * failed. A definite non-zero exit is a completed command that failed;
447
+ * #buildCompletedResult surfaces it as an error *result* (carrying
448
+ * execution details) rather than a throw.
448
449
  */
449
450
  #throwIfUnfinished(
450
451
  result: BashResult | BashInteractiveResult,
@@ -452,13 +453,14 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
452
453
  outputText: string,
453
454
  ): void {
454
455
  if (result.cancelled) {
455
- // executeBash output already carries a `[Command cancelled]` notice from
456
- // the sink; PTY/bridge interactive output does not, so annotate it here.
456
+ // Local executor output already carries a leading `[Command cancelled]`
457
+ // notice from the sink; PTY/bridge output does not, so annotate only
458
+ // the latter.
457
459
  const out = normalizeResultOutput(result);
458
- const annotated = isInteractiveResult(result) && out ? `${out}\n\n[Command aborted]` : out;
460
+ const annotated = out.startsWith("[Command cancelled]") ? out : out ? `${out}\n\n[Command aborted]` : out;
459
461
  throw new ToolError(annotated || "Command aborted");
460
462
  }
461
- if (isInteractiveResult(result) && result.timedOut) {
463
+ if (result.timedOut === true) {
462
464
  const out = normalizeResultOutput(result);
463
465
  const message =
464
466
  timeoutSec === undefined ? "Command timed out" : `Command timed out after ${timeoutSec} seconds`;
@@ -496,8 +498,12 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
496
498
  if (failedExit) outputLines.push("", formatExitCodeNotice(exitCode));
497
499
  const outputText = outputLines.join("\n");
498
500
 
499
- // Aborts / timeouts / missing-status still propagate as thrown errors.
500
- this.#throwIfUnfinished(result, timeoutSec, outputText);
501
+ // Timeouts are not failures the command ran its course. Return an error
502
+ // result (isError=true for the model) but flag timedOut so the renderer
503
+ // uses a warning border instead of error red. Both interactive and
504
+ // non-interactive results carry an explicit `timedOut` field from the
505
+ // executor/PTY layer.
506
+ const isTimeout = result.timedOut === true;
501
507
 
502
508
  const details: BashToolDetails = {};
503
509
  if (timeoutSec === undefined) {
@@ -517,6 +523,29 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
517
523
  if (failedExit) {
518
524
  details.exitCode = exitCode;
519
525
  }
526
+
527
+ if (isTimeout) {
528
+ details.timedOut = true;
529
+ const message =
530
+ timeoutSec === undefined ? "Command timed out" : `Command timed out after ${timeoutSec} seconds`;
531
+ // executeBash has already emitted this leading sink notice. PTY output
532
+ // has not, so provide the LLM-facing annotation exactly once.
533
+ if (!normalizeResultOutput(result).startsWith(`[${message}]\n`)) {
534
+ outputLines.push("", `[${message}]`);
535
+ }
536
+ const timeoutOutputText = await enforceInlineByteCap(outputLines.join("\n"), {
537
+ saveArtifact: full => saveBashOriginalArtifact(this.session, full),
538
+ });
539
+ return toolResult(details)
540
+ .text(timeoutOutputText)
541
+ .truncationFromSummary(result, { direction: "tail" })
542
+ .error()
543
+ .done();
544
+ }
545
+
546
+ // Non-timeout cancellations and missing exit status still propagate as thrown errors.
547
+ this.#throwIfUnfinished(result, timeoutSec, outputText);
548
+
520
549
  // Final defense at the tool-result boundary: no bash path (client bridge,
521
550
  // head-retention spill, minimizer miss) may emit more than
522
551
  // ~DEFAULT_MAX_BYTES inline. No-op for already-bounded output.
@@ -992,12 +1021,8 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
992
1021
  outputLines: current.output.length > 0 ? current.output.split("\n").length : 0,
993
1022
  outputBytes: current.output.length,
994
1023
  };
995
- return this.#buildCompletedResult(timedOutResult, timeoutSec, {
996
- requestedTimeoutSec,
997
- notices: pendingNotices,
998
- terminalId: handle.terminalId,
999
- wallTimeMs: performance.now() - bridgeWallTimeStart,
1000
- });
1024
+ this.#throwIfUnfinished(timedOutResult, timeoutSec, this.#formatResultOutput(timedOutResult));
1025
+ throw new ToolError("Command timed out");
1001
1026
  }
1002
1027
 
1003
1028
  if (raced.kind === "exit") {
@@ -1102,20 +1127,27 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
1102
1127
  });
1103
1128
  const wallTimeMs = performance.now() - wallTimeStart;
1104
1129
  if (result.cancelled) {
1105
- const out = normalizeResultOutput(result);
1106
- // PTY output carries no cancel/timeout notice of its own; annotate so
1107
- // the model can tell an abort from a plain failure.
1108
- const message = isInteractiveResult(result) && out ? `${out}\n\n[Command aborted]` : out || "Command aborted";
1109
- if (signal?.aborted) {
1110
- throw new ToolAbortError(message);
1130
+ // A cancelled result is either a timeout (the command's deadline fired)
1131
+ // or a user/system abort. Timeouts are handled by #buildCompletedResult
1132
+ // which returns a non-throwing error result with details.timedOut=true
1133
+ // so the renderer can show a warning border instead of error red.
1134
+ // Both interactive and non-interactive results carry an explicit
1135
+ // `timedOut` field from the executor/PTY layer.
1136
+ const isTimeout = result.timedOut === true;
1137
+ if (!isTimeout) {
1138
+ const out = normalizeResultOutput(result);
1139
+ // The local executor already prepends `[Command cancelled]`; PTY
1140
+ // output does not, so preserve one cancellation notice in either case.
1141
+ const message = out.startsWith("[Command cancelled]")
1142
+ ? out
1143
+ : out
1144
+ ? `${out}\n\n[Command aborted]`
1145
+ : "Command aborted";
1146
+ if (signal?.aborted) {
1147
+ throw new ToolAbortError(message);
1148
+ }
1149
+ throw new ToolError(message);
1111
1150
  }
1112
- throw new ToolError(message);
1113
- }
1114
- if (isInteractiveResult(result) && result.timedOut) {
1115
- const out = normalizeResultOutput(result);
1116
- const message =
1117
- timeoutSec === undefined ? "Command timed out" : `Command timed out after ${timeoutSec} seconds`;
1118
- throw new ToolError(out ? `${out}\n\n[${message}]` : message);
1119
1151
  }
1120
1152
  return this.#buildCompletedResult(result, timeoutSec, {
1121
1153
  requestedTimeoutSec,
@@ -1253,6 +1285,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1253
1285
  const isError = result.isError === true;
1254
1286
  const isPartial = options.isPartial === true;
1255
1287
  const success = !isPartial && !isError;
1288
+ const details = result.details;
1289
+ const isTimeout = details?.timedOut === true;
1256
1290
  const header =
1257
1291
  config.showHeader === false
1258
1292
  ? undefined
@@ -1263,12 +1297,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1263
1297
  title: config.resolveTitle(args, options),
1264
1298
  }
1265
1299
  : {
1266
- icon: isPartial ? "pending" : "error",
1300
+ icon: isPartial ? "pending" : isTimeout ? "warning" : "error",
1267
1301
  title: config.resolveTitle(args, options),
1268
1302
  },
1269
1303
  uiTheme,
1270
1304
  );
1271
- const details = result.details;
1272
1305
  const outputBlock = new CachedOutputBlock();
1273
1306
 
1274
1307
  // Per-instance cache for the expensive inner lines computation. Mirrors
@@ -1408,7 +1441,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1408
1441
  const framed = outputBlock.render(
1409
1442
  {
1410
1443
  header,
1411
- state: isPartial ? "pending" : isError ? "error" : "success",
1444
+ state: isPartial ? "pending" : isError ? (isTimeout ? "warning" : "error") : "success",
1412
1445
  sections: [
1413
1446
  {
1414
1447
  // Viewport-sized tail window in every state — streaming and final
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Canonical set of built-in tools that must stay top-level.
3
+ *
4
+ * These are the coding essentials the model always needs directly in its
5
+ * callable schema. `read`/`write` are additionally the `xd://` transport
6
+ * (`read xd://` lists devices, `write xd://<tool>` executes them), so demoting
7
+ * them under xdev makes every mounted device unreachable.
8
+ *
9
+ * Adapter boundaries (extension `registerTool`, SDK custom tools, RPC host
10
+ * tools) default an omitted `loadMode` to `"discoverable"`. A UI-only
11
+ * re-register of a built-in — e.g. wrapping `read`/`write`/`bash`/`edit`/`glob`
12
+ * to customize rendering — would then silently demote it to `discoverable` and,
13
+ * with `tools.xdev` on, unmount it from the top-level schema (issue #5764).
14
+ * {@link defaultLoadModeForToolName} pins these names to `"essential"` when the
15
+ * definition omits `loadMode`, so re-registering a built-in never demotes it.
16
+ */
17
+ import type { ToolLoadMode } from "@oh-my-pi/pi-agent-core";
18
+
19
+ /**
20
+ * Built-in tool names whose classes declare `loadMode = "essential"`. Kept in
21
+ * sync with the tool classes by `essential-tools.test.ts` (drift guard).
22
+ */
23
+ export const ESSENTIAL_BUILTIN_TOOL_NAMES: Record<string, true> = {
24
+ read: true,
25
+ write: true,
26
+ bash: true,
27
+ edit: true,
28
+ glob: true,
29
+ eval: true,
30
+ task: true,
31
+ hub: true,
32
+ learn: true,
33
+ manage_skill: true,
34
+ };
35
+
36
+ /**
37
+ * Resolve a tool's presentation mode at an adapter boundary. An explicit
38
+ * `declared` mode always wins. When omitted, known essential built-in names
39
+ * default to `"essential"` (so a re-register never demotes them); everything
40
+ * else defaults to `"discoverable"`.
41
+ */
42
+ export function defaultLoadModeForToolName(name: string, declared?: ToolLoadMode): ToolLoadMode {
43
+ if (declared) return declared;
44
+ return name in ESSENTIAL_BUILTIN_TOOL_NAMES ? "essential" : "discoverable";
45
+ }
@@ -7,7 +7,6 @@ import type { FetchImpl, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
7
7
  import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
8
8
  import { type Component, Text } from "@oh-my-pi/pi-tui";
9
9
  import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
10
- import { LRUCache } from "lru-cache/raw";
11
10
  import type { Settings } from "../config/settings";
12
11
  import { readEditableNotebookText } from "../edit/notebook";
13
12
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -19,6 +18,7 @@ import { renderStatusLine, urlHyperlink } from "../tui";
19
18
  import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
20
19
  import { webpExclusionForModel } from "../utils/image-loading";
21
20
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
21
+ import { CONVERTIBLE_EXTENSIONS } from "../utils/markit";
22
22
  import { ensureTool } from "../utils/tools-manager";
23
23
  import { type ArchiveFormat, listArchiveRoot, sniffArchiveFormat } from "../utils/zip";
24
24
  import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
@@ -39,21 +39,17 @@ import { clampTimeout } from "./tool-timeouts";
39
39
  // =============================================================================
40
40
 
41
41
  const FETCH_DEFAULT_MAX_LINES = 300;
42
- // Convertible document types handled by markit.
42
+ // MIME types markit can convert — one per registered converter (pdf, docx,
43
+ // pptx, xlsx, epub). Legacy `application/msword`, `application/vnd.ms-*`, and
44
+ // `application/rtf` are intentionally absent: markit has no converter for them.
43
45
  const CONVERTIBLE_MIMES = new Set([
44
46
  "application/pdf",
45
- "application/msword",
46
- "application/vnd.ms-powerpoint",
47
- "application/vnd.ms-excel",
48
47
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
49
48
  "application/vnd.openxmlformats-officedocument.presentationml.presentation",
50
49
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
51
- "application/rtf",
52
50
  "application/epub+zip",
53
51
  ]);
54
52
 
55
- const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
56
-
57
53
  const NOTEBOOK_MIMES = new Set(["application/x-ipynb+json"]);
58
54
  const NOTEBOOK_EXTENSIONS = new Set([".ipynb"]);
59
55
 
@@ -1553,24 +1549,15 @@ export interface ReadUrlToolDetails {
1553
1549
  meta?: OutputMeta;
1554
1550
  }
1555
1551
 
1556
- interface ReadUrlCacheEntry {
1552
+ interface ReadUrlEntry {
1557
1553
  artifactId?: string;
1558
1554
  artifactPath?: string;
1559
- contentPath?: string;
1560
1555
  details: ReadUrlToolDetails;
1561
1556
  image?: FetchImagePayload;
1562
1557
  output: string;
1563
1558
  content: string;
1564
1559
  }
1565
1560
 
1566
- const READ_URL_CACHE_MAX_ENTRIES = 100;
1567
- const readUrlCache = new LRUCache<string, ReadUrlCacheEntry>({ max: READ_URL_CACHE_MAX_ENTRIES });
1568
-
1569
- function getReadUrlCacheKey(session: ToolSession, requestedUrl: string, raw: boolean): string {
1570
- const scope = session.getSessionFile() ?? session.cwd;
1571
- return `${scope}::${raw ? "raw" : "rendered"}::${normalizeUrl(requestedUrl)}`;
1572
- }
1573
-
1574
1561
  async function findArtifactPath(session: ToolSession, artifactId: string): Promise<string | null> {
1575
1562
  const artifactsDir = session.getArtifactsDir?.();
1576
1563
  if (!artifactsDir) return null;
@@ -1584,25 +1571,6 @@ async function findArtifactPath(session: ToolSession, artifactId: string): Promi
1584
1571
  }
1585
1572
  }
1586
1573
 
1587
- async function readArtifactOutput(session: ToolSession, artifactId: string): Promise<string | null> {
1588
- const artifactPath = await findArtifactPath(session, artifactId);
1589
- return artifactPath ? await Bun.file(artifactPath).text() : null;
1590
- }
1591
-
1592
- async function materializeReadUrlCacheEntry(
1593
- session: ToolSession,
1594
- entry: ReadUrlCacheEntry,
1595
- ): Promise<ReadUrlCacheEntry | null> {
1596
- if (entry.artifactId) {
1597
- const artifactOutput = await readArtifactOutput(session, entry.artifactId);
1598
- if (artifactOutput !== null) {
1599
- return { ...entry, output: artifactOutput };
1600
- }
1601
- }
1602
-
1603
- return entry.output.length > 0 ? entry : null;
1604
- }
1605
-
1606
1574
  async function persistReadUrlArtifact(
1607
1575
  session: ToolSession,
1608
1576
  output: string,
@@ -1613,7 +1581,7 @@ async function persistReadUrlArtifact(
1613
1581
  return artifact;
1614
1582
  }
1615
1583
 
1616
- async function ensureReadUrlCacheArtifact(session: ToolSession, entry: ReadUrlCacheEntry): Promise<ReadUrlCacheEntry> {
1584
+ async function ensureReadUrlArtifact(session: ToolSession, entry: ReadUrlEntry): Promise<ReadUrlEntry> {
1617
1585
  if (entry.artifactId && entry.artifactPath) return entry;
1618
1586
  if (entry.artifactId) {
1619
1587
  const artifactPath = await findArtifactPath(session, entry.artifactId);
@@ -1632,19 +1600,7 @@ function readUrlContentExtension(finalUrl: string): string {
1632
1600
  }
1633
1601
  }
1634
1602
 
1635
- async function ensureReadUrlContentFile(
1636
- session: ToolSession,
1637
- entry: ReadUrlCacheEntry,
1638
- raw: boolean,
1639
- ): Promise<ReadUrlCacheEntry> {
1640
- if (entry.contentPath) {
1641
- try {
1642
- await Bun.file(entry.contentPath).stat();
1643
- return entry;
1644
- } catch {
1645
- // Recreate below when the cached scratch file was removed.
1646
- }
1647
- }
1603
+ async function materializeReadUrlContent(session: ToolSession, entry: ReadUrlEntry, raw: boolean): Promise<string> {
1648
1604
  const root = session.getArtifactsDir?.();
1649
1605
  if (!root) {
1650
1606
  throw new ToolError("Cannot search URL output because this session cannot materialize read artifacts.");
@@ -1654,20 +1610,16 @@ async function ensureReadUrlContentFile(
1654
1610
  const hash = Bun.hash(`${raw ? "raw" : "rendered"}:${entry.details.finalUrl}`).toString(36);
1655
1611
  const contentPath = path.join(dir, `${hash}${readUrlContentExtension(entry.details.finalUrl)}`);
1656
1612
  await Bun.write(contentPath, entry.content);
1657
- return { ...entry, contentPath };
1658
- }
1659
-
1660
- function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: boolean, entry: ReadUrlCacheEntry): void {
1661
- readUrlCache.set(getReadUrlCacheKey(session, requestedUrl, raw), entry);
1662
- readUrlCache.set(getReadUrlCacheKey(session, entry.details.finalUrl, raw), entry);
1613
+ return contentPath;
1663
1614
  }
1664
1615
 
1665
- async function buildReadUrlCacheEntry(
1616
+ /** Fetch and render a URL for a read or search operation. */
1617
+ export async function fetchReadUrl(
1666
1618
  session: ToolSession,
1667
1619
  params: { path: string; raw?: boolean },
1668
1620
  signal?: AbortSignal,
1669
1621
  options?: { ensureArtifact?: boolean },
1670
- ): Promise<ReadUrlCacheEntry> {
1622
+ ): Promise<ReadUrlEntry> {
1671
1623
  const { path: url, raw = false } = params;
1672
1624
 
1673
1625
  const effectiveTimeout = clampTimeout("fetch", 30);
@@ -1708,30 +1660,6 @@ async function buildReadUrlCacheEntry(
1708
1660
  };
1709
1661
  }
1710
1662
 
1711
- export async function loadReadUrlCacheEntry(
1712
- session: ToolSession,
1713
- params: { path: string; raw?: boolean },
1714
- signal?: AbortSignal,
1715
- options?: { ensureArtifact?: boolean; preferCached?: boolean },
1716
- ): Promise<ReadUrlCacheEntry> {
1717
- const raw = params.raw ?? false;
1718
- const cached = readUrlCache.get(getReadUrlCacheKey(session, params.path, raw));
1719
- if (options?.preferCached && cached) {
1720
- const prepared = options.ensureArtifact ? await ensureReadUrlCacheArtifact(session, cached) : cached;
1721
- const materialized = await materializeReadUrlCacheEntry(session, prepared);
1722
- if (materialized) {
1723
- cacheReadUrlEntry(session, params.path, raw, materialized);
1724
- return materialized;
1725
- }
1726
- }
1727
-
1728
- const fresh = await buildReadUrlCacheEntry(session, params, signal, {
1729
- ensureArtifact: options?.ensureArtifact,
1730
- });
1731
- cacheReadUrlEntry(session, params.path, raw, fresh);
1732
- return fresh;
1733
- }
1734
-
1735
1663
  /** Materialize rendered URL body text to a local file for tools that require filesystem paths. */
1736
1664
  export async function materializeReadUrlToFile(
1737
1665
  session: ToolSession,
@@ -1741,13 +1669,9 @@ export async function materializeReadUrlToFile(
1741
1669
  if (!session.settings.get("fetch.enabled")) {
1742
1670
  throw new ToolError("URL reads are disabled by settings.");
1743
1671
  }
1744
- const cacheEntry = await loadReadUrlCacheEntry(session, params, signal, { preferCached: true });
1745
- const materialized = await ensureReadUrlContentFile(session, cacheEntry, params.raw ?? false);
1746
- cacheReadUrlEntry(session, params.path, params.raw ?? false, materialized);
1747
- if (!materialized.contentPath) {
1748
- throw new ToolError("Cannot search URL output because this session cannot materialize read artifacts.");
1749
- }
1750
- return { path: materialized.contentPath, details: materialized.details };
1672
+ const entry = await fetchReadUrl(session, params, signal);
1673
+ const contentPath = await materializeReadUrlContent(session, entry, params.raw ?? false);
1674
+ return { path: contentPath, details: entry.details };
1751
1675
  }
1752
1676
 
1753
1677
  function buildUrlReadOutput(result: FetchRenderResult, content: string): string {
@@ -1768,36 +1692,35 @@ export async function executeReadUrl(
1768
1692
  params: { path: string; raw?: boolean },
1769
1693
  signal?: AbortSignal,
1770
1694
  ): Promise<AgentToolResult<ReadUrlToolDetails>> {
1771
- let cacheEntry = await loadReadUrlCacheEntry(session, params, signal, { preferCached: true });
1772
- const truncation = truncateHead(cacheEntry.output, {
1695
+ let entry = await fetchReadUrl(session, params, signal);
1696
+ const truncation = truncateHead(entry.output, {
1773
1697
  maxBytes: DEFAULT_MAX_BYTES,
1774
1698
  maxLines: FETCH_DEFAULT_MAX_LINES,
1775
1699
  });
1776
1700
  const needsArtifact = truncation.truncated;
1777
- if (needsArtifact && !cacheEntry.artifactId) {
1778
- cacheEntry = await ensureReadUrlCacheArtifact(session, cacheEntry);
1779
- cacheReadUrlEntry(session, params.path, params.raw ?? false, cacheEntry);
1701
+ if (needsArtifact && !entry.artifactId) {
1702
+ entry = await ensureReadUrlArtifact(session, entry);
1780
1703
  }
1781
- const output = needsArtifact ? truncation.content : cacheEntry.output;
1704
+ const output = needsArtifact ? truncation.content : entry.output;
1782
1705
  const details: ReadUrlToolDetails = {
1783
- ...cacheEntry.details,
1784
- truncated: Boolean(cacheEntry.details.truncated || needsArtifact),
1706
+ ...entry.details,
1707
+ truncated: Boolean(entry.details.truncated || needsArtifact),
1785
1708
  };
1786
1709
 
1787
1710
  const contentBlocks: Array<TextContent | ImageContent> = [{ type: "text", text: output }];
1788
- if (cacheEntry.image) {
1789
- contentBlocks.push({ type: "image", data: cacheEntry.image.data, mimeType: cacheEntry.image.mimeType });
1711
+ if (entry.image) {
1712
+ contentBlocks.push({ type: "image", data: entry.image.data, mimeType: entry.image.mimeType });
1790
1713
  }
1791
1714
 
1792
1715
  const resultBuilder = toolResult(details).content(contentBlocks).sourceUrl(details.finalUrl);
1793
1716
  if (needsArtifact) {
1794
- resultBuilder.truncation(truncation, { direction: "head", artifactId: cacheEntry.artifactId });
1795
- } else if (cacheEntry.details.truncated) {
1796
- const outputLines = cacheEntry.output.split("\n").length;
1797
- const outputBytes = Buffer.byteLength(cacheEntry.output, "utf-8");
1717
+ resultBuilder.truncation(truncation, { direction: "head", artifactId: entry.artifactId });
1718
+ } else if (entry.details.truncated) {
1719
+ const outputLines = entry.output.split("\n").length;
1720
+ const outputBytes = Buffer.byteLength(entry.output, "utf-8");
1798
1721
  const totalBytes = Math.max(outputBytes + 1, MAX_OUTPUT_CHARS + 1);
1799
1722
  const totalLines = outputLines + 1;
1800
- resultBuilder.truncationFromText(cacheEntry.output, {
1723
+ resultBuilder.truncationFromText(entry.output, {
1801
1724
  direction: "tail",
1802
1725
  totalLines,
1803
1726
  totalBytes,