pi-crew 0.1.15 → 0.1.16
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/CHANGELOG.md +8 -0
- package/README.md +16 -1
- package/docs/usage.md +6 -1
- package/package.json +1 -1
- package/schema.json +6 -1
- package/src/config/config.ts +10 -0
- package/src/extension/register.ts +26 -4
- package/src/extension/run-index.ts +22 -6
- package/src/extension/team-tool.ts +22 -1
- package/src/runtime/child-pi.ts +76 -11
- package/src/runtime/crew-agent-records.ts +12 -1
- package/src/runtime/task-runner.ts +48 -4
- package/src/state/event-log.ts +10 -0
- package/src/ui/crew-widget.ts +7 -7
- package/src/ui/powerbar-publisher.ts +2 -2
- package/src/ui/run-dashboard.ts +59 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.16
|
|
4
|
+
|
|
5
|
+
- Added right-side `/team-dashboard` placement with model, token, and tool detail rows for subagents.
|
|
6
|
+
- Added UI config for dashboard placement/width and model/token/tool visibility.
|
|
7
|
+
- Foreground child-process runs now continue without blocking the interactive chat and remain tied to session shutdown.
|
|
8
|
+
- Child-process observability now drops noisy `message_update`/encrypted thinking deltas and stores compact events to prevent massive JSONL/output logs from freezing sessions.
|
|
9
|
+
- Cancel now syncs agent records and writes a foreground interrupt request so queued/running agents stop appearing stale.
|
|
10
|
+
|
|
3
11
|
## 0.1.15
|
|
4
12
|
|
|
5
13
|
- Child-process model selection now uses Pi-configured/available models and auto-discovers provider/model entries from Pi settings/models config.
|
package/README.md
CHANGED
|
@@ -169,11 +169,26 @@ Supported config:
|
|
|
169
169
|
"ui": {
|
|
170
170
|
"widgetPlacement": "aboveEditor",
|
|
171
171
|
"widgetMaxLines": 8,
|
|
172
|
-
"powerbar": true
|
|
172
|
+
"powerbar": true,
|
|
173
|
+
"dashboardPlacement": "right",
|
|
174
|
+
"dashboardWidth": 52,
|
|
175
|
+
"showModel": true,
|
|
176
|
+
"showTokens": true,
|
|
177
|
+
"showTools": true
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
```
|
|
176
181
|
|
|
182
|
+
Safety notes:
|
|
183
|
+
|
|
184
|
+
- Foreground child-process runs continue in the Pi extension process and return control to chat immediately, so large workflows do not block the interactive session. They are interrupted on session shutdown. Use `async: true` only for intentionally detached runs that may survive the current session.
|
|
185
|
+
|
|
186
|
+
UI notes:
|
|
187
|
+
|
|
188
|
+
- `widgetPlacement`/`widgetMaxLines` keep the persistent active-run widget compact.
|
|
189
|
+
- `dashboardPlacement: "right"` opens `/team-dashboard` as a right-side overlay panel instead of a centered modal.
|
|
190
|
+
- `showModel`, `showTokens`, and `showTools` show worker model attempts, token usage, and tool activity in dashboard agent rows.
|
|
191
|
+
|
|
177
192
|
Show config:
|
|
178
193
|
|
|
179
194
|
```text
|
package/docs/usage.md
CHANGED
|
@@ -32,7 +32,12 @@ Supported fields:
|
|
|
32
32
|
"ui": {
|
|
33
33
|
"widgetPlacement": "aboveEditor",
|
|
34
34
|
"widgetMaxLines": 8,
|
|
35
|
-
"powerbar": true
|
|
35
|
+
"powerbar": true,
|
|
36
|
+
"dashboardPlacement": "right",
|
|
37
|
+
"dashboardWidth": 52,
|
|
38
|
+
"showModel": true,
|
|
39
|
+
"showTokens": true,
|
|
40
|
+
"showTools": true
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
```
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -86,7 +86,12 @@
|
|
|
86
86
|
"properties": {
|
|
87
87
|
"widgetPlacement": { "type": "string", "enum": ["aboveEditor", "belowEditor"] },
|
|
88
88
|
"widgetMaxLines": { "type": "integer", "minimum": 1, "maximum": 50 },
|
|
89
|
-
"powerbar": { "type": "boolean" }
|
|
89
|
+
"powerbar": { "type": "boolean" },
|
|
90
|
+
"dashboardPlacement": { "type": "string", "enum": ["center", "right"], "description": "Place /team-dashboard as a centered overlay or right-side panel." },
|
|
91
|
+
"dashboardWidth": { "type": "integer", "minimum": 32, "maximum": 120 },
|
|
92
|
+
"showModel": { "type": "boolean", "description": "Show worker model attempts in dashboard agent rows." },
|
|
93
|
+
"showTokens": { "type": "boolean", "description": "Show token usage in dashboard agent rows." },
|
|
94
|
+
"showTools": { "type": "boolean", "description": "Show tool activity in dashboard agent rows." }
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -51,6 +51,11 @@ export interface CrewUiConfig {
|
|
|
51
51
|
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
52
52
|
widgetMaxLines?: number;
|
|
53
53
|
powerbar?: boolean;
|
|
54
|
+
dashboardPlacement?: "center" | "right";
|
|
55
|
+
dashboardWidth?: number;
|
|
56
|
+
showModel?: boolean;
|
|
57
|
+
showTokens?: boolean;
|
|
58
|
+
showTools?: boolean;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
export interface AgentOverrideConfig {
|
|
@@ -309,6 +314,11 @@ function parseUiConfig(value: unknown): CrewUiConfig | undefined {
|
|
|
309
314
|
widgetPlacement: obj.widgetPlacement === "aboveEditor" || obj.widgetPlacement === "belowEditor" ? obj.widgetPlacement : undefined,
|
|
310
315
|
widgetMaxLines: parsePositiveInteger(obj.widgetMaxLines, 50),
|
|
311
316
|
powerbar: typeof obj.powerbar === "boolean" ? obj.powerbar : undefined,
|
|
317
|
+
dashboardPlacement: obj.dashboardPlacement === "center" || obj.dashboardPlacement === "right" ? obj.dashboardPlacement : undefined,
|
|
318
|
+
dashboardWidth: parsePositiveInteger(obj.dashboardWidth, 120),
|
|
319
|
+
showModel: typeof obj.showModel === "boolean" ? obj.showModel : undefined,
|
|
320
|
+
showTokens: typeof obj.showTokens === "boolean" ? obj.showTokens : undefined,
|
|
321
|
+
showTools: typeof obj.showTools === "boolean" ? obj.showTools : undefined,
|
|
312
322
|
};
|
|
313
323
|
return Object.values(ui).some((entry) => entry !== undefined) ? ui : undefined;
|
|
314
324
|
}
|
|
@@ -112,6 +112,25 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
112
112
|
let cleanedUp = false;
|
|
113
113
|
const widgetState: CrewWidgetState = { frame: 0 };
|
|
114
114
|
const foregroundControllers = new Set<AbortController>();
|
|
115
|
+
const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>): void => {
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
foregroundControllers.add(controller);
|
|
118
|
+
setImmediate(() => {
|
|
119
|
+
void runner(controller.signal)
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
ctx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
|
|
123
|
+
})
|
|
124
|
+
.finally(() => {
|
|
125
|
+
foregroundControllers.delete(controller);
|
|
126
|
+
if (currentCtx) {
|
|
127
|
+
const config = loadConfig(currentCtx.cwd).config.ui;
|
|
128
|
+
updateCrewWidget(currentCtx, widgetState, config);
|
|
129
|
+
updatePiCrewPowerbar(pi.events, currentCtx.cwd, config);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
};
|
|
115
134
|
registerAutonomousPolicy(pi);
|
|
116
135
|
rpcHandle = registerPiCrewRpc((pi as unknown as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events, () => currentCtx);
|
|
117
136
|
const cleanupRuntime = (): void => {
|
|
@@ -163,7 +182,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
163
182
|
const abort = (): void => controller.abort();
|
|
164
183
|
signal?.addEventListener("abort", abort, { once: true });
|
|
165
184
|
try {
|
|
166
|
-
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal });
|
|
185
|
+
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner) => startForegroundRun(ctx, runner) });
|
|
167
186
|
const config = loadConfig(ctx.cwd).config.ui;
|
|
168
187
|
updateCrewWidget(ctx, widgetState, config);
|
|
169
188
|
updatePiCrewPowerbar(pi.events, ctx.cwd, config);
|
|
@@ -188,7 +207,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
188
207
|
pi.registerCommand("team-run", {
|
|
189
208
|
description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
|
|
190
209
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
191
|
-
const result = await handleTeamTool(parseRunArgs(args), ctx);
|
|
210
|
+
const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner) => startForegroundRun(ctx as ExtensionContext, runner) });
|
|
192
211
|
await notifyCommandResult(ctx, commandText(result));
|
|
193
212
|
},
|
|
194
213
|
});
|
|
@@ -363,9 +382,12 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
363
382
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
364
383
|
for (;;) {
|
|
365
384
|
const runs = listRuns(ctx.cwd).slice(0, 50);
|
|
366
|
-
const
|
|
385
|
+
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
386
|
+
const rightPanel = uiConfig?.dashboardPlacement !== "center";
|
|
387
|
+
const width = rightPanel ? Math.min(120, Math.max(32, uiConfig?.dashboardWidth ?? 52)) : "90%";
|
|
388
|
+
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), {
|
|
367
389
|
overlay: true,
|
|
368
|
-
overlayOptions: { width:
|
|
390
|
+
overlayOptions: { width, minWidth: rightPanel ? 32 : undefined, maxHeight: "90%", anchor: rightPanel ? "right-center" : "center", offsetX: rightPanel ? -1 : 0, margin: rightPanel ? { top: 1, right: 1, bottom: 1, left: 0 } : 2 },
|
|
369
391
|
});
|
|
370
392
|
if (!selection) return;
|
|
371
393
|
if (selection.action === "reload") continue;
|
|
@@ -11,18 +11,34 @@ function readManifest(filePath: string): TeamRunManifest | undefined {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function collectRuns(root: string): TeamRunManifest[] {
|
|
14
|
+
function collectRuns(root: string, maxEntries?: number): TeamRunManifest[] {
|
|
15
15
|
const runsRoot = path.join(root, "state", "runs");
|
|
16
16
|
if (!fs.existsSync(runsRoot)) return [];
|
|
17
|
-
|
|
17
|
+
const entries = fs.readdirSync(runsRoot).sort((a, b) => b.localeCompare(a));
|
|
18
|
+
const selected = maxEntries !== undefined ? entries.slice(0, Math.max(0, maxEntries)) : entries;
|
|
19
|
+
return selected
|
|
18
20
|
.map((entry) => readManifest(path.join(runsRoot, entry, "manifest.json")))
|
|
19
21
|
.filter((manifest): manifest is TeamRunManifest => manifest !== undefined);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
const projectRuns = collectRuns(path.join(projectPiRoot(cwd), "teams"));
|
|
24
|
-
const userRuns = collectRuns(path.join(userPiRoot(), "extensions", "pi-crew", "runs"));
|
|
24
|
+
function mergeRuns(userRuns: TeamRunManifest[], projectRuns: TeamRunManifest[], max?: number): TeamRunManifest[] {
|
|
25
25
|
const byId = new Map<string, TeamRunManifest>();
|
|
26
26
|
for (const run of [...userRuns, ...projectRuns]) byId.set(run.runId, run);
|
|
27
|
-
|
|
27
|
+
const sorted = [...byId.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
28
|
+
return max !== undefined ? sorted.slice(0, Math.max(0, max)) : sorted;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function listRuns(cwd: string): TeamRunManifest[] {
|
|
32
|
+
return mergeRuns(
|
|
33
|
+
collectRuns(path.join(userPiRoot(), "extensions", "pi-crew", "runs")),
|
|
34
|
+
collectRuns(path.join(projectPiRoot(cwd), "teams")),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function listRecentRuns(cwd: string, max = 20): TeamRunManifest[] {
|
|
39
|
+
return mergeRuns(
|
|
40
|
+
collectRuns(path.join(userPiRoot(), "extensions", "pi-crew", "runs"), max),
|
|
41
|
+
collectRuns(path.join(projectPiRoot(cwd), "teams"), max),
|
|
42
|
+
max,
|
|
43
|
+
);
|
|
28
44
|
}
|
|
@@ -35,7 +35,7 @@ import { formatValidationReport, validateResources } from "./validate-resources.
|
|
|
35
35
|
import { formatRecommendation, recommendTeam } from "./team-recommendation.ts";
|
|
36
36
|
import { toolResult, type PiTeamsToolResult } from "./tool-result.ts";
|
|
37
37
|
import { touchWorkerHeartbeat } from "../runtime/worker-heartbeat.ts";
|
|
38
|
-
import { agentEventsPath, agentOutputPath, readCrewAgentEvents, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
38
|
+
import { agentEventsPath, agentOutputPath, readCrewAgentEvents, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents, recordFromTask, saveCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
39
39
|
import { resolveCrewRuntime } from "../runtime/runtime-resolver.ts";
|
|
40
40
|
import { probeLiveSessionRuntime } from "../runtime/live-session-runtime.ts";
|
|
41
41
|
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../runtime/agent-control.ts";
|
|
@@ -57,6 +57,7 @@ type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext
|
|
|
57
57
|
sessionManager?: { getBranch?: () => unknown[] };
|
|
58
58
|
events?: { emit?: (event: string, data: unknown) => void };
|
|
59
59
|
signal?: AbortSignal;
|
|
60
|
+
startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>) => void;
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
@@ -307,6 +308,24 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
307
308
|
const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
|
|
308
309
|
const executeWorkers = runtime.kind === "child-process";
|
|
309
310
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
311
|
+
if (executeWorkers && ctx.startForegroundRun) {
|
|
312
|
+
ctx.startForegroundRun(async (signal) => {
|
|
313
|
+
await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal });
|
|
314
|
+
});
|
|
315
|
+
const text = [
|
|
316
|
+
`Started foreground pi-crew run ${updatedManifest.runId}.`,
|
|
317
|
+
`Team: ${team.name}`,
|
|
318
|
+
`Workflow: ${workflow.name}`,
|
|
319
|
+
"Status: running",
|
|
320
|
+
`Tasks: ${tasks.length}`,
|
|
321
|
+
`Runtime: ${runtime.kind}`,
|
|
322
|
+
`State: ${updatedManifest.stateRoot}`,
|
|
323
|
+
`Artifacts: ${updatedManifest.artifactsRoot}`,
|
|
324
|
+
"",
|
|
325
|
+
"The run continues in this Pi session without blocking the chat. It will be interrupted on session shutdown. Use /team-dashboard or /team-status to watch it.",
|
|
326
|
+
].join("\n");
|
|
327
|
+
return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
|
|
328
|
+
}
|
|
310
329
|
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
|
|
311
330
|
const text = [
|
|
312
331
|
`Created pi-crew run ${executed.manifest.runId}.`,
|
|
@@ -409,6 +428,8 @@ export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
409
428
|
}
|
|
410
429
|
const tasks = loaded.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." } : task);
|
|
411
430
|
saveRunTasks(loaded.manifest, tasks);
|
|
431
|
+
try { saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process"))); } catch {}
|
|
432
|
+
try { writeForegroundInterruptRequest(loaded.manifest, "Run cancelled by user request."); } catch {}
|
|
412
433
|
const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
|
|
413
434
|
return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
|
|
414
435
|
});
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -8,8 +8,17 @@ import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
|
8
8
|
const POST_EXIT_STDIO_GUARD_MS = 3000;
|
|
9
9
|
const FINAL_DRAIN_MS = 5000;
|
|
10
10
|
const HARD_KILL_MS = 3000;
|
|
11
|
+
const MAX_CAPTURE_BYTES = 256 * 1024;
|
|
11
12
|
const activeChildProcesses = new Map<number, ChildProcess>();
|
|
12
13
|
|
|
14
|
+
function appendBoundedTail(current: string, chunk: string, maxBytes = MAX_CAPTURE_BYTES): string {
|
|
15
|
+
const combined = current + chunk;
|
|
16
|
+
if (Buffer.byteLength(combined, "utf-8") <= maxBytes) return combined;
|
|
17
|
+
let tail = combined.slice(Math.max(0, combined.length - maxBytes));
|
|
18
|
+
while (Buffer.byteLength(tail, "utf-8") > maxBytes) tail = tail.slice(1024);
|
|
19
|
+
return `[pi-crew captured output truncated to last ${Math.round(maxBytes / 1024)} KiB]\n${tail}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
function killProcessTree(pid: number | undefined): void {
|
|
14
23
|
if (!pid || !Number.isInteger(pid) || pid <= 0) return;
|
|
15
24
|
try {
|
|
@@ -59,6 +68,63 @@ function appendTranscript(input: ChildPiRunInput, line: string): void {
|
|
|
59
68
|
fs.appendFileSync(input.transcriptPath, `${line}\n`, "utf-8");
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
function compactContentPart(part: unknown): unknown | undefined {
|
|
72
|
+
const record = asRecord(part);
|
|
73
|
+
if (!record) return undefined;
|
|
74
|
+
if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? record.text : "" };
|
|
75
|
+
if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: record.input };
|
|
76
|
+
if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: record.content };
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function compactChildPiEvent(event: unknown): unknown | undefined {
|
|
81
|
+
const record = asRecord(event);
|
|
82
|
+
if (!record) return undefined;
|
|
83
|
+
if (record.type === "message_update") return undefined;
|
|
84
|
+
if (record.type === "tool_execution_start" || record.type === "tool_execution_end") {
|
|
85
|
+
return { type: record.type, toolName: record.toolName, args: record.args };
|
|
86
|
+
}
|
|
87
|
+
if (record.type === "tool_result_end" || record.type === "message_end" || record.type === "message") {
|
|
88
|
+
const message = asRecord(record.message);
|
|
89
|
+
const content = Array.isArray(message?.content) ? message.content.map(compactContentPart).filter((part) => part !== undefined) : undefined;
|
|
90
|
+
return {
|
|
91
|
+
type: record.type,
|
|
92
|
+
...(typeof record.text === "string" ? { text: record.text } : {}),
|
|
93
|
+
...(message ? { message: { role: message.role, ...(content ? { content } : {}), usage: message.usage, model: message.model, errorMessage: message.errorMessage, stopReason: message.stopReason } } : {}),
|
|
94
|
+
usage: record.usage,
|
|
95
|
+
model: record.model,
|
|
96
|
+
provider: record.provider,
|
|
97
|
+
stopReason: record.stopReason,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return record.type ? { type: record.type } : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function displayTextFromCompactEvent(event: unknown): string | undefined {
|
|
104
|
+
const record = asRecord(event);
|
|
105
|
+
if (!record) return undefined;
|
|
106
|
+
if (record.type === "tool_execution_start") {
|
|
107
|
+
return typeof record.toolName === "string" ? `tool: ${record.toolName}` : "tool started";
|
|
108
|
+
}
|
|
109
|
+
const message = asRecord(record.message);
|
|
110
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
111
|
+
const text = content.flatMap((part) => {
|
|
112
|
+
const item = asRecord(part);
|
|
113
|
+
return item?.type === "text" && typeof item.text === "string" ? [item.text] : [];
|
|
114
|
+
}).join("\n").trim();
|
|
115
|
+
return text || (typeof record.text === "string" ? record.text : undefined);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function compactChildPiLine(line: string): { persistedLine: string; event?: unknown; displayLine?: string; json: boolean } {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(line);
|
|
121
|
+
const compact = compactChildPiEvent(parsed);
|
|
122
|
+
return { json: true, event: compact, persistedLine: compact ? JSON.stringify(compact) : "", displayLine: displayTextFromCompactEvent(compact) };
|
|
123
|
+
} catch {
|
|
124
|
+
return { json: false, persistedLine: line, displayLine: line };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
62
128
|
export class ChildPiLineObserver {
|
|
63
129
|
private buffer = "";
|
|
64
130
|
private readonly input: ChildPiRunInput;
|
|
@@ -83,13 +149,10 @@ export class ChildPiLineObserver {
|
|
|
83
149
|
|
|
84
150
|
private emitLine(line: string): void {
|
|
85
151
|
if (!line.trim()) return;
|
|
86
|
-
|
|
87
|
-
this.input.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} catch {
|
|
91
|
-
// Raw stdout is allowed.
|
|
92
|
-
}
|
|
152
|
+
const compact = compactChildPiLine(line);
|
|
153
|
+
if (compact.event !== undefined) this.input.onJsonEvent?.(compact.event);
|
|
154
|
+
if (compact.persistedLine) appendTranscript(this.input, compact.persistedLine);
|
|
155
|
+
if (compact.displayLine?.trim()) this.input.onStdoutLine?.(compact.displayLine);
|
|
93
156
|
}
|
|
94
157
|
}
|
|
95
158
|
|
|
@@ -156,6 +219,10 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
156
219
|
let forcedFinalDrain = false;
|
|
157
220
|
const lineObserver = new ChildPiLineObserver({
|
|
158
221
|
...input,
|
|
222
|
+
onStdoutLine: (line) => {
|
|
223
|
+
stdout = appendBoundedTail(stdout, `${line}\n`);
|
|
224
|
+
input.onStdoutLine?.(line);
|
|
225
|
+
},
|
|
159
226
|
onJsonEvent: (event) => {
|
|
160
227
|
input.onJsonEvent?.(event);
|
|
161
228
|
if (!isFinalAssistantEvent(event) || childExited || settled || finalDrainTimer) return;
|
|
@@ -202,12 +269,10 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
202
269
|
|
|
203
270
|
input.signal?.addEventListener("abort", abort, { once: true });
|
|
204
271
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
205
|
-
|
|
206
|
-
stdout += text;
|
|
207
|
-
lineObserver.observe(text);
|
|
272
|
+
lineObserver.observe(chunk.toString("utf-8"));
|
|
208
273
|
});
|
|
209
274
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
210
|
-
stderr
|
|
275
|
+
stderr = appendBoundedTail(stderr, chunk.toString("utf-8"));
|
|
211
276
|
});
|
|
212
277
|
child.on("error", (error) => {
|
|
213
278
|
settle({ exitCode: null, stdout, stderr, error: error.message });
|
|
@@ -52,8 +52,13 @@ export function readCrewAgentStatus(manifest: TeamRunManifest, taskOrAgentId: st
|
|
|
52
52
|
return readJsonFile<CrewAgentRecord>(agentStatusPath(manifest, taskId));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const agentEventSeqCache = new Map<string, { size: number; mtimeMs: number; seq: number }>();
|
|
56
|
+
|
|
55
57
|
function nextAgentEventSeq(filePath: string): number {
|
|
56
58
|
if (!fs.existsSync(filePath)) return 1;
|
|
59
|
+
const stat = fs.statSync(filePath);
|
|
60
|
+
const cached = agentEventSeqCache.get(filePath);
|
|
61
|
+
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) return cached.seq + 1;
|
|
57
62
|
let max = 0;
|
|
58
63
|
for (const line of fs.readFileSync(filePath, "utf-8").split(/\r?\n/)) {
|
|
59
64
|
if (!line.trim()) continue;
|
|
@@ -65,13 +70,19 @@ function nextAgentEventSeq(filePath: string): number {
|
|
|
65
70
|
max += 1;
|
|
66
71
|
}
|
|
67
72
|
}
|
|
73
|
+
agentEventSeqCache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: max });
|
|
68
74
|
return max + 1;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
export function appendCrewAgentEvent(manifest: TeamRunManifest, taskId: string, event: unknown): void {
|
|
72
78
|
fs.mkdirSync(agentStateDir(manifest, taskId), { recursive: true });
|
|
73
79
|
const filePath = agentEventsPath(manifest, taskId);
|
|
74
|
-
|
|
80
|
+
const seq = nextAgentEventSeq(filePath);
|
|
81
|
+
fs.appendFileSync(filePath, `${JSON.stringify({ seq, time: new Date().toISOString(), event })}\n`, "utf-8");
|
|
82
|
+
try {
|
|
83
|
+
const stat = fs.statSync(filePath);
|
|
84
|
+
agentEventSeqCache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq });
|
|
85
|
+
} catch {}
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
export interface CrewAgentEventCursorOptions {
|
|
@@ -185,6 +185,24 @@ function applyUsageToProgress(progress: CrewAgentProgress | undefined, usage: Us
|
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
function shouldFlushProgressEvent(event: unknown): boolean {
|
|
189
|
+
const type = asRecord(event)?.type;
|
|
190
|
+
return type === "tool_execution_start" || type === "tool_execution_end" || type === "message_end" || type === "tool_result_end";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function progressEventSummary(task: TeamTaskState, event: unknown): Record<string, unknown> {
|
|
194
|
+
const type = asRecord(event)?.type;
|
|
195
|
+
return {
|
|
196
|
+
eventType: typeof type === "string" ? type : "event",
|
|
197
|
+
currentTool: task.agentProgress?.currentTool,
|
|
198
|
+
toolCount: task.agentProgress?.toolCount,
|
|
199
|
+
tokens: task.agentProgress?.tokens,
|
|
200
|
+
turns: task.agentProgress?.turns,
|
|
201
|
+
activityState: task.agentProgress?.activityState,
|
|
202
|
+
lastActivityAt: task.agentProgress?.lastActivityAt,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, startedAt: string | undefined): CrewAgentProgress {
|
|
189
207
|
const obj = asRecord(event);
|
|
190
208
|
const now = new Date().toISOString();
|
|
@@ -275,6 +293,19 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
275
293
|
let finalStderr = "";
|
|
276
294
|
modelAttempts = [];
|
|
277
295
|
const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
|
|
296
|
+
let lastAgentRecordPersistedAt = 0;
|
|
297
|
+
let lastRunProgressPersistedAt = 0;
|
|
298
|
+
const persistChildProgress = (event: unknown, force = false): void => {
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
if (force || shouldFlushProgressEvent(event) || now - lastAgentRecordPersistedAt >= 500) {
|
|
301
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
|
|
302
|
+
lastAgentRecordPersistedAt = now;
|
|
303
|
+
}
|
|
304
|
+
if (force || shouldFlushProgressEvent(event) || now - lastRunProgressPersistedAt >= 1000) {
|
|
305
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: progressEventSummary(task, event) });
|
|
306
|
+
lastRunProgressPersistedAt = now;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
278
309
|
for (let i = 0; i < attemptModels.length; i++) {
|
|
279
310
|
const model = attemptModels[i];
|
|
280
311
|
const attemptStartedAt = new Date();
|
|
@@ -291,8 +322,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
291
322
|
appendCrewAgentEvent(manifest, task.id, event);
|
|
292
323
|
task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
|
|
293
324
|
tasks = updateTask(tasks, task);
|
|
294
|
-
|
|
295
|
-
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { event } });
|
|
325
|
+
persistChildProgress(event);
|
|
296
326
|
},
|
|
297
327
|
});
|
|
298
328
|
startupEvidence = createStartupEvidence({ command: "pi", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: childResult.exitCode === 0 && !childResult.error, stderr: childResult.stderr, error: childResult.error, exitCode: childResult.exitCode });
|
|
@@ -301,6 +331,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
301
331
|
finalStderr = childResult.stderr;
|
|
302
332
|
parsedOutput = parsePiJsonOutput(childResult.stdout);
|
|
303
333
|
error = childResult.error || (childResult.exitCode && childResult.exitCode !== 0 ? childResult.stderr || `Child Pi exited with ${childResult.exitCode}` : undefined);
|
|
334
|
+
persistChildProgress({ type: "attempt_finished" }, true);
|
|
304
335
|
const attempt: ModelAttemptSummary = { model: model ?? "default", success: !error, exitCode, error };
|
|
305
336
|
modelAttempts.push(attempt);
|
|
306
337
|
logs.push(`MODEL ATTEMPT ${i + 1}: ${attempt.model}`, `success=${attempt.success}`, `exitCode=${attempt.exitCode ?? "null"}`, attempt.error ? `error=${attempt.error}` : "", "");
|
|
@@ -339,6 +370,19 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
339
370
|
}
|
|
340
371
|
} else if (runtimeKind === "live-session") {
|
|
341
372
|
const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
|
|
373
|
+
let lastAgentRecordPersistedAt = 0;
|
|
374
|
+
let lastRunProgressPersistedAt = 0;
|
|
375
|
+
const persistLiveProgress = (event: unknown, force = false): void => {
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
if (force || shouldFlushProgressEvent(event) || now - lastAgentRecordPersistedAt >= 500) {
|
|
378
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, "live-session"));
|
|
379
|
+
lastAgentRecordPersistedAt = now;
|
|
380
|
+
}
|
|
381
|
+
if (force || shouldFlushProgressEvent(event) || now - lastRunProgressPersistedAt >= 1000) {
|
|
382
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: progressEventSummary(task, event) });
|
|
383
|
+
lastRunProgressPersistedAt = now;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
342
386
|
const attemptStartedAt = new Date();
|
|
343
387
|
const liveResult = await runLiveSessionTask({
|
|
344
388
|
manifest,
|
|
@@ -357,8 +401,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
357
401
|
appendCrewAgentEvent(manifest, task.id, event);
|
|
358
402
|
task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
|
|
359
403
|
tasks = updateTask(tasks, task);
|
|
360
|
-
|
|
361
|
-
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { event } });
|
|
404
|
+
persistLiveProgress(event);
|
|
362
405
|
},
|
|
363
406
|
});
|
|
364
407
|
startupEvidence = createStartupEvidence({ command: "live-session", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: liveResult.exitCode === 0 && !liveResult.error, stderr: liveResult.stderr, error: liveResult.error, exitCode: liveResult.exitCode });
|
|
@@ -366,6 +409,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
366
409
|
error = liveResult.error || (liveResult.exitCode && liveResult.exitCode !== 0 ? liveResult.stderr || `Live session exited with ${liveResult.exitCode}` : undefined);
|
|
367
410
|
parsedOutput = { finalText: liveResult.stdout, textEvents: liveResult.stdout ? [liveResult.stdout] : [], jsonEvents: liveResult.jsonEvents, usage: liveResult.usage };
|
|
368
411
|
if (liveResult.usage) task = { ...task, usage: liveResult.usage, agentProgress: applyUsageToProgress(task.agentProgress, liveResult.usage) };
|
|
412
|
+
persistLiveProgress({ type: "attempt_finished" }, true);
|
|
369
413
|
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
370
414
|
kind: "result",
|
|
371
415
|
relativePath: `results/${task.id}.txt`,
|
package/src/state/event-log.ts
CHANGED
|
@@ -42,8 +42,13 @@ export type AppendTeamEvent = Omit<TeamEvent, "time" | "metadata"> & { metadata?
|
|
|
42
42
|
|
|
43
43
|
const TERMINAL_EVENT_TYPES = new Set(["run.blocked", "run.completed", "run.failed", "run.cancelled", "task.completed", "task.failed", "task.cancelled", "task.skipped"]);
|
|
44
44
|
|
|
45
|
+
const sequenceCache = new Map<string, { size: number; mtimeMs: number; seq: number }>();
|
|
46
|
+
|
|
45
47
|
function nextSequence(eventsPath: string): number {
|
|
46
48
|
if (!fs.existsSync(eventsPath)) return 1;
|
|
49
|
+
const stat = fs.statSync(eventsPath);
|
|
50
|
+
const cached = sequenceCache.get(eventsPath);
|
|
51
|
+
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) return cached.seq + 1;
|
|
47
52
|
let max = 0;
|
|
48
53
|
for (const line of fs.readFileSync(eventsPath, "utf-8").split("\n")) {
|
|
49
54
|
if (!line.trim()) continue;
|
|
@@ -54,6 +59,7 @@ function nextSequence(eventsPath: string): number {
|
|
|
54
59
|
max += 1;
|
|
55
60
|
}
|
|
56
61
|
}
|
|
62
|
+
sequenceCache.set(eventsPath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: max });
|
|
57
63
|
return max + 1;
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -82,6 +88,10 @@ export function appendEvent(eventsPath: string, event: AppendTeamEvent): TeamEve
|
|
|
82
88
|
fullEvent.metadata = metadata;
|
|
83
89
|
}
|
|
84
90
|
fs.appendFileSync(eventsPath, `${JSON.stringify(fullEvent)}\n`, "utf-8");
|
|
91
|
+
try {
|
|
92
|
+
const stat = fs.statSync(eventsPath);
|
|
93
|
+
sequenceCache.set(eventsPath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: fullEvent.metadata?.seq ?? 0 });
|
|
94
|
+
} catch {}
|
|
85
95
|
return fullEvent;
|
|
86
96
|
}
|
|
87
97
|
|
package/src/ui/crew-widget.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { CrewUiConfig } from "../config/config.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { listRecentRuns } from "../extension/run-index.ts";
|
|
4
4
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
5
5
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
6
|
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
@@ -106,8 +106,7 @@ function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function activeWidgetRuns(cwd: string): WidgetRun[] {
|
|
109
|
-
|
|
110
|
-
return runs.map((run) => ({ run, agents: agentsFor(run) })).filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
109
|
+
return listRecentRuns(cwd, 20).map((run) => ({ run, agents: agentsFor(run) })).filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
function statusSummary(runs: WidgetRun[]): string {
|
|
@@ -136,8 +135,8 @@ function shortRunLabel(run: TeamRunManifest): string {
|
|
|
136
135
|
return `${run.team}/${run.workflow ?? "none"}`;
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8): string[] {
|
|
140
|
-
const runs = activeWidgetRuns(cwd);
|
|
138
|
+
export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8, providedRuns?: WidgetRun[]): string[] {
|
|
139
|
+
const runs = providedRuns ?? activeWidgetRuns(cwd);
|
|
141
140
|
if (!runs.length) return [];
|
|
142
141
|
const runningGlyph = SPINNER[frame % SPINNER.length] ?? "⠋";
|
|
143
142
|
const lines: string[] = [widgetHeader(runs, runningGlyph)];
|
|
@@ -193,9 +192,10 @@ export function updateCrewWidget(ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "
|
|
|
193
192
|
if (!ctx.hasUI) return;
|
|
194
193
|
state.frame += 1;
|
|
195
194
|
const maxLines = config?.widgetMaxLines ?? 10;
|
|
196
|
-
const
|
|
195
|
+
const runs = activeWidgetRuns(ctx.cwd);
|
|
196
|
+
const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs);
|
|
197
197
|
const placement = config?.widgetPlacement ?? "aboveEditor";
|
|
198
|
-
ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(
|
|
198
|
+
ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
|
|
199
199
|
ctx.ui.setWidget(LEGACY_WIDGET_KEY, undefined, { placement });
|
|
200
200
|
if (!lines.length) {
|
|
201
201
|
ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CrewUiConfig } from "../config/config.ts";
|
|
2
|
-
import {
|
|
2
|
+
import { listRecentRuns } from "../extension/run-index.ts";
|
|
3
3
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
4
4
|
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
5
5
|
|
|
@@ -17,7 +17,7 @@ export function registerPiCrewPowerbarSegments(events: EventBus, config?: CrewUi
|
|
|
17
17
|
|
|
18
18
|
export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: CrewUiConfig): void {
|
|
19
19
|
if (config?.powerbar === false) return;
|
|
20
|
-
const active =
|
|
20
|
+
const active = listRecentRuns(cwd, 20).map((run) => {
|
|
21
21
|
let agents = [] as ReturnType<typeof readCrewAgents>;
|
|
22
22
|
try { agents = readCrewAgents(run); } catch {}
|
|
23
23
|
return { run, agents };
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
2
|
+
import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
3
3
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
4
4
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
5
5
|
import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
|
@@ -12,6 +12,12 @@ interface DashboardComponent {
|
|
|
12
12
|
|
|
13
13
|
type DashboardTheme = { fg?: (color: string, text: string) => string; bold?: (text: string) => string };
|
|
14
14
|
|
|
15
|
+
export interface RunDashboardOptions {
|
|
16
|
+
showModel?: boolean;
|
|
17
|
+
showTokens?: boolean;
|
|
18
|
+
showTools?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "events" | "agents" | "agent-events" | "agent-output" | "agent-transcript" | "reload";
|
|
16
22
|
export interface RunDashboardSelection {
|
|
17
23
|
runId: string;
|
|
@@ -89,12 +95,44 @@ function formatAge(iso: string | undefined): string | undefined {
|
|
|
89
95
|
return `${Math.floor(ms / 3_600_000)}h`;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
function
|
|
98
|
+
function readRunTasks(run: TeamRunManifest): TeamTaskState[] {
|
|
99
|
+
try {
|
|
100
|
+
if (!fs.existsSync(run.tasksPath)) return [];
|
|
101
|
+
const parsed = JSON.parse(fs.readFileSync(run.tasksPath, "utf-8"));
|
|
102
|
+
return Array.isArray(parsed) ? parsed as TeamTaskState[] : [];
|
|
103
|
+
} catch { return []; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatTokens(usage: UsageState | undefined): string | undefined {
|
|
107
|
+
if (!usage) return undefined;
|
|
108
|
+
const total = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
109
|
+
if (!total) return undefined;
|
|
110
|
+
const compact = total >= 1000 ? `${(total / 1000).toFixed(total >= 10_000 ? 0 : 1)}k` : `${total}`;
|
|
111
|
+
const parts = [`tok=${compact}`];
|
|
112
|
+
if (usage.input) parts.push(`in=${usage.input}`);
|
|
113
|
+
if (usage.output) parts.push(`out=${usage.output}`);
|
|
114
|
+
if (usage.cacheRead) parts.push(`cache=${usage.cacheRead}`);
|
|
115
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
116
|
+
return parts.join("/");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function taskForAgent(tasks: TeamTaskState[], agent: CrewAgentRecord): TeamTaskState | undefined {
|
|
120
|
+
return tasks.find((task) => task.id === agent.taskId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function modelForTask(task: TeamTaskState | undefined): string | undefined {
|
|
124
|
+
const attempts = task?.modelAttempts;
|
|
125
|
+
if (!attempts?.length) return undefined;
|
|
126
|
+
return attempts.find((attempt) => attempt.success)?.model ?? attempts.at(-1)?.model;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function agentPreviewLine(agent: CrewAgentRecord, task: TeamTaskState | undefined, options: RunDashboardOptions): string {
|
|
93
130
|
const stats = [
|
|
94
131
|
agent.progress?.activityState,
|
|
95
|
-
|
|
96
|
-
agent.
|
|
97
|
-
agent.progress?.
|
|
132
|
+
options.showModel !== false && modelForTask(task) ? `model=${modelForTask(task)}` : undefined,
|
|
133
|
+
options.showTokens !== false ? formatTokens(task?.usage) ?? (agent.progress?.tokens !== undefined ? `tok=${agent.progress.tokens}` : undefined) : undefined,
|
|
134
|
+
options.showTools !== false && agent.progress?.currentTool ? `tool=${agent.progress.currentTool}` : undefined,
|
|
135
|
+
options.showTools !== false && agent.toolUses !== undefined ? `${agent.toolUses} tools` : undefined,
|
|
98
136
|
agent.progress?.turns !== undefined ? `${agent.progress.turns} turns` : undefined,
|
|
99
137
|
agent.progress?.failedTool ? `failedTool=${agent.progress.failedTool}` : undefined,
|
|
100
138
|
agent.startedAt ? `age=${formatAge(agent.completedAt ?? agent.startedAt)}` : undefined,
|
|
@@ -103,11 +141,21 @@ function agentPreviewLine(agent: CrewAgentRecord): string {
|
|
|
103
141
|
return `Agent: ${statusIcon(agent.status)} ${agent.taskId} ${agent.role}->${agent.agent}${stats.length ? ` · ${stats.join(" · ")}` : ""}${recent ? ` ⎿ ${recent}` : ""}`;
|
|
104
142
|
}
|
|
105
143
|
|
|
106
|
-
function readAgentPreview(run: TeamRunManifest, maxLines = 5): string[] {
|
|
144
|
+
function readAgentPreview(run: TeamRunManifest, maxLines = 5, options: RunDashboardOptions = {}): string[] {
|
|
107
145
|
try {
|
|
108
146
|
const agents = readCrewAgents(run);
|
|
147
|
+
const tasks = readRunTasks(run);
|
|
109
148
|
if (!agents.length) return ["Agents: (none)"];
|
|
110
|
-
|
|
149
|
+
const totals = tasks.reduce((acc, task) => {
|
|
150
|
+
acc.input += task.usage?.input ?? 0;
|
|
151
|
+
acc.output += task.usage?.output ?? 0;
|
|
152
|
+
acc.cacheRead += task.usage?.cacheRead ?? 0;
|
|
153
|
+
acc.cacheWrite += task.usage?.cacheWrite ?? 0;
|
|
154
|
+
acc.cost += task.usage?.cost ?? 0;
|
|
155
|
+
return acc;
|
|
156
|
+
}, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
157
|
+
const header = formatTokens(totals) ? `Agents: ${formatTokens(totals)}` : "Agents:";
|
|
158
|
+
return [header, ...agents.slice(0, maxLines).map((agent) => agentPreviewLine(agent, taskForAgent(tasks, agent), options)), ...(agents.length > maxLines ? [`Agents: +${agents.length - maxLines} more`] : [])];
|
|
111
159
|
} catch (error) {
|
|
112
160
|
const message = error instanceof Error ? error.message : String(error);
|
|
113
161
|
return [`Agents: failed to read (${message})`];
|
|
@@ -154,11 +202,13 @@ export class RunDashboard implements DashboardComponent {
|
|
|
154
202
|
private readonly runs: TeamRunManifest[];
|
|
155
203
|
private readonly done: (selection: RunDashboardSelection | undefined) => void;
|
|
156
204
|
private readonly theme: DashboardTheme;
|
|
205
|
+
private readonly options: RunDashboardOptions;
|
|
157
206
|
|
|
158
|
-
constructor(runs: TeamRunManifest[], done: (selection: RunDashboardSelection | undefined) => void, theme: unknown = {}) {
|
|
207
|
+
constructor(runs: TeamRunManifest[], done: (selection: RunDashboardSelection | undefined) => void, theme: unknown = {}, options: RunDashboardOptions = {}) {
|
|
159
208
|
this.runs = runs;
|
|
160
209
|
this.done = done;
|
|
161
210
|
this.theme = theme as DashboardTheme;
|
|
211
|
+
this.options = options;
|
|
162
212
|
}
|
|
163
213
|
|
|
164
214
|
invalidate(): void {}
|
|
@@ -203,7 +253,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
203
253
|
selectedRun.async ? `Async: pid=${selectedRun.async.pid ?? "unknown"} log=${selectedRun.async.logPath}` : "Async: no",
|
|
204
254
|
`Goal: ${selectedRun.goal}`,
|
|
205
255
|
];
|
|
206
|
-
for (const detail of [...details, ...readAgentPreview(selectedRun), ...readProgressPreview(selectedRun, this.showFullProgress ? 20 : 5)]) {
|
|
256
|
+
for (const detail of [...details, ...readAgentPreview(selectedRun, this.showFullProgress ? 20 : 8, this.options), ...readProgressPreview(selectedRun, this.showFullProgress ? 20 : 5)]) {
|
|
207
257
|
lines.push(`│ ${padVisible(truncate(detail, innerWidth - 1), innerWidth - 1)}│`);
|
|
208
258
|
}
|
|
209
259
|
}
|