@wichayutdew/pi-workflows 3.1.0 → 3.3.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.
@@ -6,7 +6,10 @@ import {
6
6
  beginMainStepAttempt,
7
7
  beginSubagentStepAttempt,
8
8
  recordCurrentStepResult,
9
+ recordCurrentStepUsage,
10
+ usageAggregateFromModels,
9
11
  } from '../engine/step-trace.ts';
12
+ import type { ModelUsage } from '../engine/usage.ts';
10
13
  import { advanceRun, allowedOutcomes } from '../engine/transitions.ts';
11
14
  import { digest } from '../digest.ts';
12
15
  import type { SubagentDelegationResponse } from '../integrations/subagents/protocol.ts';
@@ -62,12 +65,14 @@ export type StepExecutionActions = {
62
65
  identity: MainStepIdentity,
63
66
  lines: ReadonlyArray<string>,
64
67
  context: ExtensionContext,
68
+ usage?: ReadonlyArray<ModelUsage>,
65
69
  ) => Promise<void>;
66
70
  recordMainStepLog: (
67
71
  this: HarnessActionContext,
68
72
  identity: MainStepIdentity,
69
73
  lines: ReadonlyArray<string>,
70
74
  context: ExtensionContext,
75
+ usage?: ReadonlyArray<ModelUsage>,
71
76
  ) => Promise<void>;
