@wichayutdew/pi-workflows 3.1.0 → 3.2.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.
- package/dist/index.js +386 -51
- package/package.json +1 -1
- package/src/engine/run-advance.ts +3 -0
- package/src/engine/run-reconciliation.ts +1 -0
- package/src/engine/run-validation.ts +38 -2
- package/src/engine/state-types.ts +10 -0
- package/src/engine/state.ts +10 -0
- package/src/engine/step-trace.ts +55 -2
- package/src/engine/usage.ts +312 -0
- package/src/harness/action-context.ts +3 -0
- package/src/harness/delegation-response-actions.ts +21 -1
- package/src/harness/step-execution-actions.ts +19 -4
- package/src/index.ts +3 -1
- package/src/integrations/subagents/client.ts +80 -1
- package/src/integrations/subagents/protocol-events.ts +9 -1
- package/src/runtime/main-step-runtime-types.ts +2 -0
- package/src/runtime/main-step-trace.ts +36 -1
- package/src/workflow-status/format-status.ts +7 -1
- package/src/workflow-status/format-usage.ts +33 -0
- package/src/workflow-status/render-path.ts +9 -2
- package/src/workflow-status/render-step-detail.ts +29 -0
- package/src/workflow-status/render-summary.ts +16 -0
- package/src/workflow-status/types.ts +2 -0
|
@@ -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
|
-
|
|
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();
|
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:
|
|
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?: {
|
|
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
|
-
|
|
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 = {
|
|
@@ -30,7 +31,9 @@ export function formatWorkflowProgressStatus(
|
|
|
30
31
|
snapshot.execution?.kind === 'subagent'
|
|
31
32
|
? ` · ${snapshot.execution.progress}`
|
|
32
33
|
: '';
|
|
33
|
-
|
|
34
|
+
const usage = workflowUsage(run);
|
|
35
|
+
const usageText = usage.models.length > 0 ? ` · ${formatUsage(usage)}` : '';
|
|
36
|
+
return `${workflowStatusIcon(run, snapshot.now)} ${run.workflowId} · step ${currentStep} · ${activity}${workerProgress}${usageText} · ${statusShortcutLabel}`;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/** Format a plain-text workflow status suitable for fallback notifications. */
|
|
@@ -50,6 +53,9 @@ export function formatWorkflowStatusText(
|
|
|
50
53
|
`Status: ${run.status}`,
|
|
51
54
|
`Step: ${run.currentStepId}`,
|
|
52
55
|
`Completed steps: ${run.history.length}`,
|
|
56
|
+
...(workflowUsage(run).models.length > 0
|
|
57
|
+
? [`Usage: ${formatUsage(workflowUsage(run))}`]
|
|
58
|
+
: []),
|
|
53
59
|
];
|
|
54
60
|
if (run.cwd && run.startCwd && run.cwd !== run.startCwd) {
|
|
55
61
|
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),
|
|
@@ -404,6 +419,20 @@ export function renderStepDetail(
|
|
|
404
419
|
...keyValueLines(theme, 'step', entry.stepId, width),
|
|
405
420
|
...keyValueLines(theme, 'visit', String(entry.visit), width),
|
|
406
421
|
...keyValueLines(theme, 'status', entry.status, width),
|
|
422
|
+
...(entry.usage
|
|
423
|
+
? [
|
|
424
|
+
...keyValueLines(theme, 'usage', formatUsage(entry.usage), width),
|
|
425
|
+
...entry.usage.models.flatMap((model) =>
|
|
426
|
+
keyValueLines(
|
|
427
|
+
theme,
|
|
428
|
+
'model',
|
|
429
|
+
`${model.provider}/${model.model} · ${formatUsage({ usage: model.usage, models: [] })}`,
|
|
430
|
+
width,
|
|
431
|
+
'muted',
|
|
432
|
+
),
|
|
433
|
+
),
|
|
434
|
+
]
|
|
435
|
+
: []),
|
|
407
436
|
...(history
|
|
408
437
|
? [
|
|
409
438
|
...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 =
|
|
@@ -38,6 +39,7 @@ export type PathEntry = {
|
|
|
38
39
|
readonly status: StepDisplayStatus;
|
|
39
40
|
readonly visit: number;
|
|
40
41
|
readonly outcome?: string;
|
|
42
|
+
readonly usage?: UsageAggregate;
|
|
41
43
|
readonly isCurrent: boolean;
|
|
42
44
|
};
|
|
43
45
|
|