@ryanfw/prompt-orchestration-pipeline 0.5.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 +14 -12
- package/src/components/JobDetail.jsx +54 -51
- package/src/components/JobTable.jsx +72 -23
- package/src/components/Layout.jsx +145 -42
- package/src/components/LiveText.jsx +47 -0
- package/src/components/PageSubheader.jsx +75 -0
- package/src/components/TaskDetailSidebar.jsx +216 -0
- package/src/components/TimerText.jsx +82 -0
- package/src/components/UploadSeed.jsx +0 -70
- package/src/components/ui/Logo.jsx +16 -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 +4 -34
- package/src/core/file-io.js +13 -28
- package/src/core/module-loader.js +54 -40
- package/src/core/pipeline-runner.js +65 -26
- package/src/core/status-writer.js +213 -58
- package/src/core/symlink-bridge.js +57 -0
- package/src/core/symlink-utils.js +94 -0
- package/src/core/task-runner.js +321 -437
- package/src/llm/index.js +258 -86
- package/src/pages/Code.jsx +351 -0
- package/src/pages/PipelineDetail.jsx +124 -15
- package/src/pages/PromptPipelineDashboard.jsx +20 -88
- package/src/providers/anthropic.js +83 -69
- package/src/providers/base.js +52 -0
- package/src/providers/deepseek.js +20 -21
- 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 +42 -28
- package/src/ui/client/api.js +134 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -179
- package/src/ui/client/index.css +15 -0
- package/src/ui/client/index.html +2 -1
- package/src/ui/client/main.jsx +19 -14
- 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-CxcrauYR.js → index-DqkbzXZ1.js} +2132 -1086
- package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
- package/src/ui/dist/index.html +4 -3
- package/src/ui/job-reader.js +0 -108
- package/src/ui/public/favicon.svg +12 -0
- package/src/ui/server.js +252 -0
- package/src/ui/sse-enhancer.js +0 -1
- package/src/ui/transformers/list-transformer.js +32 -12
- package/src/ui/transformers/status-transformer.js +29 -42
- 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 +294 -0
- package/src/utils/ui.jsx +18 -20
- 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-D6K_oQ12.css +0 -62
package/src/core/task-runner.js
CHANGED
|
@@ -4,10 +4,29 @@ import fs from "fs";
|
|
|
4
4
|
import { createLLM, getLLMEvents } from "../llm/index.js";
|
|
5
5
|
import { loadFreshModule } from "./module-loader.js";
|
|
6
6
|
import { loadEnvironment } from "./environment.js";
|
|
7
|
-
import { getConfig } from "./config.js";
|
|
8
7
|
import { createTaskFileIO } from "./file-io.js";
|
|
9
8
|
import { writeJobStatus } from "./status-writer.js";
|
|
10
9
|
import { computeDeterministicProgress } from "./progress.js";
|
|
10
|
+
import { TaskState } from "../config/statuses.js";
|
|
11
|
+
import { validateWithSchema } from "../api/validators/json.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Derives model key and token counts from LLM metric event.
|
|
15
|
+
* Returns a tuple: [modelKey, inputTokens, outputTokens].
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} metric - The LLM metric event from llm:request:complete
|
|
18
|
+
* @returns {Array<string, number, number>} [modelKey, inputTokens, outputTokens]
|
|
19
|
+
*/
|
|
20
|
+
export function deriveModelKeyAndTokens(metric) {
|
|
21
|
+
const provider = metric?.provider || "undefined";
|
|
22
|
+
const model = metric?.model || "undefined";
|
|
23
|
+
const modelKey = metric?.metadata?.alias || `${provider}:${model}`;
|
|
24
|
+
const input = Number.isFinite(metric?.promptTokens) ? metric.promptTokens : 0;
|
|
25
|
+
const output = Number.isFinite(metric?.completionTokens)
|
|
26
|
+
? metric.completionTokens
|
|
27
|
+
: 0;
|
|
28
|
+
return [modelKey, input, output];
|
|
29
|
+
}
|
|
11
30
|
|
|
12
31
|
/**
|
|
13
32
|
* Validates that a value is a plain object (not array, null, or class instance).
|
|
@@ -138,24 +157,6 @@ function ensureLogDirectory(workDir, jobId) {
|
|
|
138
157
|
return logsPath;
|
|
139
158
|
}
|
|
140
159
|
|
|
141
|
-
/**
|
|
142
|
-
* Writes a compact pre-execution snapshot for debugging stage inputs.
|
|
143
|
-
* Safe: does not throw on write failure; logs warnings instead.
|
|
144
|
-
* @param {string} stageName - Name of the stage
|
|
145
|
-
* @param {object} snapshot - Summary data to persist
|
|
146
|
-
* @param {string} logsDir - Directory to write the snapshot into
|
|
147
|
-
*/
|
|
148
|
-
function writePreExecutionSnapshot(stageName, snapshot, logsDir) {
|
|
149
|
-
const snapshotPath = path.join(logsDir, `stage-${stageName}-context.json`);
|
|
150
|
-
try {
|
|
151
|
-
fs.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.warn(
|
|
154
|
-
`[task-runner] Failed to write pre-execution snapshot for ${stageName}: ${error.message}`
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
160
|
/**
|
|
160
161
|
* Redirects console output to a log file for a stage.
|
|
161
162
|
* @param {string} logPath - The path to the log file
|
|
@@ -253,25 +254,10 @@ function persistStatusSnapshot(statusPath, updates) {
|
|
|
253
254
|
* Defines required flags (prerequisites) and produced flags (outputs) with their types.
|
|
254
255
|
*/
|
|
255
256
|
const FLAG_SCHEMAS = {
|
|
256
|
-
|
|
257
|
+
validateQuality: {
|
|
257
258
|
requires: {},
|
|
258
259
|
produces: {
|
|
259
|
-
|
|
260
|
-
lastValidationError: ["string", "object", "undefined"],
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
critique: {
|
|
264
|
-
requires: {},
|
|
265
|
-
produces: {
|
|
266
|
-
critiqueComplete: "boolean",
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
refine: {
|
|
270
|
-
requires: {
|
|
271
|
-
validationFailed: "boolean",
|
|
272
|
-
},
|
|
273
|
-
produces: {
|
|
274
|
-
refined: "boolean",
|
|
260
|
+
needsRefinement: "boolean",
|
|
275
261
|
},
|
|
276
262
|
},
|
|
277
263
|
};
|
|
@@ -328,19 +314,19 @@ const PIPELINE_STAGES = [
|
|
|
328
314
|
{
|
|
329
315
|
name: "critique",
|
|
330
316
|
handler: null, // Will be populated from dynamic module import
|
|
331
|
-
skipIf: (flags) => flags.
|
|
317
|
+
skipIf: (flags) => flags.needsRefinement !== true,
|
|
332
318
|
maxIterations: null,
|
|
333
319
|
},
|
|
334
320
|
{
|
|
335
321
|
name: "refine",
|
|
336
322
|
handler: null, // Will be populated from dynamic module import
|
|
337
|
-
skipIf: (flags) => flags.
|
|
338
|
-
maxIterations:
|
|
323
|
+
skipIf: (flags) => flags.needsRefinement !== true,
|
|
324
|
+
maxIterations: null,
|
|
339
325
|
},
|
|
340
326
|
{
|
|
341
327
|
name: "finalValidation",
|
|
342
328
|
handler: null, // Will be populated from dynamic module import
|
|
343
|
-
skipIf:
|
|
329
|
+
skipIf: (flags) => flags.needsRefinement !== true,
|
|
344
330
|
maxIterations: null,
|
|
345
331
|
},
|
|
346
332
|
{
|
|
@@ -365,12 +351,45 @@ export async function runPipeline(modulePath, initialContext = {}) {
|
|
|
365
351
|
const llmMetrics = [];
|
|
366
352
|
const llmEvents = getLLMEvents();
|
|
367
353
|
|
|
354
|
+
// Per-run write queue for serializing tokenUsage appends
|
|
355
|
+
let tokenWriteQueue = Promise.resolve();
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Appends token usage tuple to tasks-status.json with serialized writes.
|
|
359
|
+
* @param {string} workDir - Working directory path
|
|
360
|
+
* @param {string} taskName - Task identifier
|
|
361
|
+
* @param {Array<string, number, number>} tuple - [modelKey, inputTokens, outputTokens]
|
|
362
|
+
*/
|
|
363
|
+
function appendTokenUsage(workDir, taskName, tuple) {
|
|
364
|
+
tokenWriteQueue = tokenWriteQueue
|
|
365
|
+
.then(() =>
|
|
366
|
+
writeJobStatus(workDir, (snapshot) => {
|
|
367
|
+
if (!snapshot.tasks[taskName]) {
|
|
368
|
+
snapshot.tasks[taskName] = {};
|
|
369
|
+
}
|
|
370
|
+
const task = snapshot.tasks[taskName];
|
|
371
|
+
if (!Array.isArray(task.tokenUsage)) {
|
|
372
|
+
task.tokenUsage = [];
|
|
373
|
+
}
|
|
374
|
+
task.tokenUsage.push(tuple);
|
|
375
|
+
return snapshot;
|
|
376
|
+
})
|
|
377
|
+
)
|
|
378
|
+
.catch((e) => console.warn("[task-runner] tokenUsage append failed:", e));
|
|
379
|
+
}
|
|
380
|
+
|
|
368
381
|
const onLLMComplete = (metric) => {
|
|
369
382
|
llmMetrics.push({
|
|
370
383
|
...metric,
|
|
371
384
|
task: context.meta.taskName,
|
|
372
385
|
stage: context.currentStage,
|
|
373
386
|
});
|
|
387
|
+
|
|
388
|
+
// Append token usage immediately for each successful LLM completion
|
|
389
|
+
if (context.meta.workDir && context.meta.taskName) {
|
|
390
|
+
const tuple = deriveModelKeyAndTokens(metric);
|
|
391
|
+
appendTokenUsage(context.meta.workDir, context.meta.taskName, tuple);
|
|
392
|
+
}
|
|
374
393
|
};
|
|
375
394
|
|
|
376
395
|
llmEvents.on("llm:request:complete", onLLMComplete);
|
|
@@ -396,24 +415,26 @@ export async function runPipeline(modulePath, initialContext = {}) {
|
|
|
396
415
|
}
|
|
397
416
|
});
|
|
398
417
|
|
|
399
|
-
//
|
|
400
|
-
let fileIO = null;
|
|
418
|
+
// fileIO is mandatory for runner execution
|
|
401
419
|
if (
|
|
402
|
-
initialContext.workDir
|
|
403
|
-
initialContext.taskName
|
|
404
|
-
initialContext.statusPath
|
|
420
|
+
!initialContext.workDir ||
|
|
421
|
+
!initialContext.taskName ||
|
|
422
|
+
!initialContext.statusPath
|
|
405
423
|
) {
|
|
406
|
-
|
|
407
|
-
workDir: initialContext.workDir,
|
|
408
|
-
|
|
409
|
-
getStage: () => context.currentStage,
|
|
410
|
-
statusPath: initialContext.statusPath,
|
|
411
|
-
});
|
|
424
|
+
throw new Error(
|
|
425
|
+
`fileIO is required for task execution but missing required context. workDir: ${initialContext.workDir}, taskName: ${initialContext.taskName}, statusPath: ${initialContext.statusPath}`
|
|
426
|
+
);
|
|
412
427
|
}
|
|
413
428
|
|
|
414
|
-
|
|
429
|
+
const fileIO = createTaskFileIO({
|
|
430
|
+
workDir: initialContext.workDir,
|
|
431
|
+
taskName: initialContext.taskName,
|
|
432
|
+
getStage: () => context.currentStage,
|
|
433
|
+
statusPath: initialContext.statusPath,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Extract seed for new context structure
|
|
415
437
|
const seed = initialContext.seed || initialContext;
|
|
416
|
-
const maxRefinements = seed.maxRefinements ?? 1; // Default to 1 unless explicitly set
|
|
417
438
|
|
|
418
439
|
// Create new context structure with io, llm, meta, data, flags, logs, currentStage
|
|
419
440
|
const context = {
|
|
@@ -437,10 +458,11 @@ export async function runPipeline(modulePath, initialContext = {}) {
|
|
|
437
458
|
flags: {},
|
|
438
459
|
logs: [],
|
|
439
460
|
currentStage: null,
|
|
461
|
+
validators: {
|
|
462
|
+
validateWithSchema,
|
|
463
|
+
},
|
|
440
464
|
};
|
|
441
465
|
const logs = [];
|
|
442
|
-
let needsRefinement = false;
|
|
443
|
-
let refinementCount = 0;
|
|
444
466
|
let lastStageOutput = context.data.seed;
|
|
445
467
|
let lastStageName = "seed";
|
|
446
468
|
let lastExecutedStageName = "seed";
|
|
@@ -448,427 +470,290 @@ export async function runPipeline(modulePath, initialContext = {}) {
|
|
|
448
470
|
// Ensure log directory exists before stage execution
|
|
449
471
|
const logsDir = ensureLogDirectory(context.meta.workDir, context.meta.jobId);
|
|
450
472
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
for (const stageConfig of PIPELINE_STAGES) {
|
|
456
|
-
const stageName = stageConfig.name;
|
|
457
|
-
const stageHandler = stageConfig.handler;
|
|
458
|
-
|
|
459
|
-
// Skip stages when skipIf predicate returns true
|
|
460
|
-
if (stageConfig.skipIf && stageConfig.skipIf(context.flags)) {
|
|
461
|
-
context.logs.push({
|
|
462
|
-
stage: stageName,
|
|
463
|
-
action: "skipped",
|
|
464
|
-
reason: "skipIf predicate returned true",
|
|
465
|
-
timestamp: new Date().toISOString(),
|
|
466
|
-
});
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
473
|
+
// Single-pass pipeline execution
|
|
474
|
+
for (const stageConfig of PIPELINE_STAGES) {
|
|
475
|
+
const stageName = stageConfig.name;
|
|
476
|
+
const stageHandler = stageConfig.handler;
|
|
469
477
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
478
|
+
// Skip stages when skipIf predicate returns true
|
|
479
|
+
if (stageConfig.skipIf && stageConfig.skipIf(context.flags)) {
|
|
480
|
+
context.logs.push({
|
|
481
|
+
stage: stageName,
|
|
482
|
+
action: "skipped",
|
|
483
|
+
reason: "skipIf predicate returned true",
|
|
484
|
+
timestamp: new Date().toISOString(),
|
|
485
|
+
});
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
479
488
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
489
|
+
// Skip if handler is not available (not implemented)
|
|
490
|
+
if (typeof stageHandler !== "function") {
|
|
491
|
+
logs.push({
|
|
492
|
+
stage: stageName,
|
|
493
|
+
skipped: true,
|
|
494
|
+
});
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Add console output capture before stage execution using IO
|
|
499
|
+
const logName = `stage-${stageName}.log`;
|
|
500
|
+
const logPath = path.join(context.meta.workDir, "files", "logs", logName);
|
|
501
|
+
console.debug("[task-runner] stage log path resolution via IO", {
|
|
502
|
+
stage: stageName,
|
|
503
|
+
workDir: context.meta.workDir,
|
|
504
|
+
jobId: context.meta.jobId,
|
|
505
|
+
logName,
|
|
506
|
+
logPath,
|
|
507
|
+
});
|
|
508
|
+
const restoreConsole = captureConsoleOutput(logPath);
|
|
509
|
+
|
|
510
|
+
// Set current stage before execution
|
|
511
|
+
context.currentStage = stageName;
|
|
512
|
+
|
|
513
|
+
// Write stage start status using writeJobStatus
|
|
514
|
+
if (context.meta.workDir && context.meta.taskName) {
|
|
515
|
+
try {
|
|
516
|
+
await writeJobStatus(context.meta.workDir, (snapshot) => {
|
|
517
|
+
snapshot.current = context.meta.taskName;
|
|
518
|
+
snapshot.currentStage = stageName;
|
|
519
|
+
snapshot.lastUpdated = new Date().toISOString();
|
|
520
|
+
|
|
521
|
+
// Ensure task exists and update task-specific fields
|
|
522
|
+
if (!snapshot.tasks[context.meta.taskName]) {
|
|
523
|
+
snapshot.tasks[context.meta.taskName] = {};
|
|
524
|
+
}
|
|
525
|
+
snapshot.tasks[context.meta.taskName].currentStage = stageName;
|
|
526
|
+
snapshot.tasks[context.meta.taskName].state = TaskState.RUNNING;
|
|
490
527
|
});
|
|
491
|
-
|
|
528
|
+
} catch (error) {
|
|
529
|
+
// Don't fail the pipeline if status write fails
|
|
530
|
+
console.warn(`Failed to write stage start status: ${error.message}`);
|
|
492
531
|
}
|
|
532
|
+
}
|
|
493
533
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
534
|
+
// Clone data and flags before stage execution
|
|
535
|
+
const stageData = JSON.parse(JSON.stringify(context.data));
|
|
536
|
+
const stageFlags = JSON.parse(JSON.stringify(context.flags));
|
|
537
|
+
const stageContext = {
|
|
538
|
+
io: context.io,
|
|
539
|
+
llm: context.llm,
|
|
540
|
+
meta: context.meta,
|
|
541
|
+
data: stageData,
|
|
542
|
+
flags: stageFlags,
|
|
543
|
+
currentStage: stageName,
|
|
544
|
+
output: JSON.parse(
|
|
545
|
+
JSON.stringify(
|
|
546
|
+
lastStageOutput !== undefined
|
|
547
|
+
? lastStageOutput
|
|
548
|
+
: (context.data.seed ?? null)
|
|
549
|
+
)
|
|
550
|
+
),
|
|
551
|
+
previousStage: lastExecutedStageName,
|
|
552
|
+
validators: context.validators,
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Write pre-execution snapshot for debugging inputs via IO
|
|
556
|
+
const snapshot = {
|
|
557
|
+
meta: { taskName: context.meta.taskName, jobId: context.meta.jobId },
|
|
558
|
+
previousStage: lastExecutedStageName,
|
|
559
|
+
dataSummary: {
|
|
560
|
+
keys: Object.keys(context.data),
|
|
561
|
+
hasSeed: !!context.data?.seed,
|
|
562
|
+
seedKeys: Object.keys(context.data?.seed || {}),
|
|
563
|
+
seedHasData: context.data?.seed?.data !== undefined,
|
|
564
|
+
},
|
|
565
|
+
flagsSummary: {
|
|
566
|
+
keys: Object.keys(context.flags),
|
|
567
|
+
},
|
|
568
|
+
outputSummary: {
|
|
569
|
+
type: typeof stageContext.output,
|
|
570
|
+
keys:
|
|
571
|
+
stageContext.output && typeof stageContext.output === "object"
|
|
572
|
+
? Object.keys(stageContext.output).slice(0, 20)
|
|
573
|
+
: [],
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
await context.io.writeLog(
|
|
577
|
+
`stage-${stageName}-context.json`,
|
|
578
|
+
JSON.stringify(snapshot, null, 2),
|
|
579
|
+
{ mode: "replace" }
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
// Validate prerequisite flags before stage execution
|
|
583
|
+
const requiredFlags = FLAG_SCHEMAS[stageName]?.requires;
|
|
584
|
+
if (requiredFlags && Object.keys(requiredFlags).length > 0) {
|
|
585
|
+
validateFlagTypes(stageName, context.flags, requiredFlags);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Execute the stage
|
|
589
|
+
const start = performance.now();
|
|
590
|
+
let stageResult;
|
|
591
|
+
try {
|
|
592
|
+
context.logs.push({
|
|
593
|
+
stage: stageName,
|
|
594
|
+
action: "debugging",
|
|
595
|
+
data: stageContext,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
console.log("STAGE CONTEXT", JSON.stringify(stageContext, null, 2));
|
|
599
|
+
stageResult = await stageHandler(stageContext);
|
|
600
|
+
|
|
601
|
+
// Validate stage result shape after execution
|
|
602
|
+
assertStageResult(stageName, stageResult);
|
|
603
|
+
|
|
604
|
+
// Validate produced flags against schema
|
|
605
|
+
const producedFlagsSchema = FLAG_SCHEMAS[stageName]?.produces;
|
|
606
|
+
if (producedFlagsSchema) {
|
|
607
|
+
validateFlagTypes(stageName, stageResult.flags, producedFlagsSchema);
|
|
545
608
|
}
|
|
546
609
|
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
610
|
+
// Check for flag type conflicts before merging
|
|
611
|
+
checkFlagTypeConflicts(context.flags, stageResult.flags, stageName);
|
|
612
|
+
|
|
613
|
+
// Store stage output in context.data
|
|
614
|
+
context.data[stageName] = stageResult.output;
|
|
615
|
+
|
|
616
|
+
// Only update lastStageOutput and lastExecutedStageName for non-validation stages
|
|
617
|
+
// This ensures previousStage and context.output skip validation stages
|
|
618
|
+
const validationStages = [
|
|
619
|
+
"validateStructure",
|
|
620
|
+
"validateQuality",
|
|
621
|
+
"validateFinal",
|
|
622
|
+
"finalValidation",
|
|
623
|
+
];
|
|
624
|
+
if (!validationStages.includes(stageName)) {
|
|
625
|
+
lastStageOutput = stageResult.output;
|
|
626
|
+
lastExecutedStageName = stageName;
|
|
559
627
|
}
|
|
560
628
|
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
`stage-${stageName}.log`
|
|
567
|
-
);
|
|
568
|
-
console.debug("[task-runner] stage log path resolution", {
|
|
629
|
+
// Merge stage flags into context.flags
|
|
630
|
+
context.flags = { ...context.flags, ...stageResult.flags };
|
|
631
|
+
|
|
632
|
+
// Add audit log entry after stage completes
|
|
633
|
+
context.logs.push({
|
|
569
634
|
stage: stageName,
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
635
|
+
action: "completed",
|
|
636
|
+
outputType: typeof stageResult.output,
|
|
637
|
+
flagKeys: Object.keys(stageResult.flags),
|
|
638
|
+
timestamp: new Date().toISOString(),
|
|
573
639
|
});
|
|
574
|
-
const restoreConsole = captureConsoleOutput(logPath);
|
|
575
|
-
|
|
576
|
-
// Set current stage before execution
|
|
577
|
-
context.currentStage = stageName;
|
|
578
640
|
|
|
579
|
-
// Write stage
|
|
641
|
+
// Write stage completion status
|
|
580
642
|
if (context.meta.workDir && context.meta.taskName) {
|
|
581
643
|
try {
|
|
582
644
|
await writeJobStatus(context.meta.workDir, (snapshot) => {
|
|
645
|
+
// Keep current task and stage as-is since we're still within the same task
|
|
583
646
|
snapshot.current = context.meta.taskName;
|
|
584
647
|
snapshot.currentStage = stageName;
|
|
585
648
|
snapshot.lastUpdated = new Date().toISOString();
|
|
586
649
|
|
|
650
|
+
// Compute deterministic progress after stage completion
|
|
651
|
+
const pct = computeDeterministicProgress(
|
|
652
|
+
context.meta.pipelineTasks || [],
|
|
653
|
+
context.meta.taskName,
|
|
654
|
+
stageName
|
|
655
|
+
);
|
|
656
|
+
snapshot.progress = pct;
|
|
657
|
+
|
|
658
|
+
// Debug log for progress computation
|
|
659
|
+
console.debug("[task-runner] stage completion progress", {
|
|
660
|
+
task: context.meta.taskName,
|
|
661
|
+
stage: stageName,
|
|
662
|
+
progress: pct,
|
|
663
|
+
});
|
|
664
|
+
|
|
587
665
|
// Ensure task exists and update task-specific fields
|
|
588
666
|
if (!snapshot.tasks[context.meta.taskName]) {
|
|
589
667
|
snapshot.tasks[context.meta.taskName] = {};
|
|
590
668
|
}
|
|
591
669
|
snapshot.tasks[context.meta.taskName].currentStage = stageName;
|
|
592
|
-
snapshot.tasks[context.meta.taskName].state =
|
|
670
|
+
snapshot.tasks[context.meta.taskName].state = TaskState.RUNNING;
|
|
593
671
|
});
|
|
594
672
|
} catch (error) {
|
|
595
673
|
// Don't fail the pipeline if status write fails
|
|
596
|
-
console.warn(
|
|
674
|
+
console.warn(
|
|
675
|
+
`Failed to write stage completion status: ${error.message}`
|
|
676
|
+
);
|
|
597
677
|
}
|
|
598
678
|
}
|
|
599
679
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
output: JSON.parse(
|
|
611
|
-
JSON.stringify(
|
|
612
|
-
lastStageOutput !== undefined
|
|
613
|
-
? lastStageOutput
|
|
614
|
-
: (context.data.seed ?? null)
|
|
615
|
-
)
|
|
616
|
-
),
|
|
617
|
-
previousStage: lastExecutedStageName,
|
|
618
|
-
};
|
|
680
|
+
const ms = +(performance.now() - start).toFixed(2);
|
|
681
|
+
logs.push({
|
|
682
|
+
stage: stageName,
|
|
683
|
+
ok: true,
|
|
684
|
+
ms,
|
|
685
|
+
});
|
|
686
|
+
} catch (error) {
|
|
687
|
+
console.error(`Stage ${stageName} failed:`, error);
|
|
688
|
+
const ms = +(performance.now() - start).toFixed(2);
|
|
689
|
+
const errInfo = normalizeError(error);
|
|
619
690
|
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
691
|
+
// Attach debug metadata to the error envelope for richer diagnostics
|
|
692
|
+
errInfo.debug = {
|
|
693
|
+
stage: stageName,
|
|
623
694
|
previousStage: lastExecutedStageName,
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
},
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
},
|
|
634
|
-
outputSummary: {
|
|
635
|
-
type: typeof stageContext.output,
|
|
636
|
-
keys:
|
|
637
|
-
stageContext.output && typeof stageContext.output === "object"
|
|
638
|
-
? Object.keys(stageContext.output).slice(0, 20)
|
|
639
|
-
: [],
|
|
640
|
-
},
|
|
695
|
+
logPath: path.join(
|
|
696
|
+
context.meta.workDir,
|
|
697
|
+
"files",
|
|
698
|
+
"logs",
|
|
699
|
+
`stage-${stageName}.log`
|
|
700
|
+
),
|
|
701
|
+
snapshotPath: path.join(logsDir, `stage-${stageName}-context.json`),
|
|
702
|
+
dataHasSeed: !!context.data?.seed,
|
|
703
|
+
seedHasData: context.data?.seed?.data !== undefined,
|
|
704
|
+
flagsKeys: Object.keys(context.flags || {}),
|
|
641
705
|
};
|
|
642
|
-
writePreExecutionSnapshot(stageName, snapshot, logsDir);
|
|
643
|
-
|
|
644
|
-
// Validate prerequisite flags before stage execution
|
|
645
|
-
const requiredFlags = FLAG_SCHEMAS[stageName]?.requires;
|
|
646
|
-
if (requiredFlags && Object.keys(requiredFlags).length > 0) {
|
|
647
|
-
validateFlagTypes(stageName, context.flags, requiredFlags);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Execute the stage
|
|
651
|
-
const start = performance.now();
|
|
652
|
-
let stageResult;
|
|
653
|
-
try {
|
|
654
|
-
context.logs.push({
|
|
655
|
-
stage: stageName,
|
|
656
|
-
action: "debugging",
|
|
657
|
-
data: stageContext,
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
console.log("STAGE CONTEXT", JSON.stringify(stageContext, null, 2));
|
|
661
|
-
stageResult = await stageHandler(stageContext);
|
|
662
|
-
|
|
663
|
-
// Validate stage result shape after execution
|
|
664
|
-
assertStageResult(stageName, stageResult);
|
|
665
|
-
|
|
666
|
-
// Validate produced flags against schema
|
|
667
|
-
const producedFlagsSchema = FLAG_SCHEMAS[stageName]?.produces;
|
|
668
|
-
if (producedFlagsSchema) {
|
|
669
|
-
validateFlagTypes(stageName, stageResult.flags, producedFlagsSchema);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Check for flag type conflicts before merging
|
|
673
|
-
checkFlagTypeConflicts(context.flags, stageResult.flags, stageName);
|
|
674
|
-
|
|
675
|
-
// Store stage output in context.data
|
|
676
|
-
context.data[stageName] = stageResult.output;
|
|
677
|
-
lastStageName = stageName;
|
|
678
|
-
|
|
679
|
-
// Only update lastStageOutput and lastExecutedStageName for non-validation stages
|
|
680
|
-
// This ensures previousStage and context.output skip validation stages
|
|
681
|
-
const validationStages = [
|
|
682
|
-
"validateStructure",
|
|
683
|
-
"validateQuality",
|
|
684
|
-
"validateFinal",
|
|
685
|
-
"finalValidation",
|
|
686
|
-
];
|
|
687
|
-
if (!validationStages.includes(stageName)) {
|
|
688
|
-
lastStageOutput = stageResult.output;
|
|
689
|
-
lastExecutedStageName = stageName;
|
|
690
|
-
}
|
|
691
706
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
action: "completed",
|
|
699
|
-
outputType: typeof stageResult.output,
|
|
700
|
-
flagKeys: Object.keys(stageResult.flags),
|
|
701
|
-
timestamp: new Date().toISOString(),
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
// Write stage completion status
|
|
705
|
-
if (context.meta.workDir && context.meta.taskName) {
|
|
706
|
-
try {
|
|
707
|
-
await writeJobStatus(context.meta.workDir, (snapshot) => {
|
|
708
|
-
// Keep current task and stage as-is since we're still within the same task
|
|
709
|
-
snapshot.current = context.meta.taskName;
|
|
710
|
-
snapshot.currentStage = stageName;
|
|
711
|
-
snapshot.lastUpdated = new Date().toISOString();
|
|
712
|
-
|
|
713
|
-
// Compute deterministic progress after stage completion
|
|
714
|
-
const pct = computeDeterministicProgress(
|
|
715
|
-
context.meta.pipelineTasks || [],
|
|
716
|
-
context.meta.taskName,
|
|
717
|
-
stageName
|
|
718
|
-
);
|
|
719
|
-
snapshot.progress = pct;
|
|
720
|
-
|
|
721
|
-
// Debug log for progress computation
|
|
722
|
-
console.debug("[task-runner] stage completion progress", {
|
|
723
|
-
task: context.meta.taskName,
|
|
724
|
-
stage: stageName,
|
|
725
|
-
progress: pct,
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
// Ensure task exists and update task-specific fields
|
|
729
|
-
if (!snapshot.tasks[context.meta.taskName]) {
|
|
730
|
-
snapshot.tasks[context.meta.taskName] = {};
|
|
731
|
-
}
|
|
732
|
-
snapshot.tasks[context.meta.taskName].currentStage = stageName;
|
|
733
|
-
snapshot.tasks[context.meta.taskName].state = "running";
|
|
734
|
-
});
|
|
735
|
-
} catch (error) {
|
|
736
|
-
// Don't fail the pipeline if status write fails
|
|
737
|
-
console.warn(
|
|
738
|
-
`Failed to write stage completion status: ${error.message}`
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const ms = +(performance.now() - start).toFixed(2);
|
|
744
|
-
logs.push({
|
|
745
|
-
stage: stageName,
|
|
746
|
-
ok: true,
|
|
747
|
-
ms,
|
|
748
|
-
refinementCycle: refinementCount,
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
(stageName === "validateStructure" ||
|
|
753
|
-
stageName === "validateQuality") &&
|
|
754
|
-
context.flags.validationFailed &&
|
|
755
|
-
refinementCount < maxRefinements
|
|
756
|
-
) {
|
|
757
|
-
needsRefinement = true;
|
|
758
|
-
// Don't reset validationFailed here - let the refinement cycle handle it
|
|
759
|
-
break;
|
|
760
|
-
}
|
|
761
|
-
} catch (error) {
|
|
762
|
-
console.error(`Stage ${stageName} failed:`, error);
|
|
763
|
-
const ms = +(performance.now() - start).toFixed(2);
|
|
764
|
-
const errInfo = normalizeError(error);
|
|
765
|
-
|
|
766
|
-
// Attach debug metadata to the error envelope for richer diagnostics
|
|
767
|
-
errInfo.debug = {
|
|
768
|
-
stage: stageName,
|
|
769
|
-
previousStage: lastExecutedStageName,
|
|
770
|
-
refinementCycle: refinementCount,
|
|
771
|
-
logPath: path.join(
|
|
772
|
-
context.meta.workDir,
|
|
773
|
-
"files",
|
|
774
|
-
"logs",
|
|
775
|
-
`stage-${stageName}.log`
|
|
776
|
-
),
|
|
777
|
-
snapshotPath: path.join(logsDir, `stage-${stageName}-context.json`),
|
|
778
|
-
dataHasSeed: !!context.data?.seed,
|
|
779
|
-
seedHasData: context.data?.seed?.data !== undefined,
|
|
780
|
-
flagsKeys: Object.keys(context.flags || {}),
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
logs.push({
|
|
784
|
-
stage: stageName,
|
|
785
|
-
ok: false,
|
|
786
|
-
ms,
|
|
787
|
-
error: errInfo,
|
|
788
|
-
refinementCycle: refinementCount,
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
// For validation stages, trigger refinement if we haven't exceeded max refinements AND maxRefinements > 0
|
|
792
|
-
if (
|
|
793
|
-
(stageName === "validateStructure" ||
|
|
794
|
-
stageName === "validateQuality") &&
|
|
795
|
-
maxRefinements > 0 &&
|
|
796
|
-
refinementCount < maxRefinements
|
|
797
|
-
) {
|
|
798
|
-
context.flags.lastValidationError = errInfo;
|
|
799
|
-
context.flags.validationFailed = true; // Set the flag to trigger refinement
|
|
800
|
-
needsRefinement = true;
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
707
|
+
logs.push({
|
|
708
|
+
stage: stageName,
|
|
709
|
+
ok: false,
|
|
710
|
+
ms,
|
|
711
|
+
error: errInfo,
|
|
712
|
+
});
|
|
803
713
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
// Ensure task exists and update task-specific fields
|
|
814
|
-
if (!snapshot.tasks[context.meta.taskName]) {
|
|
815
|
-
snapshot.tasks[context.meta.taskName] = {};
|
|
816
|
-
}
|
|
817
|
-
snapshot.tasks[context.meta.taskName].state = "failed";
|
|
818
|
-
snapshot.tasks[context.meta.taskName].failedStage = stageName;
|
|
819
|
-
snapshot.tasks[context.meta.taskName].currentStage = stageName;
|
|
820
|
-
});
|
|
821
|
-
} catch (error) {
|
|
822
|
-
// Don't fail the pipeline if status write fails
|
|
823
|
-
console.warn(`Failed to write failure status: ${error.message}`);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
714
|
+
// Write failure status using writeJobStatus
|
|
715
|
+
if (context.meta.workDir && context.meta.taskName) {
|
|
716
|
+
try {
|
|
717
|
+
await writeJobStatus(context.meta.workDir, (snapshot) => {
|
|
718
|
+
snapshot.current = context.meta.taskName;
|
|
719
|
+
snapshot.currentStage = stageName;
|
|
720
|
+
snapshot.state = TaskState.FAILED;
|
|
721
|
+
snapshot.lastUpdated = new Date().toISOString();
|
|
826
722
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (restoreConsole) {
|
|
839
|
-
restoreConsole();
|
|
723
|
+
// Ensure task exists and update task-specific fields
|
|
724
|
+
if (!snapshot.tasks[context.meta.taskName]) {
|
|
725
|
+
snapshot.tasks[context.meta.taskName] = {};
|
|
726
|
+
}
|
|
727
|
+
snapshot.tasks[context.meta.taskName].state = TaskState.FAILED;
|
|
728
|
+
snapshot.tasks[context.meta.taskName].failedStage = stageName;
|
|
729
|
+
snapshot.tasks[context.meta.taskName].currentStage = stageName;
|
|
730
|
+
});
|
|
731
|
+
} catch (error) {
|
|
732
|
+
// Don't fail the pipeline if status write fails
|
|
733
|
+
console.warn(`Failed to write failure status: ${error.message}`);
|
|
840
734
|
}
|
|
841
735
|
}
|
|
842
|
-
}
|
|
843
736
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
737
|
+
await tokenWriteQueue.catch(() => {});
|
|
738
|
+
llmEvents.off("llm:request:complete", onLLMComplete);
|
|
739
|
+
|
|
740
|
+
// Fail immediately on any stage error
|
|
741
|
+
return {
|
|
742
|
+
ok: false,
|
|
743
|
+
failedStage: stageName,
|
|
744
|
+
error: errInfo,
|
|
745
|
+
logs,
|
|
746
|
+
context,
|
|
747
|
+
};
|
|
748
|
+
} finally {
|
|
749
|
+
// Add console output restoration after stage execution
|
|
750
|
+
restoreConsole();
|
|
853
751
|
}
|
|
854
|
-
} while (needsRefinement && refinementCount <= maxRefinements);
|
|
855
|
-
|
|
856
|
-
// Only fail on validationFailed if we actually have validation functions
|
|
857
|
-
const hasValidation =
|
|
858
|
-
typeof tasks.validateStructure === "function" ||
|
|
859
|
-
typeof tasks.validateQuality === "function";
|
|
860
|
-
|
|
861
|
-
if (context.flags.validationFailed && hasValidation) {
|
|
862
|
-
return {
|
|
863
|
-
ok: false,
|
|
864
|
-
failedStage: "final-validation",
|
|
865
|
-
error: { message: "Validation failed after all refinement attempts" },
|
|
866
|
-
logs,
|
|
867
|
-
context,
|
|
868
|
-
refinementAttempts: refinementCount,
|
|
869
|
-
};
|
|
870
752
|
}
|
|
871
753
|
|
|
754
|
+
// Flush any trailing token usage appends before cleanup
|
|
755
|
+
await tokenWriteQueue.catch(() => {}); // absorb last error to not mask pipeline result
|
|
756
|
+
|
|
872
757
|
llmEvents.off("llm:request:complete", onLLMComplete);
|
|
873
758
|
|
|
874
759
|
// Write final status with currentStage: null to indicate completion
|
|
@@ -877,7 +762,7 @@ export async function runPipeline(modulePath, initialContext = {}) {
|
|
|
877
762
|
await writeJobStatus(context.meta.workDir, (snapshot) => {
|
|
878
763
|
snapshot.current = null;
|
|
879
764
|
snapshot.currentStage = null;
|
|
880
|
-
snapshot.state =
|
|
765
|
+
snapshot.state = TaskState.DONE;
|
|
881
766
|
snapshot.progress = 100;
|
|
882
767
|
snapshot.lastUpdated = new Date().toISOString();
|
|
883
768
|
|
|
@@ -885,7 +770,7 @@ export async function runPipeline(modulePath, initialContext = {}) {
|
|
|
885
770
|
if (!snapshot.tasks[context.meta.taskName]) {
|
|
886
771
|
snapshot.tasks[context.meta.taskName] = {};
|
|
887
772
|
}
|
|
888
|
-
snapshot.tasks[context.meta.taskName].state =
|
|
773
|
+
snapshot.tasks[context.meta.taskName].state = TaskState.DONE;
|
|
889
774
|
snapshot.tasks[context.meta.taskName].currentStage = null;
|
|
890
775
|
});
|
|
891
776
|
} catch (error) {
|
|
@@ -898,7 +783,6 @@ export async function runPipeline(modulePath, initialContext = {}) {
|
|
|
898
783
|
ok: true,
|
|
899
784
|
logs,
|
|
900
785
|
context,
|
|
901
|
-
refinementAttempts: refinementCount,
|
|
902
786
|
llmMetrics,
|
|
903
787
|
};
|
|
904
788
|
}
|