@oh-my-pi/pi-coding-agent 14.9.2 → 14.9.5
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 +89 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +3 -3
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/prompt-templates.ts +0 -5
- package/src/config/settings-schema.ts +39 -1
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/eval.lark +10 -31
- package/src/eval/index.ts +1 -0
- package/src/eval/js/context-manager.ts +1 -38
- package/src/eval/js/prelude.txt +0 -2
- package/src/eval/parse.ts +156 -255
- package/src/eval/py/executor.ts +24 -8
- package/src/eval/py/index.ts +1 -0
- package/src/eval/py/prelude.py +11 -80
- package/src/eval/sniff.ts +28 -0
- package/src/export/html/template.css +50 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +229 -17
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/hashline/constants.ts +20 -0
- package/src/hashline/grammar.lark +16 -23
- package/src/hashline/hash.ts +4 -34
- package/src/hashline/input.ts +16 -2
- package/src/hashline/parser.ts +12 -1
- package/src/internal-urls/agent-protocol.ts +64 -52
- package/src/internal-urls/artifact-protocol.ts +52 -51
- package/src/internal-urls/docs-index.generated.ts +34 -1
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +50 -7
- package/src/internal-urls/mcp-protocol.ts +3 -8
- package/src/internal-urls/memory-protocol.ts +90 -59
- package/src/internal-urls/pi-protocol.ts +1 -0
- package/src/internal-urls/router.ts +40 -23
- package/src/internal-urls/rule-protocol.ts +3 -20
- package/src/internal-urls/skill-protocol.ts +5 -27
- package/src/internal-urls/types.ts +18 -2
- package/src/main.ts +1 -1
- package/src/mcp/manager.ts +17 -0
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/components/tree-selector.ts +4 -0
- package/src/modes/controllers/event-controller.ts +23 -2
- package/src/modes/controllers/mcp-command-controller.ts +7 -10
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/theme/theme.ts +27 -27
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +14 -9
- package/src/prompts/commands/orchestrate.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +0 -2
- package/src/prompts/system/project-prompt.md +10 -0
- package/src/prompts/system/subagent-system-prompt.md +18 -9
- package/src/prompts/system/subagent-user-prompt.md +1 -10
- package/src/prompts/system/system-prompt.md +159 -232
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -34
- package/src/prompts/tools/eval.md +27 -16
- package/src/prompts/tools/github.md +6 -5
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/job.md +14 -6
- package/src/prompts/tools/task.md +20 -3
- package/src/registry/agent-registry.ts +2 -1
- package/src/sdk.ts +87 -89
- package/src/session/agent-session.ts +107 -37
- package/src/session/artifacts.ts +7 -4
- package/src/session/session-manager.ts +30 -1
- package/src/ssh/connection-manager.ts +32 -16
- package/src/ssh/sshfs-mount.ts +10 -7
- package/src/system-prompt.ts +3 -9
- package/src/task/executor.ts +23 -7
- package/src/task/index.ts +57 -36
- package/src/tool-discovery/tool-index.ts +21 -8
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +3 -2
- package/src/tools/bash.ts +30 -50
- package/src/tools/browser/tab-supervisor.ts +12 -2
- package/src/tools/eval.ts +59 -44
- package/src/tools/fetch.ts +1 -1
- package/src/tools/gh.ts +140 -4
- package/src/tools/index.ts +12 -11
- package/src/tools/job.ts +48 -12
- package/src/tools/path-utils.ts +21 -1
- package/src/tools/read.ts +74 -31
- package/src/tools/search.ts +16 -3
- package/src/tools/todo-write.ts +1 -1
- package/src/utils/file-display-mode.ts +11 -5
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/internal-urls/jobs-protocol.ts +0 -119
- package/src/task/template.ts +0 -47
- package/src/tools/bash-normalize.ts +0 -107
|
@@ -16,6 +16,17 @@ import type {
|
|
|
16
16
|
WorkerInitPayload,
|
|
17
17
|
WorkerOutbound,
|
|
18
18
|
} from "./tab-protocol";
|
|
19
|
+
// Imported with `type: "file"` so Bun's bundler statically discovers the
|
|
20
|
+
// worker entry and embeds it inside `bun build --compile` single-file
|
|
21
|
+
// binaries. Without this attribute the bundler cannot reach the entry through
|
|
22
|
+
// a `new URL(..., import.meta.url)` literal stored in a local variable, and
|
|
23
|
+
// the prebuilt binary surfaces `Timed out initializing browser tab worker`
|
|
24
|
+
// (issue #1011) because `/$bunfs/root/tab-worker-entry.ts` is missing.
|
|
25
|
+
// tsgo doesn't recognize Bun's `with { type: "file" }` attribute and treats
|
|
26
|
+
// this as a normal TS source import, raising TS1192/TS5097. Bun's bundler
|
|
27
|
+
// (and runtime) honors the attribute and returns the embedded file URL.
|
|
28
|
+
// @ts-expect-error -- Bun file-URL import (see comment above).
|
|
29
|
+
import tabWorkerEntryUrl from "./tab-worker-entry.ts" with { type: "file" };
|
|
19
30
|
|
|
20
31
|
interface WorkerHandle {
|
|
21
32
|
send(msg: WorkerInbound, transferList?: Transferable[]): void;
|
|
@@ -364,8 +375,7 @@ async function raceWithTimeout<T>(
|
|
|
364
375
|
|
|
365
376
|
async function spawnTabWorker(): Promise<WorkerHandle> {
|
|
366
377
|
try {
|
|
367
|
-
const
|
|
368
|
-
const worker = new Worker(url.href, { type: "module" });
|
|
378
|
+
const worker = new Worker(tabWorkerEntryUrl, { type: "module" });
|
|
369
379
|
return wrapBunWorker(worker);
|
|
370
380
|
} catch (err) {
|
|
371
381
|
logger.warn("Bun Worker spawn failed; using inline tab worker (no sync-loop guard)", {
|
package/src/tools/eval.ts
CHANGED
|
@@ -4,11 +4,11 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
4
4
|
import { Markdown, Text } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { type Static, Type } from "@sinclair/typebox";
|
|
7
|
-
import { jsBackend, parseEvalInput, pythonBackend } from "../eval";
|
|
7
|
+
import { jsBackend, parseEvalInput, pythonBackend, sniffEvalLanguage } from "../eval";
|
|
8
8
|
import type { ExecutorBackend } from "../eval/backend";
|
|
9
9
|
import evalGrammar from "../eval/eval.lark" with { type: "text" };
|
|
10
|
-
import type
|
|
11
|
-
import type { EvalCellResult, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
|
|
10
|
+
import { ABORT_WARNING, type ParsedEvalCell } from "../eval/parse";
|
|
11
|
+
import type { EvalCellResult, EvalDisplayOutput, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
14
14
|
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
@@ -26,7 +26,7 @@ export const EVAL_DEFAULT_PREVIEW_LINES = 10;
|
|
|
26
26
|
|
|
27
27
|
export const evalSchema = Type.Object({
|
|
28
28
|
input: Type.String({
|
|
29
|
-
description: "eval input as a sequence of
|
|
29
|
+
description: "eval input as a sequence of `*** Begin <LANG>` cell headers followed by code",
|
|
30
30
|
}),
|
|
31
31
|
});
|
|
32
32
|
export type EvalToolParams = Static<typeof evalSchema>;
|
|
@@ -47,6 +47,38 @@ function formatJsonScalar(value: unknown): string {
|
|
|
47
47
|
return "[object]";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Cap per `display()` value sent back to the model. */
|
|
51
|
+
const MAX_DISPLAY_TEXT_BYTES = 8000;
|
|
52
|
+
|
|
53
|
+
function formatDisplayJsonForText(value: unknown): string {
|
|
54
|
+
let text: string;
|
|
55
|
+
try {
|
|
56
|
+
text = JSON.stringify(value, null, 2) ?? String(value);
|
|
57
|
+
} catch {
|
|
58
|
+
text = String(value);
|
|
59
|
+
}
|
|
60
|
+
if (text.length > MAX_DISPLAY_TEXT_BYTES) {
|
|
61
|
+
text = `${text.slice(0, MAX_DISPLAY_TEXT_BYTES)}\n… (${text.length - MAX_DISPLAY_TEXT_BYTES} chars truncated)`;
|
|
62
|
+
}
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format display() JSON values into text the model can see. Images are surfaced
|
|
68
|
+
* separately as ImageContent so the model can actually inspect them; this helper
|
|
69
|
+
* intentionally does not touch images.
|
|
70
|
+
*/
|
|
71
|
+
function formatDisplayOutputsForText(outputs: EvalDisplayOutput[]): string {
|
|
72
|
+
const chunks: string[] = [];
|
|
73
|
+
let displayIndex = 0;
|
|
74
|
+
for (const output of outputs) {
|
|
75
|
+
if (output.type !== "json") continue;
|
|
76
|
+
displayIndex++;
|
|
77
|
+
chunks.push(`display[${displayIndex}]:\n${formatDisplayJsonForText(output.data)}`);
|
|
78
|
+
}
|
|
79
|
+
return chunks.join("\n\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDepth = expanded ? 6 : 2): string[] {
|
|
51
83
|
const maxItems = expanded ? 20 : 5;
|
|
52
84
|
|
|
@@ -131,33 +163,6 @@ function timeoutSecondsFromMs(timeoutMs: number): number {
|
|
|
131
163
|
return clampTimeout("eval", timeoutMs / 1000);
|
|
132
164
|
}
|
|
133
165
|
|
|
134
|
-
/**
|
|
135
|
-
* Best-effort language sniff for cells with no explicit `language`.
|
|
136
|
-
*
|
|
137
|
-
* Order:
|
|
138
|
-
* 1. Shebang on first line (`#!/usr/bin/env python`, `#!/usr/bin/env node`, etc.)
|
|
139
|
-
* 2. Strong syntactic markers unique to one language. We bias false negatives over
|
|
140
|
-
* false positives — anything ambiguous returns `undefined` and the caller falls
|
|
141
|
-
* back to the default-backend rules.
|
|
142
|
-
*/
|
|
143
|
-
function sniffLanguage(code: string): EvalLanguage | undefined {
|
|
144
|
-
const stripped = code.replace(/^\s+/, "");
|
|
145
|
-
if (stripped.startsWith("#!")) {
|
|
146
|
-
const firstLine = stripped.split("\n", 1)[0]!.toLowerCase();
|
|
147
|
-
if (/(\bpython\d?\b|\bipython\b)/.test(firstLine)) return "python";
|
|
148
|
-
if (/(\bnode\b|\bbun\b|\bdeno\b|\bjavascript\b|\bjs\b)/.test(firstLine)) return "js";
|
|
149
|
-
}
|
|
150
|
-
const jsMarkers =
|
|
151
|
-
/(^|\n)\s*(const|let|var|async\s+function|function\s*\*?\s*[\w$]*\s*\(|import\s+[^\n]+\sfrom\s|export\s+(default|const|let|function|class|async)|require\s*\(|console\.\w+\s*\(|=>|;\s*$)/m;
|
|
152
|
-
const pyMarkers =
|
|
153
|
-
/(^|\n)\s*(def\s+\w+\s*\(|from\s+[\w.]+\s+import|import\s+\w+(\s+as\s+\w+)?\s*$|class\s+\w+\s*[(:]|print\s*\(|elif\s+[^\n]*:|with\s+[^\n]+:\s*$|@[\w.]+\s*$)/m;
|
|
154
|
-
const hasJs = jsMarkers.test(code);
|
|
155
|
-
const hasPy = pyMarkers.test(code);
|
|
156
|
-
if (hasJs && !hasPy) return "js";
|
|
157
|
-
if (hasPy && !hasJs) return "python";
|
|
158
|
-
return undefined;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
166
|
async function resolveBackend(
|
|
162
167
|
session: ToolSession,
|
|
163
168
|
requested: EvalLanguage | undefined,
|
|
@@ -180,7 +185,7 @@ async function resolveBackend(
|
|
|
180
185
|
return { backend: jsBackend, fallback: false };
|
|
181
186
|
}
|
|
182
187
|
// Auto-detect.
|
|
183
|
-
const sniffed =
|
|
188
|
+
const sniffed = sniffEvalLanguage(code);
|
|
184
189
|
if (sniffed === "python" && allowPy && (await pythonBackend.isAvailable(session))) {
|
|
185
190
|
return { backend: pythonBackend, fallback: false };
|
|
186
191
|
}
|
|
@@ -397,13 +402,16 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
397
402
|
const durationMs = Date.now() - startTime;
|
|
398
403
|
|
|
399
404
|
const cellStatusEvents: EvalStatusEvent[] = [];
|
|
405
|
+
const cellDisplayOutputs: EvalDisplayOutput[] = [];
|
|
400
406
|
let cellHasMarkdown = false;
|
|
401
407
|
for (const output of result.displayOutputs) {
|
|
402
408
|
if (output.type === "json") {
|
|
403
409
|
jsonOutputs.push(output.data);
|
|
410
|
+
cellDisplayOutputs.push(output);
|
|
404
411
|
}
|
|
405
412
|
if (output.type === "image") {
|
|
406
413
|
images.push({ type: "image", data: output.data, mimeType: output.mimeType });
|
|
414
|
+
cellDisplayOutputs.push(output);
|
|
407
415
|
}
|
|
408
416
|
if (output.type === "status") {
|
|
409
417
|
statusEvents.push(output.event);
|
|
@@ -414,7 +422,10 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
414
422
|
}
|
|
415
423
|
}
|
|
416
424
|
|
|
417
|
-
const
|
|
425
|
+
const stdoutTrimmed = result.output.trim();
|
|
426
|
+
const displayText = formatDisplayOutputsForText(cellDisplayOutputs);
|
|
427
|
+
const cellOutput =
|
|
428
|
+
stdoutTrimmed && displayText ? `${stdoutTrimmed}\n\n${displayText}` : stdoutTrimmed || displayText;
|
|
418
429
|
cellResult.output = cellOutput;
|
|
419
430
|
cellResult.exitCode = result.exitCode;
|
|
420
431
|
cellResult.durationMs = durationMs;
|
|
@@ -446,10 +457,11 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
446
457
|
pushUpdate();
|
|
447
458
|
const errorMsg = result.output || "Command aborted";
|
|
448
459
|
const combinedOutput = cellOutputs.join("\n\n");
|
|
460
|
+
const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
|
|
449
461
|
const outputText =
|
|
450
|
-
cells.length > 1
|
|
462
|
+
(cells.length > 1
|
|
451
463
|
? `${combinedOutput}\n\nCell ${i + 1} aborted: ${errorMsg}`
|
|
452
|
-
: combinedOutput || errorMsg;
|
|
464
|
+
: combinedOutput || errorMsg) + abortSuffix;
|
|
453
465
|
|
|
454
466
|
const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
|
|
455
467
|
const details: EvalToolDetails = {
|
|
@@ -457,14 +469,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
457
469
|
languages,
|
|
458
470
|
cells: cellResults,
|
|
459
471
|
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
460
|
-
images: images.length > 0 ? images : undefined,
|
|
461
472
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
462
473
|
isError: true,
|
|
463
474
|
};
|
|
464
475
|
if (notice) details.notice = notice;
|
|
465
476
|
|
|
466
477
|
return toolResult(details)
|
|
467
|
-
.text
|
|
478
|
+
.content([{ type: "text", text: outputText }, ...images])
|
|
468
479
|
.truncationFromSummary(summaryForMeta, { direction: "tail" })
|
|
469
480
|
.done();
|
|
470
481
|
}
|
|
@@ -473,12 +484,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
473
484
|
cellResult.status = "error";
|
|
474
485
|
pushUpdate();
|
|
475
486
|
const combinedOutput = cellOutputs.join("\n\n");
|
|
487
|
+
const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
|
|
476
488
|
const outputText =
|
|
477
|
-
cells.length > 1
|
|
489
|
+
(cells.length > 1
|
|
478
490
|
? `${combinedOutput}\n\nCell ${i + 1} failed (exit code ${result.exitCode}). Earlier cells succeeded—their state persists. Fix only cell ${i + 1}.`
|
|
479
491
|
: combinedOutput
|
|
480
492
|
? `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`
|
|
481
|
-
: `Command exited with code ${result.exitCode}
|
|
493
|
+
: `Command exited with code ${result.exitCode}`) + abortSuffix;
|
|
482
494
|
|
|
483
495
|
const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
|
|
484
496
|
const details: EvalToolDetails = {
|
|
@@ -486,14 +498,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
486
498
|
languages,
|
|
487
499
|
cells: cellResults,
|
|
488
500
|
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
489
|
-
images: images.length > 0 ? images : undefined,
|
|
490
501
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
491
502
|
isError: true,
|
|
492
503
|
};
|
|
493
504
|
if (notice) details.notice = notice;
|
|
494
505
|
|
|
495
506
|
return toolResult(details)
|
|
496
|
-
.text
|
|
507
|
+
.content([{ type: "text", text: outputText }, ...images])
|
|
497
508
|
.truncationFromSummary(summaryForMeta, { direction: "tail" })
|
|
498
509
|
.done();
|
|
499
510
|
}
|
|
@@ -503,8 +514,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
503
514
|
}
|
|
504
515
|
|
|
505
516
|
const combinedOutput = cellOutputs.join("\n\n");
|
|
517
|
+
const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
|
|
518
|
+
const hasImages = images.length > 0;
|
|
506
519
|
const outputText =
|
|
507
|
-
combinedOutput ||
|
|
520
|
+
(combinedOutput ||
|
|
521
|
+
(hasImages
|
|
522
|
+
? `(displayed ${images.length} image${images.length === 1 ? "" : "s"}; no text output)`
|
|
523
|
+
: "(no output)")) + abortSuffix;
|
|
508
524
|
const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
|
|
509
525
|
|
|
510
526
|
const details: EvalToolDetails = {
|
|
@@ -512,13 +528,12 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
512
528
|
languages,
|
|
513
529
|
cells: cellResults,
|
|
514
530
|
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
515
|
-
images: images.length > 0 ? images : undefined,
|
|
516
531
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
517
532
|
};
|
|
518
533
|
if (notice) details.notice = notice;
|
|
519
534
|
|
|
520
535
|
return toolResult(details)
|
|
521
|
-
.text
|
|
536
|
+
.content([{ type: "text", text: outputText }, ...images])
|
|
522
537
|
.truncationFromSummary(summaryForMeta, { direction: "tail" })
|
|
523
538
|
.done();
|
|
524
539
|
} finally {
|
package/src/tools/fetch.ts
CHANGED
|
@@ -1352,7 +1352,7 @@ export function renderReadUrlCall(
|
|
|
1352
1352
|
): Component {
|
|
1353
1353
|
const url = args.path ?? args.url ?? "";
|
|
1354
1354
|
const domain = getDomain(url);
|
|
1355
|
-
const path = truncate(url.replace(/^https?:\/\/[^/]+/, ""), 50, "
|
|
1355
|
+
const path = truncate(url.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
|
|
1356
1356
|
const description = `${domain}${path ? ` ${path}` : ""}`.trim();
|
|
1357
1357
|
const meta: string[] = [];
|
|
1358
1358
|
if (args.raw) meta.push("raw");
|
package/src/tools/gh.ts
CHANGED
|
@@ -260,6 +260,27 @@ const githubSchema = Type.Object({
|
|
|
260
260
|
examples: ["is:open label:bug"],
|
|
261
261
|
}),
|
|
262
262
|
),
|
|
263
|
+
since: Type.Optional(
|
|
264
|
+
Type.String({
|
|
265
|
+
description:
|
|
266
|
+
"lower-bound date for search_issues/search_prs/search_commits/search_repos. Accepts a relative duration (`<n><unit>` with unit `m`/`h`/`d`/`w`/`mo`/`y`, e.g. `3d`, `12h`, `2w`) or an ISO date (`YYYY-MM-DD`) / datetime. Translated to a `created:>=…` (or `committer-date:`/`pushed:`) qualifier; not supported by search_code.",
|
|
267
|
+
examples: ["3d", "2w", "2026-05-01"],
|
|
268
|
+
}),
|
|
269
|
+
),
|
|
270
|
+
until: Type.Optional(
|
|
271
|
+
Type.String({
|
|
272
|
+
description:
|
|
273
|
+
"upper-bound date in the same format as `since`. With both, builds a `field:since..until` range qualifier.",
|
|
274
|
+
examples: ["1d", "2026-05-09"],
|
|
275
|
+
}),
|
|
276
|
+
),
|
|
277
|
+
dateField: Type.Optional(
|
|
278
|
+
StringEnum(["created", "updated"], {
|
|
279
|
+
description:
|
|
280
|
+
"date field used by `since`/`until`. issues/prs: `created` (default) or `updated`. repos: `created` (default) or `updated` (mapped to GitHub's `pushed:`). commits: ignored — always uses `committer-date`.",
|
|
281
|
+
default: "created",
|
|
282
|
+
}),
|
|
283
|
+
),
|
|
263
284
|
limit: Type.Optional(
|
|
264
285
|
Type.Number({
|
|
265
286
|
description: "max results (search_issues, search_prs, search_code, search_commits, search_repos)",
|
|
@@ -686,6 +707,110 @@ const SEARCH_FIELDS_BY_COMMAND: Record<"issues" | "prs" | "code" | "commits" | "
|
|
|
686
707
|
repos: GH_SEARCH_REPOS_FIELDS,
|
|
687
708
|
};
|
|
688
709
|
|
|
710
|
+
const RELATIVE_DURATION_PATTERN = /^(\d+)\s*(m|h|d|w|mo|y)$/i;
|
|
711
|
+
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
712
|
+
const FIXED_UNIT_MS: Record<string, number> = {
|
|
713
|
+
m: 60_000,
|
|
714
|
+
h: 3_600_000,
|
|
715
|
+
d: 86_400_000,
|
|
716
|
+
w: 7 * 86_400_000,
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Resolve a search date bound to a GitHub-search-compatible literal. Returns
|
|
721
|
+
* either a `YYYY-MM-DD` date (relative durations and date-only inputs) or a
|
|
722
|
+
* full ISO 8601 datetime string (datetime inputs), so the caller can drop it
|
|
723
|
+
* straight into a qualifier like `created:>=<value>`.
|
|
724
|
+
*/
|
|
725
|
+
export function parseSearchDateBound(raw: string, now: Date = new Date()): string {
|
|
726
|
+
const trimmed = raw.trim();
|
|
727
|
+
if (!trimmed) {
|
|
728
|
+
throw new ToolError("date bound must not be empty");
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const relMatch = trimmed.match(RELATIVE_DURATION_PATTERN);
|
|
732
|
+
if (relMatch) {
|
|
733
|
+
const count = Number(relMatch[1]);
|
|
734
|
+
const unit = relMatch[2].toLowerCase();
|
|
735
|
+
const fixedMs = FIXED_UNIT_MS[unit];
|
|
736
|
+
let bound: Date;
|
|
737
|
+
if (fixedMs !== undefined) {
|
|
738
|
+
bound = new Date(now.getTime() - count * fixedMs);
|
|
739
|
+
} else {
|
|
740
|
+
bound = new Date(now);
|
|
741
|
+
if (unit === "mo") {
|
|
742
|
+
bound.setUTCMonth(bound.getUTCMonth() - count);
|
|
743
|
+
} else {
|
|
744
|
+
bound.setUTCFullYear(bound.getUTCFullYear() - count);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return bound.toISOString().slice(0, 10);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (ISO_DATE_PATTERN.test(trimmed)) {
|
|
751
|
+
return trimmed;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const parsedMs = Date.parse(trimmed);
|
|
755
|
+
if (!Number.isNaN(parsedMs)) {
|
|
756
|
+
return new Date(parsedMs).toISOString();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
throw new ToolError(
|
|
760
|
+
`invalid date bound: ${raw}. Expected a relative duration like "3d", "12h", "2w", an ISO date "YYYY-MM-DD", or an ISO datetime.`,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Build the GitHub-search qualifier (e.g. `created:>=2026-05-09`) for the
|
|
766
|
+
* provided bounds, or `undefined` if neither bound is set.
|
|
767
|
+
*/
|
|
768
|
+
export function buildSearchDateQualifier(
|
|
769
|
+
field: string,
|
|
770
|
+
since: string | undefined,
|
|
771
|
+
until: string | undefined,
|
|
772
|
+
now?: Date,
|
|
773
|
+
): string | undefined {
|
|
774
|
+
const sinceVal = since ? parseSearchDateBound(since, now) : undefined;
|
|
775
|
+
const untilVal = until ? parseSearchDateBound(until, now) : undefined;
|
|
776
|
+
if (sinceVal && untilVal) {
|
|
777
|
+
return `${field}:${sinceVal}..${untilVal}`;
|
|
778
|
+
}
|
|
779
|
+
if (sinceVal) {
|
|
780
|
+
return `${field}:>=${sinceVal}`;
|
|
781
|
+
}
|
|
782
|
+
if (untilVal) {
|
|
783
|
+
return `${field}:<=${untilVal}`;
|
|
784
|
+
}
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function resolveSearchDateField(
|
|
789
|
+
command: "issues" | "prs" | "commits" | "repos",
|
|
790
|
+
requested: "created" | "updated" | undefined,
|
|
791
|
+
): string {
|
|
792
|
+
if (command === "commits") {
|
|
793
|
+
return "committer-date";
|
|
794
|
+
}
|
|
795
|
+
const dateField = requested ?? "created";
|
|
796
|
+
if (command === "repos" && dateField === "updated") {
|
|
797
|
+
return "pushed";
|
|
798
|
+
}
|
|
799
|
+
return dateField;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function composeSearchQuery(parts: ReadonlyArray<string | undefined>): string {
|
|
803
|
+
const cleaned: string[] = [];
|
|
804
|
+
for (const part of parts) {
|
|
805
|
+
const trimmed = part?.trim();
|
|
806
|
+
if (trimmed) cleaned.push(trimmed);
|
|
807
|
+
}
|
|
808
|
+
if (cleaned.length === 0) {
|
|
809
|
+
throw new ToolError("query is required (or pass since/until to filter by date)");
|
|
810
|
+
}
|
|
811
|
+
return cleaned.join(" ");
|
|
812
|
+
}
|
|
813
|
+
|
|
689
814
|
function buildGhSearchArgs(
|
|
690
815
|
command: "issues" | "prs" | "code" | "commits" | "repos",
|
|
691
816
|
query: string,
|
|
@@ -2636,9 +2761,11 @@ async function executeSearchIssues(
|
|
|
2636
2761
|
params: GithubInput,
|
|
2637
2762
|
signal: AbortSignal | undefined,
|
|
2638
2763
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2639
|
-
const query = requireNonEmpty(params.query, "query");
|
|
2640
2764
|
const repo = normalizeOptionalString(params.repo);
|
|
2641
2765
|
const limit = resolveSearchLimit(params.limit);
|
|
2766
|
+
const dateField = resolveSearchDateField("issues", params.dateField);
|
|
2767
|
+
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2768
|
+
const query = composeSearchQuery([params.query, dateQualifier]);
|
|
2642
2769
|
const args = buildGhSearchArgs("issues", query, limit, repo);
|
|
2643
2770
|
|
|
2644
2771
|
const items = await git.github.json<GhSearchResult[]>(session.cwd, args, signal, {
|
|
@@ -2652,9 +2779,11 @@ async function executeSearchPrs(
|
|
|
2652
2779
|
params: GithubInput,
|
|
2653
2780
|
signal: AbortSignal | undefined,
|
|
2654
2781
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2655
|
-
const query = requireNonEmpty(params.query, "query");
|
|
2656
2782
|
const repo = normalizeOptionalString(params.repo);
|
|
2657
2783
|
const limit = resolveSearchLimit(params.limit);
|
|
2784
|
+
const dateField = resolveSearchDateField("prs", params.dateField);
|
|
2785
|
+
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2786
|
+
const query = composeSearchQuery([params.query, dateQualifier]);
|
|
2658
2787
|
const args = buildGhSearchArgs("prs", query, limit, repo);
|
|
2659
2788
|
|
|
2660
2789
|
const items = await git.github.json<GhSearchResult[]>(session.cwd, args, signal, {
|
|
@@ -2669,6 +2798,9 @@ async function executeSearchCode(
|
|
|
2669
2798
|
signal: AbortSignal | undefined,
|
|
2670
2799
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2671
2800
|
const query = requireNonEmpty(params.query, "query");
|
|
2801
|
+
if (params.since !== undefined || params.until !== undefined) {
|
|
2802
|
+
throw new ToolError("search_code does not support since/until; GitHub code search has no date qualifier.");
|
|
2803
|
+
}
|
|
2672
2804
|
const repo = normalizeOptionalString(params.repo);
|
|
2673
2805
|
const limit = resolveSearchLimit(params.limit);
|
|
2674
2806
|
const args = buildGhSearchArgs("code", query, limit, repo);
|
|
@@ -2684,9 +2816,11 @@ async function executeSearchCommits(
|
|
|
2684
2816
|
params: GithubInput,
|
|
2685
2817
|
signal: AbortSignal | undefined,
|
|
2686
2818
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2687
|
-
const query = requireNonEmpty(params.query, "query");
|
|
2688
2819
|
const repo = normalizeOptionalString(params.repo);
|
|
2689
2820
|
const limit = resolveSearchLimit(params.limit);
|
|
2821
|
+
const dateField = resolveSearchDateField("commits", params.dateField);
|
|
2822
|
+
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2823
|
+
const query = composeSearchQuery([params.query, dateQualifier]);
|
|
2690
2824
|
const args = buildGhSearchArgs("commits", query, limit, repo);
|
|
2691
2825
|
|
|
2692
2826
|
const items = await git.github.json<GhSearchCommitResult[]>(session.cwd, args, signal, {
|
|
@@ -2700,8 +2834,10 @@ async function executeSearchRepos(
|
|
|
2700
2834
|
params: GithubInput,
|
|
2701
2835
|
signal: AbortSignal | undefined,
|
|
2702
2836
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2703
|
-
const query = requireNonEmpty(params.query, "query");
|
|
2704
2837
|
const limit = resolveSearchLimit(params.limit);
|
|
2838
|
+
const dateField = resolveSearchDateField("repos", params.dateField);
|
|
2839
|
+
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2840
|
+
const query = composeSearchQuery([params.query, dateQualifier]);
|
|
2705
2841
|
const args = buildGhSearchArgs("repos", query, limit, undefined);
|
|
2706
2842
|
|
|
2707
2843
|
const items = await git.github.json<GhSearchRepoResult[]>(session.cwd, args, signal);
|
package/src/tools/index.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { $env, $flag, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import type { AsyncJobManager } from "../async";
|
|
5
4
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
6
5
|
import type { Settings } from "../config/settings";
|
|
7
6
|
import { EditTool } from "../edit";
|
|
8
7
|
import { checkPythonKernelAvailability } from "../eval/py/kernel";
|
|
9
8
|
import type { Skill } from "../extensibility/skills";
|
|
10
9
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
11
|
-
import type { InternalUrlRouter } from "../internal-urls";
|
|
12
10
|
import { LspTool } from "../lsp";
|
|
13
11
|
import type { PlanModeState } from "../plan-mode/state";
|
|
14
|
-
import type
|
|
12
|
+
import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
13
|
+
import type { ArtifactManager } from "../session/artifacts";
|
|
15
14
|
import type { CustomMessage } from "../session/messages";
|
|
16
15
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
17
16
|
import { TaskTool } from "../task";
|
|
@@ -159,6 +158,8 @@ export interface ToolSession {
|
|
|
159
158
|
agentRegistry?: AgentRegistry;
|
|
160
159
|
/** Get artifacts directory for artifact:// URLs */
|
|
161
160
|
getArtifactsDir?: () => string | null;
|
|
161
|
+
/** Get the ArtifactManager backing this session (shared across parent + subagents). */
|
|
162
|
+
getArtifactManager?: () => ArtifactManager | null;
|
|
162
163
|
/** Allocate a new artifact path and ID for session-scoped truncated output. */
|
|
163
164
|
allocateOutputArtifact?: (toolType: string) => Promise<{ id?: string; path?: string }>;
|
|
164
165
|
/** Get session spawns */
|
|
@@ -171,14 +172,8 @@ export interface ToolSession {
|
|
|
171
172
|
authStorage?: import("../session/auth-storage").AuthStorage;
|
|
172
173
|
/** Model registry for passing to subagents (avoids re-discovery) */
|
|
173
174
|
modelRegistry?: import("../config/model-registry").ModelRegistry;
|
|
174
|
-
/** MCP manager for proxying MCP calls through parent */
|
|
175
|
-
mcpManager?: import("../mcp/manager").MCPManager;
|
|
176
|
-
/** Internal URL router for protocols like agent://, skill://, and mcp:// */
|
|
177
|
-
internalRouter?: InternalUrlRouter;
|
|
178
175
|
/** Agent output manager for unique agent:// IDs across task invocations */
|
|
179
176
|
agentOutputManager?: AgentOutputManager;
|
|
180
|
-
/** Async background job manager for bash/task async execution */
|
|
181
|
-
asyncJobManager?: AsyncJobManager;
|
|
182
177
|
/** Settings instance for passing to subagents */
|
|
183
178
|
settings: Settings;
|
|
184
179
|
/** Plan mode state (if active) */
|
|
@@ -282,7 +277,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
282
277
|
browser: s => new BrowserTool(s),
|
|
283
278
|
checkpoint: CheckpointTool.createIf,
|
|
284
279
|
rewind: RewindTool.createIf,
|
|
285
|
-
task: TaskTool.create,
|
|
280
|
+
task: s => TaskTool.create(s),
|
|
286
281
|
job: JobTool.createIf,
|
|
287
282
|
recipe: RecipeTool.createIf,
|
|
288
283
|
irc: IrcTool.createIf,
|
|
@@ -443,7 +438,13 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
443
438
|
if (name === "calc") return session.settings.get("calc.enabled");
|
|
444
439
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
445
440
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
446
|
-
if (name === "irc")
|
|
441
|
+
if (name === "irc") {
|
|
442
|
+
if (!session.settings.get("irc.enabled")) return false;
|
|
443
|
+
// Main agent only needs `irc` when subagents may run concurrently (async).
|
|
444
|
+
// In sync mode main blocks on `task`, so peer messaging from main is dead weight.
|
|
445
|
+
if (!session.settings.get("async.enabled") && session.getAgentId?.() === MAIN_AGENT_ID) return false;
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
447
448
|
if (name === "recipe") return session.settings.get("recipe.enabled");
|
|
448
449
|
if (name === "retain" || name === "recall" || name === "reflect") {
|
|
449
450
|
return session.settings.get("memory.backend") === "hindsight";
|
package/src/tools/job.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
3
3
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { type Static, Type } from "@sinclair/typebox";
|
|
6
|
-
import { isBackgroundJobSupportEnabled } from "../async";
|
|
6
|
+
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
|
|
7
7
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
8
|
import type { Theme } from "../modes/theme/theme";
|
|
9
9
|
import jobDescription from "../prompts/tools/job.md" with { type: "text" };
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type ToolUIColor,
|
|
21
21
|
type ToolUIStatus,
|
|
22
22
|
} from "./render-utils";
|
|
23
|
+
import { ToolError } from "./tool-errors";
|
|
23
24
|
|
|
24
25
|
const jobSchema = Type.Object({
|
|
25
26
|
poll: Type.Optional(
|
|
@@ -34,6 +35,12 @@ const jobSchema = Type.Object({
|
|
|
34
35
|
examples: [["job-1234"]],
|
|
35
36
|
}),
|
|
36
37
|
),
|
|
38
|
+
list: Type.Optional(
|
|
39
|
+
Type.Boolean({
|
|
40
|
+
description:
|
|
41
|
+
"Return an immediate snapshot of every job spawned by this agent (running + completed within retention). Read-only \u2014 cannot be combined with `poll` or `cancel`.",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
37
44
|
});
|
|
38
45
|
|
|
39
46
|
type JobParams = Static<typeof jobSchema>;
|
|
@@ -97,7 +104,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
97
104
|
onUpdate?: AgentToolUpdateCallback<JobToolDetails>,
|
|
98
105
|
_context?: AgentToolContext,
|
|
99
106
|
): Promise<AgentToolResult<JobToolDetails>> {
|
|
100
|
-
const manager =
|
|
107
|
+
const manager = AsyncJobManager.instance();
|
|
101
108
|
if (!manager) {
|
|
102
109
|
return {
|
|
103
110
|
content: [{ type: "text", text: "Async execution is disabled; no background jobs are available." }],
|
|
@@ -105,11 +112,24 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
105
112
|
};
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
// Scope every visible operation to the calling agent. Tests / SDK
|
|
116
|
+
// consumers without an agent id see everything (legacy behavior).
|
|
117
|
+
const ownerId = this.session.getAgentId?.() ?? undefined;
|
|
118
|
+
const ownerFilter = ownerId ? { ownerId } : undefined;
|
|
119
|
+
|
|
120
|
+
// `list` is a read-only snapshot mode. Replaces the legacy `jobs://` URL.
|
|
121
|
+
if (params.list) {
|
|
122
|
+
if (params.cancel?.length || params.poll?.length) {
|
|
123
|
+
throw new ToolError("`list` cannot be combined with `poll` or `cancel`.");
|
|
124
|
+
}
|
|
125
|
+
return this.#buildResult(manager, manager.getAllJobs(ownerFilter), []);
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
const cancelIds = params.cancel ?? [];
|
|
109
129
|
const cancelOutcomes: CancelOutcome[] = [];
|
|
110
130
|
for (const id of cancelIds) {
|
|
111
131
|
const existing = manager.getJob(id);
|
|
112
|
-
if (!existing) {
|
|
132
|
+
if (!existing || (ownerId && existing.ownerId !== ownerId)) {
|
|
113
133
|
cancelOutcomes.push({ id, status: "not_found", message: `Background job not found: ${id}` });
|
|
114
134
|
continue;
|
|
115
135
|
}
|
|
@@ -121,7 +141,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
121
141
|
});
|
|
122
142
|
continue;
|
|
123
143
|
}
|
|
124
|
-
const cancelled = manager.cancel(id);
|
|
144
|
+
const cancelled = manager.cancel(id, ownerFilter);
|
|
125
145
|
cancelOutcomes.push(
|
|
126
146
|
cancelled
|
|
127
147
|
? { id, status: "cancelled", message: `Cancelled background job ${id}.` }
|
|
@@ -130,11 +150,11 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
const requestedPollIds = params.poll;
|
|
133
|
-
// If only `cancel` was provided (no `poll`), don't wait
|
|
153
|
+
// If only `cancel` was provided (no `poll`), don't wait \u2014 return immediately.
|
|
134
154
|
const shouldPoll = requestedPollIds !== undefined || cancelIds.length === 0;
|
|
135
155
|
|
|
136
156
|
if (!shouldPoll) {
|
|
137
|
-
const cancelledJobs =
|
|
157
|
+
const cancelledJobs = this.#visibleJobs(manager, cancelIds, ownerId);
|
|
138
158
|
return this.#buildResult(manager, cancelledJobs, cancelOutcomes);
|
|
139
159
|
}
|
|
140
160
|
|
|
@@ -142,12 +162,12 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
142
162
|
// - If `poll` was passed explicitly, watch exactly those (filtered to existing).
|
|
143
163
|
// - If `poll` was omitted (and so was `cancel`), default to all running jobs.
|
|
144
164
|
const jobsToWatch = requestedPollIds
|
|
145
|
-
?
|
|
146
|
-
: manager.getRunningJobs();
|
|
165
|
+
? this.#visibleJobs(manager, requestedPollIds, ownerId)
|
|
166
|
+
: manager.getRunningJobs(ownerFilter);
|
|
147
167
|
|
|
148
168
|
if (jobsToWatch.length === 0) {
|
|
149
169
|
if (cancelOutcomes.length > 0) {
|
|
150
|
-
const cancelledJobs =
|
|
170
|
+
const cancelledJobs = this.#visibleJobs(manager, cancelIds, ownerId);
|
|
151
171
|
return this.#buildResult(manager, cancelledJobs, cancelOutcomes);
|
|
152
172
|
}
|
|
153
173
|
const message = requestedPollIds?.length
|
|
@@ -176,7 +196,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
176
196
|
const watchedJobIds = runningJobs.map(job => job.id);
|
|
177
197
|
manager.watchJobs(watchedJobIds);
|
|
178
198
|
|
|
179
|
-
const cancelledJobs =
|
|
199
|
+
const cancelledJobs = this.#visibleJobs(manager, cancelIds, ownerId);
|
|
180
200
|
const allTrackedJobs = [...cancelledJobs, ...jobsToWatch];
|
|
181
201
|
|
|
182
202
|
const PROGRESS_INTERVAL_MS = 500;
|
|
@@ -219,6 +239,22 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
219
239
|
return this.#buildResult(manager, allTrackedJobs, cancelOutcomes);
|
|
220
240
|
}
|
|
221
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Resolve a list of job ids to job records visible to the calling agent.
|
|
244
|
+
* Drops missing ids and ids owned by other agents, so cross-agent inspection
|
|
245
|
+
* via the `job` tool is impossible.
|
|
246
|
+
*/
|
|
247
|
+
#visibleJobs(manager: AsyncJobManager, ids: string[], ownerId: string | undefined): AsyncJob[] {
|
|
248
|
+
const out: AsyncJob[] = [];
|
|
249
|
+
for (const id of ids) {
|
|
250
|
+
const job = manager.getJob(id);
|
|
251
|
+
if (!job) continue;
|
|
252
|
+
if (ownerId && job.ownerId !== ownerId) continue;
|
|
253
|
+
out.push(job);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
222
258
|
#snapshotJobs(
|
|
223
259
|
jobs: {
|
|
224
260
|
id: string;
|
|
@@ -232,7 +268,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
232
268
|
): JobSnapshot[] {
|
|
233
269
|
const now = Date.now();
|
|
234
270
|
return jobs.map(j => {
|
|
235
|
-
const current =
|
|
271
|
+
const current = AsyncJobManager.instance()?.getJob(j.id);
|
|
236
272
|
const latest = current ?? j;
|
|
237
273
|
return {
|
|
238
274
|
id: latest.id,
|
|
@@ -247,7 +283,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
247
283
|
}
|
|
248
284
|
|
|
249
285
|
#buildResult(
|
|
250
|
-
manager:
|
|
286
|
+
manager: AsyncJobManager,
|
|
251
287
|
jobs: {
|
|
252
288
|
id: string;
|
|
253
289
|
type: "bash" | "task";
|