@agnishc/edb-bridge 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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  All notable changes to `@agnishc/edb-bridge` will be documented in this file.
4
4
 
5
+ ## [0.16.0] - 2026-06-22
6
+
5
7
  ## [0.12.0] - 2026-05-22
6
8
 
7
9
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agnishc/edb-bridge",
3
- "version": "0.14.3",
3
+ "version": "0.16.0",
4
4
  "description": "Pi extension: lightweight inter-session message bus for orchestrator/sub-agent workflows",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -92,64 +92,7 @@ export default function edbBridgeExtension(pi: ExtensionAPI): void {
92
92
 
93
93
  const pendingAsks = new Map<string, PendingAsk>();
94
94
 
95
- // Per-session outbound ask waiters: keyed by session ID
96
- const outboundAskWaiters = new Map<
97
- string,
98
- {
99
- replyTo: string;
100
- resolve: (text: string) => void;
101
- reject: (err: Error) => void;
102
- }
103
- >();
104
-
105
- function waitForReply(sessionId: string, messageId: string, signal?: AbortSignal): Promise<string> {
106
- if (outboundAskWaiters.has(sessionId)) {
107
- return Promise.reject(new Error("Already waiting for a reply from supervisor in this session"));
108
- }
109
- if (signal?.aborted) return Promise.reject(new Error("Cancelled"));
110
-
111
- return new Promise((resolve, reject) => {
112
- const timeout = setTimeout(() => {
113
- outboundAskWaiters.delete(sessionId);
114
- reject(new Error("No reply from supervisor within 10 minutes"));
115
- }, ASK_TIMEOUT_MS);
116
-
117
- const onAbort = () => {
118
- clearTimeout(timeout);
119
- outboundAskWaiters.delete(sessionId);
120
- reject(new Error("Cancelled"));
121
- };
122
- signal?.addEventListener("abort", onAbort, { once: true });
123
-
124
- outboundAskWaiters.set(sessionId, {
125
- replyTo: messageId,
126
- resolve: (text) => {
127
- clearTimeout(timeout);
128
- signal?.removeEventListener("abort", onAbort);
129
- outboundAskWaiters.delete(sessionId);
130
- resolve(text);
131
- },
132
- reject: (err) => {
133
- clearTimeout(timeout);
134
- signal?.removeEventListener("abort", onAbort);
135
- outboundAskWaiters.delete(sessionId);
136
- reject(err);
137
- },
138
- });
139
- });
140
- }
141
-
142
95
  function handleIncoming(from: SessionInfo, message: BridgeMessage): void {
143
- // Check if this is a reply to an outbound ask
144
- if (message.replyTo) {
145
- for (const [, waiter] of outboundAskWaiters) {
146
- if (waiter.replyTo === message.replyTo) {
147
- waiter.resolve(message.content.text);
148
- return;
149
- }
150
- }
151
- }
152
-
153
96
  // Task update notification — trigger widget refresh
154
97
  if (message.type === "task_updated") {
155
98
  pi.events.emit(EV_TASK_UPDATED, message.content.data ?? {});
@@ -353,12 +296,6 @@ export default function edbBridgeExtension(pi: ExtensionAPI): void {
353
296
 
354
297
  // Clean up per-session state regardless
355
298
  if (currentSessionId) sessionBridgeCtx.delete(currentSessionId);
356
- // Clean up outbound waiters for this session only
357
- const sessionWaiter = outboundAskWaiters.get(currentSessionId ?? "");
358
- if (sessionWaiter) {
359
- sessionWaiter.reject(new Error("Session shutting down"));
360
- outboundAskWaiters.delete(currentSessionId ?? "");
361
- }
362
299
 
363
300
  if (isMainSession) {
364
301
  // Full teardown — this is the orchestrator shutting down
@@ -372,10 +309,6 @@ export default function edbBridgeExtension(pi: ExtensionAPI): void {
372
309
  ask.reject(new Error("Session shutting down"));
373
310
  }
374
311
  pendingAsks.clear();
375
- for (const waiter of outboundAskWaiters.values()) {
376
- waiter.reject(new Error("Session shutting down"));
377
- }
378
- outboundAskWaiters.clear();
379
312
  if (client) {
380
313
  await client.disconnect();
381
314
  client = null;
@@ -432,6 +365,12 @@ export default function edbBridgeExtension(pi: ExtensionAPI): void {
432
365
  pi.events.emit("bridge:supervisor_answered", { taskId: ask.taskId });
433
366
  }
434
367
 
368
+ // Resume the suspended sub-agent session with the answer.
369
+ // edb-subagents listens for this event and calls manager.resumeInBackground.
370
+ if (ask.agentId) {
371
+ pi.events.emit("bridge:resume_agent", { agentId: ask.agentId, answer: params.answer });
372
+ }
373
+
435
374
  if (!result.delivered) {
436
375
  return textResult(
437
376
  `Answer could not reach sub-agent (${result.reason ?? "disconnected"}). The ask was resolved locally.`,
@@ -453,26 +392,28 @@ export default function edbBridgeExtension(pi: ExtensionAPI): void {
453
392
  },
454
393
  });
455
394
 
456
- // ── ask_supervisor (sub-agent sessions with bridge context) ───────────────
395
+ // ── ask_supervisor (sub-agent sessions with bridge context) ———————————
457
396
 
458
397
  pi.registerTool({
459
398
  name: "ask_supervisor",
460
399
  label: "Ask Supervisor",
461
400
  description:
462
- "Ask the orchestrator a blocking question and wait for their reply before continuing.\n\n" +
463
- "Use when blocked, uncertain, or facing a decision. The tool blocks until the supervisor replies.\n" +
401
+ "Ask the orchestrator a question. Returns immediately your session will be automatically resumed with the answer.\n\n" +
402
+ "Use when blocked, uncertain, or facing a decision that requires supervisor input.\n" +
464
403
  "Do NOT use for routine completion — return results normally.",
465
- promptSnippet: "Ask the orchestrator a blocking question and wait for their reply.",
404
+ promptSnippet:
405
+ "Ask the orchestrator a question. Returns immediately — session resumes automatically with the answer.",
466
406
  promptGuidelines: [
467
407
  "Use ask_supervisor when blocked by a decision or missing critical information.",
468
408
  "Do not use for routine task completion.",
409
+ "After calling ask_supervisor, write a brief status of your progress then stop all tool calls. Your session resumes automatically when the supervisor answers.",
469
410
  ],
470
411
  parameters: Type.Object({
471
412
  question: Type.String({ description: "The question for the supervisor." }),
472
413
  task_id: Type.Optional(Type.String({ description: "Optional: linked task ID." })),
473
414
  }),
474
415
 
475
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
416
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
476
417
  const sessionId = ctx.sessionManager.getSessionId();
477
418
  const bridgeCtx = sessionBridgeCtx.get(sessionId);
478
419
 
@@ -487,8 +428,12 @@ export default function edbBridgeExtension(pi: ExtensionAPI): void {
487
428
  });
488
429
 
489
430
  const messageId = randomUUID();
490
- const replyPromise = waitForReply(sessionId, messageId, signal);
491
- replyPromise.catch(() => undefined);
431
+
432
+ // Signal edb-subagents to mark this agent as suspending BEFORE we send
433
+ // the message — ensures the flag is set before runAgent() can return.
434
+ if (bridgeCtx.agentId) {
435
+ pi.events.emit("bridge:agent_suspending", { agentId: bridgeCtx.agentId });
436
+ }
492
437
 
493
438
  try {
494
439
  const result = await c.send(bridgeCtx.parentSessionId, {
@@ -500,20 +445,27 @@ export default function edbBridgeExtension(pi: ExtensionAPI): void {
500
445
  });
501
446
 
502
447
  if (!result.delivered) {
503
- outboundAskWaiters.get(sessionId)?.reject(new Error(`Supervisor not reachable: ${result.reason}`));
504
- return textResult(`Supervisor not reachable: ${result.reason ?? "session not found"}`);
448
+ if (bridgeCtx.agentId) {
449
+ pi.events.emit("bridge:agent_suspend_cancelled", { agentId: bridgeCtx.agentId });
450
+ }
451
+ return textResult(
452
+ `Supervisor not reachable: ${result.reason ?? "session not found"}. Continuing without answer.`,
453
+ );
505
454
  }
506
455
  } catch (err) {
507
- outboundAskWaiters.get(sessionId)?.reject(new Error(getError(err)));
508
- return textResult(`Failed to send question: ${getError(err)}`);
456
+ if (bridgeCtx.agentId) {
457
+ pi.events.emit("bridge:agent_suspend_cancelled", { agentId: bridgeCtx.agentId });
458
+ }
459
+ return textResult(`Failed to send question: ${getError(err)}. Continuing without answer.`);
509
460
  }
510
461
 
511
- try {
512
- const answer = await replyPromise;
513
- return textResult(`Supervisor replied: ${answer}`);
514
- } catch (err) {
515
- return textResult(`Ask failed: ${getError(err)}`);
516
- }
462
+ // Fire-and-forget: return immediately. The supervisor answers via
463
+ // answer_subagent, which emits bridge:resume_agent to restart this session.
464
+ return textResult(
465
+ "Question sent to supervisor. " +
466
+ "Write a brief summary of your progress so far, then stop all tool calls. " +
467
+ "Your session will be resumed automatically with the supervisor's answer.",
468
+ );
517
469
  },
518
470
 
519
471
  renderCall(args, theme) {
@@ -525,10 +477,9 @@ export default function edbBridgeExtension(pi: ExtensionAPI): void {
525
477
  );
526
478
  },
527
479
 
528
- renderResult(result, { isPartial }, theme) {
529
- if (isPartial) return new Text(theme.fg("warning", "⏸ waiting for supervisor..."), 0, 0);
480
+ renderResult(result, _opts, theme) {
530
481
  const text = result.content[0]?.type === "text" ? result.content[0].text : "";
531
- return new Text(theme.fg("success", " ") + theme.fg("muted", text.slice(0, 120)), 0, 0);
482
+ return new Text(theme.fg("warning", " ") + theme.fg("muted", text.slice(0, 120)), 0, 0);
532
483
  },
533
484
  });
534
485