@dogpile/sdk 0.2.1 → 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 (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +86 -655
  3. package/dist/browser/index.js +337 -22
  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/runtime/broadcast.d.ts +1 -0
  8. package/dist/runtime/broadcast.d.ts.map +1 -1
  9. package/dist/runtime/broadcast.js +27 -6
  10. package/dist/runtime/broadcast.js.map +1 -1
  11. package/dist/runtime/coordinator.d.ts +1 -0
  12. package/dist/runtime/coordinator.d.ts.map +1 -1
  13. package/dist/runtime/coordinator.js +45 -8
  14. package/dist/runtime/coordinator.js.map +1 -1
  15. package/dist/runtime/engine.d.ts.map +1 -1
  16. package/dist/runtime/engine.js +5 -0
  17. package/dist/runtime/engine.js.map +1 -1
  18. package/dist/runtime/sequential.d.ts +1 -0
  19. package/dist/runtime/sequential.d.ts.map +1 -1
  20. package/dist/runtime/sequential.js +24 -6
  21. package/dist/runtime/sequential.js.map +1 -1
  22. package/dist/runtime/shared.d.ts +1 -0
  23. package/dist/runtime/shared.d.ts.map +1 -1
  24. package/dist/runtime/shared.js +24 -6
  25. package/dist/runtime/shared.js.map +1 -1
  26. package/dist/runtime/termination.d.ts +6 -1
  27. package/dist/runtime/termination.d.ts.map +1 -1
  28. package/dist/runtime/termination.js +75 -0
  29. package/dist/runtime/termination.js.map +1 -1
  30. package/dist/runtime/validation.d.ts.map +1 -1
  31. package/dist/runtime/validation.js +22 -0
  32. package/dist/runtime/validation.js.map +1 -1
  33. package/dist/runtime/wrap-up.d.ts +26 -0
  34. package/dist/runtime/wrap-up.d.ts.map +1 -0
  35. package/dist/runtime/wrap-up.js +178 -0
  36. package/dist/runtime/wrap-up.js.map +1 -0
  37. package/dist/types.d.ts +68 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +3 -2
  40. package/src/index.ts +3 -1
  41. package/src/runtime/broadcast.ts +49 -19
  42. package/src/runtime/coordinator.ts +83 -27
  43. package/src/runtime/engine.ts +6 -0
  44. package/src/runtime/sequential.ts +45 -19
  45. package/src/runtime/shared.ts +45 -19
  46. package/src/runtime/termination.ts +100 -0
  47. package/src/runtime/validation.ts +25 -0
  48. package/src/runtime/wrap-up.ts +257 -0
  49. package/src/types.ts +70 -0
@@ -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;
@@ -0,0 +1,257 @@
1
+ import type {
2
+ BudgetCaps,
3
+ BudgetTier,
4
+ EngineOptions,
5
+ JsonObject,
6
+ ModelMessage,
7
+ Protocol,
8
+ ProtocolConfig,
9
+ RunEvent,
10
+ TerminationCondition,
11
+ TerminationEvaluationContext,
12
+ TranscriptEntry,
13
+ WrapUpHintConfig
14
+ } from "../types.js";
15
+
16
+ interface WrapUpControllerOptions {
17
+ readonly protocol: ProtocolConfig;
18
+ readonly tier: BudgetTier;
19
+ readonly budget?: BudgetCaps;
20
+ readonly terminate?: TerminationCondition;
21
+ readonly wrapUpHint?: WrapUpHintConfig;
22
+ }
23
+
24
+ interface WrapUpContextOptions {
25
+ readonly runId: string;
26
+ readonly protocol: Protocol;
27
+ readonly protocolConfig?: ProtocolConfig;
28
+ readonly cost: TerminationEvaluationContext["cost"];
29
+ readonly events: readonly RunEvent[];
30
+ readonly transcript: readonly TranscriptEntry[];
31
+ readonly iteration?: number;
32
+ readonly protocolIteration?: number;
33
+ readonly elapsedMs?: number;
34
+ readonly metadata?: JsonObject;
35
+ }
36
+
37
+ export function createWrapUpHintController(options: WrapUpControllerOptions) {
38
+ const hint = options.wrapUpHint;
39
+ const effectiveBudget = effectiveWrapUpBudget(options.budget, options.terminate);
40
+ let emitted = false;
41
+
42
+ return {
43
+ context(context: WrapUpContextOptions): TerminationEvaluationContext {
44
+ return wrapUpEvaluationContext({
45
+ ...context,
46
+ tier: options.tier,
47
+ ...(effectiveBudget !== undefined ? { budget: effectiveBudget } : {})
48
+ });
49
+ },
50
+
51
+ inject(messages: readonly ModelMessage[], context: WrapUpContextOptions): readonly ModelMessage[] {
52
+ if (!hint || emitted) {
53
+ return messages;
54
+ }
55
+
56
+ const evaluationContext = wrapUpEvaluationContext({
57
+ ...context,
58
+ tier: options.tier,
59
+ ...(effectiveBudget !== undefined ? { budget: effectiveBudget } : {})
60
+ });
61
+
62
+ if (!shouldInjectWrapUpHint(hint, evaluationContext)) {
63
+ return messages;
64
+ }
65
+
66
+ const content = (hint.inject ?? defaultWrapUpHint)(evaluationContext);
67
+ emitted = true;
68
+
69
+ return [
70
+ messages[0] ?? { role: "system", content: "" },
71
+ { role: "system", content },
72
+ ...messages.slice(1)
73
+ ];
74
+ }
75
+ };
76
+ }
77
+
78
+ function wrapUpEvaluationContext(
79
+ options: WrapUpContextOptions & { readonly tier: BudgetTier; readonly budget?: BudgetCaps }
80
+ ): TerminationEvaluationContext {
81
+ const iteration = options.iteration ?? options.transcript.length;
82
+ const elapsedMs = options.elapsedMs;
83
+
84
+ return {
85
+ runId: options.runId,
86
+ protocol: options.protocol,
87
+ tier: options.tier,
88
+ cost: options.cost,
89
+ events: options.events,
90
+ transcript: options.transcript,
91
+ ...(options.protocolConfig !== undefined ? { protocolConfig: options.protocolConfig } : {}),
92
+ ...(iteration !== undefined ? { iteration } : {}),
93
+ ...(options.protocolIteration !== undefined ? { protocolIteration: options.protocolIteration } : {}),
94
+ ...(elapsedMs !== undefined ? { elapsedMs } : {}),
95
+ ...(options.budget !== undefined ? { budget: options.budget } : {}),
96
+ ...(options.budget !== undefined
97
+ ? {
98
+ remainingBudget: remainingBudget(options.budget, {
99
+ cost: options.cost,
100
+ iteration,
101
+ elapsedMs
102
+ })
103
+ }
104
+ : {}),
105
+ ...(options.metadata !== undefined ? { metadata: options.metadata } : {})
106
+ };
107
+ }
108
+
109
+ function shouldInjectWrapUpHint(hint: WrapUpHintConfig, context: TerminationEvaluationContext): boolean {
110
+ const iteration = context.iteration ?? context.transcript.length;
111
+
112
+ if (hint.atIteration !== undefined && iteration >= hint.atIteration) {
113
+ return true;
114
+ }
115
+
116
+ if (hint.atFraction === undefined || context.budget === undefined) {
117
+ return false;
118
+ }
119
+
120
+ const fraction = hint.atFraction;
121
+ return (
122
+ capFractionReached(iteration, context.budget.maxIterations, fraction) ||
123
+ capFractionReached(context.elapsedMs, context.budget.timeoutMs, fraction)
124
+ );
125
+ }
126
+
127
+ function capFractionReached(current: number | undefined, limit: number | undefined, fraction: number): boolean {
128
+ if (current === undefined || limit === undefined || limit <= 0) {
129
+ return false;
130
+ }
131
+
132
+ return current / limit >= fraction;
133
+ }
134
+
135
+ function remainingBudget(
136
+ budget: BudgetCaps,
137
+ current: {
138
+ readonly cost: TerminationEvaluationContext["cost"];
139
+ readonly iteration: number | undefined;
140
+ readonly elapsedMs: number | undefined;
141
+ }
142
+ ): NonNullable<TerminationEvaluationContext["remainingBudget"]> {
143
+ return {
144
+ ...(budget.maxIterations !== undefined && current.iteration !== undefined
145
+ ? { iterations: Math.max(0, budget.maxIterations - current.iteration) }
146
+ : {}),
147
+ ...(budget.timeoutMs !== undefined && current.elapsedMs !== undefined
148
+ ? { timeoutMs: Math.max(0, budget.timeoutMs - current.elapsedMs) }
149
+ : {}),
150
+ ...(budget.maxUsd !== undefined ? { usd: Math.max(0, budget.maxUsd - current.cost.usd) } : {}),
151
+ ...(budget.maxTokens !== undefined ? { tokens: Math.max(0, budget.maxTokens - current.cost.totalTokens) } : {})
152
+ };
153
+ }
154
+
155
+ function defaultWrapUpHint(context: TerminationEvaluationContext): string {
156
+ const parts: string[] = [];
157
+ const remaining = context.remainingBudget;
158
+
159
+ if (remaining?.iterations !== undefined) {
160
+ const label = remaining.iterations === 1 ? "turn" : "turns";
161
+ parts.push(`${remaining.iterations} ${label} remaining`);
162
+ }
163
+ if (remaining?.timeoutMs !== undefined) {
164
+ parts.push(`${formatRemainingTime(remaining.timeoutMs)} remaining`);
165
+ }
166
+
167
+ const remainingText =
168
+ parts.length === 0 ? "You are approaching a configured hard limit." : `You are approaching a hard limit with ${parts.join(" and ")}.`;
169
+ return `[wrap-up] ${remainingText} If you have enough context, package your work now and return a final-ready answer.`;
170
+ }
171
+
172
+ function formatRemainingTime(timeoutMs: number): string {
173
+ if (timeoutMs >= 1_000) {
174
+ return `${(timeoutMs / 1_000).toFixed(1)}s`;
175
+ }
176
+
177
+ return `${timeoutMs}ms`;
178
+ }
179
+
180
+ function effectiveWrapUpBudget(
181
+ budget: BudgetCaps | undefined,
182
+ terminate: EngineOptions["terminate"]
183
+ ): BudgetCaps | undefined {
184
+ const terminationBudget = budgetCapsFromTermination(terminate);
185
+ if (!budget && !terminationBudget) {
186
+ return undefined;
187
+ }
188
+
189
+ const maxUsd = minCap(budget?.maxUsd, terminationBudget?.maxUsd);
190
+ const maxTokens = minCap(budget?.maxTokens, terminationBudget?.maxTokens);
191
+ const maxIterations = minCap(budget?.maxIterations, terminationBudget?.maxIterations);
192
+ const timeoutMs = minCap(budget?.timeoutMs, terminationBudget?.timeoutMs);
193
+
194
+ return {
195
+ ...(maxUsd !== undefined ? { maxUsd } : {}),
196
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
197
+ ...(maxIterations !== undefined ? { maxIterations } : {}),
198
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
199
+ };
200
+ }
201
+
202
+ function budgetCapsFromTermination(condition: TerminationCondition | undefined): BudgetCaps | undefined {
203
+ if (!condition) {
204
+ return undefined;
205
+ }
206
+
207
+ switch (condition.kind) {
208
+ case "budget":
209
+ return {
210
+ ...(condition.maxUsd !== undefined ? { maxUsd: condition.maxUsd } : {}),
211
+ ...(condition.maxTokens !== undefined ? { maxTokens: condition.maxTokens } : {}),
212
+ ...(condition.maxIterations !== undefined ? { maxIterations: condition.maxIterations } : {}),
213
+ ...(condition.timeoutMs !== undefined ? { timeoutMs: condition.timeoutMs } : {})
214
+ };
215
+ case "firstOf": {
216
+ let merged: BudgetCaps | undefined;
217
+ for (const child of condition.conditions) {
218
+ merged = mergeBudgetCaps(merged, budgetCapsFromTermination(child));
219
+ }
220
+ return merged;
221
+ }
222
+ case "convergence":
223
+ case "judge":
224
+ return undefined;
225
+ }
226
+ }
227
+
228
+ function mergeBudgetCaps(left: BudgetCaps | undefined, right: BudgetCaps | undefined): BudgetCaps | undefined {
229
+ if (!left) {
230
+ return right;
231
+ }
232
+ if (!right) {
233
+ return left;
234
+ }
235
+
236
+ const maxUsd = minCap(left.maxUsd, right.maxUsd);
237
+ const maxTokens = minCap(left.maxTokens, right.maxTokens);
238
+ const maxIterations = minCap(left.maxIterations, right.maxIterations);
239
+ const timeoutMs = minCap(left.timeoutMs, right.timeoutMs);
240
+
241
+ return {
242
+ ...(maxUsd !== undefined ? { maxUsd } : {}),
243
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
244
+ ...(maxIterations !== undefined ? { maxIterations } : {}),
245
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
246
+ };
247
+ }
248
+
249
+ function minCap(left: number | undefined, right: number | undefined): number | undefined {
250
+ if (left === undefined) {
251
+ return right;
252
+ }
253
+ if (right === undefined) {
254
+ return left;
255
+ }
256
+ return Math.min(left, right);
257
+ }
package/src/types.ts CHANGED
@@ -260,6 +260,12 @@ export interface SequentialProtocolConfig {
260
260
  readonly kind: "sequential";
261
261
  /** Maximum number of agent turns to execute; defaults to `3` for named protocols. */
262
262
  readonly maxTurns?: number;
263
+ /**
264
+ * Floor for convergence and judge termination checks.
265
+ *
266
+ * Budget caps still apply immediately. Defaults to `0` when omitted.
267
+ */
268
+ readonly minTurns?: number;
263
269
  }
264
270
 
265
271
  /**
@@ -274,6 +280,12 @@ export interface CoordinatorProtocolConfig {
274
280
  readonly kind: "coordinator";
275
281
  /** Maximum number of coordinator-managed turns to execute; defaults to `3` for named protocols. */
276
282
  readonly maxTurns?: number;
283
+ /**
284
+ * Floor for convergence and judge termination checks.
285
+ *
286
+ * Budget caps still apply immediately. Defaults to `0` when omitted.
287
+ */
288
+ readonly minTurns?: number;
277
289
  }
