@ian-pascoe/pi-minimal-subagents 0.19.2 → 0.20.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
@@ -387,6 +387,18 @@ Launch Contract; targeted `subagent_status` returns the full detail.
387
387
  It removes only the waiter, leaving the child
388
388
  running and all pending delivery unclaimed. Abort signals remain errors.
389
389
 
390
+ A wait that would block also ends early when a Queued Steer is pending for the
391
+ caller's session, because Pi hands a steer to an agent only after every tool call in
392
+ the current batch returns. It returns an `event: "steered"` Wait Event with the
393
+ requested turn identity and the same compact progress snapshot as a timeout, and,
394
+ like a timeout, removes only its waiter: nothing is claimed, the child is not
395
+ cancelled, and later messages and the terminal result keep automatic fallback.
396
+ A wait that starts while a steer is pending returns `steered` at once, but a
397
+ retained result or pending Coordination Message is still returned first. Only steers
398
+ count (a user's mid-run message, a parent's Coordination Message into a busy child, or
399
+ another child's result steered into the caller); follow-ups never do. A child
400
+ session's steer clears when its next turn starts or its run ends.
401
+
390
402
  The persisted Delivery Ledger records Coordination Messages, terminal results,
391
403
  globally increasing sequence, and wait ownership before delivery. Existing items
392
404
  retain their sequence; gaps from skipped malformed records are valid. Claims can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -31,7 +31,7 @@
31
31
  "provenance": true
32
32
  },
33
33
  "dependencies": {
34
- "@ian-pascoe/pi-utils": "^0.7.1",
34
+ "@ian-pascoe/pi-utils": "^0.9.0",
35
35
  "minimatch": "^10.2.6"
36
36
  },
37
37
  "peerDependencies": {
@@ -69,7 +69,8 @@ import type {
69
69
  StatusResult,
70
70
  TurnResult,
71
71
  WaitMessageResult,
72
- WaitTimeoutResult,
72
+ WaitProgressSnapshot,
73
+ WaitSteeredResult,
73
74
  AutomaticDelivery,
74
75
  WaitDeliveredTurnResult,
75
76
  WaitResult,
@@ -106,6 +107,8 @@ interface TurnWaiter {
106
107
  timeout?: ReturnType<typeof setTimeout>;
107
108
  abortSignal?: AbortSignal;
108
109
  abortListener?: () => void;
110
+ steerSignal?: AbortSignal;
111
+ steerListener?: () => void;
109
112
  }
110
113
 
111
114
  interface PendingParentMessage {
@@ -489,13 +492,19 @@ export class MinimalSubagentsCoordinator {
489
492
  }
490
493
  }
491
494
 
492
- /** Wait for one exact turn, returning compact child progress if the waiter times out. */
495
+ /**
496
+ * Wait for one exact turn, returning compact child progress if the waiter times out. Once
497
+ * `steerSignal` aborts, a Queued Steer is pending for the caller, so a wait that would block
498
+ * ends with a `steered` Steer Yield instead, claiming nothing and leaving the child running;
499
+ * `signal` still rejects as a cancellation.
500
+ */
493
501
  wait(
494
502
  callerId: string,
495
503
  agentId: string,
496
504
  timeoutMs?: number,
497
505
  signal?: AbortSignal,
498
506
  requestedTurnId?: string,
507
+ steerSignal?: AbortSignal,
499
508
  ): Promise<WaitResult> {
500
509
  this.assertAccepting();
501
510
  this.assertCallerExists(callerId);
@@ -538,8 +547,18 @@ export class MinimalSubagentsCoordinator {
538
547
  );
539
548
  }
540
549
 
550
+ if (steerSignal?.aborted && !signal?.aborted) {
551
+ return Promise.resolve(this.steeredWaitResult(agent, turnId));
552
+ }
553
+
541
554
  return new Promise<WaitResult>((resolve, reject) => {
542
- const waiter: TurnWaiter = { callerId, resolve, reject, abortSignal: signal };
555
+ const waiter: TurnWaiter = {
556
+ callerId,
557
+ resolve,
558
+ reject,
559
+ abortSignal: signal,
560
+ steerSignal,
561
+ };
543
562
  let turnWaiters = this.waiters.get(key);
544
563
  if (!turnWaiters) {
545
564
  turnWaiters = new Set();
@@ -562,6 +581,13 @@ export class MinimalSubagentsCoordinator {
562
581
  });
563
582
  }, timeoutMs);
564
583
  }
584
+ if (steerSignal) {
585
+ waiter.steerListener = () => {
586
+ this.removeWaiter(key, waiter);
587
+ resolve(this.steeredWaitResult(agent, turnId));
588
+ };
589
+ steerSignal.addEventListener("abort", waiter.steerListener, { once: true });
590
+ }
565
591
  if (signal) {
566
592
  waiter.abortListener = () =>
567
593
  stopWaiting(new Error(`Minimal subagents wait cancelled for ${agentId}`));
@@ -571,6 +597,16 @@ export class MinimalSubagentsCoordinator {
571
597
  });
572
598
  }
