@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.
- package/README.md +115 -26
- package/package.json +2 -2
- package/src/minimal-subagents-capabilities.ts +12 -1
- package/src/minimal-subagents-config.ts +11 -6
- package/src/minimal-subagents-context.ts +180 -4
- package/src/minimal-subagents-coordinator.ts +303 -32
- package/src/minimal-subagents-delivery-ledger.ts +14 -13
- package/src/minimal-subagents-extension.ts +62 -2
- package/src/minimal-subagents-registry-wire.ts +1 -0
- package/src/minimal-subagents-registry.ts +6 -1
- package/src/minimal-subagents-render-contract.ts +33 -2
- package/src/minimal-subagents-rendering.ts +321 -52
- package/src/minimal-subagents-sessions.ts +3 -1
- package/src/minimal-subagents-status-panel.ts +26 -203
- package/src/minimal-subagents-tool-schemas.ts +20 -3
- package/src/minimal-subagents-tools.ts +109 -35
- package/src/minimal-subagents-transcript.ts +296 -0
- package/src/minimal-subagents-types.ts +28 -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.",
|
|
@@ -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({
|
|
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(
|
|
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
|
-
|
|
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
|
-
[
|
|
69
|
-
|
|
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
|
-
|
|
|
108
|
+
| SpawnResultDetails
|
|
86
109
|
| AgentMessageResult
|
|
87
110
|
| WaitResult
|
|
88
111
|
| StatusResult
|
|
89
112
|
| CancelResult
|
|
90
113
|
| DeleteResult
|
|
91
|
-
|
|
|
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
|
-
|
|
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: (
|
|
100
|
-
|
|
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:
|
|
106
|
-
) =>
|
|
107
|
-
|
|
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
|
|
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:
|
|
200
|
-
|
|
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
|
|
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.
|
|
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[] = [
|