@dogpile/sdk 0.2.2 → 0.3.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 (48) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/index.js +337 -22
  3. package/dist/browser/index.js.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/runtime/broadcast.d.ts +1 -0
  7. package/dist/runtime/broadcast.d.ts.map +1 -1
  8. package/dist/runtime/broadcast.js +27 -6
  9. package/dist/runtime/broadcast.js.map +1 -1
  10. package/dist/runtime/coordinator.d.ts +1 -0
  11. package/dist/runtime/coordinator.d.ts.map +1 -1
  12. package/dist/runtime/coordinator.js +45 -8
  13. package/dist/runtime/coordinator.js.map +1 -1
  14. package/dist/runtime/engine.d.ts.map +1 -1
  15. package/dist/runtime/engine.js +5 -0
  16. package/dist/runtime/engine.js.map +1 -1
  17. package/dist/runtime/sequential.d.ts +1 -0
  18. package/dist/runtime/sequential.d.ts.map +1 -1
  19. package/dist/runtime/sequential.js +24 -6
  20. package/dist/runtime/sequential.js.map +1 -1
  21. package/dist/runtime/shared.d.ts +1 -0
  22. package/dist/runtime/shared.d.ts.map +1 -1
  23. package/dist/runtime/shared.js +24 -6
  24. package/dist/runtime/shared.js.map +1 -1
  25. package/dist/runtime/termination.d.ts +6 -1
  26. package/dist/runtime/termination.d.ts.map +1 -1
  27. package/dist/runtime/termination.js +75 -0
  28. package/dist/runtime/termination.js.map +1 -1
  29. package/dist/runtime/validation.d.ts.map +1 -1
  30. package/dist/runtime/validation.js +22 -0
  31. package/dist/runtime/validation.js.map +1 -1
  32. package/dist/runtime/wrap-up.d.ts +26 -0
  33. package/dist/runtime/wrap-up.d.ts.map +1 -0
  34. package/dist/runtime/wrap-up.js +178 -0
  35. package/dist/runtime/wrap-up.js.map +1 -0
  36. package/dist/types.d.ts +68 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +2 -1
  39. package/src/index.ts +3 -1
  40. package/src/runtime/broadcast.ts +49 -19
  41. package/src/runtime/coordinator.ts +83 -27
  42. package/src/runtime/engine.ts +6 -0
  43. package/src/runtime/sequential.ts +45 -19
  44. package/src/runtime/shared.ts +45 -19
  45. package/src/runtime/termination.ts +100 -0
  46. package/src/runtime/validation.ts +25 -0
  47. package/src/runtime/wrap-up.ts +257 -0
  48. package/src/types.ts +70 -0
@@ -38,8 +38,9 @@ import {
38
38
  import { throwIfAborted } from "./cancellation.js";
39
39
  import { parseAgentDecision } from "./decisions.js";
40
40
  import { generateModelTurn } from "./model.js";
41
- import { evaluateTerminationStop } from "./termination.js";
41
+ import { evaluateTerminationStop, warnOnProtocolTerminationMisconfiguration } from "./termination.js";
42
42
  import { createRuntimeToolExecutor, executeModelResponseToolRequests, runtimeToolAvailability } from "./tools.js";
43
+ import { createWrapUpHintController } from "./wrap-up.js";
43
44
 
44
45
  interface CoordinatorRunOptions {
45
46
  readonly intent: string;
@@ -53,6 +54,7 @@ interface CoordinatorRunOptions {
53
54
  readonly seed?: string | number;
54
55
  readonly signal?: AbortSignal;
55
56
  readonly terminate?: TerminationCondition;
57
+ readonly wrapUpHint?: DogpileOptions["wrapUpHint"];
56
58
  readonly emit?: (event: RunEvent) => void;
57
59
  }
58
60
 
@@ -69,6 +71,15 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
69
71
  const startedAtMs = nowMs();
70
72
  let stopped = false;
71
73
  let termination: TerminationStopRecord | undefined;
74
+ const wrapUpHint = createWrapUpHintController({
75
+ protocol: options.protocol,
76
+ tier: options.tier,
77
+ ...(options.budget ? { budget: options.budget } : {}),
78
+ ...(options.terminate ? { terminate: options.terminate } : {}),
79
+ ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {})
80
+ });
81
+
82
+ warnOnProtocolTerminationMisconfiguration(options.protocol, options.terminate);
72
83
 
73
84
  const emit = (event: RunEvent): void => {
74
85
  events.push(event);
@@ -126,6 +137,9 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
126
137
  providerCalls,
127
138
  toolExecutor,
128
139
  toolAvailability,
140
+ events,
141
+ startedAtMs,
142
+ wrapUpHint,
129
143
  emit,
130
144
  recordProtocolDecision
131
145
  });
