pi-sessions 0.9.0 → 0.10.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 +4 -0
- package/dist/session-messaging/broker/process.js +234 -0
- package/dist/shared/session-broker/framing.js +58 -0
- package/dist/shared/session-broker/protocol.js +163 -0
- package/dist/shared/session-broker/socket-path.js +19 -0
- package/dist/shared/typebox.js +36 -0
- package/extensions/session-handoff/board-view-model.ts +2 -4
- package/extensions/session-handoff/bootstrap.ts +14 -16
- package/extensions/session-handoff/extract.ts +34 -16
- package/extensions/session-handoff/install.ts +1 -1
- package/extensions/session-handoff/launch-target.ts +1 -1
- package/extensions/session-handoff/metadata.ts +72 -23
- package/extensions/session-messaging/broker/spawn.ts +12 -7
- package/extensions/shared/settings.ts +12 -7
- package/extensions/subagents/classify.ts +6 -0
- package/extensions/subagents/install.ts +22 -137
- package/extensions/subagents/settle.ts +182 -0
- package/package.json +7 -3
|
@@ -24,8 +24,9 @@ import {
|
|
|
24
24
|
} from "@earendil-works/pi-coding-agent";
|
|
25
25
|
import { type Static, Type } from "typebox";
|
|
26
26
|
import { freshenModel } from "../shared/model-runtime.ts";
|
|
27
|
-
import { getDefaultHandoffRunsDir } from "../shared/settings.ts";
|
|
27
|
+
import { getDefaultHandoffRunsDir, type HandoffSettings } from "../shared/settings.ts";
|
|
28
28
|
import { parseTypeBoxValue } from "../shared/typebox.ts";
|
|
29
|
+
import { resolveModelOverride } from "./model.ts";
|
|
29
30
|
|
|
30
31
|
const MAX_HANDOFF_EXTRACTION_ATTEMPTS = 3;
|
|
31
32
|
const HANDOFF_EXTRACTION_RETRY_PROMPT =
|
|
@@ -88,22 +89,39 @@ export function resolveHandoffSource(
|
|
|
88
89
|
return sessionContext;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
export async function generateHandoffDraftFromSessionManager(
|
|
92
|
-
ctx
|
|
93
|
-
modelRuntime
|
|
94
|
-
sourceSessionManager
|
|
95
|
-
sourceLeafId
|
|
96
|
-
goal
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
signal
|
|
92
|
+
export async function generateHandoffDraftFromSessionManager({
|
|
93
|
+
ctx,
|
|
94
|
+
modelRuntime,
|
|
95
|
+
sourceSessionManager,
|
|
96
|
+
sourceLeafId,
|
|
97
|
+
goal,
|
|
98
|
+
settings,
|
|
99
|
+
destinationThinkingLevel,
|
|
100
|
+
signal,
|
|
100
101
|
requestResponse = false,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
}: {
|
|
103
|
+
ctx: ExtensionContext;
|
|
104
|
+
modelRuntime: ModelRuntime;
|
|
105
|
+
sourceSessionManager: ExtensionContext["sessionManager"];
|
|
106
|
+
sourceLeafId: string;
|
|
107
|
+
goal: string;
|
|
108
|
+
settings: HandoffSettings;
|
|
109
|
+
destinationThinkingLevel: ThinkingLevel | undefined;
|
|
110
|
+
signal?: AbortSignal | undefined;
|
|
111
|
+
requestResponse?: boolean | undefined;
|
|
112
|
+
}): Promise<HandoffDraftResult | undefined> {
|
|
113
|
+
const modelOverride = settings.model
|
|
114
|
+
? resolveModelOverride(modelRuntime, settings.model, settings.thinkingLevel)
|
|
115
|
+
: undefined;
|
|
116
|
+
let model = modelOverride?.model;
|
|
117
|
+
if (!model) {
|
|
118
|
+
if (!ctx.model) {
|
|
119
|
+
throw new Error("No model is available for handoff.");
|
|
120
|
+
}
|
|
121
|
+
model = freshenModel(modelRuntime, ctx.model);
|
|
104
122
|
}
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
const thinkingLevel =
|
|
124
|
+
modelOverride?.thinkingLevel ?? settings.thinkingLevel ?? destinationThinkingLevel;
|
|
107
125
|
const sessionContext = resolveHandoffSource(sourceSessionManager, sourceLeafId);
|
|
108
126
|
|
|
109
127
|
const conversationText = serializeConversation(convertToLlm(sessionContext.messages));
|
|
@@ -114,7 +132,7 @@ export async function generateHandoffDraftFromSessionManager(
|
|
|
114
132
|
conversationText,
|
|
115
133
|
goal,
|
|
116
134
|
thinkingLevel,
|
|
117
|
-
persistRuns,
|
|
135
|
+
settings.persistRuns,
|
|
118
136
|
signal,
|
|
119
137
|
);
|
|
120
138
|
if (!handoffContext) {
|
|
@@ -59,7 +59,7 @@ export interface HandoffLaunchTarget {
|
|
|
59
59
|
description?: string | undefined;
|
|
60
60
|
requestResponseDefault: boolean;
|
|
61
61
|
bootstrapMode: "review" | "automatic";
|
|
62
|
-
/** Subagent launches stamp their child's identity into the
|
|
62
|
+
/** Subagent launches stamp their child's identity into the child bootstrap. */
|
|
63
63
|
describeSubagentChild?(input: {
|
|
64
64
|
childSessionId: string;
|
|
65
65
|
ownerSessionId: string;
|
|
@@ -3,6 +3,7 @@ import { type Static, Type } from "typebox";
|
|
|
3
3
|
import { safeParseTypeBoxValue } from "../shared/typebox.ts";
|
|
4
4
|
import { HANDOFF_KICKOFF_CUSTOM_TYPE, HANDOFF_KICKOFF_DETAILS_SCHEMA } from "./kickoff.ts";
|
|
5
5
|
import {
|
|
6
|
+
HANDOFF_LAUNCH_VALUE_SCHEMA,
|
|
6
7
|
HANDOFF_NON_SUBAGENT_LAUNCH_SCHEMA,
|
|
7
8
|
type HandoffLaunchValue,
|
|
8
9
|
SUBAGENT_LAUNCH,
|
|
@@ -16,7 +17,7 @@ export const HANDOFF_STALE_SESSION_MESSAGE =
|
|
|
16
17
|
export const SESSION_STARTING_MESSAGE =
|
|
17
18
|
"Target session is still starting. wait for it to show up in session_search, then resend.";
|
|
18
19
|
|
|
19
|
-
// A subagent's self-knowledge, stamped into
|
|
20
|
+
// A subagent's self-knowledge, stamped into its child-local bootstrap so the
|
|
20
21
|
// child never opens the parent transcript to learn who it is.
|
|
21
22
|
export const HANDOFF_SUBAGENT_SCHEMA = Type.Object({
|
|
22
23
|
childSessionId: Type.String(),
|
|
@@ -32,18 +33,10 @@ const HANDOFF_METADATA_BASE = {
|
|
|
32
33
|
initial_prompt: Type.String(),
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
export const HANDOFF_SESSION_METADATA_SCHEMA = Type.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
subagent: HANDOFF_SUBAGENT_SCHEMA,
|
|
40
|
-
}),
|
|
41
|
-
Type.Object({
|
|
42
|
-
...HANDOFF_METADATA_BASE,
|
|
43
|
-
launch: HANDOFF_NON_SUBAGENT_LAUNCH_SCHEMA,
|
|
44
|
-
subagent: Type.Optional(Type.Never()),
|
|
45
|
-
}),
|
|
46
|
-
]);
|
|
36
|
+
export const HANDOFF_SESSION_METADATA_SCHEMA = Type.Object({
|
|
37
|
+
...HANDOFF_METADATA_BASE,
|
|
38
|
+
launch: HANDOFF_LAUNCH_VALUE_SCHEMA,
|
|
39
|
+
});
|
|
47
40
|
|
|
48
41
|
const HANDOFF_BOOTSTRAP_BASE = {
|
|
49
42
|
mode: Type.Literal("generate"),
|
|
@@ -91,21 +84,14 @@ export function createHandoffSessionMetadata(
|
|
|
91
84
|
initialPrompt: string,
|
|
92
85
|
title: string,
|
|
93
86
|
launch: HandoffLaunchValue,
|
|
94
|
-
subagent: HandoffSubagent | undefined,
|
|
95
87
|
): HandoffSessionMetadata {
|
|
96
|
-
|
|
97
|
-
origin: "handoff"
|
|
88
|
+
return {
|
|
89
|
+
origin: "handoff",
|
|
98
90
|
goal: goal.trim(),
|
|
99
91
|
title,
|
|
100
92
|
initial_prompt: initialPrompt.trim(),
|
|
93
|
+
launch,
|
|
101
94
|
};
|
|
102
|
-
if (launch === SUBAGENT_LAUNCH) {
|
|
103
|
-
if (!subagent) {
|
|
104
|
-
throw new Error("A subagent handoff requires a subagent identity block.");
|
|
105
|
-
}
|
|
106
|
-
return { ...base, launch, subagent };
|
|
107
|
-
}
|
|
108
|
-
return { ...base, launch };
|
|
109
95
|
}
|
|
110
96
|
|
|
111
97
|
export function createChildGeneratedHandoffBootstrap(options: {
|
|
@@ -195,6 +181,69 @@ export function isSessionStarting(branch: readonly SessionEntry[]): boolean {
|
|
|
195
181
|
return findPendingHandoffBootstrap(branch)?.kind === "pending";
|
|
196
182
|
}
|
|
197
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Folds the bootstrap lifecycle in one transcript pass. The bootstrap owns its
|
|
186
|
+
* data; kickoff and consumed entries only establish whether it started or ended.
|
|
187
|
+
*/
|
|
188
|
+
export function findCurrentHandoffBootstrap(
|
|
189
|
+
branch: readonly SessionEntry[],
|
|
190
|
+
): HandoffBootstrap | undefined {
|
|
191
|
+
const bootstrapsById = new Map<string, HandoffBootstrap>();
|
|
192
|
+
const bootstrapIds: string[] = [];
|
|
193
|
+
const startedBootstrapIds: string[] = [];
|
|
194
|
+
const endedBootstrapIds = new Set<string>();
|
|
195
|
+
let conversationStarted = false;
|
|
196
|
+
|
|
197
|
+
for (const entry of branch) {
|
|
198
|
+
if (entry.type === "message" && entry.message.role === "user") {
|
|
199
|
+
conversationStarted = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (entry.type === "custom_message" && entry.customType === HANDOFF_KICKOFF_CUSTOM_TYPE) {
|
|
203
|
+
conversationStarted = true;
|
|
204
|
+
const details = safeParseTypeBoxValue(HANDOFF_KICKOFF_DETAILS_SCHEMA, entry.details);
|
|
205
|
+
if (details?.bootstrapEntryId) {
|
|
206
|
+
startedBootstrapIds.push(details.bootstrapEntryId);
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (entry.type !== "custom") {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (entry.customType === HANDOFF_BOOTSTRAP_PENDING_CUSTOM_TYPE) {
|
|
214
|
+
const bootstrap = safeParseTypeBoxValue(HANDOFF_BOOTSTRAP_SCHEMA, entry.data);
|
|
215
|
+
if (bootstrap) {
|
|
216
|
+
bootstrapsById.set(entry.id, bootstrap);
|
|
217
|
+
bootstrapIds.push(entry.id);
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (entry.customType === HANDOFF_BOOTSTRAP_CONSUMED_CUSTOM_TYPE) {
|
|
222
|
+
const consumed = safeParseTypeBoxValue(HANDOFF_BOOTSTRAP_CONSUMED_SCHEMA, entry.data);
|
|
223
|
+
if (consumed) {
|
|
224
|
+
endedBootstrapIds.add(consumed.bootstrapEntryId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (let index = startedBootstrapIds.length - 1; index >= 0; index -= 1) {
|
|
230
|
+
const bootstrap = bootstrapsById.get(startedBootstrapIds[index] ?? "");
|
|
231
|
+
if (bootstrap) {
|
|
232
|
+
return bootstrap;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (conversationStarted) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
for (let index = bootstrapIds.length - 1; index >= 0; index -= 1) {
|
|
239
|
+
const bootstrapId = bootstrapIds[index];
|
|
240
|
+
if (bootstrapId && !endedBootstrapIds.has(bootstrapId)) {
|
|
241
|
+
return bootstrapsById.get(bootstrapId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
198
247
|
export function parseHandoffSessionMetadata(value: unknown): HandoffSessionMetadata | undefined {
|
|
199
248
|
return safeParseTypeBoxValue(HANDOFF_SESSION_METADATA_SCHEMA, value);
|
|
200
249
|
}
|
|
@@ -34,7 +34,15 @@ export async function spawnSessionMessagingBrokerIfNeeded(): Promise<void> {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const brokerPath = join(
|
|
37
|
+
const brokerPath = join(
|
|
38
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
39
|
+
"../../../dist/session-messaging/broker/process.js",
|
|
40
|
+
);
|
|
41
|
+
if (!existsSync(brokerPath)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Session messaging broker build not found: ${brokerPath}. Run npm install in the pi-sessions package directory.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
38
46
|
const child = spawn(process.execPath, [brokerPath], {
|
|
39
47
|
detached: true,
|
|
40
48
|
stdio: "ignore",
|
|
@@ -164,15 +172,12 @@ function releaseSpawnLock(): void {
|
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
function brokerSpawnEnv(): NodeJS.ProcessEnv {
|
|
167
|
-
|
|
168
|
-
// broker is launched as `node process.ts`. Redundant while stdio is ignored,
|
|
169
|
-
// but cheap insurance if that changes or an older Node revives the warning.
|
|
170
|
-
const env: NodeJS.ProcessEnv = { ...process.env, NODE_NO_WARNINGS: "1" };
|
|
175
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
171
176
|
// process.execPath is the Pi executable. As a Bun-compiled binary it treats a
|
|
172
177
|
// positional path as a file argument to the agent, not a script to run, which
|
|
173
178
|
// would relaunch a full agent session and recurse. BUN_BE_BUN makes it behave
|
|
174
|
-
// as the Bun CLI and execute
|
|
175
|
-
//
|
|
179
|
+
// as the Bun CLI and execute the broker. It is set whenever Bun is the runtime
|
|
180
|
+
// (compiled binary or `bun run`) and ignored by a plain Node launch.
|
|
176
181
|
if (process.versions.bun) {
|
|
177
182
|
env.BUN_BE_BUN = "1";
|
|
178
183
|
}
|
|
@@ -27,6 +27,8 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
|
27
27
|
enable: Type.Optional(Type.Boolean()),
|
|
28
28
|
pickerShortcut: Type.Optional(Type.String()),
|
|
29
29
|
persistRuns: Type.Optional(Type.Boolean()),
|
|
30
|
+
model: Type.Optional(Type.String()),
|
|
31
|
+
thinkingLevel: Type.Optional(Type.String()),
|
|
30
32
|
deferred: Type.Optional(
|
|
31
33
|
Type.Object({
|
|
32
34
|
copyToClipboard: Type.Optional(Type.Boolean()),
|
|
@@ -87,6 +89,14 @@ export interface AskSettings extends AgentModelSettings {
|
|
|
87
89
|
persistRuns: boolean;
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
export interface HandoffSettings extends AgentModelSettings {
|
|
93
|
+
pickerShortcut: KeyId;
|
|
94
|
+
persistRuns: boolean;
|
|
95
|
+
deferred: {
|
|
96
|
+
copyToClipboard: boolean;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
export interface FeatureToggles {
|
|
91
101
|
messaging: boolean;
|
|
92
102
|
subagents: boolean;
|
|
@@ -102,13 +112,7 @@ export interface SessionSettings {
|
|
|
102
112
|
subagents: {
|
|
103
113
|
maxDepth: number;
|
|
104
114
|
};
|
|
105
|
-
handoff:
|
|
106
|
-
pickerShortcut: KeyId;
|
|
107
|
-
persistRuns: boolean;
|
|
108
|
-
deferred: {
|
|
109
|
-
copyToClipboard: boolean;
|
|
110
|
-
};
|
|
111
|
-
};
|
|
115
|
+
handoff: HandoffSettings;
|
|
112
116
|
index: {
|
|
113
117
|
path: string;
|
|
114
118
|
};
|
|
@@ -222,6 +226,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
|
|
|
222
226
|
maxDepth: fileSettings.subagents?.maxDepth ?? 2,
|
|
223
227
|
},
|
|
224
228
|
handoff: {
|
|
229
|
+
...resolveAgentModelSettings(fileSettings.handoff),
|
|
225
230
|
pickerShortcut: normalizePickerShortcut(fileSettings.handoff?.pickerShortcut),
|
|
226
231
|
persistRuns: fileSettings.handoff?.persistRuns ?? false,
|
|
227
232
|
deferred: {
|
|
@@ -9,6 +9,8 @@ export type SubagentState =
|
|
|
9
9
|
| "interrupted"
|
|
10
10
|
| "unknown";
|
|
11
11
|
|
|
12
|
+
const RUNNING_SUBAGENT_STATES: ReadonlySet<SubagentState> = new Set(["starting", "busy", "active"]);
|
|
13
|
+
|
|
12
14
|
export interface SubagentEvidence {
|
|
13
15
|
hasWindow: boolean;
|
|
14
16
|
brokerLive: boolean;
|
|
@@ -20,6 +22,10 @@ export interface SubagentEvidence {
|
|
|
20
22
|
childReadable: boolean;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
export function isRunningSubagentState(state: SubagentState): boolean {
|
|
26
|
+
return RUNNING_SUBAGENT_STATES.has(state);
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export function classifySubagent(evidence: SubagentEvidence): SubagentState {
|
|
24
30
|
if (!evidence.childReadable) {
|
|
25
31
|
return "unknown";
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
getHandoffMetadataFromEntries,
|
|
5
|
-
type HandoffSubagent,
|
|
6
|
-
} from "../session-handoff/metadata.ts";
|
|
1
|
+
import type { ExtensionAPI, SessionTreeNode } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { HandoffLaunchTarget } from "../session-handoff/launch-target.ts";
|
|
7
3
|
import type {
|
|
8
4
|
MessagingHandle,
|
|
9
5
|
SendMessageRequest,
|
|
@@ -11,17 +7,14 @@ import type {
|
|
|
11
7
|
} from "../session-messaging/install.ts";
|
|
12
8
|
import type { SessionLifecycle } from "../shared/composition.ts";
|
|
13
9
|
import type { SessionSettings } from "../shared/settings.ts";
|
|
14
|
-
import {
|
|
10
|
+
import { isTmuxInstalled } from "../shared/tmux.ts";
|
|
15
11
|
import { SubagentCancellationRouter, type SubagentCancelResult } from "./cancel.ts";
|
|
16
12
|
import { createSubagentLaunchTarget, type SubagentLaunchState } from "./launch-target.ts";
|
|
17
13
|
import {
|
|
18
|
-
getChildSubagentLifecycle,
|
|
19
14
|
hasSubagentLaunchEntries,
|
|
20
|
-
SUBAGENT_CLOSED_CUSTOM_TYPE,
|
|
21
15
|
SUBAGENT_LAUNCHED_CUSTOM_TYPE,
|
|
22
16
|
SUBAGENT_REPORT_MESSAGE_CUSTOM_TYPE,
|
|
23
17
|
SUBAGENT_REPORT_RECEIVED_CUSTOM_TYPE,
|
|
24
|
-
SUBAGENT_REPORT_REMINDER_MESSAGE_CUSTOM_TYPE,
|
|
25
18
|
} from "./ledger.ts";
|
|
26
19
|
import { openReconcileSession, SubagentReconciler } from "./reconcile.ts";
|
|
27
20
|
import {
|
|
@@ -31,10 +24,14 @@ import {
|
|
|
31
24
|
} from "./report.ts";
|
|
32
25
|
import { renderSubagentReportMessage } from "./report-message-renderer.ts";
|
|
33
26
|
import { openRosterSession, type SubagentRoster, TranscriptSubagentRoster } from "./roster.ts";
|
|
27
|
+
import {
|
|
28
|
+
countSubagentReports,
|
|
29
|
+
createSettledChildLifecycle,
|
|
30
|
+
findSelfSubagentIdentity,
|
|
31
|
+
type SubagentChildSessionState,
|
|
32
|
+
} from "./settle.ts";
|
|
34
33
|
import { SubagentMessageRouter } from "./wake.ts";
|
|
35
34
|
|
|
36
|
-
const LINGER_POLL_MS = 1_000;
|
|
37
|
-
|
|
38
35
|
interface ParentSessionState extends SubagentParentSession {
|
|
39
36
|
getSessionName(): string | undefined;
|
|
40
37
|
launchState: SubagentLaunchState;
|
|
@@ -44,15 +41,9 @@ interface ParentSessionState extends SubagentParentSession {
|
|
|
44
41
|
shutdown(): void;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
interface ChildSessionState {
|
|
48
|
-
identity: HandoffSubagent;
|
|
49
|
-
requestResponse: boolean;
|
|
50
|
-
reportsAtTurnStart: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
44
|
interface CurrentSubagentSession {
|
|
54
45
|
parent: ParentSessionState;
|
|
55
|
-
child?:
|
|
46
|
+
child?: SubagentChildSessionState | undefined;
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
export interface SubagentsHandle extends SessionLifecycle {
|
|
@@ -70,16 +61,9 @@ export function installSubagents(
|
|
|
70
61
|
|
|
71
62
|
let epoch = 0;
|
|
72
63
|
let current: CurrentSubagentSession | undefined;
|
|
73
|
-
let lingerTimer: NodeJS.Timeout | undefined;
|
|
74
64
|
|
|
75
65
|
const isCurrentSession = (candidateEpoch: number): boolean =>
|
|
76
66
|
current?.parent.epoch === candidateEpoch;
|
|
77
|
-
const clearLinger = (): void => {
|
|
78
|
-
if (lingerTimer) {
|
|
79
|
-
clearTimeout(lingerTimer);
|
|
80
|
-
lingerTimer = undefined;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
67
|
const reconciler = new SubagentReconciler({
|
|
84
68
|
executor: pi,
|
|
85
69
|
messaging: deps.messaging,
|
|
@@ -87,6 +71,7 @@ export function installSubagents(
|
|
|
87
71
|
isCurrent: isCurrentSession,
|
|
88
72
|
openSession: openReconcileSession,
|
|
89
73
|
});
|
|
74
|
+
const settledChildLifecycle = createSettledChildLifecycle(pi, reconciler, isCurrentSession);
|
|
90
75
|
const roster = new TranscriptSubagentRoster({
|
|
91
76
|
executor: pi,
|
|
92
77
|
messaging: deps.messaging,
|
|
@@ -150,9 +135,9 @@ You are working as a subagent on one task delegated by a parent session. The han
|
|
|
150
135
|
});
|
|
151
136
|
|
|
152
137
|
pi.on("agent_start", (_event, ctx) => {
|
|
153
|
-
|
|
138
|
+
settledChildLifecycle.cancel();
|
|
154
139
|
if (current?.child) {
|
|
155
|
-
current.child.reportsAtTurnStart =
|
|
140
|
+
current.child.reportsAtTurnStart = countSubagentReports(ctx.sessionManager.getBranch());
|
|
156
141
|
}
|
|
157
142
|
});
|
|
158
143
|
|
|
@@ -161,28 +146,11 @@ You are working as a subagent on one task delegated by a parent session. The han
|
|
|
161
146
|
if (!session || session.parent.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
162
147
|
return;
|
|
163
148
|
}
|
|
164
|
-
|
|
165
|
-
await reconciler.reconcile()
|
|
166
|
-
|
|
149
|
+
const reconciliation = hasSubagentLaunchEntries(session.parent.getBranch())
|
|
150
|
+
? await reconciler.reconcile()
|
|
151
|
+
: undefined;
|
|
167
152
|
if (session.child) {
|
|
168
|
-
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const shouldExit = settleChild(pi, session.child, ctx.sessionManager.getBranch());
|
|
172
|
-
if (!shouldExit) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
await exitWhenUnobserved(
|
|
176
|
-
pi,
|
|
177
|
-
session.parent,
|
|
178
|
-
session.child.identity,
|
|
179
|
-
isCurrentSession,
|
|
180
|
-
(timer) => {
|
|
181
|
-
clearLinger();
|
|
182
|
-
lingerTimer = timer;
|
|
183
|
-
},
|
|
184
|
-
);
|
|
185
|
-
return;
|
|
153
|
+
await settledChildLifecycle.settle(session.parent, session.child, reconciliation);
|
|
186
154
|
}
|
|
187
155
|
});
|
|
188
156
|
|
|
@@ -202,11 +170,12 @@ You are working as a subagent on one task delegated by a parent session. The han
|
|
|
202
170
|
return [createSubagentLaunchTarget(pi, parent.launchState, isCurrentSession)];
|
|
203
171
|
},
|
|
204
172
|
async onSessionStart(_event, ctx) {
|
|
205
|
-
|
|
173
|
+
settledChildLifecycle.cancel();
|
|
206
174
|
epoch += 1;
|
|
207
175
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
208
|
-
const identity = findSelfSubagentIdentity(ctx.sessionManager);
|
|
209
176
|
const sessionManager = ctx.sessionManager;
|
|
177
|
+
const sessionStartBranch = sessionManager.getBranch();
|
|
178
|
+
const identity = findSelfSubagentIdentity(sessionId, sessionStartBranch);
|
|
210
179
|
const parent: ParentSessionState = {
|
|
211
180
|
sessionId,
|
|
212
181
|
getSessionName: () => sessionManager.getSessionName(),
|
|
@@ -227,7 +196,7 @@ You are working as a subagent on one task delegated by a parent session. The han
|
|
|
227
196
|
child: {
|
|
228
197
|
identity,
|
|
229
198
|
requestResponse: identity.requestResponse,
|
|
230
|
-
reportsAtTurnStart:
|
|
199
|
+
reportsAtTurnStart: countSubagentReports(sessionStartBranch),
|
|
231
200
|
},
|
|
232
201
|
}
|
|
233
202
|
: {}),
|
|
@@ -249,7 +218,7 @@ You are working as a subagent on one task delegated by a parent session. The han
|
|
|
249
218
|
}
|
|
250
219
|
},
|
|
251
220
|
async onSessionShutdown(event) {
|
|
252
|
-
|
|
221
|
+
settledChildLifecycle.cancel();
|
|
253
222
|
if (current && event.reason !== "reload") {
|
|
254
223
|
await reconciler.suspendForShutdown();
|
|
255
224
|
}
|
|
@@ -258,87 +227,3 @@ You are working as a subagent on one task delegated by a parent session. The han
|
|
|
258
227
|
},
|
|
259
228
|
};
|
|
260
229
|
}
|
|
261
|
-
|
|
262
|
-
function settleChild(
|
|
263
|
-
pi: ExtensionAPI,
|
|
264
|
-
child: ChildSessionState,
|
|
265
|
-
branch: readonly SessionEntry[],
|
|
266
|
-
): boolean {
|
|
267
|
-
const lifecycle = getChildSubagentLifecycle(branch);
|
|
268
|
-
const reports = lifecycle.reports.length;
|
|
269
|
-
if (reports > child.reportsAtTurnStart) {
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
|
-
if (!child.requestResponse) {
|
|
273
|
-
pi.appendEntry(SUBAGENT_CLOSED_CUSTOM_TYPE, {
|
|
274
|
-
reason: "no_response_expected",
|
|
275
|
-
});
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (lifecycle.hasReminder) {
|
|
280
|
-
pi.appendEntry(SUBAGENT_CLOSED_CUSTOM_TYPE, {
|
|
281
|
-
reason: "no_report_after_reminder",
|
|
282
|
-
});
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
pi.sendMessage(
|
|
287
|
-
{
|
|
288
|
-
customType: SUBAGENT_REPORT_REMINDER_MESSAGE_CUSTOM_TYPE,
|
|
289
|
-
content:
|
|
290
|
-
"[system] Your delegated turn settled without a task report. Call submit_task_report now with done, blocked, or incomplete status.",
|
|
291
|
-
display: true,
|
|
292
|
-
},
|
|
293
|
-
{ triggerTurn: true },
|
|
294
|
-
);
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function countReports(branch: readonly SessionEntry[]): number {
|
|
299
|
-
return getChildSubagentLifecycle(branch).reports.length;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function findSelfSubagentIdentity(session: {
|
|
303
|
-
getSessionId(): string;
|
|
304
|
-
getBranch(): readonly SessionEntry[];
|
|
305
|
-
}): HandoffSubagent | undefined {
|
|
306
|
-
const metadata = getHandoffMetadataFromEntries(session.getBranch());
|
|
307
|
-
if (metadata?.launch !== SUBAGENT_LAUNCH) {
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
return metadata.subagent.childSessionId === session.getSessionId()
|
|
311
|
-
? metadata.subagent
|
|
312
|
-
: undefined;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function exitWhenUnobserved(
|
|
316
|
-
executor: ExtensionAPI,
|
|
317
|
-
parent: ParentSessionState,
|
|
318
|
-
identity: HandoffSubagent,
|
|
319
|
-
isCurrentSession: (epoch: number) => boolean,
|
|
320
|
-
setTimer: (timer: NodeJS.Timeout) => void,
|
|
321
|
-
): Promise<void> {
|
|
322
|
-
const check = async (): Promise<void> => {
|
|
323
|
-
if (!isCurrentSession(parent.epoch) || parent.hasPendingMessages() || !parent.isIdle()) {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
let attached = false;
|
|
328
|
-
try {
|
|
329
|
-
attached = await hasAttachedTmuxClients(executor, tmuxSessionName(identity.ownerSessionId));
|
|
330
|
-
} catch {
|
|
331
|
-
attached = false;
|
|
332
|
-
}
|
|
333
|
-
if (!isCurrentSession(parent.epoch)) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
if (!attached) {
|
|
337
|
-
parent.shutdown();
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
setTimer(setTimeout(() => void check(), LINGER_POLL_MS));
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
await check();
|
|
344
|
-
}
|