@narumitw/pi-subagents 1.0.1 → 2.0.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.
Files changed (55) hide show
  1. package/README.md +198 -188
  2. package/package.json +8 -8
  3. package/src/agents/built-ins.ts +13 -66
  4. package/src/agents/catalog.ts +19 -2
  5. package/src/agents/discovery.ts +31 -15
  6. package/src/auto-transport.ts +7 -1
  7. package/src/child-peer-bridge.ts +124 -0
  8. package/src/child-peer-tools.ts +132 -0
  9. package/src/completion-delivery.ts +19 -5
  10. package/src/completion-render.ts +189 -0
  11. package/src/completion-routing.ts +24 -0
  12. package/src/config-registration.ts +29 -4
  13. package/src/config-ui.ts +11 -17
  14. package/src/consult-registration.ts +3 -2
  15. package/src/consult-render.ts +1 -1
  16. package/src/create-stateful-transport.ts +15 -2
  17. package/src/execution-ui.ts +0 -72
  18. package/src/in-process-transport.ts +39 -7
  19. package/src/inspect-tool.ts +3 -1
  20. package/src/panel-planning.ts +2 -2
  21. package/src/panel-presets.ts +3 -0
  22. package/src/params.ts +1 -1
  23. package/src/peer-communication.ts +352 -0
  24. package/src/peer-transport.ts +49 -0
  25. package/src/persistence.ts +26 -1
  26. package/src/pi-args.ts +2 -0
  27. package/src/registry-types.ts +7 -0
  28. package/src/registry.ts +240 -41
  29. package/src/render.ts +2 -41
  30. package/src/result-contract.ts +20 -5
  31. package/src/rpc-transport.ts +56 -26
  32. package/src/runner.ts +13 -1
  33. package/src/settings.ts +4 -1
  34. package/src/spawn-idempotency.ts +2 -0
  35. package/src/stateful-agent-view.ts +3 -1
  36. package/src/stateful-guidance.ts +11 -11
  37. package/src/stateful-safety.ts +0 -45
  38. package/src/stateful-tool-params.ts +11 -3
  39. package/src/stateful.ts +359 -136
  40. package/src/subagents.ts +7 -9
  41. package/src/subprocess-transport.ts +49 -28
  42. package/src/task-path.ts +65 -0
  43. package/src/transport-ui.ts +0 -6
  44. package/src/transport.ts +2 -1
  45. package/src/usage-format.ts +42 -0
  46. package/src/workflow-ui.ts +4 -4
  47. package/src/automation-contract.ts +0 -709
  48. package/src/automation-planner.ts +0 -65
  49. package/src/automation-registration.ts +0 -137
  50. package/src/automation-tool.ts +0 -40
  51. package/src/automation.ts +0 -435
  52. package/src/execution-profiles.ts +0 -95
  53. package/src/workflow-plan-compiler.ts +0 -618
  54. package/src/workflow-plan-patch.ts +0 -636
  55. package/src/workflow-planning-benchmark.ts +0 -95
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,46 +27,31 @@ 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
- import {
70
- assertFollowUpWriteAllowed,
71
- assertNoSharedWriteConflict,
72
- confirmProjectAgent,
73
- } from "./stateful-safety.js";
50
+ import { confirmProjectAgent } from "./stateful-safety.js";
51
+ import { MAX_TASK_NAME_LENGTH } from "./task-path.js";
74
52
  import { MAX_SUBAGENT_TOOL_CALLS, MAX_SUBAGENT_TURNS } from "./turn-budget.js";
75
53
 
76
- export {
77
- assertFollowUpWriteAllowed,
78
- assertNoSharedWriteConflict,
79
- isWriteCapable,
80
- } from "./stateful-safety.js";
54
+ export { isWriteCapable } from "./stateful-safety.js";
81
55
 
