@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.3
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/docs/http-api.md +66 -0
- package/package.json +1 -1
- package/src/api/__tests__/index.test.ts +311 -3
- package/src/api/index.ts +94 -45
- package/src/config/__tests__/statuses.test.ts +41 -0
- package/src/config/statuses.ts +20 -1
- package/src/core/__tests__/config.test.ts +31 -1
- package/src/core/__tests__/job-concurrency.test.ts +60 -0
- package/src/core/__tests__/job-view.test.ts +859 -0
- package/src/core/__tests__/orchestrator.test.ts +242 -0
- package/src/core/__tests__/runner-liveness.test.ts +865 -0
- package/src/core/__tests__/single-derivation.test.ts +159 -0
- package/src/core/config.ts +19 -0
- package/src/core/job-concurrency.ts +6 -1
- package/src/core/job-view.ts +329 -0
- package/src/core/orchestrator.ts +78 -6
- package/src/core/runner-liveness.ts +276 -0
- package/src/core/status-writer.ts +24 -5
- package/src/ui/client/__tests__/job-adapter.test.ts +78 -0
- package/src/ui/client/adapters/job-adapter.ts +10 -0
- package/src/ui/client/types.ts +2 -0
- package/src/ui/components/JobTable.tsx +29 -12
- package/src/ui/components/__tests__/JobTable.test.tsx +54 -1
- package/src/ui/components/types.ts +2 -0
- package/src/ui/dist/assets/{index--RH3sAt3.js → index-L6cvsCAx.js} +25 -12
- package/src/ui/dist/assets/{index--RH3sAt3.js.map → index-L6cvsCAx.js.map} +1 -1
- package/src/ui/dist/index.html +1 -1
- package/src/ui/embedded-assets.js +6 -6
- package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +57 -1
- package/src/ui/server/__tests__/job-endpoints.test.ts +115 -1
- package/src/ui/server/__tests__/sse-enhancer.test.ts +31 -0
- package/src/ui/server/config-bridge.ts +3 -4
- package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
- package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
- package/src/ui/server/endpoints/job-control-endpoints.ts +7 -68
- package/src/ui/server/endpoints/job-endpoints.ts +6 -0
- package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
- package/src/ui/state/__tests__/snapshot.test.ts +51 -0
- package/src/ui/state/__tests__/types.test.ts +98 -5
- package/src/ui/state/snapshot.ts +1 -1
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +43 -2
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +208 -22
- package/src/ui/state/transformers/list-transformer.ts +7 -3
- package/src/ui/state/transformers/status-transformer.ts +36 -170
- package/src/ui/state/types.ts +9 -47
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
deriveJobStatusFromTasks,
|
|
3
|
-
normalizeJobStatus,
|
|
4
|
-
normalizeTaskState,
|
|
5
|
-
} from "../../../config/statuses";
|
|
1
|
+
import { normalizeJobView } from "../../../core/job-view";
|
|
6
2
|
import type {
|
|
7
3
|
CanonicalJob,
|
|
8
|
-
CanonicalJobStatus,
|
|
9
4
|
CanonicalTask,
|
|
10
5
|
ComputedStatus,
|
|
11
|
-
GateInfo,
|
|
12
6
|
JobReadResult,
|
|
13
7
|
TransformationStats,
|
|
14
8
|
} from "../types";
|
|
@@ -18,141 +12,13 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
18
12
|
return value as Record<string, unknown>;
|
|
19
13
|
}
|
|
20
14
|
|
|
21
|
-
function asTaskFiles(value: unknown): CanonicalTask["files"] {
|
|
22
|
-
const record = asRecord(value);
|
|
23
|
-
return {
|
|
24
|
-
artifacts: Array.isArray(record?.["artifacts"]) ? (record["artifacts"] as string[]) : [],
|
|
25
|
-
logs: Array.isArray(record?.["logs"]) ? (record["logs"] as string[]) : [],
|
|
26
|
-
tmp: Array.isArray(record?.["tmp"]) ? (record["tmp"] as string[]) : [],
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function toGateInfo(value: unknown): GateInfo | null | undefined {
|
|
31
|
-
if (value === null) return null;
|
|
32
|
-
const record = asRecord(value);
|
|
33
|
-
if (!record) return undefined;
|
|
34
|
-
|
|
35
|
-
const artifacts = Array.isArray(record["artifacts"])
|
|
36
|
-
? record["artifacts"].filter((artifact): artifact is string => typeof artifact === "string")
|
|
37
|
-
: undefined;
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
afterTask: typeof record["afterTask"] === "string" ? record["afterTask"] : "",
|
|
41
|
-
message: typeof record["message"] === "string" ? record["message"] : "",
|
|
42
|
-
requestedAt: typeof record["requestedAt"] === "string" ? record["requestedAt"] : "",
|
|
43
|
-
...(artifacts && artifacts.length > 0 ? { artifacts } : {}),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function toTask(name: string, value: unknown): CanonicalTask {
|
|
48
|
-
const task = asRecord(value) ?? {};
|
|
49
|
-
return {
|
|
50
|
-
name,
|
|
51
|
-
state: normalizeTaskState(task["state"]),
|
|
52
|
-
files: asTaskFiles(task["files"]),
|
|
53
|
-
startedAt: typeof task["startedAt"] === "string" ? task["startedAt"] : null,
|
|
54
|
-
endedAt: typeof task["endedAt"] === "string" ? task["endedAt"] : null,
|
|
55
|
-
attempts: typeof task["attempts"] === "number" ? task["attempts"] : undefined,
|
|
56
|
-
restartCount: typeof task["restartCount"] === "number" ? task["restartCount"] : undefined,
|
|
57
|
-
retrying: typeof task["retrying"] === "boolean" ? task["retrying"] : undefined,
|
|
58
|
-
nextRetryAt: typeof task["nextRetryAt"] === "string" ? task["nextRetryAt"] : undefined,
|
|
59
|
-
lastRetryError: Object.hasOwn(task, "lastRetryError") ? task["lastRetryError"] as import("../types").RetryError | null : undefined,
|
|
60
|
-
executionTimeMs:
|
|
61
|
-
typeof task["executionTimeMs"] === "number" ? task["executionTimeMs"] : undefined,
|
|
62
|
-
refinementAttempts:
|
|
63
|
-
typeof task["refinementAttempts"] === "number"
|
|
64
|
-
? task["refinementAttempts"]
|
|
65
|
-
: undefined,
|
|
66
|
-
stageLogPath: typeof task["stageLogPath"] === "string" ? task["stageLogPath"] : undefined,
|
|
67
|
-
errorContext: task["errorContext"],
|
|
68
|
-
currentStage: typeof task["currentStage"] === "string" ? task["currentStage"] : undefined,
|
|
69
|
-
failedStage: typeof task["failedStage"] === "string" ? task["failedStage"] : undefined,
|
|
70
|
-
artifacts: task["artifacts"],
|
|
71
|
-
error: asRecord(task["error"]) as CanonicalTask["error"],
|
|
72
|
-
skipReason: typeof task["skipReason"] === "string" ? task["skipReason"] : undefined,
|
|
73
|
-
skippedBy: typeof task["skippedBy"] === "string" ? task["skippedBy"] : undefined,
|
|
74
|
-
controlApplied: typeof task["controlApplied"] === "boolean" ? task["controlApplied"] : undefined,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function getStatusValue(status: ReturnType<typeof deriveJobStatusFromTasks>): CanonicalJobStatus {
|
|
79
|
-
return status === "failed" ? "error" : status;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function getProgress(tasks: CanonicalTask[]): number {
|
|
83
|
-
if (tasks.length === 0) return 0;
|
|
84
|
-
|
|
85
|
-
const completed = tasks.filter((task) => task.state === "done" || task.state === "skipped").length;
|
|
86
|
-
return Math.floor((completed / tasks.length) * 100);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function getTitle(raw: Record<string, unknown>, jobId: string): string {
|
|
90
|
-
const title = raw["title"] ?? raw["name"];
|
|
91
|
-
return typeof title === "string" && title.trim() !== "" ? title : jobId;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getCosts(raw: Record<string, unknown>): Record<string, unknown> {
|
|
95
|
-
const explicit = asRecord(raw["costs"]);
|
|
96
|
-
if (explicit) return explicit;
|
|
97
|
-
|
|
98
|
-
// Aggregate cost and token totals from individual task entries
|
|
99
|
-
const tasks = asRecord(raw["tasks"]);
|
|
100
|
-
if (!tasks) return {};
|
|
101
|
-
|
|
102
|
-
let totalCost = 0;
|
|
103
|
-
let totalInputTokens = 0;
|
|
104
|
-
let totalOutputTokens = 0;
|
|
105
|
-
|
|
106
|
-
for (const value of Object.values(tasks)) {
|
|
107
|
-
const task = asRecord(value);
|
|
108
|
-
if (!task) continue;
|
|
109
|
-
|
|
110
|
-
// Sum from [model, input, output, cost?] tuples
|
|
111
|
-
const usage = task["tokenUsage"];
|
|
112
|
-
if (Array.isArray(usage)) {
|
|
113
|
-
for (const entry of usage) {
|
|
114
|
-
if (Array.isArray(entry) && entry.length >= 3) {
|
|
115
|
-
if (typeof entry[1] === "number") totalInputTokens += entry[1];
|
|
116
|
-
if (typeof entry[2] === "number") totalOutputTokens += entry[2];
|
|
117
|
-
if (typeof entry[3] === "number") totalCost += entry[3];
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const totalTokens = totalInputTokens + totalOutputTokens;
|
|
124
|
-
if (totalCost === 0 && totalTokens === 0) return {};
|
|
125
|
-
|
|
126
|
-
return { totalCost, totalTokens, totalInputTokens, totalOutputTokens };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
15
|
export function computeJobStatus(tasksInput: unknown): ComputedStatus {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
return { status: "pending", progress: 0 };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
status: getStatusValue(deriveJobStatusFromTasks(tasks)),
|
|
137
|
-
progress: getProgress(tasks),
|
|
138
|
-
};
|
|
16
|
+
const view = normalizeJobView({ tasks: tasksInput }, "", "");
|
|
17
|
+
return { status: view.status, progress: view.progress };
|
|
139
18
|
}
|
|
140
19
|
|
|
141
20
|
export function transformTasks(rawTasks: unknown): Record<string, CanonicalTask> {
|
|
142
|
-
|
|
143
|
-
return Object.fromEntries(
|
|
144
|
-
rawTasks.map((task, index) => {
|
|
145
|
-
const record = asRecord(task);
|
|
146
|
-
const name = typeof record?.["name"] === "string" ? (record["name"] as string) : `task-${index + 1}`;
|
|
147
|
-
return [name, toTask(name, task)];
|
|
148
|
-
}),
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const record = asRecord(rawTasks);
|
|
153
|
-
if (!record) return {};
|
|
154
|
-
|
|
155
|
-
return Object.fromEntries(Object.entries(record).map(([name, task]) => [name, toTask(name, task)]));
|
|
21
|
+
return normalizeJobView({ tasks: rawTasks }, "", "").tasks;
|
|
156
22
|
}
|
|
157
23
|
|
|
158
24
|
export function transformJobStatus(raw: unknown, jobId: string, location: string): CanonicalJob | null {
|
|
@@ -164,42 +30,41 @@ export function transformJobStatus(raw: unknown, jobId: string, location: string
|
|
|
164
30
|
console.warn(`job id mismatch: expected "${jobId}" but received "${rawJobId}"`);
|
|
165
31
|
}
|
|
166
32
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const snapshotStatus = normalizeJobStatus(record["status"] ?? record["state"]);
|
|
170
|
-
const gate = toGateInfo(record["gate"]);
|
|
171
|
-
const title = getTitle(record, jobId);
|
|
33
|
+
return normalizeJobView(raw, jobId, location);
|
|
34
|
+
}
|
|
172
35
|
|
|
36
|
+
export function buildUnreadableJob(result: JobReadResult): CanonicalJob {
|
|
37
|
+
const code = result.code ?? "UNKNOWN_READ_ERROR";
|
|
38
|
+
const detail = result.message ? ` Reader error: ${result.message}` : "";
|
|
39
|
+
const message =
|
|
40
|
+
`Status file is unreadable (${code}). ` +
|
|
41
|
+
`Repair or delete ${result.path ?? "tasks-status.json"} for job "${result.jobId}" to recover.` +
|
|
42
|
+
detail;
|
|
173
43
|
return {
|
|
174
|
-
id: jobId,
|
|
175
|
-
jobId,
|
|
176
|
-
name:
|
|
177
|
-
title,
|
|
178
|
-
status:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
pipeline: typeof record["pipeline"] === "string" ? record["pipeline"] : undefined,
|
|
189
|
-
pipelineLabel:
|
|
190
|
-
typeof record["pipelineLabel"] === "string" ? record["pipelineLabel"] : undefined,
|
|
191
|
-
pipelineConfig: asRecord(record["pipelineConfig"]) ?? undefined,
|
|
192
|
-
current: record["current"],
|
|
193
|
-
currentStage: record["currentStage"],
|
|
194
|
-
gate,
|
|
195
|
-
warnings: Array.isArray(record["warnings"]) ? (record["warnings"] as string[]) : undefined,
|
|
44
|
+
id: result.jobId,
|
|
45
|
+
jobId: result.jobId,
|
|
46
|
+
name: result.jobId,
|
|
47
|
+
title: result.jobId,
|
|
48
|
+
status: "failed",
|
|
49
|
+
progress: 0,
|
|
50
|
+
createdAt: null,
|
|
51
|
+
updatedAt: null,
|
|
52
|
+
location: result.location ?? null,
|
|
53
|
+
tasks: {},
|
|
54
|
+
files: {},
|
|
55
|
+
costs: {},
|
|
56
|
+
readable: false,
|
|
57
|
+
error: { code, message, path: result.path },
|
|
196
58
|
};
|
|
197
59
|
}
|
|
198
60
|
|
|
199
61
|
export function transformMultipleJobs(jobReadResults: JobReadResult[]): CanonicalJob[] {
|
|
200
62
|
return jobReadResults
|
|
201
|
-
.
|
|
202
|
-
|
|
63
|
+
.map((result) =>
|
|
64
|
+
result.ok
|
|
65
|
+
? transformJobStatus(result.data, result.jobId, result.location)
|
|
66
|
+
: result.code === "INVALID_JSON" ? buildUnreadableJob(result) : null,
|
|
67
|
+
)
|
|
203
68
|
.filter((job): job is CanonicalJob => job !== null);
|
|
204
69
|
}
|
|
205
70
|
|
|
@@ -208,6 +73,7 @@ export function getTransformationStats(
|
|
|
208
73
|
transformedJobs: CanonicalJob[],
|
|
209
74
|
): TransformationStats {
|
|
210
75
|
const successfulReads = readResults.filter((result) => result.ok).length;
|
|
76
|
+
const successfulTransforms = transformedJobs.filter((job) => job.readable !== false).length;
|
|
211
77
|
const statusDistribution = transformedJobs.reduce<Record<string, number>>((acc, job) => {
|
|
212
78
|
acc[job.status] = (acc[job.status] ?? 0) + 1;
|
|
213
79
|
return acc;
|
|
@@ -216,9 +82,9 @@ export function getTransformationStats(
|
|
|
216
82
|
return {
|
|
217
83
|
totalRead: readResults.length,
|
|
218
84
|
successfulReads,
|
|
219
|
-
successfulTransforms
|
|
220
|
-
failedTransforms: successfulReads -
|
|
221
|
-
transformationRate: successfulReads === 0 ? 0 :
|
|
85
|
+
successfulTransforms,
|
|
86
|
+
failedTransforms: successfulReads - successfulTransforms,
|
|
87
|
+
transformationRate: successfulReads === 0 ? 0 : successfulTransforms / successfulReads,
|
|
222
88
|
statusDistribution,
|
|
223
89
|
};
|
|
224
90
|
}
|
package/src/ui/state/types.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { JobStatusValue, TaskStateValue } from "../../config/statuses";
|
|
2
|
+
import type { JobView, JobViewTask } from "../../core/job-view";
|
|
2
3
|
|
|
3
4
|
export type ChangeType = "created" | "modified" | "deleted";
|
|
4
|
-
export type CanonicalJobStatus = JobStatusValue
|
|
5
|
+
export type CanonicalJobStatus = JobStatusValue;
|
|
5
6
|
|
|
6
7
|
export interface ChangeEntry {
|
|
7
8
|
path: string;
|
|
@@ -128,51 +129,9 @@ export interface GateInfo {
|
|
|
128
129
|
requestedAt: string;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
export
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
files: { artifacts: string[]; logs: string[]; tmp: string[] };
|
|
135
|
-
startedAt?: string | null;
|
|
136
|
-
endedAt?: string | null;
|
|
137
|
-
attempts?: number;
|
|
138
|
-
restartCount?: number;
|
|
139
|
-
retrying?: boolean;
|
|
140
|
-
nextRetryAt?: string;
|
|
141
|
-
lastRetryError?: RetryError | null;
|
|
142
|
-
executionTimeMs?: number;
|
|
143
|
-
refinementAttempts?: number;
|
|
144
|
-
stageLogPath?: string;
|
|
145
|
-
errorContext?: unknown;
|
|
146
|
-
currentStage?: string;
|
|
147
|
-
failedStage?: string;
|
|
148
|
-
artifacts?: unknown;
|
|
149
|
-
error?: { message: string; [key: string]: unknown } | null;
|
|
150
|
-
skipReason?: string;
|
|
151
|
-
skippedBy?: string;
|
|
152
|
-
controlApplied?: boolean;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface CanonicalJob {
|
|
156
|
-
id: string;
|
|
157
|
-
jobId: string;
|
|
158
|
-
name: string;
|
|
159
|
-
title: string;
|
|
160
|
-
status: CanonicalJobStatus;
|
|
161
|
-
progress: number;
|
|
162
|
-
createdAt: string | null;
|
|
163
|
-
updatedAt: string | null;
|
|
164
|
-
location: string | null;
|
|
165
|
-
tasks: Record<string, CanonicalTask>;
|
|
166
|
-
files: Record<string, unknown>;
|
|
167
|
-
costs: Record<string, unknown>;
|
|
168
|
-
pipeline?: string;
|
|
169
|
-
pipelineLabel?: string;
|
|
170
|
-
pipelineConfig?: Record<string, unknown>;
|
|
171
|
-
current?: unknown;
|
|
172
|
-
currentStage?: unknown;
|
|
173
|
-
gate?: GateInfo | null;
|
|
174
|
-
warnings?: string[];
|
|
175
|
-
}
|
|
132
|
+
export type CanonicalTask = JobViewTask;
|
|
133
|
+
|
|
134
|
+
export type CanonicalJob = JobView;
|
|
176
135
|
|
|
177
136
|
export interface JobReadResult {
|
|
178
137
|
ok: boolean;
|
|
@@ -181,6 +140,7 @@ export interface JobReadResult {
|
|
|
181
140
|
location: string;
|
|
182
141
|
code?: string;
|
|
183
142
|
message?: string;
|
|
143
|
+
path?: string;
|
|
184
144
|
}
|
|
185
145
|
|
|
186
146
|
export interface TransformationStats {
|
|
@@ -202,7 +162,7 @@ export interface JobListStats {
|
|
|
202
162
|
export interface GroupedJobs {
|
|
203
163
|
running: CanonicalJob[];
|
|
204
164
|
waiting: CanonicalJob[];
|
|
205
|
-
|
|
165
|
+
failed: CanonicalJob[];
|
|
206
166
|
pending: CanonicalJob[];
|
|
207
167
|
complete: CanonicalJob[];
|
|
208
168
|
}
|
|
@@ -234,6 +194,8 @@ export interface APIJob {
|
|
|
234
194
|
pipeline?: string;
|
|
235
195
|
pipelineLabel?: string;
|
|
236
196
|
pipelineConfig?: Record<string, unknown>;
|
|
197
|
+
readable?: boolean;
|
|
198
|
+
error?: { code: string; message: string; path?: string };
|
|
237
199
|
}
|
|
238
200
|
|
|
239
201
|
export interface AggregationStats {
|