pi-crew 0.1.35 → 0.1.37

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.
Files changed (38) hide show
  1. package/README.md +36 -0
  2. package/docs/architecture.md +8 -1
  3. package/docs/research-phase9-observability-reliability-plan.md +42 -42
  4. package/package.json +1 -1
  5. package/schema.json +42 -0
  6. package/src/config/config.ts +101 -0
  7. package/src/extension/register.ts +65 -2
  8. package/src/extension/registration/commands.ts +14 -3
  9. package/src/extension/registration/team-tool.ts +3 -1
  10. package/src/extension/team-tool/api.ts +27 -2
  11. package/src/extension/team-tool/context.ts +2 -0
  12. package/src/extension/team-tool/run.ts +2 -2
  13. package/src/extension/team-tool.ts +1 -1
  14. package/src/observability/correlation.ts +35 -0
  15. package/src/observability/event-to-metric.ts +54 -0
  16. package/src/observability/exporters/adapter.ts +24 -0
  17. package/src/observability/exporters/otlp-exporter.ts +65 -0
  18. package/src/observability/exporters/prometheus-exporter.ts +47 -0
  19. package/src/observability/metric-registry.ts +72 -0
  20. package/src/observability/metric-retention.ts +46 -0
  21. package/src/observability/metric-sink.ts +51 -0
  22. package/src/observability/metrics-primitives.ts +166 -0
  23. package/src/runtime/crash-recovery.ts +56 -0
  24. package/src/runtime/deadletter.ts +36 -0
  25. package/src/runtime/diagnostic-export.ts +8 -1
  26. package/src/runtime/heartbeat-gradient.ts +28 -0
  27. package/src/runtime/heartbeat-watcher.ts +80 -0
  28. package/src/runtime/retry-executor.ts +59 -0
  29. package/src/runtime/task-runner.ts +14 -1
  30. package/src/runtime/team-runner.ts +57 -5
  31. package/src/schema/config-schema.ts +29 -0
  32. package/src/state/event-log.ts +3 -2
  33. package/src/state/types.ts +7 -0
  34. package/src/ui/dashboard-panes/metrics-pane.ts +34 -0
  35. package/src/ui/heartbeat-aggregator.ts +15 -5
  36. package/src/ui/keybinding-map.ts +4 -2
  37. package/src/ui/run-action-dispatcher.ts +3 -2
  38. package/src/ui/run-dashboard.ts +11 -4
@@ -13,7 +13,7 @@ export const DASHBOARD_KEYS = {
13
13
  reload: ["r"],
14
14
  progressToggle: ["p"],
15
15
  },
16
- pane: { agents: ["1"], progress: ["2"], mailbox: ["3"], output: ["4"], health: ["5"] },
16
+ pane: { agents: ["1"], progress: ["2"], mailbox: ["3"], output: ["4"], health: ["5"], metrics: ["6"] },
17
17
  navigation: { up: ["k", "\u001b[A"], down: ["j", "\u001b[B"] },
18
18
  mailbox: { ack: ["A"], nudge: ["N"], compose: ["C"], preview: ["P"], ackAll: ["X"], openDetail: ["\r", "\n"] },
19
19
  health: { recovery: ["R"], killStale: ["K"], diagnosticExport: ["D"] },
@@ -53,6 +53,7 @@ export type DashboardKeyAction =
53
53
  | "pane-mailbox"
54
54
  | "pane-output"
55
55
  | "pane-health"
56
+ | "pane-metrics"
56
57
  | "up"
57
58
  | "down"
58
59
  | "mailbox-detail"
@@ -61,7 +62,7 @@ export type DashboardKeyAction =
61
62
  | "health-diagnostic-export"
62
63
  | "notifications-dismiss";
63
64
 
