@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -1
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +43 -0
- package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
- package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
- package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
- package/dist/types/cli/gallery-screenshot.d.ts +35 -0
- package/dist/types/commands/gallery.d.ts +47 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-id-affixes.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +8 -1
- package/dist/types/config/settings-schema.d.ts +32 -6
- package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
- package/dist/types/lsp/types.d.ts +10 -0
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memory-backend/index.d.ts +2 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/tool-execution.d.ts +18 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +5 -4
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-version.d.ts +11 -0
- package/dist/types/modes/setup-wizard/index.d.ts +2 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/telemetry-export.d.ts +1 -1
- package/dist/types/tools/eval-render.d.ts +1 -8
- package/dist/types/tools/fetch.d.ts +15 -7
- package/dist/types/tools/render-utils.d.ts +8 -0
- package/dist/types/tools/renderers.d.ts +16 -2
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/web/scrapers/github.d.ts +22 -0
- package/dist/types/web/search/providers/perplexity.d.ts +8 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +9 -9
- package/scripts/dev-launch +42 -0
- package/scripts/dev-launch-preload.ts +19 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/gallery-cli.ts +223 -0
- package/src/cli/gallery-fixtures/agentic.ts +292 -0
- package/src/cli/gallery-fixtures/codeintel.ts +188 -0
- package/src/cli/gallery-fixtures/edit.ts +194 -0
- package/src/cli/gallery-fixtures/fs.ts +153 -0
- package/src/cli/gallery-fixtures/index.ts +40 -0
- package/src/cli/gallery-fixtures/interaction.ts +49 -0
- package/src/cli/gallery-fixtures/memory.ts +81 -0
- package/src/cli/gallery-fixtures/misc.ts +221 -0
- package/src/cli/gallery-fixtures/search.ts +213 -0
- package/src/cli/gallery-fixtures/shell.ts +167 -0
- package/src/cli/gallery-fixtures/types.ts +41 -0
- package/src/cli/gallery-fixtures/web.ts +158 -0
- package/src/cli/gallery-screenshot.ts +279 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/gallery.ts +52 -0
- package/src/commands/launch.ts +1 -1
- package/src/config/keybindings.ts +15 -6
- package/src/config/model-equivalence.ts +35 -12
- package/src/config/model-id-affixes.ts +39 -22
- package/src/config/model-registry.ts +16 -16
- package/src/config/settings-schema.ts +18 -5
- package/src/config/settings.ts +11 -0
- package/src/dap/client.ts +14 -16
- package/src/edit/renderer.ts +36 -48
- package/src/eval/__tests__/agent-bridge.test.ts +75 -32
- package/src/eval/agent-bridge.ts +34 -7
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/plugins/doctor.ts +0 -1
- package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
- package/src/goals/tools/goal-tool.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +104 -55
- package/src/lsp/types.ts +10 -0
- package/src/main.ts +44 -49
- package/src/memory-backend/index.ts +13 -1
- package/src/memory-backend/resolve.ts +3 -5
- package/src/memory-backend/types.ts +1 -1
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tool-execution.ts +61 -16
- package/src/modes/controllers/command-controller.ts +13 -2
- package/src/modes/controllers/input-controller.ts +11 -3
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/index.ts +5 -4
- package/src/modes/interactive-mode.ts +17 -3
- package/src/modes/setup-version.ts +11 -0
- package/src/modes/setup-wizard/index.ts +3 -2
- package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/context-usage.ts +10 -6
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/sdk.ts +21 -23
- package/src/session/agent-session.ts +7 -7
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/slash-commands/helpers/usage-report.ts +2 -0
- package/src/task/executor.ts +20 -2
- package/src/task/render.ts +1 -2
- package/src/telemetry-export.ts +25 -7
- package/src/tools/eval-backends.ts +6 -17
- package/src/tools/eval-render.ts +21 -18
- package/src/tools/eval.ts +5 -4
- package/src/tools/fetch.ts +94 -84
- package/src/tools/render-utils.ts +17 -3
- package/src/tools/renderers.ts +16 -1
- package/src/tools/report-tool-issue.ts +1 -1
- package/src/tools/search.ts +173 -81
- package/src/tools/todo.ts +20 -7
- package/src/tools/write.ts +22 -1
- package/src/web/scrapers/github.ts +255 -3
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/perplexity.ts +199 -51
- package/src/web/search/render.ts +39 -54
- package/src/web/search/types.ts +5 -1
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
- package/src/eval/__tests__/shared-executors.test.ts +0 -609
|
@@ -3059,13 +3059,26 @@ export const SETTINGS_SCHEMA = {
|
|
|
3059
3059
|
],
|
|
3060
3060
|
},
|
|
3061
3061
|
},
|
|
3062
|
-
"providers.
|
|
3063
|
-
type: "
|
|
3064
|
-
|
|
3062
|
+
"providers.fetch": {
|
|
3063
|
+
type: "enum",
|
|
3064
|
+
values: ["auto", "native", "trafilatura", "lynx", "parallel", "jina"] as const,
|
|
3065
|
+
default: "auto",
|
|
3065
3066
|
ui: {
|
|
3066
3067
|
tab: "providers",
|
|
3067
|
-
label: "
|
|
3068
|
-
description: "
|
|
3068
|
+
label: "Fetch Provider",
|
|
3069
|
+
description: "Reader backend priority for the fetch/read URL tool",
|
|
3070
|
+
options: [
|
|
3071
|
+
{
|
|
3072
|
+
value: "auto",
|
|
3073
|
+
label: "Auto",
|
|
3074
|
+
description: "Priority: native > trafilatura > lynx > parallel > jina",
|
|
3075
|
+
},
|
|
3076
|
+
{ value: "native", label: "Native", description: "In-process HTML→Markdown converter (always available)" },
|
|
3077
|
+
{ value: "trafilatura", label: "Trafilatura", description: "Auto-installs via uv/pip" },
|
|
3078
|
+
{ value: "lynx", label: "Lynx", description: "Requires lynx system package" },
|
|
3079
|
+
{ value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
|
|
3080
|
+
{ value: "jina", label: "Jina", description: "Uses r.jina.ai reader (JINA_API_KEY optional)" },
|
|
3081
|
+
],
|
|
3069
3082
|
},
|
|
3070
3083
|
},
|
|
3071
3084
|
"provider.appendOnlyContext": {
|
package/src/config/settings.ts
CHANGED
|
@@ -712,6 +712,17 @@ export class Settings {
|
|
|
712
712
|
}
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
+
// providers.parallelFetch (boolean) replaced by the providers.fetch reader
|
|
716
|
+
// priority enum. The new default ("auto") supersedes both old values —
|
|
717
|
+
// Parallel is now a deep fallback in the auto chain rather than the first
|
|
718
|
+
// choice — so drop the legacy key (flat and nested) and let the enum
|
|
719
|
+
// default apply.
|
|
720
|
+
const providersObj = raw.providers as Record<string, unknown> | undefined;
|
|
721
|
+
if (providersObj && "parallelFetch" in providersObj) {
|
|
722
|
+
delete providersObj.parallelFetch;
|
|
723
|
+
}
|
|
724
|
+
delete raw["providers.parallelFetch"];
|
|
725
|
+
|
|
715
726
|
// Map legacy `memories.enabled` boolean to the explicit `memory.backend`
|
|
716
727
|
// enum if the latter hasn't been set yet. Idempotent: subsequent
|
|
717
728
|
// migrations are no-ops once memory.backend is materialised.
|
package/src/dap/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { isEnoent, logger, ptree } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
3
4
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
4
5
|
import type {
|
|
@@ -165,19 +166,7 @@ export class DapClient {
|
|
|
165
166
|
detached: true,
|
|
166
167
|
});
|
|
167
168
|
|
|
168
|
-
|
|
169
|
-
await waitForCondition(
|
|
170
|
-
() => {
|
|
171
|
-
try {
|
|
172
|
-
Bun.file(socketPath).size;
|
|
173
|
-
return true;
|
|
174
|
-
} catch {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
10_000,
|
|
179
|
-
proc,
|
|
180
|
-
);
|
|
169
|
+
await waitForCondition(() => isUnixSocketReady(socketPath), 10_000, proc);
|
|
181
170
|
|
|
182
171
|
const { readable, writeSink, socket } = await connectSocket({ unix: socketPath });
|
|
183
172
|
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
@@ -553,15 +542,24 @@ export class DapClient {
|
|
|
553
542
|
}
|
|
554
543
|
}
|
|
555
544
|
|
|
545
|
+
async function isUnixSocketReady(socketPath: string): Promise<boolean> {
|
|
546
|
+
try {
|
|
547
|
+
return (await fs.stat(socketPath)).isSocket();
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (isEnoent(error)) return false;
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
556
554
|
/** Poll a condition until it returns true, or timeout/process exit. */
|
|
557
555
|
async function waitForCondition(
|
|
558
|
-
check: () => boolean
|
|
556
|
+
check: () => boolean | Promise<boolean>,
|
|
559
557
|
timeoutMs: number,
|
|
560
558
|
proc: { exitCode: number | null },
|
|
561
559
|
): Promise<void> {
|
|
562
560
|
const deadline = Date.now() + timeoutMs;
|
|
563
561
|
while (Date.now() < deadline) {
|
|
564
|
-
if (check()) return;
|
|
562
|
+
if (await check()) return;
|
|
565
563
|
if (proc.exitCode !== null) {
|
|
566
564
|
throw new Error("Adapter process exited before socket was ready");
|
|
567
565
|
}
|
package/src/edit/renderer.ts
CHANGED
|
@@ -179,11 +179,6 @@ function countEditFiles(edits: EditRenderEntry[]): number {
|
|
|
179
179
|
return new Set(edits.map(edit => filePathFromEditEntry(edit.path)).filter(Boolean)).size;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
function countLines(text: string): number {
|
|
183
|
-
if (!text) return 0;
|
|
184
|
-
return text.split("\n").length;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
182
|
function getOperationTitle(op: Operation | undefined): string {
|
|
188
183
|
return op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
|
|
189
184
|
}
|
|
@@ -233,19 +228,22 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string)
|
|
|
233
228
|
return preview.trimEnd();
|
|
234
229
|
}
|
|
235
230
|
|
|
236
|
-
function formatStreamingDiff(
|
|
231
|
+
function formatStreamingDiff(
|
|
232
|
+
diff: string,
|
|
233
|
+
rawPath: string,
|
|
234
|
+
uiTheme: Theme,
|
|
235
|
+
expanded: boolean,
|
|
236
|
+
label = "streaming",
|
|
237
|
+
): string {
|
|
237
238
|
if (!diff) return "";
|
|
238
|
-
// "Cursor" tail window: pin the last
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// stuttered, and the earlier high-water fix traded that for a half-empty
|
|
245
|
-
// rectangle. A strict fixed-height window keeps the box steady and always
|
|
246
|
-
// full of real diff context instead of blank padding.
|
|
239
|
+
// Collapsed uses a "Cursor" tail window: pin the last
|
|
240
|
+
// EDIT_STREAMING_PREVIEW_LINES rows to the bottom so freshly streamed changes
|
|
241
|
+
// stay on screen. The whole-file diff is recomputed on every streamed chunk
|
|
242
|
+
// and its Myers alignment is not monotonic in payload length, so a hunk-aware
|
|
243
|
+
// window stutters as rows move between hunks. Expanded deliberately lifts that
|
|
244
|
+
// cap for the approval-time full view.
|
|
247
245
|
const allLines = diff.replace(/\n+$/u, "").split("\n");
|
|
248
|
-
const hiddenLines = Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
|
|
246
|
+
const hiddenLines = expanded ? 0 : Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
|
|
249
247
|
const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
|
|
250
248
|
let text = "\n\n";
|
|
251
249
|
if (hiddenLines > 0) {
|
|
@@ -256,19 +254,11 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
|
|
|
256
254
|
text += `${uiTheme.fg("dim", `… (${remainder.join(", ")} above)`)}\n`;
|
|
257
255
|
}
|
|
258
256
|
text += renderDiffColored(visible.join("\n"), { filePath: rawPath });
|
|
259
|
-
text += uiTheme.fg("dim", `\n(${label})`);
|
|
257
|
+
if (!expanded || label !== "preview") text += uiTheme.fg("dim", `\n(${label})`);
|
|
260
258
|
return text;
|
|
261
259
|
}
|
|
262
260
|
|
|
263
|
-
function
|
|
264
|
-
const icon = uiTheme.getLangIcon(language);
|
|
265
|
-
if (lineCount !== null) {
|
|
266
|
-
return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
|
|
267
|
-
}
|
|
268
|
-
return uiTheme.fg("dim", `${icon}`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: Theme): string {
|
|
261
|
+
function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: Theme, expanded: boolean): string {
|
|
272
262
|
const parts: string[] = [];
|
|
273
263
|
for (const preview of previews) {
|
|
274
264
|
if (!preview.diff && !preview.error) continue;
|
|
@@ -278,7 +268,7 @@ function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: T
|
|
|
278
268
|
continue;
|
|
279
269
|
}
|
|
280
270
|
if (preview.diff) {
|
|
281
|
-
parts.push(`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, "preview")}`);
|
|
271
|
+
parts.push(`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, expanded, "preview")}`);
|
|
282
272
|
}
|
|
283
273
|
}
|
|
284
274
|
return parts.join("");
|
|
@@ -289,16 +279,17 @@ function getCallPreview(
|
|
|
289
279
|
rawPath: string,
|
|
290
280
|
uiTheme: Theme,
|
|
291
281
|
renderContext: EditRenderContext | undefined,
|
|
282
|
+
expanded: boolean,
|
|
292
283
|
): string {
|
|
293
284
|
const multi = renderContext?.perFileDiffPreview;
|
|
294
285
|
if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) {
|
|
295
|
-
return formatMultiFileStreamingDiff(multi, uiTheme);
|
|
286
|
+
return formatMultiFileStreamingDiff(multi, uiTheme, expanded);
|
|
296
287
|
}
|
|
297
288
|
if (args.previewDiff) {
|
|
298
|
-
return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, "preview");
|
|
289
|
+
return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, expanded, "preview");
|
|
299
290
|
}
|
|
300
291
|
if (args.diff && args.op) {
|
|
301
|
-
return formatStreamingDiff(args.diff, rawPath, uiTheme);
|
|
292
|
+
return formatStreamingDiff(args.diff, rawPath, uiTheme, expanded);
|
|
302
293
|
}
|
|
303
294
|
if (args.diff) {
|
|
304
295
|
return renderPlainTextPreview(args.diff, uiTheme, rawPath);
|
|
@@ -383,6 +374,13 @@ function getApplyPatchRenderSummary(
|
|
|
383
374
|
}
|
|
384
375
|
}
|
|
385
376
|
|
|
377
|
+
function formatDiffStatsSuffix(diff: string, uiTheme: Theme): string {
|
|
378
|
+
const { added, removed, hunks } = getDiffStats(diff);
|
|
379
|
+
const stats = formatDiffStats(added, removed, hunks, uiTheme);
|
|
380
|
+
if (!stats) return "";
|
|
381
|
+
return ` ${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${stats}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
|
|
382
|
+
}
|
|
383
|
+
|
|
386
384
|
function renderDiffSection(
|
|
387
385
|
diff: string,
|
|
388
386
|
rawPath: string,
|
|
@@ -390,15 +388,6 @@ function renderDiffSection(
|
|
|
390
388
|
uiTheme: Theme,
|
|
391
389
|
renderDiffFn: (t: string, o?: { filePath?: string }) => string,
|
|
392
390
|
): string {
|
|
393
|
-
let text = "";
|
|
394
|
-
const diffStats = getDiffStats(diff);
|
|
395
|
-
text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${formatDiffStats(
|
|
396
|
-
diffStats.added,
|
|
397
|
-
diffStats.removed,
|
|
398
|
-
diffStats.hunks,
|
|
399
|
-
uiTheme,
|
|
400
|
-
)}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
|
|
401
|
-
|
|
402
391
|
const {
|
|
403
392
|
text: truncatedDiff,
|
|
404
393
|
hiddenHunks,
|
|
@@ -407,7 +396,7 @@ function renderDiffSection(
|
|
|
407
396
|
? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
|
|
408
397
|
: truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
|
|
409
398
|
|
|
410
|
-
text
|
|
399
|
+
let text = `\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
|
|
411
400
|
if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
|
|
412
401
|
const remainder: string[] = [];
|
|
413
402
|
if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
|
|
@@ -481,7 +470,7 @@ export const editToolRenderer = {
|
|
|
481
470
|
if (fileCount > 1) {
|
|
482
471
|
text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
|
|
483
472
|
}
|
|
484
|
-
text += getCallPreview(editArgs, rawPath, uiTheme, renderContext);
|
|
473
|
+
text += getCallPreview(editArgs, rawPath, uiTheme, renderContext, options.expanded);
|
|
485
474
|
if (applyPatchSummary?.error) {
|
|
486
475
|
text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), CALL_TEXT_PREVIEW_WIDTH))}`;
|
|
487
476
|
}
|
|
@@ -528,11 +517,6 @@ function renderSingleFileResult(
|
|
|
528
517
|
"";
|
|
529
518
|
const op = args?.op || firstEdit?.op || details?.op;
|
|
530
519
|
const rename = args?.rename || firstEdit?.rename || firstEdit?.move || details?.move;
|
|
531
|
-
const { language } = formatEditDescription(rawPath, uiTheme, { rename });
|
|
532
|
-
|
|
533
|
-
const editTextSource = args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch;
|
|
534
|
-
const metadataLineCount = editTextSource ? countLines(editTextSource) : null;
|
|
535
|
-
const metadataLine = op !== "delete" ? `\n${formatMetadataLine(metadataLineCount, language, uiTheme)}` : "";
|
|
536
520
|
|
|
537
521
|
const displayErrorText = isError && details && "displayErrorText" in details ? details.displayErrorText : undefined;
|
|
538
522
|
const errorText = isError
|
|
@@ -556,6 +540,11 @@ function renderSingleFileResult(
|
|
|
556
540
|
(details && !isError ? details.firstChangedLine : undefined);
|
|
557
541
|
const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine });
|
|
558
542
|
|
|
543
|
+
// Change stats ride inline on the header next to the path rather than a separate row.
|
|
544
|
+
const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
|
|
545
|
+
const headerDiff = isError ? undefined : details?.diff || previewDiff;
|
|
546
|
+
const statsSuffix = headerDiff ? formatDiffStatsSuffix(headerDiff, uiTheme) : "";
|
|
547
|
+
|
|
559
548
|
const header = renderStatusLine(
|
|
560
549
|
{
|
|
561
550
|
icon: isError ? "error" : "success",
|
|
@@ -564,8 +553,7 @@ function renderSingleFileResult(
|
|
|
564
553
|
},
|
|
565
554
|
uiTheme,
|
|
566
555
|
);
|
|
567
|
-
let text = header;
|
|
568
|
-
text += metadataLine;
|
|
556
|
+
let text = header + statsSuffix;
|
|
569
557
|
|
|
570
558
|
if (isError) {
|
|
571
559
|
if (errorText) {
|
|
@@ -231,6 +231,57 @@ describe("runEvalAgent", () => {
|
|
|
231
231
|
});
|
|
232
232
|
await expect(runEvalAgent({ prompt: "fail" }, { session: makeSession() })).rejects.toThrow("boom");
|
|
233
233
|
});
|
|
234
|
+
|
|
235
|
+
// Regression: a runtime-limit abort returns exitCode=1, stderr="", error=undefined,
|
|
236
|
+
// aborted=true, abortReason="Subagent runtime limit exceeded (...)". The previous
|
|
237
|
+
// failure-message coalesce stopped at the empty `stderr` (since `??` only skips
|
|
238
|
+
// nullish values) and shipped an empty error through the bridge — Python then
|
|
239
|
+
// surfaced the generic `bridge call '__agent__' failed`. See #2006.
|
|
240
|
+
it("surfaces abortReason for aborts that leave stderr empty", async () => {
|
|
241
|
+
mockAgents();
|
|
242
|
+
const runSpy = vi.spyOn(taskExecutor, "runSubprocess");
|
|
243
|
+
runSpy.mockImplementationOnce(async options =>
|
|
244
|
+
singleResult(options, {
|
|
245
|
+
exitCode: 1,
|
|
246
|
+
output: "",
|
|
247
|
+
stderr: "",
|
|
248
|
+
error: undefined,
|
|
249
|
+
aborted: true,
|
|
250
|
+
abortReason: "Subagent runtime limit exceeded (task.maxRuntimeMs=900000)",
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
runSpy.mockImplementationOnce(async options =>
|
|
254
|
+
singleResult(options, {
|
|
255
|
+
exitCode: 1,
|
|
256
|
+
output: "",
|
|
257
|
+
stderr: " ",
|
|
258
|
+
error: " ",
|
|
259
|
+
aborted: true,
|
|
260
|
+
abortReason: "Cancelled by caller",
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
runSpy.mockImplementationOnce(async options =>
|
|
264
|
+
singleResult(options, {
|
|
265
|
+
exitCode: 1,
|
|
266
|
+
output: "",
|
|
267
|
+
stderr: "",
|
|
268
|
+
error: undefined,
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
await expect(runEvalAgent({ prompt: "slow" }, { session: makeSession() })).rejects.toThrow(
|
|
273
|
+
"Subagent runtime limit exceeded (task.maxRuntimeMs=900000)",
|
|
274
|
+
);
|
|
275
|
+
// Whitespace-only stderr/error must not mask abortReason either.
|
|
276
|
+
await expect(runEvalAgent({ prompt: "cancelled" }, { session: makeSession() })).rejects.toThrow(
|
|
277
|
+
"Cancelled by caller",
|
|
278
|
+
);
|
|
279
|
+
// Last resort: still produce a non-empty message even when nothing useful is set,
|
|
280
|
+
// so Python never falls back to `bridge call '__agent__' failed`.
|
|
281
|
+
await expect(runEvalAgent({ prompt: "blank" }, { session: makeSession() })).rejects.toThrow(
|
|
282
|
+
"agent() subagent 'task' failed.",
|
|
283
|
+
);
|
|
284
|
+
});
|
|
234
285
|
});
|
|
235
286
|
|
|
236
287
|
describe("agent() through eval runtimes", () => {
|
|
@@ -326,18 +377,6 @@ describe("agent() through eval runtimes", () => {
|
|
|
326
377
|
singleResult(options, { output: "hello from python" }),
|
|
327
378
|
);
|
|
328
379
|
|
|
329
|
-
const probe = await executePython('print("probe")', {
|
|
330
|
-
cwd: tempDir.path(),
|
|
331
|
-
sessionId: `${sessionId}:probe`,
|
|
332
|
-
sessionFile,
|
|
333
|
-
kernelMode: "per-call",
|
|
334
|
-
});
|
|
335
|
-
if (probe.exitCode === undefined && probe.cancelled) {
|
|
336
|
-
expect(probe.output).toBe("");
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
expect(probe.exitCode).toBe(0);
|
|
340
|
-
|
|
341
380
|
const result = await executePython('print(agent("hi"))', {
|
|
342
381
|
cwd: tempDir.path(),
|
|
343
382
|
sessionId,
|
|
@@ -345,6 +384,10 @@ describe("agent() through eval runtimes", () => {
|
|
|
345
384
|
kernelMode: "per-call",
|
|
346
385
|
toolSession: session,
|
|
347
386
|
});
|
|
387
|
+
if (result.exitCode === undefined && result.cancelled) {
|
|
388
|
+
expect(result.output).toBe("");
|
|
389
|
+
return; // kernel unavailable in this environment
|
|
390
|
+
}
|
|
348
391
|
|
|
349
392
|
expect(result.exitCode).toBe(0);
|
|
350
393
|
expect(result.output.trim()).toBe("hello from python");
|
|
@@ -373,22 +416,14 @@ describe("agent() through eval runtimes", () => {
|
|
|
373
416
|
}
|
|
374
417
|
});
|
|
375
418
|
|
|
376
|
-
const probe = await executePython('print("probe")', {
|
|
377
|
-
cwd: tempDir.path(),
|
|
378
|
-
sessionId: `${sessionId}:probe`,
|
|
379
|
-
sessionFile,
|
|
380
|
-
kernelMode: "per-call",
|
|
381
|
-
});
|
|
382
|
-
if (probe.exitCode === undefined && probe.cancelled) {
|
|
383
|
-
expect(probe.output).toBe("");
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
expect(probe.exitCode).toBe(0);
|
|
387
|
-
|
|
388
419
|
const result = await executePython(
|
|
389
420
|
'import json\nprint(json.dumps(parallel([lambda n=n: agent(n) for n in ["a", "b", "c", "d"]])))',
|
|
390
421
|
{ cwd: tempDir.path(), sessionId, sessionFile, kernelMode: "per-call", toolSession: session },
|
|
391
422
|
);
|
|
423
|
+
if (result.exitCode === undefined && result.cancelled) {
|
|
424
|
+
expect(result.output).toBe("");
|
|
425
|
+
return; // kernel unavailable in this environment
|
|
426
|
+
}
|
|
392
427
|
|
|
393
428
|
expect(result.exitCode).toBe(0);
|
|
394
429
|
expect(JSON.parse(result.output.trim())).toEqual(["a", "b", "c", "d"]);
|
|
@@ -412,7 +447,14 @@ describe("agent() through eval runtimes", () => {
|
|
|
412
447
|
// The host must respond the instant the cell aborts so the kernel can
|
|
413
448
|
// unwind via KeyboardInterrupt instead of being hard-killed (which used to
|
|
414
449
|
// surface "[kernel] Python kernel shutdown" and lose all session state).
|
|
450
|
+
let inFlight = 0;
|
|
451
|
+
let markSaturated: (() => void) | undefined;
|
|
452
|
+
const saturated = new Promise<void>(resolve => {
|
|
453
|
+
markSaturated = resolve;
|
|
454
|
+
});
|
|
415
455
|
vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
|
|
456
|
+
// task.maxConcurrency=6 → six bridge calls block at once; signal then.
|
|
457
|
+
if (++inFlight >= 6) markSaturated?.();
|
|
416
458
|
await Bun.sleep(9000); // deliberately ignores options.signal
|
|
417
459
|
return singleResult(options, { output: options.assignment ?? "" });
|
|
418
460
|
});
|
|
@@ -432,8 +474,9 @@ describe("agent() through eval runtimes", () => {
|
|
|
432
474
|
expect(seed.exitCode).toBe(0);
|
|
433
475
|
|
|
434
476
|
const ac = new AbortController();
|
|
435
|
-
// Abort
|
|
436
|
-
|
|
477
|
+
// Abort the instant all six worker threads are confirmed blocked in their
|
|
478
|
+
// bridge calls (condition-driven) instead of waiting a fixed wall second.
|
|
479
|
+
void saturated.then(() => ac.abort(new Error("external interrupt")));
|
|
437
480
|
|
|
438
481
|
const start = Date.now();
|
|
439
482
|
const result = await executePython(
|
|
@@ -568,12 +611,12 @@ describe("agent() through eval runtimes", () => {
|
|
|
568
611
|
// of its own. The bridge pause must make that delegated time invisible to
|
|
569
612
|
// the watchdog.
|
|
570
613
|
vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
|
|
571
|
-
await Bun.sleep(
|
|
614
|
+
await Bun.sleep(40);
|
|
572
615
|
return singleResult(options, { output: "done" });
|
|
573
616
|
});
|
|
574
617
|
|
|
575
618
|
const ops: string[] = [];
|
|
576
|
-
using idle = new IdleTimeout(
|
|
619
|
+
using idle = new IdleTimeout(20);
|
|
577
620
|
const result = await runEvalAgent(
|
|
578
621
|
{ prompt: "investigate" },
|
|
579
622
|
{
|
|
@@ -591,7 +634,7 @@ describe("agent() through eval runtimes", () => {
|
|
|
591
634
|
expect(ops).toEqual([EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP]);
|
|
592
635
|
expect(idle.signal.aborted).toBe(false);
|
|
593
636
|
|
|
594
|
-
await Bun.sleep(
|
|
637
|
+
await Bun.sleep(60);
|
|
595
638
|
expect(idle.signal.aborted).toBe(true);
|
|
596
639
|
});
|
|
597
640
|
|
|
@@ -604,7 +647,7 @@ describe("agent() through eval runtimes", () => {
|
|
|
604
647
|
// They render as status, but timeout accounting is controlled only by the
|
|
605
648
|
// bridge pause/resume events.
|
|
606
649
|
vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
|
|
607
|
-
for (let i = 0; i <
|
|
650
|
+
for (let i = 0; i < 20; i++) {
|
|
608
651
|
options.onProgress?.({
|
|
609
652
|
index: options.index,
|
|
610
653
|
id: options.id,
|
|
@@ -621,13 +664,13 @@ describe("agent() through eval runtimes", () => {
|
|
|
621
664
|
cost: 0,
|
|
622
665
|
durationMs: i * 10,
|
|
623
666
|
});
|
|
624
|
-
await Bun.sleep(
|
|
667
|
+
await Bun.sleep(5);
|
|
625
668
|
}
|
|
626
669
|
return singleResult(options, { output: "done" });
|
|
627
670
|
});
|
|
628
671
|
|
|
629
672
|
const ops: string[] = [];
|
|
630
|
-
using idle = new IdleTimeout(
|
|
673
|
+
using idle = new IdleTimeout(40);
|
|
631
674
|
const result = await runEvalAgent(
|
|
632
675
|
{ prompt: "investigate" },
|
|
633
676
|
{
|
package/src/eval/agent-bridge.ts
CHANGED
|
@@ -13,7 +13,7 @@ import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.m
|
|
|
13
13
|
import * as taskDiscovery from "../task/discovery";
|
|
14
14
|
import * as taskExecutor from "../task/executor";
|
|
15
15
|
import { AgentOutputManager } from "../task/output-manager";
|
|
16
|
-
import type { AgentDefinition, AgentProgress } from "../task/types";
|
|
16
|
+
import type { AgentDefinition, AgentProgress, SingleResult } from "../task/types";
|
|
17
17
|
import type { ToolSession } from "../tools";
|
|
18
18
|
import { ToolError } from "../tools/tool-errors";
|
|
19
19
|
import { withBridgeTimeoutPause } from "./bridge-timeout";
|
|
@@ -173,6 +173,26 @@ function emitProgressStatus(emitStatus: ((event: JsStatusEvent) => void) | undef
|
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Coalesce a subagent failure into a non-empty, human-meaningful error message.
|
|
178
|
+
*
|
|
179
|
+
* When the executor aborts a subagent (runtime limit, parent cancellation, …)
|
|
180
|
+
* the actionable explanation lives on `abortReason`, while `error`/`stderr`
|
|
181
|
+
* are routinely empty strings. Plain `??` coalescing stops at the empty string
|
|
182
|
+
* and ships an empty error through the bridge — Python then surfaces only the
|
|
183
|
+
* generic `bridge call '__agent__' failed`. See #2006.
|
|
184
|
+
*/
|
|
185
|
+
function buildSubagentFailureMessage(agentName: string, result: SingleResult): string {
|
|
186
|
+
const abortReason = trimToUndefined(result.abortReason);
|
|
187
|
+
if (result.aborted && abortReason) return abortReason;
|
|
188
|
+
return (
|
|
189
|
+
trimToUndefined(result.error) ??
|
|
190
|
+
trimToUndefined(result.stderr) ??
|
|
191
|
+
abortReason ??
|
|
192
|
+
`agent() subagent '${agentName}' failed.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
176
196
|
/**
|
|
177
197
|
* Run a single subagent on behalf of an eval cell's `agent()` call.
|
|
178
198
|
*/
|
|
@@ -225,7 +245,6 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
225
245
|
getSessionId: options.session.getSessionId ?? (() => null),
|
|
226
246
|
};
|
|
227
247
|
const parentArtifactManager = options.session.getArtifactManager?.() ?? undefined;
|
|
228
|
-
const parentEvalSessionId = options.session.getEvalSessionId?.() ?? undefined;
|
|
229
248
|
const mcpManager = options.session.mcpManager ?? MCPManager.instance();
|
|
230
249
|
const { sessionFile, artifactsDir, contextFile } = await getArtifacts(options.session);
|
|
231
250
|
const outputManager = getOutputManager(options.session);
|
|
@@ -260,6 +279,12 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
260
279
|
authStorage: options.session.authStorage,
|
|
261
280
|
modelRegistry: options.session.modelRegistry,
|
|
262
281
|
settings: options.session.settings,
|
|
282
|
+
// Eval `agent()` subagents are never wall-clock capped: the parent
|
|
283
|
+
// cell's idle watchdog is suspended for the whole bridge call
|
|
284
|
+
// (withBridgeTimeoutPause), so a long-running phase/recovery workflow
|
|
285
|
+
// must not be killed by `task.maxRuntimeMs`. Force the limit off
|
|
286
|
+
// regardless of the inherited session setting.
|
|
287
|
+
maxRuntimeMs: 0,
|
|
263
288
|
mcpManager,
|
|
264
289
|
contextFiles,
|
|
265
290
|
skills: availableSkills,
|
|
@@ -271,14 +296,16 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
271
296
|
parentHindsightSessionState: options.session.getHindsightSessionState?.(),
|
|
272
297
|
parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
|
|
273
298
|
parentTelemetry: options.session.getTelemetry?.(),
|
|
274
|
-
parentEvalSessionId
|
|
299
|
+
// Deliberately omit parentEvalSessionId: the parent's Python kernel is
|
|
300
|
+
// blocked on this bridge call, so sharing the eval session would deadlock
|
|
301
|
+
// (subagent queues behind the parent's in-flight execution, parent waits
|
|
302
|
+
// for subagent → circular). Each bridge-spawned subagent gets its own
|
|
303
|
+
// eval session with an independent kernel.
|
|
275
304
|
}),
|
|
276
305
|
);
|
|
277
306
|
|
|
278
|
-
if (result.exitCode !== 0 || result.error) {
|
|
279
|
-
|
|
280
|
-
result.error ?? result.stderr ?? result.abortReason ?? `agent() subagent '${agentName}' failed.`;
|
|
281
|
-
throw new ToolError(failureMessage);
|
|
307
|
+
if (result.exitCode !== 0 || result.error || result.aborted) {
|
|
308
|
+
throw new ToolError(buildSubagentFailureMessage(agentName, result));
|
|
282
309
|
}
|
|
283
310
|
|
|
284
311
|
options.session.recordEvalSubagentUsage?.(result.usage?.output ?? 0);
|
|
@@ -25,7 +25,6 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
|
|
|
25
25
|
const apiKeys = [
|
|
26
26
|
{ name: "ANTHROPIC_API_KEY", description: "Anthropic API" },
|
|
27
27
|
{ name: "OPENAI_API_KEY", description: "OpenAI API" },
|
|
28
|
-
{ name: "PERPLEXITY_API_KEY", description: "Perplexity search" },
|
|
29
28
|
{ name: "EXA_API_KEY", description: "Exa search" },
|
|
30
29
|
];
|
|
31
30
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
3
|
+
type MarketplaceAutoUpdateMode = "off" | "notify" | "auto";
|
|
4
|
+
|
|
5
|
+
interface MarketplaceAutoUpdateOptions {
|
|
6
|
+
autoUpdate: MarketplaceAutoUpdateMode;
|
|
7
|
+
resolveActiveProjectRegistryPath: (cwd: string) => Promise<string | null>;
|
|
8
|
+
clearPluginRootsCache: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function scheduleMarketplaceAutoUpdate(options: MarketplaceAutoUpdateOptions): void {
|
|
12
|
+
if (options.autoUpdate === "off") {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
void runMarketplaceAutoUpdate(options);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function runMarketplaceAutoUpdate(options: MarketplaceAutoUpdateOptions): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
// Startup perf: marketplace manager pulls scraper/fetch/cache code; keep it out of the initial TUI graph.
|
|
22
|
+
const {
|
|
23
|
+
MarketplaceManager,
|
|
24
|
+
getInstalledPluginsRegistryPath,
|
|
25
|
+
getMarketplacesCacheDir,
|
|
26
|
+
getMarketplacesRegistryPath,
|
|
27
|
+
getPluginsCacheDir,
|
|
28
|
+
} = await import("./marketplace");
|
|
29
|
+
const mgr = new MarketplaceManager({
|
|
30
|
+
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
31
|
+
installedRegistryPath: getInstalledPluginsRegistryPath(),
|
|
32
|
+
projectInstalledRegistryPath: (await options.resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
|
|
33
|
+
marketplacesCacheDir: getMarketplacesCacheDir(),
|
|
34
|
+
pluginsCacheDir: getPluginsCacheDir(),
|
|
35
|
+
clearPluginRootsCache: options.clearPluginRootsCache,
|
|
36
|
+
});
|
|
37
|
+
await mgr.refreshStaleMarketplaces();
|
|
38
|
+
const updates = await mgr.checkForUpdates();
|
|
39
|
+
if (updates.length === 0) return;
|
|
40
|
+
if (options.autoUpdate === "auto") {
|
|
41
|
+
await mgr.upgradeAllPlugins();
|
|
42
|
+
logger.debug(`Auto-upgraded ${updates.length} marketplace plugin(s)`);
|
|
43
|
+
} else {
|
|
44
|
+
logger.debug(`${updates.length} marketplace plugin update(s) available — /marketplace upgrade`);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Silently ignore — network failure, corrupt data, offline.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -8,7 +8,7 @@ import type { Theme, ThemeColor } from "../../modes/theme/theme";
|
|
|
8
8
|
import goalDescription from "../../prompts/tools/goal.md" with { type: "text" };
|
|
9
9
|
import { formatDuration } from "../../slash-commands/helpers/format";
|
|
10
10
|
import type { ToolSession } from "../../tools";
|
|
11
|
-
import {
|
|
11
|
+
import { formatErrorDetail, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
12
12
|
import { ToolError } from "../../tools/tool-errors";
|
|
13
13
|
import { renderStatusLine, truncateToWidth } from "../../tui";
|
|
14
14
|
import { completionBudgetReport, remainingTokens } from "../runtime";
|
|
@@ -190,7 +190,7 @@ export const goalToolRenderer = {
|
|
|
190
190
|
|
|
191
191
|
if (result.isError) {
|
|
192
192
|
const header = renderStatusLine({ icon: "error", title: "Goal", description }, uiTheme);
|
|
193
|
-
const body =
|
|
193
|
+
const body = formatErrorDetail(fallbackText || "Goal tool failed", uiTheme);
|
|
194
194
|
return new Text([header, body].join("\n"), 0, 0);
|
|
195
195
|
}
|
|
196
196
|
|