@pi-unipi/subagents 2.4.0 → 2.4.1

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.
Files changed (66) hide show
  1. package/README.md +3 -1
  2. package/dist/agent-manager.d.ts +81 -0
  3. package/dist/agent-manager.d.ts.map +1 -0
  4. package/dist/agent-manager.js +292 -0
  5. package/dist/agent-manager.js.map +1 -0
  6. package/dist/agent-runner.d.ts +51 -0
  7. package/dist/agent-runner.d.ts.map +1 -0
  8. package/dist/agent-runner.js +262 -0
  9. package/dist/agent-runner.js.map +1 -0
  10. package/dist/config.d.ts +24 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +132 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/conversation-viewer.d.ts +40 -0
  15. package/dist/conversation-viewer.d.ts.map +1 -0
  16. package/dist/conversation-viewer.js +276 -0
  17. package/dist/conversation-viewer.js.map +1 -0
  18. package/dist/core-compat.d.ts +14 -0
  19. package/dist/core-compat.d.ts.map +1 -0
  20. package/dist/core-compat.js +24 -0
  21. package/dist/core-compat.js.map +1 -0
  22. package/dist/custom-agents.d.ts +14 -0
  23. package/dist/custom-agents.d.ts.map +1 -0
  24. package/dist/custom-agents.js +106 -0
  25. package/dist/custom-agents.js.map +1 -0
  26. package/dist/file-lock.d.ts +42 -0
  27. package/dist/file-lock.d.ts.map +1 -0
  28. package/dist/file-lock.js +91 -0
  29. package/dist/file-lock.js.map +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +751 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/model-resolver.d.ts +19 -0
  35. package/dist/model-resolver.d.ts.map +1 -0
  36. package/dist/model-resolver.js +61 -0
  37. package/dist/model-resolver.js.map +1 -0
  38. package/dist/types.d.ts +96 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +47 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/widget.d.ts +56 -0
  43. package/dist/widget.d.ts.map +1 -0
  44. package/dist/widget.js +396 -0
  45. package/dist/widget.js.map +1 -0
  46. package/package.json +10 -6
  47. package/src/__tests__/badge-generation.test.ts +0 -315
  48. package/src/__tests__/config.test.ts +0 -240
  49. package/src/__tests__/esc-propagation.test.ts +0 -162
  50. package/src/__tests__/file-lock.test.ts +0 -244
  51. package/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
  52. package/src/__tests__/workflow-integration.test.ts +0 -334
  53. package/src/agent-manager.ts +0 -334
  54. package/src/agent-runner.ts +0 -329
  55. package/src/config.ts +0 -147
  56. package/src/conversation-viewer.ts +0 -299
  57. package/src/custom-agents.ts +0 -118
  58. package/src/file-lock.ts +0 -102
  59. package/src/index.ts +0 -862
  60. package/src/model-resolver.ts +0 -79
  61. package/src/prompts.ts +0 -39
  62. package/src/skills/explore/SKILL.md +0 -32
  63. package/src/skills/work/SKILL.md +0 -40
  64. package/src/types.ts +0 -146
  65. package/src/widget.ts +0 -454
  66. package/tsconfig.json +0 -19
