@arhen/pi-core-subagent 1.3.21 → 1.3.23

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": "@arhen/pi-core-subagent",
3
- "version": "1.3.21",
3
+ "version": "1.3.23",
4
4
  "type": "module",
5
5
  "description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
6
6
  "license": "MIT",
package/src/format.ts CHANGED
@@ -86,13 +86,13 @@ export function themedTaskLine(task: TaskSnapshot, theme: Theme, activity = ""):
86
86
  const tail = `${taskStatsWithUsage(task)} · ${taskTimer(task)}`;
87
87
  // Queued task with unmet needs: show the gate it's waiting on instead of empty stats.
88
88
  const gate =
89
- task.status === "queued" && task.needs?.length ? `${theme.fg("muted", `↳ waits ${task.needs.join(",")}`)} · ` : "";
89
+ task.status === "queued" && task.needs?.length ? `${theme.fg("muted", `↳ waits ${task.needs.join(", ")}`)} · ` : "";
90
90
  if (TERMINAL.includes(task.status)) {
91
91
  return theme.fg("dim", `${statusIcon(task.status)} ${task.agent} · ${tail}`);
92
92
  }
93
93
  // Talking (mailbox/intercom tool in flight): pulse the name accent↔dim; normal otherwise.
94
94
  pulsePhase += 1;
95
- const name = isTalking(task) ? theme.fg(pulsePhase % 2 === 0 ? "accent" : "dim", `${task.agent}⇄`) : task.agent;
95
+ const name = isTalking(task) ? theme.fg(pulsePhase % 2 === 0 ? "accent" : "dim", `${task.agent} ⇄`) : task.agent;
96
96
  return `${statusIcon(task.status)} ${name} · ${gate}${activity}${colorNums(tail, theme)}`;
97
97
  }
98
98
  /**
package/src/index.ts CHANGED
@@ -310,13 +310,22 @@ export default function (pi: ExtensionAPI) {
310
310
  pi.registerTool<typeof AwaitParam, { run?: RunSnapshot }>({
311
311
  name: "await_subagent",
312
312
  label: "Await Subagent",
313
- description: "Block until a run finishes (or timeoutMs elapses). Use when you need the result before proceeding.",
313
+ description:
314
+ "Block until a run finishes (or timeoutMs elapses). While parked, child→leader messages (asks, notifies, completions) wake the wait and arrive INSIDE the result — the await doubles as the run's intercom drain, no steering queue involved.",
314
315
  parameters: AwaitParam,
315
316
  async execute(_id, params) {
316
317
  const { runId, timeoutMs } = params as { runId: string; timeoutMs?: number };
317
- const run = await manager.awaitRun(runId, timeoutMs);
318
+ const awaited = await manager.awaitRun(runId, timeoutMs);
319
+ if (!awaited) return { content: [{ type: "text", text: `Unknown runId: ${runId}` }], isError: true, details: {} };
320
+ const { run, intercom } = awaited;
318
321
  if (!run) return { content: [{ type: "text", text: `Unknown runId: ${runId}` }], isError: true, details: {} };
319
- return { content: [{ type: "text", text: makeSummary(run) }], details: { run } };
322
+ const intercomText =
323
+ intercom.length > 0
324
+ ? `\n\nIntercom while waiting:\n${intercom
325
+ .map((m) => `- [${m.kind}] ${m.agent} (${m.taskId}): ${truncateText(m.text)}`)
326
+ .join("\n")}`
327
+ : "";
328
+ return { content: [{ type: "text", text: makeSummary(run) + intercomText }], details: { run } };
320
329
  },
321
330
  });
322
331
 
package/src/manager.ts CHANGED
@@ -190,6 +190,13 @@ interface ChildEventState {
190
190
  childEndResolve?: () => void;
191
191
  }
192
192
 
193
+ export interface ParkedMsg {
194
+ kind: "ask" | "notify" | "done";
195
+ taskId: string;
196
+ agent: string;
197
+ text: string;
198
+ }
199
+
193
200
  export class SubagentManager {
194
201
  private runs = new Map<string, RunSnapshot>();
195
202
  private settlers = new Map<string, (run: RunSnapshot) => void>();
@@ -361,6 +368,11 @@ export class SubagentManager {
361
368
  /** Per-task wake-up: queued follow-up so the parent can interleave responses. */
