@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,463 @@
1
+ import type {
2
+ BudgetStopReason,
3
+ BudgetTerminationCondition,
4
+ ConvergenceTerminationCondition,
5
+ FirstOfTerminationCondition,
6
+ FirstOfTerminationConditions,
7
+ FirstOfTerminationOutput,
8
+ JudgeEvaluationDecision,
9
+ JudgeStopReason,
10
+ JudgeTerminationCondition,
11
+ JsonObject,
12
+ NormalizedStopReason,
13
+ TerminationStopRecord,
14
+ StopTerminationDecision,
15
+ TerminationCondition,
16
+ TerminationDecision,
17
+ TerminationEvaluationContext,
18
+ TranscriptEntry
19
+ } from "../types.js";
20
+
21
+ /**
22
+ * Create a budget termination condition.
23
+ *
24
+ * The returned object is JSON-serializable and can be used directly in
25
+ * `terminate` or composed with {@link firstOf}.
26
+ */
27
+ export function budget(options: Omit<BudgetTerminationCondition, "kind">): BudgetTerminationCondition {
28
+ return {
29
+ kind: "budget",
30
+ ...options
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Create a convergence termination condition.
36
+ *
37
+ * The condition fires when the run has produced `stableTurns` sufficiently
38
+ * similar protocol outputs.
39
+ */
40
+ export function convergence(options: Omit<ConvergenceTerminationCondition, "kind">): ConvergenceTerminationCondition {
41
+ return {
42
+ kind: "convergence",
43
+ ...options
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Create a judge termination condition.
49
+ *
50
+ * The rubric is stored as serializable configuration so callers can replay or
51
+ * persist traces without SDK-owned state.
52
+ */
53
+ export function judge(options: Omit<JudgeTerminationCondition, "kind">): JudgeTerminationCondition {
54
+ return {
55
+ kind: "judge",
56
+ ...options
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Compose termination conditions so whichever child fires first wins.
62
+ *
63
+ * Conditions are evaluated in the order supplied by the caller. At least one
64
+ * condition is required so the composite is always meaningful and the public
65
+ * type remains a non-empty tuple.
66
+ */
67
+ export function firstOf(...conditions: FirstOfTerminationConditions): FirstOfTerminationCondition {
68
+ if (conditions.length === 0) {
69
+ throw new RangeError("firstOf requires at least one termination condition.");
70
+ }
71
+
72
+ return {
73
+ kind: "firstOf",
74
+ conditions
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Evaluate a serializable termination condition against the current run state.
80
+ *
81
+ * Budget, convergence, judge, and firstOf conditions are enforced from their
82
+ * own normalized inputs so one stop class cannot accidentally satisfy another.
83
+ */
84
+ export function evaluateTermination(
85
+ condition: TerminationCondition,
86
+ context: TerminationEvaluationContext
87
+ ): TerminationDecision {
88
+ switch (condition.kind) {
89
+ case "budget":
90
+ return evaluateBudget(condition, context);
91
+ case "firstOf":
92
+ return evaluateFirstOf(condition, context).decision;
93
+ case "convergence":
94
+ return evaluateConvergence(condition, context);
95
+ case "judge":
96
+ return evaluateJudge(condition, context);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Evaluate an ordered firstOf composition and return the winning child, if any.
102
+ */
103
+ export function evaluateFirstOf(
104
+ condition: FirstOfTerminationCondition,
105
+ context: TerminationEvaluationContext
106
+ ): FirstOfTerminationOutput {
107
+ const evaluated: TerminationDecision[] = [];
108
+
109
+ for (const [index, child] of condition.conditions.entries()) {
110
+ const decision = evaluateTermination(child, context);
111
+ evaluated.push(decision);
112
+
113
+ if (decision.type === "stop") {
114
+ return {
115
+ kind: "firstOf-output",
116
+ decision,
117
+ winningConditionIndex: index,
118
+ evaluated
119
+ };
120
+ }
121
+ }
122
+
123
+ return {
124
+ kind: "firstOf-output",
125
+ decision: { type: "continue", condition },
126
+ winningConditionIndex: null,
127
+ evaluated
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Evaluate a termination condition and return a trace-ready stop record.
133
+ *
134
+ * Protocol runners use this helper so the first policy decision that halts a
135
+ * run is recorded exactly once on the terminal event.
136
+ */
137
+ export function evaluateTerminationStop(
138
+ condition: TerminationCondition,
139
+ context: TerminationEvaluationContext
140
+ ): TerminationStopRecord | null {
141
+ if (condition.kind === "firstOf") {
142
+ const output = evaluateFirstOf(condition, context);
143
+ if (output.decision.type !== "stop" || output.winningConditionIndex === null) {
144
+ return null;
145
+ }
146
+
147
+ const winningCondition = condition.conditions[output.winningConditionIndex];
148
+ if (!winningCondition) {
149
+ throw new RangeError("firstOf stop referenced a missing winning condition.");
150
+ }
151
+
152
+ return stopRecord(condition, output.decision, {
153
+ kind: "firstOf-stop",
154
+ winningConditionIndex: output.winningConditionIndex,
155
+ winningCondition,
156
+ firedCondition: output.decision.condition,
157
+ evaluated: output.evaluated
158
+ });
159
+ }
160
+
161
+ const decision = evaluateTermination(condition, context);
162
+ if (decision.type !== "stop") {
163
+ return null;
164
+ }
165
+
166
+ return stopRecord(condition, decision);
167
+ }
168
+
169
+ /**
170
+ * Combine independently evaluated termination decisions with SDK precedence.
171
+ *
172
+ * Budget caps win over judge decisions, and judge decisions win over
173
+ * convergence. This keeps simultaneous stops deterministic while preserving
174
+ * each evaluator's normalized stop reason on the returned decision.
175
+ */
176
+ export function combineTerminationDecisions(
177
+ decisions: readonly TerminationDecision[]
178
+ ): TerminationDecision {
179
+ const stopDecisions = decisions.filter((decision): decision is StopTerminationDecision => decision.type === "stop");
180
+ if (stopDecisions.length === 0) {
181
+ const firstDecision = decisions[0];
182
+ if (!firstDecision) {
183
+ throw new RangeError("combineTerminationDecisions requires at least one decision.");
184
+ }
185
+
186
+ return firstDecision;
187
+ }
188
+
189
+ return stopDecisions.reduce((winner, candidate) =>
190
+ stopPrecedence(candidate.normalizedReason) < stopPrecedence(winner.normalizedReason) ? candidate : winner
191
+ );
192
+ }
193
+
194
+ /**
195
+ * Evaluate cost, token, iteration, and timeout caps for a budget condition.
196
+ */
197
+ export function evaluateBudget(
198
+ condition: BudgetTerminationCondition,
199
+ context: TerminationEvaluationContext
200
+ ): TerminationDecision {
201
+ const iteration = context.iteration ?? context.transcript.length;
202
+ const elapsedMs = context.elapsedMs ?? 0;
203
+
204
+ const costStop = stopIfReached(condition, "maxUsd", "cost", context.cost.usd);
205
+ if (costStop) {
206
+ return costStop;
207
+ }
208
+
209
+ const tokenStop = stopIfReached(condition, "maxTokens", "tokens", context.cost.totalTokens);
210
+ if (tokenStop) {
211
+ return tokenStop;
212
+ }
213
+
214
+ const iterationStop = stopIfReached(condition, "maxIterations", "iterations", iteration);
215
+ if (iterationStop) {
216
+ return iterationStop;
217
+ }
218
+
219
+ const timeoutStop = stopIfReached(condition, "timeoutMs", "timeout", elapsedMs);
220
+ if (timeoutStop) {
221
+ return timeoutStop;
222
+ }
223
+
224
+ return { type: "continue", condition };
225
+ }
226
+
227
+ /**
228
+ * Evaluate protocol-level convergence from recent coordination outputs.
229
+ *
230
+ * This intentionally ignores budget caps and judge quality state. Budget and
231
+ * judge conditions can be composed with convergence through `firstOf`, but a
232
+ * convergence condition itself only reads protocol output signals.
233
+ */
234
+ export function evaluateConvergence(
235
+ condition: ConvergenceTerminationCondition,
236
+ context: TerminationEvaluationContext
237
+ ): TerminationDecision {
238
+ const stableTurns = Math.max(1, Math.ceil(condition.stableTurns));
239
+ if (context.transcript.length < stableTurns) {
240
+ return { type: "continue", condition };
241
+ }
242
+
243
+ const recentEntries = context.transcript.slice(-stableTurns);
244
+ const recentOutputs = recentEntries.map((entry) => entry.output);
245
+ const similarities = consecutiveSimilarities(recentEntries);
246
+ const observedSimilarity = similarities.length === 0 ? 1 : Math.min(...similarities);
247
+
248
+ if (observedSimilarity < condition.minSimilarity) {
249
+ return { type: "continue", condition };
250
+ }
251
+
252
+ return {
253
+ type: "stop",
254
+ condition,
255
+ reason: "convergence",
256
+ normalizedReason: "convergence",
257
+ detail: {
258
+ protocol: context.protocol,
259
+ stableTurns,
260
+ minSimilarity: condition.minSimilarity,
261
+ observedSimilarity,
262
+ outputs: recentOutputs
263
+ }
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Evaluate caller-owned judge state without reading budget or convergence data.
269
+ *
270
+ * Explicit accept/reject verdicts always halt. Score-only decisions halt when
271
+ * they meet `minScore`; when `minScore` is omitted, any score-only decision is
272
+ * treated as the judge's terminal decision.
273
+ */
274
+ export function evaluateJudge(
275
+ condition: JudgeTerminationCondition,
276
+ context: TerminationEvaluationContext
277
+ ): TerminationDecision {
278
+ const decision = context.judgeDecision ?? scoreDecisionFromQuality(context.quality);
279
+ if (!decision) {
280
+ return { type: "continue", condition };
281
+ }
282
+
283
+ switch (decision.type) {
284
+ case "accept":
285
+ return judgeStop(condition, "accepted", decision);
286
+ case "reject":
287
+ return judgeStop(condition, "rejected", decision);
288
+ case "score": {
289
+ const minScore = condition.minScore;
290
+ if (minScore !== undefined && decision.score < minScore) {
291
+ return { type: "continue", condition };
292
+ }
293
+
294
+ return judgeStop(condition, "score-threshold", decision, minScore);
295
+ }
296
+ }
297
+ }
298
+
299
+ function stopIfReached(
300
+ condition: BudgetTerminationCondition,
301
+ cap: "maxUsd" | "maxTokens" | "maxIterations" | "timeoutMs",
302
+ reason: BudgetStopReason,
303
+ observed: number
304
+ ): StopTerminationDecision | null {
305
+ const limit = condition[cap];
306
+ if (limit === undefined || observed < limit) {
307
+ return null;
308
+ }
309
+
310
+ return {
311
+ type: "stop",
312
+ condition,
313
+ reason: "budget",
314
+ normalizedReason: normalizeBudgetStopReason(reason),
315
+ budgetReason: reason,
316
+ detail: {
317
+ cap,
318
+ limit,
319
+ observed
320
+ }
321
+ };
322
+ }
323
+
324
+ function scoreDecisionFromQuality(quality: number | undefined): JudgeEvaluationDecision | null {
325
+ if (quality === undefined) {
326
+ return null;
327
+ }
328
+
329
+ return {
330
+ type: "score",
331
+ score: quality
332
+ };
333
+ }
334
+
335
+ function judgeStop(
336
+ condition: JudgeTerminationCondition,
337
+ judgeReason: JudgeStopReason,
338
+ decision: JudgeEvaluationDecision,
339
+ minScore?: number
340
+ ): StopTerminationDecision {
341
+ return {
342
+ type: "stop",
343
+ condition,
344
+ reason: "judge",
345
+ normalizedReason: normalizeJudgeStopReason(judgeReason),
346
+ judgeReason,
347
+ detail: judgeStopDetail(decision, minScore)
348
+ };
349
+ }
350
+
351
+ function normalizeBudgetStopReason(reason: BudgetStopReason): NormalizedStopReason {
352
+ switch (reason) {
353
+ case "cost":
354
+ return "budget:cost";
355
+ case "tokens":
356
+ return "budget:tokens";
357
+ case "iterations":
358
+ return "budget:iterations";
359
+ case "timeout":
360
+ return "budget:timeout";
361
+ }
362
+ }
363
+
364
+ function normalizeJudgeStopReason(reason: JudgeStopReason): NormalizedStopReason {
365
+ switch (reason) {
366
+ case "accepted":
367
+ return "judge:accepted";
368
+ case "rejected":
369
+ return "judge:rejected";
370
+ case "score-threshold":
371
+ return "judge:score-threshold";
372
+ }
373
+ }
374
+
375
+ function stopPrecedence(reason: NormalizedStopReason): number {
376
+ if (reason.startsWith("budget:")) {
377
+ return 0;
378
+ }
379
+
380
+ if (reason.startsWith("judge:")) {
381
+ return 1;
382
+ }
383
+
384
+ return 2;
385
+ }
386
+
387
+ function judgeStopDetail(decision: JudgeEvaluationDecision, minScore?: number): JsonObject {
388
+ return {
389
+ decision: decision.type,
390
+ ...(decision.score !== undefined ? { score: decision.score } : {}),
391
+ ...(minScore !== undefined ? { minScore } : {}),
392
+ ...(decision.rationale !== undefined ? { rationale: decision.rationale } : {}),
393
+ ...(decision.metadata !== undefined ? { metadata: decision.metadata } : {})
394
+ };
395
+ }
396
+
397
+ function stopRecord(
398
+ rootCondition: TerminationCondition,
399
+ decision: StopTerminationDecision,
400
+ firstOfRecord?: NonNullable<TerminationStopRecord["firstOf"]>
401
+ ): TerminationStopRecord {
402
+ return {
403
+ kind: "termination-stop",
404
+ rootCondition,
405
+ firedCondition: decision.condition,
406
+ reason: decision.reason,
407
+ normalizedReason: decision.normalizedReason,
408
+ ...(decision.budgetReason !== undefined ? { budgetReason: decision.budgetReason } : {}),
409
+ ...(decision.judgeReason !== undefined ? { judgeReason: decision.judgeReason } : {}),
410
+ ...(decision.detail !== undefined ? { detail: decision.detail } : {}),
411
+ ...(firstOfRecord !== undefined ? { firstOf: firstOfRecord } : {})
412
+ };
413
+ }
414
+
415
+ function consecutiveSimilarities(entries: readonly TranscriptEntry[]): readonly number[] {
416
+ const similarities: number[] = [];
417
+
418
+ for (let index = 1; index < entries.length; index += 1) {
419
+ const previous = entries[index - 1];
420
+ const current = entries[index];
421
+ if (previous && current) {
422
+ similarities.push(outputSimilarity(previous.output, current.output));
423
+ }
424
+ }
425
+
426
+ return similarities;
427
+ }
428
+
429
+ function outputSimilarity(left: string, right: string): number {
430
+ const normalizedLeft = normalizeOutput(left);
431
+ const normalizedRight = normalizeOutput(right);
432
+
433
+ if (normalizedLeft === normalizedRight) {
434
+ return 1;
435
+ }
436
+
437
+ const leftTokens = tokenize(normalizedLeft);
438
+ const rightTokens = tokenize(normalizedRight);
439
+ if (leftTokens.length === 0 || rightTokens.length === 0) {
440
+ return 0;
441
+ }
442
+
443
+ const leftSet = new Set(leftTokens);
444
+ const rightSet = new Set(rightTokens);
445
+ let intersection = 0;
446
+
447
+ for (const token of leftSet) {
448
+ if (rightSet.has(token)) {
449
+ intersection += 1;
450
+ }
451
+ }
452
+
453
+ const union = new Set([...leftSet, ...rightSet]).size;
454
+ return union === 0 ? 0 : intersection / union;
455
+ }
456
+
457
+ function normalizeOutput(output: string): string {
458
+ return output.trim().toLowerCase();
459
+ }
460
+
461
+ function tokenize(output: string): readonly string[] {
462
+ return output.split(/[^a-z0-9]+/u).filter((token) => token.length > 0);
463
+ }