@narumitw/pi-subagents 1.0.1 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,12 +39,12 @@
39
39
  "typebox": "*"
40
40
  },
41
41
  "devDependencies": {
42
- "@biomejs/biome": "2.5.7",
43
- "@earendil-works/pi-agent-core": "0.84.1",
44
- "@earendil-works/pi-ai": "0.84.1",
45
- "@earendil-works/pi-coding-agent": "0.84.1",
46
- "@earendil-works/pi-tui": "0.84.1",
47
- "typebox": "1.3.11",
42
+ "@biomejs/biome": "2.5.8",
43
+ "@earendil-works/pi-agent-core": "0.84.2",
44
+ "@earendil-works/pi-ai": "0.84.2",
45
+ "@earendil-works/pi-coding-agent": "0.84.2",
46
+ "@earendil-works/pi-tui": "0.84.2",
47
+ "typebox": "1.3.14",
48
48
  "typescript": "7.0.2"
49
49
  },
50
50
  "dependencies": {
@@ -1,6 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { cachedModuleLoader } from "./cached-module-loader.js";
3
- import { showSubagentHelp, showSubagentStatus } from "./config-status.js";
4
3
  import type { SubagentMenuOwner, SubagentSettingsRuntime } from "./config-ui.js";
5
4
 
6
5
  const SUBCOMMANDS = [
@@ -14,8 +13,14 @@ type ConfigUiModule = Pick<
14
13
  "showSubagentManager" | "showSubagentSettings"
15
14
  >;
16
15
 
16
+ type ConfigStatusModule = Pick<
17
+ typeof import("./config-status.js"),
18
+ "showSubagentHelp" | "showSubagentStatus"
19
+ >;
20
+
17
21
  export interface ConfigRegistrationDependencies {
18
22
  loadConfigUi?: () => Promise<ConfigUiModule>;
23
+ loadConfigStatus?: () => Promise<ConfigStatusModule>;
19
24
  }
20
25
 
21
26
  export function registerSubagentConfigLifecycle(pi: ExtensionAPI): SubagentMenuOwner {
@@ -41,6 +46,9 @@ export function registerSubagentConfigCommand(
41
46
  const loadConfigUi = cachedModuleLoader(
42
47
  dependencies.loadConfigUi ?? (() => import("./config-ui.js")),
43
48
  );
49
+ const loadConfigStatus = cachedModuleLoader<ConfigStatusModule>(
50
+ dependencies.loadConfigStatus ?? (() => import("./config-status.js")),
51
+ );
44
52
  pi.registerCommand("subagents", {
45
53
  description: "Manage current-session subagents and user settings",
46
54
  getArgumentCompletions(prefix: string) {
@@ -50,16 +58,33 @@ export function registerSubagentConfigCommand(
50
58
  },
51
59
  async handler(args, ctx) {
52
60
  const subcommand = args.trim().toLowerCase();
61
+ const runStatusCommand = async (show: (status: ConfigStatusModule) => void) => {
62
+ const generation = owner.generation;
63
+ const controller = owner.controller;
64
+ const isCurrent = () =>
65
+ generation === owner.generation &&
66
+ controller === owner.controller &&
67
+ !controller.signal.aborted;
68
+ let status: ConfigStatusModule;
69
+ try {
70
+ status = await loadConfigStatus();
71
+ } catch (error) {
72
+ if (!isCurrent()) return;
73
+ throw error;
74
+ }
75
+ if (!isCurrent()) return;
76
+ show(status);
77
+ };
53
78
  if (!subcommand && ctx.mode !== "tui") {
54
- showSubagentStatus(ctx, runtime);
79
+ await runStatusCommand((status) => status.showSubagentStatus(ctx, runtime));
55
80
  return;
56
81
  }
57
82
  if (subcommand === "status") {
58
- showSubagentStatus(ctx, runtime);
83
+ await runStatusCommand((status) => status.showSubagentStatus(ctx, runtime));
59
84
  return;
60
85
  }
61
86
  if (subcommand === "help") {
62
- showSubagentHelp(ctx, runtime);
87
+ await runStatusCommand((status) => status.showSubagentHelp(ctx, runtime));
63
88
  return;
64
89
  }
65
90
  if (!subcommand || subcommand === "settings") {
@@ -6,7 +6,6 @@ import {
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
8
8
  import type { ConsultDetails, SubagentConsultParams } from "./consult.js";
9
- import { formatUsageStats } from "./render.js";
10
9
  import {
11
10
  COLLAPSED_ANSWER_LINES,
12
11
  COLLAPSED_LIST_LIMIT,
@@ -25,6 +24,7 @@ import {
25
24
  textResult,
26
25
  toolHeader,
27
26
  } from "./render-common.js";
27
+ import { formatUsageStats } from "./usage-format.js";
28
28
 
29
29
  export function renderConsultCall(args: Partial<SubagentConsultParams>, theme: Theme) {
30
30
  const scope = args.agentScope ?? "user";
@@ -1,9 +1,9 @@
1
1
  import type { AgentConfig } from "./agents/types.js";
2
2
  import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
3
+ import type { PanelPreset } from "./panel-presets.js";
3
4
  import { type WorkItemDefinition, WorkItemLedger } from "./work-item-ledger.js";
4
5
 
5
- export const PANEL_PRESETS = ["code-review", "research", "security-review", "custom"] as const;
6
- export type PanelPreset = (typeof PANEL_PRESETS)[number];
6
+ export type { PanelPreset } from "./panel-presets.js";
7
7
 
8
8
  export interface PanelReviewerRequest {
9
9
  id: string;
@@ -0,0 +1,3 @@
1
+ export const PANEL_PRESETS = ["code-review", "research", "security-review", "custom"] as const;
2
+
3
+ export type PanelPreset = (typeof PANEL_PRESETS)[number];
package/src/params.ts CHANGED
@@ -3,7 +3,7 @@ import { type Static, Type } from "typebox";
3
3
  import { THINKING_LEVELS } from "./agents/types.js";
4
4
  import { DelegationContractSchema } from "./delegation-contract.js";
5
5
  import { MAX_CONFIGURABLE_PARALLEL_TASKS, MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
6
- import { PANEL_PRESETS } from "./panel-planning.js";
6
+ import { PANEL_PRESETS } from "./panel-presets.js";
7
7
  import { SUBAGENT_RESULT_FORMATS } from "./result-contract.js";
8
8
  import { MAX_SUBAGENT_TOOL_CALLS, MAX_SUBAGENT_TURNS } from "./turn-budget.js";
9
9
  import { VerifiedExecutionContractSchema } from "./verified-execution-schema.js";
package/src/render.ts CHANGED
@@ -7,12 +7,13 @@ import {
7
7
  type ToolRenderResultOptions,
8
8
  } from "@earendil-works/pi-coding-agent";
9
9
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
10
- import type { AgentScope, SubagentThinkingLevel } from "./agents/types.js";
10
+ import type { AgentScope } from "./agents/types.js";
11
11
  import { renderPanelCall, renderPanelResult } from "./panel-render.js";
12
12
  import { hasUsableAggregator, type SubagentParams } from "./params.js";
13
13
  import { expansionHint, formatToolActivity, safeBlock, safeLine } from "./render-common.js";
14
14
  import type { SingleResult, SubagentDetails } from "./runner.js";
15
15
  import { getResultFinalOutput, isResultError } from "./runner-outcome.js";
16
+ import { formatUsageStats } from "./usage-format.js";
16
17
 
17
18
  const COLLAPSED_ITEM_COUNT = 5;
18
19
 
@@ -25,46 +26,6 @@ function previewAgent(agent: unknown): string {
25
26
  return safeLine(agent, "...", 256);
26
27
  }
27
28
 
28
- export function formatTokens(count: number): string {
29
- if (count < 1000) return count.toString();
30
- if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
31
- if (count < 1000000) return `${Math.round(count / 1000)}k`;
32
- return `${(count / 1000000).toFixed(1)}M`;
33
- }
34
-
35
- export function formatUsageStats(
36
- usage: {
37
- input: number;
38
- output: number;
39
- cacheRead: number;
40
- cacheWrite: number;
41
- cost: number;
42
- contextTokens?: number;
43
- turns?: number;
44
- },
45
- model?: string,
46
- thinkingLevel?: SubagentThinkingLevel,
47
- actualProvider?: string,
48
- actualModel?: string,
49
- ): string {
50
- const parts: string[] = [];
51
- if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
52
- if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
53
- if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
54
- if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
55
- if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
56
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
57
- if (usage.contextTokens && usage.contextTokens > 0)
58
- parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
59
- const safeProvider = actualProvider ? safeLine(actualProvider, "", 256) : undefined;
60
- const safeModel = actualModel ? safeLine(actualModel, "", 256) : undefined;
61
- const actual =
62
- safeProvider && safeModel ? `${safeProvider}/${safeModel}` : (safeModel ?? safeProvider);
63
- if (actual ?? model) parts.push(actual ?? safeLine(model, "", 256));
64
- if (thinkingLevel) parts.push(`requested-thinking:${safeLine(thinkingLevel, "", 128)}`);
65
- return parts.join(" ");
66
- }
67
-
68
29
  function formatResultUsageStats(result: SingleResult): string {
69
30
  return formatUsageStats(
70
31
  result.usage,
package/src/settings.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
+ import { createRequire } from "node:module";
3
4
  import * as path from "node:path";
4
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
- import lockfile from "proper-lockfile";
6
6
  import type {
7
7
  AgentConfig,
8
8
  CompletionDelivery,
@@ -76,6 +76,8 @@ export {
76
76
 
77
77
  const SETTINGS_FILE = "pi-subagents.json";
78
78
  const LEGACY_SETTINGS_FILE = "pi-subagents-config.json";
79
+ const require = createRequire(import.meta.url);
80
+
79
81
  const SETTINGS_LOCK_FS_ADAPTER = {
80
82
  mkdir: fs.mkdir,
81
83
  mkdirSync: fs.mkdirSync,
@@ -511,6 +513,7 @@ function writeSettingsObjectUnlocked(settings: object, replaceCanonical?: boolea
511
513
  }
512
514
 
513
515
  function withSettingsMutationLock<T>(mutate: () => T): T {
516
+ const lockfile = require("proper-lockfile") as typeof import("proper-lockfile");
514
517
  const agentDir = getAgentDir();
515
518
  fs.mkdirSync(agentDir, { recursive: true });
516
519
  const configPath = path.join(agentDir, SETTINGS_FILE);
package/src/stateful.ts CHANGED
@@ -7,7 +7,6 @@ import { randomUUID } from "node:crypto";
7
7
  import { StringEnum } from "@earendil-works/pi-ai";
8
8
  import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
9
  import { Type } from "typebox";
10
- import { discoverAgents } from "./agents/discovery.js";
11
10
  import {
12
11
  type AgentScope,
13
12
  type CompletionDelivery,
@@ -17,20 +16,10 @@ import {
17
16
  type SubagentTransportKind,
18
17
  THINKING_LEVELS,
19
18
  } from "./agents/types.js";
20
- import { issueCapabilityGrant } from "./capability-grant.js";
21
- import { CompletionDeliveryBroker } from "./completion-delivery.js";
22
- import { buildContextSnapshot, type ContextMode, redactPrivateText } from "./context.js";
23
- import {
24
- type CreateStatefulTransportOptions,
25
- createStatefulTransport,
26
- } from "./create-stateful-transport.js";
27
- import {
28
- assertDelegationTargetAllowed,
29
- resolveSubagentTarget,
30
- targetPolicyAudit,
31
- } from "./cwd-policy.js";
32
- import { DelegationContractSchema, normalizeDelegationContract } from "./delegation-contract.js";
33
- import { assertSubagentDepthAllowed } from "./execution/runtime-policy.js";
19
+ import type { CompletionDeliveryBroker } from "./completion-delivery.js";
20
+ import type { ContextMode } from "./context.js";
21
+ import type { CreateStatefulTransportOptions } from "./create-stateful-transport.js";
22
+ import { DelegationContractSchema } from "./delegation-contract.js";
34
23
  import type { ChildSessionFactory, ParentRuntimeSnapshot } from "./in-process-transport.js";
35
24
  import {
36
25
  DEFAULT_MAX_CONTEXT_BYTES,
@@ -38,32 +27,24 @@ import {
38
27
  MAX_TOOL_MESSAGE_BYTES,
39
28
  truncateUtf8,
40
29
  } from "./limits.js";
41
- import { AgentPersistence } from "./persistence.js";
42
- import {
30
+ import type { AgentPersistence } from "./persistence.js";
31
+ import type {
43
32
  AgentRegistry,
44
- type AgentRunInspectionDetail,
45
- type AgentRunInspectionSummary,
46
- type ManagedAgent,
33
+ AgentRunInspectionDetail,
34
+ AgentRunInspectionSummary,
35
+ ManagedAgent,
47
36
  } from "./registry.js";
48
37
  import { SUBAGENT_RESULT_FORMATS, type SubagentResultFormat } from "./result-contract.js";
49
- import { buildRetainedSemanticState } from "./retained-semantic-state.js";
50
- import { evaluateSemanticCompatibility } from "./semantic-snapshot.js";
51
38
  import { DEFAULT_DELEGATION_CWD_POLICY } from "./settings/inspection.js";
52
39
  import { readSubagentSettings } from "./settings.js";
53
40
  import {
54
41
  assertSpawnIdempotencyKey,
55
- hashSpawnRequest,
56
42
  MAX_SPAWN_IDEMPOTENCY_KEY_LENGTH,
57
43
  } from "./spawn-idempotency.js";
58
44
  import { summarizeStatefulAgent } from "./stateful-agent-view.js";
59
45
  import { resolveCompletionDelivery, resolveStatefulTransportKind } from "./stateful-config.js";
60
46
  import { createSpawnPromptGuidelines } from "./stateful-guidance.js";
61
- import {
62
- assertCurrentSpawn,
63
- cleanupPersistedWorkspaces,
64
- disposeStatefulRuntime,
65
- waitForOwnedSpawn,
66
- } from "./stateful-lifecycle.js";
47
+ import { assertCurrentSpawn, waitForOwnedSpawn } from "./stateful-lifecycle.js";
67
48
  import { resolveStatefulLimits, type StatefulLimits } from "./stateful-limits.js";
68
49
  import { createStatefulToolRenderer } from "./stateful-render.js";
69
50
  import {
@@ -85,7 +66,112 @@ import {
85
66
  validateMailboxParams,
86
67
  validateManageParams,
87
68
  } from "./stateful-tool-params.js";
88
- import { WorkspaceManager } from "./workspace.js";
69
+ import type { WorkspaceManager } from "./workspace.js";
70
+
71
+ type CwdPolicyModule = typeof import("./cwd-policy.js");
72
+ type StateLifecycleModule = typeof import("./stateful-lifecycle.js");
73
+
74
+ type StatefulSessionModules = {
75
+ broker: typeof import("./completion-delivery.js");
76
+ context: typeof import("./context.js");
77
+ transport: typeof import("./create-stateful-transport.js");
78
+ cwdPolicy: CwdPolicyModule;
79
+ persistence: typeof import("./persistence.js");
80
+ registry: typeof import("./registry.js");
81
+ lifecycle: StateLifecycleModule;
82
+ };
83
+
84
+ type StatefulSpawnModules = {
85
+ agents: typeof import("./agents/discovery.js");
86
+ capabilityGrant: typeof import("./capability-grant.js");
87
+ context: typeof import("./context.js");
88
+ cwdPolicy: CwdPolicyModule;
89
+ delegationContract: typeof import("./delegation-contract.js");
90
+ runtimePolicy: typeof import("./execution/runtime-policy.js");
91
+ retainedSemanticState: typeof import("./retained-semantic-state.js");
92
+ semanticSnapshot: typeof import("./semantic-snapshot.js");
93
+ spawnIdempotency: typeof import("./spawn-idempotency.js");
94
+ };
95
+
96
+ let statefulSessionModules: Promise<StatefulSessionModules> | undefined;
97
+ let statefulSpawnModules: Promise<StatefulSpawnModules> | undefined;
98
+ let workspaceModule: Promise<typeof import("./workspace.js")> | undefined;
99
+
100
+ function loadStatefulSessionModules(): Promise<StatefulSessionModules> {
101
+ statefulSessionModules ??= Promise.all([
102
+ import("./completion-delivery.js"),
103
+ import("./context.js"),
104
+ import("./create-stateful-transport.js"),
105
+ import("./cwd-policy.js"),
106
+ import("./persistence.js"),
107
+ import("./registry.js"),
108
+ import("./stateful-lifecycle.js"),
109
+ ])
110
+ .then(([broker, context, transport, cwdPolicy, persistence, registry, lifecycle]) => ({
111
+ broker,
112
+ context,
113
+ transport,
114
+ cwdPolicy,
115
+ persistence,
116
+ registry,
117
+ lifecycle,
118
+ }))
119
+ .catch((error: unknown) => {
120
+ statefulSessionModules = undefined;
121
+ throw error;
122
+ });
123
+ return statefulSessionModules;
124
+ }
125
+
126
+ function loadStatefulSpawnModules(): Promise<StatefulSpawnModules> {
127
+ statefulSpawnModules ??= Promise.all([
128
+ import("./agents/discovery.js"),
129
+ import("./capability-grant.js"),
130
+ import("./context.js"),
131
+ import("./cwd-policy.js"),
132
+ import("./delegation-contract.js"),
133
+ import("./execution/runtime-policy.js"),
134
+ import("./retained-semantic-state.js"),
135
+ import("./semantic-snapshot.js"),
136
+ import("./spawn-idempotency.js"),
137
+ ])
138
+ .then(
139
+ ([
140
+ agents,
141
+ capabilityGrant,
142
+ context,
143
+ cwdPolicy,
144
+ delegationContract,
145
+ runtimePolicy,
146
+ retainedSemanticState,
147
+ semanticSnapshot,
148
+ spawnIdempotency,
149
+ ]) => ({
150
+ agents,
151
+ capabilityGrant,
152
+ context,
153
+ cwdPolicy,
154
+ delegationContract,
155
+ runtimePolicy,
156
+ retainedSemanticState,
157
+ semanticSnapshot,
158
+ spawnIdempotency,
159
+ }),
160
+ )
161
+ .catch((error: unknown) => {
162
+ statefulSpawnModules = undefined;
163
+ throw error;
164
+ });
165
+ return statefulSpawnModules;
166
+ }
167
+
168
+ async function loadWorkspaceModule(): Promise<typeof import("./workspace.js")> {
169
+ workspaceModule ??= import("./workspace.js").catch((error: unknown) => {
170
+ workspaceModule = undefined;
171
+ throw error;
172
+ });
173
+ return workspaceModule;
174
+ }
89
175
 
90
176
  const ContextModeSchema = Type.Union([
91
177
  StringEnum(["none", "all", "summary"] as const),
@@ -186,7 +272,13 @@ export function registerStatefulSubagents(
186
272
  let sweepTimer: NodeJS.Timeout | undefined;
187
273
  let runtimeGeneration = 0;
188
274
  let runtimeTransition: Promise<void> = Promise.resolve();
189
- const workspaceManager = dependencies.workspaceManager ?? new WorkspaceManager();
275
+ let workspaceManager = dependencies.workspaceManager;
276
+ const getWorkspaceManager = async () => {
277
+ if (workspaceManager) return workspaceManager;
278
+ const { WorkspaceManager } = await loadWorkspaceModule();
279
+ workspaceManager = new WorkspaceManager();
280
+ return workspaceManager;
281
+ };
190
282
  const isolatedAgents = new Map<string, string>();
191
283
  const seenMessageIds = new Set<string>();
192
284
  type PendingIdempotentSpawn = {
@@ -206,11 +298,12 @@ export function registerStatefulSubagents(
206
298
  const currentRegistry = registry;
207
299
  const currentPersistence = persistence;
208
300
  if (!currentRegistry) return 0;
301
+ const currentWorkspaceManager = await getWorkspaceManager();
209
302
  const count = currentRegistry.list().length;
210
303
  const clear = async () => {
211
304
  await currentRegistry.closeAll();
212
305
  if (generation !== runtimeGeneration) return;
213
- await workspaceManager.cleanupAll();
306
+ await currentWorkspaceManager.cleanupAll();
214
307
  isolatedAgents.clear();
215
308
  seenMessageIds.clear();
216
309
  await currentPersistence?.delete();
@@ -277,7 +370,12 @@ export function registerStatefulSubagents(
277
370
  seenMessageIds.clear();
278
371
  pendingIdempotentSpawns.clear();
279
372
  const initialize = async () => {
280
- const cleanupErrors = await disposeStatefulRuntime(previousRegistry, workspaceManager);
373
+ const currentWorkspaceManager = await getWorkspaceManager();
374
+ const modules = await loadStatefulSessionModules();
375
+ const cleanupErrors = await modules.lifecycle.disposeStatefulRuntime(
376
+ previousRegistry,
377
+ currentWorkspaceManager,
378
+ );
281
379
  if (generation !== runtimeGeneration) return;
282
380
  if (cleanupErrors.length > 0 && ctx.hasUI) {
283
381
  ctx.ui.notify(
@@ -293,31 +391,36 @@ export function registerStatefulSubagents(
293
391
  ctx.sessionManager.getSessionId?.() ??
294
392
  ctx.sessionManager.getSessionFile?.() ??
295
393
  `ephemeral:${ctx.cwd}`;
296
- const sessionPersistence = new AgentPersistence(owner, {
394
+ const sessionPersistence = new modules.persistence.AgentPersistence(owner, {
297
395
  retentionDays: sessionSettings.retentionDays,
298
396
  maxStoredAgents: nextLimits.maxStoredAgents,
299
397
  });
300
398
  let nextRegistry: AgentRegistry;
301
- const sessionBroker = new CompletionDeliveryBroker(pi, ctx, completionDelivery, {
302
- onDeliveryError: (error) => {
303
- if (!ctx.hasUI) return;
304
- const reason = error instanceof Error ? error.message : String(error);
305
- ctx.ui.notify(`Subagent completion delivery failed: ${reason}`, "warning");
306
- },
307
- onAcknowledged: (completions, deliveredAt) => {
308
- if (generation !== runtimeGeneration) return;
309
- for (const completion of completions) {
310
- void nextRegistry
311
- .markCompletionDelivered(completion.completionId, deliveredAt)
312
- .catch((error: unknown) => {
313
- if (!ctx.hasUI || generation !== runtimeGeneration) return;
314
- const reason = error instanceof Error ? error.message : String(error);
315
- ctx.ui.notify(`Subagent completion acknowledgement failed: ${reason}`, "warning");
316
- });
317
- }
399
+ const sessionBroker = new modules.broker.CompletionDeliveryBroker(
400
+ pi,
401
+ ctx,
402
+ completionDelivery,
403
+ {
404
+ onDeliveryError: (error) => {
405
+ if (!ctx.hasUI) return;
406
+ const reason = error instanceof Error ? error.message : String(error);
407
+ ctx.ui.notify(`Subagent completion delivery failed: ${reason}`, "warning");
408
+ },
409
+ onAcknowledged: (completions, deliveredAt) => {
410
+ if (generation !== runtimeGeneration) return;
411
+ for (const completion of completions) {
412
+ void nextRegistry
413
+ .markCompletionDelivered(completion.completionId, deliveredAt)
414
+ .catch((error: unknown) => {
415
+ if (!ctx.hasUI || generation !== runtimeGeneration) return;
416
+ const reason = error instanceof Error ? error.message : String(error);
417
+ ctx.ui.notify(`Subagent completion acknowledgement failed: ${reason}`, "warning");
418
+ });
419
+ }
420
+ },
318
421
  },
319
- });
320
- const transport = createStatefulTransport({
422
+ );
423
+ const transport = modules.transport.createStatefulTransport({
321
424
  kind: transportKind,
322
425
  modelRegistry: ctx.modelRegistry,
323
426
  getParentRuntime: () => ({ ...parentRuntime }),
@@ -325,7 +428,7 @@ export function registerStatefulSubagents(
325
428
  createInProcessSession: dependencies.createInProcessSession,
326
429
  loadTransport: dependencies.loadTransport,
327
430
  });
328
- nextRegistry = new AgentRegistry(transport, {
431
+ nextRegistry = new modules.registry.AgentRegistry(transport, {
329
432
  maxAgents: nextLimits.maxAgents,
330
433
  maxActiveTurns: nextLimits.maxActiveTurns,
331
434
  maxDepth: nextLimits.maxDepth,
@@ -344,7 +447,7 @@ export function registerStatefulSubagents(
344
447
  pi.appendEntry("pi-subagent-message", {
345
448
  senderId: message.senderId,
346
449
  recipientId: message.recipientId,
347
- content: redactPrivateText(message.content).slice(0, 160),
450
+ content: modules.context.redactPrivateText(message.content).slice(0, 160),
348
451
  });
349
452
  }
350
453
  }
@@ -354,7 +457,10 @@ export function registerStatefulSubagents(
354
457
  },
355
458
  });
356
459
  const persisted = sessionPersistence.load();
357
- const orphanCleanupFailures = await cleanupPersistedWorkspaces(persisted, workspaceManager);
460
+ const orphanCleanupFailures = await modules.lifecycle.cleanupPersistedWorkspaces(
461
+ persisted,
462
+ currentWorkspaceManager,
463
+ );
358
464
  if (ctx.hasUI && orphanCleanupFailures > 0) {
359
465
  ctx.ui.notify("Some orphaned subagent worktrees could not be cleaned", "warning");
360
466
  }
@@ -367,12 +473,14 @@ export function registerStatefulSubagents(
367
473
  )
368
474
  .flatMap((agent) => {
369
475
  try {
370
- const target = resolveSubagentTarget({
476
+ const target = modules.cwdPolicy.resolveSubagentTarget({
371
477
  workspace: ctx.cwd,
372
478
  requestedCwd: agent.cwd,
373
479
  currentProjectTrusted: ctx.isProjectTrusted(),
374
480
  });
375
- return [{ ...agent, cwd: target.cwd, target: targetPolicyAudit(target) }];
481
+ return [
482
+ { ...agent, cwd: target.cwd, target: modules.cwdPolicy.targetPolicyAudit(target) },
483
+ ];
376
484
  } catch {
377
485
  return [];
378
486
  }
@@ -383,7 +491,7 @@ export function registerStatefulSubagents(
383
491
  nextRegistry.restore(restored);
384
492
  if (generation !== runtimeGeneration) {
385
493
  sessionBroker.close();
386
- await disposeStatefulRuntime(nextRegistry, workspaceManager);
494
+ await modules.lifecycle.disposeStatefulRuntime(nextRegistry, currentWorkspaceManager);
387
495
  return;
388
496
  }
389
497
  registry = nextRegistry;
@@ -445,7 +553,9 @@ export function registerStatefulSubagents(
445
553
  seenMessageIds.clear();
446
554
  pendingIdempotentSpawns.clear();
447
555
  const shutdown = async () => {
448
- const errors = await disposeStatefulRuntime(previousRegistry, workspaceManager);
556
+ const currentWorkspaceManager = await getWorkspaceManager();
557
+ const { disposeStatefulRuntime } = await import("./stateful-lifecycle.js");
558
+ const errors = await disposeStatefulRuntime(previousRegistry, currentWorkspaceManager);
449
559
  if (errors.length > 0 && ctx.hasUI) {
450
560
  ctx.ui.notify(`Subagent shutdown cleanup reported ${errors.length} error(s).`, "warning");
451
561
  }
@@ -502,30 +612,47 @@ export function registerStatefulSubagents(
502
612
  }),
503
613
  ...createStatefulToolRenderer("spawn"),
504
614
  async execute(_id, params, signal, _update, ctx) {
615
+ const generation = runtimeGeneration;
616
+ const capturedRegistry = registry;
617
+ let modules: StatefulSpawnModules;
618
+ try {
619
+ modules = await loadStatefulSpawnModules();
620
+ } catch (error) {
621
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
622
+ throw error;
623
+ }
624
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
625
+ let currentWorkspaceManager: WorkspaceManager;
626
+ try {
627
+ currentWorkspaceManager = await getWorkspaceManager();
628
+ } catch (error) {
629
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
630
+ throw error;
631
+ }
632
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
505
633
  const scope = (params.agentScope ?? "user") as AgentScope;
506
634
  const resultFormat = (params.resultFormat ?? "text") as SubagentResultFormat;
507
- const contract = normalizeDelegationContract(params.contract);
635
+ const contract = modules.delegationContract.normalizeDelegationContract(params.contract);
508
636
  if (params.contract !== undefined && !contract) {
509
637
  throw new Error(
510
638
  "subagent_spawn contract must be a valid pi-subagents:delegation:v2 object",
511
639
  );
512
640
  }
513
- assertSubagentDepthAllowed();
641
+ modules.runtimePolicy.assertSubagentDepthAllowed();
514
642
  assertSpawnIdempotencyKey(params.idempotencyKey);
515
- const generation = runtimeGeneration;
516
643
  const currentSettings = getCurrentSettings();
517
- const target = resolveSubagentTarget({
644
+ const target = modules.cwdPolicy.resolveSubagentTarget({
518
645
  workspace: ctx.cwd,
519
646
  requestedCwd: params.cwd,
520
647
  currentProjectTrusted: ctx.isProjectTrusted(),
521
648
  });
522
- assertDelegationTargetAllowed(
649
+ modules.cwdPolicy.assertDelegationTargetAllowed(
523
650
  target,
524
651
  currentSettings?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
525
652
  );
526
653
  const cwd = target.cwd;
527
654
  const mode = resolveSpawnContextMode(params.context, params.contextEntryIds);
528
- const snapshot = buildContextSnapshot(
655
+ const snapshot = modules.context.buildContextSnapshot(
529
656
  ctx.sessionManager.getBranch(),
530
657
  mode,
531
658
  DEFAULT_MAX_CONTEXT_BYTES,
@@ -534,27 +661,28 @@ export function registerStatefulSubagents(
534
661
  if ((scope === "project" || scope === "both") && !ctx.isProjectTrusted()) {
535
662
  throw new Error("Project-local subagent definitions require a trusted project");
536
663
  }
537
- const resolvedAgents = discoverAgents(cwd, scope, currentSettings).agents;
664
+ const resolvedAgents = modules.agents.discoverAgents(cwd, scope, currentSettings).agents;
538
665
  const resolvedAgent = resolvedAgents.find((agent) => agent.name === params.agent);
539
666
  if (!resolvedAgent) {
540
667
  const available = resolvedAgents.map((agent) => agent.name).join(", ") || "none";
541
668
  throw new Error(`Unknown subagent ${params.agent}. Available agents: ${available}`);
542
669
  }
543
- const targetSnapshot = targetPolicyAudit(target);
544
- const { executionPlan, semanticSnapshot } = await buildRetainedSemanticState({
545
- agent: resolvedAgent,
546
- contract,
547
- target: targetSnapshot,
548
- cwd,
549
- workspaceMode: params.workspaceMode === "worktree" ? "worktree" : "shared",
550
- transport: transportKind,
551
- resultFormat,
552
- thinkingLevel: params.thinkingLevel,
553
- timeoutMs: params.timeoutMs,
554
- taskGeneration: 1,
555
- });
670
+ const targetSnapshot = modules.cwdPolicy.targetPolicyAudit(target);
671
+ const { executionPlan, semanticSnapshot } =
672
+ await modules.retainedSemanticState.buildRetainedSemanticState({
673
+ agent: resolvedAgent,
674
+ contract,
675
+ target: targetSnapshot,
676
+ cwd,
677
+ workspaceMode: params.workspaceMode === "worktree" ? "worktree" : "shared",
678
+ transport: transportKind,
679
+ resultFormat,
680
+ thinkingLevel: params.thinkingLevel,
681
+ timeoutMs: params.timeoutMs,
682
+ taskGeneration: 1,
683
+ });
556
684
  assertCurrentSpawn(signal, generation, runtimeGeneration);
557
- const requestHash = hashSpawnRequest({
685
+ const requestHash = modules.spawnIdempotency.hashSpawnRequest({
558
686
  agent: params.agent,
559
687
  task: params.task,
560
688
  cwd,
@@ -572,7 +700,10 @@ export function registerStatefulSubagents(
572
700
  contract,
573
701
  resultFormat,
574
702
  });
575
- const ownedRegistry = requireRegistry();
703
+ if (!capturedRegistry) {
704
+ throw new Error("Stateful subagents are not initialized for this session");
705
+ }
706
+ const ownedRegistry = capturedRegistry;
576
707
  const retained = ownedRegistry.findBySpawnIdempotencyKey(params.idempotencyKey, requestHash);
577
708
  if (retained) return result(retained, `Reused ${retained.agent} as ${retained.id}.`);
578
709
  const foundPending = params.idempotencyKey
@@ -633,17 +764,17 @@ export function registerStatefulSubagents(
633
764
  const workspaceOwner = `pending-${randomUUID()}`;
634
765
  const workspace =
635
766
  params.workspaceMode === "worktree"
636
- ? await workspaceManager.create(workspaceOwner, requestedCwd)
767
+ ? await currentWorkspaceManager.create(workspaceOwner, requestedCwd)
637
768
  : undefined;
638
769
  try {
639
770
  assertCurrentSpawn(signal, generation, runtimeGeneration);
640
771
  } catch (error) {
641
- if (workspace) await workspaceManager.cleanup(workspaceOwner);
772
+ if (workspace) await currentWorkspaceManager.cleanup(workspaceOwner);
642
773
  throw error;
643
774
  }
644
775
  let agent: ManagedAgent | undefined;
645
776
  try {
646
- const capabilityGrant = issueCapabilityGrant(
777
+ const capabilityGrant = modules.capabilityGrant.issueCapabilityGrant(
647
778
  executionPlan,
648
779
  Date.now(),
649
780
  Math.max(1, (params.timeoutMs ?? resolvedAgent.timeoutMs ?? 600_000) + 60_000),
@@ -678,12 +809,12 @@ export function registerStatefulSubagents(
678
809
  assertCurrentSpawn(signal, generation, runtimeGeneration);
679
810
  } catch (error) {
680
811
  if (agent) await ownedRegistry.closeTree(agent.id).catch(() => undefined);
681
- if (workspace) await workspaceManager.cleanup(workspaceOwner);
812
+ if (workspace) await currentWorkspaceManager.cleanup(workspaceOwner);
682
813
  throw error;
683
814
  }
684
815
  if (!agent) throw new Error("Subagent spawn completed without a retained agent");
685
816
  if (workspace && agent.cwd === workspace.path) isolatedAgents.set(agent.id, workspaceOwner);
686
- else if (workspace) await workspaceManager.cleanup(workspaceOwner);
817
+ else if (workspace) await currentWorkspaceManager.cleanup(workspaceOwner);
687
818
  assertCurrentSpawn(signal, generation, runtimeGeneration);
688
819
  resolvePending?.(agent);
689
820
  const deliveryNote =
@@ -747,25 +878,31 @@ export function registerStatefulSubagents(
747
878
  async execute(_id, params, signal, _update, ctx) {
748
879
  const generation = runtimeGeneration;
749
880
  const ownedRegistry = requireRegistry();
881
+ let modules: StatefulSpawnModules;
882
+ try {
883
+ modules = await loadStatefulSpawnModules();
884
+ } catch (error) {
885
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
886
+ throw error;
887
+ }
888
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
750
889
  const currentSettings = getCurrentSettings();
751
890
  const existing = ownedRegistry.get(params.agentId);
752
891
  if (!existing) throw new Error(`Unknown subagent: ${params.agentId}`);
753
- const currentAgent = discoverAgents(
754
- existing.cwd,
755
- existing.agentScope ?? "user",
756
- currentSettings,
757
- ).agents.find((agent) => agent.name === existing.agent);
892
+ const currentAgent = modules.agents
893
+ .discoverAgents(existing.cwd, existing.agentScope ?? "user", currentSettings)
894
+ .agents.find((agent) => agent.name === existing.agent);
758
895
  if (!currentAgent) throw new Error(`Unknown retained subagent definition: ${existing.agent}`);
759
896
  const resolvedFollowUpTarget =
760
897
  existing.workspaceMode === "worktree"
761
898
  ? undefined
762
- : resolveSubagentTarget({
899
+ : modules.cwdPolicy.resolveSubagentTarget({
763
900
  workspace: ctx.cwd,
764
901
  requestedCwd: existing.cwd,
765
902
  currentProjectTrusted: ctx.isProjectTrusted(),
766
903
  });
767
904
  if (resolvedFollowUpTarget) {
768
- assertDelegationTargetAllowed(
905
+ modules.cwdPolicy.assertDelegationTargetAllowed(
769
906
  resolvedFollowUpTarget,
770
907
  currentSettings?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
771
908
  );
@@ -776,9 +913,11 @@ export function registerStatefulSubagents(
776
913
  const currentTarget =
777
914
  existing.workspaceMode === "worktree" && existing.target
778
915
  ? existing.target
779
- : targetPolicyAudit(resolvedFollowUpTarget as NonNullable<typeof resolvedFollowUpTarget>);
916
+ : modules.cwdPolicy.targetPolicyAudit(
917
+ resolvedFollowUpTarget as NonNullable<typeof resolvedFollowUpTarget>,
918
+ );
780
919
  const { executionPlan: currentPlan, semanticSnapshot: currentSnapshot } =
781
- await buildRetainedSemanticState({
920
+ await modules.retainedSemanticState.buildRetainedSemanticState({
782
921
  agent: currentAgent,
783
922
  contract: existing.contract,
784
923
  target: currentTarget,
@@ -798,7 +937,10 @@ export function registerStatefulSubagents(
798
937
  });
799
938
  assertCurrentSpawn(signal, generation, runtimeGeneration);
800
939
  const compatibility = existing.semanticSnapshot
801
- ? evaluateSemanticCompatibility(existing.semanticSnapshot, currentSnapshot)
940
+ ? modules.semanticSnapshot.evaluateSemanticCompatibility(
941
+ existing.semanticSnapshot,
942
+ currentSnapshot,
943
+ )
802
944
  : { status: "warning" as const, changedComponents: ["legacy-missing-snapshot"] };
803
945
  if (
804
946
  (compatibility.status === "needs-revalidation" || compatibility.status === "rejected") &&
@@ -824,7 +966,7 @@ export function registerStatefulSubagents(
824
966
  isolatedAgents.has(existing.id),
825
967
  currentSettings,
826
968
  );
827
- const currentGrant = issueCapabilityGrant(
969
+ const currentGrant = modules.capabilityGrant.issueCapabilityGrant(
828
970
  currentPlan,
829
971
  Date.now(),
830
972
  Math.max(1, (params.timeoutMs ?? existing.timeoutMs ?? 600_000) + 60_000),
@@ -838,6 +980,7 @@ export function registerStatefulSubagents(
838
980
  ? { status: "warning", changedComponents: compatibility.changedComponents }
839
981
  : compatibility,
840
982
  );
983
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
841
984
  const agent = await ownedRegistry.followUp(params.agentId, params.task, {
842
985
  timeoutMs: params.timeoutMs,
843
986
  idleTimeoutMs: params.idleTimeoutMs,
@@ -860,6 +1003,14 @@ export function registerStatefulSubagents(
860
1003
  async execute(_id, params, signal): Promise<StatefulActionToolResult> {
861
1004
  const generation = runtimeGeneration;
862
1005
  const ownedRegistry = requireRegistry();
1006
+ let currentWorkspaceManager: WorkspaceManager;
1007
+ try {
1008
+ currentWorkspaceManager = await getWorkspaceManager();
1009
+ } catch (error) {
1010
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
1011
+ throw error;
1012
+ }
1013
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
863
1014
  const ownedAgent = (agentId: string): ManagedAgent => {
864
1015
  const value = ownedRegistry.get(agentId);
865
1016
  if (!value) throw new Error(`Unknown subagent: ${agentId}`);
@@ -886,7 +1037,7 @@ export function registerStatefulSubagents(
886
1037
  const existing = ownedRegistry.get(agentId);
887
1038
  if (existing?.state === "closed" && !operation.subtree) {
888
1039
  const pendingOwner = isolatedAgents.get(existing.id);
889
- if (pendingOwner) await workspaceManager.cleanup(pendingOwner);
1040
+ if (pendingOwner) await currentWorkspaceManager.cleanup(pendingOwner);
890
1041
  assertCurrentSpawn(signal, generation, runtimeGeneration);
891
1042
  isolatedAgents.delete(existing.id);
892
1043
  return result(existing, `Closed ${existing.id}.`);
@@ -896,7 +1047,7 @@ export function registerStatefulSubagents(
896
1047
  try {
897
1048
  agents = await ownedRegistry.closeTree(agentId);
898
1049
  } finally {
899
- await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, workspaceManager);
1050
+ await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, currentWorkspaceManager);
900
1051
  }
901
1052
  assertCurrentSpawn(signal, generation, runtimeGeneration);
902
1053
  return {
@@ -911,7 +1062,7 @@ export function registerStatefulSubagents(
911
1062
  try {
912
1063
  agent = await ownedRegistry.close(agentId);
913
1064
  } finally {
914
- await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, workspaceManager);
1065
+ await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, currentWorkspaceManager);
915
1066
  }
916
1067
  assertCurrentSpawn(signal, generation, runtimeGeneration);
917
1068
  return result(agent, `Closed ${agent.id}.`);
package/src/subagents.ts CHANGED
@@ -310,7 +310,6 @@ function appendAgentCatalog(baseDescription: string, catalog: string): string {
310
310
 
311
311
  export { parsePositiveInteger } from "./execution/runtime-policy.js";
312
312
  export { buildPiArgs } from "./pi-args.js";
313
- export { formatTokens, formatUsageStats } from "./render.js";
314
313
  export {
315
314
  DEFAULT_CONSULT_RESOURCE_POLICY,
316
315
  DEFAULT_CONSULTATION_CWD_POLICY,
@@ -339,3 +338,4 @@ export {
339
338
  updateDelegationWorkflowSetting,
340
339
  updateStatefulLimitSetting,
341
340
  } from "./settings.js";
341
+ export { formatTokens, formatUsageStats } from "./usage-format.js";
@@ -0,0 +1,42 @@
1
+ import type { SubagentThinkingLevel } from "./agents/types.js";
2
+ import { safeLine } from "./render-common.js";
3
+
4
+ export function formatTokens(count: number): string {
5
+ if (count < 1000) return count.toString();
6
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
7
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
8
+ return `${(count / 1000000).toFixed(1)}M`;
9
+ }
10
+
11
+ export function formatUsageStats(
12
+ usage: {
13
+ input: number;
14
+ output: number;
15
+ cacheRead: number;
16
+ cacheWrite: number;
17
+ cost: number;
18
+ contextTokens?: number;
19
+ turns?: number;
20
+ },
21
+ model?: string,
22
+ thinkingLevel?: SubagentThinkingLevel,
23
+ actualProvider?: string,
24
+ actualModel?: string,
25
+ ): string {
26
+ const parts: string[] = [];
27
+ if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
28
+ if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
29
+ if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
30
+ if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
31
+ if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
32
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
33
+ if (usage.contextTokens && usage.contextTokens > 0)
34
+ parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
35
+ const safeProvider = actualProvider ? safeLine(actualProvider, "", 256) : undefined;
36
+ const safeModel = actualModel ? safeLine(actualModel, "", 256) : undefined;
37
+ const actual =
38
+ safeProvider && safeModel ? `${safeProvider}/${safeModel}` : (safeModel ?? safeProvider);
39
+ if (actual ?? model) parts.push(actual ?? safeLine(model, "", 256));
40
+ if (thinkingLevel) parts.push(`requested-thinking:${safeLine(thinkingLevel, "", 128)}`);
41
+ return parts.join(" ");
42
+ }