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.
- package/README.md +5 -5
- package/dist/core/child-execution.js +8 -1
- package/dist/core/child-execution.js.map +1 -1
- package/dist/core/lifecycle.js +2 -0
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/pipeline-execute.d.ts +2 -0
- package/dist/core/pipeline-execute.js +132 -395
- package/dist/core/pipeline-execute.js.map +1 -1
- package/dist/core/pipeline-plan.js +96 -34
- package/dist/core/pipeline-plan.js.map +1 -1
- package/dist/core/pipeline-run-state.d.ts +49 -0
- package/dist/core/pipeline-run-state.js +284 -0
- package/dist/core/pipeline-run-state.js.map +1 -0
- package/dist/core/pipeline-step-executor.d.ts +20 -0
- package/dist/core/pipeline-step-executor.js +52 -0
- package/dist/core/pipeline-step-executor.js.map +1 -0
- package/dist/core/pipeline-types.d.ts +11 -2
- package/dist/core/pipeline-types.js.map +1 -1
- package/dist/core/pipeline-validation.d.ts +6 -0
- package/dist/core/pipeline-validation.js +41 -0
- package/dist/core/pipeline-validation.js.map +1 -0
- package/dist/run-store/run-store-ndjson.js +2 -295
- package/dist/run-store/run-store-ndjson.js.map +1 -1
- package/dist/run-store/run-store-sqlite.js +91 -42
- package/dist/run-store/run-store-sqlite.js.map +1 -1
- package/dist/run-store/run-store.d.ts +3 -2
- package/dist/run-store/run-store.js +38 -195
- package/dist/run-store/run-store.js.map +1 -1
- package/dist/studio/run-store-ui-client-controller.d.ts +9 -0
- package/dist/studio/run-store-ui-client-controller.js +479 -0
- package/dist/studio/run-store-ui-client-controller.js.map +1 -0
- package/dist/studio/run-store-ui-client-form.d.ts +9 -0
- package/dist/studio/run-store-ui-client-form.js +44 -0
- package/dist/studio/run-store-ui-client-form.js.map +1 -0
- package/dist/studio/run-store-ui-client-model.d.ts +40 -0
- package/dist/studio/run-store-ui-client-model.js +211 -0
- package/dist/studio/run-store-ui-client-model.js.map +1 -0
- package/dist/studio/run-store-ui-client-source.js +1 -1
- package/dist/studio/run-store-ui-client-transport.d.ts +25 -0
- package/dist/studio/run-store-ui-client-transport.js +322 -0
- package/dist/studio/run-store-ui-client-transport.js.map +1 -0
- package/dist/studio/run-store-ui-client-views.d.ts +24 -0
- package/dist/studio/run-store-ui-client-views.js +676 -0
- package/dist/studio/run-store-ui-client-views.js.map +1 -0
- package/dist/studio/run-store-ui-client.d.ts +2 -14
- package/dist/studio/run-store-ui-client.js +7 -1360
- package/dist/studio/run-store-ui-client.js.map +1 -1
- package/dist/studio/run-store-ui-page.d.ts +1 -1
- package/dist/studio/run-store-ui.js +44 -25
- package/dist/studio/run-store-ui.js.map +1 -1
- package/dist/tracing/tracing-codec.d.ts +6 -0
- package/dist/tracing/tracing-codec.js +829 -0
- package/dist/tracing/tracing-codec.js.map +1 -0
- package/dist/tracing/tracing-constants.d.ts +4 -0
- package/dist/tracing/tracing-constants.js +5 -0
- package/dist/tracing/tracing-constants.js.map +1 -0
- package/dist/tracing/tracing-internal.js +116 -81
- package/dist/tracing/tracing-internal.js.map +1 -1
- package/dist/tracing/tracing-otel.js +19 -3
- package/dist/tracing/tracing-otel.js.map +1 -1
- package/dist/tracing/tracing.d.ts +149 -10
- package/dist/tracing/tracing.js.map +1 -1
- package/dist/workbench/workbench-history.js +8 -1
- package/dist/workbench/workbench-history.js.map +1 -1
- package/dist/workbench/workbench-ui.js +26 -6
- package/dist/workbench/workbench-ui.js.map +1 -1
- package/docs/README.md +21 -15
- package/docs/agent-guide.md +48 -35
- package/docs/agent-skills.md +59 -0
- package/docs/api-reference.md +28 -12
- package/docs/api-report.json +29 -13
- package/docs/child-pipeline-composition.md +142 -193
- package/docs/cli.md +83 -61
- package/docs/comparison.md +63 -62
- package/docs/concepts.md +210 -272
- package/docs/getting-started.md +59 -48
- package/docs/llms.txt +7 -4
- package/docs/recipes.md +21 -18
- package/docs/remote-step-composition.md +69 -74
- package/docs/studio.md +146 -71
- package/examples/catalog/pipelines/enrich.ts +2 -2
- package/examples/catalog/tubeless.project.ts +4 -2
- package/examples/host-embedding.ts +4 -5
- package/examples/remote-steps.ts +8 -5
- package/package.json +11 -10
- package/skills/tubeless/SKILL.md +83 -0
- 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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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 (
|
|
273
|
-
identity.
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
368
|
-
|
|
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 (
|
|
384
|
-
executionContext.
|
|
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
|
-
// `
|
|
387
|
-
// objects so callers still get the typed
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
272
|
+
state.skipStep(plannedStep, {
|
|
273
|
+
reason: plannedStep.skipReason,
|
|
274
|
+
dependencyId,
|
|
275
|
+
});
|
|
403
276
|
}
|
|
404
|
-
if (cancellationError) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
498
|
-
let
|
|
499
|
-
let
|
|
500
|
-
if (
|
|
501
|
-
|
|
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
|
|
342
|
+
published = await validateStepOutput(step, value, `Pipeline ${compiled.id} step ${step.id} output`);
|
|
512
343
|
}
|
|
513
344
|
catch (error) {
|
|
514
|
-
return recordStepExecutionFailure(error,
|
|
345
|
+
return recordStepExecutionFailure(error, plannedStep, stepIndex, attempt);
|
|
515
346
|
}
|
|
516
347
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
540
|
-
|
|
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
|
-
|
|
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
|
|
379
|
+
const stepInputs = Object.fromEntries(inputEntries);
|
|
567
380
|
if (typeof step.skip === "function") {
|
|
568
381
|
let skipDecision;
|
|
569
382
|
try {
|
|
570
|
-
skipDecision =
|
|
383
|
+
skipDecision = await evaluateStepSkip(step, stepInputs, {
|
|
384
|
+
...executionContext,
|
|
385
|
+
log: tracedLogger(step.id),
|
|
386
|
+
});
|
|
571
387
|
}
|
|
572
388
|
catch (error) {
|
|
573
|
-
const
|
|
574
|
-
|
|
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(
|
|
394
|
+
if (cancelBeforeStepStart(step.id, stepIndex))
|
|
586
395
|
break;
|
|
587
|
-
}
|
|
588
396
|
if (skipDecision) {
|
|
589
|
-
if (await
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
651
|
-
|
|
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,
|
|
419
|
+
if (recordStepExecutionFailure(error, plannedStep, stepIndex, attempt))
|
|
659
420
|
break;
|
|
660
|
-
}
|
|
661
421
|
}
|
|
662
422
|
}
|
|
663
|
-
|
|
664
|
-
let finalized = false;
|
|
665
|
-
if (completed || controls.continueOnError) {
|
|
423
|
+
if (state.errors.length === 0 || controls.continueOnError) {
|
|
666
424
|
const finalizeStartedAt = runtime.now();
|
|
667
|
-
|
|
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
|
-
|
|
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
|
-
|
|
700
|
-
lifecycle.finalizeError(pipelineError, runtime.now() - finalizeStartedAt);
|
|
701
|
-
completed = false;
|
|
456
|
+
state.failFinalization(pipelineError, runtime.now() - finalizeStartedAt);
|
|
702
457
|
}
|
|
703
458
|
}
|
|
704
|
-
|
|
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");
|