@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 +1 -1
- package/src/format.ts +2 -2
- package/src/index.ts +12 -3
- package/src/manager.ts +56 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
()
|
|
1122
|
-
|
|
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
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1170
|
+
return settled.then((r) => {
|
|
1171
|
+
finish();
|
|
1172
|
+
run.awaited = true;
|
|
1173
|
+
return { run: r, intercom: msgs };
|
|
1174
|
+
});
|
|
1133
1175
|
}
|
|
1134
1176
|
}
|