@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.
Files changed (46) hide show
  1. package/docs/http-api.md +66 -0
  2. package/package.json +1 -1
  3. package/src/api/__tests__/index.test.ts +311 -3
  4. package/src/api/index.ts +94 -45
  5. package/src/config/__tests__/statuses.test.ts +41 -0
  6. package/src/config/statuses.ts +20 -1
  7. package/src/core/__tests__/config.test.ts +31 -1
  8. package/src/core/__tests__/job-concurrency.test.ts +60 -0
  9. package/src/core/__tests__/job-view.test.ts +859 -0
  10. package/src/core/__tests__/orchestrator.test.ts +242 -0
  11. package/src/core/__tests__/runner-liveness.test.ts +865 -0
  12. package/src/core/__tests__/single-derivation.test.ts +159 -0
  13. package/src/core/config.ts +19 -0
  14. package/src/core/job-concurrency.ts +6 -1
  15. package/src/core/job-view.ts +329 -0
  16. package/src/core/orchestrator.ts +78 -6
  17. package/src/core/runner-liveness.ts +276 -0
  18. package/src/core/status-writer.ts +24 -5
  19. package/src/ui/client/__tests__/job-adapter.test.ts +78 -0
  20. package/src/ui/client/adapters/job-adapter.ts +10 -0
  21. package/src/ui/client/types.ts +2 -0
  22. package/src/ui/components/JobTable.tsx +29 -12
  23. package/src/ui/components/__tests__/JobTable.test.tsx +54 -1
  24. package/src/ui/components/types.ts +2 -0
  25. package/src/ui/dist/assets/{index--RH3sAt3.js → index-L6cvsCAx.js} +25 -12
  26. package/src/ui/dist/assets/{index--RH3sAt3.js.map → index-L6cvsCAx.js.map} +1 -1
  27. package/src/ui/dist/index.html +1 -1
  28. package/src/ui/embedded-assets.js +6 -6
  29. package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
  30. package/src/ui/server/__tests__/job-control-endpoints.test.ts +57 -1
  31. package/src/ui/server/__tests__/job-endpoints.test.ts +115 -1
  32. package/src/ui/server/__tests__/sse-enhancer.test.ts +31 -0
  33. package/src/ui/server/config-bridge.ts +3 -4
  34. package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
  35. package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
  36. package/src/ui/server/endpoints/job-control-endpoints.ts +7 -68
  37. package/src/ui/server/endpoints/job-endpoints.ts +6 -0
  38. package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
  39. package/src/ui/state/__tests__/snapshot.test.ts +51 -0
  40. package/src/ui/state/__tests__/types.test.ts +98 -5
  41. package/src/ui/state/snapshot.ts +1 -1
  42. package/src/ui/state/transformers/__tests__/list-transformer.test.ts +43 -2
  43. package/src/ui/state/transformers/__tests__/status-transformer.test.ts +208 -22
  44. package/src/ui/state/transformers/list-transformer.ts +7 -3
  45. package/src/ui/state/transformers/status-transformer.ts +36 -170
  46. 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 tasks = Object.values(transformTasks(tasksInput));
131
- if (tasks.length === 0) {
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
- if (Array.isArray(rawTasks)) {
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
- const tasks = transformTasks(record["tasks"]);
168
- const computed = computeJobStatus(tasks);
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: title,
177
- title,
178
- status: snapshotStatus === "waiting" || snapshotStatus === "failed"
179
- ? getStatusValue(snapshotStatus)
180
- : computed.status,
181
- progress: computed.progress,
182
- createdAt: typeof record["createdAt"] === "string" ? record["createdAt"] : null,
183
- updatedAt: typeof record["updatedAt"] === "string" ? record["updatedAt"] : null,
184
- location,
185
- tasks,
186
- files: asRecord(record["files"]) ?? {},
187
- costs: getCosts(record),
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
- .filter((result) => result.ok)
202
- .map((result) => transformJobStatus(result.data, result.jobId, result.location))
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: transformedJobs.length,
220
- failedTransforms: successfulReads - transformedJobs.length,
221
- transformationRate: successfulReads === 0 ? 0 : transformedJobs.length / successfulReads,
85
+ successfulTransforms,
86
+ failedTransforms: successfulReads - successfulTransforms,
87
+ transformationRate: successfulReads === 0 ? 0 : successfulTransforms / successfulReads,
222
88
  statusDistribution,
223
89
  };
224
90
  }
@@ -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 | "error";
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 interface CanonicalTask {
132
- state: TaskStateValue;
133
- name: string;
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
- error: CanonicalJob[];
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 {