@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.
Files changed (61) hide show
  1. package/README.md +1 -2
  2. package/package.json +1 -2
  3. package/src/api/validators/json.js +39 -0
  4. package/src/components/DAGGrid.jsx +392 -303
  5. package/src/components/JobCard.jsx +13 -11
  6. package/src/components/JobDetail.jsx +41 -71
  7. package/src/components/JobTable.jsx +32 -22
  8. package/src/components/Layout.jsx +0 -21
  9. package/src/components/LiveText.jsx +47 -0
  10. package/src/components/TaskDetailSidebar.jsx +216 -0
  11. package/src/components/TimerText.jsx +82 -0
  12. package/src/components/ui/RestartJobModal.jsx +140 -0
  13. package/src/components/ui/toast.jsx +138 -0
  14. package/src/config/models.js +322 -0
  15. package/src/config/statuses.js +119 -0
  16. package/src/core/config.js +2 -164
  17. package/src/core/file-io.js +1 -1
  18. package/src/core/module-loader.js +54 -40
  19. package/src/core/pipeline-runner.js +52 -20
  20. package/src/core/status-writer.js +147 -3
  21. package/src/core/symlink-bridge.js +57 -0
  22. package/src/core/symlink-utils.js +94 -0
  23. package/src/core/task-runner.js +267 -443
  24. package/src/llm/index.js +167 -52
  25. package/src/pages/Code.jsx +57 -3
  26. package/src/pages/PipelineDetail.jsx +92 -22
  27. package/src/pages/PromptPipelineDashboard.jsx +15 -36
  28. package/src/providers/anthropic.js +83 -69
  29. package/src/providers/base.js +52 -0
  30. package/src/providers/deepseek.js +17 -34
  31. package/src/providers/gemini.js +226 -0
  32. package/src/providers/openai.js +36 -106
  33. package/src/providers/zhipu.js +136 -0
  34. package/src/ui/client/adapters/job-adapter.js +16 -26
  35. package/src/ui/client/api.js +134 -0
  36. package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -178
  37. package/src/ui/client/index.css +9 -0
  38. package/src/ui/client/index.html +1 -0
  39. package/src/ui/client/main.jsx +18 -15
  40. package/src/ui/client/time-store.js +161 -0
  41. package/src/ui/config-bridge.js +15 -24
  42. package/src/ui/config-bridge.node.js +15 -24
  43. package/src/ui/dist/assets/{index-WgJUlSmE.js → index-DqkbzXZ1.js} +1408 -771
  44. package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
  45. package/src/ui/dist/index.html +3 -2
  46. package/src/ui/public/favicon.svg +12 -0
  47. package/src/ui/server.js +231 -33
  48. package/src/ui/transformers/status-transformer.js +18 -31
  49. package/src/utils/dag.js +8 -4
  50. package/src/utils/duration.js +13 -19
  51. package/src/utils/formatters.js +27 -0
  52. package/src/utils/geometry-equality.js +83 -0
  53. package/src/utils/pipelines.js +5 -1
  54. package/src/utils/time-utils.js +40 -0
  55. package/src/utils/token-cost-calculator.js +4 -7
  56. package/src/utils/ui.jsx +14 -16
  57. package/src/components/ui/select.jsx +0 -27
  58. package/src/lib/utils.js +0 -6
  59. package/src/ui/client/hooks/useTicker.js +0 -26
  60. package/src/ui/config-bridge.browser.js +0 -149
  61. package/src/ui/dist/assets/style-x0V-5m8e.css +0 -62
@@ -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 = "json", // Default to JSON for legacy compatibility
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 if requested
110
- let parsed = null;
111
- if (
112
- responseFormat?.json_schema ||
113
- responseFormat?.type === "json_object" ||
114
- responseFormat === "json"
115
- ) {
116
- parsed = tryParseJSON(text);
117
- if (!parsed && attempt < maxRetries) {
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 ?? text, text, usage, raw: resp };
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
- // If tool calls present, return them (test expects this)
162
- if (classicRes?.choices?.[0]?.message?.tool_calls) {
163
- return {
164
- content: classicText,
165
- usage: classicRes?.usage,
166
- toolCalls: classicRes.choices[0].message.tool_calls,
167
- raw: classicRes,
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 ?? classicText,
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
- let parsed = null;
231
- if (
232
- responseFormat?.json_schema ||
233
- responseFormat?.type === "json_object" ||
234
- responseFormat === "json"
235
- ) {
236
- parsed = tryParseJSON(text);
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 ?? text,
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
- const ALLOWED_STATES = new Set(["pending", "running", "done", "failed"]);
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 normalizeTaskState(raw) {
11
+ function normalizeTaskStateWithWarning(raw) {
10
12
  if (!raw || typeof raw !== "string")
11
13
  return { state: "pending", warning: "missing_state" };
12
- const s = raw.toLowerCase();
13
- if (ALLOWED_STATES.has(s)) return { state: s };
14
- return { state: "pending", warning: `unknown_state:${raw}` };
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 = normalizeTaskState(t && t.state);
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 = normalizeTaskState(t && t.state);
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 = deriveStatusFromTasks(tasks);
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
+ }