@gotgenes/pi-subagents 6.0.1 → 6.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.
@@ -8,11 +8,13 @@
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
+ import { AgentRecord } from "./agent-record.js";
12
13
  import type { AgentRunner, ToolActivity } from "./agent-runner.js";
13
14
  import { debugLog } from "./debug.js";
15
+ import { buildParentSnapshot } from "./parent-snapshot.js";
14
16
  import type { RunConfig } from "./runtime.js";
15
- import type { AgentInvocation, AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
17
+ import type { AgentInvocation, IsolationMode, ParentSnapshot, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
16
18
  import { addUsage } from "./usage.js";
17
19
  import type { WorktreeManager } from "./worktree.js";
18
20
 
@@ -27,6 +29,7 @@ const DEFAULT_MAX_CONCURRENT = 4;
27
29
  export interface AgentManagerOptions {
28
30
  runner: AgentRunner;
29
31
  worktrees: WorktreeManager;
32
+ exec: ShellExec;
30
33
  maxConcurrent?: number;
31
34
  getRunConfig?: () => RunConfig;
32
35
  onStart?: OnAgentStart;
@@ -35,8 +38,7 @@ export interface AgentManagerOptions {
35
38
  }
36
39
 
37
40
  interface SpawnArgs {
38
- pi: ExtensionAPI;
39
- ctx: ExtensionContext;
41
+ snapshot: ParentSnapshot;
40
42
  type: SubagentType;
41
43
  prompt: string;
42
44
  options: SpawnOptions;
@@ -88,6 +90,7 @@ export class AgentManager {
88
90
  private onCompact?: OnAgentCompact;
89
91
  private readonly runner: AgentRunner;
90
92
  private readonly worktrees: WorktreeManager;
93
+ private readonly exec: ShellExec;
91
94
  private maxConcurrent: number;
92
95
  private getRunConfig?: () => RunConfig;
93
96
 
@@ -99,6 +102,7 @@ export class AgentManager {
99
102
  constructor(options: AgentManagerOptions) {
100
103
  this.runner = options.runner;
101
104
  this.worktrees = options.worktrees;
105
+ this.exec = options.exec;
102
106
  this.onComplete = options.onComplete;
103
107
  this.onStart = options.onStart;
104
108
  this.onCompact = options.onCompact;
@@ -125,7 +129,6 @@ export class AgentManager {
125
129
  * If the concurrency limit is reached, the agent is queued.
126
130
  */
127
131
  spawn(
128
- pi: ExtensionAPI,
129
132
  ctx: ExtensionContext,
130
133
  type: SubagentType,
131
134
  prompt: string,
@@ -133,21 +136,19 @@ export class AgentManager {
133
136
  ): string {
134
137
  const id = randomUUID().slice(0, 17);
135
138
  const abortController = new AbortController();
136
- const record: AgentRecord = {
139
+ const record = new AgentRecord({
137
140
  id,
138
141
  type,
139
142
  description: options.description,
140
143
  status: options.isBackground ? "queued" : "running",
141
- toolUses: 0,
142
144
  startedAt: Date.now(),
143
145
  abortController,
144
- lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
145
- compactionCount: 0,
146
146
  invocation: options.invocation,
147
- };
147
+ });
148
148
  this.agents.set(id, record);
149
149
 
150
- const args: SpawnArgs = { pi, ctx, type, prompt, options };
150
+ const snapshot = buildParentSnapshot(ctx, options.inheritContext);
151
+ const args: SpawnArgs = { snapshot, type, prompt, options };
151
152
 
152
153
  if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
153
154
  // Queue it — will be started when a running agent completes
@@ -167,7 +168,7 @@ export class AgentManager {
167
168
  }
168
169
 
169
170
  /** Actually start an agent (called immediately or from queue drain). */
170
- private startAgent(id: string, record: AgentRecord, { pi, ctx, type, prompt, options }: SpawnArgs) {
171
+ private startAgent(id: string, record: AgentRecord, { snapshot, type, prompt, options }: SpawnArgs) {
171
172
  // Worktree isolation: try to create a temporary git worktree. Strict —
172
173
  // fail loud if not possible (no silent fallback to main tree). Done
173
174
  // BEFORE state mutation so a throw doesn't leave the record half-running.
@@ -184,8 +185,7 @@ export class AgentManager {
184
185
  worktreeCwd = wt.path;
185
186
  }
186
187
 
187
- record.status = "running";
188
- record.startedAt = Date.now();
188
+ record.markRunning(Date.now());
189
189
  if (options.isBackground) this.runningBackground++;
190
190
  this.onStart?.(record);
191
191
 
@@ -199,14 +199,13 @@ export class AgentManager {
199
199
  const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
200
200
 
201
201
  const runConfig = this.getRunConfig?.();
202
- const promise = this.runner.run(ctx, type, prompt, {
203
- pi,
202
+ const promise = this.runner.run(snapshot, type, prompt, {
203
+ exec: this.exec,
204
204
  model: options.model,
205
205
  maxTurns: options.maxTurns,
206
206
  defaultMaxTurns: runConfig?.defaultMaxTurns,
207
207
  graceTurns: runConfig?.graceTurns,
208
208
  isolated: options.isolated,
209
- inheritContext: options.inheritContext,
210
209
  thinkingLevel: options.thinkingLevel,
211
210
  cwd: worktreeCwd,
212
211
  parentSessionFile: options.parentSessionFile,
@@ -244,27 +243,26 @@ export class AgentManager {
244
243
  },
245
244
  })
246
245
  .then(({ responseText, session, aborted, steered, sessionFile }) => {
247
- // Don't overwrite status if externally stopped via abort()
248
- if (record.status !== "stopped") {
249
- record.status = aborted ? "aborted" : steered ? "steered" : "completed";
250
- }
251
- record.result = responseText;
252
- record.session = session;
253
- record.completedAt ??= Date.now();
254
- if (sessionFile) record.outputFile = sessionFile;
255
-
256
246
  detach();
257
247
 
258
- // Clean up worktree if used
248
+ // Clean up worktree before transition so the final result includes branch text
249
+ let finalResult = responseText;
259
250
  if (record.worktree) {
260
251
  const wtResult = this.worktrees.cleanup(record.worktree, options.description);
261
252
  record.worktreeResult = wtResult;
262
253
  if (wtResult.hasChanges && wtResult.branch) {
263
- record.result = (record.result ?? "") +
264
- `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
254
+ finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
265
255
  }
266
256
  }
267
257
 
258
+ // Transition — guards against overwriting externally-stopped status
259
+ if (aborted) record.markAborted(finalResult);
260
+ else if (steered) record.markSteered(finalResult);
261
+ else record.markCompleted(finalResult);
262
+
263
+ record.session = session;
264
+ if (sessionFile) record.outputFile = sessionFile;
265
+
268
266
  if (options.isBackground) {
269
267
  this.runningBackground--;
270
268
  try { this.onComplete?.(record); } catch (err) { debugLog("onComplete callback", err); }
@@ -273,17 +271,10 @@ export class AgentManager {
273
271
  return responseText;
274
272
  })
275
273
  .catch((err) => {
276
- // Don't overwrite status if externally stopped via abort()
277
- if (record.status !== "stopped") {
278
- record.status = "error";
279
- }
280
- record.error = err instanceof Error ? err.message : String(err);
281
- record.completedAt ??= Date.now();
274
+ record.markError(err);
282
275
 
283
276
  detach();
284
277
 
285
-
286
-
287
278
  // Best-effort worktree cleanup on error
288
279
  if (record.worktree) {
289
280
  try {
@@ -314,9 +305,7 @@ export class AgentManager {
314
305
  } catch (err) {
315
306
  // Late failure (e.g. strict worktree-isolation) — surface on the record
316
307
  // so the user/agent can see it via /agents, then keep draining.
317
- record.status = "error";
318
- record.error = err instanceof Error ? err.message : String(err);
319
- record.completedAt = Date.now();
308
+ record.markError(err);
320
309
  this.onComplete?.(record);
321
310
  }
322
311
  }
@@ -327,13 +316,12 @@ export class AgentManager {
327
316
  * Foreground agents bypass the concurrency queue.
328
317
  */
329
318
  async spawnAndWait(
330
- pi: ExtensionAPI,
331
319
  ctx: ExtensionContext,
332
320
  type: SubagentType,
333
321
  prompt: string,
334
322
  options: Omit<SpawnOptions, "isBackground">,
335
323
  ): Promise<AgentRecord> {
336
- const id = this.spawn(pi, ctx, type, prompt, { ...options, isBackground: false });
324
+ const id = this.spawn(ctx, type, prompt, { ...options, isBackground: false });
337
325
  const record = this.agents.get(id)!;
338
326
  await record.promise;
339
327
  return record;
@@ -350,11 +338,7 @@ export class AgentManager {
350
338
  const record = this.agents.get(id);
351
339
  if (!record?.session) return undefined;
352
340
 
353
- record.status = "running";
354
- record.startedAt = Date.now();
355
- record.completedAt = undefined;
356
- record.result = undefined;
357
- record.error = undefined;
341
+ record.resetForResume(Date.now());
358
342
 
359
343
  try {
360
344
  const responseText = await this.runner.resume(record.session, prompt, {
@@ -370,13 +354,9 @@ export class AgentManager {
370
354
  },
371
355
  signal,
372
356
  });
373
- record.status = "completed";
374
- record.result = responseText;
375
- record.completedAt = Date.now();
357
+ record.markCompleted(responseText);
376
358
  } catch (err) {
377
- record.status = "error";
378
- record.error = err instanceof Error ? err.message : String(err);
379
- record.completedAt = Date.now();
359
+ record.markError(err);
380
360
  }
381
361
 
382
362
  return record;
@@ -399,15 +379,13 @@ export class AgentManager {
399
379
  // Remove from queue if queued
400
380
  if (record.status === "queued") {
401
381
  this.queue = this.queue.filter(q => q.id !== id);
402
- record.status = "stopped";
403
- record.completedAt = Date.now();
382
+ record.markStopped();
404
383
  return true;
405
384
  }
406
385
 
407
386
  if (record.status !== "running") return false;
408
387
  record.abortController?.abort();
409
- record.status = "stopped";
410
- record.completedAt = Date.now();
388
+ record.markStopped();
411
389
  return true;
412
390
  }
413
391
 
@@ -452,8 +430,7 @@ export class AgentManager {
452
430
  for (const queued of this.queue) {
453
431
  const record = this.agents.get(queued.id);
454
432
  if (record) {
455
- record.status = "stopped";
456
- record.completedAt = Date.now();
433
+ record.markStopped();
457
434
  count++;
458
435
  }
459
436
  }
@@ -462,8 +439,7 @@ export class AgentManager {
462
439
  for (const record of this.agents.values()) {
463
440
  if (record.status === "running") {
464
441
  record.abortController?.abort();
465
- record.status = "stopped";
466
- record.completedAt = Date.now();
442
+ record.markStopped();
467
443
  count++;
468
444
  }
469
445
  }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * agent-record.ts — AgentRecord class with encapsulated status-transition logic.
3
+ *
4
+ * Status transitions (status, result, error, startedAt, completedAt) are owned
5
+ * by the class and exposed via transition methods. External code reads these
6
+ * fields through public properties but cannot write them directly.
7
+ *
8
+ * Non-transition state (session, toolUses, lifetimeUsage, etc.) remains public.
9
+ */
10
+
11
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
12
+ import type { AgentInvocation, SubagentType } from "./types.js";
13
+ import type { LifetimeUsage } from "./usage.js";
14
+
15
+ export type AgentRecordStatus =
16
+ | "queued"
17
+ | "running"
18
+ | "completed"
19
+ | "steered"
20
+ | "aborted"
21
+ | "stopped"
22
+ | "error";
23
+
24
+ export interface AgentRecordInit {
25
+ id: string;
26
+ type: SubagentType;
27
+ description: string;
28
+ status?: AgentRecordStatus;
29
+ startedAt?: number;
30
+ completedAt?: number;
31
+ result?: string;
32
+ error?: string;
33
+ toolUses?: number;
34
+ lifetimeUsage?: LifetimeUsage;
35
+ compactionCount?: number;
36
+ abortController?: AbortController;
37
+ invocation?: AgentInvocation;
38
+ session?: AgentSession;
39
+ promise?: Promise<string>;
40
+ resultConsumed?: boolean;
41
+ pendingSteers?: string[];
42
+ worktree?: { path: string; branch: string };
43
+ worktreeResult?: { hasChanges: boolean; branch?: string };
44
+ toolCallId?: string;
45
+ outputFile?: string;
46
+ }
47
+
48
+ export class AgentRecord {
49
+ // Identity — set once at construction
50
+ readonly id: string;
51
+ readonly type: SubagentType;
52
+ readonly description: string;
53
+ readonly invocation?: AgentInvocation;
54
+
55
+ // Transition state — encapsulated behind getters, mutated only via transition methods
56
+ private _status: AgentRecordStatus;
57
+ get status(): AgentRecordStatus { return this._status; }
58
+
59
+ private _result?: string;
60
+ get result(): string | undefined { return this._result; }
61
+
62
+ private _error?: string;
63
+ get error(): string | undefined { return this._error; }
64
+
65
+ private _startedAt: number;
66
+ get startedAt(): number { return this._startedAt; }
67
+
68
+ private _completedAt?: number;
69
+ get completedAt(): number | undefined { return this._completedAt; }
70
+
71
+ // Non-transition mutable state
72
+ toolUses: number;
73
+ lifetimeUsage: LifetimeUsage;
74
+ compactionCount: number;
75
+ session?: AgentSession;
76
+ abortController?: AbortController;
77
+ promise?: Promise<string>;
78
+ resultConsumed?: boolean;
79
+ pendingSteers?: string[];
80
+ worktree?: { path: string; branch: string };
81
+ worktreeResult?: { hasChanges: boolean; branch?: string };
82
+ toolCallId?: string;
83
+ outputFile?: string;
84
+
85
+ constructor(init: AgentRecordInit) {
86
+ this.id = init.id;
87
+ this.type = init.type;
88
+ this.description = init.description;
89
+ this.invocation = init.invocation;
90
+
91
+ this._status = init.status ?? "queued";
92
+ this._result = init.result;
93
+ this._error = init.error;
94
+ this._startedAt = init.startedAt ?? Date.now();
95
+ this._completedAt = init.completedAt;
96
+
97
+ this.toolUses = init.toolUses ?? 0;
98
+ this.lifetimeUsage = init.lifetimeUsage ?? { input: 0, output: 0, cacheWrite: 0 };
99
+ this.compactionCount = init.compactionCount ?? 0;
100
+ this.abortController = init.abortController;
101
+ this.session = init.session;
102
+ this.promise = init.promise;
103
+ this.resultConsumed = init.resultConsumed;
104
+ this.pendingSteers = init.pendingSteers;
105
+ this.worktree = init.worktree;
106
+ this.worktreeResult = init.worktreeResult;
107
+ this.toolCallId = init.toolCallId;
108
+ this.outputFile = init.outputFile;
109
+ }
110
+
111
+ /** Transition to running state. Sets status and startedAt. */
112
+ markRunning(startedAt: number): void {
113
+ this._status = "running";
114
+ this._startedAt = startedAt;
115
+ }
116
+
117
+ /**
118
+ * Transition to completed state.
119
+ * Always sets result and completedAt (??=). Only changes status if not stopped.
120
+ */
121
+ markCompleted(result: string, completedAt?: number): void {
122
+ this._result = result;
123
+ this._completedAt ??= completedAt ?? Date.now();
124
+ if (this._status !== "stopped") {
125
+ this._status = "completed";
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Transition to aborted state.
131
+ * Always sets result and completedAt (??=). Only changes status if not stopped.
132
+ */
133
+ markAborted(result: string, completedAt?: number): void {
134
+ this._result = result;
135
+ this._completedAt ??= completedAt ?? Date.now();
136
+ if (this._status !== "stopped") {
137
+ this._status = "aborted";
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Transition to steered state.
143
+ * Always sets result and completedAt (??=). Only changes status if not stopped.
144
+ */
145
+ markSteered(result: string, completedAt?: number): void {
146
+ this._result = result;
147
+ this._completedAt ??= completedAt ?? Date.now();
148
+ if (this._status !== "stopped") {
149
+ this._status = "steered";
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Transition to error state.
155
+ * Always sets error (formatted) and completedAt (??=). Only changes status if not stopped.
156
+ */
157
+ markError(error: unknown, completedAt?: number): void {
158
+ this._error = error instanceof Error ? error.message : String(error);
159
+ this._completedAt ??= completedAt ?? Date.now();
160
+ if (this._status !== "stopped") {
161
+ this._status = "error";
162
+ }
163
+ }
164
+
165
+ /** Transition to stopped state. Always valid — no guard. */
166
+ markStopped(completedAt?: number): void {
167
+ this._status = "stopped";
168
+ this._completedAt = completedAt ?? Date.now();
169
+ }
170
+
171
+ /** Reset for resume: running status, new startedAt, clear completedAt/result/error. */
172
+ resetForResume(startedAt: number): void {
173
+ this._status = "running";
174
+ this._startedAt = startedAt;
175
+ this._completedAt = undefined;
176
+ this._result = undefined;
177
+ this._error = undefined;
178
+ }
179
+ }
@@ -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"];
@@ -73,13 +71,12 @@ export interface ToolActivity {
73
71
  }
74
72
 
75
73
  export interface RunOptions {
76
- /** ExtensionAPI instanceused for pi.exec() instead of execSync. */
77
- pi: ExtensionAPI;
74
+ /** Shell-exec callback for detectEnv injected from pi.exec(). */
75
+ exec: ShellExec;
78
76
  model?: Model<any>;
79
77
  maxTurns?: number;
80
78
  signal?: AbortSignal;
81
79
  isolated?: boolean;
82
- inheritContext?: boolean;
83
80
  thinkingLevel?: ThinkingLevel;
84
81
  /** Override working directory (e.g. for worktree isolation). */
85
82
  cwd?: string;
@@ -149,7 +146,7 @@ export interface ResumeOptions {
149
146
  * SDK session orchestration in runAgent/resumeAgent.
150
147
  */
151
148
  export interface AgentRunner {
152
- run(ctx: ExtensionContext, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
149
+ run(snapshot: ParentSnapshot, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
153
150
  resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string>;
154
151
  }
155
152
 
@@ -199,23 +196,23 @@ function forwardAbortSignal(
199
196
  }
200
197
 
201
198
  export async function runAgent(
202
- ctx: ExtensionContext,
199
+ snapshot: ParentSnapshot,
203
200
  type: SubagentType,
204
201
  prompt: string,
205
202
  options: RunOptions,
206
203
  ): Promise<RunResult> {
207
204
  // Resolve working directory upfront — needed for detectEnv before assembly.
208
- const effectiveCwd = options.cwd ?? ctx.cwd;
209
- const env = await detectEnv(options.pi, effectiveCwd);
205
+ const effectiveCwd = options.cwd ?? snapshot.cwd;
206
+ const env = await detectEnv(options.exec, effectiveCwd);
210
207
 
211
208
  // Assemble session configuration (synchronous, no SDK objects).
212
209
  const cfg = assembleSessionConfig(
213
210
  type,
214
211
  {
215
- cwd: ctx.cwd,
216
- parentSystemPrompt: ctx.getSystemPrompt(),
217
- parentModel: ctx.model,
218
- modelRegistry: ctx.modelRegistry,
212
+ cwd: snapshot.cwd,
213
+ parentSystemPrompt: snapshot.systemPrompt,
214
+ parentModel: snapshot.model,
215
+ modelRegistry: snapshot.modelRegistry,
219
216
  },
220
217
  {
221
218
  cwd: options.cwd,
@@ -259,7 +256,7 @@ export async function runAgent(
259
256
  agentDir,
260
257
  sessionManager,
261
258
  settingsManager: SettingsManager.create(cfg.effectiveCwd, agentDir),
262
- modelRegistry: ctx.modelRegistry,
259
+ modelRegistry: snapshot.modelRegistry as any,
263
260
  model: cfg.model as Model<any> | undefined,
264
261
  tools: cfg.toolNames,
265
262
  resourceLoader: loader,
@@ -378,13 +375,10 @@ export async function runAgent(
378
375
  const collector = collectResponseText(session);
379
376
  const cleanupAbort = forwardAbortSignal(session, options.signal);
380
377
 
381
- // Build the effective prompt: optionally prepend parent context
378
+ // Prepend parent context if it was captured at spawn time
382
379
  let effectivePrompt = prompt;
383
- if (options.inheritContext) {
384
- const parentContext = buildParentContext(ctx);
385
- if (parentContext) {
386
- effectivePrompt = parentContext + prompt;
387
- }
380
+ if (snapshot.parentContext) {
381
+ effectivePrompt = snapshot.parentContext + prompt;
388
382
  }
389
383
 
390
384
  try {
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
+ }
@@ -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,