pi-workflow-engine 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +255 -97
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
  3. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
  4. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +13 -5
  5. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
  6. package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
  7. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
  8. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
  9. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
  10. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
  12. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
  13. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
  14. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +26 -4
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
  16. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
  17. package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
  18. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
  19. package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
  20. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +82 -10
  21. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
  22. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
  23. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
  24. package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
  25. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
  26. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
  27. package/.pi/extensions/pi-workflow-engine/src/journal.ts +13 -6
  28. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
  29. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
  30. package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
  31. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
  32. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
  33. package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
  34. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
  35. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
  36. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
  37. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +127 -41
  38. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +24 -0
  39. package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +120 -0
  40. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +43 -7
  41. package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +2 -1
  42. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +1 -1
  43. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
  44. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
  45. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
  46. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
  48. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
  49. package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
  50. package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
  51. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
  52. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
  53. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
  54. package/.pi/extensions/pi-workflow-engine/src/types.ts +52 -15
  55. package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
  56. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
  57. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
  58. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -3
  59. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
  60. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
  61. package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
  62. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +151 -178
  63. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
  64. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
  65. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
  66. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
  67. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
  68. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
  69. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
  70. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
  71. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +47 -3
  72. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +65 -162
  73. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +48 -149
  74. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +28 -77
  75. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +28 -77
  76. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
  77. package/README.md +72 -141
  78. package/USAGE.md +270 -17
  79. package/package.json +1 -1
