@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.
- package/README.md +58 -4
- package/package.json +1 -1
- package/src/minimal-subagents-capabilities.ts +12 -1
- package/src/minimal-subagents-context.ts +180 -4
- package/src/minimal-subagents-coordinator.ts +115 -24
- package/src/minimal-subagents-extension.ts +3 -0
- package/src/minimal-subagents-registry.ts +3 -1
- package/src/minimal-subagents-render-contract.ts +30 -2
- package/src/minimal-subagents-rendering.ts +318 -52
- package/src/minimal-subagents-status-panel.ts +26 -203
- package/src/minimal-subagents-tool-schemas.ts +12 -3
- package/src/minimal-subagents-tools.ts +104 -33
- package/src/minimal-subagents-transcript.ts +296 -0
- package/src/minimal-subagents-types.ts +22 -5
- package/src/minimal-subagents-ui.ts +1 -1
- package/src/minimal-subagents-usage.ts +13 -2
|
@@ -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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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 =
|
|
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({
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
|
105
|
+
| SpawnResultDetails
|
|
86
106
|
| AgentMessageResult
|
|
87
107
|
| WaitResult
|
|
88
108
|
| StatusResult
|
|
89
109
|
| CancelResult
|
|
90
110
|
| DeleteResult
|
|
91
|
-
|
|
|
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
|
-
|
|
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: (
|
|
100
|
-
|
|
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:
|
|
106
|
-
) =>
|
|
107
|
-
|
|
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
|
|
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:
|
|
200
|
-
|
|
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
|
|
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.
|
|
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[] = [
|