@dogpile/sdk 0.3.1 → 0.4.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 (56) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +1 -0
  3. package/dist/browser/index.js +1595 -54
  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/providers/openai-compatible.d.ts +11 -0
  8. package/dist/providers/openai-compatible.d.ts.map +1 -1
  9. package/dist/providers/openai-compatible.js +87 -2
  10. package/dist/providers/openai-compatible.js.map +1 -1
  11. package/dist/runtime/cancellation.d.ts +26 -0
  12. package/dist/runtime/cancellation.d.ts.map +1 -1
  13. package/dist/runtime/cancellation.js +38 -1
  14. package/dist/runtime/cancellation.js.map +1 -1
  15. package/dist/runtime/coordinator.d.ts +74 -1
  16. package/dist/runtime/coordinator.d.ts.map +1 -1
  17. package/dist/runtime/coordinator.js +932 -25
  18. package/dist/runtime/coordinator.js.map +1 -1
  19. package/dist/runtime/decisions.d.ts +25 -3
  20. package/dist/runtime/decisions.d.ts.map +1 -1
  21. package/dist/runtime/decisions.js +241 -3
  22. package/dist/runtime/decisions.js.map +1 -1
  23. package/dist/runtime/defaults.d.ts +37 -1
  24. package/dist/runtime/defaults.d.ts.map +1 -1
  25. package/dist/runtime/defaults.js +347 -0
  26. package/dist/runtime/defaults.js.map +1 -1
  27. package/dist/runtime/engine.d.ts.map +1 -1
  28. package/dist/runtime/engine.js +254 -24
  29. package/dist/runtime/engine.js.map +1 -1
  30. package/dist/runtime/sequential.d.ts.map +1 -1
  31. package/dist/runtime/sequential.js +8 -1
  32. package/dist/runtime/sequential.js.map +1 -1
  33. package/dist/runtime/validation.d.ts +10 -0
  34. package/dist/runtime/validation.d.ts.map +1 -1
  35. package/dist/runtime/validation.js +73 -0
  36. package/dist/runtime/validation.js.map +1 -1
  37. package/dist/types/events.d.ts +329 -8
  38. package/dist/types/events.d.ts.map +1 -1
  39. package/dist/types/replay.d.ts +5 -1
  40. package/dist/types/replay.d.ts.map +1 -1
  41. package/dist/types.d.ts +131 -5
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/index.ts +10 -0
  46. package/src/providers/openai-compatible.ts +82 -3
  47. package/src/runtime/cancellation.ts +59 -1
  48. package/src/runtime/coordinator.ts +1170 -25
  49. package/src/runtime/decisions.ts +307 -4
  50. package/src/runtime/defaults.ts +376 -0
  51. package/src/runtime/engine.ts +363 -24
  52. package/src/runtime/sequential.ts +9 -1
  53. package/src/runtime/validation.ts +81 -0
  54. package/src/types/events.ts +359 -8
  55. package/src/types/replay.ts +12 -1
  56. package/src/types.ts +147 -3
@@ -1,6 +1,54 @@
1
- import type { AgentDecision, AgentParticipation } from "../types.js";
1
+ import {
2
+ DogpileError,
3
+ type AgentDecision,
4
+ type AgentParticipation,
5
+ type BudgetCaps,
6
+ type DelegateAgentDecision,
7
+ type ParticipateAgentDecision,
8
+ type ProtocolName
9
+ } from "../types.js";
2
10
 
