@ian-pascoe/pi-minimal-subagents 0.12.0 → 0.14.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.",
@@ -23,6 +26,13 @@ const ToolSelectionSchema = Type.Union(
23
26
  'Configurable presets: "read" adds readToolset; "modify" adds readToolset plus modifyToolset. Defaults: read, grep, find, ls; modify additionally grants bash, edit, write. Configured baseToolset applies to all selections, including "none" and exact arrays. Omit to inherit the caller\'s active ordinary tools plus base. All grants stay within the caller\'s capability ceiling; coordinator tools are injected separately.',
24
27
  },
25
28
  );
29
+ // Deliberately a plain string rather than an enum of the configured roles: settings can change, and
30
+ // the tool definition must stay byte-identical for the session so provider prompt caches survive.
31
+ const ModelRoleSchema = Type.String({
32
+ minLength: 1,
33
+ description:
34
+ "Name of a configured model role (minimalSubagents.modelRoles) listed in the Subagent tool guidelines. Resolves to the role's model and, when the role has one, its thinking level. An explicit model or thinking_level overrides the role's value. An unknown role is rejected with the configured role names.",
35
+ });
26
36
  const FRIENDLY_AGENT_ID_PATTERN = "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$";
27
37
  const CANONICAL_AGENT_ID_PATTERN =
28
38
  "^(?:root\\.)?[A-Za-z0-9][A-Za-z0-9_-]{0,63}(?:\\.[A-Za-z0-9][A-Za-z0-9_-]{0,63})*$";
@@ -38,7 +48,11 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
38
48
  firstModelId === undefined ? Type.Never() : StringEnum([firstModelId, ...remainingModelIds]);
