@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
|
@@ -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
|
|
|
@@ -130,13 +194,31 @@ function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
|
130
194
|
};
|
|
131
195
|
}
|
|
132
196
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Report a failed operation that still carries declared output: the model sees an error and
|
|
199
|
+
* codemode scripts receive `structuredContent` instead of a data-less rejection.
|
|
200
|
+
*/
|
|
201
|
+
function failedStructuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
202
|
+
prefix: string,
|
|
203
|
+
result: TDetails,
|
|
204
|
+
options: { troubleshootingHint: boolean },
|
|
205
|
+
): AgentToolResult<TDetails> & { isError: true } {
|
|
206
|
+
const success = structuredToolResult(result);
|
|
207
|
+
const text = `${prefix}:\n${textOf(success)}`;
|
|
208
|
+
return {
|
|
209
|
+
...success,
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: "text" as const,
|
|
213
|
+
text: options.troubleshootingHint ? withTroubleshootingHint(text) : text,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
isError: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function textOf(result: AgentToolResult<unknown>): string {
|
|
221
|
+
return result.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
|
|
140
222
|
}
|
|
141
223
|
|
|
142
224
|
function callerSourceTurnId(
|
|
@@ -160,10 +242,18 @@ export function createCoordinatorToolDefinitions(
|
|
|
160
242
|
name: "subagent",
|
|
161
243
|
label: "Subagent",
|
|
162
244
|
description:
|
|
163
|
-
"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.",
|
|
164
246
|
promptSnippet: "Spawn a persistent child with a prefix-free root-child ID",
|
|
165
247
|
promptGuidelines: modelRolePromptGuidelines,
|
|
166
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",
|
|
251
|
+
annotations: {
|
|
252
|
+
readOnlyHint: false,
|
|
253
|
+
destructiveHint: true,
|
|
254
|
+
idempotentHint: false,
|
|
255
|
+
openWorldHint: true,
|
|
256
|
+
},
|
|
167
257
|
async execute(_toolCallId, parameters, _signal, _onUpdate, context) {
|
|
168
258
|
return runCoordinatorToolActivity(options, async () => {
|
|
169
259
|
const result = await options.coordinator.spawn(
|
|
@@ -171,26 +261,30 @@ export function createCoordinatorToolDefinitions(
|
|
|
171
261
|
parameters,
|
|
172
262
|
options.captureCaller(context),
|
|
173
263
|
);
|
|
264
|
+
// The model and codemode receive the compact launch result; the transcript renderer
|
|
265
|
+
// additionally receives the full detail it displays.
|
|
174
266
|
const status = options.coordinator.inspectStatus(result.agent_id);
|
|
175
|
-
const details:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
...result,
|
|
179
|
-
};
|
|
180
|
-
if (status && "agent" in status) details.agent = status.agent;
|
|
181
|
-
return structuredToolResult(details);
|
|
267
|
+
const details: SpawnResultDetails = { ...result };
|
|
268
|
+
if ("agent" in status) details.agent = status.agent;
|
|
269
|
+
return { ...structuredToolResult(result), details };
|
|
182
270
|
});
|
|
183
271
|
},
|
|
184
|
-
...createCoordinatorToolRendering("subagent"),
|
|
272
|
+
...createCoordinatorToolRendering(options, "subagent"),
|
|
185
273
|
});
|
|
186
274
|
|
|
187
275
|
const messageTool = defineTool({
|
|
188
276
|
name: "agent_message",
|
|
189
277
|
label: "Agent Message",
|
|
190
278
|
description:
|
|
191
|
-
"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.",
|
|
192
280
|
promptSnippet: "Coordinate required mid-turn action with one adjacent agent",
|
|
193
281
|
parameters: options.schemas.agent_message,
|
|
282
|
+
annotations: {
|
|
283
|
+
readOnlyHint: false,
|
|
284
|
+
destructiveHint: false,
|
|
285
|
+
idempotentHint: false,
|
|
286
|
+
openWorldHint: false,
|
|
287
|
+
},
|
|
194
288
|
async execute(toolCallId, parameters) {
|
|
195
289
|
return runCoordinatorToolActivity(options, async () => {
|
|
196
290
|
const result = await options.coordinator.sendAgentMessage(
|
|
@@ -201,30 +295,48 @@ export function createCoordinatorToolDefinitions(
|
|
|
201
295
|
},
|
|
202
296
|
callerSourceTurnId(options.coordinator, options.callerId, toolCallId),
|
|
203
297
|
);
|
|
204
|
-
return
|
|
298
|
+
return result.disposition === "failed"
|
|
299
|
+
? failedStructuredToolResult("Minimal subagents message delivery failed", result, {
|
|
300
|
+
// The coordinator already appends the hint to `error`.
|
|
301
|
+
troubleshootingHint: false,
|
|
302
|
+
})
|
|
303
|
+
: structuredToolResult(result);
|
|
205
304
|
});
|
|
206
305
|
},
|
|
207
|
-
...createCoordinatorToolRendering("agent_message"),
|
|
306
|
+
...createCoordinatorToolRendering(options, "agent_message"),
|
|
208
307
|
});
|
|
209
308
|
|
|
210
309
|
const waitTool = defineTool({
|
|
211
310
|
name: "subagent_wait",
|
|
212
311
|
label: "Subagent Wait",
|
|
213
312
|
description:
|
|
214
|
-
"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.",
|
|
215
314
|
promptSnippet: "Wait for one direct child's exact turn",
|
|
216
315
|
parameters: options.schemas.subagent_wait,
|
|
316
|
+
annotations: {
|
|
317
|
+
readOnlyHint: true,
|
|
318
|
+
destructiveHint: false,
|
|
319
|
+
idempotentHint: false,
|
|
320
|
+
openWorldHint: false,
|
|
321
|
+
},
|
|
217
322
|
async execute(_toolCallId, parameters, signal, onUpdate) {
|
|
218
323
|
const startedAt = Date.now();
|
|
219
|
-
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
|
+
};
|
|
220
335
|
onUpdate?.({
|
|
221
336
|
content: [{ type: "text", text: `Waiting for ${parameters.agent_id}` }],
|
|
222
|
-
details
|
|
223
|
-
agent_id: parameters.agent_id,
|
|
224
|
-
status: "waiting",
|
|
225
|
-
elapsed_ms: Date.now() - startedAt,
|
|
226
|
-
},
|
|
337
|
+
details,
|
|
227
338
|
});
|
|
339
|
+
};
|
|
228
340
|
updateWaitingResult();
|
|
229
341
|
const waitingInterval = setInterval(updateWaitingResult, 1_000);
|
|
230
342
|
waitingInterval.unref?.();
|
|
@@ -252,22 +364,28 @@ export function createCoordinatorToolDefinitions(
|
|
|
252
364
|
clearInterval(waitingInterval);
|
|
253
365
|
}
|
|
254
366
|
},
|
|
255
|
-
...createCoordinatorToolRendering("subagent_wait"),
|
|
367
|
+
...createCoordinatorToolRendering(options, "subagent_wait"),
|
|
256
368
|
});
|
|
257
369
|
|
|
258
370
|
const statusTool = defineTool({
|
|
259
371
|
name: "subagent_status",
|
|
260
372
|
label: "Subagent Status",
|
|
261
373
|
description:
|
|
262
|
-
"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.",
|
|
263
375
|
promptSnippet: "Inspect direct child state",
|
|
264
376
|
parameters: options.schemas.subagent_status,
|
|
377
|
+
annotations: {
|
|
378
|
+
readOnlyHint: true,
|
|
379
|
+
destructiveHint: false,
|
|
380
|
+
idempotentHint: true,
|
|
381
|
+
openWorldHint: false,
|
|
382
|
+
},
|
|
265
383
|
async execute(_toolCallId, parameters) {
|
|
266
384
|
return runCoordinatorToolActivity(options, () =>
|
|
267
385
|
structuredToolResult(options.coordinator.status(options.callerId, parameters.agent_id)),
|
|
268
386
|
);
|
|
269
387
|
},
|
|
270
|
-
...createCoordinatorToolRendering("subagent_status"),
|
|
388
|
+
...createCoordinatorToolRendering(options, "subagent_status"),
|
|
271
389
|
});
|
|
272
390
|
|
|
273
391
|
const cancelTool = defineTool({
|
|
@@ -277,6 +395,12 @@ export function createCoordinatorToolDefinitions(
|
|
|
277
395
|
"Abort active work for one direct child while preserving sessions for later continuation. Recursive cancellation includes its subtree and defaults to true.",
|
|
278
396
|
promptSnippet: "Cancel active subagent turns without deleting sessions",
|
|
279
397
|
parameters: options.schemas.subagent_cancel,
|
|
398
|
+
annotations: {
|
|
399
|
+
readOnlyHint: false,
|
|
400
|
+
destructiveHint: false,
|
|
401
|
+
idempotentHint: true,
|
|
402
|
+
openWorldHint: false,
|
|
403
|
+
},
|
|
280
404
|
async execute(_toolCallId, parameters) {
|
|
281
405
|
return runCoordinatorToolActivity(options, async () =>
|
|
282
406
|
structuredToolResult(
|
|
@@ -288,7 +412,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
288
412
|
),
|
|
289
413
|
);
|
|
290
414
|
},
|
|
291
|
-
...createCoordinatorToolRendering("subagent_cancel"),
|
|
415
|
+
...createCoordinatorToolRendering(options, "subagent_cancel"),
|
|
292
416
|
});
|
|
293
417
|
|
|
294
418
|
const deleteTool = defineTool({
|
|
@@ -298,6 +422,12 @@ export function createCoordinatorToolDefinitions(
|
|
|
298
422
|
"Delete one direct child's persistent session and retain durable ID tombstones. Recursive deletion includes its subtree and defaults to true.",
|
|
299
423
|
promptSnippet: "Delete subagent sessions and tombstone their IDs",
|
|
300
424
|
parameters: options.schemas.subagent_delete,
|
|
425
|
+
annotations: {
|
|
426
|
+
readOnlyHint: false,
|
|
427
|
+
destructiveHint: true,
|
|
428
|
+
idempotentHint: true,
|
|
429
|
+
openWorldHint: false,
|
|
430
|
+
},
|
|
301
431
|
async execute(_toolCallId, parameters) {
|
|
302
432
|
return runCoordinatorToolActivity(options, async () => {
|
|
303
433
|
const result = await options.coordinator.delete(
|
|
@@ -309,12 +439,14 @@ export function createCoordinatorToolDefinitions(
|
|
|
309
439
|
options.onAttention?.(
|
|
310
440
|
`Minimal subagents deletion partially failed for ${parameters.agent_id}`,
|
|
311
441
|
);
|
|
312
|
-
|
|
442
|
+
return failedStructuredToolResult("Minimal subagents deletion partially failed", result, {
|
|
443
|
+
troubleshootingHint: true,
|
|
444
|
+
});
|
|
313
445
|
}
|
|
314
446
|
return structuredToolResult(result);
|
|
315
447
|
});
|
|
316
448
|
},
|
|
317
|
-
...createCoordinatorToolRendering("subagent_delete"),
|
|
449
|
+
...createCoordinatorToolRendering(options, "subagent_delete"),
|
|
318
450
|
});
|
|
319
451
|
|
|
320
452
|
const coordinatorTools: CoordinatorToolDefinition[] = [
|
|
@@ -0,0 +1,296 @@
|
|
|
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
|
+
import {
|
|
5
|
+
AssistantMessageComponent,
|
|
6
|
+
BashExecutionComponent,
|
|
7
|
+
BranchSummaryMessageComponent,
|
|
8
|
+
CompactionSummaryMessageComponent,
|
|
9
|
+
CustomMessageComponent,
|
|
10
|
+
ToolExecutionComponent,
|
|
11
|
+
UserMessageComponent,
|
|
12
|
+
type TruncationResult,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { Container, Text, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
15
|
+
import {
|
|
16
|
+
ACTIVITY_RAIL_WIDTH,
|
|
17
|
+
drawActivityRail,
|
|
18
|
+
renderMinimalSubagentsMessage,
|
|
19
|
+
renderMinimalSubagentsResult,
|
|
20
|
+
type MinimalSubagentsRenderTheme,
|
|
21
|
+
} from "./minimal-subagents-rendering.js";
|
|
22
|
+
import type { ChildAgentTranscriptSnapshot } from "./minimal-subagents-types.js";
|
|
23
|
+
|
|
24
|
+
interface CachedTranscriptMessage {
|
|
25
|
+
container: Container;
|
|
26
|
+
tools: Map<string, ToolExecutionComponent>;
|
|
27
|
+
expanded: boolean;
|
|
28
|
+
streaming: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Rendered transcript lines and the first line of each message. */
|
|
32
|
+
export interface TranscriptLayout {
|
|
33
|
+
lines: string[];
|
|
34
|
+
messageStarts: number[];
|
|
35
|
+
/** First line of each native component, such as one assistant reply or one tool execution. */
|
|
36
|
+
itemStarts: number[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Native components reused across renders while their source messages are unchanged. */
|
|
40
|
+
export interface TranscriptRenderCache {
|
|
41
|
+
messages: WeakMap<AgentMessage, CachedTranscriptMessage>;
|
|
42
|
+
results: WeakMap<ToolExecutionComponent, AgentMessage>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Render a Child Session Transcript with Pi's native message and tool components. */
|
|
46
|
+
export function renderTranscriptSnapshot(
|
|
47
|
+
snapshot: ChildAgentTranscriptSnapshot,
|
|
48
|
+
tui: TUI,
|
|
49
|
+
cwd: string,
|
|
50
|
+
expanded: boolean,
|
|
51
|
+
width: number,
|
|
52
|
+
cache: TranscriptRenderCache,
|
|
53
|
+
): TranscriptLayout {
|
|
54
|
+
if (snapshot.messages.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
lines: new Text(snapshot.fallback || "No conversation messages yet.", 3, 0).render(width),
|
|
57
|
+
messageStarts: [0],
|
|
58
|
+
itemStarts: [0],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const blocks: Container[] = [];
|
|
62
|
+
const tools = new Map(
|
|
63
|
+
snapshot.toolDefinitions.map((definition) => [definition.name, definition]),
|
|
64
|
+
);
|
|
65
|
+
const pendingTools = new Map<string, ToolExecutionComponent>();
|
|
66
|
+
const currentMessages = new Set(snapshot.messages);
|
|
67
|
+
|
|
68
|
+
for (const [messageIndex, message] of snapshot.messages.entries()) {
|
|
69
|
+
if (message.role === "toolResult") {
|
|
70
|
+
const paired = pendingTools.get(message.toolCallId);
|
|
71
|
+
if (paired) {
|
|
72
|
+
if (cache.results.get(paired) !== message) {
|
|
73
|
+
paired.updateResult(message);
|
|
74
|
+
cache.results.set(paired, message);
|
|
75
|
+
}
|
|
76
|
+
pendingTools.delete(message.toolCallId);
|
|
77
|
+
blocks.push(new Container());
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const streaming = messageIndex === snapshot.streamingAssistantIndex;
|
|
82
|
+
const cached = cache.messages.get(message);
|
|
83
|
+
const staleResult =
|
|
84
|
+
cached &&
|
|
85
|
+
[...cached.tools.values()].some((tool) => {
|
|
86
|
+
const result = cache.results.get(tool);
|
|
87
|
+
return result !== undefined && !currentMessages.has(result);
|
|
88
|
+
});
|
|
89
|
+
if (cached && !staleResult && cached.expanded === expanded && cached.streaming === streaming) {
|
|
90
|
+
blocks.push(cached.container);
|
|
91
|
+
for (const [id, tool] of cached.tools) pendingTools.set(id, tool);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const container = new Container();
|
|
95
|
+
const messageTools = new Map<string, ToolExecutionComponent>();
|
|
96
|
+
switch (message.role) {
|
|
97
|
+
case "user": {
|
|
98
|
+
const text = contentText(message.content, "\n\n");
|
|
99
|
+
if (text) container.addChild(new UserMessageComponent(text));
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "assistant": {
|
|
103
|
+
const assistant = new AssistantMessageComponent(message);
|
|
104
|
+
assistant.updateContent(message, messageIndex === snapshot.streamingAssistantIndex);
|
|
105
|
+
container.addChild(assistant);
|
|
106
|
+
for (const content of message.content) {
|
|
107
|
+
if (content.type !== "toolCall") continue;
|
|
108
|
+
const tool = new ToolExecutionComponent(
|
|
109
|
+
content.name,
|
|
110
|
+
content.id,
|
|
111
|
+
content.arguments,
|
|
112
|
+
{ showImages: false },
|
|
113
|
+
tools.get(content.name) ?? {},
|
|
114
|
+
tui,
|
|
115
|
+
cwd,
|
|
116
|
+
);
|
|
117
|
+
tool.setExpanded(expanded);
|
|
118
|
+
container.addChild(tool);
|
|
119
|
+
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
120
|
+
tool.updateResult({
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "text",
|
|
124
|
+
text:
|
|
125
|
+
message.stopReason === "aborted"
|
|
126
|
+
? "Operation aborted"
|
|
127
|
+
: (message.errorMessage ?? "Error"),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
isError: true,
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
pendingTools.set(content.id, tool);
|
|
134
|
+
messageTools.set(content.id, tool);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case "toolResult": {
|
|
140
|
+
const inherited = new ToolExecutionComponent(
|
|
141
|
+
message.toolName,
|
|
142
|
+
message.toolCallId,
|
|
143
|
+
{},
|
|
144
|
+
{ showImages: false },
|
|
145
|
+
{},
|
|
146
|
+
tui,
|
|
147
|
+
cwd,
|
|
148
|
+
);
|
|
149
|
+
inherited.setExpanded(expanded);
|
|
150
|
+
inherited.updateResult(message);
|
|
151
|
+
container.addChild(inherited);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "custom": {
|
|
155
|
+
if (!message.display) break;
|
|
156
|
+
const renderer =
|
|
157
|
+
message.customType === "minimal-subagents.message"
|
|
158
|
+
? renderMinimalSubagentsMessage
|
|
159
|
+
: message.customType === "minimal-subagents.result"
|
|
160
|
+
? renderMinimalSubagentsResult
|
|
161
|
+
: undefined;
|
|
162
|
+
const custom = new CustomMessageComponent(message, renderer);
|
|
163
|
+
custom.setExpanded(expanded);
|
|
164
|
+
container.addChild(custom);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "bashExecution": {
|
|
168
|
+
const bash = new BashExecutionComponent(message.command, tui, message.excludeFromContext);
|
|
169
|
+
if (message.output) bash.appendOutput(message.output);
|
|
170
|
+
bash.setComplete(
|
|
171
|
+
message.exitCode,
|
|
172
|
+
message.cancelled,
|
|
173
|
+
// SAFETY: Persisted bash messages retain only the truncation flag; BashExecutionComponent reads that flag here.
|
|
174
|
+
message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
|
|
175
|
+
message.fullOutputPath,
|
|
176
|
+
);
|
|
177
|
+
bash.setExpanded(expanded);
|
|
178
|
+
container.addChild(bash);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "branchSummary": {
|
|
182
|
+
const summary = new BranchSummaryMessageComponent(message);
|
|
183
|
+
summary.setExpanded(expanded);
|
|
184
|
+
container.addChild(summary);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "compactionSummary": {
|
|
188
|
+
const summary = new CompactionSummaryMessageComponent(message);
|
|
189
|
+
summary.setExpanded(expanded);
|
|
190
|
+
container.addChild(summary);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
cache.messages.set(message, { container, tools: messageTools, expanded, streaming });
|
|
195
|
+
blocks.push(container);
|
|
196
|
+
}
|
|
197
|
+
let length = 0;
|
|
198
|
+
const messageStarts: number[] = [];
|
|
199
|
+
const itemStarts: number[] = [];
|
|
200
|
+
// Containers concatenate their children, so rendering children one by one yields the same lines.
|
|
201
|
+
const lines = blocks.flatMap((block) => {
|
|
202
|
+
messageStarts.push(length);
|
|
203
|
+
return block.children.flatMap((child) => {
|
|
204
|
+
itemStarts.push(length);
|
|
205
|
+
// Native user-message prompt zones belong to the main terminal, not an embedded overlay.
|
|
206
|
+
const rendered = child
|
|
207
|
+
.render(width)
|
|
208
|
+
.map((line) =>
|
|
209
|
+
line
|
|
210
|
+
.replaceAll("\x1b]133;A\x07", "")
|
|
211
|
+
.replaceAll("\x1b]133;B\x07", "")
|
|
212
|
+
.replaceAll("\x1b]133;C\x07", ""),
|
|
213
|
+
);
|
|
214
|
+
length += rendered.length;
|
|
215
|
+
return rendered;
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
return { lines, messageStarts, itemStarts };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Create an empty cache for {@link renderTranscriptSnapshot}. */
|
|
222
|
+
export function createTranscriptRenderCache(): TranscriptRenderCache {
|
|
223
|
+
return { messages: new WeakMap(), results: new WeakMap() };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Collapsed live views keep at most this many lines of the child's recent work. */
|
|
227
|
+
const COLLAPSED_TRANSCRIPT_RAIL_LINES = 20;
|
|
228
|
+
|
|
229
|
+
function isSpacerLine(line: string): boolean {
|
|
230
|
+
// Includes a tool box's coloured padding rows, so each rail connector meets visible content.
|
|
231
|
+
return stripVTControlCharacters(line).trim().length === 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function withoutSpacerEdges(lines: string[]): string[] {
|
|
235
|
+
const start = lines.findIndex((line) => !isSpacerLine(line));
|
|
236
|
+
if (start < 0) return [];
|
|
237
|
+
const end = lines.findLastIndex((line) => !isSpacerLine(line));
|
|
238
|
+
return lines.slice(start, end + 1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A child's recent transcript drawn with Pi's native message and tool components, one rail item
|
|
243
|
+
* per component. Collapsed, it keeps the latest items that fit the line budget.
|
|
244
|
+
*/
|
|
245
|
+
export class TranscriptRail implements Component {
|
|
246
|
+
constructor(
|
|
247
|
+
private readonly snapshot: ChildAgentTranscriptSnapshot,
|
|
248
|
+
private readonly tui: TUI,
|
|
249
|
+
private readonly cwd: string,
|
|
250
|
+
private readonly expanded: boolean,
|
|
251
|
+
private readonly cache: TranscriptRenderCache,
|
|
252
|
+
private readonly theme: MinimalSubagentsRenderTheme,
|
|
253
|
+
) {}
|
|
254
|
+
|
|
255
|
+
render(width: number): string[] {
|
|
256
|
+
const layout = renderTranscriptSnapshot(
|
|
257
|
+
this.snapshot,
|
|
258
|
+
this.tui,
|
|
259
|
+
this.cwd,
|
|
260
|
+
this.expanded,
|
|
261
|
+
Math.max(1, width - ACTIVITY_RAIL_WIDTH),
|
|
262
|
+
this.cache,
|
|
263
|
+
);
|
|
264
|
+
const items = layout.itemStarts
|
|
265
|
+
.map((start, index) =>
|
|
266
|
+
withoutSpacerEdges(layout.lines.slice(start, layout.itemStarts[index + 1])),
|
|
267
|
+
)
|
|
268
|
+
.filter((lines) => lines.length > 0);
|
|
269
|
+
if (this.expanded) return drawActivityRail(items, this.theme);
|
|
270
|
+
let shown = 0;
|
|
271
|
+
let lineCount = 0;
|
|
272
|
+
for (const item of items.toReversed()) {
|
|
273
|
+
if (shown > 0 && lineCount + item.length > COLLAPSED_TRANSCRIPT_RAIL_LINES) break;
|
|
274
|
+
shown++;
|
|
275
|
+
lineCount += item.length;
|
|
276
|
+
}
|
|
277
|
+
const kept = items
|
|
278
|
+
.slice(items.length - shown)
|
|
279
|
+
.map((lines) => lines.slice(0, COLLAPSED_TRANSCRIPT_RAIL_LINES));
|
|
280
|
+
const earlier = items.length - shown;
|
|
281
|
+
return drawActivityRail(
|
|
282
|
+
earlier > 0
|
|
283
|
+
? [
|
|
284
|
+
[this.theme.fg("dim", `… ${earlier} earlier ${earlier === 1 ? "step" : "steps"}`)],
|
|
285
|
+
...kept,
|
|
286
|
+
]
|
|
287
|
+
: kept,
|
|
288
|
+
this.theme,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
invalidate(): void {
|
|
293
|
+
this.cache.messages = new WeakMap();
|
|
294
|
+
this.cache.results = new WeakMap();
|
|
295
|
+
}
|
|
296
|
+
}
|