@@ -1,334 +0,0 @@
1
- /**
2
- * Test: Workflow integration — `/unipi:work` with subagent support
3
- *
4
- * Verifies:
5
- * - spawn_helper and get_helper_result tools are properly defined
6
- * - Agent types (explore, work) are correctly configured
7
- * - Concurrency limit is respected
8
- * - Custom agent type loading works
9
- * - System prompt builder generates correct prompts
10
- */
11
-
12
- import { describe, it } from "node:test";
13
- import assert from "node:assert/strict";
14
-
15
- // Test type definitions
16
- const BUILTIN_TYPES = ["explore", "work"] as const;
17
-
18
- interface AgentConfig {
19
- name: string;
20
- displayName?: string;
21
- description: string;
22
- builtinToolNames?: string[];
23
- disallowedTools?: string[];
24
- extensions: true | string[] | false;
25
- skills: true | string[] | false;
26
- model?: string;
27
- thinking?: string;
28
- maxTurns?: number;
29
- systemPrompt: string;
30
- promptMode: "replace" | "append";
31
- inheritContext?: boolean;
32
- runInBackground?: boolean;
33
- isolated?: boolean;
34
- memory?: string;
35
- isDefault?: boolean;
36
- enabled?: boolean;
37
- source?: "builtin" | "project" | "global";
38
- }
39
-
40
- // Test prompt builder
41
- function buildAgentPrompt(
42
- config: AgentConfig,
43
- cwd: string,
44
- env: { isGitRepo: boolean; branch: string; platform: string },
45
- parentSystemPrompt: string,
46
- ): string {
47
- if (config.promptMode === "append") {
48
- return [
49
- parentSystemPrompt,
50
- "",
51
- "---",
52
- "",
53
- `## Agent Role: ${config.displayName ?? config.name}`,
54
- config.systemPrompt,
55
- ].join("\n");
56
- }
57
-
58
- return [
59
- `# ${config.displayName ?? config.name}`,
60
- "",
61
- config.systemPrompt,
62
- "",
63
- "---",
64
- "",
65
- `Working directory: ${cwd}`,
66
- `Git: ${env.isGitRepo ? `${env.branch} on ${env.platform}` : "not a git repo"}`,
67
- ].join("\n");
68
- }
69
-
70
- // Test concurrency manager
71
- class ConcurrencyManager {
72
- private maxConcurrent: number;
73
- private running: number = 0;
74
- private queue: Array<{ id: string; resolve: () => void }> = [];
75
-
76
- constructor(maxConcurrent: number) {
77
- this.maxConcurrent = maxConcurrent;
78
- }
79
-
80
- async acquire(id: string): Promise<() => void> {
81
- if (this.running >= this.maxConcurrent) {
82
- await new Promise<void>((resolve) => {
83
- this.queue.push({ id, resolve });
84
- });
85
- }
86
-
87
- this.running++;
88
-
89
- let released = false;
90
- return () => {
91
- if (released) return;
92
- released = true;
93
- this.running--;
94
-
95
- // Start next in queue
96
- if (this.queue.length > 0) {
97
- const next = this.queue.shift()!;
98
- next.resolve();
99
- }
100
- };
101
- }
102
-
103
- getRunning(): number {
104
- return this.running;
105
- }
106
-
107
- getQueueLength(): number {
108
- return this.queue.length;
109
- }
110
- }
111
-
112
- describe("Workflow Integration", () => {
113
- describe("Tool Definitions", () => {
114
- it("should define spawn_helper tool with correct parameters", () => {
115
- const toolDef = {
116
- name: "spawn_helper",
117
- description: "Launch a sub-agent for parallel work.",
118
- parameters: {
119
- type: "object",
120
- required: ["type", "prompt", "description"],
121
- properties: {
122
- type: { type: "string" },
123
- prompt: { type: "string" },
124
- description: { type: "string" },
125
- run_in_background: { type: "boolean" },
126
- max_turns: { type: "number" },
127
- model: { type: "string" },
128
- thinking: { type: "string" },
129
- },
130
- },
131
- };
132
-
133
- assert.equal(toolDef.name, "spawn_helper");
134
- assert.equal(toolDef.parameters.required.length, 3);
135
- assert.ok(toolDef.parameters.properties.type);
136
- assert.ok(toolDef.parameters.properties.prompt);
137
- assert.ok(toolDef.parameters.properties.description);
138
- });
139
-
140
- it("should define get_helper_result tool with correct parameters", () => {
141
- const toolDef = {
142
- name: "get_helper_result",
143
- description: "Check status and retrieve results from a background agent.",
144
- parameters: {
145
- type: "object",
146
- required: ["agent_id"],
147
- properties: {
148
- agent_id: { type: "string" },
149
- wait: { type: "boolean" },
150
- },
151
- },
152
- };
153
-
154
- assert.equal(toolDef.name, "get_helper_result");
155
- assert.equal(toolDef.parameters.required.length, 1);
156
- assert.ok(toolDef.parameters.properties.agent_id);
157
- });
158
- });
159
-
160
- describe("Agent Types", () => {
161
- it("should have explore and work as builtin types", () => {
162
- assert.deepEqual(BUILTIN_TYPES, ["explore", "work"]);
163
- });
164
-
165
- it("should define explore agent with read-only tools", () => {
166
- const exploreConfig: AgentConfig = {
167
- name: "explore",
168
- description: "Fast parallel codebase exploration",
169
- builtinToolNames: ["read", "bash", "grep", "find", "ls"],
170
- systemPrompt: "You are a read-only exploration agent.",
171
- promptMode: "replace",
172
- extensions: true,
173
- skills: true,
174
- };
175
-
176
- assert.ok(!exploreConfig.builtinToolNames?.includes("write"));
177
- assert.ok(!exploreConfig.builtinToolNames?.includes("edit"));
178
- assert.ok(exploreConfig.builtinToolNames?.includes("read"));
179
- });
180
-
181
- it("should define work agent with read-write tools", () => {
182
- const workConfig: AgentConfig = {
183
- name: "work",
184
- description: "Parallel file writes with transparent locking",
185
- builtinToolNames: ["read", "write", "edit", "bash", "grep", "find", "ls"],
186
- systemPrompt: "You are a read-write work agent.",
187
- promptMode: "replace",
188
- extensions: true,
189
- skills: true,
190
- };
191
-
192
- assert.ok(workConfig.builtinToolNames?.includes("write"));
193
- assert.ok(workConfig.builtinToolNames?.includes("edit"));
194
- assert.ok(workConfig.builtinToolNames?.includes("read"));
195
- });
196
- });
197
-
198
- describe("System Prompt Builder", () => {
199
- const env = {
200
- isGitRepo: true,
201
- branch: "main",
202
- platform: "GitHub",
203
- };
204
-
205
- it("should build replace mode prompt", () => {
206
- const config: AgentConfig = {
207
- name: "explore",
208
- displayName: "Explorer",
209
- description: "Test",
210
- systemPrompt: "Find all authentication files.",
211
- promptMode: "replace",
212
- extensions: true,
213
- skills: true,
214
- };
215
-
216
- const prompt = buildAgentPrompt(config, "/workspace", env, "Parent prompt");
217
-
218
- assert.ok(prompt.startsWith("# Explorer"));
219
- assert.ok(prompt.includes("Find all authentication files."));
220
- assert.ok(prompt.includes("Working directory: /workspace"));
221
- assert.ok(!prompt.includes("Parent prompt"));
222
- });
223
-
224
- it("should build append mode prompt", () => {
225
- const config: AgentConfig = {
226
- name: "work",
227
- displayName: "Worker",
228
- description: "Test",
229
- systemPrompt: "Refactor the auth module.",
230
- promptMode: "append",
231
- extensions: true,
232
- skills: true,
233
- };
234
-
235
- const prompt = buildAgentPrompt(config, "/workspace", env, "Parent prompt");
236
-
237
- assert.ok(prompt.startsWith("Parent prompt"));
238
- assert.ok(prompt.includes("## Agent Role: Worker"));
239
- assert.ok(prompt.includes("Refactor the auth module."));
240
- });
241
- });
242
-
243
- describe("Concurrency Limit", () => {
244
- it("should respect max concurrent limit", async () => {
245
- const manager = new ConcurrencyManager(2);
246
- const events: string[] = [];
247
-
248
- // Start 3 agents, only 2 should run immediately
249
- const release1 = await manager.acquire("agent-1");
250
- events.push("agent-1-started");
251
-
252
- const release2 = await manager.acquire("agent-2");
253
- events.push("agent-2-started");
254
-
255
- assert.equal(manager.getRunning(), 2);
256
- assert.deepEqual(events, ["agent-1-started", "agent-2-started"]);
257
-
258
- // Third agent should queue
259
- const acquire3Promise = manager.acquire("agent-3").then((release) => {
260
- events.push("agent-3-started");
261
- return release;
262
- });
263
-
264
- assert.equal(manager.getQueueLength(), 1);
265
-
266
- // Release first agent
267
- release1();
268
- const release3 = await acquire3Promise;
269
-
270
- assert.equal(manager.getRunning(), 2);
271
- assert.deepEqual(events, ["agent-1-started", "agent-2-started", "agent-3-started"]);
272
-
273
- release2();
274
- release3();
275
- });
276
-
277
- it("should queue agents in order", async () => {
278
- const manager = new ConcurrencyManager(1);
279
- const events: string[] = [];
280
-
281
- const release1 = await manager.acquire("agent-1");
282
- events.push("1");
283
-
284
- const p2 = manager.acquire("agent-2").then(r => { events.push("2"); return r; });
285
- const p3 = manager.acquire("agent-3").then(r => { events.push("3"); return r; });
286
-
287
- release1();
288
- const release2 = await p2;
289
- release2();
290
- const release3 = await p3;
291
-
292
- assert.deepEqual(events, ["1", "2", "3"]);
293
- release3();
294
- });
295
- });
296
-
297
- describe("Custom Agent Loading", () => {
298
- it("should parse agent markdown frontmatter", () => {
299
- const markdown = `---
300
- name: code-checker
301
- description: Code quality checker
302
- tools: read, grep, find, bash
303
- thinking: high
304
- ---
305
- You are a code quality checker. Review code for issues.`;
306
-
307
- // Simple frontmatter parser
308
- const match = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
309
- assert.ok(match);
310
-
311
- const frontmatter = match![1];
312
- const body = match![2];
313
-
314
- assert.ok(frontmatter.includes("name: code-checker"));
315
- assert.ok(frontmatter.includes("tools: read, grep, find, bash"));
316
- assert.ok(body.includes("You are a code quality checker."));
317
- });
318
-
319
- it("should validate required fields", () => {
320
- const validAgent = {
321
- name: "test-agent",
322
- description: "Test agent",
323
- systemPrompt: "Do something.",
324
- promptMode: "replace" as const,
325
- extensions: true as const,
326
- skills: true as const,
327
- };
328
-
329
- assert.ok(validAgent.name);
330
- assert.ok(validAgent.description);
331
- assert.ok(validAgent.systemPrompt);
332
- });
333
- });
334
- });
@@ -1,334 +0,0 @@
1
- /**
2
- * @pi-unipi/subagents — Agent manager
3
- *
4
- * Tracks agents, manages concurrency queue, handles spawn/resume/abort.
5
- * Background agents subject to concurrency limit. Foreground bypass queue.
6
- */
7
-
8
- import { randomUUID } from "node:crypto";
9
- import type { Model } from "@earendil-works/pi-ai";
10
- import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
- import { runAgent, type ToolActivity } from "./agent-runner.js";
12
- import { resolveModel, type ModelRegistry } from "./model-resolver.js";
13
- import type { AgentRecord, AgentConfig, AgentType, ThinkingLevel } from "./types.js";
14
- import { BUILTIN_CONFIGS } from "./types.js";
15
- import { loadCustomAgents } from "./custom-agents.js";
16
- import { FileLock } from "./file-lock.js";
17
-
18
- export type OnAgentComplete = (record: AgentRecord) => void;
19
- export type OnAgentStart = (record: AgentRecord) => void;
20
-
21
- /** Default max concurrent background agents. */
22
- const DEFAULT_MAX_CONCURRENT = 4;
23
-
24
- interface SpawnArgs {
25
- pi: ExtensionAPI;
26
- ctx: ExtensionContext;
27
- type: AgentType;
28
- prompt: string;
29
- options: SpawnOptions;
30
- }
31
-
32
- interface SpawnOptions {
33
- description: string;
34
- model?: Model<any>;
35
- modelInput?: string;
36
- modelRegistry?: ModelRegistry;
37
- thinkingLevel?: ThinkingLevel;
38
- maxTurns?: number;
39
- isolated?: boolean;
40
- inheritContext?: boolean;
41
- isBackground?: boolean;
42
- onToolActivity?: (activity: ToolActivity) => void;
43
- onTextDelta?: (delta: string, fullText: string) => void;
44
- onSessionCreated?: (session: AgentSession) => void;
45
- onTurnEnd?: (turnCount: number) => void;
46
- }
47
-
48
- export class AgentManager {
49
- private agents = new Map<string, AgentRecord>();
50
- private cleanupInterval: ReturnType<typeof setInterval>;
51
- private onComplete?: OnAgentComplete;
52
- private onStart?: OnAgentStart;
53
- private maxConcurrent: number;
54
- private customAgents: Map<string, AgentConfig>;
55
-
56
- /** Per-file transparent locking for write agents. */
57
- readonly fileLock = new FileLock();
58
-
59
- /** Queue of background agents waiting to start. */
60
- private queue: { id: string; args: SpawnArgs }[] = [];
61
- /** Number of currently running background agents. */
62
- private runningBackground = 0;
63
-
64
- constructor(onComplete?: OnAgentComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart?: OnAgentStart) {
65
- this.onComplete = onComplete;
66
- this.onStart = onStart;
67
- this.maxConcurrent = maxConcurrent;
68
- this.customAgents = loadCustomAgents(process.cwd());
69
- this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
70
- }
71
-
72
- /** Get resolved agent config for a type. */
73
- getAgentConfig(type: AgentType): AgentConfig | undefined {
74
- return this.customAgents.get(type) ?? BUILTIN_CONFIGS[type];
75
- }
76
-
77
- setMaxConcurrent(n: number) {
78
- this.maxConcurrent = Math.max(1, n);
79
- this.drainQueue();
80
- }
81
-
82
- getMaxConcurrent(): number {
83
- return this.maxConcurrent;
84
- }
85
-
86
- /**
87
- * Spawn an agent. Returns ID immediately for background, waits for foreground.
88
- */
89
- spawn(
90
- pi: ExtensionAPI,
91
- ctx: ExtensionContext,
92
- type: AgentType,
93
- prompt: string,
94
- options: SpawnOptions,
95
- ): string {
96
- const id = randomUUID().slice(0, 17);
97
- const abortController = new AbortController();
98
- const record: AgentRecord = {
99
- id,
100
- type,
101
- description: options.description,
102
- status: options.isBackground ? "queued" : "running",
103
- toolUses: 0,
104
- startedAt: Date.now(),
105
- abortController,
106
- lockedFiles: new Set(),
107
- };
108
- this.agents.set(id, record);
109
-
110
- const args: SpawnArgs = { pi, ctx, type, prompt, options };
111
-
112
- if (options.isBackground && this.runningBackground >= this.maxConcurrent) {
113
- this.queue.push({ id, args });
114
- return id;
115
- }
116
-
117
- this.startAgent(id, record, args);
118
- return id;
119
- }
120
-
121
- /** Actually start an agent. */
122
- private startAgent(id: string, record: AgentRecord, { pi, ctx, type, prompt, options }: SpawnArgs) {
123
- record.status = "running";
124
- record.startedAt = Date.now();
125
- if (options.isBackground) this.runningBackground++;
126
- this.onStart?.(record);
127
-
128
- // Resolve model: explicit input > config model > parent model
129
- let model = options.model;
130
- if (options.modelInput && options.modelRegistry) {
131
- const resolved = resolveModel(options.modelInput, options.modelRegistry);
132
- if (typeof resolved === "string") {
133
- // Error message — return early with error
134
- record.status = "error";
135
- record.error = resolved;
136
- record.completedAt = Date.now();
137
- if (options.isBackground) {
138
- this.runningBackground--;
139
- this.onComplete?.(record);
140
- }
141
- return;
142
- }
143
- model = resolved;
144
- }
145
-
146
- const agentConfig = this.getAgentConfig(type);
147
- const promise = runAgent(ctx, type, prompt, {
148
- pi,
149
- model,
150
- agentConfig,
151
- maxTurns: options.maxTurns,
152
- isolated: options.isolated,
153
- inheritContext: options.inheritContext,
154
- thinkingLevel: options.thinkingLevel,
155
- signal: record.abortController!.signal,
156
- onToolActivity: (activity) => {
157
- if (activity.type === "end") record.toolUses++;
158
- options.onToolActivity?.(activity);
159
- },
160
- onTurnEnd: options.onTurnEnd,
161
- onTextDelta: options.onTextDelta,
162
- onSessionCreated: (session) => {
163
- record.session = session;
164
- options.onSessionCreated?.(session);
165
- },
166
- })
167
- .then(({ responseText, session, aborted, steered }) => {
168
- if (record.status !== "stopped") {
169
- record.status = aborted ? "aborted" : steered ? "completed" : "completed";
170
- }
171
- record.result = responseText;
172
- record.session = session;
173
- record.completedAt ??= Date.now();
174
-
175
- // Release any held file locks
176
- this.fileLock.releaseAll(id);
177
- record.lockedFiles.clear();
178
-
179
- if (options.isBackground) {
180
- this.runningBackground--;
181
- this.onComplete?.(record);
182
- this.drainQueue();
183
- }
184
- return responseText;
185
- })
186
- .catch((err) => {
187
- if (record.status !== "stopped") {
188
- record.status = "error";
189
- }
190
- record.error = err instanceof Error ? err.message : String(err);
191
- record.completedAt ??= Date.now();
192
-
193
- // Release any held file locks
194
- this.fileLock.releaseAll(id);
195
- record.lockedFiles.clear();
196
-
197
- if (options.isBackground) {
198
- this.runningBackground--;
199
- this.onComplete?.(record);
200
- this.drainQueue();
201
- }
202
- return "";
203
- });
204
-
205
- record.promise = promise;
206
- }
207
-
208
- /** Start queued agents up to concurrency limit. */
209
- private drainQueue() {
210
- while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
211
- const next = this.queue.shift()!;
212
- const record = this.agents.get(next.id);
213
- if (!record || record.status !== "queued") continue;
214
- this.startAgent(next.id, record, next.args);
215
- }
216
- }
217
-
218
- /**
219
- * Spawn and wait (foreground).
220
- */
221
- async spawnAndWait(
222
- pi: ExtensionAPI,
223
- ctx: ExtensionContext,
224
- type: AgentType,
225
- prompt: string,
226
- options: Omit<SpawnOptions, "isBackground">,
227
- ): Promise<AgentRecord> {
228
- const id = this.spawn(pi, ctx, type, prompt, { ...options, isBackground: false });
229
- const record = this.agents.get(id)!;
230
- await record.promise;
231
- return record;
232
- }
233
-
234
- getRecord(id: string): AgentRecord | undefined {
235
- return this.agents.get(id);
236
- }
237
-
238
- listAgents(): AgentRecord[] {
239
- return [...this.agents.values()].sort((a, b) => b.startedAt - a.startedAt);
240
- }
241
-
242
- abort(id: string): boolean {
243
- const record = this.agents.get(id);
244
- if (!record) return false;
245
-
246
- if (record.status === "queued") {
247
- this.queue = this.queue.filter((q) => q.id !== id);
248
- record.status = "stopped";
249
- record.completedAt = Date.now();
250
- return true;
251
- }
252
-
253
- if (record.status !== "running") return false;
254
- record.abortController?.abort();
255
- record.status = "stopped";
256
- record.completedAt = Date.now();
257
- this.fileLock.releaseAll(id);
258
- record.lockedFiles.clear();
259
- return true;
260
- }
261
-
262
- /** Abort all agents (for ESC propagation). */
263
- abortAll(): number {
264
- let count = 0;
265
- for (const queued of this.queue) {
266
- const record = this.agents.get(queued.id);
267
- if (record) {
268
- record.status = "stopped";
269
- record.completedAt = Date.now();
270
- count++;
271
- }
272
- }
273
- this.queue = [];
274
- for (const record of this.agents.values()) {
275
- if (record.status === "running") {
276
- record.abortController?.abort();
277
- record.status = "stopped";
278
- record.completedAt = Date.now();
279
- count++;
280
- }
281
- }
282
- this.fileLock.clear();
283
- return count;
284
- }
285
-
286
- /** Wait for all agents. */
287
- async waitForAll(): Promise<void> {
288
- while (true) {
289
- this.drainQueue();
290
- const pending = [...this.agents.values()]
291
- .filter((r) => r.status === "running" || r.status === "queued")
292
- .map((r) => r.promise)
293
- .filter(Boolean);
294
- if (pending.length === 0) break;
295
- await Promise.allSettled(pending);
296
- }
297
- }
298
-
299
- /** Whether any agents running or queued. */
300
- hasRunning(): boolean {
301
- return [...this.agents.values()].some((r) => r.status === "running" || r.status === "queued");
302
- }
303
-
304
- /** Remove completed records. */
305
- clearCompleted(): void {
306
- for (const [id, record] of this.agents) {
307
- if (record.status === "running" || record.status === "queued") continue;
308
- record.session?.dispose?.();
309
- record.session = undefined;
310
- this.agents.delete(id);
311
- }
312
- }
313
-
314
- private cleanup() {
315
- const cutoff = Date.now() - 10 * 60_000;
316
- for (const [id, record] of this.agents) {
317
- if (record.status === "running" || record.status === "queued") continue;
318
- if ((record.completedAt ?? 0) >= cutoff) continue;
319
- record.session?.dispose?.();
320
- record.session = undefined;
321
- this.agents.delete(id);
322
- }
323
- }
324
-
325
- dispose() {
326
- clearInterval(this.cleanupInterval);
327
- this.queue = [];
328
- this.abortAll();
329
- for (const record of this.agents.values()) {
330
- record.session?.dispose();
331
- }
332
- this.agents.clear();
333
- }
334
- }