@oh-my-pi/pi-coding-agent 17.2.7 → 17.2.9
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 +57 -1
- package/dist/{CHANGELOG-1p8yvde7.md → CHANGELOG-8yy8h3bc.md} +57 -1
- package/dist/cli.js +4974 -4968
- package/dist/types/async/job-manager.d.ts +12 -0
- package/dist/types/discovery/agents.d.ts +11 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
- package/dist/types/hindsight/state.d.ts +0 -1
- package/dist/types/lsp/utils.d.ts +8 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/registry/agent-lifecycle.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +5 -3
- package/dist/types/session/turn-recovery.d.ts +6 -2
- package/dist/types/task/executor.d.ts +4 -0
- package/dist/types/tools/browser/launch.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/shell-tokenize.d.ts +21 -3
- package/dist/types/utils/changelog.d.ts +3 -2
- package/dist/types/utils/late-cleanup.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +2 -0
- package/package.json +13 -13
- package/scripts/omp +11 -2
- package/src/async/job-manager.ts +26 -0
- package/src/cli/stats-cli.ts +3 -3
- package/src/cli/update-cli.ts +2 -56
- package/src/config/config-file.ts +2 -2
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-discovery.ts +36 -2
- package/src/config/model-registry.ts +4 -1
- package/src/discovery/agents.ts +71 -3
- package/src/discovery/claude.ts +8 -5
- package/src/discovery/cursor.ts +8 -5
- package/src/discovery/gemini.ts +11 -8
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +4 -2
- package/src/edit/hashline/filesystem.ts +7 -7
- package/src/eval/agent-bridge.ts +1 -3
- package/src/eval/jl/prelude.jl +4 -4
- package/src/eval/js/shared/prelude.txt +2 -2
- package/src/eval/py/prelude.py +0 -3
- package/src/eval/rb/prelude.rb +1 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
- package/src/hindsight/state.ts +2 -22
- package/src/internal-urls/memory-protocol.ts +29 -0
- package/src/lsp/client.ts +2 -2
- package/src/lsp/utils.ts +29 -0
- package/src/modes/acp/acp-agent.ts +2 -1
- package/src/modes/components/agent-hub.ts +36 -20
- package/src/modes/components/agent-transcript-viewer.ts +1 -3
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/custom-editor.ts +2 -2
- package/src/modes/components/hook-input.ts +1 -1
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/controllers/event-controller.ts +159 -2
- package/src/modes/controllers/extension-ui-controller.ts +2 -1
- package/src/modes/print-mode.ts +29 -12
- package/src/modes/session-observer-registry.ts +5 -0
- package/src/registry/agent-lifecycle.ts +21 -3
- package/src/sdk.ts +11 -5
- package/src/session/agent-session-types.ts +8 -0
- package/src/session/agent-session.ts +156 -186
- package/src/session/session-paths.ts +68 -121
- package/src/session/settings-stream-fn.ts +7 -3
- package/src/session/turn-recovery.ts +234 -16
- package/src/session/unexpected-stop-classifier.ts +12 -3
- package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +105 -29
- package/src/task/isolation-runner.ts +19 -1
- package/src/task/structured-subagent.ts +17 -2
- package/src/tiny/worker.ts +1 -1
- package/src/tools/bash-interceptor.ts +8 -4
- package/src/tools/browser/launch.ts +37 -9
- package/src/tools/glob.ts +4 -2
- package/src/tools/path-utils.ts +13 -3
- package/src/tools/read.ts +6 -0
- package/src/tools/shell-tokenize.ts +38 -9
- package/src/utils/changelog.ts +5 -4
- package/src/utils/late-cleanup.ts +17 -0
- package/src/vibe/runtime.ts +197 -53
- package/src/web/scrapers/hackage.ts +1 -12
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/ecosia.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/firecrawl.ts +1 -1
- package/src/web/search/providers/mojeek.ts +1 -1
- package/src/web/search/providers/perplexity.ts +1 -1
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/startpage.ts +1 -1
package/src/discovery/agents.ts
CHANGED
|
@@ -28,9 +28,77 @@ const DISPLAY_NAME = "Agent Dirs (.agent/.agents)";
|
|
|
28
28
|
const PRIORITY = 70;
|
|
29
29
|
const AGENT_DIR_CANDIDATES = [".agent", ".agents"] as const;
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
interface UserPathCandidateOptions {
|
|
32
|
+
platform?: NodeJS.Platform;
|
|
33
|
+
env?: NodeJS.ProcessEnv;
|
|
34
|
+
windowsUserProfile?: () => string | undefined;
|
|
35
|
+
wslPath?: (windowsPath: string) => string | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const WINDOWS_DRIVE_PROFILE_PATTERN = /^([A-Za-z]):[\\/](.*)$/;
|
|
39
|
+
|
|
40
|
+
function isWsl(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): boolean {
|
|
41
|
+
return platform === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function convertWindowsPathToDefaultWslMount(windowsPath: string): string | undefined {
|
|
45
|
+
const trimmed = windowsPath.trim();
|
|
46
|
+
if (trimmed.length === 0) return undefined;
|
|
47
|
+
if (path.isAbsolute(trimmed)) return path.normalize(trimmed);
|
|
48
|
+
const match = WINDOWS_DRIVE_PROFILE_PATTERN.exec(trimmed);
|
|
49
|
+
if (!match) return undefined;
|
|
50
|
+
const [, drive, rest] = match;
|
|
51
|
+
const segments = rest.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
52
|
+
return path.join("/mnt", drive.toLowerCase(), ...segments);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveWithWslPath(windowsPath: string): string | undefined {
|
|
56
|
+
try {
|
|
57
|
+
const result = Bun.spawnSync(["wslpath", "-u", windowsPath], { stdout: "pipe", stderr: "ignore" });
|
|
58
|
+
if (result.exitCode !== 0) return undefined;
|
|
59
|
+
const resolved = result.stdout.toString().trim();
|
|
60
|
+
return resolved.length > 0 ? resolved : undefined;
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveWindowsUserProfile(): string | undefined {
|
|
67
|
+
try {
|
|
68
|
+
const result = Bun.spawnSync(["cmd.exe", "/d", "/c", "echo", "%USERPROFILE%"], {
|
|
69
|
+
stdout: "pipe",
|
|
70
|
+
stderr: "ignore",
|
|
71
|
+
});
|
|
72
|
+
if (result.exitCode !== 0) return undefined;
|
|
73
|
+
const resolved = result.stdout.toString().trim();
|
|
74
|
+
return resolved.length > 0 && resolved !== "%USERPROFILE%" ? resolved : undefined;
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Resolve the Windows host profile home exposed to WSL, if available. */
|
|
81
|
+
export function getWslWindowsHomeCandidate(options: UserPathCandidateOptions = {}): string | undefined {
|
|
82
|
+
const platform = options.platform ?? process.platform;
|
|
83
|
+
const env = options.env ?? process.env;
|
|
84
|
+
if (!isWsl(platform, env)) return undefined;
|
|
85
|
+
const userProfile = env.USERPROFILE ?? (options.windowsUserProfile ?? resolveWindowsUserProfile)();
|
|
86
|
+
if (!userProfile) return undefined;
|
|
87
|
+
return (options.wslPath ?? resolveWithWslPath)(userProfile) ?? convertWindowsPathToDefaultWslMount(userProfile);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getUserHomeCandidates(ctx: LoadContext): string[] {
|
|
91
|
+
const homes = [ctx.home];
|
|
92
|
+
const wslHome = getWslWindowsHomeCandidate();
|
|
93
|
+
if (wslHome && !homes.includes(wslHome)) homes.push(wslHome);
|
|
94
|
+
return homes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** User-level paths: ~/.agent[s]/<segments>, plus the Windows host profile under WSL. */
|
|
98
|
+
export function getUserPathCandidates(ctx: LoadContext, ...segments: string[]): string[] {
|
|
99
|
+
return getUserHomeCandidates(ctx).flatMap(home =>
|
|
100
|
+
AGENT_DIR_CANDIDATES.map(baseDir => path.join(home, baseDir, ...segments)),
|
|
101
|
+
);
|
|
34
102
|
}
|
|
35
103
|
|
|
36
104
|
/**
|
package/src/discovery/claude.ts
CHANGED
|
@@ -90,6 +90,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
90
90
|
const serverConfig = config as Record<string, unknown>;
|
|
91
91
|
return {
|
|
92
92
|
name,
|
|
93
|
+
enabled: typeof serverConfig.enabled === "boolean" ? serverConfig.enabled : undefined,
|
|
93
94
|
timeout: typeof serverConfig.timeout === "number" ? serverConfig.timeout : undefined,
|
|
94
95
|
command: serverConfig.command as string | undefined,
|
|
95
96
|
args: serverConfig.args as string[] | undefined,
|
|
@@ -102,17 +103,19 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
102
103
|
});
|
|
103
104
|
};
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
// Load project entries before user entries so a project `enabled: false`
|
|
107
|
+
// claims its dedupe key before a same-named user server can survive (#7654).
|
|
108
|
+
const projectOffset = userPaths.length;
|
|
109
|
+
for (let i = 0; i < projectPaths.length; i++) {
|
|
110
|
+
const servers = parseMcpServers(contents[projectOffset + i], projectPaths[i].path, projectPaths[i].level);
|
|
107
111
|
if (servers.length > 0) {
|
|
108
112
|
items.push(...servers);
|
|
109
113
|
break;
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const servers = parseMcpServers(contents[projectOffset + i], projectPaths[i].path, projectPaths[i].level);
|
|
117
|
+
for (let i = 0; i < userPaths.length; i++) {
|
|
118
|
+
const servers = parseMcpServers(contents[i], userPaths[i].path, userPaths[i].level);
|
|
116
119
|
if (servers.length > 0) {
|
|
117
120
|
items.push(...servers);
|
|
118
121
|
break;
|
package/src/discovery/cursor.ts
CHANGED
|
@@ -57,6 +57,7 @@ function parseMCPServers(
|
|
|
57
57
|
const serverConfig = config as Record<string, unknown>;
|
|
58
58
|
items.push({
|
|
59
59
|
name,
|
|
60
|
+
enabled: typeof serverConfig.enabled === "boolean" ? serverConfig.enabled : undefined,
|
|
60
61
|
command: serverConfig.command as string | undefined,
|
|
61
62
|
args: serverConfig.args as string[] | undefined,
|
|
62
63
|
env: serverConfig.env as Record<string, string> | undefined,
|
|
@@ -86,15 +87,17 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
86
87
|
|
|
87
88
|
const projectContentPromise = projectPath ? readFile(projectPath) : Promise.resolve(null);
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
// Load project entries before user entries so a project `enabled: false`
|
|
91
|
+
// claims its dedupe key before a same-named user server can survive (#7654).
|
|
92
|
+
const projectContent = await projectContentPromise;
|
|
93
|
+
if (projectContent && projectPath) {
|
|
94
|
+
const result = parseMCPServers(projectContent, projectPath, "project");
|
|
91
95
|
items.push(...result.items);
|
|
92
96
|
if (result.warning) warnings.push(result.warning);
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const result = parseMCPServers(projectContent, projectPath, "project");
|
|
99
|
+
if (userContent && userPath) {
|
|
100
|
+
const result = parseMCPServers(userContent, userPath, "user");
|
|
98
101
|
items.push(...result.items);
|
|
99
102
|
if (result.warning) warnings.push(result.warning);
|
|
100
103
|
}
|
package/src/discovery/gemini.ts
CHANGED
|
@@ -48,14 +48,8 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
48
48
|
const items: MCPServer[] = [];
|
|
49
49
|
const warnings: string[] = [];
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
if (userPath) {
|
|
54
|
-
const result = await loadMCPFromSettings(ctx, userPath, "user");
|
|
55
|
-
items.push(...result.items);
|
|
56
|
-
if (result.warnings) warnings.push(...result.warnings);
|
|
57
|
-
}
|
|
58
|
-
|
|
51
|
+
// Load project entries before user entries so a project `enabled: false`
|
|
52
|
+
// claims its dedupe key before a same-named user server can survive (#7654).
|
|
59
53
|
// Project-level: .gemini/settings.json → mcpServers
|
|
60
54
|
const projectPath = getProjectPath(ctx, "gemini", "settings.json");
|
|
61
55
|
if (projectPath) {
|
|
@@ -64,6 +58,14 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
64
58
|
if (result.warnings) warnings.push(...result.warnings);
|
|
65
59
|
}
|
|
66
60
|
|
|
61
|
+
// User-level: ~/.gemini/settings.json → mcpServers
|
|
62
|
+
const userPath = getUserPath(ctx, "gemini", "settings.json");
|
|
63
|
+
if (userPath) {
|
|
64
|
+
const result = await loadMCPFromSettings(ctx, userPath, "user");
|
|
65
|
+
items.push(...result.items);
|
|
66
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
67
|
+
}
|
|
68
|
+
|
|
67
69
|
return { items, warnings };
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -102,6 +104,7 @@ async function loadMCPFromSettings(
|
|
|
102
104
|
|
|
103
105
|
items.push({
|
|
104
106
|
name,
|
|
107
|
+
enabled: typeof raw.enabled === "boolean" ? raw.enabled : undefined,
|
|
105
108
|
command: typeof raw.command === "string" ? raw.command : undefined,
|
|
106
109
|
args: Array.isArray(raw.args) ? (raw.args as string[]) : undefined,
|
|
107
110
|
env: raw.env && typeof raw.env === "object" ? (raw.env as Record<string, string>) : undefined,
|
package/src/discovery/vscode.ts
CHANGED
|
@@ -83,6 +83,7 @@ async function loadMCPConfig(
|
|
|
83
83
|
|
|
84
84
|
const server: MCPServer = {
|
|
85
85
|
name,
|
|
86
|
+
enabled: typeof expanded.enabled === "boolean" ? expanded.enabled : undefined,
|
|
86
87
|
command: typeof expanded.command === "string" ? expanded.command : undefined,
|
|
87
88
|
args: Array.isArray(expanded.args) ? (expanded.args as string[]) : undefined,
|
|
88
89
|
env: expanded.env && typeof expanded.env === "object" ? (expanded.env as Record<string, string>) : undefined,
|
|
@@ -48,6 +48,7 @@ function parseServerConfig(
|
|
|
48
48
|
return {
|
|
49
49
|
server: {
|
|
50
50
|
name,
|
|
51
|
+
enabled: typeof server.enabled === "boolean" ? server.enabled : undefined,
|
|
51
52
|
command: server.command as string | undefined,
|
|
52
53
|
args: server.args as string[] | undefined,
|
|
53
54
|
env: server.env as Record<string, string> | undefined,
|
|
@@ -71,10 +72,11 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
71
72
|
]);
|
|
72
73
|
|
|
73
74
|
const projectContent = projectPath ? await readFile(projectPath) : null;
|
|
74
|
-
|
|
75
|
+
// Load project entries before user entries so a project `enabled: false`
|
|
76
|
+
// claims its dedupe key before a same-named user server can survive (#7654).
|
|
75
77
|
const configs: Array<{ content: string | null; path: string | null; scope: "user" | "project" }> = [
|
|
76
|
-
{ content: userContent, path: userPath, scope: "user" },
|
|
77
78
|
{ content: projectContent, path: projectPath, scope: "project" },
|
|
79
|
+
{ content: userContent, path: userPath, scope: "user" },
|
|
78
80
|
];
|
|
79
81
|
|
|
80
82
|
for (const { content, path, scope } of configs) {
|
|
@@ -87,11 +87,11 @@ export class HashlineFilesystem extends Filesystem {
|
|
|
87
87
|
return resolvePlanPath(this.session, relativePath);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
canonicalPath(relativePath: string): string {
|
|
90
|
+
override canonicalPath(relativePath: string): string {
|
|
91
91
|
return canonicalSnapshotKey(this.resolveAbsolute(relativePath));
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
allowTagPathRecovery(authoredPath: string, resolvedPath: string): boolean {
|
|
94
|
+
override allowTagPathRecovery(authoredPath: string, resolvedPath: string): boolean {
|
|
95
95
|
// Internal-URL authored targets (`local://`, `vault://`, …) are approved
|
|
96
96
|
// at the lower "read" privilege; never let one redirect onto a "write".
|
|
97
97
|
if (isInternalUrlPath(authoredPath)) return false;
|
|
@@ -125,7 +125,7 @@ export class HashlineFilesystem extends Filesystem {
|
|
|
125
125
|
return content;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
async readBinary(relativePath: string): Promise<Uint8Array | undefined> {
|
|
128
|
+
override async readBinary(relativePath: string): Promise<Uint8Array | undefined> {
|
|
129
129
|
const absolutePath = this.resolveAbsolute(relativePath);
|
|
130
130
|
if (isNotebookPath(absolutePath)) return undefined;
|
|
131
131
|
try {
|
|
@@ -136,7 +136,7 @@ export class HashlineFilesystem extends Filesystem {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
async preflightWrite(relativePath: string, options?: PreflightWriteOptions): Promise<void> {
|
|
139
|
+
override async preflightWrite(relativePath: string, options?: PreflightWriteOptions): Promise<void> {
|
|
140
140
|
const fileOp = options?.fileOp;
|
|
141
141
|
if (fileOp?.kind === "rem") {
|
|
142
142
|
enforcePlanModeWrite(this.session, relativePath, { op: "delete" });
|
|
@@ -149,7 +149,7 @@ export class HashlineFilesystem extends Filesystem {
|
|
|
149
149
|
enforcePlanModeWrite(this.session, relativePath, { op: "update" });
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
async delete(relativePath: string): Promise<void> {
|
|
152
|
+
override async delete(relativePath: string): Promise<void> {
|
|
153
153
|
enforcePlanModeWrite(this.session, relativePath, { op: "delete" });
|
|
154
154
|
const absolutePath = this.resolveAbsolute(relativePath);
|
|
155
155
|
try {
|
|
@@ -168,7 +168,7 @@ export class HashlineFilesystem extends Filesystem {
|
|
|
168
168
|
invalidateFsScanAfterWrite(absolutePath);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
async move(fromRelative: string, toRelative: string, content?: string): Promise<void> {
|
|
171
|
+
override async move(fromRelative: string, toRelative: string, content?: string): Promise<void> {
|
|
172
172
|
enforcePlanModeWrite(this.session, fromRelative, { op: "update", move: toRelative });
|
|
173
173
|
const fromAbsolute = this.resolveAbsolute(fromRelative);
|
|
174
174
|
const toAbsolute = this.resolveAbsolute(toRelative);
|
|
@@ -240,7 +240,7 @@ export class HashlineFilesystem extends Filesystem {
|
|
|
240
240
|
return { text: content };
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
async exists(relativePath: string): Promise<boolean> {
|
|
243
|
+
override async exists(relativePath: string): Promise<boolean> {
|
|
244
244
|
const absolutePath = this.resolveAbsolute(relativePath);
|
|
245
245
|
return Bun.file(absolutePath).exists();
|
|
246
246
|
}
|
package/src/eval/agent-bridge.ts
CHANGED
|
@@ -23,7 +23,6 @@ export const EVAL_AGENT_BRIDGE_NAME = "__agent__";
|
|
|
23
23
|
const agentArgsSchema = type({
|
|
24
24
|
prompt: "string>0",
|
|
25
25
|
"agent?": "string>0",
|
|
26
|
-
"model?": "string>0|string>0[]",
|
|
27
26
|
"label?": "string",
|
|
28
27
|
"schema?": "unknown",
|
|
29
28
|
"schemaMode?": "'permissive' | 'strict'",
|
|
@@ -31,12 +30,12 @@ const agentArgsSchema = type({
|
|
|
31
30
|
"apply?": "boolean",
|
|
32
31
|
"merge?": "boolean",
|
|
33
32
|
"handle?": "boolean",
|
|
33
|
+
"+": "delete",
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
interface EvalAgentArgs {
|
|
37
37
|
prompt: string;
|
|
38
38
|
agent?: string;
|
|
39
|
-
model?: string | string[];
|
|
40
39
|
label?: string;
|
|
41
40
|
schema?: unknown;
|
|
42
41
|
schemaMode?: StructuredSubagentSchemaMode;
|
|
@@ -148,7 +147,6 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
148
147
|
invocationKind: "eval",
|
|
149
148
|
assignment: parsed.prompt,
|
|
150
149
|
...(parsed.agent !== undefined ? { agent: parsed.agent } : {}),
|
|
151
|
-
...(parsed.model !== undefined ? { model: parsed.model } : {}),
|
|
152
150
|
...(Object.hasOwn(parsed, "schema") ? { outputSchema: parsed.schema } : {}),
|
|
153
151
|
...(parsed.schemaMode !== undefined ? { schemaMode: parsed.schemaMode } : {}),
|
|
154
152
|
...(parsed.label !== undefined ? { identity: { label: parsed.label } } : {}),
|
package/src/eval/jl/prelude.jl
CHANGED
|
@@ -519,14 +519,11 @@ function completion(prompt::String; model="default", system=nothing, schema=noth
|
|
|
519
519
|
return schema === nothing ? text : Main.json_parse(string(text))
|
|
520
520
|
end
|
|
521
521
|
|
|
522
|
-
function agent(prompt::String; agent="task",
|
|
522
|
+
function agent(prompt::String; agent="task", label=nothing, schema=nothing, schema_mode=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
|
|
523
523
|
args_dict = Dict{String, Any}("prompt" => prompt)
|
|
524
524
|
if agent !== nothing
|
|
525
525
|
args_dict["agent"] = agent
|
|
526
526
|
end
|
|
527
|
-
if model !== nothing
|
|
528
|
-
args_dict["model"] = model
|
|
529
|
-
end
|
|
530
527
|
if label !== nothing
|
|
531
528
|
args_dict["label"] = label
|
|
532
529
|
end
|
|
@@ -545,6 +542,9 @@ function agent(prompt::String; agent="task", model=nothing, label=nothing, schem
|
|
|
545
542
|
if merge !== nothing
|
|
546
543
|
args_dict["merge"] = Bool(merge)
|
|
547
544
|
end
|
|
545
|
+
if haskey(kwargs, :model)
|
|
546
|
+
error("agent() no longer accepts a per-call model override; the selected agent's frontmatter model is used")
|
|
547
|
+
end
|
|
548
548
|
handle_result = handle
|
|
549
549
|
for (k, v) in kwargs
|
|
550
550
|
args_dict[string(k)] = v
|
|
@@ -104,8 +104,8 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
104
104
|
"agent",
|
|
105
105
|
opts,
|
|
106
106
|
rest,
|
|
107
|
-
["agent", "
|
|
108
|
-
"{ agent,
|
|
107
|
+
["agent", "label", "schema", "isolated", "apply", "merge", "schemaMode"],
|
|
108
|
+
"{ agent, label, schema, isolated, apply, merge, schemaMode, handle }",
|
|
109
109
|
);
|
|
110
110
|
const { handle, ...callArgs } = o;
|
|
111
111
|
const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...callArgs, handle: Boolean(handle) });
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -488,7 +488,6 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
488
488
|
prompt,
|
|
489
489
|
*,
|
|
490
490
|
agent="task",
|
|
491
|
-
model=None,
|
|
492
491
|
label=None,
|
|
493
492
|
schema=None,
|
|
494
493
|
schema_mode=None,
|
|
@@ -506,8 +505,6 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
506
505
|
args = {"prompt": prompt}
|
|
507
506
|
if agent is not None:
|
|
508
507
|
args["agent"] = agent
|
|
509
|
-
if model is not None:
|
|
510
|
-
args["model"] = model
|
|
511
508
|
if label is not None:
|
|
512
509
|
args["label"] = label
|
|
513
510
|
if schema is not None:
|
package/src/eval/rb/prelude.rb
CHANGED
|
@@ -392,10 +392,9 @@ unless defined?($__omp_prelude_loaded) && $__omp_prelude_loaded
|
|
|
392
392
|
schema.nil? ? text : JSON.parse(text)
|
|
393
393
|
end
|
|
394
394
|
|
|
395
|
-
def agent(prompt, agent: "task",
|
|
395
|
+
def agent(prompt, agent: "task", label: nil, schema: nil, schema_mode: nil, isolated: nil, apply: nil, merge: nil, handle: false)
|
|
396
396
|
args = { "prompt" => prompt }
|
|
397
397
|
args["agent"] = agent unless agent.nil?
|
|
398
|
-
args["model"] = model unless model.nil?
|
|
399
398
|
args["label"] = label unless label.nil?
|
|
400
399
|
args["schema"] = schema unless schema.nil?
|
|
401
400
|
args["schemaMode"] = schema_mode unless schema_mode.nil?
|
|
@@ -54,6 +54,7 @@ import { ReadTool } from "../tools/read";
|
|
|
54
54
|
import { formatBytes } from "../tools/render-utils";
|
|
55
55
|
import { WriteTool } from "../tools/write";
|
|
56
56
|
import { EventBus } from "../utils/event-bus";
|
|
57
|
+
import { convertImageToPng } from "../utils/image-loading";
|
|
57
58
|
import { discoverExtensionPaths, loadExtensionFromFactory, loadExtensions } from "./extensions";
|
|
58
59
|
import { ExtensionRuntime } from "./extensions/loader";
|
|
59
60
|
import type { ExtensionFactory, ToolDefinition } from "./extensions/types";
|
|
@@ -384,6 +385,28 @@ async function executeLegacyBashOperations(
|
|
|
384
385
|
}
|
|
385
386
|
}
|
|
386
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Convert an image attachment to PNG using the legacy package-root contract.
|
|
390
|
+
*
|
|
391
|
+
* Invalid or unsupported image data returns `null`, matching Pi's historical
|
|
392
|
+
* helper instead of surfacing Bun's decoder error to extensions.
|
|
393
|
+
*/
|
|
394
|
+
export async function convertToPng(
|
|
395
|
+
base64Data: string,
|
|
396
|
+
mimeType: string,
|
|
397
|
+
): Promise<{ data: string; mimeType: string } | null> {
|
|
398
|
+
if (mimeType === "image/png") {
|
|
399
|
+
return { data: base64Data, mimeType };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const converted = await convertImageToPng({ type: "image", data: base64Data, mimeType });
|
|
404
|
+
return { data: converted.data, mimeType: converted.mimeType };
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
387
410
|
/** Format the active shortcut for legacy extensions that render keybinding hints. */
|
|
388
411
|
export function keyText(action: Keybinding): string {
|
|
389
412
|
return formatKeyHints(getKeybindings().getKeys(action));
|
package/src/hindsight/state.ts
CHANGED
|
@@ -411,24 +411,6 @@ export class HindsightSessionState {
|
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
async maybeRecallOnAgentStart(): Promise<void> {
|
|
415
|
-
if (!this.config.autoRecall || this.hasRecalledForFirstTurn) return;
|
|
416
|
-
const messages = extractMessages(this.session.sessionManager);
|
|
417
|
-
const lastUser = messages.findLast(m => m.role === "user");
|
|
418
|
-
if (!lastUser) return;
|
|
419
|
-
|
|
420
|
-
const query = composeRecallQuery(lastUser.content, messages, this.config.recallContextTurns);
|
|
421
|
-
const truncated = truncateRecallQuery(query, lastUser.content, this.config.recallMaxQueryChars);
|
|
422
|
-
const { context, ok } = await this.recallForContext(truncated);
|
|
423
|
-
if (!ok) return;
|
|
424
|
-
|
|
425
|
-
this.hasRecalledForFirstTurn = true;
|
|
426
|
-
if (!context) return;
|
|
427
|
-
|
|
428
|
-
this.lastRecallSnippet = context;
|
|
429
|
-
await this.#refreshBaseSystemPromptAfter("recall");
|
|
430
|
-
}
|
|
431
|
-
|
|
432
414
|
async beforeAgentStartPrompt(promptText: string): Promise<string | undefined> {
|
|
433
415
|
if (this.config.mentalModelsEnabled && this.mentalModelsLoadPromise && this.mentalModelsLoadedAt === undefined) {
|
|
434
416
|
await Promise.race([this.mentalModelsLoadPromise, Bun.sleep(MENTAL_MODEL_FIRST_TURN_DEADLINE_MS)]);
|
|
@@ -509,9 +491,7 @@ export class HindsightSessionState {
|
|
|
509
491
|
attachSessionListeners(): void {
|
|
510
492
|
this.unsubscribe?.();
|
|
511
493
|
this.unsubscribe = this.session.subscribe(event => {
|
|
512
|
-
if (event.type === "
|
|
513
|
-
void this.maybeRecallOnAgentStart();
|
|
514
|
-
} else if (event.type === "agent_end") {
|
|
494
|
+
if (event.type === "agent_end") {
|
|
515
495
|
void this.maybeRetainOnAgentEnd();
|
|
516
496
|
// Drain any queued tool-initiated retain calls now that the turn
|
|
517
497
|
// is settled. The queue is also debounced/size-bounded, but
|
|
@@ -540,7 +520,7 @@ export class HindsightSessionState {
|
|
|
540
520
|
this.retainQueue.dispose();
|
|
541
521
|
}
|
|
542
522
|
|
|
543
|
-
async #refreshBaseSystemPromptAfter(reason: "
|
|
523
|
+
async #refreshBaseSystemPromptAfter(reason: "MM load" | "MM reload" | "MM TTL reload"): Promise<void> {
|
|
544
524
|
try {
|
|
545
525
|
await this.session.refreshBaseSystemPrompt();
|
|
546
526
|
} catch (err) {
|
|
@@ -222,6 +222,18 @@ function mnemopiSessionStatesFromRegistry(): MnemopiSessionState[] {
|
|
|
222
222
|
return states;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
function memoryBackendFromContext(context?: ResolveContext): string | undefined {
|
|
226
|
+
if (!context?.settings || typeof context.settings !== "object") return undefined;
|
|
227
|
+
try {
|
|
228
|
+
const get = Reflect.get(context.settings, "get");
|
|
229
|
+
if (typeof get !== "function") return undefined;
|
|
230
|
+
const backend = Reflect.apply(get, context.settings, ["memory.backend"]);
|
|
231
|
+
return typeof backend === "string" ? backend : undefined;
|
|
232
|
+
} catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
225
237
|
/**
|
|
226
238
|
* Look up a mnemopi memory row by id across every live session's scoped banks.
|
|
227
239
|
* First hit wins; returns `null` when the id is not stored anywhere in scope.
|
|
@@ -290,6 +302,23 @@ export class MemoryProtocolHandler implements ProtocolHandler {
|
|
|
290
302
|
// clipped recall preview before overwriting it (issue #4443).
|
|
291
303
|
if (namespace !== MEMORY_NAMESPACE) {
|
|
292
304
|
const mnemopiStates = mnemopiSessionStatesFromRegistry();
|
|
305
|
+
const hindsightActive =
|
|
306
|
+
memoryBackendFromContext(context) === "hindsight" ||
|
|
307
|
+
(mnemopiStates.length === 0 &&
|
|
308
|
+
AgentRegistry.global()
|
|
309
|
+
.list()
|
|
310
|
+
.some(ref => ref.session?.getHindsightSessionState?.()));
|
|
311
|
+
if (hindsightActive) {
|
|
312
|
+
// Hindsight keeps memories server-side and exposes no
|
|
313
|
+
// `memory://<id>` addressing, yet the shared `recall` tool
|
|
314
|
+
// description still steers a follow-up `read memory://<id>`.
|
|
315
|
+
// Return a corrective pointer so that stray read self-corrects in
|
|
316
|
+
// one turn instead of derailing on the generic namespace error
|
|
317
|
+
// (issue #7587).
|
|
318
|
+
throw new Error(
|
|
319
|
+
"Hindsight memories are not addressable via memory://. Recall results are final — use `recall` to search or `reflect` to synthesize. `read memory://<id>` is only available with memory.backend=mnemopi.",
|
|
320
|
+
);
|
|
321
|
+
}
|
|
293
322
|
if (mnemopiStates.length === 0) {
|
|
294
323
|
throw new Error(
|
|
295
324
|
`Unknown memory namespace: ${namespace}. Supported: ${MEMORY_NAMESPACE} (file-backed memory summary), or a mnemopi memory id when memory.backend=mnemopi is active.`,
|
package/src/lsp/client.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
ServerConfig,
|
|
18
18
|
WorkspaceEdit,
|
|
19
19
|
} from "./types";
|
|
20
|
-
import { detectLanguageId, fileToUri } from "./utils";
|
|
20
|
+
import { detectLanguageId, EquivalentUriMap, fileToUri } from "./utils";
|
|
21
21
|
|
|
22
22
|
// =============================================================================
|
|
23
23
|
// Client State
|
|
@@ -787,7 +787,7 @@ export async function getOrCreateClient(
|
|
|
787
787
|
proc,
|
|
788
788
|
config,
|
|
789
789
|
requestId: 0,
|
|
790
|
-
diagnostics: new
|
|
790
|
+
diagnostics: new EquivalentUriMap(),
|
|
791
791
|
diagnosticsVersion: 0,
|
|
792
792
|
dynamicCapabilityRegistrations: new Map(),
|
|
793
793
|
openFiles: new Map(),
|
package/src/lsp/utils.ts
CHANGED
|
@@ -78,6 +78,35 @@ function laxUriToFile(uri: string): string {
|
|
|
78
78
|
return filePath;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Map that treats equivalent file URI spellings as the same key. */
|
|
82
|
+
export class EquivalentUriMap<Value> extends Map<string, Value> {
|
|
83
|
+
#key(uri: string): string {
|
|
84
|
+
if (!uri.startsWith("file://")) return uri;
|
|
85
|
+
const filePath = path.normalize(uriToFile(uri));
|
|
86
|
+
return process.platform === "win32" ? filePath.toLowerCase() : filePath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override delete(uri: string): boolean {
|
|
90
|
+
const key = this.#key(uri);
|
|
91
|
+
return super.delete(key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override get(uri: string): Value | undefined {
|
|
95
|
+
const key = this.#key(uri);
|
|
96
|
+
return super.get(key);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override has(uri: string): boolean {
|
|
100
|
+
const key = this.#key(uri);
|
|
101
|
+
return super.has(key);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override set(uri: string, value: Value): this {
|
|
105
|
+
const key = this.#key(uri);
|
|
106
|
+
return super.set(key, value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
81
110
|
// =============================================================================
|
|
82
111
|
// Diagnostic Formatting
|
|
83
112
|
// =============================================================================
|
|
@@ -2312,7 +2312,7 @@ export class AcpAgent implements Agent {
|
|
|
2312
2312
|
this.#clientCapabilities,
|
|
2313
2313
|
);
|
|
2314
2314
|
if (this.#clientCapabilities?.elicitation?.form != null) {
|
|
2315
|
-
record.session.setUsageFallbackConfirmer(confirmation => {
|
|
2315
|
+
record.session.setUsageFallbackConfirmer((confirmation, signal) => {
|
|
2316
2316
|
const reserve =
|
|
2317
2317
|
confirmation.remainingPercent === undefined
|
|
2318
2318
|
? "inside the configured reserve margin"
|
|
@@ -2320,6 +2320,7 @@ export class AcpAgent implements Agent {
|
|
|
2320
2320
|
return uiContext.confirm(
|
|
2321
2321
|
"Coding-plan reserve reached",
|
|
2322
2322
|
`${confirmation.from} has ${reserve}. Switch to ${confirmation.to}? Choose No to keep using the current plan.`,
|
|
2323
|
+
{ signal },
|
|
2323
2324
|
);
|
|
2324
2325
|
});
|
|
2325
2326
|
}
|