@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
@@ -17,6 +17,7 @@ import type {
17
17
 
18
18
  const protocolNames = ["coordinator", "sequential", "broadcast", "shared"] as const;
19
19
  const budgetTiers = ["fast", "balanced", "quality"] as const;
20
+ const onChildFailureModes = ["continue", "abort"] as const;
20
21
 
21
22
  type ValidationRule =
22
23
  | "required"
@@ -24,6 +25,7 @@ type ValidationRule =
24
25
  | "finite-number"
25
26
  | "non-negative-number"
26
27
  | "positive-integer"
28
+ | "positive-finite-number"
27
29
  | "non-negative-integer"
28
30
  | "range"
29
31
  | "enum"
@@ -69,12 +71,29 @@ export function validateDogpileOptions(options: DogpileOptions): void {
69
71
  validateOptionalFunction(options.evaluate, "evaluate");
70
72
  validateOptionalSeed(options.seed, "seed");
71
73
  validateOptionalAbortSignal(options.signal, "signal");
74
+ validateOptionalNonNegativeInteger(options.maxDepth, "maxDepth");
75
+ validateOptionalPositiveInteger(options.maxConcurrentChildren, "maxConcurrentChildren");
76
+ validateOptionalPositiveFiniteNumber(options.defaultSubRunTimeoutMs, "defaultSubRunTimeoutMs");
77
+ validateOptionalOnChildFailure(options.onChildFailure, "onChildFailure");
72
78
  }
73
79
 
74
80
  export function validateMissionIntent(intent: unknown, path = "intent"): void {
75
81
  validateNonEmptyString(intent, path, "intent is required.");
76
82
  }
77
83
 
84
+ /**
85
+ * Validate per-call run/stream options (`Engine.run(intent, options)` / `Engine.stream(...)`).
86
+ */
87
+ export function validateRunCallOptions(options: unknown, path = "options"): void {
88
+ if (options === undefined) {
89
+ return;
90
+ }
91
+ const record = requireRecord(options, path);
92
+ validateOptionalNonNegativeInteger(record.maxDepth, `${path}.maxDepth`);
93
+ validateOptionalPositiveInteger(record.maxConcurrentChildren, `${path}.maxConcurrentChildren`);
94
+ validateOptionalOnChildFailure(record.onChildFailure, `${path}.onChildFailure`);
95
+ }
96
+
78
97
  /**
79
98
  * Validate low-level engine configuration before normalizing reusable controls.
80
99
  */
@@ -92,6 +111,10 @@ export function validateEngineOptions(options: EngineOptions): void {
92
111
  validateOptionalFunction(options.evaluate, "evaluate");
93
112
  validateOptionalSeed(options.seed, "seed");
94
113
  validateOptionalAbortSignal(options.signal, "signal");
114
+ validateOptionalNonNegativeInteger(options.maxDepth, "maxDepth");
115
+ validateOptionalPositiveInteger(options.maxConcurrentChildren, "maxConcurrentChildren");
116
+ validateOptionalPositiveFiniteNumber(options.defaultSubRunTimeoutMs, "defaultSubRunTimeoutMs");
117
+ validateOptionalOnChildFailure(options.onChildFailure, "onChildFailure");
95
118
  }
96
119
 
97
120
  /**
@@ -195,6 +218,28 @@ function validateBudgetTier(value: BudgetTier, path: string): void {
195
218
  }
196
219
  }
197
220
 
221
+ function validateOptionalOnChildFailure(value: unknown, path: string): void {
222
+ if (value === undefined) {
223
+ return;
224
+ }
225
+ if (value === "continue" || value === "abort") {
226
+ return;
227
+ }
228
+ throw new DogpileError({
229
+ code: "invalid-configuration",
230
+ message: `Invalid onChildFailure: expected "continue" or "abort", got ${JSON.stringify(value)}`,
231
+ retryable: false,
232
+ detail: {
233
+ kind: "configuration-validation",
234
+ path,
235
+ rule: "enum",
236
+ expected: onChildFailureModes.join(" | "),
237
+ received: describeValue(value),
238
+ reason: "invalid-on-child-failure"
239
+ }
240
+ });
241
+ }
242
+
198
243
  /**
199
244
  * Validate configured model provider definitions at registration boundaries.
200
245
  */
@@ -205,6 +250,27 @@ export function validateModelProviderRegistration(value: unknown, path = "model"
205
250
  validateOptionalFunction(record.stream, `${path}.stream`);
206
251
  }
207
252
 
253
+ /**
254
+ * Engine-time defense-in-depth check that a provider's optional
255
+ * `metadata.locality` is one of the two valid values when present (Phase 3 D-03).
256
+ * Catches user-implemented providers that bypass TypeScript checks.
257
+ */
258
+ export function validateProviderLocality(
259
+ provider: ConfiguredModelProvider,
260
+ pathPrefix: string = "model"
261
+ ): void {
262
+ const loc = provider.metadata?.locality;
263
+ if (loc !== undefined && loc !== "local" && loc !== "remote") {
264
+ invalidConfiguration({
265
+ path: `${pathPrefix}.metadata.locality`,
266
+ rule: "enum",
267
+ message: `${pathPrefix}.metadata.locality must be "local" or "remote" when provided.`,
268
+ expected: "\"local\" | \"remote\"",
269
+ actual: loc
270
+ });
271
+ }
272
+ }
273
+
208
274
  function validateVercelAILanguageModel(value: unknown, path: string): void {
209
275
  const record = requireRecord(value, path);
210
276
 
@@ -704,6 +770,21 @@ function validateOptionalNonNegativeInteger(value: unknown, path: string): void
704
770
  }
705
771
  }
