@outputai/cli 0.7.1-next.d67ad85.0 → 0.7.1-next.db8ddd7.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/api/generated/api.d.ts +2 -2
- package/dist/api/generated/api.js +2 -2
- package/dist/api/workflow_catalog.d.ts +7 -0
- package/dist/api/workflow_catalog.js +11 -0
- package/dist/api/workflow_catalog.spec.d.ts +1 -0
- package/dist/api/workflow_catalog.spec.js +30 -0
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/workflow/list.d.ts +2 -0
- package/dist/commands/workflow/list.js +23 -21
- package/dist/commands/workflow/list.spec.js +57 -0
- package/dist/commands/workflow/status.js +6 -1
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/cost_calculator.d.ts +1 -5
- package/dist/services/cost_calculator.js +214 -102
- package/dist/services/cost_calculator.spec.js +329 -253
- package/dist/services/workflow_runs.js +6 -1
- package/dist/types/cost.d.ts +64 -23
- package/dist/types/cost.js +4 -0
- package/dist/utils/cost_formatter.js +65 -43
- package/dist/utils/format_workflow_result.js +4 -2
- package/dist/utils/format_workflow_result.spec.js +13 -3
- package/dist/utils/normalize_workflow_status.d.ts +8 -0
- package/dist/utils/normalize_workflow_status.js +8 -0
- package/dist/utils/normalize_workflow_status.spec.d.ts +1 -0
- package/dist/utils/normalize_workflow_status.spec.js +13 -0
- package/dist/utils/scenario_resolver.js +3 -11
- package/dist/utils/scenario_resolver.spec.js +5 -9
- package/dist/views/dev/components/workflow_status.js +1 -1
- package/dist/views/dev/hooks/use_run_detail.js +6 -1
- package/dist/views/dev/hooks/use_run_detail.spec.js +1 -1
- package/dist/views/dev/hooks/use_workflow_catalog.js +2 -6
- package/dist/views/dev/panels/runs_panel.js +1 -1
- package/oclif.manifest.json +19 -2
- package/package.json +4 -4
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Workflow runs service for fetching workflow run data from the API
|
|
3
3
|
*/
|
|
4
4
|
import { getWorkflowRuns } from '#api/generated/api.js';
|
|
5
|
+
import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
|
|
5
6
|
export async function fetchWorkflowRuns(options = {}) {
|
|
6
7
|
const params = {};
|
|
7
8
|
if (options.limit) {
|
|
@@ -21,8 +22,12 @@ export async function fetchWorkflowRuns(options = {}) {
|
|
|
21
22
|
throw new Error('API returned invalid response (missing data)');
|
|
22
23
|
}
|
|
23
24
|
const data = response.data;
|
|
25
|
+
const runs = (data.runs || []).map(run => ({
|
|
26
|
+
...run,
|
|
27
|
+
status: normalizeWorkflowStatus(run.status)
|
|
28
|
+
}));
|
|
24
29
|
return {
|
|
25
|
-
runs
|
|
30
|
+
runs,
|
|
26
31
|
count: data.count || 0
|
|
27
32
|
};
|
|
28
33
|
}
|
package/dist/types/cost.d.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Cost Calculator Types
|
|
3
3
|
*
|
|
4
4
|
* TypeScript interfaces for trace parsing, pricing configuration, and cost reports.
|
|
5
|
+
*
|
|
6
|
+
* Costs are sourced from the trace events themselves (the as-charged "original"
|
|
7
|
+
* cost), with `costs.yml` applied as an optional override layer (the "adjusted"
|
|
8
|
+
* cost). Both figures are surfaced per model and per host.
|
|
5
9
|
*/
|
|
6
10
|
export interface TokenUsage {
|
|
7
11
|
inputTokens?: number;
|
|
@@ -9,6 +13,25 @@ export interface TokenUsage {
|
|
|
9
13
|
cachedInputTokens?: number;
|
|
10
14
|
reasoningTokens?: number;
|
|
11
15
|
}
|
|
16
|
+
export type { LLMUsageEvent } from '@outputai/llm';
|
|
17
|
+
import type { LLMUsageEvent } from '@outputai/llm';
|
|
18
|
+
export type LLMUsageLine = LLMUsageEvent['usage'][number];
|
|
19
|
+
export interface HTTPCostEvent {
|
|
20
|
+
type: 'http:request:cost';
|
|
21
|
+
url: string;
|
|
22
|
+
requestId: string;
|
|
23
|
+
total: number;
|
|
24
|
+
}
|
|
25
|
+
export interface HTTPCountEvent {
|
|
26
|
+
type: 'http:request:count';
|
|
27
|
+
url: string;
|
|
28
|
+
requestId: string;
|
|
29
|
+
}
|
|
30
|
+
export interface NodeAttributes {
|
|
31
|
+
'llm:usage'?: LLMUsageEvent;
|
|
32
|
+
'http:request:cost'?: HTTPCostEvent;
|
|
33
|
+
'http:request:count'?: HTTPCountEvent;
|
|
34
|
+
}
|
|
12
35
|
export interface TraceNode {
|
|
13
36
|
id?: string;
|
|
14
37
|
kind: string;
|
|
@@ -17,15 +40,16 @@ export interface TraceNode {
|
|
|
17
40
|
endedAt?: number;
|
|
18
41
|
children?: TraceNode[];
|
|
19
42
|
input?: Record<string, unknown>;
|
|
20
|
-
output?: Record<string, unknown
|
|
21
|
-
|
|
22
|
-
};
|
|
43
|
+
output?: Record<string, unknown>;
|
|
44
|
+
attributes?: NodeAttributes;
|
|
23
45
|
}
|
|
24
46
|
export interface LLMCall {
|
|
25
47
|
stepName: string;
|
|
26
48
|
llmName: string;
|
|
27
49
|
model: string;
|
|
28
50
|
usage: TokenUsage;
|
|
51
|
+
originalCost: number;
|
|
52
|
+
lines: LLMUsageLine[];
|
|
29
53
|
}
|
|
30
54
|
export interface HTTPCall {
|
|
31
55
|
stepName: string;
|
|
@@ -34,6 +58,8 @@ export interface HTTPCall {
|
|
|
34
58
|
input: Record<string, unknown>;
|
|
35
59
|
output: Record<string, unknown>;
|
|
36
60
|
status?: number;
|
|
61
|
+
host: string;
|
|
62
|
+
originalCost?: number;
|
|
37
63
|
}
|
|
38
64
|
export interface ModelPricing {
|
|
39
65
|
provider: string;
|
|
@@ -80,47 +106,60 @@ export interface LLMCostResult {
|
|
|
80
106
|
output: number;
|
|
81
107
|
cached: number;
|
|
82
108
|
reasoning: number;
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
originalCost: number;
|
|
110
|
+
adjustedCost: number;
|
|
85
111
|
}
|
|
86
112
|
export interface ServiceCostResult {
|
|
87
113
|
step: string;
|
|
88
114
|
cost: number;
|
|
89
115
|
usage: string;
|
|
116
|
+
kind: 'computed' | 'estimated' | 'failed';
|
|
90
117
|
model?: string;
|
|
91
118
|
endpoint?: string;
|
|
92
119
|
warning?: string;
|
|
93
120
|
details?: Record<string, unknown>;
|
|
94
121
|
}
|
|
95
|
-
export interface
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
export interface HTTPCostResult {
|
|
123
|
+
step: string;
|
|
124
|
+
host: string;
|
|
125
|
+
usage: string;
|
|
126
|
+
originalCost: number;
|
|
127
|
+
adjustedCost: number;
|
|
128
|
+
}
|
|
129
|
+
export interface HostCostSummary {
|
|
130
|
+
host: string;
|
|
131
|
+
calls: HTTPCostResult[];
|
|
132
|
+
originalTotalCost: number;
|
|
133
|
+
adjustedTotalCost: number;
|
|
99
134
|
}
|
|
100
135
|
export interface CostReport {
|
|
101
136
|
traceFile: string;
|
|
102
137
|
workflowName: string;
|
|
103
138
|
durationMs: number | null;
|
|
104
139
|
llmCalls: LLMCostResult[];
|
|
105
|
-
|
|
140
|
+
llmOriginalCost: number;
|
|
141
|
+
llmAdjustedCost: number;
|
|
106
142
|
totalInputTokens: number;
|
|
107
143
|
totalOutputTokens: number;
|
|
108
144
|
totalCachedTokens: number;
|
|
109
145
|
totalReasoningTokens: number;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
146
|
+
httpCosts: HostCostSummary[];
|
|
147
|
+
httpOriginalCost: number;
|
|
148
|
+
httpAdjustedCost: number;
|
|
149
|
+
originalTotalCost: number;
|
|
113
150
|
totalCost: number;
|
|
114
151
|
}
|
|
115
152
|
export interface LLMModelSummary {
|
|
116
153
|
model: string;
|
|
117
154
|
count: number;
|
|
118
|
-
|
|
155
|
+
originalCost: number;
|
|
156
|
+
adjustedCost: number;
|
|
119
157
|
}
|
|
120
|
-
export interface
|
|
121
|
-
|
|
158
|
+
export interface HostSummary {
|
|
159
|
+
host: string;
|
|
122
160
|
callCount: number;
|
|
123
|
-
|
|
161
|
+
originalCost: number;
|
|
162
|
+
adjustedCost: number;
|
|
124
163
|
}
|
|
125
164
|
export interface VerboseFlags {
|
|
126
165
|
hasReasoning: boolean;
|
|
@@ -132,18 +171,20 @@ export interface ParsedCostData {
|
|
|
132
171
|
duration: string;
|
|
133
172
|
llmModels: LLMModelSummary[];
|
|
134
173
|
llmTotalCalls: number;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
174
|
+
llmOriginalCost: number;
|
|
175
|
+
llmAdjustedCost: number;
|
|
176
|
+
hosts: HostSummary[];
|
|
177
|
+
httpTotalCalls: number;
|
|
178
|
+
httpOriginalCost: number;
|
|
179
|
+
httpAdjustedCost: number;
|
|
139
180
|
verbose: VerboseFlags;
|
|
140
181
|
llmCalls: LLMCostResult[];
|
|
141
|
-
|
|
182
|
+
httpDetails: HostCostSummary[];
|
|
142
183
|
totalInputTokens: number;
|
|
143
184
|
totalOutputTokens: number;
|
|
144
185
|
totalCachedTokens: number;
|
|
145
186
|
totalReasoningTokens: number;
|
|
187
|
+
originalTotalCost: number;
|
|
146
188
|
totalCost: number;
|
|
147
|
-
unknownModels: string[];
|
|
148
189
|
isEmpty: boolean;
|
|
149
190
|
}
|
package/dist/types/cost.js
CHANGED
|
@@ -2,5 +2,9 @@
|
|
|
2
2
|
* Cost Calculator Types
|
|
3
3
|
*
|
|
4
4
|
* TypeScript interfaces for trace parsing, pricing configuration, and cost reports.
|
|
5
|
+
*
|
|
6
|
+
* Costs are sourced from the trace events themselves (the as-charged "original"
|
|
7
|
+
* cost), with `costs.yml` applied as an optional override layer (the "adjusted"
|
|
8
|
+
* cost). Both figures are surfaced per model and per host.
|
|
5
9
|
*/
|
|
6
10
|
export {};
|
|
@@ -29,76 +29,96 @@ export function parseCostData(report) {
|
|
|
29
29
|
const byModel = {};
|
|
30
30
|
for (const r of report.llmCalls) {
|
|
31
31
|
if (!byModel[r.model]) {
|
|
32
|
-
byModel[r.model] = { count: 0,
|
|
32
|
+
byModel[r.model] = { count: 0, originalCost: 0, adjustedCost: 0 };
|
|
33
33
|
}
|
|
34
34
|
byModel[r.model].count++;
|
|
35
|
-
byModel[r.model].
|
|
35
|
+
byModel[r.model].originalCost += r.originalCost;
|
|
36
|
+
byModel[r.model].adjustedCost += r.adjustedCost;
|
|
36
37
|
}
|
|
37
38
|
const llmModels = Object.entries(byModel)
|
|
38
|
-
.sort((a, b) => b[1].
|
|
39
|
-
.map(([model,
|
|
40
|
-
const
|
|
41
|
-
.sort((a, b) => b.
|
|
42
|
-
.map(
|
|
43
|
-
|
|
39
|
+
.sort((a, b) => b[1].adjustedCost - a[1].adjustedCost)
|
|
40
|
+
.map(([model, s]) => ({ model, ...s }));
|
|
41
|
+
const hosts = [...report.httpCosts]
|
|
42
|
+
.sort((a, b) => b.adjustedTotalCost - a.adjustedTotalCost)
|
|
43
|
+
.map(h => ({
|
|
44
|
+
host: h.host,
|
|
45
|
+
callCount: h.calls.length,
|
|
46
|
+
originalCost: h.originalTotalCost,
|
|
47
|
+
adjustedCost: h.adjustedTotalCost
|
|
48
|
+
}));
|
|
49
|
+
const httpTotalCalls = hosts.reduce((sum, h) => sum + h.callCount, 0);
|
|
44
50
|
return {
|
|
45
51
|
traceFile: report.traceFile,
|
|
46
52
|
workflowName: report.workflowName,
|
|
47
53
|
duration: report.durationMs ? `${(report.durationMs / 1000).toFixed(1)}s` : 'N/A',
|
|
48
54
|
llmModels,
|
|
49
55
|
llmTotalCalls: report.llmCalls.length,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
llmOriginalCost: report.llmOriginalCost,
|
|
57
|
+
llmAdjustedCost: report.llmAdjustedCost,
|
|
58
|
+
hosts,
|
|
59
|
+
httpTotalCalls,
|
|
60
|
+
httpOriginalCost: report.httpOriginalCost,
|
|
61
|
+
httpAdjustedCost: report.httpAdjustedCost,
|
|
54
62
|
verbose: {
|
|
55
63
|
hasReasoning: report.totalReasoningTokens > 0,
|
|
56
64
|
hasCached: report.totalCachedTokens > 0
|
|
57
65
|
},
|
|
58
66
|
llmCalls: report.llmCalls,
|
|
59
|
-
|
|
67
|
+
httpDetails: report.httpCosts,
|
|
60
68
|
totalInputTokens: report.totalInputTokens,
|
|
61
69
|
totalOutputTokens: report.totalOutputTokens,
|
|
62
70
|
totalCachedTokens: report.totalCachedTokens,
|
|
63
71
|
totalReasoningTokens: report.totalReasoningTokens,
|
|
72
|
+
originalTotalCost: report.originalTotalCost,
|
|
64
73
|
totalCost: report.totalCost,
|
|
65
|
-
|
|
66
|
-
isEmpty: report.llmCalls.length === 0 && report.services.length === 0
|
|
74
|
+
isEmpty: report.llmCalls.length === 0 && report.httpCosts.length === 0
|
|
67
75
|
};
|
|
68
76
|
}
|
|
69
77
|
function formatSummary(data) {
|
|
70
78
|
const lines = [];
|
|
71
79
|
if (data.llmModels.length > 0) {
|
|
72
80
|
const table = new Table({
|
|
73
|
-
head: ['Model', 'Calls', '
|
|
81
|
+
head: ['Model', 'Calls', 'Original', 'Adjusted'],
|
|
74
82
|
style: { head: ['cyan'] },
|
|
75
|
-
colAligns: ['left', 'right', 'right']
|
|
83
|
+
colAligns: ['left', 'right', 'right', 'right']
|
|
76
84
|
});
|
|
77
85
|
for (const m of data.llmModels) {
|
|
78
|
-
table.push([
|
|
86
|
+
table.push([
|
|
87
|
+
m.model,
|
|
88
|
+
pluralize(m.count, 'call'),
|
|
89
|
+
formatCurrency(m.originalCost),
|
|
90
|
+
formatCurrency(m.adjustedCost)
|
|
91
|
+
]);
|
|
79
92
|
}
|
|
80
93
|
table.push([
|
|
81
94
|
'Subtotal',
|
|
82
95
|
pluralize(data.llmTotalCalls, 'call'),
|
|
83
|
-
formatCurrency(data.
|
|
96
|
+
formatCurrency(data.llmOriginalCost),
|
|
97
|
+
formatCurrency(data.llmAdjustedCost)
|
|
84
98
|
]);
|
|
85
99
|
lines.push('LLM Costs:');
|
|
86
100
|
lines.push(table.toString());
|
|
87
101
|
lines.push('');
|
|
88
102
|
}
|
|
89
|
-
if (data.
|
|
103
|
+
if (data.hosts.length > 0) {
|
|
90
104
|
const table = new Table({
|
|
91
|
-
head: ['
|
|
105
|
+
head: ['Host', 'Calls', 'Original', 'Adjusted'],
|
|
92
106
|
style: { head: ['cyan'] },
|
|
93
|
-
colAligns: ['left', 'right', 'right']
|
|
107
|
+
colAligns: ['left', 'right', 'right', 'right']
|
|
94
108
|
});
|
|
95
|
-
for (const
|
|
96
|
-
table.push([
|
|
109
|
+
for (const h of data.hosts) {
|
|
110
|
+
table.push([
|
|
111
|
+
h.host,
|
|
112
|
+
pluralize(h.callCount, 'call'),
|
|
113
|
+
formatCurrency(h.originalCost),
|
|
114
|
+
formatCurrency(h.adjustedCost)
|
|
115
|
+
]);
|
|
97
116
|
}
|
|
98
117
|
table.push([
|
|
99
118
|
'Subtotal',
|
|
100
|
-
pluralize(data.
|
|
101
|
-
formatCurrency(data.
|
|
119
|
+
pluralize(data.httpTotalCalls, 'call'),
|
|
120
|
+
formatCurrency(data.httpOriginalCost),
|
|
121
|
+
formatCurrency(data.httpAdjustedCost)
|
|
102
122
|
]);
|
|
103
123
|
lines.push('API Costs:');
|
|
104
124
|
lines.push(table.toString());
|
|
@@ -119,8 +139,8 @@ function formatVerbose(data) {
|
|
|
119
139
|
head.push('Reasoning');
|
|
120
140
|
colAligns.push('right');
|
|
121
141
|
}
|
|
122
|
-
head.push('
|
|
123
|
-
colAligns.push('right');
|
|
142
|
+
head.push('Original', 'Adjusted');
|
|
143
|
+
colAligns.push('right', 'right');
|
|
124
144
|
const table = new Table({
|
|
125
145
|
head,
|
|
126
146
|
style: { head: ['cyan'] },
|
|
@@ -139,7 +159,7 @@ function formatVerbose(data) {
|
|
|
139
159
|
if (data.verbose.hasReasoning) {
|
|
140
160
|
row.push(formatNumber(r.reasoning));
|
|
141
161
|
}
|
|
142
|
-
row.push(formatCurrency(r.
|
|
162
|
+
row.push(formatCurrency(r.originalCost), formatCurrency(r.adjustedCost));
|
|
143
163
|
table.push(row);
|
|
144
164
|
}
|
|
145
165
|
const totalRow = [
|
|
@@ -154,29 +174,34 @@ function formatVerbose(data) {
|
|
|
154
174
|
if (data.verbose.hasReasoning) {
|
|
155
175
|
totalRow.push(formatNumber(data.totalReasoningTokens));
|
|
156
176
|
}
|
|
157
|
-
totalRow.push(formatCurrency(data.
|
|
177
|
+
totalRow.push(formatCurrency(data.llmOriginalCost), formatCurrency(data.llmAdjustedCost));
|
|
158
178
|
table.push(totalRow);
|
|
159
179
|
lines.push('LLM Calls:');
|
|
160
180
|
lines.push(table.toString());
|
|
161
181
|
lines.push('');
|
|
162
182
|
}
|
|
163
|
-
if (data.
|
|
183
|
+
if (data.httpDetails.length > 0) {
|
|
164
184
|
const table = new Table({
|
|
165
|
-
head: ['
|
|
185
|
+
head: ['Host', 'Step', 'Usage', 'Original', 'Adjusted'],
|
|
166
186
|
style: { head: ['cyan'] },
|
|
167
|
-
colAligns: ['left', 'left', 'right', 'right']
|
|
187
|
+
colAligns: ['left', 'left', 'right', 'right', 'right']
|
|
168
188
|
});
|
|
169
|
-
for (const
|
|
170
|
-
for (const call of
|
|
189
|
+
for (const host of data.httpDetails) {
|
|
190
|
+
for (const call of host.calls) {
|
|
171
191
|
table.push([
|
|
172
|
-
|
|
192
|
+
host.host,
|
|
173
193
|
call.step,
|
|
174
194
|
call.usage,
|
|
175
|
-
formatCurrency(call.
|
|
195
|
+
formatCurrency(call.originalCost),
|
|
196
|
+
formatCurrency(call.adjustedCost)
|
|
176
197
|
]);
|
|
177
198
|
}
|
|
178
199
|
}
|
|
179
|
-
table.push([
|
|
200
|
+
table.push([
|
|
201
|
+
'Subtotal', '', '',
|
|
202
|
+
formatCurrency(data.httpOriginalCost),
|
|
203
|
+
formatCurrency(data.httpAdjustedCost)
|
|
204
|
+
]);
|
|
180
205
|
lines.push('API Calls:');
|
|
181
206
|
lines.push(table.toString());
|
|
182
207
|
lines.push('');
|
|
@@ -203,13 +228,10 @@ export function formatCostReport(report, options = {}) {
|
|
|
203
228
|
colAligns: ['left', 'right'],
|
|
204
229
|
colWidths: [36, 12]
|
|
205
230
|
});
|
|
206
|
-
totalTable.push(['TOTAL ESTIMATED COST', formatCurrency(data.totalCost)]);
|
|
231
|
+
totalTable.push(['TOTAL ESTIMATED COST (adjusted)', formatCurrency(data.totalCost)]);
|
|
232
|
+
totalTable.push(['As-charged (from trace)', formatCurrency(data.originalTotalCost)]);
|
|
207
233
|
lines.push(totalTable.toString());
|
|
208
234
|
}
|
|
209
|
-
if (data.unknownModels.length > 0) {
|
|
210
|
-
lines.push('');
|
|
211
|
-
lines.push(`Warning: Unknown models (add to config/costs.yml): ${data.unknownModels.join(', ')}`);
|
|
212
|
-
}
|
|
213
235
|
if (data.isEmpty) {
|
|
214
236
|
lines.push('No billable calls found in trace.');
|
|
215
237
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { normalizeWorkflowStatus } from './normalize_workflow_status.js';
|
|
1
2
|
export const ERROR_STATUSES = new Set(['failed', 'canceled', 'terminated', 'timed_out']);
|
|
2
3
|
export function formatWorkflowResult(result) {
|
|
4
|
+
const status = normalizeWorkflowStatus(result.status);
|
|
3
5
|
const lines = [
|
|
4
6
|
`Workflow ID: ${result.workflowId || 'unknown'}`,
|
|
5
7
|
''
|
|
6
8
|
];
|
|
7
|
-
if (
|
|
9
|
+
if (status === 'completed') {
|
|
8
10
|
lines.push('Output:');
|
|
9
11
|
lines.push(JSON.stringify(result.output, null, 2));
|
|
10
12
|
}
|
|
11
13
|
else {
|
|
12
|
-
lines.push(`Status: ${
|
|
14
|
+
lines.push(`Status: ${status || 'unknown'}`);
|
|
13
15
|
if (result.error) {
|
|
14
16
|
lines.push(`Error: ${result.error}`);
|
|
15
17
|
}
|
|
@@ -46,16 +46,26 @@ describe('formatWorkflowResult', () => {
|
|
|
46
46
|
expect(result).toContain('Status: canceled');
|
|
47
47
|
expect(result).toContain('Error: Workflow was canceled');
|
|
48
48
|
});
|
|
49
|
-
it('should display status without error line for
|
|
49
|
+
it('should display status without error line for continued_as_new workflows', () => {
|
|
50
50
|
const result = formatWorkflowResult({
|
|
51
51
|
workflowId: 'wf-cont',
|
|
52
|
-
status: '
|
|
52
|
+
status: 'continued_as_new',
|
|
53
53
|
output: null,
|
|
54
54
|
error: null
|
|
55
55
|
});
|
|
56
|
-
expect(result).toContain('Status:
|
|
56
|
+
expect(result).toContain('Status: continued_as_new');
|
|
57
57
|
expect(result).not.toContain('Error:');
|
|
58
58
|
});
|
|
59
|
+
it('temporarily normalizes legacy continued status to continued_as_new', () => {
|
|
60
|
+
const legacyResult = {
|
|
61
|
+
workflowId: 'wf-cont',
|
|
62
|
+
status: 'continued',
|
|
63
|
+
output: null,
|
|
64
|
+
error: null
|
|
65
|
+
};
|
|
66
|
+
const result = formatWorkflowResult(legacyResult);
|
|
67
|
+
expect(result).toContain('Status: continued_as_new');
|
|
68
|
+
});
|
|
59
69
|
it('should omit error line when error is null on failed workflow', () => {
|
|
60
70
|
const result = formatWorkflowResult({
|
|
61
71
|
workflowId: 'wf-789',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary compatibility for API responses produced before CONTINUED_AS_NEW
|
|
3
|
+
* was exposed as `continued_as_new`.
|
|
4
|
+
*
|
|
5
|
+
* @param status - Workflow status from the API
|
|
6
|
+
* @returns Normalized workflow status
|
|
7
|
+
*/
|
|
8
|
+
export declare const normalizeWorkflowStatus: <T extends string | null | undefined>(status: T) => T | "continued_as_new";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary compatibility for API responses produced before CONTINUED_AS_NEW
|
|
3
|
+
* was exposed as `continued_as_new`.
|
|
4
|
+
*
|
|
5
|
+
* @param status - Workflow status from the API
|
|
6
|
+
* @returns Normalized workflow status
|
|
7
|
+
*/
|
|
8
|
+
export const normalizeWorkflowStatus = (status) => status === 'continued' ? 'continued_as_new' : status;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeWorkflowStatus } from './normalize_workflow_status.js';
|
|
3
|
+
describe('normalizeWorkflowStatus', () => {
|
|
4
|
+
it('temporarily maps continued to continued_as_new', () => {
|
|
5
|
+
expect(normalizeWorkflowStatus('continued')).toBe('continued_as_new');
|
|
6
|
+
});
|
|
7
|
+
it('leaves other statuses and nullish values unchanged', () => {
|
|
8
|
+
expect(normalizeWorkflowStatus('completed')).toBe('completed');
|
|
9
|
+
expect(normalizeWorkflowStatus('continued_as_new')).toBe('continued_as_new');
|
|
10
|
+
expect(normalizeWorkflowStatus(null)).toBeNull();
|
|
11
|
+
expect(normalizeWorkflowStatus(undefined)).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
|
|
4
4
|
import { getWorkflowsBasePath } from '#utils/paths.js';
|
|
5
5
|
const SCENARIOS_DIR = 'scenarios';
|
|
6
6
|
const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
|
|
@@ -30,17 +30,9 @@ export function findWorkflowDirectoryFromPath(workflowPath, basePath = getWorkfl
|
|
|
30
30
|
}
|
|
31
31
|
async function fetchWorkflowPath(workflowName) {
|
|
32
32
|
try {
|
|
33
|
-
const
|
|
34
|
-
const data = response?.data;
|
|
35
|
-
const workflows = data?.workflows;
|
|
36
|
-
if (!workflows) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
33
|
+
const workflows = await fetchWorkflowCatalog();
|
|
39
34
|
const workflow = workflows.find(w => w.name === workflowName);
|
|
40
|
-
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
return workflow.path ?? null;
|
|
35
|
+
return workflow?.path ?? null;
|
|
44
36
|
}
|
|
45
37
|
catch {
|
|
46
38
|
return null;
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { resolveScenarioPath, getScenarioNotFoundMessage, extractWorkflowRelativePath, findWorkflowDirectoryFromPath, listScenariosForWorkflow } from './scenario_resolver.js';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
|
-
import * as
|
|
4
|
+
import * as catalog from '#api/workflow_catalog.js';
|
|
5
5
|
vi.mock('node:fs', () => ({
|
|
6
6
|
existsSync: vi.fn(),
|
|
7
7
|
readdirSync: vi.fn()
|
|
8
8
|
}));
|
|
9
|
-
vi.mock('#api/
|
|
10
|
-
|
|
9
|
+
vi.mock('#api/workflow_catalog.js', () => ({
|
|
10
|
+
fetchWorkflowCatalog: vi.fn()
|
|
11
11
|
}));
|
|
12
12
|
function mockCatalog(workflows) {
|
|
13
|
-
vi.mocked(
|
|
14
|
-
data: { workflows },
|
|
15
|
-
status: 200,
|
|
16
|
-
headers: new Headers()
|
|
17
|
-
});
|
|
13
|
+
vi.mocked(catalog.fetchWorkflowCatalog).mockResolvedValue(workflows);
|
|
18
14
|
}
|
|
19
15
|
function mockCatalogFailure() {
|
|
20
|
-
vi.mocked(
|
|
16
|
+
vi.mocked(catalog.fetchWorkflowCatalog).mockRejectedValue(new Error('API unavailable'));
|
|
21
17
|
}
|
|
22
18
|
describe('extractWorkflowRelativePath', () => {
|
|
23
19
|
it('should extract relative path from workflow.js path', () => {
|
|
@@ -7,7 +7,7 @@ const WORKFLOW_STATUS_MAP = {
|
|
|
7
7
|
canceled: { icon: '○', color: 'gray' },
|
|
8
8
|
terminated: { icon: '✗', color: 'gray' },
|
|
9
9
|
timed_out: { icon: '✗', color: 'red' },
|
|
10
|
-
|
|
10
|
+
continued_as_new: { icon: '↻', color: 'blue' }
|
|
11
11
|
};
|
|
12
12
|
const DEFAULT_DISPLAY = { icon: '?', color: 'white' };
|
|
13
13
|
export const resolveWorkflowStatus = (status) => WORKFLOW_STATUS_MAP[status] ?? DEFAULT_DISPLAY;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { getWorkflowIdResult, getWorkflowIdRunsRidResult, getWorkflowIdTraceLog, getWorkflowIdRunsRidTraceLog } from '#api/generated/api.js';
|
|
4
|
+
import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
|
|
4
5
|
const EMPTY_DETAIL = {
|
|
5
6
|
result: null,
|
|
6
7
|
trace: null,
|
|
@@ -88,7 +89,11 @@ const fetchResult = async (workflowId, runId) => {
|
|
|
88
89
|
const response = runId ?
|
|
89
90
|
await getWorkflowIdRunsRidResult(workflowId, runId) :
|
|
90
91
|
await getWorkflowIdResult(workflowId);
|
|
91
|
-
|
|
92
|
+
const data = response.data;
|
|
93
|
+
return {
|
|
94
|
+
...data,
|
|
95
|
+
status: normalizeWorkflowStatus(data.status)
|
|
96
|
+
};
|
|
92
97
|
}
|
|
93
98
|
catch {
|
|
94
99
|
return null;
|
|
@@ -10,7 +10,7 @@ describe('isTerminalRunStatus', () => {
|
|
|
10
10
|
});
|
|
11
11
|
it('returns false for in-progress states', () => {
|
|
12
12
|
expect(isTerminalRunStatus('running')).toBe(false);
|
|
13
|
-
expect(isTerminalRunStatus('
|
|
13
|
+
expect(isTerminalRunStatus('continued_as_new')).toBe(false);
|
|
14
14
|
});
|
|
15
15
|
it('returns false for nullish input', () => {
|
|
16
16
|
expect(isTerminalRunStatus(null)).toBe(false);
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
|
|
3
3
|
import { usePoll } from '#views/dev/hooks/use_poll.js';
|
|
4
4
|
const CATALOG_INTERVAL_MS = 10_000;
|
|
5
5
|
export const useWorkflowCatalog = (enabled) => {
|
|
6
6
|
const [workflows, setWorkflows] = useState([]);
|
|
7
7
|
usePoll(enabled, CATALOG_INTERVAL_MS, async () => {
|
|
8
8
|
try {
|
|
9
|
-
|
|
10
|
-
const data = response?.data;
|
|
11
|
-
if (data?.workflows) {
|
|
12
|
-
setWorkflows(data.workflows);
|
|
13
|
-
}
|
|
9
|
+
setWorkflows(await fetchWorkflowCatalog());
|
|
14
10
|
}
|
|
15
11
|
catch {
|
|
16
12
|
// API may not be ready yet
|
package/oclif.manifest.json
CHANGED
|
@@ -704,9 +704,26 @@
|
|
|
704
704
|
"<%= config.bin %> <%= command.id %> --format table",
|
|
705
705
|
"<%= config.bin %> <%= command.id %> --format json",
|
|
706
706
|
"<%= config.bin %> <%= command.id %> --detailed",
|
|
707
|
-
"<%= config.bin %> <%= command.id %> --filter simple"
|
|
707
|
+
"<%= config.bin %> <%= command.id %> --filter simple",
|
|
708
|
+
"<%= config.bin %> <%= command.id %> --catalog my-catalog"
|
|
708
709
|
],
|
|
709
710
|
"flags": {
|
|
711
|
+
"catalog": {
|
|
712
|
+
"aliases": [
|
|
713
|
+
"task-queue"
|
|
714
|
+
],
|
|
715
|
+
"char": "c",
|
|
716
|
+
"charAliases": [
|
|
717
|
+
"q"
|
|
718
|
+
],
|
|
719
|
+
"deprecateAliases": true,
|
|
720
|
+
"description": "Catalog to list workflows from (defaults to OUTPUT_CATALOG_ID)",
|
|
721
|
+
"env": "OUTPUT_CATALOG_ID",
|
|
722
|
+
"name": "catalog",
|
|
723
|
+
"hasDynamicHelp": false,
|
|
724
|
+
"multiple": false,
|
|
725
|
+
"type": "option"
|
|
726
|
+
},
|
|
710
727
|
"format": {
|
|
711
728
|
"char": "f",
|
|
712
729
|
"description": "Output format",
|
|
@@ -1424,5 +1441,5 @@
|
|
|
1424
1441
|
]
|
|
1425
1442
|
}
|
|
1426
1443
|
},
|
|
1427
|
-
"version": "0.7.1-next.
|
|
1444
|
+
"version": "0.7.1-next.db8ddd7.0"
|
|
1428
1445
|
}
|