@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/registry.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  isCapabilityGrantActive,
11
11
  revokeCapabilityGrant,
12
12
  } from "./capability-grant.js";
13
+ import { resolveCompletionRecipient } from "./completion-routing.js";
13
14
  import type { TargetPolicyAudit } from "./cwd-policy.js";
14
15
  import type { DelegationContract } from "./delegation-contract.js";
15
16
  import {
@@ -40,8 +41,17 @@ import {
40
41
  parseAnyStructuredSubagentResult,
41
42
  type SubagentResultFormat,
42
43
  } from "./result-contract.js";
44
+ import { safeTerminalLine } from "./safe-text.js";
43
45
  import type { SemanticCompatibility, SemanticSnapshot } from "./semantic-snapshot.js";
44
46
  import { resolveStatefulLimits } from "./stateful-limits.js";
47
+ import {
48
+ deriveTaskName,
49
+ joinTaskPath,
50
+ ROOT_TASK_PATH,
51
+ resolveTaskPath,
52
+ validateTaskName,
53
+ validateTaskPath,
54
+ } from "./task-path.js";
45
55
  import { copyTurnTerminationReport } from "./timeout-checkpoint.js";
46
56
  import { type AgentTurnRunner, normalizeTransport, type SubagentTransport } from "./transport.js";
47
57
  import type { TransportTelemetry } from "./transport-types.js";
@@ -181,6 +191,7 @@ export class AgentRegistry {
181
191
  { maxAgents: this.maxAgents, maxDepth: this.maxDepth },
182
192
  ).map((record) => [record.id, record]),
183
193
  );
194
+ const restorable: Array<{ record: ManagedAgent; rootId: string; depth: number }> = [];
184
195
  for (const record of candidates.values()) {
185
196
  if (record.parentId && !candidates.has(record.parentId)) continue;
186
197
  if (record.parentId === record.id) continue;
@@ -198,12 +209,31 @@ export class AgentRegistry {
198
209
  parentId = candidates.get(parentId)?.parentId;
199
210
  }
200
211
  const depth = seen.size - 1;
201
- if (cyclic || depth > this.maxDepth) continue;
212
+ if (!cyclic && depth <= this.maxDepth) restorable.push({ record, rootId, depth });
213
+ }
214
+ restorable.sort(
215
+ (left, right) =>
216
+ left.depth - right.depth ||
217
+ left.record.createdAt - right.record.createdAt ||
218
+ left.record.id.localeCompare(right.record.id),
219
+ );
220
+ const reservedPaths = new Set(
221
+ [...this.agents.values()].flatMap((agent) =>
222
+ agent.state !== "closed" && agent.taskPath ? [agent.taskPath] : [],
223
+ ),
224
+ );
225
+ for (const { record, rootId, depth } of restorable) {
226
+ const parentPath = record.parentId
227
+ ? this.agents.get(record.parentId)?.taskPath
228
+ : ROOT_TASK_PATH;
229
+ if (!parentPath) continue;
230
+ const identity = this.reserveRestoredIdentity(record, parentPath, reservedPaths);
202
231
  for (const completion of record.pendingCompletions ?? []) {
203
232
  this.lastCompletionAt = Math.max(this.lastCompletionAt, completion.createdAt);
204
233
  }
205
234
  this.agents.set(record.id, {
206
235
  ...record,
236
+ ...identity,
207
237
  state:
208
238
  record.state === "running" || record.state === "starting" ? "interrupted" : record.state,
209
239
  rootId,
@@ -233,14 +263,26 @@ export class AgentRegistry {
233
263
  });
234
264
  }
235
265
  for (const agent of this.agents.values()) {
236
- if (!agent.parentId) continue;
237
- const parent = this.agents.get(agent.parentId);
238
- if (parent && !parent.children.includes(agent.id)) parent.children.push(agent.id);
266
+ if (agent.parentId) {
267
+ const parent = this.agents.get(agent.parentId);
268
+ if (parent && !parent.children.includes(agent.id)) parent.children.push(agent.id);
269
+ }
270
+ for (const completion of agent.pendingCompletions ?? []) {
271
+ if (!completion.recipientId || !completion.recipientPath) {
272
+ Object.assign(completion, this.completionRecipient(agent));
273
+ }
274
+ this.enqueueCompletionMessage(
275
+ agent,
276
+ completion,
277
+ completion.output || completion.error || "(no output)",
278
+ );
279
+ }
239
280
  }
240
281
  }
