@hheei/pi-subagents 0.2.3 → 0.2.4
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/dist/extension.js +3 -2
- package/dist/manager.js +115 -79
- package/dist/runtime.js +8 -0
- package/dist/widget.js +1 -1
- package/package.json +3 -3
package/dist/extension.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createSubagentWidget } from "./widget.js";
|
|
|
16
16
|
const CHILD_ENV_KEYS = Object.values(BRIDGE_ENVIRONMENT_KEYS);
|
|
17
17
|
/** How long startup waits for a child that survived a parent restart to reconnect its bridge. */
|
|
18
18
|
const ADOPT_WINDOW_MS = 5_000;
|
|
19
|
+
const START_WINDOW_MS = 20_000;
|
|
19
20
|
function diagnose(message) {
|
|
20
21
|
if (process.env.DEBUG || process.env.PI_SUBAGENTS_DEBUG) {
|
|
21
22
|
console.error(`pi-subagents: ${message}`);
|
|
@@ -237,7 +238,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
237
238
|
record,
|
|
238
239
|
tokens,
|
|
239
240
|
waitForBridge: (childId, signal) => server.waitForConnection(childId, {
|
|
240
|
-
timeoutMs:
|
|
241
|
+
timeoutMs: START_WINDOW_MS,
|
|
241
242
|
...(signal === undefined ? {} : { signal }),
|
|
242
243
|
}),
|
|
243
244
|
signal: runtime.signal,
|
|
@@ -254,7 +255,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
254
255
|
tokens,
|
|
255
256
|
host: hostSelection.adapter,
|
|
256
257
|
waitForBridge: (childId, signal) => server.waitForConnection(childId, {
|
|
257
|
-
timeoutMs:
|
|
258
|
+
timeoutMs: START_WINDOW_MS,
|
|
258
259
|
...(signal === undefined ? {} : { signal }),
|
|
259
260
|
}),
|
|
260
261
|
signal: runtime.signal,
|
package/dist/manager.js
CHANGED
|
@@ -346,7 +346,7 @@ export class SubagentManager {
|
|
|
346
346
|
...withoutRuntime,
|
|
347
347
|
intent: "stopped",
|
|
348
348
|
state: "error",
|
|
349
|
-
interrupted: "The child
|
|
349
|
+
interrupted: "The host reports that the child process exited while running; the cause is unknown. Spawn a new child for more work",
|
|
350
350
|
};
|
|
351
351
|
});
|
|
352
352
|
if (retired !== undefined)
|
|
@@ -400,7 +400,9 @@ export class SubagentManager {
|
|
|
400
400
|
this.#clearAutoReport(id);
|
|
401
401
|
const timer = setTimeout(() => {
|
|
402
402
|
this.#autoReportTimers.delete(id);
|
|
403
|
-
void this.#performAutoReport(id, record)
|
|
403
|
+
void this.#performAutoReport(id, record).catch((error) => {
|
|
404
|
+
diagnose(`failed to report child ${id}: ${errorMessage(error)}`);
|
|
405
|
+
});
|
|
404
406
|
}, 5_000);
|
|
405
407
|
timer.unref?.();
|
|
406
408
|
this.#autoReportTimers.set(id, timer);
|
|
@@ -443,7 +445,10 @@ export class SubagentManager {
|
|
|
443
445
|
async #closeSettledPanel(id) {
|
|
444
446
|
await this.#mutate(id, async () => {
|
|
445
447
|
const record = await this.#deps.registry.get(id);
|
|
446
|
-
if (record === undefined ||
|
|
448
|
+
if (record === undefined ||
|
|
449
|
+
record.state === "running" ||
|
|
450
|
+
record.intent === "stopped" ||
|
|
451
|
+
this.#working.has(id)) {
|
|
447
452
|
return;
|
|
448
453
|
}
|
|
449
454
|
const attachment = this.#attachments.get(id);
|
|
@@ -516,83 +521,95 @@ export class SubagentManager {
|
|
|
516
521
|
const record = (await this.#deps.registry.get(id)) ?? _record;
|
|
517
522
|
if (record.intent === "stopped" || record.state === "running")
|
|
518
523
|
return;
|
|
524
|
+
const activity = this.#activity.get(id) ?? 0;
|
|
525
|
+
const workVersion = this.#workVersions.get(id) ?? 0;
|
|
519
526
|
const entries = await this.#entries(id);
|
|
520
527
|
const text = entries !== undefined ? extractLastAssistantText(entries) : undefined;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
528
|
+
await this.#mutate(id, async () => {
|
|
529
|
+
const current = await this.#deps.registry.get(id);
|
|
530
|
+
if (current === undefined ||
|
|
531
|
+
current.intent === "stopped" ||
|
|
532
|
+
current.state === "running" ||
|
|
533
|
+
this.#working.has(id) ||
|
|
534
|
+
(this.#activity.get(id) ?? 0) !== activity ||
|
|
535
|
+
(this.#workVersions.get(id) ?? 0) !== workVersion)
|
|
536
|
+
return;
|
|
537
|
+
const hasError = record.state === "error" ||
|
|
538
|
+
(record.interrupted !== undefined && record.interrupted.trim() !== "");
|
|
539
|
+
const isBlocked = hasError;
|
|
540
|
+
if (!isBlocked && (text === undefined || text.trim() === "")) {
|
|
541
|
+
const projector = this.#projectors.get(id);
|
|
542
|
+
if (projector !== undefined) {
|
|
543
|
+
projector.syncState("done");
|
|
544
|
+
}
|
|
545
|
+
await this.#update(id, (current) => {
|
|
546
|
+
const { interrupted: _interrupted, ...clean } = current;
|
|
547
|
+
return {
|
|
548
|
+
...clean,
|
|
549
|
+
state: "done",
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
if (this.#taskRegistry !== undefined) {
|
|
553
|
+
this.#taskRegistry.settle(id, {
|
|
554
|
+
status: "completed",
|
|
555
|
+
output: "",
|
|
556
|
+
truncated: false,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
let message;
|
|
562
|
+
if (isBlocked) {
|
|
563
|
+
if (record.interrupted !== undefined && record.interrupted.trim() !== "") {
|
|
564
|
+
message =
|
|
565
|
+
text !== undefined && text.trim() !== ""
|
|
566
|
+
? `${record.interrupted}\n\n${text}`
|
|
567
|
+
: record.interrupted;
|
|
568
|
+
}
|
|
569
|
+
else if (text !== undefined && text.trim() !== "") {
|
|
570
|
+
message = text;
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
message = "Subagent became idle without completing its task (interrupted or failed).";
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
message = text ?? "";
|
|
578
|
+
}
|
|
579
|
+
if (this.#lastDeliveredTurn.get(id) === message)
|
|
580
|
+
return;
|
|
581
|
+
this.#lastDeliveredTurn.set(id, message);
|
|
582
|
+
const reason = isBlocked ? "blocked" : "success";
|
|
583
|
+
const taskStatus = isBlocked ? "failed" : "completed";
|
|
525
584
|
const projector = this.#projectors.get(id);
|
|
585
|
+
const updatedRecord = !isBlocked
|
|
586
|
+
? await this.#update(id, (current) => {
|
|
587
|
+
const { interrupted: _interrupted, ...clean } = current;
|
|
588
|
+
return {
|
|
589
|
+
...clean,
|
|
590
|
+
state: "done",
|
|
591
|
+
};
|
|
592
|
+
})
|
|
593
|
+
: await this.#update(id, (current) => ({
|
|
594
|
+
...current,
|
|
595
|
+
state: "blocked",
|
|
596
|
+
interrupted: current.interrupted ?? message,
|
|
597
|
+
}));
|
|
526
598
|
if (projector !== undefined) {
|
|
527
|
-
projector.syncState("done");
|
|
599
|
+
projector.syncState(isBlocked ? "blocked" : "done");
|
|
600
|
+
}
|
|
601
|
+
this.#scheduleSettledPanelClose(id);
|
|
602
|
+
if (this.#deps.channel !== undefined) {
|
|
603
|
+
await this.contactParent(id, reason, message, updatedRecord);
|
|
528
604
|
}
|
|
529
|
-
await this.#update(id, (current) => {
|
|
530
|
-
const { interrupted: _interrupted, ...clean } = current;
|
|
531
|
-
return {
|
|
532
|
-
...clean,
|
|
533
|
-
state: "done",
|
|
534
|
-
};
|
|
535
|
-
});
|
|
536
605
|
if (this.#taskRegistry !== undefined) {
|
|
537
606
|
this.#taskRegistry.settle(id, {
|
|
538
|
-
status:
|
|
539
|
-
output:
|
|
607
|
+
status: taskStatus,
|
|
608
|
+
output: message,
|
|
540
609
|
truncated: false,
|
|
541
610
|
});
|
|
542
611
|
}
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
let message;
|
|
546
|
-
if (isBlocked) {
|
|
547
|
-
if (record.interrupted !== undefined && record.interrupted.trim() !== "") {
|
|
548
|
-
message =
|
|
549
|
-
text !== undefined && text.trim() !== ""
|
|
550
|
-
? `${record.interrupted}\n\n${text}`
|
|
551
|
-
: record.interrupted;
|
|
552
|
-
}
|
|
553
|
-
else if (text !== undefined && text.trim() !== "") {
|
|
554
|
-
message = text;
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
message = "Subagent became idle without completing its task (interrupted or failed).";
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
message = text ?? "";
|
|
562
|
-
}
|
|
563
|
-
if (this.#lastDeliveredTurn.get(id) === message)
|
|
564
|
-
return;
|
|
565
|
-
this.#lastDeliveredTurn.set(id, message);
|
|
566
|
-
const reason = isBlocked ? "blocked" : "success";
|
|
567
|
-
const taskStatus = isBlocked ? "failed" : "completed";
|
|
568
|
-
const projector = this.#projectors.get(id);
|
|
569
|
-
const updatedRecord = !isBlocked
|
|
570
|
-
? await this.#update(id, (current) => {
|
|
571
|
-
const { interrupted: _interrupted, ...clean } = current;
|
|
572
|
-
return {
|
|
573
|
-
...clean,
|
|
574
|
-
state: "done",
|
|
575
|
-
};
|
|
576
|
-
})
|
|
577
|
-
: await this.#update(id, (current) => ({
|
|
578
|
-
...current,
|
|
579
|
-
state: "blocked",
|
|
580
|
-
interrupted: current.interrupted ?? message,
|
|
581
|
-
}));
|
|
582
|
-
if (projector !== undefined) {
|
|
583
|
-
projector.syncState(isBlocked ? "blocked" : "done");
|
|
584
|
-
}
|
|
585
|
-
this.#scheduleSettledPanelClose(id);
|
|
586
|
-
if (this.#deps.channel !== undefined) {
|
|
587
|
-
await this.contactParent(id, reason, message, updatedRecord);
|
|
588
|
-
}
|
|
589
|
-
if (this.#taskRegistry !== undefined) {
|
|
590
|
-
this.#taskRegistry.settle(id, {
|
|
591
|
-
status: taskStatus,
|
|
592
|
-
output: message,
|
|
593
|
-
truncated: false,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
612
|
+
});
|
|
596
613
|
}
|
|
597
614
|
/** Reads a child's session entries; exposed for Task completion inspection. */
|
|
598
615
|
async getEntries(id) {
|
|
@@ -848,7 +865,7 @@ export class SubagentManager {
|
|
|
848
865
|
childId = record.subagentId;
|
|
849
866
|
this.#working.add(childId);
|
|
850
867
|
this.#notify();
|
|
851
|
-
await withDeadline(this.#start(record), this.#deps.deadlineMs ??
|
|
868
|
+
await withDeadline(this.#start(record), this.#deps.deadlineMs ?? 60_000);
|
|
852
869
|
try {
|
|
853
870
|
await this.#request(record.subagentId, "prompt", { message: cleanTask });
|
|
854
871
|
}
|
|
@@ -904,17 +921,36 @@ export class SubagentManager {
|
|
|
904
921
|
};
|
|
905
922
|
}
|
|
906
923
|
catch (error) {
|
|
907
|
-
if (childId
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
924
|
+
if (childId === undefined)
|
|
925
|
+
return failure("spawn", errorMessage(error));
|
|
926
|
+
const failedId = childId;
|
|
927
|
+
const reason = errorMessage(error);
|
|
928
|
+
const failed = await this.#mutate(failedId, async () => {
|
|
929
|
+
const record = await this.#deps.registry.get(failedId);
|
|
930
|
+
const uncertain = this.#runtimeStarted(failedId) ||
|
|
931
|
+
this.#unresolvedRuntimes.has(failedId) ||
|
|
932
|
+
record?.runtime !== undefined;
|
|
933
|
+
if (uncertain)
|
|
934
|
+
this.#unresolvedRuntimes.add(failedId);
|
|
935
|
+
if (!this.#isLive(failedId))
|
|
936
|
+
this.#working.delete(failedId);
|
|
937
|
+
await this.#update(failedId, (current) => ({
|
|
938
|
+
...current,
|
|
939
|
+
state: "error",
|
|
940
|
+
interrupted: reason,
|
|
941
|
+
}));
|
|
942
|
+
this.#projectors.get(failedId)?.syncState("error");
|
|
943
|
+
return failure("spawn", reason, failedId, "error", [
|
|
944
|
+
"registry record persisted",
|
|
945
|
+
...(uncertain ? ["runtime start or cleanup unconfirmed"] : ["no runtime remains"]),
|
|
946
|
+
], !uncertain);
|
|
947
|
+
});
|
|
948
|
+
return failed;
|
|
914
949
|
}
|
|
915
950
|
}
|
|
916
951
|
send(id, rawMessage, mode = "auto", signal) {
|
|
917
952
|
const message = stripHindsightContent(rawMessage);
|
|
953
|
+
this.#bumpActivity(id);
|
|
918
954
|
this.#clearSettledPanelClose(id);
|
|
919
955
|
return this.#mutate(id, async () => {
|
|
920
956
|
const record = await this.#deps.registry.get(id);
|
|
@@ -958,7 +994,7 @@ export class SubagentManager {
|
|
|
958
994
|
this.#projectors.set(id, createStateProjector(resumedRecord.state));
|
|
959
995
|
}
|
|
960
996
|
try {
|
|
961
|
-
await withDeadline(this.#start(resumedRecord), this.#deps.deadlineMs ??
|
|
997
|
+
await withDeadline(this.#start(resumedRecord), this.#deps.deadlineMs ?? 60_000);
|
|
962
998
|
isResumed = true;
|
|
963
999
|
}
|
|
964
1000
|
catch (error) {
|
package/dist/runtime.js
CHANGED
|
@@ -167,6 +167,10 @@ export async function launchChild(options) {
|
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
options.tokens.forget(plan.runtimeIdentity);
|
|
170
|
+
await options.registry.update(record.subagentId, undefined, (current) => {
|
|
171
|
+
const { runtime: _runtime, ...rest } = current;
|
|
172
|
+
return rest;
|
|
173
|
+
}, plan.runtimeIdentity);
|
|
170
174
|
return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
|
|
171
175
|
}
|
|
172
176
|
/**
|
|
@@ -214,6 +218,10 @@ export async function openChildPanel(options) {
|
|
|
214
218
|
};
|
|
215
219
|
}
|
|
216
220
|
options.tokens.forget(plan.runtimeIdentity);
|
|
221
|
+
await options.registry.update(record.subagentId, undefined, (current) => {
|
|
222
|
+
const { runtime: _runtime, ...rest } = current;
|
|
223
|
+
return rest;
|
|
224
|
+
}, plan.runtimeIdentity);
|
|
217
225
|
return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
|
|
218
226
|
}
|
|
219
227
|
/** Creates the runtime directory if needed; the socket itself is 0600 in a 0700 directory. */
|
package/dist/widget.js
CHANGED
|
@@ -54,7 +54,7 @@ export function renderSubagentWidget(children, width, theme, nowMs = Date.now())
|
|
|
54
54
|
const glyphStyled = theme.fg(tone, glyph);
|
|
55
55
|
const idStyled = theme.fg("dim", `#${child.id}`);
|
|
56
56
|
const nameStyled = theme.fg("text", child.displayName ?? child.agent);
|
|
57
|
-
const elapsed = formatElapsed(child.createdAt, nowMs);
|
|
57
|
+
const elapsed = formatElapsed(child.createdAt, isTerminalState(child.state) ? Date.parse(child.updatedAt) : nowMs);
|
|
58
58
|
const timeStyled = theme.fg("dim", elapsed === "" ? "0s" : elapsed);
|
|
59
59
|
const stateStyled = theme.fg(tone, visual);
|
|
60
60
|
const turnsCount = child.usage?.turns ?? 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hheei/pi-subagents",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Independent background Pi subagent sessions with durable identity and RPC control.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"typebox": "*"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@earendil-works/pi-coding-agent": "1.0.
|
|
47
|
-
"@earendil-works/pi-tui": "1.0.
|
|
46
|
+
"@earendil-works/pi-coding-agent": "1.0.2",
|
|
47
|
+
"@earendil-works/pi-tui": "1.0.2"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
|