@ian-pascoe/pi-minimal-subagents 0.6.5 → 0.7.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 +33 -9
- package/package.json +1 -1
- package/src/minimal-subagents-config.ts +18 -14
- package/src/minimal-subagents-context.ts +19 -86
- package/src/minimal-subagents-coordinator.ts +9 -1
- package/src/minimal-subagents-extension.ts +89 -0
- package/src/minimal-subagents-fork-lifecycle.ts +1 -1
- package/src/minimal-subagents-registry-wire.ts +0 -4
- package/src/minimal-subagents-registry.ts +2 -6
- package/src/minimal-subagents-render-contract.ts +5 -9
- package/src/minimal-subagents-rendering.ts +18 -0
- package/src/minimal-subagents-sessions.ts +83 -7
- package/src/minimal-subagents-settings-writer.ts +9 -8
- package/src/minimal-subagents-status-panel.ts +308 -103
- package/src/minimal-subagents-types.ts +6 -4
- package/src/minimal-subagents-ui.ts +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { unlink } from "node:fs/promises";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
findCutPoint,
|
|
13
13
|
generateSummaryWithUsage,
|
|
14
14
|
ModelRuntime,
|
|
15
|
+
parseSessionEntries,
|
|
15
16
|
SessionManager,
|
|
16
17
|
SettingsManager,
|
|
17
18
|
sessionEntryToContextMessages,
|
|
@@ -362,6 +363,9 @@ export function verifyChildSessionIdentity(
|
|
|
362
363
|
`Minimal subagents session identity mismatch: session ID for ${agent.agent_id}`,
|
|
363
364
|
);
|
|
364
365
|
}
|
|
366
|
+
if (agent.session_leaf_id && !sessionManager.getEntry(agent.session_leaf_id)) {
|
|
367
|
+
throw new Error(`Minimal subagents session identity mismatch: leaf for ${agent.agent_id}`);
|
|
368
|
+
}
|
|
365
369
|
const identityBranch = sessionManager.getBranch(agent.session_leaf_id);
|
|
366
370
|
const generation = findLatestForkGeneration(identityBranch);
|
|
367
371
|
const identity =
|
|
@@ -401,9 +405,6 @@ export function verifyChildSessionIdentity(
|
|
|
401
405
|
);
|
|
402
406
|
}
|
|
403
407
|
}
|
|
404
|
-
if (agent.session_leaf_id && !sessionManager.getEntry(agent.session_leaf_id)) {
|
|
405
|
-
throw new Error(`Minimal subagents session identity mismatch: leaf for ${agent.agent_id}`);
|
|
406
|
-
}
|
|
407
408
|
}
|
|
408
409
|
|
|
409
410
|
/** Find durable keyed evidence for exactly-once wait or custom-result delivery. */
|
|
@@ -532,6 +533,10 @@ export async function captureChildTurnOutcome(
|
|
|
532
533
|
class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
533
534
|
private aborted = false;
|
|
534
535
|
private readonly unsubscribe: () => void;
|
|
536
|
+
private transcriptLeafId: string | null | undefined;
|
|
537
|
+
private readonly transcriptEntries = new WeakMap<SessionEntry, AgentMessage[]>();
|
|
538
|
+
private transcriptMessages: AgentMessage[] = [];
|
|
539
|
+
private transcriptSources = new Set<AgentMessage>();
|
|
535
540
|
|
|
536
541
|
constructor(
|
|
537
542
|
private readonly session: AgentSession,
|
|
@@ -629,10 +634,46 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
629
634
|
}
|
|
630
635
|
|
|
631
636
|
snapshotActivityTranscript(): ChildAgentTranscriptSnapshot {
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
637
|
+
const manager = this.session.sessionManager;
|
|
638
|
+
const leafId = manager.getLeafId();
|
|
639
|
+
if (this.transcriptLeafId !== leafId) {
|
|
640
|
+
this.transcriptSources = new Set();
|
|
641
|
+
this.transcriptMessages = manager.getBranch().flatMap((entry) => {
|
|
642
|
+
const source = sessionEntryToContextMessages(entry);
|
|
643
|
+
for (const message of source) this.transcriptSources.add(message);
|
|
644
|
+
let messages = this.transcriptEntries.get(entry);
|
|
645
|
+
if (!messages) {
|
|
646
|
+
messages = selectChildAgentTranscript(source).messages;
|
|
647
|
+
this.transcriptEntries.set(entry, messages);
|
|
648
|
+
}
|
|
649
|
+
return messages;
|
|
650
|
+
});
|
|
651
|
+
this.transcriptLeafId = leafId;
|
|
652
|
+
}
|
|
653
|
+
const state = this.session.state;
|
|
654
|
+
// Native message_end finalizes agent state before async extension handlers persist it.
|
|
655
|
+
const pending = state.messages.filter(
|
|
656
|
+
(message) =>
|
|
657
|
+
(message.role === "user" ||
|
|
658
|
+
message.role === "assistant" ||
|
|
659
|
+
message.role === "toolResult") &&
|
|
660
|
+
!this.transcriptSources.has(message),
|
|
661
|
+
);
|
|
662
|
+
const streaming = state.streamingMessage;
|
|
663
|
+
const tail = selectChildAgentTranscript(
|
|
664
|
+
pending,
|
|
665
|
+
streaming && !this.transcriptSources.has(streaming) ? streaming : undefined,
|
|
635
666
|
);
|
|
667
|
+
const snapshot: ChildAgentTranscriptSnapshot = {
|
|
668
|
+
messages: tail.messages.length
|
|
669
|
+
? [...this.transcriptMessages, ...tail.messages]
|
|
670
|
+
: this.transcriptMessages,
|
|
671
|
+
streamingAssistantIndex:
|
|
672
|
+
tail.streamingAssistantIndex === undefined
|
|
673
|
+
? undefined
|
|
674
|
+
: this.transcriptMessages.length + tail.streamingAssistantIndex,
|
|
675
|
+
toolDefinitions: [],
|
|
676
|
+
};
|
|
636
677
|
const toolNames = new Set<string>();
|
|
637
678
|
for (const message of snapshot.messages) {
|
|
638
679
|
if (message.role !== "assistant") continue;
|
|
@@ -740,6 +781,7 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
740
781
|
private readonly availableToolNames: Set<string>;
|
|
741
782
|
private readonly discoveredToolNames = new Map<string, Promise<Set<string>>>();
|
|
742
783
|
private readonly sessionFileTrash: SessionFileTrashCapability;
|
|
784
|
+
private savedTranscript?: { key: string; snapshot: ChildAgentTranscriptSnapshot };
|
|
743
785
|
|
|
744
786
|
constructor(private readonly options: PiAgentSessionFactoryOptions) {
|
|
745
787
|
this.modelById = new Map(
|
|
@@ -763,6 +805,40 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
|
|
|
763
805
|
});
|
|
764
806
|
}
|
|
765
807
|
|
|
808
|
+
/** Read one verified saved Child Session Position without opening a writable runtime. */
|
|
809
|
+
readTranscript(agent: PersistedAgent): ChildAgentTranscriptSnapshot {
|
|
810
|
+
if (!agent.session_file || !agent.session_id || !agent.session_leaf_id) {
|
|
811
|
+
throw new Error(`Child Session Position is unavailable for ${agent.agent_id}.`);
|
|
812
|
+
}
|
|
813
|
+
const sessionFile = canonicalPath(agent.session_file);
|
|
814
|
+
const stat = statSync(sessionFile);
|
|
815
|
+
const key = JSON.stringify([
|
|
816
|
+
sessionFile,
|
|
817
|
+
agent.agent_id,
|
|
818
|
+
agent.parent_id,
|
|
819
|
+
agent.created_at,
|
|
820
|
+
agent.session_id,
|
|
821
|
+
agent.session_leaf_id,
|
|
822
|
+
stat.dev,
|
|
823
|
+
stat.ino,
|
|
824
|
+
stat.size,
|
|
825
|
+
stat.mtimeMs,
|
|
826
|
+
stat.ctimeMs,
|
|
827
|
+
]);
|
|
828
|
+
if (this.savedTranscript?.key === key) return this.savedTranscript.snapshot;
|
|
829
|
+
const entries = parseSessionEntries(readFileSync(sessionFile, "utf8"));
|
|
830
|
+
if (entries[0]?.type !== "session")
|
|
831
|
+
throw new Error(`Invalid child session file: ${sessionFile}`);
|
|
832
|
+
// SessionManager.open can migrate/rewrite files; an in-memory reader cannot write them.
|
|
833
|
+
const manager = SessionManager.inMemory(this.options.cwd, undefined, entries);
|
|
834
|
+
verifyChildSessionIdentity(manager, agent, this.options.rootSessionId);
|
|
835
|
+
const snapshot = selectChildAgentTranscript(
|
|
836
|
+
manager.getBranch(agent.session_leaf_id).flatMap(sessionEntryToContextMessages),
|
|
837
|
+
);
|
|
838
|
+
this.savedTranscript = { key, snapshot };
|
|
839
|
+
return snapshot;
|
|
840
|
+
}
|
|
841
|
+
|
|
766
842
|
resolveLaunchMissingDependencies(agent: PersistedAgent): Promise<string[]> {
|
|
767
843
|
return this.findMissingDependencies(agent, false);
|
|
768
844
|
}
|
|
@@ -4,8 +4,6 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import lockfile from "proper-lockfile";
|
|
7
|
-
import { Type } from "typebox";
|
|
8
|
-
import { Value } from "typebox/value";
|
|
9
7
|
|
|
10
8
|
/** Identifies the standard Pi settings file changed by a Subagent Access command. */
|
|
11
9
|
export type MinimalSubagentsSettingsScope = "global" | "project";
|
|
@@ -64,8 +62,10 @@ interface ExistingSettingsDocument {
|
|
|
64
62
|
readonly mode: number;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
function isSettingsJsonObject(value: JsonValue | undefined): value is SettingsJsonObject {
|
|
66
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- JSON.parse already established JSON data; distinguish object roots and settings blocks from primitives and arrays.
|
|
67
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
68
|
+
}
|
|
69
69
|
const SETTINGS_LOCK_RETRY_DELAY_MS = 20;
|
|
70
70
|
const SETTINGS_LOCK_RETRIES = 100;
|
|
71
71
|
const NEW_SETTINGS_FILE_MODE = 0o600;
|
|
@@ -92,7 +92,8 @@ function parseSettingsDocument(
|
|
|
92
92
|
scope: MinimalSubagentsSettingsScope,
|
|
93
93
|
path: string,
|
|
94
94
|
): ParsedSettingsDocument {
|
|
95
|
-
|
|
95
|
+
// JSON.parse is the provenance for this JSON type; the object shape is checked below.
|
|
96
|
+
let parsed: JsonValue;
|
|
96
97
|
try {
|
|
97
98
|
parsed = JSON.parse(stripUtf8Bom(content));
|
|
98
99
|
} catch (cause) {
|
|
@@ -109,11 +110,11 @@ function parseSettingsDocument(
|
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
if (!
|
|
113
|
+
if (!isSettingsJsonObject(parsed)) {
|
|
113
114
|
return { ok: false, error: settingsContractError(scope, path, "expected an object root") };
|
|
114
115
|
}
|
|
115
116
|
const minimalSubagents = parsed.minimalSubagents;
|
|
116
|
-
if (minimalSubagents !== undefined && !
|
|
117
|
+
if (minimalSubagents !== undefined && !isSettingsJsonObject(minimalSubagents)) {
|
|
117
118
|
return {
|
|
118
119
|
ok: false,
|
|
119
120
|
error: settingsContractError(
|
|
@@ -131,7 +132,7 @@ function mutateMinimalSubagentsEnabled(
|
|
|
131
132
|
enabled: boolean | undefined,
|
|
132
133
|
): void {
|
|
133
134
|
const currentMinimalSubagents = settings.minimalSubagents;
|
|
134
|
-
const minimalSubagents =
|
|
135
|
+
const minimalSubagents = isSettingsJsonObject(currentMinimalSubagents)
|
|
135
136
|
? currentMinimalSubagents
|
|
136
137
|
: {};
|
|
137
138
|
|