39
49
  return {
40
50
  subagent: Type.Object({
41
- task: Type.String({ minLength: 1, description: "Task for the persistent child agent" }),
51
+ task: Type.String({
52
+ minLength: 1,
53
+ description:
54
+ "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.",
55
+ }),
42
56
  agent_id: Type.Optional(
43
57
  Type.String({
44
58
  pattern: FRIENDLY_AGENT_ID_PATTERN,
@@ -48,6 +62,7 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
48
62
  ),
49
63
  session_context: Type.Optional(SessionContextSchema),
50
64
  project_context: Type.Optional(ProjectContextSchema),
65
+ role: Type.Optional(ModelRoleSchema),
51
66
  model: Type.Optional(explicitModelSchema),
52
67
  thinking_level: Type.Optional(ThinkingLevelSchema),
53
68
  tools: Type.Optional(ToolSelectionSchema),
@@ -69,7 +84,9 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
69
84
  timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
70
85
  }),
71
86
  subagent_status: Type.Object({
72
- agent_id: Type.Optional(canonicalAgentIdSchema("Direct child canonical agent ID")),
87
+ agent_id: Type.Optional(
88
+ canonicalAgentIdSchema("Direct child canonical agent ID; the root may name any descendant"),
89
+ ),
73
90
  }),
74
91
  subagent_cancel: Type.Object({
75
92
  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. */
@@ -65,8 +82,11 @@ function buildModelRolePromptGuidelines(
65
82
  return ` - ${role.name} → model=${role.model}${thinkingGuidance}${role.hint ? ` — ${role.hint}` : ""}`;
66
83
  });
67
84
  return [
68
- ["Configured model roles are guidance, not constraints:", ...roleLines].join("\n"),
69
- "Choose a model based on the task. A listed thinking_level is a preference, not a constraint. Callers choose thinking_level independently for roles without one.",
85
+ [
86
+ "Configured model roles are guidance, not constraints. Pass role to launch with a role's model and thinking_level; an explicit model or thinking_level overrides the role's value:",
87
+ ...roleLines,
88
+ ].join("\n"),
89
+ "Choose a role or model based on the task. A listed thinking_level is a preference, not a constraint. Callers choose thinking_level independently for roles without one.",
70
90
  ];
71
91
  }
72
92
 
@@ -81,37 +101,84 @@ async function runCoordinatorToolActivity<T>(
81
101
  }
82
102
  }
83
103
 
104
+ /** The compact launch result, plus the full agent detail its transcript renderer displays. */
105
+ type SpawnResultDetails = SpawnResult & { agent?: AgentDetail };
106
+
84
107
  type CoordinatorToolResultDetails =
85
- | SpawnResult
108
+ | SpawnResultDetails
86
109
  | AgentMessageResult
87
110
  | WaitResult
88
111
  | StatusResult
89
112
  | CancelResult
90
113
  | DeleteResult
91
- | { agent_id: string; status: "waiting"; elapsed_ms: number };
114
+ | WaitProgressDetails;
115
+
116
+ /** Partial `subagent_wait` details: elapsed wait time plus the child's running-turn progress. */
117
+ type WaitProgressDetails = {
118
+ agent_id: string;
119
+ status: "waiting";
120
+ elapsed_ms: number;
121
+ } & Partial<ActiveTurnProgress>;
92
122
 
93
123
  type CoordinatorToolDefinition = ToolDefinition & {
94
124
  readonly outputSchema: (typeof CoordinatorToolOutputSchemas)[keyof typeof CoordinatorToolOutputSchemas];
95
125
  };
96
126
 
97
- function createCoordinatorToolRendering(toolName: CoordinatorToolName) {
127
+ /** Native transcript components are re-rendered on each wait update, so their render requests are not needed. */
128
+ const NON_RENDERING_TUI: Pick<TUI, "requestRender"> = { requestRender: () => undefined };
129
+
130
+ /** Pi's per-call render context; the installed Pi does not export its named type. */
131
+ type CoordinatorToolRenderContext = Parameters<NonNullable<ToolDefinition["renderResult"]>>[3];
132
+
133
+ /** Renderer state Pi keeps for one tool row across its partial and final renders. */
134
+ interface CoordinatorToolRowState {
135
+ liveTurnCache?: TranscriptRenderCache;
136
+ }
137
+
138
+ function createCoordinatorToolRendering(
139
+ options: CoordinatorToolDefinitionOptions,
140
+ toolName: CoordinatorToolName,
141
+ ) {
98
142
  return {
99
- renderCall: (args: CoordinatorToolCallInput, theme: Theme) =>
100
- renderCoordinatorToolCall(toolName, args, theme),
143
+ renderCall: (
144
+ args: CoordinatorToolCallInput,
145
+ theme: Theme,
146
+ context: { expanded: boolean } | undefined,
147
+ ) => renderCoordinatorToolCall(toolName, args, theme, context?.expanded ?? false),
101
148
  renderResult: (
102
149
  result: AgentToolResult<CoordinatorToolResultDetails>,
103
150
  renderOptions: ToolRenderResultOptions,
104
151
  theme: Theme,
105
- context: { args: CoordinatorToolCallInput; isError: boolean },
106
- ) =>
107
- renderCoordinatorToolResult(
152
+ context: CoordinatorToolRenderContext,
153
+ ) => {
154
+ const rowState: CoordinatorToolRowState = context.state;
155
+ const renderLiveTurn: LiveTurnRenderer = (agentId, turnId, expanded) => {
156
+ const snapshot = options.coordinator.inspectActiveTurnTranscript(
157
+ options.callerId,
158
+ agentId,
159
+ turnId,
160
+ );
161
+ if (!snapshot || snapshot.messages.length === 0) return undefined;
162
+ return new TranscriptRail(
163
+ snapshot,
164
+ // SAFETY: Native transcript components only call requestRender on the TUI they receive.
165
+ NON_RENDERING_TUI as TUI,
166
+ context.cwd,
167
+ expanded,
168
+ (rowState.liveTurnCache ??= createTranscriptRenderCache()),
169
+ theme,
170
+ );
171
+ };
172
+ return renderCoordinatorToolResult(
108
173
  toolName,
109
174
  result,
110
175
  renderOptions,
111
176
  theme,
112
177
  context.args,
113
178
  context.isError,
114
- ),
179
+ toolName === "subagent_wait" ? renderLiveTurn : undefined,
180
+ );
181
+ },
115
182
  };
116
183
  }
117
184
 
@@ -178,10 +245,12 @@ export function createCoordinatorToolDefinitions(
178
245
  name: "subagent",
179
246
  label: "Subagent",
180
247
  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.",
248
+ "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
249
  promptSnippet: "Spawn a persistent child with a prefix-free root-child ID",
183
250
  promptGuidelines: modelRolePromptGuidelines,
184
251
  parameters: options.schemas.subagent,
252
+ // A spawn must finish registering before a wait or message in the same batch can target it.
253
+ executionMode: "sequential",
185
254
  annotations: {
186
255
  readOnlyHint: false,
187
256
  destructiveHint: true,
@@ -195,24 +264,22 @@ export function createCoordinatorToolDefinitions(
195
264
  parameters,
196
265
  options.captureCaller(context),
197
266
  );
267
+ // The model and codemode receive the compact launch result; the transcript renderer
268
+ // additionally receives the full detail it displays.
198
269
  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);
270
+ const details: SpawnResultDetails = { ...result };
271
+ if ("agent" in status) details.agent = status.agent;
272
+ return { ...structuredToolResult(result), details };
206
273
  });
207
274
  },
