@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,24 +1,88 @@
1
+ import { DogpileError } from "../types.js";
1
2
  import { createRunId, elapsedMs, nowMs, providerCallIdFor } from "./ids.js";
2
- import { addCost, createReplayTraceBudget, createReplayTraceBudgetStateChanges, createReplayTraceFinalOutput, createReplayTraceProtocolDecision, createReplayTraceRunInputs, createReplayTraceSeed, createRunAccounting, createRunEventLog, createRunMetadata, createRunUsage, createTranscriptLink, emptyCost, nextProviderCallId } from "./defaults.js";
3
- import { throwIfAborted } from "./cancellation.js";
4
- import { parseAgentDecision } from "./decisions.js";
3
+ import { addCost, createReplayTraceBudget, createReplayTraceBudgetStateChanges, createReplayTraceFinalOutput, createReplayTraceProtocolDecision, createReplayTraceRunInputs, createReplayTraceSeed, createRunAccounting, createRunEventLog, createRunMetadata, createRunUsage, createTranscriptLink, emptyCost, lastCostBearingEventCost, nextProviderCallId } from "./defaults.js";
4
+ import { classifyAbortReason, classifyChildTimeoutSource, createAbortErrorFromSignal, createEngineDeadlineTimeoutError, throwIfAborted } from "./cancellation.js";
5
+ import { assertDepthWithinLimit, parseAgentDecision } from "./decisions.js";
5
6
  import { generateModelTurn } from "./model.js";
6
7
  import { evaluateTerminationStop, warnOnProtocolTerminationMisconfiguration } from "./termination.js";
7
8
  import { createRuntimeToolExecutor, executeModelResponseToolRequests, runtimeToolAvailability } from "./tools.js";
8
9
  import { createWrapUpHintController } from "./wrap-up.js";
