@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.
Files changed (67) 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 +14 -12
  6. package/src/components/JobDetail.jsx +54 -51
  7. package/src/components/JobTable.jsx +72 -23
  8. package/src/components/Layout.jsx +145 -42
  9. package/src/components/LiveText.jsx +47 -0
  10. package/src/components/PageSubheader.jsx +75 -0
  11. package/src/components/TaskDetailSidebar.jsx +216 -0
  12. package/src/components/TimerText.jsx +82 -0
  13. package/src/components/UploadSeed.jsx +0 -70
  14. package/src/components/ui/Logo.jsx +16 -0
  15. package/src/components/ui/RestartJobModal.jsx +140 -0
  16. package/src/components/ui/toast.jsx +138 -0
  17. package/src/config/models.js +322 -0
  18. package/src/config/statuses.js +119 -0
  19. package/src/core/config.js +4 -34
  20. package/src/core/file-io.js +13 -28
  21. package/src/core/module-loader.js +54 -40
  22. package/src/core/pipeline-runner.js +65 -26
  23. package/src/core/status-writer.js +213 -58
  24. package/src/core/symlink-bridge.js +57 -0
  25. package/src/core/symlink-utils.js +94 -0
  26. package/src/core/task-runner.js +321 -437
  27. package/src/llm/index.js +258 -86
  28. package/src/pages/Code.jsx +351 -0
  29. package/src/pages/PipelineDetail.jsx +124 -15
  30. package/src/pages/PromptPipelineDashboard.jsx +20 -88
  31. package/src/providers/anthropic.js +83 -69
  32. package/src/providers/base.js +52 -0
  33. package/src/providers/deepseek.js +20 -21
  34. package/src/providers/gemini.js +226 -0
  35. package/src/providers/openai.js +36 -106
  36. package/src/providers/zhipu.js +136 -0
  37. package/src/ui/client/adapters/job-adapter.js +42 -28
  38. package/src/ui/client/api.js +134 -0
  39. package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -179
  40. package/src/ui/client/index.css +15 -0
  41. package/src/ui/client/index.html +2 -1
  42. package/src/ui/client/main.jsx +19 -14
  43. package/src/ui/client/time-store.js +161 -0
  44. package/src/ui/config-bridge.js +15 -24
  45. package/src/ui/config-bridge.node.js +15 -24
  46. package/src/ui/dist/assets/{index-CxcrauYR.js → index-DqkbzXZ1.js} +2132 -1086
  47. package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
  48. package/src/ui/dist/index.html +4 -3
  49. package/src/ui/job-reader.js +0 -108
  50. package/src/ui/public/favicon.svg +12 -0
  51. package/src/ui/server.js +252 -0
  52. package/src/ui/sse-enhancer.js +0 -1
  53. package/src/ui/transformers/list-transformer.js +32 -12
  54. package/src/ui/transformers/status-transformer.js +29 -42
  55. package/src/utils/dag.js +8 -4
  56. package/src/utils/duration.js +13 -19
  57. package/src/utils/formatters.js +27 -0
  58. package/src/utils/geometry-equality.js +83 -0
  59. package/src/utils/pipelines.js +5 -1
  60. package/src/utils/time-utils.js +40 -0
  61. package/src/utils/token-cost-calculator.js +294 -0
  62. package/src/utils/ui.jsx +18 -20
  63. package/src/components/ui/select.jsx +0 -27
  64. package/src/lib/utils.js +0 -6
  65. package/src/ui/client/hooks/useTicker.js +0 -26
  66. package/src/ui/config-bridge.browser.js +0 -149
  67. package/src/ui/dist/assets/style-D6K_oQ12.css +0 -62
