@d3ara1n/pi-subagent 1.5.1 → 1.6.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/README.md +1 -1
- package/package.json +1 -1
- package/src/index.ts +48 -6
- package/src/reminder.test.ts +1 -1
- package/src/reminder.ts +1 -1
package/README.md
CHANGED
|
@@ -220,7 +220,7 @@ Typical flow:
|
|
|
220
220
|
|
|
221
221
|
Semantics worth knowing:
|
|
222
222
|
|
|
223
|
-
- **Results are pull-only
|
|
223
|
+
- **Results are pull-only for the model.** A purple completion notice is shown to the user, but nothing delivers the result to the model or wakes it up. The model owns the collection point: `subagent_wait`, then `subagent_check` each run. The inbox reminder (below) lists unclaimed runs on every request, but it never pushes results.
|
|
224
224
|
- **Background runs survive turn cancellation** and are unaffected by a cancelled `subagent_wait` — cancelling the wait never cancels the runs; call `subagent_wait` or `subagent_check` again later.
|
|
225
225
|
- **Read-once collection:** `subagent_check` on a terminal run returns the result and frees it — the output now lives in the conversation history, and only a lightweight tombstone stays in the registry (`/subagent:status` lists it under "Collected"). Re-checking a collected id explains that its result is already in the history.
|
|
226
226
|
- **Cancellation keeps the partial output.** `subagent_cancel(id, reason?)` kills the child (SIGTERM, escalating to SIGKILL) and settles the run as `cancelled` — its own stop reason in the same family as `timeout`/`budget_exceeded` (TUI warning styling ⏹, not the error-red ✗ of real failures) — with whatever it had produced. The `reason` becomes the error message verbatim, so whoever reads the partial output later via `subagent_check` — or the audit history — sees `cancelled — <reason>`; the source is distinguishable too (`user: ...` for `/subagent:cancel`, the model's own words for the tool, `session shutdown` for reaping). Cancelling does not collect: `subagent_check` still returns the partial output once, and `subagent_wait` reports the run as `cancelled (partial output kept)`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-subagent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Role-based subagent orchestration for pi — delegates tasks to specialized pi child processes with configurable model roles",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -57,6 +57,8 @@ import {
|
|
|
57
57
|
renderWaitResult,
|
|
58
58
|
} from "./render-async.ts";
|
|
59
59
|
|
|
60
|
+
const BACKGROUND_COMPLETION_MESSAGE_TYPE = "subagent-completion";
|
|
61
|
+
|
|
60
62
|
// ── Extension entry ────────────────────────────────────────────────
|
|
61
63
|
|
|
62
64
|
export default function subagentExtension(pi: ExtensionAPI) {
|
|
@@ -107,6 +109,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
107
109
|
const backgroundRuns = new Map<string, RunHandle>();
|
|
108
110
|
const collectedRuns = new Map<string, CollectedRun>();
|
|
109
111
|
let runCounter = 0;
|
|
112
|
+
let sessionGeneration = 0;
|
|
110
113
|
|
|
111
114
|
// ── Live-run reaping ─────────────────────────────────────────
|
|
112
115
|
// Every in-flight run (foreground and background alike), removed once
|
|
@@ -162,7 +165,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
162
165
|
"BACKGROUND DELEGATION:",
|
|
163
166
|
"",
|
|
164
167
|
"- Use it only when you have your own work this turn (including an ongoing discussion with the user) while the run executes; otherwise let the call block and return the result directly.",
|
|
165
|
-
"- Results are pull-only —
|
|
168
|
+
"- Results are pull-only for the model — a completion notice is shown to the user, but nothing wakes you or delivers the result. Dispatching means owning the collection point: finish your own work, then subagent_check(id) for each result. Use subagent_wait(ids) to block until the run finish.",
|
|
166
169
|
"- Cancel a run you no longer need with subagent_cancel(id) — the child stops and its partial output stays in the registry for subagent_check to collect.",
|
|
167
170
|
"- Background delegation works only in the top-level session.",
|
|
168
171
|
);
|
|
@@ -189,6 +192,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
189
192
|
rebuildGuidelines(availableRoles);
|
|
190
193
|
|
|
191
194
|
pi.on("session_start", async (_event, ctx) => {
|
|
195
|
+
sessionGeneration += 1;
|
|
192
196
|
config = loadSubagentConfig(ctx.cwd);
|
|
193
197
|
concurrencyGate = new AsyncSemaphore(config.maxConcurrency);
|
|
194
198
|
|
|
@@ -220,12 +224,21 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
220
224
|
});
|
|
221
225
|
|
|
222
226
|
pi.on("context", async (event) => {
|
|
227
|
+
// Completion notices are persisted custom messages so the user can see
|
|
228
|
+
// them in the transcript, but they are deliberately UI-only. Keep the
|
|
229
|
+
// model on the reminder/check path instead of duplicating the notice in
|
|
230
|
+
// its context.
|
|
231
|
+
const messages = event.messages.filter(
|
|
232
|
+
(message) =>
|
|
233
|
+
message.role !== "custom" || message.customType !== BACKGROUND_COMPLETION_MESSAGE_TYPE,
|
|
234
|
+
);
|
|
235
|
+
|
|
223
236
|
// The model's inbox: every unclaimed background run, injected at a
|
|
224
|
-
// cache-stable head position before every provider call. Empty inbox
|
|
225
|
-
//
|
|
237
|
+
// cache-stable head position before every provider call. Empty inbox and
|
|
238
|
+
// no filtered notices → context stays untouched, cache fully stable.
|
|
226
239
|
const reminder = buildInboxReminder(backgroundRuns.values());
|
|
227
|
-
if (!reminder) return;
|
|
228
|
-
return { messages: injectReminder(
|
|
240
|
+
if (!reminder && messages.length === event.messages.length) return;
|
|
241
|
+
return { messages: reminder ? injectReminder(messages, reminder) : messages };
|
|
229
242
|
});
|
|
230
243
|
|
|
231
244
|
pi.on("tool_result", (event) => {
|
|
@@ -243,6 +256,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
243
256
|
// runs are audited to history, gates release. Without this, background
|
|
244
257
|
// children would burn tokens as unwaitable orphans after /reload or /new.
|
|
245
258
|
pi.on("session_shutdown", () => {
|
|
259
|
+
sessionGeneration += 1;
|
|
246
260
|
for (const run of liveRuns) run.abort("session shutdown");
|
|
247
261
|
});
|
|
248
262
|
|
|
@@ -275,7 +289,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
275
289
|
background: Type.Optional(
|
|
276
290
|
Type.Boolean({
|
|
277
291
|
description:
|
|
278
|
-
"Non-blocking: returns an id immediately so you can do your own work (or keep discussing with the user) while the run executes — not for parallelism (several foreground calls in one turn already run concurrently). Results are pull-only: nothing delivers
|
|
292
|
+
"Non-blocking: returns an id immediately so you can do your own work (or keep discussing with the user) while the run executes — not for parallelism (several foreground calls in one turn already run concurrently). Results are pull-only for the model: a completion notice is shown to the user, but nothing delivers the result or wakes you; fetch with subagent_wait/subagent_check when your own work is done. If the next thing you'd do is wait for the result, omit this and let the call block.",
|
|
279
293
|
}),
|
|
280
294
|
),
|
|
281
295
|
cwd: Type.Optional(Type.String({ description: "Working directory (defaults to current)" })),
|
|
@@ -339,6 +353,34 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
339
353
|
// ── Background: return the id immediately; the pipeline keeps running. ──
|
|
340
354
|
if (params.background) {
|
|
341
355
|
backgroundRuns.set(run.id, run);
|
|
356
|
+
const runGeneration = sessionGeneration;
|
|
357
|
+
void run.promise.then((result) => {
|
|
358
|
+
// A collected run already has a visible check result. Do not emit a
|
|
359
|
+
// second notice, and never publish completions from an old session.
|
|
360
|
+
if (!backgroundRuns.has(run.id) || runGeneration !== sessionGeneration) return;
|
|
361
|
+
|
|
362
|
+
const outcome = isFailedResult(result)
|
|
363
|
+
? "failed"
|
|
364
|
+
: result.stopReason === "cancelled"
|
|
365
|
+
? "cancelled"
|
|
366
|
+
: "finished";
|
|
367
|
+
const detail =
|
|
368
|
+
outcome === "failed"
|
|
369
|
+
? result.errorMessage || result.stderr
|
|
370
|
+
: outcome === "cancelled"
|
|
371
|
+
? result.errorMessage
|
|
372
|
+
: result.summary;
|
|
373
|
+
const detailText = detail?.trim() ? ` — ${taskPreview(detail)}` : "";
|
|
374
|
+
pi.sendMessage(
|
|
375
|
+
{
|
|
376
|
+
customType: BACKGROUND_COMPLETION_MESSAGE_TYPE,
|
|
377
|
+
content: `Background subagent ${run.id} (${run.role}) ${outcome}: "${taskPreview(run.task)}"${detailText}`,
|
|
378
|
+
display: true,
|
|
379
|
+
details: { id: run.id, role: run.role, outcome },
|
|
380
|
+
},
|
|
381
|
+
{ triggerTurn: false },
|
|
382
|
+
);
|
|
383
|
+
});
|
|
342
384
|
return {
|
|
343
385
|
content: [
|
|
344
386
|
{ type: "text", text: `Background subagent started — id: ${run.id} (${params.role}).` },
|
package/src/reminder.test.ts
CHANGED
|
@@ -129,7 +129,7 @@ describe("buildInboxReminder", () => {
|
|
|
129
129
|
|
|
130
130
|
test("header explains pull-only collection semantics", () => {
|
|
131
131
|
const text = buildInboxReminder([entry({ id: "sub-1", state: "running" })])!;
|
|
132
|
-
assert.match(text, /^\[background subagent runs — results are pull-only/);
|
|
132
|
+
assert.match(text, /^\[background subagent runs — results are pull-only for the model/);
|
|
133
133
|
assert.match(text, /already collected\]/);
|
|
134
134
|
});
|
|
135
135
|
});
|
package/src/reminder.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface InboxEntry {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const INBOX_HEADER =
|
|
31
|
-
"[background subagent runs — results are pull-only:
|
|
31
|
+
"[background subagent runs — results are pull-only for the model: no completion notice wakes you. subagent_wait, then subagent_check to collect each run; a terminal check removes it from this list; runs missing here were already collected]";
|
|
32
32
|
|
|
33
33
|
/** `42s`, `3m12s`, `4m` — whole seconds, no live clocks. */
|
|
34
34
|
function formatDuration(totalSec: number): string {
|