@ryanfw/prompt-orchestration-pipeline 0.6.0 → 0.8.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 (62) 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 -26
  20. package/src/core/status-writer.js +147 -3
  21. package/src/core/symlink-bridge.js +55 -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 -38
  48. package/src/ui/transformers/status-transformer.js +18 -31
  49. package/src/ui/watcher.js +5 -1
  50. package/src/utils/dag.js +8 -4
  51. package/src/utils/duration.js +13 -19
  52. package/src/utils/formatters.js +27 -0
  53. package/src/utils/geometry-equality.js +83 -0
  54. package/src/utils/pipelines.js +5 -1
  55. package/src/utils/time-utils.js +40 -0
  56. package/src/utils/token-cost-calculator.js +4 -7
  57. package/src/utils/ui.jsx +14 -16
  58. package/src/components/ui/select.jsx +0 -27
  59. package/src/lib/utils.js +0 -6
  60. package/src/ui/client/hooks/useTicker.js +0 -26
  61. package/src/ui/config-bridge.browser.js +0 -149
  62. package/src/ui/dist/assets/style-x0V-5m8e.css +0 -62
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { TaskState } from "../config/statuses.js";
3
4
 
4
5
  // Lazy import SSE registry to avoid circular dependencies
5
6
  let sseRegistry = null;
