@ian-pascoe/pi-minimal-subagents 0.14.0 → 0.15.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.
package/README.md CHANGED
@@ -40,14 +40,18 @@ From this package checkout, load the source directly with
40
40
  `agent_message` in the same tool batch can target the new child.
41
41
  2. `subagent_wait` claims the turn's result, or leave it unclaimed and the final
42
42
  response arrives automatically as a steer message. Without `turn_id`, a wait
43
- skips turns whose result you already claimed or received automatically.
43
+ skips turns whose result you already claimed or received automatically, and
44
+ never repeats an automatically delivered result: it reports
45
+ `already_delivered: true` instead of the output.
44
46
  3. `agent_message` continues an idle child: the result reports
45
47
  `disposition: "started-turn"` with the new `turn_id`. A plain
46
48
  `subagent_wait({ agent_id })` targets that new turn, not the first result;
47
49
  pass `turn_id` to re-read a turn whose result is still retained, claimed or
48
50
  not.
49
51
  4. `subagent_status` inspects children; `subagent_cancel` stops work but keeps
50
- the session; `subagent_delete` removes it. Deleted IDs cannot be reused.
52
+ the session; `subagent_delete` removes it. Deleted IDs cannot be reused, and
53
+ spawning with an existing `agent_id` fails with that child's state and the
54
+ next step (`agent_message`, `subagent_delete`, or another `agent_id`).
51
55
 
52
56
  ## Configuration
53
57
 
@@ -277,10 +281,11 @@ children: `subagent`, `agent_message`, `subagent_wait`, `subagent_status`,
277
281
  only the three adjacent-coordination tools: `agent_message`, `subagent_wait`,
278
282
  and `subagent_status`.
279
283
 
280
- Targeted `subagent_status` includes `recent_activity`, a bounded tail of message
284
+ A running child's `latest_activity_at` advances with the timestamps of its latest
285
+ messages and tool results. Targeted `subagent_status` includes `recent_activity`, a bounded tail of message
281
286
  text, reasoning, tool calls, and tool results. It includes the current streaming
282
- assistant message but omits image data. Timeout Wait Events include the same
283
- detailed status snapshot. Children target only direct children; the Root Agent
287
+ assistant message but omits image data. Timeout Wait Events carry only a compact
288
+ progress snapshot. Children target only direct children; the Root Agent
284
289
  may inspect any descendant. Model-facing status reports a `child_count` but no
285
290
  nested `children` summaries. Reported `usage` always includes `cacheWrite1h`
286
291
  and `reasoning`, as `0` when the provider reports none. `subagent_cancel`
@@ -336,17 +341,24 @@ intermediate Wait Event containing a Coordination Message before the child turn
336
341
  settles. That event claims only its message, so later unconsumed messages and the
337
342
  terminal result retain automatic fallback. If the turn has already settled, a
338
343
  wait returns its terminal result with queued messages in `messages`; waiting
339
- again for the same settled turn returns the same result while it is retained. Pass
340
- optional `turn_id` to address a retained turn exactly, including one whose
341
- result you already claimed (a claimed result stays retained until Delivery
342
- Evidence settles it). Without it, waits skip claimed turns and turns whose
343
- result was already delivered to you automatically, and select the oldest
344
- remaining observable turn, falling back to the active, then latest, turn. A
344
+ again for a settled turn you claimed returns the same result while it is
345
+ retained. Pass optional `turn_id` to address a retained turn exactly, including
346
+ one whose result you already claimed (a claimed result stays retained until
347
+ Delivery Evidence settles it). Without it, waits skip claimed turns and turns
348
+ whose result was already delivered to you automatically, and select the oldest
349
+ remaining observable turn, falling back to the active, then latest, turn. When
350
+ that fallback reaches a result already delivered to you automatically, whether
351
+ still queued or settled, the wait returns `event: "turn"` with
352
+ `already_delivered: true`, the turn identity, and status, but no output; it does
353
+ not claim the turn, and `turn_id` rereads the full result. A
345
354
  caller may have only one outstanding wait for the same source turn; a
