@ryan_nookpi/pi-extension-subagent 0.3.4 → 0.4.1
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/cli.ts +20 -12
- package/commands.ts +2 -1
- package/constants.ts +6 -0
- package/lifecycle.ts +1 -0
- package/package.json +1 -1
- package/run-utils.ts +120 -1
- package/store.ts +11 -1
- package/tool-execute.ts +98 -1
- package/tool-render.ts +99 -4
- package/types.ts +24 -0
package/cli.ts
CHANGED
|
@@ -53,8 +53,9 @@ export const SUBAGENT_CLI_HELP_TEXT = [
|
|
|
53
53
|
" subagent help",
|
|
54
54
|
" subagent agents",
|
|
55
55
|
" subagent runs",
|
|
56
|
-
" subagent status <runId>",
|
|
57
|
-
" subagent detail <runId>",
|
|
56
|
+
" subagent status <runId|groupId>",
|
|
57
|
+
" subagent detail <runId|groupId>",
|
|
58
|
+
" (groupId = the b_.../p_... id returned by batch/chain launches; finished groups are retained briefly)",
|
|
58
59
|
"",
|
|
59
60
|
" Execution:",
|
|
60
61
|
" subagent run <agent> [--main|--isolated] -- <task>",
|
|
@@ -167,6 +168,11 @@ function parseInteger(raw: string): number | null {
|
|
|
167
168
|
return Number.isInteger(value) ? value : null;
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
/** Group IDs returned by batch (`b_...`) and chain (`p_...`) launches. */
|
|
172
|
+
function isGroupId(raw: string): boolean {
|
|
173
|
+
return /^[bp]_/.test(raw);
|
|
174
|
+
}
|
|
175
|
+
|
|
170
176
|
function parseRunTarget(
|
|
171
177
|
raw: string,
|
|
172
178
|
knownRunIds: number[] | undefined,
|
|
@@ -462,33 +468,35 @@ export function parseSubagentToolCommand(
|
|
|
462
468
|
return { type: "params", params: { asyncAction: "list" } };
|
|
463
469
|
|
|
464
470
|
case "status": {
|
|
465
|
-
const
|
|
466
|
-
if (!
|
|
471
|
+
const idRaw = args[0];
|
|
472
|
+
if (!idRaw)
|
|
467
473
|
return {
|
|
468
474
|
type: "error",
|
|
469
|
-
message: `❌ status requires <runId>\n\n✓ Example: subagent status 22\n\nSee all runs with: subagent runs`,
|
|
475
|
+
message: `❌ status requires <runId|groupId>\n\n✓ Example: subagent status 22\n✓ Example: subagent status b_1712... (batch/chain)\n\nSee all runs with: subagent runs`,
|
|
470
476
|
};
|
|
471
|
-
|
|
477
|
+
if (isGroupId(idRaw)) return { type: "params", params: { asyncAction: "status", groupId: idRaw } };
|
|
478
|
+
const runId = parseInteger(idRaw);
|
|
472
479
|
if (runId === null)
|
|
473
480
|
return {
|
|
474
481
|
type: "error",
|
|
475
|
-
message: `❌ Invalid
|
|
482
|
+
message: `❌ Invalid id: "${idRaw}"\n\nUse a numeric runId or a batch/chain groupId (b_.../p_...). See all runs with: subagent runs`,
|
|
476
483
|
};
|
|
477
484
|
return { type: "params", params: { asyncAction: "status", runId } };
|
|
478
485
|
}
|
|
479
486
|
|
|
480
487
|
case "detail": {
|
|
481
|
-
const
|
|
482
|
-
if (!
|
|
488
|
+
const idRaw = args[0];
|
|
489
|
+
if (!idRaw)
|
|
483
490
|
return {
|
|
484
491
|
type: "error",
|
|
485
|
-
message: `❌ detail requires <runId>\n\n✓ Example: subagent detail 22\n\nSee all runs with: subagent runs`,
|
|
492
|
+
message: `❌ detail requires <runId|groupId>\n\n✓ Example: subagent detail 22\n✓ Example: subagent detail b_1712... (batch/chain)\n\nSee all runs with: subagent runs`,
|
|
486
493
|
};
|
|
487
|
-
|
|
494
|
+
if (isGroupId(idRaw)) return { type: "params", params: { asyncAction: "detail", groupId: idRaw } };
|
|
495
|
+
const runId = parseInteger(idRaw);
|
|
488
496
|
if (runId === null)
|
|
489
497
|
return {
|
|
490
498
|
type: "error",
|
|
491
|
-
message: `❌ Invalid
|
|
499
|
+
message: `❌ Invalid id: "${idRaw}"\n\nUse a numeric runId or a batch/chain groupId (b_.../p_...). See all runs with: subagent runs`,
|
|
492
500
|
};
|
|
493
501
|
return { type: "params", params: { asyncAction: "detail", runId } };
|
|
494
502
|
}
|
package/commands.ts
CHANGED
|
@@ -51,7 +51,7 @@ import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session
|
|
|
51
51
|
import { SUBAGENT_COMMANDS, type SubagentCommandName } from "./registration-manifest.js";
|
|
52
52
|
import { readSessionReplayItems, SubagentSessionReplayOverlay } from "./replay.js";
|
|
53
53
|
import { invokeWithAutoRetry, MAX_SUBAGENT_AUTO_RETRIES } from "./retry.js";
|
|
54
|
-
import { getLatestRun, removeRun, trimCommandRunHistory } from "./run-utils.js";
|
|
54
|
+
import { evictStaleFinishedGroups, getLatestRun, removeRun, trimCommandRunHistory } from "./run-utils.js";
|
|
55
55
|
import {
|
|
56
56
|
getFinalOutput,
|
|
57
57
|
getLastNonEmptyLine,
|
|
@@ -824,6 +824,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
|
|
|
824
824
|
}
|
|
825
825
|
|
|
826
826
|
evictStalePendingGroupCompletions(STALE_PENDING_COMPLETION_MS);
|
|
827
|
+
evictStaleFinishedGroups(store);
|
|
827
828
|
|
|
828
829
|
// Fallback: if this session has no subagent markers at all, but we recently
|
|
829
830
|
// had in-memory runs for the same session file, reuse that snapshot so
|
package/constants.ts
CHANGED
|
@@ -30,6 +30,12 @@ export const SUBAGENT_STRONG_WAIT_MESSAGE =
|
|
|
30
30
|
/** Maximum age (ms) for pending cross-session completions before eviction. */
|
|
31
31
|
export const STALE_PENDING_COMPLETION_MS = 30 * 60 * 1_000;
|
|
32
32
|
|
|
33
|
+
/** Max number of finished batch/chain group snapshots retained for `status`/`detail` queries. */
|
|
34
|
+
export const MAX_FINISHED_GROUPS = 20;
|
|
35
|
+
|
|
36
|
+
/** Max age (ms) a finished group snapshot is retained before eviction. */
|
|
37
|
+
export const FINISHED_GROUP_TTL_MS = 30 * 60 * 1_000;
|
|
38
|
+
|
|
33
39
|
/** Short label shown in the widget when inside a child session. */
|
|
34
40
|
export const PARENT_HINT = "↩ parent (><)";
|
|
35
41
|
|
package/lifecycle.ts
CHANGED
|
@@ -114,6 +114,7 @@ export function shutdownSubagentRuns(store: SubagentStore, pi: ExtensionAPI, rea
|
|
|
114
114
|
store.globalLiveRuns.clear();
|
|
115
115
|
store.batchGroups.clear();
|
|
116
116
|
store.pipelines.clear();
|
|
117
|
+
store.finishedGroups.clear();
|
|
117
118
|
store.recentLaunchTimestamps.clear();
|
|
118
119
|
store.commandRuns.clear();
|
|
119
120
|
store.commandWidgetCtx = null;
|
package/package.json
CHANGED
package/run-utils.ts
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { FINISHED_GROUP_TTL_MS, MAX_FINISHED_GROUPS, STATUS_OUTPUT_PREVIEW_MAX_CHARS } from "./constants.js";
|
|
10
11
|
import type { SubagentStore } from "./store.js";
|
|
11
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
BatchGroupState,
|
|
14
|
+
CommandRunState,
|
|
15
|
+
FinishedGroupMember,
|
|
16
|
+
FinishedGroupSnapshot,
|
|
17
|
+
PipelineState,
|
|
18
|
+
} from "./types.js";
|
|
12
19
|
import { updateCommandRunsWidget, type WidgetRenderCtx } from "./widget.js";
|
|
13
20
|
|
|
14
21
|
export interface RemoveRunOptions {
|
|
@@ -201,3 +208,115 @@ export function trimCommandRunHistory(
|
|
|
201
208
|
|
|
202
209
|
return removedRunIds;
|
|
203
210
|
}
|
|
211
|
+
|
|
212
|
+
// ── Finished-group retention ────────────────────────────────────────────────
|
|
213
|
+
// Batch/chain groups are deleted from the live store once their completion is
|
|
214
|
+
// delivered. To keep `subagent status/detail <groupId>` working for a short
|
|
215
|
+
// window afterward, we retain an immutable snapshot per finished group.
|
|
216
|
+
|
|
217
|
+
function memberOutput(run: CommandRunState, fallback?: string): string {
|
|
218
|
+
return run.lastOutput?.trim() || run.lastLine?.trim() || fallback?.trim() || "(no output)";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Build a finished-group snapshot from a completed batch group. */
|
|
222
|
+
export function snapshotBatchGroup(
|
|
223
|
+
store: SubagentStore,
|
|
224
|
+
batch: BatchGroupState,
|
|
225
|
+
terminalStatus: FinishedGroupSnapshot["terminalStatus"],
|
|
226
|
+
): FinishedGroupSnapshot {
|
|
227
|
+
let failed = 0;
|
|
228
|
+
const members: FinishedGroupMember[] = batch.runIds.map((runId) => {
|
|
229
|
+
const run = store.commandRuns.get(runId);
|
|
230
|
+
if (!run) {
|
|
231
|
+
if (batch.failedRunIds.has(runId)) failed++;
|
|
232
|
+
return {
|
|
233
|
+
summaryLine: `#${runId} [gone] (run no longer available)`,
|
|
234
|
+
output: batch.pendingResults.get(runId)?.trim() || "(no output)",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (run.status === "error" || batch.failedRunIds.has(runId)) failed++;
|
|
238
|
+
return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run, batch.pendingResults.get(runId)) };
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
groupId: batch.batchId,
|
|
242
|
+
kind: "batch",
|
|
243
|
+
terminalStatus,
|
|
244
|
+
finishedAt: Date.now(),
|
|
245
|
+
total: batch.runIds.length,
|
|
246
|
+
failed,
|
|
247
|
+
members,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Build a finished-group snapshot from a completed pipeline. */
|
|
252
|
+
export function snapshotPipeline(
|
|
253
|
+
pipeline: PipelineState,
|
|
254
|
+
terminalStatus: FinishedGroupSnapshot["terminalStatus"],
|
|
255
|
+
): FinishedGroupSnapshot {
|
|
256
|
+
const members: FinishedGroupMember[] = pipeline.stepResults.map((step, index) => ({
|
|
257
|
+
summaryLine: `Step ${index + 1} · #${step.runId} ${step.agent} · ${step.status}`,
|
|
258
|
+
output: step.output?.trim() || "(no output)",
|
|
259
|
+
task: step.task,
|
|
260
|
+
}));
|
|
261
|
+
return {
|
|
262
|
+
groupId: pipeline.pipelineId,
|
|
263
|
+
kind: "chain",
|
|
264
|
+
terminalStatus,
|
|
265
|
+
finishedAt: Date.now(),
|
|
266
|
+
total: pipeline.stepResults.length,
|
|
267
|
+
failed: pipeline.stepResults.filter((step) => step.status === "error").length,
|
|
268
|
+
members,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Retain a finished-group snapshot, evicting the oldest entries past the cap. */
|
|
273
|
+
export function retireFinishedGroup(store: SubagentStore, snapshot: FinishedGroupSnapshot): void {
|
|
274
|
+
// Re-insert to refresh insertion order (most-recent last).
|
|
275
|
+
store.finishedGroups.delete(snapshot.groupId);
|
|
276
|
+
store.finishedGroups.set(snapshot.groupId, snapshot);
|
|
277
|
+
while (store.finishedGroups.size > MAX_FINISHED_GROUPS) {
|
|
278
|
+
const oldest = store.finishedGroups.keys().next().value;
|
|
279
|
+
if (oldest === undefined) break;
|
|
280
|
+
store.finishedGroups.delete(oldest);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Evict finished-group snapshots older than the retention TTL. Returns count removed. */
|
|
285
|
+
export function evictStaleFinishedGroups(store: SubagentStore, now = Date.now()): number {
|
|
286
|
+
let removed = 0;
|
|
287
|
+
for (const [groupId, snapshot] of store.finishedGroups) {
|
|
288
|
+
if (now - snapshot.finishedAt > FINISHED_GROUP_TTL_MS) {
|
|
289
|
+
store.finishedGroups.delete(groupId);
|
|
290
|
+
removed++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return removed;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function formatFinishedAge(finishedAt: number, now = Date.now()): string {
|
|
297
|
+
const seconds = Math.max(0, Math.round((now - finishedAt) / 1000));
|
|
298
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
299
|
+
const minutes = Math.round(seconds / 60);
|
|
300
|
+
return `${minutes}m ago`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Render a retained finished-group snapshot for `status` (summary) or `detail` (with output). */
|
|
304
|
+
export function formatFinishedGroupStatus(snapshot: FinishedGroupSnapshot, detailed: boolean): string {
|
|
305
|
+
const label = snapshot.kind === "batch" ? "subagent-batch" : "subagent-chain";
|
|
306
|
+
const failedSuffix = snapshot.failed > 0 ? `, ${snapshot.failed} failed` : "";
|
|
307
|
+
const header = `[${label}#${snapshot.groupId}] ${snapshot.terminalStatus} · ${snapshot.total} ${
|
|
308
|
+
snapshot.kind === "batch" ? "runs" : "steps"
|
|
309
|
+
}${failedSuffix} · finished ${formatFinishedAge(snapshot.finishedAt)}`;
|
|
310
|
+
const body = snapshot.members
|
|
311
|
+
.map((member) => {
|
|
312
|
+
if (!detailed) return member.summaryLine;
|
|
313
|
+
const output =
|
|
314
|
+
member.output.length > STATUS_OUTPUT_PREVIEW_MAX_CHARS
|
|
315
|
+
? `${member.output.slice(0, STATUS_OUTPUT_PREVIEW_MAX_CHARS)}\n\n... [truncated]`
|
|
316
|
+
: member.output;
|
|
317
|
+
const taskLine = member.task ? `Task: ${member.task}\n` : "";
|
|
318
|
+
return `${member.summaryLine}\n${taskLine}${output}`;
|
|
319
|
+
})
|
|
320
|
+
.join(detailed ? "\n\n" : "\n");
|
|
321
|
+
return `${header}\n\n${body}`;
|
|
322
|
+
}
|
package/store.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
import type { Message } from "@earendil-works/pi-ai";
|
|
6
6
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
7
7
|
import { getDisplayItems, getFinalOutput, getLastNonEmptyLine, getLatestActivityPreview } from "./runner.js";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
BatchGroupState,
|
|
10
|
+
CommandRunState,
|
|
11
|
+
FinishedGroupSnapshot,
|
|
12
|
+
GlobalRunEntry,
|
|
13
|
+
PipelineState,
|
|
14
|
+
SingleResult,
|
|
15
|
+
} from "./types.js";
|
|
9
16
|
import type { WidgetRenderCtx } from "./widget.js";
|
|
10
17
|
|
|
11
18
|
export const COLLAPSED_ITEM_COUNT = 10;
|
|
@@ -40,6 +47,8 @@ export interface SubagentStore {
|
|
|
40
47
|
batchGroups: Map<string, BatchGroupState>;
|
|
41
48
|
/** In-memory sequential pipelines launched via the tool. */
|
|
42
49
|
pipelines: Map<string, PipelineState>;
|
|
50
|
+
/** Retained snapshots of finished batch/chain groups, keyed by groupId (insertion-ordered). */
|
|
51
|
+
finishedGroups: Map<string, FinishedGroupSnapshot>;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
export function createStore(): SubagentStore {
|
|
@@ -59,6 +68,7 @@ export function createStore(): SubagentStore {
|
|
|
59
68
|
recentLaunchTimestamps: new Map(),
|
|
60
69
|
batchGroups: new Map(),
|
|
61
70
|
pipelines: new Map(),
|
|
71
|
+
finishedGroups: new Map(),
|
|
62
72
|
};
|
|
63
73
|
}
|
|
64
74
|
|
package/tool-execute.ts
CHANGED
|
@@ -41,7 +41,17 @@ import {
|
|
|
41
41
|
import { clearPendingGroupCompletion, upsertPendingGroupCompletion } from "./group-pending.js";
|
|
42
42
|
import { enqueueSubagentInvocation } from "./invocation-queue.js";
|
|
43
43
|
import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session.js";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
clearFinishedRuns,
|
|
46
|
+
evictStaleFinishedGroups,
|
|
47
|
+
formatCommandRunSummary,
|
|
48
|
+
formatFinishedGroupStatus,
|
|
49
|
+
removeRun,
|
|
50
|
+
retireFinishedGroup,
|
|
51
|
+
snapshotBatchGroup,
|
|
52
|
+
snapshotPipeline,
|
|
53
|
+
trimCommandRunHistory,
|
|
54
|
+
} from "./run-utils.js";
|
|
45
55
|
import { getFinalOutput, getLastNonEmptyLine, runSingleAgent } from "./runner.js";
|
|
46
56
|
import {
|
|
47
57
|
buildMainContextText,
|
|
@@ -54,10 +64,12 @@ import {
|
|
|
54
64
|
import { formatStarterPackNotice, offerStarterPackIfEmpty } from "./starter-pack.js";
|
|
55
65
|
import { type SubagentStore, updateRunFromResult } from "./store.js";
|
|
56
66
|
import type {
|
|
67
|
+
BatchGroupState,
|
|
57
68
|
BatchOrChainItem,
|
|
58
69
|
CommandRunState,
|
|
59
70
|
OnUpdateCallback,
|
|
60
71
|
PendingCompletion,
|
|
72
|
+
PipelineState,
|
|
61
73
|
PipelineStepResult,
|
|
62
74
|
SingleResult,
|
|
63
75
|
SubagentDetails,
|
|
@@ -583,6 +595,47 @@ function formatPipelineSummary(
|
|
|
583
595
|
return `[subagent-chain#${pipelineId}] ${terminalStatus}\n\n${steps}`;
|
|
584
596
|
}
|
|
585
597
|
|
|
598
|
+
function previewOutput(output: string): string {
|
|
599
|
+
const trimmed = output.trim() || "(no output yet)";
|
|
600
|
+
return trimmed.length > STATUS_OUTPUT_PREVIEW_MAX_CHARS
|
|
601
|
+
? `${trimmed.slice(0, STATUS_OUTPUT_PREVIEW_MAX_CHARS)}\n\n... [truncated]`
|
|
602
|
+
: trimmed;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function formatBatchGroupStatus(store: SubagentStore, batch: BatchGroupState, detailed: boolean): string {
|
|
606
|
+
const total = batch.runIds.length;
|
|
607
|
+
const done = batch.completedRunIds.size;
|
|
608
|
+
const failed = batch.failedRunIds.size;
|
|
609
|
+
const header = `[subagent-batch#${batch.batchId}] running · ${done}/${total} finished${
|
|
610
|
+
failed > 0 ? `, ${failed} failed` : ""
|
|
611
|
+
}`;
|
|
612
|
+
const body = batch.runIds
|
|
613
|
+
.map((runId) => {
|
|
614
|
+
const run = store.commandRuns.get(runId);
|
|
615
|
+
if (!run) return `#${runId} (unavailable)`;
|
|
616
|
+
const summary = formatCommandRunSummary(run);
|
|
617
|
+
if (!detailed) return summary;
|
|
618
|
+
return `${summary}\n${previewOutput(run.lastOutput ?? run.lastLine ?? "")}`;
|
|
619
|
+
})
|
|
620
|
+
.join(detailed ? "\n\n" : "\n");
|
|
621
|
+
return `${header}\n\n${body}`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function formatPipelineGroupStatus(store: SubagentStore, pipeline: PipelineState, detailed: boolean): string {
|
|
625
|
+
const finishedCount = pipeline.stepResults.length;
|
|
626
|
+
const header = `[subagent-chain#${pipeline.pipelineId}] running · step ${pipeline.currentIndex + 1} (${finishedCount} finished)`;
|
|
627
|
+
const parts = pipeline.stepResults.map((step, index) => {
|
|
628
|
+
const line = `Step ${index + 1} · #${step.runId} ${step.agent} · ${step.status}`;
|
|
629
|
+
return detailed ? `${line}\nTask: ${step.task}\n${previewOutput(step.output)}` : line;
|
|
630
|
+
});
|
|
631
|
+
const runningRunId = pipeline.stepRunIds[finishedCount];
|
|
632
|
+
if (runningRunId !== undefined) {
|
|
633
|
+
const run = store.commandRuns.get(runningRunId);
|
|
634
|
+
if (run) parts.push(`Step ${finishedCount + 1} · ${formatCommandRunSummary(run)}`);
|
|
635
|
+
}
|
|
636
|
+
return `${header}\n\n${parts.join(detailed ? "\n\n" : "\n")}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
586
639
|
function toLaunchSummary(
|
|
587
640
|
runState: Pick<CommandRunState, "agent" | "id" | "batchId" | "pipelineId" | "pipelineStepIndex">,
|
|
588
641
|
mode: SubagentLaunchSummary["mode"],
|
|
@@ -842,6 +895,43 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
842
895
|
};
|
|
843
896
|
}
|
|
844
897
|
|
|
898
|
+
if ((asyncAction === "status" || asyncAction === "detail") && typeof cmdParams.groupId === "string") {
|
|
899
|
+
evictStaleFinishedGroups(store);
|
|
900
|
+
const groupId = cmdParams.groupId;
|
|
901
|
+
const detailed = asyncAction === "detail";
|
|
902
|
+
const finished = store.finishedGroups.get(groupId);
|
|
903
|
+
if (finished) {
|
|
904
|
+
return {
|
|
905
|
+
content: [{ type: "text", text: withIdleRunWarning(formatFinishedGroupStatus(finished, detailed)) }],
|
|
906
|
+
details: makeDetails("single"),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
const batch = store.batchGroups.get(groupId);
|
|
910
|
+
if (batch) {
|
|
911
|
+
return {
|
|
912
|
+
content: [{ type: "text", text: withIdleRunWarning(formatBatchGroupStatus(store, batch, detailed)) }],
|
|
913
|
+
details: makeDetails("single"),
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
const pipeline = store.pipelines.get(groupId);
|
|
917
|
+
if (pipeline) {
|
|
918
|
+
return {
|
|
919
|
+
content: [{ type: "text", text: withIdleRunWarning(formatPipelineGroupStatus(store, pipeline, detailed)) }],
|
|
920
|
+
details: makeDetails("single"),
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
content: [
|
|
925
|
+
{
|
|
926
|
+
type: "text",
|
|
927
|
+
text: `Unknown subagent group "${groupId}". Finished groups are retained only briefly; use \`subagent runs\` to inspect individual runs.`,
|
|
928
|
+
},
|
|
929
|
+
],
|
|
930
|
+
details: makeDetails("single"),
|
|
931
|
+
isError: true,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
845
935
|
const rawRunIds = Array.isArray(cmdParams.runIds) ? cmdParams.runIds : undefined;
|
|
846
936
|
const invalidRunIds = (rawRunIds ?? []).filter((value) => !Number.isInteger(value));
|
|
847
937
|
if (invalidRunIds.length > 0) {
|
|
@@ -1471,6 +1561,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1471
1561
|
const orderedRuns = runStates.map(({ runState }) => runState);
|
|
1472
1562
|
const hasError = finalizedRuns.some((finalized) => finalized.isError);
|
|
1473
1563
|
const content = formatBatchSummary(batchId, orderedRuns, hasError ? "error" : "completed");
|
|
1564
|
+
const batchForSnapshot = store.batchGroups.get(batchId);
|
|
1565
|
+
if (batchForSnapshot)
|
|
1566
|
+
retireFinishedGroup(store, snapshotBatchGroup(store, batchForSnapshot, hasError ? "error" : "completed"));
|
|
1474
1567
|
for (const { runState } of runStates) cleanupRunAfterFinalDelivery(runState.id);
|
|
1475
1568
|
clearPendingGroupCompletion("batch", batchId);
|
|
1476
1569
|
store.batchGroups.delete(batchId);
|
|
@@ -1525,6 +1618,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1525
1618
|
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1526
1619
|
},
|
|
1527
1620
|
};
|
|
1621
|
+
retireFinishedGroup(store, snapshotBatchGroup(store, batch, batchTerminalStatus));
|
|
1528
1622
|
if (isInOriginSession(ctx, batch.originSessionFile)) {
|
|
1529
1623
|
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1530
1624
|
clearPendingGroupCompletion("batch", batchId);
|
|
@@ -1579,6 +1673,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1579
1673
|
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1580
1674
|
},
|
|
1581
1675
|
};
|
|
1676
|
+
retireFinishedGroup(store, snapshotBatchGroup(store, batch, "error"));
|
|
1582
1677
|
if (isInOriginSession(ctx, batch.originSessionFile)) {
|
|
1583
1678
|
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1584
1679
|
clearPendingGroupCompletion("batch", batchId);
|
|
@@ -1754,6 +1849,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1754
1849
|
const hasError = pipeline.stepResults.some((step) => step.status === "error");
|
|
1755
1850
|
if (terminalStatus === "completed" && hasError) terminalStatus = "error";
|
|
1756
1851
|
const content = formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus);
|
|
1852
|
+
retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
|
|
1757
1853
|
for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
|
|
1758
1854
|
clearPendingGroupCompletion("chain", pipelineId);
|
|
1759
1855
|
store.pipelines.delete(pipelineId);
|
|
@@ -1892,6 +1988,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1892
1988
|
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1893
1989
|
},
|
|
1894
1990
|
};
|
|
1991
|
+
retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
|
|
1895
1992
|
if (isInOriginSession(ctx, pipeline.originSessionFile)) {
|
|
1896
1993
|
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1897
1994
|
clearPendingGroupCompletion("chain", pipelineId);
|
package/tool-render.ts
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getMarkdownTheme, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Container, Markdown, Spacer, Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
+
import { parseSubagentToolCommand } from "./cli.js";
|
|
9
10
|
import { formatToolCall, formatUsageStats } from "./format.js";
|
|
10
11
|
import { getDisplayItems, getFinalOutput } from "./runner.js";
|
|
11
|
-
import { COLLAPSED_ITEM_COUNT } from "./store.js";
|
|
12
|
+
import { COLLAPSED_ITEM_COUNT, truncateText } from "./store.js";
|
|
12
13
|
import type { DisplayItem, SubagentDetails } from "./types.js";
|
|
13
14
|
|
|
14
15
|
// ─── Helpers (internal) ──────────────────────────────────────────────────────
|
|
@@ -24,6 +25,90 @@ type ToolRenderResult = {
|
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
type ToolRenderArgs = { command?: unknown };
|
|
28
|
+
type ToolCallRenderContext = { expanded: boolean };
|
|
29
|
+
type CompactBlock = { agent: string; task: string };
|
|
30
|
+
|
|
31
|
+
const COMPACT_TASK_PREVIEW_WIDTH = 72;
|
|
32
|
+
const COMPACT_AGENT_WIDTH = 20;
|
|
33
|
+
const COMPACT_MAX_ITEMS = 3;
|
|
34
|
+
|
|
35
|
+
function normalizeTaskPreview(task: string): string {
|
|
36
|
+
return truncateText(task.replace(/\s+/g, " ").trim(), COMPACT_TASK_PREVIEW_WIDTH);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getContextLabel(params: Record<string, unknown>): "main" | "isolated" {
|
|
40
|
+
return params.contextMode === "main" ? "main" : "isolated";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getCompactBlocks(value: unknown): CompactBlock[] | null {
|
|
44
|
+
if (!Array.isArray(value)) return null;
|
|
45
|
+
const blocks: CompactBlock[] = [];
|
|
46
|
+
for (const item of value) {
|
|
47
|
+
if (!item || typeof item !== "object") return null;
|
|
48
|
+
const agent = "agent" in item ? item.agent : undefined;
|
|
49
|
+
const task = "task" in item ? item.task : undefined;
|
|
50
|
+
if (typeof agent !== "string" || typeof task !== "string") return null;
|
|
51
|
+
blocks.push({ agent, task });
|
|
52
|
+
}
|
|
53
|
+
return blocks;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatCompactAgent(agent: string, width: number): string {
|
|
57
|
+
const label = truncateText(agent, COMPACT_AGENT_WIDTH);
|
|
58
|
+
return `${label}${" ".repeat(Math.max(0, width - visibleWidth(label)))}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderCompactBlocks(kind: "batch" | "chain", params: Record<string, unknown>): string | null {
|
|
62
|
+
const blocks = getCompactBlocks(kind === "batch" ? params.runs : params.steps);
|
|
63
|
+
if (!blocks || blocks.length === 0) return null;
|
|
64
|
+
|
|
65
|
+
const visibleBlocks = blocks.slice(0, COMPACT_MAX_ITEMS);
|
|
66
|
+
const agentWidth = Math.max(
|
|
67
|
+
...visibleBlocks.map((block) => visibleWidth(truncateText(block.agent, COMPACT_AGENT_WIDTH))),
|
|
68
|
+
);
|
|
69
|
+
const contextLabel = getContextLabel(params);
|
|
70
|
+
const executionLabel = kind === "batch" ? "parallel" : "sequential";
|
|
71
|
+
const lines = [` ${kind} · ${contextLabel} · ${blocks.length} ${executionLabel}`];
|
|
72
|
+
|
|
73
|
+
for (const [index, block] of visibleBlocks.entries()) {
|
|
74
|
+
const agent = formatCompactAgent(block.agent, agentWidth);
|
|
75
|
+
const task = normalizeTaskPreview(block.task);
|
|
76
|
+
if (kind === "batch") {
|
|
77
|
+
lines.push(` ∥ ${agent} ${task}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const connector = index === 0 ? "" : `${" ".repeat(index - 1)}└─ `;
|
|
82
|
+
lines.push(` ${connector}${index + 1} ${agent} ${task}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hiddenCount = blocks.length - visibleBlocks.length;
|
|
86
|
+
if (hiddenCount > 0) {
|
|
87
|
+
if (kind === "batch") lines.push(` ∥ +${hiddenCount} jobs`);
|
|
88
|
+
else lines.push(` ${" ".repeat(visibleBlocks.length - 1)}└─ +${hiddenCount} steps`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderCompactLaunch(command: unknown): string | null {
|
|
95
|
+
const parsed = parseSubagentToolCommand(command);
|
|
96
|
+
if (parsed.type !== "params") return null;
|
|
97
|
+
|
|
98
|
+
const { params } = parsed;
|
|
99
|
+
if (params.asyncAction === "batch") return renderCompactBlocks("batch", params);
|
|
100
|
+
if (params.asyncAction === "chain") return renderCompactBlocks("chain", params);
|
|
101
|
+
if (typeof params.task !== "string") return null;
|
|
102
|
+
|
|
103
|
+
const contextLabel = getContextLabel(params);
|
|
104
|
+
const task = normalizeTaskPreview(params.task);
|
|
105
|
+
if (typeof params.runId === "number") {
|
|
106
|
+
const agent = typeof params.agent === "string" ? ` · ${params.agent}` : "";
|
|
107
|
+
return ` continue · #${params.runId}${agent} · ${contextLabel}\n ${task}`;
|
|
108
|
+
}
|
|
109
|
+
if (typeof params.agent !== "string") return null;
|
|
110
|
+
return ` run · ${params.agent} · ${contextLabel}\n ${task}`;
|
|
111
|
+
}
|
|
27
112
|
|
|
28
113
|
function renderDisplayItems(items: DisplayItem[], expanded: boolean, theme: RenderTheme, limit?: number): string {
|
|
29
114
|
const toShow = limit ? items.slice(-limit) : items;
|
|
@@ -43,15 +128,25 @@ function renderDisplayItems(items: DisplayItem[], expanded: boolean, theme: Rend
|
|
|
43
128
|
|
|
44
129
|
// ─── renderCall ──────────────────────────────────────────────────────────────
|
|
45
130
|
|
|
46
|
-
export function renderSubagentToolCall(
|
|
131
|
+
export function renderSubagentToolCall(
|
|
132
|
+
args: ToolRenderArgs,
|
|
133
|
+
theme: RenderTheme,
|
|
134
|
+
context: ToolCallRenderContext = { expanded: false },
|
|
135
|
+
) {
|
|
47
136
|
const raw = typeof args.command === "string" ? args.command.trim() : "";
|
|
48
137
|
const command = raw || "subagent help";
|
|
138
|
+
let text = theme.fg("toolTitle", theme.bold("subagent ")) + theme.fg("accent", "cli");
|
|
139
|
+
|
|
140
|
+
if (!context.expanded) {
|
|
141
|
+
const compactLaunch = renderCompactLaunch(args.command);
|
|
142
|
+
if (compactLaunch) return new Text(`${text}\n${theme.fg("dim", compactLaunch)}`, 0, 0);
|
|
143
|
+
}
|
|
144
|
+
|
|
49
145
|
const MAX_CALL_LINES = 5;
|
|
50
146
|
const commandLines = command.split("\n");
|
|
51
147
|
const truncated = commandLines.length > MAX_CALL_LINES;
|
|
52
148
|
const preview = truncated ? commandLines.slice(0, MAX_CALL_LINES).join("\n") : command;
|
|
53
149
|
|
|
54
|
-
let text = theme.fg("toolTitle", theme.bold("subagent ")) + theme.fg("accent", "cli");
|
|
55
150
|
text += `\n ${theme.fg("dim", preview)}`;
|
|
56
151
|
if (truncated) text += `\n ${theme.fg("muted", `... +${commandLines.length - MAX_CALL_LINES} more lines`)}`;
|
|
57
152
|
return new Text(text, 0, 0);
|
package/types.ts
CHANGED
|
@@ -183,6 +183,30 @@ export interface PipelineState {
|
|
|
183
183
|
pendingCompletion?: PendingCompletion;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/** A single member (batch run or chain step) captured in a finished-group snapshot. */
|
|
187
|
+
export interface FinishedGroupMember {
|
|
188
|
+
/** Pre-rendered one-line summary, frozen at retirement time. */
|
|
189
|
+
summaryLine: string;
|
|
190
|
+
/** Full member output, truncated only when rendered. */
|
|
191
|
+
output: string;
|
|
192
|
+
/** Chain step task (omitted for batch runs). */
|
|
193
|
+
task?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Immutable snapshot of a completed batch/chain group, retained briefly so
|
|
198
|
+
* `subagent status/detail <groupId>` still works after the live group is gone.
|
|
199
|
+
*/
|
|
200
|
+
export interface FinishedGroupSnapshot {
|
|
201
|
+
groupId: string;
|
|
202
|
+
kind: "batch" | "chain";
|
|
203
|
+
terminalStatus: "completed" | "error" | "stopped";
|
|
204
|
+
finishedAt: number;
|
|
205
|
+
total: number;
|
|
206
|
+
failed: number;
|
|
207
|
+
members: FinishedGroupMember[];
|
|
208
|
+
}
|
|
209
|
+
|
|
186
210
|
export const ListAgentsParams = Type.Object({});
|
|
187
211
|
|
|
188
212
|
export const SubagentParams = Type.Object({
|