@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,771 @@
1
+ import { DogpileError } from "../types.js";
2
+ import type {
3
+ AgentSpec,
4
+ BudgetCaps,
5
+ BudgetTier,
6
+ ConfiguredModelProvider,
7
+ DogpileOptions,
8
+ EngineOptions,
9
+ JsonObject,
10
+ JsonValue,
11
+ ProtocolConfig,
12
+ ProtocolName,
13
+ ProtocolSelection,
14
+ RuntimeTool,
15
+ TerminationCondition
16
+ } from "../types.js";
17
+
18
+ const protocolNames = ["coordinator", "sequential", "broadcast", "shared"] as const;
19
+ const budgetTiers = ["fast", "balanced", "quality"] as const;
20
+
21
+ type ValidationRule =
22
+ | "required"
23
+ | "non-empty-string"
24
+ | "finite-number"
25
+ | "non-negative-number"
26
+ | "positive-integer"
27
+ | "non-negative-integer"
28
+ | "range"
29
+ | "enum"
30
+ | "object"
31
+ | "array"
32
+ | "boolean"
33
+ | "function"
34
+ | "json-object"
35
+ | "abort-signal"
36
+ | "model-provider"
37
+ | "runtime-tool"
38
+ | "termination-condition";
39
+
40
+ interface ValidationFailureOptions {
41
+ readonly path: string;
42
+ readonly rule: ValidationRule;
43
+ readonly message: string;
44
+ readonly expected: string;
45
+ readonly actual: unknown;
46
+ }
47
+
48
+ /**
49
+ * Validate high-level caller options before any protocol execution starts.
50
+ */
51
+ export function validateDogpileOptions(options: DogpileOptions): void {
52
+ requireRecord(options, "options");
53
+ validateMissionIntent(options.intent);
54
+
55
+ if (options.protocol !== undefined) {
56
+ validateProtocolSelection(options.protocol, "protocol");
57
+ }
58
+ if (options.tier !== undefined) {
59
+ validateBudgetTier(options.tier, "tier");
60
+ }
61
+
62
+ validateModelProviderRegistration(options.model, "model");
63
+ validateOptionalAgents(options.agents, "agents");
64
+ validateOptionalRuntimeTools(options.tools, "tools");
65
+ validateOptionalTemperature(options.temperature, "temperature");
66
+ validateOptionalBudgetCaps(options.budget, "budget");
67
+ validateOptionalTerminationCondition(options.terminate, "terminate");
68
+ validateOptionalFunction(options.evaluate, "evaluate");
69
+ validateOptionalSeed(options.seed, "seed");
70
+ validateOptionalAbortSignal(options.signal, "signal");
71
+ }
72
+
73
+ export function validateMissionIntent(intent: unknown, path = "intent"): void {
74
+ validateNonEmptyString(intent, path, "intent is required.");
75
+ }
76
+
77
+ /**
78
+ * Validate low-level engine configuration before normalizing reusable controls.
79
+ */
80
+ export function validateEngineOptions(options: EngineOptions): void {
81
+ requireRecord(options, "options");
82
+ validateProtocolSelection(options.protocol, "protocol");
83
+ validateBudgetTier(options.tier, "tier");
84
+ validateModelProviderRegistration(options.model, "model");
85
+ validateOptionalAgents(options.agents, "agents");
86
+ validateOptionalRuntimeTools(options.tools, "tools");
87
+ validateOptionalTemperature(options.temperature, "temperature");
88
+ validateOptionalBudgetCaps(options.budget, "budget");
89
+ validateOptionalTerminationCondition(options.terminate, "terminate");
90
+ validateOptionalFunction(options.evaluate, "evaluate");
91
+ validateOptionalSeed(options.seed, "seed");
92
+ validateOptionalAbortSignal(options.signal, "signal");
93
+ }
94
+
95
+ /**
96
+ * Validate Vercel AI adapter factory options at construction time.
97
+ */
98
+ export function validateVercelAIProviderOptions(options: unknown): void {
99
+ const record = requireRecord(options, "options");
100
+
101
+ if (record.model === undefined) {
102
+ invalidConfiguration({
103
+ path: "model",
104
+ rule: "required",
105
+ message: "model is required.",
106
+ expected: "a Vercel AI language model",
107
+ actual: record.model
108
+ });
109
+ }
110
+ if (typeof record.model === "string") {
111
+ validateNonEmptyString(record.model, "model", "model must not be empty.");
112
+ } else {
113
+ validateVercelAILanguageModel(record.model, "model");
114
+ }
115
+
116
+ validateOptionalNonEmptyString(record.id, "id");
117
+ validateOptionalBoolean(record.streaming, "streaming");
118
+ validateOptionalFunction(record.generateText, "generateText");
119
+ validateOptionalFunction(record.streamText, "streamText");
120
+ validateOptionalFunction(record.costEstimator, "costEstimator");
121
+ validateOptionalPositiveInteger(record.maxOutputTokens, "maxOutputTokens");
122
+ validateOptionalNumberInRange(record.topP, "topP", 0, 1);
123
+ validateOptionalPositiveInteger(record.topK, "topK");
124
+ validateOptionalNumberInRange(record.presencePenalty, "presencePenalty", -2, 2);
125
+ validateOptionalNumberInRange(record.frequencyPenalty, "frequencyPenalty", -2, 2);
126
+ validateOptionalStringArray(record.stopSequences, "stopSequences");
127
+ validateOptionalInteger(record.seed, "seed");
128
+ validateOptionalNonNegativeInteger(record.maxRetries, "maxRetries");
129
+ validateOptionalAbortSignal(record.abortSignal, "abortSignal");
130
+ validateOptionalHeaders(record.headers, "headers");
131
+ validateOptionalProviderOptions(record.providerOptions, "providerOptions");
132
+ validateOptionalArray(record.activeTools, "activeTools");
133
+ validateOptionalFunction(record.runtimeToolIdForName, "runtimeToolIdForName");
134
+ }
135
+
136
+ function validateProtocolSelection(value: ProtocolSelection, path: string): void {
137
+ if (typeof value === "string") {
138
+ if (!isProtocolName(value)) {
139
+ invalidConfiguration({
140
+ path,
141
+ rule: "enum",
142
+ message: "protocol must be one of coordinator, sequential, broadcast, or shared.",
143
+ expected: protocolNames.join(" | "),
144
+ actual: value
145
+ });
146
+ }
147
+ return;
148
+ }
149
+
150
+ validateProtocolConfig(value, path);
151
+ }
152
+
153
+ function validateProtocolConfig(value: ProtocolConfig, path: string): void {
154
+ const record = requireRecord(value, path);
155
+ const kind = record.kind;
156
+
157
+ if (!isProtocolName(kind)) {
158
+ invalidConfiguration({
159
+ path: `${path}.kind`,
160
+ rule: "enum",
161
+ message: "protocol config kind must be one of coordinator, sequential, broadcast, or shared.",
162
+ expected: protocolNames.join(" | "),
163
+ actual: kind
164
+ });
165
+ }
166
+
167
+ switch (kind) {
168
+ case "coordinator":
169
+ case "sequential":
170
+ case "shared":
171
+ validateOptionalPositiveInteger(record.maxTurns, `${path}.maxTurns`);
172
+ if (kind === "shared") {
173
+ validateOptionalString(record.organizationalMemory, `${path}.organizationalMemory`);
174
+ }
175
+ return;
176
+ case "broadcast":
177
+ validateOptionalPositiveInteger(record.maxRounds, `${path}.maxRounds`);
178
+ return;
179
+ }
180
+ }
181
+
182
+ function validateBudgetTier(value: BudgetTier, path: string): void {
183
+ if (!isBudgetTier(value)) {
184
+ invalidConfiguration({
185
+ path,
186
+ rule: "enum",
187
+ message: "tier must be one of fast, balanced, or quality.",
188
+ expected: budgetTiers.join(" | "),
189
+ actual: value
190
+ });
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Validate configured model provider definitions at registration boundaries.
196
+ */
197
+ export function validateModelProviderRegistration(value: unknown, path = "model"): asserts value is ConfiguredModelProvider {
198
+ const record = requireRecord(value, path);
199
+ validateNonEmptyString(record.id, `${path}.id`, "model.id is required.");
200
+ validateFunction(record.generate, `${path}.generate`);
201
+ validateOptionalFunction(record.stream, `${path}.stream`);
202
+ }
203
+
204
+ function validateVercelAILanguageModel(value: unknown, path: string): void {
205
+ const record = requireRecord(value, path);
206
+
207
+ if (record.specificationVersion !== "v2" && record.specificationVersion !== "v3") {
208
+ invalidConfiguration({
209
+ path: `${path}.specificationVersion`,
210
+ rule: "model-provider",
211
+ message: "model.specificationVersion must be v2 or v3.",
212
+ expected: "v2 | v3",
213
+ actual: record.specificationVersion
214
+ });
215
+ }
216
+
217
+ validateNonEmptyString(record.provider, `${path}.provider`, "model.provider is required.");
218
+ validateNonEmptyString(record.modelId, `${path}.modelId`, "model.modelId is required.");
219
+ validateFunction(record.doGenerate, `${path}.doGenerate`);
220
+ validateFunction(record.doStream, `${path}.doStream`);
221
+ }
222
+
223
+ function validateOptionalAgents(value: readonly AgentSpec[] | undefined, path: string): void {
224
+ if (value === undefined) {
225
+ return;
226
+ }
227
+ if (!Array.isArray(value)) {
228
+ invalidConfiguration({
229
+ path,
230
+ rule: "array",
231
+ message: "agents must be an array when provided.",
232
+ expected: "readonly AgentSpec[]",
233
+ actual: value
234
+ });
235
+ }
236
+ if (value.length === 0) {
237
+ invalidConfiguration({
238
+ path,
239
+ rule: "array",
240
+ message: "agents must contain at least one participant when provided.",
241
+ expected: "non-empty readonly AgentSpec[]",
242
+ actual: value
243
+ });
244
+ }
245
+
246
+ value.forEach((agent, index) => {
247
+ const agentPath = `${path}[${index}]`;
248
+ const record = requireRecord(agent, agentPath);
249
+ validateNonEmptyString(record.id, `${agentPath}.id`, "agent.id is required.");
250
+ validateNonEmptyString(record.role, `${agentPath}.role`, "agent.role is required.");
251
+ validateOptionalString(record.instructions, `${agentPath}.instructions`);
252
+ });
253
+ }
254
+
255
+ function validateOptionalRuntimeTools(value: readonly RuntimeTool<JsonObject, JsonValue>[] | undefined, path: string): void {
256
+ if (value === undefined) {
257
+ return;
258
+ }
259
+
260
+ validateRuntimeToolRegistrations(value, path);
261
+ }
262
+
263
+ /**
264
+ * Validate runtime tool definitions at registration boundaries.
265
+ */
266
+ export function validateRuntimeToolRegistrations(value: unknown, path = "tools"): void {
267
+ if (!Array.isArray(value)) {
268
+ invalidConfiguration({
269
+ path,
270
+ rule: "array",
271
+ message: "tools must be an array when provided.",
272
+ expected: "readonly RuntimeTool[]",
273
+ actual: value
274
+ });
275
+ }
276
+
277
+ value.forEach((tool, index) => validateRuntimeTool(tool, `${path}[${index}]`));
278
+ }
279
+
280
+ function validateRuntimeTool(value: RuntimeTool<JsonObject, JsonValue>, path: string): void {
281
+ const record = requireRecord(value, path);
282
+ const identity = requireRecord(record.identity, `${path}.identity`);
283
+ validateNonEmptyString(identity.id, `${path}.identity.id`, "tool identity id is required.");
284
+ validateNonEmptyString(identity.name, `${path}.identity.name`, "tool identity name is required.");
285
+ validateOptionalString(identity.namespace, `${path}.identity.namespace`);
286
+ validateOptionalString(identity.version, `${path}.identity.version`);
287
+ validateOptionalString(identity.description, `${path}.identity.description`);
288
+
289
+ const inputSchema = requireRecord(record.inputSchema, `${path}.inputSchema`);
290
+ if (inputSchema.kind !== "json-schema") {
291
+ invalidConfiguration({
292
+ path: `${path}.inputSchema.kind`,
293
+ rule: "runtime-tool",
294
+ message: "tool inputSchema.kind must be json-schema.",
295
+ expected: "json-schema",
296
+ actual: inputSchema.kind
297
+ });
298
+ }
299
+ validateJsonObject(inputSchema.schema, `${path}.inputSchema.schema`);
300
+ validateOptionalString(inputSchema.description, `${path}.inputSchema.description`);
301
+ validateOptionalArray(record.permissions, `${path}.permissions`);
302
+ validateOptionalFunction(record.validateInput, `${path}.validateInput`);
303
+ validateFunction(record.execute, `${path}.execute`);
304
+ }
305
+
306
+ function validateOptionalBudgetCaps(value: BudgetCaps | undefined, path: string): void {
307
+ if (value === undefined) {
308
+ return;
309
+ }
310
+
311
+ validateBudgetCaps(value, path);
312
+ }
313
+
314
+ function validateBudgetCaps(value: BudgetCaps, path: string): void {
315
+ const record = requireRecord(value, path);
316
+ validateOptionalNonNegativeNumber(record.maxUsd, `${path}.maxUsd`);
317
+ validateOptionalNonNegativeInteger(record.maxTokens, `${path}.maxTokens`);
318
+ validateOptionalNonNegativeInteger(record.maxIterations, `${path}.maxIterations`);
319
+ validateOptionalNonNegativeInteger(record.timeoutMs, `${path}.timeoutMs`);
320
+ validateOptionalNumberInRange(record.qualityWeight, `${path}.qualityWeight`, 0, 1);
321
+ }
322
+
323
+ function validateOptionalTerminationCondition(value: TerminationCondition | undefined, path: string): void {
324
+ if (value === undefined) {
325
+ return;
326
+ }
327
+
328
+ validateTerminationCondition(value, path, new Set<object>());
329
+ }
330
+
331
+ function validateTerminationCondition(value: TerminationCondition, path: string, stack: Set<object>): void {
332
+ const record = requireRecord(value, path);
333
+ if (stack.has(record)) {
334
+ invalidConfiguration({
335
+ path,
336
+ rule: "termination-condition",
337
+ message: "termination conditions must not contain cycles.",
338
+ expected: "acyclic termination condition",
339
+ actual: value
340
+ });
341
+ }
342
+
343
+ stack.add(record);
344
+ try {
345
+ switch (record.kind) {
346
+ case "budget":
347
+ validateBudgetCaps(record, path);
348
+ return;
349
+ case "convergence":
350
+ validatePositiveInteger(record.stableTurns, `${path}.stableTurns`);
351
+ validateNumberInRange(record.minSimilarity, `${path}.minSimilarity`, 0, 1);
352
+ return;
353
+ case "judge":
354
+ validateJudgeRubric(record.rubric, `${path}.rubric`);
355
+ validateOptionalNumberInRange(record.minScore, `${path}.minScore`, 0, 1);
356
+ return;
357
+ case "firstOf":
358
+ validateFirstOfConditions(record.conditions, `${path}.conditions`, stack);
359
+ return;
360
+ default:
361
+ invalidConfiguration({
362
+ path: `${path}.kind`,
363
+ rule: "termination-condition",
364
+ message: "termination condition kind must be budget, convergence, judge, or firstOf.",
365
+ expected: "budget | convergence | judge | firstOf",
366
+ actual: record.kind
367
+ });
368
+ }
369
+ } finally {
370
+ stack.delete(record);
371
+ }
372
+ }
373
+
374
+ function validateFirstOfConditions(value: unknown, path: string, stack: Set<object>): void {
375
+ if (!Array.isArray(value)) {
376
+ invalidConfiguration({
377
+ path,
378
+ rule: "array",
379
+ message: "firstOf conditions must be a non-empty array.",
380
+ expected: "non-empty termination condition array",
381
+ actual: value
382
+ });
383
+ }
384
+ if (value.length === 0) {
385
+ invalidConfiguration({
386
+ path,
387
+ rule: "array",
388
+ message: "firstOf conditions must contain at least one condition.",
389
+ expected: "non-empty termination condition array",
390
+ actual: value
391
+ });
392
+ }
393
+
394
+ value.forEach((condition, index) => {
395
+ validateTerminationCondition(condition as TerminationCondition, `${path}[${index}]`, stack);
396
+ });
397
+ }
398
+
399
+ function validateJudgeRubric(value: unknown, path: string): void {
400
+ if (typeof value === "string") {
401
+ validateNonEmptyString(value, path, "judge rubric must not be empty.");
402
+ return;
403
+ }
404
+
405
+ validateJsonObject(value, path);
406
+ }
407
+
408
+ function validateOptionalTemperature(value: number | undefined, path: string): void {
409
+ validateOptionalNumberInRange(value, path, 0, 2);
410
+ }
411
+
412
+ function validateOptionalSeed(value: string | number | undefined, path: string): void {
413
+ if (value === undefined) {
414
+ return;
415
+ }
416
+ if (typeof value === "string") {
417
+ return;
418
+ }
419
+ if (typeof value === "number" && Number.isFinite(value)) {
420
+ return;
421
+ }
422
+
423
+ invalidConfiguration({
424
+ path,
425
+ rule: "finite-number",
426
+ message: "seed must be a string or finite number when provided.",
427
+ expected: "string or finite number",
428
+ actual: value
429
+ });
430
+ }
431
+
432
+ function validateOptionalHeaders(value: unknown, path: string): void {
433
+ if (value === undefined) {
434
+ return;
435
+ }
436
+
437
+ const record = requireRecord(value, path);
438
+ for (const [key, headerValue] of Object.entries(record)) {
439
+ if (headerValue !== undefined && typeof headerValue !== "string") {
440
+ invalidConfiguration({
441
+ path: `${path}.${key}`,
442
+ rule: "non-empty-string",
443
+ message: "headers values must be strings or undefined.",
444
+ expected: "string | undefined",
445
+ actual: headerValue
446
+ });
447
+ }
448
+ }
449
+ }
450
+
451
+ function validateOptionalProviderOptions(value: unknown, path: string): void {
452
+ if (value === undefined) {
453
+ return;
454
+ }
455
+
456
+ const record = requireRecord(value, path);
457
+ for (const [key, providerOptions] of Object.entries(record)) {
458
+ validateJsonObject(providerOptions, `${path}.${key}`);
459
+ }
460
+ }
461
+
462
+ function validateJsonObject(value: unknown, path: string): void {
463
+ if (!isJsonValue(value, new Set<object>()) || !isRecord(value)) {
464
+ invalidConfiguration({
465
+ path,
466
+ rule: "json-object",
467
+ message: "value must be a JSON-compatible object.",
468
+ expected: "JSON-compatible object",
469
+ actual: value
470
+ });
471
+ }
472
+ }
473
+
474
+ function isJsonValue(value: unknown, stack: Set<object>): value is JsonValue {
475
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
476
+ return true;
477
+ }
478
+ if (typeof value === "number") {
479
+ return Number.isFinite(value);
480
+ }
481
+ if (Array.isArray(value)) {
482
+ if (stack.has(value)) {
483
+ return false;
484
+ }
485
+ stack.add(value);
486
+ const valid = value.every((child) => isJsonValue(child, stack));
487
+ stack.delete(value);
488
+ return valid;
489
+ }
490
+ if (isRecord(value)) {
491
+ if (stack.has(value)) {
492
+ return false;
493
+ }
494
+ stack.add(value);
495
+ const valid = Object.values(value).every((child) => isJsonValue(child, stack));
496
+ stack.delete(value);
497
+ return valid;
498
+ }
499
+
500
+ return false;
501
+ }
502
+
503
+ function validateOptionalString(value: unknown, path: string): void {
504
+ if (value === undefined) {
505
+ return;
506
+ }
507
+ if (typeof value !== "string") {
508
+ invalidConfiguration({
509
+ path,
510
+ rule: "non-empty-string",
511
+ message: "value must be a string when provided.",
512
+ expected: "string",
513
+ actual: value
514
+ });
515
+ }
516
+ }
517
+
518
+ function validateOptionalNonEmptyString(value: unknown, path: string): void {
519
+ if (value === undefined) {
520
+ return;
521
+ }
522
+ validateNonEmptyString(value, path, `${path} must not be empty.`);
523
+ }
524
+
525
+ function validateNonEmptyString(value: unknown, path: string, message: string): void {
526
+ if (typeof value !== "string" || value.trim().length === 0) {
527
+ invalidConfiguration({
528
+ path,
529
+ rule: "non-empty-string",
530
+ message,
531
+ expected: "non-empty string",
532
+ actual: value
533
+ });
534
+ }
535
+ }
536
+
537
+ function validateOptionalBoolean(value: unknown, path: string): void {
538
+ if (value === undefined) {
539
+ return;
540
+ }
541
+ if (typeof value !== "boolean") {
542
+ invalidConfiguration({
543
+ path,
544
+ rule: "boolean",
545
+ message: "value must be a boolean when provided.",
546
+ expected: "boolean",
547
+ actual: value
548
+ });
549
+ }
550
+ }
551
+
552
+ function validateOptionalFunction(value: unknown, path: string): void {
553
+ if (value === undefined) {
554
+ return;
555
+ }
556
+ validateFunction(value, path);
557
+ }
558
+
559
+ function validateFunction(value: unknown, path: string): void {
560
+ if (typeof value !== "function") {
561
+ invalidConfiguration({
562
+ path,
563
+ rule: "function",
564
+ message: "value must be a function.",
565
+ expected: "function",
566
+ actual: value
567
+ });
568
+ }
569
+ }
570
+
571
+ function validateOptionalArray(value: unknown, path: string): void {
572
+ if (value === undefined) {
573
+ return;
574
+ }
575
+ if (!Array.isArray(value)) {
576
+ invalidConfiguration({
577
+ path,
578
+ rule: "array",
579
+ message: "value must be an array when provided.",
580
+ expected: "array",
581
+ actual: value
582
+ });
583
+ }
584
+ }
585
+
586
+ function validateOptionalStringArray(value: unknown, path: string): void {
587
+ if (value === undefined) {
588
+ return;
589
+ }
590
+ if (!Array.isArray(value)) {
591
+ invalidConfiguration({
592
+ path,
593
+ rule: "array",
594
+ message: "value must be an array of strings when provided.",
595
+ expected: "string[]",
596
+ actual: value
597
+ });
598
+ }
599
+ value.forEach((item, index) => validateNonEmptyString(item, `${path}[${index}]`, "array item must be a string."));
600
+ }
601
+
602
+ function validateOptionalAbortSignal(value: unknown, path: string): void {
603
+ if (value === undefined) {
604
+ return;
605
+ }
606
+ if (!isAbortSignalLike(value)) {
607
+ invalidConfiguration({
608
+ path,
609
+ rule: "abort-signal",
610
+ message: "value must be an AbortSignal when provided.",
611
+ expected: "AbortSignal",
612
+ actual: value
613
+ });
614
+ }
615
+ }
616
+
617
+ function isAbortSignalLike(value: unknown): value is AbortSignal {
618
+ if (!isRecord(value)) {
619
+ return false;
620
+ }
621
+
622
+ return (
623
+ typeof value.aborted === "boolean" &&
624
+ typeof value.addEventListener === "function" &&
625
+ typeof value.removeEventListener === "function"
626
+ );
627
+ }
628
+
629
+ function validateOptionalInteger(value: unknown, path: string): void {
630
+ if (value === undefined) {
631
+ return;
632
+ }
633
+ validateInteger(value, path);
634
+ }
635
+
636
+ function validateInteger(value: unknown, path: string): void {
637
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
638
+ invalidConfiguration({
639
+ path,
640
+ rule: "finite-number",
641
+ message: "value must be a finite integer.",
642
+ expected: "finite integer",
643
+ actual: value
644
+ });
645
+ }
646
+ }
647
+
648
+ function validateOptionalPositiveInteger(value: unknown, path: string): void {
649
+ if (value === undefined) {
650
+ return;
651
+ }
652
+ validatePositiveInteger(value, path);
653
+ }
654
+
655
+ function validatePositiveInteger(value: unknown, path: string): void {
656
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
657
+ invalidConfiguration({
658
+ path,
659
+ rule: "positive-integer",
660
+ message: "value must be a positive integer.",
661
+ expected: "integer >= 1",
662
+ actual: value
663
+ });
664
+ }
665
+ }
666
+
667
+ function validateOptionalNonNegativeInteger(value: unknown, path: string): void {
668
+ if (value === undefined) {
669
+ return;
670
+ }
671
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
672
+ invalidConfiguration({
673
+ path,
674
+ rule: "non-negative-integer",
675
+ message: "value must be a non-negative integer.",
676
+ expected: "integer >= 0",
677
+ actual: value
678
+ });
679
+ }
680
+ }
681
+
682
+ function validateOptionalNonNegativeNumber(value: unknown, path: string): void {
683
+ if (value === undefined) {
684
+ return;
685
+ }
686
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
687
+ invalidConfiguration({
688
+ path,
689
+ rule: "non-negative-number",
690
+ message: "value must be a non-negative finite number.",
691
+ expected: "finite number >= 0",
692
+ actual: value
693
+ });
694
+ }
695
+ }
696
+
697
+ function validateOptionalNumberInRange(value: unknown, path: string, min: number, max: number): void {
698
+ if (value === undefined) {
699
+ return;
700
+ }
701
+ validateNumberInRange(value, path, min, max);
702
+ }
703
+
704
+ function validateNumberInRange(value: unknown, path: string, min: number, max: number): void {
705
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
706
+ invalidConfiguration({
707
+ path,
708
+ rule: "range",
709
+ message: `value must be a finite number in the inclusive range ${min}..${max}.`,
710
+ expected: `finite number in ${min}..${max}`,
711
+ actual: value
712
+ });
713
+ }
714
+ }
715
+
716
+ function requireRecord(value: unknown, path: string): Record<string, unknown> {
717
+ if (!isRecord(value)) {
718
+ invalidConfiguration({
719
+ path,
720
+ rule: "object",
721
+ message: "value must be an object.",
722
+ expected: "object",
723
+ actual: value
724
+ });
725
+ }
726
+
727
+ return value;
728
+ }
729
+
730
+ function invalidConfiguration(options: ValidationFailureOptions): never {
731
+ throw new DogpileError({
732
+ code: "invalid-configuration",
733
+ message: `Invalid Dogpile configuration at ${options.path}: ${options.message}`,
734
+ retryable: false,
735
+ detail: {
736
+ kind: "configuration-validation",
737
+ path: options.path,
738
+ rule: options.rule,
739
+ expected: options.expected,
740
+ received: describeValue(options.actual)
741
+ }
742
+ });
743
+ }
744
+
745
+ function isProtocolName(value: unknown): value is ProtocolName {
746
+ return typeof value === "string" && protocolNames.includes(value as ProtocolName);
747
+ }
748
+
749
+ function isBudgetTier(value: unknown): value is BudgetTier {
750
+ return typeof value === "string" && budgetTiers.includes(value as BudgetTier);
751
+ }
752
+
753
+ function isRecord(value: unknown): value is Record<string, unknown> {
754
+ return typeof value === "object" && value !== null && !Array.isArray(value);
755
+ }
756
+
757
+ function describeValue(value: unknown): string {
758
+ if (value === null) {
759
+ return "null";
760
+ }
761
+ if (value === undefined) {
762
+ return "undefined";
763
+ }
764
+ if (Array.isArray(value)) {
765
+ return "array";
766
+ }
767
+ if (typeof value === "number" && !Number.isFinite(value)) {
768
+ return String(value);
769
+ }
770
+ return typeof value;
771
+ }