pi-crew 0.1.16 → 0.1.17

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.17
4
+
5
+ - Fixed terminal/completed workers being incorrectly escalated as stale heartbeat blockers after all tasks completed.
6
+ - Cleaned child-process result extraction so result artifacts prefer final assistant output and no longer include worker prompt/context.
7
+ - Made `/team-dashboard` visibly render as a top-right sidebar by default with explicit right-sidebar title text.
8
+ - Added per-subagent model and usage fields to agent records, status output, and dashboard fallbacks so model/token totals stay visible while and after workers run.
9
+
3
10
  ## 0.1.16
4
11
 
5
12
  - Added right-side `/team-dashboard` placement with model, token, and tool detail rows for subagents.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -7,7 +7,7 @@ import { notifyActiveRuns } from "./session-summary.ts";
7
7
  import { piTeamsHelp } from "./help.ts";
8
8
  import { handleTeamManagerCommand } from "./team-manager-command.ts";
9
9
  import { handleTeamTool, type TeamToolDetails } from "./team-tool.ts";
10
- import { listRuns } from "./run-index.ts";
10
+ import { listRecentRuns } from "./run-index.ts";
11
11
  import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
12
12
  import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
13
13
  import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
@@ -381,13 +381,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
381
381
  description: "Open a pi-crew run dashboard overlay",
382
382
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
383
383
  for (;;) {
384
- const runs = listRuns(ctx.cwd).slice(0, 50);
384
+ const runs = listRecentRuns(ctx.cwd, 50);
385
385
  const uiConfig = loadConfig(ctx.cwd).config.ui;
386
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 }), {
387
+ const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
388
+ const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), {
389
389
  overlay: true,
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 },
390
+ overlayOptions: rightPanel
391
+ ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } }
392
+ : { width, maxHeight: "90%", anchor: "center", margin: 2 },
391
393
  });
392
394
  if (!selection) return;
393
395
  if (selection.action === "reload") continue;
@@ -384,7 +384,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
384
384
  ...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