@@ -150,6 +164,11 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
150
164
  providerCallSlots,
151
165
  toolExecutor,
152
166
  toolAvailability,
167
+ totalCost,
168
+ events,
169
+ transcript: planTranscript,
170
+ startedAtMs,
171
+ wrapUpHint,
153
172
  emit
154
173
  })
155
174
  )
@@ -201,6 +220,9 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
201
220
  providerCalls,
202
221
  toolExecutor,
203
222
  toolAvailability,
223
+ events,
224
+ startedAtMs,
225
+ wrapUpHint,
204
226
  emit,
205
227
  recordProtocolDecision
206
228
  });
@@ -290,16 +312,20 @@ export async function runCoordinator(options: CoordinatorRunOptions): Promise<Ru
290
312
  return stopped;
291
313
  }
292
314
 
293
- const stopRecord = evaluateTerminationStop(options.terminate, {
294
- runId,
295
- protocol: "coordinator",
296
- tier: options.tier,
297
- cost: totalCost,
298
- events,
299
- transcript,
300
- iteration: transcript.length,
301
- elapsedMs: elapsedMs(startedAtMs)
302
- });
315
+ const stopRecord = evaluateTerminationStop(
316
+ options.terminate,
317
+ wrapUpHint.context({
318
+ runId,
319
+ protocol: "coordinator",
320
+ protocolConfig: options.protocol,
321
+ protocolIteration: transcript.length,
322
+ cost: totalCost,
323
+ events,
324
+ transcript,
325
+ iteration: transcript.length,
326
+ elapsedMs: elapsedMs(startedAtMs)
327
+ })
328
+ );
303
329
 
