pi-crew 0.1.11 → 0.1.12
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/package.json
CHANGED
|
@@ -15,6 +15,7 @@ import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerb
|
|
|
15
15
|
import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-viewer.ts";
|
|
16
16
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
17
17
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
18
|
+
import { terminateActiveChildPiProcesses } from "../runtime/child-pi.ts";
|
|
18
19
|
|
|
19
20
|
function parseRunArgs(args: string): TeamToolParamsValue {
|
|
20
21
|
const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -103,6 +104,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
103
104
|
let currentCtx: ExtensionContext | undefined;
|
|
104
105
|
let rpcHandle: PiCrewRpcHandle | undefined;
|
|
105
106
|
const widgetState: CrewWidgetState = { frame: 0 };
|
|
107
|
+
const foregroundControllers = new Set<AbortController>();
|
|
106
108
|
registerAutonomousPolicy(pi);
|
|
107
109
|
rpcHandle = registerPiCrewRpc((pi as unknown as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events, () => currentCtx);
|
|
108
110
|
|
|
@@ -123,6 +125,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
123
125
|
widgetState.interval.unref?.();
|
|
124
126
|
});
|
|
125
127
|
pi.on("session_shutdown", () => {
|
|
128
|
+
for (const controller of foregroundControllers) controller.abort();
|
|
129
|
+
terminateActiveChildPiProcesses();
|
|
126
130
|
stopAsyncRunNotifier(notifierState);
|
|
127
131
|
stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
|
|
128
132
|
clearPiCrewPowerbar(pi.events);
|
|
@@ -137,12 +141,21 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
137
141
|
description: "Coordinate Pi teams. Use proactively for complex multi-file work, planning, implementation, tests, reviews, security audits, research, async/background runs, and worktree-isolated execution. Use action='recommend' when unsure which team/workflow to choose. Destructive actions require explicit user confirmation.",
|
|
138
142
|
promptSnippet: "Use the team tool proactively for coordinated multi-agent work. If unsure, call { action: 'recommend', goal } first, then run or plan with the suggested team/workflow.",
|
|
139
143
|
parameters: TeamToolParams as never,
|
|
140
|
-
async execute(_id, params,
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
foregroundControllers.add(controller);
|
|
147
|
+
const abort = (): void => controller.abort();
|
|
148
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
149
|
+
try {
|
|
150
|
+
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal });
|
|
151
|
+
const config = loadConfig(ctx.cwd).config.ui;
|
|
152
|
+
updateCrewWidget(ctx, widgetState, config);
|
|
153
|
+
updatePiCrewPowerbar(pi.events, ctx.cwd, config);
|
|
154
|
+
return output;
|
|
155
|
+
} finally {
|
|
156
|
+
signal?.removeEventListener("abort", abort);
|
|
157
|
+
foregroundControllers.delete(controller);
|
|
158
|
+
}
|
|
146
159
|
},
|
|
147
160
|
};
|
|
148
161
|
|
|
@@ -12,7 +12,7 @@ function isFinished(run: TeamRunManifest): boolean {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
|
|
15
|
-
const finished = listRuns(cwd).filter(isFinished).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
15
|
+
const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
16
16
|
const kept = finished.slice(0, keep).map((run) => run.runId);
|
|
17
17
|
const removed: string[] = [];
|
|
18
18
|
for (const run of finished.slice(keep)) {
|
|
@@ -56,6 +56,7 @@ type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext
|
|
|
56
56
|
modelRegistry?: unknown;
|
|
57
57
|
sessionManager?: { getBranch?: () => unknown[] };
|
|
58
58
|
events?: { emit?: (event: string, data: unknown) => void };
|
|
59
|
+
signal?: AbortSignal;
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
@@ -306,7 +307,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
306
307
|
const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
|
|
307
308
|
const executeWorkers = runtime.kind === "child-process";
|
|
308
309
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
309
|
-
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry });
|
|
310
|
+
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
|
|
310
311
|
const text = [
|
|
311
312
|
`Created pi-crew run ${executed.manifest.runId}.`,
|
|
312
313
|
`Team: ${team.name}`,
|
|
@@ -429,7 +430,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
429
430
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
430
431
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
431
432
|
const executeWorkers = runtime.kind === "child-process";
|
|
432
|
-
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry });
|
|
433
|
+
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
|
|
433
434
|
return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
434
435
|
});
|
|
435
436
|
}
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
@@ -8,6 +8,29 @@ import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
|
8
8
|
const POST_EXIT_STDIO_GUARD_MS = 3000;
|
|
9
9
|
const FINAL_DRAIN_MS = 5000;
|
|
10
10
|
const HARD_KILL_MS = 3000;
|
|
11
|
+
const activeChildProcesses = new Map<number, ChildProcess>();
|
|
12
|
+
|
|
13
|
+
function killProcessTree(pid: number | undefined): void {
|
|
14
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) return;
|
|
15
|
+
try {
|
|
16
|
+
if (process.platform === "win32") {
|
|
17
|
+
spawn("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try { process.kill(-pid, "SIGTERM"); } catch { process.kill(pid, "SIGTERM"); }
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch {} }
|
|
23
|
+
}, HARD_KILL_MS).unref?.();
|
|
24
|
+
} catch {
|
|
25
|
+
// Ignore shutdown races.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function terminateActiveChildPiProcesses(): number {
|
|
30
|
+
const pids = [...activeChildProcesses.keys()];
|
|
31
|
+
for (const pid of pids) killProcessTree(pid);
|
|
32
|
+
return pids.length;
|
|
33
|
+
}
|
|
11
34
|
|
|
12
35
|
export interface ChildPiRunInput {
|
|
13
36
|
cwd: string;
|
|
@@ -118,7 +141,9 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
118
141
|
cwd: input.cwd,
|
|
119
142
|
env: { ...process.env, ...built.env },
|
|
120
143
|
stdio: ["ignore", "pipe", "pipe"],
|
|
144
|
+
detached: process.platform !== "win32",
|
|
121
145
|
});
|
|
146
|
+
if (child.pid) activeChildProcesses.set(child.pid, child);
|
|
122
147
|
let stdout = "";
|
|
123
148
|
let stderr = "";
|
|
124
149
|
let settled = false;
|
|
@@ -167,6 +192,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
167
192
|
};
|
|
168
193
|
|
|
169
194
|
const abort = (): void => {
|
|
195
|
+
killProcessTree(child.pid);
|
|
170
196
|
try {
|
|
171
197
|
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
172
198
|
} catch {
|
|
@@ -187,6 +213,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
187
213
|
settle({ exitCode: null, stdout, stderr, error: error.message });
|
|
188
214
|
});
|
|
189
215
|
child.on("exit", () => {
|
|
216
|
+
if (child.pid) activeChildProcesses.delete(child.pid);
|
|
190
217
|
childExited = true;
|
|
191
218
|
clearFinalDrainTimers();
|
|
192
219
|
postExitGuard = setTimeout(() => {
|
|
@@ -196,6 +223,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
196
223
|
postExitGuard.unref?.();
|
|
197
224
|
});
|
|
198
225
|
child.on("close", (exitCode) => {
|
|
226
|
+
if (child.pid) activeChildProcesses.delete(child.pid);
|
|
199
227
|
settle({ exitCode, stdout, stderr, ...(forcedFinalDrain && !stderr.trim() ? { error: `Child Pi did not exit within ${finalDrainMs}ms after final assistant message; termination was requested.` } : {}) });
|
|
200
228
|
});
|
|
201
229
|
});
|
|
@@ -38,6 +38,13 @@ export function isLikelyOrphanedActiveRun(run: TeamRunManifest, agents: CrewAgen
|
|
|
38
38
|
return agents.every((agent) => agent.status === "queued" && !agent.completedAt && !agent.progress);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function hasDurableActiveAgentEvidence(agent: CrewAgentRecord): boolean {
|
|
42
|
+
if (agent.status !== "running" && agent.status !== "queued") return false;
|
|
43
|
+
return Boolean(agent.statusPath || agent.eventsPath || agent.outputPath || agent.progress || agent.toolUses || agent.jsonEvents);
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
export function isDisplayActiveRun(run: TeamRunManifest, agents: CrewAgentRecord[] = [], now = Date.now()): boolean {
|
|
42
|
-
|
|
47
|
+
if (!isActiveRunStatus(run.status) || isLikelyOrphanedActiveRun(run, agents, now)) return false;
|
|
48
|
+
if (agents.length === 0) return true;
|
|
49
|
+
return agents.some(hasDurableActiveAgentEvidence);
|
|
43
50
|
}
|