@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
|
@@ -456,6 +456,10 @@
|
|
|
456
456
|
const content = truncate(normalize(extractContent(msg.content)));
|
|
457
457
|
return labelHtml + `<span class="tree-role-user">user:</span> ${escapeHtml(content)}`;
|
|
458
458
|
}
|
|
459
|
+
if (msg.role === 'developer') {
|
|
460
|
+
const content = truncate(normalize(extractContent(msg.content)));
|
|
461
|
+
return labelHtml + `<span class="tree-role-developer">developer:</span> ${escapeHtml(content)}`;
|
|
462
|
+
}
|
|
459
463
|
if (msg.role === 'assistant') {
|
|
460
464
|
const textContent = truncate(normalize(extractContent(msg.content)));
|
|
461
465
|
if (textContent) {
|
|
@@ -1258,12 +1262,14 @@
|
|
|
1258
1262
|
return html;
|
|
1259
1263
|
}
|
|
1260
1264
|
|
|
1261
|
-
// Parse `***
|
|
1262
|
-
// `===== <info> =====`
|
|
1263
|
-
// before
|
|
1265
|
+
// Parse `*** Cell <attrs>` headers (canonical), plus legacy
|
|
1266
|
+
// `*** Begin <LANG>` headers and `===== <info> =====` bars used in
|
|
1267
|
+
// older transcripts. Cells emitted before each format cutover still
|
|
1268
|
+
// need to render in HTML exports.
|
|
1264
1269
|
function parseEvalCells(input) {
|
|
1265
1270
|
const text = String(input);
|
|
1266
|
-
if (/^[*]{2,}\s*
|
|
1271
|
+
if (/^[*]{2,}\s*Cell\b/im.test(text)) return parseEvalCellsCell(text);
|
|
1272
|
+
if (/^[*]{2,}\s*Begin\b/im.test(text)) return parseEvalCellsBegin(text);
|
|
1267
1273
|
return parseEvalCellsLegacy(text);
|
|
1268
1274
|
}
|
|
1269
1275
|
|
|
@@ -1275,7 +1281,93 @@
|
|
|
1275
1281
|
return null;
|
|
1276
1282
|
}
|
|
1277
1283
|
|
|
1278
|
-
|
|
1284
|
+
// Tokenize a `*** Cell` header attribute list, preserving quoted
|
|
1285
|
+
// segments. Mirrors `tokenizeCellAttrs` in src/eval/parse.ts.
|
|
1286
|
+
function tokenizeCellAttrsHtml(input) {
|
|
1287
|
+
const tokens = [];
|
|
1288
|
+
let i = 0;
|
|
1289
|
+
while (i < input.length) {
|
|
1290
|
+
while (i < input.length && /\s/.test(input[i])) i++;
|
|
1291
|
+
if (i >= input.length) break;
|
|
1292
|
+
let tok = '';
|
|
1293
|
+
while (i < input.length && !/\s/.test(input[i])) {
|
|
1294
|
+
const ch = input[i];
|
|
1295
|
+
if (ch === '"' || ch === "'") {
|
|
1296
|
+
tok += ch; i++;
|
|
1297
|
+
while (i < input.length && input[i] !== ch) { tok += input[i]; i++; }
|
|
1298
|
+
if (i < input.length) { tok += input[i]; i++; }
|
|
1299
|
+
} else { tok += ch; i++; }
|
|
1300
|
+
}
|
|
1301
|
+
tokens.push(tok);
|
|
1302
|
+
}
|
|
1303
|
+
return tokens;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function parseEvalCellsCell(text) {
|
|
1307
|
+
const STARS = '\\*{2,}';
|
|
1308
|
+
const CELL = new RegExp('^' + STARS + '\\s*Cell\\b\\s*(.*)$', 'i');
|
|
1309
|
+
const END = new RegExp('^' + STARS + '\\s*End\\b.*$', 'i');
|
|
1310
|
+
const ATTR = /^([a-zA-Z][\w-]*)(?::(?:"([^"]*)"|'([^']*)'|(.*)))?$/;
|
|
1311
|
+
const DUR = /^\d+(?:ms|s|m)?$/;
|
|
1312
|
+
const ID_KEYS = ['id', 'title', 'name', 'cell', 'file', 'label'];
|
|
1313
|
+
const T_KEYS = ['t', 'timeout', 'duration', 'time'];
|
|
1314
|
+
const RST_KEYS = ['rst', 'reset'];
|
|
1315
|
+
const lines = text.split('\n');
|
|
1316
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
1317
|
+
const cells = [];
|
|
1318
|
+
let i = 0;
|
|
1319
|
+
while (i < lines.length && lines[i].trim() === '') i++;
|
|
1320
|
+
while (i < lines.length) {
|
|
1321
|
+
const m = CELL.exec(lines[i]);
|
|
1322
|
+
if (!m) { i++; continue; }
|
|
1323
|
+
const tokens = tokenizeCellAttrsHtml(m[1] || '');
|
|
1324
|
+
let lang = null;
|
|
1325
|
+
let title = '';
|
|
1326
|
+
const attrs = [];
|
|
1327
|
+
let bareReset = false;
|
|
1328
|
+
const titleParts = [];
|
|
1329
|
+
for (const tok of tokens) {
|
|
1330
|
+
const lower = tok.toLowerCase();
|
|
1331
|
+
if (RST_KEYS.indexOf(lower) >= 0) { bareReset = true; continue; }
|
|
1332
|
+
const am = ATTR.exec(tok);
|
|
1333
|
+
if (am && tok.indexOf(':') >= 0) {
|
|
1334
|
+
const key = am[1].toLowerCase();
|
|
1335
|
+
const value = am[2] !== undefined ? am[2] : am[3] !== undefined ? am[3] : (am[4] || '');
|
|
1336
|
+
const lc = evalLangAlias(key);
|
|
1337
|
+
if (lc) {
|
|
1338
|
+
if (!lang) lang = lc;
|
|
1339
|
+
if (!title && value) title = value;
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
if (ID_KEYS.indexOf(key) >= 0) { if (!title) title = value; continue; }
|
|
1343
|
+
if (T_KEYS.indexOf(key) >= 0) { attrs.push('t=' + value); continue; }
|
|
1344
|
+
if (RST_KEYS.indexOf(key) >= 0) { attrs.push('rst'); continue; }
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const lc = evalLangAlias(tok);
|
|
1348
|
+
if (lc && !lang) { lang = lc; continue; }
|
|
1349
|
+
if (DUR.test(tok)) { attrs.push('t=' + tok); continue; }
|
|
1350
|
+
titleParts.push(tok);
|
|
1351
|
+
}
|
|
1352
|
+
if (!title && titleParts.length) title = titleParts.join(' ');
|
|
1353
|
+
if (bareReset) attrs.push('rst');
|
|
1354
|
+
lang = lang || 'py';
|
|
1355
|
+
i++;
|
|
1356
|
+
const codeLines = [];
|
|
1357
|
+
while (i < lines.length) {
|
|
1358
|
+
if (END.test(lines[i])) { i++; break; }
|
|
1359
|
+
if (CELL.test(lines[i])) break;
|
|
1360
|
+
codeLines.push(lines[i]);
|
|
1361
|
+
i++;
|
|
1362
|
+
}
|
|
1363
|
+
while (codeLines.length && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
|
|
1364
|
+
cells.push({ lang, title, attrs, code: codeLines.join('\n') });
|
|
1365
|
+
while (i < lines.length && lines[i].trim() === '') i++;
|
|
1366
|
+
}
|
|
1367
|
+
return cells;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function parseEvalCellsBegin(text) {
|
|
1279
1371
|
const STARS = '\\*{2,}';
|
|
1280
1372
|
const BEGIN = new RegExp('^' + STARS + '\\s*Begin\\b\\s*(\\S+)?\\s*$', 'i');
|
|
1281
1373
|
const END = new RegExp('^' + STARS + '\\s*End\\b.*$', 'i');
|
|
@@ -1648,6 +1740,18 @@
|
|
|
1648
1740
|
return html;
|
|
1649
1741
|
}
|
|
1650
1742
|
|
|
1743
|
+
if (msg.role === 'developer') {
|
|
1744
|
+
let html = `<div class="user-message developer-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
|
1745
|
+
const content = msg.content;
|
|
1746
|
+
const text = typeof content === 'string' ? content :
|
|
1747
|
+
content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
1748
|
+
if (text.trim()) {
|
|
1749
|
+
html += `<div class="markdown-content">${safeMarkedParse(text)}</div>`;
|
|
1750
|
+
}
|
|
1751
|
+
html += '</div>';
|
|
1752
|
+
return html;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1651
1755
|
if (msg.role === 'assistant') {
|
|
1652
1756
|
let html = `<div class="assistant-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
|
1653
1757
|
|
|
@@ -1750,7 +1854,7 @@
|
|
|
1750
1854
|
// ============================================================
|
|
1751
1855
|
|
|
1752
1856
|
function computeStats(entryList) {
|
|
1753
|
-
let userMessages = 0, assistantMessages = 0, toolResults = 0;
|
|
1857
|
+
let userMessages = 0, developerMessages = 0, assistantMessages = 0, toolResults = 0;
|
|
1754
1858
|
let customMessages = 0, compactions = 0, branchSummaries = 0, toolCalls = 0;
|
|
1755
1859
|
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1756
1860
|
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
@@ -1760,6 +1864,7 @@
|
|
|
1760
1864
|
if (entry.type === 'message') {
|
|
1761
1865
|
const msg = entry.message;
|
|
1762
1866
|
if (msg.role === 'user') userMessages++;
|
|
1867
|
+
if (msg.role === 'developer') developerMessages++;
|
|
1763
1868
|
if (msg.role === 'assistant') {
|
|
1764
1869
|
assistantMessages++;
|
|
1765
1870
|
if (msg.model) models.add(msg.provider ? `${msg.provider}/${msg.model}` : msg.model);
|
|
@@ -1787,7 +1892,7 @@
|
|
|
1787
1892
|
}
|
|
1788
1893
|
}
|
|
1789
1894
|
|
|
1790
|
-
return { userMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) };
|
|
1895
|
+
return { userMessages, developerMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) };
|
|
1791
1896
|
}
|
|
1792
1897
|
|
|
1793
1898
|
const globalStats = computeStats(entries);
|
|
@@ -1803,6 +1908,7 @@
|
|
|
1803
1908
|
|
|
1804
1909
|
const msgParts = [];
|
|
1805
1910
|
if (globalStats.userMessages) msgParts.push(`${globalStats.userMessages} user`);
|
|
1911
|
+
if (globalStats.developerMessages) msgParts.push(`${globalStats.developerMessages} developer`);
|
|
1806
1912
|
if (globalStats.assistantMessages) msgParts.push(`${globalStats.assistantMessages} assistant`);
|
|
1807
1913
|
if (globalStats.toolResults) msgParts.push(`${globalStats.toolResults} tool results`);
|
|
1808
1914
|
if (globalStats.customMessages) msgParts.push(`${globalStats.customMessages} custom`);
|
|
@@ -117,6 +117,31 @@ export async function getEnabledPlugins(cwd: string): Promise<InstalledPlugin[]>
|
|
|
117
117
|
// Path Resolution
|
|
118
118
|
// =============================================================================
|
|
119
119
|
|
|
120
|
+
const MANIFEST_ENTRY_INDEX_NAMES = ["index.ts", "index.js", "index.mjs", "index.cjs"];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a plugin manifest entry to a concrete loadable file path. Returns the
|
|
124
|
+
* file path itself when the entry points at a file, the matching index file when
|
|
125
|
+
* the entry points at a directory containing index.{ts,js,mjs,cjs}, and null
|
|
126
|
+
* when no entry exists at the joined path.
|
|
127
|
+
*/
|
|
128
|
+
function resolveManifestEntryFile(joined: string): string | null {
|
|
129
|
+
let stats: fs.Stats;
|
|
130
|
+
try {
|
|
131
|
+
stats = fs.statSync(joined);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (stats.isDirectory()) {
|
|
136
|
+
for (const name of MANIFEST_ENTRY_INDEX_NAMES) {
|
|
137
|
+
const candidate = path.join(joined, name);
|
|
138
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return joined;
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
/**
|
|
121
146
|
* Generic path resolver for plugin manifest entries (tools, hooks, commands, extensions).
|
|
122
147
|
* Handles both single-string and string[] base entries, plus feature-specific entries.
|
|
@@ -130,8 +155,8 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
|
|
|
130
155
|
if (base) {
|
|
131
156
|
const entries = Array.isArray(base) ? base : [base];
|
|
132
157
|
for (const entry of entries) {
|
|
133
|
-
const resolved = path.join(plugin.path, entry);
|
|
134
|
-
if (
|
|
158
|
+
const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
|
|
159
|
+
if (resolved) {
|
|
135
160
|
paths.push(resolved);
|
|
136
161
|
}
|
|
137
162
|
}
|
|
@@ -146,8 +171,8 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
|
|
|
146
171
|
|
|
147
172
|
if (feat[key]) {
|
|
148
173
|
for (const entry of feat[key]) {
|
|
149
|
-
const resolved = path.join(plugin.path, entry);
|
|
150
|
-
if (
|
|
174
|
+
const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
|
|
175
|
+
if (resolved) {
|
|
151
176
|
paths.push(resolved);
|
|
152
177
|
}
|
|
153
178
|
}
|
|
@@ -160,8 +185,8 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
|
|
|
160
185
|
|
|
161
186
|
if (feat[key]) {
|
|
162
187
|
for (const entry of feat[key]) {
|
|
163
|
-
const resolved = path.join(plugin.path, entry);
|
|
164
|
-
if (
|
|
188
|
+
const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
|
|
189
|
+
if (resolved) {
|
|
165
190
|
paths.push(resolved);
|
|
166
191
|
}
|
|
167
192
|
}
|
|
@@ -28,6 +28,26 @@ export interface LoadSkillsResult {
|
|
|
28
28
|
warnings: SkillWarning[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
let activeSkills: readonly Skill[] = [];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Process-global snapshot of skills the active session loaded.
|
|
35
|
+
* Read by internal URL protocol handlers (skill://).
|
|
36
|
+
*/
|
|
37
|
+
export function getActiveSkills(): readonly Skill[] {
|
|
38
|
+
return activeSkills;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Replace the active skill snapshot. Called once per top-level session. */
|
|
42
|
+
export function setActiveSkills(value: readonly Skill[]): void {
|
|
43
|
+
activeSkills = value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Reset the active skill snapshot. Test-only. */
|
|
47
|
+
export function resetActiveSkillsForTests(): void {
|
|
48
|
+
activeSkills = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
export interface LoadSkillsFromDirOptions {
|
|
32
52
|
/** Directory to scan for skills */
|
|
33
53
|
dir: string;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protocol handler for agent:// URLs.
|
|
3
3
|
*
|
|
4
|
-
* Resolves agent output IDs
|
|
4
|
+
* Resolves agent output IDs against the artifacts directories of every active
|
|
5
|
+
* session. Parents and subagents share outputs via this registry: a subagent
|
|
6
|
+
* can read its parent's output IDs because both sessions are registered in
|
|
7
|
+
* the shared context.
|
|
5
8
|
*
|
|
6
9
|
* URL forms:
|
|
7
10
|
* - agent://<id> - Full output content
|
|
@@ -11,27 +14,27 @@
|
|
|
11
14
|
import * as fs from "node:fs/promises";
|
|
12
15
|
import * as path from "node:path";
|
|
13
16
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
17
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
14
18
|
import { applyQuery, pathToQuery } from "./json-query";
|
|
15
19
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
16
20
|
|
|
17
|
-
export interface AgentProtocolOptions {
|
|
18
|
-
/**
|
|
19
|
-
* Returns the artifacts directory path, or null if no session.
|
|
20
|
-
* Artifacts directory is the session file path without .jsonl extension.
|
|
21
|
-
*/
|
|
22
|
-
getArtifactsDir: () => string | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
/**
|
|
26
|
-
*
|
|
22
|
+
* Snapshot of artifacts dirs for every registered session, deduped.
|
|
23
|
+
*
|
|
24
|
+
* Prefers `sessionManager.getArtifactsDir()` because subagents adopt the
|
|
25
|
+
* parent's manager and report the parent's dir there; dedup then collapses
|
|
26
|
+
* the whole agent tree to one entry. Falls back to the raw session file
|
|
27
|
+
* when no live session reference is attached.
|
|
27
28
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
function artifactsDirsFromRegistry(): string[] {
|
|
30
|
+
const dirs: string[] = [];
|
|
31
|
+
for (const ref of AgentRegistry.global().list()) {
|
|
32
|
+
const dir =
|
|
33
|
+
ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
|
|
34
|
+
if (!dir) continue;
|
|
35
|
+
if (!dirs.includes(dir)) dirs.push(dir);
|
|
34
36
|
}
|
|
37
|
+
return dirs;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -44,30 +47,12 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
44
47
|
readonly scheme = "agent";
|
|
45
48
|
readonly immutable = true;
|
|
46
49
|
|
|
47
|
-
constructor(private readonly options: AgentProtocolOptions) {}
|
|
48
|
-
|
|
49
50
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
50
|
-
const artifactsDir = this.options.getArtifactsDir();
|
|
51
|
-
if (!artifactsDir) {
|
|
52
|
-
throw new Error("No session - agent outputs unavailable");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
await fs.stat(artifactsDir);
|
|
57
|
-
} catch (err) {
|
|
58
|
-
if (isEnoent(err)) {
|
|
59
|
-
throw new Error("No artifacts directory found");
|
|
60
|
-
}
|
|
61
|
-
throw err;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Extract output ID from host
|
|
65
51
|
const outputId = url.rawHost || url.hostname;
|
|
66
52
|
if (!outputId) {
|
|
67
53
|
throw new Error("agent:// URL requires an output ID: agent://<id>");
|
|
68
54
|
}
|
|
69
55
|
|
|
70
|
-
// Check for conflicting extraction methods
|
|
71
56
|
const urlPath = url.pathname;
|
|
72
57
|
const queryParam = url.searchParams.get("q");
|
|
73
58
|
const hasPathExtraction = urlPath && urlPath !== "/" && urlPath !== "";
|
|
@@ -77,28 +62,57 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
77
62
|
throw new Error("agent:// URL cannot combine path extraction with ?q=");
|
|
78
63
|
}
|
|
79
64
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
const dirs = artifactsDirsFromRegistry();
|
|
66
|
+
|
|
67
|
+
if (dirs.length === 0) {
|
|
68
|
+
throw new Error("No session - agent outputs unavailable");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let foundPath: string | undefined;
|
|
72
|
+
let anyDirExists = false;
|
|
73
|
+
const availableIds = new Set<string>();
|
|
74
|
+
|
|
75
|
+
for (const dir of dirs) {
|
|
76
|
+
try {
|
|
77
|
+
await fs.stat(dir);
|
|
78
|
+
anyDirExists = true;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (isEnoent(err)) continue;
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
const candidate = path.join(dir, `${outputId}.md`);
|
|
84
|
+
try {
|
|
85
|
+
await fs.stat(candidate);
|
|
86
|
+
foundPath = candidate;
|
|
87
|
+
break;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (!isEnoent(err)) throw err;
|
|
90
|
+
try {
|
|
91
|
+
const files = await fs.readdir(dir);
|
|
92
|
+
for (const f of files) {
|
|
93
|
+
if (f.endsWith(".md")) availableIds.add(f.replace(/\.md$/, ""));
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Listing failures are non-fatal; continue searching.
|
|
97
|
+
}
|
|
89
98
|
}
|
|
90
|
-
throw err;
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
if (!anyDirExists) {
|
|
102
|
+
throw new Error("No artifacts directory found");
|
|
103
|
+
}
|
|
95
104
|
|
|
96
|
-
|
|
105
|
+
if (!foundPath) {
|
|
106
|
+
const availableStr = availableIds.size > 0 ? [...availableIds].join(", ") : "none";
|
|
107
|
+
throw new Error(`Not found: ${outputId}\nAvailable: ${availableStr}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rawContent = await Bun.file(foundPath).text();
|
|
111
|
+
const notes: string[] = [];
|
|
97
112
|
let content = rawContent;
|
|
98
113
|
let contentType: InternalResource["contentType"] = "text/markdown";
|
|
99
114
|
|
|
100
115
|
if (hasPathExtraction || hasQueryExtraction) {
|
|
101
|
-
// Parse JSON
|
|
102
116
|
let jsonValue: unknown;
|
|
103
117
|
try {
|
|
104
118
|
jsonValue = JSON.parse(rawContent);
|
|
@@ -107,9 +121,7 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
107
121
|
throw new Error(`Output ${outputId} is not valid JSON: ${message}`);
|
|
108
122
|
}
|
|
109
123
|
|
|
110
|
-
// Convert path to query if needed
|
|
111
124
|
const query = hasPathExtraction ? pathToQuery(urlPath) : queryParam!;
|
|
112
|
-
|
|
113
125
|
if (query) {
|
|
114
126
|
const extracted = applyQuery(jsonValue, query);
|
|
115
127
|
try {
|
|
@@ -119,7 +131,6 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
119
131
|
}
|
|
120
132
|
notes.push(`Extracted: ${query}`);
|
|
121
133
|
} else {
|
|
122
|
-
// Empty path/query means return full JSON
|
|
123
134
|
content = JSON.stringify(jsonValue, null, 2);
|
|
124
135
|
}
|
|
125
136
|
contentType = "application/json";
|
|
@@ -130,7 +141,7 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
130
141
|
content,
|
|
131
142
|
contentType,
|
|
132
143
|
size: Buffer.byteLength(content, "utf-8"),
|
|
133
|
-
sourcePath:
|
|
144
|
+
sourcePath: foundPath,
|
|
134
145
|
notes,
|
|
135
146
|
};
|
|
136
147
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protocol handler for artifact:// URLs.
|
|
3
3
|
*
|
|
4
|
-
* Resolves artifact IDs
|
|
5
|
-
* Unlike agent://, artifacts are raw text with no JSON extraction.
|
|
4
|
+
* Resolves artifact IDs against the artifacts directories of every active
|
|
5
|
+
* session. Unlike agent://, artifacts are raw text with no JSON extraction.
|
|
6
6
|
*
|
|
7
7
|
* URL form:
|
|
8
8
|
* - artifact://<id> - Full artifact content
|
|
@@ -12,87 +12,87 @@
|
|
|
12
12
|
import * as fs from "node:fs/promises";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
15
16
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
16
17
|
|
|
17
|
-
export interface ArtifactProtocolOptions {
|
|
18
|
-
/**
|
|
19
|
-
* Returns the artifacts directory path, or null if no session.
|
|
20
|
-
*/
|
|
21
|
-
getArtifactsDir: () => string | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
/**
|
|
25
|
-
*
|
|
19
|
+
* Snapshot of artifacts dirs across all registered sessions, deduped.
|
|
20
|
+
*
|
|
21
|
+
* Subagents adopt their parent's `ArtifactManager`, so their
|
|
22
|
+
* `sessionManager.getArtifactsDir()` returns the parent's dir; dedup
|
|
23
|
+
* collapses parent + N subagents to a single entry.
|
|
26
24
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} catch {
|
|
35
|
-
return [];
|
|
25
|
+
function artifactsDirsFromRegistry(): string[] {
|
|
26
|
+
const dirs: string[] = [];
|
|
27
|
+
for (const ref of AgentRegistry.global().list()) {
|
|
28
|
+
const dir =
|
|
29
|
+
ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
|
|
30
|
+
if (!dir) continue;
|
|
31
|
+
if (!dirs.includes(dir)) dirs.push(dir);
|
|
36
32
|
}
|
|
33
|
+
return dirs;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
/**
|
|
40
|
-
* Handler for artifact:// URLs.
|
|
41
|
-
*
|
|
42
|
-
* Resolves numeric artifact IDs to their text content.
|
|
43
|
-
* Artifacts are created by tools when output is truncated.
|
|
44
|
-
*/
|
|
45
36
|
export class ArtifactProtocolHandler implements ProtocolHandler {
|
|
46
37
|
readonly scheme = "artifact";
|
|
47
38
|
readonly immutable = true;
|
|
48
39
|
|
|
49
|
-
constructor(private readonly options: ArtifactProtocolOptions) {}
|
|
50
|
-
|
|
51
40
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
52
|
-
const artifactsDir = this.options.getArtifactsDir();
|
|
53
|
-
if (!artifactsDir) {
|
|
54
|
-
throw new Error("No session - artifacts unavailable");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Extract artifact ID from host
|
|
58
41
|
const id = url.rawHost || url.hostname;
|
|
59
42
|
if (!id) {
|
|
60
43
|
throw new Error("artifact:// URL requires a numeric ID: artifact://0");
|
|
61
44
|
}
|
|
62
|
-
|
|
63
|
-
// Validate ID is numeric
|
|
64
45
|
if (!/^\d+$/.test(id)) {
|
|
65
46
|
throw new Error(`artifact:// ID must be numeric, got: ${id}`);
|
|
66
47
|
}
|
|
67
48
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
49
|
+
const dirs = artifactsDirsFromRegistry();
|
|
50
|
+
|
|
51
|
+
if (dirs.length === 0) {
|
|
52
|
+
throw new Error("No session - artifacts unavailable");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let foundPath: string | undefined;
|
|
56
|
+
let anyDirExists = false;
|
|
57
|
+
const availableIds = new Set<string>();
|
|
58
|
+
|
|
59
|
+
for (const dir of dirs) {
|
|
60
|
+
let files: string[];
|
|
61
|
+
try {
|
|
62
|
+
files = await fs.readdir(dir);
|
|
63
|
+
anyDirExists = true;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (isEnoent(err)) continue;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
const match = files.find(f => f.startsWith(`${id}.`));
|
|
69
|
+
if (match) {
|
|
70
|
+
foundPath = path.join(dir, match);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
for (const f of files) {
|
|
74
|
+
const m = f.match(/^(\d+)\./);
|
|
75
|
+
if (m) availableIds.add(m[1]);
|
|
75
76
|
}
|
|
76
|
-
throw err;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
if (!anyDirExists) {
|
|
80
|
+
throw new Error("No artifacts directory found");
|
|
81
|
+
}
|
|
80
82
|
|
|
81
|
-
if (!
|
|
82
|
-
const
|
|
83
|
-
const availableStr =
|
|
83
|
+
if (!foundPath) {
|
|
84
|
+
const sorted = [...availableIds].sort((a, b) => Number(a) - Number(b));
|
|
85
|
+
const availableStr = sorted.length > 0 ? sorted.join(", ") : "none";
|
|
84
86
|
throw new Error(`Artifact ${id} not found. Available: ${availableStr}`);
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
const
|
|
88
|
-
const content = await Bun.file(filePath).text();
|
|
89
|
-
|
|
89
|
+
const content = await Bun.file(foundPath).text();
|
|
90
90
|
return {
|
|
91
91
|
url: url.href,
|
|
92
92
|
content,
|
|
93
93
|
contentType: "text/plain",
|
|
94
94
|
size: Buffer.byteLength(content, "utf-8"),
|
|
95
|
-
sourcePath:
|
|
95
|
+
sourcePath: foundPath,
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
}
|