304
330
  if (!stopRecord) {
305
331
  return false;
@@ -343,6 +369,9 @@ interface CoordinatorTurnOptions {
343
369
  readonly providerCalls: ReplayTraceProviderCall[];
344
370
  readonly toolExecutor: RuntimeToolExecutor;
345
371
  readonly toolAvailability: JsonObject;
372
+ readonly events: RunEvent[];
373
+ readonly startedAtMs: number;
374
+ readonly wrapUpHint: ReturnType<typeof createWrapUpHintController>;
346
375
  readonly emit: (event: RunEvent) => void;
347
376
  readonly recordProtocolDecision: (
348
377
  event: RunEvent,
@@ -366,16 +395,27 @@ async function runCoordinatorTurn(turn: CoordinatorTurnOptions): Promise<CostSum
366
395
  phase: turn.phase,
367
396
  ...turn.toolAvailability
368
397
  },
369
- messages: [
370
- {
371
- role: "system",
372
- content: buildSystemPrompt(turn.agent, turn.coordinator)
373
- },
398
+ messages: turn.wrapUpHint.inject(
399
+ [
400
+ {
401
+ role: "system",
402
+ content: buildSystemPrompt(turn.agent, turn.coordinator)
403
+ },
404
+ {
405
+ role: "user",
406
+ content: turn.input
407
+ }
408
+ ],
374
409
  {
375
- role: "user",
376
- content: turn.input
410
+ runId: turn.runId,
411
+ protocol: "coordinator",
412
+ cost: turn.totalCost,
413
+ events: turn.events,
414
+ transcript: turn.transcript,
415
+ iteration: turn.transcript.length,
416
+ elapsedMs: elapsedMs(turn.startedAtMs)
377
417
  }
378
- ]
418
+ )
379
419
  };
380
420
  const response = await generateModelTurn({
381
421
  model: turn.options.model,
@@ -445,6 +485,11 @@ interface CoordinatorWorkerTurnOptions {
445
485
  readonly providerCallSlots: ReplayTraceProviderCall[];
446
486
  readonly toolExecutor: RuntimeToolExecutor;
447
487
  readonly toolAvailability: JsonObject;
488
+ readonly totalCost: CostSummary;
489
+ readonly events: RunEvent[];
490
+ readonly transcript: readonly TranscriptEntry[];
491
+ readonly startedAtMs: number;
492
+ readonly wrapUpHint: ReturnType<typeof createWrapUpHintController>;
448
493
  readonly emit: (event: RunEvent) => void;
449
494
  }
450
495
 
@@ -473,16 +518,27 @@ async function runCoordinatorWorkerTurn(turn: CoordinatorWorkerTurnOptions): Pro
473
518
  phase: "worker",
474
519
  ...turn.toolAvailability
475
520
  },
476
- messages: [
477
- {
478
- role: "system",
479
- content: buildSystemPrompt(turn.agent, turn.coordinator)
480
- },
521
+ messages: turn.wrapUpHint.inject(
522
+ [
523
+ {
524
+ role: "system",
525
+ content: buildSystemPrompt(turn.agent, turn.coordinator)
526
+ },
527
+ {
528
+ role: "user",
529
+ content: turn.input
530
+ }
531
+ ],
481
532
  {
482
- role: "user",
483
- content: turn.input
533
+ runId: turn.runId,
534
+ protocol: "coordinator",
535
+ cost: turn.totalCost,
536
+ events: turn.events,
537
+ transcript: turn.transcript,
538
+ iteration: turn.turn - 1,
539
+ elapsedMs: elapsedMs(turn.startedAtMs)
484
540
  }
485
- ]
541
+ )
486
542
  };
487
543
  const response = await generateModelTurn({
488
544
  model: turn.options.model,
@@ -84,6 +84,7 @@ export function createEngine(options: EngineOptions): Engine {
84
84
  ...(options.seed !== undefined ? { seed: options.seed } : {}),
85
85
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
86
86
  ...(terminate ? { terminate } : {}),
87
+ ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
87
88
  ...(options.evaluate ? { evaluate: options.evaluate } : {})
88
89
  });
89
90
  },
@@ -516,6 +517,7 @@ interface RunProtocolOptions {
516
517
  readonly seed?: EngineOptions["seed"];
517
518
  readonly signal?: EngineOptions["signal"];
518
519
  readonly terminate?: EngineOptions["terminate"];
520
+ readonly wrapUpHint?: EngineOptions["wrapUpHint"];
519
521
  readonly emit?: (event: RunEvent) => void;
520
522
  }
521
523
 
@@ -618,6 +620,7 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
618
620
  ...(options.seed !== undefined ? { seed: options.seed } : {}),
619
621
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
620
622
  ...(options.terminate ? { terminate: options.terminate } : {}),
623
+ ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
621
624
  ...(options.emit ? { emit: options.emit } : {})
622
625
  });
623
626
  case "broadcast":
@@ -633,6 +636,7 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
633
636
  ...(options.seed !== undefined ? { seed: options.seed } : {}),
634
637
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
635
638
  ...(options.terminate ? { terminate: options.terminate } : {}),
639
+ ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
636
640
  ...(options.emit ? { emit: options.emit } : {})
637
641
  });
638
642
  case "coordinator":
@@ -648,6 +652,7 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
648
652
  ...(options.seed !== undefined ? { seed: options.seed } : {}),
649
653
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
650
654
  ...(options.terminate ? { terminate: options.terminate } : {}),
655
+ ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
651
656
  ...(options.emit ? { emit: options.emit } : {})
652
657
  });
653
658
  case "shared":
@@ -663,6 +668,7 @@ function runProtocol(options: RunProtocolOptions): Promise<RunResult> {
663
668
  ...(options.seed !== undefined ? { seed: options.seed } : {}),
664
669
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
665
670
  ...(options.terminate ? { terminate: options.terminate } : {}),
671
+ ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {}),
666
672
  ...(options.emit ? { emit: options.emit } : {})