362
369
  private notifyTask(run: RunSnapshot, task: TaskSnapshot, kind: "completed" | "failed" | "aborted"): void {
363
370
  const body = makeTaskNotice(run, task, kind);
371
+ // Parked leader (await_subagent) receives completions through the wait — no queue.
372
+ if (this.collectParked(run.id, { kind: "done", taskId: task.id, agent: task.agent, text: body })) {
373
+ this.emit("subagent:notification", { runId: run.id, taskId: task.id, kind, body });
374
+ return;
375
+ }
364
376
  try {
365
377
  this.pi.sendUserMessage(body, { deliverAs: "followUp" });
366
378
  } catch {
@@ -492,6 +504,11 @@ export class SubagentManager {
492
504
  onAskParent: async (_taskId, question) => {
493
505
  this.updateTask(run, task, { status: "awaiting_parent" }, ctx);
494
506
  this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
507
+ // While the leader is parked in await_subagent, the question rides the wait
508
+ // instead of the steering queue — no boundary needed, no starvation.
509
+ if (this.collectParked(run.id, { kind: "ask", taskId: task.id, agent: task.agent, text: question })) {
510
+ return "Your question was delivered to the parent (they're waiting on this run). Keep working; the answer arrives via the pending reply.";
511
+ }
495
512
  // A blocking run's parent can't reply mid-tool (followUp only fires after the
496
513
  // tool returns) — only background runs can truly wait for the answer.
497
514
  if (!run.background) {
@@ -513,6 +530,7 @@ export class SubagentManager {
513
530
  },
514
531
  onNotifyParent: (_taskId, message, level) => {
515
532
  this.emit("subagent:intercom", { runId: run.id, taskId: task.id, kind: "notify", level, message });
533
+ if (this.collectParked(run.id, { kind: "notify", taskId: task.id, agent: task.agent, text: message })) return;
516
534
  if (!run.awaited) {
517
535
  try {
518
536
  this.pi.sendUserMessage(`[Subagent ${task.agent}] ${message}`, { deliverAs: "followUp" });
@@ -530,6 +548,7 @@ export class SubagentManager {
530
548
  level: "info",
531
549
  message: text,
532
550
  });
551
+ if (this.collectParked(run.id, { kind: "notify", taskId: task.id, agent: task.agent, text })) return true;
533
552
  if (!run.awaited) {
534
553
  try {
535
554
  this.pi.sendUserMessage(`[Subagent ${task.agent}] ${text}`, { deliverAs: "followUp" });
@@ -1095,40 +1114,63 @@ export class SubagentManager {
1095
1114
  s(cloneRun(run));
1096
1115
  }
1097
1116
 
1098
- awaitRun(runId: string, timeoutMs?: number): Promise<RunSnapshot | undefined> {
1117
+ /** Child→leader messages collected while the parent is parked in await_subagent. */
1118
+ /** Child→leader messages collected while the parent is parked in await_subagent. */
1119
+ private parked = new Map<string, { msgs: ParkedMsg[]; wake: () => void }>();
1120
+
1121
+ /** While the parent is parked on this run, deliver the message through the wait instead of the steering queue. */
1122
+ private collectParked(runId: string, msg: ParkedMsg): boolean {
1123
+ const p = this.parked.get(runId);
1124
+ if (!p) return false;
1125
+ if (p.msgs.length < 24) p.msgs.push(msg);
1126
+ p.wake(); // resolve the parked await early — the leader breathes on every message
1127
+ return true;
1128
+ }
1129
+
1130
+ awaitRun(
1131
+ runId: string,
1132
+ timeoutMs?: number,
1133
+ ): Promise<{ run: RunSnapshot | undefined; intercom: ParkedMsg[] } | undefined> {
1099
1134
  const run = this.runs.get(runId);
1100
1135
  if (!run) return Promise.resolve(undefined);
1136
+ const finish = (): void => {
1137
+ this.parked.delete(runId);
1138
+ };
1101
1139
  if (TERMINAL.includes(run.status)) {
1102
1140
  run.awaited = true;
1103
- return Promise.resolve(cloneRun(run));
1141
+ return Promise.resolve({ run: cloneRun(run), intercom: [] });
1104
1142
  }
1143
+ const msgs: ParkedMsg[] = [];
1105
1144
  const settled = new Promise<RunSnapshot | undefined>((resolve) => {
1106
1145
  const prev = this.settlers.get(runId);
1107
1146
  this.settlers.set(runId, (r) => {
1108
1147
  prev?.(r);
1109
1148
  resolve(r);
1110
1149
  });
1150
+ // A child→leader message while parked wakes the wait: the leader gets it
1151
+ // IN the await result, no steering queue, no turn boundary needed.
1152
+ this.parked.set(runId, { msgs, wake: () => resolve(cloneRun(run)) });
1111
1153
  });
1112
1154
  if (timeoutMs) {
1113
1155
  return Promise.race([
1114
1156
  settled.then((r) => {
1115
- // settled won: the parent got the real result — suppress the notice.
1157
+ finish();
1116
1158
  run.awaited = true;
1117
- return r;
1159
+ return { run: r, intercom: msgs };
1118
1160
  }),
1119
- new Promise<RunSnapshot | undefined>((resolve) => {
1120
- const timer = setTimeout(
1121
- () => resolve(this.runs.get(runId) ? cloneRun(this.runs.get(runId)!) : undefined),
1122
- timeoutMs,
1123
- );
1161
+ new Promise<{ run: RunSnapshot | undefined; intercom: ParkedMsg[] } | undefined>((resolve) => {
1162
+ const timer = setTimeout(() => {
1163
+ finish();
1164
+ resolve(this.runs.get(runId) ? { run: cloneRun(this.runs.get(runId)!), intercom: msgs } : undefined);
1165
+ }, timeoutMs);
1124
1166
  settled.then(() => clearTimeout(timer));
1125
1167
  }),
1126
1168
  ]);
1127
1169
  }
1128
- // Awaiting to completion: parent gets the real result, so suppress the
1129
- // completion notice. On timeout we resolve a snapshot and leave awaited
1130
- // unset, so the parent still receives the completion notification.
1131
- run.awaited = true;
1132
- return settled;
1170
+ return settled.then((r) => {
1171
+ finish();
1172
+ run.awaited = true;
1173
+ return { run: r, intercom: msgs };
1174
+ });
1133
1175
  }
1134
1176
  }