@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.
- package/CHANGELOG.md +162 -40
- package/dist/cli.js +4559 -4543
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +39 -10
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +42 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +27 -5
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +19 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +17 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/file-processor.ts +1 -2
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +46 -9
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/exec/bash-executor.ts +14 -5
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lsp/client.ts +58 -1
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +255 -24
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +191 -31
- package/src/modes/interactive-mode.ts +169 -62
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +343 -122
- package/src/session/agent-session.ts +1359 -368
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +57 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +100 -22
- package/src/task/index.ts +258 -429
- package/src/task/label.ts +2 -0
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +58 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/fetch.ts +28 -105
- package/src/tools/gh.ts +169 -2
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +50 -15
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +62 -29
- package/src/tools/todo.ts +126 -13
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/title-generator.ts +15 -4
- package/src/utils/zip.ts +16 -1
- package/src/web/search/providers/kimi.ts +18 -12
- 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
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
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
|
-
//
|
|
456
|
-
// the sink; PTY/bridge
|
|
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 =
|
|
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 (
|
|
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
|
-
//
|
|
500
|
-
|
|
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
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
+
}
|
package/src/tools/fetch.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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<
|
|
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
|
|
1745
|
-
const
|
|
1746
|
-
|
|
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
|
|
1772
|
-
const truncation = truncateHead(
|
|
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 && !
|
|
1778
|
-
|
|
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 :
|
|
1704
|
+
const output = needsArtifact ? truncation.content : entry.output;
|
|
1782
1705
|
const details: ReadUrlToolDetails = {
|
|
1783
|
-
...
|
|
1784
|
-
truncated: Boolean(
|
|
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 (
|
|
1789
|
-
contentBlocks.push({ type: "image", data:
|
|
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:
|
|
1795
|
-
} else if (
|
|
1796
|
-
const outputLines =
|
|
1797
|
-
const outputBytes = Buffer.byteLength(
|
|
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(
|
|
1723
|
+
resultBuilder.truncationFromText(entry.output, {
|
|
1801
1724
|
direction: "tail",
|
|
1802
1725
|
totalLines,
|
|
1803
1726
|
totalBytes,
|