@oh-my-pi/pi-coding-agent 14.9.3 → 14.9.7
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 +96 -0
- package/package.json +7 -7
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/cli/setup-cli.ts +14 -161
- package/src/cli/stats-cli.ts +56 -2
- package/src/cli.ts +0 -1
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/settings-schema.ts +1 -11
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/eval.lark +30 -10
- package/src/eval/js/context-manager.ts +334 -601
- package/src/eval/js/shared/helpers.ts +237 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -2
- package/src/eval/js/shared/rewrite-imports.ts +211 -0
- package/src/eval/js/shared/runtime.ts +168 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +2 -4
- package/src/eval/js/worker-core.ts +146 -0
- package/src/eval/js/worker-entry.ts +24 -0
- package/src/eval/js/worker-protocol.ts +41 -0
- package/src/eval/parse.ts +218 -49
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +97 -96
- package/src/eval/py/index.ts +2 -2
- package/src/eval/py/kernel.ts +472 -900
- package/src/eval/py/prelude.py +106 -87
- package/src/eval/py/runner.py +879 -0
- package/src/eval/py/runtime.ts +3 -16
- package/src/eval/py/tool-bridge.ts +137 -0
- package/src/export/html/template.css +12 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +113 -7
- 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 +35 -3
- 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/command-controller.ts +0 -23
- 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 +15 -30
- 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 -21
- package/src/session/artifacts.ts +7 -4
- package/src/session/history-storage.ts +77 -19
- 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-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +98 -7
- package/src/tools/browser/tab-worker.ts +104 -58
- package/src/tools/eval.ts +49 -11
- 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/web/search/index.ts +6 -4
- package/src/cli/jupyter-cli.ts +0 -106
- package/src/commands/jupyter.ts +0 -32
- package/src/eval/py/cancellation.ts +0 -28
- package/src/eval/py/gateway-coordinator.ts +0 -424
- package/src/internal-urls/jobs-protocol.ts +0 -120
- package/src/prompts/system/now-prompt.md +0 -7
- /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
|
@@ -19,6 +19,12 @@ type HistoryRow = {
|
|
|
19
19
|
|
|
20
20
|
const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
|
|
21
21
|
|
|
22
|
+
// Escape LIKE wildcards so user input is treated as literal text.
|
|
23
|
+
// Matches the `ESCAPE '\\'` clause used by substring-search statements.
|
|
24
|
+
function escapeLikePattern(text: string): string {
|
|
25
|
+
return text.replace(/[\\%_]/g, "\\$&");
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
class AsyncDrain<T> {
|
|
23
29
|
#queue?: T[];
|
|
24
30
|
#promise = Promise.resolve();
|
|
@@ -63,6 +69,8 @@ export class HistoryStorage {
|
|
|
63
69
|
#recentStmt: Statement;
|
|
64
70
|
#searchStmt: Statement;
|
|
65
71
|
#lastPromptStmt: Statement;
|
|
72
|
+
// Cache substring-fallback prepared statements keyed by token count.
|
|
73
|
+
#substringStmts = new Map<number, Statement>();
|
|
66
74
|
|
|
67
75
|
// In-memory cache of last prompt to avoid sync DB reads on add
|
|
68
76
|
#lastPromptCache: string | null = null;
|
|
@@ -167,16 +175,53 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
167
175
|
const safeLimit = this.#normalizeLimit(limit);
|
|
168
176
|
if (safeLimit === 0) return [];
|
|
169
177
|
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
178
|
+
const tokens = this.#tokenize(query);
|
|
179
|
+
if (tokens.length === 0) return [];
|
|
172
180
|
|
|
181
|
+
// 1. FTS5 prefix match (token AND, prefix-wildcard per token).
|
|
182
|
+
// Handles punctuation by tokenizing query the same way unicode61 tokenizer
|
|
183
|
+
// indexed the stored text, so "git-commit" -> "git"* "commit"*.
|
|
184
|
+
const ftsQuery = tokens.map(tok => `"${tok.replace(/"/g, '""')}"*`).join(" ");
|
|
185
|
+
let ftsRows: HistoryRow[] = [];
|
|
173
186
|
try {
|
|
174
|
-
|
|
175
|
-
return rows.map(row => this.#toEntry(row));
|
|
187
|
+
ftsRows = this.#searchStmt.all(ftsQuery, safeLimit) as HistoryRow[];
|
|
176
188
|
} catch (error) {
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
// Malformed FTS expression - fall through to substring path.
|
|
190
|
+
logger.debug("HistoryStorage FTS query failed, using substring only", { error: String(error) });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (ftsRows.length >= safeLimit) {
|
|
194
|
+
return ftsRows.map(row => this.#toEntry(row));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 2. Substring fallback (token-AND LIKE). Catches infix matches FTS5's
|
|
198
|
+
// prefix-only wildcard cannot reach (e.g. "mit" -> "commit"). Bounded
|
|
199
|
+
// by safeLimit, ordered by recency - no full-table load into JS.
|
|
200
|
+
let subRows: HistoryRow[] = [];
|
|
201
|
+
try {
|
|
202
|
+
subRows = this.#searchSubstring(tokens, safeLimit);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logger.error("HistoryStorage substring search failed", { error: String(error) });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (ftsRows.length === 0) {
|
|
208
|
+
return subRows.map(row => this.#toEntry(row));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const seen = new Set<number>();
|
|
212
|
+
const merged: HistoryEntry[] = [];
|
|
213
|
+
for (const row of ftsRows) {
|
|
214
|
+
if (seen.has(row.id)) continue;
|
|
215
|
+
seen.add(row.id);
|
|
216
|
+
merged.push(this.#toEntry(row));
|
|
179
217
|
}
|
|
218
|
+
for (const row of subRows) {
|
|
219
|
+
if (merged.length >= safeLimit) break;
|
|
220
|
+
if (seen.has(row.id)) continue;
|
|
221
|
+
seen.add(row.id);
|
|
222
|
+
merged.push(this.#toEntry(row));
|
|
223
|
+
}
|
|
224
|
+
return merged;
|
|
180
225
|
}
|
|
181
226
|
|
|
182
227
|
#ensureDir(dbPath: string): void {
|
|
@@ -225,21 +270,34 @@ END;
|
|
|
225
270
|
return Math.min(clamped, 1000);
|
|
226
271
|
}
|
|
227
272
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Split on non-alphanumeric runs, mirroring FTS5's `unicode61` tokenizer so
|
|
275
|
+
* query tokens align with how stored prompts were indexed. Lowercases for
|
|
276
|
+
* stable substring matching.
|
|
277
|
+
*/
|
|
278
|
+
#tokenize(query: string): string[] {
|
|
279
|
+
return query
|
|
280
|
+
.toLowerCase()
|
|
281
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
282
|
+
.filter(tok => tok.length > 0);
|
|
283
|
+
}
|
|
234
284
|
|
|
235
|
-
|
|
285
|
+
#searchSubstring(tokens: string[], limit: number): HistoryRow[] {
|
|
286
|
+
const stmt = this.#getSubstringStmt(tokens.length);
|
|
287
|
+
const params: unknown[] = tokens.map(tok => `%${escapeLikePattern(tok)}%`);
|
|
288
|
+
params.push(limit);
|
|
289
|
+
return stmt.all(...(params as [string, ...unknown[]])) as HistoryRow[];
|
|
290
|
+
}
|
|
236
291
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
292
|
+
#getSubstringStmt(tokenCount: number): Statement {
|
|
293
|
+
let stmt = this.#substringStmts.get(tokenCount);
|
|
294
|
+
if (stmt) return stmt;
|
|
295
|
+
const whereClause = Array(tokenCount).fill("prompt LIKE ? ESCAPE '\\' COLLATE NOCASE").join(" AND ");
|
|
296
|
+
stmt = this.#db.prepare(
|
|
297
|
+
`SELECT id, prompt, created_at, cwd FROM history WHERE ${whereClause} ORDER BY created_at DESC, id DESC LIMIT ?`,
|
|
298
|
+
);
|
|
299
|
+
this.#substringStmts.set(tokenCount, stmt);
|
|
300
|
+
return stmt;
|
|
243
301
|
}
|
|
244
302
|
|
|
245
303
|
#toEntry(row: HistoryRow): HistoryEntry {
|
|
@@ -275,6 +275,7 @@ export type ReadonlySessionManager = Pick<
|
|
|
275
275
|
| "getSessionFile"
|
|
276
276
|
| "getSessionName"
|
|
277
277
|
| "getArtifactsDir"
|
|
278
|
+
| "getArtifactManager"
|
|
278
279
|
| "allocateArtifactPath"
|
|
279
280
|
| "saveArtifact"
|
|
280
281
|
| "getArtifactPath"
|
|
@@ -1622,6 +1623,10 @@ export class SessionManager {
|
|
|
1622
1623
|
#persistErrorReported = false;
|
|
1623
1624
|
#artifactManager: ArtifactManager | null = null;
|
|
1624
1625
|
#artifactManagerSessionFile: string | null = null;
|
|
1626
|
+
// When set, take precedence over the lazily-derived per-session manager.
|
|
1627
|
+
// Subagents adopt the parent's manager so artifact IDs are unique across the
|
|
1628
|
+
// whole agent tree and all files land in the parent's artifacts dir.
|
|
1629
|
+
#adoptedArtifactManager: ArtifactManager | null = null;
|
|
1625
1630
|
// In-memory artifact fallback for non-persistent sessions (persist=false).
|
|
1626
1631
|
// Keyed by sequential numeric ID string; mirrors the file-based ArtifactManager ID scheme.
|
|
1627
1632
|
#inMemoryArtifacts: Map<string, string> | null = null;
|
|
@@ -1675,6 +1680,7 @@ export class SessionManager {
|
|
|
1675
1680
|
this.#persistErrorReported = false;
|
|
1676
1681
|
this.#artifactManager = null;
|
|
1677
1682
|
this.#artifactManagerSessionFile = null;
|
|
1683
|
+
this.#adoptedArtifactManager = null;
|
|
1678
1684
|
this.#buildIndex();
|
|
1679
1685
|
if (this.#sessionFile) {
|
|
1680
1686
|
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
@@ -2120,17 +2126,40 @@ export class SessionManager {
|
|
|
2120
2126
|
/**
|
|
2121
2127
|
* Returns the session artifacts directory path (session file path without .jsonl).
|
|
2122
2128
|
* Returns null when the session is not persisted to a file.
|
|
2129
|
+
* When this session has adopted an external ArtifactManager (subagent case),
|
|
2130
|
+
* returns that manager's directory so reads/writes land in the shared parent
|
|
2131
|
+
* dir instead of a private (non-existent) subdir.
|
|
2123
2132
|
*/
|
|
2124
2133
|
getArtifactsDir(): string | null {
|
|
2134
|
+
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager.dir;
|
|
2125
2135
|
const sessionFile = this.#sessionFile;
|
|
2126
2136
|
return sessionFile ? sessionFile.slice(0, -6) : null;
|
|
2127
2137
|
}
|
|
2128
2138
|
|
|
2139
|
+
/**
|
|
2140
|
+
* Adopt an externally-owned ArtifactManager. Used by subagents to share
|
|
2141
|
+
* the parent session's artifact directory and ID counter.
|
|
2142
|
+
*/
|
|
2143
|
+
adoptArtifactManager(manager: ArtifactManager): void {
|
|
2144
|
+
this.#adoptedArtifactManager = manager;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* Returns the ArtifactManager this session writes through. Lazily creates
|
|
2149
|
+
* one bound to the current session file unless an external manager was
|
|
2150
|
+
* adopted via `adoptArtifactManager`. Returns null only for non-persistent
|
|
2151
|
+
* sessions with no adopted manager.
|
|
2152
|
+
*/
|
|
2153
|
+
getArtifactManager(): ArtifactManager | null {
|
|
2154
|
+
return this.#getOrCreateArtifactManager();
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2129
2157
|
/**
|
|
2130
2158
|
* Returns an artifact manager bound to the current session file.
|
|
2131
2159
|
* Recreates the manager when the active session file changes.
|
|
2132
2160
|
*/
|
|
2133
2161
|
#getOrCreateArtifactManager(): ArtifactManager | null {
|
|
2162
|
+
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager;
|
|
2134
2163
|
const sessionFile = this.#sessionFile;
|
|
2135
2164
|
if (!sessionFile) {
|
|
2136
2165
|
this.#artifactManager = null;
|
|
@@ -2142,7 +2171,7 @@ export class SessionManager {
|
|
|
2142
2171
|
return this.#artifactManager;
|
|
2143
2172
|
}
|
|
2144
2173
|
|
|
2145
|
-
const manager = new ArtifactManager(sessionFile);
|
|
2174
|
+
const manager = new ArtifactManager(sessionFile.slice(0, -6));
|
|
2146
2175
|
this.#artifactManager = manager;
|
|
2147
2176
|
this.#artifactManagerSessionFile = sessionFile;
|
|
2148
2177
|
return manager;
|
|
@@ -15,6 +15,11 @@ export interface SSHConnectionTarget {
|
|
|
15
15
|
|
|
16
16
|
export type SSHHostOs = "windows" | "linux" | "macos" | "unknown";
|
|
17
17
|
export type SSHHostShell = "cmd" | "powershell" | "bash" | "zsh" | "sh" | "unknown";
|
|
18
|
+
export type SshPlatform = typeof process.platform;
|
|
19
|
+
|
|
20
|
+
export function supportsSshControlMaster(platform: SshPlatform = process.platform): boolean {
|
|
21
|
+
return platform !== "win32";
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
export interface SSHHostInfo {
|
|
20
25
|
version: number;
|
|
@@ -33,6 +38,10 @@ const activeHosts = new Map<string, SSHConnectionTarget>();
|
|
|
33
38
|
const pendingConnections = new Map<string, Promise<void>>();
|
|
34
39
|
const hostInfoCache = new Map<string, SSHHostInfo>();
|
|
35
40
|
|
|
41
|
+
interface SSHArgsOptions {
|
|
42
|
+
platform?: SshPlatform;
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
function ensureControlDir() {
|
|
37
46
|
fs.mkdirSync(CONTROL_DIR, { recursive: true, mode: 0o700 });
|
|
38
47
|
try {
|
|
@@ -66,20 +75,14 @@ async function validateKeyPermissions(keyPath?: string): Promise<void> {
|
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
function buildCommonArgs(host: SSHConnectionTarget): string[] {
|
|
70
|
-
const args = [
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"ControlMaster=auto",
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"ControlPersist=3600",
|
|
78
|
-
"-o",
|
|
79
|
-
"BatchMode=yes",
|
|
80
|
-
"-o",
|
|
81
|
-
"StrictHostKeyChecking=accept-new",
|
|
82
|
-
];
|
|
78
|
+
function buildCommonArgs(host: SSHConnectionTarget, options?: SSHArgsOptions): string[] {
|
|
79
|
+
const args = ["-n"];
|
|
80
|
+
|
|
81
|
+
if (supportsSshControlMaster(options?.platform)) {
|
|
82
|
+
args.push("-o", "ControlMaster=auto", "-o", `ControlPath=${CONTROL_PATH}`, "-o", "ControlPersist=3600");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
args.push("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new");
|
|
83
86
|
|
|
84
87
|
if (host.port) {
|
|
85
88
|
args.push("-p", String(host.port));
|
|
@@ -357,9 +360,13 @@ export async function ensureHostInfo(host: SSHConnectionTarget): Promise<SSHHost
|
|
|
357
360
|
return probeHostInfo(host);
|
|
358
361
|
}
|
|
359
362
|
|
|
360
|
-
export async function buildRemoteCommand(
|
|
363
|
+
export async function buildRemoteCommand(
|
|
364
|
+
host: SSHConnectionTarget,
|
|
365
|
+
command: string,
|
|
366
|
+
options?: SSHArgsOptions,
|
|
367
|
+
): Promise<string[]> {
|
|
361
368
|
await validateKeyPermissions(host.keyPath);
|
|
362
|
-
return [...buildCommonArgs(host), buildSshTarget(host.username, host.host), command];
|
|
369
|
+
return [...buildCommonArgs(host, options), buildSshTarget(host.username, host.host), command];
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
let registered = false;
|
|
@@ -385,6 +392,14 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
|
|
385
392
|
}
|
|
386
393
|
|
|
387
394
|
const target = buildSshTarget(host.username, host.host);
|
|
395
|
+
if (!supportsSshControlMaster()) {
|
|
396
|
+
activeHosts.set(key, host);
|
|
397
|
+
if (!hostInfoCache.has(key) && !(await loadHostInfoFromDisk(host))) {
|
|
398
|
+
await probeHostInfo(host);
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
388
403
|
const check = await runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
|
|
389
404
|
if (check.exitCode === 0) {
|
|
390
405
|
activeHosts.set(key, host);
|
|
@@ -415,6 +430,7 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
|
|
415
430
|
}
|
|
416
431
|
|
|
417
432
|
async function closeConnectionInternal(host: SSHConnectionTarget): Promise<void> {
|
|
433
|
+
if (!supportsSshControlMaster()) return;
|
|
418
434
|
const target = buildSshTarget(host.username, host.host);
|
|
419
435
|
await runSshSync(["-O", "exit", ...buildCommonArgs(host), target]);
|
|
420
436
|
}
|
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
|
}
|