@@ -0,0 +1,51 @@
1
+ import { truncateToWidth } from "@earendil-works/pi-tui";
2
+
3
+ const VIEWPORT_HEIGHT_RATIO = 0.8;
4
+ const VIEWPORT_MARGIN_ROWS = 2;
5
+
6
+ export const WORKFLOW_VIEWER_OVERLAY_OPTIONS = {
7
+ overlay: true,
8
+ overlayOptions: {
9
+ anchor: "center",
10
+ width: "80%",
11
+ minWidth: 40,
12
+ maxHeight: "80%",
13
+ margin: 1,
14
+ },
15
+ } as const;
16
+
17
+ export function workflowViewerHeight(terminalRows: number): number {
18
+ const rows = Math.max(1, terminalRows);
19
+ const availableRows = Math.max(1, rows - VIEWPORT_MARGIN_ROWS);
20
+ const proportionalRows = Math.floor(rows * VIEWPORT_HEIGHT_RATIO);
21
+ return Math.min(availableRows, Math.max(3, proportionalRows));
22
+ }
23
+
24
+ export function fitWorkflowViewerRow(text: string, width: number): string {
25
+ return truncateToWidth(text, Math.max(0, width), "...", true);
26
+ }
27
+
28
+ export function fitWorkflowViewerRows(lines: readonly string[], height: number): string[] {
29
+ const visible = lines.slice(0, Math.max(0, height));
30
+ return [...visible, ...Array.from({ length: Math.max(0, height - visible.length) }, () => "")];
31
+ }
32
+
33
+ interface WorkflowViewerViewport<T> {
34
+ readonly visible: readonly T[];
35
+ readonly percentage: number;
36
+ }
37
+
38
+ export function centerWorkflowViewerViewport<T>(
39
+ rows: readonly T[],
40
+ height: number,
41
+ selectedIndex: number,
42
+ ): WorkflowViewerViewport<T> {
43
+ const visibleHeight = Math.max(0, Math.floor(height));
44
+ const clampedSelection = Math.min(Math.max(0, rows.length - 1), Math.max(0, selectedIndex));
45
+ const maxStart = Math.max(0, rows.length - visibleHeight);
46
+ const start = Math.min(maxStart, Math.max(0, clampedSelection - Math.floor(visibleHeight / 2)));
47
+ const visible = rows.slice(start, start + visibleHeight);
48
+ const end = Math.min(rows.length, start + visible.length);
49
+ const percentage = rows.length <= visibleHeight ? 100 : Math.round((end / rows.length) * 100);
50
+ return { visible, percentage };
51
+ }
@@ -1,31 +1,18 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import type { AgentRowSnapshot, PhaseSnapshot, WorkflowProgressSnapshot } from "../progress.ts";
3
- import { agentDetailParts, agentLabelColor, formatCount, formatDuration, statusIcon, truncateDisplay } from "./workflow-format.ts";
2
+ import type { AgentRowSnapshot, PhaseSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
3
+ import { formatWorkflowUsageLine } from "../usage.ts";
4
+ import { agentDetailParts, agentLabelColor, formatCount, formatDuration, statusIcon } from "./workflow-format.ts";
4
5
 
5
- const MAX_WIDGET_LINES = 12;
6
- const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
7
-
8
- export interface WorkflowWidget {
9
- nextFrame(): void;
10
- render(width: number, theme: Theme): string[];
11
- invalidate(): void;
12
- }
13
-
14
- export function createWorkflowWidget(snapshotProvider: () => WorkflowProgressSnapshot): WorkflowWidget {
15
- return new LiveWorkflowWidget(snapshotProvider);
16
- }
6
+ const MAX_WIDGET_LINES = 10;
17
7
 
18
8
  export function renderWorkflowWidgetLines(
19
9
  snapshot: WorkflowProgressSnapshot,
20
- frame: number,
21
- width: number,
22
10
  theme: Theme,
23
11
  ): string[] {
24
- const safeWidth = Math.max(1, width);
25
12
  const counts = countAgents(snapshot.phases);
26
13
  const active = counts.running + counts.queued;
27
14
  const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
28
- const spinner = active > 0 ? theme.fg("accent", SPINNER[frame % SPINNER.length]) : theme.fg("success", "✓");
15
+ const activity = active > 0 ? theme.fg("accent", "●") : theme.fg("success", "✓");
29
16
 
30
17
  const headingParts = [`${counts.done}/${counts.total} done`, elapsed];
31
18
  if (counts.running > 0) headingParts.unshift(`${counts.running} running`);
@@ -33,10 +20,7 @@ export function renderWorkflowWidgetLines(
33
20
  if (counts.failed > 0) headingParts.unshift(theme.fg("error", `${counts.failed} failed`));
34
21
 
35
22
  const lines: string[] = [
36
- truncateDisplay(
37
- `${spinner} ${theme.bold(snapshot.title)} ${theme.fg("dim", "·")} ${theme.fg("muted", snapshot.currentPhase)} ${theme.fg("dim", "·")} ${headingParts.join(` ${theme.fg("dim", "·")} `)}`,
38
- safeWidth,
39
- ),
23
+ `${activity} ${theme.bold(snapshot.title)} ${theme.fg("dim", "·")} ${theme.fg("muted", snapshot.currentPhase)} ${theme.fg("dim", "·")} ${headingParts.join(` ${theme.fg("dim", "·")} `)}`,
40
24
  ];
41
25
 
42
26
  const footer = footerLine(snapshot, theme);
@@ -46,53 +30,13 @@ export function renderWorkflowWidgetLines(
46
30
  const visibleBodyLinesToRender = reserveHiddenLine ? body.lines.slice(0, Math.max(0, bodyBudget - 1)) : body.lines;
47
31
  const hidden = body.hidden + (body.lines.length - visibleBodyLinesToRender.length);
48
32
 
49
- for (const line of visibleBodyLinesToRender) lines.push(truncateDisplay(line, safeWidth));
33
+ lines.push(...visibleBodyLinesToRender);
50
34
  if (hidden > 0 && lines.length < MAX_WIDGET_LINES) {
51
- lines.push(truncateDisplay(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hidden} more`)}`, safeWidth));
35
+ lines.push(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hidden} more`)}`);
52
36
  }
53
- if (footer && lines.length < MAX_WIDGET_LINES) lines.push(truncateDisplay(footer, safeWidth));
54
-
55
- return lines.slice(0, MAX_WIDGET_LINES).map((line) => truncateDisplay(line, safeWidth));
56
- }
57
-
58
- class LiveWorkflowWidget implements WorkflowWidget {
59
- private frame = 0;
60
- private cachedWidth: number | undefined;
61
- private cachedFrame: number | undefined;
62
- private cachedDoneAt: number | undefined;
63
- private cachedLines: string[] | undefined;
64
-
65
- constructor(private readonly snapshotProvider: () => WorkflowProgressSnapshot) {}
37
+ if (footer && lines.length < MAX_WIDGET_LINES) lines.push(footer);
66
38
 
67
- nextFrame(): void {
68
- this.frame = (this.frame + 1) % SPINNER.length;
69
- this.invalidate();
70
- }
71
-
72
- render(width: number, theme: Theme): string[] {
73
- const snapshot = this.snapshotProvider();
74
- if (
75
- this.cachedLines &&
76
- this.cachedWidth === width &&
77
- this.cachedFrame === this.frame &&
78
- this.cachedDoneAt === snapshot.doneAt
79
- ) {
80
- return this.cachedLines;
81
- }
82
-
83
- this.cachedLines = renderWorkflowWidgetLines(snapshot, this.frame, width, theme);
84
- this.cachedWidth = width;
85
- this.cachedFrame = this.frame;
86
- this.cachedDoneAt = snapshot.doneAt;
87
- return this.cachedLines;
88
- }
89
-
90
- invalidate(): void {
91
- this.cachedWidth = undefined;
92
- this.cachedFrame = undefined;
93
- this.cachedDoneAt = undefined;
94
- this.cachedLines = undefined;
95
- }
39
+ return lines.slice(0, MAX_WIDGET_LINES);
96
40
  }
97
41
 
98
42
  interface AgentCounts {
@@ -173,9 +117,10 @@ function agentLine(agent: AgentRowSnapshot, theme: Theme): string {
173
117
  }
174
118
 
175
119
  function footerLine(snapshot: WorkflowProgressSnapshot, theme: Theme): string | undefined {
120
+ const usage = formatWorkflowUsageLine(snapshot.usage);
176
121
  const counters = snapshot.counters.slice(0, 4).map((counter) => `${counter.label} ${formatCount(counter.value)}`);
177
122
  const latestLog = snapshot.logs[snapshot.logs.length - 1];
178
- const parts = counters.length > 0 ? counters : [];
123
+ const parts = usage ? [usage, ...counters] : counters;
179
124
  if (latestLog) parts.push(latestLog);
180
125
  if (parts.length === 0) return undefined;
181
126
  return `${theme.fg("dim", "└─")} ${theme.fg("dim", parts.join(" · "))}`;
@@ -6,12 +6,22 @@ export interface WorkflowUsageCost {
6
6
  readonly total: number;
7
7
  }
8
8
 
9
+ export type WorkflowUsageCoverage = "none" | "partial" | "complete";
10
+
11
+ export interface WorkflowUsageComponentCoverage {
12
+ readonly input: WorkflowUsageCoverage;
13
+ readonly output: WorkflowUsageCoverage;
14
+ readonly cacheRead: WorkflowUsageCoverage;
15
+ readonly cacheWrite: WorkflowUsageCoverage;
16
+ }
17
+
9
18
  export interface WorkflowUsageTotals {
10
19
  readonly input: number;
11
20
  readonly output: number;
12
21
  readonly cacheRead: number;
13
22
  readonly cacheWrite: number;
14
23
  readonly totalTokens: number;
24
+ readonly coverage: WorkflowUsageComponentCoverage;
15
25
  readonly cost: WorkflowUsageCost;
16
26
  }
17
27
 
@@ -36,7 +46,21 @@ export interface WorkflowUsageSink {
36
46
  }
37
47
 
38
48
  const ZERO_COST: WorkflowUsageCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
39
- const ZERO_TOTALS: WorkflowUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: ZERO_COST };
49
+ const ZERO_COVERAGE: WorkflowUsageComponentCoverage = {
50
+ input: "none",
51
+ output: "none",
52
+ cacheRead: "none",
53
+ cacheWrite: "none",
54
+ };
55
+ const ZERO_TOTALS: WorkflowUsageTotals = {
56
+ input: 0,
57
+ output: 0,
58
+ cacheRead: 0,
59
+ cacheWrite: 0,
60
+ totalTokens: 0,
61
+ coverage: ZERO_COVERAGE,
62
+ cost: ZERO_COST,
63
+ };
40
64
 
41
65
  interface AssistantUsageMessage {
42
66
  readonly provider?: string;
@@ -47,6 +71,8 @@ interface AssistantUsageMessage {
47
71
  export class WorkflowUsageRecorder implements WorkflowUsageSink {
48
72
  private readonly agents: WorkflowAgentUsage[] = [];
49
73
 
74
+ constructor(private readonly onSnapshot?: (snapshot: WorkflowUsageSnapshot) => void) {}
75
+
50
76
  recordAgentSession(input: { label: string; phase?: string; messages: readonly unknown[] }): void {
51
77
  const assistantMessages = input.messages.flatMap((message) => {
52
78
  const parsed = parseAssistantUsageMessage(message);
@@ -64,6 +90,7 @@ export class WorkflowUsageRecorder implements WorkflowUsageSink {
64
90
  assistantMessages: assistantMessages.length,
65
91
  usage,
66
92
  });
93
+ this.onSnapshot?.(this.snapshot());
67
94
  }
68
95
 
69
96
  snapshot(): WorkflowUsageSnapshot {
@@ -79,8 +106,8 @@ export class WorkflowUsageRecorder implements WorkflowUsageSink {
79
106
  }
80
107
  }
81
108
 
82
- export function createWorkflowUsageRecorder(): WorkflowUsageSink {
83
- return new WorkflowUsageRecorder();
109
+ export function createWorkflowUsageRecorder(onSnapshot?: (snapshot: WorkflowUsageSnapshot) => void): WorkflowUsageSink {
110
+ return new WorkflowUsageRecorder(onSnapshot);
84
111
  }
85
112
 
86
113
  export function emptyWorkflowUsageTotals(): WorkflowUsageTotals {
@@ -96,19 +123,45 @@ export function isWorkflowUsageSnapshot(value: unknown): value is WorkflowUsageS
96
123
 
97
124
  export function hasWorkflowUsage(snapshot: unknown): snapshot is WorkflowUsageSnapshot {
98
125
  if (!isWorkflowUsageSnapshot(snapshot)) return false;
99
- return snapshot.assistantMessages > 0 || snapshot.totals.totalTokens > 0 || snapshot.totals.cost.total > 0;
126
+ return snapshot.totals.totalTokens > 0 || snapshot.totals.cost.total > 0;
100
127
  }
101
128
 
102
129
  export function formatWorkflowUsageLine(snapshot: unknown): string | undefined {
103
130
  if (!hasWorkflowUsage(snapshot)) return undefined;
104
- const parts = [`↑${formatUsageCount(snapshot.totals.input)}`, `↓${formatUsageCount(snapshot.totals.output)}`];
105
- if (snapshot.totals.cacheRead > 0) parts.push(`R${formatUsageCount(snapshot.totals.cacheRead)}`);
106
- if (snapshot.totals.cacheWrite > 0) parts.push(`W${formatUsageCount(snapshot.totals.cacheWrite)}`);
131
+ const parts = formatWorkflowUsageComponents(snapshot.totals);
107
132
  parts.push(`cost $${snapshot.totals.cost.total.toFixed(3)}`);
108
133
  parts.push(`agents ${snapshot.agents.length}`);
109
134
  return `Usage: ${parts.join(" · ")}`;
110
135
  }
111
136
 
137
+ function formatWorkflowUsageComponents(totals: WorkflowUsageTotals): string[] {
138
+ const parts: string[] = [];
139
+ const completeCacheBreakdown =
140
+ totals.coverage.cacheRead === "complete" && totals.coverage.cacheWrite === "complete";
141
+ appendUsageComponent(parts, completeCacheBreakdown ? "fresh" : "input", totals.input, totals.coverage.input);
142
+ appendUsageComponent(parts, "cache read", totals.cacheRead, totals.coverage.cacheRead);
143
+ appendUsageComponent(parts, "cache write", totals.cacheWrite, totals.coverage.cacheWrite);
144
+ appendUsageComponent(parts, "output", totals.output, totals.coverage.output);
145
+
146
+ const knownComponents = totals.input + totals.output + totals.cacheRead + totals.cacheWrite;
147
+ if (parts.length === 0 && totals.totalTokens > 0) parts.push(`tokens ${formatUsageCount(totals.totalTokens)}`);
148
+ else if (!hasCompleteCoverage(totals.coverage) && totals.totalTokens !== knownComponents) {
149
+ parts.push(`total ${formatUsageCount(totals.totalTokens)}`);
150
+ }
151
+ return parts;
152
+ }
153
+
154
+ function appendUsageComponent(
155
+ parts: string[],
156
+ label: string,
157
+ value: number,
158
+ coverage: WorkflowUsageCoverage,
159
+ ): void {
160
+ if (value <= 0 || coverage === "none") return;
161
+ const count = formatUsageCount(value);
162
+ parts.push(`${label} ${coverage === "partial" ? `≥${count}` : count}`);
163
+ }
164
+
112
165
  function isWorkflowAgentUsage(value: unknown): value is WorkflowAgentUsage {
113
166
  if (!isRecord(value)) return false;
114
167
  if (typeof value.label !== "string") return false;
@@ -126,9 +179,20 @@ function isWorkflowUsageTotals(value: unknown): value is WorkflowUsageTotals {
126
179
  if (finiteNumber(value.cacheRead) === undefined) return false;
127
180
  if (finiteNumber(value.cacheWrite) === undefined) return false;
128
181
  if (finiteNumber(value.totalTokens) === undefined) return false;
182
+ if (!isWorkflowUsageCoverage(value.coverage)) return false;
129
183
  return isWorkflowUsageCost(value.cost);
130
184
  }
131
185
 
186
+ function isWorkflowUsageCoverage(value: unknown): value is WorkflowUsageComponentCoverage {
187
+ if (!isRecord(value)) return false;
188
+ return (
189
+ isCoverageValue(value.input) &&
190
+ isCoverageValue(value.output) &&
191
+ isCoverageValue(value.cacheRead) &&
192
+ isCoverageValue(value.cacheWrite)
193
+ );
194
+ }
195
+
132
196
  function isWorkflowUsageCost(value: unknown): value is WorkflowUsageCost {
133
197
  if (!isRecord(value)) return false;
134
198
  return (
@@ -158,14 +222,26 @@ function parseUsageTotals(value: unknown): WorkflowUsageTotals | undefined {
158
222
  const output = finiteNumber(value.output);
159
223
  const cacheRead = finiteNumber(value.cacheRead);
160
224
  const cacheWrite = finiteNumber(value.cacheWrite);
225
+ const reportedTotal = finiteNumber(value.totalTokens);
161
226
  const cost = parseUsageCost(value.cost);
162
- if (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined || !cost) return undefined;
227
+ if (!cost) return undefined;
228
+ if (input === undefined && output === undefined && cacheRead === undefined && cacheWrite === undefined && reportedTotal === undefined) {
229
+ return undefined;
230
+ }
231
+ const coverage: WorkflowUsageComponentCoverage = {
232
+ input: input === undefined ? "none" : "complete",
233
+ output: output === undefined ? "none" : "complete",
234
+ cacheRead: cacheRead === undefined ? "none" : "complete",
235
+ cacheWrite: cacheWrite === undefined ? "none" : "complete",
236
+ };
237
+ const componentTotal = (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
163
238
  return {
164
- input,
165
- output,
166
- cacheRead,
167
- cacheWrite,
168
- totalTokens: input + output + cacheRead + cacheWrite,
239
+ input: input ?? 0,
240
+ output: output ?? 0,
241
+ cacheRead: cacheRead ?? 0,
242
+ cacheWrite: cacheWrite ?? 0,
243
+ totalTokens: hasCompleteCoverage(coverage) ? componentTotal : (reportedTotal ?? componentTotal),
244
+ coverage,
169
245
  cost,
170
246
  };
171
247
  }
@@ -177,12 +253,20 @@ function parseUsageCost(value: unknown): WorkflowUsageCost | undefined {
177
253
  const cacheRead = finiteNumber(value.cacheRead);
178
254
  const cacheWrite = finiteNumber(value.cacheWrite);
179
255
  const total = finiteNumber(value.total);
180
- if (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined || total === undefined) return undefined;
181
- return { input, output, cacheRead, cacheWrite, total };
256
+ if (total === undefined) return undefined;
257
+ return {
258
+ input: input ?? 0,
259
+ output: output ?? 0,
260
+ cacheRead: cacheRead ?? 0,
261
+ cacheWrite: cacheWrite ?? 0,
262
+ total,
263
+ };
182
264
  }
183
265
 
184
266
  function sumTotals(values: readonly WorkflowUsageTotals[]): WorkflowUsageTotals {
185
- return values.reduce((sum, value) => addTotals(sum, value), emptyWorkflowUsageTotals());
267
+ const [first, ...rest] = values;
268
+ if (!first) return emptyWorkflowUsageTotals();
269
+ return rest.reduce(addTotals, cloneTotals(first));
186
270
  }
187
271
 
188
272
  function addTotals(left: WorkflowUsageTotals, right: WorkflowUsageTotals): WorkflowUsageTotals {
@@ -192,6 +276,12 @@ function addTotals(left: WorkflowUsageTotals, right: WorkflowUsageTotals): Workf
192
276
  cacheRead: left.cacheRead + right.cacheRead,
193
277
  cacheWrite: left.cacheWrite + right.cacheWrite,
194
278
  totalTokens: left.totalTokens + right.totalTokens,
279
+ coverage: {
280
+ input: combineCoverage(left.coverage.input, right.coverage.input),
281
+ output: combineCoverage(left.coverage.output, right.coverage.output),
282
+ cacheRead: combineCoverage(left.coverage.cacheRead, right.coverage.cacheRead),
283
+ cacheWrite: combineCoverage(left.coverage.cacheWrite, right.coverage.cacheWrite),
284
+ },
195
285
  cost: {
196
286
  input: left.cost.input + right.cost.input,
197
287
  output: left.cost.output + right.cost.output,
@@ -209,10 +299,28 @@ function cloneTotals(value: WorkflowUsageTotals): WorkflowUsageTotals {
209
299
  cacheRead: value.cacheRead,
210
300
  cacheWrite: value.cacheWrite,
211
301
  totalTokens: value.totalTokens,
302
+ coverage: { ...value.coverage },
212
303
  cost: { ...value.cost },
213
304
  };
214
305
  }
215
306
 
307
+ function combineCoverage(left: WorkflowUsageCoverage, right: WorkflowUsageCoverage): WorkflowUsageCoverage {
308
+ return left === right ? left : "partial";
309
+ }
310
+
311
+ function hasCompleteCoverage(coverage: WorkflowUsageComponentCoverage): boolean {
312
+ return (
313
+ coverage.input === "complete" &&
314
+ coverage.output === "complete" &&
315
+ coverage.cacheRead === "complete" &&
316
+ coverage.cacheWrite === "complete"
317
+ );
318
+ }
319
+
320
+ function isCoverageValue(value: unknown): value is WorkflowUsageCoverage {
321
+ return value === "none" || value === "partial" || value === "complete";
322
+ }
323
+
216
324
  function formatUsageCount(count: number): string {
217
325
  if (count < 1000) return count.toString();
218
326
  if (count < 10000) return `${(count / 1000).toFixed(1)}k`;