@oh-my-pi/pi-coding-agent 14.9.3 โ 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 +44 -0
- package/package.json +7 -7
- 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/settings-schema.ts +1 -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/js/context-manager.ts +1 -38
- package/src/eval/js/prelude.txt +0 -2
- 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/export/html/template.css +12 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +20 -2
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/internal-urls/agent-protocol.ts +63 -52
- package/src/internal-urls/artifact-protocol.ts +51 -51
- package/src/internal-urls/docs-index.generated.ts +33 -1
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +49 -7
- package/src/internal-urls/mcp-protocol.ts +2 -8
- package/src/internal-urls/memory-protocol.ts +89 -59
- package/src/internal-urls/router.ts +38 -22
- package/src/internal-urls/rule-protocol.ts +2 -20
- package/src/internal-urls/skill-protocol.ts +4 -27
- 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/project-prompt.md +10 -2
- package/src/prompts/system/subagent-system-prompt.md +8 -8
- package/src/prompts/system/system-prompt.md +13 -7
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -10
- package/src/prompts/tools/eval.md +1 -3
- 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 +58 -20
- 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 +0 -5
- package/src/task/executor.ts +14 -2
- package/src/task/index.ts +19 -5
- 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 +15 -9
- package/src/tools/browser/tab-supervisor.ts +12 -2
- package/src/tools/eval.ts +48 -10
- 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/read.ts +5 -4
- package/src/tools/search.ts +3 -2
- package/src/tools/todo-write.ts +1 -1
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/internal-urls/jobs-protocol.ts +0 -120
- package/src/prompts/system/now-prompt.md +0 -7
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";
|
package/src/tools/read.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { getFileReadCache } from "../edit/file-read-cache";
|
|
|
12
12
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
13
13
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
14
|
import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../hashline/hash";
|
|
15
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
15
16
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
16
17
|
import type { InternalUrl } from "../internal-urls/types";
|
|
17
18
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
@@ -431,7 +432,7 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
|
|
|
431
432
|
const readSchema = Type.Object({
|
|
432
433
|
path: Type.String({
|
|
433
434
|
description: 'path or url; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
|
|
434
|
-
examples: ["src/foo.ts", "src/foo.ts:50-100", "https://example.com
|
|
435
|
+
examples: ["src/foo.ts", "src/foo.ts:50-100", "https://example.com/:1-40"],
|
|
435
436
|
}),
|
|
436
437
|
});
|
|
437
438
|
|
|
@@ -1181,8 +1182,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1181
1182
|
|
|
1182
1183
|
// Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://)
|
|
1183
1184
|
const internalTarget = splitPathAndSel(readPath);
|
|
1184
|
-
const internalRouter =
|
|
1185
|
-
if (internalRouter
|
|
1185
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
1186
|
+
if (internalRouter.canHandle(internalTarget.path)) {
|
|
1186
1187
|
const parsed = parseSel(internalTarget.sel);
|
|
1187
1188
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1188
1189
|
return this.#handleInternalUrl(internalTarget.path, offset, limit, { raw: isRawSelector(parsed) });
|
|
@@ -1551,7 +1552,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1551
1552
|
limit?: number,
|
|
1552
1553
|
options?: { raw?: boolean },
|
|
1553
1554
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1554
|
-
const internalRouter =
|
|
1555
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
1555
1556
|
|
|
1556
1557
|
// Check if URL has query extraction (agent:// only).
|
|
1557
1558
|
// Use parseInternalUrl which handles colons in host (namespaced skills).
|
package/src/tools/search.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
9
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
11
12
|
import type { Theme } from "../modes/theme/theme";
|
|
12
13
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
13
14
|
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
@@ -131,14 +132,14 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
131
132
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
132
133
|
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
133
134
|
}
|
|
134
|
-
const internalRouter =
|
|
135
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
135
136
|
const resolvedPathInputs: string[] = [];
|
|
136
137
|
// Absolute filesystem paths whose source is immutable (e.g. artifact://,
|
|
137
138
|
// pi://, skill://). Hashline anchors are suppressed for these on a
|
|
138
139
|
// per-file basis, leaving editable mixed-in files untouched.
|
|
139
140
|
const immutableSourcePaths = new Set<string>();
|
|
140
141
|
for (const rawPath of rawPaths) {
|
|
141
|
-
if (!internalRouter
|
|
142
|
+
if (!internalRouter.canHandle(rawPath)) {
|
|
142
143
|
resolvedPathInputs.push(rawPath);
|
|
143
144
|
continue;
|
|
144
145
|
}
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -631,7 +631,7 @@ function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme): string[] {
|
|
|
631
631
|
for (const task of phase.tasks) {
|
|
632
632
|
if (task.status !== "in_progress" || !task.notes || task.notes.length === 0) continue;
|
|
633
633
|
const bar = uiTheme.fg("dim", uiTheme.tree.vertical);
|
|
634
|
-
const title = uiTheme.fg("dim", chalk.italic(
|
|
634
|
+
const title = uiTheme.fg("dim", chalk.italic(`ยง notes โ ${task.content}`));
|
|
635
635
|
lines.push("");
|
|
636
636
|
lines.push(` ${title}`);
|
|
637
637
|
for (let j = 0; j < task.notes.length; j++) {
|
|
@@ -273,7 +273,7 @@ export const handleMastodon: SpecialHandler = async (
|
|
|
273
273
|
md += `### ${formatDate(status.created_at)}\n\n`;
|
|
274
274
|
const content = await htmlToBasicMarkdown(status.content);
|
|
275
275
|
md += `${content}\n\n`;
|
|
276
|
-
md +=
|
|
276
|
+
md += `๐ฌ ${status.replies_count} ยท ๐ ${status.reblogs_count} ยท โญ ${status.favourites_count}\n\n`;
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
}
|
|
@@ -32,19 +32,19 @@ interface RepologyPackage {
|
|
|
32
32
|
function statusIndicator(status: string): string {
|
|
33
33
|
switch (status) {
|
|
34
34
|
case "newest":
|
|
35
|
-
return "
|
|
35
|
+
return "โ
"; // green check
|
|
36
36
|
case "devel":
|
|
37
|
-
return "
|
|
37
|
+
return "๐ง"; // construction
|
|
38
38
|
case "unique":
|
|
39
|
-
return "
|
|
39
|
+
return "๐ต"; // blue circle
|
|
40
40
|
case "outdated":
|
|
41
|
-
return "
|
|
41
|
+
return "๐ด"; // red circle
|
|
42
42
|
case "legacy":
|
|
43
|
-
return "
|
|
43
|
+
return "โ \uFE0F"; // warning
|
|
44
44
|
case "rolling":
|
|
45
|
-
return "
|
|
45
|
+
return "๐"; // arrows
|
|
46
46
|
default:
|
|
47
|
-
return "
|
|
47
|
+
return "โ"; // minus
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import type { AsyncJobManager } from "../async";
|
|
2
|
-
import { formatDuration } from "../tools/render-utils";
|
|
3
|
-
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
4
|
-
|
|
5
|
-
export interface JobsProtocolOptions {
|
|
6
|
-
getAsyncJobManager: () => AsyncJobManager | undefined;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function formatJobTime(startTime: number): string {
|
|
10
|
-
return new Date(startTime).toISOString();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function formatJobDuration(startTime: number): string {
|
|
14
|
-
return formatDuration(Math.max(0, Date.now() - startTime));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeJobId(url: InternalUrl): string {
|
|
18
|
-
const host = url.rawHost || url.hostname;
|
|
19
|
-
const pathname = (url.rawPathname ?? url.pathname).replace(/^\/+/, "").trim();
|
|
20
|
-
if (host && pathname) return `${host}/${pathname}`;
|
|
21
|
-
if (host) return host;
|
|
22
|
-
return pathname;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class JobsProtocolHandler implements ProtocolHandler {
|
|
26
|
-
readonly scheme = "jobs";
|
|
27
|
-
readonly immutable = true;
|
|
28
|
-
|
|
29
|
-
constructor(private readonly options: JobsProtocolOptions) {}
|
|
30
|
-
|
|
31
|
-
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
32
|
-
const manager = this.options.getAsyncJobManager();
|
|
33
|
-
if (!manager) {
|
|
34
|
-
const content =
|
|
35
|
-
"# Jobs\n\nBackground job support is disabled. Enable `async.enabled` or `bash.autoBackground.enabled` to use jobs://.";
|
|
36
|
-
return {
|
|
37
|
-
url: url.href,
|
|
38
|
-
content,
|
|
39
|
-
contentType: "text/markdown",
|
|
40
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const jobId = normalizeJobId(url);
|
|
45
|
-
if (!jobId) {
|
|
46
|
-
return this.#listJobs(url, manager);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return this.#getJob(url, manager, jobId);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
#listJobs(url: InternalUrl, manager: AsyncJobManager): InternalResource {
|
|
53
|
-
const jobs = manager.getAllJobs();
|
|
54
|
-
const running = jobs.filter(job => job.status === "running").sort((a, b) => a.startTime - b.startTime);
|
|
55
|
-
const done = jobs.filter(job => job.status !== "running").sort((a, b) => b.startTime - a.startTime);
|
|
56
|
-
const ordered = [...running, ...done];
|
|
57
|
-
|
|
58
|
-
if (ordered.length === 0) {
|
|
59
|
-
const content = "# Jobs\n\nNo background jobs found.";
|
|
60
|
-
return {
|
|
61
|
-
url: url.href,
|
|
62
|
-
content,
|
|
63
|
-
contentType: "text/markdown",
|
|
64
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const lines = ordered.map(job => {
|
|
69
|
-
return `- \`${job.id}\` [${job.type}] **${job.status}** โ ${job.label} \n started: ${formatJobTime(job.startTime)} ยท duration: ${formatJobDuration(job.startTime)}`;
|
|
70
|
-
});
|
|
71
|
-
const content = `# Jobs\n\n${ordered.length} job${ordered.length === 1 ? "" : "s"}\n\n${lines.join("\n")}`;
|
|
72
|
-
return {
|
|
73
|
-
url: url.href,
|
|
74
|
-
content,
|
|
75
|
-
contentType: "text/markdown",
|
|
76
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
#getJob(url: InternalUrl, manager: AsyncJobManager, jobId: string): InternalResource {
|
|
81
|
-
const job = manager.getJob(jobId);
|
|
82
|
-
if (!job) {
|
|
83
|
-
const content = `# Job Not Found\n\n404: No async job found with id \`${jobId}\`.`;
|
|
84
|
-
return {
|
|
85
|
-
url: url.href,
|
|
86
|
-
content,
|
|
87
|
-
contentType: "text/markdown",
|
|
88
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const sections = [
|
|
93
|
-
`# Job ${job.id}`,
|
|
94
|
-
"",
|
|
95
|
-
`- type: ${job.type}`,
|
|
96
|
-
`- status: ${job.status}`,
|
|
97
|
-
`- label: ${job.label}`,
|
|
98
|
-
`- start: ${formatJobTime(job.startTime)}`,
|
|
99
|
-
`- duration: ${formatJobDuration(job.startTime)}`,
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
if (job.status === "completed" && job.resultText) {
|
|
103
|
-
sections.push("", "## Result", "", "```", job.resultText, "```");
|
|
104
|
-
}
|
|
105
|
-
if (job.status === "failed" && job.errorText) {
|
|
106
|
-
sections.push("", "## Error", "", "```", job.errorText, "```");
|
|
107
|
-
}
|
|
108
|
-
if (job.status === "cancelled" && job.errorText) {
|
|
109
|
-
sections.push("", "## Cancellation", "", "```", job.errorText, "```");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const content = sections.join("\n");
|
|
113
|
-
return {
|
|
114
|
-
url: url.href,
|
|
115
|
-
content,
|
|
116
|
-
contentType: "text/markdown",
|
|
117
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
Today is {{date}}, and the current working directory is '{{cwd}}'.
|
|
2
|
-
|
|
3
|
-
<critical>
|
|
4
|
-
- Each response **MUST** advance the task. There is no stopping condition other than completion.
|
|
5
|
-
- You **MUST** default to informed action; do not ask for confirmation when tools or repo context can answer.
|
|
6
|
-
- You **MUST** verify the effect of significant behavioral changes before yielding: run the specific test, command, or scenario that covers your change.
|
|
7
|
-
</critical>
|