@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.
- package/CHANGELOG.md +136 -0
- package/README.md +1 -0
- package/dist/browser/index.js +1595 -54
- package/dist/browser/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/providers/openai-compatible.d.ts +11 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +87 -2
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/runtime/cancellation.d.ts +26 -0
- package/dist/runtime/cancellation.d.ts.map +1 -1
- package/dist/runtime/cancellation.js +38 -1
- package/dist/runtime/cancellation.js.map +1 -1
- package/dist/runtime/coordinator.d.ts +74 -1
- package/dist/runtime/coordinator.d.ts.map +1 -1
- package/dist/runtime/coordinator.js +932 -25
- package/dist/runtime/coordinator.js.map +1 -1
- package/dist/runtime/decisions.d.ts +25 -3
- package/dist/runtime/decisions.d.ts.map +1 -1
- package/dist/runtime/decisions.js +241 -3
- package/dist/runtime/decisions.js.map +1 -1
- package/dist/runtime/defaults.d.ts +37 -1
- package/dist/runtime/defaults.d.ts.map +1 -1
- package/dist/runtime/defaults.js +347 -0
- package/dist/runtime/defaults.js.map +1 -1
- package/dist/runtime/engine.d.ts.map +1 -1
- package/dist/runtime/engine.js +254 -24
- package/dist/runtime/engine.js.map +1 -1
- package/dist/runtime/sequential.d.ts.map +1 -1
- package/dist/runtime/sequential.js +8 -1
- package/dist/runtime/sequential.js.map +1 -1
- package/dist/runtime/validation.d.ts +10 -0
- package/dist/runtime/validation.d.ts.map +1 -1
- package/dist/runtime/validation.js +73 -0
- package/dist/runtime/validation.js.map +1 -1
- package/dist/types/events.d.ts +329 -8
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/replay.d.ts +5 -1
- package/dist/types/replay.d.ts.map +1 -1
- package/dist/types.d.ts +131 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +10 -0
- package/src/providers/openai-compatible.ts +82 -3
- package/src/runtime/cancellation.ts +59 -1
- package/src/runtime/coordinator.ts +1170 -25
- package/src/runtime/decisions.ts +307 -4
- package/src/runtime/defaults.ts +376 -0
- package/src/runtime/engine.ts +363 -24
- package/src/runtime/sequential.ts +9 -1
- package/src/runtime/validation.ts +81 -0
- package/src/types/events.ts +359 -8
- package/src/types/replay.ts +12 -1
- package/src/types.ts +147 -3
package/src/runtime/decisions.ts
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
+
}
|