@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/ssh/sshfs-mount.ts
CHANGED
|
@@ -2,7 +2,12 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { $which, getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { $ } from "bun";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getControlDir,
|
|
7
|
+
getControlPathTemplate,
|
|
8
|
+
type SSHConnectionTarget,
|
|
9
|
+
supportsSshControlMaster,
|
|
10
|
+
} from "./connection-manager";
|
|
6
11
|
import { buildSshTarget, sanitizeHostName } from "./utils";
|
|
7
12
|
|
|
8
13
|
const REMOTE_DIR = getRemoteDir();
|
|
@@ -40,14 +45,12 @@ function buildSshfsArgs(host: SSHConnectionTarget): string[] {
|
|
|
40
45
|
"BatchMode=yes",
|
|
41
46
|
"-o",
|
|
42
47
|
"StrictHostKeyChecking=accept-new",
|
|
43
|
-
"-o",
|
|
44
|
-
"ControlMaster=auto",
|
|
45
|
-
"-o",
|
|
46
|
-
`ControlPath=${CONTROL_PATH}`,
|
|
47
|
-
"-o",
|
|
48
|
-
"ControlPersist=3600",
|
|
49
48
|
];
|
|
50
49
|
|
|
50
|
+
if (supportsSshControlMaster()) {
|
|
51
|
+
args.push("-o", "ControlMaster=auto", "-o", `ControlPath=${CONTROL_PATH}`, "-o", "ControlPersist=3600");
|
|
52
|
+
}
|
|
53
|
+
|
|
51
54
|
if (host.port) {
|
|
52
55
|
args.push("-p", String(host.port));
|
|
53
56
|
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -12,7 +12,6 @@ import type { SkillsSettings } from "./config/settings";
|
|
|
12
12
|
import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
|
|
13
13
|
import { loadSkills, type Skill } from "./extensibility/skills";
|
|
14
14
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
15
|
-
import nowPromptTemplate from "./prompts/system/now-prompt.md" with { type: "text" };
|
|
16
15
|
import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
|
|
17
16
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
18
17
|
import { shortenPath } from "./tools/render-utils";
|
|
@@ -575,10 +574,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
575
574
|
if (projectPrompt) {
|
|
576
575
|
systemPrompt.push(projectPrompt);
|
|
577
576
|
}
|
|
578
|
-
const nowPrompt = prompt.render(nowPromptTemplate, data).trim();
|
|
579
|
-
if (nowPrompt) {
|
|
580
|
-
systemPrompt.push(nowPrompt);
|
|
581
|
-
}
|
|
582
577
|
|
|
583
578
|
return { systemPrompt };
|
|
584
579
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -26,9 +26,11 @@ import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md
|
|
|
26
26
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
27
27
|
import { createAgentSession, discoverAuthStorage } from "../sdk";
|
|
28
28
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
29
|
+
import type { ArtifactManager } from "../session/artifacts";
|
|
29
30
|
import type { AuthStorage } from "../session/auth-storage";
|
|
30
31
|
import { SessionManager } from "../session/session-manager";
|
|
31
|
-
import {
|
|
32
|
+
import { truncateTail } from "../session/streaming-output";
|
|
33
|
+
import type { ContextFileEntry } from "../tools";
|
|
32
34
|
import { jtdToJsonSchema, normalizeSchema } from "../tools/jtd-to-json-schema";
|
|
33
35
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
34
36
|
import type { EventBus } from "../utils/event-bus";
|
|
@@ -172,6 +174,12 @@ export interface ExecutorOptions {
|
|
|
172
174
|
settings?: Settings;
|
|
173
175
|
/** Override local:// protocol options so subagent shares parent's local:// root */
|
|
174
176
|
localProtocolOptions?: LocalProtocolOptions;
|
|
177
|
+
/**
|
|
178
|
+
* Parent session's ArtifactManager. Subagent adopts it so artifact IDs are
|
|
179
|
+
* unique across the whole agent tree and all artifacts land in the parent's
|
|
180
|
+
* artifacts directory (no per-subagent subdir).
|
|
181
|
+
*/
|
|
182
|
+
parentArtifactManager?: ArtifactManager;
|
|
175
183
|
parentHindsightSessionState?: HindsightSessionState;
|
|
176
184
|
}
|
|
177
185
|
|
|
@@ -563,6 +571,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
563
571
|
|
|
564
572
|
const lspEnabled = enableLsp ?? true;
|
|
565
573
|
const ircEnabled = subagentSettings.get("irc.enabled") === true;
|
|
574
|
+
const contextFileForPrompt = ircEnabled ? undefined : options.contextFile;
|
|
566
575
|
const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
|
|
567
576
|
|
|
568
577
|
const outputChunks: string[] = [];
|
|
@@ -975,6 +984,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
975
984
|
const sessionManager = sessionFile
|
|
976
985
|
? await SessionManager.open(sessionFile)
|
|
977
986
|
: SessionManager.inMemory(worktree ?? cwd);
|
|
987
|
+
if (options.parentArtifactManager) {
|
|
988
|
+
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
989
|
+
}
|
|
978
990
|
|
|
979
991
|
const mcpProxyTools = options.mcpManager ? createMCPProxyTools(options.mcpManager) : [];
|
|
980
992
|
const enableMCP = !options.mcpManager;
|
|
@@ -1001,7 +1013,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1001
1013
|
context: options.context?.trim() ?? "",
|
|
1002
1014
|
worktree: worktree ?? "",
|
|
1003
1015
|
outputSchema: normalizedOutputSchema,
|
|
1004
|
-
contextFile:
|
|
1016
|
+
contextFile: contextFileForPrompt,
|
|
1005
1017
|
ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
|
|
1006
1018
|
ircSelfId: ircEnabled ? id : "",
|
|
1007
1019
|
});
|
package/src/task/index.ts
CHANGED
|
@@ -20,7 +20,9 @@ import type { Usage } from "@oh-my-pi/pi-ai";
|
|
|
20
20
|
import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
21
21
|
import type { TSchema } from "@sinclair/typebox";
|
|
22
22
|
import type { ToolSession } from "..";
|
|
23
|
+
import { AsyncJobManager } from "../async";
|
|
23
24
|
import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
25
|
+
import { MCPManager } from "../mcp/manager";
|
|
24
26
|
import type { Theme } from "../modes/theme/theme";
|
|
25
27
|
import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
|
|
26
28
|
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
|
|
@@ -141,6 +143,7 @@ function renderDescription(
|
|
|
141
143
|
asyncEnabled: boolean,
|
|
142
144
|
disabledAgents: string[],
|
|
143
145
|
simpleMode: TaskSimpleMode,
|
|
146
|
+
ircEnabled: boolean,
|
|
144
147
|
): string {
|
|
145
148
|
const filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
|
|
146
149
|
const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
|
|
@@ -151,6 +154,7 @@ function renderDescription(
|
|
|
151
154
|
asyncEnabled,
|
|
152
155
|
contextEnabled,
|
|
153
156
|
customSchemaEnabled,
|
|
157
|
+
ircEnabled,
|
|
154
158
|
defaultMode: simpleMode === "default",
|
|
155
159
|
schemaFreeMode: simpleMode === "schema-free",
|
|
156
160
|
independentMode: simpleMode === "independent",
|
|
@@ -229,6 +233,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
229
233
|
this.session.settings.get("async.enabled"),
|
|
230
234
|
disabledAgents,
|
|
231
235
|
this.#getTaskSimpleMode(),
|
|
236
|
+
this.session.settings.get("irc.enabled") === true,
|
|
232
237
|
);
|
|
233
238
|
}
|
|
234
239
|
private constructor(
|
|
@@ -270,7 +275,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
270
275
|
return this.#executeSync(_toolCallId, params, signal, onUpdate);
|
|
271
276
|
}
|
|
272
277
|
|
|
273
|
-
const manager =
|
|
278
|
+
const manager = AsyncJobManager.instance();
|
|
274
279
|
if (!manager) {
|
|
275
280
|
return {
|
|
276
281
|
content: [{ type: "text", text: "Async execution is enabled but no async job manager is available." }],
|
|
@@ -444,6 +449,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
444
449
|
},
|
|
445
450
|
{
|
|
446
451
|
id: label,
|
|
452
|
+
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
447
453
|
onProgress: (text, details) => {
|
|
448
454
|
const progressDetails =
|
|
449
455
|
(details as TaskToolDetails | undefined) ??
|
|
@@ -729,6 +735,10 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
729
735
|
getSessionId: this.session.getSessionId ?? (() => null),
|
|
730
736
|
};
|
|
731
737
|
|
|
738
|
+
// Subagents adopt the parent's ArtifactManager so artifact IDs are unique
|
|
739
|
+
// across the whole tree and outputs land flat in the parent's dir.
|
|
740
|
+
const parentArtifactManager = this.session.getArtifactManager?.() ?? undefined;
|
|
741
|
+
|
|
732
742
|
// Initialize progress tracking
|
|
733
743
|
const progressMap = new Map<number, AgentProgress>();
|
|
734
744
|
|
|
@@ -785,9 +795,11 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
785
795
|
};
|
|
786
796
|
}
|
|
787
797
|
|
|
788
|
-
// Write parent conversation context for subagents
|
|
798
|
+
// Write parent conversation context for subagents. When IRC is available,
|
|
799
|
+
// subagents should ask live peers instead of reading a stale markdown dump.
|
|
789
800
|
await fs.mkdir(effectiveArtifactsDir, { recursive: true });
|
|
790
|
-
const
|
|
801
|
+
const shouldWriteConversationContext = this.session.settings.get("irc.enabled") !== true;
|
|
802
|
+
const compactContext = shouldWriteConversationContext ? this.session.getCompactContext?.() : undefined;
|
|
791
803
|
let contextFilePath: string | undefined;
|
|
792
804
|
if (compactContext) {
|
|
793
805
|
contextFilePath = path.join(effectiveArtifactsDir, "context.md");
|
|
@@ -867,12 +879,13 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
867
879
|
authStorage: this.session.authStorage,
|
|
868
880
|
modelRegistry: this.session.modelRegistry,
|
|
869
881
|
settings: this.session.settings,
|
|
870
|
-
mcpManager:
|
|
882
|
+
mcpManager: MCPManager.instance(),
|
|
871
883
|
contextFiles,
|
|
872
884
|
skills: availableSkills,
|
|
873
885
|
workspaceTree: this.session.workspaceTree,
|
|
874
886
|
promptTemplates,
|
|
875
887
|
localProtocolOptions,
|
|
888
|
+
parentArtifactManager,
|
|
876
889
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
877
890
|
});
|
|
878
891
|
}
|
|
@@ -925,12 +938,13 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
925
938
|
authStorage: this.session.authStorage,
|
|
926
939
|
modelRegistry: this.session.modelRegistry,
|
|
927
940
|
settings: this.session.settings,
|
|
928
|
-
mcpManager:
|
|
941
|
+
mcpManager: MCPManager.instance(),
|
|
929
942
|
contextFiles,
|
|
930
943
|
skills: availableSkills,
|
|
931
944
|
workspaceTree: this.session.workspaceTree,
|
|
932
945
|
promptTemplates,
|
|
933
946
|
localProtocolOptions,
|
|
947
|
+
parentArtifactManager,
|
|
934
948
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
935
949
|
});
|
|
936
950
|
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
@@ -89,6 +89,7 @@ export interface DiscoverableMCPSearchResult {
|
|
|
89
89
|
|
|
90
90
|
const BM25_K1 = 1.2;
|
|
91
91
|
const BM25_B = 0.75;
|
|
92
|
+
const BM25_DELTA = 1.0;
|
|
92
93
|
const FIELD_WEIGHTS = {
|
|
93
94
|
name: 6,
|
|
94
95
|
label: 4,
|
|
@@ -112,13 +113,24 @@ function getSchemaPropertyKeys(parameters: unknown): string[] {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
function tokenize(value: string): string[] {
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
return (
|
|
117
|
+
value
|
|
118
|
+
.normalize("NFKD")
|
|
119
|
+
// Drop combining marks (accents) so "café" → "cafe".
|
|
120
|
+
.replace(/\p{M}+/gu, "")
|
|
121
|
+
// Split ACRONYMBoundary: "MCPTool" → "MCP Tool".
|
|
122
|
+
.replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, "$1 $2")
|
|
123
|
+
// Split camelCase / digit→letter: "fooBar" → "foo Bar", "v2Beta" → "v2 Beta".
|
|
124
|
+
.replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, "$1 $2")
|
|
125
|
+
// Everything that isn't a letter or digit becomes a separator. This subsumes markdown
|
|
126
|
+
// punctuation (`|*_`#-~>[]()`), box-drawing glyphs (─│┌), em/en dashes, smart quotes,
|
|
127
|
+
// zero-width spaces, NBSPs, etc.
|
|
128
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
129
|
+
.toLowerCase()
|
|
130
|
+
.trim()
|
|
131
|
+
.split(/\s+/)
|
|
132
|
+
.filter(token => token.length > 0)
|
|
133
|
+
);
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
function addWeightedTokens(termFrequencies: Map<string, number>, value: string | undefined, weight: number): void {
|
|
@@ -274,7 +286,8 @@ export function searchDiscoverableTools(
|
|
|
274
286
|
const documentFrequency = index.documentFrequencies.get(token) ?? 0;
|
|
275
287
|
const idf = Math.log(1 + (index.documents.length - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
276
288
|
const normalization = BM25_K1 * (1 - BM25_B + BM25_B * (document.length / index.averageLength));
|
|
277
|
-
score +=
|
|
289
|
+
score +=
|
|
290
|
+
queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization) + BM25_DELTA);
|
|
278
291
|
}
|
|
279
292
|
return { tool: document.tool, score };
|
|
280
293
|
})
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
8
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
9
|
import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
|
|
10
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
10
11
|
import type { Theme } from "../modes/theme/theme";
|
|
11
12
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
12
13
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
@@ -213,10 +214,10 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
213
214
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
214
215
|
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
215
216
|
}
|
|
216
|
-
const internalRouter =
|
|
217
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
217
218
|
const resolvedPathInputs: string[] = [];
|
|
218
219
|
for (const rawPath of rawPaths) {
|
|
219
|
-
if (!internalRouter
|
|
220
|
+
if (!internalRouter.canHandle(rawPath)) {
|
|
220
221
|
resolvedPathInputs.push(rawPath);
|
|
221
222
|
continue;
|
|
222
223
|
}
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
8
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
9
10
|
import type { Theme } from "../modes/theme/theme";
|
|
10
11
|
import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
|
|
11
12
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
@@ -158,10 +159,10 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
158
159
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
159
160
|
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
160
161
|
}
|
|
161
|
-
const internalRouter =
|
|
162
|
+
const internalRouter = InternalUrlRouter.instance();
|
|
162
163
|
const resolvedPathInputs: string[] = [];
|
|
163
164
|
for (const rawPath of rawPaths) {
|
|
164
|
-
if (!internalRouter
|
|
165
|
+
if (!internalRouter.canHandle(rawPath)) {
|
|
165
166
|
resolvedPathInputs.push(rawPath);
|
|
166
167
|
continue;
|
|
167
168
|
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -4,8 +4,10 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
4
4
|
import { ImageProtocol, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { $env, getProjectDir, isEnoent, prompt } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { AsyncJobManager } from "../async";
|
|
7
8
|
import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
8
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
|
+
import { InternalUrlRouter } from "../internal-urls";
|
|
9
11
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
10
12
|
import type { Theme } from "../modes/theme/theme";
|
|
11
13
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
@@ -326,7 +328,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
326
328
|
}
|
|
327
329
|
lines.push(`Background job ${jobId} started: ${label}`);
|
|
328
330
|
lines.push("Result will be delivered automatically when complete.");
|
|
329
|
-
lines.push(
|
|
331
|
+
lines.push(
|
|
332
|
+
`You can use \`job\` to poll until complete, but prefer to continue with another task in the meanwhile if it's not blocking.`,
|
|
333
|
+
);
|
|
330
334
|
return {
|
|
331
335
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
332
336
|
details,
|
|
@@ -349,7 +353,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
349
353
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
350
354
|
startBackgrounded: boolean;
|
|
351
355
|
}): ManagedBashJobHandle {
|
|
352
|
-
const manager =
|
|
356
|
+
const manager = AsyncJobManager.instance();
|
|
353
357
|
if (!manager) {
|
|
354
358
|
throw new ToolError("Background job manager unavailable for this session.");
|
|
355
359
|
}
|
|
@@ -399,6 +403,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
399
403
|
}
|
|
400
404
|
},
|
|
401
405
|
{
|
|
406
|
+
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
402
407
|
onProgress: async (text, details) => {
|
|
403
408
|
latestText = text;
|
|
404
409
|
await options.onUpdate?.({
|
|
@@ -501,7 +506,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
501
506
|
|
|
502
507
|
const internalUrlOptions: InternalUrlExpansionOptions = {
|
|
503
508
|
skills: this.session.skills ?? [],
|
|
504
|
-
internalRouter:
|
|
509
|
+
internalRouter: InternalUrlRouter.instance(),
|
|
505
510
|
localOptions: {
|
|
506
511
|
getArtifactsDir: this.session.getArtifactsDir,
|
|
507
512
|
getSessionId: this.session.getSessionId,
|
|
@@ -549,7 +554,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
549
554
|
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
|
|
550
555
|
|
|
551
556
|
if (asyncRequested) {
|
|
552
|
-
if (!
|
|
557
|
+
if (!AsyncJobManager.instance()) {
|
|
553
558
|
throw new ToolError("Async job manager unavailable for this session.");
|
|
554
559
|
}
|
|
555
560
|
const job = this.#startManagedBashJob({
|
|
@@ -570,7 +575,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
570
575
|
});
|
|
571
576
|
}
|
|
572
577
|
|
|
573
|
-
|
|
578
|
+
const autoBgManager = AsyncJobManager.instance();
|
|
579
|
+
if (this.#autoBackgroundEnabled && !pty && autoBgManager) {
|
|
574
580
|
const autoBackgroundWaitMs = this.#resolveAutoBackgroundWaitMs(timeoutMs);
|
|
575
581
|
const startBackgrounded = autoBackgroundWaitMs === 0;
|
|
576
582
|
const job = this.#startManagedBashJob({
|
|
@@ -593,16 +599,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
593
599
|
}
|
|
594
600
|
const waitResult = await this.#waitForManagedBashJob(job, autoBackgroundWaitMs, signal);
|
|
595
601
|
if (waitResult.kind === "completed") {
|
|
596
|
-
|
|
602
|
+
autoBgManager.acknowledgeDeliveries([job.jobId]);
|
|
597
603
|
return waitResult.result;
|
|
598
604
|
}
|
|
599
605
|
if (waitResult.kind === "failed") {
|
|
600
|
-
|
|
606
|
+
autoBgManager.acknowledgeDeliveries([job.jobId]);
|
|
601
607
|
throw waitResult.error;
|
|
602
608
|
}
|
|
603
609
|
if (waitResult.kind === "aborted") {
|
|
604
|
-
|
|
605
|
-
|
|
610
|
+
autoBgManager.cancel(job.jobId);
|
|
611
|
+
autoBgManager.acknowledgeDeliveries([job.jobId]);
|
|
606
612
|
throw new ToolAbortError(job.getLatestText() || "Command aborted");
|
|
607
613
|
}
|
|
608
614
|
job.setBackgrounded(true);
|
|
@@ -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
|
@@ -8,7 +8,7 @@ import { jsBackend, parseEvalInput, pythonBackend, sniffEvalLanguage } from "../
|
|
|
8
8
|
import type { ExecutorBackend } from "../eval/backend";
|
|
9
9
|
import evalGrammar from "../eval/eval.lark" with { type: "text" };
|
|
10
10
|
import { ABORT_WARNING, type ParsedEvalCell } from "../eval/parse";
|
|
11
|
-
import type { EvalCellResult, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
|
|
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";
|
|
@@ -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
|
|
|
@@ -370,13 +402,16 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
370
402
|
const durationMs = Date.now() - startTime;
|
|
371
403
|
|
|
372
404
|
const cellStatusEvents: EvalStatusEvent[] = [];
|
|
405
|
+
const cellDisplayOutputs: EvalDisplayOutput[] = [];
|
|
373
406
|
let cellHasMarkdown = false;
|
|
374
407
|
for (const output of result.displayOutputs) {
|
|
375
408
|
if (output.type === "json") {
|
|
376
409
|
jsonOutputs.push(output.data);
|
|
410
|
+
cellDisplayOutputs.push(output);
|
|
377
411
|
}
|
|
378
412
|
if (output.type === "image") {
|
|
379
413
|
images.push({ type: "image", data: output.data, mimeType: output.mimeType });
|
|
414
|
+
cellDisplayOutputs.push(output);
|
|
380
415
|
}
|
|
381
416
|
if (output.type === "status") {
|
|
382
417
|
statusEvents.push(output.event);
|
|
@@ -387,7 +422,10 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
387
422
|
}
|
|
388
423
|
}
|
|
389
424
|
|
|
390
|
-
const
|
|
425
|
+
const stdoutTrimmed = result.output.trim();
|
|
426
|
+
const displayText = formatDisplayOutputsForText(cellDisplayOutputs);
|
|
427
|
+
const cellOutput =
|
|
428
|
+
stdoutTrimmed && displayText ? `${stdoutTrimmed}\n\n${displayText}` : stdoutTrimmed || displayText;
|
|
391
429
|
cellResult.output = cellOutput;
|
|
392
430
|
cellResult.exitCode = result.exitCode;
|
|
393
431
|
cellResult.durationMs = durationMs;
|
|
@@ -431,14 +469,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
431
469
|
languages,
|
|
432
470
|
cells: cellResults,
|
|
433
471
|
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
434
|
-
images: images.length > 0 ? images : undefined,
|
|
435
472
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
436
473
|
isError: true,
|
|
437
474
|
};
|
|
438
475
|
if (notice) details.notice = notice;
|
|
439
476
|
|
|
440
477
|
return toolResult(details)
|
|
441
|
-
.text
|
|
478
|
+
.content([{ type: "text", text: outputText }, ...images])
|
|
442
479
|
.truncationFromSummary(summaryForMeta, { direction: "tail" })
|
|
443
480
|
.done();
|
|
444
481
|
}
|
|
@@ -461,14 +498,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
461
498
|
languages,
|
|
462
499
|
cells: cellResults,
|
|
463
500
|
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
464
|
-
images: images.length > 0 ? images : undefined,
|
|
465
501
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
466
502
|
isError: true,
|
|
467
503
|
};
|
|
468
504
|
if (notice) details.notice = notice;
|
|
469
505
|
|
|
470
506
|
return toolResult(details)
|
|
471
|
-
.text
|
|
507
|
+
.content([{ type: "text", text: outputText }, ...images])
|
|
472
508
|
.truncationFromSummary(summaryForMeta, { direction: "tail" })
|
|
473
509
|
.done();
|
|
474
510
|
}
|
|
@@ -479,9 +515,12 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
479
515
|
|
|
480
516
|
const combinedOutput = cellOutputs.join("\n\n");
|
|
481
517
|
const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
|
|
518
|
+
const hasImages = images.length > 0;
|
|
482
519
|
const outputText =
|
|
483
|
-
(combinedOutput ||
|
|
484
|
-
|
|
520
|
+
(combinedOutput ||
|
|
521
|
+
(hasImages
|
|
522
|
+
? `(displayed ${images.length} image${images.length === 1 ? "" : "s"}; no text output)`
|
|
523
|
+
: "(no output)")) + abortSuffix;
|
|
485
524
|
const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
|
|
486
525
|
|
|
487
526
|
const details: EvalToolDetails = {
|
|
@@ -489,13 +528,12 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
489
528
|
languages,
|
|
490
529
|
cells: cellResults,
|
|
491
530
|
jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
|
|
492
|
-
images: images.length > 0 ? images : undefined,
|
|
493
531
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
494
532
|
};
|
|
495
533
|
if (notice) details.notice = notice;
|
|
496
534
|
|
|
497
535
|
return toolResult(details)
|
|
498
|
-
.text
|
|
536
|
+
.content([{ type: "text", text: outputText }, ...images])
|
|
499
537
|
.truncationFromSummary(summaryForMeta, { direction: "tail" })
|
|
500
538
|
.done();
|
|
501
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");
|