@ryanfw/prompt-orchestration-pipeline 0.6.0 → 0.7.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/README.md +1 -2
- package/package.json +1 -2
- package/src/api/validators/json.js +39 -0
- package/src/components/DAGGrid.jsx +392 -303
- package/src/components/JobCard.jsx +13 -11
- package/src/components/JobDetail.jsx +41 -71
- package/src/components/JobTable.jsx +32 -22
- package/src/components/Layout.jsx +0 -21
- package/src/components/LiveText.jsx +47 -0
- package/src/components/TaskDetailSidebar.jsx +216 -0
- package/src/components/TimerText.jsx +82 -0
- package/src/components/ui/RestartJobModal.jsx +140 -0
- package/src/components/ui/toast.jsx +138 -0
- package/src/config/models.js +322 -0
- package/src/config/statuses.js +119 -0
- package/src/core/config.js +2 -164
- package/src/core/file-io.js +1 -1
- package/src/core/module-loader.js +54 -40
- package/src/core/pipeline-runner.js +52 -20
- package/src/core/status-writer.js +147 -3
- package/src/core/symlink-bridge.js +57 -0
- package/src/core/symlink-utils.js +94 -0
- package/src/core/task-runner.js +267 -443
- package/src/llm/index.js +167 -52
- package/src/pages/Code.jsx +57 -3
- package/src/pages/PipelineDetail.jsx +92 -22
- package/src/pages/PromptPipelineDashboard.jsx +15 -36
- package/src/providers/anthropic.js +83 -69
- package/src/providers/base.js +52 -0
- package/src/providers/deepseek.js +17 -34
- package/src/providers/gemini.js +226 -0
- package/src/providers/openai.js +36 -106
- package/src/providers/zhipu.js +136 -0
- package/src/ui/client/adapters/job-adapter.js +16 -26
- package/src/ui/client/api.js +134 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -178
- package/src/ui/client/index.css +9 -0
- package/src/ui/client/index.html +1 -0
- package/src/ui/client/main.jsx +18 -15
- package/src/ui/client/time-store.js +161 -0
- package/src/ui/config-bridge.js +15 -24
- package/src/ui/config-bridge.node.js +15 -24
- package/src/ui/dist/assets/{index-WgJUlSmE.js → index-DqkbzXZ1.js} +1408 -771
- package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
- package/src/ui/dist/index.html +3 -2
- package/src/ui/public/favicon.svg +12 -0
- package/src/ui/server.js +231 -33
- package/src/ui/transformers/status-transformer.js +18 -31
- package/src/utils/dag.js +8 -4
- package/src/utils/duration.js +13 -19
- package/src/utils/formatters.js +27 -0
- package/src/utils/geometry-equality.js +83 -0
- package/src/utils/pipelines.js +5 -1
- package/src/utils/time-utils.js +40 -0
- package/src/utils/token-cost-calculator.js +4 -7
- package/src/utils/ui.jsx +14 -16
- package/src/components/ui/select.jsx +0 -27
- package/src/lib/utils.js +0 -6
- package/src/ui/client/hooks/useTicker.js +0 -26
- package/src/ui/config-bridge.browser.js +0 -149
- package/src/ui/dist/assets/style-x0V-5m8e.css +0 -62
package/src/providers/openai.js
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
isRetryableError,
|
|
5
5
|
sleep,
|
|
6
6
|
tryParseJSON,
|
|
7
|
+
ensureJsonResponseFormat,
|
|
8
|
+
ProviderJsonParseError,
|
|
7
9
|
} from "./base.js";
|
|
8
10
|
|
|
9
11
|
let client = null;
|
|
@@ -31,9 +33,7 @@ export async function openaiChat({
|
|
|
31
33
|
temperature,
|
|
32
34
|
maxTokens,
|
|
33
35
|
max_tokens, // Explicitly destructure to prevent it from being in ...rest
|
|
34
|
-
responseFormat
|
|
35
|
-
tools,
|
|
36
|
-
toolChoice,
|
|
36
|
+
responseFormat,
|
|
37
37
|
seed,
|
|
38
38
|
stop,
|
|
39
39
|
topP,
|
|
@@ -46,6 +46,9 @@ export async function openaiChat({
|
|
|
46
46
|
console.log("[OpenAI] Model:", model);
|
|
47
47
|
console.log("[OpenAI] Response format:", responseFormat);
|
|
48
48
|
|
|
49
|
+
// Enforce JSON mode - reject calls without proper JSON responseFormat
|
|
50
|
+
ensureJsonResponseFormat(responseFormat, "OpenAI");
|
|
51
|
+
|
|
49
52
|
const openai = getClient();
|
|
50
53
|
if (!openai) throw new Error("OpenAI API key not configured");
|
|
51
54
|
|
|
@@ -106,22 +109,19 @@ export async function openaiChat({
|
|
|
106
109
|
total_tokens: promptTokens + completionTokens,
|
|
107
110
|
};
|
|
108
111
|
|
|
109
|
-
// Parse JSON
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
lastError = new Error("Failed to parse JSON response");
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
112
|
+
// Parse JSON - this is now required for all calls
|
|
113
|
+
const parsed = tryParseJSON(text);
|
|
114
|
+
if (!parsed) {
|
|
115
|
+
throw new ProviderJsonParseError(
|
|
116
|
+
"OpenAI",
|
|
117
|
+
model,
|
|
118
|
+
text.substring(0, 200),
|
|
119
|
+
"Failed to parse JSON response from Responses API"
|
|
120
|
+
);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
console.log("[OpenAI] Returning response from Responses API");
|
|
124
|
-
return { content: parsed
|
|
124
|
+
return { content: parsed, text, usage, raw: resp };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// ---------- CLASSIC CHAT COMPLETIONS path (non-GPT-5) ----------
|
|
@@ -136,8 +136,6 @@ export async function openaiChat({
|
|
|
136
136
|
presence_penalty: presencePenalty,
|
|
137
137
|
seed,
|
|
138
138
|
stop,
|
|
139
|
-
tools,
|
|
140
|
-
tool_choice: toolChoice,
|
|
141
139
|
stream: false,
|
|
142
140
|
};
|
|
143
141
|
|
|
@@ -158,31 +156,19 @@ export async function openaiChat({
|
|
|
158
156
|
classicText.length
|
|
159
157
|
);
|
|
160
158
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let classicParsed = null;
|
|
172
|
-
if (
|
|
173
|
-
responseFormat?.json_schema ||
|
|
174
|
-
responseFormat?.type === "json_object" ||
|
|
175
|
-
responseFormat === "json"
|
|
176
|
-
) {
|
|
177
|
-
classicParsed = tryParseJSON(classicText);
|
|
178
|
-
if (!classicParsed && attempt < maxRetries) {
|
|
179
|
-
lastError = new Error("Failed to parse JSON response");
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
159
|
+
// Parse JSON - this is now required for all calls
|
|
160
|
+
const classicParsed = tryParseJSON(classicText);
|
|
161
|
+
if (!classicParsed) {
|
|
162
|
+
throw new ProviderJsonParseError(
|
|
163
|
+
"OpenAI",
|
|
164
|
+
model,
|
|
165
|
+
classicText.substring(0, 200),
|
|
166
|
+
"Failed to parse JSON response from Classic API"
|
|
167
|
+
);
|
|
182
168
|
}
|
|
183
169
|
|
|
184
170
|
return {
|
|
185
|
-
content: classicParsed
|
|
171
|
+
content: classicParsed,
|
|
186
172
|
text: classicText,
|
|
187
173
|
usage: classicRes?.usage,
|
|
188
174
|
raw: classicRes,
|
|
@@ -211,8 +197,6 @@ export async function openaiChat({
|
|
|
211
197
|
presence_penalty: presencePenalty,
|
|
212
198
|
seed,
|
|
213
199
|
stop,
|
|
214
|
-
tools,
|
|
215
|
-
tool_choice: toolChoice,
|
|
216
200
|
stream: false,
|
|
217
201
|
};
|
|
218
202
|
|
|
@@ -227,17 +211,19 @@ export async function openaiChat({
|
|
|
227
211
|
const classicRes = await openai.chat.completions.create(classicReq);
|
|
228
212
|
const text = classicRes?.choices?.[0]?.message?.content ?? "";
|
|
229
213
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
214
|
+
// Parse JSON - this is now required for all calls
|
|
215
|
+
const parsed = tryParseJSON(text);
|
|
216
|
+
if (!parsed) {
|
|
217
|
+
throw new ProviderJsonParseError(
|
|
218
|
+
"OpenAI",
|
|
219
|
+
model,
|
|
220
|
+
text.substring(0, 200),
|
|
221
|
+
"Failed to parse JSON response from fallback Classic API"
|
|
222
|
+
);
|
|
237
223
|
}
|
|
238
224
|
|
|
239
225
|
return {
|
|
240
|
-
content: parsed
|
|
226
|
+
content: parsed,
|
|
241
227
|
text,
|
|
242
228
|
usage: classicRes?.usage,
|
|
243
229
|
raw: classicRes,
|
|
@@ -256,59 +242,3 @@ export async function openaiChat({
|
|
|
256
242
|
|
|
257
243
|
throw lastError || new Error(`Failed after ${maxRetries + 1} attempts`);
|
|
258
244
|
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Convenience helper used widely in the codebase.
|
|
262
|
-
* For tests, this hits the Responses API directly (even for non-GPT-5).
|
|
263
|
-
* Always attempts to coerce JSON on return (falls back to string).
|
|
264
|
-
*/
|
|
265
|
-
export async function queryChatGPT(system, prompt, options = {}) {
|
|
266
|
-
console.log("\n[OpenAI] Starting queryChatGPT call");
|
|
267
|
-
console.log("[OpenAI] Model:", options.model || "gpt-5-chat-latest");
|
|
268
|
-
|
|
269
|
-
const openai = getClient();
|
|
270
|
-
if (!openai) throw new Error("OpenAI API key not configured");
|
|
271
|
-
|
|
272
|
-
const { systemMsg, userMsg } = extractMessages([
|
|
273
|
-
{ role: "system", content: system },
|
|
274
|
-
{ role: "user", content: prompt },
|
|
275
|
-
]);
|
|
276
|
-
console.log("[OpenAI] System message length:", systemMsg.length);
|
|
277
|
-
console.log("[OpenAI] User message length:", userMsg.length);
|
|
278
|
-
|
|
279
|
-
const req = {
|
|
280
|
-
model: options.model || "gpt-5-chat-latest",
|
|
281
|
-
instructions: systemMsg,
|
|
282
|
-
input: userMsg,
|
|
283
|
-
max_output_tokens: options.maxTokens ?? 25000,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
// Note: Responses API does not support temperature, top_p, frequency_penalty,
|
|
287
|
-
// presence_penalty, seed, or stop parameters. These are only for Chat Completions API.
|
|
288
|
-
|
|
289
|
-
// Response format / schema mapping for Responses API
|
|
290
|
-
if (options.schema) {
|
|
291
|
-
req.text = {
|
|
292
|
-
format: {
|
|
293
|
-
type: "json_schema",
|
|
294
|
-
name: options.schemaName || "Response",
|
|
295
|
-
schema: options.schema,
|
|
296
|
-
},
|
|
297
|
-
};
|
|
298
|
-
} else if (
|
|
299
|
-
options.response_format?.type === "json_object" ||
|
|
300
|
-
options.response_format === "json"
|
|
301
|
-
) {
|
|
302
|
-
req.text = { format: { type: "json_object" } };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
console.log("[OpenAI] Calling responses.create...");
|
|
306
|
-
const resp = await openai.responses.create(req);
|
|
307
|
-
const text = resp.output_text ?? "";
|
|
308
|
-
console.log("[OpenAI] Response received, text length:", text.length);
|
|
309
|
-
|
|
310
|
-
// Always try to parse JSON; fall back to string
|
|
311
|
-
const parsed = tryParseJSON(text);
|
|
312
|
-
console.log("[OpenAI] Parsed result:", parsed ? "JSON" : "text");
|
|
313
|
-
return parsed ?? text;
|
|
314
|
-
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractMessages,
|
|
3
|
+
isRetryableError,
|
|
4
|
+
sleep,
|
|
5
|
+
tryParseJSON,
|
|
6
|
+
ensureJsonResponseFormat,
|
|
7
|
+
ProviderJsonParseError,
|
|
8
|
+
} from "./base.js";
|
|
9
|
+
|
|
10
|
+
export async function zhipuChat({
|
|
11
|
+
messages,
|
|
12
|
+
model = "glm-4-plus",
|
|
13
|
+
temperature = 0.7,
|
|
14
|
+
maxTokens = 8192,
|
|
15
|
+
responseFormat = "json",
|
|
16
|
+
topP,
|
|
17
|
+
stop,
|
|
18
|
+
maxRetries = 3,
|
|
19
|
+
}) {
|
|
20
|
+
console.log("\n[Zhipu] Starting zhipuChat call");
|
|
21
|
+
console.log("[Zhipu] Model:", model);
|
|
22
|
+
console.log("[Zhipu] Response format:", responseFormat);
|
|
23
|
+
|
|
24
|
+
// Enforce JSON mode - reject calls without proper JSON responseFormat
|
|
25
|
+
ensureJsonResponseFormat(responseFormat, "Zhipu");
|
|
26
|
+
|
|
27
|
+
if (!process.env.ZHIPU_API_KEY) {
|
|
28
|
+
throw new Error("Zhipu API key not configured");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { systemMsg, userMsg } = extractMessages(messages);
|
|
32
|
+
console.log("[Zhipu] System message length:", systemMsg.length);
|
|
33
|
+
console.log("[Zhipu] User message length:", userMsg.length);
|
|
34
|
+
|
|
35
|
+
// Build system guard for JSON enforcement
|
|
36
|
+
let system = systemMsg;
|
|
37
|
+
|
|
38
|
+
if (responseFormat === "json" || responseFormat?.type === "json_object") {
|
|
39
|
+
system = `${systemMsg}\n\nYou must output strict JSON only with no extra text.`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (responseFormat?.json_schema) {
|
|
43
|
+
system = `${systemMsg}\n\nYou must output strict JSON only matching this schema (no extra text):\n${JSON.stringify(responseFormat.json_schema)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let lastError;
|
|
47
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
48
|
+
if (attempt > 0) {
|
|
49
|
+
await sleep(Math.pow(2, attempt) * 1000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
console.log(`[Zhipu] Attempt ${attempt + 1}/${maxRetries + 1}`);
|
|
54
|
+
|
|
55
|
+
const requestBody = {
|
|
56
|
+
model,
|
|
57
|
+
messages: [
|
|
58
|
+
...(system ? [{ role: "system", content: system }] : []),
|
|
59
|
+
{ role: "user", content: userMsg },
|
|
60
|
+
],
|
|
61
|
+
temperature,
|
|
62
|
+
max_tokens: maxTokens,
|
|
63
|
+
...(topP !== undefined ? { top_p: topP } : {}),
|
|
64
|
+
...(stop !== undefined ? { stop: stop } : {}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
console.log("[Zhipu] Calling Zhipu API...");
|
|
68
|
+
const response = await fetch("https://api.z.ai/api/coding/paas/v4", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
Authorization: `Bearer ${process.env.ZHIPU_API_KEY}`,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(requestBody),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const error = await response
|
|
79
|
+
.json()
|
|
80
|
+
.catch(() => ({ error: response.statusText }));
|
|
81
|
+
throw { status: response.status, ...error };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
console.log("[Zhipu] Response received from Zhipu API");
|
|
86
|
+
|
|
87
|
+
// Extract text from response
|
|
88
|
+
const text = data?.choices?.[0]?.message?.content || "";
|
|
89
|
+
console.log("[Zhipu] Response text length:", text.length);
|
|
90
|
+
|
|
91
|
+
// Parse JSON - this is required for all calls
|
|
92
|
+
const parsed = tryParseJSON(text);
|
|
93
|
+
if (!parsed) {
|
|
94
|
+
throw new ProviderJsonParseError(
|
|
95
|
+
"Zhipu",
|
|
96
|
+
model,
|
|
97
|
+
text.substring(0, 200),
|
|
98
|
+
"Failed to parse JSON response from Zhipu API"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Normalize usage (if provided)
|
|
103
|
+
const prompt_tokens = data?.usage?.prompt_tokens;
|
|
104
|
+
const completion_tokens = data?.usage?.completion_tokens;
|
|
105
|
+
const total_tokens = (prompt_tokens ?? 0) + (completion_tokens ?? 0);
|
|
106
|
+
const usage =
|
|
107
|
+
prompt_tokens != null && completion_tokens != null
|
|
108
|
+
? { prompt_tokens, completion_tokens, total_tokens }
|
|
109
|
+
: undefined;
|
|
110
|
+
|
|
111
|
+
console.log("[Zhipu] Returning response from Zhipu API");
|
|
112
|
+
return {
|
|
113
|
+
content: parsed,
|
|
114
|
+
text,
|
|
115
|
+
...(usage ? { usage } : {}),
|
|
116
|
+
raw: data,
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
lastError = error;
|
|
120
|
+
const msg = error?.error?.message || error?.message || "";
|
|
121
|
+
console.error("[Zhipu] Error occurred:", msg);
|
|
122
|
+
console.error("[Zhipu] Error status:", error?.status);
|
|
123
|
+
|
|
124
|
+
if (error.status === 401) throw error;
|
|
125
|
+
|
|
126
|
+
if (isRetryableError(error) && attempt < maxRetries) {
|
|
127
|
+
console.log("[Zhipu] Retrying due to retryable error");
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (attempt === maxRetries) throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw lastError || new Error(`Failed after ${maxRetries + 1} attempts`);
|
|
136
|
+
}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { derivePipelineMetadata } from "../../../utils/pipelines.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
normalizeTaskState,
|
|
4
|
+
deriveJobStatusFromTasks,
|
|
5
|
+
} from "../../../config/statuses.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Normalize a raw task state into canonical enum.
|
|
7
9
|
* Returns { state, warning? } where warning is a string if normalization occurred.
|
|
8
10
|
*/
|
|
9
|
-
function
|
|
11
|
+
function normalizeTaskStateWithWarning(raw) {
|
|
10
12
|
if (!raw || typeof raw !== "string")
|
|
11
13
|
return { state: "pending", warning: "missing_state" };
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
const normalizedState = normalizeTaskState(raw);
|
|
16
|
+
|
|
17
|
+
if (raw !== normalizedState) {
|
|
18
|
+
return { state: normalizedState, warning: `unknown_state:${raw}` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { state: normalizedState };
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
/**
|
|
@@ -28,7 +35,7 @@ function normalizeTasks(rawTasks) {
|
|
|
28
35
|
// Object shape - canonical format
|
|
29
36
|
const tasks = {};
|
|
30
37
|
Object.entries(rawTasks).forEach(([name, t]) => {
|
|
31
|
-
const ns =
|
|
38
|
+
const ns = normalizeTaskStateWithWarning(t && t.state);
|
|
32
39
|
if (ns.warning) warnings.push(`${name}:${ns.warning}`);
|
|
33
40
|
const taskObj = {
|
|
34
41
|
name,
|
|
@@ -79,7 +86,7 @@ function normalizeTasks(rawTasks) {
|
|
|
79
86
|
const tasks = {};
|
|
80
87
|
rawTasks.forEach((t, idx) => {
|
|
81
88
|
const name = t && t.name ? String(t.name) : `task-${idx}`;
|
|
82
|
-
const ns =
|
|
89
|
+
const ns = normalizeTaskStateWithWarning(t && t.state);
|
|
83
90
|
if (ns.warning) warnings.push(`${name}:${ns.warning}`);
|
|
84
91
|
tasks[name] = {
|
|
85
92
|
name,
|
|
@@ -112,23 +119,6 @@ function normalizeTasks(rawTasks) {
|
|
|
112
119
|
return { tasks: {}, warnings: ["invalid_tasks_shape"] };
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
/**
|
|
116
|
-
* Derive status from tasks when status is missing/invalid.
|
|
117
|
-
* Rules:
|
|
118
|
-
* - failed if any task state === 'failed'
|
|
119
|
-
* - running if >=1 running and none failed
|
|
120
|
-
* - complete if all done
|
|
121
|
-
* - pending otherwise
|
|
122
|
-
*/
|
|
123
|
-
function deriveStatusFromTasks(tasks) {
|
|
124
|
-
const taskList = Object.values(tasks);
|
|
125
|
-
if (!Array.isArray(taskList) || taskList.length === 0) return "pending";
|
|
126
|
-
if (taskList.some((t) => t.state === "failed")) return "failed";
|
|
127
|
-
if (taskList.some((t) => t.state === "running")) return "running";
|
|
128
|
-
if (taskList.every((t) => t.state === "done")) return "complete";
|
|
129
|
-
return "pending";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
122
|
/**
|
|
133
123
|
* Clamp number to 0..100 and ensure integer.
|
|
134
124
|
*/
|
|
@@ -147,7 +137,7 @@ function computeJobSummaryStats(tasks) {
|
|
|
147
137
|
(acc, t) => acc + (t.state === "done" ? 1 : 0),
|
|
148
138
|
0
|
|
149
139
|
);
|
|
150
|
-
const status =
|
|
140
|
+
const status = deriveJobStatusFromTasks(Object.values(tasks));
|
|
151
141
|
const progress =
|
|
152
142
|
taskCount > 0 ? Math.round((doneCount / taskCount) * 100) : 0;
|
|
153
143
|
return { status, progress, doneCount, taskCount };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side API helpers for making HTTP requests to the backend
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Restart a job with clean-slate mode
|
|
7
|
+
*
|
|
8
|
+
* @param {string} jobId - The ID of the job to restart
|
|
9
|
+
* @param {Object} opts - Options object
|
|
10
|
+
* @param {Object} opts.options - Additional options for the restart
|
|
11
|
+
* @param {boolean} opts.options.clearTokenUsage - Whether to clear token usage (default: true)
|
|
12
|
+
* @returns {Promise<Object>} Parsed JSON response from the server
|
|
13
|
+
* @throws {Object} Structured error object with { code, message } for non-2xx responses
|
|
14
|
+
*/
|
|
15
|
+
export async function restartJob(jobId, opts = {}) {
|
|
16
|
+
const options = {
|
|
17
|
+
clearTokenUsage: true,
|
|
18
|
+
...opts.options,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const requestBody = opts.fromTask
|
|
22
|
+
? { fromTask: opts.fromTask, options }
|
|
23
|
+
: { mode: "clean-slate", options };
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(
|
|
27
|
+
`/api/jobs/${encodeURIComponent(jobId)}/restart`,
|
|
28
|
+
{
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(requestBody),
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
// Try to parse error response, fall back to status text if parsing fails
|
|
39
|
+
let errorData;
|
|
40
|
+
try {
|
|
41
|
+
errorData = await response.json();
|
|
42
|
+
} catch {
|
|
43
|
+
errorData = { message: response.statusText };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Throw structured error with code and message
|
|
47
|
+
throw {
|
|
48
|
+
code: errorData.code || getErrorCodeFromStatus(response.status),
|
|
49
|
+
message: getRestartErrorMessage(errorData, response.status),
|
|
50
|
+
status: response.status,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Return parsed JSON for successful responses
|
|
55
|
+
return await response.json();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Re-throw structured errors as-is
|
|
58
|
+
if (error.code && error.message) {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle network errors or other unexpected errors
|
|
63
|
+
throw {
|
|
64
|
+
code: "network_error",
|
|
65
|
+
message: error.message || "Failed to connect to server",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Map HTTP status codes to error codes for structured error handling
|
|
72
|
+
*/
|
|
73
|
+
function getErrorCodeFromStatus(status) {
|
|
74
|
+
switch (status) {
|
|
75
|
+
case 404:
|
|
76
|
+
return "job_not_found";
|
|
77
|
+
case 409:
|
|
78
|
+
return "conflict";
|
|
79
|
+
case 500:
|
|
80
|
+
return "spawn_failed";
|
|
81
|
+
default:
|
|
82
|
+
return "unknown_error";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Map HTTP status codes to error messages for structured error handling
|
|
88
|
+
*/
|
|
89
|
+
function getErrorMessageFromStatus(status) {
|
|
90
|
+
switch (status) {
|
|
91
|
+
case 404:
|
|
92
|
+
return "Job not found";
|
|
93
|
+
case 409:
|
|
94
|
+
return "Job restart conflict";
|
|
95
|
+
case 500:
|
|
96
|
+
return "Failed to start restart";
|
|
97
|
+
default:
|
|
98
|
+
return `Request failed with status ${status}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get specific error message from error response for restart functionality
|
|
104
|
+
*/
|
|
105
|
+
function getRestartErrorMessage(errorData, status) {
|
|
106
|
+
// Handle specific 409 conflict errors
|
|
107
|
+
if (status === 409) {
|
|
108
|
+
if (errorData.code === "job_running") {
|
|
109
|
+
return "Job is currently running; restart is unavailable.";
|
|
110
|
+
}
|
|
111
|
+
if (errorData.code === "unsupported_lifecycle") {
|
|
112
|
+
return "Job must be in current to restart.";
|
|
113
|
+
}
|
|
114
|
+
if (errorData.message?.includes("job_running")) {
|
|
115
|
+
return "Job is currently running; restart is unavailable.";
|
|
116
|
+
}
|
|
117
|
+
if (errorData.message?.includes("unsupported_lifecycle")) {
|
|
118
|
+
return "Job must be in current to restart.";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle 404 errors
|
|
123
|
+
if (status === 404) {
|
|
124
|
+
return "Job not found.";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle 500 errors
|
|
128
|
+
if (status === 500) {
|
|
129
|
+
return "Failed to start restart. Try again.";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fall back to provided message or default
|
|
133
|
+
return errorData.message || "Failed to restart job.";
|
|
134
|
+
}
|