208
- ...createCoordinatorToolRendering("subagent"),
275
+ ...createCoordinatorToolRendering(options, "subagent"),
209
276
  });
210
277
 
211
278
  const messageTool = defineTool({
212
279
  name: "agent_message",
213
280
  label: "Agent Message",
214
281
  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.",
282
+ "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
283
  promptSnippet: "Coordinate required mid-turn action with one adjacent agent",
217
284
  parameters: options.schemas.agent_message,
218
285
  annotations: {
@@ -239,14 +306,14 @@ export function createCoordinatorToolDefinitions(
239
306
  : structuredToolResult(result);
240
307
  });
241
308
  },
242
- ...createCoordinatorToolRendering("agent_message"),
309
+ ...createCoordinatorToolRendering(options, "agent_message"),
243
310
  });
244
311
 
245
312
  const waitTool = defineTool({
246
313
  name: "subagent_wait",
247
314
  label: "Subagent Wait",
248
315
  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.",
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.",
250
317
  promptSnippet: "Wait for one direct child's exact turn",
251
318
  parameters: options.schemas.subagent_wait,
252
319
  annotations: {
@@ -257,15 +324,22 @@ export function createCoordinatorToolDefinitions(
257
324
  },
258
325
  async execute(_toolCallId, parameters, signal, onUpdate) {
259
326
  const startedAt = Date.now();
260
- const updateWaitingResult = () =>
327
+ const updateWaitingResult = () => {
328
+ const details: WaitProgressDetails = {
329
+ agent_id: parameters.agent_id,
330
+ status: "waiting",
331
+ elapsed_ms: Date.now() - startedAt,
332
+ ...options.coordinator.previewActiveTurn(
333
+ options.callerId,
334
+ parameters.agent_id,
335
+ parameters.turn_id,
336
+ ),
337
+ };
261
338
  onUpdate?.({
262
339
  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
- },
340
+ details,
268
341
  });
342
+ };
269
343
  updateWaitingResult();
270
344
  const waitingInterval = setInterval(updateWaitingResult, 1_000);
271
345
  waitingInterval.unref?.();
@@ -293,14 +367,14 @@ export function createCoordinatorToolDefinitions(
293
367
  clearInterval(waitingInterval);
294
368
  }
295
369
  },
296
- ...createCoordinatorToolRendering("subagent_wait"),
370
+ ...createCoordinatorToolRendering(options, "subagent_wait"),
297
371
  });
298
372
 
299
373
  const statusTool = defineTool({
300
374
  name: "subagent_status",
301
375
  label: "Subagent Status",
302
376
  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.",
377
+ "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
378
  promptSnippet: "Inspect direct child state",
305
379
  parameters: options.schemas.subagent_status,
306
380
  annotations: {
@@ -314,7 +388,7 @@ export function createCoordinatorToolDefinitions(
314
388
  structuredToolResult(options.coordinator.status(options.callerId, parameters.agent_id)),
315
389
  );
316
390
  },
317
- ...createCoordinatorToolRendering("subagent_status"),
391
+ ...createCoordinatorToolRendering(options, "subagent_status"),
318
392
  });
319
393
 
320
394
  const cancelTool = defineTool({
@@ -341,7 +415,7 @@ export function createCoordinatorToolDefinitions(
341
415
  ),
342
416
  );
343
417
  },
344
- ...createCoordinatorToolRendering("subagent_cancel"),
418
+ ...createCoordinatorToolRendering(options, "subagent_cancel"),
345
419
  });
346
420
 
347
421
  const deleteTool = defineTool({
@@ -375,7 +449,7 @@ export function createCoordinatorToolDefinitions(
375
449
  return structuredToolResult(result);
376
450
  });
377
451
  },
378
- ...createCoordinatorToolRendering("subagent_delete"),
452
+ ...createCoordinatorToolRendering(options, "subagent_delete"),
379
453
  });
380
454
 
381
455
  const coordinatorTools: CoordinatorToolDefinition[] = [