241
282
 
242
283
  async spawn(input: {
243
284
  agent: string;
285
+ taskName?: string;
244
286
  task: string;
245
287
  cwd: string;
246
288
  agentScope?: "user" | "project" | "both";
@@ -296,8 +338,15 @@ export class AgentRegistry {
296
338
  if (depth > this.maxDepth) throw new Error(`Subagent depth limit reached (${this.maxDepth})`);
297
339
  const now = this.now();
298
340
  const id = `sa_${randomUUID()}`;
341
+ const taskName = input.taskName ? validateTaskName(input.taskName) : deriveTaskName(id);
342
+ const taskPath = joinTaskPath(parent?.taskPath ?? ROOT_TASK_PATH, taskName);
343
+ if (this.findByTaskPath(taskPath)) {
344
+ throw new Error(`Canonical subagent task path is already retained: ${taskPath}`);
345
+ }
299
346
  const record: ManagedAgent = {
300
347
  id,
348
+ taskName,
349
+ taskPath,
301
350
  agent: input.agent,
302
351
  parentId: parent?.id,
303
352
  rootId: parent?.rootId ?? id,
@@ -389,9 +438,7 @@ export class AgentRegistry {
389
438
  ) {
390
439
  throw new Error(`Agent ${id} cannot accept follow-up while ${agent.state}`);
391
440
  }
392
- const unread = agent.mailbox.filter((message) => !message.readAt);
393
- const readAt = this.now();
394
- for (const message of unread) message.readAt = readAt;
441
+ const unread = agent.mailbox.filter((message) => !message.readAt).slice(-20);
395
442
  agent.currentMailboxMessageIds = unread.map((message) => message.id);
396
443
  this.startTurn(agent, boundedTask, options);
397
444
  return this.copy(agent);
@@ -427,18 +474,13 @@ export class AgentRegistry {
427
474
  if (deduplicationKey && deduplicationKey.length > 256) {
428
475
  throw new Error("Subagent mailbox deduplication keys cannot exceed 256 characters");
429
476
  }
430
- const recipient = this.require(recipientId);
477
+ const sender = senderId === "root" ? undefined : this.require(senderId);
478
+ if (sender?.state === "closed")
479
+ throw new Error(`Closed agent ${sender.id} cannot send messages`);
480
+ const recipient = this.require(recipientId, sender?.id);
431
481
  if (recipient.state === "closed")
432
482
  throw new Error(`Cannot message closed agent ${recipient.id}`);
433
- if (senderId !== "root") {
434
- const sender = this.require(senderId);
435
- if (sender.state === "closed")
436
- throw new Error(`Closed agent ${sender.id} cannot send messages`);
437
- if (sender.rootId !== recipient.rootId) {
438
- throw new Error("Subagent mailbox messages cannot cross agent trees");
439
- }
440
- }
441
- const message = this.enqueueMessage(recipient, content, senderId, deduplicationKey);
483
+ const message = this.enqueueMessage(recipient, content, sender?.id ?? "root", deduplicationKey);
442
484
  await this.changed();
443
485
  return { ...message };
444
486
  }
@@ -461,6 +503,58 @@ export class AgentRegistry {
461
503
  return unread.map((message) => ({ ...message }));
462
504
  }
463
505
 
506
+ async acknowledgeVisibleMessages(
507
+ id: string,
508
+ messageIds: readonly string[],
509
+ completionIds: readonly string[] = [],
510
+ visibleAt = this.now(),
511
+ ): Promise<void> {
512
+ const agent = this.require(id);
513
+ const visibleMessageIds = new Set(messageIds);
514
+ const visibleCompletionIds = new Set(completionIds);
515
+ const changedMessages: AgentMailboxMessage[] = [];
516
+ const removedCompletions: Array<{
517
+ owner: ManagedAgent;
518
+ completion: NonNullable<ManagedAgent["pendingCompletions"]>[number];
519
+ }> = [];
520
+ for (const message of agent.mailbox) {
521
+ if (message.readAt === undefined && visibleMessageIds.has(message.id)) {
522
+ message.readAt = visibleAt;
523
+ changedMessages.push(message);
524
+ }
525
+ if (message.completionId && visibleMessageIds.has(message.id)) {
526
+ visibleCompletionIds.add(message.completionId);
527
+ }
528
+ }
529
+ for (const owner of this.agents.values()) {
530
+ for (const completion of owner.pendingCompletions ?? []) {
531
+ if (!visibleCompletionIds.has(completion.completionId)) continue;
532
+ if (completion.recipientId !== agent.id && completion.recipientPath !== agent.taskPath) {
533
+ continue;
534
+ }
535
+ removedCompletions.push({ owner, completion });
536
+ }
537
+ }
538
+ if (changedMessages.length === 0 && removedCompletions.length === 0) return;
539
+ for (const { owner, completion } of removedCompletions) {
540
+ owner.pendingCompletions = (owner.pendingCompletions ?? []).filter(
541
+ (candidate) => candidate.completionId !== completion.completionId,
542
+ );
543
+ }
544
+ agent.updatedAt = Math.max(agent.updatedAt, visibleAt);
545
+ try {
546
+ await this.changed(true);
547
+ } catch (error) {
548
+ for (const message of changedMessages) message.readAt = undefined;
549
+ for (const { owner, completion } of removedCompletions) {
550
+ owner.pendingCompletions = [...(owner.pendingCompletions ?? []), completion].sort(
551
+ (left, right) => left.createdAt - right.createdAt || left.generation - right.generation,
552
+ );
553
+ }
554
+ throw error;
555
+ }
556
+ }
557
+
464
558
  async wait(
465
559
  id: string,
466
560
  timeoutMs = 30_000,
@@ -471,7 +565,8 @@ export class AgentRegistry {
471
565
  }
472
566
  if (signal?.aborted) throw waitAbortError();
473
567
  const agent = this.require(id);
474
- const running = this.running.get(id);
568
+ const agentId = agent.id;
569
+ const running = this.running.get(agentId);
475
570
  if (!running) return { timedOut: false, agent: this.copy(agent) };
476
571
  let timer: NodeJS.Timeout | undefined;
477
572
  let onAbort: (() => void) | undefined;
@@ -487,7 +582,7 @@ export class AgentRegistry {
487
582
  if (onAbort) signal?.removeEventListener("abort", onAbort);
488
583
  if (result === "aborted") throw waitAbortError();
489
584
  return result === "timeout"
490
- ? { timedOut: true, agent: this.copy(this.require(id)) }
585
+ ? { timedOut: true, agent: this.copy(this.require(agentId)) }
491
586
  : { timedOut: false, agent: this.copy(result) };
492
587
  }
493
588
 
@@ -504,6 +599,7 @@ export class AgentRegistry {
504
599
 
505
600
  async interrupt(id: string): Promise<ManagedAgent> {
506
601
  const agent = this.require(id);
602
+ const agentId = agent.id;
507
603
  if (agent.state !== "running" && agent.state !== "starting")
508
604
  throw new Error(`Agent ${id} is not running`);
509
605
  if (agent.capabilityGrant?.state === "active") {
@@ -517,11 +613,13 @@ export class AgentRegistry {
517
613
  agent.executionPlan = rotateExecutionPlanGeneration(agent.executionPlan);
518
614
  }
519
615
  if (agent.state === "starting") {
520
- const index = this.queue.findIndex((entry) => entry.agent.id === id);
616
+ const index = this.queue.findIndex((entry) => entry.agent.id === agentId);
521
617
  if (index >= 0) {
522
618
  const [entry] = this.queue.splice(index, 1);
619
+ const recipient = this.completionRecipient(agent);
523
620
  const persistedCompletion = {
524
621
  completionId: `completion:${agent.id}:${randomUUID()}`,
622
+ ...recipient,
525
623
  runId: agent.currentRunId ?? `run:${agent.id}:${randomUUID()}`,
526
624
  generation: agent.currentTurnGeneration ?? agent.turnGeneration ?? 1,
527
625
  task: truncateUtf8(entry.task, 256).text,
@@ -531,6 +629,7 @@ export class AgentRegistry {
531
629
  };
532
630
  agent.state = "interrupted";
533
631
  agent.pendingCompletions = [...(agent.pendingCompletions ?? []), persistedCompletion];
632
+ this.enqueueCompletionMessage(agent, persistedCompletion, persistedCompletion.error);
534
633
  clearCurrentTurn(agent);
535
634
  agent.updatedAt = this.now();
536
635
  const persisted = await this.persistTerminalState().then(
@@ -542,14 +641,14 @@ export class AgentRegistry {
542
641
  agent: this.copy(agent),
543
642
  };
544
643
  entry.resolve(agent);
545
- this.running.delete(id);
644
+ this.running.delete(agentId);
546
645
  if (persisted) await this.notifyTurnComplete(completion);
547
646
  return this.copy(agent);
548
647
  }
549
648
  }
550
- this.controllers.get(id)?.abort();
551
- await this.running.get(id);
552
- return this.copy(this.require(id));
649
+ this.controllers.get(agentId)?.abort();
650
+ await this.running.get(agentId);
651
+ return this.copy(this.require(agentId));
553
652
  }
554
653
 
555
654
  async closeTree(id: string): Promise<ManagedAgent[]> {
@@ -574,6 +673,7 @@ export class AgentRegistry {
574
673
 
575
674
  async close(id: string): Promise<ManagedAgent> {
576
675
  const agent = this.require(id);
676
+ const agentId = agent.id;
577
677
  if (agent.state === "closed") throw new Error(`Agent ${id} is already closed`);
578
678
  if (agent.children.some((childId) => this.agents.get(childId)?.state !== "closed")) {
579
679
  throw new Error(`Agent ${id} has active descendants; close the subtree instead`);
@@ -587,20 +687,20 @@ export class AgentRegistry {
587
687
  }
588
688
  }
589
689
  if (agent.state === "starting") {
590
- const index = this.queue.findIndex((entry) => entry.agent.id === id);
690
+ const index = this.queue.findIndex((entry) => entry.agent.id === agentId);
591
691
  if (index >= 0) {
592
692
  const [entry] = this.queue.splice(index, 1);
593
693
  entry.resolve(agent);
594
- this.running.delete(id);
694
+ this.running.delete(agentId);
595
695
  }
596
696
  }
597
- this.controllers.get(id)?.abort();
598
- await this.running.get(id)?.catch(() => undefined);
697
+ this.controllers.get(agentId)?.abort();
698
+ await this.running.get(agentId)?.catch(() => undefined);
599
699
  agent.state = "closed";
600
700
  agent.updatedAt = this.now();
601
701
  if (agent.parentId) {
602
702
  const parent = this.agents.get(agent.parentId);
603
- if (parent) parent.children = parent.children.filter((childId) => childId !== id);
703
+ if (parent) parent.children = parent.children.filter((childId) => childId !== agentId);
604
704
  }
605
705
  clearCurrentTurn(agent);
606
706
  let releaseError: unknown;
@@ -697,7 +797,7 @@ export class AgentRegistry {
697
797
  }
698
798
 
699
799
  getInspection(id: string): AgentRunInspectionDetail | undefined {
700
- const agent = this.agents.get(id);
800
+ const agent = this.findReference(id);
701
801
  if (!agent) return undefined;
702
802
  return {
703
803
  ...this.inspectSummary(agent),
@@ -755,8 +855,12 @@ export class AgentRegistry {
755
855
  .map((agent) => this.copy(agent));
756
856
  }
757
857
 
758
- get(id: string): ManagedAgent | undefined {
759
- const agent = this.agents.get(id);
858
+ get(reference: string): ManagedAgent | undefined {
859
+ return this.resolveAgent(reference);
860
+ }
861
+
862
+ resolveAgent(reference: string, senderId = "root"): ManagedAgent | undefined {
863
+ const agent = this.findReference(reference, senderId);
760
864
  return agent ? this.copy(agent) : undefined;
761
865
  }
762
866
 
@@ -892,8 +996,10 @@ export class AgentRegistry {
892
996
  agent.state = "failed";
893
997
  agent.error = "Capability grant expired or no longer matches the accepted plan";
894
998
  agent.outcome = classifyStructuredOutcome("failed", "capability-grant-invalid");
999
+ const recipient = this.completionRecipient(agent);
895
1000
  const persistedCompletion = {
896
1001
  completionId: `completion:${agent.id}:${randomUUID()}`,
1002
+ ...recipient,
897
1003
  runId: agent.currentRunId ?? `run:${agent.id}:${randomUUID()}`,
898
1004
  generation: agent.currentTurnGeneration ?? agent.turnGeneration ?? 1,
899
1005
  task: truncateUtf8(task, 256).text,
@@ -902,6 +1008,7 @@ export class AgentRegistry {
902
1008
  createdAt: this.completionCreatedAt(),
903
1009
  };
904
1010
  agent.pendingCompletions = [...(agent.pendingCompletions ?? []), persistedCompletion];
1011
+ this.enqueueCompletionMessage(agent, persistedCompletion, persistedCompletion.error ?? "");
905
1012
  clearCurrentTurn(agent);
906
1013
  agent.updatedAt = this.now();
907
1014
  void this.persistTerminalState()
@@ -930,6 +1037,7 @@ export class AgentRegistry {
930
1037
  let completionContent = "";
931
1038
  let completionOutput = "";
932
1039
  let completionError: string | undefined;
1040
+ const visibleMailboxMessageIds = [...(agent.currentMailboxMessageIds ?? [])];
933
1041
  void this.transport
934
1042
  .runTurn(this.copy(agent), task, controller.signal, (progress) => {
935
1043
  agent.telemetry = {
@@ -942,6 +1050,14 @@ export class AgentRegistry {
942
1050
  };
943
1051
  })
944
1052
  .then(async (outcome) => {
1053
+ if (visibleMailboxMessageIds.length > 0) {
1054
+ await this.acknowledgeVisibleMessages(
1055
+ agent.id,
1056
+ visibleMailboxMessageIds,
1057
+ [],
1058
+ this.now(),
1059
+ ).catch(() => undefined);
1060
+ }
945
1061
  const output = truncateUtf8(outcome.output, this.maxTurnOutputBytes).text;
946
1062
  const error = outcome.error
947
1063
  ? truncateUtf8(outcome.error, this.maxTurnOutputBytes).text
@@ -1068,8 +1184,10 @@ export class AgentRegistry {
1068
1184
  return agent;
1069
1185
  })
1070
1186
  .finally(async () => {
1187
+ const recipient = this.completionRecipient(agent);
1071
1188
  const persistedCompletion = {
1072
1189
  completionId: completionKey,
1190
+ ...recipient,
1073
1191
  runId,
1074
1192
  generation: turnGeneration,
1075
1193
  task: truncateUtf8(task, 256).text,
@@ -1078,12 +1196,7 @@ export class AgentRegistry {
1078
1196
  createdAt: this.completionCreatedAt(),
1079
1197
  };
1080
1198
  agent.pendingCompletions = [...(agent.pendingCompletions ?? []), persistedCompletion];
1081
- if (agent.parentId) {
1082
- const parent = this.agents.get(agent.parentId);
1083
- if (parent && parent.state !== "closed") {
1084
- this.enqueueMessage(parent, completionContent, agent.id, completionKey);
1085
- }
1086
- }
1199
+ this.enqueueCompletionMessage(agent, persistedCompletion, completionContent);
1087
1200
  clearCurrentTurn(agent);
1088
1201
  agent.updatedAt = this.now();
1089
1202
  const persisted = await this.persistTerminalState().then(
@@ -1113,11 +1226,38 @@ export class AgentRegistry {
1113
1226
  }
1114
1227
  }
1115
1228
 
1229
+ private completionRecipient(
1230
+ agent: ManagedAgent,
1231
+ ): Pick<
1232
+ NonNullable<ManagedAgent["pendingCompletions"]>[number],
1233
+ "recipientId" | "recipientPath"
1234
+ > {
1235
+ return resolveCompletionRecipient(agent, (id) => this.agents.get(id));
1236
+ }
1237
+
1238
+ private enqueueCompletionMessage(
1239
+ agent: ManagedAgent,
1240
+ completion: NonNullable<ManagedAgent["pendingCompletions"]>[number],
1241
+ content: string,
1242
+ ): void {
1243
+ if (!completion.recipientId || completion.recipientId === "root") return;
1244
+ const recipient = this.agents.get(completion.recipientId);
1245
+ if (!recipient || recipient.state === "closed") return;
1246
+ this.enqueueMessage(
1247
+ recipient,
1248
+ content || "(no output)",
1249
+ agent.id,
1250
+ completion.completionId,
1251
+ completion.completionId,
1252
+ );
1253
+ }
1254
+
1116
1255
  private enqueueMessage(
1117
1256
  recipient: ManagedAgent,
1118
1257
  content: string,
1119
1258
  senderId: string,
1120
1259
  deduplicationKey?: string,
1260
+ completionId?: string,
1121
1261
  ): AgentMailboxMessage {
1122
1262
  if (deduplicationKey) {
1123
1263
  const existing = recipient.mailbox.find(
@@ -1133,6 +1273,7 @@ export class AgentRegistry {
1133
1273
  content: bounded.text,
1134
1274
  createdAt: this.now(),
1135
1275
  deduplicationKey,
1276
+ completionId,
1136
1277
  };
1137
1278
  recipient.mailbox.push(message);
1138
1279
  recipient.mailbox = recipient.mailbox.slice(-this.maxMailboxMessages);
@@ -1154,12 +1295,68 @@ export class AgentRegistry {
1154
1295
  return result;
1155
1296
  }
1156
1297
 
1157
- private require(id: string): ManagedAgent {
1158
- const agent = this.agents.get(id);
1159
- if (!agent) throw new Error(`Unknown subagent: ${id}`);
1298
+ private require(reference: string, senderId = "root"): ManagedAgent {
1299
+ const agent = this.findReference(reference, senderId);
1300
+ if (!agent) throw new Error(`Unknown subagent: ${safeTerminalLine(reference, 256)}`);
1160
1301
  return agent;
1161
1302
  }
1162
1303
 
1304
+ private findReference(reference: string, senderId = "root"): ManagedAgent | undefined {
1305
+ const direct = this.agents.get(reference);
1306
+ if (direct) return direct;
1307
+ const sender =
1308
+ senderId === "root"
1309
+ ? undefined
1310
+ : (this.agents.get(senderId) ?? this.findByTaskPath(senderId));
1311
+ if (senderId !== "root" && !sender) return undefined;
1312
+ let taskPath: string;
1313
+ try {
1314
+ taskPath = resolveTaskPath(sender?.taskPath ?? ROOT_TASK_PATH, reference);
1315
+ } catch {
1316
+ return undefined;
1317
+ }
1318
+ return this.findByTaskPath(taskPath);
1319
+ }
1320
+
1321
+ private findByTaskPath(taskPath: string): ManagedAgent | undefined {
1322
+ return [...this.agents.values()].find(
1323
+ (agent) => agent.state !== "closed" && agent.taskPath === taskPath,
1324
+ );
1325
+ }
1326
+
1327
+ private reserveRestoredIdentity(
1328
+ record: ManagedAgent,
1329
+ parentPath: string,
1330
+ reservedPaths: Set<string>,
1331
+ ): Pick<ManagedAgent, "taskName" | "taskPath"> {
1332
+ let taskName: string | undefined;
1333
+ try {
1334
+ taskName = record.taskName ? validateTaskName(record.taskName) : undefined;
1335
+ } catch {
1336
+ taskName = undefined;
1337
+ }
1338
+ if (!taskName && record.taskPath) {
1339
+ try {
1340
+ const storedPath = validateTaskPath(record.taskPath);
1341
+ const storedName = storedPath.slice(storedPath.lastIndexOf("/") + 1);
1342
+ taskName = validateTaskName(storedName);
1343
+ } catch {
1344
+ taskName = undefined;
1345
+ }
1346
+ }
1347
+ taskName ??= deriveTaskName(record.id);
1348
+ let taskPath = joinTaskPath(parentPath, taskName);
1349
+ if (reservedPaths.has(taskPath)) {
1350
+ taskName = deriveTaskName(record.id);
1351
+ taskPath = joinTaskPath(parentPath, taskName);
1352
+ }
1353
+ if (reservedPaths.has(taskPath)) {
1354
+ throw new Error(`Cannot restore duplicate canonical subagent task path: ${taskPath}`);
1355
+ }
1356
+ reservedPaths.add(taskPath);
1357
+ return { taskName, taskPath };
1358
+ }
1359
+
1163
1360
  private retainedCount(): number {
1164
1361
  return [...this.agents.values()].filter((agent) => agent.state !== "closed").length;
1165
1362
  }
@@ -1272,6 +1469,8 @@ export class AgentRegistry {
1272
1469
  }
1273
1470
  return {
1274
1471
  id: agent.id,
1472
+ taskName: agent.taskName,
1473
+ taskPath: agent.taskPath,
1275
1474
  agent: agent.agent,
1276
1475
  state: agent.state,
1277
1476
  createdAt: agent.createdAt,
@@ -110,15 +110,30 @@ export function structuredResultInstruction(format: SubagentResultFormat | undef
110
110
  ].join(" ");
111
111
  }
112
112
  if (format === "structured-v2") {
113
+ const minimumResult = JSON.stringify({
114
+ version: "pi-subagents:result:v2",
115
+ status: "completed",
116
+ summary: "Concise outcome",
117
+ claims: [],
118
+ artifacts: [],
119
+ changes: [],
120
+ verification: [],
121
+ limitations: [],
122
+ unresolvedDependencies: [],
123
+ });
113
124
  return [
114
125
  "Return the final answer as one JSON object and no surrounding prose.",
115
- 'Use exactly version "pi-subagents:result:v2".',
116
- "Required fields are status, summary, claims, artifacts, changes, verification, limitations, and unresolvedDependencies.",
126
+ "Minimum valid result:",
127
+ minimumResult,
128
+ "Keep every top-level field shown, including empty arrays.",
117
129
  `status must be one of ${SUBAGENT_OUTCOME_STATUSES.join(", ")}.`,
118
- "Each claim must include claim, classification (observed, inferred, or unverified), and an evidence string array.",
119
- "Each artifact must include id and kind; each change must include path and summary; each verification item must include status (passed, failed, or not-run) and summary.",
130
+ 'Claim item shape: {"claim":"Observed fact","classification":"observed","evidence":["path:line"]}.',
131
+ 'Artifact item shape: {"id":"artifact-id","kind":"file","version":"optional","location":"optional","digest":"optional"}.',
132
+ 'Change item shape: {"path":"path/to/file","summary":"What changed"}.',
133
+ 'Verification item shape: {"status":"passed","summary":"What was checked","command":"optional","evidence":["optional"]}.',
134
+ "Claim classification must be observed, inferred, or unverified; verification status must be passed, failed, or not-run.",
120
135
  "Use optional reasonCode for non-completed outcomes and optional provenance for taskId, inputArtifacts, and repositoryGeneration; executor-owned generation and plan identity are stamped after parsing.",
121
- ].join(" ");
136
+ ].join("\n");
122
137
  }
123
138
  return "";
124
139
  }