@ian-pascoe/pi-minimal-subagents 0.12.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.
@@ -1,21 +1,10 @@
1
1
  import { stripVTControlCharacters } from "node:util";
2
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
- import { contentText } from "@earendil-works/pi-ai";
4
2
  import {
5
- AssistantMessageComponent,
6
- BashExecutionComponent,
7
- BranchSummaryMessageComponent,
8
- CompactionSummaryMessageComponent,
9
- CustomMessageComponent,
10
- ToolExecutionComponent,
11
- UserMessageComponent,
12
3
  type ExtensionContext,
13
4
  type KeybindingsManager,
14
5
  type Theme,
15
- type TruncationResult,
16
6
  } from "@earendil-works/pi-coding-agent";
17
7
  import {
18
- Container,
19
8
  Text,
20
9
  matchesKey,
21
10
  truncateToWidth,
@@ -28,11 +17,14 @@ import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinato
28
17
  import {
29
18
  formatSubagentDuration,
30
19
  orderActiveAgentSubtrees,
31
- renderMinimalSubagentsMessage,
32
- renderMinimalSubagentsResult,
33
20
  subagentStatusLadder,
34
21
  } from "./minimal-subagents-rendering.js";
35
22
  import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
23
+ import {
24
+ createTranscriptRenderCache,
25
+ renderTranscriptSnapshot,
26
+ type TranscriptLayout,
27
+ } from "./minimal-subagents-transcript.js";
36
28
  import type { SubagentAccessSnapshot } from "./minimal-subagents-access.js";
37
29
  import type {
38
30
  AgentSummary,
@@ -66,7 +58,7 @@ function flattenStatusAgents(status: HierarchyStatusResult): FlattenedStatusAgen
66
58
  const flattened: FlattenedStatusAgent[] = [];
67
59
  const visit = (agent: AgentSummary, depth: number): void => {
68
60
  flattened.push({ agent, depth });
69
- for (const child of agent.children) visit(child, depth + 1);
61
+ for (const child of agent.children ?? []) visit(child, depth + 1);
70
62
  };
71
63
  const roots = "agents" in status ? status.agents : [status.agent];
72
64
  for (const agent of orderActiveAgentSubtrees(roots)) visit(agent, 0);
@@ -90,16 +82,20 @@ function statusAccessSourceLabel(source: SubagentAccessSnapshot["source"]): stri
90
82
  }
91
83
  }
92
84
 
