pi-fast-subagent 0.3.0 → 0.5.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 +74 -3
- package/background-job-manager.ts +178 -0
- package/background-types.ts +36 -0
- package/index.ts +363 -13
- package/package.json +18 -3
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ 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
|
|
12
|
+
- Slash commands for background job status + cancellation via selector UI
|
|
11
13
|
- Per-call model override
|
|
12
14
|
- User + project agent discovery
|
|
13
15
|
- Project agents override user agents
|
|
@@ -61,22 +63,64 @@ You are code exploration specialist. Read relevant files, trace data flow, summa
|
|
|
61
63
|
|
|
62
64
|
## Slash Commands
|
|
63
65
|
|
|
64
|
-
### `/agent`
|
|
66
|
+
### `/fast-subagent:agent`
|
|
65
67
|
|
|
66
68
|
List all available agents:
|
|
67
69
|
|
|
68
70
|
```
|
|
69
|
-
/agent
|
|
71
|
+
/fast-subagent:agent
|
|
70
72
|
```
|
|
71
73
|
|
|
72
74
|
Show details for a specific agent (description, file path, model, tools, system prompt):
|
|
73
75
|
|
|
74
76
|
```
|
|
75
|
-
/agent scout
|
|
77
|
+
/fast-subagent:agent scout
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
Tab-completion is supported for agent names.
|
|
79
81
|
|
|
82
|
+
### `/fast-subagent:bg`
|
|
83
|
+
|
|
84
|
+
Detach running foreground subagent to background:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
/fast-subagent:bg fg_ab12cd34
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Omit job id to list active foreground jobs:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
/fast-subagent:bg
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `/fast-subagent:bg-status`
|
|
97
|
+
|
|
98
|
+
Show active background subagents in selector UI. Arrow keys move selection. Enter shows full details for selected job.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
/fast-subagent:bg-status
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Show details for specific background job:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
/fast-subagent:bg-status sa_ab12cd34
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `/fast-subagent:bg-cancel`
|
|
111
|
+
|
|
112
|
+
Cancel running background subagent. Omit job id to open selector UI, then choose job with arrow keys.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
/fast-subagent:bg-cancel
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Cancel specific background job directly:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
/fast-subagent:bg-cancel sa_ab12cd34
|
|
122
|
+
```
|
|
123
|
+
|
|
80
124
|
## Usage
|
|
81
125
|
|
|
82
126
|
### List agents
|
|
@@ -125,6 +169,33 @@ subagent({
|
|
|
125
169
|
})
|
|
126
170
|
```
|
|
127
171
|
|
|
172
|
+
### Background (fire-and-forget)
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
// Dispatch — returns job ID immediately
|
|
176
|
+
subagent({ agent: "scout", task: "Explore src", background: true })
|
|
177
|
+
// → { jobId: "sa_ab12cd34", status: "running" }
|
|
178
|
+
|
|
179
|
+
// Poll — check result / progress
|
|
180
|
+
subagent({ action: "poll", jobId: "sa_ab12cd34" })
|
|
181
|
+
|
|
182
|
+
// Cancel
|
|
183
|
+
subagent({ action: "cancel", jobId: "sa_ab12cd34" })
|
|
184
|
+
|
|
185
|
+
// List all background jobs
|
|
186
|
+
subagent({ action: "status" })
|
|
187
|
+
|
|
188
|
+
// Detach a running foreground job to background
|
|
189
|
+
subagent({ action: "detach", jobId: "fg_ab12cd34" })
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Roadmap
|
|
193
|
+
|
|
194
|
+
Goal: keep this extension **small and focused** — aligned with pi's philosophy of minimal, composable tooling. No feature creep. Every addition must earn its place.
|
|
195
|
+
|
|
196
|
+
- **UI/UX polish** — improve visibility of running subagents: clearer status lines, better progress feedback, agent name + task always visible during execution
|
|
197
|
+
- **Background agents** — ala claude code
|
|
198
|
+
|
|
128
199
|
## Notes
|
|
129
200
|
|
|
130
201
|
- Async/background isolation not supported in-process
|
|
@@ -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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Agent .md files are compatible with pi-subagents frontmatter format.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
11
12
|
import type {
|
|
12
13
|
AgentToolResult,
|
|
13
14
|
AgentToolUpdateCallback,
|
|
@@ -15,8 +16,10 @@ import type {
|
|
|
15
16
|
ExtensionContext,
|
|
16
17
|
ToolRenderResultOptions,
|
|
17
18
|
} from "@mariozechner/pi-coding-agent";
|
|
19
|
+
import { BackgroundJobManager } from "./background-job-manager.js";
|
|
20
|
+
import type { BackgroundHandleLike, BackgroundJobResult, BackgroundSubagentJob } from "./background-types.js";
|
|
18
21
|
import { Theme } from "@mariozechner/pi-coding-agent";
|
|
19
|
-
import { truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
22
|
+
import { Key, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
20
23
|
import { truncateToVisualLines, keyHint } from "@mariozechner/pi-coding-agent";
|
|
21
24
|
import {
|
|
22
25
|
AuthStorage,
|
|
@@ -94,6 +97,9 @@ function summarizeToolArgs(toolName: unknown, toolInput: unknown): string {
|
|
|
94
97
|
|
|
95
98
|
let _authStorage: ReturnType<typeof AuthStorage.create> | null = null;
|
|
96
99
|
let _modelRegistry: ReturnType<typeof ModelRegistry.create> | null = null;
|
|
100
|
+
let _bgManager: BackgroundJobManager | null = null;
|
|
101
|
+
let _onBgJobComplete: ((job: BackgroundSubagentJob) => void) | null = null;
|
|
102
|
+
let _setBgStatus: ((text: string | undefined) => void) | null = null;
|
|
97
103
|
|
|
98
104
|
function getAuth() {
|
|
99
105
|
if (!_authStorage) _authStorage = AuthStorage.create();
|
|
@@ -101,6 +107,27 @@ function getAuth() {
|
|
|
101
107
|
return { authStorage: _authStorage, modelRegistry: _modelRegistry };
|
|
102
108
|
}
|
|
103
109
|
|
|
110
|
+
function getBgManager(): BackgroundJobManager {
|
|
111
|
+
if (!_bgManager) _bgManager = new BackgroundJobManager({
|
|
112
|
+
onJobComplete: (job) => _onBgJobComplete?.(job),
|
|
113
|
+
});
|
|
114
|
+
return _bgManager;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function refreshBgStatus(): void {
|
|
118
|
+
const running = getBgManager().getRunningJobs();
|
|
119
|
+
_setBgStatus?.(running.length > 0 ? `⧗ ${running.length} bg agent${running.length > 1 ? "s" : ""}` : undefined);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Foreground detach registry ───────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
interface ForegroundDetachEntry {
|
|
125
|
+
agentName: string;
|
|
126
|
+
task: string;
|
|
127
|
+
detach: () => string; // returns bg job id
|
|
128
|
+
}
|
|
129
|
+
const _fgJobs = new Map<string, ForegroundDetachEntry>();
|
|
130
|
+
|
|
104
131
|
// ─── In-process runner ───────────────────────────────────────────────────────
|
|
105
132
|
|
|
106
133
|
const MAX_DEPTH = 2;
|
|
@@ -142,6 +169,7 @@ interface SubagentDetails {
|
|
|
142
169
|
running: boolean;
|
|
143
170
|
elapsedMs?: number;
|
|
144
171
|
model?: string;
|
|
172
|
+
backgroundJobId?: string;
|
|
145
173
|
toolCalls: ToolCallEntry[];
|
|
146
174
|
}
|
|
147
175
|
|
|
@@ -154,6 +182,26 @@ function formatDuration(ms: number): string {
|
|
|
154
182
|
return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
|
|
155
183
|
}
|
|
156
184
|
|
|
185
|
+
function summarizeTask(task: string, max = 60): string {
|
|
186
|
+
return task.length > max ? task.slice(0, max - 3) + "..." : task;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatBgJobSummary(job: BackgroundSubagentJob, now = Date.now()): string {
|
|
190
|
+
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
|
|
191
|
+
return `${job.id} [${job.status}] ${job.agentName} · ${dur} · ${summarizeTask(job.task)}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): string {
|
|
195
|
+
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
|
|
196
|
+
const lines = [`${job.id} [${job.status}] ${job.agentName} · ${dur}`, `Task: ${job.task}`];
|
|
197
|
+
if (job.model) lines.push(`Model: ${job.model}`);
|
|
198
|
+
if (job.status === "completed") lines.push(`\nResult:\n${job.resultSummary ?? "(no output)"}`);
|
|
199
|
+
if (job.status === "failed") lines.push(`\nError: ${job.error ?? "(unknown)"}`);
|
|
200
|
+
if (job.status === "cancelled") lines.push("\nCancelled.");
|
|
201
|
+
if (job.status === "running") lines.push("\nStill running.");
|
|
202
|
+
return lines.join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
157
205
|
// Module-level depth counter — avoids process.env race conditions in parallel mode
|
|
158
206
|
let _currentDepth = 0;
|
|
159
207
|
|
|
@@ -231,7 +279,10 @@ async function runAgent(
|
|
|
231
279
|
const toolCalls: ToolCallEntry[] = [];
|
|
232
280
|
const toolStartTimes = new Map<string, number>();
|
|
233
281
|
|
|
282
|
+
let done = false;
|
|
283
|
+
|
|
234
284
|
function emitUpdate(): void {
|
|
285
|
+
if (done) return;
|
|
235
286
|
onUpdate?.({
|
|
236
287
|
content: [{ type: "text", text: currentDelta || lastOutput || "" }],
|
|
237
288
|
details: {
|
|
@@ -354,6 +405,7 @@ async function runAgent(
|
|
|
354
405
|
exitCode = 1;
|
|
355
406
|
error = signal?.aborted ? "Aborted" : e instanceof Error ? e.message : String(e);
|
|
356
407
|
} finally {
|
|
408
|
+
done = true;
|
|
357
409
|
clearInterval(heartbeat);
|
|
358
410
|
unsubscribe();
|
|
359
411
|
session.dispose();
|
|
@@ -436,14 +488,22 @@ const SubagentParams = Type.Object({
|
|
|
436
488
|
Type.Number({ description: "Max parallel concurrency (default: 4)", default: 4 }),
|
|
437
489
|
),
|
|
438
490
|
|
|
491
|
+
// Background
|
|
492
|
+
background: Type.Optional(Type.Boolean({ description: "Run in background, returns job ID immediately" })),
|
|
493
|
+
jobId: Type.Optional(Type.String({ description: "Job ID for poll/cancel" })),
|
|
494
|
+
|
|
439
495
|
// Management
|
|
440
496
|
action: Type.Optional(
|
|
441
497
|
Type.Union(
|
|
442
498
|
[
|
|
443
499
|
Type.Literal("list"),
|
|
444
500
|
Type.Literal("get"),
|
|
501
|
+
Type.Literal("status"),
|
|
502
|
+
Type.Literal("poll"),
|
|
503
|
+
Type.Literal("cancel"),
|
|
504
|
+
Type.Literal("detach"),
|
|
445
505
|
],
|
|
446
|
-
{ description: "'list'
|
|
506
|
+
{ description: "'list'/'get' for agents, 'status' for bg jobs, 'poll'/'cancel' for a specific job, 'detach' to move a foreground job to background" },
|
|
447
507
|
),
|
|
448
508
|
),
|
|
449
509
|
agentScope: Type.Optional(
|
|
@@ -457,9 +517,65 @@ const SubagentParams = Type.Object({
|
|
|
457
517
|
// ─── Extension entry point ────────────────────────────────────────────────────
|
|
458
518
|
|
|
459
519
|
export default function (pi: ExtensionAPI) {
|
|
520
|
+
// ─── Status keys ────────────────────────────────────────────────────────────────────
|
|
521
|
+
const BG_STATUS_KEY = "fast-subagent-bg";
|
|
522
|
+
const FG_STATUS_KEY = "fast-subagent-fg";
|
|
523
|
+
|
|
524
|
+
// ─── Background job lifecycle ─────────────────────────────────────────────────────
|
|
525
|
+
_onBgJobComplete = (job) => {
|
|
526
|
+
refreshBgStatus();
|
|
527
|
+
const elapsed = job.completedAt ? ((job.completedAt - job.startedAt) / 1000).toFixed(1) : "?";
|
|
528
|
+
const statusEmoji = job.status === "completed" ? "✓" : "✗";
|
|
529
|
+
const taskPreview = job.task.length > 80 ? `${job.task.slice(0, 80)}…` : job.task;
|
|
530
|
+
const output = job.status === "completed"
|
|
531
|
+
? (job.resultSummary ?? "(no output)")
|
|
532
|
+
: `Error: ${job.error ?? "unknown"}`;
|
|
533
|
+
const modelInfo = job.model ? ` · ${job.model}` : "";
|
|
534
|
+
pi.sendUserMessage(
|
|
535
|
+
[
|
|
536
|
+
`**Background subagent ${statusEmoji}: ${job.id}** (${job.agentName}, ${elapsed}s${modelInfo})`,
|
|
537
|
+
`> ${taskPreview}`,
|
|
538
|
+
``,
|
|
539
|
+
output,
|
|
540
|
+
].join("\n"),
|
|
541
|
+
{ deliverAs: "followUp" },
|
|
542
|
+
);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
546
|
+
_setBgStatus = (text) => ctx.ui.setStatus(BG_STATUS_KEY, text);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
pi.on("session_shutdown", async () => {
|
|
550
|
+
getBgManager().shutdown();
|
|
551
|
+
_bgManager = null;
|
|
552
|
+
_setBgStatus = null;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// ─── Ctrl+Shift+B — move foreground subagent to background ─────────────────────────
|
|
556
|
+
pi.registerShortcut(Key.ctrlShift("b"), {
|
|
557
|
+
description: "Move foreground subagent to background",
|
|
558
|
+
handler: async (ctx) => {
|
|
559
|
+
const entry = [..._fgJobs.values()][0];
|
|
560
|
+
if (!entry) {
|
|
561
|
+
ctx.ui.notify("No foreground subagent running.", "info");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
const bgJobId = entry.detach();
|
|
566
|
+
ctx.ui.notify(
|
|
567
|
+
`Moved ${entry.agentName} to background as ${bgJobId}. Completion will be announced automatically.`,
|
|
568
|
+
"info",
|
|
569
|
+
);
|
|
570
|
+
} catch (e) {
|
|
571
|
+
ctx.ui.notify(e instanceof Error ? e.message : String(e), "error");
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
|
|
460
576
|
// ─── /agent slash command ─────────────────────────────────────────────────
|
|
461
|
-
pi.registerCommand("agent", {
|
|
462
|
-
description: "List available subagents. Usage: /agent [name] — show details for a specific agent.",
|
|
577
|
+
pi.registerCommand("fast-subagent:agent", {
|
|
578
|
+
description: "List available subagents. Usage: /fast-subagent:agent [name] — show details for a specific agent.",
|
|
463
579
|
getArgumentCompletions(prefix: string) {
|
|
464
580
|
const agents = discoverAgents(process.cwd());
|
|
465
581
|
return agents
|
|
@@ -518,11 +634,132 @@ export default function (pi: ExtensionAPI) {
|
|
|
518
634
|
}
|
|
519
635
|
}
|
|
520
636
|
lines.push("");
|
|
521
|
-
lines.push("Tip: /agent <name> for details · Add .md files to .pi/agents/ to create new agents");
|
|
637
|
+
lines.push("Tip: /fast-subagent:agent <name> for details · Add .md files to .pi/agents/ to create new agents");
|
|
522
638
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
523
639
|
},
|
|
524
640
|
});
|
|
525
641
|
|
|
642
|
+
// ─── /bg slash command ────────────────────────────────────────────────────
|
|
643
|
+
pi.registerCommand("fast-subagent:bg", {
|
|
644
|
+
description: "Move a running foreground subagent to background. Shortcut: Ctrl+Shift+B. Usage: /fast-subagent:bg [fg-job-id] — omit ID to list active foreground jobs.",
|
|
645
|
+
getArgumentCompletions(_prefix: string) {
|
|
646
|
+
return [..._fgJobs.keys()].map((id) => ({ value: id, label: id }));
|
|
647
|
+
},
|
|
648
|
+
async handler(args: string, ctx) {
|
|
649
|
+
const id = args.trim();
|
|
650
|
+
if (!id) {
|
|
651
|
+
if (_fgJobs.size === 0) {
|
|
652
|
+
ctx.ui.notify("No active foreground subagent jobs.", "info");
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const lines = ["Active foreground jobs (use /fast-subagent:bg <id> to detach):"];
|
|
656
|
+
for (const [fgId, entry] of _fgJobs) {
|
|
657
|
+
lines.push(` ${fgId} ${entry.agentName}: ${summarizeTask(entry.task)}`);
|
|
658
|
+
}
|
|
659
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const entry = _fgJobs.get(id);
|
|
663
|
+
if (!entry) {
|
|
664
|
+
ctx.ui.notify(`Foreground job "${id}" not found (already done or invalid).`, "warning");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const bgJobId = entry.detach();
|
|
668
|
+
ctx.ui.notify(
|
|
669
|
+
`Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`,
|
|
670
|
+
"info",
|
|
671
|
+
);
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// ─── /bg-status slash command ─────────────────────────────────────────────
|
|
676
|
+
pi.registerCommand("fast-subagent:bg-status", {
|
|
677
|
+
description: "Show active background subagents. Usage: /fast-subagent:bg-status [sa-job-id] — omit ID to open selector.",
|
|
678
|
+
getArgumentCompletions(prefix: string) {
|
|
679
|
+
return getBgManager().getAllJobs()
|
|
680
|
+
.filter((job) => job.id.startsWith(prefix))
|
|
681
|
+
.map((job) => ({ value: job.id, label: formatBgJobSummary(job) }));
|
|
682
|
+
},
|
|
683
|
+
async handler(args: string, ctx) {
|
|
684
|
+
const id = args.trim();
|
|
685
|
+
if (id) {
|
|
686
|
+
const job = getBgManager().getJob(id);
|
|
687
|
+
if (!job) {
|
|
688
|
+
ctx.ui.notify(`Background job "${id}" not found.`, "warning");
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
ctx.ui.notify(formatBgJobDetails(job), "info");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const jobs = getBgManager().getRunningJobs().sort((a, b) => b.startedAt - a.startedAt);
|
|
696
|
+
if (jobs.length === 0) {
|
|
697
|
+
ctx.ui.notify("No active background subagent jobs.", "info");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const options = jobs.map((job) => formatBgJobSummary(job));
|
|
702
|
+
const selected = await ctx.ui.select("Active background subagents", options);
|
|
703
|
+
if (!selected) return;
|
|
704
|
+
|
|
705
|
+
const jobId = selected.split(" ")[0] ?? "";
|
|
706
|
+
const job = getBgManager().getJob(jobId);
|
|
707
|
+
if (!job) {
|
|
708
|
+
ctx.ui.notify(`Background job "${jobId}" not found.`, "warning");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
ctx.ui.notify(formatBgJobDetails(job), "info");
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// ─── /bg-cancel slash command ─────────────────────────────────────────────
|
|
716
|
+
pi.registerCommand("fast-subagent:bg-cancel", {
|
|
717
|
+
description: "Cancel running background subagent. Usage: /fast-subagent:bg-cancel [sa-job-id] — omit ID to choose with arrow keys.",
|
|
718
|
+
getArgumentCompletions(prefix: string) {
|
|
719
|
+
return getBgManager().getRunningJobs()
|
|
720
|
+
.filter((job) => job.id.startsWith(prefix))
|
|
721
|
+
.map((job) => ({ value: job.id, label: formatBgJobSummary(job) }));
|
|
722
|
+
},
|
|
723
|
+
async handler(args: string, ctx) {
|
|
724
|
+
let jobId = args.trim();
|
|
725
|
+
|
|
726
|
+
if (!jobId) {
|
|
727
|
+
const jobs = getBgManager().getRunningJobs().sort((a, b) => b.startedAt - a.startedAt);
|
|
728
|
+
if (jobs.length === 0) {
|
|
729
|
+
ctx.ui.notify("No running background subagent jobs to cancel.", "info");
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const options = jobs.map((job) => formatBgJobSummary(job));
|
|
734
|
+
const selected = await ctx.ui.select("Cancel background subagent", options);
|
|
735
|
+
if (!selected) return;
|
|
736
|
+
jobId = selected.split(" ")[0] ?? "";
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const job = getBgManager().getJob(jobId);
|
|
740
|
+
if (!job) {
|
|
741
|
+
ctx.ui.notify(`Background job "${jobId}" not found.`, "warning");
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (job.status !== "running") {
|
|
745
|
+
ctx.ui.notify(`Background job "${jobId}" already ${job.status}.`, "info");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const confirmed = await ctx.ui.confirm(
|
|
750
|
+
"Cancel background subagent?",
|
|
751
|
+
`${formatBgJobSummary(job)}\n\nTask:\n${job.task}`,
|
|
752
|
+
);
|
|
753
|
+
if (!confirmed) return;
|
|
754
|
+
|
|
755
|
+
const result = getBgManager().cancel(jobId);
|
|
756
|
+
const msg = result === "cancelled" ? `Background job "${jobId}" cancelled.`
|
|
757
|
+
: result === "already_done" ? `Background job "${jobId}" already completed.`
|
|
758
|
+
: `Background job "${jobId}" not found.`;
|
|
759
|
+
ctx.ui.notify(msg, result === "cancelled" ? "info" : "warning");
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
|
|
526
763
|
pi.registerTool({
|
|
527
764
|
name: "subagent",
|
|
528
765
|
label: "Subagent",
|
|
@@ -614,6 +851,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
614
851
|
}
|
|
615
852
|
|
|
616
853
|
function statusLine(): string {
|
|
854
|
+
if (details.backgroundJobId) return `moved to background · ${details.backgroundJobId}`;
|
|
617
855
|
if (details.running) {
|
|
618
856
|
const parts: string[] = ["running"];
|
|
619
857
|
if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
|
|
@@ -701,6 +939,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
701
939
|
: "";
|
|
702
940
|
const statusWithHint = [status, expandHint].filter(Boolean).join(" ");
|
|
703
941
|
if (statusWithHint) out.push(truncateToWidth(statusWithHint, width, "..."));
|
|
942
|
+
if (details.running && !details.backgroundJobId)
|
|
943
|
+
out.push(truncateToWidth(theme.fg("dim", "Ctrl+Shift+B: move to background"), width, "..."));
|
|
704
944
|
|
|
705
945
|
return out;
|
|
706
946
|
},
|
|
@@ -721,7 +961,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
721
961
|
};
|
|
722
962
|
|
|
723
963
|
// ── Management: list ──────────────────────────────────────────────────────
|
|
724
|
-
if (params.action === "list" || (!params.agent && !params.tasks)) {
|
|
964
|
+
if (params.action === "list" || (!params.action && !params.agent && !params.tasks)) {
|
|
725
965
|
if (agents.length === 0) {
|
|
726
966
|
return {
|
|
727
967
|
content: [{
|
|
@@ -750,20 +990,130 @@ export default function (pi: ExtensionAPI) {
|
|
|
750
990
|
return { content: [{ type: "text", text: info }] };
|
|
751
991
|
}
|
|
752
992
|
|
|
993
|
+
// ── Background status ───────────────────────────────────────────────────
|
|
994
|
+
if (params.action === "status") {
|
|
995
|
+
const jobs = getBgManager().getAllJobs();
|
|
996
|
+
if (jobs.length === 0) return { content: [{ type: "text", text: "No background jobs." }] };
|
|
997
|
+
const lines = jobs.map((j) => {
|
|
998
|
+
const dur = j.completedAt ? formatDuration(j.completedAt - j.startedAt) : formatDuration(Date.now() - j.startedAt);
|
|
999
|
+
return `${j.id} [${j.status}] ${j.agentName} · ${dur} · ${j.task.length > 50 ? j.task.slice(0, 47) + "..." : j.task}`;
|
|
1000
|
+
});
|
|
1001
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// ── Background poll ────────────────────────────────────────────────────────
|
|
1005
|
+
if (params.action === "poll") {
|
|
1006
|
+
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to poll." }] };
|
|
1007
|
+
const job = getBgManager().getJob(params.jobId);
|
|
1008
|
+
if (!job) return { content: [{ type: "text", text: `Job ${params.jobId} not found (completed and evicted, or invalid).` }] };
|
|
1009
|
+
const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(Date.now() - job.startedAt);
|
|
1010
|
+
const parts = [`${job.id} [${job.status}] ${job.agentName} · ${dur}`, `Task: ${job.task}`];
|
|
1011
|
+
if (job.status === "completed") parts.push(`\nResult:\n${job.resultSummary ?? "(no output)"}`);
|
|
1012
|
+
if (job.status === "failed") parts.push(`\nError: ${job.error ?? "(unknown)"}`);
|
|
1013
|
+
if (job.status === "running") parts.push("Still running — poll again later.");
|
|
1014
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// ── Background cancel ──────────────────────────────────────────────────────
|
|
1018
|
+
if (params.action === "cancel") {
|
|
1019
|
+
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId to cancel." }] };
|
|
1020
|
+
const result = getBgManager().cancel(params.jobId);
|
|
1021
|
+
const msg = result === "cancelled" ? `Job ${params.jobId} cancelled.`
|
|
1022
|
+
: result === "already_done" ? `Job ${params.jobId} already completed.`
|
|
1023
|
+
: `Job ${params.jobId} not found.`;
|
|
1024
|
+
return { content: [{ type: "text", text: msg }] };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ── Foreground → background detach ────────────────────────────────────────
|
|
1028
|
+
if (params.action === "detach") {
|
|
1029
|
+
if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId (fg_xxxxx) to detach." }] };
|
|
1030
|
+
const fgEntry = _fgJobs.get(params.jobId);
|
|
1031
|
+
if (!fgEntry) return { content: [{ type: "text", text: `Foreground job "${params.jobId}" not found (already completed or invalid).` }] };
|
|
1032
|
+
const bgJobId = fgEntry.detach();
|
|
1033
|
+
return { content: [{ type: "text", text: `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.` }] };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
753
1036
|
// ── Single mode ───────────────────────────────────────────────────────────
|
|
754
1037
|
if (params.agent && params.task) {
|
|
755
1038
|
const { agent, error } = findAgent(params.agent);
|
|
756
1039
|
if (error || !agent) return { content: [{ type: "text", text: error ?? "Not found" }] };
|
|
757
1040
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1041
|
+
// Background dispatch — fire and forget
|
|
1042
|
+
if (params.background) {
|
|
1043
|
+
const bgAbort = new AbortController();
|
|
1044
|
+
const handle: BackgroundHandleLike = { abort: () => bgAbort.abort() };
|
|
1045
|
+
const resultPromise: Promise<BackgroundJobResult> = runAgent(
|
|
1046
|
+
agent, params.task, cwd, params.model, bgAbort.signal, undefined
|
|
1047
|
+
).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
|
|
1048
|
+
const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
|
|
1049
|
+
return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Foreground run with detach support
|
|
1053
|
+
const fgId = `fg_${randomUUID().slice(0, 8)}`;
|
|
1054
|
+
const agentAbort = new AbortController();
|
|
1055
|
+
const forwardAbort = () => agentAbort.abort();
|
|
1056
|
+
signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
1057
|
+
|
|
1058
|
+
let detachResolveFn: ((bgJobId: string) => void) | null = null;
|
|
1059
|
+
const detachPromise = new Promise<string>((resolve) => { detachResolveFn = resolve; });
|
|
1060
|
+
|
|
1061
|
+
// Wrap onUpdate so detach can stop forwarding updates to the parent
|
|
1062
|
+
// agent's listener (which becomes invalid once execute() returns).
|
|
1063
|
+
let forwardUpdates = true;
|
|
1064
|
+
const wrappedOnUpdate: OnUpdate | undefined = onUpdate
|
|
1065
|
+
? (partial) => { if (forwardUpdates) onUpdate(partial); }
|
|
1066
|
+
: undefined;
|
|
1067
|
+
|
|
1068
|
+
const agentRunPromise: Promise<RunResult> = runAgent(
|
|
1069
|
+
agent, params.task, cwd, params.model, agentAbort.signal, wrappedOnUpdate,
|
|
765
1070
|
);
|
|
766
1071
|
|
|
1072
|
+
// Derived promise for the bg manager (used only if we detach)
|
|
1073
|
+
const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
|
|
1074
|
+
.then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
|
|
1075
|
+
|
|
1076
|
+
_fgJobs.set(fgId, {
|
|
1077
|
+
agentName: agent.name,
|
|
1078
|
+
task: params.task,
|
|
1079
|
+
detach: () => {
|
|
1080
|
+
forwardUpdates = false;
|
|
1081
|
+
signal?.removeEventListener("abort", forwardAbort);
|
|
1082
|
+
const bgHandle: BackgroundHandleLike = { abort: () => agentAbort.abort() };
|
|
1083
|
+
const bgJobId = getBgManager().adoptHandle(agent.name, params.task, cwd, bgHandle, bgResultPromise);
|
|
1084
|
+
refreshBgStatus();
|
|
1085
|
+
detachResolveFn?.(bgJobId);
|
|
1086
|
+
return bgJobId;
|
|
1087
|
+
},
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
ctx.ui.setStatus(FG_STATUS_KEY, `${agent.name} running · Ctrl+Shift+B to move to background`);
|
|
1091
|
+
|
|
1092
|
+
let runResult: RunResult | null = null;
|
|
1093
|
+
const outcome = await Promise.race([
|
|
1094
|
+
agentRunPromise.then((r) => { runResult = r; return "done" as const; }),
|
|
1095
|
+
detachPromise.then(() => "detached" as const),
|
|
1096
|
+
]).finally(() => {
|
|
1097
|
+
_fgJobs.delete(fgId);
|
|
1098
|
+
signal?.removeEventListener("abort", forwardAbort);
|
|
1099
|
+
ctx.ui.setStatus(FG_STATUS_KEY, undefined);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
if (outcome === "detached") {
|
|
1103
|
+
const bgJobId = await detachPromise; // already resolved — instant
|
|
1104
|
+
return {
|
|
1105
|
+
content: [{ type: "text", text: `Moved to background: ${bgJobId}. Completion will be announced automatically.` }],
|
|
1106
|
+
details: {
|
|
1107
|
+
task: params.task,
|
|
1108
|
+
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
1109
|
+
running: false,
|
|
1110
|
+
backgroundJobId: bgJobId,
|
|
1111
|
+
toolCalls: [],
|
|
1112
|
+
} satisfies SubagentDetails,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const result = runResult!;
|
|
767
1117
|
return {
|
|
768
1118
|
content: [{ type: "text", text: getFinalText(result) }],
|
|
769
1119
|
details: {
|
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.5.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
|
],
|