@agnishc/edb-subagents 0.14.2 → 0.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agnishc/edb-subagents",
3
- "version": "0.14.2",
3
+ "version": "0.15.1",
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",
@@ -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. */
@@ -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" && a.status !== "queued" && a.completedAt && this.shouldShowFinished(a.id, a.status),
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 = 1 line, queued = 1 line.
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. Finished agents
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) {