64
- export function dashboardActionForKey(data: string, activePane?: "agents" | "progress" | "mailbox" | "output" | "health"): DashboardKeyAction | undefined {
65
+ export function dashboardActionForKey(data: string, activePane?: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics"): DashboardKeyAction | undefined {
65
66
  if (includes(DASHBOARD_KEYS.close, data)) return "close";
66
67
  if (activePane === "mailbox" && includes(DASHBOARD_KEYS.mailbox.openDetail, data)) return "mailbox-detail";
67
68
  if (activePane === "health") {
@@ -86,6 +87,7 @@ export function dashboardActionForKey(data: string, activePane?: "agents" | "pro
86
87
  if (includes(DASHBOARD_KEYS.pane.mailbox, data)) return "pane-mailbox";
87
88
  if (includes(DASHBOARD_KEYS.pane.output, data)) return "pane-output";
88
89
  if (includes(DASHBOARD_KEYS.pane.health, data)) return "pane-health";
90
+ if (includes(DASHBOARD_KEYS.pane.metrics, data)) return "pane-metrics";
89
91
  if (includes(DASHBOARD_KEYS.navigation.up, data)) return "up";
90
92
  if (includes(DASHBOARD_KEYS.navigation.down, data)) return "down";
91
93
  return undefined;
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { MetricRegistry } from "../observability/metric-registry.ts";
2
3
  import { handleTeamTool } from "../extension/team-tool.ts";
3
4
  import { isToolError, textFromToolResult } from "../extension/tool-result.ts";
4
5
  import { loadRunManifestById, saveRunTasks } from "../state/state-store.ts";
@@ -91,9 +92,9 @@ export async function dispatchKillStaleWorkers(ctx: ExtensionContext, runId: str
91
92
  }
92
93
  }
93
94
 
94
- export async function dispatchDiagnosticExport(ctx: ExtensionContext, runId: string): Promise<RunActionResult> {
95
+ export async function dispatchDiagnosticExport(ctx: ExtensionContext, runId: string, options: { registry?: MetricRegistry } = {}): Promise<RunActionResult> {
95
96
  try {
96
- const exported = await exportDiagnostic(ctx, runId);
97
+ const exported = await exportDiagnostic(ctx, runId, options);
97
98
  return { ok: true, message: `Diagnostic exported to ${exported.path}`, data: exported.path };
98
99
  } catch (error) {
99
100
  return err(error);
@@ -17,9 +17,11 @@ import { renderMailboxPane } from "./dashboard-panes/mailbox-pane.ts";
17
17
  import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
18
18
  import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
19
19
  import { renderHealthPane } from "./dashboard-panes/health-pane.ts";
20
+ import { renderMetricsPane } from "./dashboard-panes/metrics-pane.ts";
20
21
  import { dashboardActionForKey } from "./keybinding-map.ts";
21
22
  import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
22
23
  import { spinnerBucket, spinnerFrame } from "./spinner.ts";
24
+ import type { MetricRegistry } from "../observability/metric-registry.ts";
23
25
 
24
26
  interface DashboardComponent {
25
27
  invalidate(): void;
@@ -34,6 +36,7 @@ export interface RunDashboardOptions {
34
36
  showTools?: boolean;
35
37
  snapshotCache?: RunSnapshotCache;
36
38
  runProvider?: () => TeamRunManifest[];
39
+ registry?: MetricRegistry;
37
40
  }
38
41
 
39
42
  export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "events" | "agents" | "agent-events" | "agent-output" | "agent-transcript" | "mailbox" | "reload" | "mailbox-detail" | "health-recovery" | "health-kill-stale" | "health-diagnostic-export" | "notifications-dismiss";
@@ -221,7 +224,7 @@ function countByStatus(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache
221
224
  export class RunDashboard implements DashboardComponent {
222
225
  private selected = 0;
223
226
  private showFullProgress = false;
224
- private activePane: "agents" | "progress" | "mailbox" | "output" | "health" = "agents";
227
+ private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = "agents";
225
228
  private runs: TeamRunManifest[];
226
229
  private readonly done: (selection: RunDashboardSelection | undefined) => void;
227
230
  private readonly theme: CrewTheme;
@@ -265,7 +268,8 @@ export class RunDashboard implements DashboardComponent {
265
268
  if (status === "running" || agents.some((agent) => agent.status === "running")) hasRunning = true;
266
269
  return snapshot?.signature ?? `${displayRun.runId}:${displayRun.status}:${displayRun.updatedAt}:${status}`;
267
270
  }).join("|");
268
- return `${this.selected}:${this.showFullProgress ? 1 : 0}:${this.activePane}:${statuses}${hasRunning ? `:spin=${spinnerBucket()}` : ""}`;
271
+ const metricsSig = this.activePane === "metrics" ? `:metrics=${this.options.registry?.snapshot().length ?? 0}:${spinnerBucket()}` : "";
272
+ return `${this.selected}:${this.showFullProgress ? 1 : 0}:${this.activePane}:${statuses}${hasRunning ? `:spin=${spinnerBucket()}` : ""}${metricsSig}`;
269
273
  }
270
274
 
271
275
  invalidate(): void {
@@ -295,7 +299,7 @@ export class RunDashboard implements DashboardComponent {
295
299
  border("╭", "╮"),
296
300
  `│ ${pad(truncate(`${fg("accent", "▐")} ${this.theme.bold(this.options.placement === "right" ? "pi-crew right sidebar (anchored top-right)" : "pi-crew dashboard")}`, innerWidth - 1), innerWidth - 1)}│`,
297
301
  `│ ${pad(truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs, this.options.snapshotCache)}`, innerWidth - 1), innerWidth - 1)}│`,
298
- `│ ${pad(truncate(`↑/↓ select • 1 agents 2 progress 3 mailbox 4 output 5 health • s/u/a/i actions • R/K/D health • H hush`, innerWidth - 1), innerWidth - 1)}│`,
302
+ `│ ${pad(truncate(`↑/↓ select • 1 agents 2 progress 3 mailbox 4 output 5 health 6 metrics • s/u/a/i actions • R/K/D health • H hush`, innerWidth - 1), innerWidth - 1)}│`,
299
303
  border("├", "┤"),
300
304
  ];
301
305
  if (this.runs.length === 0) {
@@ -340,7 +344,9 @@ export class RunDashboard implements DashboardComponent {
340
344
  ? renderMailboxPane(selectedSnapshot)
341
345
  : this.activePane === "health"
342
346
  ? renderHealthPane(selectedSnapshot, { isForeground: selectedDisplayRun.async ? false : true })
343
- : renderTranscriptPane(selectedSnapshot)
347
+ : this.activePane === "metrics"
348
+ ? renderMetricsPane(selectedSnapshot, { registry: this.options.registry })
349
+ : renderTranscriptPane(selectedSnapshot)
344
350
  : [
345
351
  ...readAgentPreview(selectedDisplayRun, this.showFullProgress ? 20 : 8, this.options),
346
352
  ...readProgressPreview(selectedDisplayRun, this.showFullProgress ? 20 : 5),
@@ -412,6 +418,7 @@ export class RunDashboard implements DashboardComponent {
412
418
  else if (action === "pane-mailbox") this.activePane = "mailbox";
413
419
  else if (action === "pane-output") this.activePane = "output";
414
420
  else if (action === "pane-health") this.activePane = "health";
421
+ else if (action === "pane-metrics") this.activePane = "metrics";
415
422
  else if (action === "up") this.selected = Math.max(0, this.selected - 1);
416
423
  else if (action === "down") {
417
424
  const selectableCount = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).length;