pi-subagents 0.24.3 → 0.25.0
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 +26 -5
- package/README.md +19 -11
- package/package.json +4 -8
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +46 -10
- package/src/agents/agent-management.ts +5 -0
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +30 -6
- package/src/agents/skills.ts +25 -23
- package/src/extension/config.ts +16 -0
- package/src/extension/fanout-child.ts +170 -0
- package/src/extension/index.ts +13 -25
- package/src/intercom/intercom-bridge.ts +2 -1
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +107 -7
- package/src/runs/background/async-job-tracker.ts +57 -14
- package/src/runs/background/async-resume.ts +28 -15
- package/src/runs/background/async-status.ts +60 -30
- package/src/runs/background/result-watcher.ts +111 -54
- package/src/runs/background/run-id-resolver.ts +83 -0
- package/src/runs/background/run-status.ts +79 -3
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +66 -18
- package/src/runs/foreground/chain-execution.ts +6 -0
- package/src/runs/foreground/execution.ts +21 -5
- package/src/runs/foreground/subagent-executor.ts +314 -18
- package/src/runs/shared/completion-guard.ts +23 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- package/src/runs/shared/nested-events.ts +819 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +115 -0
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +67 -5
- package/src/runs/shared/run-history.ts +12 -7
- package/src/runs/shared/single-output.ts +12 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +25 -5
- package/src/shared/artifacts.ts +2 -2
- package/src/shared/types.ts +95 -0
- package/src/shared/utils.ts +11 -1
- package/src/tui/render.ts +254 -153
package/src/agents/skills.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { execSync } from "node:child_process";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { getAgentDir } from "../shared/utils.ts";
|
|
9
10
|
|
|
10
11
|
export type SkillSource =
|
|
11
12
|
| "project"
|
|
@@ -46,11 +47,10 @@ interface SkillSearchPath {
|
|
|
46
47
|
const skillCache = new Map<string, SkillCacheEntry>();
|
|
47
48
|
const MAX_CACHE_SIZE = 50;
|
|
48
49
|
|
|
49
|
-
let loadSkillsCache: { cwd: string; skills: CachedSkillEntry[]; timestamp: number } | null = null;
|
|
50
|
+
let loadSkillsCache: { cwd: string; agentDir: string; skills: CachedSkillEntry[]; timestamp: number } | null = null;
|
|
50
51
|
const LOAD_SKILLS_CACHE_TTL_MS = 5000;
|
|
51
52
|
|
|
52
53
|
const CONFIG_DIR = ".pi";
|
|
53
|
-
const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
|
|
54
54
|
const SUBAGENT_ORCHESTRATION_SKILL = "pi-subagents";
|
|
55
55
|
|
|
56
56
|
const SOURCE_PRIORITY: Record<SkillSource, number> = {
|
|
@@ -133,10 +133,10 @@ function getGlobalNpmRoot(): string | null {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
function collectInstalledPackageSkillPaths(cwd: string): SkillSearchPath[] {
|
|
136
|
+
function collectInstalledPackageSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
|
|
137
137
|
const dirs: SkillSearchPath[] = [
|
|
138
138
|
{ path: path.join(cwd, CONFIG_DIR, "npm", "node_modules"), source: "project-package" },
|
|
139
|
-
{ path: path.join(
|
|
139
|
+
{ path: path.join(agentDir, "npm", "node_modules"), source: "user-package" },
|
|
140
140
|
];
|
|
141
141
|
|
|
142
142
|
const globalRoot = getGlobalNpmRoot();
|
|
@@ -184,11 +184,11 @@ function collectInstalledPackageSkillPaths(cwd: string): SkillSearchPath[] {
|
|
|
184
184
|
return results;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function collectSettingsSkillPaths(cwd: string): SkillSearchPath[] {
|
|
187
|
+
function collectSettingsSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
|
|
188
188
|
const results: SkillSearchPath[] = [];
|
|
189
189
|
const settingsFiles = [
|
|
190
190
|
{ file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR), source: "project-settings" as const },
|
|
191
|
-
{ file: path.join(
|
|
191
|
+
{ file: path.join(agentDir, "settings.json"), base: agentDir, source: "user-settings" as const },
|
|
192
192
|
];
|
|
193
193
|
|
|
194
194
|
for (const { file, base, source } of settingsFiles) {
|
|
@@ -285,10 +285,10 @@ function resolveSettingsPackageRoot(source: string, baseDir: string): string | u
|
|
|
285
285
|
return undefined;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
function collectSettingsPackageSkillPaths(cwd: string): SkillSearchPath[] {
|
|
288
|
+
function collectSettingsPackageSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
|
|
289
289
|
const settingsFiles = [
|
|
290
290
|
{ file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR), source: "project-package" as const },
|
|
291
|
-
{ file: path.join(
|
|
291
|
+
{ file: path.join(agentDir, "settings.json"), base: agentDir, source: "user-package" as const },
|
|
292
292
|
];
|
|
293
293
|
const results: SkillSearchPath[] = [];
|
|
294
294
|
|
|
@@ -315,16 +315,16 @@ function collectSettingsPackageSkillPaths(cwd: string): SkillSearchPath[] {
|
|
|
315
315
|
return results;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
function buildSkillPaths(cwd: string): SkillSearchPath[] {
|
|
318
|
+
function buildSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
|
|
319
319
|
const skillPaths: SkillSearchPath[] = [
|
|
320
320
|
{ path: path.join(cwd, CONFIG_DIR, "skills"), source: "project" },
|
|
321
321
|
{ path: path.join(cwd, ".agents", "skills"), source: "project" },
|
|
322
|
-
{ path: path.join(
|
|
322
|
+
{ path: path.join(agentDir, "skills"), source: "user" },
|
|
323
323
|
{ path: path.join(os.homedir(), ".agents", "skills"), source: "user" },
|
|
324
|
-
...collectInstalledPackageSkillPaths(cwd),
|
|
325
|
-
...collectSettingsPackageSkillPaths(cwd),
|
|
324
|
+
...collectInstalledPackageSkillPaths(cwd, agentDir),
|
|
325
|
+
...collectSettingsPackageSkillPaths(cwd, agentDir),
|
|
326
326
|
...extractSkillPathsFromPackageRoot(cwd, "project-package"),
|
|
327
|
-
...collectSettingsSkillPaths(cwd),
|
|
327
|
+
...collectSettingsSkillPaths(cwd, agentDir),
|
|
328
328
|
];
|
|
329
329
|
|
|
330
330
|
const deduped = new Map<string, SkillSearchPath>();
|
|
@@ -337,15 +337,16 @@ function buildSkillPaths(cwd: string): SkillSearchPath[] {
|
|
|
337
337
|
return [...deduped.values()];
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
function inferSkillSource(filePath: string, cwd: string, sourceHint?: SkillSource): SkillSource {
|
|
340
|
+
function inferSkillSource(filePath: string, cwd: string, agentDir: string, sourceHint?: SkillSource): SkillSource {
|
|
341
341
|
if (sourceHint) return sourceHint;
|
|
342
342
|
|
|
343
343
|
const projectConfigRoot = path.resolve(cwd, CONFIG_DIR);
|
|
344
344
|
const projectSkillsRoot = path.resolve(cwd, CONFIG_DIR, "skills");
|
|
345
345
|
const projectPackagesRoot = path.resolve(cwd, CONFIG_DIR, "npm", "node_modules");
|
|
346
346
|
const projectAgentsRoot = path.resolve(cwd, ".agents");
|
|
347
|
-
const userSkillsRoot = path.resolve(
|
|
348
|
-
const userPackagesRoot = path.resolve(
|
|
347
|
+
const userSkillsRoot = path.resolve(agentDir, "skills");
|
|
348
|
+
const userPackagesRoot = path.resolve(agentDir, "npm", "node_modules");
|
|
349
|
+
const userAgentRoot = path.resolve(agentDir);
|
|
349
350
|
const userAgentsRoot = path.resolve(os.homedir(), ".agents");
|
|
350
351
|
|
|
351
352
|
if (isWithinPath(filePath, projectPackagesRoot)) return "project-package";
|
|
@@ -354,7 +355,7 @@ function inferSkillSource(filePath: string, cwd: string, sourceHint?: SkillSourc
|
|
|
354
355
|
|
|
355
356
|
if (isWithinPath(filePath, userPackagesRoot)) return "user-package";
|
|
356
357
|
if (isWithinPath(filePath, userSkillsRoot) || isWithinPath(filePath, userAgentsRoot)) return "user";
|
|
357
|
-
if (isWithinPath(filePath,
|
|
358
|
+
if (isWithinPath(filePath, userAgentRoot)) return "user-settings";
|
|
358
359
|
|
|
359
360
|
const globalRoot = getGlobalNpmRoot();
|
|
360
361
|
if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
|
|
@@ -390,7 +391,7 @@ function maybeReadSkillDescription(filePath: string): string | undefined {
|
|
|
390
391
|
}
|
|
391
392
|
}
|
|
392
393
|
|
|
393
|
-
function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): CachedSkillEntry[] {
|
|
394
|
+
function collectFilesystemSkills(cwd: string, agentDir: string, skillPaths: SkillSearchPath[]): CachedSkillEntry[] {
|
|
394
395
|
const entries: CachedSkillEntry[] = [];
|
|
395
396
|
const seen = new Set<string>();
|
|
396
397
|
let order = 0;
|
|
@@ -403,7 +404,7 @@ function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): Ca
|
|
|
403
404
|
entries.push({
|
|
404
405
|
name,
|
|
405
406
|
filePath: resolvedFile,
|
|
406
|
-
source: inferSkillSource(resolvedFile, cwd, sourceHint),
|
|
407
|
+
source: inferSkillSource(resolvedFile, cwd, agentDir, sourceHint),
|
|
407
408
|
description: maybeReadSkillDescription(resolvedFile),
|
|
408
409
|
order: order++,
|
|
409
410
|
});
|
|
@@ -464,12 +465,13 @@ function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): Ca
|
|
|
464
465
|
|
|
465
466
|
function getCachedSkills(cwd: string): CachedSkillEntry[] {
|
|
466
467
|
const now = Date.now();
|
|
467
|
-
|
|
468
|
+
const agentDir = getAgentDir();
|
|
469
|
+
if (loadSkillsCache && loadSkillsCache.cwd === cwd && loadSkillsCache.agentDir === agentDir && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
|
|
468
470
|
return loadSkillsCache.skills;
|
|
469
471
|
}
|
|
470
472
|
|
|
471
|
-
const skillPaths = buildSkillPaths(cwd);
|
|
472
|
-
const loaded = collectFilesystemSkills(cwd, skillPaths);
|
|
473
|
+
const skillPaths = buildSkillPaths(cwd, agentDir);
|
|
474
|
+
const loaded = collectFilesystemSkills(cwd, agentDir, skillPaths);
|
|
473
475
|
const dedupedByName = new Map<string, CachedSkillEntry>();
|
|
474
476
|
|
|
475
477
|
for (const entry of loaded) {
|
|
@@ -478,7 +480,7 @@ function getCachedSkills(cwd: string): CachedSkillEntry[] {
|
|
|
478
480
|
}
|
|
479
481
|
|
|
480
482
|
const skills = [...dedupedByName.values()].sort((a, b) => a.order - b.order);
|
|
481
|
-
loadSkillsCache = { cwd, skills, timestamp: now };
|
|
483
|
+
loadSkillsCache = { cwd, agentDir, skills, timestamp: now };
|
|
482
484
|
return skills;
|
|
483
485
|
}
|
|
484
486
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ExtensionConfig } from "../shared/types.ts";
|
|
4
|
+
import { getAgentDir } from "../shared/utils.ts";
|
|
5
|
+
|
|
6
|
+
export function loadConfig(): ExtensionConfig {
|
|
7
|
+
const configPath = path.join(getAgentDir(), "extensions", "subagent", "config.json");
|
|
8
|
+
try {
|
|
9
|
+
if (fs.existsSync(configPath)) {
|
|
10
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
|
|
11
|
+
}
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error(`Failed to load subagent config from '${configPath}':`, error);
|
|
14
|
+
}
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { discoverAgents } from "../agents/agents.ts";
|
|
6
|
+
import { getArtifactsDir } from "../shared/artifacts.ts";
|
|
7
|
+
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
8
|
+
import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
9
|
+
import { readNestedControlRequests, resolveNestedRouteFromEnv, writeNestedControlResult } from "../runs/shared/nested-events.ts";
|
|
10
|
+
import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
|
|
11
|
+
import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
|
|
12
|
+
import { SubagentParams } from "./schemas.ts";
|
|
13
|
+
import { loadConfig } from "./config.ts";
|
|
14
|
+
import { type Details, type SubagentState } from "../shared/types.ts";
|
|
15
|
+
|
|
16
|
+
function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
17
|
+
if (parentSessionFile) {
|
|
18
|
+
const baseName = path.basename(parentSessionFile, ".jsonl");
|
|
19
|
+
const sessionsDir = path.dirname(parentSessionFile);
|
|
20
|
+
return path.join(sessionsDir, baseName);
|
|
21
|
+
}
|
|
22
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function expandTilde(p: string): string {
|
|
26
|
+
return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createChildSafeState(): SubagentState {
|
|
30
|
+
return {
|
|
31
|
+
baseCwd: "",
|
|
32
|
+
currentSessionId: null,
|
|
33
|
+
asyncJobs: new Map(),
|
|
34
|
+
foregroundRuns: new Map(),
|
|
35
|
+
foregroundControls: new Map(),
|
|
36
|
+
lastForegroundControlId: null,
|
|
37
|
+
pendingForegroundControlNotices: new Map(),
|
|
38
|
+
cleanupTimers: new Map(),
|
|
39
|
+
lastUiContext: null,
|
|
40
|
+
poller: null,
|
|
41
|
+
completionSeen: new Map(),
|
|
42
|
+
watcher: null,
|
|
43
|
+
watcherRestartTimer: null,
|
|
44
|
+
resultFileCoalescer: {
|
|
45
|
+
schedule: () => false,
|
|
46
|
+
clear: () => {},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState): NodeJS.Timeout | undefined {
|
|
52
|
+
let route;
|
|
53
|
+
try {
|
|
54
|
+
route = resolveNestedRouteFromEnv();
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (!route) return undefined;
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
const inFlight = new Set<string>();
|
|
61
|
+
const pendingResults = new Map<string, Parameters<typeof writeNestedControlResult>[1]>();
|
|
62
|
+
const timer = setInterval(() => {
|
|
63
|
+
try {
|
|
64
|
+
for (const request of readNestedControlRequests(route)) {
|
|
65
|
+
if (seen.has(request.requestId) || inFlight.has(request.requestId)) continue;
|
|
66
|
+
inFlight.add(request.requestId);
|
|
67
|
+
void (async () => {
|
|
68
|
+
try {
|
|
69
|
+
let result = pendingResults.get(request.requestId);
|
|
70
|
+
if (!result) {
|
|
71
|
+
let ok = false;
|
|
72
|
+
let message = "Control request failed.";
|
|
73
|
+
try {
|
|
74
|
+
const control = state.foregroundControls.get(request.targetRunId);
|
|
75
|
+
if (!control) {
|
|
76
|
+
message = `Nested run ${request.targetRunId} is not active in this fanout child.`;
|
|
77
|
+
} else if (request.action === "interrupt") {
|
|
78
|
+
ok = control.interrupt?.() === true;
|
|
79
|
+
message = ok
|
|
80
|
+
? `Interrupt requested for nested run ${request.targetRunId}.`
|
|
81
|
+
: `Nested run ${request.targetRunId} has no active child step to interrupt.`;
|
|
82
|
+
} else if (!request.message?.trim()) {
|
|
83
|
+
message = "Nested resume requires message.";
|
|
84
|
+
} else if (!control.currentAgent) {
|
|
85
|
+
message = `Nested run ${request.targetRunId} has no active child message route.`;
|
|
86
|
+
} else {
|
|
87
|
+
const index = control.currentIndex ?? 0;
|
|
88
|
+
const target = resolveSubagentIntercomTarget(request.targetRunId, control.currentAgent, index);
|
|
89
|
+
ok = await deliverSubagentIntercomMessageEvent(
|
|
90
|
+
pi.events,
|
|
91
|
+
target,
|
|
92
|
+
`Follow-up for nested run ${request.targetRunId} (${control.currentAgent}):\n\n${request.message.trim()}`,
|
|
93
|
+
500,
|
|
94
|
+
{ source: "nested-resume", runId: request.targetRunId, agent: control.currentAgent, index },
|
|
95
|
+
);
|
|
96
|
+
message = ok
|
|
97
|
+
? `Delivered follow-up to live nested run ${request.targetRunId}.`
|
|
98
|
+
: `Nested child intercom target is not registered: ${target}`;
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
}
|
|
103
|
+
result = { ts: Date.now(), requestId: request.requestId, targetRunId: request.targetRunId, ok, message };
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
writeNestedControlResult(route, result);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
pendingResults.set(request.requestId, result);
|
|
109
|
+
console.error(`Failed to write nested control result for request '${request.requestId}' targeting '${request.targetRunId}' via inbox '${route.controlInbox}'; keeping request for retry:`, error);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
pendingResults.delete(request.requestId);
|
|
113
|
+
seen.add(request.requestId);
|
|
114
|
+
try { fs.unlinkSync(request.filePath); } catch {}
|
|
115
|
+
} finally {
|
|
116
|
+
inFlight.delete(request.requestId);
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`Failed to poll nested control inbox '${route.controlInbox}' for root '${route.rootRunId}':`, error);
|
|
122
|
+
}
|
|
123
|
+
}, 200);
|
|
124
|
+
timer.unref?.();
|
|
125
|
+
return timer;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI): void {
|
|
129
|
+
if (process.env[SUBAGENT_CHILD_ENV] !== "1" || process.env[SUBAGENT_FANOUT_CHILD_ENV] !== "1") return;
|
|
130
|
+
|
|
131
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
132
|
+
const registeredKey = "__piSubagentFanoutChildRegisteredApis";
|
|
133
|
+
const registeredApis = globalStore[registeredKey] instanceof WeakSet
|
|
134
|
+
? globalStore[registeredKey] as WeakSet<ExtensionAPI>
|
|
135
|
+
: new WeakSet<ExtensionAPI>();
|
|
136
|
+
globalStore[registeredKey] = registeredApis;
|
|
137
|
+
if (registeredApis.has(pi)) return;
|
|
138
|
+
registeredApis.add(pi);
|
|
139
|
+
|
|
140
|
+
const config = loadConfig();
|
|
141
|
+
const state = createChildSafeState();
|
|
142
|
+
const executor = createSubagentExecutor({
|
|
143
|
+
pi,
|
|
144
|
+
state,
|
|
145
|
+
config,
|
|
146
|
+
asyncByDefault: config.asyncByDefault === true,
|
|
147
|
+
tempArtifactsDir: getArtifactsDir(null),
|
|
148
|
+
getSubagentSessionRoot,
|
|
149
|
+
expandTilde,
|
|
150
|
+
discoverAgents,
|
|
151
|
+
allowMutatingManagementActions: false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const tool: ToolDefinition<typeof SubagentParams, Details> = {
|
|
155
|
+
name: "subagent",
|
|
156
|
+
label: "Subagent",
|
|
157
|
+
description: [
|
|
158
|
+
"Delegate to subagents from child-safe fanout mode.",
|
|
159
|
+
"Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
|
|
160
|
+
"Agent config mutation actions create, update, and delete are blocked in this mode.",
|
|
161
|
+
].join("\n"),
|
|
162
|
+
parameters: SubagentParams,
|
|
163
|
+
execute(id, params, signal, onUpdate, ctx) {
|
|
164
|
+
return executor.execute(id, params as SubagentParamsLike, signal, onUpdate, ctx);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
pi.registerTool(tool);
|
|
169
|
+
startNestedControlInboxListener(pi, state);
|
|
170
|
+
}
|
package/src/extension/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { discoverAgents } from "../agents/agents.ts";
|
|
|
22
22
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
23
23
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
24
24
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
25
|
-
import { renderWidget, renderSubagentResult
|
|
25
|
+
import { clearLegacyResultAnimationTimer, renderWidget, renderSubagentResult } from "../tui/render.ts";
|
|
26
26
|
import { SubagentParams } from "./schemas.ts";
|
|
27
27
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
28
28
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
@@ -33,11 +33,12 @@ import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
|
|
|
33
33
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
|
|
34
34
|
import { inspectSubagentStatus } from "../runs/background/run-status.ts";
|
|
35
35
|
import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
|
|
36
|
-
import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
36
|
+
import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
37
|
+
import registerFanoutChildSubagentExtension from "./fanout-child.ts";
|
|
37
38
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
39
|
+
import { loadConfig } from "./config.ts";
|
|
38
40
|
import {
|
|
39
41
|
type Details,
|
|
40
|
-
type ExtensionConfig,
|
|
41
42
|
type SubagentState,
|
|
42
43
|
ASYNC_DIR,
|
|
43
44
|
DEFAULT_ARTIFACT_CONFIG,
|
|
@@ -56,6 +57,8 @@ import {
|
|
|
56
57
|
type SubagentControlMessageDetails,
|
|
57
58
|
} from "./control-notices.ts";
|
|
58
59
|
|
|
60
|
+
export { loadConfig } from "./config.ts";
|
|
61
|
+
|
|
59
62
|
/**
|
|
60
63
|
* Derive subagent session base directory from parent session file.
|
|
61
64
|
* If parent session is ~/.pi/agent/sessions/abc123.jsonl,
|
|
@@ -72,18 +75,6 @@ function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
|
72
75
|
return fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"));
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
function loadConfig(): ExtensionConfig {
|
|
76
|
-
const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");
|
|
77
|
-
try {
|
|
78
|
-
if (fs.existsSync(configPath)) {
|
|
79
|
-
return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.error(`Failed to load subagent config from '${configPath}':`, error);
|
|
83
|
-
}
|
|
84
|
-
return {};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
78
|
function expandTilde(p: string): string {
|
|
88
79
|
return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
|
|
89
80
|
}
|
|
@@ -142,14 +133,11 @@ function createSlashResultComponent(
|
|
|
142
133
|
details: SlashMessageDetails,
|
|
143
134
|
options: { expanded: boolean },
|
|
144
135
|
theme: ExtensionContext["ui"]["theme"],
|
|
145
|
-
requestRender: () => void,
|
|
146
136
|
): Container {
|
|
147
137
|
const container = new Container();
|
|
148
|
-
const animationState: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> } = {};
|
|
149
138
|
let lastVersion = -1;
|
|
150
139
|
container.render = (width: number): string[] => {
|
|
151
140
|
const snapshot = getSlashRenderableSnapshot(details);
|
|
152
|
-
syncResultAnimation(snapshot.result, { state: animationState, invalidate: requestRender });
|
|
153
141
|
if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
|
|
154
142
|
lastVersion = snapshot.version;
|
|
155
143
|
rebuildSlashResultContainer(container, snapshot.result, options, theme);
|
|
@@ -220,7 +208,10 @@ class SubagentControlNoticeComponent implements Component {
|
|
|
220
208
|
}
|
|
221
209
|
|
|
222
210
|
export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
223
|
-
if (process.env[SUBAGENT_CHILD_ENV] === "1")
|
|
211
|
+
if (process.env[SUBAGENT_CHILD_ENV] === "1") {
|
|
212
|
+
if (process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1") registerFanoutChildSubagentExtension(pi);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
224
215
|
const globalStore = globalThis as Record<string, unknown>;
|
|
225
216
|
const runtimeCleanupStoreKey = "__piSubagentRuntimeCleanup";
|
|
226
217
|
const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
|
|
@@ -271,8 +262,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
271
262
|
primeExistingResults();
|
|
272
263
|
|
|
273
264
|
const runtimeCleanup = () => {
|
|
274
|
-
stopWidgetAnimation();
|
|
275
|
-
stopResultAnimations();
|
|
276
265
|
stopResultWatcher();
|
|
277
266
|
clearPendingForegroundControlNotices(state);
|
|
278
267
|
if (state.poller) {
|
|
@@ -297,7 +286,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
297
286
|
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
298
287
|
const details = resolveSlashMessageDetails(message.details);
|
|
299
288
|
if (!details) return undefined;
|
|
300
|
-
return createSlashResultComponent(details, options, theme
|
|
289
|
+
return createSlashResultComponent(details, options, theme);
|
|
301
290
|
});
|
|
302
291
|
|
|
303
292
|
pi.registerMessageRenderer<SubagentNotifyDetails>("subagent-notify", (message, options, theme) => {
|
|
@@ -466,7 +455,7 @@ DIAGNOSTICS:
|
|
|
466
455
|
},
|
|
467
456
|
|
|
468
457
|
renderResult(result, options, theme, context) {
|
|
469
|
-
|
|
458
|
+
clearLegacyResultAnimationTimer(context);
|
|
470
459
|
return renderSubagentResult(result, options, theme);
|
|
471
460
|
},
|
|
472
461
|
|
|
@@ -514,6 +503,7 @@ DIAGNOSTICS:
|
|
|
514
503
|
state.lastUiContext = ctx;
|
|
515
504
|
if (state.asyncJobs.size > 0) {
|
|
516
505
|
renderWidget(ctx, Array.from(state.asyncJobs.values()));
|
|
506
|
+
ctx.ui.requestRender?.();
|
|
517
507
|
ensurePoller();
|
|
518
508
|
}
|
|
519
509
|
});
|
|
@@ -569,8 +559,6 @@ DIAGNOSTICS:
|
|
|
569
559
|
slashBridge.dispose();
|
|
570
560
|
promptTemplateBridge.cancelAll();
|
|
571
561
|
promptTemplateBridge.dispose();
|
|
572
|
-
stopWidgetAnimation();
|
|
573
|
-
stopResultAnimations();
|
|
574
562
|
if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
|
|
575
563
|
delete globalStore[runtimeCleanupStoreKey];
|
|
576
564
|
}
|
|
@@ -4,12 +4,13 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { AgentConfig } from "../agents/agents.ts";
|
|
6
6
|
import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
|
|
7
|
+
import { getAgentDir } from "../shared/utils.ts";
|
|
7
8
|
|
|
8
9
|
const PI_INTERCOM_PACKAGE_NAME = "pi-intercom";
|
|
9
10
|
const CONFIG_DIR = ".pi";
|
|
10
11
|
|
|
11
12
|
function defaultAgentDir(): string {
|
|
12
|
-
return
|
|
13
|
+
return getAgentDir();
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function defaultIntercomExtensionDir(agentDir = defaultAgentDir()): string {
|
|
@@ -3,6 +3,8 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import {
|
|
4
4
|
type Details,
|
|
5
5
|
type IntercomEventBus,
|
|
6
|
+
type NestedRunSummary,
|
|
7
|
+
type PublicNestedRunSummary,
|
|
6
8
|
type SingleResult,
|
|
7
9
|
type SubagentResultIntercomChild,
|
|
8
10
|
type SubagentResultIntercomPayload,
|
|
@@ -60,6 +62,110 @@ function resolveGroupedStatus(children: SubagentResultIntercomChild[]): Subagent
|
|
|
60
62
|
return "failed";
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
function compactNestedRun(run: NestedRunSummary | PublicNestedRunSummary, depth = 0): PublicNestedRunSummary {
|
|
66
|
+
return {
|
|
67
|
+
id: run.id,
|
|
68
|
+
parentRunId: run.parentRunId,
|
|
69
|
+
...(run.parentStepIndex !== undefined ? { parentStepIndex: run.parentStepIndex } : {}),
|
|
70
|
+
...(run.parentAgent ? { parentAgent: run.parentAgent } : {}),
|
|
71
|
+
depth: run.depth,
|
|
72
|
+
path: run.path.slice(0, 4).map((part) => ({
|
|
73
|
+
runId: part.runId,
|
|
74
|
+
...(part.stepIndex !== undefined ? { stepIndex: part.stepIndex } : {}),
|
|
75
|
+
...(part.agent ? { agent: part.agent } : {}),
|
|
76
|
+
})),
|
|
77
|
+
...(run.asyncDir ? { asyncDir: run.asyncDir } : {}),
|
|
78
|
+
...(run.sessionId ? { sessionId: run.sessionId } : {}),
|
|
79
|
+
...(run.sessionFile ? { sessionFile: run.sessionFile } : {}),
|
|
80
|
+
...(run.intercomTarget ? { intercomTarget: run.intercomTarget } : {}),
|
|
81
|
+
...(run.ownerIntercomTarget ? { ownerIntercomTarget: run.ownerIntercomTarget } : {}),
|
|
82
|
+
...(run.leafIntercomTarget ? { leafIntercomTarget: run.leafIntercomTarget } : {}),
|
|
83
|
+
...(run.ownerState ? { ownerState: run.ownerState } : {}),
|
|
84
|
+
...(run.mode ? { mode: run.mode } : {}),
|
|
85
|
+
state: run.state,
|
|
86
|
+
...(run.agent ? { agent: run.agent } : {}),
|
|
87
|
+
...(run.agents?.length ? { agents: run.agents.slice(0, 12) } : {}),
|
|
88
|
+
...(run.currentStep !== undefined ? { currentStep: run.currentStep } : {}),
|
|
89
|
+
...(run.chainStepCount !== undefined ? { chainStepCount: run.chainStepCount } : {}),
|
|
90
|
+
...(run.parallelGroups?.length ? { parallelGroups: run.parallelGroups.slice(0, 8) } : {}),
|
|
91
|
+
...(run.activityState ? { activityState: run.activityState } : {}),
|
|
92
|
+
...(run.lastActivityAt !== undefined ? { lastActivityAt: run.lastActivityAt } : {}),
|
|
93
|
+
...(run.currentTool ? { currentTool: run.currentTool } : {}),
|
|
94
|
+
...(run.currentToolStartedAt !== undefined ? { currentToolStartedAt: run.currentToolStartedAt } : {}),
|
|
95
|
+
...(run.currentPath ? { currentPath: run.currentPath } : {}),
|
|
96
|
+
...(run.turnCount !== undefined ? { turnCount: run.turnCount } : {}),
|
|
97
|
+
...(run.toolCount !== undefined ? { toolCount: run.toolCount } : {}),
|
|
98
|
+
...(run.totalTokens ? { totalTokens: run.totalTokens } : {}),
|
|
99
|
+
...(run.startedAt !== undefined ? { startedAt: run.startedAt } : {}),
|
|
100
|
+
...(run.endedAt !== undefined ? { endedAt: run.endedAt } : {}),
|
|
101
|
+
...(run.lastUpdate !== undefined ? { lastUpdate: run.lastUpdate } : {}),
|
|
102
|
+
...(run.error ? { error: run.error } : {}),
|
|
103
|
+
...(run.steps?.length ? { steps: run.steps.slice(0, 12).map((step) => ({
|
|
104
|
+
agent: step.agent,
|
|
105
|
+
status: step.status,
|
|
106
|
+
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
107
|
+
...(step.activityState ? { activityState: step.activityState } : {}),
|
|
108
|
+
...(step.lastActivityAt !== undefined ? { lastActivityAt: step.lastActivityAt } : {}),
|
|
109
|
+
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
110
|
+
...(step.currentToolStartedAt !== undefined ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
|
|
111
|
+
...(step.currentPath ? { currentPath: step.currentPath } : {}),
|
|
112
|
+
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
113
|
+
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
114
|
+
...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
|
|
115
|
+
...(step.endedAt !== undefined ? { endedAt: step.endedAt } : {}),
|
|
116
|
+
...(step.error ? { error: step.error } : {}),
|
|
117
|
+
...(depth < 2 && step.children?.length ? { children: step.children.slice(0, 8).map((child) => compactNestedRun(child, depth + 1)) } : {}),
|
|
118
|
+
})) } : {}),
|
|
119
|
+
...(depth < 2 && run.children?.length ? { children: run.children.slice(0, 8).map((child) => compactNestedRun(child, depth + 1)) } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function compactNestedResultChildren(children: Array<NestedRunSummary | PublicNestedRunSummary> | undefined): PublicNestedRunSummary[] | undefined {
|
|
124
|
+
if (!children?.length) return undefined;
|
|
125
|
+
return children.slice(0, 16).map((child) => compactNestedRun(child));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function attachNestedChildrenToResultChildren(
|
|
129
|
+
runId: string,
|
|
130
|
+
children: SubagentResultIntercomChild[],
|
|
131
|
+
nestedChildren: NestedRunSummary[] | undefined,
|
|
132
|
+
): SubagentResultIntercomChild[] {
|
|
133
|
+
const compact = compactNestedResultChildren(nestedChildren);
|
|
134
|
+
if (!compact?.length) return children.map((child) => ({ ...child, children: compactNestedResultChildren(child.children) }));
|
|
135
|
+
return children.map((child, index) => {
|
|
136
|
+
const childIndex = child.index ?? index;
|
|
137
|
+
const alreadyAttachedIds = new Set(child.children?.map((nested) => nested.id) ?? []);
|
|
138
|
+
const attached = compact.filter((nested) => nested.parentRunId === runId && nested.parentStepIndex === childIndex && !alreadyAttachedIds.has(nested.id));
|
|
139
|
+
const fallbackAttached = children.length === 1
|
|
140
|
+
? compact.filter((nested) => nested.parentRunId === runId && nested.parentStepIndex === undefined && !alreadyAttachedIds.has(nested.id))
|
|
141
|
+
: [];
|
|
142
|
+
const merged = compactNestedResultChildren([...(child.children ?? []), ...attached, ...fallbackAttached]);
|
|
143
|
+
return merged?.length ? { ...child, children: merged } : { ...child, children: undefined };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatNestedResultLines(children: PublicNestedRunSummary[] | undefined): string[] {
|
|
148
|
+
if (!children?.length) return [];
|
|
149
|
+
const lines = ["Nested subagents:"];
|
|
150
|
+
let remaining = 10;
|
|
151
|
+
const append = (runs: PublicNestedRunSummary[] | undefined, indent: string): void => {
|
|
152
|
+
for (const run of runs ?? []) {
|
|
153
|
+
if (remaining <= 0) {
|
|
154
|
+
lines.push(`${indent}↳ +more nested runs; inspect status for full tree`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
remaining--;
|
|
158
|
+
const label = run.agent ?? run.agents?.join("+") ?? run.id;
|
|
159
|
+
lines.push(`${indent}↳ ${label} — ${run.state} [${run.id}]`);
|
|
160
|
+
if (run.sessionFile) lines.push(`${indent} Session: ${run.sessionFile}`);
|
|
161
|
+
append(run.children, `${indent} `);
|
|
162
|
+
for (const step of run.steps ?? []) append(step.children, `${indent} `);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
append(children, "");
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
63
169
|
interface GroupedResultIntercomMessageInput {
|
|
64
170
|
to: string;
|
|
65
171
|
runId: string;
|
|
@@ -128,6 +234,7 @@ function formatSubagentResultIntercomMessage(input: {
|
|
|
128
234
|
if (child.intercomTarget) lines.push(`${input.source === "async" ? "Previous intercom target" : "Run intercom target"}: ${child.intercomTarget}`);
|
|
129
235
|
if (child.artifactPath) lines.push(`Output artifact: ${child.artifactPath}`);
|
|
130
236
|
if (child.sessionPath) lines.push(`Session: ${child.sessionPath}`);
|
|
237
|
+
lines.push(...formatNestedResultLines(child.children));
|
|
131
238
|
lines.push("Summary:");
|
|
132
239
|
lines.push(child.summary);
|
|
133
240
|
}
|
|
@@ -139,6 +246,7 @@ export function buildSubagentResultIntercomPayload(input: GroupedResultIntercomM
|
|
|
139
246
|
const children = input.children.map((child) => ({
|
|
140
247
|
...child,
|
|
141
248
|
summary: child.summary.trim() || "(no output)",
|
|
249
|
+
children: compactNestedResultChildren(child.children),
|
|
142
250
|
}));
|
|
143
251
|
const status = resolveGroupedStatus(children);
|
|
144
252
|
const summary = formatStatusCounts(countStatuses(children));
|