tubeless 0.2.2 → 0.2.3

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 (87) hide show
  1. package/README.md +5 -5
  2. package/dist/core/child-execution.js +8 -1
  3. package/dist/core/child-execution.js.map +1 -1
  4. package/dist/core/lifecycle.js +2 -0
  5. package/dist/core/lifecycle.js.map +1 -1
  6. package/dist/core/pipeline-execute.d.ts +2 -0
  7. package/dist/core/pipeline-execute.js +132 -395
  8. package/dist/core/pipeline-execute.js.map +1 -1
  9. package/dist/core/pipeline-plan.js +96 -34
  10. package/dist/core/pipeline-plan.js.map +1 -1
  11. package/dist/core/pipeline-run-state.d.ts +49 -0
  12. package/dist/core/pipeline-run-state.js +284 -0
  13. package/dist/core/pipeline-run-state.js.map +1 -0
  14. package/dist/core/pipeline-step-executor.d.ts +20 -0
  15. package/dist/core/pipeline-step-executor.js +52 -0
  16. package/dist/core/pipeline-step-executor.js.map +1 -0
  17. package/dist/core/pipeline-types.d.ts +11 -2
  18. package/dist/core/pipeline-types.js.map +1 -1
  19. package/dist/core/pipeline-validation.d.ts +6 -0
  20. package/dist/core/pipeline-validation.js +41 -0
  21. package/dist/core/pipeline-validation.js.map +1 -0
  22. package/dist/run-store/run-store-ndjson.js +2 -295
  23. package/dist/run-store/run-store-ndjson.js.map +1 -1
  24. package/dist/run-store/run-store-sqlite.js +91 -42
  25. package/dist/run-store/run-store-sqlite.js.map +1 -1
  26. package/dist/run-store/run-store.d.ts +3 -2
  27. package/dist/run-store/run-store.js +38 -195
  28. package/dist/run-store/run-store.js.map +1 -1
  29. package/dist/studio/run-store-ui-client-controller.d.ts +9 -0
  30. package/dist/studio/run-store-ui-client-controller.js +479 -0
  31. package/dist/studio/run-store-ui-client-controller.js.map +1 -0
  32. package/dist/studio/run-store-ui-client-form.d.ts +9 -0
  33. package/dist/studio/run-store-ui-client-form.js +44 -0
  34. package/dist/studio/run-store-ui-client-form.js.map +1 -0
  35. package/dist/studio/run-store-ui-client-model.d.ts +40 -0
  36. package/dist/studio/run-store-ui-client-model.js +211 -0
  37. package/dist/studio/run-store-ui-client-model.js.map +1 -0
  38. package/dist/studio/run-store-ui-client-source.js +1 -1
  39. package/dist/studio/run-store-ui-client-transport.d.ts +25 -0
  40. package/dist/studio/run-store-ui-client-transport.js +322 -0
  41. package/dist/studio/run-store-ui-client-transport.js.map +1 -0
  42. package/dist/studio/run-store-ui-client-views.d.ts +24 -0
  43. package/dist/studio/run-store-ui-client-views.js +676 -0
  44. package/dist/studio/run-store-ui-client-views.js.map +1 -0
  45. package/dist/studio/run-store-ui-client.d.ts +2 -14
  46. package/dist/studio/run-store-ui-client.js +7 -1360
  47. package/dist/studio/run-store-ui-client.js.map +1 -1
  48. package/dist/studio/run-store-ui-page.d.ts +1 -1
  49. package/dist/studio/run-store-ui.js +44 -25
  50. package/dist/studio/run-store-ui.js.map +1 -1
  51. package/dist/tracing/tracing-codec.d.ts +6 -0
  52. package/dist/tracing/tracing-codec.js +829 -0
  53. package/dist/tracing/tracing-codec.js.map +1 -0
  54. package/dist/tracing/tracing-constants.d.ts +4 -0
  55. package/dist/tracing/tracing-constants.js +5 -0
  56. package/dist/tracing/tracing-constants.js.map +1 -0
  57. package/dist/tracing/tracing-internal.js +116 -81
  58. package/dist/tracing/tracing-internal.js.map +1 -1
  59. package/dist/tracing/tracing-otel.js +19 -3
  60. package/dist/tracing/tracing-otel.js.map +1 -1
  61. package/dist/tracing/tracing.d.ts +149 -10
  62. package/dist/tracing/tracing.js.map +1 -1
  63. package/dist/workbench/workbench-history.js +8 -1
  64. package/dist/workbench/workbench-history.js.map +1 -1
  65. package/dist/workbench/workbench-ui.js +26 -6
  66. package/dist/workbench/workbench-ui.js.map +1 -1
  67. package/docs/README.md +21 -15
  68. package/docs/agent-guide.md +48 -35
  69. package/docs/agent-skills.md +59 -0
  70. package/docs/api-reference.md +28 -12
  71. package/docs/api-report.json +29 -13
  72. package/docs/child-pipeline-composition.md +142 -193
  73. package/docs/cli.md +83 -61
  74. package/docs/comparison.md +63 -62
  75. package/docs/concepts.md +210 -272
  76. package/docs/getting-started.md +59 -48
  77. package/docs/llms.txt +7 -4
  78. package/docs/recipes.md +21 -18
  79. package/docs/remote-step-composition.md +69 -74
  80. package/docs/studio.md +146 -71
  81. package/examples/catalog/pipelines/enrich.ts +2 -2
  82. package/examples/catalog/tubeless.project.ts +4 -2
  83. package/examples/host-embedding.ts +4 -5
  84. package/examples/remote-steps.ts +8 -5
  85. package/package.json +11 -10
  86. package/skills/tubeless/SKILL.md +83 -0
  87. package/skills/tubeless-make-pipeline/SKILL.md +126 -0