@@ -57,7 +58,7 @@ const createStatusWriterLogger = (jobId) => {
57
58
  function createDefaultStatus(jobId) {
58
59
  return {
59
60
  id: jobId,
60
- state: "pending",
61
+ state: TaskState.PENDING,
61
62
  current: null,
62
63
  currentStage: null,
63
64
  lastUpdated: new Date().toISOString(),
@@ -131,7 +132,7 @@ function validateStatusSnapshot(snapshot) {
131
132
 
132
133
  // Ensure required root fields exist
133
134
  if (typeof snapshot.state !== "string") {
134
- snapshot.state = "pending";
135
+ snapshot.state = TaskState.PENDING;
135
136
  }
136
137
  if (snapshot.current !== null && typeof snapshot.current !== "string") {
137
138
  snapshot.current = null;
@@ -214,7 +215,13 @@ export async function writeJobStatus(jobDir, updateFn) {
214
215
  const validated = validateStatusSnapshot(current);
215
216
 
216
217
  // Apply user updates
217
- const maybeUpdated = updateFn(validated);
218
+ let maybeUpdated;
219
+ try {
220
+ maybeUpdated = updateFn(validated);
221
+ } catch (error) {
222
+ console.error(`[${jobId}] Error executing update function:`, error);
223
+ throw new Error(`Update function failed: ${error.message}`);
224
+ }
218
225
  const snapshot = validateStatusSnapshot(
219
226
  maybeUpdated === undefined ? validated : maybeUpdated
220
227
  );
@@ -340,3 +347,140 @@ export async function updateTaskStatus(jobDir, taskId, taskUpdateFn) {
340
347
  return snapshot;
341
348
  });
342
349
  }
350
+
351
+ /**
352
+ * Reset a job from a specific task onward, preserving prior completed tasks
353
+ *
354
+ * @param {string} jobDir - Job directory path containing tasks-status.json
355
+ * @param {string} fromTask - Task identifier to restart from (inclusive)
356
+ * @param {Object} options - Reset options
357
+ * @param {boolean} [options.clearTokenUsage=true] - Whether to clear token usage arrays
358
+ * @returns {Promise<Object>} The updated status snapshot
359
+ */
360
+ export async function resetJobFromTask(
361
+ jobDir,
362
+ fromTask,
363
+ { clearTokenUsage = true } = {}
364
+ ) {
365
+ if (!jobDir || typeof jobDir !== "string") {
366
+ throw new Error("jobDir must be a non-empty string");
367
+ }
368
+
369
+ if (!fromTask || typeof fromTask !== "string") {
370
+ throw new Error("fromTask must be a non-empty string");
371
+ }
372
+
373
+ return writeJobStatus(jobDir, (snapshot) => {
374
+ // Reset root-level status
375
+ snapshot.state = TaskState.PENDING;
376
+ snapshot.current = null;
377
+ snapshot.currentStage = null;
378
+ snapshot.progress = 0;
379
+ snapshot.lastUpdated = new Date().toISOString();
380
+
381
+ // Ensure tasks object exists
382
+ if (!snapshot.tasks || typeof snapshot.tasks !== "object") {
383
+ snapshot.tasks = {};
384
+ }
385
+
386
+ // Compute progress based on preserved (done) tasks before fromTask
387
+ let doneCount = 0;
388
+ const taskKeys = Object.keys(snapshot.tasks);
389
+ for (const taskId of taskKeys) {
390
+ if (snapshot.tasks[taskId]?.state === TaskState.DONE) {
391
+ doneCount++;
392
+ }
393
+ }
394
+ snapshot.progress =
395
+ taskKeys.length > 0 ? (doneCount / taskKeys.length) * 100 : 0;
396
+
397
+ // Reset tasks from fromTask onward to pending; keep earlier tasks as-is
398
+ for (const taskId of taskKeys) {
399
+ const task = snapshot.tasks[taskId];
400
+ if (!task) continue; // ensure task object exists
401
+
402
+ const shouldReset =
403
+ taskKeys.indexOf(taskId) >= taskKeys.indexOf(fromTask);
404
+ if (shouldReset) {
405
+ // Reset task state and metadata
406
+ task.state = TaskState.PENDING;
407
+ task.currentStage = null;
408
+
409
+ // Remove error-related fields
410
+ delete task.failedStage;
411
+ delete task.error;
412
+
413
+ // Reset counters
414
+ task.attempts = 0;
415
+ task.refinementAttempts = 0;
416
+
417
+ // Clear token usage if requested
418
+ if (clearTokenUsage) {
419
+ task.tokenUsage = [];
420
+ }
421
+ }
422
+ // If task appears before fromTask and is not done, keep its state untouched
423
+ // This preserves upstream work if user restarts from a mid-pipeline task
424
+ }
425
+
426
+ // Preserve files.* arrays - do not modify them
427
+ // This ensures generated files are preserved during restart
428
+
429
+ return snapshot;
430
+ });
431
+ }
432
+
433
+ /**
434
+ * Reset a job and all its tasks to clean-slate state atomically
435
+ *
436
+ * @param {string} jobDir - Job directory path containing tasks-status.json
437
+ * @param {Object} options - Reset options
438
+ * @param {boolean} [options.clearTokenUsage=true] - Whether to clear token usage arrays
439
+ * @returns {Promise<Object>} The updated status snapshot
440
+ */
441
+ export async function resetJobToCleanSlate(
442
+ jobDir,
443
+ { clearTokenUsage = true } = {}
444
+ ) {
445
+ if (!jobDir || typeof jobDir !== "string") {
446
+ throw new Error("jobDir must be a non-empty string");
447
+ }
448
+
449
+ return writeJobStatus(jobDir, (snapshot) => {
450
+ // Reset root-level status
451
+ snapshot.state = TaskState.PENDING;
452
+ snapshot.current = null;
453
+ snapshot.currentStage = null;
454
+ snapshot.progress = 0;
455
+ snapshot.lastUpdated = new Date().toISOString();
456
+
457
+ // Reset all tasks
458
+ if (snapshot.tasks && typeof snapshot.tasks === "object") {
459
+ for (const taskId of Object.keys(snapshot.tasks)) {
460
+ const task = snapshot.tasks[taskId];
461
+
462
+ // Reset task state
463
+ task.state = TaskState.PENDING;
464
+ task.currentStage = null;
465
+
466
+ // Remove error-related fields
467
+ delete task.failedStage;
468
+ delete task.error;
469
+
470
+ // Reset counters
471
+ task.attempts = 0;
472
+ task.refinementAttempts = 0;
473
+
474
+ // Clear token usage if requested
475
+ if (clearTokenUsage) {
476
+ task.tokenUsage = [];
477
+ }
478
+ }
479
+ }
480
+
481
+ // Preserve files.* arrays - do not modify them
482
+ // This ensures generated files are preserved during restart
483
+
484
+ return snapshot;
485
+ });
486
+ }
@@ -0,0 +1,55 @@
1
+ import path from "node:path";
2
+ import { ensureSymlink } from "./symlink-utils.js";
3
+
4
+ /**
5
+ * Creates a taskDir symlink bridge to ensure deterministic module resolution.
6
+ *
7
+ * This function creates two symlinks in the task directory:
8
+ * - taskDir/node_modules -> adjacent to PO_ROOT/node_modules (for bare package specifiers)
9
+ * - taskDir/_task_root -> dirname(taskModulePath) (for relative imports)
10
+ *
11
+ * @param {Object} options - Configuration options
12
+ * @param {string} options.taskDir - The task directory where symlinks should be created
13
+ * @param {string} options.poRoot - The repository root directory
14
+ * @param {string} options.taskModulePath - Absolute path to the original task module
15
+ * @returns {string} The relocated entry path for the task module
16
+ * @throws {Error} If symlink creation fails
17
+ */
18
+ export async function ensureTaskSymlinkBridge({
19
+ taskDir,
20
+ poRoot,
21
+ taskModulePath,
22
+ }) {
23
+ // Normalize all paths to absolute paths
24
+ const normalizedTaskDir = path.resolve(taskDir);
25
+ const normalizedPoRoot = path.resolve(poRoot);
26
+ const normalizedTaskModulePath = path.resolve(taskModulePath);
27
+
28
+ // Ensure the task directory exists
29
+ await import("node:fs/promises").then((fs) =>
30
+ fs.mkdir(normalizedTaskDir, { recursive: true })
31
+ );
32
+
33
+ // Create symlink for node_modules -> adjacent to PO_ROOT
34
+ const nodeModulesLink = path.join(normalizedTaskDir, "node_modules");
35
+ const nodeModulesTarget = path.join(
36
+ path.resolve(normalizedPoRoot, ".."),
37
+ "node_modules"
38
+ );
39
+ await ensureSymlink(nodeModulesLink, nodeModulesTarget, "dir");
40
+
41
+ // Create symlink for _task_root -> dirname(taskModulePath)
42
+ const taskRootLink = path.join(normalizedTaskDir, "_task_root");
43
+ const taskRootTarget = path.dirname(normalizedTaskModulePath);
44
+ await ensureSymlink(taskRootLink, taskRootTarget, "dir");
45
+
46
+ // Return the relocated entry path
47
+ const taskModuleBasename = path.basename(normalizedTaskModulePath);
48
+ const relocatedEntry = path.join(
49
+ normalizedTaskDir,
50
+ "_task_root",
51
+ taskModuleBasename
52
+ );
53
+
54
+ return relocatedEntry;
55
+ }
@@ -0,0 +1,94 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Creates an idempotent symlink, safely handling existing files/symlinks.
6
+ *
7
+ * @param {string} linkPath - Path where the symlink should be created
8
+ * @param {string} targetPath - Path that the symlink should point to
9
+ * @param {'file' | 'dir'} type - Type of symlink to create
10
+ * @throws {Error} If symlink creation fails on non-POSIX systems
11
+ */
12
+ export async function ensureSymlink(linkPath, targetPath, type) {
13
+ try {
14
+ // Check if linkPath already exists
15
+ const stats = await fs.lstat(linkPath).catch(() => null);
16
+
17
+ if (stats) {
18
+ if (stats.isSymbolicLink()) {
19
+ // If it's already a symlink pointing to the correct target, we're done
20
+ const existingTarget = await fs.readlink(linkPath);
21
+ if (existingTarget === targetPath) {
22
+ return;
23
+ }
24
+ // If it points to a different target, remove it
25
+ await fs.unlink(linkPath);
26
+ } else if (stats.isDirectory()) {
27
+ // If it's a directory, remove it recursively
28
+ await fs.rmdir(linkPath, { recursive: true });
29
+ } else {
30
+ // If it's a file, remove it
31
+ await fs.unlink(linkPath);
32
+ }
33
+ }
34
+
35
+ // Ensure parent directory exists
36
+ const parentDir = path.dirname(linkPath);
37
+ await fs.mkdir(parentDir, { recursive: true });
38
+
39
+ // Create the symlink
40
+ await fs.symlink(targetPath, linkPath, type);
41
+ } catch (error) {
42
+ // Re-throw with more context
43
+ throw new Error(
44
+ `Failed to create symlink from ${linkPath} -> ${targetPath}: ${error.message}`
45
+ );
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Removes task symlinks from a completed job directory to avoid dangling links.
51
+ *
52
+ * @param {string} completedJobDir - Path to the completed job directory (e.g., COMPLETE_DIR/jobId)
53
+ */
54
+ export async function cleanupTaskSymlinks(completedJobDir) {
55
+ const tasksDir = path.join(completedJobDir, "tasks");
56
+
57
+ try {
58
+ // Check if tasks directory exists
59
+ const tasksStats = await fs.lstat(tasksDir).catch(() => null);
60
+ if (!tasksStats || !tasksStats.isDirectory()) {
61
+ return; // No tasks directory to clean up
62
+ }
63
+
64
+ // Get all task directories
65
+ const taskDirs = await fs.readdir(tasksDir, { withFileTypes: true });
66
+
67
+ for (const taskDir of taskDirs) {
68
+ if (!taskDir.isDirectory()) continue;
69
+
70
+ const taskPath = path.join(tasksDir, taskDir.name);
71
+
72
+ // Remove specific symlinks if they exist and are actually symlinks
73
+ const symlinksToRemove = ["node_modules", "project", "_task_root"];
74
+
75
+ for (const linkName of symlinksToRemove) {
76
+ const linkPath = path.join(taskPath, linkName);
77
+
78
+ try {
79
+ const stats = await fs.lstat(linkPath);
80
+ if (stats.isSymbolicLink()) {
81
+ await fs.unlink(linkPath);
82
+ }
83
+ } catch {
84
+ // Ignore errors (file doesn't exist, permissions, etc.)
85
+ }
86
+ }
87
+ }
88
+ } catch (error) {
89
+ // Log but don't fail - cleanup is optional
90
+ console.warn(
91
+ `Warning: Failed to cleanup task symlinks in ${completedJobDir}: ${error.message}`
92
+ );
93
+ }
94
+ }