@dogpile/sdk 0.3.1 → 0.4.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 (56) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +1 -0
  3. package/dist/browser/index.js +1595 -54
  4. package/dist/browser/index.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/providers/openai-compatible.d.ts +11 -0
  8. package/dist/providers/openai-compatible.d.ts.map +1 -1
  9. package/dist/providers/openai-compatible.js +87 -2
  10. package/dist/providers/openai-compatible.js.map +1 -1
  11. package/dist/runtime/cancellation.d.ts +26 -0
  12. package/dist/runtime/cancellation.d.ts.map +1 -1
  13. package/dist/runtime/cancellation.js +38 -1
  14. package/dist/runtime/cancellation.js.map +1 -1
  15. package/dist/runtime/coordinator.d.ts +74 -1
  16. package/dist/runtime/coordinator.d.ts.map +1 -1
  17. package/dist/runtime/coordinator.js +932 -25
  18. package/dist/runtime/coordinator.js.map +1 -1
  19. package/dist/runtime/decisions.d.ts +25 -3
  20. package/dist/runtime/decisions.d.ts.map +1 -1
  21. package/dist/runtime/decisions.js +241 -3
  22. package/dist/runtime/decisions.js.map +1 -1
  23. package/dist/runtime/defaults.d.ts +37 -1
  24. package/dist/runtime/defaults.d.ts.map +1 -1
  25. package/dist/runtime/defaults.js +347 -0
  26. package/dist/runtime/defaults.js.map +1 -1
  27. package/dist/runtime/engine.d.ts.map +1 -1
  28. package/dist/runtime/engine.js +254 -24
  29. package/dist/runtime/engine.js.map +1 -1
  30. package/dist/runtime/sequential.d.ts.map +1 -1
  31. package/dist/runtime/sequential.js +8 -1
  32. package/dist/runtime/sequential.js.map +1 -1
  33. package/dist/runtime/validation.d.ts +10 -0
  34. package/dist/runtime/validation.d.ts.map +1 -1
  35. package/dist/runtime/validation.js +73 -0
  36. package/dist/runtime/validation.js.map +1 -1
  37. package/dist/types/events.d.ts +329 -8
  38. package/dist/types/events.d.ts.map +1 -1
  39. package/dist/types/replay.d.ts +5 -1
  40. package/dist/types/replay.d.ts.map +1 -1
  41. package/dist/types.d.ts +131 -5
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/index.ts +10 -0
  46. package/src/providers/openai-compatible.ts +82 -3
  47. package/src/runtime/cancellation.ts +59 -1
  48. package/src/runtime/coordinator.ts +1170 -25
  49. package/src/runtime/decisions.ts +307 -4
  50. package/src/runtime/defaults.ts +376 -0
  51. package/src/runtime/engine.ts +363 -24
  52. package/src/runtime/sequential.ts +9 -1
  53. package/src/runtime/validation.ts +81 -0
  54. package/src/types/events.ts +359 -8
  55. package/src/types/replay.ts +12 -1
  56. package/src/types.ts +147 -3
@@ -1,6 +1,8 @@
1
1
  import { DogpileError } from "../types.js";
