@ryan_nookpi/pi-extension-subagent 0.5.6 → 0.5.8
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/README.md +3 -3
- package/async-task-lifecycle.ts +270 -0
- package/async-task-provider.ts +623 -0
- package/cli.ts +9 -2
- package/commands.ts +446 -416
- package/index.ts +20 -2
- package/package.json +3 -1
- package/run-utils.ts +3 -3
- package/runner.ts +33 -1
- package/store.ts +1 -0
- package/tool-execute.ts +174 -68
- package/types.ts +4 -1
package/index.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
31
|
+
import { SubagentAsyncTasks } from "./async-task-lifecycle.js";
|
|
31
32
|
import { HANG_CHECK_INTERVAL_MS } from "./constants.js";
|
|
32
33
|
import { SUBAGENT_COMMANDS, SUBAGENT_SHORTCUTS, type SubagentCommandName } from "./registration-manifest.js";
|
|
33
34
|
|
|
@@ -40,6 +41,7 @@ interface SubagentCore {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export default function (pi: ExtensionAPI) {
|
|
44
|
+
const asyncTasks = new SubagentAsyncTasks(pi);
|
|
43
45
|
let core: SubagentCore | null = null;
|
|
44
46
|
let corePromise: Promise<SubagentCore> | null = null;
|
|
45
47
|
/** Serializes session lifecycle work so events apply in dispatch order. */
|
|
@@ -54,6 +56,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
54
56
|
import("./store.js"),
|
|
55
57
|
]);
|
|
56
58
|
const store = storeMod.createStore();
|
|
59
|
+
store.asyncTasks = asyncTasks;
|
|
57
60
|
const registrations = commands.registerAll(pi, store);
|
|
58
61
|
core = { store, commands, registrations, escalation, lifecycle };
|
|
59
62
|
return core;
|
|
@@ -116,11 +119,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
116
119
|
}, 0);
|
|
117
120
|
|
|
118
121
|
let hangCheckTimer: ReturnType<typeof setInterval> | undefined;
|
|
122
|
+
let sessionGeneration = 0;
|
|
119
123
|
|
|
120
124
|
pi.on("session_start", (_event, ctx) => {
|
|
121
|
-
|
|
125
|
+
const generation = ++sessionGeneration;
|
|
126
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
127
|
+
// Fence tool admission synchronously, before lazy lifecycle work can yield.
|
|
128
|
+
try {
|
|
129
|
+
if (pi.events) asyncTasks.bind(sessionId);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
process.stderr.write(`[subagent] session binding failed: ${error instanceof Error ? error.message : error}\n`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
enqueue(async (c) => {
|
|
135
|
+
if (generation !== sessionGeneration) return;
|
|
136
|
+
if (pi.events) await asyncTasks.provider.whenDiscovered();
|
|
137
|
+
if (generation !== sessionGeneration || ctx.sessionManager.getSessionId() !== sessionId) return;
|
|
122
138
|
c.store.disposed = false;
|
|
123
|
-
c.commands.handleSessionStart(pi, c.store, ctx as unknown as ExtensionContext);
|
|
139
|
+
c.commands.handleSessionStart(asyncTasks.wrap(pi), c.store, ctx as unknown as ExtensionContext);
|
|
124
140
|
c.escalation.maybeRegisterAskMaster(pi, ctx);
|
|
125
141
|
});
|
|
126
142
|
if (!hangCheckTimer) {
|
|
@@ -131,6 +147,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
131
147
|
});
|
|
132
148
|
|
|
133
149
|
pi.on("session_shutdown", async (event) => {
|
|
150
|
+
++sessionGeneration;
|
|
151
|
+
asyncTasks.shutdown();
|
|
134
152
|
if (hangCheckTimer) {
|
|
135
153
|
clearInterval(hangCheckTimer);
|
|
136
154
|
hangCheckTimer = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryan_nookpi/pi-extension-subagent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"format.ts",
|
|
40
40
|
"group-pending.ts",
|
|
41
41
|
"index.ts",
|
|
42
|
+
"async-task-provider.ts",
|
|
43
|
+
"async-task-lifecycle.ts",
|
|
42
44
|
"lifecycle.ts",
|
|
43
45
|
"invocation-queue.ts",
|
|
44
46
|
"live-preview.ts",
|
package/run-utils.ts
CHANGED
|
@@ -242,18 +242,18 @@ export function snapshotBatchGroup(
|
|
|
242
242
|
let failed = 0;
|
|
243
243
|
let aborted = 0;
|
|
244
244
|
const members: FinishedGroupMember[] = batch.runIds.map((runId) => {
|
|
245
|
-
const run = store.commandRuns.get(runId);
|
|
245
|
+
const run = batch.completedRuns.get(runId) ?? store.commandRuns.get(runId);
|
|
246
246
|
if (!run) {
|
|
247
247
|
if (batch.abortedRunIds.has(runId)) aborted++;
|
|
248
248
|
else if (batch.failedRunIds.has(runId)) failed++;
|
|
249
249
|
return {
|
|
250
250
|
summaryLine: `#${runId} [gone] (run no longer available)`,
|
|
251
|
-
output:
|
|
251
|
+
output: "(no output)",
|
|
252
252
|
};
|
|
253
253
|
}
|
|
254
254
|
if (run.errorClass === "aborted" || batch.abortedRunIds.has(runId)) aborted++;
|
|
255
255
|
else if (run.status === "error" || batch.failedRunIds.has(runId)) failed++;
|
|
256
|
-
return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run
|
|
256
|
+
return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run) };
|
|
257
257
|
});
|
|
258
258
|
return {
|
|
259
259
|
groupId: batch.batchId,
|
package/runner.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn } from "node:child_process";
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { AgentConfig } from "./agents.js";
|
|
10
|
+
import { currentResourceObserver, trackSubagentRunner } from "./async-task-lifecycle.js";
|
|
10
11
|
import { buildClaudeArgs } from "./claude-args.js";
|
|
11
12
|
import { createSidecarWriter } from "./claude-sidecar-writer.js";
|
|
12
13
|
import {
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
} from "./utils/agent-utils.js";
|
|
44
45
|
|
|
45
46
|
export interface RunSingleAgentSessionConfig {
|
|
47
|
+
asyncRunId?: number;
|
|
46
48
|
sessionFile?: string;
|
|
47
49
|
resumeSessionId?: string;
|
|
48
50
|
sidecarSessionFile?: string;
|
|
@@ -198,7 +200,21 @@ export function computeAgentAliasHints(agents: AgentConfig[]): string {
|
|
|
198
200
|
|
|
199
201
|
// ─── Single Agent Execution ──────────────────────────────────────────────────
|
|
200
202
|
|
|
201
|
-
export async function runSingleAgent(
|
|
203
|
+
export async function runSingleAgent(...args: Parameters<typeof runSingleAgentUntracked>): Promise<SingleResult> {
|
|
204
|
+
const config = typeof args[8] === "object" ? args[8] : undefined;
|
|
205
|
+
return trackSubagentRunner(
|
|
206
|
+
args[2],
|
|
207
|
+
args[5],
|
|
208
|
+
(signal) => {
|
|
209
|
+
const invocation: Parameters<typeof runSingleAgentUntracked> = [...args];
|
|
210
|
+
invocation[5] = signal;
|
|
211
|
+
return runSingleAgentUntracked(...invocation);
|
|
212
|
+
},
|
|
213
|
+
config?.asyncRunId,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function runSingleAgentUntracked(
|
|
202
218
|
defaultCwd: string,
|
|
203
219
|
agents: AgentConfig[],
|
|
204
220
|
agentName: string,
|
|
@@ -209,6 +225,7 @@ export async function runSingleAgent(
|
|
|
209
225
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
210
226
|
sessionConfig?: string | RunSingleAgentSessionConfig,
|
|
211
227
|
): Promise<SingleResult> {
|
|
228
|
+
currentResourceObserver()("settled");
|
|
212
229
|
const normalizedSessionConfig = normalizeRunSessionConfig(sessionConfig);
|
|
213
230
|
const sessionFile = normalizedSessionConfig.sessionFile;
|
|
214
231
|
const resumeSessionId = normalizedSessionConfig.resumeSessionId;
|
|
@@ -246,7 +263,10 @@ export async function runSingleAgent(
|
|
|
246
263
|
|
|
247
264
|
if (agent.runtime === "claude") {
|
|
248
265
|
if (resolveClaudeRuntimeMode(defaultCwd) === "sdk") {
|
|
266
|
+
// SDK query.close() has no process-exit acknowledgement.
|
|
267
|
+
currentResourceObserver()("unknown");
|
|
249
268
|
const { runClaudeAgentViaSdk } = await import("./claude-sdk-runner.js");
|
|
269
|
+
if (signal?.aborted) throw new Error("Subagent was aborted");
|
|
250
270
|
return runClaudeAgentViaSdk(
|
|
251
271
|
defaultCwd,
|
|
252
272
|
agent,
|
|
@@ -300,6 +320,7 @@ async function runClaudeAgent(
|
|
|
300
320
|
sidecarSessionFile?: string,
|
|
301
321
|
onDiagnostic?: RunnerDiagnosticSink,
|
|
302
322
|
): Promise<SingleResult> {
|
|
323
|
+
const observeResource = currentResourceObserver();
|
|
303
324
|
try {
|
|
304
325
|
validateClaudeRuntimeModel(agent.model);
|
|
305
326
|
} catch (err: any) {
|
|
@@ -392,7 +413,9 @@ async function runClaudeAgent(
|
|
|
392
413
|
});
|
|
393
414
|
|
|
394
415
|
const exitCode = await new Promise<number>((resolve) => {
|
|
416
|
+
if (signal?.aborted) throw new Error("Subagent was aborted");
|
|
395
417
|
const proc = spawn("claude", args, { cwd: defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
418
|
+
observeResource("active");
|
|
396
419
|
let buffer = "";
|
|
397
420
|
let procExited = false;
|
|
398
421
|
let settled = false;
|
|
@@ -564,6 +587,7 @@ async function runClaudeAgent(
|
|
|
564
587
|
});
|
|
565
588
|
|
|
566
589
|
proc.on("exit", (code, terminationSignal) => {
|
|
590
|
+
observeResource("settled");
|
|
567
591
|
procExited = true;
|
|
568
592
|
lastExitCode = normalizeChildExitCode(code, terminationSignal, lastExitCode);
|
|
569
593
|
lastProcessSignal = terminationSignal;
|
|
@@ -583,6 +607,7 @@ async function runClaudeAgent(
|
|
|
583
607
|
});
|
|
584
608
|
|
|
585
609
|
proc.on("close", (code, terminationSignal) => {
|
|
610
|
+
observeResource("settled");
|
|
586
611
|
procExited = true;
|
|
587
612
|
lastProcessSignal = terminationSignal ?? lastProcessSignal;
|
|
588
613
|
diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
|
|
@@ -593,6 +618,7 @@ async function runClaudeAgent(
|
|
|
593
618
|
});
|
|
594
619
|
|
|
595
620
|
proc.on("error", (error) => {
|
|
621
|
+
observeResource(proc.pid ? "unknown" : "settled");
|
|
596
622
|
procExited = true;
|
|
597
623
|
stderrBuf += `[runner] process error: ${error?.message || String(error)}\n`;
|
|
598
624
|
diagnose({
|
|
@@ -658,6 +684,7 @@ async function runPiAgent(
|
|
|
658
684
|
persistedSessionBaseOffset = 0,
|
|
659
685
|
onDiagnostic?: RunnerDiagnosticSink,
|
|
660
686
|
): Promise<SingleResult> {
|
|
687
|
+
const observeResource = currentResourceObserver();
|
|
661
688
|
const args: string[] = ["--mode", "json", "-p"];
|
|
662
689
|
if (sessionFile) args.push("--session", sessionFile);
|
|
663
690
|
else args.push("--no-session");
|
|
@@ -721,7 +748,9 @@ async function runPiAgent(
|
|
|
721
748
|
args.push(normalizeTaskForSubagentPrompt(task));
|
|
722
749
|
|
|
723
750
|
const exitCode = await new Promise<number>((resolve) => {
|
|
751
|
+
if (signal?.aborted) throw new Error("Subagent was aborted");
|
|
724
752
|
const proc = spawn("pi", args, { cwd: defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
753
|
+
observeResource("active");
|
|
725
754
|
let buffer = "";
|
|
726
755
|
let procExited = false;
|
|
727
756
|
let settled = false;
|
|
@@ -1089,6 +1118,7 @@ async function runPiAgent(
|
|
|
1089
1118
|
});
|
|
1090
1119
|
|
|
1091
1120
|
proc.on("exit", (code, terminationSignal) => {
|
|
1121
|
+
observeResource("settled");
|
|
1092
1122
|
procExited = true;
|
|
1093
1123
|
lastExitCode = normalizeChildExitCode(code, terminationSignal, lastExitCode);
|
|
1094
1124
|
lastProcessSignal = terminationSignal;
|
|
@@ -1105,6 +1135,7 @@ async function runPiAgent(
|
|
|
1105
1135
|
});
|
|
1106
1136
|
|
|
1107
1137
|
proc.on("close", (code, terminationSignal) => {
|
|
1138
|
+
observeResource("settled");
|
|
1108
1139
|
procExited = true;
|
|
1109
1140
|
lastProcessSignal = terminationSignal ?? lastProcessSignal;
|
|
1110
1141
|
diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
|
|
@@ -1113,6 +1144,7 @@ async function runPiAgent(
|
|
|
1113
1144
|
});
|
|
1114
1145
|
|
|
1115
1146
|
proc.on("error", (error) => {
|
|
1147
|
+
observeResource(proc.pid ? "unknown" : "settled");
|
|
1116
1148
|
procExited = true;
|
|
1117
1149
|
appendStderrDiagnostic(currentResult, `process error: ${error?.message || String(error)}`);
|
|
1118
1150
|
diagnose({
|
package/store.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { WidgetRenderCtx } from "./widget.js";
|
|
|
18
18
|
export const COLLAPSED_ITEM_COUNT = 10;
|
|
19
19
|
|
|
20
20
|
export interface SubagentStore {
|
|
21
|
+
asyncTasks?: import("./async-task-lifecycle.js").SubagentAsyncTasks;
|
|
21
22
|
/** True after session_shutdown; async callbacks must not use captured runtime APIs. */
|
|
22
23
|
disposed: boolean;
|
|
23
24
|
commandRuns: Map<number, CommandRunState>;
|