@reaatech/media-pipeline-mcp-core 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +446 -0
- package/dist/index.cjs +2252 -0
- package/dist/index.d.cts +3173 -0
- package/dist/index.d.ts +3173 -0
- package/dist/index.js +2157 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2157 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/types/index.ts
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
var ArtifactTypeSchema = z.enum(["image", "video", "audio", "text", "document"]);
|
|
11
|
+
var ArtifactSchema = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
type: ArtifactTypeSchema,
|
|
14
|
+
uri: z.string(),
|
|
15
|
+
mimeType: z.string(),
|
|
16
|
+
metadata: z.record(z.unknown()).default({}),
|
|
17
|
+
sourceStep: z.string().optional(),
|
|
18
|
+
createdAt: z.string().datetime().optional()
|
|
19
|
+
});
|
|
20
|
+
var QualityGateActionSchema = z.enum(["fail", "retry", "warn"]);
|
|
21
|
+
var QualityGateSchema = z.object({
|
|
22
|
+
type: z.enum(["llm-judge", "threshold", "dimension-check", "custom"]),
|
|
23
|
+
config: z.record(z.unknown()),
|
|
24
|
+
action: QualityGateActionSchema,
|
|
25
|
+
maxRetries: z.number().int().min(0).optional()
|
|
26
|
+
});
|
|
27
|
+
var BudgetConfigSchema = z.object({
|
|
28
|
+
maxUsd: z.number().nonnegative(),
|
|
29
|
+
onExceed: z.enum(["abort", "suspend"]),
|
|
30
|
+
warnAtPct: z.number().min(0).max(1).optional()
|
|
31
|
+
});
|
|
32
|
+
var CacheConfigSchema = z.object({
|
|
33
|
+
mode: z.enum(["use", "refresh", "skip"]).optional(),
|
|
34
|
+
ttlSeconds: z.number().int().positive().optional(),
|
|
35
|
+
scope: z.enum(["global", "tenant"]).optional()
|
|
36
|
+
});
|
|
37
|
+
var RouteCandidateSchema = z.object({
|
|
38
|
+
provider: z.string(),
|
|
39
|
+
model: z.string(),
|
|
40
|
+
maxQueueMs: z.number().int().positive().optional(),
|
|
41
|
+
maxUsd: z.number().nonnegative().optional(),
|
|
42
|
+
inputOverrides: z.record(z.unknown()).optional(),
|
|
43
|
+
weight: z.number().nonnegative().optional()
|
|
44
|
+
});
|
|
45
|
+
var RouteConfigSchema = z.object({
|
|
46
|
+
strategy: z.enum(["first-success", "cheapest-acceptable", "fastest"]),
|
|
47
|
+
candidates: z.array(RouteCandidateSchema).min(1),
|
|
48
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
49
|
+
healthTtlMs: z.number().int().positive().optional()
|
|
50
|
+
});
|
|
51
|
+
var JudgeRubricSchema = z.object({
|
|
52
|
+
dimensions: z.array(
|
|
53
|
+
z.object({
|
|
54
|
+
name: z.string(),
|
|
55
|
+
weight: z.number().min(0).max(1),
|
|
56
|
+
description: z.string()
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
});
|
|
60
|
+
var JudgeConfigSchema = z.discriminatedUnion("type", [
|
|
61
|
+
z.object({
|
|
62
|
+
type: z.literal("llm-judge"),
|
|
63
|
+
criteria: z.string(),
|
|
64
|
+
model: z.string().optional(),
|
|
65
|
+
provider: z.string().optional(),
|
|
66
|
+
rubric: JudgeRubricSchema.optional()
|
|
67
|
+
}),
|
|
68
|
+
z.object({
|
|
69
|
+
type: z.literal("image-judge"),
|
|
70
|
+
criteria: z.enum(["clip-score", "aesthetic"]),
|
|
71
|
+
reference: z.string().optional()
|
|
72
|
+
}),
|
|
73
|
+
z.object({ type: z.literal("rule"), expression: z.string() }),
|
|
74
|
+
z.object({ type: z.literal("custom"), toolName: z.string() })
|
|
75
|
+
]);
|
|
76
|
+
var VariantsConfigSchema = z.object({
|
|
77
|
+
n: z.number().int().min(2).max(16),
|
|
78
|
+
seedStrategy: z.enum(["random", "sequential", "fixed-list"]).optional(),
|
|
79
|
+
seeds: z.array(z.number().int()).optional(),
|
|
80
|
+
judge: JudgeConfigSchema,
|
|
81
|
+
loserAction: z.enum(["archive", "discard"]).optional(),
|
|
82
|
+
perVariantCandidate: z.boolean().optional(),
|
|
83
|
+
minScore: z.number().min(0).max(1).optional()
|
|
84
|
+
});
|
|
85
|
+
var VoiceRefSchema = z.object({
|
|
86
|
+
provider: z.enum(["elevenlabs", "openai", "google", "deepgram-tts"]),
|
|
87
|
+
voiceId: z.string(),
|
|
88
|
+
settings: z.record(z.unknown()).optional()
|
|
89
|
+
});
|
|
90
|
+
var StyleRefSchema = z.object({
|
|
91
|
+
description: z.string(),
|
|
92
|
+
negative: z.string().optional(),
|
|
93
|
+
perProvider: z.record(z.object({ description: z.string().optional(), negative: z.string().optional() })).optional()
|
|
94
|
+
});
|
|
95
|
+
var BrandKitSchema = z.object({
|
|
96
|
+
primaryColor: z.string().optional(),
|
|
97
|
+
secondaryColor: z.string().optional(),
|
|
98
|
+
fontFamily: z.string().optional(),
|
|
99
|
+
logoArtifactId: z.string().optional(),
|
|
100
|
+
extras: z.record(z.unknown()).optional()
|
|
101
|
+
});
|
|
102
|
+
var ContextRefSchema = z.discriminatedUnion("kind", [
|
|
103
|
+
z.object({ kind: z.literal("voice"), name: z.string() }),
|
|
104
|
+
z.object({ kind: z.literal("style"), name: z.string() }),
|
|
105
|
+
z.object({ kind: z.literal("brand"), key: z.string() })
|
|
106
|
+
]);
|
|
107
|
+
var RunContextSchema = z.object({
|
|
108
|
+
voices: z.record(VoiceRefSchema).optional(),
|
|
109
|
+
styles: z.record(StyleRefSchema).optional(),
|
|
110
|
+
brandKit: BrandKitSchema.optional(),
|
|
111
|
+
vars: z.record(z.unknown()).optional()
|
|
112
|
+
});
|
|
113
|
+
var PipelineStepSchema = z.object({
|
|
114
|
+
id: z.string(),
|
|
115
|
+
operation: z.string(),
|
|
116
|
+
inputs: z.record(z.string()),
|
|
117
|
+
config: z.record(z.unknown()).default({}),
|
|
118
|
+
qualityGate: QualityGateSchema.optional(),
|
|
119
|
+
variants: VariantsConfigSchema.optional(),
|
|
120
|
+
gates: z.array(QualityGateSchema).optional(),
|
|
121
|
+
cache: CacheConfigSchema.optional(),
|
|
122
|
+
route: RouteConfigSchema.optional()
|
|
123
|
+
});
|
|
124
|
+
var PipelineStatusSchema = z.enum([
|
|
125
|
+
"pending",
|
|
126
|
+
"running",
|
|
127
|
+
"completed",
|
|
128
|
+
"failed",
|
|
129
|
+
"gated",
|
|
130
|
+
"cancelled"
|
|
131
|
+
]);
|
|
132
|
+
var PipelineSchema = z.object({
|
|
133
|
+
id: z.string(),
|
|
134
|
+
steps: z.array(PipelineStepSchema),
|
|
135
|
+
status: PipelineStatusSchema.default("pending"),
|
|
136
|
+
artifacts: z.map(z.string(), ArtifactSchema).default(/* @__PURE__ */ new Map()),
|
|
137
|
+
failedStep: z.string().optional(),
|
|
138
|
+
gatedStep: z.string().optional(),
|
|
139
|
+
currentStep: z.string().optional(),
|
|
140
|
+
completedSteps: z.array(z.string()).default([]),
|
|
141
|
+
startedAt: z.string().datetime().optional(),
|
|
142
|
+
completedAt: z.string().datetime().optional()
|
|
143
|
+
});
|
|
144
|
+
var PipelineDefinitionSchema = z.object({
|
|
145
|
+
id: z.string(),
|
|
146
|
+
steps: z.array(
|
|
147
|
+
z.object({
|
|
148
|
+
id: z.string(),
|
|
149
|
+
operation: z.string(),
|
|
150
|
+
inputs: z.record(z.string()),
|
|
151
|
+
config: z.record(z.unknown()).default({}),
|
|
152
|
+
qualityGate: QualityGateSchema.optional(),
|
|
153
|
+
variants: VariantsConfigSchema.optional(),
|
|
154
|
+
gates: z.array(QualityGateSchema).optional(),
|
|
155
|
+
cache: CacheConfigSchema.optional(),
|
|
156
|
+
route: RouteConfigSchema.optional()
|
|
157
|
+
})
|
|
158
|
+
),
|
|
159
|
+
resumable: z.boolean().default(true).optional(),
|
|
160
|
+
budget: BudgetConfigSchema.optional(),
|
|
161
|
+
context: RunContextSchema.optional()
|
|
162
|
+
});
|
|
163
|
+
var ProviderInputSchema = z.object({
|
|
164
|
+
operation: z.string(),
|
|
165
|
+
inputs: z.record(z.unknown()),
|
|
166
|
+
config: z.record(z.unknown()).optional()
|
|
167
|
+
});
|
|
168
|
+
var ProviderOutputSchema = z.object({
|
|
169
|
+
artifact: ArtifactSchema,
|
|
170
|
+
cost_usd: z.number().optional(),
|
|
171
|
+
duration_ms: z.number().optional(),
|
|
172
|
+
metadata: z.record(z.unknown()).optional()
|
|
173
|
+
});
|
|
174
|
+
var QualityGateResultSchema = z.object({
|
|
175
|
+
passed: z.boolean(),
|
|
176
|
+
reasoning: z.string().optional(),
|
|
177
|
+
score: z.number().optional(),
|
|
178
|
+
action: QualityGateActionSchema
|
|
179
|
+
});
|
|
180
|
+
var PipelineEventTypeSchema = z.enum([
|
|
181
|
+
// Legacy
|
|
182
|
+
"pipeline:start",
|
|
183
|
+
"pipeline:complete",
|
|
184
|
+
"pipeline:failed",
|
|
185
|
+
"pipeline:gated",
|
|
186
|
+
"step:start",
|
|
187
|
+
"step:complete",
|
|
188
|
+
"step:failed",
|
|
189
|
+
"step:gated",
|
|
190
|
+
"step:retry",
|
|
191
|
+
// Spec-canonical (§0.1)
|
|
192
|
+
"run-created",
|
|
193
|
+
"run-started",
|
|
194
|
+
"run-completed",
|
|
195
|
+
"run-failed",
|
|
196
|
+
"run-suspended",
|
|
197
|
+
"run-resumed",
|
|
198
|
+
"step-started",
|
|
199
|
+
"step-progress",
|
|
200
|
+
"step-completed",
|
|
201
|
+
"step-failed",
|
|
202
|
+
"step-cached",
|
|
203
|
+
"step-gated"
|
|
204
|
+
]);
|
|
205
|
+
var PipelineEventSchema = z.object({
|
|
206
|
+
type: PipelineEventTypeSchema,
|
|
207
|
+
pipelineId: z.string(),
|
|
208
|
+
stepId: z.string().optional(),
|
|
209
|
+
artifactId: z.string().optional(),
|
|
210
|
+
timestamp: z.string().datetime(),
|
|
211
|
+
data: z.record(z.unknown()).optional()
|
|
212
|
+
});
|
|
213
|
+
var CostRecordSchema = z.object({
|
|
214
|
+
operation: z.string(),
|
|
215
|
+
provider: z.string(),
|
|
216
|
+
model: z.string().optional(),
|
|
217
|
+
cost_usd: z.number(),
|
|
218
|
+
artifactId: z.string().optional(),
|
|
219
|
+
pipelineId: z.string().optional(),
|
|
220
|
+
timestamp: z.string().datetime()
|
|
221
|
+
});
|
|
222
|
+
var CostSummarySchema = z.object({
|
|
223
|
+
total_usd: z.number(),
|
|
224
|
+
by_operation: z.map(z.string(), z.number()).default(/* @__PURE__ */ new Map()),
|
|
225
|
+
by_provider: z.map(z.string(), z.number()).default(/* @__PURE__ */ new Map()),
|
|
226
|
+
by_pipeline: z.map(z.string(), z.number()).default(/* @__PURE__ */ new Map())
|
|
227
|
+
});
|
|
228
|
+
var ArtifactMetaSchema = z.object({
|
|
229
|
+
id: z.string(),
|
|
230
|
+
type: ArtifactTypeSchema,
|
|
231
|
+
mimeType: z.string(),
|
|
232
|
+
size: z.number().optional(),
|
|
233
|
+
metadata: z.record(z.unknown()).optional(),
|
|
234
|
+
createdAt: z.string().datetime().optional(),
|
|
235
|
+
sourceStep: z.string().optional()
|
|
236
|
+
});
|
|
237
|
+
var StorageResultSchema = z.object({
|
|
238
|
+
data: z.instanceof(ReadableStream).or(z.instanceof(Buffer)),
|
|
239
|
+
meta: ArtifactMetaSchema
|
|
240
|
+
});
|
|
241
|
+
var ValidationResultSchema = z.object({
|
|
242
|
+
valid: z.boolean(),
|
|
243
|
+
errors: z.array(z.string()).default([]),
|
|
244
|
+
warnings: z.array(z.string()).default([]),
|
|
245
|
+
estimated_cost_usd: z.number().optional(),
|
|
246
|
+
estimated_duration_ms: z.number().optional()
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// src/artifact-registry.ts
|
|
250
|
+
import { v4 as uuidv4 } from "uuid";
|
|
251
|
+
var ArtifactRegistry = class {
|
|
252
|
+
artifacts = /* @__PURE__ */ new Map();
|
|
253
|
+
register(artifact) {
|
|
254
|
+
return this.registerWithId(uuidv4(), artifact);
|
|
255
|
+
}
|
|
256
|
+
registerWithId(id, artifact) {
|
|
257
|
+
const fullArtifact = {
|
|
258
|
+
...artifact,
|
|
259
|
+
id,
|
|
260
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
261
|
+
};
|
|
262
|
+
this.artifacts.set(id, fullArtifact);
|
|
263
|
+
return fullArtifact;
|
|
264
|
+
}
|
|
265
|
+
get(id) {
|
|
266
|
+
return this.artifacts.get(id);
|
|
267
|
+
}
|
|
268
|
+
delete(id) {
|
|
269
|
+
return this.artifacts.delete(id);
|
|
270
|
+
}
|
|
271
|
+
list() {
|
|
272
|
+
return Array.from(this.artifacts.values());
|
|
273
|
+
}
|
|
274
|
+
findBySourceStep(stepId) {
|
|
275
|
+
let latest;
|
|
276
|
+
for (const artifact of this.artifacts.values()) {
|
|
277
|
+
if (artifact.sourceStep === stepId) {
|
|
278
|
+
if (!latest || artifact.createdAt && latest.createdAt && artifact.createdAt > latest.createdAt) {
|
|
279
|
+
latest = artifact;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return latest;
|
|
284
|
+
}
|
|
285
|
+
deleteBySourceStep(stepId) {
|
|
286
|
+
let deleted = 0;
|
|
287
|
+
for (const [id, artifact] of this.artifacts.entries()) {
|
|
288
|
+
if (artifact.sourceStep === stepId) {
|
|
289
|
+
this.artifacts.delete(id);
|
|
290
|
+
deleted++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return deleted;
|
|
294
|
+
}
|
|
295
|
+
clear() {
|
|
296
|
+
this.artifacts.clear();
|
|
297
|
+
}
|
|
298
|
+
size() {
|
|
299
|
+
return this.artifacts.size;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// src/quality-gates/evaluator.ts
|
|
304
|
+
var ThresholdEvaluator = class {
|
|
305
|
+
async evaluate(gate, artifact) {
|
|
306
|
+
const checks = gate.config.checks;
|
|
307
|
+
if (!Array.isArray(checks)) {
|
|
308
|
+
return {
|
|
309
|
+
passed: false,
|
|
310
|
+
reasoning: "Invalid threshold configuration: checks must be an array",
|
|
311
|
+
action: gate.action
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const failures = [];
|
|
315
|
+
for (const check of checks) {
|
|
316
|
+
const { field, operator, value } = check;
|
|
317
|
+
const actual = field.startsWith("metadata.") ? this.getNestedValue({ metadata: artifact.metadata }, field) : this.getNestedValue(artifact.metadata, field);
|
|
318
|
+
if (actual === void 0) {
|
|
319
|
+
failures.push(`Field '${field}' not found in artifact metadata`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const numActual = Number(actual);
|
|
323
|
+
if (Number.isNaN(numActual)) {
|
|
324
|
+
failures.push(`Field '${field}' is not numeric: ${actual}`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const passed = this.compare(numActual, operator, value);
|
|
328
|
+
if (!passed) {
|
|
329
|
+
failures.push(`${field} (${numActual}) ${operator} ${value} failed`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
passed: failures.length === 0,
|
|
334
|
+
reasoning: failures.length > 0 ? failures.join("; ") : "All checks passed",
|
|
335
|
+
action: gate.action
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
getNestedValue(obj, path) {
|
|
339
|
+
return path.split(".").reduce((current, key) => {
|
|
340
|
+
if (current === null || current === void 0) return void 0;
|
|
341
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") return void 0;
|
|
342
|
+
if (typeof current === "object") {
|
|
343
|
+
return current[key];
|
|
344
|
+
}
|
|
345
|
+
return void 0;
|
|
346
|
+
}, obj);
|
|
347
|
+
}
|
|
348
|
+
compare(actual, operator, expected) {
|
|
349
|
+
switch (operator) {
|
|
350
|
+
case ">=":
|
|
351
|
+
return actual >= expected;
|
|
352
|
+
case "<=":
|
|
353
|
+
return actual <= expected;
|
|
354
|
+
case ">":
|
|
355
|
+
return actual > expected;
|
|
356
|
+
case "<":
|
|
357
|
+
return actual < expected;
|
|
358
|
+
case "==":
|
|
359
|
+
case "===":
|
|
360
|
+
return actual === expected;
|
|
361
|
+
case "!=":
|
|
362
|
+
return actual !== expected;
|
|
363
|
+
default:
|
|
364
|
+
throw new Error(`Unknown operator: ${operator}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
var DimensionCheckEvaluator = class {
|
|
369
|
+
async evaluate(gate, artifact) {
|
|
370
|
+
const { expectedWidth, expectedHeight, tolerance = 0 } = gate.config;
|
|
371
|
+
const width = expectedWidth;
|
|
372
|
+
const height = expectedHeight;
|
|
373
|
+
const tol = tolerance;
|
|
374
|
+
if (typeof width !== "number" || typeof height !== "number") {
|
|
375
|
+
return {
|
|
376
|
+
passed: false,
|
|
377
|
+
reasoning: "Invalid dimension-check configuration: expectedWidth and expectedHeight must be numbers",
|
|
378
|
+
action: gate.action
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const actualWidth = artifact.metadata.width;
|
|
382
|
+
const actualHeight = artifact.metadata.height;
|
|
383
|
+
if (actualWidth === void 0 || actualHeight === void 0) {
|
|
384
|
+
return {
|
|
385
|
+
passed: false,
|
|
386
|
+
reasoning: "Artifact missing width or height metadata",
|
|
387
|
+
action: gate.action
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const widthDiff = Math.abs(actualWidth - width);
|
|
391
|
+
const heightDiff = Math.abs(actualHeight - height);
|
|
392
|
+
const widthTolerance = width * tol;
|
|
393
|
+
const heightTolerance = height * tol;
|
|
394
|
+
const widthOk = widthDiff <= widthTolerance;
|
|
395
|
+
const heightOk = heightDiff <= heightTolerance;
|
|
396
|
+
if (widthOk && heightOk) {
|
|
397
|
+
return {
|
|
398
|
+
passed: true,
|
|
399
|
+
reasoning: `Dimensions ${actualWidth}x${actualHeight} match expected ${width}x${height} within ${tol * 100}% tolerance`,
|
|
400
|
+
action: gate.action
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
passed: false,
|
|
405
|
+
reasoning: `Dimensions ${actualWidth}x${actualHeight} do not match expected ${width}x${height} within ${tol * 100}% tolerance`,
|
|
406
|
+
action: gate.action
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
var LLMJudgeEvaluator = class {
|
|
411
|
+
evaluateFn;
|
|
412
|
+
constructor(evaluateFn) {
|
|
413
|
+
this.evaluateFn = evaluateFn;
|
|
414
|
+
}
|
|
415
|
+
async evaluate(gate, artifact) {
|
|
416
|
+
const { prompt, timeout } = gate.config;
|
|
417
|
+
if (typeof prompt !== "string") {
|
|
418
|
+
return {
|
|
419
|
+
passed: false,
|
|
420
|
+
reasoning: "Invalid LLM-judge configuration: prompt must be a string",
|
|
421
|
+
action: gate.action
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const timeoutMs = typeof timeout === "number" ? timeout : 3e4;
|
|
425
|
+
try {
|
|
426
|
+
let timeoutId;
|
|
427
|
+
const result = await Promise.race([
|
|
428
|
+
this.evaluateFn(prompt, artifact),
|
|
429
|
+
new Promise((_, reject) => {
|
|
430
|
+
timeoutId = setTimeout(
|
|
431
|
+
() => reject(new Error(`LLM-judge timeout after ${timeoutMs}ms`)),
|
|
432
|
+
timeoutMs
|
|
433
|
+
);
|
|
434
|
+
})
|
|
435
|
+
]);
|
|
436
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
437
|
+
return {
|
|
438
|
+
passed: result.pass,
|
|
439
|
+
reasoning: result.reasoning,
|
|
440
|
+
score: result.score,
|
|
441
|
+
action: gate.action
|
|
442
|
+
};
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return {
|
|
445
|
+
passed: false,
|
|
446
|
+
reasoning: `LLM-judge evaluation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
447
|
+
action: gate.action
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
var CustomEvaluator = class {
|
|
453
|
+
checkFn;
|
|
454
|
+
constructor(checkFn) {
|
|
455
|
+
this.checkFn = checkFn;
|
|
456
|
+
}
|
|
457
|
+
async evaluate(gate, artifact) {
|
|
458
|
+
try {
|
|
459
|
+
const passed = await this.checkFn(artifact, gate.config);
|
|
460
|
+
return {
|
|
461
|
+
passed,
|
|
462
|
+
reasoning: passed ? "Custom check passed" : "Custom check failed",
|
|
463
|
+
action: gate.action
|
|
464
|
+
};
|
|
465
|
+
} catch (error) {
|
|
466
|
+
return {
|
|
467
|
+
passed: false,
|
|
468
|
+
reasoning: `Custom check error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
469
|
+
action: gate.action
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
function createQualityGateEvaluator(gate, llmJudgeFn, customCheckFn) {
|
|
475
|
+
switch (gate.type) {
|
|
476
|
+
case "threshold":
|
|
477
|
+
return new ThresholdEvaluator();
|
|
478
|
+
case "dimension-check":
|
|
479
|
+
return new DimensionCheckEvaluator();
|
|
480
|
+
case "llm-judge":
|
|
481
|
+
if (!llmJudgeFn) {
|
|
482
|
+
throw new Error("LLM-judge evaluator requires an evaluate function");
|
|
483
|
+
}
|
|
484
|
+
return new LLMJudgeEvaluator(llmJudgeFn);
|
|
485
|
+
case "custom":
|
|
486
|
+
if (!customCheckFn) {
|
|
487
|
+
throw new Error("Custom evaluator requires a check function to be provided");
|
|
488
|
+
}
|
|
489
|
+
return new CustomEvaluator(customCheckFn);
|
|
490
|
+
default:
|
|
491
|
+
throw new Error(`Unknown quality gate type: ${gate.type}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/pipeline-validator.ts
|
|
496
|
+
var PipelineValidator = class {
|
|
497
|
+
providerAvailability;
|
|
498
|
+
constructor(providerAvailability) {
|
|
499
|
+
this.providerAvailability = providerAvailability;
|
|
500
|
+
}
|
|
501
|
+
validate(definition) {
|
|
502
|
+
const errors = [];
|
|
503
|
+
const warnings = [];
|
|
504
|
+
const schemaResult = PipelineDefinitionSchema.safeParse(definition);
|
|
505
|
+
if (!schemaResult.success) {
|
|
506
|
+
errors.push("Pipeline definition schema validation failed");
|
|
507
|
+
for (const issue of schemaResult.error.issues) {
|
|
508
|
+
errors.push(` - ${issue.path.join(".")}: ${issue.message}`);
|
|
509
|
+
}
|
|
510
|
+
return { valid: false, errors, warnings };
|
|
511
|
+
}
|
|
512
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
513
|
+
for (const step of definition.steps) {
|
|
514
|
+
if (stepIds.has(step.id)) {
|
|
515
|
+
errors.push(`Duplicate step ID: ${step.id}`);
|
|
516
|
+
}
|
|
517
|
+
if (step.id.includes("..") || step.id.includes("/") || step.id.includes("\\")) {
|
|
518
|
+
errors.push(`Invalid step ID '${step.id}': contains path traversal characters`);
|
|
519
|
+
}
|
|
520
|
+
stepIds.add(step.id);
|
|
521
|
+
}
|
|
522
|
+
const referenceErrors = this.validateReferences(definition);
|
|
523
|
+
errors.push(...referenceErrors);
|
|
524
|
+
const providerErrors = this.validateProviders(definition);
|
|
525
|
+
errors.push(...providerErrors);
|
|
526
|
+
const gateWarnings = this.validateQualityGates(definition);
|
|
527
|
+
warnings.push(...gateWarnings);
|
|
528
|
+
let estimatedCost = 0;
|
|
529
|
+
let estimatedDuration = 0;
|
|
530
|
+
for (const step of definition.steps) {
|
|
531
|
+
if (this.providerAvailability.isAvailable(step.operation)) {
|
|
532
|
+
estimatedCost += this.providerAvailability.getEstimatedCost(step.operation, step.config);
|
|
533
|
+
estimatedDuration += this.providerAvailability.getEstimatedDuration(
|
|
534
|
+
step.operation,
|
|
535
|
+
step.config
|
|
536
|
+
);
|
|
537
|
+
} else {
|
|
538
|
+
warnings.push(`Provider not available for operation: ${step.operation}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
valid: errors.length === 0,
|
|
543
|
+
errors,
|
|
544
|
+
warnings,
|
|
545
|
+
estimated_cost_usd: estimatedCost,
|
|
546
|
+
estimated_duration_ms: estimatedDuration
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
validateReferences(definition) {
|
|
550
|
+
const errors = [];
|
|
551
|
+
const definedStepIds = new Set(definition.steps.map((s) => s.id));
|
|
552
|
+
const outputReferences = /* @__PURE__ */ new Map();
|
|
553
|
+
for (const step of definition.steps) {
|
|
554
|
+
for (const [paramName, inputValue] of Object.entries(step.inputs)) {
|
|
555
|
+
const match = inputValue.match(/^\{\{(\w+)\.output\}\}$/);
|
|
556
|
+
if (match) {
|
|
557
|
+
const referencedStepId = match[1];
|
|
558
|
+
if (!definedStepIds.has(referencedStepId)) {
|
|
559
|
+
errors.push(
|
|
560
|
+
`Step '${step.id}' references non-existent step '${referencedStepId}' in input '${paramName}'`
|
|
561
|
+
);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const referencedIndex = definition.steps.findIndex((s) => s.id === referencedStepId);
|
|
565
|
+
const currentIndex = definition.steps.findIndex((s) => s.id === step.id);
|
|
566
|
+
if (referencedIndex >= currentIndex) {
|
|
567
|
+
errors.push(
|
|
568
|
+
`Step '${step.id}' references future step '${referencedStepId}' (circular/forward reference not allowed)`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
if (!outputReferences.has(referencedStepId)) {
|
|
572
|
+
outputReferences.set(referencedStepId, step.id);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return errors;
|
|
578
|
+
}
|
|
579
|
+
validateProviders(definition) {
|
|
580
|
+
const errors = [];
|
|
581
|
+
for (const step of definition.steps) {
|
|
582
|
+
if (!this.providerAvailability.isAvailable(step.operation)) {
|
|
583
|
+
errors.push(`No provider available for operation '${step.operation}' in step '${step.id}'`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return errors;
|
|
587
|
+
}
|
|
588
|
+
validateQualityGates(definition) {
|
|
589
|
+
const warnings = [];
|
|
590
|
+
for (const step of definition.steps) {
|
|
591
|
+
if (step.qualityGate) {
|
|
592
|
+
const gate = step.qualityGate;
|
|
593
|
+
if (gate.action === "retry" && gate.maxRetries === void 0) {
|
|
594
|
+
warnings.push(
|
|
595
|
+
`Step '${step.id}' has retry action but no maxRetries specified (defaulting to 1)`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
if (gate.type === "llm-judge" && !gate.config.prompt) {
|
|
599
|
+
warnings.push(`Step '${step.id}' has llm-judge gate without a prompt`);
|
|
600
|
+
}
|
|
601
|
+
if (gate.type === "threshold" && !Array.isArray(gate.config.checks)) {
|
|
602
|
+
warnings.push(`Step '${step.id}' has threshold gate without checks configured`);
|
|
603
|
+
}
|
|
604
|
+
if (gate.type === "dimension-check" && (gate.config.expectedWidth === void 0 || gate.config.expectedHeight === void 0)) {
|
|
605
|
+
warnings.push(`Step '${step.id}' has dimension-check gate without expected dimensions`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const stepExt = step;
|
|
609
|
+
if (stepExt.gates && Array.isArray(stepExt.gates)) {
|
|
610
|
+
for (let gi = 0; gi < stepExt.gates.length; gi++) {
|
|
611
|
+
const gate = stepExt.gates[gi];
|
|
612
|
+
if (gate.action === "retry" && gate.maxRetries === void 0) {
|
|
613
|
+
warnings.push(
|
|
614
|
+
`Step '${step.id}' gate[${gi}] has retry action but no maxRetries specified`
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
this.validateBudgetConfig(definition, warnings);
|
|
621
|
+
this.validateVariantsConfig(definition, warnings);
|
|
622
|
+
this.validateRunContext(definition, warnings);
|
|
623
|
+
return warnings;
|
|
624
|
+
}
|
|
625
|
+
validateBudgetConfig(definition, warnings) {
|
|
626
|
+
const budget = definition.budget;
|
|
627
|
+
if (budget) {
|
|
628
|
+
if (budget.maxUsd !== void 0 && budget.maxUsd <= 0) {
|
|
629
|
+
warnings.push("Budget maxUsd must be greater than 0");
|
|
630
|
+
}
|
|
631
|
+
if (budget.onExceed && !["abort", "suspend"].includes(budget.onExceed)) {
|
|
632
|
+
warnings.push(`Budget onExceed must be 'abort' or 'suspend', got '${budget.onExceed}'`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
validateVariantsConfig(definition, warnings) {
|
|
637
|
+
for (const step of definition.steps) {
|
|
638
|
+
const variants = step.variants;
|
|
639
|
+
if (variants) {
|
|
640
|
+
if (variants.n !== void 0 && (variants.n < 2 || variants.n > 16)) {
|
|
641
|
+
warnings.push(`Step '${step.id}' variants.n must be between 2 and 16, got ${variants.n}`);
|
|
642
|
+
}
|
|
643
|
+
if (variants.seedStrategy !== void 0 && !["random", "sequential", "fixed-list"].includes(variants.seedStrategy)) {
|
|
644
|
+
warnings.push(
|
|
645
|
+
`Step '${step.id}' variants.seedStrategy must be one of random, sequential, fixed-list`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (!variants.judge) {
|
|
649
|
+
warnings.push(`Step '${step.id}' has variants without a judge configuration`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
validateRunContext(definition, warnings) {
|
|
655
|
+
const context = definition.context;
|
|
656
|
+
if (context) {
|
|
657
|
+
if (context.voices) {
|
|
658
|
+
for (const [name, ref] of Object.entries(context.voices)) {
|
|
659
|
+
const voiceRef = ref;
|
|
660
|
+
if (voiceRef.provider && !["elevenlabs", "openai", "google", "deepgram-tts"].includes(voiceRef.provider)) {
|
|
661
|
+
warnings.push(`Context voice '${name}' has unknown provider '${voiceRef.provider}'`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (context.styles) {
|
|
666
|
+
for (const [name] of Object.entries(context.styles)) {
|
|
667
|
+
if (name.includes("..") || name.includes("/")) {
|
|
668
|
+
warnings.push(`Context style name '${name}' contains invalid characters`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// src/pipeline-executor.ts
|
|
677
|
+
import { randomUUID } from "crypto";
|
|
678
|
+
|
|
679
|
+
// src/errors.ts
|
|
680
|
+
var A2AError = class extends Error {
|
|
681
|
+
code;
|
|
682
|
+
retryable;
|
|
683
|
+
constructor(message) {
|
|
684
|
+
super(message);
|
|
685
|
+
this.name = "A2AError";
|
|
686
|
+
this.code = "A2A_ERROR";
|
|
687
|
+
this.retryable = false;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
var IdempotencyConflictError = class extends A2AError {
|
|
691
|
+
constructor(reason, existingRunId) {
|
|
692
|
+
super(
|
|
693
|
+
`Idempotency conflict: ${reason}${existingRunId ? ` (existing run: ${existingRunId})` : ""}`
|
|
694
|
+
);
|
|
695
|
+
this.reason = reason;
|
|
696
|
+
this.existingRunId = existingRunId;
|
|
697
|
+
}
|
|
698
|
+
reason;
|
|
699
|
+
existingRunId;
|
|
700
|
+
code = "IDEMPOTENCY_CONFLICT";
|
|
701
|
+
retryable = false;
|
|
702
|
+
};
|
|
703
|
+
var BudgetExceededError = class extends A2AError {
|
|
704
|
+
constructor(spentUsd, capUsd, scope) {
|
|
705
|
+
super(`Budget exceeded: $${spentUsd} spent, $${capUsd} cap (${scope})`);
|
|
706
|
+
this.spentUsd = spentUsd;
|
|
707
|
+
this.capUsd = capUsd;
|
|
708
|
+
this.scope = scope;
|
|
709
|
+
}
|
|
710
|
+
spentUsd;
|
|
711
|
+
capUsd;
|
|
712
|
+
scope;
|
|
713
|
+
code = "BUDGET_EXCEEDED";
|
|
714
|
+
retryable = false;
|
|
715
|
+
};
|
|
716
|
+
var RunNotFoundError = class extends A2AError {
|
|
717
|
+
code = "RUN_NOT_FOUND";
|
|
718
|
+
retryable = false;
|
|
719
|
+
constructor() {
|
|
720
|
+
super("Run not found");
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
var RunInProgressError = class extends A2AError {
|
|
724
|
+
code = "RUN_IN_PROGRESS";
|
|
725
|
+
retryable = true;
|
|
726
|
+
constructor() {
|
|
727
|
+
super("Run is already in progress");
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
var RunNotResumableError = class extends A2AError {
|
|
731
|
+
code = "RUN_NOT_RESUMABLE";
|
|
732
|
+
retryable = false;
|
|
733
|
+
constructor() {
|
|
734
|
+
super("Run is not resumable");
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
var WebhookSignatureInvalidError = class extends A2AError {
|
|
738
|
+
code = "WEBHOOK_SIGNATURE_INVALID";
|
|
739
|
+
retryable = false;
|
|
740
|
+
constructor() {
|
|
741
|
+
super("Webhook signature is invalid");
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
var WebhookProviderUnknownError = class extends A2AError {
|
|
745
|
+
code = "WEBHOOK_PROVIDER_UNKNOWN";
|
|
746
|
+
retryable = false;
|
|
747
|
+
constructor() {
|
|
748
|
+
super("Webhook provider is unknown");
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
var StateStoreUnavailableError = class extends A2AError {
|
|
752
|
+
code = "STATE_STORE_UNAVAILABLE";
|
|
753
|
+
retryable = true;
|
|
754
|
+
constructor() {
|
|
755
|
+
super("State store is unavailable");
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
var EstimateUnsupportedError = class extends A2AError {
|
|
759
|
+
code = "ESTIMATE_UNSUPPORTED";
|
|
760
|
+
retryable = false;
|
|
761
|
+
constructor() {
|
|
762
|
+
super("Cost estimation is not supported for this operation");
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
var ArtifactNotFoundError = class extends A2AError {
|
|
766
|
+
code = "ARTIFACT_NOT_FOUND";
|
|
767
|
+
retryable = false;
|
|
768
|
+
constructor() {
|
|
769
|
+
super("Artifact not found");
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
var RouterAllCandidatesFailedError = class extends A2AError {
|
|
773
|
+
constructor(attemptedCandidates, lastError) {
|
|
774
|
+
super(`All router candidates failed: ${attemptedCandidates.join(", ")}`);
|
|
775
|
+
this.attemptedCandidates = attemptedCandidates;
|
|
776
|
+
this.lastError = lastError;
|
|
777
|
+
}
|
|
778
|
+
attemptedCandidates;
|
|
779
|
+
lastError;
|
|
780
|
+
code = "ROUTER_ALL_CANDIDATES_FAILED";
|
|
781
|
+
retryable = false;
|
|
782
|
+
};
|
|
783
|
+
var RouterNoCandidatesError = class extends A2AError {
|
|
784
|
+
code = "ROUTER_NO_CANDIDATES";
|
|
785
|
+
retryable = false;
|
|
786
|
+
constructor() {
|
|
787
|
+
super("No eligible router candidates found");
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
var RouterFastestIneligibleError = class extends A2AError {
|
|
791
|
+
code = "ROUTER_FASTEST_INELIGIBLE";
|
|
792
|
+
retryable = false;
|
|
793
|
+
constructor() {
|
|
794
|
+
super("Fastest router candidate is ineligible");
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
var SafetyGateRejectedError = class extends A2AError {
|
|
798
|
+
constructor(category, score) {
|
|
799
|
+
super(`Safety gate rejected: ${category} (score: ${score})`);
|
|
800
|
+
this.category = category;
|
|
801
|
+
this.score = score;
|
|
802
|
+
}
|
|
803
|
+
category;
|
|
804
|
+
score;
|
|
805
|
+
code = "SAFETY_GATE_REJECTED";
|
|
806
|
+
retryable = false;
|
|
807
|
+
};
|
|
808
|
+
var TenantNotFoundError = class extends A2AError {
|
|
809
|
+
code = "TENANT_NOT_FOUND";
|
|
810
|
+
retryable = false;
|
|
811
|
+
constructor() {
|
|
812
|
+
super("Tenant not found");
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
var KeyVaultUnavailableError = class extends A2AError {
|
|
816
|
+
code = "KEY_VAULT_UNAVAILABLE";
|
|
817
|
+
retryable = true;
|
|
818
|
+
constructor() {
|
|
819
|
+
super("Key vault is unavailable");
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
var FfmpegUnavailableError = class extends A2AError {
|
|
823
|
+
code = "FFMPEG_UNAVAILABLE";
|
|
824
|
+
retryable = false;
|
|
825
|
+
};
|
|
826
|
+
var VariantsAllRejectedError = class extends A2AError {
|
|
827
|
+
constructor(reason) {
|
|
828
|
+
super();
|
|
829
|
+
this.reason = reason;
|
|
830
|
+
}
|
|
831
|
+
reason;
|
|
832
|
+
code = "VARIANTS_ALL_REJECTED";
|
|
833
|
+
retryable = false;
|
|
834
|
+
};
|
|
835
|
+
var JudgeUnavailableError = class extends A2AError {
|
|
836
|
+
code = "JUDGE_UNAVAILABLE";
|
|
837
|
+
retryable = true;
|
|
838
|
+
};
|
|
839
|
+
var WorkflowNotFoundError = class extends A2AError {
|
|
840
|
+
constructor(workflowName) {
|
|
841
|
+
super(workflowName ? `Workflow not found: ${workflowName}` : "Workflow not found");
|
|
842
|
+
this.workflowName = workflowName;
|
|
843
|
+
}
|
|
844
|
+
workflowName;
|
|
845
|
+
code = "WORKFLOW_NOT_FOUND";
|
|
846
|
+
retryable = false;
|
|
847
|
+
};
|
|
848
|
+
var WorkflowExpiredError = class extends A2AError {
|
|
849
|
+
constructor(detail) {
|
|
850
|
+
super(
|
|
851
|
+
`Workflow expired${detail ? `: ${detail}` : " \u2014 did not complete within retention period"}`
|
|
852
|
+
);
|
|
853
|
+
this.detail = detail;
|
|
854
|
+
}
|
|
855
|
+
detail;
|
|
856
|
+
code = "WORKFLOW_EXPIRED";
|
|
857
|
+
retryable = false;
|
|
858
|
+
};
|
|
859
|
+
var ContextRefUnknownError = class extends A2AError {
|
|
860
|
+
constructor(kind, name) {
|
|
861
|
+
super();
|
|
862
|
+
this.kind = kind;
|
|
863
|
+
this.name = name;
|
|
864
|
+
}
|
|
865
|
+
kind;
|
|
866
|
+
name;
|
|
867
|
+
code = "CONTEXT_REF_UNKNOWN";
|
|
868
|
+
retryable = false;
|
|
869
|
+
};
|
|
870
|
+
var ContextRefTypeError = class extends A2AError {
|
|
871
|
+
constructor(stepOp, refKind) {
|
|
872
|
+
super();
|
|
873
|
+
this.stepOp = stepOp;
|
|
874
|
+
this.refKind = refKind;
|
|
875
|
+
}
|
|
876
|
+
stepOp;
|
|
877
|
+
refKind;
|
|
878
|
+
code = "CONTEXT_REF_TYPE_MISMATCH";
|
|
879
|
+
retryable = false;
|
|
880
|
+
};
|
|
881
|
+
var LoudnessGateFailedError = class extends A2AError {
|
|
882
|
+
code = "LOUDNESS_GATE_FAILED";
|
|
883
|
+
retryable = false;
|
|
884
|
+
};
|
|
885
|
+
var TenantPolicyViolationError = class extends A2AError {
|
|
886
|
+
code = "TENANT_POLICY_VIOLATION";
|
|
887
|
+
retryable = false;
|
|
888
|
+
};
|
|
889
|
+
var ProvenanceSigningFailedError = class extends A2AError {
|
|
890
|
+
code = "PROVENANCE_SIGNING_FAILED";
|
|
891
|
+
retryable = false;
|
|
892
|
+
};
|
|
893
|
+
var SafetyProviderUnavailableError = class extends A2AError {
|
|
894
|
+
code = "SAFETY_PROVIDER_UNAVAILABLE";
|
|
895
|
+
retryable = true;
|
|
896
|
+
};
|
|
897
|
+
var RatioUnsupportedError = class extends A2AError {
|
|
898
|
+
constructor(ratio, provider) {
|
|
899
|
+
super(`Ratio ${ratio} not natively supported by provider ${provider}`);
|
|
900
|
+
this.ratio = ratio;
|
|
901
|
+
this.provider = provider;
|
|
902
|
+
}
|
|
903
|
+
ratio;
|
|
904
|
+
provider;
|
|
905
|
+
code = "RATIO_UNSUPPORTED";
|
|
906
|
+
retryable = false;
|
|
907
|
+
};
|
|
908
|
+
var InvalidInputError = class extends A2AError {
|
|
909
|
+
constructor(reason) {
|
|
910
|
+
super(`Invalid input: ${reason}`);
|
|
911
|
+
this.reason = reason;
|
|
912
|
+
}
|
|
913
|
+
reason;
|
|
914
|
+
code = "INVALID_INPUT";
|
|
915
|
+
retryable = false;
|
|
916
|
+
};
|
|
917
|
+
var FormatUnsupportedError = class extends A2AError {
|
|
918
|
+
constructor(format, operation) {
|
|
919
|
+
super(`Format unsupported: ${format} for ${operation}`);
|
|
920
|
+
this.format = format;
|
|
921
|
+
this.operation = operation;
|
|
922
|
+
}
|
|
923
|
+
format;
|
|
924
|
+
operation;
|
|
925
|
+
code = "FORMAT_UNSUPPORTED";
|
|
926
|
+
retryable = false;
|
|
927
|
+
};
|
|
928
|
+
var ArtifactAccessDeniedError = class extends A2AError {
|
|
929
|
+
code = "ARTIFACT_ACCESS_DENIED";
|
|
930
|
+
retryable = false;
|
|
931
|
+
};
|
|
932
|
+
var InvalidResourceUriError = class extends A2AError {
|
|
933
|
+
constructor(uri) {
|
|
934
|
+
super(uri ? `Invalid resource URI: ${uri}` : "Invalid resource URI");
|
|
935
|
+
this.uri = uri;
|
|
936
|
+
}
|
|
937
|
+
uri;
|
|
938
|
+
code = "INVALID_RESOURCE_URI";
|
|
939
|
+
retryable = false;
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// src/pipeline-executor.ts
|
|
943
|
+
var LEGACY_TO_SPEC_EVENT = {
|
|
944
|
+
"pipeline:start": "run-started",
|
|
945
|
+
"pipeline:complete": "run-completed",
|
|
946
|
+
"pipeline:failed": "run-failed",
|
|
947
|
+
"pipeline:gated": "run-suspended",
|
|
948
|
+
"step:start": "step-started",
|
|
949
|
+
"step:complete": "step-completed",
|
|
950
|
+
"step:failed": "step-failed",
|
|
951
|
+
"step:gated": "step-gated"
|
|
952
|
+
};
|
|
953
|
+
function createStepStateRecord(stepId, status) {
|
|
954
|
+
return { stepId, status: status ?? "pending", attempts: 0 };
|
|
955
|
+
}
|
|
956
|
+
var PipelineExecutor = class {
|
|
957
|
+
registry;
|
|
958
|
+
providers = /* @__PURE__ */ new Map();
|
|
959
|
+
defaultPipelineTimeoutMs;
|
|
960
|
+
llmJudgeFn;
|
|
961
|
+
customCheckFn;
|
|
962
|
+
prepareInputs;
|
|
963
|
+
persistArtifact;
|
|
964
|
+
onEvent;
|
|
965
|
+
onCost;
|
|
966
|
+
persistence;
|
|
967
|
+
ledger;
|
|
968
|
+
routeStepFn;
|
|
969
|
+
variantsStepFn;
|
|
970
|
+
ratiosStepFn;
|
|
971
|
+
context;
|
|
972
|
+
contextResolveFn;
|
|
973
|
+
gateEvalFn;
|
|
974
|
+
tenantPolicyEnforceFn;
|
|
975
|
+
signProvenance;
|
|
976
|
+
constructor(options) {
|
|
977
|
+
this.registry = new ArtifactRegistry();
|
|
978
|
+
this.defaultPipelineTimeoutMs = options.defaultPipelineTimeoutMs ?? 3e5;
|
|
979
|
+
this.llmJudgeFn = options.llmJudgeFn;
|
|
980
|
+
this.customCheckFn = options.customCheckFn;
|
|
981
|
+
this.prepareInputs = options.prepareInputs;
|
|
982
|
+
this.persistArtifact = options.persistArtifact;
|
|
983
|
+
for (const provider of options.providers) {
|
|
984
|
+
for (const op of provider.supportedOperations) {
|
|
985
|
+
this.providers.set(op, provider);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
this.onEvent = options.onEvent;
|
|
989
|
+
this.onCost = options.onCost;
|
|
990
|
+
this.persistence = options.persistence;
|
|
991
|
+
this.ledger = options.ledger;
|
|
992
|
+
this.routeStepFn = options.routeStepFn;
|
|
993
|
+
this.variantsStepFn = options.variantsStepFn;
|
|
994
|
+
this.ratiosStepFn = options.ratiosStepFn;
|
|
995
|
+
this.context = options.context;
|
|
996
|
+
this.contextResolveFn = options.contextResolveFn;
|
|
997
|
+
this.gateEvalFn = options.gateEvalFn;
|
|
998
|
+
this.tenantPolicyEnforceFn = options.tenantPolicyEnforceFn;
|
|
999
|
+
this.signProvenance = options.signProvenance;
|
|
1000
|
+
}
|
|
1001
|
+
async execute(definition, options) {
|
|
1002
|
+
const pipeline = {
|
|
1003
|
+
id: definition.id,
|
|
1004
|
+
steps: definition.steps,
|
|
1005
|
+
status: "running",
|
|
1006
|
+
artifacts: /* @__PURE__ */ new Map(),
|
|
1007
|
+
completedSteps: [],
|
|
1008
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1009
|
+
};
|
|
1010
|
+
const stepStates = definition.steps.map(
|
|
1011
|
+
(s) => createStepStateRecord(s.id, "pending")
|
|
1012
|
+
);
|
|
1013
|
+
const runId = options?.runId ?? randomUUID();
|
|
1014
|
+
this.currentPipelineDefHash = this.hashPipelineDefinition(definition);
|
|
1015
|
+
const budget = definition.budget;
|
|
1016
|
+
let runCost = 0;
|
|
1017
|
+
if (this.persistence) {
|
|
1018
|
+
const runRecord = {
|
|
1019
|
+
runId,
|
|
1020
|
+
pipelineId: definition.id,
|
|
1021
|
+
status: "running",
|
|
1022
|
+
definition,
|
|
1023
|
+
stepStates,
|
|
1024
|
+
artifacts: {},
|
|
1025
|
+
totalCostUsd: 0,
|
|
1026
|
+
startedAt: pipeline.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1027
|
+
resumable: definition.resumable ?? true
|
|
1028
|
+
};
|
|
1029
|
+
await this.persistence.createRun(runRecord);
|
|
1030
|
+
}
|
|
1031
|
+
this.emitEvent({
|
|
1032
|
+
type: "pipeline:start",
|
|
1033
|
+
pipelineId: pipeline.id,
|
|
1034
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1035
|
+
data: { runId }
|
|
1036
|
+
});
|
|
1037
|
+
const pipelineTimeout = setTimeout(() => {
|
|
1038
|
+
if (pipeline.status === "running") {
|
|
1039
|
+
pipeline.status = "failed";
|
|
1040
|
+
pipeline.failedStep = pipeline.currentStep;
|
|
1041
|
+
this.emitEvent({
|
|
1042
|
+
type: "pipeline:failed",
|
|
1043
|
+
pipelineId: pipeline.id,
|
|
1044
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1045
|
+
data: { reason: "Pipeline timeout exceeded" }
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}, this.defaultPipelineTimeoutMs);
|
|
1049
|
+
try {
|
|
1050
|
+
for (let stepIndex = 0; stepIndex < pipeline.steps.length; stepIndex++) {
|
|
1051
|
+
const step = pipeline.steps[stepIndex];
|
|
1052
|
+
pipeline.currentStep = step.id;
|
|
1053
|
+
if (budget) {
|
|
1054
|
+
stepStates[stepIndex].status = "running";
|
|
1055
|
+
await this.updateRunState(runId, stepStates);
|
|
1056
|
+
const estimate = await this.quickEstimateStep(step, budget);
|
|
1057
|
+
if (runCost + estimate.usdHigh > budget.maxUsd) {
|
|
1058
|
+
if (budget.onExceed === "abort") {
|
|
1059
|
+
throw new BudgetExceededError(runCost + estimate.usdHigh, budget.maxUsd, "run");
|
|
1060
|
+
}
|
|
1061
|
+
pipeline.status = "gated";
|
|
1062
|
+
pipeline.gatedStep = step.id;
|
|
1063
|
+
stepStates[stepIndex].status = "gated";
|
|
1064
|
+
await this.updateRunState(runId, stepStates, runCost);
|
|
1065
|
+
this.emitEvent({
|
|
1066
|
+
type: "pipeline:gated",
|
|
1067
|
+
pipelineId: pipeline.id,
|
|
1068
|
+
stepId: step.id,
|
|
1069
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1070
|
+
data: {
|
|
1071
|
+
reason: `Budget preflight: $${estimate.usdHigh} would exceed $${budget.maxUsd}`
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
break;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const result = await this.executeStep(step, pipeline);
|
|
1078
|
+
if (pipeline.status === "failed") {
|
|
1079
|
+
stepStates[stepIndex].status = "failed";
|
|
1080
|
+
await this.updateRunState(runId, stepStates, runCost);
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
if (result.status === "failed") {
|
|
1084
|
+
pipeline.status = "failed";
|
|
1085
|
+
pipeline.failedStep = step.id;
|
|
1086
|
+
stepStates[stepIndex].status = "failed";
|
|
1087
|
+
await this.updateRunState(runId, stepStates, runCost);
|
|
1088
|
+
this.emitEvent({
|
|
1089
|
+
type: "pipeline:failed",
|
|
1090
|
+
pipelineId: pipeline.id,
|
|
1091
|
+
stepId: step.id,
|
|
1092
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1093
|
+
});
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
if (result.status === "gated") {
|
|
1097
|
+
pipeline.status = "gated";
|
|
1098
|
+
pipeline.gatedStep = step.id;
|
|
1099
|
+
stepStates[stepIndex].status = "gated";
|
|
1100
|
+
await this.updateRunState(runId, stepStates, runCost);
|
|
1101
|
+
this.emitEvent({
|
|
1102
|
+
type: "pipeline:gated",
|
|
1103
|
+
pipelineId: pipeline.id,
|
|
1104
|
+
stepId: step.id,
|
|
1105
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1106
|
+
});
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
if (result.artifact) {
|
|
1110
|
+
pipeline.artifacts.set(result.artifact.id, result.artifact);
|
|
1111
|
+
}
|
|
1112
|
+
stepStates[stepIndex].status = "completed";
|
|
1113
|
+
stepStates[stepIndex].artifactId = result.artifact?.id;
|
|
1114
|
+
pipeline.completedSteps.push(step.id);
|
|
1115
|
+
const stepCost = result.costUsd ?? 0;
|
|
1116
|
+
if (stepCost > 0) {
|
|
1117
|
+
const prior = runCost;
|
|
1118
|
+
runCost += stepCost;
|
|
1119
|
+
if (budget) {
|
|
1120
|
+
const warnAtPct = budget.warnAtPct ?? 0.8;
|
|
1121
|
+
const threshold = warnAtPct * budget.maxUsd;
|
|
1122
|
+
if (prior < threshold && runCost >= threshold) {
|
|
1123
|
+
this.emitEvent({
|
|
1124
|
+
type: "step-progress",
|
|
1125
|
+
pipelineId: pipeline.id,
|
|
1126
|
+
stepId: step.id,
|
|
1127
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1128
|
+
data: {
|
|
1129
|
+
budgetWarning: true,
|
|
1130
|
+
costUsdAccrued: runCost,
|
|
1131
|
+
warnAtPct,
|
|
1132
|
+
maxUsd: budget.maxUsd
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (pipeline.status === "running") {
|
|
1140
|
+
pipeline.status = "completed";
|
|
1141
|
+
await this.updateRunState(runId, stepStates, runCost, "completed");
|
|
1142
|
+
this.emitEvent({
|
|
1143
|
+
type: "pipeline:complete",
|
|
1144
|
+
pipelineId: pipeline.id,
|
|
1145
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
pipeline.status = "failed";
|
|
1150
|
+
pipeline.failedStep = pipeline.currentStep;
|
|
1151
|
+
await this.updateRunState(runId, stepStates, runCost, "failed", error);
|
|
1152
|
+
this.emitEvent({
|
|
1153
|
+
type: "pipeline:failed",
|
|
1154
|
+
pipelineId: pipeline.id,
|
|
1155
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1156
|
+
data: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
1157
|
+
});
|
|
1158
|
+
} finally {
|
|
1159
|
+
clearTimeout(pipelineTimeout);
|
|
1160
|
+
pipeline.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1161
|
+
pipeline.currentStep = void 0;
|
|
1162
|
+
}
|
|
1163
|
+
return pipeline;
|
|
1164
|
+
}
|
|
1165
|
+
async resume(first, second) {
|
|
1166
|
+
if (typeof first === "string") {
|
|
1167
|
+
return this.resumeByRunId(first, second);
|
|
1168
|
+
}
|
|
1169
|
+
return this.resumeLegacy(first, second);
|
|
1170
|
+
}
|
|
1171
|
+
async resumeByRunId(runId, fromStepId) {
|
|
1172
|
+
if (!this.persistence) {
|
|
1173
|
+
throw new Error("Persistence store required for resume");
|
|
1174
|
+
}
|
|
1175
|
+
const acquired = await this.persistence.acquireLock(runId);
|
|
1176
|
+
if (!acquired) {
|
|
1177
|
+
throw new RunInProgressError();
|
|
1178
|
+
}
|
|
1179
|
+
try {
|
|
1180
|
+
const run = await this.persistence.getRun(runId);
|
|
1181
|
+
if (!run) {
|
|
1182
|
+
throw new Error(`Run not found: ${runId}`);
|
|
1183
|
+
}
|
|
1184
|
+
if (run.status === "completed" || run.status === "cancelled") {
|
|
1185
|
+
throw new RunNotResumableError();
|
|
1186
|
+
}
|
|
1187
|
+
if (run.resumable === false) {
|
|
1188
|
+
throw new RunNotResumableError();
|
|
1189
|
+
}
|
|
1190
|
+
const pipeline = {
|
|
1191
|
+
id: run.pipelineId,
|
|
1192
|
+
steps: run.definition.steps,
|
|
1193
|
+
status: "running",
|
|
1194
|
+
artifacts: /* @__PURE__ */ new Map(),
|
|
1195
|
+
completedSteps: [],
|
|
1196
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1197
|
+
};
|
|
1198
|
+
const stepStates = [...run.stepStates];
|
|
1199
|
+
const runCost = run.totalCostUsd;
|
|
1200
|
+
const budget = run.definition.budget;
|
|
1201
|
+
this.currentPipelineDefHash = this.hashPipelineDefinition(run.definition);
|
|
1202
|
+
for (const [stepId, artifactId] of Object.entries(run.artifacts)) {
|
|
1203
|
+
const step = run.definition.steps.find((s) => s.id === stepId);
|
|
1204
|
+
if (step) {
|
|
1205
|
+
const art = {
|
|
1206
|
+
id: artifactId,
|
|
1207
|
+
type: "image",
|
|
1208
|
+
uri: `run://${runId}/${artifactId}`,
|
|
1209
|
+
mimeType: "application/octet-stream",
|
|
1210
|
+
metadata: {},
|
|
1211
|
+
sourceStep: stepId,
|
|
1212
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1213
|
+
};
|
|
1214
|
+
this.registry.registerWithId(artifactId, art);
|
|
1215
|
+
pipeline.artifacts.set(artifactId, art);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const startStepIndex = fromStepId ? run.definition.steps.findIndex((s) => s.id === fromStepId) : run.stepStates.findIndex(
|
|
1219
|
+
(s) => s.status !== "completed" && s.status !== "skipped" && s.status !== "cached"
|
|
1220
|
+
);
|
|
1221
|
+
if (startStepIndex === -1) {
|
|
1222
|
+
pipeline.status = "completed";
|
|
1223
|
+
return pipeline;
|
|
1224
|
+
}
|
|
1225
|
+
for (let i = 0; i < startStepIndex; i++) {
|
|
1226
|
+
if (stepStates[i].status !== "completed") {
|
|
1227
|
+
stepStates[i].status = "skipped";
|
|
1228
|
+
}
|
|
1229
|
+
if (stepStates[i].artifactId) {
|
|
1230
|
+
pipeline.completedSteps.push(run.definition.steps[i].id);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
for (let i = startStepIndex; i < stepStates.length; i++) {
|
|
1234
|
+
stepStates[i].attempts = 0;
|
|
1235
|
+
}
|
|
1236
|
+
await this.persistence.updateRun(runId, {
|
|
1237
|
+
status: "running",
|
|
1238
|
+
stepStates
|
|
1239
|
+
});
|
|
1240
|
+
for (let i = startStepIndex; i < run.definition.steps.length; i++) {
|
|
1241
|
+
const step = run.definition.steps[i];
|
|
1242
|
+
pipeline.currentStep = step.id;
|
|
1243
|
+
if (stepStates[i].status === "completed" || stepStates[i].status === "cached") {
|
|
1244
|
+
pipeline.completedSteps.push(step.id);
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (budget) {
|
|
1248
|
+
stepStates[i].status = "running";
|
|
1249
|
+
await this.persistence.updateRun(runId, { stepStates });
|
|
1250
|
+
const estimate = await this.quickEstimateStep(step, budget);
|
|
1251
|
+
if (runCost + estimate.usdHigh > budget.maxUsd) {
|
|
1252
|
+
if (budget.onExceed === "abort") {
|
|
1253
|
+
throw new BudgetExceededError(runCost + estimate.usdHigh, budget.maxUsd, "run");
|
|
1254
|
+
}
|
|
1255
|
+
pipeline.status = "gated";
|
|
1256
|
+
pipeline.gatedStep = step.id;
|
|
1257
|
+
stepStates[i].status = "gated";
|
|
1258
|
+
await this.persistence.updateRun(runId, {
|
|
1259
|
+
status: "gated",
|
|
1260
|
+
stepStates,
|
|
1261
|
+
totalCostUsd: runCost
|
|
1262
|
+
});
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
const result = await this.executeStep(step, pipeline);
|
|
1267
|
+
if (result.status === "failed") {
|
|
1268
|
+
pipeline.status = "failed";
|
|
1269
|
+
pipeline.failedStep = step.id;
|
|
1270
|
+
stepStates[i].status = "failed";
|
|
1271
|
+
await this.persistence.updateRun(runId, {
|
|
1272
|
+
status: "failed",
|
|
1273
|
+
stepStates,
|
|
1274
|
+
totalCostUsd: runCost
|
|
1275
|
+
});
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
if (result.status === "gated") {
|
|
1279
|
+
pipeline.status = "gated";
|
|
1280
|
+
pipeline.gatedStep = step.id;
|
|
1281
|
+
stepStates[i].status = "gated";
|
|
1282
|
+
await this.persistence.updateRun(runId, {
|
|
1283
|
+
status: "gated",
|
|
1284
|
+
stepStates,
|
|
1285
|
+
totalCostUsd: runCost
|
|
1286
|
+
});
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
if (result.artifact) {
|
|
1290
|
+
pipeline.artifacts.set(result.artifact.id, result.artifact);
|
|
1291
|
+
}
|
|
1292
|
+
stepStates[i].status = "completed";
|
|
1293
|
+
stepStates[i].artifactId = result.artifact?.id;
|
|
1294
|
+
pipeline.completedSteps.push(step.id);
|
|
1295
|
+
}
|
|
1296
|
+
if (pipeline.status === "running") {
|
|
1297
|
+
pipeline.status = "completed";
|
|
1298
|
+
await this.persistence.updateRun(runId, {
|
|
1299
|
+
status: "completed",
|
|
1300
|
+
stepStates,
|
|
1301
|
+
totalCostUsd: runCost,
|
|
1302
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1303
|
+
error: void 0
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
pipeline.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1307
|
+
pipeline.currentStep = void 0;
|
|
1308
|
+
return pipeline;
|
|
1309
|
+
} finally {
|
|
1310
|
+
await this.persistence.releaseLock(runId);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async resumeLegacy(pipeline, action) {
|
|
1314
|
+
if (pipeline.status !== "gated" && pipeline.status !== "failed") {
|
|
1315
|
+
throw new Error(`Cannot resume pipeline with status: ${pipeline.status}`);
|
|
1316
|
+
}
|
|
1317
|
+
if (action === "abort") {
|
|
1318
|
+
pipeline.status = "failed";
|
|
1319
|
+
pipeline.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1320
|
+
return pipeline;
|
|
1321
|
+
}
|
|
1322
|
+
const resumeStepId = pipeline.status === "gated" ? pipeline.gatedStep : pipeline.failedStep;
|
|
1323
|
+
const resumeStepLabel = pipeline.status === "gated" ? "gated" : "failed";
|
|
1324
|
+
const resumeStepIndex = pipeline.steps.findIndex((s) => s.id === resumeStepId);
|
|
1325
|
+
if (!resumeStepId || resumeStepIndex === -1) {
|
|
1326
|
+
throw new Error(
|
|
1327
|
+
`${resumeStepLabel[0].toUpperCase() + resumeStepLabel.slice(1)} step not found`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
if (action === "skip") {
|
|
1331
|
+
const resumeStep = pipeline.steps[resumeStepIndex];
|
|
1332
|
+
if (!pipeline.completedSteps.includes(resumeStep.id)) {
|
|
1333
|
+
pipeline.completedSteps.push(resumeStep.id);
|
|
1334
|
+
}
|
|
1335
|
+
pipeline.status = "running";
|
|
1336
|
+
pipeline.gatedStep = void 0;
|
|
1337
|
+
pipeline.failedStep = void 0;
|
|
1338
|
+
for (let i = resumeStepIndex + 1; i < pipeline.steps.length; i++) {
|
|
1339
|
+
const step = pipeline.steps[i];
|
|
1340
|
+
pipeline.currentStep = step.id;
|
|
1341
|
+
const result = await this.executeStep(step, pipeline);
|
|
1342
|
+
if (result.status === "failed") {
|
|
1343
|
+
pipeline.status = "failed";
|
|
1344
|
+
pipeline.failedStep = step.id;
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
1347
|
+
if (result.status === "gated") {
|
|
1348
|
+
pipeline.status = "gated";
|
|
1349
|
+
pipeline.gatedStep = step.id;
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
if (result.artifact) {
|
|
1353
|
+
pipeline.artifacts.set(result.artifact.id, result.artifact);
|
|
1354
|
+
}
|
|
1355
|
+
pipeline.completedSteps.push(step.id);
|
|
1356
|
+
}
|
|
1357
|
+
if (pipeline.status === "running") {
|
|
1358
|
+
pipeline.status = "completed";
|
|
1359
|
+
}
|
|
1360
|
+
} else if (action === "retry") {
|
|
1361
|
+
const step = pipeline.steps[resumeStepIndex];
|
|
1362
|
+
const existingArtifact = this.registry.findBySourceStep(step.id);
|
|
1363
|
+
if (existingArtifact) {
|
|
1364
|
+
this.registry.delete(existingArtifact.id);
|
|
1365
|
+
pipeline.artifacts.delete(existingArtifact.id);
|
|
1366
|
+
}
|
|
1367
|
+
const stepsAfterGate = pipeline.completedSteps.filter((s) => {
|
|
1368
|
+
const index = pipeline.steps.findIndex((ps) => ps.id === s);
|
|
1369
|
+
return index >= resumeStepIndex;
|
|
1370
|
+
});
|
|
1371
|
+
pipeline.completedSteps = pipeline.completedSteps.filter((s) => !stepsAfterGate.includes(s));
|
|
1372
|
+
pipeline.status = "running";
|
|
1373
|
+
pipeline.gatedStep = void 0;
|
|
1374
|
+
pipeline.failedStep = void 0;
|
|
1375
|
+
for (let i = resumeStepIndex; i < pipeline.steps.length; i++) {
|
|
1376
|
+
const currentStep = pipeline.steps[i];
|
|
1377
|
+
pipeline.currentStep = currentStep.id;
|
|
1378
|
+
const result = await this.executeStep(currentStep, pipeline);
|
|
1379
|
+
if (result.status === "failed") {
|
|
1380
|
+
pipeline.status = "failed";
|
|
1381
|
+
pipeline.failedStep = currentStep.id;
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
if (result.status === "gated") {
|
|
1385
|
+
pipeline.status = "gated";
|
|
1386
|
+
pipeline.gatedStep = currentStep.id;
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
if (result.artifact) {
|
|
1390
|
+
pipeline.artifacts.set(result.artifact.id, result.artifact);
|
|
1391
|
+
}
|
|
1392
|
+
if (!pipeline.completedSteps.includes(currentStep.id)) {
|
|
1393
|
+
pipeline.completedSteps.push(currentStep.id);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (pipeline.status === "running") {
|
|
1397
|
+
pipeline.status = "completed";
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
pipeline.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1401
|
+
pipeline.currentStep = void 0;
|
|
1402
|
+
return pipeline;
|
|
1403
|
+
}
|
|
1404
|
+
async estimate(definition) {
|
|
1405
|
+
const perStep = [];
|
|
1406
|
+
const warnings = [];
|
|
1407
|
+
for (const step of definition.steps) {
|
|
1408
|
+
const provider = this.providers.get(step.operation);
|
|
1409
|
+
if (!provider) {
|
|
1410
|
+
warnings.push({
|
|
1411
|
+
stepId: step.id,
|
|
1412
|
+
code: "no-estimator",
|
|
1413
|
+
message: `No provider available for operation '${step.operation}'`
|
|
1414
|
+
});
|
|
1415
|
+
perStep.push({
|
|
1416
|
+
stepId: step.id,
|
|
1417
|
+
operation: step.operation,
|
|
1418
|
+
provider: "unknown",
|
|
1419
|
+
modelId: step.config?.model ?? "default",
|
|
1420
|
+
usdLow: 0,
|
|
1421
|
+
usdHigh: 0.01,
|
|
1422
|
+
estimable: false,
|
|
1423
|
+
fallbackUsed: "default-bound"
|
|
1424
|
+
});
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
const estimate = await this.quickEstimateStep(step);
|
|
1428
|
+
const stepEntry = {
|
|
1429
|
+
stepId: step.id,
|
|
1430
|
+
operation: step.operation,
|
|
1431
|
+
provider: provider.name,
|
|
1432
|
+
modelId: step.config?.model ?? "default",
|
|
1433
|
+
usdLow: estimate.usdLow,
|
|
1434
|
+
usdHigh: estimate.usdHigh,
|
|
1435
|
+
estimable: estimate.estimable
|
|
1436
|
+
};
|
|
1437
|
+
if (!estimate.estimable) {
|
|
1438
|
+
stepEntry.fallbackUsed = "default-bound";
|
|
1439
|
+
warnings.push({
|
|
1440
|
+
stepId: step.id,
|
|
1441
|
+
code: "no-estimator",
|
|
1442
|
+
message: `Provider '${provider.name}' did not return an estimate for '${step.operation}'`
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
const maxTokens = step.inputs?.max_tokens;
|
|
1446
|
+
if (typeof maxTokens === "number" && maxTokens > 0) {
|
|
1447
|
+
warnings.push({
|
|
1448
|
+
stepId: step.id,
|
|
1449
|
+
code: "variable-output",
|
|
1450
|
+
message: `Step '${step.id}' has variable output (max_tokens=${maxTokens}); usdLow uses ~30% of cap, usdHigh uses 100%`
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
perStep.push(stepEntry);
|
|
1454
|
+
const stepRoute = step.route;
|
|
1455
|
+
if (stepRoute?.candidates && stepRoute.candidates.length > 1) {
|
|
1456
|
+
warnings.push({
|
|
1457
|
+
stepId: step.id,
|
|
1458
|
+
code: "router-spread",
|
|
1459
|
+
message: `Step '${step.id}' routes across ${stepRoute.candidates.length} candidates, estimates may vary`
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
for (const inputVal of Object.values(step.inputs)) {
|
|
1463
|
+
if (inputVal.includes("{{") && inputVal.includes(".output}}")) {
|
|
1464
|
+
warnings.push({
|
|
1465
|
+
stepId: step.id,
|
|
1466
|
+
code: "depends-on-prior-step",
|
|
1467
|
+
message: `Step '${step.id}' cost depends on variable output from prior step`
|
|
1468
|
+
});
|
|
1469
|
+
break;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
const totalUsdLow = perStep.reduce((s, e) => s + e.usdLow, 0);
|
|
1474
|
+
const totalUsdHigh = perStep.reduce((s, e) => s + e.usdHigh, 0);
|
|
1475
|
+
return { totalUsdLow, totalUsdHigh, perStep, warnings };
|
|
1476
|
+
}
|
|
1477
|
+
// ─── Private Methods ──────────────────────────────────────────────────────
|
|
1478
|
+
async updateRunState(runId, stepStates, totalCostUsd, status, error) {
|
|
1479
|
+
if (!this.persistence) return;
|
|
1480
|
+
const patch = { stepStates };
|
|
1481
|
+
if (totalCostUsd !== void 0) patch.totalCostUsd = totalCostUsd;
|
|
1482
|
+
if (status) patch.status = status;
|
|
1483
|
+
if (error) patch.error = error instanceof Error ? error.message : "Unknown error";
|
|
1484
|
+
await this.persistence.updateRun(
|
|
1485
|
+
runId,
|
|
1486
|
+
patch
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Estimate the per-step cost band for budget preflight (F4) and dry-run (F5).
|
|
1491
|
+
*
|
|
1492
|
+
* Calls `provider.estimateCost` when available — that's what the spec wants, and
|
|
1493
|
+
* what makes preflight actually enforce caps against real pricing.
|
|
1494
|
+
*
|
|
1495
|
+
* When the provider doesn't implement estimateCost (legacy mocks, certain test fakes),
|
|
1496
|
+
* falls back to a tiny default band so preflight doesn't catastrophically block.
|
|
1497
|
+
* Variable-output ops widen the high band: when max_tokens is present, treat low as
|
|
1498
|
+
* ~30% of max and high as 100% per plan §F5.
|
|
1499
|
+
*/
|
|
1500
|
+
async quickEstimateStep(step, _budget) {
|
|
1501
|
+
const provider = this.providers.get(step.operation);
|
|
1502
|
+
if (!provider) {
|
|
1503
|
+
return { usdLow: 0, usdHigh: 0.01, estimable: false };
|
|
1504
|
+
}
|
|
1505
|
+
if (typeof provider.estimateCost !== "function") {
|
|
1506
|
+
return { usdLow: 1e-3, usdHigh: 0.01, estimable: false };
|
|
1507
|
+
}
|
|
1508
|
+
try {
|
|
1509
|
+
const est = await provider.estimateCost({
|
|
1510
|
+
operation: step.operation,
|
|
1511
|
+
params: step.inputs,
|
|
1512
|
+
config: step.config ?? {}
|
|
1513
|
+
});
|
|
1514
|
+
const baseHigh = est.costUsd;
|
|
1515
|
+
const maxTokens = step.inputs?.max_tokens;
|
|
1516
|
+
const isVariableOutput = typeof maxTokens === "number" && maxTokens > 0;
|
|
1517
|
+
return {
|
|
1518
|
+
usdLow: isVariableOutput ? baseHigh * 0.3 : baseHigh,
|
|
1519
|
+
usdHigh: baseHigh,
|
|
1520
|
+
estimable: true,
|
|
1521
|
+
estimatedDurationMs: est.estimatedDurationMs
|
|
1522
|
+
};
|
|
1523
|
+
} catch {
|
|
1524
|
+
return { usdLow: 1e-3, usdHigh: 0.01, estimable: false };
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async executeStep(step, pipeline) {
|
|
1528
|
+
const maxRetries = step.qualityGate?.action === "retry" ? step.qualityGate.maxRetries ?? 1 : 0;
|
|
1529
|
+
let lastResult = null;
|
|
1530
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1531
|
+
if (attempt > 0) {
|
|
1532
|
+
this.registry.deleteBySourceStep(step.id);
|
|
1533
|
+
this.emitEvent({
|
|
1534
|
+
type: "step:retry",
|
|
1535
|
+
pipelineId: pipeline.id,
|
|
1536
|
+
stepId: step.id,
|
|
1537
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1538
|
+
data: { attempt: attempt + 1, maxRetries }
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
this.emitEvent({
|
|
1542
|
+
type: "step:start",
|
|
1543
|
+
pipelineId: pipeline.id,
|
|
1544
|
+
stepId: step.id,
|
|
1545
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1546
|
+
data: { attempt: attempt + 1 }
|
|
1547
|
+
});
|
|
1548
|
+
try {
|
|
1549
|
+
const result = await this.executeStepOnce(step, pipeline);
|
|
1550
|
+
if (!result) {
|
|
1551
|
+
return { status: "failed" };
|
|
1552
|
+
}
|
|
1553
|
+
if (step.qualityGate) {
|
|
1554
|
+
const gateResult = await this.evaluateQualityGate(step.qualityGate, result.artifact);
|
|
1555
|
+
if (!gateResult.passed) {
|
|
1556
|
+
if (gateResult.action === "fail") {
|
|
1557
|
+
this.emitEvent({
|
|
1558
|
+
type: "step:failed",
|
|
1559
|
+
pipelineId: pipeline.id,
|
|
1560
|
+
stepId: step.id,
|
|
1561
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1562
|
+
data: { reason: gateResult.reasoning }
|
|
1563
|
+
});
|
|
1564
|
+
return { status: "failed", artifact: result.artifact };
|
|
1565
|
+
}
|
|
1566
|
+
if (gateResult.action === "retry" && attempt < maxRetries) {
|
|
1567
|
+
this.emitEvent({
|
|
1568
|
+
type: "step:gated",
|
|
1569
|
+
pipelineId: pipeline.id,
|
|
1570
|
+
stepId: step.id,
|
|
1571
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1572
|
+
data: { reason: gateResult.reasoning, willRetry: true }
|
|
1573
|
+
});
|
|
1574
|
+
lastResult = { status: "completed", artifact: result.artifact };
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
if (gateResult.action === "retry" && attempt >= maxRetries) {
|
|
1578
|
+
this.emitEvent({
|
|
1579
|
+
type: "step:gated",
|
|
1580
|
+
pipelineId: pipeline.id,
|
|
1581
|
+
stepId: step.id,
|
|
1582
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1583
|
+
data: { reason: gateResult.reasoning, maxRetriesExceeded: true }
|
|
1584
|
+
});
|
|
1585
|
+
return { status: "gated", artifact: result.artifact };
|
|
1586
|
+
}
|
|
1587
|
+
if (gateResult.action === "warn") {
|
|
1588
|
+
console.warn(`Quality gate warning for step ${step.id}: ${gateResult.reasoning}`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
const gates = step.gates;
|
|
1593
|
+
if (gates && gates.length > 0) {
|
|
1594
|
+
for (const gate of gates) {
|
|
1595
|
+
if (gate.type === "safety" && this.gateEvalFn) {
|
|
1596
|
+
const gateResult2 = await this.gateEvalFn({
|
|
1597
|
+
gate,
|
|
1598
|
+
artifact: result.artifact,
|
|
1599
|
+
artifactUri: result.artifact.uri,
|
|
1600
|
+
stepId: step.id
|
|
1601
|
+
});
|
|
1602
|
+
if (gateResult2) {
|
|
1603
|
+
if (!gateResult2.passed) {
|
|
1604
|
+
const safetyResult = gateResult2;
|
|
1605
|
+
if (gateResult2.action === "fail") {
|
|
1606
|
+
throw new SafetyGateRejectedError(
|
|
1607
|
+
safetyResult.reasoning ?? "csam",
|
|
1608
|
+
safetyResult.score ?? 0
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
this.emitEvent({
|
|
1612
|
+
type: "step:gated",
|
|
1613
|
+
pipelineId: pipeline.id,
|
|
1614
|
+
stepId: step.id,
|
|
1615
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1616
|
+
data: { reason: `Safety gate rejected: ${safetyResult.reasoning}` }
|
|
1617
|
+
});
|
|
1618
|
+
return { status: "gated", artifact: result.artifact };
|
|
1619
|
+
}
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (gate.type === "loudness" && this.gateEvalFn) {
|
|
1624
|
+
const gateResult2 = await this.gateEvalFn({
|
|
1625
|
+
gate,
|
|
1626
|
+
artifact: result.artifact,
|
|
1627
|
+
artifactUri: result.artifact.uri,
|
|
1628
|
+
stepId: step.id
|
|
1629
|
+
});
|
|
1630
|
+
if (gateResult2) {
|
|
1631
|
+
if (!gateResult2.passed) {
|
|
1632
|
+
if (gateResult2.action === "fail") {
|
|
1633
|
+
throw new LoudnessGateFailedError();
|
|
1634
|
+
}
|
|
1635
|
+
this.emitEvent({
|
|
1636
|
+
type: "step:gated",
|
|
1637
|
+
pipelineId: pipeline.id,
|
|
1638
|
+
stepId: step.id,
|
|
1639
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1640
|
+
data: { reason: "Loudness gate failed" }
|
|
1641
|
+
});
|
|
1642
|
+
return { status: "gated", artifact: result.artifact };
|
|
1643
|
+
}
|
|
1644
|
+
if (gateResult2.resultArtifact) {
|
|
1645
|
+
return { status: "completed", artifact: gateResult2.resultArtifact };
|
|
1646
|
+
}
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const gateResult = await this.evaluateQualityGate(
|
|
1651
|
+
gate,
|
|
1652
|
+
result.artifact
|
|
1653
|
+
);
|
|
1654
|
+
if (!gateResult.passed) {
|
|
1655
|
+
this.emitEvent({
|
|
1656
|
+
type: "step:gated",
|
|
1657
|
+
pipelineId: pipeline.id,
|
|
1658
|
+
stepId: step.id,
|
|
1659
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1660
|
+
data: { reason: `Additional gate failed: ${gateResult.reasoning}` }
|
|
1661
|
+
});
|
|
1662
|
+
return { status: "gated", artifact: result.artifact };
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
this.emitEvent({
|
|
1667
|
+
type: "step:complete",
|
|
1668
|
+
pipelineId: pipeline.id,
|
|
1669
|
+
stepId: step.id,
|
|
1670
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1671
|
+
data: { artifactId: result.artifact.id }
|
|
1672
|
+
});
|
|
1673
|
+
return { status: "completed", artifact: result.artifact, costUsd: result.costUsd };
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
this.emitEvent({
|
|
1676
|
+
type: "step:failed",
|
|
1677
|
+
pipelineId: pipeline.id,
|
|
1678
|
+
stepId: step.id,
|
|
1679
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1680
|
+
data: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
1681
|
+
});
|
|
1682
|
+
if (attempt < maxRetries) {
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
return { status: "failed" };
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return lastResult ?? { status: "failed" };
|
|
1689
|
+
}
|
|
1690
|
+
async executeStepOnce(step, pipeline) {
|
|
1691
|
+
let resolvedInputs = await this.resolveInputs(step.inputs);
|
|
1692
|
+
const stepExt = step;
|
|
1693
|
+
if (this.contextResolveFn && this.context && stepExt.contextRefs) {
|
|
1694
|
+
const providerName = this.providers.get(step.operation)?.name ?? "unknown";
|
|
1695
|
+
resolvedInputs = this.contextResolveFn({
|
|
1696
|
+
inputs: resolvedInputs,
|
|
1697
|
+
context: this.context,
|
|
1698
|
+
providerName
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
const route = stepExt.route;
|
|
1702
|
+
if (route && this.routeStepFn) {
|
|
1703
|
+
const routeResult = await this.routeStepFn({
|
|
1704
|
+
route,
|
|
1705
|
+
operation: step.operation,
|
|
1706
|
+
resolvedInputs,
|
|
1707
|
+
stepConfig: step.config,
|
|
1708
|
+
pipelineId: pipeline.id,
|
|
1709
|
+
stepId: step.id,
|
|
1710
|
+
getProviderByName: (name) => this.providers.get(name) ?? this.findProviderByName(name)
|
|
1711
|
+
});
|
|
1712
|
+
if (routeResult) {
|
|
1713
|
+
return { artifact: routeResult.artifact };
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
const variants = stepExt.variants;
|
|
1717
|
+
if (variants && this.variantsStepFn) {
|
|
1718
|
+
const variantsResult = await this.variantsStepFn({
|
|
1719
|
+
variants,
|
|
1720
|
+
step,
|
|
1721
|
+
resolvedInputs,
|
|
1722
|
+
pipelineId: pipeline.id,
|
|
1723
|
+
stepId: step.id
|
|
1724
|
+
});
|
|
1725
|
+
if (variantsResult) {
|
|
1726
|
+
return { artifact: variantsResult.artifact };
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
const ratios = stepExt.ratios;
|
|
1730
|
+
if (ratios && this.ratiosStepFn) {
|
|
1731
|
+
const ratiosResult = await this.ratiosStepFn({
|
|
1732
|
+
ratios,
|
|
1733
|
+
operation: step.operation,
|
|
1734
|
+
resolvedInputs,
|
|
1735
|
+
stepConfig: step.config,
|
|
1736
|
+
stepId: step.id
|
|
1737
|
+
});
|
|
1738
|
+
if (ratiosResult) {
|
|
1739
|
+
return { artifact: ratiosResult.artifact };
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const provider = this.providers.get(step.operation);
|
|
1743
|
+
if (!provider) {
|
|
1744
|
+
throw new Error(`No provider available for operation: ${step.operation}`);
|
|
1745
|
+
}
|
|
1746
|
+
if (this.tenantPolicyEnforceFn) {
|
|
1747
|
+
this.tenantPolicyEnforceFn(provider.name, step.config.model);
|
|
1748
|
+
}
|
|
1749
|
+
const providerInputs = this.prepareInputs ? await this.prepareInputs(step.operation, resolvedInputs) : resolvedInputs;
|
|
1750
|
+
const cacheConfig = stepExt.cache;
|
|
1751
|
+
const providerWithCache = provider;
|
|
1752
|
+
const result = cacheConfig && typeof providerWithCache.executeWithCache === "function" ? await providerWithCache.executeWithCache(
|
|
1753
|
+
{ operation: step.operation, params: providerInputs, config: step.config },
|
|
1754
|
+
cacheConfig.mode ? { mode: cacheConfig.mode, ttlSeconds: cacheConfig.ttlSeconds } : void 0
|
|
1755
|
+
) : await provider.execute(step.operation, providerInputs, step.config);
|
|
1756
|
+
const artifactId = randomUUID();
|
|
1757
|
+
const persisted = this.persistArtifact ? await this.persistArtifact({
|
|
1758
|
+
artifactId,
|
|
1759
|
+
operation: step.operation,
|
|
1760
|
+
data: result.data,
|
|
1761
|
+
artifact: {
|
|
1762
|
+
...result.artifact,
|
|
1763
|
+
sourceStep: step.id
|
|
1764
|
+
},
|
|
1765
|
+
pipelineId: pipeline.id,
|
|
1766
|
+
stepId: step.id
|
|
1767
|
+
}) : void 0;
|
|
1768
|
+
const artifact = this.registry.registerWithId(artifactId, {
|
|
1769
|
+
...result.artifact,
|
|
1770
|
+
uri: persisted?.uri ?? result.artifact.uri,
|
|
1771
|
+
sourceStep: step.id
|
|
1772
|
+
});
|
|
1773
|
+
if (result.cost_usd !== void 0) {
|
|
1774
|
+
this.onCost?.({
|
|
1775
|
+
operation: step.operation,
|
|
1776
|
+
provider: provider.name,
|
|
1777
|
+
model: step.config.model,
|
|
1778
|
+
cost_usd: result.cost_usd,
|
|
1779
|
+
artifactId: artifact.id,
|
|
1780
|
+
pipelineId: pipeline.id,
|
|
1781
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1782
|
+
});
|
|
1783
|
+
if (this.ledger) {
|
|
1784
|
+
await this.ledger.charge({
|
|
1785
|
+
runId: pipeline.id,
|
|
1786
|
+
stepId: step.id,
|
|
1787
|
+
operation: step.operation,
|
|
1788
|
+
provider: provider.name,
|
|
1789
|
+
costUsd: result.cost_usd
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
if (this.signProvenance) {
|
|
1794
|
+
const ingredientArtifactIds = [];
|
|
1795
|
+
for (const value of Object.values(step.inputs ?? {})) {
|
|
1796
|
+
const m = value.match?.(/^\{\{(\w+)\.output\}\}$/);
|
|
1797
|
+
if (m) {
|
|
1798
|
+
const upstream = this.registry.findBySourceStep(m[1]);
|
|
1799
|
+
if (upstream) ingredientArtifactIds.push(upstream.id);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
await this.signProvenance({
|
|
1803
|
+
artifactId: artifact.id,
|
|
1804
|
+
runId: pipeline.id,
|
|
1805
|
+
// executor's Pipeline.id is the runId for in-tree runs
|
|
1806
|
+
pipelineDefHash: this.currentPipelineDefHash ?? "",
|
|
1807
|
+
stepId: step.id,
|
|
1808
|
+
operation: step.operation,
|
|
1809
|
+
providerId: provider.name,
|
|
1810
|
+
modelId: step.config?.model,
|
|
1811
|
+
ingredientArtifactIds
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
return { artifact, costUsd: result.cost_usd };
|
|
1815
|
+
}
|
|
1816
|
+
/** Hash the normalized pipeline definition for provenance manifests (F17) and for
|
|
1817
|
+
* PipelineRunRecord auditing. Same canonical-json rule as F2 cache keys. */
|
|
1818
|
+
hashPipelineDefinition(def) {
|
|
1819
|
+
const normalized = { id: def.id, steps: def.steps };
|
|
1820
|
+
const canonical = JSON.stringify(normalized, Object.keys(normalized).sort());
|
|
1821
|
+
const { createHash } = __require("crypto");
|
|
1822
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
1823
|
+
}
|
|
1824
|
+
/** Set inside execute()/resume() before stepping through steps; read by signProvenance. */
|
|
1825
|
+
currentPipelineDefHash;
|
|
1826
|
+
findProviderByName(name) {
|
|
1827
|
+
for (const provider of this.providers.values()) {
|
|
1828
|
+
if (provider.name === name) return provider;
|
|
1829
|
+
}
|
|
1830
|
+
return void 0;
|
|
1831
|
+
}
|
|
1832
|
+
async resolveInputs(inputSpec) {
|
|
1833
|
+
const resolved = {};
|
|
1834
|
+
for (const [key, value] of Object.entries(inputSpec)) {
|
|
1835
|
+
const match = value.match(/^\{\{(\w+)\.output\}\}$/);
|
|
1836
|
+
if (match) {
|
|
1837
|
+
const stepId = match[1];
|
|
1838
|
+
const artifact = this.registry.findBySourceStep(stepId);
|
|
1839
|
+
if (!artifact) {
|
|
1840
|
+
throw new ArtifactNotFoundError();
|
|
1841
|
+
}
|
|
1842
|
+
resolved[key] = artifact.id;
|
|
1843
|
+
} else {
|
|
1844
|
+
resolved[key] = value;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
return resolved;
|
|
1848
|
+
}
|
|
1849
|
+
async evaluateQualityGate(_gate, artifact) {
|
|
1850
|
+
const evaluator = createQualityGateEvaluator(_gate, this.llmJudgeFn, this.customCheckFn);
|
|
1851
|
+
return await evaluator.evaluate(_gate, artifact);
|
|
1852
|
+
}
|
|
1853
|
+
emitEvent(event) {
|
|
1854
|
+
this.onEvent?.(event);
|
|
1855
|
+
const specType = LEGACY_TO_SPEC_EVENT[event.type];
|
|
1856
|
+
if (specType && specType !== event.type) {
|
|
1857
|
+
this.onEvent?.({ ...event, type: specType });
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
getRegistry() {
|
|
1861
|
+
return this.registry;
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
// src/pipeline-estimator.ts
|
|
1866
|
+
var PipelineEstimator = class {
|
|
1867
|
+
ledger;
|
|
1868
|
+
estimateOperation;
|
|
1869
|
+
constructor(options = {}) {
|
|
1870
|
+
this.ledger = options.ledger;
|
|
1871
|
+
this.estimateOperation = options.estimateOperation;
|
|
1872
|
+
}
|
|
1873
|
+
async estimate(pipeline) {
|
|
1874
|
+
const perStep = [];
|
|
1875
|
+
const warnings = [];
|
|
1876
|
+
for (const step of pipeline.steps) {
|
|
1877
|
+
let usdLow = 1e-3;
|
|
1878
|
+
let usdHigh = 0.01;
|
|
1879
|
+
let estimable = true;
|
|
1880
|
+
let fallbackUsed;
|
|
1881
|
+
const provider = "unknown";
|
|
1882
|
+
const modelId = step.config.model ?? "default";
|
|
1883
|
+
if (this.estimateOperation) {
|
|
1884
|
+
const opEst = await this.estimateOperation(step.operation, step.config);
|
|
1885
|
+
if (opEst !== null) {
|
|
1886
|
+
usdLow = opEst.usdLow;
|
|
1887
|
+
usdHigh = opEst.usdHigh;
|
|
1888
|
+
} else {
|
|
1889
|
+
estimable = false;
|
|
1890
|
+
fallbackUsed = "default-bound";
|
|
1891
|
+
warnings.push({
|
|
1892
|
+
stepId: step.id,
|
|
1893
|
+
code: "no-estimator",
|
|
1894
|
+
message: `No estimator available for operation '${step.operation}'`
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
} else {
|
|
1898
|
+
estimable = false;
|
|
1899
|
+
fallbackUsed = "default-bound";
|
|
1900
|
+
}
|
|
1901
|
+
if (!estimable && this.ledger) {
|
|
1902
|
+
try {
|
|
1903
|
+
const historical = await this.ledger.getTotalCost();
|
|
1904
|
+
if (historical > 0) {
|
|
1905
|
+
fallbackUsed = "cached-stats";
|
|
1906
|
+
}
|
|
1907
|
+
} catch {
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const route = step.route;
|
|
1911
|
+
if (route?.candidates && route.candidates.length > 1) {
|
|
1912
|
+
warnings.push({
|
|
1913
|
+
stepId: step.id,
|
|
1914
|
+
code: "router-spread",
|
|
1915
|
+
message: `Step '${step.id}' routes across ${route.candidates.length} candidates, estimates may vary`
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
for (const inputVal of Object.values(step.inputs)) {
|
|
1919
|
+
if (typeof inputVal === "string" && inputVal.includes("{{") && inputVal.includes(".output}}")) {
|
|
1920
|
+
warnings.push({
|
|
1921
|
+
stepId: step.id,
|
|
1922
|
+
code: "depends-on-prior-step",
|
|
1923
|
+
message: `Step '${step.id}' cost depends on variable output from prior step`
|
|
1924
|
+
});
|
|
1925
|
+
break;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
perStep.push({
|
|
1929
|
+
stepId: step.id,
|
|
1930
|
+
operation: step.operation,
|
|
1931
|
+
provider,
|
|
1932
|
+
modelId,
|
|
1933
|
+
usdLow,
|
|
1934
|
+
usdHigh,
|
|
1935
|
+
estimable,
|
|
1936
|
+
...fallbackUsed ? { fallbackUsed } : {}
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
const totalUsdLow = perStep.reduce((s, e) => s + e.usdLow, 0);
|
|
1940
|
+
const totalUsdHigh = perStep.reduce((s, e) => s + e.usdHigh, 0);
|
|
1941
|
+
return { totalUsdLow, totalUsdHigh, perStep, warnings };
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
// src/mock-provider.ts
|
|
1946
|
+
var MockProvider = class {
|
|
1947
|
+
name;
|
|
1948
|
+
supportedOperations;
|
|
1949
|
+
delay;
|
|
1950
|
+
failureRate;
|
|
1951
|
+
baseCost;
|
|
1952
|
+
alwaysPass;
|
|
1953
|
+
constructor(config = {}) {
|
|
1954
|
+
this.name = config.name ?? "mock";
|
|
1955
|
+
this.supportedOperations = config.operations ?? [
|
|
1956
|
+
"mock.generate",
|
|
1957
|
+
"mock.transform",
|
|
1958
|
+
"mock.extract"
|
|
1959
|
+
];
|
|
1960
|
+
this.delay = config.delay ?? 100;
|
|
1961
|
+
this.failureRate = config.failureRate ?? 0;
|
|
1962
|
+
this.baseCost = config.baseCost ?? 1e-3;
|
|
1963
|
+
this.alwaysPass = config.alwaysPass ?? false;
|
|
1964
|
+
}
|
|
1965
|
+
async execute(operation, _inputs, config) {
|
|
1966
|
+
await new Promise((resolve) => setTimeout(resolve, this.delay));
|
|
1967
|
+
if (Math.random() < this.failureRate) {
|
|
1968
|
+
throw new Error("Mock provider simulated failure");
|
|
1969
|
+
}
|
|
1970
|
+
let type = "image";
|
|
1971
|
+
let mimeType = "image/png";
|
|
1972
|
+
if (operation.includes("audio")) {
|
|
1973
|
+
type = "audio";
|
|
1974
|
+
mimeType = "audio/mpeg";
|
|
1975
|
+
} else if (operation.includes("video")) {
|
|
1976
|
+
type = "video";
|
|
1977
|
+
mimeType = "video/mp4";
|
|
1978
|
+
} else if (operation.includes("text") || operation.includes("extract")) {
|
|
1979
|
+
type = "text";
|
|
1980
|
+
mimeType = "text/plain";
|
|
1981
|
+
} else if (operation.includes("document")) {
|
|
1982
|
+
type = "document";
|
|
1983
|
+
mimeType = "application/pdf";
|
|
1984
|
+
}
|
|
1985
|
+
const metadata = {
|
|
1986
|
+
width: 1024,
|
|
1987
|
+
height: 1024,
|
|
1988
|
+
quality: this.alwaysPass ? 0.99 : 0.9,
|
|
1989
|
+
...config
|
|
1990
|
+
};
|
|
1991
|
+
if (type === "audio") {
|
|
1992
|
+
metadata.duration = 30;
|
|
1993
|
+
metadata.sampleRate = 44100;
|
|
1994
|
+
} else if (type === "video") {
|
|
1995
|
+
metadata.duration = 10;
|
|
1996
|
+
metadata.fps = 30;
|
|
1997
|
+
}
|
|
1998
|
+
const uri = `mock://${operation}/${Date.now()}.${mimeType.split("/")[1]}`;
|
|
1999
|
+
return {
|
|
2000
|
+
data: Buffer.from(JSON.stringify({ operation, metadata })),
|
|
2001
|
+
artifact: {
|
|
2002
|
+
type,
|
|
2003
|
+
uri,
|
|
2004
|
+
mimeType,
|
|
2005
|
+
metadata,
|
|
2006
|
+
sourceStep: void 0
|
|
2007
|
+
},
|
|
2008
|
+
cost_usd: this.baseCost,
|
|
2009
|
+
duration_ms: this.delay
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
async healthCheck() {
|
|
2013
|
+
return Math.random() > this.failureRate;
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
var mockOperations = {
|
|
2017
|
+
generate: "mock.generate",
|
|
2018
|
+
transform: "mock.transform",
|
|
2019
|
+
extract: "mock.extract",
|
|
2020
|
+
imageGenerate: "image.generate",
|
|
2021
|
+
imageUpscale: "image.upscale",
|
|
2022
|
+
imageRemoveBackground: "image.remove_background",
|
|
2023
|
+
audioTts: "audio.tts",
|
|
2024
|
+
audioStt: "audio.stt"
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
// src/event-bus.ts
|
|
2028
|
+
function createEventBus() {
|
|
2029
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
2030
|
+
function on(kind, handler) {
|
|
2031
|
+
if (!handlers.has(kind)) {
|
|
2032
|
+
handlers.set(kind, /* @__PURE__ */ new Set());
|
|
2033
|
+
}
|
|
2034
|
+
handlers.get(kind)?.add(handler);
|
|
2035
|
+
return () => {
|
|
2036
|
+
handlers.get(kind)?.delete(handler);
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
function emit(event) {
|
|
2040
|
+
const kindHandlers = handlers.get(event.kind);
|
|
2041
|
+
if (kindHandlers) {
|
|
2042
|
+
for (const handler of kindHandlers) {
|
|
2043
|
+
const result = handler(event);
|
|
2044
|
+
if (result instanceof Promise) {
|
|
2045
|
+
result.catch((err) => {
|
|
2046
|
+
console.error(`EventBus handler error for ${event.kind}:`, err);
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
function awaitEvent(kind, predicate, timeoutMs) {
|
|
2053
|
+
return new Promise((resolve, reject) => {
|
|
2054
|
+
let disposer = () => {
|
|
2055
|
+
};
|
|
2056
|
+
const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
|
|
2057
|
+
disposer();
|
|
2058
|
+
reject(new Error(`await(${kind}) timed out after ${timeoutMs}ms`));
|
|
2059
|
+
}, timeoutMs) : void 0;
|
|
2060
|
+
const removeHandler = on(kind, (event) => {
|
|
2061
|
+
const match = predicate ? predicate(event) : true;
|
|
2062
|
+
if (match) {
|
|
2063
|
+
if (timer) clearTimeout(timer);
|
|
2064
|
+
removeHandler();
|
|
2065
|
+
resolve(event);
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
disposer = () => {
|
|
2069
|
+
if (timer) clearTimeout(timer);
|
|
2070
|
+
removeHandler();
|
|
2071
|
+
};
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
return {
|
|
2075
|
+
on,
|
|
2076
|
+
emit,
|
|
2077
|
+
await: awaitEvent
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
export {
|
|
2081
|
+
A2AError,
|
|
2082
|
+
ArtifactAccessDeniedError,
|
|
2083
|
+
ArtifactMetaSchema,
|
|
2084
|
+
ArtifactNotFoundError,
|
|
2085
|
+
ArtifactRegistry,
|
|
2086
|
+
ArtifactSchema,
|
|
2087
|
+
ArtifactTypeSchema,
|
|
2088
|
+
BrandKitSchema,
|
|
2089
|
+
BudgetConfigSchema,
|
|
2090
|
+
BudgetExceededError,
|
|
2091
|
+
CacheConfigSchema,
|
|
2092
|
+
ContextRefSchema,
|
|
2093
|
+
ContextRefTypeError,
|
|
2094
|
+
ContextRefUnknownError,
|
|
2095
|
+
CostRecordSchema,
|
|
2096
|
+
CostSummarySchema,
|
|
2097
|
+
CustomEvaluator,
|
|
2098
|
+
DimensionCheckEvaluator,
|
|
2099
|
+
EstimateUnsupportedError,
|
|
2100
|
+
FfmpegUnavailableError,
|
|
2101
|
+
FormatUnsupportedError,
|
|
2102
|
+
IdempotencyConflictError,
|
|
2103
|
+
InvalidInputError,
|
|
2104
|
+
InvalidResourceUriError,
|
|
2105
|
+
JudgeConfigSchema,
|
|
2106
|
+
JudgeRubricSchema,
|
|
2107
|
+
JudgeUnavailableError,
|
|
2108
|
+
KeyVaultUnavailableError,
|
|
2109
|
+
LLMJudgeEvaluator,
|
|
2110
|
+
LoudnessGateFailedError,
|
|
2111
|
+
MockProvider,
|
|
2112
|
+
PipelineDefinitionSchema,
|
|
2113
|
+
PipelineEstimator,
|
|
2114
|
+
PipelineEventSchema,
|
|
2115
|
+
PipelineEventTypeSchema,
|
|
2116
|
+
PipelineExecutor,
|
|
2117
|
+
PipelineSchema,
|
|
2118
|
+
PipelineStatusSchema,
|
|
2119
|
+
PipelineStepSchema,
|
|
2120
|
+
PipelineValidator,
|
|
2121
|
+
ProvenanceSigningFailedError,
|
|
2122
|
+
ProviderInputSchema,
|
|
2123
|
+
ProviderOutputSchema,
|
|
2124
|
+
QualityGateActionSchema,
|
|
2125
|
+
QualityGateResultSchema,
|
|
2126
|
+
QualityGateSchema,
|
|
2127
|
+
RatioUnsupportedError,
|
|
2128
|
+
RouteCandidateSchema,
|
|
2129
|
+
RouteConfigSchema,
|
|
2130
|
+
RouterAllCandidatesFailedError,
|
|
2131
|
+
RouterFastestIneligibleError,
|
|
2132
|
+
RouterNoCandidatesError,
|
|
2133
|
+
RunContextSchema,
|
|
2134
|
+
RunInProgressError,
|
|
2135
|
+
RunNotFoundError,
|
|
2136
|
+
RunNotResumableError,
|
|
2137
|
+
SafetyGateRejectedError,
|
|
2138
|
+
SafetyProviderUnavailableError,
|
|
2139
|
+
StateStoreUnavailableError,
|
|
2140
|
+
StorageResultSchema,
|
|
2141
|
+
StyleRefSchema,
|
|
2142
|
+
TenantNotFoundError,
|
|
2143
|
+
TenantPolicyViolationError,
|
|
2144
|
+
ThresholdEvaluator,
|
|
2145
|
+
ValidationResultSchema,
|
|
2146
|
+
VariantsAllRejectedError,
|
|
2147
|
+
VariantsConfigSchema,
|
|
2148
|
+
VoiceRefSchema,
|
|
2149
|
+
WebhookProviderUnknownError,
|
|
2150
|
+
WebhookSignatureInvalidError,
|
|
2151
|
+
WorkflowExpiredError,
|
|
2152
|
+
WorkflowNotFoundError,
|
|
2153
|
+
createEventBus,
|
|
2154
|
+
createQualityGateEvaluator,
|
|
2155
|
+
createStepStateRecord,
|
|
2156
|
+
mockOperations
|
|
2157
|
+
};
|