@gotgenes/pi-subagents 13.0.0 → 13.2.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/CHANGELOG.md +17 -0
- package/dist/public.d.ts +1 -1
- package/docs/architecture/architecture.md +80 -43
- package/docs/plans/0265-born-complete-subagent-session.md +330 -0
- package/docs/plans/0277-encapsulate-agent-session.md +304 -0
- package/docs/retro/0264-remove-extension-lifecycle-control.md +41 -0
- package/docs/retro/0265-born-complete-subagent-session.md +97 -0
- package/docs/retro/0277-encapsulate-agent-session.md +39 -0
- package/package.json +1 -1
- package/src/index.ts +3 -3
- package/src/lifecycle/agent-manager.ts +11 -10
- package/src/lifecycle/agent.ts +99 -59
- package/src/lifecycle/create-subagent-session.ts +242 -0
- package/src/lifecycle/subagent-session.ts +234 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/observation/notification.ts +2 -2
- package/src/runtime.ts +1 -1
- package/src/service/service-adapter.ts +1 -7
- package/src/session/conversation.ts +49 -0
- package/src/session/session-config.ts +8 -8
- package/src/settings.ts +1 -1
- package/src/tools/agent-tool.ts +1 -1
- package/src/tools/background-spawner.ts +4 -3
- package/src/tools/foreground-runner.ts +4 -3
- package/src/tools/get-result-tool.ts +4 -8
- package/src/tools/spawn-config.ts +1 -1
- package/src/tools/steer-tool.ts +7 -13
- package/src/ui/agent-menu.ts +1 -3
- package/src/ui/conversation-viewer.ts +5 -10
- package/src/lifecycle/agent-runner.ts +0 -464
- package/src/lifecycle/execution-state.ts +0 -17
package/src/lifecycle/agent.ts
CHANGED
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
* The child's working directory is supplied by a registered WorkspaceProvider
|
|
15
15
|
* (the workspace seam); with no provider the child runs in the parent cwd.
|
|
16
16
|
*
|
|
17
|
-
* Phase-specific collaborators (
|
|
17
|
+
* Phase-specific collaborators (subagentSession, notification) are attached
|
|
18
18
|
* after construction as lifecycle information becomes available.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { Model } from "@earendil-works/pi-ai";
|
|
22
|
-
import type {
|
|
22
|
+
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
23
23
|
import { debugLog } from "#src/debug";
|
|
24
|
-
import type {
|
|
25
|
-
import type { ExecutionState } from "#src/lifecycle/execution-state";
|
|
24
|
+
import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent-session";
|
|
26
25
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
26
|
+
import type { SubagentSession, TurnLoopResult } from "#src/lifecycle/subagent-session";
|
|
27
27
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
28
28
|
import { addUsage } from "#src/lifecycle/usage";
|
|
29
29
|
import type { Workspace, WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
@@ -36,8 +36,8 @@ import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SubagentType,
|
|
|
36
36
|
export interface AgentLifecycleObserver {
|
|
37
37
|
/** Fires when the agent transitions to running (inside run(), after markRunning). */
|
|
38
38
|
onStarted?(agent: Agent): void;
|
|
39
|
-
/** Fires
|
|
40
|
-
onSessionCreated?(agent: Agent
|
|
39
|
+
/** Fires once the session is created — the agent's subagentSession is now available. */
|
|
40
|
+
onSessionCreated?(agent: Agent): void;
|
|
41
41
|
/** Fires once when the run completes or fails (for concurrency drain). */
|
|
42
42
|
onRunFinished?(agent: Agent): void;
|
|
43
43
|
/** Fires on compaction events during the run. */
|
|
@@ -68,7 +68,8 @@ export interface AgentInit {
|
|
|
68
68
|
error?: string;
|
|
69
69
|
|
|
70
70
|
// Shared deps (required for run(), optional for tests)
|
|
71
|
-
|
|
71
|
+
/** Assembly factory that produces a born-complete SubagentSession. */
|
|
72
|
+
createSubagentSession?: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
72
73
|
observer?: AgentLifecycleObserver;
|
|
73
74
|
getRunConfig?: () => RunConfig;
|
|
74
75
|
/** Resolves the registered workspace provider (if any) at run-start. */
|
|
@@ -126,7 +127,7 @@ export class Agent {
|
|
|
126
127
|
promise?: Promise<void>;
|
|
127
128
|
|
|
128
129
|
// Shared deps — optional (required for run())
|
|
129
|
-
private readonly
|
|
130
|
+
private readonly _createSubagentSession?: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
130
131
|
readonly observer?: AgentLifecycleObserver;
|
|
131
132
|
private readonly _getRunConfig?: () => RunConfig;
|
|
132
133
|
private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
@@ -144,7 +145,8 @@ export class Agent {
|
|
|
144
145
|
private readonly _signal?: AbortSignal;
|
|
145
146
|
|
|
146
147
|
// Phase-specific collaborators — each born complete when their info becomes available
|
|
147
|
-
|
|
148
|
+
/** The born-complete child session — set when the factory returns inside run(). */
|
|
149
|
+
subagentSession?: SubagentSession;
|
|
148
150
|
notification?: NotificationState;
|
|
149
151
|
|
|
150
152
|
// Steer buffer — messages queued before the session is ready
|
|
@@ -152,14 +154,50 @@ export class Agent {
|
|
|
152
154
|
/** Number of steer messages waiting to be delivered. */
|
|
153
155
|
get pendingSteerCount(): number { return this._pendingSteers.length; }
|
|
154
156
|
|
|
155
|
-
/** The active agent session, or undefined before the session is created. */
|
|
156
|
-
get session(): AgentSession | undefined {
|
|
157
|
-
return this.execution?.session;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
157
|
/** Path to the agent's session JSONL file, or undefined if not yet available. */
|
|
161
158
|
get outputFile(): string | undefined {
|
|
162
|
-
return this.
|
|
159
|
+
return this.subagentSession?.outputFile;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Returns true when a SubagentSession is available (session is ready). */
|
|
163
|
+
isSessionReady(): boolean {
|
|
164
|
+
return this.subagentSession != null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Deliver or buffer a steer message.
|
|
169
|
+
* Returns true when delivered immediately; false when buffered for later delivery.
|
|
170
|
+
*/
|
|
171
|
+
async steer(message: string): Promise<boolean> {
|
|
172
|
+
if (!this.subagentSession) {
|
|
173
|
+
this.queueSteer(message);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
await this.subagentSession.steer(message);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Return the session conversation as formatted text, or undefined if no session. */
|
|
181
|
+
getConversation(): string | undefined {
|
|
182
|
+
return this.subagentSession?.getConversation();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Return the session context window utilization (0-100), or null if unavailable. */
|
|
186
|
+
getContextPercent(): number | null {
|
|
187
|
+
return this.subagentSession?.getContextPercent() ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Subscribe to session events for live updates (e.g., conversation viewer).
|
|
192
|
+
* Returns an unsubscribe function, or undefined if no session is available.
|
|
193
|
+
*/
|
|
194
|
+
subscribeToUpdates(fn: (event: AgentSessionEvent) => void): (() => void) | undefined {
|
|
195
|
+
return this.subagentSession?.subscribe(fn);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The session's message history, or an empty array if no session. */
|
|
199
|
+
get messages(): readonly unknown[] {
|
|
200
|
+
return this.subagentSession?.messages ?? [];
|
|
163
201
|
}
|
|
164
202
|
|
|
165
203
|
constructor(init: AgentInit) {
|
|
@@ -185,7 +223,7 @@ export class Agent {
|
|
|
185
223
|
this.abortController = new AbortController();
|
|
186
224
|
|
|
187
225
|
// Shared deps
|
|
188
|
-
this.
|
|
226
|
+
this._createSubagentSession = init.createSubagentSession;
|
|
189
227
|
this.observer = init.observer;
|
|
190
228
|
this._getRunConfig = init.getRunConfig;
|
|
191
229
|
this._getWorkspaceProvider = init.getWorkspaceProvider;
|
|
@@ -207,16 +245,16 @@ export class Agent {
|
|
|
207
245
|
}
|
|
208
246
|
|
|
209
247
|
/**
|
|
210
|
-
* Execute the full agent lifecycle: workspace preparation,
|
|
211
|
-
*
|
|
248
|
+
* Execute the full agent lifecycle: workspace preparation, session creation
|
|
249
|
+
* via the factory, observer wiring, the turn loop, workspace disposal, and
|
|
212
250
|
* status transitions.
|
|
213
251
|
*
|
|
214
|
-
* Requires
|
|
252
|
+
* Requires the session factory and snapshot to be set at construction.
|
|
215
253
|
* The returned promise always resolves (errors are captured internally).
|
|
216
254
|
*/
|
|
217
255
|
async run(): Promise<void> {
|
|
218
|
-
if (!this.
|
|
219
|
-
throw new Error("Agent not configured for execution — missing
|
|
256
|
+
if (!this._createSubagentSession) {
|
|
257
|
+
throw new Error("Agent not configured for execution — missing session factory");
|
|
220
258
|
}
|
|
221
259
|
if (!this._snapshot || !this._prompt) {
|
|
222
260
|
throw new Error("Agent not configured for execution — missing snapshot or prompt");
|
|
@@ -247,29 +285,34 @@ export class Agent {
|
|
|
247
285
|
return;
|
|
248
286
|
}
|
|
249
287
|
|
|
250
|
-
const runConfig = this._getRunConfig?.();
|
|
251
288
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
289
|
+
this.subagentSession = await this._createSubagentSession({
|
|
290
|
+
snapshot: this._snapshot,
|
|
291
|
+
type: this.type,
|
|
292
|
+
cwd,
|
|
293
|
+
parentSession: this._parentSession,
|
|
257
294
|
model: this._model,
|
|
295
|
+
thinkingLevel: this._thinkingLevel,
|
|
296
|
+
});
|
|
297
|
+
} catch (err) {
|
|
298
|
+
// The factory disposed its own session on a post-creation failure.
|
|
299
|
+
this.failRun(err);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.flushPendingSteers();
|
|
304
|
+
this.attachObserver(subscribeAgentObserver(this.subagentSession, this, {
|
|
305
|
+
onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
|
|
306
|
+
}));
|
|
307
|
+
this.observer?.onSessionCreated?.(this);
|
|
308
|
+
|
|
309
|
+
const runConfig = this._getRunConfig?.();
|
|
310
|
+
try {
|
|
311
|
+
const result = await this.subagentSession.runTurnLoop(this._prompt, {
|
|
258
312
|
maxTurns: this._maxTurns,
|
|
259
313
|
defaultMaxTurns: runConfig?.defaultMaxTurns,
|
|
260
314
|
graceTurns: runConfig?.graceTurns,
|
|
261
|
-
thinkingLevel: this._thinkingLevel,
|
|
262
315
|
signal: this.abortController.signal,
|
|
263
|
-
onSessionCreated: (session) => {
|
|
264
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- sessionManager is typed as always present but Pi SDK may not provide it
|
|
265
|
-
const outputFile = session.sessionManager?.getSessionFile?.() ?? undefined;
|
|
266
|
-
this.execution = { session, outputFile };
|
|
267
|
-
this.flushPendingSteers(session);
|
|
268
|
-
this.attachObserver(subscribeAgentObserver(session, this, {
|
|
269
|
-
onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
|
|
270
|
-
}));
|
|
271
|
-
this.observer?.onSessionCreated?.(this, session);
|
|
272
|
-
},
|
|
273
316
|
});
|
|
274
317
|
this.completeRun(result);
|
|
275
318
|
} catch (err) {
|
|
@@ -281,27 +324,24 @@ export class Agent {
|
|
|
281
324
|
* Resume an existing session with a new prompt, managing the observer
|
|
282
325
|
* subscription lifecycle internally (same wiring as run()).
|
|
283
326
|
*
|
|
284
|
-
* Requires
|
|
327
|
+
* Requires an existing SubagentSession (set when the original run created it).
|
|
285
328
|
* The returned promise always resolves (errors are captured internally).
|
|
286
|
-
* The parent signal flows straight through to
|
|
329
|
+
* The parent signal flows straight through to resumeTurnLoop — resume does not
|
|
287
330
|
* route through this.abortController.
|
|
288
331
|
*/
|
|
289
332
|
async resume(prompt: string, signal?: AbortSignal): Promise<void> {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
const session = this.session;
|
|
294
|
-
if (!session) {
|
|
333
|
+
const subagentSession = this.subagentSession;
|
|
334
|
+
if (!subagentSession) {
|
|
295
335
|
throw new Error("Agent not configured for resume — missing session");
|
|
296
336
|
}
|
|
297
337
|
|
|
298
338
|
this.resetForResume(Date.now());
|
|
299
|
-
this.attachObserver(subscribeAgentObserver(
|
|
339
|
+
this.attachObserver(subscribeAgentObserver(subagentSession, this, {
|
|
300
340
|
onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
|
|
301
341
|
}));
|
|
302
342
|
|
|
303
343
|
try {
|
|
304
|
-
const responseText = await
|
|
344
|
+
const responseText = await subagentSession.resumeTurnLoop(prompt, signal);
|
|
305
345
|
this.markCompleted(responseText);
|
|
306
346
|
} catch (err) {
|
|
307
347
|
this.markError(err);
|
|
@@ -399,19 +439,19 @@ export class Agent {
|
|
|
399
439
|
|
|
400
440
|
/**
|
|
401
441
|
* Buffer a steer message for delivery once the session is ready.
|
|
402
|
-
* Called
|
|
442
|
+
* Called internally from steer() before the session is ready.
|
|
403
443
|
*/
|
|
404
|
-
queueSteer(message: string): void {
|
|
444
|
+
private queueSteer(message: string): void {
|
|
405
445
|
this._pendingSteers.push(message);
|
|
406
446
|
}
|
|
407
447
|
|
|
408
448
|
/**
|
|
409
449
|
* Flush all buffered steer messages to the session and clear the buffer.
|
|
410
|
-
* Called
|
|
450
|
+
* Called once the session is available (inside run()).
|
|
411
451
|
*/
|
|
412
|
-
flushPendingSteers(
|
|
452
|
+
private flushPendingSteers(): void {
|
|
413
453
|
for (const msg of this._pendingSteers) {
|
|
414
|
-
|
|
454
|
+
this.subagentSession?.steer(msg).catch(() => {});
|
|
415
455
|
}
|
|
416
456
|
this._pendingSteers = [];
|
|
417
457
|
}
|
|
@@ -451,8 +491,8 @@ export class Agent {
|
|
|
451
491
|
this._detachFn = undefined;
|
|
452
492
|
}
|
|
453
493
|
|
|
454
|
-
/** Complete a run: release listeners, dispose the workspace, status transition,
|
|
455
|
-
completeRun(result:
|
|
494
|
+
/** Complete a run: release listeners, dispose the workspace, status transition, notify observer. */
|
|
495
|
+
completeRun(result: TurnLoopResult): void {
|
|
456
496
|
this.releaseListeners();
|
|
457
497
|
|
|
458
498
|
let finalResult = result.responseText;
|
|
@@ -470,14 +510,14 @@ export class Agent {
|
|
|
470
510
|
else if (result.steered) this.markSteered(finalResult);
|
|
471
511
|
else this.markCompleted(finalResult);
|
|
472
512
|
|
|
473
|
-
this.execution = {
|
|
474
|
-
session: result.session,
|
|
475
|
-
outputFile: result.sessionFile ?? this.execution?.outputFile,
|
|
476
|
-
};
|
|
477
|
-
|
|
478
513
|
this.observer?.onRunFinished?.(this);
|
|
479
514
|
}
|
|
480
515
|
|
|
516
|
+
/** Dispose the wrapped session, firing the `disposed` lifecycle event. */
|
|
517
|
+
disposeSession(): void {
|
|
518
|
+
this.subagentSession?.dispose();
|
|
519
|
+
}
|
|
520
|
+
|
|
481
521
|
/** Fail a run: mark error, release listeners, best-effort workspace dispose, notify observer. */
|
|
482
522
|
failRun(err: unknown): void {
|
|
483
523
|
this.markError(err);
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create-subagent-session.ts — Assembly factory for born-complete child sessions (issue #265).
|
|
3
|
+
*
|
|
4
|
+
* `createSubagentSession()` does the assembly portion that the old runner's
|
|
5
|
+
* `runAgent()` did up front: detect the environment, assemble the session config,
|
|
6
|
+
* create the SDK session, publish `spawning`/`session-created`, bind extensions,
|
|
7
|
+
* and apply the recursion guard. It returns a fully usable `SubagentSession` —
|
|
8
|
+
* `Agent` then only coordinates (turn loop, steer, dispose).
|
|
9
|
+
*
|
|
10
|
+
* The factory takes a resolved `cwd` value, never the WorkspaceProvider: `cwd`
|
|
11
|
+
* is a value the factory consumes directly (detectEnv, assembleSessionConfig,
|
|
12
|
+
* createSession), so threading the provider through here would be a relay smell.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
16
|
+
import {
|
|
17
|
+
type AgentSession,
|
|
18
|
+
type SettingsManager,
|
|
19
|
+
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
21
|
+
import type { ChildLifecyclePublisher } from "#src/lifecycle/child-lifecycle";
|
|
22
|
+
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
23
|
+
import { SubagentSession } from "#src/lifecycle/subagent-session";
|
|
24
|
+
import type { EnvInfo } from "#src/session/env";
|
|
25
|
+
import { type AssemblerIO, assembleSessionConfig } from "#src/session/session-config";
|
|
26
|
+
import type { ParentSessionInfo, ShellExec, SubagentType, ThinkingLevel } from "#src/types";
|
|
27
|
+
|
|
28
|
+
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
29
|
+
const EXCLUDED_TOOL_NAMES = ["subagent", "get_subagent_result", "steer_subagent"];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Apply the recursion guard: remove this extension's dispatch tools from the
|
|
33
|
+
* child's active set. Runs after `bindExtensions` so extension-registered tools
|
|
34
|
+
* are also covered. Unconditional: children always load the parent's extensions.
|
|
35
|
+
*/
|
|
36
|
+
function applyRecursionGuard(session: AgentSession): void {
|
|
37
|
+
const filtered = session
|
|
38
|
+
.getActiveToolNames()
|
|
39
|
+
.filter((t) => !EXCLUDED_TOOL_NAMES.includes(t));
|
|
40
|
+
session.setActiveToolsByName(filtered);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── IO boundary ───────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/** Minimal resource-loader contract used by the factory. */
|
|
46
|
+
export interface ResourceLoaderLike {
|
|
47
|
+
reload(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Minimal session-manager contract used by the factory. */
|
|
51
|
+
export interface SessionManagerLike {
|
|
52
|
+
newSession(opts: { parentSession?: string }): void;
|
|
53
|
+
getSessionFile(): string | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Options passed to EnvironmentIO/SessionFactoryIO methods. */
|
|
57
|
+
export interface ResourceLoaderOptions {
|
|
58
|
+
cwd: string;
|
|
59
|
+
agentDir: string;
|
|
60
|
+
noPromptTemplates?: boolean;
|
|
61
|
+
noThemes?: boolean;
|
|
62
|
+
noContextFiles?: boolean;
|
|
63
|
+
systemPromptOverride?: () => string;
|
|
64
|
+
/** Override the append system prompt. Receives the current base value; return the replacement. */
|
|
65
|
+
appendSystemPromptOverride?: (base: string[]) => string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Options passed to SessionFactoryIO.createSession. */
|
|
69
|
+
export interface CreateSessionOptions {
|
|
70
|
+
cwd: string;
|
|
71
|
+
agentDir: string;
|
|
72
|
+
sessionManager: SessionManagerLike;
|
|
73
|
+
settingsManager: SettingsManager;
|
|
74
|
+
modelRegistry: unknown;
|
|
75
|
+
model?: unknown;
|
|
76
|
+
tools: string[];
|
|
77
|
+
resourceLoader: ResourceLoaderLike;
|
|
78
|
+
thinkingLevel?: ThinkingLevel;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Environment discovery - detect runtime context and resolve directories.
|
|
83
|
+
*
|
|
84
|
+
* Decouples the factory from direct process/SDK reads so each can be stubbed
|
|
85
|
+
* independently in tests.
|
|
86
|
+
*/
|
|
87
|
+
export interface EnvironmentIO {
|
|
88
|
+
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
89
|
+
getAgentDir: () => string;
|
|
90
|
+
deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Session factory - create SDK objects for a child agent session.
|
|
95
|
+
*
|
|
96
|
+
* Decouples the factory from direct Pi SDK imports and sibling-module IO,
|
|
97
|
+
* making it testable via plain stub objects without vi.mock().
|
|
98
|
+
*/
|
|
99
|
+
export interface SessionFactoryIO {
|
|
100
|
+
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
|
|
101
|
+
createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
|
|
102
|
+
createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
|
|
103
|
+
createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
|
|
104
|
+
assemblerIO: AssemblerIO;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* IO boundary injected into createSubagentSession().
|
|
109
|
+
*
|
|
110
|
+
* Intersection of EnvironmentIO and SessionFactoryIO — callers satisfy both
|
|
111
|
+
* sub-interfaces via TypeScript's structural typing.
|
|
112
|
+
*/
|
|
113
|
+
export type SubagentSessionIO = EnvironmentIO & SessionFactoryIO;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Dependencies injected at construction time — the IO boundary plus the two
|
|
117
|
+
* static domain deps (exec, registry) every creation needs.
|
|
118
|
+
*/
|
|
119
|
+
export interface SubagentSessionDeps {
|
|
120
|
+
io: SubagentSessionIO;
|
|
121
|
+
exec: ShellExec;
|
|
122
|
+
registry: AgentConfigLookup;
|
|
123
|
+
/** Publishes the child-execution lifecycle so consumers can observe it. */
|
|
124
|
+
lifecycle: ChildLifecyclePublisher;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Per-spawn parameters — the fields that vary per child session. */
|
|
128
|
+
export interface CreateSubagentSessionParams {
|
|
129
|
+
snapshot: ParentSnapshot;
|
|
130
|
+
type: SubagentType;
|
|
131
|
+
/** Resolved workspace cwd; undefined → parent cwd. */
|
|
132
|
+
cwd?: string;
|
|
133
|
+
/** Parent session identity (file path + session ID). */
|
|
134
|
+
parentSession?: ParentSessionInfo;
|
|
135
|
+
model?: Model<any>;
|
|
136
|
+
thinkingLevel?: ThinkingLevel;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build a born-complete SubagentSession: assemble config, create the SDK
|
|
141
|
+
* session, publish lifecycle events, bind extensions, apply the recursion guard.
|
|
142
|
+
*/
|
|
143
|
+
export async function createSubagentSession(
|
|
144
|
+
params: CreateSubagentSessionParams,
|
|
145
|
+
deps: SubagentSessionDeps,
|
|
146
|
+
): Promise<SubagentSession> {
|
|
147
|
+
const { snapshot, type } = params;
|
|
148
|
+
const parentSessionId = params.parentSession?.parentSessionId;
|
|
149
|
+
deps.lifecycle.spawning({ agentName: type, parentSessionId });
|
|
150
|
+
|
|
151
|
+
// Resolve working directory upfront - needed for detectEnv before assembly.
|
|
152
|
+
const effectiveCwd = params.cwd ?? snapshot.cwd;
|
|
153
|
+
const env = await deps.io.detectEnv(deps.exec, effectiveCwd);
|
|
154
|
+
|
|
155
|
+
// Assemble session configuration (synchronous, no SDK objects).
|
|
156
|
+
const cfg = assembleSessionConfig(
|
|
157
|
+
type,
|
|
158
|
+
{
|
|
159
|
+
cwd: snapshot.cwd,
|
|
160
|
+
parentSystemPrompt: snapshot.systemPrompt,
|
|
161
|
+
parentModel: snapshot.model,
|
|
162
|
+
modelRegistry: snapshot.modelRegistry,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
cwd: params.cwd,
|
|
166
|
+
model: params.model,
|
|
167
|
+
thinkingLevel: params.thinkingLevel,
|
|
168
|
+
},
|
|
169
|
+
env,
|
|
170
|
+
deps.registry,
|
|
171
|
+
deps.io.assemblerIO,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const agentDir = deps.io.getAgentDir();
|
|
175
|
+
|
|
176
|
+
// Children always load the parent's extensions and skills.
|
|
177
|
+
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md - upstream's
|
|
178
|
+
// buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
|
|
179
|
+
// would defeat prompt_mode: replace. Parent context, if wanted, reaches the
|
|
180
|
+
// subagent via prompt_mode: append (parentSystemPrompt is embedded in
|
|
181
|
+
// systemPromptOverride) or inherit_context (conversation).
|
|
182
|
+
const loader = deps.io.createResourceLoader({
|
|
183
|
+
cwd: cfg.effectiveCwd,
|
|
184
|
+
agentDir,
|
|
185
|
+
noPromptTemplates: true,
|
|
186
|
+
noThemes: true,
|
|
187
|
+
noContextFiles: true,
|
|
188
|
+
systemPromptOverride: () => cfg.systemPrompt,
|
|
189
|
+
appendSystemPromptOverride: () => [],
|
|
190
|
+
});
|
|
191
|
+
await loader.reload();
|
|
192
|
+
|
|
193
|
+
// Create a persisted SessionManager so transcripts are written in Pi's
|
|
194
|
+
// official JSONL format. Falls back to a temp directory when the parent
|
|
195
|
+
// session is not persisted (e.g. headless/API mode).
|
|
196
|
+
const sessionDir = deps.io.deriveSessionDir(params.parentSession?.parentSessionFile, cfg.effectiveCwd);
|
|
197
|
+
const sessionManager = deps.io.createSessionManager(cfg.effectiveCwd, sessionDir);
|
|
198
|
+
sessionManager.newSession({ parentSession: params.parentSession?.parentSessionId });
|
|
199
|
+
|
|
200
|
+
const { session } = await deps.io.createSession({
|
|
201
|
+
cwd: cfg.effectiveCwd,
|
|
202
|
+
agentDir,
|
|
203
|
+
sessionManager,
|
|
204
|
+
settingsManager: deps.io.createSettingsManager(cfg.effectiveCwd, agentDir),
|
|
205
|
+
modelRegistry: snapshot.modelRegistry,
|
|
206
|
+
model: cfg.model,
|
|
207
|
+
tools: cfg.toolNames,
|
|
208
|
+
resourceLoader: loader,
|
|
209
|
+
thinkingLevel: cfg.thinkingLevel,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const subagentSession = new SubagentSession(session, {
|
|
213
|
+
outputFile: sessionManager.getSessionFile(),
|
|
214
|
+
sessionDir,
|
|
215
|
+
agentName: type,
|
|
216
|
+
agentMaxTurns: cfg.agentMaxTurns,
|
|
217
|
+
parentContext: snapshot.parentContext,
|
|
218
|
+
lifecycle: deps.lifecycle,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Publish session-created before bindExtensions() so observers (e.g. the
|
|
222
|
+
// permission system) can register the child synchronously and have their
|
|
223
|
+
// entry in place for the first permission check during child extension
|
|
224
|
+
// initialization. The event bus dispatches synchronously, so a synchronous
|
|
225
|
+
// subscriber completes before this returns.
|
|
226
|
+
deps.lifecycle.sessionCreated({ sessionDir, agentName: type, parentSessionId });
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
// Bind extensions so that session_start fires and extensions can initialize.
|
|
230
|
+
await session.bindExtensions({});
|
|
231
|
+
// Apply recursion guard after bindExtensions so extension-registered tools
|
|
232
|
+
// are included in the post-bind active set.
|
|
233
|
+
applyRecursionGuard(session);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
// Binding failed after session-created — dispose (emit disposed +
|
|
236
|
+
// session.dispose()) before rethrowing so registration is never leaked.
|
|
237
|
+
subagentSession.dispose();
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return subagentSession;
|
|
242
|
+
}
|