346
355
  concurrent duplicate is rejected instead of competing for one Wait Event.
347
356
  When `timeout_ms` expires, the wait returns an observational `event: "timeout"`
348
- with the requested turn identity and the same detailed Child Agent status used
349
- by targeted `subagent_status`. It removes only the waiter, leaving the child
357
+ with the requested turn identity and a compact, bounded progress snapshot
358
+ (`state`, `elapsed_ms`, `latest_activity_at`, `total_tokens`, and the labels of
359
+ the last three Recent Activity items as `recent_activity_labels`), under about 1 KB. It omits the task and
360
+ Launch Contract; targeted `subagent_status` returns the full detail.
361
+ It removes only the waiter, leaving the child
350
362
  running and all pending delivery unclaimed. Abort signals remain errors.
351
363
 
352
364
  The persisted Delivery Ledger records Coordination Messages, terminal results,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -69,11 +69,17 @@ import type {
69
69
  StatusResult,
70
70
  TurnResult,
71
71
  WaitMessageResult,
72
+ WaitTimeoutResult,
73
+ WaitDeliveredTurnResult,
72
74
  WaitResult,
73
75
  } from "./minimal-subagents-types.js";
74
76
 
75
77
  const FRIENDLY_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
76
78
  const RESERVED_AGENT_IDS = new Set(["root", "parent"]);
79
+ /** Recent Activity labels a timed-out wait reports. */
80
+ const WAIT_TIMEOUT_ACTIVITY_LIMIT = 3;
81
+ /** Characters kept of each label so a timed-out wait stays bounded whatever a tool is named. */
82
+ const WAIT_TIMEOUT_LABEL_MAX_LENGTH = 80;
77
83
  const RECENT_MESSAGE_LIMIT = 20;
78
84
  const DEFAULT_AUTOMATIC_DELIVERY_GRACE_MS = 1_000;
79
85
 
@@ -145,6 +151,27 @@ function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] =
145
151
  return messages.length === 0 ? terminal : { ...terminal, messages: structuredClone(messages) };
146
152
  }
147
153
 
154
+ /** A terminal Wait Event naming an already delivered result without repeating its output. */
155
+ function alreadyDeliveredWaitResult(
156
+ result: TurnResult,
157
+ messages: WaitMessageResult[],
158
+ ): WaitDeliveredTurnResult {
159
+ const delivered: WaitDeliveredTurnResult = {
160
+ event: "turn",
161
+ agent_id: result.agent_id,
162
+ turn_id: result.turn_id,
163
+ status: result.status,
164
+ already_delivered: true,
165
+ };
166
+ if (messages.length > 0) delivered.messages = structuredClone(messages);
167
+ return delivered;
168
+ }
169
+
170
+ /** The message ID of an automatic result message, which is also its Delivery Evidence key. */
171
+ function automaticResultMessageId(sourceAgentId: string, sourceTurnId: string): string {
172
+ return `result:${sourceAgentId}:${sourceTurnId}`;
173
+ }
174
+
148
175
  function combineCoordinatorMessages(messages: readonly CoordinatorMessage[]): CoordinatorMessage {
149
176
  const latest = messages.at(-1);
150
177
  if (!latest) throw new Error("Minimal subagents message batch must not be empty");
@@ -238,7 +265,7 @@ export class MinimalSubagentsCoordinator {
238
265
  );
239
266
  }
240
267
  if (this.agents.has(agentId) || this.pendingAgentIds.has(agentId)) {
241
- throw new Error(`Minimal subagents duplicate agent ID: ${agentId}`);
268
+ throw new Error(this.duplicateAgentIdMessage(agentId));
242
269
  }
243
270
 
244
271
  const sessionContext = parameters.session_context ?? "omit";
@@ -459,7 +486,7 @@ export class MinimalSubagentsCoordinator {
459
486
  }
460
487
  }
461
488
 
