@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
|
@@ -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:
|
|
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 =
|
|
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
|
-
|
|
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,57 @@
|
|
|
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 three symlinks in the task directory:
|
|
8
|
+
* - taskDir/node_modules -> {poRoot}/node_modules (for bare package specifiers)
|
|
9
|
+
* - taskDir/project -> {poRoot} (optional convenience for absolute project paths)
|
|
10
|
+
* - taskDir/_task_root -> dirname(taskModulePath) (for relative imports)
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} options - Configuration options
|
|
13
|
+
* @param {string} options.taskDir - The task directory where symlinks should be created
|
|
14
|
+
* @param {string} options.poRoot - The repository root directory
|
|
15
|
+
* @param {string} options.taskModulePath - Absolute path to the original task module
|
|
16
|
+
* @returns {string} The relocated entry path for the task module
|
|
17
|
+
* @throws {Error} If symlink creation fails
|
|
18
|
+
*/
|
|
19
|
+
export async function ensureTaskSymlinkBridge({
|
|
20
|
+
taskDir,
|
|
21
|
+
poRoot,
|
|
22
|
+
taskModulePath,
|
|
23
|
+
}) {
|
|
24
|
+
// Normalize all paths to absolute paths
|
|
25
|
+
const normalizedTaskDir = path.resolve(taskDir);
|
|
26
|
+
const normalizedPoRoot = path.resolve(poRoot);
|
|
27
|
+
const normalizedTaskModulePath = path.resolve(taskModulePath);
|
|
28
|
+
|
|
29
|
+
// Ensure the task directory exists
|
|
30
|
+
await import("node:fs/promises").then((fs) =>
|
|
31
|
+
fs.mkdir(normalizedTaskDir, { recursive: true })
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Create symlink for node_modules -> {poRoot}/node_modules
|
|
35
|
+
const nodeModulesLink = path.join(normalizedTaskDir, "node_modules");
|
|
36
|
+
const nodeModulesTarget = path.join(normalizedPoRoot, "node_modules");
|
|
37
|
+
await ensureSymlink(nodeModulesLink, nodeModulesTarget, "dir");
|
|
38
|
+
|
|
39
|
+
// Create symlink for project -> {poRoot}
|
|
40
|
+
const projectLink = path.join(normalizedTaskDir, "project");
|
|
41
|
+
await ensureSymlink(projectLink, normalizedPoRoot, "dir");
|
|
42
|
+
|
|
43
|
+
// Create symlink for _task_root -> dirname(taskModulePath)
|
|
44
|
+
const taskRootLink = path.join(normalizedTaskDir, "_task_root");
|
|
45
|
+
const taskRootTarget = path.dirname(normalizedTaskModulePath);
|
|
46
|
+
await ensureSymlink(taskRootLink, taskRootTarget, "dir");
|
|
47
|
+
|
|
48
|
+
// Return the relocated entry path
|
|
49
|
+
const taskModuleBasename = path.basename(normalizedTaskModulePath);
|
|
50
|
+
const relocatedEntry = path.join(
|
|
51
|
+
normalizedTaskDir,
|
|
52
|
+
"_task_root",
|
|
53
|
+
taskModuleBasename
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return relocatedEntry;
|
|
57
|
+
}
|
|
@@ -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
|
+
}
|