@clanker-code/pi-subagents 0.10.8 → 0.11.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/AGENTS.md +2 -0
- package/CHANGELOG.md +17 -0
- package/README.md +22 -2
- package/bugs.txt +57 -0
- package/dist/agent-manager.d.ts +8 -0
- package/dist/agent-manager.js +54 -22
- package/dist/agent-runner.d.ts +3 -0
- package/dist/agent-runner.js +12 -3
- package/dist/dashboard-ui.d.ts +15 -0
- package/dist/dashboard-ui.js +206 -0
- package/dist/default-agents.js +0 -1
- package/dist/index.js +96 -11
- package/dist/peek.js +8 -2
- package/dist/subagent-list-clear.d.ts +57 -0
- package/dist/subagent-list-clear.js +331 -0
- package/dist/ui/agent-tool-rendering.js +1 -1
- package/dist/ui/agent-widget-tree.js +19 -2
- package/dist/ui/agent-widget.d.ts +7 -1
- package/dist/ui/agent-widget.js +52 -10
- package/package.json +1 -1
- package/src/agent-manager.ts +44 -13
- package/src/agent-runner.ts +14 -3
- package/src/dashboard-ui.ts +270 -0
- package/src/default-agents.ts +0 -1
- package/src/index.ts +113 -15
- package/src/peek.ts +7 -2
- package/src/subagent-list-clear.ts +405 -0
- package/src/ui/agent-tool-rendering.ts +1 -1
- package/src/ui/agent-widget-tree.ts +16 -2
- package/src/ui/agent-widget.ts +50 -10
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { getConfig } from "../agent-types.js";
|
|
3
|
+
import { getSessionTokens } from "../usage.js";
|
|
3
4
|
function statusRank(status) {
|
|
4
5
|
if (status === "running")
|
|
5
6
|
return 0;
|
|
@@ -55,6 +56,14 @@ function formatElapsed(startedAt, now) {
|
|
|
55
56
|
const rest = seconds % 60;
|
|
56
57
|
return rest > 0 ? `${minutes}m ${rest}s` : `${minutes}m`;
|
|
57
58
|
}
|
|
59
|
+
/** Compact token count for widget rows: "12.3k tok", "1.2M tok". */
|
|
60
|
+
function formatCompactTokens(count) {
|
|
61
|
+
if (count >= 1_000_000)
|
|
62
|
+
return `${(count / 1_000_000).toFixed(1)}M tok`;
|
|
63
|
+
if (count >= 1_000)
|
|
64
|
+
return `${(count / 1_000).toFixed(1)}k tok`;
|
|
65
|
+
return `${count} tok`;
|
|
66
|
+
}
|
|
58
67
|
function statusIcon(snapshot, frame, theme) {
|
|
59
68
|
if (snapshot.status === "running")
|
|
60
69
|
return theme.fg("accent", frame);
|
|
@@ -62,6 +71,8 @@ function statusIcon(snapshot, frame, theme) {
|
|
|
62
71
|
return theme.fg("muted", "◦");
|
|
63
72
|
if (snapshot.status === "completed")
|
|
64
73
|
return theme.fg("success", "✓");
|
|
74
|
+
if (snapshot.status === "steered")
|
|
75
|
+
return theme.fg("warning", "✓");
|
|
65
76
|
if (snapshot.status === "stopped")
|
|
66
77
|
return theme.fg("dim", "■");
|
|
67
78
|
return theme.fg("error", "✗");
|
|
@@ -77,13 +88,19 @@ function collectRows(nodes, options, mode, prefix = "") {
|
|
|
77
88
|
const childPrefix = prefix + (isLast ? " " : "│ ");
|
|
78
89
|
const s = node.snapshot;
|
|
79
90
|
const name = displayName(s.type);
|
|
80
|
-
const
|
|
91
|
+
const elapsedUntil = s.status === "running" || s.status === "queued" ? (options.now ?? Date.now()) : (s.completedAt ?? options.now ?? Date.now());
|
|
92
|
+
const elapsed = formatElapsed(s.startedAt, elapsedUntil);
|
|
81
93
|
const stats = [];
|
|
82
94
|
if (s.activity?.turnCount)
|
|
83
95
|
stats.push(`↻${s.activity.turnCount}`);
|
|
84
96
|
if (s.toolUses > 0)
|
|
85
97
|
stats.push(`${s.toolUses} tool${s.toolUses === 1 ? "" : "s"}`);
|
|
86
98
|
stats.push(elapsed);
|
|
99
|
+
if (s.activity?.session) {
|
|
100
|
+
const tokens = getSessionTokens(s.activity.session);
|
|
101
|
+
if (tokens > 0)
|
|
102
|
+
stats.push(formatCompactTokens(tokens));
|
|
103
|
+
}
|
|
87
104
|
const orphan = node.orphaned ? " ⚠ orphan" : "";
|
|
88
105
|
const error = s.error ? ` error: ${s.error}` : "";
|
|
89
106
|
rows.push(`${prefix}${connector} ${statusIcon(s, options.frame, options.theme)} ${options.theme.bold(name)} ${options.theme.fg("muted", s.description)} ${options.theme.fg("dim", `· ${stats.join(" · ")}${orphan}${error}`)}`);
|
|
@@ -126,5 +143,5 @@ export function renderAgentTree(records, options) {
|
|
|
126
143
|
? options.theme.fg("dim", ` ${active} running · ${queued} queued · depth ${maxDepth}/4`)
|
|
127
144
|
: "";
|
|
128
145
|
const heading = `${active > 0 ? options.theme.fg("accent", "●") : options.theme.fg("dim", "○")} ${options.theme.fg(active > 0 ? "accent" : "dim", "Agents")}${suffix}`;
|
|
129
|
-
return applyOverflow([heading, ...rows], options.maxLines, options.width);
|
|
146
|
+
return applyOverflow([heading, ...rows], options.maxLines, options.width, mode === "rich" ? "lines" : "agents");
|
|
130
147
|
}
|
|
@@ -113,8 +113,14 @@ export declare class AgentWidget {
|
|
|
113
113
|
private widgetInterval;
|
|
114
114
|
/** Tracks how many turns each finished agent has survived. Key: agent ID, Value: turns since finished. */
|
|
115
115
|
private finishedTurnAge;
|
|
116
|
+
/** Tracks wall-clock finish time so long-running turns cannot keep completed rows forever. */
|
|
117
|
+
private finishedAt;
|
|
116
118
|
/** How many extra turns errors/aborted agents linger (completed agents clear after 1 turn). */
|
|
117
119
|
private static readonly ERROR_LINGER_TURNS;
|
|
120
|
+
/** Max wall-clock linger for successful completions when no new parent turn starts. */
|
|
121
|
+
private static readonly COMPLETED_LINGER_MS;
|
|
122
|
+
/** Max wall-clock linger for non-success outcomes when no new parent turn starts. */
|
|
123
|
+
private static readonly ERROR_LINGER_MS;
|
|
118
124
|
/** Whether the widget callback is currently registered with the TUI. */
|
|
119
125
|
private widgetRegistered;
|
|
120
126
|
/** Cached TUI reference from widget factory callback, used for requestRender(). */
|
|
@@ -142,7 +148,7 @@ export declare class AgentWidget {
|
|
|
142
148
|
/** Check if a finished agent should still be shown in the widget. */
|
|
143
149
|
private shouldShowFinished;
|
|
144
150
|
/** Record an agent as finished (call when agent completes). */
|
|
145
|
-
markFinished(agentId: string): void;
|
|
151
|
+
markFinished(agentId: string, completedAt?: number): void;
|
|
146
152
|
private recordToSnapshot;
|
|
147
153
|
private visibleSnapshots;
|
|
148
154
|
/**
|
package/dist/ui/agent-widget.js
CHANGED
|
@@ -14,7 +14,7 @@ const DEFAULT_STATUS_TEXT_WIDTH = 20;
|
|
|
14
14
|
/** Braille spinner frames for animated running indicator. */
|
|
15
15
|
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
16
16
|
/** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
|
|
17
|
-
export const ERROR_STATUSES = new Set(["error", "aborted", "
|
|
17
|
+
export const ERROR_STATUSES = new Set(["error", "aborted", "stopped"]);
|
|
18
18
|
/** Tool name → human-readable action for activity descriptions. */
|
|
19
19
|
const TOOL_DISPLAY = {
|
|
20
20
|
read: "reading",
|
|
@@ -193,8 +193,14 @@ export class AgentWidget {
|
|
|
193
193
|
widgetInterval;
|
|
194
194
|
/** Tracks how many turns each finished agent has survived. Key: agent ID, Value: turns since finished. */
|
|
195
195
|
finishedTurnAge = new Map();
|
|
196
|
+
/** Tracks wall-clock finish time so long-running turns cannot keep completed rows forever. */
|
|
197
|
+
finishedAt = new Map();
|
|
196
198
|
/** How many extra turns errors/aborted agents linger (completed agents clear after 1 turn). */
|
|
197
199
|
static ERROR_LINGER_TURNS = 2;
|
|
200
|
+
/** Max wall-clock linger for successful completions when no new parent turn starts. */
|
|
201
|
+
static COMPLETED_LINGER_MS = 30_000;
|
|
202
|
+
/** Max wall-clock linger for non-success outcomes when no new parent turn starts. */
|
|
203
|
+
static ERROR_LINGER_MS = 120_000;
|
|
198
204
|
/** Whether the widget callback is currently registered with the TUI. */
|
|
199
205
|
widgetRegistered = false;
|
|
200
206
|
/** Cached TUI reference from widget factory callback, used for requestRender(). */
|
|
@@ -215,17 +221,25 @@ export class AgentWidget {
|
|
|
215
221
|
}
|
|
216
222
|
upsertSnapshot(snapshot) {
|
|
217
223
|
this.descendantSnapshots.set(snapshot.id, snapshot);
|
|
218
|
-
if (snapshot.status
|
|
219
|
-
this.
|
|
224
|
+
if (snapshot.status === "running" || snapshot.status === "queued") {
|
|
225
|
+
this.finishedTurnAge.delete(snapshot.id);
|
|
226
|
+
this.finishedAt.delete(snapshot.id);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
this.markFinished(snapshot.id, snapshot.completedAt);
|
|
220
230
|
}
|
|
221
231
|
this.update();
|
|
222
232
|
}
|
|
223
233
|
removeSnapshot(id) {
|
|
224
234
|
this.descendantSnapshots.delete(id);
|
|
235
|
+
this.finishedTurnAge.delete(id);
|
|
236
|
+
this.finishedAt.delete(id);
|
|
225
237
|
this.update();
|
|
226
238
|
}
|
|
227
239
|
clearSnapshots() {
|
|
228
240
|
this.descendantSnapshots.clear();
|
|
241
|
+
this.finishedTurnAge.clear();
|
|
242
|
+
this.finishedAt.clear();
|
|
229
243
|
this.update();
|
|
230
244
|
}
|
|
231
245
|
/** Set the UI context (grabbed from first tool execution). */
|
|
@@ -258,16 +272,25 @@ export class AgentWidget {
|
|
|
258
272
|
}
|
|
259
273
|
}
|
|
260
274
|
/** Check if a finished agent should still be shown in the widget. */
|
|
261
|
-
shouldShowFinished(agentId, status) {
|
|
275
|
+
shouldShowFinished(agentId, status, completedAt) {
|
|
262
276
|
const age = this.finishedTurnAge.get(agentId) ?? 0;
|
|
263
277
|
const maxAge = ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_TURNS : 1;
|
|
264
|
-
|
|
278
|
+
if (age >= maxAge)
|
|
279
|
+
return false;
|
|
280
|
+
const finishedAt = this.finishedAt.get(agentId) ?? completedAt;
|
|
281
|
+
if (finishedAt == null)
|
|
282
|
+
return true;
|
|
283
|
+
const maxMs = ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_MS : AgentWidget.COMPLETED_LINGER_MS;
|
|
284
|
+
return Date.now() - finishedAt < maxMs;
|
|
265
285
|
}
|
|
266
286
|
/** Record an agent as finished (call when agent completes). */
|
|
267
|
-
markFinished(agentId) {
|
|
287
|
+
markFinished(agentId, completedAt = Date.now()) {
|
|
268
288
|
if (!this.finishedTurnAge.has(agentId)) {
|
|
269
289
|
this.finishedTurnAge.set(agentId, 0);
|
|
270
290
|
}
|
|
291
|
+
if (!this.finishedAt.has(agentId)) {
|
|
292
|
+
this.finishedAt.set(agentId, completedAt);
|
|
293
|
+
}
|
|
271
294
|
}
|
|
272
295
|
recordToSnapshot(a) {
|
|
273
296
|
const activity = this.agentActivity.get(a.id);
|
|
@@ -290,13 +313,25 @@ export class AgentWidget {
|
|
|
290
313
|
const merged = new Map(this.descendantSnapshots);
|
|
291
314
|
const allAgents = this.manager.listAgents();
|
|
292
315
|
for (const a of allAgents) {
|
|
293
|
-
if (a.status === "running" || a.status === "queued"
|
|
316
|
+
if (a.status === "running" || a.status === "queued") {
|
|
317
|
+
this.finishedTurnAge.delete(a.id);
|
|
318
|
+
this.finishedAt.delete(a.id);
|
|
319
|
+
merged.set(a.id, this.recordToSnapshot(a));
|
|
320
|
+
}
|
|
321
|
+
else if (a.completedAt && this.shouldShowFinished(a.id, a.status, a.completedAt)) {
|
|
294
322
|
merged.set(a.id, this.recordToSnapshot(a));
|
|
295
323
|
}
|
|
296
324
|
}
|
|
325
|
+
const liveRecordIds = new Set(allAgents.map(a => a.id));
|
|
297
326
|
for (const [id, snapshot] of merged) {
|
|
298
|
-
if (snapshot.status !== "running" && snapshot.status !== "queued" &&
|
|
327
|
+
if (snapshot.status !== "running" && snapshot.status !== "queued" && !this.shouldShowFinished(id, snapshot.status, snapshot.completedAt)) {
|
|
299
328
|
merged.delete(id);
|
|
329
|
+
if (this.descendantSnapshots.has(id))
|
|
330
|
+
this.descendantSnapshots.delete(id);
|
|
331
|
+
if (!liveRecordIds.has(id)) {
|
|
332
|
+
this.finishedTurnAge.delete(id);
|
|
333
|
+
this.finishedAt.delete(id);
|
|
334
|
+
}
|
|
300
335
|
}
|
|
301
336
|
}
|
|
302
337
|
return [...merged.values()];
|
|
@@ -334,7 +369,7 @@ export class AgentWidget {
|
|
|
334
369
|
else if (a.status === "queued") {
|
|
335
370
|
queuedCount++;
|
|
336
371
|
}
|
|
337
|
-
else if (
|
|
372
|
+
else if (this.shouldShowFinished(a.id, a.status, a.completedAt)) {
|
|
338
373
|
hasFinished = true;
|
|
339
374
|
}
|
|
340
375
|
}
|
|
@@ -356,11 +391,14 @@ export class AgentWidget {
|
|
|
356
391
|
}
|
|
357
392
|
// Clean up stale entries
|
|
358
393
|
for (const [id] of this.finishedTurnAge) {
|
|
359
|
-
if (!allAgents.some(a => a.id === id))
|
|
394
|
+
if (!allAgents.some(a => a.id === id) && !this.descendantSnapshots.has(id)) {
|
|
360
395
|
this.finishedTurnAge.delete(id);
|
|
396
|
+
this.finishedAt.delete(id);
|
|
397
|
+
}
|
|
361
398
|
}
|
|
362
399
|
return;
|
|
363
400
|
}
|
|
401
|
+
this.ensureTimer();
|
|
364
402
|
// Status bar — only call setStatus when the text actually changes
|
|
365
403
|
const statusWidth = this.tui?.terminal.columns ?? DEFAULT_STATUS_TEXT_WIDTH;
|
|
366
404
|
const newStatusText = hasActive
|
|
@@ -403,6 +441,10 @@ export class AgentWidget {
|
|
|
403
441
|
}
|
|
404
442
|
this.widgetRegistered = false;
|
|
405
443
|
this.tui = undefined;
|
|
444
|
+
this.uiCtx = undefined;
|
|
406
445
|
this.lastStatusText = undefined;
|
|
446
|
+
this.descendantSnapshots.clear();
|
|
447
|
+
this.finishedTurnAge.clear();
|
|
448
|
+
this.finishedAt.clear();
|
|
407
449
|
}
|
|
408
450
|
}
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -126,6 +126,10 @@ export class AgentManager {
|
|
|
126
126
|
private queue: { id: string; args: SpawnArgs }[] = [];
|
|
127
127
|
/** Number of currently running background agents. */
|
|
128
128
|
private runningBackground = 0;
|
|
129
|
+
/** Background agents by id; foreground agents must not emit background completion callbacks. */
|
|
130
|
+
private backgroundAgentIds = new Set<string>();
|
|
131
|
+
/** Background terminal callbacks already emitted; prevents abort/settle double delivery. */
|
|
132
|
+
private completedBackgroundCallbacks = new Set<string>();
|
|
129
133
|
|
|
130
134
|
constructor(
|
|
131
135
|
onComplete?: OnAgentComplete,
|
|
@@ -194,6 +198,7 @@ export class AgentManager {
|
|
|
194
198
|
options.onOutputFileCreated?.(record.outputFile, id);
|
|
195
199
|
}
|
|
196
200
|
this.agents.set(id, record);
|
|
201
|
+
if (options.isBackground) this.backgroundAgentIds.add(id);
|
|
197
202
|
|
|
198
203
|
const args: SpawnArgs = { pi, ctx, type, prompt, options };
|
|
199
204
|
|
|
@@ -208,12 +213,22 @@ export class AgentManager {
|
|
|
208
213
|
try {
|
|
209
214
|
this.startAgent(id, record, args);
|
|
210
215
|
} catch (err) {
|
|
216
|
+
this.backgroundAgentIds.delete(id);
|
|
211
217
|
this.agents.delete(id);
|
|
212
218
|
throw err;
|
|
213
219
|
}
|
|
214
220
|
return id;
|
|
215
221
|
}
|
|
216
222
|
|
|
223
|
+
/** Emit background completion once, optionally releasing a running concurrency slot. */
|
|
224
|
+
private completeBackground(record: AgentRecord, releaseRunningSlot: boolean, drain = true): void {
|
|
225
|
+
if (this.completedBackgroundCallbacks.has(record.id)) return;
|
|
226
|
+
this.completedBackgroundCallbacks.add(record.id);
|
|
227
|
+
if (releaseRunningSlot) this.runningBackground = Math.max(0, this.runningBackground - 1);
|
|
228
|
+
try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
|
|
229
|
+
if (drain) this.drainQueue();
|
|
230
|
+
}
|
|
231
|
+
|
|
217
232
|
/** Actually start an agent (called immediately or from queue drain). */
|
|
218
233
|
private startAgent(id: string, record: AgentRecord, { pi, ctx, type, prompt, options }: SpawnArgs) {
|
|
219
234
|
// Re-validate a caller-supplied cwd: queued spawns can start minutes after
|
|
@@ -338,9 +353,7 @@ export class AgentManager {
|
|
|
338
353
|
}
|
|
339
354
|
|
|
340
355
|
if (options.isBackground) {
|
|
341
|
-
this.
|
|
342
|
-
try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
|
|
343
|
-
this.drainQueue();
|
|
356
|
+
this.completeBackground(record, true);
|
|
344
357
|
}
|
|
345
358
|
return responseText;
|
|
346
359
|
})
|
|
@@ -369,9 +382,7 @@ export class AgentManager {
|
|
|
369
382
|
}
|
|
370
383
|
|
|
371
384
|
if (options.isBackground) {
|
|
372
|
-
this.
|
|
373
|
-
this.onComplete?.(record);
|
|
374
|
-
this.drainQueue();
|
|
385
|
+
this.completeBackground(record, true);
|
|
375
386
|
}
|
|
376
387
|
return "";
|
|
377
388
|
});
|
|
@@ -393,7 +404,7 @@ export class AgentManager {
|
|
|
393
404
|
record.status = "error";
|
|
394
405
|
record.error = err instanceof Error ? err.message : String(err);
|
|
395
406
|
record.completedAt = Date.now();
|
|
396
|
-
this.
|
|
407
|
+
this.completeBackground(record, false, false);
|
|
397
408
|
}
|
|
398
409
|
}
|
|
399
410
|
}
|
|
@@ -434,6 +445,8 @@ export class AgentManager {
|
|
|
434
445
|
record.error = undefined;
|
|
435
446
|
record.resultConsumed = false;
|
|
436
447
|
record.abortController = new AbortController();
|
|
448
|
+
this.backgroundAgentIds.add(id);
|
|
449
|
+
this.completedBackgroundCallbacks.delete(id);
|
|
437
450
|
this.runningBackground++;
|
|
438
451
|
this.onStart?.(record);
|
|
439
452
|
|
|
@@ -463,18 +476,14 @@ export class AgentManager {
|
|
|
463
476
|
record.result = responseText;
|
|
464
477
|
record.completedAt = Date.now();
|
|
465
478
|
detach();
|
|
466
|
-
this.
|
|
467
|
-
try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
|
|
468
|
-
this.drainQueue();
|
|
479
|
+
this.completeBackground(record, true);
|
|
469
480
|
return responseText;
|
|
470
481
|
}).catch((err) => {
|
|
471
482
|
if (record.status !== "stopped") record.status = "error";
|
|
472
483
|
record.error = err instanceof Error ? err.message : String(err);
|
|
473
484
|
record.completedAt = Date.now();
|
|
474
485
|
detach();
|
|
475
|
-
this.
|
|
476
|
-
try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
|
|
477
|
-
this.drainQueue();
|
|
486
|
+
this.completeBackground(record, true);
|
|
478
487
|
return "";
|
|
479
488
|
});
|
|
480
489
|
|
|
@@ -501,6 +510,7 @@ export class AgentManager {
|
|
|
501
510
|
this.queue = this.queue.filter(q => q.id !== id);
|
|
502
511
|
record.status = "stopped";
|
|
503
512
|
record.completedAt = Date.now();
|
|
513
|
+
this.completeBackground(record, false);
|
|
504
514
|
return true;
|
|
505
515
|
}
|
|
506
516
|
|
|
@@ -508,6 +518,7 @@ export class AgentManager {
|
|
|
508
518
|
record.abortController?.abort();
|
|
509
519
|
record.status = "stopped";
|
|
510
520
|
record.completedAt = Date.now();
|
|
521
|
+
if (this.backgroundAgentIds.has(id)) this.completeBackground(record, true);
|
|
511
522
|
return true;
|
|
512
523
|
}
|
|
513
524
|
|
|
@@ -515,9 +526,24 @@ export class AgentManager {
|
|
|
515
526
|
private removeRecord(id: string, record: AgentRecord): void {
|
|
516
527
|
record.session?.dispose?.();
|
|
517
528
|
record.session = undefined;
|
|
529
|
+
this.backgroundAgentIds.delete(id);
|
|
530
|
+
this.completedBackgroundCallbacks.delete(id);
|
|
518
531
|
this.agents.delete(id);
|
|
519
532
|
}
|
|
520
533
|
|
|
534
|
+
/** Remove selected terminal records. Running and queued records are never removed. */
|
|
535
|
+
clearRecords(ids: string[]): string[] {
|
|
536
|
+
const removed: string[] = [];
|
|
537
|
+
for (const id of ids) {
|
|
538
|
+
const record = this.agents.get(id);
|
|
539
|
+
if (!record) continue;
|
|
540
|
+
if (record.status === "running" || record.status === "queued") continue;
|
|
541
|
+
this.removeRecord(id, record);
|
|
542
|
+
removed.push(id);
|
|
543
|
+
}
|
|
544
|
+
return removed;
|
|
545
|
+
}
|
|
546
|
+
|
|
521
547
|
private cleanup() {
|
|
522
548
|
const cutoff = Date.now() - 10 * 60_000;
|
|
523
549
|
for (const [id, record] of this.agents) {
|
|
@@ -554,6 +580,7 @@ export class AgentManager {
|
|
|
554
580
|
if (record) {
|
|
555
581
|
record.status = "stopped";
|
|
556
582
|
record.completedAt = Date.now();
|
|
583
|
+
if (this.backgroundAgentIds.has(record.id)) this.completedBackgroundCallbacks.add(record.id);
|
|
557
584
|
count++;
|
|
558
585
|
}
|
|
559
586
|
}
|
|
@@ -564,9 +591,11 @@ export class AgentManager {
|
|
|
564
591
|
record.abortController?.abort();
|
|
565
592
|
record.status = "stopped";
|
|
566
593
|
record.completedAt = Date.now();
|
|
594
|
+
if (this.backgroundAgentIds.has(record.id)) this.completedBackgroundCallbacks.add(record.id);
|
|
567
595
|
count++;
|
|
568
596
|
}
|
|
569
597
|
}
|
|
598
|
+
this.runningBackground = 0;
|
|
570
599
|
return count;
|
|
571
600
|
}
|
|
572
601
|
|
|
@@ -593,6 +622,8 @@ export class AgentManager {
|
|
|
593
622
|
record.session?.dispose();
|
|
594
623
|
}
|
|
595
624
|
this.agents.clear();
|
|
625
|
+
this.backgroundAgentIds.clear();
|
|
626
|
+
this.completedBackgroundCallbacks.clear();
|
|
596
627
|
// Prune any orphaned git worktrees (crash recovery)
|
|
597
628
|
try { pruneWorktrees(process.cwd()); } catch { /* ignore */ }
|
|
598
629
|
// Also prune repos that caller-supplied cwds created worktrees in — a clean
|
package/src/agent-runner.ts
CHANGED
|
@@ -36,6 +36,8 @@ export const SUBAGENT_TOOL_NAMES = {
|
|
|
36
36
|
AGENT: "Agent",
|
|
37
37
|
GET_RESULT: "get_subagent_result",
|
|
38
38
|
STEER: "steer_subagent",
|
|
39
|
+
LIST_SUBAGENTS: "list_subagents",
|
|
40
|
+
CLEAR_SUBAGENTS: "clear_subagents",
|
|
39
41
|
LIST_MODELS: "list_models",
|
|
40
42
|
} as const;
|
|
41
43
|
|
|
@@ -69,7 +71,12 @@ function getLoadingExtensionAgentId(): string | undefined {
|
|
|
69
71
|
return value && typeof value.agentId === "string" ? value.agentId : undefined;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
function getLoadingExtensionParentAgentId(): string | undefined {
|
|
75
|
+
const value = (globalThis as any)[EXTENSION_DEPTH_KEY];
|
|
76
|
+
return value && typeof value.parentAgentId === "string" ? value.parentAgentId : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function withLoadingExtensionDepth<T>(depth: number, agentId: string | undefined, parentAgentId: string | undefined, fn: () => Promise<T>): Promise<T> {
|
|
73
80
|
const previous = extensionDepthLoadChain;
|
|
74
81
|
let release!: () => void;
|
|
75
82
|
extensionDepthLoadChain = new Promise<void>((resolve) => {
|
|
@@ -80,7 +87,7 @@ async function withLoadingExtensionDepth<T>(depth: number, agentId: string | und
|
|
|
80
87
|
try {
|
|
81
88
|
const g = globalThis as any;
|
|
82
89
|
const prev = g[EXTENSION_DEPTH_KEY];
|
|
83
|
-
g[EXTENSION_DEPTH_KEY] = { depth, agentId };
|
|
90
|
+
g[EXTENSION_DEPTH_KEY] = { depth, agentId, parentAgentId };
|
|
84
91
|
try {
|
|
85
92
|
return await fn();
|
|
86
93
|
} finally {
|
|
@@ -100,6 +107,10 @@ export function getCurrentExtensionAgentId(): string | undefined {
|
|
|
100
107
|
return getLoadingExtensionAgentId();
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
export function getCurrentExtensionParentAgentId(): string | undefined {
|
|
111
|
+
return getLoadingExtensionParentAgentId();
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
/**
|
|
104
115
|
* Canonical name of an extension for `extensions: [...]` allowlist matching.
|
|
105
116
|
* Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
|
|
@@ -557,7 +568,7 @@ export async function runAgent(
|
|
|
557
568
|
systemPromptOverride: () => systemPrompt,
|
|
558
569
|
appendSystemPromptOverride: () => [],
|
|
559
570
|
});
|
|
560
|
-
await withLoadingExtensionDepth(depth, options.agentId, () => loader.reload());
|
|
571
|
+
await withLoadingExtensionDepth(depth, options.agentId, options.parentAgentId, () => loader.reload());
|
|
561
572
|
|
|
562
573
|
// Plain entries in `tools:` are expected to be built-in names (extension tools
|
|
563
574
|
// go through `ext:`), so an unknown name there is unambiguously a typo. Previously
|