667
673
  });
668
674
  }
@@ -37,8 +37,9 @@ import {
37
37
  import { throwIfAborted } from "./cancellation.js";
38
38
  import { isParticipatingDecision, parseAgentDecision } from "./decisions.js";
39
39
  import { generateModelTurn } from "./model.js";
40
- import { evaluateTerminationStop } from "./termination.js";
40
+ import { evaluateTerminationStop, warnOnProtocolTerminationMisconfiguration } from "./termination.js";
41
41
  import { createRuntimeToolExecutor, executeModelResponseToolRequests, runtimeToolAvailability } from "./tools.js";
42
+ import { createWrapUpHintController } from "./wrap-up.js";
42
43
 
43
44
  interface SequentialRunOptions {
44
45
  readonly intent: string;
@@ -52,6 +53,7 @@ interface SequentialRunOptions {
52
53
  readonly seed?: string | number;
53
54
  readonly signal?: AbortSignal;
54
55
  readonly terminate?: TerminationCondition;
56
+ readonly wrapUpHint?: DogpileOptions["wrapUpHint"];
55
57
  readonly emit?: (event: RunEvent) => void;
56
58
  }
57
59
 
@@ -67,6 +69,15 @@ export async function runSequential(options: SequentialRunOptions): Promise<RunR
67
69
  const startedAtMs = nowMs();
68
70
  let stopped = false;
69
71
  let termination: TerminationStopRecord | undefined;
72
+ const wrapUpHint = createWrapUpHintController({
73
+ protocol: options.protocol,
74
+ tier: options.tier,
75
+ ...(options.budget ? { budget: options.budget } : {}),
76
+ ...(options.terminate ? { terminate: options.terminate } : {}),
77
+ ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {})
78
+ });
79
+
80
+ warnOnProtocolTerminationMisconfiguration(options.protocol, options.terminate);
70
81
 
71
82
  const emit = (event: RunEvent): void => {
72
83
  events.push(event);
@@ -129,16 +140,27 @@ export async function runSequential(options: SequentialRunOptions): Promise<RunR
129
140
  turn,
130
141
  ...toolAvailability
131
142
  },
132
- messages: [
133
- {
134
- role: "system",
135
- content: buildSystemPrompt(agent)
136
- },
143
+ messages: wrapUpHint.inject(
144
+ [
145
+ {
146
+ role: "system",
147
+ content: buildSystemPrompt(agent)
148
+ },
149
+ {
150
+ role: "user",
151
+ content: input
152
+ }
153
+ ],
137
154
  {
138
- role: "user",
139
- content: input
155
+ runId,
156
+ protocol: "sequential",
157
+ cost: totalCost,
158
+ events,
159
+ transcript,
160
+ iteration: transcript.length,
161
+ elapsedMs: elapsedMs(startedAtMs)
140
162
  }
141
- ]
163
+ )
142
164
  };
