@gotgenes/pi-subagents 6.1.0 → 6.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.
@@ -8,13 +8,14 @@
8
8
 
9
9
  import { randomUUID } from "node:crypto";
10
10
  import type { Model } from "@earendil-works/pi-ai";
11
- import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
+ import type { AgentSession, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
12
  import { AgentRecord } from "./agent-record.js";
13
- import type { AgentRunner, ToolActivity } from "./agent-runner.js";
13
+ import type { AgentRunner } from "./agent-runner.js";
14
14
  import { debugLog } from "./debug.js";
15
+ import { buildParentSnapshot } from "./parent-snapshot.js";
16
+ import { subscribeRecordObserver } from "./record-observer.js";
15
17
  import type { RunConfig } from "./runtime.js";
16
- import type { AgentInvocation, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
17
- import { addUsage } from "./usage.js";
18
+ import type { AgentInvocation, IsolationMode, ParentSnapshot, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
18
19
  import type { WorktreeManager } from "./worktree.js";
19
20
 
20
21
  export type OnAgentComplete = (record: AgentRecord) => void;
@@ -28,6 +29,7 @@ const DEFAULT_MAX_CONCURRENT = 4;
28
29
  export interface AgentManagerOptions {
29
30
  runner: AgentRunner;
30
31
  worktrees: WorktreeManager;
32
+ exec: ShellExec;
31
33
  maxConcurrent?: number;
32
34
  getRunConfig?: () => RunConfig;
33
35
  onStart?: OnAgentStart;
@@ -36,8 +38,7 @@ export interface AgentManagerOptions {
36
38
  }
37
39
 
38
40
  interface SpawnArgs {
39
- pi: ExtensionAPI;
40
- ctx: ExtensionContext;
41
+ snapshot: ParentSnapshot;
41
42
  type: SubagentType;
42
43
  prompt: string;
43
44
  options: SpawnOptions;
@@ -63,18 +64,8 @@ export interface SpawnOptions {
63
64
  invocation?: AgentInvocation;
64
65
  /** Parent abort signal — when aborted, the subagent is also stopped. */
65
66
  signal?: AbortSignal;
66
- /** Called on tool start/end with activity info (for streaming progress to UI). */
67
- onToolActivity?: (activity: ToolActivity) => void;
68
- /** Called on streaming text deltas from the assistant response. */
69
- onTextDelta?: (delta: string, fullText: string) => void;
70
- /** Called when the agent session is created (for accessing session stats). */
67
+ /** Called when the agent session is created the one remaining callback. */
71
68
  onSessionCreated?: (session: AgentSession) => void;
72
- /** Called at the end of each agentic turn with the cumulative count. */
73
- onTurnEnd?: (turnCount: number) => void;
74
- /** Called once per assistant message_end with that message's usage delta. */
75
- onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
76
- /** Called when the session successfully compacts. */
77
- onCompaction?: (info: CompactionInfo) => void;
78
69
  /** Path to the parent session's JSONL file (for deriving the subagent session directory). */
79
70
  parentSessionFile?: string;
80
71
  /** Session ID of the parent agent (stored in the child session's parentSession header). */
@@ -89,6 +80,7 @@ export class AgentManager {
89
80
  private onCompact?: OnAgentCompact;
90
81
  private readonly runner: AgentRunner;
91
82
  private readonly worktrees: WorktreeManager;
83
+ private readonly exec: ShellExec;
92
84
  private maxConcurrent: number;
93
85
  private getRunConfig?: () => RunConfig;
94
86
 
@@ -100,6 +92,7 @@ export class AgentManager {
100
92
  constructor(options: AgentManagerOptions) {
101
93
  this.runner = options.runner;
102
94
  this.worktrees = options.worktrees;
95
+ this.exec = options.exec;
103
96
  this.onComplete = options.onComplete;
104
97
  this.onStart = options.onStart;
105
98
  this.onCompact = options.onCompact;
@@ -126,7 +119,6 @@ export class AgentManager {
126
119
  * If the concurrency limit is reached, the agent is queued.
127
120
  */
128
121
  spawn(
129
- pi: ExtensionAPI,
130
122
  ctx: ExtensionContext,
131
123
  type: SubagentType,
132
124
  prompt: string,
@@ -145,7 +137,8 @@ export class AgentManager {
145
137
  });
146
138
  this.agents.set(id, record);
147
139
 
148
- const args: SpawnArgs = { pi, ctx, type, prompt, options };
140
+ const snapshot = buildParentSnapshot(ctx, options.inheritContext);
141
+ const args: SpawnArgs = { snapshot, type, prompt, options };
149
142
 
150
143
  if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
151
144
  // Queue it — will be started when a running agent completes
@@ -165,7 +158,7 @@ export class AgentManager {
165
158
  }
166
159
 
167
160
  /** Actually start an agent (called immediately or from queue drain). */
168
- private startAgent(id: string, record: AgentRecord, { pi, ctx, type, prompt, options }: SpawnArgs) {
161
+ private startAgent(id: string, record: AgentRecord, { snapshot, type, prompt, options }: SpawnArgs) {
169
162
  // Worktree isolation: try to create a temporary git worktree. Strict —
170
163
  // fail loud if not possible (no silent fallback to main tree). Done
171
164
  // BEFORE state mutation so a throw doesn't leave the record half-running.
@@ -195,35 +188,21 @@ export class AgentManager {
195
188
  }
196
189
  const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
197
190
 
191
+ let unsubRecordObserver: (() => void) | undefined;
192
+
198
193
  const runConfig = this.getRunConfig?.();
199
- const promise = this.runner.run(ctx, type, prompt, {
200
- pi,
194
+ const promise = this.runner.run(snapshot, type, prompt, {
195
+ exec: this.exec,
201
196
  model: options.model,
202
197
  maxTurns: options.maxTurns,
203
198
  defaultMaxTurns: runConfig?.defaultMaxTurns,
204
199
  graceTurns: runConfig?.graceTurns,
205
200
  isolated: options.isolated,
206
- inheritContext: options.inheritContext,
207
201
  thinkingLevel: options.thinkingLevel,
208
202
  cwd: worktreeCwd,
209
203
  parentSessionFile: options.parentSessionFile,
210
204
  parentSessionId: options.parentSessionId,
211
205
  signal: record.abortController!.signal,
212
- onToolActivity: (activity) => {
213
- if (activity.type === "end") record.toolUses++;
214
- options.onToolActivity?.(activity);
215
- },
216
- onTurnEnd: options.onTurnEnd,
217
- onTextDelta: options.onTextDelta,
218
- onAssistantUsage: (usage) => {
219
- addUsage(record.lifetimeUsage, usage);
220
- options.onAssistantUsage?.(usage);
221
- },
222
- onCompaction: (info) => {
223
- record.compactionCount++;
224
- this.onCompact?.(record, info);
225
- options.onCompaction?.(info);
226
- },
227
206
  onSessionCreated: (session) => {
228
207
  record.session = session;
229
208
  // Capture the session file path early so it's available for display
@@ -237,10 +216,15 @@ export class AgentManager {
237
216
  }
238
217
  record.pendingSteers = undefined;
239
218
  }
219
+ // Subscribe record observer for stats accumulation
220
+ unsubRecordObserver = subscribeRecordObserver(session, record, {
221
+ onCompact: (r, info) => this.onCompact?.(r, info),
222
+ });
240
223
  options.onSessionCreated?.(session);
241
224
  },
242
225
  })
243
226
  .then(({ responseText, session, aborted, steered, sessionFile }) => {
227
+ unsubRecordObserver?.();
244
228
  detach();
245
229
 
246
230
  // Clean up worktree before transition so the final result includes branch text
@@ -271,6 +255,7 @@ export class AgentManager {
271
255
  .catch((err) => {
272
256
  record.markError(err);
273
257
 
258
+ unsubRecordObserver?.();
274
259
  detach();
275
260
 
276
261
  // Best-effort worktree cleanup on error
@@ -314,13 +299,12 @@ export class AgentManager {
314
299
  * Foreground agents bypass the concurrency queue.
315
300
  */
316
301
  async spawnAndWait(
317
- pi: ExtensionAPI,
318
302
  ctx: ExtensionContext,
319
303
  type: SubagentType,
320
304
  prompt: string,
321
305
  options: Omit<SpawnOptions, "isBackground">,
322
306
  ): Promise<AgentRecord> {
323
- const id = this.spawn(pi, ctx, type, prompt, { ...options, isBackground: false });
307
+ const id = this.spawn(ctx, type, prompt, { ...options, isBackground: false });
324
308
  const record = this.agents.get(id)!;
325
309
  await record.promise;
326
310
  return record;
@@ -339,23 +323,19 @@ export class AgentManager {
339
323
 
340
324
  record.resetForResume(Date.now());
341
325
 
326
+ const unsubResume = subscribeRecordObserver(record.session, record, {
327
+ onCompact: (r, info) => this.onCompact?.(r, info),
328
+ });
329
+
342
330
  try {
343
331
  const responseText = await this.runner.resume(record.session, prompt, {
344
- onToolActivity: (activity) => {
345
- if (activity.type === "end") record.toolUses++;
346
- },
347
- onAssistantUsage: (usage) => {
348
- addUsage(record.lifetimeUsage, usage);
349
- },
350
- onCompaction: (info) => {
351
- record.compactionCount++;
352
- this.onCompact?.(record, info);
353
- },
354
332
  signal,
355
333
  });
356
334
  record.markCompleted(responseText);
357
335
  } catch (err) {
358
336
  record.markError(err);
337
+ } finally {
338
+ unsubResume();
359
339
  }
360
340
 
361
341
  return record;
@@ -3,22 +3,20 @@
3
3
  */
4
4
 
5
5
  import type { Model } from "@earendil-works/pi-ai";
6
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
6
  import {
8
7
  type AgentSession,
9
8
  type AgentSessionEvent,
10
9
  createAgentSession,
11
10
  DefaultResourceLoader,
12
- type ExtensionAPI,
13
11
  getAgentDir,
14
12
  SessionManager,
15
13
  SettingsManager,
16
14
  } from "@earendil-works/pi-coding-agent";
17
- import { buildParentContext, extractText } from "./context.js";
15
+ import { extractText } from "./context.js";
18
16
  import { detectEnv } from "./env.js";
19
17
  import { assembleSessionConfig } from "./session-config.js";
20
18
  import { deriveSubagentSessionDir } from "./session-dir.js";
21
- import type { SubagentType, ThinkingLevel } from "./types.js";
19
+ import type { ParentSnapshot, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
22
20
 
23
21
  /** Names of tools registered by this extension that subagents must NOT inherit. */
24
22
  const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
@@ -66,20 +64,13 @@ export function normalizeMaxTurns(n: number | undefined): number | undefined {
66
64
  }
67
65
 
68
66
 
69
- /** Info about a tool event in the subagent. */
70
- export interface ToolActivity {
71
- type: "start" | "end";
72
- toolName: string;
73
- }
74
-
75
67
  export interface RunOptions {
76
- /** ExtensionAPI instanceused for pi.exec() instead of execSync. */
77
- pi: ExtensionAPI;
68
+ /** Shell-exec callback for detectEnv injected from pi.exec(). */
69
+ exec: ShellExec;
78
70
  model?: Model<any>;
79
71
  maxTurns?: number;
80
72
  signal?: AbortSignal;
81
73
  isolated?: boolean;
82
- inheritContext?: boolean;
83
74
  thinkingLevel?: ThinkingLevel;
84
75
  /** Override working directory (e.g. for worktree isolation). */
85
76
  cwd?: string;
@@ -87,31 +78,8 @@ export interface RunOptions {
87
78
  parentSessionFile?: string;
88
79
  /** Session ID of the parent agent (stored in the child session's parentSession header). */
89
80
  parentSessionId?: string;
90
- /** Called on tool start/end with activity info. */
91
- onToolActivity?: (activity: ToolActivity) => void;
92
- /** Called on streaming text deltas from the assistant response. */
93
- onTextDelta?: (delta: string, fullText: string) => void;
81
+ /** Called once after session creation session delivery mechanism. */
94
82
  onSessionCreated?: (session: AgentSession) => void;
95
- /** Called at the end of each agentic turn with the cumulative count. */
96
- onTurnEnd?: (turnCount: number) => void;
97
- /**
98
- * Called once per assistant message_end with that message's usage delta.
99
- * Lets callers maintain a lifetime accumulator that survives compaction
100
- * (which replaces session.state.messages and resets stats-derived sums).
101
- */
102
- onAssistantUsage?: (usage: {
103
- input: number;
104
- output: number;
105
- cacheWrite: number;
106
- }) => void;
107
- /**
108
- * Called when the session successfully compacts. `tokensBefore` is upstream's
109
- * pre-compaction context size estimate. Aborted compactions don't fire.
110
- */
111
- onCompaction?: (info: {
112
- reason: "manual" | "threshold" | "overflow";
113
- tokensBefore: number;
114
- }) => void;
115
83
  /**
116
84
  * Default max turns from runtime config. Falls back to the module-scope
117
85
  * `defaultMaxTurns` during the lift-and-shift migration; superseded by
@@ -138,9 +106,6 @@ export interface RunResult {
138
106
 
139
107
  /** Options for resuming an existing agent session. */
140
108
  export interface ResumeOptions {
141
- onToolActivity?: (activity: ToolActivity) => void;
142
- onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
143
- onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
144
109
  signal?: AbortSignal;
145
110
  }
146
111
 
@@ -149,7 +114,7 @@ export interface ResumeOptions {
149
114
  * SDK session orchestration in runAgent/resumeAgent.
150
115
  */
151
116
  export interface AgentRunner {
152
- run(ctx: ExtensionContext, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
117
+ run(snapshot: ParentSnapshot, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
153
118
  resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string>;
154
119
  }
155
120
 
@@ -199,23 +164,23 @@ function forwardAbortSignal(
199
164
  }
200
165
 
201
166
  export async function runAgent(
202
- ctx: ExtensionContext,
167
+ snapshot: ParentSnapshot,
203
168
  type: SubagentType,
204
169
  prompt: string,
205
170
  options: RunOptions,
206
171
  ): Promise<RunResult> {
207
172
  // Resolve working directory upfront — needed for detectEnv before assembly.
208
- const effectiveCwd = options.cwd ?? ctx.cwd;
209
- const env = await detectEnv(options.pi, effectiveCwd);
173
+ const effectiveCwd = options.cwd ?? snapshot.cwd;
174
+ const env = await detectEnv(options.exec, effectiveCwd);
210
175
 
211
176
  // Assemble session configuration (synchronous, no SDK objects).
212
177
  const cfg = assembleSessionConfig(
213
178
  type,
214
179
  {
215
- cwd: ctx.cwd,
216
- parentSystemPrompt: ctx.getSystemPrompt(),
217
- parentModel: ctx.model,
218
- modelRegistry: ctx.modelRegistry,
180
+ cwd: snapshot.cwd,
181
+ parentSystemPrompt: snapshot.systemPrompt,
182
+ parentModel: snapshot.model,
183
+ modelRegistry: snapshot.modelRegistry,
219
184
  },
220
185
  {
221
186
  cwd: options.cwd,
@@ -259,7 +224,7 @@ export async function runAgent(
259
224
  agentDir,
260
225
  sessionManager,
261
226
  settingsManager: SettingsManager.create(cfg.effectiveCwd, agentDir),
262
- modelRegistry: ctx.modelRegistry,
227
+ modelRegistry: snapshot.modelRegistry as any,
263
228
  model: cfg.model as Model<any> | undefined,
264
229
  tools: cfg.toolNames,
265
230
  resourceLoader: loader,
@@ -287,14 +252,7 @@ export async function runAgent(
287
252
  // (e.g. loading credentials, setting up state). Placed after tool filtering
288
253
  // so extension-provided skills/prompts from extendResourcesFromExtensions()
289
254
  // respect the active tool set. All ExtensionBindings fields are optional.
290
- await session.bindExtensions({
291
- onError: (err) => {
292
- options.onToolActivity?.({
293
- type: "end",
294
- toolName: `extension-error:${err.extensionPath}`,
295
- });
296
- },
297
- });
255
+ await session.bindExtensions({});
298
256
 
299
257
  // Patch 2 (RepOne #443): re-filter active tools after bindExtensions.
300
258
  // Extension-registered tools (added during bindExtensions) are not in the
@@ -322,11 +280,9 @@ export async function runAgent(
322
280
  let softLimitReached = false;
323
281
  let aborted = false;
324
282
 
325
- let currentMessageText = "";
326
283
  const unsubTurns = session.subscribe((event: AgentSessionEvent) => {
327
284
  if (event.type === "turn_end") {
328
285
  turnCount++;
329
- options.onTurnEnd?.(turnCount);
330
286
  if (maxTurns != null) {
331
287
  if (!softLimitReached && turnCount >= maxTurns) {
332
288
  softLimitReached = true;
@@ -339,52 +295,15 @@ export async function runAgent(
339
295
  }
340
296
  }
341
297
  }
342
- if (event.type === "message_start") {
343
- currentMessageText = "";
344
- }
345
- if (
346
- event.type === "message_update" &&
347
- event.assistantMessageEvent.type === "text_delta"
348
- ) {
349
- currentMessageText += event.assistantMessageEvent.delta;
350
- options.onTextDelta?.(
351
- event.assistantMessageEvent.delta,
352
- currentMessageText,
353
- );
354
- }
355
- if (event.type === "tool_execution_start") {
356
- options.onToolActivity?.({ type: "start", toolName: event.toolName });
357
- }
358
- if (event.type === "tool_execution_end") {
359
- options.onToolActivity?.({ type: "end", toolName: event.toolName });
360
- }
361
- if (event.type === "message_end" && event.message.role === "assistant") {
362
- const u = (event.message as any).usage;
363
- if (u)
364
- options.onAssistantUsage?.({
365
- input: u.input ?? 0,
366
- output: u.output ?? 0,
367
- cacheWrite: u.cacheWrite ?? 0,
368
- });
369
- }
370
- if (event.type === "compaction_end" && !event.aborted && event.result) {
371
- options.onCompaction?.({
372
- reason: event.reason,
373
- tokensBefore: event.result.tokensBefore,
374
- });
375
- }
376
298
  });
377
299
 
378
300
  const collector = collectResponseText(session);
379
301
  const cleanupAbort = forwardAbortSignal(session, options.signal);
380
302
 
381
- // Build the effective prompt: optionally prepend parent context
303
+ // Prepend parent context if it was captured at spawn time
382
304
  let effectivePrompt = prompt;
383
- if (options.inheritContext) {
384
- const parentContext = buildParentContext(ctx);
385
- if (parentContext) {
386
- effectivePrompt = parentContext + prompt;
387
- }
305
+ if (snapshot.parentContext) {
306
+ effectivePrompt = snapshot.parentContext + prompt;
388
307
  }
389
308
 
390
309
  try {
@@ -417,46 +336,10 @@ export async function resumeAgent(
417
336
  const collector = collectResponseText(session);
418
337
  const cleanupAbort = forwardAbortSignal(session, options.signal);
419
338
 
420
- const unsubEvents =
421
- options.onToolActivity || options.onAssistantUsage || options.onCompaction
422
- ? session.subscribe((event: AgentSessionEvent) => {
423
- if (event.type === "tool_execution_start")
424
- options.onToolActivity?.({
425
- type: "start",
426
- toolName: event.toolName,
427
- });
428
- if (event.type === "tool_execution_end")
429
- options.onToolActivity?.({ type: "end", toolName: event.toolName });
430
- if (
431
- event.type === "message_end" &&
432
- event.message.role === "assistant"
433
- ) {
434
- const u = (event.message as any).usage;
435
- if (u)
436
- options.onAssistantUsage?.({
437
- input: u.input ?? 0,
438
- output: u.output ?? 0,
439
- cacheWrite: u.cacheWrite ?? 0,
440
- });
441
- }
442
- if (
443
- event.type === "compaction_end" &&
444
- !event.aborted &&
445
- event.result
446
- ) {
447
- options.onCompaction?.({
448
- reason: event.reason,
449
- tokensBefore: event.result.tokensBefore,
450
- });
451
- }
452
- })
453
- : () => {};
454
-
455
339
  try {
456
340
  await session.prompt(prompt);
457
341
  } finally {
458
342
  collector.unsubscribe();
459
- unsubEvents();
460
343
  cleanupAbort();
461
344
  }
462
345
 
package/src/env.ts CHANGED
@@ -2,16 +2,15 @@
2
2
  * env.ts — Detect environment info (git, platform) for subagent system prompts.
3
3
  */
4
4
 
5
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
5
  import { debugLog } from "./debug.js";
7
- import type { EnvInfo } from "./types.js";
6
+ import type { EnvInfo, ShellExec } from "./types.js";
8
7
 
9
- export async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo> {
8
+ export async function detectEnv(exec: ShellExec, cwd: string): Promise<EnvInfo> {
10
9
  let isGitRepo = false;
11
10
  let branch = "";
12
11
 
13
12
  try {
14
- const result = await pi.exec("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 5000 });
13
+ const result = await exec("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 5000 });
15
14
  isGitRepo = result.code === 0 && result.stdout.trim() === "true";
16
15
  } catch (err) {
17
16
  debugLog("git rev-parse", err);
@@ -19,7 +18,7 @@ export async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo>
19
18
 
20
19
  if (isGitRepo) {
21
20
  try {
22
- const result = await pi.exec("git", ["branch", "--show-current"], { cwd, timeout: 5000 });
21
+ const result = await exec("git", ["branch", "--show-current"], { cwd, timeout: 5000 });
23
22
  branch = result.code === 0 ? result.stdout.trim() : "unknown";
24
23
  } catch (err) {
25
24
  debugLog("git branch", err);
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ export default function (pi: ExtensionAPI) {
66
66
  const manager = new AgentManager({
67
67
  runner: { run: runAgent, resume: resumeAgent },
68
68
  worktrees: new GitWorktreeManager(process.cwd()),
69
+ exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
69
70
  onComplete: (record) => {
70
71
  // Emit lifecycle event based on terminal status
71
72
  const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
@@ -185,8 +186,8 @@ export default function (pi: ExtensionAPI) {
185
186
 
186
187
  pi.registerTool(defineTool(createAgentTool({
187
188
  manager: {
188
- spawn: (ctx, type, prompt, opts) => manager.spawn(pi, ctx, type, prompt, opts),
189
- spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(pi, ctx, type, prompt, opts),
189
+ spawn: (ctx, type, prompt, opts) => manager.spawn(ctx, type, prompt, opts),
190
+ spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(ctx, type, prompt, opts),
190
191
  resume: (id, prompt, signal) => manager.resume(id, prompt, signal),
191
192
  getRecord: (id) => manager.getRecord(id),
192
193
  getMaxConcurrent: () => manager.getMaxConcurrent(),
@@ -229,7 +230,7 @@ export default function (pi: ExtensionAPI) {
229
230
  manager: {
230
231
  listAgents: () => manager.listAgents(),
231
232
  getRecord: (id) => manager.getRecord(id),
232
- spawnAndWait: (piArg, ctx, type, prompt, opts) => manager.spawnAndWait(piArg ?? pi, ctx, type, prompt, opts),
233
+ spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(ctx, type, prompt, opts),
233
234
  getMaxConcurrent: () => manager.getMaxConcurrent(),
234
235
  setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
235
236
  },
@@ -0,0 +1,27 @@
1
+ /**
2
+ * parent-snapshot.ts — Capture parent session state as a plain data snapshot.
3
+ */
4
+
5
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+ import { buildParentContext } from "./context.js";
7
+ import type { ParentSnapshot } from "./types.js";
8
+
9
+ /**
10
+ * Build an immutable snapshot of the parent session state.
11
+ *
12
+ * Called once at spawn time so queued agents capture state as it existed
13
+ * when the user requested the agent, not when a queue slot opens.
14
+ */
15
+ export function buildParentSnapshot(
16
+ ctx: ExtensionContext,
17
+ inheritContext?: boolean,
18
+ ): ParentSnapshot {
19
+ const parentContext = inheritContext ? buildParentContext(ctx) : undefined;
20
+ return {
21
+ cwd: ctx.cwd,
22
+ systemPrompt: ctx.getSystemPrompt(),
23
+ model: ctx.model,
24
+ modelRegistry: ctx.modelRegistry,
25
+ parentContext: parentContext || undefined,
26
+ };
27
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * record-observer.ts — Subscribes to session events and updates AgentRecord stats.
3
+ *
4
+ * Replaces the scattered callback-wrapping logic in AgentManager's startAgent()
5
+ * and resume() with a single direct subscription.
6
+ */
7
+
8
+ import type { CompactionInfo } from "./agent-manager.js";
9
+ import type { AgentRecord } from "./agent-record.js";
10
+ import { addUsage } from "./usage.js";
11
+
12
+ /** Narrow session interface — only the subscribe method needed by the observer. */
13
+ interface SubscribableSession {
14
+ subscribe(fn: (event: any) => void): () => void;
15
+ }
16
+
17
+ export interface RecordObserverOptions {
18
+ onCompact?: (record: AgentRecord, info: CompactionInfo) => void;
19
+ }
20
+
21
+ /**
22
+ * Subscribe to session events and accumulate stats on the agent record.
23
+ *
24
+ * Handles:
25
+ * - `tool_execution_end` → `record.toolUses++`
26
+ * - `message_end` (assistant, with usage) → `addUsage(record.lifetimeUsage, …)`
27
+ * - `compaction_end` (not aborted) → `record.compactionCount++`, call `onCompact`
28
+ *
29
+ * @returns An unsubscribe function.
30
+ */
31
+ export function subscribeRecordObserver(
32
+ session: SubscribableSession,
33
+ record: AgentRecord,
34
+ options?: RecordObserverOptions,
35
+ ): () => void {
36
+ return session.subscribe((event: any) => {
37
+ if (event.type === "tool_execution_end") {
38
+ record.toolUses++;
39
+ }
40
+
41
+ if (event.type === "message_end" && event.message?.role === "assistant") {
42
+ const u = event.message.usage;
43
+ if (u) {
44
+ addUsage(record.lifetimeUsage, {
45
+ input: u.input ?? 0,
46
+ output: u.output ?? 0,
47
+ cacheWrite: u.cacheWrite ?? 0,
48
+ });
49
+ }
50
+ }
51
+
52
+ if (event.type === "compaction_end" && !event.aborted && event.result) {
53
+ record.compactionCount++;
54
+ options?.onCompact?.(record, {
55
+ reason: event.reason,
56
+ tokensBefore: event.result.tokensBefore,
57
+ });
58
+ }
59
+ });
60
+ }
@@ -11,7 +11,7 @@ import type { AgentRecord } from "./types.js";
11
11
 
12
12
  /** Narrow interface for the AgentManager — avoids coupling to the concrete class. */
13
13
  export interface AgentManagerLike {
14
- spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: unknown): string;
14
+ spawn(ctx: unknown, type: string, prompt: string, options: unknown): string;
15
15
  getRecord(id: string): AgentRecord | undefined;
16
16
  listAgents(): AgentRecord[];
17
17
  abort(id: string): boolean;
@@ -54,7 +54,7 @@ export function createSubagentsService(deps: AdapterDeps): SubagentsService {
54
54
  const description = options?.description ?? prompt.slice(0, 80);
55
55
  const isBackground = !(options?.foreground ?? false);
56
56
 
57
- return manager.spawn(session.pi, session.ctx, type, prompt, {
57
+ return manager.spawn(session.ctx, type, prompt, {
58
58
  description,
59
59
  model,
60
60
  maxTurns: options?.maxTurns,