10
+ /**
11
+ * Hard-coded loop guard for the delegate dispatch in the coordinator plan
12
+ * turn. After this many consecutive delegate decisions the coordinator throws
13
+ * `invalid-configuration` (T-03-01). Not a public option.
14
+ */
15
+ const MAX_DISPATCH_PER_TURN = 8;
16
+ const DEFAULT_MAX_CONCURRENT_CHILDREN = 4;
17
+ function createSemaphore(maxConcurrent) {
18
+ let inFlight = 0;
19
+ const waiters = [];
20
+ return {
21
+ acquire() {
22
+ if (inFlight < maxConcurrent) {
23
+ inFlight += 1;
24
+ return Promise.resolve();
25
+ }
26
+ return new Promise((resolve) => {
27
+ waiters.push(() => {
28
+ inFlight += 1;
29
+ resolve();
30
+ });
31
+ });
32
+ },
33
+ release() {
34
+ inFlight -= 1;
35
+ const next = waiters.shift();
36
+ if (next !== undefined) {
37
+ next();
38
+ }
39
+ },
40
+ get inFlight() {
41
+ return inFlight;
42
+ },
43
+ get queued() {
44
+ return waiters.length;
45
+ }
46
+ };
47
+ }
48
+ /**
49
+ * Walk the coordinator's active provider set and return the FIRST provider
50
+ * whose metadata.locality === "local", or undefined if none found.
51
+ *
52
+ * Walk order (forward-compat): options.model first, then options.agents in
53
+ * declaration order. AgentSpec has no `model` field today (Phase 3 D-11
54
+ * forward-compat scaffolding); the agent walk uses optional chaining and
55
+ * effectively no-ops until a future phase adds AgentSpec.model.
56
+ */
57
+ function findFirstLocalProvider(options) {
58
+ if (options.model.metadata?.locality === "local") {
59
+ return options.model;
60
+ }
61
+ // Forward-compat: AgentSpec.model not yet declared (Phase 3 D-11). Walk no-ops today; ready for caller-defined trees in a future milestone.
62
+ for (const agent of options.agents) {
63
+ const agentModel = agent.model;
64
+ if (agentModel?.metadata?.locality === "local") {
65
+ return agentModel;
66
+ }
67
+ }
68
+ return undefined;
69
+ }
9
70
  export async function runCoordinator(options) {
10
71
  const runId = createRunId();
11
72
  const events = [];
12
73
  const transcript = [];
13
74
  const protocolDecisions = [];
14
75
  const providerCalls = [];
76
+ const dispatchedChildren = new Map();
15
77
  let totalCost = emptyCost();
78
+ let concurrencyClampEmitted = false; // D-12: emit once per run, never per-engine.
16
79
  const maxTurns = options.protocol.maxTurns ?? options.agents.length;
17
80
  const activeAgents = options.agents.slice(0, maxTurns);
18
81
  const coordinator = activeAgents[0];
19
82
  const startedAtMs = nowMs();
20
83
  let stopped = false;
21
84
  let termination;
85
+ let triggeringFailureForAbortMode;
22
86
  const wrapUpHint = createWrapUpHintController({
23
87
  protocol: options.protocol,
24
88
  tier: options.tier,
@@ -34,6 +98,61 @@ export async function runCoordinator(options) {
34
98
  const recordProtocolDecision = (event, decisionOptions) => {
35
99
  protocolDecisions.push(createReplayTraceProtocolDecision("coordinator", event, events.length - 1, decisionOptions));
36
100
  };
101
+ const drainOnParentAbort = (reasonSource) => {
102
+ const reason = classifyAbortReason(reasonSource);
103
+ for (const child of dispatchedChildren.values()) {
104
+ if (child.closed) {
105
+ continue;
106
+ }
107
+ const partialCost = child.started
108
+ ? lastCostBearingEventCost(child.childEvents) ?? emptyCost()
109
+ : emptyCost();
110
+ const partialTrace = buildPartialTrace({
111
+ childRunId: child.childRunId,
112
+ events: [...child.childEvents],
113
+ startedAtMs: child.startedAtMs,
114
+ protocol: child.decision.protocol,
115
+ tier: options.tier,
116
+ modelProviderId: options.model.id,
117
+ agents: options.agents,
118
+ intent: child.decision.intent,
119
+ temperature: options.temperature,
120
+ ...(child.childTimeoutMs !== undefined ? { childTimeoutMs: child.childTimeoutMs } : {}),
121
+ ...(options.seed !== undefined ? { seed: options.seed } : {})
122
+ });
123
+ const failedEvent = {
124
+ type: "sub-run-failed",
125
+ runId,
126
+ at: new Date().toISOString(),
127
+ childRunId: child.childRunId,
128
+ parentRunId: runId,
129
+ parentDecisionId: child.parentDecisionId,
130
+ parentDecisionArrayIndex: child.parentDecisionArrayIndex,
131
+ error: child.started
132
+ ? {
133
+ code: "aborted",
134
+ message: "Parent run aborted.",
135
+ detail: {
136
+ reason
137
+ }
138
+ }
139
+ : {
140
+ code: "aborted",
141
+ message: "Sibling delegate failed; queued delegate never started.",
142
+ detail: {
143
+ reason: "sibling-failed"
144
+ }
145
+ },
146
+ partialTrace,
147
+ partialCost
148
+ };
149
+ child.closed = true;
150
+ totalCost = addCost(totalCost, partialCost);
151
+ emit(failedEvent);
152
+ recordProtocolDecision(failedEvent);
153
+ }
154
+ };
155
+ options.registerAbortDrain?.(drainOnParentAbort);
37
156
  const toolExecutor = createRuntimeToolExecutor({
38
157
  runId,
39
158
  protocol: "coordinator",
@@ -61,24 +180,265 @@ export async function runCoordinator(options) {
61
180
  }
62
181
  if (coordinator) {
63
182
  if (!stopIfNeeded()) {
64
- totalCost = await runCoordinatorTurn({
65
- agent: coordinator,
66
- coordinator,
67
- input: buildCoordinatorPlanInput(options.intent, coordinator),
68
- phase: "plan",
69
- options,
70
- runId,
71
- transcript,
72
- totalCost,
73
- providerCalls,
74
- toolExecutor,
75
- toolAvailability,
76
- events,
77
- startedAtMs,
78
- wrapUpHint,
79
- emit,
80
- recordProtocolDecision
81
- });
183
+ // Delegate dispatch loop (D-11/D-16/D-17/D-18). Phase 1 limits delegation
184
+ // to the coordinator's plan turn; workers cannot delegate. The loop
185
+ // re-issues the coordinator plan turn after each successful sub-run with
186
+ // the projected D-17 result tagged into the next prompt and a synthetic
187
+ // D-18 transcript entry already appended. `partialTrace` for failed
188
+ // sub-runs is captured via a tee'd emit buffer locally — `runProtocol`'s
189
+ // error contract is unchanged.
190
+ let dispatchInput = buildCoordinatorPlanInput(options.intent, coordinator);
191
+ let dispatchCount = 0;
192
+ while (true) {
193
+ const turnOutcome = await runCoordinatorTurn({
194
+ agent: coordinator,
195
+ coordinator,
196
+ input: dispatchInput,
197
+ phase: "plan",
198
+ options,
199
+ runId,
200
+ transcript,
201
+ totalCost,
202
+ providerCalls,
203
+ toolExecutor,
204
+ toolAvailability,
205
+ events,
206
+ startedAtMs,
207
+ wrapUpHint,
208
+ emit,
209
+ recordProtocolDecision
210
+ });
211
+ totalCost = turnOutcome.totalCost;
212
+ if (turnOutcome.decision === undefined) {
213
+ break;
214
+ }
215
+ const delegates = Array.isArray(turnOutcome.decision)
216
+ ? turnOutcome.decision
217
+ : turnOutcome.decision.type === "delegate"
218
+ ? [turnOutcome.decision]
219
+ : [];
220
+ if (delegates.length === 0) {
221
+ break;
222
+ }
223
+ if (dispatchCount + delegates.length > MAX_DISPATCH_PER_TURN) {
224
+ throw new DogpileError({
225
+ code: "invalid-configuration",
226
+ message: `Coordinator plan turn delegated ${delegates.length} more children after ${dispatchCount}; max is ${MAX_DISPATCH_PER_TURN}.`,
227
+ retryable: false,
228
+ detail: {
229
+ kind: "delegate-validation",
230
+ path: "decision",
231
+ reason: "loop-guard-exceeded",
232
+ maxDispatchPerTurn: MAX_DISPATCH_PER_TURN
233
+ }
234
+ });
235
+ }
236
+ const parentDecisionId = String(events.length - 1);
237
+ const parentDepth = options.currentDepth ?? 0;
238
+ const decisionMax = delegates.reduce((max, delegate) => Math.min(max, delegate.maxConcurrentChildren ?? Number.POSITIVE_INFINITY), Number.POSITIVE_INFINITY);
239
+ let effectiveForTurn = Math.min(options.effectiveMaxConcurrentChildren ?? DEFAULT_MAX_CONCURRENT_CHILDREN, decisionMax);
240
+ const requestedMax = effectiveForTurn;
241
+ const localProvider = findFirstLocalProvider(options);
242
+ if (localProvider !== undefined) {
243
+ effectiveForTurn = 1;
244
+ if (!concurrencyClampEmitted) {
245
+ const clampEvent = {
246
+ type: "sub-run-concurrency-clamped",
247
+ runId,
248
+ at: new Date().toISOString(),
249
+ requestedMax,
250
+ effectiveMax: 1,
251
+ reason: "local-provider-detected",
252
+ providerId: localProvider.id
253
+ };
254
+ emit(clampEvent);
255
+ recordProtocolDecision(clampEvent);
256
+ concurrencyClampEmitted = true;
257
+ }
258
+ }
259
+ const semaphore = createSemaphore(effectiveForTurn);
260
+ const childRunIds = delegates.map(() => createRunId());
261
+ const dispatchedForTurn = delegates.map((delegate, index) => {
262
+ const childRunId = childRunIds[index];
263
+ if (childRunId === undefined) {
264
+ throw new Error("missing child run id");
265
+ }
266
+ const dispatchedChild = {
267
+ childRunId,
268
+ decision: delegate,
269
+ parentDecisionId,
270
+ parentDecisionArrayIndex: index,
271
+ parentDepth,
272
+ controller: new AbortController(),
273
+ removeParentListener: undefined,
274
+ childEvents: [],
275
+ started: false,
276
+ closed: false,
277
+ startedAtMs: Date.now(),
278
+ childTimeoutMs: undefined,
279
+ failure: undefined
280
+ };
281
+ dispatchedChildren.set(childRunId, dispatchedChild);
282
+ return dispatchedChild;
283
+ });
284
+ const dispatchResults = [];
285
+ let firstFailureIndex;
286
+ const tasks = delegates.map(async (delegate, index) => {
287
+ const childRunId = childRunIds[index];
288
+ if (childRunId === undefined) {
289
+ throw new Error("missing child run id");
290
+ }
291
+ if (semaphore.inFlight >= effectiveForTurn) {
292
+ const queuedEvent = {
293
+ type: "sub-run-queued",
294
+ runId,
295
+ at: new Date().toISOString(),
296
+ childRunId,
297
+ parentRunId: runId,
298
+ parentDecisionId,
299
+ parentDecisionArrayIndex: index,
300
+ protocol: delegate.protocol,
301
+ intent: delegate.intent,
302
+ depth: parentDepth + 1,
303
+ queuePosition: semaphore.queued
304
+ };
305
+ emit(queuedEvent);
306
+ recordProtocolDecision(queuedEvent);
307
+ }
308
+ await semaphore.acquire();
309
+ try {
310
+ const dispatchedChild = dispatchedForTurn[index];
311
+ if (!dispatchedChild) {
312
+ throw new Error("missing dispatched child");
313
+ }
314
+ if (firstFailureIndex !== undefined) {
315
+ if (dispatchedChild.closed) {
316
+ dispatchResults.push({
317
+ index,
318
+ result: {
319
+ nextInput: "",
320
+ taggedText: `[sub-run ${childRunId}]: skipped because the parent run aborted`,
321
+ completedAtMs: Date.now()
322
+ }
323
+ });
324
+ return;
325
+ }
326
+ const partialCost = emptyCost();
327
+ const partialTrace = buildPartialTrace({
328
+ childRunId,
329
+ events: [],
330
+ startedAtMs: Date.now(),
331
+ protocol: delegate.protocol,
332
+ tier: options.tier,
333
+ modelProviderId: options.model.id,
334
+ agents: options.agents,
335
+ intent: delegate.intent,
336
+ temperature: options.temperature,
337
+ ...(options.seed !== undefined ? { seed: options.seed } : {})
338
+ });
339
+ const failedEvent = {
340
+ type: "sub-run-failed",
341
+ runId,
342
+ at: new Date().toISOString(),
343
+ childRunId,
344
+ parentRunId: runId,
345
+ parentDecisionId,
346
+ parentDecisionArrayIndex: index,
347
+ error: {
348
+ code: "aborted",
349
+ message: "Sibling delegate failed; queued delegate never started.",
350
+ detail: {
351
+ reason: "sibling-failed"
352
+ }
353
+ },
354
+ partialTrace,
355
+ partialCost
356
+ };
357
+ emit(failedEvent);
358
+ recordProtocolDecision(failedEvent);
359
+ dispatchedChild.closed = true;
360
+ dispatchResults.push({
361
+ index,
362
+ result: {
363
+ nextInput: "",
364
+ taggedText: `[sub-run ${childRunId}]: skipped because a sibling delegate failed`,
365
+ completedAtMs: Date.now()
366
+ }
367
+ });
368
+ return;
369
+ }
370
+ const result = await dispatchDelegate({
371
+ decision: delegate,
372
+ childRunId,
373
+ parentDecisionId,
374
+ parentDecisionArrayIndex: index,
375
+ parentDepth,
376
+ parentRunId: runId,
377
+ options,
378
+ transcript,
379
+ emit,
380
+ recordProtocolDecision,
381
+ recordSubRunCost: (cost) => {
382
+ totalCost = addCost(totalCost, cost);
383
+ },
384
+ dispatchedChild
385
+ });
386
+ dispatchResults.push({ index, result });
387
+ }
388
+ catch (error) {
389
+ firstFailureIndex ??= index;
390
+ const dispatchedChild = dispatchedForTurn[index];
391
+ const failure = dispatchedChild?.failure;
392
+ if (delegates.length === 1 &&
393
+ (options.onChildFailure === "abort" || failure === undefined || isDelegateValidationError(error))) {
394
+ throw error;
395
+ }
396
+ const failureMessage = error instanceof Error ? error.message : String(error);
397
+ let taggedText = `[sub-run ${childRunId} failed]: ${failureMessage}`;
398
+ if (failure) {
399
+ const error = failure.error;
400
+ taggedText = `[sub-run ${childRunId} failed | code=${error.code} | spent=$${failure.partialCost.usd.toFixed(3)}]: ${error.message}`;
401
+ }
402
+ dispatchResults.push({
403
+ index,
404
+ result: {
405
+ nextInput: "",
406
+ taggedText,
407
+ completedAtMs: Date.now()
408
+ }
409
+ });
410
+ }
411
+ finally {
412
+ semaphore.release();
413
+ }
414
+ });
415
+ const settled = await Promise.allSettled(tasks);
416
+ const firstRejected = settled.find((result) => result.status === "rejected");
417
+ if (firstRejected?.status === "rejected" &&
418
+ delegates.length === 1 &&
419
+ (options.onChildFailure === "abort" || dispatchResults.length === 0)) {
420
+ throw firstRejected.reason;
421
+ }
422
+ dispatchResults.sort((a, b) => a.result.completedAtMs - b.result.completedAtMs);
423
+ const taggedResults = dispatchResults.map((entry) => entry.result.taggedText).join("\n\n");
424
+ const currentWaveFailures = dispatchedForTurn
425
+ .map((child) => child.failure)
426
+ .filter((failure) => failure !== undefined);
427
+ if (options.onChildFailure === "abort" && currentWaveFailures.length > 0) {
428
+ triggeringFailureForAbortMode ??= currentWaveFailures[0];
429
+ break;
430
+ }
431
+ const failuresSection = buildFailuresSection(currentWaveFailures);
432
+ const coordinatorAgent = options.agents[0] ?? { id: "coordinator", role: "coordinator" };
433
+ const baseInput = buildCoordinatorPlanInput(options.intent, coordinatorAgent);
434
+ dispatchInput = [
435
+ baseInput,
436
+ taggedResults,
437
+ failuresSection,
438
+ "Using the sub-run results above, decide the next step (participate or delegate)."
439
+ ].filter((section) => Boolean(section)).join("\n\n");
440
+ dispatchCount += delegates.length;
441
+ }
82
442
  stopIfNeeded();
83
443
  }
84
444
  if (!stopIfNeeded()) {
@@ -136,7 +496,7 @@ export async function runCoordinator(options) {
136
496
  stopIfNeeded();
137
497
  }
138
498
  if (!stopIfNeeded()) {
139
- totalCost = await runCoordinatorTurn({
499
+ const synthesisOutcome = await runCoordinatorTurn({
140
500
  agent: coordinator,
141
501
  coordinator,
142
502
  input: buildFinalSynthesisInput(options.intent, transcript, coordinator),
@@ -154,6 +514,20 @@ export async function runCoordinator(options) {
154
514
  emit,
155
515
  recordProtocolDecision
156
516
  });
517
+ totalCost = synthesisOutcome.totalCost;
518
+ // Phase 1: final-synthesis turn cannot delegate.
519
+ if (Array.isArray(synthesisOutcome.decision) || synthesisOutcome.decision?.type === "delegate") {
520
+ throw new DogpileError({
521
+ code: "invalid-configuration",
522
+ message: "Coordinator final-synthesis turn cannot emit a delegate decision in Phase 1",
523
+ retryable: false,
524
+ detail: {
525
+ kind: "delegate-validation",
526
+ path: "decision",
527
+ phase: "final-synthesis"
528
+ }
529
+ });
530
+ }
157
531
  stopIfNeeded();
158
532
  }
159
533
  }
@@ -208,6 +582,7 @@ export async function runCoordinator(options) {
208
582
  cost: totalCost,
209
583
  transcript: createTranscriptLink(transcript)
210
584
  }),
585
+ ...(triggeringFailureForAbortMode !== undefined ? { triggeringFailureForAbortMode } : {}),
211
586
  events,
212
587
  transcript
213
588
  },
@@ -273,6 +648,10 @@ export async function runCoordinator(options) {
273
648
  });
274
649
  }
275
650
  }
651
+ function isDelegateValidationError(error) {
652
+ return DogpileError.isInstance(error) && error.code === "invalid-configuration" &&
653
+ error.detail?.["kind"] === "delegate-validation";
654
+ }
276
655
  async function runCoordinatorTurn(turn) {
277
656
  throwIfAborted(turn.options.signal, turn.options.model.id);
278
657
  const request = {
@@ -319,7 +698,11 @@ async function runCoordinatorTurn(turn) {
319
698
  turn.providerCalls.push(call);
320
699
  }
321
700
  });
322
- const decision = parseAgentDecision(response.text);
701
+ const decision = parseAgentDecision(response.text, {
702
+ parentProviderId: turn.options.model.id,
703
+ currentDepth: turn.options.currentDepth ?? 0,
704
+ maxDepth: turn.options.effectiveMaxDepth ?? Number.POSITIVE_INFINITY
705
+ });
323
706
  const totalCost = addCost(turn.totalCost, responseCost(response));
324
707
  const toolCalls = await executeModelResponseToolRequests({
325
708
  response,
@@ -357,7 +740,7 @@ async function runCoordinatorTurn(turn) {
357
740
  phase: turn.phase,
358
741
  transcriptEntryCount: turn.transcript.length
359
742
  });
360
- return totalCost;
743
+ return { totalCost, decision };
361
744
  }
362
745
  async function runCoordinatorWorkerTurn(turn) {
363
746
  throwIfAborted(turn.options.signal, turn.options.model.id);
@@ -405,7 +788,23 @@ async function runCoordinatorWorkerTurn(turn) {
405
788
  turn.providerCallSlots[turn.providerCallIndex] = call;
406
789
  }
407
790
  });
408
- const decision = parseAgentDecision(response.text);
791
+ const decision = parseAgentDecision(response.text, {
792
+ parentProviderId: turn.options.model.id,
793
+ currentDepth: turn.options.currentDepth ?? 0,
794
+ maxDepth: turn.options.effectiveMaxDepth ?? Number.POSITIVE_INFINITY
795
+ });
796
+ if (Array.isArray(decision) || decision?.type === "delegate") {
797
+ throw new DogpileError({
798
+ code: "invalid-configuration",
799
+ message: "Workers cannot emit delegate decisions in Phase 1",
800
+ retryable: false,
801
+ detail: {
802
+ kind: "delegate-validation",
803
+ path: "decision",
804
+ phase: "worker"
805
+ }
806
+ });
807
+ }
409
808
  const toolCalls = await executeModelResponseToolRequests({
410
809
  response,
411
810
  executor: turn.toolExecutor,
@@ -436,6 +835,34 @@ function buildSystemPrompt(agent, coordinator) {
436
835
  function buildCoordinatorPlanInput(intent, coordinator) {
437
836
  return `Mission: ${intent}\nCoordinator ${coordinator.id}: assign the work, name the plan, and provide the first contribution.`;
438
837
  }
838
+ function buildFailuresSection(failures) {
839
+ if (failures.length === 0) {
840
+ return null;
841
+ }
842
+ return [
843
+ "## Sub-run failures since last decision",
844
+ "",
845
+ "```json",
846
+ JSON.stringify(failures, null, 2),
847
+ "```"
848
+ ].join("\n");
849
+ }
850
+ function dispatchWaveFailureFromEvent(intent, event) {
851
+ const reason = typeof event.error.detail?.["reason"] === "string" ? event.error.detail["reason"] : undefined;
852
+ if (reason === "sibling-failed" || reason === "parent-aborted") {
853
+ return undefined;
854
+ }
855
+ return {
856
+ childRunId: event.childRunId,
857
+ intent,
858
+ error: {
859
+ code: event.error.code,
860
+ message: event.error.message,
861
+ ...(reason !== undefined ? { detail: { reason } } : {})
862
+ },
863
+ partialCost: { usd: event.partialCost.usd }
864
+ };
865
+ }
439
866
  function buildWorkerInput(intent, transcript, coordinator) {
440
867
  const prior = transcript
441
868
  .map((entry) => `${entry.role} (${entry.agentId}): ${entry.output}`)
@@ -456,4 +883,484 @@ function responseCost(response) {
456
883
  totalTokens: response.usage?.totalTokens ?? 0
457
884
  };
458
885
  }
886
+ /**
887
+ * Dispatch a single delegate decision as a recursive sub-run.
888
+ *
889
+ * D-11: child reuses the parent provider object verbatim.
890
+ * D-16: `recursive: true` flag set when both parent and child protocol are
891
+ * `coordinator`.
892
+ * D-17: tagged result text appended to the next coordinator prompt.
893
+ * D-18: synthetic transcript entry pushed for replay/provenance.
894
+ *
895
+ * On thrown error from the child engine, builds `partialTrace` from a locally
896
+ * tee'd `childEvents` buffer — `runProtocol`'s error contract is unchanged.
897
+ */
898
+ async function dispatchDelegate(input) {
899
+ const { decision, options } = input;
900
+ // Dispatcher-time depth gate (D-14). Same error shape as the parser; this
901
+ // is the TOCTOU defense for any state mutation between parse and dispatch.
902
+ // Fires BEFORE sub-run-started is emitted so failed dispatches do not show
903
+ // up in the trace as half-started sub-runs.
904
+ if (options.effectiveMaxDepth !== undefined) {
905
+ assertDepthWithinLimit(input.parentDepth, options.effectiveMaxDepth);
906
+ }
907
+ const childRunId = input.childRunId ?? createRunId();
908
+ const recursive = decision.protocol === "coordinator";
909
+ const decisionTimeoutMs = decision.budget?.timeoutMs;
910
+ const parentDeadlineMs = options.parentDeadlineMs;
911
+ // BUDGET-02 / D-12: deadline-based remaining-time math. Children inherit
912
+ // `parentDeadlineMs - now()`, not a static `parent.budget.timeoutMs`. If the
913
+ // parent's deadline has already elapsed, throw `code: "aborted"` with
914
+ // `detail.reason: "timeout"` BEFORE `sub-run-started` is emitted.
915
+ const remainingMs = parentDeadlineMs !== undefined ? Math.max(0, parentDeadlineMs - Date.now()) : undefined;
916
+ if (parentDeadlineMs !== undefined && remainingMs === 0) {
917
+ throw new DogpileError({
918
+ code: "aborted",
919
+ message: "Parent deadline elapsed before sub-run dispatch.",
920
+ retryable: false,
921
+ providerId: options.model.id,
922
+ detail: { reason: "timeout" }
923
+ });
924
+ }
925
+ // Resolve child timeout with precedence (D-12 / D-14):
926
+ // decision.budget.timeoutMs > parent's remaining > defaultSubRunTimeoutMs > undefined.
927
+ // When the decision-level timeout exceeds the parent's remaining, CLAMP
928
+ // (no longer throw) and emit a `sub-run-budget-clamped` event below.
929
+ let childTimeoutMs;
930
+ let clampedFrom;
931
+ if (remainingMs !== undefined) {
932
+ if (decisionTimeoutMs !== undefined) {
933
+ if (decisionTimeoutMs > remainingMs) {
934
+ clampedFrom = decisionTimeoutMs;
935
+ childTimeoutMs = remainingMs;
936
+ }
937
+ else {
938
+ childTimeoutMs = decisionTimeoutMs;
939
+ }
940
+ }
941
+ else {
942
+ childTimeoutMs = remainingMs;
943
+ }
944
+ }
945
+ else if (decisionTimeoutMs !== undefined) {
946
+ childTimeoutMs = decisionTimeoutMs;
947
+ }
948
+ else if (options.defaultSubRunTimeoutMs !== undefined) {
949
+ childTimeoutMs = options.defaultSubRunTimeoutMs;
950
+ }
951
+ if (!options.runProtocol) {
952
+ throw new DogpileError({
953
+ code: "invalid-configuration",
954
+ message: "Coordinator delegate dispatch requires the engine `runProtocol` callback. " +
955
+ "Use `Dogpile.run` / `createEngine` rather than calling `runCoordinator` directly when delegate is in play.",
956
+ retryable: false,
957
+ detail: {
958
+ kind: "delegate-validation",
959
+ path: "runProtocol"
960
+ }
961
+ });
962
+ }
963
+ // Buffered tee for partialTrace capture — see Plan 03 step 8.
964
+ const childEvents = input.dispatchedChild.childEvents;
965
+ const parentEmit = input.emit;
966
+ const teedEmit = (event) => {
967
+ childEvents.push(event);
968
+ if (input.dispatchedChild.closed) {
969
+ return;
970
+ }
971
+ if (options.streamEvents && options.emit) {
972
+ const inbound = event.parentRunIds;
973
+ options.emit({
974
+ ...event,
975
+ parentRunIds: [input.parentRunId, ...(inbound ?? [])]
976
+ });
977
+ }
978
+ };
979
+ const childStartedAt = Date.now();
980
+ input.dispatchedChild.startedAtMs = childStartedAt;
981
+ // BUDGET-02 / D-12: emit clamp event BEFORE sub-run-started so the trace
982
+ // records "this child's requested timeout was reduced to fit parent's
983
+ // remaining deadline." Skipped on the happy path (no clamp, no event).
984
+ if (clampedFrom !== undefined && childTimeoutMs !== undefined) {
985
+ const clampEvent = {
986
+ type: "sub-run-budget-clamped",
987
+ runId: input.parentRunId,
988
+ at: new Date().toISOString(),
989
+ childRunId,
990
+ parentRunId: input.parentRunId,
991
+ parentDecisionId: input.parentDecisionId,
992
+ requestedTimeoutMs: clampedFrom,
993
+ clampedTimeoutMs: childTimeoutMs,
994
+ reason: "exceeded-parent-remaining"
995
+ };
996
+ input.emit(clampEvent);
997
+ input.recordProtocolDecision(clampEvent);
998
+ }
999
+ const startEvent = {
1000
+ type: "sub-run-started",
1001
+ runId: input.parentRunId,
1002
+ at: new Date().toISOString(),
1003
+ childRunId,
1004
+ parentRunId: input.parentRunId,
1005
+ parentDecisionId: input.parentDecisionId,
1006
+ parentDecisionArrayIndex: input.parentDecisionArrayIndex,
1007
+ protocol: decision.protocol,
1008
+ intent: decision.intent,
1009
+ depth: input.parentDepth + 1,
1010
+ ...(recursive ? { recursive: true } : {})
1011
+ };
1012
+ parentEmit(startEvent);
1013
+ input.recordProtocolDecision(startEvent);
1014
+ // BUDGET-01 / D-07: derive a per-child AbortController so child engines see
1015
+ // their own signal. Listener forwards parent.signal.reason verbatim, so
1016
+ // detail.reason classification (parent-aborted vs timeout) is preserved.
1017
+ // Phase 4 STREAM-03 hook: per-child cancel handle attaches here.
1018
+ const parentSignal = options.signal;
1019
+ let removeParentAbortListener;
1020
+ if (parentSignal !== undefined) {
1021
+ if (parentSignal.aborted) {
1022
+ input.dispatchedChild.controller.abort(parentSignal.reason);
1023
+ }
1024
+ else {
1025
+ const handler = () => {
1026
+ input.dispatchedChild.controller.abort(parentSignal.reason);
1027
+ };
1028
+ parentSignal.addEventListener("abort", handler, { once: true });
1029
+ removeParentAbortListener = () => {
1030
+ parentSignal.removeEventListener("abort", handler);
1031
+ };
1032
+ }
1033
+ }
1034
+ input.dispatchedChild.removeParentListener = removeParentAbortListener;
1035
+ input.dispatchedChild.started = true;
1036
+ input.dispatchedChild.childTimeoutMs = childTimeoutMs;
1037
+ const childDeadlineReason = childTimeoutMs !== undefined && parentDeadlineMs === undefined
1038
+ ? createEngineDeadlineTimeoutError(options.model.id, childTimeoutMs)
1039
+ : undefined;
1040
+ const childDeadlineTimer = childDeadlineReason !== undefined
1041
+ ? setTimeout(() => {
1042
+ input.dispatchedChild.controller.abort(childDeadlineReason);
1043
+ }, childTimeoutMs)
1044
+ : undefined;
1045
+ const childOptions = {
1046
+ intent: decision.intent,
1047
+ protocol: decision.protocol,
1048
+ tier: options.tier,
1049
+ model: options.model, // D-11: same provider instance verbatim
1050
+ agents: options.agents,
1051
+ tools: options.tools,
1052
+ temperature: options.temperature,
1053
+ ...(childTimeoutMs !== undefined ? { budget: { timeoutMs: childTimeoutMs } } : {}),
1054
+ signal: input.dispatchedChild.controller.signal,
1055
+ emit: teedEmit,
1056
+ ...(options.streamEvents !== undefined ? { streamEvents: options.streamEvents } : {}),
1057
+ currentDepth: input.parentDepth + 1,
1058
+ ...(options.effectiveMaxDepth !== undefined ? { effectiveMaxDepth: options.effectiveMaxDepth } : {}),
1059
+ ...(options.effectiveMaxConcurrentChildren !== undefined
1060
+ ? { effectiveMaxConcurrentChildren: options.effectiveMaxConcurrentChildren }
1061
+ : {}),
1062
+ ...(options.onChildFailure !== undefined ? { onChildFailure: options.onChildFailure } : {}),
1063
+ // BUDGET-02 / D-12: forward the ROOT deadline so depth-N grandchildren
1064
+ // see the same `parentDeadlineMs` rather than a fresh per-level snapshot.
1065
+ ...(parentDeadlineMs !== undefined ? { parentDeadlineMs } : {}),
1066
+ ...(options.defaultSubRunTimeoutMs !== undefined
1067
+ ? { defaultSubRunTimeoutMs: options.defaultSubRunTimeoutMs }
1068
+ : {})
1069
+ };
1070
+ let subResult;
1071
+ try {
1072
+ subResult = await options.runProtocol(childOptions);
1073
+ }
1074
+ catch (error) {
1075
+ if (childDeadlineTimer !== undefined) {
1076
+ clearTimeout(childDeadlineTimer);
1077
+ }
1078
+ removeParentAbortListener?.();
1079
+ if (input.dispatchedChild.closed) {
1080
+ const enrichedError = enrichAbortErrorWithParentReason(error, parentSignal);
1081
+ if (DogpileError.isInstance(enrichedError)) {
1082
+ throw enrichedError;
1083
+ }
1084
+ throw error;
1085
+ }
1086
+ const failedDecision = {
1087
+ type: "delegate",
1088
+ protocol: decision.protocol,
1089
+ intent: decision.intent,
1090
+ ...(decision.model !== undefined ? { model: decision.model } : {}),
1091
+ ...(decision.budget !== undefined ? { budget: decision.budget } : {})
1092
+ };
1093
+ const partialTrace = buildPartialTrace({
1094
+ childRunId,
1095
+ events: childEvents,
1096
+ startedAtMs: childStartedAt,
1097
+ protocol: decision.protocol,
1098
+ tier: options.tier,
1099
+ modelProviderId: options.model.id,
1100
+ agents: options.agents,
1101
+ intent: decision.intent,
1102
+ temperature: options.temperature,
1103
+ ...(childTimeoutMs !== undefined ? { childTimeoutMs } : {}),
1104
+ ...(options.seed !== undefined ? { seed: options.seed } : {})
1105
+ });
1106
+ // BUDGET-01 / D-08: when the child aborted because the parent.signal
1107
+ // aborted, lock detail.reason on the surfaced error. Upstream engine
1108
+ // wrapping (e.g., createStreamCancellationError) attaches its own
1109
+ // detail.status; we add detail.reason so consumers can discriminate
1110
+ // parent-aborted vs timeout regardless of which engine path produced the
1111
+ // abort error.
1112
+ const enrichedError = enrichProviderTimeoutSource(enrichAbortErrorWithParentReason(error, parentSignal), {
1113
+ ...(decisionTimeoutMs !== undefined ? { decisionTimeoutMs } : {}),
1114
+ ...(options.defaultSubRunTimeoutMs !== undefined
1115
+ ? { engineDefaultTimeoutMs: options.defaultSubRunTimeoutMs }
1116
+ : {})
1117
+ });
1118
+ if (DogpileError.isInstance(enrichedError)) {
1119
+ options.failureInstancesByChildRunId?.set(childRunId, enrichedError);
1120
+ }
1121
+ const errorPayload = errorPayloadFromUnknown(enrichedError, failedDecision);
1122
+ // BUDGET-03 / D-02: capture real provider spend before the throw and
1123
+ // roll it into the parent's totalCost BEFORE emitting sub-run-failed.
1124
+ const partialCost = lastCostBearingEventCost(childEvents) ?? emptyCost();
1125
+ input.recordSubRunCost(partialCost);
1126
+ const failEvent = {
1127
+ type: "sub-run-failed",
1128
+ runId: input.parentRunId,
1129
+ at: new Date().toISOString(),
1130
+ childRunId,
1131
+ parentRunId: input.parentRunId,
1132
+ parentDecisionId: input.parentDecisionId,
1133
+ parentDecisionArrayIndex: input.parentDecisionArrayIndex,
1134
+ error: errorPayload,
1135
+ partialTrace,
1136
+ partialCost
1137
+ };
1138
+ parentEmit(failEvent);
1139
+ input.recordProtocolDecision(failEvent);
1140
+ input.dispatchedChild.closed = true;
1141
+ input.dispatchedChild.failure = dispatchWaveFailureFromEvent(decision.intent, failEvent);
1142
+ // Re-throw a DogpileError so the parent run terminates with a typed error.
1143
+ if (DogpileError.isInstance(enrichedError)) {
1144
+ throw enrichedError;
1145
+ }
1146
+ throw new DogpileError({
1147
+ code: "invalid-configuration",
1148
+ message: error instanceof Error ? error.message : String(error),
1149
+ retryable: false,
1150
+ detail: {
1151
+ kind: "delegate-validation",
1152
+ path: "decision",
1153
+ reason: "child-run-failed"
1154
+ }
1155
+ });
1156
+ }
1157
+ if (childDeadlineTimer !== undefined) {
1158
+ clearTimeout(childDeadlineTimer);
1159
+ }
1160
+ removeParentAbortListener?.();
1161
+ // BUDGET-03 / D-01: roll child's full cost into the parent's totalCost
1162
+ // BEFORE emitting sub-run-completed. The next agent-turn / final event will
1163
+ // read totalCost from the closure scope, preserving the existing
1164
+ // "last cost-bearing event === final.cost" invariant.
1165
+ input.recordSubRunCost(subResult.cost);
1166
+ const completedEvent = {
1167
+ type: "sub-run-completed",
1168
+ runId: input.parentRunId,
1169
+ at: new Date().toISOString(),
1170
+ childRunId,
1171
+ parentRunId: input.parentRunId,
1172
+ parentDecisionId: input.parentDecisionId,
1173
+ parentDecisionArrayIndex: input.parentDecisionArrayIndex,
1174
+ subResult
1175
+ };
1176
+ parentEmit(completedEvent);
1177
+ input.recordProtocolDecision(completedEvent);
1178
+ input.dispatchedChild.closed = true;
1179
+ // BUDGET-01 / D-10: parent.signal aborted AFTER the child completed but
1180
+ // before we advance to the next coordinator turn. Emit a marker event so
1181
+ // streaming subscribers see "parent gave up after sub-run" provenance,
1182
+ // then re-throw the parent's abort reason. Non-streaming run() rejects with
1183
+ // the thrown error and does NOT preserve the marker — engine.ts does not
1184
+ // attach the parent events array to the rejected error (verified at
1185
+ // engine.ts:230-239). Streaming-subscriber observability is the contract.
1186
+ if (parentSignal?.aborted) {
1187
+ const abortMarker = {
1188
+ type: "sub-run-parent-aborted",
1189
+ runId: input.parentRunId,
1190
+ at: new Date().toISOString(),
1191
+ childRunId,
1192
+ parentRunId: input.parentRunId,
1193
+ reason: "parent-aborted"
1194
+ };
1195
+ parentEmit(abortMarker);
1196
+ input.recordProtocolDecision(abortMarker);
1197
+ throw enrichAbortErrorWithParentReason(createAbortErrorFromSignal(parentSignal, options.model.id), parentSignal);
1198
+ }
1199
+ // D-18 synthetic transcript entry.
1200
+ const decisionAsJson = {
1201
+ type: "delegate",
1202
+ protocol: decision.protocol,
1203
+ intent: decision.intent,
1204
+ ...(decision.model !== undefined ? { model: decision.model } : {}),
1205
+ ...(decision.budget !== undefined ? { budget: decision.budget } : {})
1206
+ };
1207
+ const taggedText = renderSubRunResult(childRunId, subResult);
1208
+ input.transcript.push({
1209
+ agentId: `sub-run:${childRunId}`,
1210
+ role: "delegate-result",
1211
+ input: JSON.stringify(decisionAsJson),
1212
+ output: taggedText
1213
+ });
1214
+ // Build the next coordinator prompt by appending the D-17 tagged block.
1215
+ const coordinatorAgent = options.agents[0];
1216
+ const baseInput = buildCoordinatorPlanInput(input.options.intent, coordinatorAgent ?? {
1217
+ id: "coordinator",
1218
+ role: "coordinator"
1219
+ });
1220
+ return {
1221
+ nextInput: `${baseInput}\n\n${taggedText}\n\nUsing the sub-run result above, decide the next step (participate or delegate).`,
1222
+ taggedText,
1223
+ completedAtMs: Date.now()
1224
+ };
1225
+ }
1226
+ /**
1227
+ * D-17 prompt-injection helper. Renders a child `RunResult` as the canonical
1228
+ * tagged-result block injected into the parent coordinator's next prompt.
1229
+ *
1230
+ * Format:
1231
+ * `[sub-run <childRunId>]: <output>`
1232
+ * `[sub-run <childRunId> stats]: turns=<N> costUsd=<X> durationMs=<Y>`
1233
+ *
1234
+ * The stats line is a soft contract — field names stable, ordering stable.
1235
+ */
1236
+ function renderSubRunResult(childRunId, subResult) {
1237
+ const turns = subResult.transcript.length;
1238
+ const costUsd = subResult.cost.usd ?? 0;
1239
+ const startedAt = subResult.trace.events[0]?.at;
1240
+ const endedAt = subResult.trace.events.at(-1)?.at;
1241
+ const durationMs = startedAt && endedAt
1242
+ ? Math.max(0, Date.parse(endedAt) - Date.parse(startedAt))
1243
+ : 0;
1244
+ return [
1245
+ `[sub-run ${childRunId}]: ${subResult.output}`,
1246
+ `[sub-run ${childRunId} stats]: turns=${turns} costUsd=${costUsd} durationMs=${durationMs}`
1247
+ ].join("\n");
1248
+ }
1249
+ /**
1250
+ * Build a JSON-serializable {@link Trace} for `sub-run-failed.partialTrace`
1251
+ * from a buffered tee of child emits. Keeps `runProtocol`'s error contract
1252
+ * unchanged — Plan 03 step 8.
1253
+ */
1254
+ function buildPartialTrace(input) {
1255
+ const protocolName = typeof input.protocol === "string" ? input.protocol : input.protocol.kind;
1256
+ const protocolConfig = typeof input.protocol === "string"
1257
+ ? { kind: input.protocol }
1258
+ : input.protocol;
1259
+ return {
1260
+ schemaVersion: "1.0",
1261
+ runId: input.childRunId,
1262
+ protocol: protocolName,
1263
+ tier: input.tier,
1264
+ modelProviderId: input.modelProviderId,
1265
+ agentsUsed: input.agents,
1266
+ inputs: createReplayTraceRunInputs({
1267
+ intent: input.intent,
1268
+ protocol: protocolConfig,
1269
+ tier: input.tier,
1270
+ modelProviderId: input.modelProviderId,
1271
+ agents: input.agents,
1272
+ temperature: input.temperature
1273
+ }),
1274
+ budget: createReplayTraceBudget({
1275
+ tier: input.tier,
1276
+ ...(input.childTimeoutMs !== undefined ? { caps: { timeoutMs: input.childTimeoutMs } } : {})
1277
+ }),
1278
+ budgetStateChanges: createReplayTraceBudgetStateChanges(input.events),
1279
+ seed: createReplayTraceSeed(input.seed),
1280
+ protocolDecisions: [],
1281
+ providerCalls: [],
1282
+ finalOutput: {
1283
+ kind: "replay-trace-final-output",
1284
+ output: "",
1285
+ cost: emptyCost(),
1286
+ completedAt: new Date().toISOString(),
1287
+ transcript: createTranscriptLink([])
1288
+ },
1289
+ events: input.events,
1290
+ transcript: []
1291
+ };
1292
+ }
1293
+ /**
1294
+ * BUDGET-01 / D-08: when a child sub-run threw because the parent's signal
1295
+ * aborted, lock the `detail.reason` discriminator on the resulting
1296
+ * `code: "aborted"` error. Preserves any pre-existing detail keys (e.g.,
1297
+ * `detail.status: "cancelled"` attached by `createStreamCancellationError`).
1298
+ *
1299
+ * No-op when:
1300
+ * - parent.signal is undefined or not aborted (child failure was unrelated)
1301
+ * - error is not a DogpileError with `code: "aborted"`
1302
+ * - error already has a `detail.reason` set (preserve upstream classification)
1303
+ */
1304
+ function enrichAbortErrorWithParentReason(error, parentSignal) {
1305
+ if (parentSignal === undefined || !parentSignal.aborted) {
1306
+ return error;
1307
+ }
1308
+ if (!DogpileError.isInstance(error) || error.code !== "aborted") {
1309
+ return error;
1310
+ }
1311
+ const existingDetail = error.detail ?? {};
1312
+ if (existingDetail["reason"] !== undefined) {
1313
+ return error;
1314
+ }
1315
+ const reason = classifyAbortReason(parentSignal.reason);
1316
+ return new DogpileError({
1317
+ code: "aborted",
1318
+ message: error.message,
1319
+ retryable: error.retryable ?? false,
1320
+ ...(error.providerId !== undefined ? { providerId: error.providerId } : {}),
1321
+ detail: { ...existingDetail, reason },
1322
+ ...(error.cause !== undefined ? { cause: error.cause } : {})
1323
+ });
1324
+ }
1325
+ function enrichProviderTimeoutSource(error, context) {
1326
+ if (!DogpileError.isInstance(error) || error.code !== "provider-timeout") {
1327
+ return error;
1328
+ }
1329
+ const existingDetail = error.detail ?? {};
1330
+ if (existingDetail["source"] !== undefined) {
1331
+ return error;
1332
+ }
1333
+ const source = classifyChildTimeoutSource(error, {
1334
+ ...context,
1335
+ isProviderError: true
1336
+ });
1337
+ return new DogpileError({
1338
+ code: "provider-timeout",
1339
+ message: error.message,
1340
+ retryable: error.retryable ?? true,
1341
+ ...(error.providerId !== undefined ? { providerId: error.providerId } : {}),
1342
+ detail: { ...existingDetail, source },
1343
+ ...(error.cause !== undefined ? { cause: error.cause } : {})
1344
+ });
1345
+ }
1346
+ function errorPayloadFromUnknown(error, failedDecision) {
1347
+ if (DogpileError.isInstance(error)) {
1348
+ const detail = {
1349
+ ...(error.detail ?? {}),
1350
+ failedDecision
1351
+ };
1352
+ return {
1353
+ code: error.code,
1354
+ message: error.message,
1355
+ ...(error.providerId !== undefined ? { providerId: error.providerId } : {}),
1356
+ detail
1357
+ };
1358
+ }
1359
+ const message = error instanceof Error ? error.message : String(error);
1360
+ return {
1361
+ code: "invalid-configuration",
1362
+ message,
1363
+ detail: { failedDecision }
1364
+ };
1365
+ }
459
1366
  //# sourceMappingURL=coordinator.js.map