@hheei/pi-subagents 0.2.4 → 0.3.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 +32 -17
- package/dist/agent-resolver.d.ts +9 -0
- package/dist/agent-resolver.js +278 -51
- package/dist/bridge-client.d.ts +5 -0
- package/dist/bridge-client.js +6 -0
- package/dist/builtin-agents.js +50 -37
- package/dist/child-bridge.d.ts +0 -8
- package/dist/child-bridge.js +60 -43
- package/dist/child-control.d.ts +5 -3
- package/dist/child-control.js +176 -35
- package/dist/commands.d.ts +1 -4
- package/dist/commands.js +1 -52
- package/dist/domain.d.ts +12 -12
- package/dist/domain.js +2 -1
- package/dist/extension.d.ts +5 -3
- package/dist/extension.js +39 -42
- package/dist/host-adapter.js +1 -1
- package/dist/info.d.ts +9 -0
- package/dist/info.js +54 -0
- package/dist/launch-spec.d.ts +0 -1
- package/dist/launch-spec.js +38 -18
- package/dist/manager.d.ts +4 -2
- package/dist/manager.js +207 -58
- package/dist/registry.js +19 -5
- package/dist/rpc-events.d.ts +1 -1
- package/dist/rpc-events.js +0 -1
- package/dist/session-bootstrap.d.ts +2 -1
- package/dist/session-bootstrap.js +22 -14
- package/dist/state.d.ts +13 -0
- package/dist/state.js +59 -16
- package/dist/status.d.ts +14 -0
- package/dist/status.js +36 -0
- package/dist/tool-activation.d.ts +3 -4
- package/dist/tool-activation.js +13 -215
- package/dist/tool-presentation.d.ts +17 -0
- package/dist/tool-presentation.js +160 -0
- package/dist/tools.d.ts +6 -1
- package/dist/tools.js +133 -180
- package/package.json +6 -6
- package/dist/session-fork.d.ts +0 -16
- package/dist/session-fork.js +0 -67
- package/dist/widget.d.ts +0 -12
- package/dist/widget.js +0 -149
package/dist/manager.js
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { errorMessage, isRecord } from "@hheei/pi-ext-core";
|
|
3
|
+
import { isOperationError } from "./domain.js";
|
|
2
4
|
import { stripHindsightContent } from "./launch-spec.js";
|
|
3
5
|
import { BridgeError, isChildInputPayload, isChildLifecyclePayload, isContactReportPayload, } from "./protocol.js";
|
|
4
6
|
import { findSessionFile, planSessionPlacement } from "./session-bootstrap.js";
|
|
5
|
-
|
|
7
|
+
async function readSessionJsonlEntries(filePath) {
|
|
8
|
+
const content = await readFile(filePath, "utf8");
|
|
9
|
+
const entries = [];
|
|
10
|
+
for (const line of content.split("\n")) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
if (trimmed.length > 0) {
|
|
13
|
+
try {
|
|
14
|
+
entries.push(JSON.parse(trimmed));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Ignore corrupted line
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return entries;
|
|
22
|
+
}
|
|
6
23
|
import { createStateProjector } from "./state.js";
|
|
7
24
|
/**
|
|
8
25
|
* How long an idle child keeps its runtime. A background child holds a whole Pi process and its
|
|
9
|
-
* session lock, so an idle one is reclaimed after this window and a later `
|
|
26
|
+
* session lock, so an idle one is reclaimed after this window and a later `agent_send` starts it
|
|
10
27
|
* again from the same session.
|
|
11
28
|
*/
|
|
12
29
|
export const SUBAGENT_IDLE_TIMEOUT_MS = 15_000;
|
|
@@ -29,9 +46,19 @@ function failure(operation, reason, childId, state, sideEffects = [], safeToRetr
|
|
|
29
46
|
safeToRetry,
|
|
30
47
|
};
|
|
31
48
|
}
|
|
32
|
-
function
|
|
49
|
+
function acknowledgedPath(response) {
|
|
50
|
+
if (!isRecord(response) ||
|
|
51
|
+
response.accepted !== true ||
|
|
52
|
+
typeof response.path !== "string" ||
|
|
53
|
+
response.path === "") {
|
|
54
|
+
throw new BridgeError("invalid_state", "Child did not confirm its session path");
|
|
55
|
+
}
|
|
56
|
+
return response.path;
|
|
57
|
+
}
|
|
58
|
+
function publicChild(record, live, path = record.sessionPath) {
|
|
33
59
|
return {
|
|
34
60
|
id: record.subagentId,
|
|
61
|
+
...(path === undefined ? {} : { path }),
|
|
35
62
|
agent: record.launchConfig.agent.name,
|
|
36
63
|
...(record.launchConfig.agent.displayName === undefined
|
|
37
64
|
? {}
|
|
@@ -40,12 +67,12 @@ function publicChild(record, live) {
|
|
|
40
67
|
presentation: record.presentation,
|
|
41
68
|
cwd: record.cwd,
|
|
42
69
|
sessionId: record.sessionId,
|
|
70
|
+
...(record.initialTask === undefined ? {} : { task: record.initialTask }),
|
|
43
71
|
...(record.latestSummary === undefined ? {} : { summary: record.latestSummary }),
|
|
44
72
|
...(record.activeTool === undefined ? {} : { activeTool: record.activeTool }),
|
|
45
73
|
...(record.usage === undefined ? {} : { usage: record.usage }),
|
|
46
74
|
...(record.interrupted === undefined ? {} : { interrupted: record.interrupted }),
|
|
47
75
|
freshness: live ? "live" : "last_known",
|
|
48
|
-
interactive: record.launchConfig.interactive,
|
|
49
76
|
model: record.launchConfig.model,
|
|
50
77
|
thinking: record.launchConfig.thinking,
|
|
51
78
|
createdAt: record.createdAt,
|
|
@@ -131,12 +158,15 @@ export class SubagentManager {
|
|
|
131
158
|
/** Bumped by every inbound bridge signal, so a reclaim in flight can tell the child moved. */
|
|
132
159
|
#activity = new Map();
|
|
133
160
|
#projectors = new Map();
|
|
161
|
+
/** Display paths do not imply that a session has been flushed to disk. */
|
|
162
|
+
#paths = new Map();
|
|
134
163
|
/** Known child work in flight, separate from failures recorded before Pi finally settles. */
|
|
135
164
|
#working = new Set();
|
|
136
165
|
/** A settled event from an older run cannot clear a newer synchronous start signal. */
|
|
137
166
|
#workVersions = new Map();
|
|
138
167
|
#settledPanelCloseTimers = new Map();
|
|
139
|
-
#
|
|
168
|
+
#pendingAutoReports = new Map();
|
|
169
|
+
#autoReportBatchTimer;
|
|
140
170
|
#lastDeliveredTurn = new Map();
|
|
141
171
|
#stopping = new Set();
|
|
142
172
|
#listeners = new Set();
|
|
@@ -168,7 +198,7 @@ export class SubagentManager {
|
|
|
168
198
|
this.#childEventListeners.delete(listener);
|
|
169
199
|
};
|
|
170
200
|
}
|
|
171
|
-
/** Presentation-only:
|
|
201
|
+
/** Presentation-only: status and similar projections re-read list() after this. */
|
|
172
202
|
onChange(listener) {
|
|
173
203
|
this.#listeners.add(listener);
|
|
174
204
|
return () => {
|
|
@@ -176,6 +206,11 @@ export class SubagentManager {
|
|
|
176
206
|
};
|
|
177
207
|
}
|
|
178
208
|
dispose() {
|
|
209
|
+
if (this.#autoReportBatchTimer !== undefined) {
|
|
210
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
211
|
+
this.#autoReportBatchTimer = undefined;
|
|
212
|
+
}
|
|
213
|
+
this.#pendingAutoReports.clear();
|
|
179
214
|
for (const timer of this.#settledPanelCloseTimers.values()) {
|
|
180
215
|
clearTimeout(timer);
|
|
181
216
|
}
|
|
@@ -190,12 +225,18 @@ export class SubagentManager {
|
|
|
190
225
|
*/
|
|
191
226
|
closeLocalConnections() {
|
|
192
227
|
this.dispose();
|
|
228
|
+
if (this.#autoReportBatchTimer !== undefined) {
|
|
229
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
230
|
+
this.#autoReportBatchTimer = undefined;
|
|
231
|
+
}
|
|
232
|
+
this.#pendingAutoReports.clear();
|
|
193
233
|
this.#processes.clear();
|
|
194
234
|
this.#attachments.clear();
|
|
195
235
|
this.#leftSessions.clear();
|
|
196
236
|
this.#unresolvedRuntimes.clear();
|
|
197
237
|
this.#activity.clear();
|
|
198
238
|
this.#projectors.clear();
|
|
239
|
+
this.#paths.clear();
|
|
199
240
|
this.#working.clear();
|
|
200
241
|
this.#workVersions.clear();
|
|
201
242
|
this.#notify();
|
|
@@ -299,13 +340,15 @@ export class SubagentManager {
|
|
|
299
340
|
// A panel the host confirms is gone stops holding this child — the handle it left behind is
|
|
300
341
|
// also what a retried stop would act on, so it is released here before refusing.
|
|
301
342
|
await this.#releasePanel(id).catch(() => undefined);
|
|
302
|
-
if ((await this.#deps.registry.get(id))?.intent === "stopped") {
|
|
303
|
-
return STOPPED_SEND_REASON;
|
|
304
|
-
}
|
|
305
343
|
}
|
|
306
344
|
if (this.#runtimeStarted(id)) {
|
|
307
345
|
return "This child's previous runtime is not confirmed gone (its process or panel may still be running); stop the child to end it, then spawn a new one for this work";
|
|
308
346
|
}
|
|
347
|
+
if (this.#attachments.has(id)) {
|
|
348
|
+
if ((await this.#deps.registry.get(id))?.intent === "stopped") {
|
|
349
|
+
return STOPPED_SEND_REASON;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
309
352
|
if (this.#unresolvedRuntimes.has(id)) {
|
|
310
353
|
return "This child's panel runtime is disconnected and its exit cannot be confirmed; no replacement was started. Close any remaining panel, stop the child, then spawn a new child for this work";
|
|
311
354
|
}
|
|
@@ -397,21 +440,29 @@ export class SubagentManager {
|
|
|
397
440
|
* only ever closed on a judgement, never on a guess.
|
|
398
441
|
*/
|
|
399
442
|
#scheduleAutoReport(id, record) {
|
|
400
|
-
this.#
|
|
401
|
-
|
|
402
|
-
this.#
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
443
|
+
this.#pendingAutoReports.set(id, record);
|
|
444
|
+
if (this.#autoReportBatchTimer !== undefined) {
|
|
445
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
446
|
+
}
|
|
447
|
+
// Batch reports: if all children settled, flush in 500ms; otherwise wait up to 10000ms for others
|
|
448
|
+
const delayMs = this.#working.size > 0 ? 10_000 : 500;
|
|
449
|
+
this.#autoReportBatchTimer = setTimeout(async () => {
|
|
450
|
+
this.#autoReportBatchTimer = undefined;
|
|
451
|
+
const batch = [...this.#pendingAutoReports.entries()];
|
|
452
|
+
this.#pendingAutoReports.clear();
|
|
453
|
+
for (const [childId, childRecord] of batch) {
|
|
454
|
+
await this.#performAutoReport(childId, childRecord).catch((error) => {
|
|
455
|
+
diagnose(`failed to report child ${childId}: ${errorMessage(error)}`);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}, delayMs);
|
|
459
|
+
this.#autoReportBatchTimer.unref?.();
|
|
409
460
|
}
|
|
410
461
|
#clearAutoReport(id) {
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
clearTimeout(
|
|
414
|
-
this.#
|
|
462
|
+
this.#pendingAutoReports.delete(id);
|
|
463
|
+
if (this.#pendingAutoReports.size === 0 && this.#autoReportBatchTimer !== undefined) {
|
|
464
|
+
clearTimeout(this.#autoReportBatchTimer);
|
|
465
|
+
this.#autoReportBatchTimer = undefined;
|
|
415
466
|
}
|
|
416
467
|
}
|
|
417
468
|
#scheduleSettledPanelClose(id) {
|
|
@@ -534,10 +585,14 @@ export class SubagentManager {
|
|
|
534
585
|
(this.#activity.get(id) ?? 0) !== activity ||
|
|
535
586
|
(this.#workVersions.get(id) ?? 0) !== workVersion)
|
|
536
587
|
return;
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
588
|
+
const isError = current.state === "error" || record.state === "error";
|
|
589
|
+
const isBlocked = !isError &&
|
|
590
|
+
(current.state === "blocked" ||
|
|
591
|
+
record.state === "blocked" ||
|
|
592
|
+
(current.interrupted !== undefined && current.interrupted.trim() !== "") ||
|
|
593
|
+
(record.interrupted !== undefined && record.interrupted.trim() !== ""));
|
|
594
|
+
const failed = isError || isBlocked;
|
|
595
|
+
if (!failed && (text === undefined || text.trim() === "")) {
|
|
541
596
|
const projector = this.#projectors.get(id);
|
|
542
597
|
if (projector !== undefined) {
|
|
543
598
|
projector.syncState("done");
|
|
@@ -559,12 +614,16 @@ export class SubagentManager {
|
|
|
559
614
|
return;
|
|
560
615
|
}
|
|
561
616
|
let message;
|
|
562
|
-
|
|
563
|
-
|
|
617
|
+
const interruptedText = current.interrupted?.trim() || record.interrupted?.trim();
|
|
618
|
+
if (isError) {
|
|
619
|
+
message = interruptedText || "Subagent failed without error details.";
|
|
620
|
+
}
|
|
621
|
+
else if (isBlocked) {
|
|
622
|
+
if (interruptedText !== undefined && interruptedText !== "") {
|
|
564
623
|
message =
|
|
565
624
|
text !== undefined && text.trim() !== ""
|
|
566
|
-
? `${
|
|
567
|
-
:
|
|
625
|
+
? `${interruptedText}\n\n${text}`
|
|
626
|
+
: interruptedText;
|
|
568
627
|
}
|
|
569
628
|
else if (text !== undefined && text.trim() !== "") {
|
|
570
629
|
message = text;
|
|
@@ -576,13 +635,16 @@ export class SubagentManager {
|
|
|
576
635
|
else {
|
|
577
636
|
message = text ?? "";
|
|
578
637
|
}
|
|
579
|
-
|
|
638
|
+
const lastDelivered = this.#lastDeliveredTurn.get(id);
|
|
639
|
+
if (lastDelivered !== undefined &&
|
|
640
|
+
lastDelivered.workVersion === workVersion &&
|
|
641
|
+
lastDelivered.message === message) {
|
|
580
642
|
return;
|
|
581
|
-
|
|
582
|
-
const reason = isBlocked ? "blocked" : "success";
|
|
583
|
-
const taskStatus =
|
|
643
|
+
}
|
|
644
|
+
const reason = isError ? "error" : isBlocked ? "blocked" : "success";
|
|
645
|
+
const taskStatus = failed ? "failed" : "completed";
|
|
584
646
|
const projector = this.#projectors.get(id);
|
|
585
|
-
const updatedRecord = !
|
|
647
|
+
const updatedRecord = !failed
|
|
586
648
|
? await this.#update(id, (current) => {
|
|
587
649
|
const { interrupted: _interrupted, ...clean } = current;
|
|
588
650
|
return {
|
|
@@ -592,16 +654,13 @@ export class SubagentManager {
|
|
|
592
654
|
})
|
|
593
655
|
: await this.#update(id, (current) => ({
|
|
594
656
|
...current,
|
|
595
|
-
state: "blocked",
|
|
657
|
+
state: isError ? "error" : "blocked",
|
|
596
658
|
interrupted: current.interrupted ?? message,
|
|
597
659
|
}));
|
|
598
660
|
if (projector !== undefined) {
|
|
599
|
-
projector.syncState(isBlocked ? "blocked" : "done");
|
|
661
|
+
projector.syncState(isError ? "error" : isBlocked ? "blocked" : "done");
|
|
600
662
|
}
|
|
601
663
|
this.#scheduleSettledPanelClose(id);
|
|
602
|
-
if (this.#deps.channel !== undefined) {
|
|
603
|
-
await this.contactParent(id, reason, message, updatedRecord);
|
|
604
|
-
}
|
|
605
664
|
if (this.#taskRegistry !== undefined) {
|
|
606
665
|
this.#taskRegistry.settle(id, {
|
|
607
666
|
status: taskStatus,
|
|
@@ -609,6 +668,16 @@ export class SubagentManager {
|
|
|
609
668
|
truncated: false,
|
|
610
669
|
});
|
|
611
670
|
}
|
|
671
|
+
let deliveryOk = true;
|
|
672
|
+
if (this.#deps.channel !== undefined) {
|
|
673
|
+
const contactResult = await this.contactParent(id, reason, message, updatedRecord);
|
|
674
|
+
if (isOperationError(contactResult)) {
|
|
675
|
+
deliveryOk = false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (deliveryOk) {
|
|
679
|
+
this.#lastDeliveredTurn.set(id, { workVersion, message });
|
|
680
|
+
}
|
|
612
681
|
});
|
|
613
682
|
}
|
|
614
683
|
/** Reads a child's session entries; exposed for Task completion inspection. */
|
|
@@ -745,11 +814,37 @@ export class SubagentManager {
|
|
|
745
814
|
*/
|
|
746
815
|
async #settleUnrecovered(record) {
|
|
747
816
|
this.#working.delete(record.subagentId);
|
|
817
|
+
this.#projectors.delete(record.subagentId);
|
|
748
818
|
const retired = record.runtime;
|
|
749
|
-
if (retired === undefined)
|
|
750
|
-
return;
|
|
751
819
|
if (record.presentation === "panel") {
|
|
752
|
-
|
|
820
|
+
if (retired !== undefined) {
|
|
821
|
+
this.#unresolvedRuntimes.add(record.subagentId);
|
|
822
|
+
}
|
|
823
|
+
await this.#update(record.subagentId, (current) => {
|
|
824
|
+
if (current.intent === "stopped")
|
|
825
|
+
return current;
|
|
826
|
+
if (current.state !== "running")
|
|
827
|
+
return current;
|
|
828
|
+
return {
|
|
829
|
+
...current,
|
|
830
|
+
state: "blocked",
|
|
831
|
+
interrupted: "The child panel runtime did not reconnect; its process exit is not confirmed",
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (retired === undefined) {
|
|
837
|
+
await this.#update(record.subagentId, (current) => {
|
|
838
|
+
if (current.intent === "stopped")
|
|
839
|
+
return current;
|
|
840
|
+
if (current.state !== "running")
|
|
841
|
+
return current;
|
|
842
|
+
return {
|
|
843
|
+
...current,
|
|
844
|
+
state: "done",
|
|
845
|
+
interrupted: "The child runtime ended with the parent process; a later send starts it again from the same session",
|
|
846
|
+
};
|
|
847
|
+
});
|
|
753
848
|
return;
|
|
754
849
|
}
|
|
755
850
|
await this.#update(record.subagentId, (current) => {
|
|
@@ -779,6 +874,9 @@ export class SubagentManager {
|
|
|
779
874
|
throw new BridgeError("invalid_state", `Child ${id} did not confirm its current work state`);
|
|
780
875
|
}
|
|
781
876
|
const idle = state.idle && !state.pendingMessages;
|
|
877
|
+
if (typeof state.path === "string" && state.path !== "") {
|
|
878
|
+
this.#paths.set(id, state.path);
|
|
879
|
+
}
|
|
782
880
|
if ((this.#workVersions.get(id) ?? 0) === workVersion) {
|
|
783
881
|
if (idle)
|
|
784
882
|
this.#working.delete(id);
|
|
@@ -803,8 +901,9 @@ export class SubagentManager {
|
|
|
803
901
|
? current
|
|
804
902
|
: (({ unacknowledgedInput: _input, ...rest }) => rest)(current);
|
|
805
903
|
const hasError = snapshot.state === "error";
|
|
904
|
+
const { interrupted: _interrupted, ...withoutInterrupted } = base;
|
|
806
905
|
return {
|
|
807
|
-
...
|
|
906
|
+
...withoutInterrupted,
|
|
808
907
|
state: hasError ? "error" : idle ? "done" : "running",
|
|
809
908
|
...(snapshot.summary === undefined ? {} : { latestSummary: snapshot.summary }),
|
|
810
909
|
usage: snapshot.usage,
|
|
@@ -866,8 +965,10 @@ export class SubagentManager {
|
|
|
866
965
|
this.#working.add(childId);
|
|
867
966
|
this.#notify();
|
|
868
967
|
await withDeadline(this.#start(record), this.#deps.deadlineMs ?? 60_000);
|
|
968
|
+
let path;
|
|
869
969
|
try {
|
|
870
|
-
await this.#request(record.subagentId, "prompt", { message: cleanTask });
|
|
970
|
+
path = acknowledgedPath(await this.#request(record.subagentId, "prompt", { message: cleanTask }));
|
|
971
|
+
this.#paths.set(record.subagentId, path);
|
|
871
972
|
}
|
|
872
973
|
catch (error) {
|
|
873
974
|
let failureState = "error";
|
|
@@ -908,6 +1009,9 @@ export class SubagentManager {
|
|
|
908
1009
|
output: `Subagent ${record.subagentId} (${input.agent}) is running`,
|
|
909
1010
|
truncated: false,
|
|
910
1011
|
}),
|
|
1012
|
+
consume: () => {
|
|
1013
|
+
this.#deps.channel?.dismiss?.(record.subagentId);
|
|
1014
|
+
},
|
|
911
1015
|
}),
|
|
912
1016
|
});
|
|
913
1017
|
}
|
|
@@ -916,7 +1020,8 @@ export class SubagentManager {
|
|
|
916
1020
|
}
|
|
917
1021
|
}
|
|
918
1022
|
return {
|
|
919
|
-
child: publicChild((await this.#deps.registry.get(record.subagentId)) ?? record, true),
|
|
1023
|
+
child: publicChild((await this.#deps.registry.get(record.subagentId)) ?? record, true, path),
|
|
1024
|
+
path,
|
|
920
1025
|
...(placement.note === undefined ? {} : { note: placement.note }),
|
|
921
1026
|
};
|
|
922
1027
|
}
|
|
@@ -956,10 +1061,11 @@ export class SubagentManager {
|
|
|
956
1061
|
const record = await this.#deps.registry.get(id);
|
|
957
1062
|
if (record === undefined)
|
|
958
1063
|
return failure("send", "Unknown child", id);
|
|
959
|
-
if (record.intent === "stopped"
|
|
1064
|
+
if (record.intent === "stopped") {
|
|
960
1065
|
const doubt = await this.#runtimeDoubt(id);
|
|
961
|
-
|
|
962
|
-
|
|
1066
|
+
if (doubt !== undefined) {
|
|
1067
|
+
return failure("send", doubt, id, record.state, ["runtime metadata retained"], false);
|
|
1068
|
+
}
|
|
963
1069
|
}
|
|
964
1070
|
let isResumed = false;
|
|
965
1071
|
// Deliverability is the bridge connection, minus a process that left the delegated session:
|
|
@@ -984,6 +1090,7 @@ export class SubagentManager {
|
|
|
984
1090
|
const resumedRecord = await this.#update(id, (current) => ({
|
|
985
1091
|
...current,
|
|
986
1092
|
state: "running",
|
|
1093
|
+
intent: "active",
|
|
987
1094
|
persistence,
|
|
988
1095
|
...(sessionPath === undefined ? {} : { sessionPath }),
|
|
989
1096
|
launchConfig: sessionPath === undefined
|
|
@@ -996,6 +1103,31 @@ export class SubagentManager {
|
|
|
996
1103
|
try {
|
|
997
1104
|
await withDeadline(this.#start(resumedRecord), this.#deps.deadlineMs ?? 60_000);
|
|
998
1105
|
isResumed = true;
|
|
1106
|
+
if (this.#taskRegistry !== undefined) {
|
|
1107
|
+
try {
|
|
1108
|
+
this.#taskRegistry.create({
|
|
1109
|
+
id: resumedRecord.subagentId,
|
|
1110
|
+
type: "agent",
|
|
1111
|
+
purpose: `${resumedRecord.launchConfig.agent}: resumed task`,
|
|
1112
|
+
inlineResult: true,
|
|
1113
|
+
begin: () => ({
|
|
1114
|
+
stop: () => {
|
|
1115
|
+
void this.stop(resumedRecord.subagentId);
|
|
1116
|
+
},
|
|
1117
|
+
describe: () => ({
|
|
1118
|
+
output: `Subagent ${resumedRecord.subagentId} (${resumedRecord.launchConfig.agent}) is running`,
|
|
1119
|
+
truncated: false,
|
|
1120
|
+
}),
|
|
1121
|
+
consume: () => {
|
|
1122
|
+
this.#deps.channel?.dismiss?.(resumedRecord.subagentId);
|
|
1123
|
+
},
|
|
1124
|
+
}),
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
catch (regError) {
|
|
1128
|
+
diagnose(`failed to renew background task for subagent ${resumedRecord.subagentId}: ${errorMessage(regError)}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
999
1131
|
}
|
|
1000
1132
|
catch (error) {
|
|
1001
1133
|
const reason = errorMessage(error);
|
|
@@ -1042,23 +1174,38 @@ export class SubagentManager {
|
|
|
1042
1174
|
return {
|
|
1043
1175
|
...clean,
|
|
1044
1176
|
state: "running",
|
|
1177
|
+
intent: "active",
|
|
1045
1178
|
unacknowledgedInput: message,
|
|
1046
1179
|
};
|
|
1047
1180
|
});
|
|
1048
1181
|
try {
|
|
1049
|
-
await this.#request(id, operation, { message });
|
|
1182
|
+
const path = acknowledgedPath(await this.#request(id, operation, { message }));
|
|
1183
|
+
this.#paths.set(id, path);
|
|
1050
1184
|
}
|
|
1051
1185
|
catch (error) {
|
|
1052
1186
|
return failure("send", errorMessage(error), id, pending.state, ["input persisted as unacknowledged before dispatch"], false);
|
|
1053
1187
|
}
|
|
1054
|
-
return publicChild((await this.#deps.registry.get(id)) ??
|
|
1188
|
+
return publicChild((await this.#deps.registry.get(id)) ?? pending, true, this.#paths.get(id));
|
|
1055
1189
|
});
|
|
1056
1190
|
}
|
|
1057
1191
|
async get(id) {
|
|
1058
1192
|
const record = await this.#deps.registry.get(id);
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1193
|
+
if (record === undefined)
|
|
1194
|
+
return failure("get", "Unknown child", id);
|
|
1195
|
+
const live = this.#isLive(id);
|
|
1196
|
+
const projector = this.#projectors.get(id);
|
|
1197
|
+
if (projector !== undefined) {
|
|
1198
|
+
const snap = projector.snapshot();
|
|
1199
|
+
const mergedRecord = {
|
|
1200
|
+
...record,
|
|
1201
|
+
...(snap.activeTool !== undefined ? { activeTool: snap.activeTool } : {}),
|
|
1202
|
+
...(snap.summary !== undefined ? { latestSummary: snap.summary } : {}),
|
|
1203
|
+
...(snap.interrupted !== undefined ? { interrupted: snap.interrupted } : {}),
|
|
1204
|
+
usage: snap.usage ?? record.usage,
|
|
1205
|
+
};
|
|
1206
|
+
return publicChild(mergedRecord, live, this.#paths.get(id));
|
|
1207
|
+
}
|
|
1208
|
+
return publicChild(record, live, this.#paths.get(id));
|
|
1062
1209
|
}
|
|
1063
1210
|
async list() {
|
|
1064
1211
|
const records = await this.#deps.registry.list();
|
|
@@ -1092,7 +1239,6 @@ export class SubagentManager {
|
|
|
1092
1239
|
stopped = await this.#update(id, (current) => ({
|
|
1093
1240
|
...current,
|
|
1094
1241
|
intent: "stopped",
|
|
1095
|
-
state: "error",
|
|
1096
1242
|
}));
|
|
1097
1243
|
}
|
|
1098
1244
|
catch (error) {
|
|
@@ -1122,6 +1268,7 @@ export class SubagentManager {
|
|
|
1122
1268
|
// bridge-only stop.
|
|
1123
1269
|
const cleanup = await attachment.cleanup();
|
|
1124
1270
|
if (!(await this.#panelGone(attachment))) {
|
|
1271
|
+
await this.#update(id, (current) => ({ ...current, state: "error" })).catch(() => undefined);
|
|
1125
1272
|
return failure("stop", `The child's ${attachment.identity.host} panel is not confirmed gone (${cleanup.stderr || "its state could not be observed"}): the child process may still be running`, id, "error", ["stopped intent persisted", "runtime metadata retained"], true);
|
|
1126
1273
|
}
|
|
1127
1274
|
this.#attachments.delete(id);
|
|
@@ -1148,7 +1295,10 @@ export class SubagentManager {
|
|
|
1148
1295
|
if (current !== undefined) {
|
|
1149
1296
|
stopped = await this.#deps.registry.update(id, current.revision, (value) => {
|
|
1150
1297
|
const { runtime: _runtime, ...withoutRuntime } = value;
|
|
1151
|
-
return
|
|
1298
|
+
return {
|
|
1299
|
+
...withoutRuntime,
|
|
1300
|
+
state: "done",
|
|
1301
|
+
};
|
|
1152
1302
|
});
|
|
1153
1303
|
}
|
|
1154
1304
|
if (this.#taskRegistry !== undefined) {
|
|
@@ -1212,7 +1362,6 @@ export class SubagentManager {
|
|
|
1212
1362
|
}
|
|
1213
1363
|
await this.#update(id, (current) => ({
|
|
1214
1364
|
...current,
|
|
1215
|
-
intent: "stopped",
|
|
1216
1365
|
state: "error",
|
|
1217
1366
|
interrupted: message,
|
|
1218
1367
|
}));
|
|
@@ -1410,7 +1559,7 @@ export class SubagentManager {
|
|
|
1410
1559
|
return;
|
|
1411
1560
|
projector.applyEvent(event);
|
|
1412
1561
|
let confirmedInput;
|
|
1413
|
-
if (value.type === "agent_end") {
|
|
1562
|
+
if (value.type === "turn_end" || value.type === "agent_end") {
|
|
1414
1563
|
// A finished turn is exactly when the session file has the message, so this is where
|
|
1415
1564
|
// unacknowledged input becomes part of the child's conversation.
|
|
1416
1565
|
const entries = await this.#entries(id);
|
package/dist/registry.js
CHANGED
|
@@ -57,11 +57,10 @@ const LAUNCH_CONFIG_FIELDS = {
|
|
|
57
57
|
skills: true,
|
|
58
58
|
prompt: true,
|
|
59
59
|
bridgeExtensionPath: true,
|
|
60
|
-
interactive: true,
|
|
61
60
|
title: true,
|
|
62
61
|
task: true,
|
|
63
|
-
referencedSessionPath: true,
|
|
64
62
|
codemodeOnly: true,
|
|
63
|
+
mcp: true,
|
|
65
64
|
};
|
|
66
65
|
const AGENT_FIELDS = {
|
|
67
66
|
name: true,
|
|
@@ -221,6 +220,23 @@ function parseRuntime(value, path) {
|
|
|
221
220
|
const endpoint = expectString(raw.endpoint, "runtime.endpoint", path);
|
|
222
221
|
return Object.freeze({ runtimeIdentity, endpoint });
|
|
223
222
|
}
|
|
223
|
+
function parseMcpSelection(value, path) {
|
|
224
|
+
if (value === undefined)
|
|
225
|
+
return Object.freeze({ enabled: true });
|
|
226
|
+
const raw = expectObject(value, "launchConfig.mcp", path);
|
|
227
|
+
const enabled = expectOptionalBoolean(raw.enabled, "launchConfig.mcp.enabled", path, true);
|
|
228
|
+
const servers = raw.servers === undefined
|
|
229
|
+
? undefined
|
|
230
|
+
: Object.freeze(expectStringArray(raw.servers, "launchConfig.mcp.servers", path));
|
|
231
|
+
const excluded = raw.excluded === undefined
|
|
232
|
+
? undefined
|
|
233
|
+
: Object.freeze(expectStringArray(raw.excluded, "launchConfig.mcp.excluded", path));
|
|
234
|
+
return Object.freeze({
|
|
235
|
+
enabled,
|
|
236
|
+
...(servers === undefined ? {} : { servers }),
|
|
237
|
+
...(excluded === undefined ? {} : { excluded }),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
224
240
|
function parseLaunchConfig(value, path) {
|
|
225
241
|
const raw = expectObject(value, "launchConfig", path);
|
|
226
242
|
expectKeys(raw, LAUNCH_CONFIG_FIELDS, "launchConfig", path);
|
|
@@ -228,7 +244,6 @@ function parseLaunchConfig(value, path) {
|
|
|
228
244
|
if (!isSessionId(sessionId))
|
|
229
245
|
throw invalid(path, "launchConfig.sessionId is not a valid session id");
|
|
230
246
|
const sessionPath = expectOptionalString(raw.sessionPath, "launchConfig.sessionPath", path);
|
|
231
|
-
const referencedSessionPath = expectOptionalString(raw.referencedSessionPath, "launchConfig.referencedSessionPath", path);
|
|
232
247
|
const extensions = parseSelection(raw.extensions, path, "extensions");
|
|
233
248
|
const bridgeExtensionPath = expectString(raw.bridgeExtensionPath, "launchConfig.bridgeExtensionPath", path);
|
|
234
249
|
if (!extensions.paths.includes(bridgeExtensionPath)) {
|
|
@@ -245,7 +260,6 @@ function parseLaunchConfig(value, path) {
|
|
|
245
260
|
sessionId,
|
|
246
261
|
sessionDir: expectString(raw.sessionDir, "launchConfig.sessionDir", path),
|
|
247
262
|
...(sessionPath === undefined ? {} : { sessionPath }),
|
|
248
|
-
...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
|
|
249
263
|
agent: parseAgent(raw.agent, path),
|
|
250
264
|
model: parseModel(raw.model, path),
|
|
251
265
|
thinking: parseThinking(raw.thinking, path),
|
|
@@ -253,9 +267,9 @@ function parseLaunchConfig(value, path) {
|
|
|
253
267
|
excludeTools: Object.freeze(expectStringArray(raw.excludeTools, "launchConfig.excludeTools", path)),
|
|
254
268
|
extensions,
|
|
255
269
|
skills: parseSelection(raw.skills, path, "skills"),
|
|
270
|
+
mcp: parseMcpSelection(raw.mcp, path),
|
|
256
271
|
prompt: expectString(raw.prompt, "launchConfig.prompt", path),
|
|
257
272
|
bridgeExtensionPath,
|
|
258
|
-
interactive: expectOptionalBoolean(raw.interactive, "launchConfig.interactive", path, false),
|
|
259
273
|
...(expectOptionalBoolean(raw.codemodeOnly, "launchConfig.codemodeOnly", path, false)
|
|
260
274
|
? { codemodeOnly: true }
|
|
261
275
|
: {}),
|
package/dist/rpc-events.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* The Pi events a child forwards to its parent. Everything else — streaming token noise,
|
|
3
3
|
* undocumented internals — is dropped, so the parent's projection only sees turn-level facts.
|
|
4
4
|
*/
|
|
5
|
-
export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "
|
|
5
|
+
export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_end", "auto_retry", "auto_retry_start", "auto_retry_end", "message_update", "agent_settled"];
|
|
6
6
|
export declare function shouldForwardPiEvent(event: unknown): boolean;
|
package/dist/rpc-events.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ModelRegistryLike, ParentAgentDefaults, SkillCatalogEntry } from "./agent-resolver.js";
|
|
2
2
|
import type { EffectiveLaunchConfig, PersistenceState, PiInvocation, Presentation, SpawnSubagentInput, SubagentRecord } from "./domain.js";
|
|
3
3
|
import type { SubagentRegistry } from "./registry.js";
|
|
4
|
+
/** Reads all JSON entries from a session file on disk. */
|
|
5
|
+
export declare function readSessionJsonlEntries(sessionPath: string): Promise<unknown[]>;
|
|
4
6
|
export interface SessionPlacement {
|
|
5
7
|
readonly sessionId: string;
|
|
6
8
|
readonly sessionDir: string;
|
|
@@ -62,7 +64,6 @@ export interface ResolveSubagentLaunchOptions {
|
|
|
62
64
|
readonly existingSubagentIds?: readonly string[];
|
|
63
65
|
readonly enforceEnabled?: boolean;
|
|
64
66
|
readonly sessionPath?: string;
|
|
65
|
-
readonly referencedSessionPath?: string;
|
|
66
67
|
}
|
|
67
68
|
/**
|
|
68
69
|
* One resolution chain for a spawn: agent definition + explicit parent defaults
|