@dogpile/sdk 0.1.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 (88) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +16 -0
  3. package/README.md +842 -0
  4. package/dist/browser/index.d.ts +8 -0
  5. package/dist/browser/index.d.ts.map +1 -0
  6. package/dist/browser/index.js +4493 -0
  7. package/dist/browser/index.js.map +1 -0
  8. package/dist/index.d.ts +17 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +14 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/providers/openai-compatible.d.ts +44 -0
  13. package/dist/providers/openai-compatible.d.ts.map +1 -0
  14. package/dist/providers/openai-compatible.js +305 -0
  15. package/dist/providers/openai-compatible.js.map +1 -0
  16. package/dist/runtime/broadcast.d.ts +18 -0
  17. package/dist/runtime/broadcast.d.ts.map +1 -0
  18. package/dist/runtime/broadcast.js +335 -0
  19. package/dist/runtime/broadcast.js.map +1 -0
  20. package/dist/runtime/cancellation.d.ts +6 -0
  21. package/dist/runtime/cancellation.d.ts.map +1 -0
  22. package/dist/runtime/cancellation.js +35 -0
  23. package/dist/runtime/cancellation.js.map +1 -0
  24. package/dist/runtime/coordinator.d.ts +18 -0
  25. package/dist/runtime/coordinator.d.ts.map +1 -0
  26. package/dist/runtime/coordinator.js +434 -0
  27. package/dist/runtime/coordinator.js.map +1 -0
  28. package/dist/runtime/decisions.d.ts +5 -0
  29. package/dist/runtime/decisions.d.ts.map +1 -0
  30. package/dist/runtime/decisions.js +31 -0
  31. package/dist/runtime/decisions.js.map +1 -0
  32. package/dist/runtime/defaults.d.ts +63 -0
  33. package/dist/runtime/defaults.d.ts.map +1 -0
  34. package/dist/runtime/defaults.js +426 -0
  35. package/dist/runtime/defaults.js.map +1 -0
  36. package/dist/runtime/engine.d.ts +79 -0
  37. package/dist/runtime/engine.d.ts.map +1 -0
  38. package/dist/runtime/engine.js +723 -0
  39. package/dist/runtime/engine.js.map +1 -0
  40. package/dist/runtime/model.d.ts +14 -0
  41. package/dist/runtime/model.d.ts.map +1 -0
  42. package/dist/runtime/model.js +82 -0
  43. package/dist/runtime/model.js.map +1 -0
  44. package/dist/runtime/sequential.d.ts +18 -0
  45. package/dist/runtime/sequential.d.ts.map +1 -0
  46. package/dist/runtime/sequential.js +277 -0
  47. package/dist/runtime/sequential.js.map +1 -0
  48. package/dist/runtime/shared.d.ts +18 -0
  49. package/dist/runtime/shared.d.ts.map +1 -0
  50. package/dist/runtime/shared.js +288 -0
  51. package/dist/runtime/shared.js.map +1 -0
  52. package/dist/runtime/termination.d.ts +77 -0
  53. package/dist/runtime/termination.d.ts.map +1 -0
  54. package/dist/runtime/termination.js +355 -0
  55. package/dist/runtime/termination.js.map +1 -0
  56. package/dist/runtime/tools.d.ts +314 -0
  57. package/dist/runtime/tools.d.ts.map +1 -0
  58. package/dist/runtime/tools.js +969 -0
  59. package/dist/runtime/tools.js.map +1 -0
  60. package/dist/runtime/validation.d.ts +23 -0
  61. package/dist/runtime/validation.d.ts.map +1 -0
  62. package/dist/runtime/validation.js +656 -0
  63. package/dist/runtime/validation.js.map +1 -0
  64. package/dist/types.d.ts +2434 -0
  65. package/dist/types.d.ts.map +1 -0
  66. package/dist/types.js +81 -0
  67. package/dist/types.js.map +1 -0
  68. package/package.json +157 -0
  69. package/src/browser/index.ts +7 -0
  70. package/src/index.ts +195 -0
  71. package/src/providers/openai-compatible.ts +406 -0
  72. package/src/runtime/broadcast.test.ts +355 -0
  73. package/src/runtime/broadcast.ts +428 -0
  74. package/src/runtime/cancellation.ts +40 -0
  75. package/src/runtime/coordinator.test.ts +468 -0
  76. package/src/runtime/coordinator.ts +581 -0
  77. package/src/runtime/decisions.ts +38 -0
  78. package/src/runtime/defaults.ts +547 -0
  79. package/src/runtime/engine.ts +880 -0
  80. package/src/runtime/model.ts +117 -0
  81. package/src/runtime/sequential.test.ts +262 -0
  82. package/src/runtime/sequential.ts +357 -0
  83. package/src/runtime/shared.test.ts +265 -0
  84. package/src/runtime/shared.ts +367 -0
  85. package/src/runtime/termination.ts +463 -0
  86. package/src/runtime/tools.ts +1518 -0
  87. package/src/runtime/validation.ts +771 -0
  88. package/src/types.ts +2729 -0