278
290
 
279
291
  /**
@@ -288,6 +300,12 @@ export interface BroadcastProtocolConfig {
288
300
  readonly kind: "broadcast";
289
301
  /** Maximum number of broadcast/merge rounds to execute; defaults to `2` for named protocols. */
290
302
  readonly maxRounds?: number;
303
+ /**
304
+ * Floor for convergence and judge termination checks.
305
+ *
306
+ * Budget caps still apply immediately. Defaults to `0` when omitted.
307
+ */
308
+ readonly minRounds?: number;
291
309
  }
292
310
 
293
311
  /**
@@ -302,6 +320,12 @@ export interface SharedProtocolConfig {
302
320
  readonly kind: "shared";
303
321
  /** Maximum number of shared-state turns to execute; defaults to `3` for named protocols. */
304
322
  readonly maxTurns?: number;
323
+ /**
324
+ * Floor for convergence and judge termination checks.
325
+ *
326
+ * Budget caps still apply immediately. Defaults to `0` when omitted.
327
+ */
328
+ readonly minTurns?: number;
305
329
  /** Optional organizational memory snapshot visible to every shared agent. */
306
330
  readonly organizationalMemory?: string;
307
331
  }
@@ -526,6 +550,8 @@ export interface TerminationEvaluationContext {
526
550
  readonly runId: string;
527
551
  /** Protocol currently executing. */
528
552
  readonly protocol: Protocol;
553
+ /** Exact normalized protocol configuration when the evaluator needs protocol-specific limits. */
554
+ readonly protocolConfig?: ProtocolConfig;
529
555
  /** Cost/quality tier selected for the run. */
530
556
  readonly tier: BudgetTier;
531
557
  /** Current accumulated cost and token usage. */
@@ -536,8 +562,14 @@ export interface TerminationEvaluationContext {
536
562
  readonly transcript: readonly TranscriptEntry[];
537
563
  /** Completed model-turn iterations at the evaluation point. */
538
564
  readonly iteration?: number;
565
+ /** Protocol-native progress count: turns for sequential/coordinator/shared, rounds for broadcast. */
566
+ readonly protocolIteration?: number;
539
567
  /** Elapsed runtime in milliseconds at the evaluation point. */
540
568
  readonly elapsedMs?: number;
569
+ /** Effective hard caps visible to this evaluation point. */
570
+ readonly budget?: BudgetCaps;
571
+ /** Remaining headroom computed from the effective hard caps at this evaluation point. */
572
+ readonly remainingBudget?: RemainingBudget;
541
573
  /** Optional normalized judge or quality score in the inclusive range `0..1`. */
542
574
  readonly quality?: NormalizedQualityScore;
543
575
  /** Optional caller-owned judge decision for judge termination checks. */
@@ -546,6 +578,20 @@ export interface TerminationEvaluationContext {
546
578
  readonly metadata?: JsonObject;
547
579
  }
548
580
 
581
+ /**
582
+ * Remaining budget headroom derived from the current evaluation context.
583
+ */
584
+ export interface RemainingBudget {
585
+ /** Remaining turn iterations before an iteration cap is reached. */
586
+ readonly iterations?: number;
587
+ /** Remaining elapsed milliseconds before a timeout cap is reached. */
588
+ readonly timeoutMs?: number;
589
+ /** Remaining spend in US dollars before a cost cap is reached. */
590
+ readonly usd?: number;
591
+ /** Remaining total tokens before a token cap is reached. */
592
+ readonly tokens?: number;
593
+ }
594
+
549
595
  /**
550
596
  * Decision returned by a termination condition evaluator.
551
597
  */
@@ -2547,6 +2593,26 @@ export interface BudgetCostTierOptions {
2547
2593
  readonly budget?: BudgetCaps;
2548
2594
  }
2549
2595
 
2596
+ /**
2597
+ * Advisory wrap-up hint injected into the next model turn near a hard cap.
2598
+ */
2599
+ export interface WrapUpHintConfig {
2600
+ /** Absolute completed model-turn iteration at which to inject the hint once. */
2601
+ readonly atIteration?: number;
2602
+ /**
2603
+ * Fraction of `maxIterations` or `timeoutMs` at which to inject the hint once.
2604
+ *
2605
+ * `0.8` means the next turn after reaching 80% of a supported cap receives
2606
+ * the wrap-up hint.
2607
+ */
2608
+ readonly atFraction?: number;
2609
+ /**
2610
+ * Optional custom hint builder. When omitted, the SDK injects a default
2611
+ * message that describes the remaining turn and/or time budget.
2612
+ */
2613
+ readonly inject?: (context: TerminationEvaluationContext) => string;
2614
+ }
2615
+
2550
2616
  /**
2551
2617
  * Options accepted by the high-level single-call workflow APIs.
2552
2618
  *
@@ -2576,6 +2642,8 @@ export interface DogpileOptions extends BudgetCostTierOptions {
2576
2642
  readonly temperature?: number;
2577
2643
  /** Optional composable termination policy for budget, convergence, judge, or firstOf stop conditions. */
2578
2644
  readonly terminate?: TerminationCondition;
2645
+ /** Optional one-shot advisory hint injected into the next model turn near a hard cap. */
2646
+ readonly wrapUpHint?: WrapUpHintConfig;
2579
2647
  /** Optional caller-owned evaluator that supplies quality and evaluation data. */
2580
2648
  readonly evaluate?: RunEvaluator;
2581
2649
  /** Optional deterministic seed recorded in the replay trace. */
@@ -2638,6 +2706,8 @@ export interface EngineOptions {
2638
2706
  readonly budget?: Omit<Budget, "tier">;
2639
2707
  /** Optional composable termination policy for budget, convergence, judge, or firstOf stop conditions. */
2640
2708
  readonly terminate?: TerminationCondition;
2709
+ /** Optional one-shot advisory hint injected into the next model turn near a hard cap. */
2710
+ readonly wrapUpHint?: WrapUpHintConfig;
2641
2711
  /** Optional caller-owned evaluator that supplies quality and evaluation data. */
2642
2712
  readonly evaluate?: RunEvaluator;
2643
2713
  /** Optional deterministic seed recorded in the replay trace. */