462
- /** Wait for one exact turn, returning detailed child status if the waiter times out. */
489
+ /** Wait for one exact turn, returning compact child progress if the waiter times out. */
463
490
  wait(
464
491
  callerId: string,
465
492
  agentId: string,
@@ -486,6 +513,11 @@ export class MinimalSubagentsCoordinator {
486
513
  (agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
487
514
  if (retainedResult) {
488
515
  const messages = this.drainPendingParentMessages(callerId, agentId, turnId);
516
+ // A default wait falls back to the latest turn, whose result may already have been delivered
517
+ // automatically; repeating its output would deliver it twice. An explicit turn_id rereads it.
518
+ if (requestedTurnId === undefined && this.wasAlreadyDelivered(callerId, retainedResult)) {
519
+ return Promise.resolve(alreadyDeliveredWaitResult(retainedResult, messages));
520
+ }
489
521
  this.claimTerminalDelivery(callerId, retainedResult);
490
522
  return Promise.resolve(terminalWaitResult(retainedResult, messages));
491
523
  }
@@ -519,7 +551,7 @@ export class MinimalSubagentsCoordinator {
519
551
  agent_id: agentId,
520
552
  turn_id: turnId,
521
553
  timeout_ms: timeoutMs,
522
- agent: this.buildAgentDetail(agent, false),
554
+ ...this.buildWaitTimeoutProgress(agent),
523
555
  });
524
556
  }, timeoutMs);
525
557
  }
@@ -1335,7 +1367,7 @@ export class MinimalSubagentsCoordinator {
1335
1367
  source_agent_id: delivery.source_agent_id,
1336
1368
  destination_agent_id: delivery.destination_agent_id,
1337
1369
  source_turn_id: result.turn_id,
1338
- message_id: `result:${delivery.source_agent_id}:${result.turn_id}`,
1370
+ message_id: automaticResultMessageId(delivery.source_agent_id, result.turn_id),
1339
1371
  status: result.status,
1340
1372
  elapsed_ms: result.elapsed_ms,
1341
1373
  usage: result.usage,
@@ -1737,6 +1769,22 @@ export class MinimalSubagentsCoordinator {
1737
1769
  );
1738
1770
  }
1739
1771
 
1772
+ /**
1773
+ * Whether this result was already delivered to the caller automatically: it is handed, or the
1774
+ * caller's branch holds its automatic result message. A wait's own tool result does not count.
1775
+ */
1776
+ private wasAlreadyDelivered(callerId: string, result: TurnResult): boolean {
1777
+ return (
1778
+ this.handedTerminalKeys.has(deliveryTurnKey(result.agent_id, result.turn_id)) ||
1779
+ this.hasRecipientDeliveryEvidence(
1780
+ callerId,
1781
+ result.agent_id,
1782
+ result.turn_id,
1783
+ automaticResultMessageId(result.agent_id, result.turn_id),
1784
+ )
1785
+ );
1786
+ }
1787
+
1740
1788
  private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