3
- export function parseAgentDecision(output: string): AgentDecision | undefined {
11
+ const PROTOCOL_NAMES: readonly ProtocolName[] = ["coordinator", "sequential", "broadcast", "shared"];
12
+
13
+ /**
14
+ * Optional context for {@link parseAgentDecision}. Phase 1 uses this to enforce
15
+ * D-11 (delegate `model` must match the parent provider). Future phases will
16
+ * extend this with depth/maxDepth fields.
17
+ */
18
+ export interface ParseAgentDecisionContext {
19
+ readonly currentDepth?: number;
20
+ readonly maxDepth?: number;
21
+ readonly parentProviderId?: string;
22
+ }
23
+
24
+ export function parseAgentDecision(
25
+ output: string,
26
+ context: ParseAgentDecisionContext = {}
27
+ ): ParticipateAgentDecision | DelegateAgentDecision | DelegateAgentDecision[] | undefined {
28
+ const delegateBlock = matchDelegateBlock(output);
29
+ if (delegateBlock !== undefined) {
30
+ return parseDelegateDecision(delegateBlock, context);
31
+ }
32
+
33
+ return parseParticipateDecision(output);
34
+ }
35
+
36
+ export function isParticipatingDecision(
37
+ decision: AgentDecision | readonly DelegateAgentDecision[] | undefined
38
+ ): boolean {
39
+ if (decision === undefined || isDelegateDecisionArray(decision) || decision.type !== "participate") {
40
+ return false;
41
+ }
42
+ return decision.participation !== "abstain";
43
+ }
44
+
45
+ function isDelegateDecisionArray(
46
+ decision: AgentDecision | readonly DelegateAgentDecision[]
47
+ ): decision is readonly DelegateAgentDecision[] {
48
+ return Array.isArray(decision);
49
+ }
50
+
51
+ function parseParticipateDecision(output: string): ParticipateAgentDecision | undefined {
4
52
  const selectedRole = matchLine(output, /^role_selected:\s*(.+)$/imu);
5
53
  const participation = matchLine(output, /^participation:\s*(contribute|abstain)$/imu);
6
54
  const rationale = matchLine(output, /^rationale:\s*(.+)$/imu);
@@ -11,6 +59,7 @@ export function parseAgentDecision(output: string): AgentDecision | undefined {
11
59
  }
12
60
 
13
61
  return {
62
+ type: "participate",
14
63
  selectedRole,
15
64
  participation,
16
65
  rationale,
@@ -18,8 +67,251 @@ export function parseAgentDecision(output: string): AgentDecision | undefined {
18
67
  };
19
68
  }
20
69
 
21
- export function isParticipatingDecision(decision: AgentDecision | undefined): boolean {
22
- return decision?.participation !== "abstain";
70
+ /**
71
+ * Locate a `delegate:` line followed by a fenced JSON block in the agent's
72
+ * output. Returns the raw JSON text inside the fence, or `undefined` when no
73
+ * delegate block is present. Tolerates ```` ```json ```` and bare ```` ``` ````.
74
+ */
75
+ function matchDelegateBlock(output: string): string | undefined {
76
+ // Match `delegate:` on its own line, optional whitespace, then a fenced block.
77
+ // Use [\s\S] to match across newlines and a non-greedy capture so we stop at
78
+ // the first closing fence. The `m` flag scopes ^/$ per-line; `i` allows
79
+ // `Delegate:` casing.
80
+ const pattern = /^delegate:\s*\r?\n\s*```(?:json)?\s*\r?\n([\s\S]*?)\r?\n\s*```/imu;
81
+ const match = output.match(pattern);
82
+ return match?.[1];
83
+ }
84
+
85
+ function parseDelegateDecision(
86
+ jsonText: string,
87
+ context: ParseAgentDecisionContext
88
+ ): DelegateAgentDecision | DelegateAgentDecision[] {
89
+ let parsed: unknown;
90
+ try {
91
+ parsed = JSON.parse(jsonText);
92
+ } catch (error) {
93
+ const reason = error instanceof Error ? error.message : String(error);
94
+ throwInvalidDelegate({
95
+ path: "decision",
96
+ message: `delegate JSON did not parse: ${reason}`,
97
+ expected: "valid JSON object",
98
+ received: truncate(jsonText)
99
+ });
100
+ }
101
+
102
+ if (Array.isArray(parsed)) {
103
+ if (parsed.length === 0) {
104
+ throwInvalidDelegate({
105
+ path: "decision",
106
+ message: "delegate array must not be empty.",
107
+ expected: "array with 1..8 delegate objects",
108
+ received: "empty array"
109
+ });
110
+ }
111
+ return parsed.map((item) => parseSingleDelegateObject(item, context));
112
+ }
113
+
114
+ return parseSingleDelegateObject(parsed, context);
115
+ }
116
+
117
+ function parseSingleDelegateObject(
118
+ parsed: unknown,
119
+ context: ParseAgentDecisionContext
120
+ ): DelegateAgentDecision {
121
+ if (parsed === null || typeof parsed !== "object") {
122
+ throwInvalidDelegate({
123
+ path: "decision",
124
+ message: "delegate decision must be a JSON object.",
125
+ expected: "object",
126
+ received: describe(parsed)
127
+ });
128
+ }
129
+
130
+ const record = parsed as Record<string, unknown>;
131
+
132
+ const protocol = record["protocol"];
133
+ if (typeof protocol !== "string" || !PROTOCOL_NAMES.includes(protocol as ProtocolName)) {
134
+ throwInvalidDelegate({
135
+ path: "decision.protocol",
136
+ message: `protocol "${describe(protocol)}" is not a known coordination protocol.`,
137
+ expected: PROTOCOL_NAMES.join(" | "),
138
+ received: describe(protocol)
139
+ });
140
+ }
141
+
142
+ const intentRaw = record["intent"];
143
+ const intent = typeof intentRaw === "string" ? intentRaw.trim() : "";
144
+ if (intent.length === 0) {
145
+ throwInvalidDelegate({
146
+ path: "decision.intent",
147
+ message: "delegate decision must include a non-empty intent string.",
148
+ expected: "non-empty string",
149
+ received: describe(intentRaw)
150
+ });
151
+ }
152
+
153
+ const result: {
154
+ type: "delegate";
155
+ protocol: ProtocolName;
156
+ intent: string;
157
+ model?: string;
158
+ budget?: BudgetCaps;
159
+ maxConcurrentChildren?: number;
160
+ } = {
161
+ type: "delegate",
162
+ protocol: protocol as ProtocolName,
163
+ intent
164
+ };
165
+
166
+ if (record["model"] !== undefined) {
167
+ const model = record["model"];
168
+ if (typeof model !== "string" || model.length === 0) {
169
+ throwInvalidDelegate({
170
+ path: "decision.model",
171
+ message: "delegate decision model must be a non-empty string when present.",
172
+ expected: "non-empty string",
173
+ received: describe(model)
174
+ });
175
+ }
176
+ if (context.parentProviderId !== undefined && model !== context.parentProviderId) {
177
+ throwInvalidDelegate({
178
+ path: "decision.model",
179
+ message: `delegate decision model "${model}" does not match parent provider id "${context.parentProviderId}".`,
180
+ expected: context.parentProviderId,
181
+ received: model
182
+ });
183
+ }
184
+ result.model = model;
185
+ }
186
+
187
+ if (record["budget"] !== undefined) {
188
+ result.budget = parseDelegateBudget(record["budget"]);
189
+ }
190
+
191
+ if (record["maxConcurrentChildren"] !== undefined) {
192
+ const value = record["maxConcurrentChildren"];
193
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
194
+ throwInvalidDelegate({
195
+ path: "decision.maxConcurrentChildren",
196
+ message: "delegate decision maxConcurrentChildren must be a positive integer when present.",
197
+ expected: "integer >= 1",
198
+ received: describe(value)
199
+ });
200
+ }
201
+ result.maxConcurrentChildren = value;
202
+ }
203
+
204
+ // Parse-time depth-overflow check (D-14). The dispatcher re-checks at
205
+ // dispatch time as a TOCTOU defense — see assertDepthWithinLimit.
206
+ if (context.currentDepth !== undefined && context.maxDepth !== undefined) {
207
+ if (context.currentDepth + 1 > context.maxDepth) {
208
+ throw depthOverflowError(context.currentDepth, context.maxDepth);
209
+ }
210
+ }
211
+
212
+ return result;
213
+ }
214
+
215
+ /**
216
+ * Build the canonical depth-overflow `DogpileError`. Used by the parser (this
217
+ * file) and the coordinator dispatcher; kept here so both call sites produce
218
+ * the exact same error shape (D-14, D-15).
219
+ */
220
+ export function depthOverflowError(currentDepth: number, maxDepth: number): DogpileError {
221
+ return new DogpileError({
222
+ code: "invalid-configuration",
223
+ message: `Depth overflow: cannot dispatch sub-run at depth ${currentDepth + 1} (maxDepth = ${maxDepth}).`,
224
+ retryable: false,
225
+ detail: {
226
+ kind: "delegate-validation",
227
+ path: "decision.protocol",
228
+ reason: "depth-overflow",
229
+ currentDepth,
230
+ maxDepth
231
+ }
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Dispatcher-time depth gate. Throws the same error shape the parser uses; the
237
+ * dual gate (parser + dispatcher) defends against any TOCTOU window between
238
+ * decision parsing and child-run spin-up (D-14).
239
+ */
240
+ export function assertDepthWithinLimit(currentDepth: number, maxDepth: number): void {
241
+ if (currentDepth + 1 > maxDepth) {
242
+ throw depthOverflowError(currentDepth, maxDepth);
243
+ }
244
+ }
245
+
246
+ function parseDelegateBudget(raw: unknown): BudgetCaps {
247
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
248
+ throwInvalidDelegate({
249
+ path: "decision.budget",
250
+ message: "delegate decision budget must be an object.",
251
+ expected: "object",
252
+ received: describe(raw)
253
+ });
254
+ }
255
+ const record = raw as Record<string, unknown>;
256
+ const budget: { -readonly [K in keyof BudgetCaps]: BudgetCaps[K] } = {};
257
+ if (record["timeoutMs"] !== undefined) {
258
+ const value = record["timeoutMs"];
259
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
260
+ throwInvalidDelegate({
261
+ path: "decision.budget.timeoutMs",
262
+ message: "delegate decision budget.timeoutMs must be a non-negative integer.",
263
+ expected: "integer >= 0",
264
+ received: describe(value)
265
+ });
266
+ }
267
+ budget.timeoutMs = value;
268
+ }
269
+ if (record["maxTokens"] !== undefined) {
270
+ const value = record["maxTokens"];
271
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
272
+ throwInvalidDelegate({
273
+ path: "decision.budget.maxTokens",
274
+ message: "delegate decision budget.maxTokens must be a non-negative integer.",
275
+ expected: "integer >= 0",
276
+ received: describe(value)
277
+ });
278
+ }
279
+ budget.maxTokens = value;
280
+ }
281
+ if (record["maxIterations"] !== undefined) {
282
+ const value = record["maxIterations"];
283
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
284
+ throwInvalidDelegate({
285
+ path: "decision.budget.maxIterations",
286
+ message: "delegate decision budget.maxIterations must be a non-negative integer.",
287
+ expected: "integer >= 0",
288
+ received: describe(value)
289
+ });
290
+ }
291
+ budget.maxIterations = value;
292
+ }
293
+ return budget as BudgetCaps;
294
+ }
295
+
296
+ interface DelegateValidationFailure {
297
+ readonly path: string;
298
+ readonly message: string;
299
+ readonly expected: string;
300
+ readonly received: string;
301
+ }
302
+
303
+ function throwInvalidDelegate(failure: DelegateValidationFailure): never {
304
+ throw new DogpileError({
305
+ code: "invalid-configuration",
306
+ message: `Invalid Dogpile configuration at ${failure.path}: ${failure.message}`,
307
+ retryable: false,
308
+ detail: {
309
+ kind: "delegate-validation",
310
+ path: failure.path,
311
+ expected: failure.expected,
312
+ received: failure.received
313
+ }
314
+ });
23
315
  }
24
316
 
25
317
  function matchLine(output: string, pattern: RegExp): string | undefined {
@@ -36,3 +328,14 @@ function matchContribution(output: string): string | undefined {
36
328
  export function isAgentParticipation(value: string): value is AgentParticipation {
37
329
  return value === "contribute" || value === "abstain";
38
330
  }
331
+
332
+ function describe(value: unknown): string {
333
+ if (value === null) return "null";
334
+ if (Array.isArray(value)) return "array";
335
+ if (typeof value === "string") return JSON.stringify(value).slice(0, 200);
336
+ return typeof value;
337
+ }
338
+
339
+ function truncate(value: string): string {
340
+ return value.length > 200 ? `${value.slice(0, 200)}…` : value;
341
+ }