82
56
  import {
83
57
  MailboxParamsSchema,
@@ -85,7 +59,126 @@ import {
85
59
  validateMailboxParams,
86
60
  validateManageParams,
87
61
  } from "./stateful-tool-params.js";
88
- import { WorkspaceManager } from "./workspace.js";
62
+ import type { WorkspaceManager } from "./workspace.js";
63
+
64
+ type CwdPolicyModule = typeof import("./cwd-policy.js");
65
+ type StateLifecycleModule = typeof import("./stateful-lifecycle.js");
66
+
67
+ type StatefulSessionModules = {
68
+ broker: typeof import("./completion-delivery.js");
69
+ peerCommunication: typeof import("./peer-communication.js");
70
+ context: typeof import("./context.js");
71
+ transport: typeof import("./create-stateful-transport.js");
72
+ cwdPolicy: CwdPolicyModule;
73
+ persistence: typeof import("./persistence.js");
74
+ registry: typeof import("./registry.js");
75
+ lifecycle: StateLifecycleModule;
76
+ };
77
+
78
+ type StatefulSpawnModules = {
79
+ agents: typeof import("./agents/discovery.js");
80
+ capabilityGrant: typeof import("./capability-grant.js");
81
+ context: typeof import("./context.js");
82
+ cwdPolicy: CwdPolicyModule;
83
+ delegationContract: typeof import("./delegation-contract.js");
84
+ runtimePolicy: typeof import("./execution/runtime-policy.js");
85
+ retainedSemanticState: typeof import("./retained-semantic-state.js");
86
+ semanticSnapshot: typeof import("./semantic-snapshot.js");
87
+ spawnIdempotency: typeof import("./spawn-idempotency.js");
88
+ };
89
+
90
+ let statefulSessionModules: Promise<StatefulSessionModules> | undefined;
91
+ let statefulSpawnModules: Promise<StatefulSpawnModules> | undefined;
92
+ let workspaceModule: Promise<typeof import("./workspace.js")> | undefined;
93
+
94
+ function loadStatefulSessionModules(): Promise<StatefulSessionModules> {
95
+ statefulSessionModules ??= Promise.all([
96
+ import("./completion-delivery.js"),
97
+ import("./peer-communication.js"),
98
+ import("./context.js"),
99
+ import("./create-stateful-transport.js"),
100
+ import("./cwd-policy.js"),
101
+ import("./persistence.js"),
102
+ import("./registry.js"),
103
+ import("./stateful-lifecycle.js"),
104
+ ])
105
+ .then(
106
+ ([
107
+ broker,
108
+ peerCommunication,
109
+ context,
110
+ transport,
111
+ cwdPolicy,
112
+ persistence,
113
+ registry,
114
+ lifecycle,
115
+ ]) => ({
116
+ broker,
117
+ peerCommunication,
118
+ context,
119
+ transport,
120
+ cwdPolicy,
121
+ persistence,
122
+ registry,
123
+ lifecycle,
124
+ }),
125
+ )
126
+ .catch((error: unknown) => {
127
+ statefulSessionModules = undefined;
128
+ throw error;
129
+ });
130
+ return statefulSessionModules;
131
+ }
132
+
133
+ function loadStatefulSpawnModules(): Promise<StatefulSpawnModules> {
134
+ statefulSpawnModules ??= Promise.all([
135
+ import("./agents/discovery.js"),
136
+ import("./capability-grant.js"),
137
+ import("./context.js"),
138
+ import("./cwd-policy.js"),
139
+ import("./delegation-contract.js"),
140
+ import("./execution/runtime-policy.js"),
141
+ import("./retained-semantic-state.js"),
142
+ import("./semantic-snapshot.js"),
143
+ import("./spawn-idempotency.js"),
144
+ ])
145
+ .then(
146
+ ([
147
+ agents,
148
+ capabilityGrant,
149
+ context,
150
+ cwdPolicy,
151
+ delegationContract,
152
+ runtimePolicy,
153
+ retainedSemanticState,
154
+ semanticSnapshot,
155
+ spawnIdempotency,
156
+ ]) => ({
157
+ agents,
158
+ capabilityGrant,
159
+ context,
160
+ cwdPolicy,
161
+ delegationContract,
162
+ runtimePolicy,
163
+ retainedSemanticState,
164
+ semanticSnapshot,
165
+ spawnIdempotency,
166
+ }),
167
+ )
168
+ .catch((error: unknown) => {
169
+ statefulSpawnModules = undefined;
170
+ throw error;
171
+ });
172
+ return statefulSpawnModules;
173
+ }
174
+
175
+ async function loadWorkspaceModule(): Promise<typeof import("./workspace.js")> {
176
+ workspaceModule ??= import("./workspace.js").catch((error: unknown) => {
177
+ workspaceModule = undefined;
178
+ throw error;
179
+ });
180
+ return workspaceModule;
181
+ }
89
182
 
90
183
  const ContextModeSchema = Type.Union([
91
184
  StringEnum(["none", "all", "summary"] as const),
@@ -180,13 +273,20 @@ export function registerStatefulSubagents(
180
273
  let runtimeLimits = resolveStatefulLimits(settings);
181
274
  let agentCatalog = "";
182
275
  let completionBroker: CompletionDeliveryBroker | undefined;
276
+ let peerBroker: import("./peer-communication.js").PeerCommunicationBroker | undefined;
183
277
  let refreshSpawnToolRegistration: (() => void) | undefined;
184
278
  let registry: AgentRegistry | undefined;
185
279
  let persistence: AgentPersistence | undefined;
186
280
  let sweepTimer: NodeJS.Timeout | undefined;
187
281
  let runtimeGeneration = 0;
188
282
  let runtimeTransition: Promise<void> = Promise.resolve();
189
- const workspaceManager = dependencies.workspaceManager ?? new WorkspaceManager();
283
+ let workspaceManager = dependencies.workspaceManager;
284
+ const getWorkspaceManager = async () => {
285
+ if (workspaceManager) return workspaceManager;
286
+ const { WorkspaceManager } = await loadWorkspaceModule();
287
+ workspaceManager = new WorkspaceManager();
288
+ return workspaceManager;
289
+ };
190
290
  const isolatedAgents = new Map<string, string>();
191
291
  const seenMessageIds = new Set<string>();
192
292
  type PendingIdempotentSpawn = {
@@ -206,11 +306,12 @@ export function registerStatefulSubagents(
206
306
  const currentRegistry = registry;
207
307
  const currentPersistence = persistence;
208
308
  if (!currentRegistry) return 0;
309
+ const currentWorkspaceManager = await getWorkspaceManager();
209
310
  const count = currentRegistry.list().length;
210
311
  const clear = async () => {
211
312
  await currentRegistry.closeAll();
212
313
  if (generation !== runtimeGeneration) return;
213
- await workspaceManager.cleanupAll();
314
+ await currentWorkspaceManager.cleanupAll();
214
315
  isolatedAgents.clear();
215
316
  seenMessageIds.clear();
216
317
  await currentPersistence?.delete();
@@ -268,6 +369,8 @@ export function registerStatefulSubagents(
268
369
  const generation = ++runtimeGeneration;
269
370
  completionBroker?.close();
270
371
  completionBroker = undefined;
372
+ const previousPeerBroker = peerBroker;
373
+ peerBroker = undefined;
271
374
  if (sweepTimer) clearInterval(sweepTimer);
272
375
  sweepTimer = undefined;
273
376
  const previousRegistry = registry;
@@ -277,7 +380,13 @@ export function registerStatefulSubagents(
277
380
  seenMessageIds.clear();
278
381
  pendingIdempotentSpawns.clear();
279
382
  const initialize = async () => {
280
- const cleanupErrors = await disposeStatefulRuntime(previousRegistry, workspaceManager);
383
+ await previousPeerBroker?.close();
384
+ const currentWorkspaceManager = await getWorkspaceManager();
385
+ const modules = await loadStatefulSessionModules();
386
+ const cleanupErrors = await modules.lifecycle.disposeStatefulRuntime(
387
+ previousRegistry,
388
+ currentWorkspaceManager,
389
+ );
281
390
  if (generation !== runtimeGeneration) return;
282
391
  if (cleanupErrors.length > 0 && ctx.hasUI) {
283
392
  ctx.ui.notify(
@@ -293,39 +402,75 @@ export function registerStatefulSubagents(
293
402
  ctx.sessionManager.getSessionId?.() ??
294
403
  ctx.sessionManager.getSessionFile?.() ??
295
404
  `ephemeral:${ctx.cwd}`;
296
- const sessionPersistence = new AgentPersistence(owner, {
405
+ const sessionPersistence = new modules.persistence.AgentPersistence(owner, {
297
406
  retentionDays: sessionSettings.retentionDays,
298
407
  maxStoredAgents: nextLimits.maxStoredAgents,
299
408
  });
300
409
  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");
410
+ let transport: import("./transport.js").SubagentTransport;
411
+ const sessionBroker = new modules.broker.CompletionDeliveryBroker(
412
+ pi,
413
+ ctx,
414
+ completionDelivery,
415
+ {
416
+ onDeliveryError: (error) => {
417
+ if (!ctx.hasUI) return;
418
+ const reason = error instanceof Error ? error.message : String(error);
419
+ ctx.ui.notify(`Subagent completion delivery failed: ${reason}`, "warning");
420
+ },
421
+ onAcknowledged: (completions, deliveredAt) => {
422
+ if (generation !== runtimeGeneration) return;
423
+ for (const completion of completions) {
424
+ void nextRegistry
425
+ .markCompletionDelivered(completion.completionId, deliveredAt)
426
+ .catch((error: unknown) => {
427
+ if (!ctx.hasUI || generation !== runtimeGeneration) return;
428
+ const reason = error instanceof Error ? error.message : String(error);
429
+ ctx.ui.notify(`Subagent completion acknowledgement failed: ${reason}`, "warning");
430
+ });
431
+ }
432
+ },
306
433
  },
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
- }
434
+ );
435
+ const sessionPeerBroker = new modules.peerCommunication.PeerCommunicationBroker({
436
+ getRegistry: () => nextRegistry,
437
+ sendRoot: ({ message, senderPath }) => {
438
+ pi.appendEntry("pi-subagent-peer-message", {
439
+ messageId: message.id,
440
+ senderId: message.senderId,
441
+ senderPath,
442
+ content: modules.context.redactPrivateText(message.content),
443
+ });
444
+ pi.sendMessage(
445
+ {
446
+ customType: "pi-subagent-peer-message",
447
+ content: [
448
+ "Message Type: SUBAGENT_PEER_MESSAGE",
449
+ "Protocol: pi-subagents:v1",
450
+ `Message ID: ${message.id}`,
451
+ `Sender ID: ${message.senderId}`,
452
+ `Sender Path: ${senderPath}`,
453
+ "Payload:",
454
+ message.content,
455
+ ].join("\n"),
456
+ display: true,
457
+ details: { ...message, senderPath },
458
+ },
459
+ { deliverAs: "steer", triggerTurn: false },
460
+ );
318
461
  },
462
+ dispatch: (recipient, message) => transport.deliverMessage?.(recipient, message) ?? false,
319
463
  });
320
- const transport = createStatefulTransport({
464
+ transport = modules.transport.createStatefulTransport({
321
465
  kind: transportKind,
322
466
  modelRegistry: ctx.modelRegistry,
323
467
  getParentRuntime: () => ({ ...parentRuntime }),
324
468
  getSettings: getCurrentSettings,
325
469
  createInProcessSession: dependencies.createInProcessSession,
470
+ peerRuntime: sessionPeerBroker,
326
471
  loadTransport: dependencies.loadTransport,
327
472
  });
328
- nextRegistry = new AgentRegistry(transport, {
473
+ nextRegistry = new modules.registry.AgentRegistry(transport, {
329
474
  maxAgents: nextLimits.maxAgents,
330
475
  maxActiveTurns: nextLimits.maxActiveTurns,
331
476
  maxDepth: nextLimits.maxDepth,
@@ -344,17 +489,42 @@ export function registerStatefulSubagents(
344
489
  pi.appendEntry("pi-subagent-message", {
345
490
  senderId: message.senderId,
346
491
  recipientId: message.recipientId,
347
- content: redactPrivateText(message.content).slice(0, 160),
492
+ content: modules.context.redactPrivateText(message.content).slice(0, 160),
348
493
  });
494
+ if (
495
+ agent.state === "running" &&
496
+ message.deduplicationKey?.startsWith("completion:")
497
+ ) {
498
+ try {
499
+ await transport.deliverMessage?.(agent, {
500
+ ...message,
501
+ content: modules.context.redactPrivateText(message.content),
502
+ });
503
+ } catch {
504
+ // The durable parent mailbox remains the retry path for the next turn.
505
+ }
506
+ if (generation !== runtimeGeneration) return;
507
+ }
349
508
  }
350
509
  }
351
510
  },
352
511
  onTurnComplete: (completion) => {
353
- if (generation === runtimeGeneration) sessionBroker.enqueue(completion);
512
+ if (generation === runtimeGeneration && completion.recipientId === "root") {
513
+ sessionBroker.enqueue(completion);
514
+ }
354
515
  },
355
516
  });
356
517
  const persisted = sessionPersistence.load();
357
- const orphanCleanupFailures = await cleanupPersistedWorkspaces(persisted, workspaceManager);
518
+ const orphanCleanupFailures = await modules.lifecycle.cleanupPersistedWorkspaces(
519
+ persisted,
520
+ currentWorkspaceManager,
521
+ );
522
+ if (generation !== runtimeGeneration) {
523
+ sessionBroker.close();
524
+ await sessionPeerBroker.close();
525
+ await modules.lifecycle.disposeStatefulRuntime(nextRegistry, currentWorkspaceManager);
526
+ return;
527
+ }
358
528
  if (ctx.hasUI && orphanCleanupFailures > 0) {
359
529
  ctx.ui.notify("Some orphaned subagent worktrees could not be cleaned", "warning");
360
530
  }
@@ -367,12 +537,14 @@ export function registerStatefulSubagents(
367
537
  )
368
538
  .flatMap((agent) => {
369
539
  try {
370
- const target = resolveSubagentTarget({
540
+ const target = modules.cwdPolicy.resolveSubagentTarget({
371
541
  workspace: ctx.cwd,
372
542
  requestedCwd: agent.cwd,
373
543
  currentProjectTrusted: ctx.isProjectTrusted(),
374
544
  });
375
- return [{ ...agent, cwd: target.cwd, target: targetPolicyAudit(target) }];
545
+ return [
546
+ { ...agent, cwd: target.cwd, target: modules.cwdPolicy.targetPolicyAudit(target) },
547
+ ];
376
548
  } catch {
377
549
  return [];
378
550
  }
@@ -383,14 +555,16 @@ export function registerStatefulSubagents(
383
555
  nextRegistry.restore(restored);
384
556
  if (generation !== runtimeGeneration) {
385
557
  sessionBroker.close();
386
- await disposeStatefulRuntime(nextRegistry, workspaceManager);
558
+ await sessionPeerBroker.close();
559
+ await modules.lifecycle.disposeStatefulRuntime(nextRegistry, currentWorkspaceManager);
387
560
  return;
388
561
  }
389
562
  registry = nextRegistry;
390
563
  persistence = sessionPersistence;
391
564
  completionBroker = sessionBroker;
565
+ peerBroker = sessionPeerBroker;
392
566
  for (const completion of nextRegistry.listPendingCompletions()) {
393
- sessionBroker.enqueue(completion);
567
+ if (completion.recipientId === "root") sessionBroker.enqueue(completion);
394
568
  }
395
569
  runtimeLimits = nextLimits;
396
570
  refreshSpawnToolRegistration?.();
@@ -436,6 +610,8 @@ export function registerStatefulSubagents(
436
610
  runtimeGeneration++;
437
611
  completionBroker?.close();
438
612
  completionBroker = undefined;
613
+ const previousPeerBroker = peerBroker;
614
+ peerBroker = undefined;
439
615
  if (sweepTimer) clearInterval(sweepTimer);
440
616
  sweepTimer = undefined;
441
617
  const previousRegistry = registry;
@@ -445,7 +621,10 @@ export function registerStatefulSubagents(
445
621
  seenMessageIds.clear();
446
622
  pendingIdempotentSpawns.clear();
447
623
  const shutdown = async () => {
448
- const errors = await disposeStatefulRuntime(previousRegistry, workspaceManager);
624
+ await previousPeerBroker?.close();
625
+ const currentWorkspaceManager = await getWorkspaceManager();
626
+ const { disposeStatefulRuntime } = await import("./stateful-lifecycle.js");
627
+ const errors = await disposeStatefulRuntime(previousRegistry, currentWorkspaceManager);
449
628
  if (errors.length > 0 && ctx.hasUI) {
450
629
  ctx.ui.notify(`Subagent shutdown cleanup reported ${errors.length} error(s).`, "warning");
451
630
  }
@@ -456,7 +635,7 @@ export function registerStatefulSubagents(
456
635
  });
457
636
 
458
637
  const baseSpawnDescription = () =>
459
- `Start an addressable background subagent with an optional thinking level and execution budgets chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously. Detached capacity: ${runtimeLimits.maxAgents} retained agents, ${runtimeLimits.maxActiveTurns} active turns, ${runtimeLimits.maxChildrenPerAgent} direct children per agent, and depth ${runtimeLimits.maxDepth}. Working-directory target policy: ${dependencies.getSettings?.()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`;
638
+ `Start an addressable background subagent with an opaque agentId and canonical taskPath, plus an optional thinking level and execution budgets chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously. Detached capacity: ${runtimeLimits.maxAgents} retained agents, ${runtimeLimits.maxActiveTurns} active turns, ${runtimeLimits.maxChildrenPerAgent} direct children per agent, and depth ${runtimeLimits.maxDepth}. Working-directory target policy: ${dependencies.getSettings?.()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`;
460
639
  const spawnTool = defineTool({
461
640
  name: "subagent_spawn",
462
641
  label: "Spawn Subagent",
@@ -465,6 +644,15 @@ export function registerStatefulSubagents(
465
644
  promptGuidelines: createSpawnPromptGuidelines(completionDelivery, blockingEnabled),
466
645
  parameters: Type.Object({
467
646
  agent: Type.String({ minLength: 1 }),
647
+ taskName: Type.Optional(
648
+ Type.String({
649
+ minLength: 1,
650
+ maxLength: MAX_TASK_NAME_LENGTH,
651
+ pattern: "^[a-z0-9_]+$",
652
+ description:
653
+ "Canonical path segment for this task; use lowercase letters, digits, and underscores.",
654
+ }),
655
+ ),
468
656
  task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
469
657
  thinkingLevel: Type.Optional(StatefulThinkingLevelSchema),
470
658
  timeoutMs: Type.Optional(StatefulTimeoutSchema),
@@ -476,13 +664,18 @@ export function registerStatefulSubagents(
476
664
  contextEntryIds: Type.Optional(
477
665
  Type.Array(Type.String(), { description: "Optional selected session entry IDs." }),
478
666
  ),
479
- parentId: Type.Optional(Type.String({ description: "Optional parent agent ID." })),
667
+ parentId: Type.Optional(
668
+ Type.String({ description: "Optional parent agent ID or canonical task path." }),
669
+ ),
480
670
  allowConcurrentWrites: Type.Optional(
481
- Type.Boolean({ description: "Override the shared-workspace write conflict guard." }),
671
+ Type.Boolean({
672
+ description:
673
+ "Deprecated compatibility field; shared-workspace concurrency is allowed by default.",
674
+ }),
482
675
  ),
483
676
  workspaceMode: Type.Optional(
484
677
  StringEnum(["shared", "worktree"] as const, {
485
- description: "Use the shared workspace or an opt-in disposable Git worktree.",
678
+ description: "Use the shared workspace (default) or an opt-in disposable Git worktree.",
486
679
  }),
487
680
  ),
488
681
  contract: Type.Optional(DelegationContractSchema),
@@ -502,30 +695,47 @@ export function registerStatefulSubagents(
502
695
  }),
503
696
  ...createStatefulToolRenderer("spawn"),
504
697
  async execute(_id, params, signal, _update, ctx) {
698
+ const generation = runtimeGeneration;
699
+ const capturedRegistry = registry;
700
+ let modules: StatefulSpawnModules;
701
+ try {
702
+ modules = await loadStatefulSpawnModules();
703
+ } catch (error) {
704
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
705
+ throw error;
706
+ }
707
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
708
+ let currentWorkspaceManager: WorkspaceManager;
709
+ try {
710
+ currentWorkspaceManager = await getWorkspaceManager();
711
+ } catch (error) {
712
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
713
+ throw error;
714
+ }
715
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
505
716
  const scope = (params.agentScope ?? "user") as AgentScope;
506
717
  const resultFormat = (params.resultFormat ?? "text") as SubagentResultFormat;
507
- const contract = normalizeDelegationContract(params.contract);
718
+ const contract = modules.delegationContract.normalizeDelegationContract(params.contract);
508
719
  if (params.contract !== undefined && !contract) {
509
720
  throw new Error(
510
721
  "subagent_spawn contract must be a valid pi-subagents:delegation:v2 object",
511
722
  );
512
723
  }
513
- assertSubagentDepthAllowed();
724
+ modules.runtimePolicy.assertSubagentDepthAllowed();
514
725
  assertSpawnIdempotencyKey(params.idempotencyKey);
515
- const generation = runtimeGeneration;
516
726
  const currentSettings = getCurrentSettings();
517
- const target = resolveSubagentTarget({
727
+ const target = modules.cwdPolicy.resolveSubagentTarget({
518
728
  workspace: ctx.cwd,
519
729
  requestedCwd: params.cwd,
520
730
  currentProjectTrusted: ctx.isProjectTrusted(),
521
731
  });
522
- assertDelegationTargetAllowed(
732
+ modules.cwdPolicy.assertDelegationTargetAllowed(
523
733
  target,
524
734
  currentSettings?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
525
735
  );
526
736
  const cwd = target.cwd;
527
737
  const mode = resolveSpawnContextMode(params.context, params.contextEntryIds);
528
- const snapshot = buildContextSnapshot(
738
+ const snapshot = modules.context.buildContextSnapshot(
529
739
  ctx.sessionManager.getBranch(),
530
740
  mode,
531
741
  DEFAULT_MAX_CONTEXT_BYTES,
@@ -534,28 +744,30 @@ export function registerStatefulSubagents(
534
744
  if ((scope === "project" || scope === "both") && !ctx.isProjectTrusted()) {
535
745
  throw new Error("Project-local subagent definitions require a trusted project");
536
746
  }
537
- const resolvedAgents = discoverAgents(cwd, scope, currentSettings).agents;
747
+ const resolvedAgents = modules.agents.discoverAgents(cwd, scope, currentSettings).agents;
538
748
  const resolvedAgent = resolvedAgents.find((agent) => agent.name === params.agent);
539
749
  if (!resolvedAgent) {
540
750
  const available = resolvedAgents.map((agent) => agent.name).join(", ") || "none";
541
751
  throw new Error(`Unknown subagent ${params.agent}. Available agents: ${available}`);
542
752
  }
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
- });
753
+ const targetSnapshot = modules.cwdPolicy.targetPolicyAudit(target);
754
+ const { executionPlan, semanticSnapshot } =
755
+ await modules.retainedSemanticState.buildRetainedSemanticState({
756
+ agent: resolvedAgent,
757
+ contract,
758
+ target: targetSnapshot,
759
+ cwd,
760
+ workspaceMode: params.workspaceMode === "worktree" ? "worktree" : "shared",
761
+ transport: transportKind,
762
+ resultFormat,
763
+ thinkingLevel: params.thinkingLevel,
764
+ timeoutMs: params.timeoutMs,
765
+ taskGeneration: 1,
766
+ });
556
767
  assertCurrentSpawn(signal, generation, runtimeGeneration);
557
- const requestHash = hashSpawnRequest({
768
+ const requestHash = modules.spawnIdempotency.hashSpawnRequest({
558
769
  agent: params.agent,
770
+ taskName: params.taskName,
559
771
  task: params.task,
560
772
  cwd,
561
773
  agentScope: scope,
@@ -572,7 +784,10 @@ export function registerStatefulSubagents(
572
784
  contract,
573
785
  resultFormat,
574
786
  });
575
- const ownedRegistry = requireRegistry();
787
+ if (!capturedRegistry) {
788
+ throw new Error("Stateful subagents are not initialized for this session");
789
+ }
790
+ const ownedRegistry = capturedRegistry;
576
791
  const retained = ownedRegistry.findBySpawnIdempotencyKey(params.idempotencyKey, requestHash);
577
792
  if (retained) return result(retained, `Reused ${retained.agent} as ${retained.id}.`);
578
793
  const foundPending = params.idempotencyKey
@@ -621,35 +836,27 @@ export function registerStatefulSubagents(
621
836
  throw new Error("Project-local subagent definitions cannot run in a detached worktree");
622
837
  }
623
838
  const requestedCwd = cwd;
624
- if ((params.workspaceMode ?? "shared") === "shared" && !params.allowConcurrentWrites) {
625
- assertNoSharedWriteConflict(
626
- ownedRegistry,
627
- params.agent,
628
- requestedCwd,
629
- scope,
630
- currentSettings,
631
- );
632
- }
633
839
  const workspaceOwner = `pending-${randomUUID()}`;
634
840
  const workspace =
635
841
  params.workspaceMode === "worktree"
636
- ? await workspaceManager.create(workspaceOwner, requestedCwd)
842
+ ? await currentWorkspaceManager.create(workspaceOwner, requestedCwd)
637
843
  : undefined;
638
844
  try {
639
845
  assertCurrentSpawn(signal, generation, runtimeGeneration);
640
846
  } catch (error) {
641
- if (workspace) await workspaceManager.cleanup(workspaceOwner);
847
+ if (workspace) await currentWorkspaceManager.cleanup(workspaceOwner);
642
848
  throw error;
643
849
  }
644
850
  let agent: ManagedAgent | undefined;
645
851
  try {
646
- const capabilityGrant = issueCapabilityGrant(
852
+ const capabilityGrant = modules.capabilityGrant.issueCapabilityGrant(
647
853
  executionPlan,
648
854
  Date.now(),
649
855
  Math.max(1, (params.timeoutMs ?? resolvedAgent.timeoutMs ?? 600_000) + 60_000),
650
856
  );
651
857
  agent = await ownedRegistry.spawn({
652
858
  agent: params.agent,
859
+ taskName: params.taskName,
653
860
  task: params.task,
654
861
  cwd: workspace?.path ?? requestedCwd,
655
862
  agentScope: scope,
@@ -678,21 +885,21 @@ export function registerStatefulSubagents(
678
885
  assertCurrentSpawn(signal, generation, runtimeGeneration);
679
886
  } catch (error) {
680
887
  if (agent) await ownedRegistry.closeTree(agent.id).catch(() => undefined);
681
- if (workspace) await workspaceManager.cleanup(workspaceOwner);
888
+ if (workspace) await currentWorkspaceManager.cleanup(workspaceOwner);
682
889
  throw error;
683
890
  }
684
891
  if (!agent) throw new Error("Subagent spawn completed without a retained agent");
685
892
  if (workspace && agent.cwd === workspace.path) isolatedAgents.set(agent.id, workspaceOwner);
686
- else if (workspace) await workspaceManager.cleanup(workspaceOwner);
893
+ else if (workspace) await currentWorkspaceManager.cleanup(workspaceOwner);
687
894
  assertCurrentSpawn(signal, generation, runtimeGeneration);
688
895
  resolvePending?.(agent);
689
896
  const deliveryNote =
690
897
  completionDelivery === "auto-resume"
691
- ? "If no useful local work remains, briefly tell the user what was launched and end the response; auto-resume will request synthesis after completion."
692
- : "End the response without the result only when the current response does not depend on it; next-turn delivery will not wake an idle root.";
898
+ ? "Auto-resume will request synthesis after completion."
899
+ : "The current response must not depend on the result because next-turn delivery will not wake an idle root.";
693
900
  return result(
694
901
  agent,
695
- `Spawned ${agent.agent} as ${agent.id}. Do useful non-overlapping work immediately. ${deliveryNote} Do not poll for progress.`,
902
+ `Spawned ${agent.agent} as ${agent.taskPath ?? agent.id} (${agent.id}). Continue the identified non-overlapping local work immediately; do not merely announce the spawn or end while useful local work remains. Only an explicit user-requested specialist model, tool-profile, or isolation exception may lack concurrent local work. ${deliveryNote} Do not poll for progress.`,
696
903
  );
697
904
  } catch (error) {
698
905
  rejectPending?.(error);
@@ -722,7 +929,7 @@ export function registerStatefulSubagents(
722
929
  "Send follow-up work to a reusable retained subagent and start a new turn. Semantic resource skew requires explicit revalidation. Use subagent_mailbox for queue-only messages.",
723
930
  promptSnippet: "Start a new detached follow-up turn on a retained subagent",
724
931
  parameters: Type.Object({
725
- agentId: Type.String(),
932
+ agentId: Type.String({ description: "Retained agent ID or canonical task path." }),
726
933
  task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
727
934
  timeoutMs: Type.Optional(
728
935
  Type.Integer({
@@ -740,32 +947,41 @@ export function registerStatefulSubagents(
740
947
  }),
741
948
  ),
742
949
  allowConcurrentWrites: Type.Optional(
743
- Type.Boolean({ description: "Override the shared-workspace write conflict guard." }),
950
+ Type.Boolean({
951
+ description:
952
+ "Deprecated compatibility field; shared-workspace concurrency is allowed by default.",
953
+ }),
744
954
  ),
745
955
  }),
746
956
  ...createStatefulToolRenderer("send"),
747
957
  async execute(_id, params, signal, _update, ctx) {
748
958
  const generation = runtimeGeneration;
749
959
  const ownedRegistry = requireRegistry();
960
+ let modules: StatefulSpawnModules;
961
+ try {
962
+ modules = await loadStatefulSpawnModules();
963
+ } catch (error) {
964
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
965
+ throw error;
966
+ }
967
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
750
968
  const currentSettings = getCurrentSettings();
751
969
  const existing = ownedRegistry.get(params.agentId);
752
970
  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);
971
+ const currentAgent = modules.agents
972
+ .discoverAgents(existing.cwd, existing.agentScope ?? "user", currentSettings)
973
+ .agents.find((agent) => agent.name === existing.agent);
758
974
  if (!currentAgent) throw new Error(`Unknown retained subagent definition: ${existing.agent}`);
759
975
  const resolvedFollowUpTarget =
760
976
  existing.workspaceMode === "worktree"
761
977
  ? undefined
762
- : resolveSubagentTarget({
978
+ : modules.cwdPolicy.resolveSubagentTarget({
763
979
  workspace: ctx.cwd,
764
980
  requestedCwd: existing.cwd,
765
981
  currentProjectTrusted: ctx.isProjectTrusted(),
766
982
  });
767
983
  if (resolvedFollowUpTarget) {
768
- assertDelegationTargetAllowed(
984
+ modules.cwdPolicy.assertDelegationTargetAllowed(
769
985
  resolvedFollowUpTarget,
770
986
  currentSettings?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
771
987
  );
@@ -776,9 +992,11 @@ export function registerStatefulSubagents(
776
992
  const currentTarget =
777
993
  existing.workspaceMode === "worktree" && existing.target
778
994
  ? existing.target
779
- : targetPolicyAudit(resolvedFollowUpTarget as NonNullable<typeof resolvedFollowUpTarget>);
995
+ : modules.cwdPolicy.targetPolicyAudit(
996
+ resolvedFollowUpTarget as NonNullable<typeof resolvedFollowUpTarget>,
997
+ );
780
998
  const { executionPlan: currentPlan, semanticSnapshot: currentSnapshot } =
781
- await buildRetainedSemanticState({
999
+ await modules.retainedSemanticState.buildRetainedSemanticState({
782
1000
  agent: currentAgent,
783
1001
  contract: existing.contract,
784
1002
  target: currentTarget,
@@ -798,7 +1016,10 @@ export function registerStatefulSubagents(
798
1016
  });
799
1017
  assertCurrentSpawn(signal, generation, runtimeGeneration);
800
1018
  const compatibility = existing.semanticSnapshot
801
- ? evaluateSemanticCompatibility(existing.semanticSnapshot, currentSnapshot)
1019
+ ? modules.semanticSnapshot.evaluateSemanticCompatibility(
1020
+ existing.semanticSnapshot,
1021
+ currentSnapshot,
1022
+ )
802
1023
  : { status: "warning" as const, changedComponents: ["legacy-missing-snapshot"] };
803
1024
  if (
804
1025
  (compatibility.status === "needs-revalidation" || compatibility.status === "rejected") &&
@@ -817,14 +1038,7 @@ export function registerStatefulSubagents(
817
1038
  currentSettings,
818
1039
  );
819
1040
  assertCurrentSpawn(signal, generation, runtimeGeneration);
820
- assertFollowUpWriteAllowed(
821
- ownedRegistry,
822
- existing,
823
- params.allowConcurrentWrites ?? false,
824
- isolatedAgents.has(existing.id),
825
- currentSettings,
826
- );
827
- const currentGrant = issueCapabilityGrant(
1041
+ const currentGrant = modules.capabilityGrant.issueCapabilityGrant(
828
1042
  currentPlan,
829
1043
  Date.now(),
830
1044
  Math.max(1, (params.timeoutMs ?? existing.timeoutMs ?? 600_000) + 60_000),
@@ -838,6 +1052,7 @@ export function registerStatefulSubagents(
838
1052
  ? { status: "warning", changedComponents: compatibility.changedComponents }
839
1053
  : compatibility,
840
1054
  );
1055
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
841
1056
  const agent = await ownedRegistry.followUp(params.agentId, params.task, {
842
1057
  timeoutMs: params.timeoutMs,
843
1058
  idleTimeoutMs: params.idleTimeoutMs,
@@ -860,6 +1075,14 @@ export function registerStatefulSubagents(
860
1075
  async execute(_id, params, signal): Promise<StatefulActionToolResult> {
861
1076
  const generation = runtimeGeneration;
862
1077
  const ownedRegistry = requireRegistry();
1078
+ let currentWorkspaceManager: WorkspaceManager;
1079
+ try {
1080
+ currentWorkspaceManager = await getWorkspaceManager();
1081
+ } catch (error) {
1082
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
1083
+ throw error;
1084
+ }
1085
+ assertCurrentSpawn(signal, generation, runtimeGeneration);
863
1086
  const ownedAgent = (agentId: string): ManagedAgent => {
864
1087
  const value = ownedRegistry.get(agentId);
865
1088
  if (!value) throw new Error(`Unknown subagent: ${agentId}`);
@@ -886,7 +1109,7 @@ export function registerStatefulSubagents(
886
1109
  const existing = ownedRegistry.get(agentId);
887
1110
  if (existing?.state === "closed" && !operation.subtree) {
888
1111
  const pendingOwner = isolatedAgents.get(existing.id);
889
- if (pendingOwner) await workspaceManager.cleanup(pendingOwner);
1112
+ if (pendingOwner) await currentWorkspaceManager.cleanup(pendingOwner);
890
1113
  assertCurrentSpawn(signal, generation, runtimeGeneration);
891
1114
  isolatedAgents.delete(existing.id);
892
1115
  return result(existing, `Closed ${existing.id}.`);
@@ -896,7 +1119,7 @@ export function registerStatefulSubagents(
896
1119
  try {
897
1120
  agents = await ownedRegistry.closeTree(agentId);
898
1121
  } finally {
899
- await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, workspaceManager);
1122
+ await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, currentWorkspaceManager);
900
1123
  }
901
1124
  assertCurrentSpawn(signal, generation, runtimeGeneration);
902
1125
  return {
@@ -911,7 +1134,7 @@ export function registerStatefulSubagents(
911
1134
  try {
912
1135
  agent = await ownedRegistry.close(agentId);
913
1136
  } finally {
914
- await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, workspaceManager);
1137
+ await cleanupClosedWorkspaces(ownedRegistry, isolatedAgents, currentWorkspaceManager);
915
1138
  }
916
1139
  assertCurrentSpawn(signal, generation, runtimeGeneration);
917
1140
  return result(agent, `Closed ${agent.id}.`);