2
2
  import type {
3
+ AbortedEvent,
3
4
  BudgetTier,
5
+ DogpileErrorCode,
4
6
  DogpileOptions,
5
7
  Engine,
6
8
  EngineOptions,
@@ -8,9 +10,11 @@ import type {
8
10
  JsonObject,
9
11
  JsonValue,
10
12
  ProtocolSelection,
13
+ RunCallOptions,
11
14
  RunEvaluation,
12
15
  RunEvent,
13
16
  RunResult,
17
+ SubRunFailedEvent,
14
18
  StreamErrorEvent,
15
19
  StreamEvent,
16
20
  StreamEventSubscriber,
@@ -19,7 +23,7 @@ import type {
19
23
  Trace
20
24
  } from "../types.js";
21
25
  import { runBroadcast } from "./broadcast.js";
22
- import { runCoordinator } from "./coordinator.js";
26
+ import { runCoordinator, type AbortDrainFn } from "./coordinator.js";
23
27
  import {
24
28
  createReplayTraceFinalOutput,
25
29
  createReplayTraceBudgetStateChanges,
@@ -32,13 +36,29 @@ import {
32
36
  defaultAgents,
33
37
  normalizeProtocol,
34
38
  orderAgentsForTemperature,
39
+ recomputeAccountingFromTrace,
40
+ resolveOnChildFailure,
35
41
  tierTemperature
36
42
  } from "./defaults.js";
37
43
  import { runSequential } from "./sequential.js";
38
44
  import { runShared } from "./shared.js";
39
- import { createAbortErrorFromSignal, createTimeoutError } from "./cancellation.js";
45
+ import {
46
+ classifyChildTimeoutSource,
47
+ createAbortErrorFromSignal,
48
+ createEngineDeadlineTimeoutError,
49
+ createTimeoutError
50
+ } from "./cancellation.js";
40
51
  import { budget as budgetCondition } from "./termination.js";
41
- import { validateDogpileOptions, validateEngineOptions, validateMissionIntent } from "./validation.js";
52
+ import {
53
+ validateDogpileOptions,
54
+ validateEngineOptions,
55
+ validateMissionIntent,
56
+ validateProviderLocality,
57
+ validateRunCallOptions
58
+ } from "./validation.js";
59
+
60
+ const DEFAULT_MAX_DEPTH = 4;
61
+ const DEFAULT_MAX_CONCURRENT_CHILDREN = 4;
42
62
 
43
63
  const defaultHighLevelProtocol = "sequential";
44
64
  const defaultHighLevelTier = "balanced";
@@ -67,10 +87,34 @@ export function createEngine(options: EngineOptions): Engine {
67
87
  const temperature = options.temperature ?? tierTemperature(options.tier);
68
88
  const agents = orderAgentsForTemperature(options.agents ?? defaultAgents(), temperature, options.seed);
69
89
  const terminate = options.terminate ?? (options.budget ? conditionFromBudget(options.budget) : undefined);
90
+ const engineMaxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
91
+ const engineMaxConcurrentChildren = options.maxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN;
92
+ const engineOnChildFailure = options.onChildFailure;
70
93
 
71
94
  return {
72
- run(intent: string): Promise<RunResult> {
95
+ run(intent: string, runOptions?: RunCallOptions): Promise<RunResult> {
73
96
  validateMissionIntent(intent);
97
+ validateRunCallOptions(runOptions);
98
+ validateProviderLocality(options.model, "model");
99
+
100
+ const effectiveMaxDepth = Math.min(
101
+ engineMaxDepth,
102
+ runOptions?.maxDepth ?? Number.POSITIVE_INFINITY
103
+ );
104
+ assertRunDoesNotRaiseEngineMax(
105
+ "maxConcurrentChildren",
106
+ runOptions?.maxConcurrentChildren,
107
+ engineMaxConcurrentChildren
108
+ );
109
+ const effectiveMaxConcurrentChildren = Math.min(
110
+ engineMaxConcurrentChildren,
111
+ runOptions?.maxConcurrentChildren ?? Number.POSITIVE_INFINITY
112
+ );
113
+ const onChildFailure = resolveOnChildFailure(runOptions?.onChildFailure, engineOnChildFailure);
114
+
115
+ const startedAtMs = Date.now();
116
+ const parentDeadlineMs =
117
+ options.budget?.timeoutMs !== undefined ? startedAtMs + options.budget.timeoutMs : undefined;
74
118
 
75
119
  return runNonStreamingProtocol({
76
120
  intent,
@@ -85,12 +129,37 @@ export function createEngine(options: EngineOptions): Engine {
85
129
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
86
130
  ...(terminate ? { terminate } : {}),
87
131
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
88
- ...(options.evaluate ? { evaluate: options.evaluate } : {})
132
+ ...(options.evaluate ? { evaluate: options.evaluate } : {}),
133
+ currentDepth: 0,
134
+ effectiveMaxDepth,
135
+ effectiveMaxConcurrentChildren,
136
+ onChildFailure,
137
+ ...(parentDeadlineMs !== undefined ? { parentDeadlineMs } : {}),
138
+ ...(options.defaultSubRunTimeoutMs !== undefined
139
+ ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs }
140
+ : {})
89
141
  });
90
142
  },
91
143
 
92
- stream(intent: string): StreamHandle {
144
+ stream(intent: string, runOptions?: RunCallOptions): StreamHandle {
93
145
  validateMissionIntent(intent);
146
+ validateRunCallOptions(runOptions);
147
+ validateProviderLocality(options.model, "model");
148
+
149
+ const effectiveMaxDepth = Math.min(
150
+ engineMaxDepth,
151
+ runOptions?.maxDepth ?? Number.POSITIVE_INFINITY
152
+ );
153
+ assertRunDoesNotRaiseEngineMax(
154
+ "maxConcurrentChildren",
155
+ runOptions?.maxConcurrentChildren,
156
+ engineMaxConcurrentChildren
157
+ );
158
+ const effectiveMaxConcurrentChildren = Math.min(
159
+ engineMaxConcurrentChildren,
160
+ runOptions?.maxConcurrentChildren ?? Number.POSITIVE_INFINITY
161
+ );
162
+ const onChildFailure = resolveOnChildFailure(runOptions?.onChildFailure, engineOnChildFailure);
94
163
 
95
164
  const pendingEvents: StreamEvent[] = [];
96
165
  const pendingResolvers: Array<(value: IteratorResult<StreamEvent>) => void> = [];
@@ -105,7 +174,10 @@ export function createEngine(options: EngineOptions): Engine {
105
174
  const abortRace = createAbortRace(abortController.signal, options.model.id);
106
175
  let complete = false;
107
176
  let lastRunId = "";
177
+ let rootRunId: string | undefined;
108
178
  let pendingFinalEvent: FinalEvent | undefined;
179
+ let activeAbortDrain: AbortDrainFn | undefined;
180
+ const failureInstancesByChildRunId = new Map<string, DogpileError>();
109
181
  let status: StreamHandleStatus = "running";
110
182
  let resolveResult!: (result: RunResult) => void;
111
183
  let rejectResult!: (error: unknown) => void;
@@ -163,6 +235,9 @@ export function createEngine(options: EngineOptions): Engine {
163
235
  }
164
236
 
165
237
  try {
238
+ const streamStartedAtMs = Date.now();
239
+ const streamParentDeadlineMs =
240
+ options.budget?.timeoutMs !== undefined ? streamStartedAtMs + options.budget.timeoutMs : undefined;
166
241
  const baseResult = await abortRace.run(runProtocol({
167
242
  intent,
168
243
  protocol,
@@ -175,22 +250,44 @@ export function createEngine(options: EngineOptions): Engine {
175
250
  ...(options.seed !== undefined ? { seed: options.seed } : {}),
176
251
  signal: abortController.signal,
177
252
  ...(terminate ? { terminate } : {}),
253
+ currentDepth: 0,
254
+ effectiveMaxDepth,
255
+ effectiveMaxConcurrentChildren,
256
+ onChildFailure,
257
+ ...(streamParentDeadlineMs !== undefined ? { parentDeadlineMs: streamParentDeadlineMs } : {}),
258
+ ...(options.defaultSubRunTimeoutMs !== undefined
259
+ ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs }
260
+ : {}),
261
+ streamEvents: true,
178
262
  emit(event: RunEvent): void {
179
263
  if (status !== "running") {
180
264
  return;
181
265
  }
182
266
 
267
+ const parentRunIds = (event as { readonly parentRunIds?: readonly string[] }).parentRunIds;
268
+ if (rootRunId === undefined && parentRunIds === undefined) {
269
+ rootRunId = event.runId;
270
+ }
271
+
183
272
  lastRunId = event.runId;
184
- if (event.type === "final") {
273
+ if (event.type === "final" && event.runId === rootRunId) {
185
274
  pendingFinalEvent = event;
186
275
  return;
187
276
  }
188
277
  publish(event);
189
- }
278
+ },
279
+ registerAbortDrain(drain: AbortDrainFn): void {
280
+ activeAbortDrain = drain;
281
+ },
282
+ failureInstancesByChildRunId
190
283
  }));
191
284
  if (status !== "running") {
192
285
  return;
193
286
  }
287
+ const terminalThrow = resolveRuntimeTerminalThrow(baseResult.trace, failureInstancesByChildRunId);
288
+ if (terminalThrow) {
289
+ throw terminalThrow;
290
+ }
194
291
 
195
292
  const finalizedResult = await abortRace.run(applyRunEvaluation(baseResult, options.evaluate));
196
293
  if (status !== "running") {
@@ -213,6 +310,10 @@ export function createEngine(options: EngineOptions): Engine {
213
310
 
214
311
  const runtimeError = timeoutLifecycle.translateError(error);
215
312
  status = isCancellationError(runtimeError) ? "cancelled" : "failed";
313
+ if (shouldPublishAborted(runtimeError)) {
314
+ activeAbortDrain?.(runtimeError);
315
+ publish(createStreamAbortedEvent(runtimeError, lastRunId));
316
+ }
216
317
  publish(createStreamErrorEvent(runtimeError, lastRunId));
217
318
  closeStream();
218
319
  rejectResult(runtimeError);
@@ -225,9 +326,11 @@ export function createEngine(options: EngineOptions): Engine {
225
326
  }
226
327
 
227
328
  const error = createStreamCancellationError(options.model.id, cause);
228
- status = "cancelled";
229
329
  abortController.abort(error);
330
+ activeAbortDrain?.(error);
331
+ publish(createStreamAbortedEvent(error, lastRunId));
230
332
  publish(createStreamErrorEvent(error, lastRunId));
333
+ status = "cancelled";
231
334
  closeStream();
232
335
  rejectResult(error);
233
336
  }
@@ -238,6 +341,7 @@ export function createEngine(options: EngineOptions): Engine {
238
341
  }
239
342
 
240
343
  complete = true;
344
+ failureInstancesByChildRunId.clear();
241
345
  removeCallerAbortListener();
242
346
  timeoutLifecycle.cleanup();
243
347
  abortRace.cleanup();
@@ -303,6 +407,7 @@ function createNonStreamingAbortLifecycle(options: {
303
407
  readonly callerSignal?: AbortSignal | undefined;
304
408
  readonly timeoutMs?: number | undefined;
305
409
  readonly providerId: string;
410
+ readonly timeoutErrorSource?: "runtime" | "engine";
306
411
  }): AbortLifecycle {
307
412
  if (options.timeoutMs === undefined) {
308
413
  return {
@@ -321,7 +426,8 @@ function createNonStreamingAbortLifecycle(options: {
321
426
  const timeoutLifecycle = createTimeoutAbortLifecycle({
322
427
  abortController,
323
428
  timeoutMs: options.timeoutMs,
324
- providerId: options.providerId
429
+ providerId: options.providerId,
430
+ timeoutErrorSource: options.timeoutErrorSource ?? "runtime"
325
431
  });
326
432
  const abortRace = createAbortRace(abortController.signal, options.providerId);
327
433
  const removeCallerAbortListener = wireCallerAbortSignal(options.callerSignal, abortController, () => {
@@ -348,6 +454,7 @@ function createTimeoutAbortLifecycle(options: {
348
454
  readonly abortController: AbortController;
349
455
  readonly timeoutMs?: number | undefined;
350
456
  readonly providerId: string;
457
+ readonly timeoutErrorSource?: "runtime" | "engine";
351
458
  }): TimeoutAbortLifecycle {
352
459
  if (options.timeoutMs === undefined) {
353
460
  return {
@@ -358,7 +465,14 @@ function createTimeoutAbortLifecycle(options: {
358
465
  };
359
466
  }
360
467
 
361
- const timeoutError = createTimeoutError(options.providerId, options.timeoutMs);
468
+ const timeoutSource = classifyChildTimeoutSource(undefined, {
469
+ ...(options.timeoutErrorSource === "engine" ? { engineDefaultTimeoutMs: options.timeoutMs } : {}),
470
+ isProviderError: false
471
+ });
472
+ const timeoutError =
473
+ options.timeoutErrorSource === "engine" && timeoutSource === "engine"
474
+ ? createEngineDeadlineTimeoutError(options.providerId, options.timeoutMs)
475
+ : createTimeoutError(options.providerId, options.timeoutMs);
362
476
  const timeoutId = setTimeout(() => {
363
477
  options.abortController.abort(timeoutError);
364
478
  }, options.timeoutMs);
@@ -454,6 +568,28 @@ function readAbortSignalReason(signal: AbortSignal | undefined): unknown {
454
568
  return signal?.aborted ? signal.reason : undefined;
455
569
  }
456
570
 
571
+ function createStreamAbortedEvent(error: unknown, runId: string): AbortedEvent {
572
+ return {
573
+ type: "aborted",
574
+ runId,
575
+ at: new Date().toISOString(),
576
+ reason: streamAbortedReason(error)
577
+ };
578
+ }
579
+
580
+ function shouldPublishAborted(error: unknown): boolean {
581
+ return DogpileError.isInstance(error) && (error.code === "aborted" || error.code === "timeout");
582
+ }
583
+
584
+ function streamAbortedReason(error: unknown): AbortedEvent["reason"] {
585
+ if (DogpileError.isInstance(error)) {
586
+ if (error.code === "timeout" || error.detail?.["reason"] === "timeout") {
587
+ return "timeout";
588
+ }
589
+ }
590
+ return "parent-aborted";
591
+ }
592
+
457
593
  function createStreamErrorEvent(error: unknown, runId: string): StreamErrorEvent {
458
594
  if (DogpileError.isInstance(error)) {
459
595
  return {
@@ -519,15 +655,49 @@ interface RunProtocolOptions {
519
655
  readonly terminate?: EngineOptions["terminate"];
520
656
  readonly wrapUpHint?: EngineOptions["wrapUpHint"];
521
657
  readonly emit?: (event: RunEvent) => void;
658
+ readonly streamEvents?: boolean;
659
+ /**
660
+ * Current recursion depth. Top-level runs use 0; the coordinator dispatch
661
+ * loop increments before invoking {@link runProtocol} for a child run.
662
+ * Plan 04 will wire `effectiveMaxDepth` validation around this value.
663
+ */
664
+ readonly currentDepth?: number;
665
+ /**
666
+ * Effective max recursion depth. Plan 04 enforces; Plan 03 plumbs the param.
667
+ */
668
+ readonly effectiveMaxDepth?: number;
669
+ /** Effective max delegated child concurrency resolved at run start. */
670
+ readonly effectiveMaxConcurrentChildren?: number;
671
+ readonly onChildFailure?: EngineOptions["onChildFailure"];
672
+ /**
673
+ * Root-run deadline (epoch ms) threaded through every recursive coordinator
674
+ * dispatch (BUDGET-02 / D-12). Children inherit `parentDeadlineMs - now()`
675
+ * as their default timeout window.
676
+ */
677
+ readonly parentDeadlineMs?: number;
678
+ /**
679
+ * Engine-level fallback sub-run timeout (BUDGET-02 / D-14). Applied only when
680
+ * neither the parent nor the decision specifies a `budget.timeoutMs`.
681
+ */
682
+ readonly defaultSubRunTimeoutMs?: number;
683
+ readonly registerAbortDrain?: (drain: AbortDrainFn) => void;
684
+ readonly failureInstancesByChildRunId?: Map<string, DogpileError>;
522
685
  }
523
686
 
524
687
  type NonStreamingProtocolOptions = Omit<RunProtocolOptions, "emit"> & Pick<EngineOptions, "evaluate">;
525
688
 
526
689
  async function runNonStreamingProtocol(options: NonStreamingProtocolOptions): Promise<RunResult> {
690
+ const failureInstancesByChildRunId = new Map<string, DogpileError>();
527
691
  const abortLifecycle = createNonStreamingAbortLifecycle({
528
692
  callerSignal: options.signal,
529
693
  timeoutMs: runtimeTimeoutMs(options),
530
- providerId: options.model.id
694
+ providerId: options.model.id,
695
+ timeoutErrorSource:
696
+ options.currentDepth !== undefined &&
697
+ options.currentDepth > 0 &&
698
+ options.parentDeadlineMs === undefined
699
+ ? "engine"
700
+ : "runtime"
531
701
  });
532
702
 
533
703
  try {
@@ -537,7 +707,8 @@ async function runNonStreamingProtocol(options: NonStreamingProtocolOptions): Pr
537
707
  ...(abortLifecycle.signal !== undefined ? { signal: abortLifecycle.signal } : {}),
538
708
  emit(event: RunEvent): void {
539
709
  emittedEvents.push(event);
540
- }
710
+ },
711
+ failureInstancesByChildRunId
541
712
  }));
542
713
  const events = emittedEvents.length > 0 ? emittedEvents : result.trace.events;
543
714
  const trace = {
@@ -559,10 +730,15 @@ async function runNonStreamingProtocol(options: NonStreamingProtocolOptions): Pr
559
730
  eventLog: createRunEventLog(trace.runId, trace.protocol, events),
560
731
  trace
561
732
  };
733
+ const terminalThrow = resolveRuntimeTerminalThrow(runResult.trace, failureInstancesByChildRunId);
734
+ if (terminalThrow) {
735
+ throw terminalThrow;
736
+ }
562
737
  return canonicalizeRunResult(await abortLifecycle.run(applyRunEvaluation(runResult, options.evaluate)));
563
738
  } catch (error: unknown) {
564
739
  throw abortLifecycle.translateError(error);
565
740
  } finally {
741
+ failureInstancesByChildRunId.clear();
566
742
  abortLifecycle.cleanup();
567
743
  }
568
744
  }
@@ -653,7 +829,25 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
653
829
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
654
830
  ...(options.terminate ? { terminate: options.terminate } : {}),
655
831
  ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
656
- ...(options.emit ? { emit: options.emit } : {})
832
+ ...(options.emit ? { emit: options.emit } : {}),
833
+ ...(options.streamEvents !== undefined ? { streamEvents: options.streamEvents } : {}),
834
+ currentDepth: options.currentDepth ?? 0,
835
+ effectiveMaxDepth: options.effectiveMaxDepth ?? Infinity,
836
+ effectiveMaxConcurrentChildren: options.effectiveMaxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN,
837
+ onChildFailure: options.onChildFailure ?? "continue",
838
+ ...(options.parentDeadlineMs !== undefined ? { parentDeadlineMs: options.parentDeadlineMs } : {}),
839
+ ...(options.defaultSubRunTimeoutMs !== undefined
840
+ ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs }
841
+ : {}),
842
+ ...(options.registerAbortDrain !== undefined ? { registerAbortDrain: options.registerAbortDrain } : {}),
843
+ ...(options.failureInstancesByChildRunId !== undefined
844
+ ? { failureInstancesByChildRunId: options.failureInstancesByChildRunId }
845
+ : {}),
846
+ runProtocol: (childInput) =>
847
+ runProtocol({
848
+ ...childInput,
849
+ protocol: normalizeProtocol(childInput.protocol)
850
+ })
657
851
  });
658
852
  case "shared":
659
853
  return runShared({
@@ -726,6 +920,15 @@ export function stream(options: DogpileOptions): StreamHandle {
726
920
  export function replay(trace: Trace): RunResult {
727
921
  const cost = trace.finalOutput.cost;
728
922
  const lastEvent = trace.events.at(-1);
923
+ // D-08 / D-10: rebuild accounting recursively from the saved trace and
924
+ // verify every embedded sub-run's recorded accounting matches what the
925
+ // child trace recomputes. Mismatches throw `invalid-configuration` with
926
+ // `detail.reason: "trace-accounting-mismatch"`. No provider invocation.
927
+ const accounting = recomputeAccountingFromTrace(trace);
928
+ const replayThrow = resolveReplayTerminalThrow(trace);
929
+ if (replayThrow) {
930
+ throw replayThrow;
931
+ }
729
932
  const baseResult = {
730
933
  output: trace.finalOutput.output,
731
934
  eventLog: createRunEventLog(trace.runId, trace.protocol, trace.events),
@@ -740,13 +943,7 @@ export function replay(trace: Trace): RunResult {
740
943
  agentsUsed: trace.agentsUsed,
741
944
  events: trace.events
742
945
  }),
743
- accounting: createRunAccounting({
744
- tier: trace.tier,
745
- ...(trace.budget.caps ? { budget: trace.budget.caps } : {}),
746
- ...(trace.budget.termination ? { termination: trace.budget.termination } : {}),
747
- cost,
748
- events: trace.events
749
- }),
946
+ accounting,
750
947
  cost
751
948
  };
752
949
 
@@ -761,6 +958,104 @@ export function replay(trace: Trace): RunResult {
761
958
  };
762
959
  }
763
960
 
961
+ function resolveRuntimeTerminalThrow(
962
+ trace: Trace,
963
+ failureInstancesByChildRunId: ReadonlyMap<string, DogpileError>
964
+ ): DogpileError | null {
965
+ if (trace.triggeringFailureForAbortMode !== undefined) {
966
+ return failureInstancesByChildRunId.get(trace.triggeringFailureForAbortMode.childRunId) ?? null;
967
+ }
968
+
969
+ const finalEvent = trace.events.at(-1);
970
+ if (finalEvent?.type !== "final" || finalEvent.termination === undefined) {
971
+ return null;
972
+ }
973
+
974
+ const lastFailure = findLastRealFailure(trace.events, failureInstancesByChildRunId);
975
+ if (lastFailure === null) {
976
+ return null;
977
+ }
978
+ if (hasFinalSynthesisAfterEvent(trace, lastFailure.eventIndex)) {
979
+ return null;
980
+ }
981
+ return lastFailure.error;
982
+ }
983
+
984
+ function findLastRealFailure(
985
+ events: readonly RunEvent[],
986
+ failureInstancesByChildRunId: ReadonlyMap<string, DogpileError>
987
+ ): { readonly error: DogpileError; readonly eventIndex: number } | null {
988
+ for (let index = events.length - 1; index >= 0; index -= 1) {
989
+ const event = events[index];
990
+ if (event?.type !== "sub-run-failed") {
991
+ continue;
992
+ }
993
+ const instance = failureInstancesByChildRunId.get(event.childRunId);
994
+ if (instance) {
995
+ return { error: instance, eventIndex: index };
996
+ }
997
+ }
998
+ return null;
999
+ }
1000
+
1001
+ function resolveReplayTerminalThrow(trace: Trace): DogpileError | null {
1002
+ if (trace.triggeringFailureForAbortMode !== undefined) {
1003
+ return dogpileErrorFromSerializedPayload(trace.triggeringFailureForAbortMode.error);
1004
+ }
1005
+
1006
+ const finalEvent = trace.events.at(-1);
1007
+ if (finalEvent?.type !== "final" || finalEvent.termination === undefined) {
1008
+ return null;
1009
+ }
1010
+
1011
+ const lastFailure = reconstructLastRealFailure(trace.events);
1012
+ if (lastFailure === null) {
1013
+ return null;
1014
+ }
1015
+ if (hasFinalSynthesisAfterEvent(trace, lastFailure.eventIndex)) {
1016
+ return null;
1017
+ }
1018
+ return lastFailure.error;
1019
+ }
1020
+
1021
+ function reconstructLastRealFailure(
1022
+ events: readonly RunEvent[]
1023
+ ): { readonly error: DogpileError; readonly eventIndex: number } | null {
1024
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1025
+ const event = events[index];
1026
+ if (event?.type !== "sub-run-failed" || isSyntheticSubRunFailure(event)) {
1027
+ continue;
1028
+ }
1029
+ return { error: dogpileErrorFromSerializedPayload(event.error), eventIndex: index };
1030
+ }
1031
+ return null;
1032
+ }
1033
+
1034
+ function hasFinalSynthesisAfterEvent(trace: Trace, eventIndex: number): boolean {
1035
+ return trace.protocolDecisions.some((decision) => {
1036
+ return decision.phase === "final-synthesis" && decision.eventIndex > eventIndex;
1037
+ });
1038
+ }
1039
+
1040
+ function isSyntheticSubRunFailure(event: SubRunFailedEvent): boolean {
1041
+ const reason = event.error.detail?.["reason"];
1042
+ return reason === "sibling-failed" || reason === "parent-aborted";
1043
+ }
1044
+
1045
+ function dogpileErrorFromSerializedPayload(input: {
1046
+ readonly code: string;
1047
+ readonly message: string;
1048
+ readonly providerId?: string;
1049
+ readonly detail?: JsonObject;
1050
+ }): DogpileError {
1051
+ return new DogpileError({
1052
+ code: input.code as DogpileErrorCode,
1053
+ message: input.message,
1054
+ ...(input.providerId !== undefined ? { providerId: input.providerId } : {}),
1055
+ ...(input.detail !== undefined ? { detail: input.detail } : {})
1056
+ });
1057
+ }
1058
+
764
1059
  /**
765
1060
  * Replay a saved completed trace as a stream without invoking a model provider.
766
1061
  *
@@ -772,6 +1067,7 @@ export function replay(trace: Trace): RunResult {
772
1067
  */
773
1068
  export function replayStream(trace: Trace): StreamHandle {
774
1069
  const result = Promise.resolve(replay(trace));
1070
+ const replayEvents = replayStreamEvents(trace);
775
1071
 
776
1072
  return {
777
1073
  get status(): StreamHandleStatus {
@@ -782,7 +1078,7 @@ export function replayStream(trace: Trace): StreamHandle {
782
1078
  // Replay streams are already completed snapshots, so cancellation is a no-op.
783
1079
  },
784
1080
  subscribe(subscriber: StreamEventSubscriber) {
785
- for (const event of trace.events) {
1081
+ for (const event of replayEvents) {
786
1082
  subscriber(event);
787
1083
  }
788
1084
 
@@ -797,7 +1093,7 @@ export function replayStream(trace: Trace): StreamHandle {
797
1093
 
798
1094
  return {
799
1095
  next(): Promise<IteratorResult<StreamEvent>> {
800
- const event = trace.events[index];
1096
+ const event = replayEvents[index];
801
1097
  if (event) {
802
1098
  index += 1;
803
1099
  return Promise.resolve({ done: false, value: event });
@@ -810,6 +1106,31 @@ export function replayStream(trace: Trace): StreamHandle {
810
1106
  };
811
1107
  }
812
1108
 
1109
+ function replayStreamEvents(trace: Trace, parentRunIds: readonly string[] = []): StreamEvent[] {
1110
+ const events: StreamEvent[] = [];
1111
+
1112
+ for (const event of trace.events) {
1113
+ if (event.type === "sub-run-completed") {
1114
+ events.push(...replayStreamEvents(event.subResult.trace, [...parentRunIds, trace.runId]));
1115
+ }
1116
+ events.push(wrapReplayStreamEvent(event, parentRunIds));
1117
+ }
1118
+
1119
+ return events;
1120
+ }
1121
+
1122
+ function wrapReplayStreamEvent(event: RunEvent, parentRunIds: readonly string[]): StreamEvent {
1123
+ if (parentRunIds.length === 0) {
1124
+ return event;
1125
+ }
1126
+
1127
+ const inbound = (event as { readonly parentRunIds?: readonly string[] }).parentRunIds;
1128
+ return {
1129
+ ...event,
1130
+ parentRunIds: [...parentRunIds, ...(inbound ?? [])]
1131
+ } as StreamEvent;
1132
+ }
1133
+
813
1134
  function wireCallerAbortSignal(
814
1135
  callerSignal: AbortSignal | undefined,
815
1136
  abortController: AbortController,
@@ -844,7 +1165,8 @@ function createStreamCancellationError(providerId: string, cause?: unknown): Dog
844
1165
  providerId,
845
1166
  ...(cause !== undefined ? { cause } : {}),
846
1167
  detail: {
847
- status: "cancelled"
1168
+ status: "cancelled",
1169
+ reason: "parent-aborted"
848
1170
  }
849
1171
  });
850
1172
  }
@@ -865,6 +1187,23 @@ function withHighLevelDefaults(options: DogpileOptions): NormalizedDogpileOption
865
1187
  };
866
1188
  }
867
1189
 
1190
+ function assertRunDoesNotRaiseEngineMax(path: string, runValue: number | undefined, engineValue: number): void {
1191
+ if (runValue === undefined || runValue <= engineValue) {
1192
+ return;
1193
+ }
1194
+ throw new DogpileError({
1195
+ code: "invalid-configuration",
1196
+ message: `${path} cannot raise the engine ceiling (${engineValue}).`,
1197
+ retryable: false,
1198
+ detail: {
1199
+ kind: "configuration-validation",
1200
+ path,
1201
+ expected: `integer <= ${engineValue}`,
1202
+ actual: runValue
1203
+ }
1204
+ });
1205
+ }
1206
+
868
1207
  /**
869
1208
  * Branded high-level SDK namespace.
870
1209
  *
@@ -218,7 +218,15 @@ export async function runSequential(options: SequentialRunOptions): Promise<RunR
218
218
  }
219
219
  }
220
220
 
221
- const output = [...transcript].reverse().find((entry) => isParticipatingDecision(entry.decision))?.output ?? "";
221
+ // Preferred: most recent entry with an explicit participating decision.
222
+ // Fallback: most recent entry that has no parsed decision at all (preserves
223
+ // pre-discriminated-union behavior where unparsed turns were treated as
224
+ // participating). Delegate decisions are explicitly non-participating.
225
+ const reversed = [...transcript].reverse();
226
+ const output =
227
+ reversed.find((entry) => isParticipatingDecision(entry.decision))?.output ??
228
+ reversed.find((entry) => entry.decision === undefined)?.output ??
229
+ "";
222
230
  throwIfAborted(options.signal, options.model.id);
223
231
  const final: RunEvent = {
224
232
  type: "final",