573
599
 
600
+ /** The Steer Yield: the timeout's progress snapshot, with nothing claimed or cancelled. */
601
+ private steeredWaitResult(agent: PersistedAgent, turnId: string): WaitSteeredResult {
602
+ return {
603
+ event: "steered",
604
+ agent_id: agent.agent_id,
605
+ turn_id: turnId,
606
+ ...this.buildWaitTimeoutProgress(agent),
607
+ };
608
+ }
609
+
574
610
  /** Return direct-child status for a child caller; the root may inspect any descendant. */
575
611
  status(callerId: string, agentId?: string): StatusResult {
576
612
  this.assertCallerExists(callerId);
@@ -1918,9 +1954,7 @@ export class MinimalSubagentsCoordinator {
1918
1954
  }
1919
1955
 
1920
1956
  /** The compact progress a timed-out wait reports; `subagent_status` has the full detail. */
1921
- private buildWaitTimeoutProgress(
1922
- agent: PersistedAgent,
1923
- ): Omit<WaitTimeoutResult, "event" | "agent_id" | "turn_id" | "timeout_ms"> {
1957
+ private buildWaitTimeoutProgress(agent: PersistedAgent): WaitProgressSnapshot {
1924
1958
  const summary = this.buildAgentSummary(agent, false);
1925
1959
  const runtime = this.runtimes.get(agent.agent_id);
1926
1960
  const messages =
@@ -2099,6 +2133,9 @@ export class MinimalSubagentsCoordinator {
2099
2133
  if (waiter.abortSignal && waiter.abortListener) {
2100
2134
  waiter.abortSignal.removeEventListener("abort", waiter.abortListener);
2101
2135
  }
2136
+ if (waiter.steerSignal && waiter.steerListener) {
2137
+ waiter.steerSignal.removeEventListener("abort", waiter.steerListener);
2138
+ }
2102
2139
  }
2103
2140
 
2104
2141
  private resolveWaiterWithMessage(
@@ -20,6 +20,7 @@ import {
20
20
  type TurnEndEvent,
21
21
  } from "@earendil-works/pi-coding-agent";
22
22
  import { isKeyRelease, isKeyRepeat, matchesKey } from "@earendil-works/pi-tui";
23
+ import { raiseQueuedSteer, trackQueuedSteers } from "@ian-pascoe/pi-utils/queued-steer";
23
24
  import { expandMessageOnClick, noticeText } from "@ian-pascoe/pi-utils/ui";
24
25
  import {
25
26
  createSubagentAccessBranchRecord,
@@ -189,7 +190,11 @@ function createRootConversationEndpoint(
189
190
  return {
190
191
  async queueCoordinatorMessage(message) {
191
192
  // A running root takes the message as a clearable steer; an idle root starts a turn with it.
192
- if (!context.isIdle()) onSteer();
193
+ if (!context.isIdle()) {
194
+ onSteer();
195
+ // Pi hands the steer over only after the running tool batch ends, so waits there yield.
196
+ raiseQueuedSteer(context.sessionManager.getSessionId());
197
+ }
193
198
  pi.sendMessage(
194
199
  {
195
200
  customType: message.customType,
@@ -548,6 +553,7 @@ export class MinimalSubagentsLifecycleController {
548
553
  * registered only while a result waits in the root's steer queue (`watchRootTurnEnds`).
549
554
  */
550
555
  register(): void {
556
+ trackQueuedSteers(this.pi);
551
557
  const rootTools = createCoordinatorToolDefinitions({
552
558
  coordinator: this.coordinatorOperations,
553
559
  callerId: "root",
@@ -163,6 +163,16 @@ const WaitTimeoutDetailsSchema = Type.Object({
163
163
  /** Pre-compact timeout results carried the full child status. */
164
164
  agent: Type.Optional(RenderStatusAgentSchema),
165
165
  });
166
+ const WaitSteeredDetailsSchema = Type.Object({
167
+ event: Type.Literal("steered"),
168
+ agent_id: Type.String(),
169
+ turn_id: Type.String(),
170
+ state: Type.Optional(Type.String()),
171
+ elapsed_ms: Type.Optional(Type.Number()),
172
+ latest_activity_at: Type.Optional(Type.String()),
173
+ total_tokens: Type.Optional(Type.Number()),
174
+ recent_activity_labels: Type.Optional(Type.Array(Type.String())),
175
+ });
166
176
  const StatusDetailsSchema = Type.Union([
167
177
  Type.Object({
168
178
  parent_id: Type.Optional(Type.String()),
@@ -331,6 +341,17 @@ export const CoordinatorToolOutputSchemas = {
331
341
  recent_activity_labels: Type.Array(Type.String()),
332
342
  ...CoordinatorWaitSourceOutputSchema,
333
343
  }),
344
+ Type.Object({
345
+ event: Type.Literal("steered"),
346
+ agent_id: Type.String(),
347
+ turn_id: Type.String(),
348
+ state: Type.Union([Type.Literal("running"), Type.Literal("idle")]),
349
+ elapsed_ms: Type.Optional(Type.Number()),
350
+ latest_activity_at: Type.Optional(Type.String()),
351
+ total_tokens: Type.Optional(Type.Number()),
352
+ recent_activity_labels: Type.Array(Type.String()),
353
+ ...CoordinatorWaitSourceOutputSchema,
354
+ }),
334
355
  ]),
335
356
  subagent_status: Type.Union([
336
357
  Type.Object({
@@ -351,6 +372,7 @@ const WaitRenderDetailsSchema = Type.Union([
351
372
  WaitMessageDetailsSchema,
352
373
  WaitTurnDetailsSchema,
353
374
  WaitTimeoutDetailsSchema,
375
+ WaitSteeredDetailsSchema,
354
376
  ]);
355
377
 
356
378
  /** Partial `subagent_wait` updates: elapsed wait time and, while the child runs, its progress. */
@@ -597,13 +597,20 @@ function alreadyDeliveredText(details: { delivery_pending?: boolean }): string {
597
597
  return details.delivery_pending ? DELIVERY_PENDING_TEXT : ALREADY_DELIVERED_TEXT;
598
598
  }
599
599
 
600
- /** The activity labels of a timeout result, including pre-compact results that carried the full status. */
601
- function timeoutActivityLabels(
602
- details: Extract<WaitRenderDetails, { event: "timeout" }>,
603
- ): readonly string[] {
600
+ /** A wait that ended early with a progress snapshot: a timeout or a Steer Yield. */
601
+ type WaitEarlyEndRenderDetails = Extract<WaitRenderDetails, { event: "timeout" | "steered" }>;
602
+
603
+ function isWaitProgress(details: WaitRenderDetails): details is WaitEarlyEndRenderDetails {
604
+ return details.event === "timeout" || details.event === "steered";
605
+ }
606
+
607
+ /** The activity labels of a timeout or steered result, including pre-compact results that carried the full status. */
608
+ function timeoutActivityLabels(details: WaitEarlyEndRenderDetails): readonly string[] {
604
609
  return (
605
610
  details.recent_activity_labels ??
606
- latestProgressActivity(details.agent?.recent_activity ?? []).map((entry) => entry.label)
611
+ latestProgressActivity(
612
+ ("agent" in details ? details.agent?.recent_activity : undefined) ?? [],
613
+ ).map((entry) => entry.label)
607
614
  );
608
615
  }
609
616
 
@@ -614,7 +621,7 @@ function collapsedWaitBody(
614
621
  theme: MinimalSubagentsRenderTheme,
615
622
  ): Component | undefined {
616
623
  if (details.event === "message") return collapsedText(theme, details.message);
617
- if (details.event === "timeout") {
624
+ if (isWaitProgress(details)) {
618
625
  const labels = timeoutActivityLabels(details);
619
626
  return labels.length > 0
620
627
  ? collapsedText(theme, theme.fg("dim", labels.join(" \u00b7 ")))
@@ -643,18 +650,18 @@ function renderWaitResult(
643
650
  ? "message"
644
651
  : details.event === "timeout"
645
652
  ? "timed out"
646
- : details.status;
653
+ : details.event === "steered"
654
+ ? "steered"
655
+ : details.status;
647
656
  const duration = formatSubagentDuration(
648
657
  details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
649
658
  );
650
- const usage = details.event === "timeout" ? undefined : details.usage;
659
+ const usage = isWaitProgress(details) ? undefined : details.usage;
651
660
  const tokens = formatSubagentTokenCount(
652
- details.event === "timeout" ? details.total_tokens : usage?.totalTokens,
661
+ isWaitProgress(details) ? details.total_tokens : usage?.totalTokens,
653
662
  );
654
663
  const drainedMessageCount =
655
- details.event === "message" || details.event === "timeout"
656
- ? 0
657
- : (details.messages?.length ?? 0);
664
+ details.event === "message" || isWaitProgress(details) ? 0 : (details.messages?.length ?? 0);
658
665
  const metrics = [
659
666
  duration,
660
667
  tokens ? `${tokens} tokens` : undefined,
@@ -674,10 +681,9 @@ function renderWaitResult(
674
681
  container.addChild(renderLabelValue(theme, "Message ID", details.message_id));
675
682
  return container;
676
683
  }
677
- if (details.event === "timeout") {
678
- container.addChild(
679
- renderLabelValue(theme, "State", details.state ?? details.agent?.state ?? "unknown"),
680
- );
684
+ if (isWaitProgress(details)) {
685
+ const legacyState = "agent" in details ? details.agent?.state : undefined;
686
+ container.addChild(renderLabelValue(theme, "State", details.state ?? legacyState ?? "unknown"));
681
687
  if (details.latest_activity_at) {
682
688
  container.addChild(renderLabelValue(theme, "Latest activity", details.latest_activity_at));
683
689
  }
@@ -17,7 +17,9 @@ import type {
17
17
  ToolSelection,
18
18
  WaitDeliveredTurnResult,
19
19
  WaitMessageResult,
20
+ WaitProgressSnapshot,
20
21
  WaitResult,
22
+ WaitSteeredResult,
21
23
  WaitTimeoutResult,
22
24
  WaitTurnResult,
23
25
  } from "./minimal-subagents-types.js";
@@ -157,12 +159,8 @@ function formatWaitTurnText(result: WaitTurnResult): string {
157
159
  return `${lines.join("\n")}\n\n${result.output || "(no output)"}`;
158
160
  }
159
161
 
160
- /** The observational timeout snapshot; the wait never cancels the child. */
161
- function formatWaitTimeoutText(result: WaitTimeoutResult): string {
162
- const lines = [
163
- `Wait timed out after ${formatSubagentDuration(result.timeout_ms) ?? `${result.timeout_ms}ms`}; ${result.agent_id} is ${result.state} on turn ${result.turn_id} and was not cancelled.`,
164
- ];
165
- const progress = joinParts([
162
+ function waitProgressText(result: WaitProgressSnapshot): string {
163
+ return joinParts([
166
164
  result.elapsed_ms === undefined
167
165
  ? undefined
168
166
  : `elapsed ${formatSubagentDuration(result.elapsed_ms)}`,
@@ -171,6 +169,27 @@ function formatWaitTimeoutText(result: WaitTimeoutResult): string {
171
169
  ? undefined
172
170
  : `${formatSubagentTokenCount(result.total_tokens)} tokens`,
173
171
  ]);
172
+ }
173
+
174
+ /** The observational timeout snapshot; the wait never cancels the child. */
175
+ function formatWaitTimeoutText(result: WaitTimeoutResult): string {
176
+ const lines = [
177
+ `Wait timed out after ${formatSubagentDuration(result.timeout_ms) ?? `${result.timeout_ms}ms`}; ${result.agent_id} is ${result.state} on turn ${result.turn_id} and was not cancelled.`,
178
+ ];
179
+ const progress = waitProgressText(result);
180
+ if (progress) lines.push(progress);
181
+ if (result.recent_activity_labels.length > 0) {
182
+ lines.push(`recent activity: ${result.recent_activity_labels.join(", ")}`);
183
+ }
184
+ return lines.join("\n");
185
+ }
186
+
187
+ /** The Steer Yield snapshot: a Queued Steer ended the wait, and the child was not cancelled. */
188
+ function formatWaitSteeredText(result: WaitSteeredResult): string {
189
+ const lines = [
190
+ `Wait ended early: a queued steer is waiting for you. ${result.agent_id} is ${result.state} on turn ${result.turn_id} and was not cancelled; wait again to resume.`,
191
+ ];
192
+ const progress = waitProgressText(result);
174
193
  if (progress) lines.push(progress);
175
194
  if (result.recent_activity_labels.length > 0) {
176
195
  lines.push(`recent activity: ${result.recent_activity_labels.join(", ")}`);
@@ -178,7 +197,7 @@ function formatWaitTimeoutText(result: WaitTimeoutResult): string {
178
197
  return lines.join("\n");
179
198
  }
180
199
 
181
- /** Any Wait Event: a message, a settled or already-delivered turn, or a timeout. */
200
+ /** Any Wait Event: a message, a settled or already-delivered turn, a timeout, or a steer yield. */
182
201
  export function formatWaitResultText(result: WaitResult): string {
183
202
  if ("already_delivered" in result) return formatWaitDeliveredText(result);
184
203
  switch (result.event) {
@@ -188,6 +207,8 @@ export function formatWaitResultText(result: WaitResult): string {
188
207
  return formatWaitTurnText(result);
189
208
  case "timeout":
190
209
  return formatWaitTimeoutText(result);
210
+ case "steered":
211
+ return formatWaitSteeredText(result);
191
212
  }
192
213
  }
193
214
 
@@ -27,6 +27,7 @@ import {
27
27
  readAgentLineage,
28
28
  type AgentLineage,
29
29
  } from "@ian-pascoe/pi-utils/agent-lineage";
30
+ import { raiseQueuedSteer, settleQueuedSteers } from "@ian-pascoe/pi-utils/queued-steer";
30
31
  import type { Static, TSchema } from "typebox";
31
32
  import { Value } from "typebox/value";
32
33
  import {
@@ -525,11 +526,12 @@ export function findDeliveryEvidence(
525
526
  ))
526
527
  );
527
528
  }
528
- // A message, timeout, or already-delivered wait result never carried the turn's terminal result.
529
+ // A message, timeout, steer yield, or already-delivered wait result never carried the turn's terminal result.
529
530
  if (
530
531
  waitToolResult &&
531
532
  (details.event === "message" ||
532
533
  details.event === "timeout" ||
534
+ details.event === "steered" ||
533
535
  details.already_delivered === true)
534
536
  )
535
537
  return false;
@@ -631,6 +633,11 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
631
633
  // Keep this child-only; AgentSession.setSteeringMode would overwrite the user's global setting.
632
634
  session.agent.steeringMode = "all";
633
635
  this.unsubscribe = session.subscribe((event) => {
636
+ // Child sessions get no Pi `input` events, so this runtime settles its own Queued Steers.
637
+ if (event.type === "turn_start" || event.type === "agent_end") {
638
+ settleQueuedSteers(this.sessionId);
639
+ return;
640
+ }
634
641
  if (event.type !== "entry_appended") return;
635
642
  if (
636
643
  event.entry.type === "custom_message" ||
@@ -641,6 +648,10 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
641
648
  });
642
649
  }
643
650
 
651
+ private get sessionId(): string {
652
+ return this.session.sessionManager.getSessionId();
653
+ }
654
+
644
655
  get sessionLeafId(): string | undefined {
645
656
  return this.session.sessionManager.getLeafId() ?? undefined;
646
657
  }
@@ -660,6 +671,8 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
660
671
  }
661
672
 
662
673
  runMessage(message: CoordinatorMessage): Promise<RuntimeTurnOutcome> {
674
+ // An idle child starts a turn with the message; a streaming child queues it as a steer.
675
+ if (this.session.isStreaming) raiseQueuedSteer(this.sessionId);
663
676
  return this.captureTurn(() =>
664
677
  this.session.sendCustomMessage(
665
678
  {
@@ -682,6 +695,7 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
682
695
  details: message.details,
683
696
  timestamp: Date.now(),
684
697
  });
698
+ raiseQueuedSteer(this.sessionId);
685
699
  }
686
700
 
687
701
  async abort(): Promise<void> {
@@ -691,6 +705,7 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
691
705
 
692
706
  async dispose(): Promise<void> {
693
707
  this.unsubscribe();
708
+ settleQueuedSteers(this.sessionId);
694
709
  try {
695
710
  await this.observer?.dispose();
696
711
  // Let child extensions release services such as MCP server connections.
@@ -11,6 +11,7 @@ import {
11
11
  } from "@earendil-works/pi-coding-agent";
12
12
  import { Container, type Component, type TUI } from "@earendil-works/pi-tui";
13
13
  import { acceptNullForOptionalArguments } from "@ian-pascoe/pi-utils/null-optional-arguments";
14
+ import { watchQueuedSteer } from "@ian-pascoe/pi-utils/queued-steer";
14
15
  import { appendDurationFooter, callDurationFooter } from "@ian-pascoe/pi-utils/ui";
15
16
  import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
16
17
  import { withTroubleshootingHint } from "./troubleshooting-skill.js";
@@ -359,7 +360,7 @@ export function createCoordinatorToolDefinitions(
359
360
  name: "subagent_wait",
360
361
  label: "Subagent Wait",
361
362
  description:
362
- "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. If it was handed to you but is not yet in your conversation, delivery_pending=true and it arrives as a separate message; do not reread. 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.",
363
+ "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. If it was handed to you but is not yet in your conversation, delivery_pending=true and it arrives as a separate message; do not reread. 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. A queued steer for you returns event=steered with the same snapshot, immediately if one is already pending; it claims nothing and never cancels the child, so wait again to resume.",
363
364
  promptSnippet: "Wait for one direct child's exact turn",
364
365
  parameters: options.schemas.subagent_wait,
365
366
  annotations: {
@@ -368,7 +369,7 @@ export function createCoordinatorToolDefinitions(
368
369
  idempotentHint: false,
369
370
  openWorldHint: false,
370
371
  },
371
- async execute(_toolCallId, parameters, signal, onUpdate) {
372
+ async execute(_toolCallId, parameters, signal, onUpdate, context) {
372
373
  const startedAt = Date.now();
373
374
  const updateWaitingResult = () => {
374
375
  const details: WaitProgressDetails = {
@@ -389,6 +390,8 @@ export function createCoordinatorToolDefinitions(
389
390
  updateWaitingResult();
390
391
  const waitingInterval = setInterval(updateWaitingResult, 1_000);
391
392
  waitingInterval.unref?.();
393
+ // Pi hands a Queued Steer over only after this tool call returns, so end the wait for it.
394
+ const steerWatch = watchQueuedSteer(context.sessionManager.getSessionId());
392
395
  try {
393
396
  return await runCoordinatorToolActivity(options, async () => {
394
397
  const result = await options.coordinator.wait(
@@ -397,6 +400,7 @@ export function createCoordinatorToolDefinitions(
397
400
  parameters.timeout_ms,
398
401
  signal,
399
402
  parameters.turn_id,
403
+ steerWatch.signal,
400
404
  );
401
405
  const details = {
402
406
  ...result,
@@ -410,6 +414,7 @@ export function createCoordinatorToolDefinitions(
410
414
  };
411
415
  });
412
416
  } finally {
417
+ steerWatch.dispose();
413
418
  clearInterval(waitingInterval);
414
419
  }
415
420
  },
@@ -119,13 +119,8 @@ export interface WaitDeliveredTurnResult extends Pick<
119
119
  messages?: WaitMessageResult[];
120
120
  }
121
121
 
122
- /** Reports an observational wait timeout with a compact, bounded child progress snapshot. */
123
- export interface WaitTimeoutResult {
124
- event: "timeout";
125
- agent_id: string;
126
- turn_id: string;
127
- /** Requested timeout duration that expired. */
128
- timeout_ms: number;
122
+ /** The compact, bounded child progress snapshot an observational wait ending early reports. */
123
+ export interface WaitProgressSnapshot {
129
124
  state: AgentState;
130
125
  elapsed_ms?: number;
131
126
  latest_activity_at?: string;
@@ -135,12 +130,32 @@ export interface WaitTimeoutResult {
135
130
  recent_activity_labels: string[];
136
131
  }
137
132
 
138
- /** Reports one message, terminal turn, or timeout returned by subagent_wait. */
133
+ /** Reports an observational wait timeout with a compact, bounded child progress snapshot. */
134
+ export interface WaitTimeoutResult extends WaitProgressSnapshot {
135
+ event: "timeout";
136
+ agent_id: string;
137
+ turn_id: string;
138
+ /** Requested timeout duration that expired. */
139
+ timeout_ms: number;
140
+ }
141
+
142
+ /**
143
+ * Reports a Steer Yield: the wait ended early because a Queued Steer is pending for the caller,
144
+ * with the same progress snapshot as a timeout. The child keeps running and nothing is claimed.
145
+ */
146
+ export interface WaitSteeredResult extends WaitProgressSnapshot {
147
+ event: "steered";
148
+ agent_id: string;
149
+ turn_id: string;
150
+ }
151
+
152
+ /** Reports one message, terminal turn, timeout, or steer yield returned by subagent_wait. */
139
153
  export type WaitResult =
140
154
  | WaitMessageResult
141
155
  | WaitTurnResult
142
156
  | WaitDeliveredTurnResult
143
- | WaitTimeoutResult;
157
+ | WaitTimeoutResult
158
+ | WaitSteeredResult;
144
159
 
145
160
  /** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
146
161
  export interface AgentSummary extends RuntimeProfile {