@proposit/proposit-core 1.10.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/expressions.d.ts +29 -0
  3. package/dist/cli/commands/expressions.d.ts.map +1 -1
  4. package/dist/cli/commands/expressions.js +118 -92
  5. package/dist/cli/commands/expressions.js.map +1 -1
  6. package/dist/extensions/argument-ingestion/shared/finalize-response-v2.d.ts.map +1 -1
  7. package/dist/extensions/argument-ingestion/shared/finalize-response-v2.js +18 -2
  8. package/dist/extensions/argument-ingestion/shared/finalize-response-v2.js.map +1 -1
  9. package/dist/extensions/{ieee → citations/ieee}/citation-claim.d.ts +53 -59
  10. package/dist/extensions/{ieee → citations/ieee}/citation-claim.d.ts.map +1 -1
  11. package/dist/extensions/{ieee → citations/ieee}/citation-claim.js +2 -2
  12. package/dist/extensions/citations/ieee/citation-claim.js.map +1 -0
  13. package/dist/extensions/citations/ieee/formatting.d.ts.map +1 -0
  14. package/dist/extensions/{ieee → citations/ieee}/formatting.js +0 -1
  15. package/dist/extensions/citations/ieee/formatting.js.map +1 -0
  16. package/dist/extensions/citations/ieee/index.d.ts.map +1 -0
  17. package/dist/extensions/citations/ieee/index.js.map +1 -0
  18. package/dist/extensions/{ieee → citations/ieee}/references.d.ts +161 -181
  19. package/dist/extensions/citations/ieee/references.d.ts.map +1 -0
  20. package/dist/extensions/{ieee → citations/ieee}/references.js +48 -22
  21. package/dist/extensions/citations/ieee/references.js.map +1 -0
  22. package/dist/extensions/{ieee → citations/ieee}/relaxed.d.ts +159 -180
  23. package/dist/extensions/citations/ieee/relaxed.d.ts.map +1 -0
  24. package/dist/extensions/{ieee → citations/ieee}/relaxed.js +1 -4
  25. package/dist/extensions/{ieee → citations/ieee}/relaxed.js.map +1 -1
  26. package/dist/extensions/citations/ieee/segment-builder.d.ts.map +1 -0
  27. package/dist/extensions/citations/ieee/segment-builder.js.map +1 -0
  28. package/dist/extensions/{ieee → citations/ieee}/segment-templates.d.ts +0 -1
  29. package/dist/extensions/citations/ieee/segment-templates.d.ts.map +1 -0
  30. package/dist/extensions/{ieee → citations/ieee}/segment-templates.js +0 -27
  31. package/dist/extensions/citations/ieee/segment-templates.js.map +1 -0
  32. package/dist/extensions/citations/unparsed/index.d.ts +2 -0
  33. package/dist/extensions/citations/unparsed/index.d.ts.map +1 -0
  34. package/dist/extensions/citations/unparsed/index.js +2 -0
  35. package/dist/extensions/citations/unparsed/index.js.map +1 -0
  36. package/dist/extensions/citations/unparsed/unparsed-citation.d.ts +11 -0
  37. package/dist/extensions/citations/unparsed/unparsed-citation.d.ts.map +1 -0
  38. package/dist/extensions/citations/unparsed/unparsed-citation.js +22 -0
  39. package/dist/extensions/citations/unparsed/unparsed-citation.js.map +1 -0
  40. package/dist/extensions/openai/errors.d.ts +8 -0
  41. package/dist/extensions/openai/errors.d.ts.map +1 -1
  42. package/dist/extensions/openai/errors.js +58 -0
  43. package/dist/extensions/openai/errors.js.map +1 -1
  44. package/dist/extensions/openai/index.d.ts +4 -2
  45. package/dist/extensions/openai/index.d.ts.map +1 -1
  46. package/dist/extensions/openai/index.js +2 -1
  47. package/dist/extensions/openai/index.js.map +1 -1
  48. package/dist/extensions/openai/openai-http.d.ts +30 -0
  49. package/dist/extensions/openai/openai-http.d.ts.map +1 -0
  50. package/dist/extensions/openai/openai-http.js +310 -0
  51. package/dist/extensions/openai/openai-http.js.map +1 -0
  52. package/dist/extensions/openai/openai-parsing.d.ts +34 -0
  53. package/dist/extensions/openai/openai-parsing.d.ts.map +1 -0
  54. package/dist/extensions/openai/openai-parsing.js +226 -0
  55. package/dist/extensions/openai/openai-parsing.js.map +1 -0
  56. package/dist/extensions/openai/openai-retrieval.d.ts +150 -0
  57. package/dist/extensions/openai/openai-retrieval.d.ts.map +1 -0
  58. package/dist/extensions/openai/openai-retrieval.js +248 -0
  59. package/dist/extensions/openai/openai-retrieval.js.map +1 -0
  60. package/dist/extensions/openai/openai-tools.d.ts +9 -0
  61. package/dist/extensions/openai/openai-tools.d.ts.map +1 -0
  62. package/dist/extensions/openai/openai-tools.js +93 -0
  63. package/dist/extensions/openai/openai-tools.js.map +1 -0
  64. package/dist/extensions/openai/provider.d.ts +1 -100
  65. package/dist/extensions/openai/provider.d.ts.map +1 -1
  66. package/dist/extensions/openai/provider.js +5 -794
  67. package/dist/extensions/openai/provider.js.map +1 -1
  68. package/dist/extensions/openai/types.d.ts +1 -0
  69. package/dist/extensions/openai/types.d.ts.map +1 -1
  70. package/dist/extensions/openai/types.js +4 -1
  71. package/dist/extensions/openai/types.js.map +1 -1
  72. package/dist/lib/core/premise-engine.d.ts +15 -0
  73. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  74. package/dist/lib/core/premise-engine.js +62 -127
  75. package/dist/lib/core/premise-engine.js.map +1 -1
  76. package/dist/lib/index.d.ts +3 -3
  77. package/dist/lib/index.d.ts.map +1 -1
  78. package/dist/lib/index.js +1 -1
  79. package/dist/lib/index.js.map +1 -1
  80. package/dist/lib/llm/index.d.ts +1 -1
  81. package/dist/lib/llm/index.d.ts.map +1 -1
  82. package/dist/lib/llm/types.d.ts +40 -0
  83. package/dist/lib/llm/types.d.ts.map +1 -1
  84. package/dist/lib/parsing/prompt-builder.d.ts.map +1 -1
  85. package/dist/lib/parsing/prompt-builder.js +14 -3
  86. package/dist/lib/parsing/prompt-builder.js.map +1 -1
  87. package/dist/lib/pipelines/execute.d.ts +175 -3
  88. package/dist/lib/pipelines/execute.d.ts.map +1 -1
  89. package/dist/lib/pipelines/execute.js +574 -219
  90. package/dist/lib/pipelines/execute.js.map +1 -1
  91. package/dist/lib/pipelines/index.d.ts +3 -3
  92. package/dist/lib/pipelines/index.d.ts.map +1 -1
  93. package/dist/lib/pipelines/index.js +2 -2
  94. package/dist/lib/pipelines/index.js.map +1 -1
  95. package/dist/lib/pipelines/stage-helpers.d.ts +89 -1
  96. package/dist/lib/pipelines/stage-helpers.d.ts.map +1 -1
  97. package/dist/lib/pipelines/stage-helpers.js +225 -31
  98. package/dist/lib/pipelines/stage-helpers.js.map +1 -1
  99. package/package.json +10 -5
  100. package/dist/extensions/ieee/citation-claim.js.map +0 -1
  101. package/dist/extensions/ieee/formatting.d.ts.map +0 -1
  102. package/dist/extensions/ieee/formatting.js.map +0 -1
  103. package/dist/extensions/ieee/index.d.ts.map +0 -1
  104. package/dist/extensions/ieee/index.js.map +0 -1
  105. package/dist/extensions/ieee/references.d.ts.map +0 -1
  106. package/dist/extensions/ieee/references.js.map +0 -1
  107. package/dist/extensions/ieee/relaxed.d.ts.map +0 -1
  108. package/dist/extensions/ieee/segment-builder.d.ts.map +0 -1
  109. package/dist/extensions/ieee/segment-builder.js.map +0 -1
  110. package/dist/extensions/ieee/segment-templates.d.ts.map +0 -1
  111. package/dist/extensions/ieee/segment-templates.js.map +0 -1
  112. /package/dist/extensions/{ieee → citations/ieee}/formatting.d.ts +0 -0
  113. /package/dist/extensions/{ieee → citations/ieee}/index.d.ts +0 -0
  114. /package/dist/extensions/{ieee → citations/ieee}/index.js +0 -0
  115. /package/dist/extensions/{ieee → citations/ieee}/segment-builder.d.ts +0 -0
  116. /package/dist/extensions/{ieee → citations/ieee}/segment-builder.js +0 -0
@@ -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 stageDeps = stage.dependsOn.map((d) => depId(d));
240
- const stageStartAt = now();
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
- let output = null;
503
- const finalizeRequiredOk = () => {
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