385
385
  `Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
386
386
  "Agents:",
387
- ...(crewAgents.length ? crewAgents.map((agent) => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`) : ["- (none)"]),
387
+ ...(crewAgents.length ? crewAgents.map((agent) => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`) : ["- (none)"]),
388
388
  "Policy decisions:",
389
389
  ...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
390
390
  `Total usage: ${formatUsage(totalUsage)}`,
@@ -9,6 +9,7 @@ const POST_EXIT_STDIO_GUARD_MS = 3000;
9
9
  const FINAL_DRAIN_MS = 5000;
10
10
  const HARD_KILL_MS = 3000;
11
11
  const MAX_CAPTURE_BYTES = 256 * 1024;
12
+ const MAX_COMPACT_CONTENT_CHARS = 4096;
12
13
  const activeChildProcesses = new Map<number, ChildProcess>();
13
14
 
14
15
  function appendBoundedTail(current: string, chunk: string, maxBytes = MAX_CAPTURE_BYTES): string {
@@ -68,12 +69,27 @@ function appendTranscript(input: ChildPiRunInput, line: string): void {
68
69
  fs.appendFileSync(input.transcriptPath, `${line}\n`, "utf-8");
69
70
  }
70
71
 
72
+ function compactString(value: string, maxChars = MAX_COMPACT_CONTENT_CHARS): string {
73
+ if (value.length <= maxChars) return value;
74
+ return `${value.slice(0, maxChars)}\n[pi-crew compacted ${value.length - maxChars} chars]`;
75
+ }
76
+
77
+ function compactValue(value: unknown): unknown {
78
+ if (typeof value === "string") return compactString(value);
79
+ if (Array.isArray(value)) return value.slice(0, 20).map(compactValue);
80
+ const record = asRecord(value);
81
+ if (!record) return value;
82
+ const compacted: Record<string, unknown> = {};
83
+ for (const [key, entry] of Object.entries(record).slice(0, 20)) compacted[key] = compactValue(entry);
84
+ return compacted;
85
+ }
86
+
71
87
  function compactContentPart(part: unknown): unknown | undefined {
72
88
  const record = asRecord(part);
73
89
  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 };
90
+ if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? compactString(record.text) : "" };
91
+ if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: compactValue(record.input) };
92
+ if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: compactValue(record.content) };
77
93
  return undefined;
78
94
  }
79
95
 
@@ -106,7 +122,9 @@ function displayTextFromCompactEvent(event: unknown): string | undefined {
106
122
  if (record.type === "tool_execution_start") {
107
123
  return typeof record.toolName === "string" ? `tool: ${record.toolName}` : "tool started";
108
124
  }
125
+ if (record.type !== "message" && record.type !== "message_end") return undefined;
109
126
  const message = asRecord(record.message);
127
+ if (message?.role !== undefined && message.role !== "assistant") return undefined;
110
128
  const content = Array.isArray(message?.content) ? message.content : [];
111
129
  const text = content.flatMap((part) => {
112
130
  const item = asRecord(part);
@@ -124,6 +124,12 @@ export function emptyCrewAgentProgress(): CrewAgentProgress {
124
124
  return { recentTools: [], recentOutput: [], toolCount: 0 };
125
125
  }
126
126
 
127
+ function modelFromTask(task: TeamTaskState): string | undefined {
128
+ const attempts = task.modelAttempts;
129
+ if (!attempts?.length) return undefined;
130
+ return attempts.find((attempt) => attempt.success)?.model ?? attempts.at(-1)?.model;
131
+ }
132
+
127
133
  export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, runtime: CrewRuntimeKind): CrewAgentRecord {
128
134
  return {
129
135
  id: `${manifest.runId}:${task.id}`,
@@ -142,6 +148,8 @@ export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, r
142
148
  outputPath: agentOutputPath(manifest, task.id),
143
149
  toolUses: task.agentProgress?.toolCount,
144
150
  jsonEvents: task.jsonEvents,
151
+ model: modelFromTask(task),
152
+ usage: task.usage,
145
153
  progress: task.agentProgress,
146
154
  error: task.error,
147
155
  };
@@ -1,4 +1,5 @@
1
1
  import type { TeamTaskStatus } from "../state/contracts.ts";
2
+ import type { UsageState } from "../state/types.ts";
2
3
 
3
4
  export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
4
5
  export type CrewAgentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped";
@@ -41,6 +42,8 @@ export interface CrewAgentRecord {
41
42
  outputPath?: string;
42
43
  toolUses?: number;
43
44
  jsonEvents?: number;
45
+ model?: string;
46
+ usage?: UsageState;
44
47
  progress?: CrewAgentProgress;
45
48
  error?: string;
46
49
  }
@@ -72,13 +72,14 @@ function textFromContent(content: unknown): string[] {
72
72
  function extractText(value: unknown): string[] {
73
73
  const obj = asRecord(value);
74
74
  if (!obj) return [];
75
+ const message = asRecord(obj.message);
76
+ if (message?.role !== undefined && message.role !== "assistant") return [];
75
77
  const text: string[] = [];
76
78
  if (typeof obj.text === "string") text.push(obj.text);
77
79
  if (typeof obj.output === "string") text.push(obj.output);
78
80
  if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
79
81
  if (typeof obj.final_output === "string") text.push(obj.final_output);
80
- text.push(...textFromContent(obj.content));
81
- const message = asRecord(obj.message);
82
+ if (!message) text.push(...textFromContent(obj.content));
82
83
  if (message) text.push(...textFromContent(message.content));
83
84
  return text.filter((entry) => entry.trim().length > 0);
84
85
  }
@@ -56,7 +56,7 @@ export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
56
56
  const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
57
57
  decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
58
58
  }
59
- if (task.heartbeat && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
59
+ if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
60
60
  decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
61
61
  }
62
62
  if (task.taskPacket?.verification) {
@@ -190,6 +190,17 @@ function shouldFlushProgressEvent(event: unknown): boolean {
190
190
  return type === "tool_execution_start" || type === "tool_execution_end" || type === "message_end" || type === "tool_result_end";
191
191
  }
192
192
 
193
+ function cleanResultText(text: string | undefined): string | undefined {
194
+ const trimmed = text?.trim();
195
+ if (!trimmed) return undefined;
196
+ const doneIndex = trimmed.lastIndexOf("\nDONE\n");
197
+ if (doneIndex >= 0) return trimmed.slice(doneIndex + 1).trim();
198
+ if (trimmed === "DONE" || trimmed.startsWith("DONE\n")) return trimmed;
199
+ const fencedPromptIndex = trimmed.lastIndexOf("</file>");
200
+ if (fencedPromptIndex >= 0 && fencedPromptIndex < trimmed.length - 7) return trimmed.slice(fencedPromptIndex + 7).trim() || trimmed;
201
+ return trimmed;
202
+ }
203
+
193
204
  function progressEventSummary(task: TeamTaskState, event: unknown): Record<string, unknown> {
194
205
  const type = asRecord(event)?.type;
195
206
  return {
@@ -309,6 +320,10 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
309
320
  for (let i = 0; i < attemptModels.length; i++) {
310
321
  const model = attemptModels[i];
311
322
  const attemptStartedAt = new Date();
323
+ const pendingAttempt: ModelAttemptSummary = { model: model ?? "default", success: false };
324
+ task = { ...task, modelAttempts: [...modelAttempts, pendingAttempt] };
325
+ tasks = updateTask(tasks, task);
326
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
312
327
  const childResult = await runChildPi({
313
328
  cwd: task.cwd,
314
329
  task: prompt,
@@ -329,11 +344,13 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
329
344
  exitCode = childResult.exitCode;
330
345
  finalStdout = childResult.stdout;
331
346
  finalStderr = childResult.stderr;
332
- parsedOutput = parsePiJsonOutput(childResult.stdout);
347
+ parsedOutput = parsePiJsonOutput(fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : childResult.stdout);
333
348
  error = childResult.error || (childResult.exitCode && childResult.exitCode !== 0 ? childResult.stderr || `Child Pi exited with ${childResult.exitCode}` : undefined);
334
349
  persistChildProgress({ type: "attempt_finished" }, true);
335
350
  const attempt: ModelAttemptSummary = { model: model ?? "default", success: !error, exitCode, error };
336
351
  modelAttempts.push(attempt);
352
+ task = { ...task, modelAttempts: [...modelAttempts] };
353
+ tasks = updateTask(tasks, task);
337
354
  logs.push(`MODEL ATTEMPT ${i + 1}: ${attempt.model}`, `success=${attempt.success}`, `exitCode=${attempt.exitCode ?? "null"}`, attempt.error ? `error=${attempt.error}` : "", "");
338
355
  if (!error) break;
339
356
  const nextModel = attemptModels[i + 1];
@@ -343,7 +360,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
343
360
  resultArtifact = writeArtifact(manifest.artifactsRoot, {
344
361
  kind: "result",
345
362
  relativePath: `results/${task.id}.txt`,
346
- content: parsedOutput?.finalText || finalStdout || finalStderr || "(no output)",
363
+ content: cleanResultText(parsedOutput?.finalText) ?? cleanResultText(finalStdout) ?? cleanResultText(finalStderr) ?? "(no output)",
347
364
  producer: task.id,
348
365
  });
349
366
  logArtifact = writeArtifact(manifest.artifactsRoot, {
@@ -13,6 +13,7 @@ interface DashboardComponent {
13
13
  type DashboardTheme = { fg?: (color: string, text: string) => string; bold?: (text: string) => string };
14
14
 
15
15
  export interface RunDashboardOptions {
16
+ placement?: "center" | "right";
16
17
  showModel?: boolean;
17
18
  showTokens?: boolean;
18
19
  showTools?: boolean;
@@ -126,11 +127,19 @@ function modelForTask(task: TeamTaskState | undefined): string | undefined {
126
127
  return attempts.find((attempt) => attempt.success)?.model ?? attempts.at(-1)?.model;
127
128
  }
128
129
 
130
+ function modelForAgent(agent: CrewAgentRecord, task: TeamTaskState | undefined): string | undefined {
131
+ return modelForTask(task) ?? agent.model;
132
+ }
133
+
134
+ function usageForAgent(agent: CrewAgentRecord, task: TeamTaskState | undefined): UsageState | undefined {
135
+ return task?.usage ?? agent.usage;
136
+ }
137
+
129
138
  function agentPreviewLine(agent: CrewAgentRecord, task: TeamTaskState | undefined, options: RunDashboardOptions): string {
130
139
  const stats = [
131
140
  agent.progress?.activityState,
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,
141
+ options.showModel !== false && modelForAgent(agent, task) ? `model=${modelForAgent(agent, task)}` : undefined,
142
+ options.showTokens !== false ? formatTokens(usageForAgent(agent, task)) ?? (agent.progress?.tokens !== undefined ? `tok=${agent.progress.tokens}` : undefined) : undefined,
134
143
  options.showTools !== false && agent.progress?.currentTool ? `tool=${agent.progress.currentTool}` : undefined,
135
144
  options.showTools !== false && agent.toolUses !== undefined ? `${agent.toolUses} tools` : undefined,
136
145
  agent.progress?.turns !== undefined ? `${agent.progress.turns} turns` : undefined,
@@ -219,9 +228,10 @@ export class RunDashboard implements DashboardComponent {
219
228
  const innerWidth = Math.max(20, width - 4);
220
229
  const borderWidth = Math.min(innerWidth, Math.max(0, width - 2));
221
230
  const border = (text: string) => fg("border", text);
231
+ const title = this.options.placement === "right" ? "pi-crew right sidebar" : "pi-crew dashboard";
222
232
  const lines = [
223
233
  border(`╭${"─".repeat(borderWidth)}╮`),
224
- `│ ${padVisible(truncate(`${fg("accent", "")} ${bold("pi-crew dashboard")}`, innerWidth - 1), innerWidth - 1)}│`,
234
+ `│ ${padVisible(truncate(`${fg("accent", "")} ${bold(title)} ${this.options.placement === "right" ? fg("dim", "anchored top-right") : ""}`, innerWidth - 1), innerWidth - 1)}│`,
225
235
  `│ ${padVisible(truncate(fg("dim", "↑/↓/j/k select • r reload • p progress • s/u/a/i actions • d agents • e/v/o viewers • q close"), innerWidth - 1), innerWidth - 1)}│`,
226
236
  `│ ${padVisible(truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs)}`, innerWidth - 1), innerWidth - 1)}│`,
227
237
  border(`├${"─".repeat(borderWidth)}┤`),