@@ -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
- validateStructure: {
257
+ validateQuality: {
257
258
  requires: {},
258
259
  produces: {
259
- validationFailed: "boolean",
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.validationFailed === false,
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.validationFailed === false,
338
- maxIterations: (seed) => seed.maxRefinements || 1,
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: null,
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
- // Create fileIO singleton if we have the required context
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
- fileIO = createTaskFileIO({
407
- workDir: initialContext.workDir,
408
- taskName: initialContext.taskName,
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
- // Extract seed and maxRefinements for new context structure
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
- do {
452
- needsRefinement = false;
453
- let preRefinedThisCycle = false;
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
- // Skip if handler is not available (not implemented)
471
- if (typeof stageHandler !== "function") {
472
- logs.push({
473
- stage: stageName,
474
- skipped: true,
475
- refinementCycle: refinementCount,
476
- });
477
- continue;
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
- // Skip ingestion and preProcessing during refinement cycles
481
- if (
482
- refinementCount > 0 &&
483
- ["ingestion", "preProcessing"].includes(stageName)
484
- ) {
485
- logs.push({
486
- stage: stageName,
487
- skipped: true,
488
- reason: "refinement-cycle",
489
- refinementCycle: refinementCount,
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
- continue;
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
- // Handle pre-refinement logic for validation stages
495
- if (
496
- refinementCount > 0 &&
497
- !preRefinedThisCycle &&
498
- !context.flags.refined &&
499
- (stageName === "validateStructure" || stageName === "validateQuality")
500
- ) {
501
- for (const s of ["critique", "refine"]) {
502
- const sConfig = PIPELINE_STAGES.find((config) => config.name === s);
503
- const sHandler = sConfig?.handler;
504
- if (typeof sHandler !== "function") {
505
- logs.push({
506
- stage: s,
507
- skipped: true,
508
- reason: "pre-refine-missing",
509
- refinementCycle: refinementCount,
510
- });
511
- continue;
512
- }
513
- const sStart = performance.now();
514
- try {
515
- const r = await sHandler(context);
516
- const sMs = +(performance.now() - sStart).toFixed(2);
517
- logs.push({
518
- stage: s,
519
- ok: true,
520
- ms: sMs,
521
- refinementCycle: refinementCount,
522
- reason: "pre-validate",
523
- });
524
- } catch (error) {
525
- const sMs = +(performance.now() - sStart).toFixed(2);
526
- const errInfo = normalizeError(error);
527
- logs.push({
528
- stage: s,
529
- ok: false,
530
- ms: sMs,
531
- error: errInfo,
532
- refinementCycle: refinementCount,
533
- });
534
- return {
535
- ok: false,
536
- failedStage: s,
537
- error: errInfo,
538
- logs,
539
- context,
540
- refinementAttempts: refinementCount,
541
- };
542
- }
543
- }
544
- preRefinedThisCycle = true;
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
- // Skip critique and refine if already pre-refined
548
- if (
549
- preRefinedThisCycle &&
550
- (stageName === "critique" || stageName === "refine")
551
- ) {
552
- logs.push({
553
- stage: stageName,
554
- skipped: true,
555
- reason: "already-pre-refined",
556
- refinementCycle: refinementCount,
557
- });
558
- continue;
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
- // Add console output capture before stage execution
562
- const logPath = path.join(
563
- context.meta.workDir,
564
- "files",
565
- "logs",
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
- workDir: context.meta.workDir,
571
- jobId: context.meta.jobId,
572
- logPath,
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 start status using writeJobStatus
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 = "running";
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(`Failed to write stage start status: ${error.message}`);
674
+ console.warn(
675
+ `Failed to write stage completion status: ${error.message}`
676
+ );
597
677
  }
598
678
  }
599
679
 
600
- // Clone data and flags before stage execution
601
- const stageData = JSON.parse(JSON.stringify(context.data));
602
- const stageFlags = JSON.parse(JSON.stringify(context.flags));
603
- const stageContext = {
604
- io: context.io,
605
- llm: context.llm,
606
- meta: context.meta,
607
- data: stageData,
608
- flags: stageFlags,
609
- currentStage: stageName,
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
- // Write pre-execution snapshot for debugging inputs
621
- const snapshot = {
622
- meta: { taskName: context.meta.taskName, jobId: context.meta.jobId },
691
+ // Attach debug metadata to the error envelope for richer diagnostics
692
+ errInfo.debug = {
693
+ stage: stageName,
623
694
  previousStage: lastExecutedStageName,
624
- refinementCycle: refinementCount,
625
- dataSummary: {
626
- keys: Object.keys(context.data),
627
- hasSeed: !!context.data?.seed,
628
- seedKeys: Object.keys(context.data?.seed || {}),
629
- seedHasData: context.data?.seed?.data !== undefined,
630
- },
631
- flagsSummary: {
632
- keys: Object.keys(context.flags),
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
- // Merge stage flags into context.flags
693
- context.flags = { ...context.flags, ...stageResult.flags };
694
-
695
- // Add audit log entry after stage completes
696
- context.logs.push({
697
- stage: stageName,
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
- // Write failure status using writeJobStatus
805
- if (context.meta.workDir && context.meta.taskName) {
806
- try {
807
- await writeJobStatus(context.meta.workDir, (snapshot) => {
808
- snapshot.current = context.meta.taskName;
809
- snapshot.currentStage = stageName;
810
- snapshot.state = "failed";
811
- snapshot.lastUpdated = new Date().toISOString();
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
- // For non-validation stages or when refinements are exhausted, fail immediately
828
- return {
829
- ok: false,
830
- failedStage: stageName,
831
- error: errInfo,
832
- logs,
833
- context,
834
- refinementAttempts: refinementCount,
835
- };
836
- } finally {
837
- // Add console output restoration after stage execution
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
- if (needsRefinement) {
845
- refinementCount++;
846
- logs.push({
847
- stage: "refinement-trigger",
848
- refinementCycle: refinementCount,
849
- reason: context.flags.lastValidationError
850
- ? "validation-error"
851
- : "validation-failed-flag",
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 = "done";
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 = "done";
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
  }