@narumitw/pi-subagents 1.0.2 → 2.0.1

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 (47) hide show
  1. package/README.md +198 -188
  2. package/package.json +2 -2
  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-ui.ts +11 -17
  13. package/src/consult-registration.ts +3 -2
  14. package/src/create-stateful-transport.ts +15 -2
  15. package/src/execution-ui.ts +0 -72
  16. package/src/in-process-transport.ts +39 -7
  17. package/src/inspect-tool.ts +3 -1
  18. package/src/peer-communication.ts +352 -0
  19. package/src/peer-transport.ts +49 -0
  20. package/src/persistence.ts +26 -1
  21. package/src/pi-args.ts +2 -0
  22. package/src/registry-types.ts +7 -0
  23. package/src/registry.ts +240 -41
  24. package/src/result-contract.ts +20 -5
  25. package/src/rpc-transport.ts +56 -26
  26. package/src/runner.ts +13 -1
  27. package/src/spawn-idempotency.ts +2 -0
  28. package/src/stateful-agent-view.ts +3 -1
  29. package/src/stateful-guidance.ts +11 -11
  30. package/src/stateful-safety.ts +0 -45
  31. package/src/stateful-tool-params.ts +11 -3
  32. package/src/stateful.ts +119 -47
  33. package/src/subagents.ts +6 -8
  34. package/src/subprocess-transport.ts +49 -28
  35. package/src/task-path.ts +65 -0
  36. package/src/transport-ui.ts +0 -6
  37. package/src/transport.ts +2 -1
  38. package/src/workflow-ui.ts +4 -4
  39. package/src/automation-contract.ts +0 -709
  40. package/src/automation-planner.ts +0 -65
  41. package/src/automation-registration.ts +0 -137
  42. package/src/automation-tool.ts +0 -40
  43. package/src/automation.ts +0 -435
  44. package/src/execution-profiles.ts +0 -95
  45. package/src/workflow-plan-compiler.ts +0 -618
  46. package/src/workflow-plan-patch.ts +0 -636
  47. package/src/workflow-planning-benchmark.ts +0 -95
package/src/stateful.ts CHANGED
@@ -47,18 +47,11 @@ import { createSpawnPromptGuidelines } from "./stateful-guidance.js";
47
47
  import { assertCurrentSpawn, waitForOwnedSpawn } from "./stateful-lifecycle.js";
48
48
  import { resolveStatefulLimits, type StatefulLimits } from "./stateful-limits.js";
49
49
  import { createStatefulToolRenderer } from "./stateful-render.js";
50
- import {
51
- assertFollowUpWriteAllowed,
52
- assertNoSharedWriteConflict,
53
- confirmProjectAgent,
54
- } from "./stateful-safety.js";
50
+ import { confirmProjectAgent } from "./stateful-safety.js";
51
+ import { MAX_TASK_NAME_LENGTH } from "./task-path.js";
55
52
  import { MAX_SUBAGENT_TOOL_CALLS, MAX_SUBAGENT_TURNS } from "./turn-budget.js";
56
53
 
57
- export {
58
- assertFollowUpWriteAllowed,
59
- assertNoSharedWriteConflict,
60
- isWriteCapable,
61
- } from "./stateful-safety.js";
54
+ export { isWriteCapable } from "./stateful-safety.js";
62
55
 
