pi-fast-subagent 0.9.3 → 0.9.4

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.
Files changed (3) hide show
  1. package/index.ts +1 -3
  2. package/package.json +1 -1
  3. package/runner.ts +25 -35
package/index.ts CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  } from "./format.js";
28
28
  import { defaultLoaderPool } from "./loader-pool.js";
29
29
  import { renderSubagentCall, renderSubagentResult } from "./render.js";
30
- import { getCurrentDepth, mapConcurrent, runAgent } from "./runner.js";
30
+ import { mapConcurrent, runAgent } from "./runner.js";
31
31
  import { SubagentParams } from "./schemas.js";
32
32
  import type { AgentRowStatus, OnUpdate, RunResult, SubagentDetails, ToolCallEntry } from "./types.js";
33
33
 
@@ -532,7 +532,6 @@ export default function (pi: ExtensionAPI) {
532
532
 
533
533
  emitParallel(true);
534
534
 
535
- const parentDepth = getCurrentDepth();
536
535
  const allResults = await mapConcurrent(expanded, concurrency, async (t, i) => {
537
536
  parallelAgents[i]!.status = "running";
538
537
  emitParallel(true);
@@ -556,7 +555,6 @@ export default function (pi: ExtensionAPI) {
556
555
  t.model,
557
556
  signal,
558
557
  agentOnUpdate,
559
- parentDepth,
560
558
  );
561
559
  parallelAgents[i]!.status = result.exitCode === 0 ? "done" : "error";
562
560
  parallelAgents[i]!.durMs = Date.now() - agentStart;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-subagent",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "In-process subagent delegation for pi with single, parallel, and background modes",
5
5
  "type": "module",
6
6
  "keywords": [
package/runner.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  * via `onUpdate`, and enforces the per-agent `maxDepth` gate on nested calls.
6
6
  */
7
7
 
8
+ import { AsyncLocalStorage } from "node:async_hooks";
9
+
8
10
  import {
9
11
  AuthStorage,
10
12
  createAgentSession,
@@ -32,8 +34,6 @@ export function getAuth() {
32
34
  // ─── Depth gating ────────────────────────────────────────────────────────────
33
35
 
34
36
  export const DEFAULT_MAX_DEPTH = 0;
35
- export const DEPTH_ENV = "PI_FAST_SUBAGENT_DEPTH";
36
- export const MAX_DEPTH_ENV = "PI_FAST_SUBAGENT_MAX_DEPTH";
37
37
 
38
38
  /**
39
39
  * Pure helper: given the current nesting depth and the allowed max depth,
@@ -53,13 +53,22 @@ export function checkDepthGate(depth: number, maxDepth: number): { allowed: bool
53
53
  return { allowed: true };
54
54
  }
55
55
 
56
- // Module-level depth counters for nested in-process subagent calls.
57
- let _currentDepth = 0;
58
- let _currentMaxDepth = DEFAULT_MAX_DEPTH;
56
+ export interface DepthState {
57
+ depth: number;
58
+ maxDepth: number;
59
+ }
60
+
61
+ const TOP_LEVEL_DEPTH: DepthState = { depth: 0, maxDepth: DEFAULT_MAX_DEPTH };
62
+ const _depthContext = new AsyncLocalStorage<DepthState>();
63
+
64
+ /** Read the depth/maxDepth in the current async context, or top-level defaults if none. */
65
+ export function getDepthState(): DepthState {
66
+ return _depthContext.getStore() ?? TOP_LEVEL_DEPTH;
67
+ }
59
68
 
60
- /** Read-only accessors for the current in-flight depth (used by parallel mode). */
61
- export function getCurrentDepth(): number {
62
- return _currentDepth;
69
+ /** Run `fn` with `state` set as the current async-scoped depth context. */
70
+ export function runWithDepth<T>(state: DepthState, fn: () => Promise<T>): Promise<T> {
71
+ return _depthContext.run(state, fn);
63
72
  }
64
73
 
65
74
  // ─── runAgent ────────────────────────────────────────────────────────────────
@@ -75,12 +84,11 @@ export async function runAgent(
75
84
  modelOverride: string | undefined,
76
85
  signal: AbortSignal | undefined,
77
86
  onUpdate: OnUpdate | undefined,
78
- parentDepth?: number,
79
87
  deps: RunAgentDeps = {},
80
88
  ): Promise<RunResult> {
81
89
  const pool = deps.loaderPool ?? defaultLoaderPool;
82
- const depth = parentDepth ?? _currentDepth;
83
- const gate = checkDepthGate(depth, _currentMaxDepth);
90
+ const { depth, maxDepth } = getDepthState();
91
+ const gate = checkDepthGate(depth, maxDepth);
84
92
  if (!gate.allowed) {
85
93
  return {
86
94
  output: "",
@@ -114,8 +122,6 @@ export async function runAgent(
114
122
  });
115
123
  await allowUiPaint(coldLoader);
116
124
 
117
- const createPrevEnvDepth = process.env[DEPTH_ENV];
118
- process.env[DEPTH_ENV] = String(depth + 1);
119
125
  const loaderLease = await pool.acquire(
120
126
  cwd,
121
127
  agentDir,
@@ -136,8 +142,6 @@ export async function runAgent(
136
142
  session = created.session;
137
143
  } catch (e) {
138
144
  loaderLease.release();
139
- if (createPrevEnvDepth === undefined) delete process.env[DEPTH_ENV];
140
- else process.env[DEPTH_ENV] = createPrevEnvDepth;
141
145
  return {
142
146
  output: "",
143
147
  exitCode: 1,
@@ -146,8 +150,6 @@ export async function runAgent(
146
150
  usage: { input: 0, output: 0, cost: 0, turns: 0 },
147
151
  };
148
152
  }
149
- if (createPrevEnvDepth === undefined) delete process.env[DEPTH_ENV];
150
- else process.env[DEPTH_ENV] = createPrevEnvDepth;
151
153
 
152
154
  // Resolve and apply model
153
155
  const modelStr = modelOverride ?? agent.model;
@@ -308,17 +310,11 @@ export async function runAgent(
308
310
  });
309
311
  });
310
312
 
311
- // Propagate depth to nested calls. `maxDepth` is per-agent and defaults to 0,
312
- // so subagents cannot spawn subagents unless their frontmatter opts in.
313
- const prevEnvDepth = process.env[DEPTH_ENV];
314
- const prevEnvMaxDepth = process.env[MAX_DEPTH_ENV];
315
- const prevDepth = _currentDepth;
316
- const prevMaxDepth = _currentMaxDepth;
317
- const maxDepth = Math.max(DEFAULT_MAX_DEPTH, agent.maxDepth ?? DEFAULT_MAX_DEPTH);
318
- _currentDepth = depth + 1;
319
- _currentMaxDepth = depth + maxDepth;
320
- process.env[DEPTH_ENV] = String(_currentDepth);
321
- process.env[MAX_DEPTH_ENV] = String(_currentMaxDepth);
313
+ // Per-agent maxDepth; defaults to 0 so subagents can't spawn subagents unless
314
+ // their frontmatter opts in. AsyncLocalStorage scopes this to the nested
315
+ // call, so overlapping parallel agents don't trample each other's state.
316
+ const agentMaxDepth = Math.max(DEFAULT_MAX_DEPTH, agent.maxDepth ?? DEFAULT_MAX_DEPTH);
317
+ const childState: DepthState = { depth: depth + 1, maxDepth: depth + agentMaxDepth };
322
318
 
323
319
  let exitCode = 0;
324
320
  let error: string | undefined;
@@ -329,7 +325,7 @@ export async function runAgent(
329
325
  const onAbort = () => void session.abort();
330
326
  signal?.addEventListener("abort", onAbort, { once: true });
331
327
  try {
332
- await session.prompt(task);
328
+ await runWithDepth(childState, () => session.prompt(task));
333
329
  } finally {
334
330
  signal?.removeEventListener("abort", onAbort);
335
331
  }
@@ -342,12 +338,6 @@ export async function runAgent(
342
338
  unsubscribe();
343
339
  session.dispose();
344
340
  loaderLease.release();
345
- if (prevEnvDepth === undefined) delete process.env[DEPTH_ENV];
346
- else process.env[DEPTH_ENV] = prevEnvDepth;
347
- if (prevEnvMaxDepth === undefined) delete process.env[MAX_DEPTH_ENV];
348
- else process.env[MAX_DEPTH_ENV] = prevEnvMaxDepth;
349
- _currentDepth = prevDepth;
350
- _currentMaxDepth = prevMaxDepth;
351
341
  }
352
342
 
353
343
  return { output: lastOutput, exitCode, error, model: detectedModel, toolCalls, executionEvents, usage };