pi-fast-subagent 0.3.0 โ 0.4.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/README.md +1 -0
- package/background-job-manager.ts +178 -0
- package/background-types.ts +36 -0
- package/index.ts +66 -2
- package/package.json +18 -3
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ Runs subagents with `createAgentSession()` in same process instead of spawning `
|
|
|
8
8
|
|
|
9
9
|
- Single mode: `{ agent, task }`
|
|
10
10
|
- Parallel mode: `{ tasks: [...] }`
|
|
11
|
+
- Background mode: `{ agent, task, background: true }` โ fire-and-forget with poll/cancel
|
|
11
12
|
- Per-call model override
|
|
12
13
|
- User + project agent discovery
|
|
13
14
|
- Project agents override user agents
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
BackgroundHandleLike,
|
|
4
|
+
BackgroundJobManagerOptions,
|
|
5
|
+
BackgroundJobResult,
|
|
6
|
+
BackgroundSubagentJob,
|
|
7
|
+
} from "./background-types.js";
|
|
8
|
+
|
|
9
|
+
export type { BackgroundSubagentJob } from "./background-types.js";
|
|
10
|
+
|
|
11
|
+
export class BackgroundJobManager {
|
|
12
|
+
private jobs = new Map<string, BackgroundSubagentJob>();
|
|
13
|
+
private evictionTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
14
|
+
private maxRunning: number;
|
|
15
|
+
private maxTotal: number;
|
|
16
|
+
private evictionMs: number;
|
|
17
|
+
private onJobComplete?: (job: BackgroundSubagentJob) => void;
|
|
18
|
+
private isShutdown = false;
|
|
19
|
+
|
|
20
|
+
constructor(options: BackgroundJobManagerOptions = {}) {
|
|
21
|
+
this.maxRunning = options.maxRunning ?? 10;
|
|
22
|
+
this.maxTotal = options.maxTotal ?? 50;
|
|
23
|
+
this.evictionMs = options.evictionMs ?? 5 * 60 * 1000;
|
|
24
|
+
this.onJobComplete = options.onJobComplete;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
adoptHandle(
|
|
28
|
+
agentName: string,
|
|
29
|
+
task: string,
|
|
30
|
+
cwd: string,
|
|
31
|
+
handle: BackgroundHandleLike,
|
|
32
|
+
resultPromise: Promise<BackgroundJobResult>,
|
|
33
|
+
): string {
|
|
34
|
+
handle.detach?.();
|
|
35
|
+
|
|
36
|
+
const abortController = new AbortController();
|
|
37
|
+
const onAbort = () => handle.abort();
|
|
38
|
+
abortController.signal.addEventListener("abort", onAbort, { once: true });
|
|
39
|
+
|
|
40
|
+
return this.attachJob(agentName, task, cwd, abortController, resultPromise.finally(() => {
|
|
41
|
+
abortController.signal.removeEventListener("abort", onAbort);
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
cancel(id: string): "cancelled" | "not_found" | "already_done" {
|
|
46
|
+
const job = this.jobs.get(id);
|
|
47
|
+
if (!job) return "not_found";
|
|
48
|
+
if (job.status !== "running") return "already_done";
|
|
49
|
+
|
|
50
|
+
job.status = "cancelled";
|
|
51
|
+
job.completedAt = Date.now();
|
|
52
|
+
job.abortController.abort();
|
|
53
|
+
this.scheduleEviction(id);
|
|
54
|
+
return "cancelled";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getJob(id: string): BackgroundSubagentJob | undefined {
|
|
58
|
+
return this.jobs.get(id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getRunningJobs(): BackgroundSubagentJob[] {
|
|
62
|
+
return [...this.jobs.values()].filter((job) => job.status === "running");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getAllJobs(): BackgroundSubagentJob[] {
|
|
66
|
+
return [...this.jobs.values()];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
shutdown(): void {
|
|
70
|
+
this.isShutdown = true;
|
|
71
|
+
for (const timer of this.evictionTimers.values()) {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
this.evictionTimers.clear();
|
|
75
|
+
|
|
76
|
+
for (const job of this.jobs.values()) {
|
|
77
|
+
if (job.status === "running") {
|
|
78
|
+
job.status = "cancelled";
|
|
79
|
+
job.completedAt = Date.now();
|
|
80
|
+
job.abortController.abort();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private attachJob(
|
|
86
|
+
agentName: string,
|
|
87
|
+
task: string,
|
|
88
|
+
cwd: string,
|
|
89
|
+
abortController: AbortController,
|
|
90
|
+
resultPromise: Promise<BackgroundJobResult>,
|
|
91
|
+
): string {
|
|
92
|
+
if (this.getRunningJobs().length >= this.maxRunning) {
|
|
93
|
+
throw new Error(`Maximum concurrent background subagents reached (${this.maxRunning}).`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.jobs.size >= this.maxTotal) {
|
|
97
|
+
this.evictOldestCompleted();
|
|
98
|
+
if (this.jobs.size >= this.maxTotal) {
|
|
99
|
+
throw new Error(`Maximum total background subagent jobs reached (${this.maxTotal}).`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const id = `sa_${randomUUID().slice(0, 8)}`;
|
|
104
|
+
const job: BackgroundSubagentJob = {
|
|
105
|
+
id,
|
|
106
|
+
agentName,
|
|
107
|
+
task,
|
|
108
|
+
cwd,
|
|
109
|
+
status: "running",
|
|
110
|
+
startedAt: Date.now(),
|
|
111
|
+
abortController,
|
|
112
|
+
promise: undefined as unknown as Promise<void>,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
job.promise = resultPromise
|
|
116
|
+
.then((result) => {
|
|
117
|
+
if (job.status === "cancelled") {
|
|
118
|
+
this.scheduleEviction(id);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
job.status = result.exitCode === 0 ? "completed" : "failed";
|
|
122
|
+
job.completedAt = Date.now();
|
|
123
|
+
job.exitCode = result.exitCode;
|
|
124
|
+
job.resultSummary = result.summary;
|
|
125
|
+
job.error = result.error;
|
|
126
|
+
job.model = result.model;
|
|
127
|
+
this.scheduleEviction(id);
|
|
128
|
+
this.deliverResult(job);
|
|
129
|
+
})
|
|
130
|
+
.catch((error) => {
|
|
131
|
+
if (job.status === "cancelled") {
|
|
132
|
+
this.scheduleEviction(id);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
job.status = "failed";
|
|
136
|
+
job.completedAt = Date.now();
|
|
137
|
+
job.exitCode = 1;
|
|
138
|
+
job.error = error instanceof Error ? error.message : String(error);
|
|
139
|
+
this.scheduleEviction(id);
|
|
140
|
+
this.deliverResult(job);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.jobs.set(id, job);
|
|
144
|
+
return id;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private deliverResult(job: BackgroundSubagentJob): void {
|
|
148
|
+
if (!this.onJobComplete) return;
|
|
149
|
+
queueMicrotask(() => this.onJobComplete?.(job));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private scheduleEviction(id: string): void {
|
|
153
|
+
if (this.isShutdown) return;
|
|
154
|
+
const existing = this.evictionTimers.get(id);
|
|
155
|
+
if (existing) clearTimeout(existing);
|
|
156
|
+
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
this.evictionTimers.delete(id);
|
|
159
|
+
this.jobs.delete(id);
|
|
160
|
+
}, this.evictionMs);
|
|
161
|
+
|
|
162
|
+
this.evictionTimers.set(id, timer);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private evictOldestCompleted(): void {
|
|
166
|
+
let oldest: BackgroundSubagentJob | undefined;
|
|
167
|
+
for (const job of this.jobs.values()) {
|
|
168
|
+
if (job.status === "running") continue;
|
|
169
|
+
if (!oldest || job.startedAt < oldest.startedAt) oldest = job;
|
|
170
|
+
}
|
|
171
|
+
if (!oldest) return;
|
|
172
|
+
|
|
173
|
+
const timer = this.evictionTimers.get(oldest.id);
|
|
174
|
+
if (timer) clearTimeout(timer);
|
|
175
|
+
this.evictionTimers.delete(oldest.id);
|
|
176
|
+
this.jobs.delete(oldest.id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type BackgroundJobStatus = "running" | "completed" | "failed" | "cancelled";
|
|
2
|
+
|
|
3
|
+
export interface BackgroundSubagentJob {
|
|
4
|
+
id: string;
|
|
5
|
+
agentName: string;
|
|
6
|
+
task: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
status: BackgroundJobStatus;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
completedAt?: number;
|
|
11
|
+
exitCode?: number;
|
|
12
|
+
resultSummary?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
abortController: AbortController;
|
|
16
|
+
promise: Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BackgroundJobManagerOptions {
|
|
20
|
+
maxRunning?: number;
|
|
21
|
+
maxTotal?: number;
|
|
22
|
+
evictionMs?: number;
|
|
23
|
+
onJobComplete?: (job: BackgroundSubagentJob) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BackgroundJobResult {
|
|
27
|
+
summary: string;
|
|
28
|
+
exitCode: number;
|
|
29
|
+
error?: string;
|
|
30
|
+
model?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BackgroundHandleLike {
|
|
34
|
+
abort: () => void;
|
|
35
|
+
detach?: () => void;
|
|
36
|
+
}
|
package/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ import type {
|
|
|
15
15
|
ExtensionContext,
|
|
16
16
|
ToolRenderResultOptions,
|
|
17
17
|
} from "@mariozechner/pi-coding-agent";
|
|
18
|
+
import { BackgroundJobManager } from "./background-job-manager.js";
|
|
19
|
+
import type { BackgroundHandleLike, BackgroundJobResult } from "./background-types.js";
|
|
18
20
|
import { Theme } from "@mariozechner/pi-coding-agent";
|
|
19
21
|
import { truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
20
22
|
import { truncateToVisualLines, keyHint } from "@mariozechner/pi-coding-agent";
|
|
@@ -94,6 +96,7 @@ function summarizeToolArgs(toolName: unknown, toolInput: unknown): string {
|
|
|
94
96
|
|
|
95
97
|
let _authStorage: ReturnType<typeof AuthStorage.create> | null = null;
|
|
96
98
|
let _modelRegistry: ReturnType<typeof ModelRegistry.create> | null = null;
|
|
99
|
+
let _bgManager: BackgroundJobManager | null = null;
|
|
97
100
|
|
|
98
101
|
function getAuth() {
|
|
99
102
|
if (!_authStorage) _authStorage = AuthStorage.create();
|
|
@@ -101,6 +104,11 @@ function getAuth() {
|
|
|
101
104
|
return { authStorage: _authStorage, modelRegistry: _modelRegistry };
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
function getBgManager(): BackgroundJobManager {
|
|
108
|
+
if (!_bgManager) _bgManager = new BackgroundJobManager();
|
|
109
|
+
return _bgManager;
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
// โโโ In-process runner โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
105
113
|
|
|
106
114
|
const MAX_DEPTH = 2;
|
|
@@ -231,7 +239,10 @@ async function runAgent(
|
|
|
231
239
|
const toolCalls: ToolCallEntry[] = [];
|
|
232
240
|
const toolStartTimes = new Map<string, number>();
|
|
233
241
|
|
|
242
|
+
let done = false;
|
|
243
|
+
|
|
234
244
|
function emitUpdate(): void {
|
|
245
|
+
if (done) return;
|
|
235
246
|
onUpdate?.({
|
|
236
247
|
content: [{ type: "text", text: currentDelta || lastOutput || "" }],
|
|
237
248
|
details: {
|
|
@@ -354,6 +365,7 @@ async function runAgent(
|
|
|
354
365
|
exitCode = 1;
|
|
355
366
|
error = signal?.aborted ? "Aborted" : e instanceof Error ? e.message : String(e);
|
|
356
367
|
} finally {
|
|
368
|
+
done = true;
|
|
357
369
|
clearInterval(heartbeat);
|
|
358
370
|
unsubscribe();
|
|
359
371
|
session.dispose();
|
|
@@ -436,14 +448,21 @@ const SubagentParams = Type.Object({
|
|
|
436
448
|
Type.Number({ description: "Max parallel concurrency (default: 4)", default: 4 }),
|
|
437
449
|
),
|
|
438
450
|
|
|
451
|
+
// Background
|
|
452
|
+
background: Type.Optional(Type.Boolean({ description: "Run in background, returns job ID immediately" })),
|
|
453
|
+
jobId: Type.Optional(Type.String({ description: "Job ID for poll/cancel" })),
|
|
454
|
+
|
|
439
455
|
// Management
|
|
440
456
|
action: Type.Optional(
|
|
441
457
|
Type.Union(
|
|
442
458
|
[
|
|
443
459
|
Type.Literal("list"),
|
|
444
460
|
Type.Literal("get"),
|
|
461
|
+
Type.Literal("status"),
|
|
462
|
+
Type.Literal("poll"),
|
|
463
|
+
Type.Literal("cancel"),
|
|
445
464
|
],
|
|
446
|
-
{ description: "'list'
|
|
465
|
+
{ description: "'list'/'get' for agents, 'status' for bg jobs, 'poll'/'cancel' for a specific job" },
|
|
447
466
|
),
|
|
448
467
|
),
|
|
449
468
|
agentScope: Type.Optional(
|
|
@@ -721,7 +740,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
721
740
|
};
|
|
722
741
|
|
|
723
742
|
// โโ Management: list โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
724
|
-
if (params.action === "list" || (!params.agent && !params.tasks)) {
|
|
743
|
+
if (params.action === "list" || (!params.action && !params.agent && !params.tasks)) {
|
|
725
744
|
if (agents.length === 0) {
|
|
726
745
|
return {
|
|
727
746
|
content: [{
|
|
@@ -750,11 +769,56 @@ export default function (pi: ExtensionAPI) {
|
|
|
750
769
|
return { content: [{ type: "text", text: info }] };
|
|
751
770
|
}
|
|
752
771
|
|
|
772
|
+
// โโ Background status โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
773
|
+
if (params.action === "status") {
|
|
774
|
+
const jobs = getBgManager().getAllJobs();
|
|
775
|
+
if (jobs.length === 0) return { content: [{ type: "text", text: "No background jobs." }] };
|
|
776
|
+
const lines = jobs.map((j) => {
|
|
777
|
+
const dur = j.completedAt ? formatDuration(j.completedAt - j.startedAt) : formatDuration(Date.now() - j.startedAt);
|
|
778
|
+
return `${j.id} [${j.status}] ${j.agentName} ยท ${dur} ยท ${j.task.length > 50 ? j.task.slice(0, 47) + "..." : j.task}`;
|
|
779
|
+
});
|
|
780
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// โโ Background poll โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
784
|
+
if (params.action === "poll") {
|
|
785
|
+
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to poll." }] };
|
|
786
|
+
const job = getBgManager().getJob(params.jobId);
|
|
787
|
+
if (!job) return { content: [{ type: "text", text: `Job ${params.jobId} not found (completed and evicted, or invalid).` }] };
|
|
788
|
+
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(Date.now() - job.startedAt);
|
|
789
|
+
const parts = [`${job.id} [${job.status}] ${job.agentName} ยท ${dur}`, `Task: ${job.task}`];
|
|
790
|
+
if (job.status === "completed") parts.push(`\nResult:\n${job.resultSummary ?? "(no output)"}`);
|
|
791
|
+
if (job.status === "failed") parts.push(`\nError: ${job.error ?? "(unknown)"}`);
|
|
792
|
+
if (job.status === "running") parts.push("Still running โ poll again later.");
|
|
793
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// โโ Background cancel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
797
|
+
if (params.action === "cancel") {
|
|
798
|
+
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to cancel." }] };
|
|
799
|
+
const result = getBgManager().cancel(params.jobId);
|
|
800
|
+
const msg = result === "cancelled" ? `Job ${params.jobId} cancelled.`
|
|
801
|
+
: result === "already_done" ? `Job ${params.jobId} already completed.`
|
|
802
|
+
: `Job ${params.jobId} not found.`;
|
|
803
|
+
return { content: [{ type: "text", text: msg }] };
|
|
804
|
+
}
|
|
805
|
+
|
|
753
806
|
// โโ Single mode โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
754
807
|
if (params.agent && params.task) {
|
|
755
808
|
const { agent, error } = findAgent(params.agent);
|
|
756
809
|
if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
|
|
757
810
|
|
|
811
|
+
// Background dispatch โ fire and forget
|
|
812
|
+
if (params.background) {
|
|
813
|
+
const bgAbort = new AbortController();
|
|
814
|
+
const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
|
|
815
|
+
const resultPromise: Promise<BackgroundJobResult> = runAgent(
|
|
816
|
+
agent, params.task, cwd, params.model, bgAbort.signal, undefined
|
|
817
|
+
).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
|
|
818
|
+
const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
|
|
819
|
+
return { content: [{ type: "text", text: `Background job started: ${jobId}\nCheck progress: subagent({ action: "poll", jobId: "${jobId}" })` }] };
|
|
820
|
+
}
|
|
821
|
+
|
|
758
822
|
const result = await runAgent(
|
|
759
823
|
agent,
|
|
760
824
|
params.task,
|
package/package.json
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-fast-subagent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "In-process subagent delegation for pi with single, parallel, and
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "In-process subagent delegation for pi with single, parallel, and background modes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi",
|
|
9
|
+
"pi-extension",
|
|
9
10
|
"subagent",
|
|
10
11
|
"agents",
|
|
11
|
-
"extension"
|
|
12
|
+
"extension",
|
|
13
|
+
"delegation",
|
|
14
|
+
"parallel",
|
|
15
|
+
"concurrent",
|
|
16
|
+
"in-process",
|
|
17
|
+
"fast",
|
|
18
|
+
"coding-agent",
|
|
19
|
+
"ai-agent",
|
|
20
|
+
"llm",
|
|
21
|
+
"task-runner",
|
|
22
|
+
"scout",
|
|
23
|
+
"workflow",
|
|
24
|
+
"automation"
|
|
12
25
|
],
|
|
13
26
|
"homepage": "https://github.com/tuansondinh/pi-fast-subagent#readme",
|
|
14
27
|
"repository": {
|
|
@@ -21,6 +34,8 @@
|
|
|
21
34
|
"files": [
|
|
22
35
|
"index.ts",
|
|
23
36
|
"agents.ts",
|
|
37
|
+
"background-job-manager.ts",
|
|
38
|
+
"background-types.ts",
|
|
24
39
|
"agents/*.md",
|
|
25
40
|
"README.md"
|
|
26
41
|
],
|