@proposit/proposit-core 1.10.0 → 1.11.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/dist/extensions/openai/index.d.ts +1 -1
- package/dist/extensions/openai/index.d.ts.map +1 -1
- package/dist/extensions/openai/index.js +1 -1
- package/dist/extensions/openai/index.js.map +1 -1
- package/dist/extensions/openai/provider.d.ts +49 -1
- package/dist/extensions/openai/provider.d.ts.map +1 -1
- package/dist/extensions/openai/provider.js +92 -0
- package/dist/extensions/openai/provider.js.map +1 -1
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/llm/index.d.ts +1 -1
- package/dist/lib/llm/index.d.ts.map +1 -1
- package/dist/lib/llm/types.d.ts +40 -0
- package/dist/lib/llm/types.d.ts.map +1 -1
- package/dist/lib/pipelines/execute.d.ts +175 -3
- package/dist/lib/pipelines/execute.d.ts.map +1 -1
- package/dist/lib/pipelines/execute.js +574 -219
- package/dist/lib/pipelines/execute.js.map +1 -1
- package/dist/lib/pipelines/index.d.ts +2 -2
- package/dist/lib/pipelines/index.d.ts.map +1 -1
- package/dist/lib/pipelines/index.js +1 -1
- package/dist/lib/pipelines/index.js.map +1 -1
- package/dist/lib/pipelines/stage-helpers.d.ts +80 -1
- package/dist/lib/pipelines/stage-helpers.d.ts.map +1 -1
- package/dist/lib/pipelines/stage-helpers.js +214 -31
- package/dist/lib/pipelines/stage-helpers.js.map +1 -1
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// with `output: null` for abort; we throw for misconfiguration).
|
|
15
15
|
import { Value } from "typebox/value";
|
|
16
16
|
import { depId, isOptionalDep } from "./types.js";
|
|
17
|
-
import { LlmStageRetryExhaustedError, StageAbortedError, SubPipelineFailedError, readStashedTokenUsage, } from "./stage-helpers.js";
|
|
17
|
+
import { LlmStageRetryExhaustedError, StageAbortedError, SubPipelineFailedError, readStashedTokenUsage, readLlmStageConfig, buildLlmRequest, applyRetrySuffix, validateLlmOutcome, failureRetryReason, } from "./stage-helpers.js";
|
|
18
18
|
import { debugPipelineEnd, debugPipelineStart, debugStageEnd, debugStageStart, } from "./debug-log.js";
|
|
19
19
|
export class PipelineConfigurationError extends Error {
|
|
20
20
|
code;
|
|
@@ -113,6 +113,242 @@ function validateDag(pipeline) {
|
|
|
113
113
|
}
|
|
114
114
|
return { stageById };
|
|
115
115
|
}
|
|
116
|
+
// Build the per-stage / finalize `ctx`. `allowedDeps` is the set of
|
|
117
|
+
// stage ids this context may read (the stage's or finalize's own
|
|
118
|
+
// `dependsOn`); `ctx.get` returns the output only for a `completed`
|
|
119
|
+
// upstream, exactly as the monolithic run does.
|
|
120
|
+
function makeStageContext(state, allowedDeps, contextLabel) {
|
|
121
|
+
return {
|
|
122
|
+
input: state.input,
|
|
123
|
+
get(stageId) {
|
|
124
|
+
if (!allowedDeps.has(stageId)) {
|
|
125
|
+
throw new PipelineConfigurationError({
|
|
126
|
+
code: "GET_OUTSIDE_DEPS",
|
|
127
|
+
message: `${contextLabel} called ctx.get("${stageId}"), which is not in its dependsOn.`,
|
|
128
|
+
stageId: contextLabel,
|
|
129
|
+
depId: stageId,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const record = state.records.get(stageId);
|
|
133
|
+
if (!record)
|
|
134
|
+
return undefined;
|
|
135
|
+
if (record.outcome !== "completed")
|
|
136
|
+
return undefined;
|
|
137
|
+
return record.output;
|
|
138
|
+
},
|
|
139
|
+
stageStatus(stageId) {
|
|
140
|
+
// Mirror the `ctx.get` strictness: stages may only
|
|
141
|
+
// query the status of stages declared in their own
|
|
142
|
+
// `dependsOn` (required OR optional). Querying a
|
|
143
|
+
// non-dependency is a caller bug — surface it loudly
|
|
144
|
+
// rather than silently returning "skipped" for a
|
|
145
|
+
// stage id the calling stage shouldn't be peeking at.
|
|
146
|
+
if (!allowedDeps.has(stageId)) {
|
|
147
|
+
throw new PipelineConfigurationError({
|
|
148
|
+
code: "STATUS_OUTSIDE_DEPS",
|
|
149
|
+
message: `${contextLabel} called ctx.stageStatus("${stageId}"), which is not in its dependsOn.`,
|
|
150
|
+
stageId: contextLabel,
|
|
151
|
+
depId: stageId,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const record = state.records.get(stageId);
|
|
155
|
+
if (record)
|
|
156
|
+
return record.outcome;
|
|
157
|
+
return "skipped";
|
|
158
|
+
},
|
|
159
|
+
llm: state.llm,
|
|
160
|
+
generateId: state.generateId,
|
|
161
|
+
signal: state.signal,
|
|
162
|
+
emit: state.emit,
|
|
163
|
+
addFailure: (failure) => {
|
|
164
|
+
state.failures.push({ ...failure, stage: contextLabel });
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Execute exactly one stage against the supplied `ctx` + `state`. Records
|
|
169
|
+
// the stage's outcome into `state.records`, pushes any failure into
|
|
170
|
+
// `state.failures`, emits the `stage:start` / `stage:end` bookends, and
|
|
171
|
+
// routes a `ctx.get`-on-non-dep `PipelineConfigurationError` through
|
|
172
|
+
// `state.setConfigError`. The single source of truth for per-stage
|
|
173
|
+
// execution semantics, shared by the scheduler and `executeStage`.
|
|
174
|
+
async function runOneStage(stage, ctx, state) {
|
|
175
|
+
const stageDeps = stage.dependsOn.map((d) => depId(d));
|
|
176
|
+
const stageStartAt = now();
|
|
177
|
+
const finishStage = (args) => {
|
|
178
|
+
const endAt = now();
|
|
179
|
+
const event = args.tokenUsage !== undefined
|
|
180
|
+
? {
|
|
181
|
+
kind: "stage:end",
|
|
182
|
+
stageId: stage.id,
|
|
183
|
+
status: args.status,
|
|
184
|
+
tokenUsage: args.tokenUsage,
|
|
185
|
+
at: endAt,
|
|
186
|
+
}
|
|
187
|
+
: {
|
|
188
|
+
kind: "stage:end",
|
|
189
|
+
stageId: stage.id,
|
|
190
|
+
status: args.status,
|
|
191
|
+
at: endAt,
|
|
192
|
+
};
|
|
193
|
+
state.emit(event);
|
|
194
|
+
debugStageEnd({
|
|
195
|
+
stageId: stage.id,
|
|
196
|
+
status: args.status,
|
|
197
|
+
durationMs: endAt - stageStartAt,
|
|
198
|
+
outputPresence: args.outputPresent
|
|
199
|
+
? "present"
|
|
200
|
+
: "null-or-undefined",
|
|
201
|
+
tokenUsage: args.tokenUsage,
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
if (state.signal.aborted) {
|
|
205
|
+
// Pending stages don't start once aborted. Emit `stage:start`
|
|
206
|
+
// before `stage:end` so consumers walking the event stream
|
|
207
|
+
// for symmetric pairs (e.g. a server SSE bridge) see
|
|
208
|
+
// a balanced sequence — every `stage:end` is preceded by a
|
|
209
|
+
// matching `stage:start`.
|
|
210
|
+
state.emit({ kind: "stage:start", stageId: stage.id, at: stageStartAt });
|
|
211
|
+
debugStageStart({ stageId: stage.id, deps: stageDeps });
|
|
212
|
+
state.records.set(stage.id, { outcome: "skipped", output: undefined });
|
|
213
|
+
finishStage({ status: "skipped", outputPresent: false });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
state.emit({ kind: "stage:start", stageId: stage.id, at: stageStartAt });
|
|
217
|
+
debugStageStart({ stageId: stage.id, deps: stageDeps });
|
|
218
|
+
try {
|
|
219
|
+
const output = await stage.run(ctx);
|
|
220
|
+
if (!Value.Check(stage.outputSchema, output)) {
|
|
221
|
+
const errors = [...Value.Errors(stage.outputSchema, output)];
|
|
222
|
+
const message = errors
|
|
223
|
+
.map((e) => `${e.instancePath}: ${e.message}`)
|
|
224
|
+
.join("; ");
|
|
225
|
+
state.failures.push({
|
|
226
|
+
stage: stage.id,
|
|
227
|
+
code: "OUTPUT_SCHEMA_INVALID",
|
|
228
|
+
message,
|
|
229
|
+
severity: "error",
|
|
230
|
+
});
|
|
231
|
+
state.records.set(stage.id, {
|
|
232
|
+
outcome: "failed",
|
|
233
|
+
output: undefined,
|
|
234
|
+
});
|
|
235
|
+
finishStage({ status: "failed", outputPresent: false });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const tokenUsage = readStashedTokenUsage(ctx, stage.id);
|
|
239
|
+
state.records.set(stage.id, {
|
|
240
|
+
outcome: "completed",
|
|
241
|
+
output,
|
|
242
|
+
tokenUsage,
|
|
243
|
+
});
|
|
244
|
+
finishStage({
|
|
245
|
+
status: "completed",
|
|
246
|
+
tokenUsage,
|
|
247
|
+
outputPresent: output !== null && output !== undefined,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
if (err instanceof PipelineConfigurationError) {
|
|
252
|
+
// ctx.get violation — caller bug. Route through the
|
|
253
|
+
// disposition seam, mark the stage failed for bookkeeping,
|
|
254
|
+
// and emit stage:end so consumers see a clean per-stage close.
|
|
255
|
+
state.records.set(stage.id, {
|
|
256
|
+
outcome: "failed",
|
|
257
|
+
output: undefined,
|
|
258
|
+
});
|
|
259
|
+
finishStage({ status: "failed", outputPresent: false });
|
|
260
|
+
state.setConfigError(err);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (err instanceof StageAbortedError) {
|
|
264
|
+
// Caller cancellation surfaced mid-stage. This is not
|
|
265
|
+
// a stage failure to report — no ProcessingFailure is
|
|
266
|
+
// recorded — and the outcome is `skipped` rather than
|
|
267
|
+
// `failed` so consumers can distinguish abort from a
|
|
268
|
+
// genuine provider error.
|
|
269
|
+
state.records.set(stage.id, {
|
|
270
|
+
outcome: "skipped",
|
|
271
|
+
output: undefined,
|
|
272
|
+
});
|
|
273
|
+
finishStage({ status: "skipped", outputPresent: false });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (err instanceof LlmStageRetryExhaustedError) {
|
|
277
|
+
state.failures.push({
|
|
278
|
+
stage: stage.id,
|
|
279
|
+
code: err.code,
|
|
280
|
+
message: err.message,
|
|
281
|
+
severity: "error",
|
|
282
|
+
context: err.failureContext,
|
|
283
|
+
});
|
|
284
|
+
state.records.set(stage.id, {
|
|
285
|
+
outcome: "failed",
|
|
286
|
+
output: undefined,
|
|
287
|
+
});
|
|
288
|
+
finishStage({ status: "failed", outputPresent: false });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (err instanceof SubPipelineFailedError) {
|
|
292
|
+
state.failures.push({
|
|
293
|
+
stage: stage.id,
|
|
294
|
+
code: err.code,
|
|
295
|
+
message: err.message,
|
|
296
|
+
severity: "error",
|
|
297
|
+
context: err.failureContext,
|
|
298
|
+
});
|
|
299
|
+
state.records.set(stage.id, {
|
|
300
|
+
outcome: "failed",
|
|
301
|
+
output: undefined,
|
|
302
|
+
});
|
|
303
|
+
finishStage({ status: "failed", outputPresent: false });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
307
|
+
state.failures.push({
|
|
308
|
+
stage: stage.id,
|
|
309
|
+
code: "STAGE_UNCAUGHT_ERROR",
|
|
310
|
+
message,
|
|
311
|
+
severity: "error",
|
|
312
|
+
});
|
|
313
|
+
state.records.set(stage.id, { outcome: "failed", output: undefined });
|
|
314
|
+
finishStage({ status: "failed", outputPresent: false });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Run the pipeline's finalize against the supplied `ctx` + `state`.
|
|
318
|
+
// Applies the `finalizeRequiredOk()` gate (output stays `null` when a
|
|
319
|
+
// required finalize dep is not `completed`) and captures a thrown
|
|
320
|
+
// finalize as a `FINALIZE_UNCAUGHT_ERROR` failure. The single source of
|
|
321
|
+
// truth for finalize semantics, shared by the scheduler and
|
|
322
|
+
// `executeFinalize`. Returns the finalize output, or `null` when the
|
|
323
|
+
// gate blocks it / the run is aborted / finalize threw.
|
|
324
|
+
function runFinalize(pipeline, ctx, state) {
|
|
325
|
+
const finalizeRequiredOk = () => {
|
|
326
|
+
for (const dep of pipeline.finalize.dependsOn) {
|
|
327
|
+
if (isOptionalDep(dep))
|
|
328
|
+
continue;
|
|
329
|
+
const record = state.records.get(depId(dep));
|
|
330
|
+
if (record?.outcome !== "completed")
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
};
|
|
335
|
+
if (!finalizeRequiredOk() || state.signal.aborted) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
return pipeline.finalize.run(ctx);
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
343
|
+
state.failures.push({
|
|
344
|
+
stage: "finalize",
|
|
345
|
+
code: "FINALIZE_UNCAUGHT_ERROR",
|
|
346
|
+
message,
|
|
347
|
+
severity: "error",
|
|
348
|
+
});
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
116
352
|
export async function executePipeline(pipeline, input, deps) {
|
|
117
353
|
// 1. Input validation. A schema mismatch is a caller bug; we throw.
|
|
118
354
|
// Value.Parse infers its return type from the schema; since
|
|
@@ -150,6 +386,22 @@ export async function executePipeline(pipeline, input, deps) {
|
|
|
150
386
|
// first one and re-throw after the scheduler drains so the
|
|
151
387
|
// executor still emits the bookend events.
|
|
152
388
|
let capturedConfigError = null;
|
|
389
|
+
// The explicit run state shared with the extracted `runOneStage` /
|
|
390
|
+
// `runFinalize` bodies. The scheduler's disposition for a
|
|
391
|
+
// `ctx.get`-on-non-dep config error is "capture the first, re-throw
|
|
392
|
+
// after the bookends" (see the drain-end re-throw below).
|
|
393
|
+
const state = {
|
|
394
|
+
records,
|
|
395
|
+
failures,
|
|
396
|
+
signal,
|
|
397
|
+
emit,
|
|
398
|
+
generateId,
|
|
399
|
+
llm: deps.llm,
|
|
400
|
+
input: validatedInput,
|
|
401
|
+
setConfigError: (error) => {
|
|
402
|
+
capturedConfigError ??= error;
|
|
403
|
+
},
|
|
404
|
+
};
|
|
153
405
|
// Build per-stage `ctx.get` dep sets up front so we can throw on
|
|
154
406
|
// out-of-deps access.
|
|
155
407
|
const stageDepIds = new Map();
|
|
@@ -158,55 +410,6 @@ export async function executePipeline(pipeline, input, deps) {
|
|
|
158
410
|
}
|
|
159
411
|
const finalizeDepIds = new Set(pipeline.finalize.dependsOn.map((d) => depId(d)));
|
|
160
412
|
// -- Helpers --
|
|
161
|
-
const makeCtx = (allowedDeps, contextLabel) => {
|
|
162
|
-
const ctx = {
|
|
163
|
-
input: validatedInput,
|
|
164
|
-
get(stageId) {
|
|
165
|
-
if (!allowedDeps.has(stageId)) {
|
|
166
|
-
throw new PipelineConfigurationError({
|
|
167
|
-
code: "GET_OUTSIDE_DEPS",
|
|
168
|
-
message: `${contextLabel} called ctx.get("${stageId}"), which is not in its dependsOn.`,
|
|
169
|
-
stageId: contextLabel,
|
|
170
|
-
depId: stageId,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
const record = records.get(stageId);
|
|
174
|
-
if (!record)
|
|
175
|
-
return undefined;
|
|
176
|
-
if (record.outcome !== "completed")
|
|
177
|
-
return undefined;
|
|
178
|
-
return record.output;
|
|
179
|
-
},
|
|
180
|
-
stageStatus(stageId) {
|
|
181
|
-
// Mirror the `ctx.get` strictness: stages may only
|
|
182
|
-
// query the status of stages declared in their own
|
|
183
|
-
// `dependsOn` (required OR optional). Querying a
|
|
184
|
-
// non-dependency is a caller bug — surface it loudly
|
|
185
|
-
// rather than silently returning "skipped" for a
|
|
186
|
-
// stage id the calling stage shouldn't be peeking at.
|
|
187
|
-
if (!allowedDeps.has(stageId)) {
|
|
188
|
-
throw new PipelineConfigurationError({
|
|
189
|
-
code: "STATUS_OUTSIDE_DEPS",
|
|
190
|
-
message: `${contextLabel} called ctx.stageStatus("${stageId}"), which is not in its dependsOn.`,
|
|
191
|
-
stageId: contextLabel,
|
|
192
|
-
depId: stageId,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
const record = records.get(stageId);
|
|
196
|
-
if (record)
|
|
197
|
-
return record.outcome;
|
|
198
|
-
return "skipped";
|
|
199
|
-
},
|
|
200
|
-
llm: deps.llm,
|
|
201
|
-
generateId,
|
|
202
|
-
signal,
|
|
203
|
-
emit,
|
|
204
|
-
addFailure: (failure) => {
|
|
205
|
-
failures.push({ ...failure, stage: contextLabel });
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
return ctx;
|
|
209
|
-
};
|
|
210
413
|
const isStageEligible = (stage) => {
|
|
211
414
|
for (const dep of stage.dependsOn) {
|
|
212
415
|
const id = depId(dep);
|
|
@@ -236,148 +439,8 @@ export async function executePipeline(pipeline, input, deps) {
|
|
|
236
439
|
};
|
|
237
440
|
// -- Stage execution --
|
|
238
441
|
const runStage = async (stage) => {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
const finishStage = (args) => {
|
|
242
|
-
const endAt = now();
|
|
243
|
-
const event = args.tokenUsage !== undefined
|
|
244
|
-
? {
|
|
245
|
-
kind: "stage:end",
|
|
246
|
-
stageId: stage.id,
|
|
247
|
-
status: args.status,
|
|
248
|
-
tokenUsage: args.tokenUsage,
|
|
249
|
-
at: endAt,
|
|
250
|
-
}
|
|
251
|
-
: {
|
|
252
|
-
kind: "stage:end",
|
|
253
|
-
stageId: stage.id,
|
|
254
|
-
status: args.status,
|
|
255
|
-
at: endAt,
|
|
256
|
-
};
|
|
257
|
-
emit(event);
|
|
258
|
-
debugStageEnd({
|
|
259
|
-
stageId: stage.id,
|
|
260
|
-
status: args.status,
|
|
261
|
-
durationMs: endAt - stageStartAt,
|
|
262
|
-
outputPresence: args.outputPresent
|
|
263
|
-
? "present"
|
|
264
|
-
: "null-or-undefined",
|
|
265
|
-
tokenUsage: args.tokenUsage,
|
|
266
|
-
});
|
|
267
|
-
};
|
|
268
|
-
if (signal.aborted) {
|
|
269
|
-
// Pending stages don't start once aborted. Emit `stage:start`
|
|
270
|
-
// before `stage:end` so consumers walking the event stream
|
|
271
|
-
// for symmetric pairs (e.g. a server SSE bridge) see
|
|
272
|
-
// a balanced sequence — every `stage:end` is preceded by a
|
|
273
|
-
// matching `stage:start`.
|
|
274
|
-
emit({ kind: "stage:start", stageId: stage.id, at: stageStartAt });
|
|
275
|
-
debugStageStart({ stageId: stage.id, deps: stageDeps });
|
|
276
|
-
records.set(stage.id, { outcome: "skipped", output: undefined });
|
|
277
|
-
finishStage({ status: "skipped", outputPresent: false });
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
emit({ kind: "stage:start", stageId: stage.id, at: stageStartAt });
|
|
281
|
-
debugStageStart({ stageId: stage.id, deps: stageDeps });
|
|
282
|
-
const ctx = makeCtx(stageDepIds.get(stage.id) ?? new Set(), stage.id);
|
|
283
|
-
try {
|
|
284
|
-
const output = await stage.run(ctx);
|
|
285
|
-
if (!Value.Check(stage.outputSchema, output)) {
|
|
286
|
-
const errors = [...Value.Errors(stage.outputSchema, output)];
|
|
287
|
-
const message = errors
|
|
288
|
-
.map((e) => `${e.instancePath}: ${e.message}`)
|
|
289
|
-
.join("; ");
|
|
290
|
-
failures.push({
|
|
291
|
-
stage: stage.id,
|
|
292
|
-
code: "OUTPUT_SCHEMA_INVALID",
|
|
293
|
-
message,
|
|
294
|
-
severity: "error",
|
|
295
|
-
});
|
|
296
|
-
records.set(stage.id, {
|
|
297
|
-
outcome: "failed",
|
|
298
|
-
output: undefined,
|
|
299
|
-
});
|
|
300
|
-
finishStage({ status: "failed", outputPresent: false });
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const tokenUsage = readStashedTokenUsage(ctx, stage.id);
|
|
304
|
-
records.set(stage.id, {
|
|
305
|
-
outcome: "completed",
|
|
306
|
-
output,
|
|
307
|
-
tokenUsage,
|
|
308
|
-
});
|
|
309
|
-
finishStage({
|
|
310
|
-
status: "completed",
|
|
311
|
-
tokenUsage,
|
|
312
|
-
outputPresent: output !== null && output !== undefined,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
catch (err) {
|
|
316
|
-
if (err instanceof PipelineConfigurationError) {
|
|
317
|
-
// ctx.get violation — caller bug. Stash for re-throw,
|
|
318
|
-
// mark the stage failed for bookkeeping, and emit
|
|
319
|
-
// stage:end so consumers see a clean per-stage close.
|
|
320
|
-
records.set(stage.id, {
|
|
321
|
-
outcome: "failed",
|
|
322
|
-
output: undefined,
|
|
323
|
-
});
|
|
324
|
-
finishStage({ status: "failed", outputPresent: false });
|
|
325
|
-
capturedConfigError ??= err;
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
if (err instanceof StageAbortedError) {
|
|
329
|
-
// Caller cancellation surfaced mid-stage. This is not
|
|
330
|
-
// a stage failure to report — no ProcessingFailure is
|
|
331
|
-
// recorded — and the outcome is `skipped` rather than
|
|
332
|
-
// `failed` so consumers can distinguish abort from a
|
|
333
|
-
// genuine provider error.
|
|
334
|
-
records.set(stage.id, {
|
|
335
|
-
outcome: "skipped",
|
|
336
|
-
output: undefined,
|
|
337
|
-
});
|
|
338
|
-
finishStage({ status: "skipped", outputPresent: false });
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (err instanceof LlmStageRetryExhaustedError) {
|
|
342
|
-
failures.push({
|
|
343
|
-
stage: stage.id,
|
|
344
|
-
code: err.code,
|
|
345
|
-
message: err.message,
|
|
346
|
-
severity: "error",
|
|
347
|
-
context: err.failureContext,
|
|
348
|
-
});
|
|
349
|
-
records.set(stage.id, {
|
|
350
|
-
outcome: "failed",
|
|
351
|
-
output: undefined,
|
|
352
|
-
});
|
|
353
|
-
finishStage({ status: "failed", outputPresent: false });
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
if (err instanceof SubPipelineFailedError) {
|
|
357
|
-
failures.push({
|
|
358
|
-
stage: stage.id,
|
|
359
|
-
code: err.code,
|
|
360
|
-
message: err.message,
|
|
361
|
-
severity: "error",
|
|
362
|
-
context: err.failureContext,
|
|
363
|
-
});
|
|
364
|
-
records.set(stage.id, {
|
|
365
|
-
outcome: "failed",
|
|
366
|
-
output: undefined,
|
|
367
|
-
});
|
|
368
|
-
finishStage({ status: "failed", outputPresent: false });
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
372
|
-
failures.push({
|
|
373
|
-
stage: stage.id,
|
|
374
|
-
code: "STAGE_UNCAUGHT_ERROR",
|
|
375
|
-
message,
|
|
376
|
-
severity: "error",
|
|
377
|
-
});
|
|
378
|
-
records.set(stage.id, { outcome: "failed", output: undefined });
|
|
379
|
-
finishStage({ status: "failed", outputPresent: false });
|
|
380
|
-
}
|
|
442
|
+
const ctx = makeStageContext(state, stageDepIds.get(stage.id) ?? new Set(), stage.id);
|
|
443
|
+
await runOneStage(stage, ctx, state);
|
|
381
444
|
};
|
|
382
445
|
// -- Scheduler loop --
|
|
383
446
|
//
|
|
@@ -499,33 +562,8 @@ export async function executePipeline(pipeline, input, deps) {
|
|
|
499
562
|
throw err;
|
|
500
563
|
}
|
|
501
564
|
// -- Finalize --
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
for (const dep of pipeline.finalize.dependsOn) {
|
|
505
|
-
if (isOptionalDep(dep))
|
|
506
|
-
continue;
|
|
507
|
-
const record = records.get(depId(dep));
|
|
508
|
-
if (record?.outcome !== "completed")
|
|
509
|
-
return false;
|
|
510
|
-
}
|
|
511
|
-
return true;
|
|
512
|
-
};
|
|
513
|
-
if (finalizeRequiredOk() && !signal.aborted) {
|
|
514
|
-
const finalizeCtx = makeCtx(finalizeDepIds, "finalize");
|
|
515
|
-
try {
|
|
516
|
-
output = pipeline.finalize.run(finalizeCtx);
|
|
517
|
-
}
|
|
518
|
-
catch (err) {
|
|
519
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
520
|
-
failures.push({
|
|
521
|
-
stage: "finalize",
|
|
522
|
-
code: "FINALIZE_UNCAUGHT_ERROR",
|
|
523
|
-
message,
|
|
524
|
-
severity: "error",
|
|
525
|
-
});
|
|
526
|
-
output = null;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
565
|
+
const finalizeCtx = makeStageContext(state, finalizeDepIds, "finalize");
|
|
566
|
+
const output = runFinalize(pipeline, finalizeCtx, state);
|
|
529
567
|
// Aggregate stage outcomes + token usage.
|
|
530
568
|
const stageOutcomes = {};
|
|
531
569
|
let aggregatedTokens;
|
|
@@ -568,4 +606,321 @@ export async function executePipeline(pipeline, input, deps) {
|
|
|
568
606
|
tokenUsage: aggregatedTokens,
|
|
569
607
|
};
|
|
570
608
|
}
|
|
609
|
+
// Seed a fresh `records` map from the caller-supplied `upstream`,
|
|
610
|
+
// keeping only the entries the consumer (a stage or finalize) actually
|
|
611
|
+
// depends on, and dropping `output` for any non-`completed` record so a
|
|
612
|
+
// caller bug can't leak a stale output into a skipped/failed dependency.
|
|
613
|
+
function seedRecordsFromUpstream(upstream, depIds) {
|
|
614
|
+
const records = new Map();
|
|
615
|
+
for (const id of depIds) {
|
|
616
|
+
const supplied = upstream[id];
|
|
617
|
+
if (!supplied)
|
|
618
|
+
continue;
|
|
619
|
+
if (supplied.outcome === "completed") {
|
|
620
|
+
records.set(id, {
|
|
621
|
+
outcome: "completed",
|
|
622
|
+
output: supplied.output,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
records.set(id, {
|
|
627
|
+
outcome: supplied.outcome,
|
|
628
|
+
output: undefined,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return records;
|
|
633
|
+
}
|
|
634
|
+
// Shared run-state builder for the single-shot entry points. The
|
|
635
|
+
// `setConfigError` disposition differs from the whole-DAG scheduler's:
|
|
636
|
+
// a `ctx.get`-on-non-dep error throws straight out of the entry point
|
|
637
|
+
// (there are no run-level bookends to emit first), so it is surfaced
|
|
638
|
+
// directly to the caller as the caller bug it is.
|
|
639
|
+
function buildSingleShotState(upstream, depIds, input, deps) {
|
|
640
|
+
return {
|
|
641
|
+
records: seedRecordsFromUpstream(upstream, depIds),
|
|
642
|
+
failures: [],
|
|
643
|
+
signal: deps.signal ?? new AbortController().signal,
|
|
644
|
+
emit: deps.onEvent ?? noopEmit,
|
|
645
|
+
generateId: deps.generateId ?? defaultGenerateId,
|
|
646
|
+
llm: deps.llm,
|
|
647
|
+
input,
|
|
648
|
+
setConfigError: (error) => {
|
|
649
|
+
throw error;
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Run a single stage of `pipeline` against the caller-supplied upstream
|
|
655
|
+
* records, without re-running the whole DAG. The upstream map carries
|
|
656
|
+
* each dependency's `{ outcome, output? }` so `ctx.get` / `ctx.stageStatus`
|
|
657
|
+
* reproduce monolithic-run semantics exactly. `input` is validated +
|
|
658
|
+
* transformed via `Value.Parse(pipeline.inputSchema, input)` (a schema
|
|
659
|
+
* mismatch throws, same as `executePipeline`) and the PARSED value seeds
|
|
660
|
+
* `ctx.input`.
|
|
661
|
+
*
|
|
662
|
+
* Emits the per-stage events only (`stage:start`, `stage:llm-request`,
|
|
663
|
+
* `stage:llm-response-created`, `stage:llm-call`, `stage:retry`,
|
|
664
|
+
* `stage:end`) — no `pipeline:*` bookends. Throws `PipelineConfigurationError`
|
|
665
|
+
* (`UNKNOWN_STAGE`) when `stageId` is not in `pipeline.stages`, and throws a
|
|
666
|
+
* `PipelineConfigurationError` (`GET_OUTSIDE_DEPS` / `STATUS_OUTSIDE_DEPS`)
|
|
667
|
+
* out directly when the stage reads a non-dependency — both are caller
|
|
668
|
+
* bugs, surfaced rather than swallowed into the result.
|
|
669
|
+
*
|
|
670
|
+
* The caller may pass a superset of `upstream` records; `executeStage`
|
|
671
|
+
* uses the stage's own `dependsOn` to pick the relevant ones. It does NOT
|
|
672
|
+
* decide whether the stage SHOULD run given its upstream outcomes — a
|
|
673
|
+
* required-failed upstream just means `ctx.get` returns `undefined`; the
|
|
674
|
+
* skip decision belongs to the caller's scheduler.
|
|
675
|
+
*/
|
|
676
|
+
export async function executeStage(pipeline, stageId, upstream, input, deps) {
|
|
677
|
+
const stage = pipeline.stages.find((s) => s.id === stageId);
|
|
678
|
+
if (!stage) {
|
|
679
|
+
throw new PipelineConfigurationError({
|
|
680
|
+
code: "UNKNOWN_STAGE",
|
|
681
|
+
message: `Pipeline "${pipeline.id}" has no stage "${stageId}".`,
|
|
682
|
+
stageId,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
// Input-validation parity with `executePipeline`: parse + seed the
|
|
686
|
+
// PARSED (Default/Convert/Clean-transformed) value into ctx.input.
|
|
687
|
+
const parsedInput = Value.Parse(pipeline.inputSchema, input);
|
|
688
|
+
const depIds = new Set(stage.dependsOn.map((d) => depId(d)));
|
|
689
|
+
const state = buildSingleShotState(upstream, depIds, parsedInput, deps);
|
|
690
|
+
const ctx = makeStageContext(state, depIds, stage.id);
|
|
691
|
+
await runOneStage(stage, ctx, state);
|
|
692
|
+
const record = state.records.get(stage.id);
|
|
693
|
+
const outcome = record?.outcome ?? "skipped";
|
|
694
|
+
const result = {
|
|
695
|
+
outcome,
|
|
696
|
+
failures: state.failures,
|
|
697
|
+
};
|
|
698
|
+
if (outcome === "completed") {
|
|
699
|
+
result.output = record?.output;
|
|
700
|
+
if (record?.tokenUsage !== undefined) {
|
|
701
|
+
result.tokenUsage = record.tokenUsage;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Run `pipeline.finalize` against the caller-supplied upstream records,
|
|
708
|
+
* without re-running the whole DAG. Symmetric with `executeStage`:
|
|
709
|
+
* `input` is parsed via `Value.Parse(pipeline.inputSchema, input)` and the
|
|
710
|
+
* PARSED value seeds the finalize `ctx.input`; the finalize `ctx` is built
|
|
711
|
+
* with `pipeline.finalize.dependsOn` as its allowed-dep set; the
|
|
712
|
+
* required-finalize-dep gate (`output` stays `null` if any required dep is
|
|
713
|
+
* not `completed`) and the `FINALIZE_UNCAUGHT_ERROR` capture match
|
|
714
|
+
* `executePipeline`.
|
|
715
|
+
*
|
|
716
|
+
* Emits NO events (finalize is not a stage — it has no `stage:*`
|
|
717
|
+
* lifecycle — and there are no `pipeline:*` bookends). `async` purely for
|
|
718
|
+
* signature symmetry with `executeStage`; `TPipelineFinalize.run` stays
|
|
719
|
+
* synchronous and the `async` wrapper just resolves its result.
|
|
720
|
+
*/
|
|
721
|
+
// `async` is deliberate (a Promise-returning signature symmetric with
|
|
722
|
+
// `executeStage`, so callers `await` both uniformly) even though the
|
|
723
|
+
// synchronous finalize body has nothing to await — the eslint
|
|
724
|
+
// require-await rule does not apply here.
|
|
725
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
726
|
+
export async function executeFinalize(pipeline, upstream, input, deps) {
|
|
727
|
+
// Input-validation parity with `executePipeline` (see `executeStage`).
|
|
728
|
+
const parsedInput = Value.Parse(pipeline.inputSchema, input);
|
|
729
|
+
const depIds = new Set(pipeline.finalize.dependsOn.map((d) => depId(d)));
|
|
730
|
+
// Finalize emits no events, so swallow any caller-supplied onEvent.
|
|
731
|
+
const state = buildSingleShotState(upstream, depIds, parsedInput, {
|
|
732
|
+
...deps,
|
|
733
|
+
onEvent: undefined,
|
|
734
|
+
});
|
|
735
|
+
const ctx = makeStageContext(state, depIds, "finalize");
|
|
736
|
+
const output = runFinalize(pipeline, ctx, state);
|
|
737
|
+
return { output, failures: state.failures };
|
|
738
|
+
}
|
|
739
|
+
// -- Launch / complete split for LLM-background stages -------------------
|
|
740
|
+
//
|
|
741
|
+
// A durable orchestrator (e.g. a server workflow) cannot block a single
|
|
742
|
+
// step for an LLM call's full duration. `launchStage` submits the
|
|
743
|
+
// background response and returns its `responseId` WITHOUT awaiting;
|
|
744
|
+
// `completeStage` — in a later invocation, after the response completed —
|
|
745
|
+
// validates the retrieved response into a `TExecuteStageResult`. Both
|
|
746
|
+
// reuse the package-internal `llmStage` seam (`buildLlmRequest` /
|
|
747
|
+
// `validateLlmOutcome`) so prompt assembly + output validation have a
|
|
748
|
+
// single implementation shared with the in-process `llmStage` loop.
|
|
749
|
+
// Resolve the LLM config carried by an `llmStage`-built stage, or throw a
|
|
750
|
+
// clear error when the looked-up stage is not an LLM stage (no carrier).
|
|
751
|
+
function requireLlmStage(pipeline, stageId, fnName) {
|
|
752
|
+
const stage = pipeline.stages.find((s) => s.id === stageId);
|
|
753
|
+
if (!stage) {
|
|
754
|
+
throw new PipelineConfigurationError({
|
|
755
|
+
code: "UNKNOWN_STAGE",
|
|
756
|
+
message: `Pipeline "${pipeline.id}" has no stage "${stageId}".`,
|
|
757
|
+
stageId,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
const cfg = readLlmStageConfig(stage);
|
|
761
|
+
if (!cfg) {
|
|
762
|
+
throw new PipelineConfigurationError({
|
|
763
|
+
code: "UNKNOWN_STAGE",
|
|
764
|
+
message: `${fnName} requires an LLM stage, but stage "${stageId}" in pipeline "${pipeline.id}" is not one (it carries no LLM config). Run deterministic stages via executeStage.`,
|
|
765
|
+
stageId,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
return { stage, cfg };
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Launch an LLM-background stage: rehydrate `ctx` from `upstream` +
|
|
772
|
+
* parsed input, build the request via the shared seam, submit it via the
|
|
773
|
+
* injected `deps.submitBackgroundResponse`, and return
|
|
774
|
+
* `{ responseId, status }` WITHOUT awaiting completion.
|
|
775
|
+
*
|
|
776
|
+
* Emits `stage:start`, `stage:llm-request`, and `stage:llm-response-created`
|
|
777
|
+
* (from the submit's returned id) — but NO `stage:llm-call` / `stage:end`
|
|
778
|
+
* (the completion side emits those, in a later invocation). The
|
|
779
|
+
* per-stage event pair therefore spans two invocations; an `onEvent`
|
|
780
|
+
* consumer must NOT assume a balanced start↔end per call.
|
|
781
|
+
*
|
|
782
|
+
* `deps.submitBackgroundResponse` is REQUIRED; `launchStage` throws if it
|
|
783
|
+
* is absent. `stageId` must name an LLM stage (built by `llmStage`);
|
|
784
|
+
* a non-LLM stage throws. `attempt` (default 1) lets a re-launch rebuild
|
|
785
|
+
* the retry-suffixed user message for attempt 2+.
|
|
786
|
+
*/
|
|
787
|
+
export async function launchStage(pipeline, stageId, upstream, input, deps, attempt = 1) {
|
|
788
|
+
const submit = deps.submitBackgroundResponse;
|
|
789
|
+
if (!submit) {
|
|
790
|
+
throw new PipelineConfigurationError({
|
|
791
|
+
code: "UNKNOWN_STAGE",
|
|
792
|
+
message: `launchStage requires deps.submitBackgroundResponse (the submit-only background-response capability), but it was not supplied.`,
|
|
793
|
+
stageId,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
const { stage, cfg } = requireLlmStage(pipeline, stageId, "launchStage");
|
|
797
|
+
// Input-validation parity: parse + seed the parsed ctx.input. The
|
|
798
|
+
// stage's `ctx` reads its own dependsOn as allowed deps.
|
|
799
|
+
const parsedInput = Value.Parse(pipeline.inputSchema, input);
|
|
800
|
+
const allowedDeps = new Set(stage.dependsOn.map((d) => depId(d)));
|
|
801
|
+
const state = buildSingleShotState(upstream, allowedDeps, parsedInput, deps);
|
|
802
|
+
const ctx = makeStageContext(state, allowedDeps, stageId);
|
|
803
|
+
// Compute the per-attempt user message. Attempt 1 is the prompt's
|
|
804
|
+
// user message; attempt 2+ appends the same retry-suffix the
|
|
805
|
+
// in-process loop adds after a schema-validation failure (the shared
|
|
806
|
+
// `applyRetrySuffix` helper). The exact prior-attempt validation
|
|
807
|
+
// error does not cross the durable suspend, so the re-launch suffix
|
|
808
|
+
// carries a generic prior-error note — the wrapper text matches the
|
|
809
|
+
// in-process loop's phrasing.
|
|
810
|
+
const baseUser = cfg.buildPrompt(ctx).user;
|
|
811
|
+
const userMessage = attempt > 1
|
|
812
|
+
? applyRetrySuffix(baseUser, "the previous attempt's output did not conform to the schema", cfg.retryPolicy.maxAppendedErrorBytes ?? 2048)
|
|
813
|
+
: baseUser;
|
|
814
|
+
const { req } = buildLlmRequest(cfg, ctx, userMessage);
|
|
815
|
+
state.emit({ kind: "stage:start", stageId, at: now() });
|
|
816
|
+
state.emit({
|
|
817
|
+
kind: "stage:llm-request",
|
|
818
|
+
stageId,
|
|
819
|
+
attempt,
|
|
820
|
+
prompts: { system: req.systemPrompt, user: req.userMessage },
|
|
821
|
+
at: now(),
|
|
822
|
+
});
|
|
823
|
+
// The req is already TLlmRequest<unknown> (the recovered config is
|
|
824
|
+
// generic-erased at the lookup boundary); the typed output is
|
|
825
|
+
// recovered in completeStage via the stage's outputSchema.
|
|
826
|
+
const submitResult = await submit(req, {
|
|
827
|
+
apiKey: resolveApiKey(deps),
|
|
828
|
+
signal: deps.signal,
|
|
829
|
+
});
|
|
830
|
+
state.emit({
|
|
831
|
+
kind: "stage:llm-response-created",
|
|
832
|
+
stageId,
|
|
833
|
+
attempt,
|
|
834
|
+
responseId: submitResult.responseId,
|
|
835
|
+
at: now(),
|
|
836
|
+
});
|
|
837
|
+
return submitResult;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Complete an LLM-background stage from its retrieved response. Recovers
|
|
841
|
+
* the stage's LLM config, parses the RAW assistant text in
|
|
842
|
+
* `retrieved.output` against the stage's schema (via the shared seam),
|
|
843
|
+
* classifies a non-`completed` status per the launch/complete table, and
|
|
844
|
+
* returns the standard `TExecuteStageResult`.
|
|
845
|
+
*
|
|
846
|
+
* Emits `stage:llm-call` + `stage:end` (NO `stage:start` — that fired in
|
|
847
|
+
* the launch invocation). `tokenUsage` is taken directly from
|
|
848
|
+
* `retrieved.tokenUsage` (the per-`ctx` WeakMap cannot bridge the two
|
|
849
|
+
* invocations). On a RETRYABLE failure the result carries `retryReason`
|
|
850
|
+
* (the reason code); a fail-fast failure (`failed` envelope,
|
|
851
|
+
* `content_filter`) carries none; a `cancelled` response settles as
|
|
852
|
+
* `outcome: "skipped"` with no `ProcessingFailure`.
|
|
853
|
+
*
|
|
854
|
+
* `stageId` must name an LLM stage; a non-LLM stage throws.
|
|
855
|
+
*/
|
|
856
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
857
|
+
export async function completeStage(pipeline, stageId, retrieved, deps, attempt = 1) {
|
|
858
|
+
const { cfg } = requireLlmStage(pipeline, stageId, "completeStage");
|
|
859
|
+
const emit = deps.onEvent ?? noopEmit;
|
|
860
|
+
const validated = validateLlmOutcome(cfg, retrieved.output, retrieved.status, retrieved.incompleteReason);
|
|
861
|
+
// The output shown on stage:llm-call is the parsed value when the
|
|
862
|
+
// response parsed + validated; otherwise the raw assistant text (so a
|
|
863
|
+
// consumer's bridge can persist whatever the model returned).
|
|
864
|
+
const callOutput = validated.output !== undefined ? validated.output : retrieved.output;
|
|
865
|
+
emit({
|
|
866
|
+
kind: "stage:llm-call",
|
|
867
|
+
stageId,
|
|
868
|
+
attempt,
|
|
869
|
+
prompts: { system: "", user: "" },
|
|
870
|
+
output: callOutput,
|
|
871
|
+
tokenUsage: retrieved.tokenUsage ?? { input: 0, output: 0 },
|
|
872
|
+
rawResponseId: retrieved.rawResponseId,
|
|
873
|
+
validationError: validated.validationError,
|
|
874
|
+
at: now(),
|
|
875
|
+
});
|
|
876
|
+
const failures = [];
|
|
877
|
+
let retryReason;
|
|
878
|
+
if (validated.outcome === "failed" && validated.failure) {
|
|
879
|
+
// A cancelled response settled as `skipped` above (no failure);
|
|
880
|
+
// only genuine failures push a ProcessingFailure.
|
|
881
|
+
failures.push({
|
|
882
|
+
stage: stageId,
|
|
883
|
+
code: validated.failure.code,
|
|
884
|
+
message: validated.failure.message,
|
|
885
|
+
severity: "error",
|
|
886
|
+
});
|
|
887
|
+
retryReason = failureRetryReason(validated.failure);
|
|
888
|
+
}
|
|
889
|
+
const stageEndEvent = retrieved.tokenUsage !== undefined
|
|
890
|
+
? {
|
|
891
|
+
kind: "stage:end",
|
|
892
|
+
stageId,
|
|
893
|
+
status: validated.outcome,
|
|
894
|
+
tokenUsage: retrieved.tokenUsage,
|
|
895
|
+
at: now(),
|
|
896
|
+
}
|
|
897
|
+
: {
|
|
898
|
+
kind: "stage:end",
|
|
899
|
+
stageId,
|
|
900
|
+
status: validated.outcome,
|
|
901
|
+
at: now(),
|
|
902
|
+
};
|
|
903
|
+
emit(stageEndEvent);
|
|
904
|
+
const result = {
|
|
905
|
+
outcome: validated.outcome,
|
|
906
|
+
failures,
|
|
907
|
+
};
|
|
908
|
+
if (validated.outcome === "completed") {
|
|
909
|
+
result.output = validated.output;
|
|
910
|
+
}
|
|
911
|
+
if (retrieved.tokenUsage !== undefined) {
|
|
912
|
+
result.tokenUsage = retrieved.tokenUsage;
|
|
913
|
+
}
|
|
914
|
+
if (retryReason !== undefined) {
|
|
915
|
+
result.retryReason = retryReason;
|
|
916
|
+
}
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
// API-key resolution for the submit dep. The injected
|
|
920
|
+
// `submitBackgroundResponse` is apiKey-bound by the consumer, so core
|
|
921
|
+
// passes an empty key — the bound capability ignores it. (Kept as a seam
|
|
922
|
+
// in case a future dep shape threads the key through deps.)
|
|
923
|
+
function resolveApiKey(_deps) {
|
|
924
|
+
return "";
|
|
925
|
+
}
|
|
571
926
|
//# sourceMappingURL=execute.js.map
|