706
772
 
773
+ function validateOptionalPositiveFiniteNumber(value: unknown, path: string): void {
774
+ if (value === undefined) {
775
+ return;
776
+ }
777
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
778
+ invalidConfiguration({
779
+ path,
780
+ rule: "positive-finite-number",
781
+ message: "value must be a positive finite number.",
782
+ expected: "finite number > 0",
783
+ actual: value
784
+ });
785
+ }
786
+ }
787
+
707
788
  function validateOptionalNonNegativeNumber(value: unknown, path: string): void {
708
789
  if (value === undefined) {
709
790
  return;
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ BudgetCaps,
2
3
  BudgetStopReason,
3
4
  CostSummary,
4
5
  JsonObject,
@@ -9,11 +10,14 @@ import type {
9
10
  ModelResponse,
10
11
  NormalizedQualityScore,
11
12
  Protocol,
13
+ ProtocolName,
12
14
  RunEvaluation,
15
+ RunResult,
13
16
  RuntimeToolIdentity,
14
17
  RuntimeToolPermission,
15
18
  RuntimeToolResult,
16
- TerminationStopRecord
19
+ TerminationStopRecord,
20
+ Trace
17
21
  } from "../types.js";
18
22
 
19
23
 
@@ -39,6 +43,8 @@ export interface RoleAssignmentEvent {
39
43
  readonly type: "role-assignment";
40
44
  /** Stable run id shared by all events in one workflow. */
41
45
  readonly runId: string;
46
+ /** Root-first ancestry chain when bubbled through a parent stream. */
47
+ readonly parentRunIds?: readonly string[];
42
48
  /** ISO-8601 event timestamp. */
43
49
  readonly at: string;
44
50
  /** Agent receiving the role assignment. */
@@ -63,6 +69,8 @@ export interface ModelRequestEvent {
63
69
  readonly type: "model-request";
64
70
  /** Stable run id shared by all events in one workflow. */
65
71
  readonly runId: string;
72
+ /** Root-first ancestry chain when bubbled through a parent stream. */
73
+ readonly parentRunIds?: readonly string[];
66
74
  /** ISO-8601 event timestamp. */
67
75
  readonly at: string;
68
76
  /** Stable provider call id within the run. */
@@ -91,6 +99,8 @@ export interface ModelResponseEvent {
91
99
  readonly type: "model-response";
92
100
  /** Stable run id shared by all events in one workflow. */
93
101
  readonly runId: string;
102
+ /** Root-first ancestry chain when bubbled through a parent stream. */
103
+ readonly parentRunIds?: readonly string[];
94
104
  /** ISO-8601 event timestamp. */
95
105
  readonly at: string;
96
106
  /** Stable provider call id within the run. */
@@ -131,6 +141,8 @@ export interface ModelOutputChunkEvent {
131
141
  readonly type: "model-output-chunk";
132
142
  /** Stable run id shared by all events in one workflow. */
133
143
  readonly runId: string;
144
+ /** Root-first ancestry chain when bubbled through a parent stream. */
145
+ readonly parentRunIds?: readonly string[];
134
146
  /** ISO-8601 event timestamp. */
135
147
  readonly at: string;
136
148
  /** Agent currently producing output. */
@@ -160,6 +172,8 @@ export interface ToolCallEvent {
160
172
  readonly type: "tool-call";
161
173
  /** Stable run id shared by all events in one workflow. */
162
174
  readonly runId: string;
175
+ /** Root-first ancestry chain when bubbled through a parent stream. */
176
+ readonly parentRunIds?: readonly string[];
163
177
  /** ISO-8601 event timestamp. */
164
178
  readonly at: string;
165
179
  /** Stable tool call id within the run. */
@@ -187,6 +201,8 @@ export interface ToolResultEvent {
187
201
  readonly type: "tool-result";
188
202
  /** Stable run id shared by all events in one workflow. */
189
203
  readonly runId: string;
204
+ /** Root-first ancestry chain when bubbled through a parent stream. */
205
+ readonly parentRunIds?: readonly string[];
190
206
  /** ISO-8601 event timestamp. */
191
207
  readonly at: string;
192
208
  /** Stable tool call id within the run. */
@@ -211,7 +227,9 @@ export interface ToolResultEvent {
211
227
  * metadata so reproduction harnesses can distinguish contribution from
212
228
  * voluntary abstention without reparsing raw text.
213
229
  */
214
- export interface AgentDecision {
230
+ export interface ParticipateAgentDecision {
231
+ /** Discriminant marking this as a participate-style decision. */
232
+ readonly type: "participate";
215
233
  /** Task-specific role selected by the agent for this turn. */
216
234
  readonly selectedRole: string;
217
235
  /** Whether the agent contributed or voluntarily abstained. */
@@ -222,6 +240,46 @@ export interface AgentDecision {
222
240
  readonly contribution: string;
223
241
  }
224
242
 
243
+ /**
244
+ * Decision emitted by a coordinator agent that delegates a sub-mission to a
245
+ * coordination protocol rather than contributing directly. The runtime
246
+ * dispatches a child run when this decision is returned.
247
+ */
248
+ export interface DelegateAgentDecision {
249
+ /** Discriminant marking this as a delegate-style decision. */
250
+ readonly type: "delegate";
251
+ /** Coordination protocol the child sub-run will execute. */
252
+ readonly protocol: ProtocolName;
253
+ /** Mission text passed to the child sub-run. */
254
+ readonly intent: string;
255
+ /**
256
+ * Optional model provider id assertion. When set, the runtime requires the
257
+ * value to match the parent's `ConfiguredModelProvider.id` (D-11) — child
258
+ * runs always inherit the parent provider instance verbatim.
259
+ */
260
+ readonly model?: string;
261
+ /** Optional per-decision budget caps applied to the child run. */
262
+ readonly budget?: BudgetCaps;
263
+ /**
264
+ * Optional per-decision child concurrency ceiling. This can only lower the
265
+ * engine/run effective max for the current coordinator fan-out turn (D-05).
266
+ */
267
+ readonly maxConcurrentChildren?: number;
268
+ }
269
+
270
+ /**
271
+ * Discriminated union of structured agent decisions parsed from model output.
272
+ *
273
+ * - `participate`: paper-style turn contribution; carries the four labeled
274
+ * fields (`selectedRole`, `participation`, `rationale`, `contribution`).
275
+ * - `delegate`: coordinator-only delegation to a child sub-run. The runtime
276
+ * dispatches a sub-mission when this branch is returned.
277
+ *
278
+ * Consumers MUST narrow on `decision.type === "participate"` before reading
279
+ * paper-style fields.
280
+ */
281
+ export type AgentDecision = ParticipateAgentDecision | DelegateAgentDecision;
282
+
225
283
  /**
226
284
  * Agent participation state for a paper-style turn decision.
227
285
  */
@@ -256,6 +314,8 @@ export interface TurnEvent {
256
314
  readonly type: "agent-turn";
257
315
  /** Stable run id shared by all events in one workflow. */
258
316
  readonly runId: string;
317
+ /** Root-first ancestry chain when bubbled through a parent stream. */
318
+ readonly parentRunIds?: readonly string[];
259
319
  /** ISO-8601 event timestamp. */
260
320
  readonly at: string;
261
321
  /** Agent that produced this turn. */
@@ -267,7 +327,7 @@ export interface TurnEvent {
267
327
  /** Model output produced by the agent. */
268
328
  readonly output: string;
269
329
  /** Optional structured role/participation decision parsed from model output. */
270
- readonly decision?: AgentDecision;
330
+ readonly decision?: AgentDecision | readonly DelegateAgentDecision[];
271
331
  /** Cumulative cost after this turn. */
272
332
  readonly cost: CostSummary;
273
333
  }
@@ -296,7 +356,7 @@ export interface BroadcastContribution {
296
356
  /** Independent model output produced for the shared mission. */
297
357
  readonly output: string;
298
358
  /** Optional structured role/participation decision parsed from model output. */
299
- readonly decision?: AgentDecision;
359
+ readonly decision?: AgentDecision | readonly DelegateAgentDecision[];
300
360
  }
301
361
 
302
362
  /**
@@ -323,6 +383,8 @@ export interface BroadcastEvent {
323
383
  readonly type: "broadcast";
324
384
  /** Stable run id shared by all events in one workflow. */
325
385
  readonly runId: string;
386
+ /** Root-first ancestry chain when bubbled through a parent stream. */
387
+ readonly parentRunIds?: readonly string[];
326
388
  /** ISO-8601 event timestamp. */
327
389
  readonly at: string;
328
390
  /** One-based broadcast round number. */
@@ -346,6 +408,8 @@ export interface BudgetStopEvent {
346
408
  readonly type: "budget-stop";
347
409
  /** Stable run id shared by all events in one workflow. */
348
410
  readonly runId: string;
411
+ /** Root-first ancestry chain when bubbled through a parent stream. */
412
+ readonly parentRunIds?: readonly string[];
349
413
  /** ISO-8601 event timestamp. */
350
414
  readonly at: string;
351
415
  /** Normalized machine-readable budget stop reason. */
@@ -418,6 +482,259 @@ export interface FinalEvent {
418
482
  readonly termination?: TerminationStopRecord;
419
483
  }
420
484
 
485
+ /**
486
+ * Event emitted when the coordinator dispatches a delegated sub-run.
487
+ *
488
+ * @remarks
489
+ * Recorded immediately before the child run starts executing. Carries the
490
+ * child's run id, the parent decision id that triggered the dispatch, and the
491
+ * resolved protocol/intent/depth. The `recursive` flag marks the diagnostic
492
+ * case where a coordinator delegates to another coordinator (D-16).
493
+ *
494
+ * The event's `runId` is the PARENT run id, matching the existing trace
495
+ * convention; `parentRunId` duplicates it for explicit cross-reference.
496
+ */
497
+ export interface SubRunStartedEvent {
498
+ /** Discriminant for event rendering and exhaustive switches. */
499
+ readonly type: "sub-run-started";
500
+ /** Parent run id; matches the surrounding trace runId. */
501
+ readonly runId: string;
502
+ /** Root-first ancestry chain when bubbled through a parent stream. */
503
+ readonly parentRunIds?: readonly string[];
504
+ /** ISO-8601 event timestamp. */
505
+ readonly at: string;
506
+ /** Child run id assigned to the dispatched sub-run. */
507
+ readonly childRunId: string;
508
+ /** Parent run id (duplicates `runId` for explicit cross-reference). */
509
+ readonly parentRunId: string;
510
+ /** Replay decision id of the parent decision that triggered this sub-run. */
511
+ readonly parentDecisionId: string;
512
+ /**
513
+ * 0-indexed position of this delegate within the fan-out array of its
514
+ * originating coordinator plan-turn (Phase 3 D-10). Single-delegate turns
515
+ * use `0` for backward compatibility. Together with `parentDecisionId`, this
516
+ * uniquely identifies the delegate within a fan-out.
517
+ */
518
+ readonly parentDecisionArrayIndex: number;
519
+ /** Coordination protocol the child run will execute. */
520
+ readonly protocol: ProtocolName;
521
+ /** Mission intent passed to the child run. */
522
+ readonly intent: string;
523
+ /** Recursion depth of the child run (1 for first-level sub-run). */
524
+ readonly depth: number;
525
+ /**
526
+ * Diagnostic flag set when a coordinator delegates to another coordinator
527
+ * (parent protocol === "coordinator" and child protocol === "coordinator").
528
+ */
529
+ readonly recursive?: boolean;
530
+ }
531
+
532
+ /**
533
+ * Event emitted when a delegated sub-run completes successfully.
534
+ *
535
+ * @remarks
536
+ * Carries the full {@link RunResult} as `subResult`, including the embedded
537
+ * child {@link Trace}. Replay walks the parent event sequence and recurses on
538
+ * `subResult.trace` to rehydrate sub-run accounting without re-invoking the
539
+ * provider (D-08).
540
+ */
541
+ export interface SubRunCompletedEvent {
542
+ /** Discriminant for event rendering and exhaustive switches. */
543
+ readonly type: "sub-run-completed";
544
+ /** Parent run id; matches the surrounding trace runId. */
545
+ readonly runId: string;
546
+ /** Root-first ancestry chain when bubbled through a parent stream. */
547
+ readonly parentRunIds?: readonly string[];
548
+ /** ISO-8601 event timestamp. */
549
+ readonly at: string;
550
+ /** Child run id that produced this result. */
551
+ readonly childRunId: string;
552
+ /** Parent run id (duplicates `runId` for explicit cross-reference). */
553
+ readonly parentRunId: string;
554
+ /** Replay decision id of the parent decision that triggered the sub-run. */
555
+ readonly parentDecisionId: string;
556
+ /**
557
+ * 0-indexed position of this delegate within the fan-out array of its
558
+ * originating coordinator plan-turn (Phase 3 D-10). Single-delegate turns
559
+ * use `0` for backward compatibility. Together with `parentDecisionId`, this
560
+ * uniquely identifies the delegate within a fan-out.
561
+ */
562
+ readonly parentDecisionArrayIndex: number;
563
+ /** Full child {@link RunResult}, including the embedded child {@link Trace}. */
564
+ readonly subResult: RunResult;
565
+ }
566
+
567
+ /**
568
+ * Event emitted when a delegated sub-run fails before completion.
569
+ *
570
+ * @remarks
571
+ * Captures a structured `error` plus the partial {@link Trace} accumulated
572
+ * before failure. The same `Trace` shape used by completed runs is preserved
573
+ * — consumers can replay or inspect the partial child events without bespoke
574
+ * deserialization logic.
575
+ */
576
+ export interface SubRunFailedEvent {
577
+ /** Discriminant for event rendering and exhaustive switches. */
578
+ readonly type: "sub-run-failed";
579
+ /** Parent run id; matches the surrounding trace runId. */
580
+ readonly runId: string;
581
+ /** Root-first ancestry chain when bubbled through a parent stream. */
582
+ readonly parentRunIds?: readonly string[];
583
+ /** ISO-8601 event timestamp. */
584
+ readonly at: string;
585
+ /** Child run id that failed. */
586
+ readonly childRunId: string;
587
+ /** Parent run id (duplicates `runId` for explicit cross-reference). */
588
+ readonly parentRunId: string;
589
+ /** Replay decision id of the parent decision that triggered the sub-run. */
590
+ readonly parentDecisionId: string;
591
+ /**
592
+ * 0-indexed position of this delegate within the fan-out array of its
593
+ * originating coordinator plan-turn (Phase 3 D-10). Single-delegate turns
594
+ * use `0` for backward compatibility. Together with `parentDecisionId`, this
595
+ * uniquely identifies the delegate within a fan-out.
596
+ */
597
+ readonly parentDecisionArrayIndex: number;
598
+ /** Structured failure description. */
599
+ readonly error: {
600
+ /** Stable error code (matches DogpileError code shape). */
601
+ readonly code: string;
602
+ /** Human-readable failure description. */
603
+ readonly message: string;
604
+ /** Provider id when the failure originated in a model call. */
605
+ readonly providerId?: string;
606
+ /** Optional structured detail (e.g., the failed delegate decision payload). */
607
+ readonly detail?: JsonObject;
608
+ };
609
+ /** Partial child {@link Trace} accumulated up to the failure point. */
610
+ readonly partialTrace: Trace;
611
+ /**
612
+ * Cost from provider calls completed before the child threw (BUDGET-03 / D-02).
613
+ *
614
+ * Equals `lastCostBearingEventCost(partialTrace.events) ?? emptyCost()`. The
615
+ * parent rolls this into its own `accounting.cost` so failed children
616
+ * contribute their real wallet spend.
617
+ */
618
+ readonly partialCost: CostSummary;
619
+ }
620
+
621
+ /**
622
+ * Event emitted when the parent's `signal` aborts AFTER a sub-run has already
623
+ * completed successfully but BEFORE the parent advances to its next coordinator
624
+ * turn (BUDGET-01 / D-10).
625
+ *
626
+ * @remarks
627
+ * Provides replay/streaming provenance for "parent gave up after a successful
628
+ * child finished." The marker is observable on `Dogpile.stream()` subscribers
629
+ * before the run errors with `code: "aborted"`. Non-streaming `run()` rejects
630
+ * with the abort error and does NOT expose the marker — `engine.ts` does not
631
+ * attach the parent events array to the rejected error (verified at
632
+ * `engine.ts:230-239`). Streaming-subscriber observability is the contract.
633
+ */
634
+ export interface SubRunParentAbortedEvent {
635
+ /** Discriminant for event rendering and exhaustive switches. */
636
+ readonly type: "sub-run-parent-aborted";
637
+ /** Parent run id; matches the surrounding trace runId. */
638
+ readonly runId: string;
639
+ /** Root-first ancestry chain when bubbled through a parent stream. */
640
+ readonly parentRunIds?: readonly string[];
641
+ /** ISO-8601 event timestamp. */
642
+ readonly at: string;
643
+ /** Most-recent completed child run id whose completion preceded the abort. */
644
+ readonly childRunId: string;
645
+ /** Parent run id (duplicates `runId` for explicit cross-reference). */
646
+ readonly parentRunId: string;
647
+ /** Discriminator (currently always "parent-aborted"; reserved for future variants). */
648
+ readonly reason: "parent-aborted";
649
+ }
650
+
651
+ /**
652
+ * Event emitted when a delegated sub-run's requested `budget.timeoutMs`
653
+ * exceeds the parent's remaining deadline and is therefore clamped to the
654
+ * parent's remaining time (BUDGET-02 / D-12).
655
+ *
656
+ * @remarks
657
+ * Emitted on the parent trace BEFORE `sub-run-started`. When the requested
658
+ * decision-level timeout fits within the parent's remaining deadline, the
659
+ * event is NOT emitted (zero-overhead happy path). The parent's deadline is
660
+ * a hard ceiling for the whole tree, so any decision-level override that
661
+ * exceeds it is silently clamped rather than throwing — recording the
662
+ * requested-vs-clamped pair on the trace preserves provenance for replay.
663
+ */
664
+ export interface SubRunBudgetClampedEvent {
665
+ /** Discriminant for event rendering and exhaustive switches. */
666
+ readonly type: "sub-run-budget-clamped";
667
+ /** Parent run id; matches the surrounding trace runId. */
668
+ readonly runId: string;
669
+ /** Root-first ancestry chain when bubbled through a parent stream. */
670
+ readonly parentRunIds?: readonly string[];
671
+ /** ISO-8601 event timestamp. */
672
+ readonly at: string;
673
+ /** Child run id whose budget was clamped. */
674
+ readonly childRunId: string;
675
+ /** Parent run id (duplicates `runId` for explicit cross-reference). */
676
+ readonly parentRunId: string;
677
+ /** Replay decision id of the parent decision that triggered the sub-run. */
678
+ readonly parentDecisionId: string;
679
+ /** The decision's originally requested `budget.timeoutMs` value (milliseconds). */
680
+ readonly requestedTimeoutMs: number;
681
+ /** The clamped child timeout actually applied (parent's remaining deadline, in milliseconds). */
682
+ readonly clampedTimeoutMs: number;
683
+ /** Discriminator for the clamp cause (currently always "exceeded-parent-remaining"). */
684
+ readonly reason: "exceeded-parent-remaining";
685
+ }
686
+
687
+ /**
688
+ * Event emitted when a delegated sub-run is enqueued because the
689
+ * coordinator's effective `maxConcurrentChildren` budget is fully in flight
690
+ * (Phase 3 D-07). Emitted ONLY when the slot is not immediately free —
691
+ * no-pressure runs do NOT emit this event.
692
+ *
693
+ * Three-event timeline under pressure: sub-run-queued → sub-run-started →
694
+ * sub-run-completed (or sub-run-failed). Without pressure: sub-run-started
695
+ * → completion (no queued event).
696
+ */
697
+ export interface SubRunQueuedEvent {
698
+ readonly type: "sub-run-queued";
699
+ readonly runId: string;
700
+ readonly parentRunIds?: readonly string[];
701
+ readonly at: string;
702
+ readonly childRunId: string;
703
+ readonly parentRunId: string;
704
+ readonly parentDecisionId: string;
705
+ readonly parentDecisionArrayIndex: number;
706
+ readonly protocol: ProtocolName;
707
+ readonly intent: string;
708
+ readonly depth: number;
709
+ readonly queuePosition: number;
710
+ }
711
+
712
+ /**
713
+ * Event emitted ONCE per run when the runtime detects a `"local"` provider
714
+ * in the coordinator's active tree and clamps `maxConcurrentChildren` to 1
715
+ * (Phase 3 CONCURRENCY-02 / D-12). Emitted at the FIRST delegate dispatch
716
+ * where the local-provider check trips. Subsequent dispatches in the same
717
+ * run do NOT re-emit. Runs with no delegates, or runs with delegates but
718
+ * no local provider, never emit this event.
719
+ *
720
+ * The clamp is silent (no throw, no console output) per D-13 — this event
721
+ * IS the warning surface; subscribers can react via the engine's `emit`
722
+ * callback.
723
+ */
724
+ export interface SubRunConcurrencyClampedEvent {
725
+ readonly type: "sub-run-concurrency-clamped";
726
+ readonly runId: string;
727
+ readonly parentRunIds?: readonly string[];
728
+ readonly at: string;
729
+ /** The pre-clamp effective max that would have applied (engine/run/decision min). */
730
+ readonly requestedMax: number;
731
+ /** Always 1 — locality-clamp is a fixed cap. */
732
+ readonly effectiveMax: 1;
733
+ readonly reason: "local-provider-detected";
734
+ /** Stable id of the FIRST local provider found during the active-tree walk. */
735
+ readonly providerId: string;
736
+ }
737
+
421
738
  /**
422
739
  * Successful coordination event emitted by Dogpile and persisted in traces.
423
740
  *
@@ -434,6 +751,11 @@ export interface FinalEvent {
434
751
  * - `tool-result`: one runtime tool invocation completed.
435
752
  * - `agent-turn`: one agent completed a prompt/response turn.
436
753
  * - `broadcast`: a broadcast round gathered independent contributions.
754
+ * - `sub-run-started`: a delegated sub-run was dispatched.
755
+ * - `sub-run-completed`: a delegated sub-run completed and embedded its full result.
756
+ * - `sub-run-failed`: a delegated sub-run failed before completion.
757
+ * - `sub-run-queued`: a delegated sub-run waited for a concurrency slot.
758
+ * - `sub-run-concurrency-clamped`: a local provider forced child concurrency to 1.
437
759
  * - `budget-stop`: a configured budget cap halted further model turns.
438
760
  * - `final`: the run completed and produced the final output.
439
761
  *
@@ -464,6 +786,13 @@ export type RunEvent =
464
786
  | ToolResultEvent
465
787
  | TurnEvent
466
788
  | BroadcastEvent
789
+ | SubRunStartedEvent
790
+ | SubRunCompletedEvent
791
+ | SubRunFailedEvent
792
+ | SubRunParentAbortedEvent
793
+ | SubRunBudgetClampedEvent
794
+ | SubRunQueuedEvent
795
+ | SubRunConcurrencyClampedEvent
467
796
  | BudgetStopEvent
468
797
  | FinalEvent;
469
798
 
@@ -483,10 +812,33 @@ export type ToolActivityEvent = ToolCallEvent | ToolResultEvent;
483
812
  * Lifecycle event yielded by `stream()`.
484
813
  *
485
814
  * These events describe workflow coordination state rather than model text.
486
- * Role assignment establishes the participant roster, while `budget-stop`
487
- * records a lifecycle halt before the terminal completion event.
815
+ * Role assignment establishes the participant roster, `budget-stop` records a
816
+ * lifecycle halt before the terminal completion event, and the `sub-run-*`
817
+ * variants surface delegated child-run dispatch boundaries.
488
818
  */
489
- export type StreamLifecycleEvent = RoleAssignmentEvent | BudgetStopEvent;
819
+ export type StreamLifecycleEvent =
820
+ | RoleAssignmentEvent
821
+ | BudgetStopEvent
822
+ | SubRunStartedEvent
823
+ | SubRunCompletedEvent
824
+ | SubRunFailedEvent
825
+ | SubRunParentAbortedEvent
826
+ | SubRunBudgetClampedEvent
827
+ | SubRunQueuedEvent
828
+ | SubRunConcurrencyClampedEvent
829
+ | AbortedEvent;
830
+
831
+ /**
832
+ * Lifecycle event yielded by `stream()` when a run is aborted.
833
+ */
834
+ export interface AbortedEvent {
835
+ readonly type: "aborted";
836
+ readonly runId: string;
837
+ readonly at: string;
838
+ readonly reason: "parent-aborted" | "timeout";
839
+ readonly detail?: JsonObject;
840
+ readonly parentRunIds?: readonly string[];
841
+ }
490
842
 
491
843
  /**
492
844
  * Output event yielded by `stream()`.
@@ -541,4 +893,3 @@ export type StreamCompletionEvent = FinalEvent;
541
893
  * {@link RunResult} trace to return.
542
894
  */
543
895
  export type StreamEvent = StreamLifecycleEvent | StreamOutputEvent | StreamErrorEvent | StreamCompletionEvent;
544
-
@@ -110,7 +110,14 @@ export type ReplayTraceProtocolDecisionType =
110
110
  | "complete-tool-call"
111
111
  | "collect-broadcast-round"
112
112
  | "stop-for-budget"
113
- | "finalize-output";
113
+ | "finalize-output"
114
+ | "start-sub-run"
115
+ | "complete-sub-run"
116
+ | "fail-sub-run"
117
+ | "mark-sub-run-parent-aborted"
118
+ | "mark-sub-run-budget-clamped"
119
+ | "queue-sub-run"
120
+ | "mark-sub-run-concurrency-clamped";
114
121
 
115
122
  /**
116
123
  * Protocol-level decision appended during execution.
@@ -136,6 +143,10 @@ export interface ReplayTraceProtocolDecision {
136
143
  readonly callId?: string;
137
144
  /** Provider involved in the decision, when model-scoped. */
138
145
  readonly providerId?: string;
146
+ /** Child run involved in a delegated sub-run decision. */
147
+ readonly childRunId?: string;
148
+ /** FIFO queue position for sub-run queue decisions. */
149
+ readonly queuePosition?: number;
139
150
  /** Tool call involved in the decision, when tool-scoped. */
140
151
  readonly toolCallId?: string;
141
152
  /** Tool identity involved in the decision, when tool-scoped. */