@@ -0,0 +1,880 @@
1
+ import { DogpileError } from "../types.js";
2
+ import type {
3
+ BudgetTier,
4
+ DogpileOptions,
5
+ Engine,
6
+ EngineOptions,
7
+ FinalEvent,
8
+ JsonObject,
9
+ JsonValue,
10
+ ProtocolSelection,
11
+ RunEvaluation,
12
+ RunEvent,
13
+ RunResult,
14
+ StreamErrorEvent,
15
+ StreamEvent,
16
+ StreamEventSubscriber,
17
+ StreamHandle,
18
+ StreamHandleStatus,
19
+ Trace
20
+ } from "../types.js";
21
+ import { runBroadcast } from "./broadcast.js";
22
+ import { runCoordinator } from "./coordinator.js";
23
+ import {
24
+ createReplayTraceFinalOutput,
25
+ createReplayTraceBudgetStateChanges,
26
+ canonicalizeRunResult,
27
+ canonicalizeSerializable,
28
+ createRunAccounting,
29
+ createRunEventLog,
30
+ createRunMetadata,
31
+ createRunUsage,
32
+ defaultAgents,
33
+ normalizeProtocol,
34
+ orderAgentsForTemperature,
35
+ tierTemperature
36
+ } from "./defaults.js";
37
+ import { runSequential } from "./sequential.js";
38
+ import { runShared } from "./shared.js";
39
+ import { createAbortErrorFromSignal, createTimeoutError } from "./cancellation.js";
40
+ import { budget as budgetCondition } from "./termination.js";
41
+ import { validateDogpileOptions, validateEngineOptions, validateMissionIntent } from "./validation.js";
42
+
43
+ const defaultHighLevelProtocol = "sequential";
44
+ const defaultHighLevelTier = "balanced";
45
+
46
+ type NormalizedDogpileOptions = Omit<DogpileOptions, "protocol" | "tier"> & {
47
+ readonly protocol: ProtocolSelection;
48
+ readonly tier: BudgetTier;
49
+ };
50
+
51
+ /**
52
+ * Create a reusable low-level protocol engine.
53
+ *
54
+ * @remarks
55
+ * Use this escape hatch to hold protocol, tier, model, agents, and budget caps
56
+ * constant across repeated missions. Most application code can call
57
+ * {@link run}, {@link stream}, or {@link Dogpile.pile} directly.
58
+ *
59
+ * The returned engine is stateless between calls: each `run()` or `stream()`
60
+ * invocation produces its own serializable trace, event log, and transcript.
61
+ */
62
+ export function createEngine(options: EngineOptions): Engine {
63
+ validateEngineOptions(options);
64
+
65
+ const protocol = normalizeProtocol(options.protocol);
66
+ const tools = options.tools ?? [];
67
+ const temperature = options.temperature ?? tierTemperature(options.tier);
68
+ const agents = orderAgentsForTemperature(options.agents ?? defaultAgents(), temperature, options.seed);
69
+ const terminate = options.terminate ?? (options.budget ? conditionFromBudget(options.budget) : undefined);
70
+
71
+ return {
72
+ run(intent: string): Promise<RunResult> {
73
+ validateMissionIntent(intent);
74
+
75
+ return runNonStreamingProtocol({
76
+ intent,
77
+ protocol,
78
+ tier: options.tier,
79
+ model: options.model,
80
+ agents,
81
+ tools,
82
+ temperature,
83
+ ...(options.budget ? { budget: options.budget } : {}),
84
+ ...(options.seed !== undefined ? { seed: options.seed } : {}),
85
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
86
+ ...(terminate ? { terminate } : {}),
87
+ ...(options.evaluate ? { evaluate: options.evaluate } : {})
88
+ });
89
+ },
90
+
91
+ stream(intent: string): StreamHandle {
92
+ validateMissionIntent(intent);
93
+
94
+ const pendingEvents: StreamEvent[] = [];
95
+ const pendingResolvers: Array<(value: IteratorResult<StreamEvent>) => void> = [];
96
+ const emittedEvents: StreamEvent[] = [];
97
+ const subscribers = new Set<StreamEventSubscriber>();
98
+ const abortController = new AbortController();
99
+ const timeoutLifecycle = createTimeoutAbortLifecycle({
100
+ abortController,
101
+ timeoutMs: runtimeTimeoutMs({ budget: options.budget, terminate }),
102
+ providerId: options.model.id
103
+ });
104
+ const abortRace = createAbortRace(abortController.signal, options.model.id);
105
+ let complete = false;
106
+ let lastRunId = "";
107
+ let pendingFinalEvent: FinalEvent | undefined;
108
+ let status: StreamHandleStatus = "running";
109
+ let resolveResult!: (result: RunResult) => void;
110
+ let rejectResult!: (error: unknown) => void;
111
+ let removeCallerAbortListener = (): void => {};
112
+
113
+ const result = new Promise<RunResult>((resolve, reject) => {
114
+ resolveResult = resolve;
115
+ rejectResult = reject;
116
+ });
117
+ removeCallerAbortListener = wireCallerAbortSignal(options.signal, abortController, cancelRun);
118
+ void execute();
119
+
120
+ return {
121
+ get status(): StreamHandleStatus {
122
+ return status;
123
+ },
124
+ result,
125
+ cancel(): void {
126
+ cancelRun();
127
+ },
128
+ subscribe(subscriber: StreamEventSubscriber) {
129
+ subscribers.add(subscriber);
130
+
131
+ for (const event of emittedEvents) {
132
+ subscriber(event);
133
+ }
134
+
135
+ return {
136
+ unsubscribe(): void {
137
+ subscribers.delete(subscriber);
138
+ }
139
+ };
140
+ },
141
+ [Symbol.asyncIterator](): AsyncIterator<StreamEvent> {
142
+ return {
143
+ next(): Promise<IteratorResult<StreamEvent>> {
144
+ const event = pendingEvents.shift();
145
+ if (event) {
146
+ return Promise.resolve({ done: false, value: event });
147
+ }
148
+ if (complete) {
149
+ return Promise.resolve({ done: true, value: undefined });
150
+ }
151
+ return new Promise<IteratorResult<StreamEvent>>((resolve) => {
152
+ pendingResolvers.push(resolve);
153
+ });
154
+ }
155
+ };
156
+ }
157
+ };
158
+
159
+ async function execute(): Promise<void> {
160
+ if (status !== "running") {
161
+ return;
162
+ }
163
+
164
+ try {
165
+ const baseResult = await abortRace.run(runProtocol({
166
+ intent,
167
+ protocol,
168
+ tier: options.tier,
169
+ model: options.model,
170
+ agents,
171
+ tools,
172
+ temperature,
173
+ ...(options.budget ? { budget: options.budget } : {}),
174
+ ...(options.seed !== undefined ? { seed: options.seed } : {}),
175
+ signal: abortController.signal,
176
+ ...(terminate ? { terminate } : {}),
177
+ emit(event: RunEvent): void {
178
+ if (status !== "running") {
179
+ return;
180
+ }
181
+
182
+ lastRunId = event.runId;
183
+ if (event.type === "final") {
184
+ pendingFinalEvent = event;
185
+ return;
186
+ }
187
+ publish(event);
188
+ }
189
+ }));
190
+ if (status !== "running") {
191
+ return;
192
+ }
193
+
194
+ const finalizedResult = await abortRace.run(applyRunEvaluation(baseResult, options.evaluate));
195
+ if (status !== "running") {
196
+ return;
197
+ }
198
+
199
+ const finalEvent = finalizedResult.trace.events.at(-1);
200
+ if (finalEvent?.type === "final") {
201
+ publish(finalEvent);
202
+ } else if (pendingFinalEvent) {
203
+ publish(pendingFinalEvent);
204
+ }
205
+ status = "completed";
206
+ closeStream();
207
+ resolveResult(finalizedResult);
208
+ } catch (error: unknown) {
209
+ if (isStreamHandleStatus(status, "cancelled")) {
210
+ return;
211
+ }
212
+
213
+ const runtimeError = timeoutLifecycle.translateError(error);
214
+ status = isCancellationError(runtimeError) ? "cancelled" : "failed";
215
+ publish(createStreamErrorEvent(runtimeError, lastRunId));
216
+ closeStream();
217
+ rejectResult(runtimeError);
218
+ }
219
+ }
220
+
221
+ function cancelRun(cause?: unknown): void {
222
+ if (status !== "running") {
223
+ return;
224
+ }
225
+
226
+ const error = createStreamCancellationError(options.model.id, cause);
227
+ status = "cancelled";
228
+ abortController.abort(error);
229
+ publish(createStreamErrorEvent(error, lastRunId));
230
+ closeStream();
231
+ rejectResult(error);
232
+ }
233
+
234
+ function closeStream(): void {
235
+ if (complete) {
236
+ return;
237
+ }
238
+
239
+ complete = true;
240
+ removeCallerAbortListener();
241
+ timeoutLifecycle.cleanup();
242
+ abortRace.cleanup();
243
+ subscribers.clear();
244
+ for (const resolver of pendingResolvers.splice(0)) {
245
+ resolver({ done: true, value: undefined });
246
+ }
247
+ }
248
+
249
+ function publish(event: StreamEvent): void {
250
+ if (complete) {
251
+ return;
252
+ }
253
+
254
+ const canonicalEvent = canonicalizeSerializable(event);
255
+ emittedEvents.push(canonicalEvent);
256
+
257
+ for (const subscriber of subscribers) {
258
+ try {
259
+ subscriber(canonicalEvent);
260
+ } catch {
261
+ // Subscriber failures should not cancel the underlying SDK run.
262
+ }
263
+ }
264
+
265
+ const resolver = pendingResolvers.shift();
266
+ if (resolver) {
267
+ resolver({ done: false, value: canonicalEvent });
268
+ return;
269
+ }
270
+ pendingEvents.push(canonicalEvent);
271
+ }
272
+ }
273
+ };
274
+ }
275
+
276
+ function isStreamHandleStatus(status: StreamHandleStatus, expected: StreamHandleStatus): boolean {
277
+ return status === expected;
278
+ }
279
+
280
+ function conditionFromBudget(budget: NonNullable<EngineOptions["budget"]>): ReturnType<typeof budgetCondition> {
281
+ return budgetCondition({
282
+ ...(budget.maxUsd !== undefined ? { maxUsd: budget.maxUsd } : {}),
283
+ ...(budget.maxTokens !== undefined ? { maxTokens: budget.maxTokens } : {}),
284
+ ...(budget.maxIterations !== undefined ? { maxIterations: budget.maxIterations } : {}),
285
+ ...(budget.timeoutMs !== undefined ? { timeoutMs: budget.timeoutMs } : {})
286
+ });
287
+ }
288
+
289
+ interface AbortLifecycle {
290
+ readonly signal: AbortSignal | undefined;
291
+ run<T>(operation: Promise<T>): Promise<T>;
292
+ translateError(error: unknown): unknown;
293
+ cleanup(): void;
294
+ }
295
+
296
+ interface TimeoutAbortLifecycle {
297
+ translateError(error: unknown): unknown;
298
+ cleanup(): void;
299
+ }
300
+
301
+ function createNonStreamingAbortLifecycle(options: {
302
+ readonly callerSignal?: AbortSignal | undefined;
303
+ readonly timeoutMs?: number | undefined;
304
+ readonly providerId: string;
305
+ }): AbortLifecycle {
306
+ if (options.timeoutMs === undefined) {
307
+ return {
308
+ signal: options.callerSignal,
309
+ async run<T>(operation: Promise<T>): Promise<T> {
310
+ return await operation;
311
+ },
312
+ translateError(error: unknown): unknown {
313
+ return error;
314
+ },
315
+ cleanup(): void {}
316
+ };
317
+ }
318
+
319
+ const abortController = new AbortController();
320
+ const timeoutLifecycle = createTimeoutAbortLifecycle({
321
+ abortController,
322
+ timeoutMs: options.timeoutMs,
323
+ providerId: options.providerId
324
+ });
325
+ const abortRace = createAbortRace(abortController.signal, options.providerId);
326
+ const removeCallerAbortListener = wireCallerAbortSignal(options.callerSignal, abortController, () => {
327
+ abortController.abort(readAbortSignalReason(options.callerSignal));
328
+ });
329
+
330
+ return {
331
+ signal: abortController.signal,
332
+ async run<T>(operation: Promise<T>): Promise<T> {
333
+ return await abortRace.run(operation);
334
+ },
335
+ translateError(error: unknown): unknown {
336
+ return timeoutLifecycle.translateError(error);
337
+ },
338
+ cleanup(): void {
339
+ timeoutLifecycle.cleanup();
340
+ abortRace.cleanup();
341
+ removeCallerAbortListener();
342
+ }
343
+ };
344
+ }
345
+
346
+ function createTimeoutAbortLifecycle(options: {
347
+ readonly abortController: AbortController;
348
+ readonly timeoutMs?: number | undefined;
349
+ readonly providerId: string;
350
+ }): TimeoutAbortLifecycle {
351
+ if (options.timeoutMs === undefined) {
352
+ return {
353
+ translateError(error: unknown): unknown {
354
+ return error;
355
+ },
356
+ cleanup(): void {}
357
+ };
358
+ }
359
+
360
+ const timeoutError = createTimeoutError(options.providerId, options.timeoutMs);
361
+ const timeoutId = setTimeout(() => {
362
+ options.abortController.abort(timeoutError);
363
+ }, options.timeoutMs);
364
+
365
+ return {
366
+ translateError(error: unknown): unknown {
367
+ return options.abortController.signal.reason === timeoutError ? timeoutError : error;
368
+ },
369
+ cleanup(): void {
370
+ clearTimeout(timeoutId);
371
+ }
372
+ };
373
+ }
374
+
375
+ function createAbortRace(signal: AbortSignal, providerId: string): AbortLifecycle {
376
+ let cleanupAbortListener = (): void => {};
377
+
378
+ return {
379
+ signal,
380
+ async run<T>(operation: Promise<T>): Promise<T> {
381
+ if (signal.aborted) {
382
+ throw createAbortErrorFromSignal(signal, providerId);
383
+ }
384
+
385
+ const abortPromise = new Promise<never>((_, reject) => {
386
+ const abortHandler = (): void => {
387
+ cleanupAbortListener();
388
+ reject(createAbortErrorFromSignal(signal, providerId));
389
+ };
390
+
391
+ cleanupAbortListener = (): void => {
392
+ signal.removeEventListener("abort", abortHandler);
393
+ };
394
+ signal.addEventListener("abort", abortHandler, { once: true });
395
+ });
396
+
397
+ try {
398
+ return await Promise.race([operation, abortPromise]);
399
+ } finally {
400
+ cleanupAbortListener();
401
+ cleanupAbortListener = (): void => {};
402
+ }
403
+ },
404
+ translateError(error: unknown): unknown {
405
+ return error;
406
+ },
407
+ cleanup(): void {
408
+ cleanupAbortListener();
409
+ cleanupAbortListener = (): void => {};
410
+ }
411
+ };
412
+ }
413
+
414
+ function runtimeTimeoutMs(options: {
415
+ readonly budget?: EngineOptions["budget"] | undefined;
416
+ readonly terminate?: EngineOptions["terminate"] | undefined;
417
+ }): number | undefined {
418
+ const budgetTimeoutMs = options.budget?.timeoutMs;
419
+ const terminationTimeoutMs = timeoutMsFromTermination(options.terminate);
420
+
421
+ if (budgetTimeoutMs === undefined) {
422
+ return terminationTimeoutMs;
423
+ }
424
+ if (terminationTimeoutMs === undefined) {
425
+ return budgetTimeoutMs;
426
+ }
427
+ return Math.min(budgetTimeoutMs, terminationTimeoutMs);
428
+ }
429
+
430
+ function timeoutMsFromTermination(condition: EngineOptions["terminate"] | undefined): number | undefined {
431
+ if (!condition) {
432
+ return undefined;
433
+ }
434
+
435
+ switch (condition.kind) {
436
+ case "budget":
437
+ return condition.timeoutMs;
438
+ case "firstOf":
439
+ return condition.conditions.reduce<number | undefined>((current, child) => {
440
+ const childTimeoutMs = timeoutMsFromTermination(child);
441
+ if (childTimeoutMs === undefined) {
442
+ return current;
443
+ }
444
+ return current === undefined ? childTimeoutMs : Math.min(current, childTimeoutMs);
445
+ }, undefined);
446
+ case "convergence":
447
+ case "judge":
448
+ return undefined;
449
+ }
450
+ }
451
+
452
+ function readAbortSignalReason(signal: AbortSignal | undefined): unknown {
453
+ return signal?.aborted ? signal.reason : undefined;
454
+ }
455
+
456
+ function createStreamErrorEvent(error: unknown, runId: string): StreamErrorEvent {
457
+ if (DogpileError.isInstance(error)) {
458
+ return {
459
+ type: "error",
460
+ runId,
461
+ at: new Date().toISOString(),
462
+ name: error.name,
463
+ message: error.message,
464
+ detail: dogpileErrorStreamDetail(error)
465
+ };
466
+ }
467
+
468
+ if (error instanceof Error) {
469
+ return {
470
+ type: "error",
471
+ runId,
472
+ at: new Date().toISOString(),
473
+ name: error.name,
474
+ message: error.message
475
+ };
476
+ }
477
+
478
+ return {
479
+ type: "error",
480
+ runId,
481
+ at: new Date().toISOString(),
482
+ name: "Error",
483
+ message: String(error)
484
+ };
485
+ }
486
+
487
+ function dogpileErrorStreamDetail(error: DogpileError): JsonObject {
488
+ const detail: Record<string, JsonValue> = {
489
+ code: error.code
490
+ };
491
+
492
+ if (error.providerId !== undefined) {
493
+ detail.providerId = error.providerId;
494
+ }
495
+ if (error.retryable !== undefined) {
496
+ detail.retryable = error.retryable;
497
+ }
498
+ if (error.detail !== undefined) {
499
+ for (const [key, value] of Object.entries(error.detail)) {
500
+ detail[key] = value;
501
+ }
502
+ }
503
+
504
+ return detail;
505
+ }
506
+
507
+ interface RunProtocolOptions {
508
+ readonly intent: string;
509
+ readonly protocol: ReturnType<typeof normalizeProtocol>;
510
+ readonly tier: EngineOptions["tier"];
511
+ readonly model: EngineOptions["model"];
512
+ readonly agents: readonly NonNullable<EngineOptions["agents"]>[number][];
513
+ readonly tools: NonNullable<EngineOptions["tools"]>;
514
+ readonly temperature: number;
515
+ readonly budget?: EngineOptions["budget"];
516
+ readonly seed?: EngineOptions["seed"];
517
+ readonly signal?: EngineOptions["signal"];
518
+ readonly terminate?: EngineOptions["terminate"];
519
+ readonly emit?: (event: RunEvent) => void;
520
+ }
521
+
522
+ type NonStreamingProtocolOptions = Omit<RunProtocolOptions, "emit"> & Pick<EngineOptions, "evaluate">;
523
+
524
+ async function runNonStreamingProtocol(options: NonStreamingProtocolOptions): Promise<RunResult> {
525
+ const abortLifecycle = createNonStreamingAbortLifecycle({
526
+ callerSignal: options.signal,
527
+ timeoutMs: runtimeTimeoutMs(options),
528
+ providerId: options.model.id
529
+ });
530
+
531
+ try {
532
+ const emittedEvents: RunEvent[] = [];
533
+ const result = await abortLifecycle.run(runProtocol({
534
+ ...options,
535
+ ...(abortLifecycle.signal !== undefined ? { signal: abortLifecycle.signal } : {}),
536
+ emit(event: RunEvent): void {
537
+ emittedEvents.push(event);
538
+ }
539
+ }));
540
+ const events = emittedEvents.length > 0 ? emittedEvents : result.trace.events;
541
+ const trace = {
542
+ ...result.trace,
543
+ events,
544
+ budgetStateChanges: createReplayTraceBudgetStateChanges(events),
545
+ finalOutput: createReplayTraceFinalOutput(result.output, events.at(-1) ?? result.trace.events.at(-1)!)
546
+ };
547
+
548
+ const runResult = {
549
+ ...result,
550
+ accounting: createRunAccounting({
551
+ tier: trace.tier,
552
+ ...(trace.budget.caps ? { budget: trace.budget.caps } : {}),
553
+ ...(trace.budget.termination ? { termination: trace.budget.termination } : {}),
554
+ cost: result.cost,
555
+ events
556
+ }),
557
+ eventLog: createRunEventLog(trace.runId, trace.protocol, events),
558
+ trace
559
+ };
560
+ return canonicalizeRunResult(await abortLifecycle.run(applyRunEvaluation(runResult, options.evaluate)));
561
+ } catch (error: unknown) {
562
+ throw abortLifecycle.translateError(error);
563
+ } finally {
564
+ abortLifecycle.cleanup();
565
+ }
566
+ }
567
+
568
+ async function applyRunEvaluation(
569
+ result: RunResult,
570
+ evaluate: EngineOptions["evaluate"]
571
+ ): Promise<RunResult> {
572
+ if (!evaluate) {
573
+ return canonicalizeRunResult(result);
574
+ }
575
+
576
+ const evaluation = await evaluate(result);
577
+ const events = result.trace.events.map((event, index): RunEvent => {
578
+ if (index !== result.trace.events.length - 1 || event.type !== "final") {
579
+ return event;
580
+ }
581
+
582
+ return finalEventWithEvaluation(event, evaluation);
583
+ });
584
+ const trace = {
585
+ ...result.trace,
586
+ events
587
+ };
588
+
589
+ return canonicalizeRunResult({
590
+ ...result,
591
+ quality: evaluation.quality,
592
+ evaluation,
593
+ trace,
594
+ eventLog: createRunEventLog(trace.runId, trace.protocol, events)
595
+ });
596
+ }
597
+
598
+ function finalEventWithEvaluation(event: FinalEvent, evaluation: RunEvaluation): FinalEvent {
599
+ return {
600
+ ...event,
601
+ quality: evaluation.quality,
602
+ evaluation
603
+ };
604
+ }
605
+
606
+ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
607
+ switch (options.protocol.kind) {
608
+ case "sequential":
609
+ return runSequential({
610
+ intent: options.intent,
611
+ protocol: options.protocol,
612
+ tier: options.tier,
613
+ model: options.model,
614
+ agents: options.agents,
615
+ tools: options.tools,
616
+ temperature: options.temperature,
617
+ ...(options.budget ? { budget: options.budget } : {}),
618
+ ...(options.seed !== undefined ? { seed: options.seed } : {}),
619
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
620
+ ...(options.terminate ? { terminate: options.terminate } : {}),
621
+ ...(options.emit ? { emit: options.emit } : {})
622
+ });
623
+ case "broadcast":
624
+ return runBroadcast({
625
+ intent: options.intent,
626
+ protocol: options.protocol,
627
+ tier: options.tier,
628
+ model: options.model,
629
+ agents: options.agents,
630
+ tools: options.tools,
631
+ temperature: options.temperature,
632
+ ...(options.budget ? { budget: options.budget } : {}),
633
+ ...(options.seed !== undefined ? { seed: options.seed } : {}),
634
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
635
+ ...(options.terminate ? { terminate: options.terminate } : {}),
636
+ ...(options.emit ? { emit: options.emit } : {})
637
+ });
638
+ case "coordinator":
639
+ return runCoordinator({
640
+ intent: options.intent,
641
+ protocol: options.protocol,
642
+ tier: options.tier,
643
+ model: options.model,
644
+ agents: options.agents,
645
+ tools: options.tools,
646
+ temperature: options.temperature,
647
+ ...(options.budget ? { budget: options.budget } : {}),
648
+ ...(options.seed !== undefined ? { seed: options.seed } : {}),
649
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
650
+ ...(options.terminate ? { terminate: options.terminate } : {}),
651
+ ...(options.emit ? { emit: options.emit } : {})
652
+ });
653
+ case "shared":
654
+ return runShared({
655
+ intent: options.intent,
656
+ protocol: options.protocol,
657
+ tier: options.tier,
658
+ model: options.model,
659
+ agents: options.agents,
660
+ tools: options.tools,
661
+ temperature: options.temperature,
662
+ ...(options.budget ? { budget: options.budget } : {}),
663
+ ...(options.seed !== undefined ? { seed: options.seed } : {}),
664
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
665
+ ...(options.terminate ? { terminate: options.terminate } : {}),
666
+ ...(options.emit ? { emit: options.emit } : {})
667
+ });
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Run a multi-agent workflow in a single call.
673
+ *
674
+ * @remarks
675
+ * Supply a mission through `intent` and provide a configured model provider.
676
+ * Omitted high-level controls default to Sequential coordination and the
677
+ * `balanced` tier. The returned
678
+ * {@link RunResult} contains the final `output`, a JSON-serializable `trace`,
679
+ * direct `transcript` access, aggregate `cost`, and optional `quality`.
680
+ *
681
+ * Use {@link createEngine} when a research harness needs to reuse normalized
682
+ * protocol/model/agent settings across many missions.
683
+ */
684
+ export function run(options: DogpileOptions): Promise<RunResult> {
685
+ validateDogpileOptions(options);
686
+
687
+ const { intent, ...engineOptions } = withHighLevelDefaults(options);
688
+ return createEngine(engineOptions).run(intent);
689
+ }
690
+
691
+ /**
692
+ * Stream a multi-agent workflow and await the final result.
693
+ *
694
+ * @remarks
695
+ * The returned handle is an async iterable of {@link RunEvent} values with a
696
+ * `result` promise for the same {@link RunResult} shape returned by
697
+ * {@link run}. This supports live event logs and trace UIs without requiring
698
+ * SDK-managed storage.
699
+ *
700
+ * Streaming and final traces use the same event shapes, so callers can render
701
+ * progress live and persist the completed trace without translation.
702
+ */
703
+ export function stream(options: DogpileOptions): StreamHandle {
704
+ validateDogpileOptions(options);
705
+
706
+ const { intent, ...engineOptions } = withHighLevelDefaults(options);
707
+ return createEngine(engineOptions).stream(intent);
708
+ }
709
+
710
+ /**
711
+ * Rehydrate the public result shape from a saved completed trace artifact.
712
+ *
713
+ * @remarks
714
+ * This is the caller-facing replay entrypoint for persisted traces. It does
715
+ * not call the model provider or require SDK-owned storage; it reconstructs
716
+ * the ergonomic {@link RunResult} wrapper from the JSON-serializable
717
+ * {@link Trace} returned by a previous `run()`, `stream()`, or
718
+ * `Dogpile.pile()` call.
719
+ */
720
+ export function replay(trace: Trace): RunResult {
721
+ const cost = trace.finalOutput.cost;
722
+ const lastEvent = trace.events.at(-1);
723
+ const baseResult = {
724
+ output: trace.finalOutput.output,
725
+ eventLog: createRunEventLog(trace.runId, trace.protocol, trace.events),
726
+ trace,
727
+ transcript: trace.transcript,
728
+ usage: createRunUsage(cost),
729
+ metadata: createRunMetadata({
730
+ runId: trace.runId,
731
+ protocol: trace.protocol,
732
+ tier: trace.tier,
733
+ modelProviderId: trace.modelProviderId,
734
+ agentsUsed: trace.agentsUsed,
735
+ events: trace.events
736
+ }),
737
+ accounting: createRunAccounting({
738
+ tier: trace.tier,
739
+ ...(trace.budget.caps ? { budget: trace.budget.caps } : {}),
740
+ ...(trace.budget.termination ? { termination: trace.budget.termination } : {}),
741
+ cost,
742
+ events: trace.events
743
+ }),
744
+ cost
745
+ };
746
+
747
+ if (lastEvent?.type !== "final") {
748
+ return baseResult;
749
+ }
750
+
751
+ return {
752
+ ...baseResult,
753
+ ...(lastEvent.quality !== undefined ? { quality: lastEvent.quality } : {}),
754
+ ...(lastEvent.evaluation !== undefined ? { evaluation: lastEvent.evaluation } : {})
755
+ };
756
+ }
757
+
758
+ /**
759
+ * Replay a saved completed trace as a stream without invoking a model provider.
760
+ *
761
+ * @remarks
762
+ * This is the streaming counterpart to {@link replay}. It yields the exact
763
+ * saved {@link Trace.events} in order and resolves {@link StreamHandle.result}
764
+ * to the rehydrated {@link RunResult}. Since all data comes from the trace,
765
+ * replay remains storage-free and provider-free.
766
+ */
767
+ export function replayStream(trace: Trace): StreamHandle {
768
+ const result = Promise.resolve(replay(trace));
769
+
770
+ return {
771
+ get status(): StreamHandleStatus {
772
+ return "completed";
773
+ },
774
+ result,
775
+ cancel(): void {
776
+ // Replay streams are already completed snapshots, so cancellation is a no-op.
777
+ },
778
+ subscribe(subscriber: StreamEventSubscriber) {
779
+ for (const event of trace.events) {
780
+ subscriber(event);
781
+ }
782
+
783
+ return {
784
+ unsubscribe(): void {
785
+ // Replay subscriptions are finite snapshots; there is no live source to detach from.
786
+ }
787
+ };
788
+ },
789
+ [Symbol.asyncIterator](): AsyncIterator<StreamEvent> {
790
+ let index = 0;
791
+
792
+ return {
793
+ next(): Promise<IteratorResult<StreamEvent>> {
794
+ const event = trace.events[index];
795
+ if (event) {
796
+ index += 1;
797
+ return Promise.resolve({ done: false, value: event });
798
+ }
799
+
800
+ return Promise.resolve({ done: true, value: undefined });
801
+ }
802
+ };
803
+ }
804
+ };
805
+ }
806
+
807
+ function wireCallerAbortSignal(
808
+ callerSignal: AbortSignal | undefined,
809
+ abortController: AbortController,
810
+ cancelRun: (reason?: unknown) => void
811
+ ): () => void {
812
+ if (!callerSignal) {
813
+ return (): void => {};
814
+ }
815
+
816
+ const cancelFromCaller = (): void => {
817
+ cancelRun(readAbortSignalReason(callerSignal));
818
+ };
819
+
820
+ if (callerSignal.aborted) {
821
+ cancelFromCaller();
822
+ return (): void => {};
823
+ }
824
+
825
+ callerSignal.addEventListener("abort", cancelFromCaller, { once: true });
826
+ const remove = (): void => {
827
+ callerSignal.removeEventListener("abort", cancelFromCaller);
828
+ };
829
+ abortController.signal.addEventListener("abort", remove, { once: true });
830
+ return remove;
831
+ }
832
+
833
+ function createStreamCancellationError(providerId: string, cause?: unknown): DogpileError {
834
+ return new DogpileError({
835
+ code: "aborted",
836
+ message: "The operation was aborted.",
837
+ retryable: false,
838
+ providerId,
839
+ ...(cause !== undefined ? { cause } : {}),
840
+ detail: {
841
+ status: "cancelled"
842
+ }
843
+ });
844
+ }
845
+
846
+ function isCancellationError(error: unknown): boolean {
847
+ if (DogpileError.isInstance(error)) {
848
+ return error.code === "aborted";
849
+ }
850
+
851
+ return error instanceof Error && error.name === "AbortError";
852
+ }
853
+
854
+ function withHighLevelDefaults(options: DogpileOptions): NormalizedDogpileOptions {
855
+ return {
856
+ ...options,
857
+ protocol: options.protocol ?? defaultHighLevelProtocol,
858
+ tier: options.tier ?? defaultHighLevelTier
859
+ };
860
+ }
861
+
862
+ /**
863
+ * Branded high-level SDK namespace.
864
+ *
865
+ * `Dogpile.pile()` is the ergonomic caller-facing workflow API. It uses the
866
+ * non-streaming execution path and resolves only after the protocol completes,
867
+ * returning `{ output, eventLog, transcript, usage, metadata, trace, cost,
868
+ * quality }`.
869
+ */
870
+ function pile(options: DogpileOptions): Promise<RunResult> {
871
+ return run(options);
872
+ }
873
+
874
+ export const Dogpile = {
875
+ pile,
876
+ replay,
877
+ replayStream,
878
+ stream,
879
+ createEngine
880
+ } as const;