93
- interface CachedTranscriptMessage {
94
- container: Container;
95
- tools: Map<string, ToolExecutionComponent>;
96
- expanded: boolean;
97
- streaming: boolean;
98
- }
99
-
100
- interface TranscriptLayout {
101
- lines: string[];
102
- messageStarts: number[];
85
+ const BACKGROUND_CLEARING_SEQUENCES = ["\u001b[0m", "\u001b[m", "\u001b[49m"] as const;
86
+
87
+ /**
88
+ * Re-open the panel background after each sequence that clears it. Truncation ends its cut text
89
+ * and ellipsis with full resets, and embedded native components may reset or close their own
90
+ * backgrounds; without this the rest of the row (ellipsis, padding, border) loses the panel fill.
91
+ */
92
+ function keepBackgroundThroughResets(text: string, backgroundOpen: string): string {
93
+ if (!backgroundOpen) return text;
94
+ let result = text;
95
+ for (const sequence of BACKGROUND_CLEARING_SEQUENCES) {
96
+ result = result.replaceAll(sequence, `${sequence}${backgroundOpen}`);
97
+ }
98
+ return result;
103
99
  }
104
100
 
105
101
  function transcriptText(line: string): string {
@@ -137,180 +133,6 @@ function anchoredTranscriptOffset(
137
133
  return nextStart + Math.max(0, newLines.length - 1);
138
134
  }
139
135
 
140
- interface TranscriptRenderCache {
141
- messages: WeakMap<AgentMessage, CachedTranscriptMessage>;
142
- results: WeakMap<ToolExecutionComponent, AgentMessage>;
143
- }
144
-
145
- function renderTranscriptSnapshot(
146
- snapshot: ChildAgentTranscriptSnapshot,
147
- tui: TUI,
148
- cwd: string,
149
- expanded: boolean,
150
- width: number,
151
- cache: TranscriptRenderCache,
152
- ): TranscriptLayout {
153
- if (snapshot.messages.length === 0) {
154
- return {
155
- lines: new Text(snapshot.fallback || "No conversation messages yet.", 3, 0).render(width),
156
- messageStarts: [0],
157
- };
158
- }
159
- const blocks: Container[] = [];
160
- const tools = new Map(
161
- snapshot.toolDefinitions.map((definition) => [definition.name, definition]),
162
- );
163
- const pendingTools = new Map<string, ToolExecutionComponent>();
164
- const currentMessages = new Set(snapshot.messages);
165
-
166
- for (const [messageIndex, message] of snapshot.messages.entries()) {
167
- if (message.role === "toolResult") {
168
- const paired = pendingTools.get(message.toolCallId);
169
- if (paired) {
170
- if (cache.results.get(paired) !== message) {
171
- paired.updateResult(message);
172
- cache.results.set(paired, message);
173
- }
174
- pendingTools.delete(message.toolCallId);
175
- blocks.push(new Container());
176
- continue;
177
- }
178
- }
179
- const streaming = messageIndex === snapshot.streamingAssistantIndex;
180
- const cached = cache.messages.get(message);
181
- const staleResult =
182
- cached &&
183
- [...cached.tools.values()].some((tool) => {
184
- const result = cache.results.get(tool);
185
- return result !== undefined && !currentMessages.has(result);
186
- });
187
- if (cached && !staleResult && cached.expanded === expanded && cached.streaming === streaming) {
188
- blocks.push(cached.container);
189
- for (const [id, tool] of cached.tools) pendingTools.set(id, tool);
190
- continue;
191
- }
192
- const container = new Container();
193
- const messageTools = new Map<string, ToolExecutionComponent>();
194
- switch (message.role) {
195
- case "user": {
196
- const text = contentText(message.content, "\n\n");
197
- if (text) container.addChild(new UserMessageComponent(text));
198
- break;
199
- }
200
- case "assistant": {
201
- const assistant = new AssistantMessageComponent(message);
202
- assistant.updateContent(message, messageIndex === snapshot.streamingAssistantIndex);
203
- container.addChild(assistant);
204
- for (const content of message.content) {
205
- if (content.type !== "toolCall") continue;
206
- const tool = new ToolExecutionComponent(
207
- content.name,
208
- content.id,
209
- content.arguments,
210
- { showImages: false },
211
- tools.get(content.name) ?? {},
212
- tui,
213
- cwd,
214
- );
215
- tool.setExpanded(expanded);
216
- container.addChild(tool);
217
- if (message.stopReason === "aborted" || message.stopReason === "error") {
218
- tool.updateResult({
219
- content: [
220
- {
221
- type: "text",
222
- text:
223
- message.stopReason === "aborted"
224
- ? "Operation aborted"
225
- : (message.errorMessage ?? "Error"),
226
- },
227
- ],
228
- isError: true,
229
- });
230
- } else {
231
- pendingTools.set(content.id, tool);
232
- messageTools.set(content.id, tool);
233
- }
234
- }
235
- break;
236
- }
237
- case "toolResult": {
238
- const inherited = new ToolExecutionComponent(
239
- message.toolName,
240
- message.toolCallId,
241
- {},
242
- { showImages: false },
243
- {},
244
- tui,
245
- cwd,
246
- );
247
- inherited.setExpanded(expanded);
248
- inherited.updateResult(message);
249
- container.addChild(inherited);
250
- break;
251
- }
252
- case "custom": {
253
- if (!message.display) break;
254
- const renderer =
255
- message.customType === "minimal-subagents.message"
256
- ? renderMinimalSubagentsMessage
257
- : message.customType === "minimal-subagents.result"
258
- ? renderMinimalSubagentsResult
259
- : undefined;
260
- const custom = new CustomMessageComponent(message, renderer);
261
- custom.setExpanded(expanded);
262
- container.addChild(custom);
263
- break;
264
- }
265
- case "bashExecution": {
266
- const bash = new BashExecutionComponent(message.command, tui, message.excludeFromContext);
267
- if (message.output) bash.appendOutput(message.output);
268
- bash.setComplete(
269
- message.exitCode,
270
- message.cancelled,
271
- // SAFETY: Persisted bash messages retain only the truncation flag; BashExecutionComponent reads that flag here.
272
- message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
273
- message.fullOutputPath,
274
- );
275
- bash.setExpanded(expanded);
276
- container.addChild(bash);
277
- break;
278
- }
279
- case "branchSummary": {
280
- const summary = new BranchSummaryMessageComponent(message);
281
- summary.setExpanded(expanded);
282
- container.addChild(summary);
283
- break;
284
- }
285
- case "compactionSummary": {
286
- const summary = new CompactionSummaryMessageComponent(message);
287
- summary.setExpanded(expanded);
288
- container.addChild(summary);
289
- break;
290
- }
291
- }
292
- cache.messages.set(message, { container, tools: messageTools, expanded, streaming });
293
- blocks.push(container);
294
- }
295
- let length = 0;
296
- const messageStarts: number[] = [];
297
- const lines = blocks.flatMap((block) => {
298
- messageStarts.push(length);
299
- // Native user-message prompt zones belong to the main terminal, not an embedded overlay.
300
- const rendered = block
301
- .render(width)
302
- .map((line) =>
303
- line
304
- .replaceAll("\x1b]133;A\x07", "")
305
- .replaceAll("\x1b]133;B\x07", "")
306
- .replaceAll("\x1b]133;C\x07", ""),
307
- );
308
- length += rendered.length;
309
- return rendered;
310
- });
311
- return { lines, messageStarts };
312
- }
313
-
314
136
  /** Interactive, read-only Child Agent hierarchy and transcript status component. */
315
137
  export class MinimalSubagentsStatusPanelComponent implements Component {
316
138
  private status!: HierarchyStatusResult;
@@ -324,10 +146,7 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
324
146
  private following = true;
325
147
  private transcriptLineCount = 0;
326
148
  private transcriptLayout?: TranscriptLayout;
327
- private readonly transcriptCache: TranscriptRenderCache = {
328
- messages: new WeakMap(),
329
- results: new WeakMap(),
330
- };
149
+ private readonly transcriptCache = createTranscriptRenderCache();
331
150
  private bodyHeight = 1;
332
151
  private ensureSelectionVisible = true;
333
152
  private toolOutputExpanded = false;
@@ -461,8 +280,12 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
461
280
  const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + this.bodyHeight);
462
281
  while (visibleBody.length < this.bodyHeight) visibleBody.push("");
463
282
  const border = (text: string) => this.theme.fg("border", text);
283
+ const backgroundOpen = this.theme.getBgAnsi("customMessageBg");
464
284
  const rows = [...visibleHeader, ...visibleBody, ...help].map((line) => {
465
- const content = truncateToWidth(line, innerWidth, "…");
285
+ const content = keepBackgroundThroughResets(
286
+ truncateToWidth(line, innerWidth, "…"),
287
+ backgroundOpen,
288
+ );
466
289
  return this.theme.bg(
467
290
  "customMessageBg",
468
291
  `${border("│")} ${content}${" ".repeat(innerWidth - visibleWidth(content))} ${border("│")}`,
@@ -2,7 +2,10 @@ import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import { Type } from "typebox";
3
3
  import { THINKING_LEVELS } from "./minimal-subagents-capabilities.js";
4
4
 
5
- const SessionContextSchema = StringEnum(["inherit", "compact", "omit"] as const);
5
+ const SessionContextSchema = StringEnum(["inherit", "compact", "omit"] as const, {
6
+ description:
7
+ "Default omit: the child starts blank, so write a self-contained task. inherit copies your whole conversation (costly; no prompt-cache reuse); compact summarizes it first (adds a summarization call). Opt in only when the child needs the discussion itself.",
8
+ });
6
9
  const ProjectContextSchema = StringEnum(["inherit", "omit"] as const, {
7
10
  description:
8
11
  "Whether to include project-scoped AGENTS.md instructions and skills. Settings, extensions, providers, and tools remain available.",
@@ -38,7 +41,11 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
38
41
  firstModelId === undefined ? Type.Never() : StringEnum([firstModelId, ...remainingModelIds]);
39
42
  return {
40
43
  subagent: Type.Object({
41
- task: Type.String({ minLength: 1, description: "Task for the persistent child agent" }),
44
+ task: Type.String({
45
+ minLength: 1,
46
+ description:
47
+ "Self-contained brief for the persistent child agent: the goal and relevant background, target files and non-goals, the change or question, and observable acceptance criteria. Reference large inputs by file path instead of pasting them.",
48
+ }),
42
49
  agent_id: Type.Optional(
43
50
  Type.String({
44
51
  pattern: FRIENDLY_AGENT_ID_PATTERN,
@@ -69,7 +76,9 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
69
76
  timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
70
77
  }),
71
78
  subagent_status: Type.Object({
72
- agent_id: Type.Optional(canonicalAgentIdSchema("Direct child canonical agent ID")),
79
+ agent_id: Type.Optional(
80
+ canonicalAgentIdSchema("Direct child canonical agent ID; the root may name any descendant"),
81
+ ),
73
82
  }),
74
83
  subagent_cancel: Type.Object({
75
84
  agent_id: canonicalAgentIdSchema("Direct child canonical agent ID"),
@@ -9,6 +9,7 @@ import {
9
9
  type ToolDefinition,
10
10
  type ToolRenderResultOptions,
11
11
  } from "@earendil-works/pi-coding-agent";
12
+ import type { TUI } from "@earendil-works/pi-tui";
12
13
  import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
13
14
  import { withTroubleshootingHint } from "./troubleshooting-skill.js";
14
15
  import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
@@ -16,13 +17,21 @@ import {
16
17
  renderCoordinatorToolCall,
17
18
  renderCoordinatorToolResult,
18
19
  type CoordinatorToolName,
20
+ type LiveTurnRenderer,
19
21
  } from "./minimal-subagents-rendering.js";
22
+ import {
23
+ createTranscriptRenderCache,
24
+ TranscriptRail,
25
+ type TranscriptRenderCache,
26
+ } from "./minimal-subagents-transcript.js";
20
27
  import {
21
28
  CoordinatorToolOutputSchemas,
22
29
  type CoordinatorToolCallInput,
23
30
  } from "./minimal-subagents-render-contract.js";
24
31
  import type { createCoordinatorToolSchemas } from "./minimal-subagents-tool-schemas.js";
25
32
  import type {
33
+ ActiveTurnProgress,
34
+ AgentDetail,
26
35
  AgentMessageResult,
27
36
  CallerSnapshot,
28
37
  CancelResult,
@@ -41,7 +50,15 @@ const ORDINARY_CHILD_COORDINATOR_TOOL_NAMES = new Set([
41
50
  /** Coordinator operations consumed by the six public coordinator tool definitions. */
42
51
  export type CoordinatorToolOperations = Pick<
43
52
  MinimalSubagentsCoordinator,
44
- "spawn" | "inspectStatus" | "sendAgentMessage" | "wait" | "status" | "cancel" | "delete"
53
+ | "spawn"
54
+ | "inspectStatus"
55
+ | "previewActiveTurn"
56
+ | "inspectActiveTurnTranscript"
57
+ | "sendAgentMessage"
58
+ | "wait"
59
+ | "status"
60
+ | "cancel"
61
+ | "delete"
45
62
  >;
46
63
 
47
64
  /** Dependencies and caller policy used to create caller-bound coordinator tools. */
@@ -81,37 +98,84 @@ async function runCoordinatorToolActivity<T>(
81
98
  }
82
99
  }
83
100
 
101
+ /** The compact launch result, plus the full agent detail its transcript renderer displays. */
102
+ type SpawnResultDetails = SpawnResult & { agent?: AgentDetail };
103
+
84
104
  type CoordinatorToolResultDetails =
85
- | SpawnResult
105
+ | SpawnResultDetails
86
106
  | AgentMessageResult
87
107
  | WaitResult
88
108
  | StatusResult
89
109
  | CancelResult
90
110
  | DeleteResult
91
- | { agent_id: string; status: "waiting"; elapsed_ms: number };
111
+ | WaitProgressDetails;
112
+
113
+ /** Partial `subagent_wait` details: elapsed wait time plus the child's running-turn progress. */
114
+ type WaitProgressDetails = {
115
+ agent_id: string;
116
+ status: "waiting";
117
+ elapsed_ms: number;
118
+ } & Partial<ActiveTurnProgress>;
92
119
 
93
120
  type CoordinatorToolDefinition = ToolDefinition & {
94
121
  readonly outputSchema: (typeof CoordinatorToolOutputSchemas)[keyof typeof CoordinatorToolOutputSchemas];
95
122
  };
96
123
 
97
- function createCoordinatorToolRendering(toolName: CoordinatorToolName) {
124
+ /** Native transcript components are re-rendered on each wait update, so their render requests are not needed. */
125
+ const NON_RENDERING_TUI: Pick<TUI, "requestRender"> = { requestRender: () => undefined };
126
+
127
+ /** Pi's per-call render context; the installed Pi does not export its named type. */
128
+ type CoordinatorToolRenderContext = Parameters<NonNullable<ToolDefinition["renderResult"]>>[3];
129
+
130
+ /** Renderer state Pi keeps for one tool row across its partial and final renders. */
131
+ interface CoordinatorToolRowState {
132
+ liveTurnCache?: TranscriptRenderCache;
133
+ }
134
+
135
+ function createCoordinatorToolRendering(
136
+ options: CoordinatorToolDefinitionOptions,
137
+ toolName: CoordinatorToolName,
138
+ ) {
98
139
  return {
99
- renderCall: (args: CoordinatorToolCallInput, theme: Theme) =>
100
- renderCoordinatorToolCall(toolName, args, theme),
140
+ renderCall: (
141
+ args: CoordinatorToolCallInput,
142
+ theme: Theme,
143
+ context: { expanded: boolean } | undefined,
144
+ ) => renderCoordinatorToolCall(toolName, args, theme, context?.expanded ?? false),
101
145
  renderResult: (
102
146
  result: AgentToolResult<CoordinatorToolResultDetails>,
103
147
  renderOptions: ToolRenderResultOptions,
104
148
  theme: Theme,
105
- context: { args: CoordinatorToolCallInput; isError: boolean },
106
- ) =>
107
- renderCoordinatorToolResult(
149
+ context: CoordinatorToolRenderContext,
150
+ ) => {
151
+ const rowState: CoordinatorToolRowState = context.state;
152
+ const renderLiveTurn: LiveTurnRenderer = (agentId, turnId, expanded) => {
153
+ const snapshot = options.coordinator.inspectActiveTurnTranscript(
154
+ options.callerId,
155
+ agentId,
156
+ turnId,
157
+ );
158
+ if (!snapshot || snapshot.messages.length === 0) return undefined;
159
+ return new TranscriptRail(
160
+ snapshot,
161
+ // SAFETY: Native transcript components only call requestRender on the TUI they receive.
162
+ NON_RENDERING_TUI as TUI,
163
+ context.cwd,
164
+ expanded,
165
+ (rowState.liveTurnCache ??= createTranscriptRenderCache()),
166
+ theme,
167
+ );
168
+ };
169
+ return renderCoordinatorToolResult(
108
170
  toolName,
109
171
  result,
110
172
  renderOptions,
111
173
  theme,
112
174
  context.args,
113
175
  context.isError,
114
- ),
176
+ toolName === "subagent_wait" ? renderLiveTurn : undefined,
177
+ );
178
+ },
115
179
  };
116
180
  }
117
181
 
@@ -178,10 +242,12 @@ export function createCoordinatorToolDefinitions(
178
242
  name: "subagent",
179
243
  label: "Subagent",
180
244
  description:
181
- "Create a persistent nested agent asynchronously. Returns its canonical agent ID and active turn ID immediately. Root-child IDs omit the root prefix; nested IDs retain the parent path.",
245
+ "Create a persistent nested agent asynchronously. Returns its canonical agent ID, active turn ID, and resolved model and tools immediately. Root-child IDs omit the root prefix; nested IDs retain the parent path. Children start without your conversation unless session_context opts in, so the task must be self-contained. Its final response is delivered automatically unless you claim it with subagent_wait. To continue an idle child later, send it agent_message; that starts a new turn you can wait on.",
182
246
  promptSnippet: "Spawn a persistent child with a prefix-free root-child ID",
183
247
  promptGuidelines: modelRolePromptGuidelines,
184
248
  parameters: options.schemas.subagent,
249
+ // A spawn must finish registering before a wait or message in the same batch can target it.
250
+ executionMode: "sequential",
185
251
  annotations: {
186
252
  readOnlyHint: false,
187
253
  destructiveHint: true,
@@ -195,24 +261,22 @@ export function createCoordinatorToolDefinitions(
195
261
  parameters,
196
262
  options.captureCaller(context),
197
263
  );
264
+ // The model and codemode receive the compact launch result; the transcript renderer
265
+ // additionally receives the full detail it displays.
198
266
  const status = options.coordinator.inspectStatus(result.agent_id);
199
- const details: SpawnResult & {
200
- agent?: import("./minimal-subagents-types.js").AgentDetail;
201
- } = {
202
- ...result,
203
- };
204
- if (status && "agent" in status) details.agent = status.agent;
205
- return structuredToolResult(details);
267
+ const details: SpawnResultDetails = { ...result };
268
+ if ("agent" in status) details.agent = status.agent;
269
+ return { ...structuredToolResult(result), details };
206
270
  });
207
271
  },
208
- ...createCoordinatorToolRendering("subagent"),
272
+ ...createCoordinatorToolRendering(options, "subagent"),
209
273
  });
210
274
 
211
275
  const messageTool = defineTool({
212
276
  name: "agent_message",
213
277
  label: "Agent Message",
214
278
  description:
215
- "Send one mid-turn coordination message to a direct parent, direct sibling, or direct child. The result says whether it was delivered through an active wait, queued for the recipient, or failed.",
279
+ "Send one coordination message to a direct parent, direct sibling, or direct child. The result says whether it was delivered through an active wait, queued into the recipient's active turn, started a new turn on an idle child (with its turn_id), or failed.",
216
280
  promptSnippet: "Coordinate required mid-turn action with one adjacent agent",
217
281
  parameters: options.schemas.agent_message,
218
282
  annotations: {
@@ -239,14 +303,14 @@ export function createCoordinatorToolDefinitions(
239
303
  : structuredToolResult(result);
240
304
  });
241
305
  },
242
- ...createCoordinatorToolRendering("agent_message"),
306
+ ...createCoordinatorToolRendering(options, "agent_message"),
243
307
  });
244
308
 
245
309
  const waitTool = defineTool({
246
310
  name: "subagent_wait",
247
311
  label: "Subagent Wait",
248
312
  description:
249
- "Wait for one direct child's oldest observable turn, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. An already settled turn returns event=turn once with queued messages in messages. Timeout returns event=timeout with detailed child status and never cancels the child.",
313
+ "Wait for one direct child's oldest observable turn, 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.",
250
314
  promptSnippet: "Wait for one direct child's exact turn",
251
315
  parameters: options.schemas.subagent_wait,
252
316
  annotations: {
@@ -257,15 +321,22 @@ export function createCoordinatorToolDefinitions(
257
321
  },
258
322
  async execute(_toolCallId, parameters, signal, onUpdate) {
259
323
  const startedAt = Date.now();
260
- const updateWaitingResult = () =>
324
+ const updateWaitingResult = () => {
325
+ const details: WaitProgressDetails = {
326
+ agent_id: parameters.agent_id,
327
+ status: "waiting",
328
+ elapsed_ms: Date.now() - startedAt,
329
+ ...options.coordinator.previewActiveTurn(
330
+ options.callerId,
331
+ parameters.agent_id,
332
+ parameters.turn_id,
333
+ ),
334
+ };
261
335
  onUpdate?.({
262
336
  content: [{ type: "text", text: `Waiting for ${parameters.agent_id}` }],
263
- details: {
264
- agent_id: parameters.agent_id,
265
- status: "waiting",
266
- elapsed_ms: Date.now() - startedAt,
267
- },
337
+ details,
268
338
  });
339
+ };
269
340
  updateWaitingResult();
270
341
  const waitingInterval = setInterval(updateWaitingResult, 1_000);
271
342
  waitingInterval.unref?.();
@@ -293,14 +364,14 @@ export function createCoordinatorToolDefinitions(
293
364
  clearInterval(waitingInterval);
294
365
  }
295
366
  },
296
- ...createCoordinatorToolRendering("subagent_wait"),
367
+ ...createCoordinatorToolRendering(options, "subagent_wait"),
297
368
  });
298
369
 
299
370
  const statusTool = defineTool({
300
371
  name: "subagent_status",
301
372
  label: "Subagent Status",
302
373
  description:
303
- "List direct children when agent_id is omitted, or inspect one direct child's launch contract, result, usage, dependencies, and bounded recent activity including message text and reasoning.",
374
+ "List direct children when agent_id is omitted, or inspect one direct child's launch contract, result, usage, dependencies, and bounded recent activity including message text and reasoning. The root may inspect any descendant.",
304
375
  promptSnippet: "Inspect direct child state",
305
376
  parameters: options.schemas.subagent_status,
306
377
  annotations: {
@@ -314,7 +385,7 @@ export function createCoordinatorToolDefinitions(
314
385
  structuredToolResult(options.coordinator.status(options.callerId, parameters.agent_id)),
315
386
  );
316
387
  },
317
- ...createCoordinatorToolRendering("subagent_status"),
388
+ ...createCoordinatorToolRendering(options, "subagent_status"),
318
389
  });
319
390
 
320
391
  const cancelTool = defineTool({
@@ -341,7 +412,7 @@ export function createCoordinatorToolDefinitions(
341
412
  ),
342
413
  );
343
414
  },
344
- ...createCoordinatorToolRendering("subagent_cancel"),
415
+ ...createCoordinatorToolRendering(options, "subagent_cancel"),
345
416
  });
346
417
 
347
418
  const deleteTool = defineTool({
@@ -375,7 +446,7 @@ export function createCoordinatorToolDefinitions(
375
446
  return structuredToolResult(result);
376
447
  });
377
448
  },
378
- ...createCoordinatorToolRendering("subagent_delete"),
449
+ ...createCoordinatorToolRendering(options, "subagent_delete"),
379
450
  });
380
451
 
381
452
  const coordinatorTools: CoordinatorToolDefinition[] = [