1741
1789
  return this.hasRecipientDeliveryEvidence(
1742
1790
  delivery.destination_agent_id,
@@ -1795,6 +1843,20 @@ export class MinimalSubagentsCoordinator {
1795
1843
  this.removeSettledEmptyTurnClaim(delivery.source_agent_id, delivery.source_turn_id);
1796
1844
  }
1797
1845
 
1846
+ /** Name an existing agent ID's state and the next steps that are valid for that state. */
1847
+ private duplicateAgentIdMessage(agentId: string): string {
1848
+ const prefix = `Minimal subagents duplicate agent ID: ${agentId}`;
1849
+ const existing = this.agents.get(agentId);
1850
+ if (!existing) {
1851
+ return `${prefix} is still being created; retry shortly or choose another agent_id`;
1852
+ }
1853
+ if (existing.availability === "unavailable") {
1854
+ return `${prefix} already exists (unavailable). Use subagent_delete to remove it (a deleted ID cannot be reused), or choose another agent_id.`;
1855
+ }
1856
+ const state = existing.active_turn_id ? "running" : "idle";
1857
+ return `${prefix} already exists (${state}). Use agent_message to continue it, subagent_delete to remove it (a deleted ID cannot be reused), or choose another agent_id.`;
1858
+ }
1859
+
1798
1860
  private buildAgentSummary(agent: PersistedAgent, includeDescendants = true): AgentSummary {
1799
1861
  const directChildren = this.childrenOf(agent.agent_id);
1800
1862
  const elapsed = agent.active_turn_started_at
@@ -1817,7 +1879,7 @@ export class MinimalSubagentsCoordinator {
1817
1879
  ...runtimeProfile,
1818
1880
  tools: runtime?.getActiveToolNames?.() ?? [...agent.launch_contract.ordinary_tools],
1819
1881
  elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
1820
- latest_activity_at: agent.latest_activity_at ?? agent.created_at,
1882
+ latest_activity_at: this.latestActivityAt(agent, runtime),
1821
1883
  task: agent.task,
1822
1884
  child_count: directChildren.length,
1823
1885
  };
@@ -1827,6 +1889,41 @@ export class MinimalSubagentsCoordinator {
1827
1889
  return summary;
1828
1890
  }
1829
1891
 
1892
+ /**
1893
+ * The persisted activity time, advanced by the timestamps of the messages and tool results a
1894
+ * running child has produced since. Only running children can have newer unpersisted activity.
1895
+ */
1896
+ private latestActivityAt(agent: PersistedAgent, runtime: ChildAgentRuntime | undefined): string {
1897
+ const persisted = agent.latest_activity_at ?? agent.created_at;
1898
+ if (!agent.active_turn_id || !runtime) return persisted;
1899
+ const persistedMs = Date.parse(persisted);
1900
+ let latestMs = persistedMs;
1901
+ for (const { timestamp } of runtime.snapshotActivityMessages()) {
1902
+ if (Number.isFinite(timestamp) && timestamp > latestMs) latestMs = timestamp;
1903
+ }
1904
+ return latestMs > persistedMs ? new Date(latestMs).toISOString() : persisted;
1905
+ }
1906
+
1907
+ /** The compact progress a timed-out wait reports; `subagent_status` has the full detail. */
1908
+ private buildWaitTimeoutProgress(
1909
+ agent: PersistedAgent,
1910
+ ): Omit<WaitTimeoutResult, "event" | "agent_id" | "turn_id" | "timeout_ms"> {
1911
+ const summary = this.buildAgentSummary(agent, false);
1912
+ const runtime = this.runtimes.get(agent.agent_id);
1913
+ const messages =
1914
+ runtime?.snapshotActivityMessages() ?? this.inspectTranscript(agent.agent_id).messages;
1915
+ const usage = runtime?.getUsage() ?? agent.latest_result?.usage;
1916
+ return {
1917
+ state: summary.state,
1918
+ elapsed_ms: summary.elapsed_ms,
1919
+ latest_activity_at: summary.latest_activity_at,
1920
+ total_tokens: usage?.totalTokens,
1921
+ recent_activity_labels: buildRecentAgentActivity(messages)
1922
+ .slice(-WAIT_TIMEOUT_ACTIVITY_LIMIT)
1923
+ .map((activity) => activity.label.slice(0, WAIT_TIMEOUT_LABEL_MAX_LENGTH)),
1924
+ };
1925
+ }
1926
+
1830
1927
  private buildAgentDetail(agent: PersistedAgent, includeDescendants = true): AgentDetail {
1831
1928
  const summary = this.buildAgentSummary(agent, includeDescendants);
1832
1929
  const runtime = this.runtimes.get(agent.agent_id);
@@ -146,6 +146,7 @@ const WaitTurnDetailsSchema = Type.Object({
146
146
  error: Type.Optional(Type.String()),
147
147
  elapsed_ms: Type.Optional(Type.Number()),
148
148
  usage: Type.Optional(RenderUsageSchema),
149
+ already_delivered: Type.Optional(Type.Boolean()),
149
150
  messages: Type.Optional(Type.Array(WaitMessageDetailsSchema)),
150
151
  });
151
152
  const WaitTimeoutDetailsSchema = Type.Object({
@@ -153,7 +154,13 @@ const WaitTimeoutDetailsSchema = Type.Object({
153
154
  agent_id: Type.String(),
154
155
  turn_id: Type.String(),
155
156
  timeout_ms: Type.Number(),
156
- agent: RenderStatusAgentSchema,
157
+ state: Type.Optional(Type.String()),
158
+ elapsed_ms: Type.Optional(Type.Number()),
159
+ latest_activity_at: Type.Optional(Type.String()),
160
+ total_tokens: Type.Optional(Type.Number()),
161
+ recent_activity_labels: Type.Optional(Type.Array(Type.String())),
162
+ /** Pre-compact timeout results carried the full child status. */
163
+ agent: Type.Optional(RenderStatusAgentSchema),
157
164
  });
158
165
  const StatusDetailsSchema = Type.Union([
159
166
  Type.Object({
@@ -300,12 +307,23 @@ export const CoordinatorToolOutputSchemas = {
300
307
  messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
301
308
  ...CoordinatorWaitSourceOutputSchema,
302
309
  }),
310
+ Type.Object({
311
+ ...Type.Pick(CoordinatorTurnOutputSchema, ["agent_id", "turn_id", "status"]).properties,
312
+ event: Type.Literal("turn"),
313
+ already_delivered: Type.Literal(true),
314
+ messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
315
+ ...CoordinatorWaitSourceOutputSchema,
316
+ }),
303
317
  Type.Object({
304
318
  event: Type.Literal("timeout"),
305
319
  agent_id: Type.String(),
306
320
  turn_id: Type.String(),
307
321
  timeout_ms: Type.Number(),
308
- agent: CoordinatorAgentDetailOutputSchema,
322
+ state: Type.Union([Type.Literal("running"), Type.Literal("idle")]),
323
+ elapsed_ms: Type.Optional(Type.Number()),
324
+ latest_activity_at: Type.Optional(Type.String()),
325
+ total_tokens: Type.Optional(Type.Number()),
326
+ recent_activity_labels: Type.Array(Type.String()),
309
327
  ...CoordinatorWaitSourceOutputSchema,
310
328
  }),
311
329
  ]),
@@ -582,6 +582,19 @@ function renderWaitProgress(
582
582
  return container;
583
583
  }
584
584
 
585
+ /** Shown instead of the output of a result already delivered to the parent automatically. */
586
+ const ALREADY_DELIVERED_TEXT = "Already delivered automatically; wait with turn_id to reread it.";
587
+
588
+ /** The activity labels of a timeout result, including pre-compact results that carried the full status. */
589
+ function timeoutActivityLabels(
590
+ details: Extract<WaitRenderDetails, { event: "timeout" }>,
591
+ ): readonly string[] {
592
+ return (
593
+ details.recent_activity_labels ??
594
+ latestProgressActivity(details.agent?.recent_activity ?? []).map((entry) => entry.label)
595
+ );
596
+ }
597
+
585
598
  /** The collapsed body of a settled wait: what the child said, or why it stopped. */
586
599
  function collapsedWaitBody(
587
600
  details: WaitRenderDetails,
@@ -590,8 +603,13 @@ function collapsedWaitBody(
590
603
  ): Component | undefined {
591
604
  if (details.event === "message") return collapsedTextPreview(details.message, theme);
592
605
  if (details.event === "timeout") {
593
- const activity = latestProgressActivity(details.agent.recent_activity ?? []);
594
- return activity.length > 0 ? new ActivityLines(activity, theme) : undefined;
606
+ const labels = timeoutActivityLabels(details);
607
+ return labels.length > 0
608
+ ? collapsedTextPreview(theme.fg("dim", labels.join(" \u00b7 ")), theme)
609
+ : undefined;
610
+ }
611
+ if (details.already_delivered) {
612
+ return collapsedTextPreview(theme.fg("muted", ALREADY_DELIVERED_TEXT), theme);
595
613
  }
596
614
  const output = details.output ?? "";
597
615
  if (status === "completed") {
@@ -621,7 +639,9 @@ function renderWaitResult(
621
639
  details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
622
640
  );
623
641
  const usage = details.event === "timeout" ? undefined : details.usage;
624
- const tokens = formatSubagentTokenCount(usage?.totalTokens);
642
+ const tokens = formatSubagentTokenCount(
643
+ details.event === "timeout" ? details.total_tokens : usage?.totalTokens,
644
+ );
625
645
  const drainedMessageCount =
626
646
  details.event === "message" || details.event === "timeout"
627
647
  ? 0
@@ -652,12 +672,15 @@ function renderWaitResult(
652
672
  return container;
653
673
  }
654
674
  if (details.event === "timeout") {
655
- appendComponentSection(
656
- container,
657
- theme,
658
- "Child status",
659
- renderDetailedStatusAgent(details.agent, options, theme),
675
+ container.addChild(
676
+ renderLabelValue(theme, "State", details.state ?? details.agent?.state ?? "unknown"),
660
677
  );
678
+ if (details.latest_activity_at) {
679
+ container.addChild(renderLabelValue(theme, "Latest activity", details.latest_activity_at));
680
+ }
681
+ const labels = timeoutActivityLabels(details);
682
+ if (labels.length > 0)
683
+ appendTextSection(container, theme, "Recent activity", labels.join("\n"));
661
684
  return container;
662
685
  }
663
686
  if (details.messages && details.messages.length > 0) {
@@ -669,7 +692,9 @@ function renderWaitResult(
669
692
  );
670
693
  }
671
694
  const output = details.output ?? "";
672
- if (status === "completed") {
695
+ if (details.already_delivered) {
696
+ appendTextSection(container, theme, "Output", ALREADY_DELIVERED_TEXT);
697
+ } else if (status === "completed") {
673
698
  if (output.length > 0) {
674
699
  appendComponentSection(
675
700
  container,
@@ -36,6 +36,7 @@ const DeliveryEvidenceMessageSchema = Type.Object({
36
36
  /** Parses durable custom-message and wait-tool Delivery Evidence details. */
37
37
  export const DeliveryEvidenceDetailsSchema = Type.Object({
38
38
  event: Type.Optional(Type.String()),
39
+ already_delivered: Type.Optional(Type.Boolean()),
39
40
  source_agent_id: Type.String(),
40
41
  source_turn_id: Type.String(),
41
42
  delivery_id: Type.Optional(Type.String()),
@@ -503,8 +503,13 @@ export function findDeliveryEvidence(
503
503
  ))
504
504
  );
505
505
  }
506
- // A message or timeout wait result never carried the turn's terminal result.
507
- if (waitToolResult && (details.event === "message" || details.event === "timeout"))
506
+ // A message, timeout, or already-delivered wait result never carried the turn's terminal result.
507
+ if (
508
+ waitToolResult &&
509
+ (details.event === "message" ||
510
+ details.event === "timeout" ||
511
+ details.already_delivered === true)
512
+ )
508
513
  return false;
509
514
  return details.source_agent_id === sourceAgentId && details.source_turn_id === sourceTurnId;
510
515
  });
@@ -38,6 +38,7 @@ import type {
38
38
  DeleteResult,
39
39
  SpawnResult,
40
40
  StatusResult,
41
+ WaitDeliveredTurnResult,
41
42
  WaitResult,
42
43
  } from "./minimal-subagents-types.js";
43
44
 
@@ -197,6 +198,22 @@ function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
197
198
  };
198
199
  }
199
200
 
201
+ /**
202
+ * One line saying an already-delivered result is not repeated and how to reread it, followed by
203
+ * any Coordination Messages the wait drained, which the parent has not seen yet.
204
+ */
205
+ function alreadyDeliveredContent(
206
+ result: WaitDeliveredTurnResult,
207
+ ): AgentToolResult<WaitResult>["content"] {
208
+ const notice = `Result of ${result.agent_id} turn ${result.turn_id} (${result.status}) was already delivered automatically; call subagent_wait with turn_id "${result.turn_id}" to reread it.`;
209
+ if (!result.messages) return [{ type: "text", text: notice }];
210
+ const messages = truncateHead(JSON.stringify({ messages: result.messages }, null, 2), {
211
+ maxBytes: DEFAULT_MAX_BYTES,
212
+ maxLines: DEFAULT_MAX_LINES,
213
+ });
214
+ return [{ type: "text", text: `${notice}\n${messages.content}` }];
215
+ }
216
+
200
217
  /**
201
218
  * Report a failed operation that still carries declared output: the model sees an error and
202
219
  * codemode scripts receive `structuredContent` instead of a data-less rejection.
@@ -313,7 +330,7 @@ export function createCoordinatorToolDefinitions(
313
330
  name: "subagent_wait",
314
331
  label: "Subagent Wait",
315
332
  description:
316
- "Wait for one direct child's oldest observable turn whose result you have neither claimed nor received, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. A settled turn returns event=turn, with any queued messages in messages; waiting again returns the same result. Timeout returns event=timeout with detailed child status and never cancels the child.",
333
+ "Wait for one direct child's oldest observable turn whose result you have neither claimed nor received, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. A settled turn returns event=turn, with any queued messages in messages; waiting again returns a result you claimed in full. Without turn_id, a result already delivered to you automatically returns event=turn with already_delivered=true and no output; pass its turn_id to reread it. Timeout returns a compact event=timeout (state, elapsed_ms, latest_activity_at, total_tokens, last few activity labels; use subagent_status for detail) and never cancels the child.",
317
334
  promptSnippet: "Wait for one direct child's exact turn",
318
335
  parameters: options.schemas.subagent_wait,
319
336
  annotations: {
@@ -358,7 +375,10 @@ export function createCoordinatorToolDefinitions(
358
375
  source_turn_id: result.turn_id,
359
376
  };
360
377
  return {
361
- ...structuredToolResult(result),
378
+ content:
379
+ "already_delivered" in result
380
+ ? alreadyDeliveredContent(result)
381
+ : structuredToolResult(result).content,
362
382
  details,
363
383
  structuredContent: JSON.parse(JSON.stringify(details)),
364
384
  };
@@ -98,19 +98,41 @@ export interface WaitTurnResult extends TurnResult {
98
98
  messages?: WaitMessageResult[];
99
99
  }
100
100
 
101
- /** Reports an observational wait timeout with the current detailed child status. */
101
+ /**
102
+ * Reports that a default wait selected a terminal turn whose result was already delivered
103
+ * automatically, handed or settled, so the output is not repeated; an explicit `turn_id` rereads it.
104
+ */
105
+ export interface WaitDeliveredTurnResult extends Pick<
106
+ TurnResult,
107
+ "agent_id" | "turn_id" | "status"
108
+ > {
109
+ event: "turn";
110
+ already_delivered: true;
111
+ messages?: WaitMessageResult[];
112
+ }
113
+
114
+ /** Reports an observational wait timeout with a compact, bounded child progress snapshot. */
102
115
  export interface WaitTimeoutResult {
103
116
  event: "timeout";
104
117
  agent_id: string;
105
118
  turn_id: string;
106
119
  /** Requested timeout duration that expired. */
107
120
  timeout_ms: number;
108
- /** Detailed child status captured when the timeout callback won. */
109
- agent: AgentDetail;
121
+ state: AgentState;
122
+ elapsed_ms?: number;
123
+ latest_activity_at?: string;
124
+ /** Total tokens the child has used, when usage is known. */
125
+ total_tokens?: number;
126
+ /** Labels of the child's last few Recent Activity items, oldest first. */
127
+ recent_activity_labels: string[];
110
128
  }
111
129
 
112
130
  /** Reports one message, terminal turn, or timeout returned by subagent_wait. */
113
- export type WaitResult = WaitMessageResult | WaitTurnResult | WaitTimeoutResult;
131
+ export type WaitResult =
132
+ | WaitMessageResult
133
+ | WaitTurnResult
134
+ | WaitDeliveredTurnResult
135
+ | WaitTimeoutResult;
114
136
 
115
137
  /** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
116
138
  export interface AgentSummary extends RuntimeProfile {