@ian-pascoe/pi-minimal-subagents 0.11.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 +70 -8
- package/package.json +6 -6
- package/src/minimal-subagents-capabilities.ts +12 -1
- package/src/minimal-subagents-child-resources.ts +14 -17
- 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 +174 -42
- 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"),
|