63
56
  import {
64
57
  MailboxParamsSchema,
@@ -73,6 +66,7 @@ type StateLifecycleModule = typeof import("./stateful-lifecycle.js");
73
66
 
74
67
  type StatefulSessionModules = {
75
68
  broker: typeof import("./completion-delivery.js");
69
+ peerCommunication: typeof import("./peer-communication.js");
76
70
  context: typeof import("./context.js");
77
71
  transport: typeof import("./create-stateful-transport.js");
78
72
  cwdPolicy: CwdPolicyModule;
@@ -100,6 +94,7 @@ let workspaceModule: Promise<typeof import("./workspace.js")> | undefined;
100
94
  function loadStatefulSessionModules(): Promise<StatefulSessionModules> {
101
95
  statefulSessionModules ??= Promise.all([
102
96
  import("./completion-delivery.js"),
97
+ import("./peer-communication.js"),
103
98
  import("./context.js"),
104
99
  import("./create-stateful-transport.js"),
105
100
  import("./cwd-policy.js"),
@@ -107,15 +102,27 @@ function loadStatefulSessionModules(): Promise<StatefulSessionModules> {
107
102
  import("./registry.js"),
108
103
  import("./stateful-lifecycle.js"),
109
104
  ])
110
- .then(([broker, context, transport, cwdPolicy, persistence, registry, lifecycle]) => ({
111
- broker,
112
- context,
113
- transport,
114
- cwdPolicy,
115
- persistence,
116
- registry,
117
- lifecycle,
118
- }))
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
+ )
119
126
  .catch((error: unknown) => {
120
127
  statefulSessionModules = undefined;
121
128
  throw error;
@@ -266,6 +273,7 @@ export function registerStatefulSubagents(
266
273
  let runtimeLimits = resolveStatefulLimits(settings);
267
274
  let agentCatalog = "";
268
275
  let completionBroker: CompletionDeliveryBroker | undefined;
276
+ let peerBroker: import("./peer-communication.js").PeerCommunicationBroker | undefined;
269
277
  let refreshSpawnToolRegistration: (() => void) | undefined;
270
278
  let registry: AgentRegistry | undefined;
271
279
  let persistence: AgentPersistence | undefined;
@@ -361,6 +369,8 @@ export function registerStatefulSubagents(
361
369
  const generation = ++runtimeGeneration;
362
370
  completionBroker?.close();
363
371
  completionBroker = undefined;
372
+ const previousPeerBroker = peerBroker;
373
+ peerBroker = undefined;
364
374
  if (sweepTimer) clearInterval(sweepTimer);
365
375
  sweepTimer = undefined;
366
376
  const previousRegistry = registry;
@@ -370,6 +380,7 @@ export function registerStatefulSubagents(
370
380
  seenMessageIds.clear();
371
381
  pendingIdempotentSpawns.clear();
372
382
  const initialize = async () => {
383
+ await previousPeerBroker?.close();
373
384
  const currentWorkspaceManager = await getWorkspaceManager();
374
385
  const modules = await loadStatefulSessionModules();
375
386
  const cleanupErrors = await modules.lifecycle.disposeStatefulRuntime(
@@ -396,6 +407,7 @@ export function registerStatefulSubagents(
396
407
  maxStoredAgents: nextLimits.maxStoredAgents,
397
408
  });
398
409
  let nextRegistry: AgentRegistry;
410
+ let transport: import("./transport.js").SubagentTransport;
399
411
  const sessionBroker = new modules.broker.CompletionDeliveryBroker(
400
412
  pi,
401
413
  ctx,
@@ -420,12 +432,42 @@ export function registerStatefulSubagents(
420
432
  },
421
433
  },
422
434
  );
423
- const transport = modules.transport.createStatefulTransport({
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
+ );
461
+ },
462
+ dispatch: (recipient, message) => transport.deliverMessage?.(recipient, message) ?? false,
463
+ });
464
+ transport = modules.transport.createStatefulTransport({
424
465
  kind: transportKind,
425
466
  modelRegistry: ctx.modelRegistry,
426
467
  getParentRuntime: () => ({ ...parentRuntime }),
427
468
  getSettings: getCurrentSettings,
428
469
  createInProcessSession: dependencies.createInProcessSession,
470
+ peerRuntime: sessionPeerBroker,
429
471
  loadTransport: dependencies.loadTransport,
430
472
  });
431
473
  nextRegistry = new modules.registry.AgentRegistry(transport, {
@@ -449,11 +491,27 @@ export function registerStatefulSubagents(
449
491
  recipientId: message.recipientId,
450
492
  content: modules.context.redactPrivateText(message.content).slice(0, 160),
451
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
+ }
452
508
  }
453
509
  }
454
510
  },
455
511
  onTurnComplete: (completion) => {
456
- if (generation === runtimeGeneration) sessionBroker.enqueue(completion);
512
+ if (generation === runtimeGeneration && completion.recipientId === "root") {
513
+ sessionBroker.enqueue(completion);
514
+ }
457
515
  },
458
516
  });
459
517
  const persisted = sessionPersistence.load();
@@ -461,6 +519,12 @@ export function registerStatefulSubagents(
461
519
  persisted,
462
520
  currentWorkspaceManager,
463
521
  );
522
+ if (generation !== runtimeGeneration) {
523
+ sessionBroker.close();
524
+ await sessionPeerBroker.close();
525
+ await modules.lifecycle.disposeStatefulRuntime(nextRegistry, currentWorkspaceManager);
526
+ return;
527
+ }
464
528
  if (ctx.hasUI && orphanCleanupFailures > 0) {
465
529
  ctx.ui.notify("Some orphaned subagent worktrees could not be cleaned", "warning");
466
530
  }
@@ -491,14 +555,16 @@ export function registerStatefulSubagents(
491
555
  nextRegistry.restore(restored);
492
556
  if (generation !== runtimeGeneration) {
493
557
  sessionBroker.close();
558
+ await sessionPeerBroker.close();
494
559
  await modules.lifecycle.disposeStatefulRuntime(nextRegistry, currentWorkspaceManager);
495
560
  return;
496
561
  }
497
562
  registry = nextRegistry;
498
563
  persistence = sessionPersistence;
499
564
  completionBroker = sessionBroker;
565
+ peerBroker = sessionPeerBroker;
500
566
  for (const completion of nextRegistry.listPendingCompletions()) {
501
- sessionBroker.enqueue(completion);
567
+ if (completion.recipientId === "root") sessionBroker.enqueue(completion);
502
568
  }
503
569
  runtimeLimits = nextLimits;
504
570
  refreshSpawnToolRegistration?.();
@@ -544,6 +610,8 @@ export function registerStatefulSubagents(
544
610
  runtimeGeneration++;
545
611
  completionBroker?.close();
546
612
  completionBroker = undefined;
613
+ const previousPeerBroker = peerBroker;
614
+ peerBroker = undefined;
547
615
  if (sweepTimer) clearInterval(sweepTimer);
548
616
  sweepTimer = undefined;
549
617
  const previousRegistry = registry;
@@ -553,6 +621,7 @@ export function registerStatefulSubagents(
553
621
  seenMessageIds.clear();
554
622
  pendingIdempotentSpawns.clear();
555
623
  const shutdown = async () => {
624
+ await previousPeerBroker?.close();
556
625
  const currentWorkspaceManager = await getWorkspaceManager();
557
626
  const { disposeStatefulRuntime } = await import("./stateful-lifecycle.js");
558
627
  const errors = await disposeStatefulRuntime(previousRegistry, currentWorkspaceManager);
@@ -566,7 +635,7 @@ export function registerStatefulSubagents(
566
635
  });
567
636
 
568
637
  const baseSpawnDescription = () =>
569
- `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.`;
570
639
  const spawnTool = defineTool({
571
640
  name: "subagent_spawn",
572
641
  label: "Spawn Subagent",
@@ -575,6 +644,15 @@ export function registerStatefulSubagents(
575
644
  promptGuidelines: createSpawnPromptGuidelines(completionDelivery, blockingEnabled),
576
645
  parameters: Type.Object({
577
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
+ ),
578
656
  task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
579
657
  thinkingLevel: Type.Optional(StatefulThinkingLevelSchema),
580
658
  timeoutMs: Type.Optional(StatefulTimeoutSchema),
@@ -586,13 +664,18 @@ export function registerStatefulSubagents(
586
664
  contextEntryIds: Type.Optional(
587
665
  Type.Array(Type.String(), { description: "Optional selected session entry IDs." }),
588
666
  ),
589
- 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
+ ),
590
670
  allowConcurrentWrites: Type.Optional(
591
- 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
+ }),
592
675
  ),
593
676
  workspaceMode: Type.Optional(
594
677
  StringEnum(["shared", "worktree"] as const, {
595
- 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.",
596
679
  }),
597
680
  ),
598
681
  contract: Type.Optional(DelegationContractSchema),
@@ -684,6 +767,7 @@ export function registerStatefulSubagents(
684
767
  assertCurrentSpawn(signal, generation, runtimeGeneration);
685
768
  const requestHash = modules.spawnIdempotency.hashSpawnRequest({
686
769
  agent: params.agent,
770
+ taskName: params.taskName,
687
771
  task: params.task,
688
772
  cwd,
689
773
  agentScope: scope,
@@ -752,15 +836,6 @@ export function registerStatefulSubagents(
752
836
  throw new Error("Project-local subagent definitions cannot run in a detached worktree");
753
837
  }
754
838
  const requestedCwd = cwd;
755
- if ((params.workspaceMode ?? "shared") === "shared" && !params.allowConcurrentWrites) {
756
- assertNoSharedWriteConflict(
757
- ownedRegistry,
758
- params.agent,
759
- requestedCwd,
760
- scope,
761
- currentSettings,
762
- );
763
- }
764
839
  const workspaceOwner = `pending-${randomUUID()}`;
765
840
  const workspace =
766
841
  params.workspaceMode === "worktree"
@@ -781,6 +856,7 @@ export function registerStatefulSubagents(
781
856
  );
782
857
  agent = await ownedRegistry.spawn({
783
858
  agent: params.agent,
859
+ taskName: params.taskName,
784
860
  task: params.task,
785
861
  cwd: workspace?.path ?? requestedCwd,
786
862
  agentScope: scope,
@@ -819,11 +895,11 @@ export function registerStatefulSubagents(
819
895
  resolvePending?.(agent);
820
896
  const deliveryNote =
821
897
  completionDelivery === "auto-resume"
822
- ? "If no useful local work remains, briefly tell the user what was launched and end the response; auto-resume will request synthesis after completion."
823
- : "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.";
824
900
  return result(
825
901
  agent,
826
- `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.`,
827
903
  );
828
904
  } catch (error) {
829
905
  rejectPending?.(error);
@@ -853,7 +929,7 @@ export function registerStatefulSubagents(
853
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.",
854
930
  promptSnippet: "Start a new detached follow-up turn on a retained subagent",
855
931
  parameters: Type.Object({
856
- agentId: Type.String(),
932
+ agentId: Type.String({ description: "Retained agent ID or canonical task path." }),
857
933
  task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
858
934
  timeoutMs: Type.Optional(
859
935
  Type.Integer({
@@ -871,7 +947,10 @@ export function registerStatefulSubagents(
871
947
  }),
872
948
  ),
873
949
  allowConcurrentWrites: Type.Optional(
874
- 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
+ }),
875
954
  ),
876
955
  }),
877
956
  ...createStatefulToolRenderer("send"),
@@ -959,13 +1038,6 @@ export function registerStatefulSubagents(
959
1038
  currentSettings,
960
1039
  );
961
1040
  assertCurrentSpawn(signal, generation, runtimeGeneration);
962
- assertFollowUpWriteAllowed(
963
- ownedRegistry,
964
- existing,
965
- params.allowConcurrentWrites ?? false,
966
- isolatedAgents.has(existing.id),
967
- currentSettings,
968
- );
969
1041
  const currentGrant = modules.capabilityGrant.issueCapabilityGrant(
970
1042
  currentPlan,
971
1043
  Date.now(),
package/src/subagents.ts CHANGED
@@ -21,11 +21,8 @@ import type {
21
21
  DelegationCwdPolicy,
22
22
  SubagentSettings,
23
23
  } from "./agents/types.js";
24
- import {
25
- type AutomationRegistrationDependencies,
26
- registerSubagentAutomation,
27
- } from "./automation-registration.js";
28
24
  import { cachedModuleLoader, throwIfAborted } from "./cached-module-loader.js";
25
+ import { renderCompletionMessage, SUBAGENT_COMPLETION_MESSAGE_TYPE } from "./completion-render.js";
29
26
  import {
30
27
  type ConfigRegistrationDependencies,
31
28
  registerSubagentConfigCommand,
@@ -60,13 +57,13 @@ type BlockingExecutionModule = Pick<typeof import("./execution.js"), "executeSub
60
57
  export interface SubagentsDependencies {
61
58
  loadBlockingExecution?: () => Promise<BlockingExecutionModule>;
62
59
  loadStatefulTransport?: () => Promise<SubagentTransport>;
63
- automation?: AutomationRegistrationDependencies;
64
60
  config?: ConfigRegistrationDependencies;
65
61
  consult?: ConsultRegistrationDependencies;
66
62
  inspect?: InspectRegistrationDependencies;
67
63
  }
68
64
 
69
65
  export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies = {}) {
66
+ pi.registerMessageRenderer(SUBAGENT_COMPLETION_MESSAGE_TYPE, renderCompletionMessage);
70
67
  const loadBlockingExecution = cachedModuleLoader(
71
68
  dependencies.loadBlockingExecution ?? (() => import("./execution.js")),
72
69
  );
@@ -78,9 +75,6 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
78
75
  const refreshBlockingCatalog = blockingEnabled
79
76
  ? registerBlockingSubagent(pi, () => currentSettings, loadBlockingExecution)
80
77
  : () => undefined;
81
- if (blockingEnabled) {
82
- registerSubagentAutomation(pi, { getSettings: () => currentSettings }, dependencies.automation);
83
- }
84
78
  let refreshStatefulCatalog: (catalog: string) => void = () => undefined;
85
79
  let refreshConsultCatalog: (catalog: string) => void = () => undefined;
86
80
 
@@ -225,7 +219,11 @@ function registerBlockingSubagent(
225
219
  ].join(" ");
226
220
  const promptGuidelines = () => [
227
221
  "Use subagent only when delegation fits; the main agent should decide how many subagents to spawn from task shape instead of waiting for the user to specify a count.",
222
+ "The main agent retains overall planning, immediate critical-path work, integration, final verification, and the final answer.",
228
223
  "Use no subagent for simple answers, quick targeted edits, latency-sensitive one-step work, tasks requiring frequent user back-and-forth, or critical-path work the main agent can perform directly.",
224
+ "One ordinary implementation worker should not replace work the main agent can perform directly; use a blocking single only when intentional synchronous isolation or a user-requested specialist justifies waiting.",
225
+ "Keep ordinary planning in the main agent, or use explicit workflow mode when a genuine dependency graph requires caller-authored orchestration.",
226
+ "Keep ordinary review in the main agent with a review skill and deterministic checks; reserve panel mode or custom verifier agents for consequential independent verification.",
229
227
  "Use the blocking subagent tool only when delegated outputs are required before the main agent's next action and waiting is intentional; the main agent cannot process queued steering until the call returns.",
230
228
  "Use a blocking subagent single, parallel, chain, workflow, panel, or fan-in call only when synchronous context or output isolation is worth making the main agent unavailable while it runs.",
231
229
  `If a blocking parallel subagent call is genuinely required, keep tasks independent, stay within the configured max ${resolveBlockingMaxParallelTasks(getSettings())}, and avoid write-heavy implementation touching the same files or shared state.`,
@@ -1,5 +1,11 @@
1
1
  import { discoverAgents } from "./agents/discovery.js";
2
2
  import type { AgentConfig, SubagentSettings, SubagentThinkingLevel } from "./agents/types.js";
3
+ import {
4
+ CHILD_PEER_TOOL_NAMES,
5
+ childPeerBridgePath,
6
+ type PeerTransportRuntime,
7
+ peerBridgeEnvironment,
8
+ } from "./peer-transport.js";
3
9
  import { resolvePiPromptResources } from "./prompt-resources.js";
4
10
  import type { ManagedAgent, TurnOutcome } from "./registry.js";
5
11
  import { getResultFinalOutput, runSingleAgent, type SubagentDetails } from "./runner.js";
@@ -17,6 +23,7 @@ export function resolveStatefulSubprocessThinkingLevel(
17
23
 
18
24
  export interface SubprocessTransportOptions {
19
25
  getSettings?: () => SubagentSettings | undefined;
26
+ peerRuntime?: PeerTransportRuntime;
20
27
  }
21
28
 
22
29
  export class SubprocessTransport implements SubagentTransport {
@@ -54,35 +61,49 @@ export class SubprocessTransport implements SubagentTransport {
54
61
  projectAgentsDir: discovery.projectAgentsDir,
55
62
  results,
56
63
  });
57
- const single = await runSingleAgent(
58
- record.cwd,
59
- discovery.agents,
60
- record.agent,
61
- boundedTask.text,
62
- undefined,
63
- undefined,
64
- signal,
65
- resolveStatefulSubprocessThinkingLevel(discovery.agents, record),
66
- record.currentTimeoutMs ?? record.timeoutMs ?? resolveStatefulTurnTimeout(agent),
67
- undefined,
68
- makeDetails,
69
- undefined,
70
- {
71
- projectTrust,
72
- ...(record.executionPlan ? { tools: record.executionPlan.effectiveTools } : {}),
73
- appendSystemPromptPaths: promptResources?.appendSystemPromptPaths,
74
- timeoutResultFormat: record.resultFormat,
75
- turnLimits: {
76
- idleTimeoutMs: record.currentIdleTimeoutMs ?? record.idleTimeoutMs,
77
- maxTurns: record.currentMaxTurns ?? record.maxTurns,
78
- maxToolCalls: record.currentMaxToolCalls ?? record.maxToolCalls,
64
+ const credentials = this.options.peerRuntime
65
+ ? await this.options.peerRuntime.issueCredentials(
66
+ record.id,
67
+ record.currentTurnGeneration ?? record.turnGeneration ?? 1,
68
+ )
69
+ : undefined;
70
+ let single: Awaited<ReturnType<typeof runSingleAgent>>;
71
+ try {
72
+ single = await runSingleAgent(
73
+ record.cwd,
74
+ discovery.agents,
75
+ record.agent,
76
+ boundedTask.text,
77
+ undefined,
78
+ undefined,
79
+ signal,
80
+ resolveStatefulSubprocessThinkingLevel(discovery.agents, record),
81
+ record.currentTimeoutMs ?? record.timeoutMs ?? resolveStatefulTurnTimeout(agent),
82
+ undefined,
83
+ makeDetails,
84
+ undefined,
85
+ {
86
+ projectTrust,
87
+ ...(record.executionPlan ? { tools: record.executionPlan.effectiveTools } : {}),
88
+ appendSystemPromptPaths: promptResources?.appendSystemPromptPaths,
89
+ extensionPaths: credentials ? [childPeerBridgePath()] : undefined,
90
+ additionalTools: credentials ? [...CHILD_PEER_TOOL_NAMES] : undefined,
91
+ env: credentials ? peerBridgeEnvironment(credentials) : undefined,
92
+ timeoutResultFormat: record.resultFormat,
93
+ turnLimits: {
94
+ idleTimeoutMs: record.currentIdleTimeoutMs ?? record.idleTimeoutMs,
95
+ maxTurns: record.currentMaxTurns ?? record.maxTurns,
96
+ maxToolCalls: record.currentMaxToolCalls ?? record.maxToolCalls,
97
+ },
98
+ resultFormat: record.resultFormat,
99
+ contract: record.contract,
100
+ executionPlan: record.executionPlan,
101
+ displayTask: task,
79
102
  },
80
- resultFormat: record.resultFormat,
81
- contract: record.contract,
82
- executionPlan: record.executionPlan,
83
- displayTask: task,
84
- },
85
- );
103
+ );
104
+ } finally {
105
+ this.options.peerRuntime?.revoke(record.id);
106
+ }
86
107
  const settledAt = Date.now();
87
108
  const telemetry: TransportTelemetry = {
88
109
  ...starting,
@@ -0,0 +1,65 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const ROOT_TASK_PATH = "/root";
4
+ export const MAX_TASK_NAME_LENGTH = 128;
5
+ export const MAX_TASK_PATH_LENGTH = 2_048;
6
+
7
+ const TASK_NAME_PATTERN = /^[a-z0-9_]+$/u;
8
+
9
+ export function validateTaskName(value: string): string {
10
+ if (!value) throw new Error("Subagent taskName must not be empty");
11
+ if (value === "root" || value === "." || value === "..") {
12
+ throw new Error(`Subagent taskName ${JSON.stringify(value)} is reserved`);
13
+ }
14
+ if (value.length > MAX_TASK_NAME_LENGTH) {
15
+ throw new Error(`Subagent taskName cannot exceed ${MAX_TASK_NAME_LENGTH} characters`);
16
+ }
17
+ if (value.includes("/")) throw new Error("Subagent taskName must not contain `/`");
18
+ if (!TASK_NAME_PATTERN.test(value)) {
19
+ throw new Error(
20
+ "Subagent taskName must use only lowercase ASCII letters, digits, and underscores",
21
+ );
22
+ }
23
+ return value;
24
+ }
25
+
26
+ export function validateTaskPath(value: string): string {
27
+ if (!value.startsWith(`${ROOT_TASK_PATH}/`) && value !== ROOT_TASK_PATH) {
28
+ throw new Error("Canonical subagent task paths must start with `/root`");
29
+ }
30
+ if (value.length > MAX_TASK_PATH_LENGTH) {
31
+ throw new Error(
32
+ `Canonical subagent task paths cannot exceed ${MAX_TASK_PATH_LENGTH} characters`,
33
+ );
34
+ }
35
+ if (value.endsWith("/") || value.includes("//")) {
36
+ throw new Error("Canonical subagent task paths must not contain empty segments");
37
+ }
38
+ for (const segment of value.slice(ROOT_TASK_PATH.length + 1).split("/")) {
39
+ if (segment) validateTaskName(segment);
40
+ }
41
+ return value;
42
+ }
43
+
44
+ export function joinTaskPath(parentPath: string, taskName: string): string {
45
+ const parent = validateTaskPath(parentPath);
46
+ const name = validateTaskName(taskName);
47
+ return validateTaskPath(`${parent}/${name}`);
48
+ }
49
+
50
+ export function resolveTaskPath(senderPath: string, reference: string): string {
51
+ if (!reference) throw new Error("Subagent task path reference must not be empty");
52
+ if (reference.startsWith("/")) return validateTaskPath(reference);
53
+ const sender = validateTaskPath(senderPath);
54
+ if (reference.endsWith("/")) {
55
+ throw new Error("Relative subagent task paths must not end with `/`");
56
+ }
57
+ let path = sender;
58
+ for (const segment of reference.split("/")) path = joinTaskPath(path, segment);
59
+ return path;
60
+ }
61
+
62
+ export function deriveTaskName(agentId: string): string {
63
+ const digest = createHash("sha256").update(agentId).digest("hex").slice(0, 32);
64
+ return `agent_${digest}`;
65
+ }
@@ -148,12 +148,6 @@ export function responsivenessSetupScreen(runtime: TransportUiRuntime) {
148
148
  description: "Choose separately whether an idle root resumes for synthesis",
149
149
  to: "settings" as const,
150
150
  },
151
- {
152
- id: "thinking",
153
- label: "Thinking profiles",
154
- description: "Preview explicit Fast, Balanced, or Deep per-agent defaults",
155
- to: "execution-profiles" as const,
156
- },
157
151
  { id: "back", label: "Back", action: "back" as const },
158
152
  ],
159
153
  hint: "back" as const,
package/src/transport.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ManagedAgent, TurnOutcome } from "./registry.js";
1
+ import type { AgentMailboxMessage, ManagedAgent, TurnOutcome } from "./registry.js";
2
2
  import type { TransportProgressCallback } from "./transport-types.js";
3
3
 
4
4
  export interface SubagentTransport {
@@ -9,6 +9,7 @@ export interface SubagentTransport {
9
9
  signal: AbortSignal,
10
10
  onProgress?: TransportProgressCallback,
11
11
  ): Promise<TurnOutcome>;
12
+ deliverMessage?(agent: ManagedAgent, message: AgentMailboxMessage): Promise<boolean>;
12
13
  release?(agent: ManagedAgent): Promise<void>;
13
14
  shutdown?(): Promise<void>;
14
15
  }
@@ -46,15 +46,15 @@ function workflowEffects(current: DelegationWorkflow, next: DelegationWorkflow):
46
46
  if (blockingEnabled(current) !== blockingEnabled(next)) {
47
47
  effects.push(
48
48
  blockingEnabled(next)
49
- ? "Add blocking `subagent`, explicit `subagent_auto`, and read-only `subagent_consult`"
50
- : "Remove blocking `subagent`, explicit `subagent_auto`, and read-only `subagent_consult`",
49
+ ? "Add blocking `subagent` and read-only `subagent_consult`"
50
+ : "Remove blocking `subagent` and read-only `subagent_consult`",
51
51
  );
52
52
  }
53
53
  if (asyncEnabled(current) !== asyncEnabled(next)) {
54
54
  effects.push(
55
55
  asyncEnabled(next)
56
- ? "Add reusable async lifecycle tools"
57
- : "Remove reusable async lifecycle tools",
56
+ ? "Add async `subagent_spawn`, `subagent_send`, `subagent_manage`, and `subagent_mailbox`"
57
+ : "Remove async `subagent_spawn`, `subagent_send`, `subagent_manage`, and `subagent_mailbox`",
58
58
  );
59
59
  }
60
60
  return effects;