143
165
  const response = await generateModelTurn({
144
166
  model: options.model,
@@ -277,16 +299,20 @@ export async function runSequential(options: SequentialRunOptions): Promise<RunR
277
299
  return stopped;
278
300
  }
279
301
 
280
- const stopRecord = evaluateTerminationStop(options.terminate, {
281
- runId,
282
- protocol: "sequential",
283
- tier: options.tier,
284
- cost: totalCost,
285
- events,
286
- transcript,
287
- iteration: transcript.length,
288
- elapsedMs: elapsedMs(startedAtMs)
289
- });
302
+ const stopRecord = evaluateTerminationStop(
303
+ options.terminate,
304
+ wrapUpHint.context({
305
+ runId,
306
+ protocol: "sequential",
307
+ protocolConfig: options.protocol,
308
+ protocolIteration: transcript.length,
309
+ cost: totalCost,
310
+ events,
311
+ transcript,
312
+ iteration: transcript.length,
313
+ elapsedMs: elapsedMs(startedAtMs)
314
+ })
315
+ );
290
316
 
291
317
  if (!stopRecord) {
292
318
  return false;
@@ -36,8 +36,9 @@ import {
36
36
  import { throwIfAborted } from "./cancellation.js";
37
37
  import { parseAgentDecision } from "./decisions.js";
38
38
  import { generateModelTurn } from "./model.js";
39
- import { evaluateTerminationStop } from "./termination.js";
39
+ import { evaluateTerminationStop, warnOnProtocolTerminationMisconfiguration } from "./termination.js";
40
40
  import { createRuntimeToolExecutor, executeModelResponseToolRequests, runtimeToolAvailability } from "./tools.js";
41
+ import { createWrapUpHintController } from "./wrap-up.js";
41
42
 
42
43
  interface SharedRunOptions {
43
44
  readonly intent: string;
@@ -51,6 +52,7 @@ interface SharedRunOptions {
51
52
  readonly seed?: string | number;
52
53
  readonly signal?: AbortSignal;
53
54
  readonly terminate?: TerminationCondition;
55
+ readonly wrapUpHint?: DogpileOptions["wrapUpHint"];
54
56
  readonly emit?: (event: RunEvent) => void;
55
57
  }
56
58
 
@@ -67,6 +69,15 @@ export async function runShared(options: SharedRunOptions): Promise<RunResult> {
67
69
  const startedAtMs = nowMs();
68
70
  let stopped = false;
69
71
  let termination: TerminationStopRecord | undefined;
72
+ const wrapUpHint = createWrapUpHintController({
73
+ protocol: options.protocol,
74
+ tier: options.tier,
75
+ ...(options.budget ? { budget: options.budget } : {}),
76
+ ...(options.terminate ? { terminate: options.terminate } : {}),
77
+ ...(options.wrapUpHint ? { wrapUpHint: options.wrapUpHint } : {})
78
+ });
79
+
80
+ warnOnProtocolTerminationMisconfiguration(options.protocol, options.terminate);
70
81
 
71
82
  const emit = (event: RunEvent): void => {
72
83
  events.push(event);
@@ -126,16 +137,27 @@ export async function runShared(options: SharedRunOptions): Promise<RunResult> {
126
137
  turn,
127
138
  ...toolAvailability
128
139
  },
129
- messages: [
130
- {
131
- role: "system",
132
- content: buildSystemPrompt(agent)
133
- },
140
+ messages: wrapUpHint.inject(
141
+ [
142
+ {
143
+ role: "system",
144
+ content: buildSystemPrompt(agent)
145
+ },
146
+ {
147
+ role: "user",
148
+ content: input
149
+ }
150
+ ],
134
151
  {
135
- role: "user",
136
- content: input
152
+ runId,
153
+ protocol: "shared",
154
+ cost: totalCost,
155
+ events,
156
+ transcript,
157
+ iteration: transcript.length,
158
+ elapsedMs: elapsedMs(startedAtMs)
137
159
  }
138
- ]
160
+ )
139
161
  };
140
162
  const response = await generateModelTurn({
141
163
  model: options.model,
@@ -285,16 +307,20 @@ export async function runShared(options: SharedRunOptions): Promise<RunResult> {
285
307
  return stopped;
286
308
  }
287
309
 
288
- const stopRecord = evaluateTerminationStop(options.terminate, {
289
- runId,
290
- protocol: "shared",
291
- tier: options.tier,
292
- cost: totalCost,
293
- events,
294
- transcript,
295
- iteration: transcript.length,
296
- elapsedMs: elapsedMs(startedAtMs)
297
- });
310
+ const stopRecord = evaluateTerminationStop(
311
+ options.terminate,
312
+ wrapUpHint.context({
313
+ runId,
314
+ protocol: "shared",
315
+ protocolConfig: options.protocol,
316
+ protocolIteration: transcript.length,
317
+ cost: totalCost,
318
+ events,
319
+ transcript,
320
+ iteration: transcript.length,
321
+ elapsedMs: elapsedMs(startedAtMs)
322
+ })
323
+ );
298
324
 
299
325
  if (!stopRecord) {
300
326
  return false;
@@ -1,7 +1,9 @@
1
1
  import type {
2
+ BroadcastProtocolConfig,
2
3
  BudgetStopReason,
3
4
  BudgetTerminationCondition,
4
5
  ConvergenceTerminationCondition,
6
+ CoordinatorProtocolConfig,
5
7
  FirstOfTerminationCondition,
6
8
  FirstOfTerminationConditions,
7
9
  FirstOfTerminationOutput,
@@ -10,6 +12,9 @@ import type {
10
12
  JudgeTerminationCondition,
11
13
  JsonObject,
12
14
  NormalizedStopReason,
15
+ ProtocolConfig,
16
+ SequentialProtocolConfig,
17
+ SharedProtocolConfig,
13
18
  TerminationStopRecord,
14
19
  StopTerminationDecision,
15
20
  TerminationCondition,
@@ -85,6 +90,10 @@ export function evaluateTermination(
85
90
  condition: TerminationCondition,
86
91
  context: TerminationEvaluationContext
87
92
  ): TerminationDecision {
93
+ if (isTerminationFloorBlocked(condition, context)) {
94
+ return { type: "continue", condition };
95
+ }
96
+
88
97
  switch (condition.kind) {
89
98
  case "budget":
90
99
  return evaluateBudget(condition, context);
@@ -166,6 +175,30 @@ export function evaluateTerminationStop(
166
175
  return stopRecord(condition, decision);
167
176
  }
168
177
 
178
+ /**
179
+ * Warn when a protocol-level termination floor cannot be satisfied because a
180
+ * lower iteration cap will stop the run first.
181
+ */
182
+ export function warnOnProtocolTerminationMisconfiguration(
183
+ protocol: ProtocolConfig,
184
+ terminate: TerminationCondition | undefined,
185
+ warn: (message: string) => void = console.warn
186
+ ): void {
187
+ const minTurns = protocolMinTurns(protocol);
188
+ if (minTurns === undefined || !terminate) {
189
+ return;
190
+ }
191
+
192
+ const limitingIterationBudget = smallestIterationBudget(terminate);
193
+ if (limitingIterationBudget === undefined || limitingIterationBudget >= minTurns) {
194
+ return;
195
+ }
196
+
197
+ warn(
198
+ `[dogpile] protocol.minTurns (${minTurns}) exceeds terminate budget maxIterations (${limitingIterationBudget}); maxIterations will win.`
199
+ );
200
+ }
201
+
169
202
  /**
170
203
  * Combine independently evaluated termination decisions with SDK precedence.
171
204
  *
@@ -384,6 +417,73 @@ function stopPrecedence(reason: NormalizedStopReason): number {
384
417
  return 2;
385
418
  }
386
419
 
420
+ function isTerminationFloorBlocked(condition: TerminationCondition, context: TerminationEvaluationContext): boolean {
421
+ if (condition.kind !== "convergence" && condition.kind !== "judge") {
422
+ return false;
423
+ }
424
+
425
+ const floor = protocolTerminationFloor(context.protocolConfig);
426
+ if (floor === undefined || floor <= 0) {
427
+ return false;
428
+ }
429
+
430
+ const progress = protocolProgress(context);
431
+ return progress < floor;
432
+ }
433
+
434
+ function protocolTerminationFloor(protocol: ProtocolConfig | undefined): number | undefined {
435
+ if (!protocol) {
436
+ return undefined;
437
+ }
438
+
439
+ switch (protocol.kind) {
440
+ case "broadcast":
441
+ return protocol.minRounds;
442
+ case "coordinator":
443
+ case "sequential":
444
+ case "shared":
445
+ return protocol.minTurns;
446
+ }
447
+ }
448
+
449
+ function protocolProgress(context: TerminationEvaluationContext): number {
450
+ return context.protocolIteration ?? context.iteration ?? context.transcript.length;
451
+ }
452
+
453
+ function protocolMinTurns(
454
+ protocol: ProtocolConfig
455
+ ): SequentialProtocolConfig["minTurns"] | CoordinatorProtocolConfig["minTurns"] | SharedProtocolConfig["minTurns"] {
456
+ switch (protocol.kind) {
457
+ case "broadcast":
458
+ return undefined;
459
+ case "coordinator":
460
+ case "sequential":
461
+ case "shared":
462
+ return protocol.minTurns;
463
+ }
464
+ }
465
+
466
+ function smallestIterationBudget(condition: TerminationCondition): number | undefined {
467
+ switch (condition.kind) {
468
+ case "budget":
469
+ return condition.maxIterations;
470
+ case "convergence":
471
+ case "judge":
472
+ return undefined;
473
+ case "firstOf": {
474
+ let smallest: number | undefined;
475
+ for (const child of condition.conditions) {
476
+ const budget = smallestIterationBudget(child);
477
+ if (budget === undefined) {
478
+ continue;
479
+ }
480
+ smallest = smallest === undefined ? budget : Math.min(smallest, budget);
481
+ }
482
+ return smallest;
483
+ }
484
+ }
485
+ }
486
+
387
487
  function judgeStopDetail(decision: JudgeEvaluationDecision, minScore?: number): JsonObject {
388
488
  return {
389
489
  decision: decision.type,
@@ -65,6 +65,7 @@ export function validateDogpileOptions(options: DogpileOptions): void {
65
65
  validateOptionalTemperature(options.temperature, "temperature");
66
66
  validateOptionalBudgetCaps(options.budget, "budget");
67
67
  validateOptionalTerminationCondition(options.terminate, "terminate");
68
+ validateOptionalWrapUpHint(options.wrapUpHint, "wrapUpHint");
68
69
  validateOptionalFunction(options.evaluate, "evaluate");
69
70
  validateOptionalSeed(options.seed, "seed");
70
71
  validateOptionalAbortSignal(options.signal, "signal");
@@ -87,6 +88,7 @@ export function validateEngineOptions(options: EngineOptions): void {
87
88
  validateOptionalTemperature(options.temperature, "temperature");
88
89
  validateOptionalBudgetCaps(options.budget, "budget");
89
90
  validateOptionalTerminationCondition(options.terminate, "terminate");
91
+ validateOptionalWrapUpHint(options.wrapUpHint, "wrapUpHint");
90
92
  validateOptionalFunction(options.evaluate, "evaluate");
91
93
  validateOptionalSeed(options.seed, "seed");
92
94
  validateOptionalAbortSignal(options.signal, "signal");
@@ -169,12 +171,14 @@ function validateProtocolConfig(value: ProtocolConfig, path: string): void {
169
171
  case "sequential":
170
172
  case "shared":
171
173
  validateOptionalPositiveInteger(record.maxTurns, `${path}.maxTurns`);
174
+ validateOptionalNonNegativeInteger(record.minTurns, `${path}.minTurns`);
172
175
  if (kind === "shared") {
173
176
  validateOptionalString(record.organizationalMemory, `${path}.organizationalMemory`);
174
177
  }
175
178
  return;
176
179
  case "broadcast":
177
180
  validateOptionalPositiveInteger(record.maxRounds, `${path}.maxRounds`);
181
+ validateOptionalNonNegativeInteger(record.minRounds, `${path}.minRounds`);
178
182
  return;
179
183
  }
180
184
  }
@@ -409,6 +413,27 @@ function validateOptionalTemperature(value: number | undefined, path: string): v
409
413
  validateOptionalNumberInRange(value, path, 0, 2);
410
414
  }
411
415
 
416
+ function validateOptionalWrapUpHint(value: unknown, path: string): void {
417
+ if (value === undefined) {
418
+ return;
419
+ }
420
+
421
+ const record = requireRecord(value, path);
422
+ validateOptionalNonNegativeInteger(record.atIteration, `${path}.atIteration`);
423
+ validateOptionalNumberInRange(record.atFraction, `${path}.atFraction`, 0, 1);
424
+ validateOptionalFunction(record.inject, `${path}.inject`);
425
+
426
+ if (record.atIteration === undefined && record.atFraction === undefined) {
427
+ invalidConfiguration({
428
+ path,
429
+ rule: "object",
430
+ message: "wrapUpHint must configure atIteration or atFraction.",
431
+ expected: "WrapUpHintConfig with atIteration or atFraction",
432
+ actual: value
433
+ });
434
+ }
435
+ }
436
+
412
437
  function validateOptionalSeed(value: string | number | undefined, path: string): void {
413
438
  if (value === undefined) {
414
439
  return;