@agnishc/edb-subagents 0.14.3 → 0.16.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/CHANGELOG.md +2 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +16 -2
- package/src/index.ts +45 -0
- package/src/types.ts +6 -1
- package/src/ui/agent-widget.ts +38 -6
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agnishc/edb-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Pi extension: Claude Code-style autonomous sub-agents with live widget, parallel execution, mid-run steering, and custom agent types",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/agent-manager.ts
CHANGED
|
@@ -296,6 +296,18 @@ export class AgentManager {
|
|
|
296
296
|
|
|
297
297
|
const promise = runWithFallback()
|
|
298
298
|
.then(({ responseText, session, aborted, steered }) => {
|
|
299
|
+
// If agent sent ask_supervisor and yielded, suspend instead of completing.
|
|
300
|
+
// The session stays alive — resumeInBackground will restart it with the answer.
|
|
301
|
+
if (record.pendingSupervisorAsk && record.status !== "stopped") {
|
|
302
|
+
record.status = "suspended";
|
|
303
|
+
record.session = session;
|
|
304
|
+
// Decrement so the queue can drain — this agent is no longer "running"
|
|
305
|
+
if (options.isBackground) {
|
|
306
|
+
this.runningBackground--;
|
|
307
|
+
this.drainQueue();
|
|
308
|
+
}
|
|
309
|
+
return responseText;
|
|
310
|
+
}
|
|
299
311
|
// Don't overwrite status if externally stopped via abort()
|
|
300
312
|
if (record.status !== "stopped") {
|
|
301
313
|
record.status = aborted ? "aborted" : steered ? "steered" : "completed";
|
|
@@ -432,11 +444,13 @@ export class AgentManager {
|
|
|
432
444
|
/**
|
|
433
445
|
* Resume an existing agent session with a new prompt in the background (non-blocking).
|
|
434
446
|
* Returns immediately; the record's promise resolves when done.
|
|
447
|
+
* Also clears pendingSupervisorAsk so the agent transitions cleanly from "suspended".
|
|
435
448
|
*/
|
|
436
449
|
resumeInBackground(id: string, prompt: string): AgentRecord | undefined {
|
|
437
450
|
const record = this.agents.get(id);
|
|
438
451
|
if (!record?.session) return undefined;
|
|
439
452
|
|
|
453
|
+
record.pendingSupervisorAsk = false;
|
|
440
454
|
record.status = "running";
|
|
441
455
|
record.startedAt = Date.now();
|
|
442
456
|
record.completedAt = undefined;
|
|
@@ -557,7 +571,7 @@ export class AgentManager {
|
|
|
557
571
|
private cleanup() {
|
|
558
572
|
const cutoff = Date.now() - 10 * 60_000;
|
|
559
573
|
for (const [id, record] of this.agents) {
|
|
560
|
-
if (record.status === "running" || record.status === "queued") continue;
|
|
574
|
+
if (record.status === "running" || record.status === "queued" || record.status === "suspended") continue;
|
|
561
575
|
if ((record.completedAt ?? 0) >= cutoff) continue;
|
|
562
576
|
this.removeRecord(id, record);
|
|
563
577
|
}
|
|
@@ -569,7 +583,7 @@ export class AgentManager {
|
|
|
569
583
|
*/
|
|
570
584
|
clearCompleted(): void {
|
|
571
585
|
for (const [id, record] of this.agents) {
|
|
572
|
-
if (record.status === "running" || record.status === "queued") continue;
|
|
586
|
+
if (record.status === "running" || record.status === "queued" || record.status === "suspended") continue;
|
|
573
587
|
this.removeRecord(id, record);
|
|
574
588
|
}
|
|
575
589
|
}
|
package/src/index.ts
CHANGED
|
@@ -543,6 +543,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
543
543
|
if (p?.path) todoStorePath = p.path;
|
|
544
544
|
});
|
|
545
545
|
|
|
546
|
+
// ── bridge:agent_suspending ──────────────────────────────────────────────
|
|
547
|
+
// Fired by edb-bridge when a sub-agent calls ask_supervisor.
|
|
548
|
+
// Mark the agent so that when runAgent() returns we suspend instead of completing.
|
|
549
|
+
pi.events.on("bridge:agent_suspending", (payload: unknown) => {
|
|
550
|
+
const p = payload as { agentId?: string } | undefined;
|
|
551
|
+
if (!p?.agentId) return;
|
|
552
|
+
const record = manager.getRecord(p.agentId);
|
|
553
|
+
if (record) {
|
|
554
|
+
record.pendingSupervisorAsk = true;
|
|
555
|
+
widget.update();
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Fired by edb-bridge when ask_supervisor fails to reach the supervisor.
|
|
560
|
+
// Undo the suspending flag so the agent completes normally.
|
|
561
|
+
pi.events.on("bridge:agent_suspend_cancelled", (payload: unknown) => {
|
|
562
|
+
const p = payload as { agentId?: string } | undefined;
|
|
563
|
+
if (!p?.agentId) return;
|
|
564
|
+
const record = manager.getRecord(p.agentId);
|
|
565
|
+
if (record) {
|
|
566
|
+
record.pendingSupervisorAsk = false;
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// ── bridge:resume_agent ─────────────────────────────────────────────────
|
|
571
|
+
// Fired by edb-bridge when answer_subagent is called.
|
|
572
|
+
// Resume the suspended agent with the supervisor's answer.
|
|
573
|
+
pi.events.on("bridge:resume_agent", (payload: unknown) => {
|
|
574
|
+
const p = payload as { agentId?: string; answer?: string } | undefined;
|
|
575
|
+
if (!p?.agentId || !p.answer) return;
|
|
576
|
+
const record = manager.getRecord(p.agentId);
|
|
577
|
+
if (!record?.session) return;
|
|
578
|
+
|
|
579
|
+
const resumed = manager.resumeInBackground(p.agentId, `Supervisor replied: ${p.answer}`);
|
|
580
|
+
if (resumed) {
|
|
581
|
+
// Ensure activity tracking exists for the resumed agent
|
|
582
|
+
if (!agentActivity.has(p.agentId)) {
|
|
583
|
+
const { state } = createActivityTracker(undefined, () => widget.update());
|
|
584
|
+
agentActivity.set(p.agentId, state);
|
|
585
|
+
}
|
|
586
|
+
widget.ensureTimer();
|
|
587
|
+
widget.update();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
546
591
|
/**
|
|
547
592
|
* Read a task from the shared task store (synchronous, best-effort).
|
|
548
593
|
* Returns undefined if the store is not available or the task doesn't exist.
|
package/src/types.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface AgentRecord {
|
|
|
73
73
|
agentName?: string;
|
|
74
74
|
/** Linked task ID in the shared task store. Used for blocked-state check in get_subagent_result. */
|
|
75
75
|
taskId?: string;
|
|
76
|
-
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error";
|
|
76
|
+
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error" | "suspended";
|
|
77
77
|
result?: string;
|
|
78
78
|
error?: string;
|
|
79
79
|
toolUses: number;
|
|
@@ -88,6 +88,11 @@ export interface AgentRecord {
|
|
|
88
88
|
resultConsumed?: boolean;
|
|
89
89
|
/** Steering messages queued before the session was ready. */
|
|
90
90
|
pendingSteers?: string[];
|
|
91
|
+
/**
|
|
92
|
+
* True when the agent sent ask_supervisor and is waiting for the supervisor's answer.
|
|
93
|
+
* The session is still alive — the agent will be resumed via manager.resumeInBackground.
|
|
94
|
+
*/
|
|
95
|
+
pendingSupervisorAsk?: boolean;
|
|
91
96
|
/** Worktree info if the agent is running in an isolated worktree. */
|
|
92
97
|
worktree?: { path: string; branch: string };
|
|
93
98
|
/** Worktree cleanup result after agent completion. */
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -324,12 +324,17 @@ export class AgentWidget {
|
|
|
324
324
|
const allAgents = this.manager.listAgents();
|
|
325
325
|
const running = allAgents.filter((a) => a.status === "running");
|
|
326
326
|
const queued = allAgents.filter((a) => a.status === "queued");
|
|
327
|
+
const suspended = allAgents.filter((a) => a.status === "suspended");
|
|
327
328
|
const finished = allAgents.filter(
|
|
328
329
|
(a) =>
|
|
329
|
-
a.status !== "running" &&
|
|
330
|
+
a.status !== "running" &&
|
|
331
|
+
a.status !== "queued" &&
|
|
332
|
+
a.status !== "suspended" &&
|
|
333
|
+
a.completedAt &&
|
|
334
|
+
this.shouldShowFinished(a.id, a.status),
|
|
330
335
|
);
|
|
331
336
|
|
|
332
|
-
const hasActive = running.length > 0 || queued.length > 0;
|
|
337
|
+
const hasActive = running.length > 0 || queued.length > 0 || suspended.length > 0;
|
|
333
338
|
const hasFinished = finished.length > 0;
|
|
334
339
|
|
|
335
340
|
// Nothing to show — return empty (widget will be unregistered by update())
|
|
@@ -342,7 +347,7 @@ export class AgentWidget {
|
|
|
342
347
|
const frame = SPINNER[this.widgetFrame % SPINNER.length];
|
|
343
348
|
|
|
344
349
|
// Build sections separately for overflow-aware assembly.
|
|
345
|
-
// Each running agent = 2 lines (header + activity), finished
|
|
350
|
+
// Each running agent = 2 lines (header + activity), suspended/finished/queued = 1 line.
|
|
346
351
|
|
|
347
352
|
const finishedLines: string[] = [];
|
|
348
353
|
for (const a of finished) {
|
|
@@ -380,6 +385,19 @@ export class AgentWidget {
|
|
|
380
385
|
]);
|
|
381
386
|
}
|
|
382
387
|
|
|
388
|
+
const suspendedLines: string[] = [];
|
|
389
|
+
for (const a of suspended) {
|
|
390
|
+
const name = getDisplayName(a.type);
|
|
391
|
+
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
392
|
+
suspendedLines.push(
|
|
393
|
+
truncate(
|
|
394
|
+
`${theme.fg("dim", "├─")} ${theme.fg("warning", "⏸")} ${theme.fg("dim", name)}` +
|
|
395
|
+
`${a.agentName ? ` ${theme.fg("dim", a.agentName)}` : ""} ${theme.fg("dim", a.description)}` +
|
|
396
|
+
` ${theme.fg("dim", "·")} ${theme.fg("dim", `waiting for supervisor · ${elapsed}`)}`,
|
|
397
|
+
),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
383
401
|
const queuedLine =
|
|
384
402
|
queued.length > 0
|
|
385
403
|
? truncate(
|
|
@@ -389,7 +407,7 @@ export class AgentWidget {
|
|
|
389
407
|
|
|
390
408
|
// Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
|
|
391
409
|
const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
|
|
392
|
-
const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
|
|
410
|
+
const totalBody = finishedLines.length + runningLines.length * 2 + suspendedLines.length + (queuedLine ? 1 : 0);
|
|
393
411
|
|
|
394
412
|
const lines: string[] = [truncate(`${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor, "Agents")}`)];
|
|
395
413
|
|
|
@@ -397,6 +415,7 @@ export class AgentWidget {
|
|
|
397
415
|
// Everything fits — add all lines and fix up connectors for the last item.
|
|
398
416
|
lines.push(...finishedLines);
|
|
399
417
|
for (const pair of runningLines) lines.push(...pair);
|
|
418
|
+
lines.push(...suspendedLines);
|
|
400
419
|
if (queuedLine) lines.push(queuedLine);
|
|
401
420
|
|
|
402
421
|
// Fix last connector: swap ├─ → └─ and │ → space for activity lines.
|
|
@@ -436,7 +455,17 @@ export class AgentWidget {
|
|
|
436
455
|
budget--;
|
|
437
456
|
}
|
|
438
457
|
|
|
439
|
-
// 3.
|
|
458
|
+
// 3. Suspended agents
|
|
459
|
+
for (const sl of suspendedLines) {
|
|
460
|
+
if (budget >= 1) {
|
|
461
|
+
lines.push(sl);
|
|
462
|
+
budget--;
|
|
463
|
+
} else {
|
|
464
|
+
hiddenFinished++;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 4. Finished agents
|
|
440
469
|
for (const fl of finishedLines) {
|
|
441
470
|
if (budget >= 1) {
|
|
442
471
|
lines.push(fl);
|
|
@@ -470,17 +499,20 @@ export class AgentWidget {
|
|
|
470
499
|
// Lightweight existence checks — full categorization happens in renderWidget()
|
|
471
500
|
let runningCount = 0;
|
|
472
501
|
let queuedCount = 0;
|
|
502
|
+
let suspendedCount = 0;
|
|
473
503
|
let hasFinished = false;
|
|
474
504
|
for (const a of allAgents) {
|
|
475
505
|
if (a.status === "running") {
|
|
476
506
|
runningCount++;
|
|
477
507
|
} else if (a.status === "queued") {
|
|
478
508
|
queuedCount++;
|
|
509
|
+
} else if (a.status === "suspended") {
|
|
510
|
+
suspendedCount++;
|
|
479
511
|
} else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) {
|
|
480
512
|
hasFinished = true;
|
|
481
513
|
}
|
|
482
514
|
}
|
|
483
|
-
const hasActive = runningCount > 0 || queuedCount > 0;
|
|
515
|
+
const hasActive = runningCount > 0 || queuedCount > 0 || suspendedCount > 0;
|
|
484
516
|
|
|
485
517
|
// Nothing to show — clear widget
|
|
486
518
|
if (!hasActive && !hasFinished) {
|