72
77
  queueMainStepResult: (
73
78
  this: HarnessActionContext,
@@ -236,8 +241,8 @@ function launchMainStep(
236
241
  summaryMaxChars: workflow.definition.summaryMaxChars,
237
242
  ...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
238
243
  ...(step.workspace ? { workspace: structuredClone(step.workspace) } : {}),
239
- onTrace: (lines, context) =>
240
- this.queueMainStepLog(identity, lines, context),
244
+ onTrace: (lines, context, usage) =>
245
+ this.queueMainStepLog(identity, lines, context, usage),
241
246
  onSettled: (result, context) =>
242
247
  this.queueMainStepResult(identity, result, context),
243
248
  };
@@ -302,9 +307,10 @@ function queueMainStepLog(
302
307
  identity: MainStepIdentity,
303
308
  lines: ReadonlyArray<string>,
304
309
  context: ExtensionContext,
310
+ usage?: ReadonlyArray<ModelUsage>,
305
311
  ): Promise<void> {
306
312
  return this.mutationQueue
307
- .run(() => this.recordMainStepLog(identity, lines, context))
313
+ .run(() => this.recordMainStepLog(identity, lines, context, usage))
308
314
  .catch(() => {
309
315
  // Status evidence is best-effort and must never pause step execution.
310
316
  });
@@ -315,6 +321,7 @@ async function recordMainStepLog(
315
321
  identity: MainStepIdentity,
316
322
  lines: ReadonlyArray<string>,
317
323
  context: ExtensionContext,
324
+ usage?: ReadonlyArray<ModelUsage>,
318
325
  ): Promise<void> {
319
326
  if (
320
327
  !hasCurrentMainStepIdentity(this, identity) ||
@@ -327,12 +334,20 @@ async function recordMainStepLog(
327
334
  return;
328
335
  }
329
336
  this.latestContext = context;
330
- const traced = appendMainStepLog(
337
+ let traced = appendMainStepLog(
331
338
  this.run,
332
339
  identity.requestId,
333
340
  lines,
334
341
  this.dependencies.now(),
335
342
  );
343
+ if (usage && usage.length > 0) {
344
+ traced = recordCurrentStepUsage(
345
+ traced,
346
+ identity.requestId,
347
+ usageAggregateFromModels(usage),
348
+ this.dependencies.now(),
349
+ );
350
+ }
336
351
  if (traced === this.run) return;
337
352
  this.run = traced;
338
353
  this.persist();
@@ -11,6 +11,7 @@ export type ActiveDelegation = {
11
11
  policy: ChildStepPolicy;
12
12
  transcriptTask: string;
13
13
  agent: string;
14
+ model?: string;
14
15
  progress?: string;
15
16
  activityLog?: Array<string>;
16
17
  cancelling?: boolean;
package/src/index.ts CHANGED
@@ -32,7 +32,9 @@ const DEFAULT_DEPENDENCIES = {
32
32
  loadSettings,
33
33
  userWorkflowDirectory: defaultUserWorkflowDirectory,
34
34
  runtimeEnvironment: (): PiWorkflowsRuntimeEnvironment => ({
35
- isSubagentChild: process.env.PI_WORKFLOWS_CHILD === '1',
35
+ isSubagentChild:
36
+ process.env.PI_WORKFLOWS_CHILD === '1' &&
37
+ process.env.PI_WORKFLOWS_CHILD_RUNTIME === '1',
36
38
  childAgent: process.env.PI_WORKFLOWS_CHILD_AGENT?.trim(),
37
39
  }),
38
40
  registerChildRuntime: (pi, childAgent): void => {
@@ -4,11 +4,17 @@ import type {
4
4
  SubagentDelegationRequest,
5
5
  SubagentDelegationResponse,
6
6
  SubagentDelegationUpdate,
7
+ SubagentModelUsage,
7
8
  } from './protocol-events.ts';
8
9
  import type {
9
10
  DelegationDiagnostic,
10
11
  DelegationDiagnosticCall,
11
12
  } from './diagnostics.ts';
13
+ import {
14
+ emptyUsageAggregate,
15
+ mergeUsage,
16
+ modelUsageFromMessage,
17
+ } from '../../engine/usage.ts';
12
18
 
13
19
  export type DelegateOptions = {
14
20
  readonly signal?: AbortSignal;
@@ -57,6 +63,7 @@ export function directWorkerResponse(
57
63
  signal: NodeJS.Signals | null,
58
64
  stderr: string,
59
65
  diagnostic?: DelegationDiagnostic,
66
+ usage: ReadonlyArray<SubagentModelUsage> = [],
60
67
  ): SubagentDelegationResponse {
61
68
  const status = code === 0 ? 'completed' : signal ? 'cancelled' : 'failed';
62
69
  return {
@@ -68,6 +75,7 @@ export function directWorkerResponse(
68
75
  ? { error: stderr.trim().slice(-4_000) }
69
76
  : {}),
70
77
  ...(diagnostic ? { diagnostic } : {}),
78
+ ...(usage.length > 0 ? { usage } : {}),
71
79
  };
72
80
  }
73
81
 
@@ -77,7 +85,12 @@ type WorkerJsonEvent = {
77
85
  readonly toolName?: unknown;
78
86
  readonly isError?: unknown;
79
87
  readonly args?: unknown;
80
- readonly message?: { readonly role?: unknown };
88
+ readonly message?: {
89
+ readonly role?: unknown;
90
+ readonly provider?: unknown;
91
+ readonly model?: unknown;
92
+ readonly usage?: unknown;
93
+ };
81
94
  readonly assistantMessageEvent?: {
82
95
  readonly type?: unknown;
83
96
  readonly delta?: unknown;
@@ -92,6 +105,33 @@ type WorkerProgress = {
92
105
 
93
106
  const MAX_PROGRESS_DETAIL_CHARS = 480;
94
107
  const MAX_DIAGNOSTIC_CALLS = 64;
108
+
109
+ /** Extracts usage only from finalized worker messages, never stream updates. */
110
+ export function workerUsageFromJsonLine(
111
+ line: string,
112
+ fallbackProvider?: string,
113
+ fallbackModel?: string,
114
+ ): ReadonlyArray<SubagentModelUsage> {
115
+ let event: WorkerJsonEvent;
116
+ try {
117
+ const parsed: unknown = JSON.parse(line);
118
+ if (typeof parsed !== 'object' || parsed === null) return [];
119
+ event = parsed;
120
+ } catch {
121
+ return [];
122
+ }
123
+ if (event.type !== 'message_end' || !event.message) return [];
124
+ const role = event.message.role;
125
+ if (role !== 'assistant' && role !== 'toolResult' && role !== 'tool')
126
+ return [];
127
+ const usage = modelUsageFromMessage(
128
+ event.message,
129
+ fallbackProvider,
130
+ fallbackModel,
131
+ );
132
+ return usage ? [usage] : [];
133
+ }
134
+
95
135
  const SECRET_KEY = /authorization|cookie|password|secret|token|api[-_]?key/i;
96
136
 
97
137
  function redactProgressValue(value: unknown, key = ''): unknown {
@@ -275,6 +315,7 @@ export function createSubagentDelegationClient(
275
315
  env: {
276
316
  ...process.env,
277
317
  PI_WORKFLOWS_CHILD: '1',
318
+ PI_WORKFLOWS_CHILD_RUNTIME: '1',
278
319
  PI_WORKFLOWS_CHILD_AGENT: request.agent,
279
320
  },
280
321
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -285,7 +326,30 @@ export function createSubagentDelegationClient(
285
326
  let toolCount = 0;
286
327
  let responseText = '';
287
328
  const diagnostic = createDiagnostic();
329
+ let lastProvider: string | undefined;
330
+ let lastModel: string | undefined;
331
+ let usage = emptyUsageAggregate();
288
332
  const stdoutDecoder = new StringDecoder('utf8');
333
+ const updateLastModel = (line: string): void => {
334
+ let event: WorkerJsonEvent;
335
+ try {
336
+ const parsed: unknown = JSON.parse(line);
337
+ if (typeof parsed !== 'object' || parsed === null) return;
338
+ event = parsed;
339
+ } catch {
340
+ return;
341
+ }
342
+ if (
343
+ event.type === 'message_end' &&
344
+ event.message &&
345
+ event.message.role === 'assistant'
346
+ ) {
347
+ if (typeof event.message.provider === 'string')
348
+ lastProvider = event.message.provider;
349
+ if (typeof event.message.model === 'string')
350
+ lastModel = event.message.model;
351
+ }
352
+ };
289
353
  const consumeWorkerLines = (): void => {
290
354
  while (true) {
291
355
  const newline = stdoutBuffer.indexOf('\n');
@@ -293,6 +357,20 @@ export function createSubagentDelegationClient(
293
357
  const line = stdoutBuffer.slice(0, newline);
294
358
  stdoutBuffer = stdoutBuffer.slice(newline + 1);
295
359
  recordWorkerDiagnostic(line, diagnostic);
360
+ updateLastModel(line);
361
+ const lineUsage = workerUsageFromJsonLine(
362
+ line,
363
+ lastProvider,
364
+ lastModel,
365
+ );
366
+ if (lineUsage.length > 0) {
367
+ usage = mergeUsage(usage, lineUsage);
368
+ const latest = lineUsage[lineUsage.length - 1];
369
+ if (latest) {
370
+ lastProvider = latest.provider;
371
+ lastModel = latest.model;
372
+ }
373
+ }
296
374
  const progress = workerProgressFromJsonLine(
297
375
  line,
298
376
  request.requestId,
@@ -332,6 +410,7 @@ export function createSubagentDelegationClient(
332
410
  signal,
333
411
  stderr,
334
412
  diagnosticSnapshot(diagnostic),
413
+ usage.models,
335
414
  ),
336
415
  );
337
416
  });
@@ -1,5 +1,5 @@
1
1
  import type { DelegationDiagnostic } from './diagnostics.ts';
2
-
2
+ import type { UsageTotals } from '../../engine/usage.ts';
3
3
  export const SUBAGENT_DELEGATION_PROTOCOL_VERSION = 1 as const;
4
4
  export const SUBAGENT_DELEGATION_REQUEST_EVENT =
5
5
  'prompt-template:subagent:request';
@@ -35,6 +35,12 @@ export type SubagentDelegationUpdate = {
35
35
  };
36
36
  export type SubagentDelegationStatus = 'completed' | 'failed' | 'cancelled';
37
37
 
38
+ export type SubagentModelUsage = {
39
+ readonly provider: string;
40
+ readonly model: string;
41
+ readonly usage: UsageTotals;
42
+ };
43
+
38
44
  export type SubagentDelegationResponse = {
39
45
  readonly version?: number;
40
46
  readonly requestId: string;
@@ -44,4 +50,6 @@ export type SubagentDelegationResponse = {
44
50
  readonly exitCode?: number;
45
51
  readonly warnings?: ReadonlyArray<string>;
46
52
  readonly diagnostic?: DelegationDiagnostic;
53
+ /** Usage captured from terminal worker messages only. */
54
+ readonly usage?: ReadonlyArray<SubagentModelUsage>;
47
55
  };
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
2
2
  import type { WorkflowStep } from '../config/types.ts';
3
+ import type { ModelUsage } from '../engine/usage.ts';
3
4
  import type { invalidCompletionCallIds } from '../policy/completion-batch.ts';
4
5
  import type { freezeToolInput } from '../policy/immutable-input.ts';
5
6
  import type { authorizeToolCall, resolveActiveTools } from '../policy/tools.ts';
@@ -24,6 +25,7 @@ export type MainStepExecution = StepResultPolicy & {
24
25
  readonly onTrace: (
25
26
  lines: ReadonlyArray<string>,
26
27
  context: ExtensionContext,
28
+ usage?: ReadonlyArray<ModelUsage>,
27
29
  ) => Promise<void> | void;
28
30
  /** Handles the captured result after Pi fully settles the agent run. */
29
31
  readonly onSettled: (
@@ -1,12 +1,45 @@
1
1
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
2
  import { stepLogLinesFromTurn, textOnlyUserMessage } from '../step-log.ts';
3
+ import { modelUsageFromMessage, type ModelUsage } from '../engine/usage.ts';
3
4
  import type { MainStepRuntimeState } from './main-step-runtime-types.ts';
4
5
 
6
+ type UnknownRecord = Readonly<Record<string, unknown>>;
7
+ const isRecord = (value: unknown): value is UnknownRecord =>
8
+ value !== null && typeof value === 'object' && !Array.isArray(value);
9
+
5
10
  type RegisterMainStepTraceOptions = {
6
11
  readonly pi: ExtensionAPI;
7
12
  readonly state: MainStepRuntimeState;
8
13
  };
9
14
 
15
+ /** Extracts finalized assistant and nested tool-result usage from one Pi turn. */
16
+ export function mainTurnUsage(event: {
17
+ readonly message?: unknown;
18
+ readonly toolResults?: ReadonlyArray<unknown>;
19
+ }): ReadonlyArray<ModelUsage> {
20
+ const messageUsage = modelUsageFromMessage(event.message);
21
+ const fallbackProvider =
22
+ messageUsage?.provider ??
23
+ (isRecord(event.message) && typeof event.message.provider === 'string'
24
+ ? event.message.provider
25
+ : undefined);
26
+ const fallbackModel =
27
+ messageUsage?.model ??
28
+ (isRecord(event.message) && typeof event.message.model === 'string'
29
+ ? event.message.model
30
+ : undefined);
31
+ const entries: Array<ModelUsage> = messageUsage ? [messageUsage] : [];
32
+ for (const toolResult of event.toolResults ?? []) {
33
+ const toolUsage = modelUsageFromMessage(
34
+ toolResult,
35
+ fallbackProvider,
36
+ fallbackModel,
37
+ );
38
+ if (toolUsage) entries.push(toolUsage);
39
+ }
40
+ return entries;
41
+ }
42
+
10
43
  /** Arms a trace only after Pi finalizes the exact extension-supplied task. */
11
44
  export function armMainStepTrace(
12
45
  state: MainStepRuntimeState,
@@ -33,7 +66,9 @@ export function registerMainStepTrace({
33
66
  if (!active || !state.traceArmed || state.traceClosed) return;
34
67
  const lines = stepLogLinesFromTurn(event.message, event.toolResults);
35
68
  try {
36
- if (lines.length > 0) await active.onTrace(lines, context);
69
+ const usage = mainTurnUsage(event);
70
+ if (lines.length > 0 || usage.length > 0)
71
+ await active.onTrace(lines, context, usage);
37
72
  } catch {
38
73
  // Status evidence is best-effort and must never interrupt the agent loop.
39
74
  } finally {
@@ -6,6 +6,7 @@ import {
6
6
  workflowStatusIcon,
7
7
  } from './formatting.ts';
8
8
  import { renderBoard } from './render-board.ts';
9
+ import { formatUsage, workflowUsage } from './format-usage.ts';
9
10
  import type { WorkflowStatusSnapshot, WorkflowStatusTheme } from './types.ts';
10
11
 
11
12
  const UNSTYLED_THEME = {
@@ -26,11 +27,17 @@ export function formatWorkflowProgressStatus(
26
27
  );
27
28
  const activity =
28
29
  run.status === 'awaiting-gate' ? 'awaiting review' : 'working';
30
+ const workerModel =
31
+ snapshot.execution?.kind === 'subagent' && snapshot.execution.model
32
+ ? ` · model ${snapshot.execution.model}`
33
+ : '';
29
34
  const workerProgress =
30
35
  snapshot.execution?.kind === 'subagent'
31
36
  ? ` · ${snapshot.execution.progress}`
32
37
  : '';
33
- return `${workflowStatusIcon(run, snapshot.now)} ${run.workflowId} · step ${currentStep} · ${activity}${workerProgress} · ${statusShortcutLabel}`;
38
+ const usage = workflowUsage(run);
39
+ const usageText = usage.models.length > 0 ? ` · ${formatUsage(usage)}` : '';
40
+ return `${workflowStatusIcon(run, snapshot.now)} ${run.workflowId} · step ${currentStep} · ${activity}${workerModel}${workerProgress}${usageText} · ${statusShortcutLabel}`;
34
41
  }
35
42
 
36
43
  /** Format a plain-text workflow status suitable for fallback notifications. */
@@ -50,6 +57,9 @@ export function formatWorkflowStatusText(
50
57
  `Status: ${run.status}`,
51
58
  `Step: ${run.currentStepId}`,
52
59
  `Completed steps: ${run.history.length}`,
60
+ ...(workflowUsage(run).models.length > 0
61
+ ? [`Usage: ${formatUsage(workflowUsage(run))}`]
62
+ : []),
53
63
  ];
54
64
  if (run.cwd && run.startCwd && run.cwd !== run.startCwd) {
55
65
  lines.push(`Workspace: ${run.cwd}`);
@@ -0,0 +1,33 @@
1
+ import {
2
+ emptyUsageAggregate,
3
+ mergeUsage,
4
+ type UsageAggregate,
5
+ type UsageTotals,
6
+ } from '../engine/usage.ts';
7
+ import type { WorkflowRun } from '../engine/state.ts';
8
+
9
+ export function workflowUsage(run: WorkflowRun): UsageAggregate {
10
+ return [
11
+ ...run.history.map((entry) => entry.usage),
12
+ run.currentStepUsage,
13
+ ].reduce<UsageAggregate>(
14
+ (total, usage) => (usage ? mergeUsage(total, usage.models) : total),
15
+ emptyUsageAggregate(),
16
+ );
17
+ }
18
+
19
+ export function formatUsd(value: number): string {
20
+ if (value === 0) return '$0.00';
21
+ return value < 0.01 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
22
+ }
23
+
24
+ export function formatTokens(usage: UsageTotals): string {
25
+ const parts = [`${usage.inputTokens} in`, `${usage.outputTokens} out`];
26
+ const cache = usage.cacheReadTokens + usage.cacheWriteTokens;
27
+ if (cache > 0) parts.push(`${cache} cache`);
28
+ return parts.join(' · ');
29
+ }
30
+
31
+ export function formatUsage(usage: UsageAggregate): string {
32
+ return `${formatUsd(usage.usage.totalCostUsd)} · ${formatTokens(usage.usage)}`;
33
+ }
@@ -10,6 +10,7 @@ import {
10
10
  stepTitle,
11
11
  } from './formatting.ts';
12
12
  import { joinColumns, padAnsi } from './layout.ts';
13
+ import { formatUsd } from './format-usage.ts';
13
14
  import type {
14
15
  PathEntry,
15
16
  WorkflowStatusSnapshot,
@@ -32,6 +33,7 @@ function historyPathEntry(
32
33
  status: 'completed',
33
34
  visit,
34
35
  outcome: entry.outcome,
36
+ ...(entry.usage ? { usage: entry.usage } : {}),
35
37
  isCurrent: false,
36
38
  };
37
39
  }
@@ -60,6 +62,7 @@ export function buildPathEntries(
60
62
  visits.get(run.currentStepId) ?? 0,
61
63
  run.visits[run.currentStepId] ?? 1,
62
64
  ),
65
+ ...(run.currentStepUsage ? { usage: run.currentStepUsage } : {}),
63
66
  isCurrent: true,
64
67
  },
65
68
  ];
@@ -76,6 +79,7 @@ export function buildPathEntries(
76
79
  title: stepTitle(workflow, run.currentStepId),
77
80
  status: 'completed',
78
81
  visit: Math.max(1, run.visits[run.currentStepId] ?? 1),
82
+ ...(run.currentStepUsage ? { usage: run.currentStepUsage } : {}),
79
83
  isCurrent: true,
80
84
  },
81
85
  ];
@@ -124,9 +128,12 @@ export function renderPathLines(
124
128
  entry.isCurrent ? 'text' : 'muted',
125
129
  entry.title,
126
130
  )}${visit}`;
131
+ const cost = entry.usage?.models.length
132
+ ? ` · ${formatUsd(entry.usage.usage.totalCostUsd)}`
133
+ : '';
127
134
  const right = entry.outcome
128
- ? `${statusLabel(entry.status)} · ${inline(entry.outcome)}`
129
- : statusLabel(entry.status);
135
+ ? `${statusLabel(entry.status)} · ${inline(entry.outcome)}${cost}`
136
+ : `${statusLabel(entry.status)}${cost}`;
130
137
  const row = joinColumns(
131
138
  left,
132
139
  theme.fg(statusColor(entry.status), right),
@@ -5,6 +5,7 @@ import type {
5
5
  } from '../engine/state.ts';
6
6
  import { redactStepDetailText } from '../step-log.ts';
7
7
  import { boxed, keyValueLines } from './layout.ts';
8
+ import { formatUsage } from './format-usage.ts';
8
9
  import { buildPathEntries } from './render-path.ts';
9
10
  import type { StepTranscriptLog } from './transcript-reader.ts';
10
11
  import type { WorkflowStatusSnapshot, WorkflowStatusTheme } from './types.ts';
@@ -241,6 +242,20 @@ function renderAttempt(
241
242
  return [
242
243
  theme.bold(theme.fg('accent', `Attempt ${attemptNumber} · ${actor}`)),
243
244
  ...keyValueLines(theme, 'request', attempt.requestId, width, 'muted'),
245
+ ...(attempt.usage
246
+ ? [
247
+ ...keyValueLines(theme, 'usage', formatUsage(attempt.usage), width),
248
+ ...attempt.usage.models.flatMap((entry) =>
249
+ keyValueLines(
250
+ theme,
251
+ 'model',
252
+ `${entry.provider}/${entry.model} · ${formatUsage({ usage: entry.usage, models: [] })}`,
253
+ width,
254
+ 'muted',
255
+ ),
256
+ ),
257
+ ]
258
+ : []),
244
259
  '',
245
260
  theme.bold('Requirement fed to the agent'),
246
261
  ...wrapPlain(`${attempt.task}${truncation}`, width, theme),
@@ -296,6 +311,9 @@ function renderLiveWorkerSession(
296
311
  width,
297
312
  'accent',
298
313
  ),
314
+ ...(snapshot.execution.model
315
+ ? keyValueLines(theme, 'model', snapshot.execution.model, width, 'muted')
316
+ : []),
299
317
  ...keyValueLines(
300
318
  theme,
301
319
  'state',
@@ -404,6 +422,20 @@ export function renderStepDetail(
404
422
  ...keyValueLines(theme, 'step', entry.stepId, width),
405
423
  ...keyValueLines(theme, 'visit', String(entry.visit), width),
406
424
  ...keyValueLines(theme, 'status', entry.status, width),
425
+ ...(entry.usage
426
+ ? [
427
+ ...keyValueLines(theme, 'usage', formatUsage(entry.usage), width),
428
+ ...entry.usage.models.flatMap((model) =>
429
+ keyValueLines(
430
+ theme,
431
+ 'model',
432
+ `${model.provider}/${model.model} · ${formatUsage({ usage: model.usage, models: [] })}`,
433
+ width,
434
+ 'muted',
435
+ ),
436
+ ),
437
+ ]
438
+ : []),
407
439
  ...(history
408
440
  ? [
409
441
  ...keyValueLines(theme, 'outcome', history.outcome, width, 'success'),
@@ -14,6 +14,7 @@ import {
14
14
  stepTitle,
15
15
  } from './formatting.ts';
16
16
  import { clampRows, keyValueLines } from './layout.ts';
17
+ import { formatUsage, workflowUsage } from './format-usage.ts';
17
18
  import type { WorkflowStatusSnapshot, WorkflowStatusTheme } from './types.ts';
18
19
 
19
20
  const MAX_REASON_ROWS = 5;
@@ -113,6 +114,21 @@ export function renderSummaryLines(
113
114
  ),
114
115
  ];
115
116
 
117
+ const usage = workflowUsage(run);
118
+ if (usage.models.length > 0) {
119
+ lines.push(...keyValueLines(theme, 'usage', formatUsage(usage), width));
120
+ lines.push(
121
+ ...usage.models.flatMap((entry) =>
122
+ keyValueLines(
123
+ theme,
124
+ 'model',
125
+ `${entry.provider}/${entry.model} · ${formatUsage({ usage: entry.usage, models: [] })}`,
126
+ width,
127
+ 'muted',
128
+ ),
129
+ ),
130
+ );
131
+ }
116
132
  if (run.cwd && run.startCwd && run.cwd !== run.startCwd) {
117
133
  lines.push(...keyValueLines(theme, 'workspace', run.cwd, width, 'accent'));
118
134
  }
@@ -3,6 +3,7 @@ import type { TUI } from '@earendil-works/pi-tui';
3
3
  import type { LoadedWorkflow } from '../config/types.ts';
4
4
  import type { WorkflowRun, WorkflowRunStatus } from '../engine/state.ts';
5
5
  import type { StepExecutionAttempt } from '../engine/state.ts';
6
+ import type { UsageAggregate } from '../engine/usage.ts';
6
7
  import type { StepTranscriptLog } from './transcript-reader.ts';
7
8
 
8
9
  export type WorkflowStatusExecution =
@@ -14,6 +15,7 @@ export type WorkflowStatusExecution =
14
15
  readonly agent: string;
15
16
  readonly requestId: string;
16
17
  readonly progress: string;
18
+ readonly model?: string;
17
19
  readonly activityLog?: ReadonlyArray<string>;
18
20
  };
19
21
 
@@ -38,6 +40,7 @@ export type PathEntry = {
38
40
  readonly status: StepDisplayStatus;
39
41
  readonly visit: number;
40
42
  readonly outcome?: string;
43
+ readonly usage?: UsageAggregate;
41
44
  readonly isCurrent: boolean;
42
45
  };
43
46