@ian-pascoe/pi-minimal-subagents 0.11.0 → 0.13.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.
@@ -2,10 +2,14 @@ import { randomUUID } from "node:crypto";
2
2
  import {
3
3
  assembleImportedContext,
4
4
  boundRecentAgentText,
5
+ buildActiveTurnProgress,
6
+ buildInheritedContextTaskPrompt,
5
7
  buildRecentAgentActivity,
6
8
  contextContainsImages,
9
+ selectActiveTurnTranscript,
7
10
  selectChildAgentTranscript,
8
11
  } from "./minimal-subagents-context.js";
12
+
9
13
  import {
10
14
  canAgentContractSpawn,
11
15
  DEFAULT_MAX_SUBAGENT_DEPTH,
@@ -39,6 +43,7 @@ import { addCoordinatorMessageEnvelope } from "./minimal-subagents-message-envel
39
43
  import { unavailableAgent } from "./minimal-subagents-sessions.js";
40
44
  import { createRegistryEvent } from "./minimal-subagents-registry.js";
41
45
  import type {
46
+ ActiveTurnProgress,
42
47
  AgentDetail,
43
48
  AgentMessageDisposition,
44
49
  AgentMessageResult,
@@ -70,6 +75,16 @@ const RESERVED_AGENT_IDS = new Set(["root", "parent"]);
70
75
  const RECENT_MESSAGE_LIMIT = 20;
71
76
  const DEFAULT_AUTOMATIC_DELIVERY_GRACE_MS = 1_000;
72
77
 
78
+ /** Assistant messages a waiting parent's live view renders from the child's running turn. */
79
+ const ACTIVE_TURN_TRANSCRIPT_ASSISTANT_MESSAGES = 3;
80
+
81
+ /** A direct child's running turn as its waiting parent may observe it. */
82
+ interface ObservedActiveTurn {
83
+ turnId: string;
84
+ startedAtMs: number;
85
+ runtime: ChildAgentRuntime;
86
+ }
87
+
73
88
  interface MessageParameters {
74
89
  agent_id?: string;
75
90
  message: string;
@@ -197,13 +212,15 @@ export class MinimalSubagentsCoordinator {
197
212
  this.validateFriendlyId(friendlyId);
198
213
  const agentId = this.buildChildAgentId(callerId, friendlyId);
199
214
  if (this.tombstones.has(agentId)) {
200
- throw new Error(`Minimal subagents agent ID is tombstoned: ${agentId}`);
215
+ throw new Error(
216
+ `Minimal subagents agent ID ${agentId} belonged to a deleted agent and cannot be reused; choose a different agent_id`,
217
+ );
201
218
  }
202
219
  if (this.agents.has(agentId) || this.pendingAgentIds.has(agentId)) {
203
220
  throw new Error(`Minimal subagents duplicate agent ID: ${agentId}`);
204
221
  }
205
222
 
206
- const sessionContext = parameters.session_context ?? "inherit";
223
+ const sessionContext = parameters.session_context ?? "omit";
207
224
  const projectContext = parameters.project_context ?? "inherit";
208
225
  const model = parameters.model ?? caller.model;
209
226
  const requestedThinking = parameters.thinking_level ?? caller.thinkingLevel;
@@ -218,7 +235,7 @@ export class MinimalSubagentsCoordinator {
218
235
  toolsets: this.dependencies.toolsets,
219
236
  });
220
237
  const committedMessages = structuredClone(caller.messages);
221
- const imported = assembleImportedContext(sessionContext, committedMessages);
238
+ const imported = assembleImportedContext(sessionContext, committedMessages, callerId);
222
239
  if (
223
240
  contextContainsImages(imported.messages) &&
224
241
  !this.dependencies.sessions.modelSupportsImages(model)
@@ -296,11 +313,12 @@ export class MinimalSubagentsCoordinator {
296
313
  createRegistryEvent(this.dependencies.registry.rootSessionId, "agent-created", { agent }),
297
314
  );
298
315
  const turnId = this.beginTurn(agent);
299
- if (toolWarnings.length > 0) {
316
+ const warnings = [...new Set(toolWarnings)];
317
+ if (warnings.length > 0) {
300
318
  this.dependencies.notify?.({
301
319
  type: "tool-warning",
302
320
  agentId,
303
- message: `Minimal subagents tool warnings for ${agentId}:\n- ${[...new Set(toolWarnings)].join("\n- ")}`,
321
+ message: `Minimal subagents tool warnings for ${agentId}:\n- ${warnings.join("\n- ")}`,
304
322
  });
305
323
  }
306
324
  this.dependencies.notify?.({
@@ -313,13 +331,25 @@ export class MinimalSubagentsCoordinator {
313
331
  this.initializeAndRunPrompt(
314
332
  agentId,
315
333
  turnId,
316
- parameters.task,
334
+ imported.messages.length > 0
335
+ ? buildInheritedContextTaskPrompt(parameters.task, agentId, callerId)
336
+ : parameters.task,
317
337
  imported.compact,
318
338
  caller.model,
319
339
  caller.thinkingLevel,
320
340
  ),
321
341
  );
322
- return { agent_id: agentId, turn_id: turnId, status: "running" };
342
+ const result: SpawnResult = {
343
+ agent_id: agentId,
344
+ turn_id: turnId,
345
+ status: "running",
346
+ model: agent.launch_contract.model,
347
+ thinking_level: agent.launch_contract.thinking_level,
348
+ tools: [...agent.launch_contract.ordinary_tools],
349
+ delegation: agent.launch_contract.delegation ?? "none",
350
+ };
351
+ if (warnings.length > 0) result.warnings = warnings;
352
+ return result;
323
353
  }
324
354
 
325
355
  /** Capture immutable launch defaults for a nested caller from its active child runtime. */
@@ -365,10 +395,12 @@ export class MinimalSubagentsCoordinator {
365
395
  this.queuePendingParentMessage(targetId, message, delivery);
366
396
  return { agent_id: targetId, message_id: messageId, disposition: "queued" };
367
397
  }
368
- const disposition = await this.enqueueRecipientDelivery(targetId, async () =>
398
+ const { disposition, turnId } = await this.enqueueRecipientDelivery(targetId, async () =>
369
399
  this.deliverExplicitMessage(message, targetId, delivery),
370
400
  );
371
- return { agent_id: targetId, message_id: messageId, disposition };
401
+ const result: AgentMessageResult = { agent_id: targetId, message_id: messageId, disposition };
402
+ if (turnId) result.turn_id = turnId;
403
+ return result;
372
404
  } catch (error) {
373
405
  const deliveryError = error instanceof Error ? error.message : String(error);
374
406
  if (this.isCoordinationDeliveryCurrent(delivery)) {
@@ -423,7 +455,9 @@ export class MinimalSubagentsCoordinator {
423
455
  if (pendingMessage) return Promise.resolve(pendingMessage);
424
456
  if (agent.active_turn_id !== turnId) {
425
457
  return Promise.reject(
426
- new Error(`Minimal subagents wait: turn ${turnId} is no longer retained for ${agentId}`),
458
+ new Error(
459
+ `Minimal subagents wait: turn ${turnId} is unknown or no longer retained for ${agentId}; omit turn_id to wait for its oldest observable turn`,
460
+ ),
427
461
  );
428
462
  }
429
463
 
@@ -460,11 +494,11 @@ export class MinimalSubagentsCoordinator {
460
494
  });
461
495
  }
462
496
 
463
- /** Return direct-child status authorized for one root or child caller. */
497
+ /** Return direct-child status for a child caller; the root may inspect any descendant. */
464
498
  status(callerId: string, agentId?: string): StatusResult {
465
499
  this.assertCallerExists(callerId);
466
500
  if (agentId !== undefined) {
467
- this.assertCallerTargetsDirectChild(callerId, agentId, "status");
501
+ if (callerId !== "root") this.assertCallerTargetsDirectChild(callerId, agentId, "status");
468
502
  return { agent: this.buildAgentDetail(this.requireAgent(agentId), false) };
469
503
  }
470
504
  return {
@@ -482,6 +516,56 @@ export class MinimalSubagentsCoordinator {
482
516
  };
483
517
  }
484
518
 
519
+ /**
520
+ * Find a direct child's running turn for its parent's live wait view. Only the direct parent
521
+ * observes it, and a requested `turnId` must be the active turn.
522
+ */
523
+ private observeActiveTurn(
524
+ callerId: string,
525
+ agentId: string,
526
+ turnId: string | undefined,
527
+ ): ObservedActiveTurn | undefined {
528
+ const agent = this.agents.get(agentId);
529
+ if (!agent || agent.parent_id !== callerId) return undefined;
530
+ const activeTurnId = agent.active_turn_id;
531
+ const startedAt = agent.active_turn_started_at;
532
+ if (!activeTurnId || !startedAt || (turnId !== undefined && turnId !== activeTurnId)) {
533
+ return undefined;
534
+ }
535
+ const runtime = this.runtimes.get(agentId);
536
+ if (!runtime) return undefined;
537
+ return { turnId: activeTurnId, startedAtMs: Date.parse(startedAt), runtime };
538
+ }
539
+
540
+ /** Count a direct child's running-turn tool calls for its waiting parent. */
541
+ previewActiveTurn(
542
+ callerId: string,
543
+ agentId: string,
544
+ turnId?: string,
545
+ ): ActiveTurnProgress | undefined {
546
+ const turn = this.observeActiveTurn(callerId, agentId, turnId);
547
+ if (!turn) return undefined;
548
+ return {
549
+ turn_id: turn.turnId,
550
+ ...buildActiveTurnProgress(turn.runtime.snapshotActivityMessages(), turn.startedAtMs),
551
+ };
552
+ }
553
+
554
+ /** Return a direct child's Active Turn Tail, with its tool definitions, for its waiting parent. */
555
+ inspectActiveTurnTranscript(
556
+ callerId: string,
557
+ agentId: string,
558
+ turnId?: string,
559
+ ): ChildAgentTranscriptSnapshot | undefined {
560
+ const turn = this.observeActiveTurn(callerId, agentId, turnId);
561
+ if (!turn) return undefined;
562
+ return selectActiveTurnTranscript(
563
+ this.inspectTranscript(agentId),
564
+ turn.startedAtMs,
565
+ ACTIVE_TURN_TRANSCRIPT_ASSISTANT_MESSAGES,
566
+ );
567
+ }
568
+
485
569
  /** Lazily inspect one Child Session Transcript without restoring a missing runtime. */
486
570
  inspectTranscript(agentId: string): ChildAgentTranscriptSnapshot {
487
571
  const agent = this.requireAgent(agentId);
@@ -528,15 +612,18 @@ export class MinimalSubagentsCoordinator {
528
612
  this.assertCallerCanManageAgent(callerId, agentId, "cancel");
529
613
  const target = this.requireUsableAgent(agentId, "cancel");
530
614
  const affected = recursive ? [target, ...this.descendantsOf(agentId)] : [target];
615
+ const affectedAgentIds: string[] = [];
531
616
  const cancelledTurnIds: string[] = [];
532
617
  for (const agent of affected) {
533
618
  const cancelledTurnId = await this.cancelActiveTurn(agent);
534
- if (cancelledTurnId) cancelledTurnIds.push(cancelledTurnId);
619
+ if (!cancelledTurnId) continue;
620
+ affectedAgentIds.push(agent.agent_id);
621
+ cancelledTurnIds.push(cancelledTurnId);
535
622
  }
536
623
  return {
537
624
  agent_id: agentId,
538
625
  recursive,
539
- affected_agent_ids: affected.map((agent) => agent.agent_id),
626
+ affected_agent_ids: affectedAgentIds,
540
627
  cancelled_turn_ids: cancelledTurnIds,
541
628
  };
542
629
  }
@@ -1315,24 +1402,27 @@ export class MinimalSubagentsCoordinator {
1315
1402
  message: CoordinatorMessage,
1316
1403
  targetId: string,
1317
1404
  delivery: PersistedCoordinationDelivery,
1318
- ): Promise<AgentMessageDisposition> {
1405
+ ): Promise<{ disposition: AgentMessageDisposition; turnId?: string }> {
1319
1406
  this.waitHandedDeliveryIds.add(delivery.delivery_id);
1320
1407
  try {
1321
- await this.deliverToRecipient(targetId, message, () =>
1408
+ const startedTurnId = await this.deliverToRecipient(targetId, message, () =>
1322
1409
  this.isCoordinationDeliveryCurrent(delivery),
1323
1410
  );
1324
- return "queued";
1411
+ return startedTurnId
1412
+ ? { disposition: "started-turn", turnId: startedTurnId }
1413
+ : { disposition: "queued" };
1325
1414
  } catch (error) {
1326
1415
  this.waitHandedDeliveryIds.delete(delivery.delivery_id);
1327
1416
  throw error;
1328
1417
  }
1329
1418
  }
1330
1419
 
1420
+ /** Deliver one message, returning the turn ID when it starts a new turn on an idle child. */
1331
1421
  private async deliverToRecipient(
1332
1422
  targetId: string,
1333
1423
  message: CoordinatorMessage,
1334
1424
  isCurrentDelivery: () => boolean = () => true,
1335
- ): Promise<void> {
1425
+ ): Promise<string | undefined> {
1336
1426
  if (!isCurrentDelivery()) {
1337
1427
  throw new Error("Minimal subagents delivery abandoned after session branch change");
1338
1428
  }
@@ -1352,7 +1442,7 @@ export class MinimalSubagentsCoordinator {
1352
1442
  const visibleMessage = addCoordinatorMessageEnvelope(message);
1353
1443
  if (target.active_turn_id || runtime.isRunning) {
1354
1444
  await runtime.queueCoordinatorMessage(visibleMessage);
1355
- return;
1445
+ return undefined;
1356
1446
  }
1357
1447
  const turnId = this.beginTurn(target);
1358
1448
  const runMessage = runtime
@@ -1374,6 +1464,7 @@ export class MinimalSubagentsCoordinator {
1374
1464
  }
1375
1465
  });
1376
1466
  this.trackBackgroundOperation(runMessage);
1467
+ return turnId;
1377
1468
  }
1378
1469
 
1379
1470
  private trackBackgroundOperation(operation: Promise<void>): void {
@@ -1527,9 +1618,6 @@ export class MinimalSubagentsCoordinator {
1527
1618
 
1528
1619
  private buildAgentSummary(agent: PersistedAgent, includeDescendants = true): AgentSummary {
1529
1620
  const directChildren = this.childrenOf(agent.agent_id);
1530
- const children = includeDescendants
1531
- ? directChildren.map((child) => this.buildAgentSummary(child))
1532
- : [];
1533
1621
  const elapsed = agent.active_turn_started_at
1534
1622
  ? Math.max(0, this.now().getTime() - new Date(agent.active_turn_started_at).getTime())
1535
1623
  : undefined;
@@ -1538,7 +1626,7 @@ export class MinimalSubagentsCoordinator {
1538
1626
  model: agent.launch_contract.model,
1539
1627
  thinking_level: agent.launch_contract.thinking_level,
1540
1628
  };
1541
- return {
1629
+ const summary: AgentSummary = {
1542
1630
  agent_id: agent.agent_id,
1543
1631
  parent_id: agent.parent_id,
1544
1632
  state: agent.active_turn_id ? "running" : "idle",
@@ -1553,8 +1641,11 @@ export class MinimalSubagentsCoordinator {
1553
1641
  latest_activity_at: agent.latest_activity_at ?? agent.created_at,
1554
1642
  task: agent.task,
1555
1643
  child_count: directChildren.length,
1556
- children,
1557
1644
  };
1645
+ if (includeDescendants) {
1646
+ summary.children = directChildren.map((child) => this.buildAgentSummary(child));
1647
+ }
1648
+ return summary;
1558
1649
  }
1559
1650
 
1560
1651
  private buildAgentDetail(agent: PersistedAgent, includeDescendants = true): AgentDetail {
@@ -505,6 +505,9 @@ export class MinimalSubagentsLifecycleController {
505
505
  private readonly coordinatorOperations: CoordinatorToolOperations = {
506
506
  spawn: (...args) => this.requireCoordinator().spawn(...args),
507
507
  inspectStatus: (...args) => this.requireCoordinator().inspectStatus(...args),
508
+ previewActiveTurn: (...args) => this.requireCoordinator().previewActiveTurn(...args),
509
+ inspectActiveTurnTranscript: (...args) =>
510
+ this.requireCoordinator().inspectActiveTurnTranscript(...args),
508
511
  sendAgentMessage: (...args) => this.requireCoordinator().sendAgentMessage(...args),
509
512
  wait: (...args) => this.requireCoordinator().wait(...args),
510
513
  status: (...args) => this.requireCoordinator().status(...args),
@@ -1,6 +1,7 @@
1
1
  import type { JsonValue } from "@earendil-works/pi-ai";
2
2
  import { Value } from "typebox/value";
3
3
  import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
4
+ import { normalizeMinimalSubagentsUsage } from "./minimal-subagents-usage.js";
4
5
  import {
5
6
  createDeliveryLedger,
6
7
  deliveryLedgerSnapshot,
@@ -280,7 +281,8 @@ function cloneRegistryTurnResult(value: RegistryTurnResultWire): TurnResult {
280
281
  output: value.output,
281
282
  };
282
283
  if (value.error !== undefined) result.error = value.error;
283
- if (value.usage !== undefined) result.usage = structuredClone(value.usage);
284
+ // Records from older versions may lack optional usage counters.
285
+ if (value.usage !== undefined) result.usage = normalizeMinimalSubagentsUsage(value.usage);
284
286
  if (value.elapsed_ms !== undefined) result.elapsed_ms = value.elapsed_ms;
285
287
  return result;
286
288
  }
@@ -90,6 +90,7 @@ const SpawnCallArgumentsSchema = Type.Object({
90
90
  model: Type.Optional(Type.String()),
91
91
  thinking_level: Type.Optional(Type.String()),
92
92
  delegation: Type.Optional(Type.String()),
93
+ tools: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
93
94
  });
94
95
  const MessageCallArgumentsSchema = Type.Object({
95
96
  agent_id: Type.Optional(Type.String()),
@@ -113,6 +114,7 @@ const CurrentMessageDetailsSchema = Type.Object({
113
114
  agent_id: Type.String(),
114
115
  message_id: Type.Optional(Type.String()),
115
116
  disposition: Type.String(),
117
+ turn_id: Type.Optional(Type.String()),
116
118
  behavior: Type.Optional(Type.String()),
117
119
  error: Type.Optional(Type.String()),
118
120
  });
@@ -209,7 +211,7 @@ const CoordinatorAgentSummaryOutputSchema = Type.Object({
209
211
  latest_activity_at: Type.Optional(Type.String()),
210
212
  task: Type.Optional(Type.String()),
211
213
  child_count: Type.Number(),
212
- children: Type.Array(Type.Unknown()),
214
+ children: Type.Optional(Type.Array(Type.Unknown())),
213
215
  model: Type.String(),
214
216
  thinking_level: Type.String(),
215
217
  });
@@ -266,7 +268,11 @@ export const CoordinatorToolOutputSchemas = {
266
268
  agent_id: Type.String(),
267
269
  turn_id: Type.String(),
268
270
  status: Type.Literal("running"),
269
- agent: Type.Optional(CoordinatorAgentDetailOutputSchema),
271
+ model: Type.String(),
272
+ thinking_level: Type.String(),
273
+ tools: Type.Array(Type.String()),
274
+ delegation: Type.Union([Type.Literal("none"), Type.Literal("fanout")]),
275
+ warnings: Type.Optional(Type.Array(Type.String())),
270
276
  }),
271
277
  agent_message: Type.Object({
272
278
  agent_id: Type.String(),
@@ -274,8 +280,10 @@ export const CoordinatorToolOutputSchemas = {
274
280
  disposition: Type.Union([
275
281
  Type.Literal("delivered-via-wait"),
276
282
  Type.Literal("queued"),
283
+ Type.Literal("started-turn"),
277
284
  Type.Literal("failed"),
278
285
  ]),
286
+ turn_id: Type.Optional(Type.String()),
279
287
  error: Type.Optional(Type.String()),
280
288
  }),
281
289
  subagent_wait: Type.Union([
@@ -319,6 +327,25 @@ const WaitRenderDetailsSchema = Type.Union([
319
327
  WaitTimeoutDetailsSchema,
320
328
  ]);
321
329
 
330
+ /** Partial `subagent_wait` updates: elapsed wait time and, while the child runs, its progress. */
331
+ const WaitProgressRenderDetailsSchema = Type.Object({
332
+ agent_id: Type.String(),
333
+ status: Type.Literal("waiting"),
334
+ elapsed_ms: Type.Number(),
335
+ turn_id: Type.Optional(Type.String()),
336
+ tool_calls: Type.Optional(Type.Number()),
337
+ });
338
+
339
+ export type WaitProgressRenderDetails = Static<typeof WaitProgressRenderDetailsSchema>;
340
+
341
+ /** Parse one partial `subagent_wait` progress update. */
342
+ export function parseWaitProgressDetails(
343
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Partial tool details are unparsed until the progress schema checks them below.
344
+ details: unknown,
345
+ ): WaitProgressRenderDetails | undefined {
346
+ return Value.Check(WaitProgressRenderDetailsSchema, details) ? details : undefined;
347
+ }
348
+
322
349
  export type SpawnCallArguments = Static<typeof SpawnCallArgumentsSchema>;
323
350
  export type MessageCallArguments = Static<typeof MessageCallArgumentsSchema>;
324
351
  export type WaitCallArguments = Static<typeof WaitCallArgumentsSchema>;
@@ -331,6 +358,7 @@ export type StatusRenderDetails = Static<typeof StatusDetailsSchema>;
331
358
  export type CancelRenderDetails = Static<typeof CancelDetailsSchema>;
332
359
  export type DeleteRenderDetails = Static<typeof DeleteDetailsSchema>;
333
360
  export type RenderStatusAgent = Static<typeof RenderStatusAgentSchema>;
361
+ export type RenderRecentActivity = Static<typeof RenderRecentActivitySchema>;
334
362
 
335
363
  /** Raw tool arguments supplied by Pi's tool-rendering interface. */
336
364
  export type CoordinatorToolCallInput = Parameters<NonNullable<ToolDefinition["renderCall"]>>[0];