@@ -3,16 +3,18 @@ import { PipelineChildError } from "./child-execution.js";
3
3
  import { brandTubelessError } from "../utilities/tubeless-error.js";
4
4
  import { createPipelineLifecycleObserver } from "./lifecycle.js";
5
5
  import { formatPipelineError } from "./pipeline-diagnostics.js";
6
- import { createRunId, RUN_MODEL_VERSION } from "./pipeline-ids.js";
6
+ import { createRunId } from "./pipeline-ids.js";
7
7
  import { PIPELINE_FINALIZE_STEP_ID, compiledStepGraph, decideStepDisposition, planStepById, stepToPlanStep, } from "./pipeline-plan.js";
8
+ import { PipelineRunState, isCancellationOnly, } from "./pipeline-run-state.js";
9
+ import { evaluateStepSkip, executeStepAttempt, validateStepOutput, } from "./pipeline-step-executor.js";
10
+ import { PipelineBoundaryValidationError, validateStandardSchema } from "./pipeline-validation.js";
8
11
  import { createPipelineTraceEmitter } from "../tracing/tracing-internal.js";
9
- function isCancellationOnly(errors) {
10
- return errors.length > 0 && errors.every(({ kind }) => kind === "cancellation");
11
- }
12
- function terminalRunStatus(errors) {
13
- if (errors.length === 0)
14
- return "completed";
15
- return isCancellationOnly(errors) ? "cancelled" : "failed";
12
+ const PIPELINE_RUN_ID_OBSERVER = Symbol.for("tubeless.pipeline.runIdObserver");
13
+ /** Observe an execution ID from an internal host without allowing it to be overridden. */
14
+ export function observePipelineRunId(context, observer) {
15
+ // SAFETY: the internal observer is intentionally hidden from the returned
16
+ // public type and can observe, but never choose, the generated run ID.
17
+ return { ...context, [PIPELINE_RUN_ID_OBSERVER]: observer };
16
18
  }
17
19
  const PIPELINE_LOGGER_BASE = Symbol("pipelineLoggerBase");
18
20
  function basePipelineLogger(log) {
@@ -46,46 +48,6 @@ export class PipelineExecutionError extends Error {
46
48
  brandTubelessError(this, "pipeline-execution");
47
49
  }
48
50
  }
49
- class PipelineBoundaryValidationError extends Error {
50
- issues;
51
- constructor(message, issues, cause) {
52
- super(message, cause === undefined ? undefined : { cause });
53
- this.issues = issues;
54
- this.name = "PipelineBoundaryValidationError";
55
- }
56
- }
57
- function normalizeValidationPath(path) {
58
- if (!path || path.length === 0)
59
- return undefined;
60
- return path.map((segment) => {
61
- const key = typeof segment === "object" && segment !== null ? segment.key : segment;
62
- return typeof key === "number" || typeof key === "string" ? key : String(key);
63
- });
64
- }
65
- function renderValidationIssue(issue) {
66
- const location = issue.path?.map(String).join(".");
67
- return location ? `${location}: ${issue.message}` : issue.message;
68
- }
69
- async function validateStandardSchema(schema, value, boundary) {
70
- let result;
71
- try {
72
- result = await schema["~standard"].validate(value);
73
- }
74
- catch (error) {
75
- throw new PipelineBoundaryValidationError(`${boundary} schema (${schema["~standard"].vendor}) threw while validating`, [], error);
76
- }
77
- if (result.issues) {
78
- const issues = result.issues.map((issue) => {
79
- const path = normalizeValidationPath(issue.path);
80
- const normalized = { message: issue.message };
81
- if (path)
82
- normalized.path = path;
83
- return normalized;
84
- });
85
- throw new PipelineBoundaryValidationError(`${boundary} validation failed: ${issues.map(renderValidationIssue).join("; ")}`, issues);
86
- }
87
- return result.value;
88
- }
89
51
  const MAX_PIPELINE_CAUSE_DEPTH = 8;
90
52
  function readErrorField(value, field) {
91
53
  try {
@@ -201,81 +163,29 @@ export function isPipelineCancellation(error, runtime) {
201
163
  function isCancelledResult(result) {
202
164
  return result.status === "cancelled";
203
165
  }
204
- function errorRunResult(runtime, pipelineId, dryRun, startedAt, errors, identity) {
205
- const finishedAtMs = runtime.now();
206
- const result = {
207
- pipelineId,
208
- dryRun,
209
- errors,
210
- finalized: false,
211
- finishedAtMs,
212
- runId: identity.runId,
213
- startedAtMs: startedAt,
214
- status: terminalRunStatus(errors),
215
- steps: [],
216
- version: RUN_MODEL_VERSION,
217
- };
218
- if (identity.parentRunId)
219
- result.parentRunId = identity.parentRunId;
220
- return result;
221
- }
222
166
  function throwIfAborted(runtime) {
223
167
  throwIfSignalAborted(runtime.signal, "Pipeline run");
224
168
  }
225
- function recordSkip(step, stepId, reports, reportsByStepId, reason, message, dependencyId, attemptId, startedAtMs, finishedAtMs) {
226
- const report = {
227
- id: stepId,
228
- name: step.name,
229
- description: step.description,
230
- finishedAtMs,
231
- reason,
232
- status: "skipped",
233
- };
234
- if (attemptId)
235
- report.attemptId = attemptId;
236
- if (startedAtMs !== undefined)
237
- report.startedAtMs = startedAtMs;
238
- if (dependencyId)
239
- report.dependencyId = dependencyId;
240
- if (message)
241
- report.message = message;
242
- reports.push(report);
243
- reportsByStepId.set(stepId, report);
244
- return report;
245
- }
246
- function normalizeStepSkipDecision(decision) {
247
- if (decision == null || decision === false)
248
- return null;
249
- if (typeof decision === "string") {
250
- const reason = decision.trim();
251
- return reason.length > 0 ? { reason } : null;
252
- }
253
- if (typeof decision === "object" && typeof decision.reason === "string") {
254
- const reason = decision.reason.trim();
255
- if (reason.length === 0)
256
- return null;
257
- return { reason, value: decision.value };
258
- }
259
- return null;
260
- }
261
169
  export async function executePlannedRun(input) {
262
170
  const { compiled, controls, runtime } = input;
263
- const optionsSchema = compiled.optionsSchema;
264
- const targetIds = compiled.targetIds;
265
- // SAFETY: `domainOptions` is the user-supplied options object; if a schema
266
- // is present it is re-validated below before assignment to `pipelineOptions`.
267
- let pipelineOptions = input.domainOptions;
268
- const startedAt = runtime.now();
269
- const runId = runtime.runId ?? createRunId(compiled.id);
270
- const parentRunId = runtime.parentRunId;
171
+ const startedAtMs = runtime.now();
172
+ const runId = createRunId(compiled.id);
173
+ try {
174
+ // SAFETY: only internal hosts attach this observation callback. A callback
175
+ // failure must not change whether the pipeline execution itself can start.
176
+ runtime[PIPELINE_RUN_ID_OBSERVER]?.(runId);
177
+ }
178
+ catch {
179
+ // Ignore observation failures; execution identity remains package-owned.
180
+ }
181
+ const correlationId = runtime.correlationId ?? runtime.runId;
271
182
  const identity = { runId };
272
- if (parentRunId)
273
- identity.parentRunId = parentRunId;
183
+ if (correlationId !== undefined)
184
+ identity.correlationId = correlationId;
185
+ if (runtime.parentRunId)
186
+ identity.parentRunId = runtime.parentRunId;
274
187
  const dryRun = controls.dryRun === true;
275
- const trace = createPipelineTraceEmitter(compiled.id, runtime.tracing, basePipelineLogger(runtime.log), {
276
- ...identity,
277
- itemKey: runtime.tracing?.itemKey,
278
- }, runtime.now);
188
+ const trace = createPipelineTraceEmitter(compiled.id, runtime.tracing, basePipelineLogger(runtime.log), { ...identity, itemKey: runtime.tracing?.itemKey }, runtime.now);
279
189
  const lifecycle = createPipelineLifecycleObserver(compiled.id, runtime, trace);
280
190
  const tracedLogger = (stepId, attemptId) => {
281
191
  if (!trace)
@@ -298,53 +208,18 @@ export async function executePlannedRun(input) {
298
208
  log[PIPELINE_LOGGER_BASE] = base;
299
209
  return log;
300
210
  };
301
- let nextAttemptSequence = 0;
302
- const beginAttempt = (stepId, startedAtMs = runtime.now()) => ({
303
- attemptId: `${runId}:attempt:${(nextAttemptSequence += 1).toString(36)}`,
304
- startedAtMs,
305
- stepId,
306
- });
307
- const currentStepStatuses = new Map();
308
- const publishStepStatus = (event, traceStatus = true) => {
309
- const previous = currentStepStatuses.get(event.step.id);
310
- const valid = (previous === undefined && event.status === "planned") ||
311
- (previous === "planned" &&
312
- (event.status === "running" ||
313
- event.status === "skipped" ||
314
- event.status === "cancelled")) ||
315
- (previous === "running" &&
316
- (event.status === "running" ||
317
- event.status === "completed" ||
318
- event.status === "skipped" ||
319
- event.status === "cancelled" ||
320
- event.status === "failed"));
321
- if (!valid) {
322
- throw new Error(`Invalid step status transition for ${event.step.id}: ${previous ?? "unobserved"} -> ${event.status}`);
323
- }
324
- currentStepStatuses.set(event.step.id, event.status);
325
- lifecycle.stepStatus(event, traceStatus);
326
- };
327
- const publishStepReport = (step, report) => {
328
- switch (report.status) {
329
- case "cancelled":
330
- case "failed":
331
- case "skipped":
332
- case "completed":
333
- publishStepStatus({ ...report, pipelineId: compiled.id, step });
334
- }
335
- };
336
- const runPlan = input.plan;
337
- const plannedSteps = planStepById(runPlan);
338
- lifecycle.pipelineStart(runPlan, targetIds);
339
- if (!runPlan.ok) {
340
- const result = errorRunResult(runtime, compiled.id, dryRun, startedAt, runPlan.errors, identity);
341
- lifecycle.pipelineComplete(result);
342
- await lifecycle.flush();
343
- return result;
211
+ const state = new PipelineRunState(compiled.id, dryRun, startedAtMs, identity, runtime.now, lifecycle);
212
+ state.start(input.plan, compiled.targetIds);
213
+ if (!input.plan.ok) {
214
+ state.recordRunErrors(input.plan.errors);
215
+ return state.finish();
344
216
  }
345
- if (optionsSchema) {
217
+ // SAFETY: `domainOptions` is the user-supplied options object; if a schema
218
+ // is present it is re-validated below before assignment to `pipelineOptions`.
219
+ let pipelineOptions = input.domainOptions;
220
+ if (compiled.optionsSchema) {
346
221
  try {
347
- const validated = await validateStandardSchema(optionsSchema, input.domainOptions, `Pipeline ${compiled.id} options`);
222
+ const validated = await validateStandardSchema(compiled.optionsSchema, input.domainOptions, `Pipeline ${compiled.id} options`);
348
223
  if (typeof validated !== "object" || validated === null || Array.isArray(validated)) {
349
224
  throw new PipelineBoundaryValidationError(`Pipeline ${compiled.id} options schema returned a non-object value`, [{ message: "Expected the validated options value to be an object" }]);
350
225
  }
@@ -353,25 +228,18 @@ export async function executePlannedRun(input) {
353
228
  pipelineOptions = validated;
354
229
  }
355
230
  catch (error) {
356
- const pipelineError = toPipelineError(error, {
357
- code: "TUBELESS_OPTIONS_VALIDATION_FAILED",
358
- kind: "validation",
359
- phase: "execution",
360
- });
361
- const result = errorRunResult(runtime, compiled.id, dryRun, startedAt, [pipelineError], identity);
362
- lifecycle.pipelineComplete(result);
363
- await lifecycle.flush();
364
- return result;
231
+ state.recordRunErrors([
232
+ toPipelineError(error, {
233
+ code: "TUBELESS_OPTIONS_VALIDATION_FAILED",
234
+ kind: "validation",
235
+ phase: "execution",
236
+ }),
237
+ ]);
238
+ return state.finish();
365
239
  }
366
240
  }
367
- for (const step of runPlan.steps) {
368
- publishStepStatus({ pipelineId: compiled.id, status: "planned", step });
369
- }
370
- const outputs = new Map();
371
- const reports = [];
372
- const errors = [];
373
- const reportsByStepId = new Map();
374
- let completed = true;
241
+ for (const step of input.plan.steps)
242
+ state.planStep(step);
375
243
  const executionContext = {
376
244
  ...runtime,
377
245
  dryRun,
@@ -380,50 +248,50 @@ export async function executePlannedRun(input) {
380
248
  runId,
381
249
  trace: trace?.context,
382
250
  };
383
- if (parentRunId)
384
- executionContext.parentRunId = parentRunId;
251
+ if (correlationId !== undefined)
252
+ executionContext.correlationId = correlationId;
253
+ if (runtime.parentRunId)
254
+ executionContext.parentRunId = runtime.parentRunId;
255
+ const plannedSteps = planStepById(input.plan);
385
256
  const stepsById = new Map(compiled.orderedSteps.map((step) => [step.id, step]));
386
- // `runPlan.steps` is already topologically ordered. Rehydrate from the original step
387
- // objects so callers still get the typed `run` functions rather than plan metadata.
257
+ // `input.plan.steps` is already topologically ordered. Rehydrate from the original step
258
+ // objects so callers still get the typed handlers rather than plan metadata.
388
259
  // SAFETY: every planned step id originates from `compiled.orderedSteps`, so the
389
260
  // map lookup always finds the matching `AnyStep<TOptions>`.
390
- const orderedSteps = runPlan.steps.map((step) => stepsById.get(step.id));
261
+ const orderedSteps = input.plan.steps.map((step) => stepsById.get(step.id));
262
+ const plannedStepFor = (step) => plannedSteps.get(step.id) ??
263
+ stepToPlanStep(step, true, undefined, undefined, compiledStepGraph(compiled, step));
391
264
  const recordRemainingStates = (fromIndex, failedStepId, cancellationError) => {
392
265
  for (let index = fromIndex; index < orderedSteps.length; index++) {
393
266
  const step = orderedSteps[index];
394
- const plannedStep = plannedSteps.get(step.id) ??
395
- stepToPlanStep(step, true, undefined, undefined, compiledStepGraph(compiled, step));
267
+ const plannedStep = plannedStepFor(step);
396
268
  if (plannedStep.skipReason) {
397
269
  const dependencyId = plannedStep.skipReason === "unmet-dependency"
398
270
  ? plannedStep.dependencies.find((id) => plannedSteps.get(id)?.skipReason !== undefined)
399
271
  : undefined;
400
- const report = recordSkip(step, step.id, reports, reportsByStepId, plannedStep.skipReason, undefined, dependencyId, undefined, undefined, runtime.now());
401
- publishStepReport(plannedStep, report);
402
- continue;
272
+ state.skipStep(plannedStep, {
273
+ reason: plannedStep.skipReason,
274
+ dependencyId,
275
+ });
403
276
  }
404
- if (cancellationError) {
405
- const report = {
406
- id: step.id,
407
- name: step.name,
408
- description: step.description,
409
- error: { ...cancellationError, stepId: step.id },
410
- finishedAtMs: runtime.now(),
411
- status: "cancelled",
412
- };
413
- reports.push(report);
414
- reportsByStepId.set(step.id, report);
415
- publishStepReport(plannedStep, report);
416
- continue;
277
+ else if (cancellationError) {
278
+ state.cancelStep(plannedStep, { ...cancellationError, stepId: step.id }, false);
279
+ }
280
+ else {
281
+ state.skipStep(plannedStep, {
282
+ reason: "fail-fast",
283
+ message: failedStepId
284
+ ? `Not run because fail-fast stopped after ${failedStepId} failed.`
285
+ : "Not run because the pipeline was aborted before this step started.",
286
+ dependencyId: failedStepId,
287
+ });
417
288
  }
418
- const report = recordSkip(step, step.id, reports, reportsByStepId, "fail-fast", failedStepId
419
- ? `Not run because fail-fast stopped after ${failedStepId} failed.`
420
- : "Not run because the pipeline was aborted before this step started.", failedStepId, undefined, undefined, runtime.now());
421
- publishStepReport(plannedStep, report);
422
289
  }
423
290
  };
424
291
  const cancelBeforeStepStart = (stepId, stepIndex) => {
425
292
  try {
426
293
  throwIfAborted(runtime);
294
+ return false;
427
295
  }
428
296
  catch (error) {
429
297
  const pipelineError = toPipelineError(error, {
@@ -432,14 +300,12 @@ export async function executePlannedRun(input) {
432
300
  phase: "execution",
433
301
  stepId,
434
302
  });
435
- errors.push(pipelineError);
436
- completed = false;
303
+ state.recordRunErrors([pipelineError]);
437
304
  recordRemainingStates(stepIndex, undefined, pipelineError);
438
305
  return true;
439
306
  }
440
- return false;
441
307
  };
442
- const recordStepExecutionFailure = (error, plannedStep, step, stepId, stepIndex, attempt) => {
308
+ const recordStepExecutionFailure = (error, plannedStep, stepIndex, attempt) => {
443
309
  const cancelled = isPipelineCancellation(error, runtime);
444
310
  const childFailure = error instanceof PipelineExecutionError || error instanceof PipelineChildError;
445
311
  const validationFailure = error instanceof PipelineBoundaryValidationError;
@@ -459,217 +325,109 @@ export async function executePlannedRun(input) {
459
325
  ? "child"
460
326
  : "step",
461
327
  phase: "execution",
462
- stepId,
328
+ stepId: plannedStep.id,
463
329
  });
464
- errors.push(pipelineError);
465
- const finishedAtMs = runtime.now();
466
- const report = cancelled
467
- ? {
468
- attemptId: attempt.attemptId,
469
- id: stepId,
470
- name: step.name,
471
- description: step.description,
472
- error: pipelineError,
473
- finishedAtMs,
474
- startedAtMs: attempt.startedAtMs,
475
- status: "cancelled",
476
- }
477
- : {
478
- attemptId: attempt.attemptId,
479
- id: stepId,
480
- name: step.name,
481
- description: step.description,
482
- error: pipelineError,
483
- finishedAtMs,
484
- startedAtMs: attempt.startedAtMs,
485
- status: "failed",
486
- };
487
- reports.push(report);
488
- reportsByStepId.set(stepId, report);
489
- publishStepReport(plannedStep, report);
490
- completed = false;
491
- if (!controls.continueOnError) {
492
- recordRemainingStates(stepIndex + 1, stepId, report.status === "cancelled" ? pipelineError : undefined);
493
- return true;
494
- }
495
- return false;
330
+ state.failStep(plannedStep, attempt, pipelineError);
331
+ if (controls.continueOnError)
332
+ return false;
333
+ recordRemainingStates(stepIndex + 1, plannedStep.id, cancelled ? pipelineError : undefined);
334
+ return true;
496
335
  };
497
- const recordPublishedStepValue = async (args) => {
498
- let published = args.value;
499
- let attempt = args.attempt;
500
- if (args.step.outputSchema) {
501
- if (!attempt) {
502
- attempt = beginAttempt(args.stepId, runtime.now());
503
- publishStepStatus({
504
- attemptId: attempt.attemptId,
505
- pipelineId: compiled.id,
506
- status: "running",
507
- step: args.plannedStep,
508
- });
509
- }
336
+ const recordPolicySkip = async (plannedStep, step, stepIndex, reason, value) => {
337
+ let attempt;
338
+ let published = value;
339
+ if (step.outputSchema) {
340
+ attempt = state.beginAttempt(plannedStep);
510
341
  try {
511
- published = await validateStandardSchema(args.step.outputSchema, published, `Pipeline ${compiled.id} step ${args.stepId} output`);
342
+ published = await validateStepOutput(step, value, `Pipeline ${compiled.id} step ${step.id} output`);
512
343
  }
513
344
  catch (error) {
514
- return recordStepExecutionFailure(error, args.plannedStep, args.step, args.stepId, args.stepIndex, attempt);
345
+ return recordStepExecutionFailure(error, plannedStep, stepIndex, attempt);
515
346
  }
516
347
  }
517
- outputs.set(args.stepId, published);
518
- if (args.kind === "complete") {
519
- const completeAttempt = attempt;
520
- const report = {
521
- attemptId: completeAttempt.attemptId,
522
- id: args.stepId,
523
- name: args.step.name,
524
- description: args.step.description,
525
- finishedAtMs: runtime.now(),
526
- startedAtMs: completeAttempt.startedAtMs,
527
- status: "completed",
528
- };
529
- reports.push(report);
530
- reportsByStepId.set(args.stepId, report);
531
- publishStepReport(args.plannedStep, report);
532
- return false;
533
- }
534
- const report = recordSkip(args.step, args.stepId, reports, reportsByStepId, "policy", args.skipMessage, undefined, attempt?.attemptId, attempt?.startedAtMs, runtime.now());
535
- publishStepReport(args.plannedStep, report);
348
+ state.skipStep(plannedStep, {
349
+ attempt,
350
+ message: reason,
351
+ output: { value: published },
352
+ reason: "policy",
353
+ });
536
354
  return false;
537
355
  };
538
356
  for (const [stepIndex, step] of orderedSteps.entries()) {
539
- const stepId = step.id;
540
- const graph = compiledStepGraph(compiled, step);
541
- const plannedStep = plannedSteps.get(stepId) ?? stepToPlanStep(step, true, undefined, undefined, graph);
542
- if (cancelBeforeStepStart(stepId, stepIndex)) {
357
+ const plannedStep = plannedStepFor(step);
358
+ if (cancelBeforeStepStart(step.id, stepIndex))
543
359
  break;
544
- }
360
+ const graph = compiledStepGraph(compiled, step);
545
361
  const disposition = decideStepDisposition({
546
362
  dryRun,
547
363
  graph,
548
364
  planned: plannedStep,
549
- reportsByStepId,
365
+ reportsByStepId: state.reportsByStepId,
550
366
  step,
551
367
  });
552
368
  if (disposition.kind === "skip") {
553
- const report = recordSkip(step, stepId, reports, reportsByStepId, disposition.reason, disposition.message, disposition.dependencyId, undefined, undefined, runtime.now());
554
- publishStepReport(plannedStep, report);
369
+ state.skipStep(plannedStep, disposition);
555
370
  continue;
556
371
  }
557
372
  const inputEntries = [];
558
- for (const dep of graph.dependsOn) {
559
- inputEntries.push([dep.id, outputs.get(dep.id)]);
560
- }
373
+ for (const dep of graph.dependsOn)
374
+ inputEntries.push([dep.id, state.outputs.get(dep.id)]);
561
375
  for (const dep of graph.optionalDependsOn) {
562
- if (outputs.has(dep.id)) {
563
- inputEntries.push([dep.id, outputs.get(dep.id)]);
564
- }
376
+ if (state.outputs.has(dep.id))
377
+ inputEntries.push([dep.id, state.outputs.get(dep.id)]);
565
378
  }
566
- const inputs = Object.fromEntries(inputEntries);
379
+ const stepInputs = Object.fromEntries(inputEntries);
567
380
  if (typeof step.skip === "function") {
568
381
  let skipDecision;
569
382
  try {
570
- skipDecision = normalizeStepSkipDecision(await step.skip(inputs, { ...executionContext, log: tracedLogger(stepId) }));
383
+ skipDecision = await evaluateStepSkip(step, stepInputs, {
384
+ ...executionContext,
385
+ log: tracedLogger(step.id),
386
+ });
571
387
  }
572
388
  catch (error) {
573
- const skipAttempt = beginAttempt(stepId, runtime.now());
574
- publishStepStatus({
575
- attemptId: skipAttempt.attemptId,
576
- pipelineId: compiled.id,
577
- status: "running",
578
- step: plannedStep,
579
- });
580
- if (recordStepExecutionFailure(error, plannedStep, step, stepId, stepIndex, skipAttempt)) {
389
+ const attempt = state.beginAttempt(plannedStep);
390
+ if (recordStepExecutionFailure(error, plannedStep, stepIndex, attempt))
581
391
  break;
582
- }
583
392
  continue;
584
393
  }
585
- if (cancelBeforeStepStart(stepId, stepIndex)) {
394
+ if (cancelBeforeStepStart(step.id, stepIndex))
586
395
  break;
587
- }
588
396
  if (skipDecision) {
589
- if (await recordPublishedStepValue({
590
- kind: "policy-skip",
591
- plannedStep,
592
- skipMessage: skipDecision.reason,
593
- step,
594
- stepId,
595
- stepIndex,
596
- value: skipDecision.value,
597
- })) {
397
+ if (await recordPolicySkip(plannedStep, step, stepIndex, skipDecision.reason, skipDecision.value)) {
598
398
  break;
599
399
  }
600
400
  continue;
601
401
  }
602
402
  }
603
- const stepStartedAt = runtime.now();
604
- const attempt = beginAttempt(stepId, stepStartedAt);
605
- publishStepStatus({
606
- attemptId: attempt.attemptId,
607
- pipelineId: compiled.id,
608
- status: "running",
609
- step: plannedStep,
610
- });
403
+ const attempt = state.beginAttempt(plannedStep);
611
404
  try {
612
- let acceptsProgress = true;
613
- const publishProgress = (progress) => {
614
- if (!acceptsProgress)
615
- return;
616
- publishStepStatus({
617
- attemptId: attempt.attemptId,
618
- pipelineId: compiled.id,
619
- progress,
620
- status: "running",
621
- step: plannedStep,
622
- });
623
- };
624
- const stepContext = {
625
- ...executionContext,
405
+ const output = await executeStepAttempt({
626
406
  attemptId: attempt.attemptId,
627
- log: tracedLogger(stepId, attempt.attemptId),
628
- reportAttempt: (attempt, attributes) => {
629
- lifecycle.reportAttempt(stepId, attempt, attributes, stepContext.attemptId);
630
- },
631
- reportProgress: (progress) => {
632
- publishProgress(progress);
633
- },
634
- };
635
- let output;
636
- try {
637
- output =
638
- dryRun && typeof step.dryRun === "function"
639
- ? await step.dryRun(inputs, stepContext)
640
- : await step.run(inputs, stepContext);
641
- }
642
- finally {
643
- acceptsProgress = false;
644
- }
645
- if (await recordPublishedStepValue({
646
- attempt,
647
- kind: "complete",
648
- plannedStep,
407
+ context: executionContext,
408
+ dryRun,
409
+ inputs: stepInputs,
410
+ log: tracedLogger(step.id, attempt.attemptId),
411
+ onProgress: (progress) => state.reportProgress(plannedStep, attempt, progress),
412
+ onReportAttempt: (number, attributes) => lifecycle.reportAttempt(step.id, number, attributes, attempt.attemptId),
413
+ outputBoundary: `Pipeline ${compiled.id} step ${step.id} output`,
649
414
  step,
650
- stepId,
651
- stepIndex,
652
- value: output,
653
- })) {
654
- break;
655
- }
415
+ });
416
+ state.completeStep(plannedStep, attempt, output);
656
417
  }
657
418
  catch (error) {
658
- if (recordStepExecutionFailure(error, plannedStep, step, stepId, stepIndex, attempt)) {
419
+ if (recordStepExecutionFailure(error, plannedStep, stepIndex, attempt))
659
420
  break;
660
- }
661
421
  }
662
422
  }
663
- let value;
664
- let finalized = false;
665
- if (completed || controls.continueOnError) {
423
+ if (state.errors.length === 0 || controls.continueOnError) {
666
424
  const finalizeStartedAt = runtime.now();
667
- lifecycle.finalizeStart();
425
+ state.beginFinalization();
668
426
  try {
669
427
  throwIfAborted(runtime);
670
- // SAFETY: `outputs` maps step ids to their produced values, which is
428
+ // SAFETY: `state.outputs` maps step ids to their produced values, which is
671
429
  // exactly the shape the finalizer's outputs parameter describes.
672
- const finalOutputs = Object.fromEntries(outputs);
430
+ const finalOutputs = Object.fromEntries(state.outputs);
673
431
  const finalizedValue = await compiled.finalize(finalOutputs, {
674
432
  ...executionContext,
675
433
  log: tracedLogger(PIPELINE_FINALIZE_STEP_ID),
@@ -677,11 +435,10 @@ export async function executePlannedRun(input) {
677
435
  // SAFETY: with a result schema the value is validated against
678
436
  // `TPipelineResult`; without one the finalizer's declared return type
679
437
  // is `TPipelineResult`, so the cast only restores that type.
680
- value = compiled.resultSchema
438
+ const value = compiled.resultSchema
681
439
  ? (await validateStandardSchema(compiled.resultSchema, finalizedValue, `Pipeline ${compiled.id} final result`))
682
440
  : finalizedValue;
683
- finalized = true;
684
- lifecycle.finalizeComplete(runtime.now() - finalizeStartedAt, value);
441
+ state.completeFinalization(value, runtime.now() - finalizeStartedAt);
685
442
  }
686
443
  catch (error) {
687
444
  const cancelled = isPipelineCancellation(error, runtime);
@@ -696,30 +453,10 @@ export async function executePlannedRun(input) {
696
453
  phase: "finalization",
697
454
  stepId: PIPELINE_FINALIZE_STEP_ID,
698
455
  });
699
- errors.push(pipelineError);
700
- lifecycle.finalizeError(pipelineError, runtime.now() - finalizeStartedAt);
701
- completed = false;
456
+ state.failFinalization(pipelineError, runtime.now() - finalizeStartedAt);
702
457
  }
703
458
  }
704
- const finishedAtMs = runtime.now();
705
- const result = {
706
- pipelineId: compiled.id,
707
- dryRun,
708
- errors,
709
- finalized,
710
- finishedAtMs,
711
- runId,
712
- startedAtMs: startedAt,
713
- status: terminalRunStatus(errors),
714
- steps: reports,
715
- value,
716
- version: RUN_MODEL_VERSION,
717
- };
718
- if (parentRunId)
719
- result.parentRunId = parentRunId;
720
- lifecycle.pipelineComplete(result);
721
- await lifecycle.flush();
722
- return result;
459
+ return state.finish();
723
460
  }
724
461
  const fallbackNow = () => Date.now();
725
462
  const fallbackSleep = (durationMs, signal) => abortableSleep(durationMs, signal, "Pipeline sleep");