@dogpile/sdk 0.3.1 → 0.5.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 +201 -0
- package/README.md +1 -0
- package/dist/browser/index.js +2328 -237
- package/dist/browser/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.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 +88 -2
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/runtime/audit.d.ts +42 -0
- package/dist/runtime/audit.d.ts.map +1 -0
- package/dist/runtime/audit.js +73 -0
- package/dist/runtime/audit.js.map +1 -0
- package/dist/runtime/broadcast.d.ts.map +1 -1
- package/dist/runtime/broadcast.js +39 -36
- package/dist/runtime/broadcast.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 +79 -1
- package/dist/runtime/coordinator.d.ts.map +1 -1
- package/dist/runtime/coordinator.js +979 -61
- 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 +359 -4
- package/dist/runtime/defaults.js.map +1 -1
- package/dist/runtime/engine.d.ts +17 -4
- package/dist/runtime/engine.d.ts.map +1 -1
- package/dist/runtime/engine.js +770 -35
- package/dist/runtime/engine.js.map +1 -1
- package/dist/runtime/health.d.ts +51 -0
- package/dist/runtime/health.d.ts.map +1 -0
- package/dist/runtime/health.js +85 -0
- package/dist/runtime/health.js.map +1 -0
- package/dist/runtime/introspection.d.ts +96 -0
- package/dist/runtime/introspection.d.ts.map +1 -0
- package/dist/runtime/introspection.js +31 -0
- package/dist/runtime/introspection.js.map +1 -0
- package/dist/runtime/metrics.d.ts +44 -0
- package/dist/runtime/metrics.d.ts.map +1 -0
- package/dist/runtime/metrics.js +12 -0
- package/dist/runtime/metrics.js.map +1 -0
- package/dist/runtime/model.d.ts.map +1 -1
- package/dist/runtime/model.js +34 -7
- package/dist/runtime/model.js.map +1 -1
- package/dist/runtime/provenance.d.ts +25 -0
- package/dist/runtime/provenance.d.ts.map +1 -0
- package/dist/runtime/provenance.js +13 -0
- package/dist/runtime/provenance.js.map +1 -0
- package/dist/runtime/sequential.d.ts.map +1 -1
- package/dist/runtime/sequential.js +47 -37
- package/dist/runtime/sequential.js.map +1 -1
- package/dist/runtime/shared.d.ts.map +1 -1
- package/dist/runtime/shared.js +39 -36
- package/dist/runtime/shared.js.map +1 -1
- package/dist/runtime/tracing.d.ts +31 -0
- package/dist/runtime/tracing.d.ts.map +1 -0
- package/dist/runtime/tracing.js +18 -0
- package/dist/runtime/tracing.js.map +1 -0
- 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 +339 -12
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/replay.d.ts +7 -1
- package/dist/types/replay.d.ts.map +1 -1
- package/dist/types.d.ts +255 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +39 -1
- package/src/index.ts +15 -0
- package/src/providers/openai-compatible.ts +83 -3
- package/src/runtime/audit.ts +121 -0
- package/src/runtime/broadcast.ts +40 -37
- package/src/runtime/cancellation.ts +59 -1
- package/src/runtime/coordinator.ts +1221 -61
- package/src/runtime/decisions.ts +307 -4
- package/src/runtime/defaults.ts +389 -4
- package/src/runtime/engine.ts +1004 -35
- package/src/runtime/health.ts +136 -0
- package/src/runtime/introspection.ts +122 -0
- package/src/runtime/metrics.ts +45 -0
- package/src/runtime/model.ts +38 -6
- package/src/runtime/provenance.ts +43 -0
- package/src/runtime/sequential.ts +49 -38
- package/src/runtime/shared.ts +40 -37
- package/src/runtime/tracing.ts +35 -0
- package/src/runtime/validation.ts +81 -0
- package/src/types/events.ts +369 -12
- package/src/types/replay.ts +14 -1
- package/src/types.ts +279 -4
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { consoleLogger, loggerFromEvents, noopLogger } from "./runtime/logger.js
|
|
|
12
12
|
export type { Logger, LoggerFromEventsOptions, LogLevel } from "./runtime/logger.js";
|
|
13
13
|
export { DEFAULT_RETRYABLE_DOGPILE_CODES, withRetry } from "./runtime/retry.js";
|
|
14
14
|
export type { RetryAttemptInfo, RetryJitterMode, RetryPolicy } from "./runtime/retry.js";
|
|
15
|
+
export { DOGPILE_SPAN_NAMES } from "./runtime/tracing.js";
|
|
16
|
+
export type { DogpileSpan, DogpileSpanOptions, DogpileTracer } from "./runtime/tracing.js";
|
|
15
17
|
export { DogpileError } from "./types.js";
|
|
16
18
|
export type {
|
|
17
19
|
OpenAICompatibleChatCompletionChoice,
|
|
@@ -75,9 +77,11 @@ export {
|
|
|
75
77
|
judge
|
|
76
78
|
} from "./runtime/termination.js";
|
|
77
79
|
export type {
|
|
80
|
+
AbortedEvent,
|
|
78
81
|
AgentSpec,
|
|
79
82
|
AgentDecision,
|
|
80
83
|
AgentParticipation,
|
|
84
|
+
AnomalyCode,
|
|
81
85
|
BroadcastContribution,
|
|
82
86
|
BroadcastEvent,
|
|
83
87
|
BroadcastProtocolConfig,
|
|
@@ -93,12 +97,15 @@ export type {
|
|
|
93
97
|
CoordinationProtocolSelection,
|
|
94
98
|
CostSummary,
|
|
95
99
|
CoordinatorProtocolConfig,
|
|
100
|
+
DelegateAgentDecision,
|
|
96
101
|
DogpileErrorCode,
|
|
97
102
|
DogpileErrorOptions,
|
|
98
103
|
DogpileOptions,
|
|
99
104
|
Engine,
|
|
100
105
|
EngineOptions,
|
|
106
|
+
RunCallOptions,
|
|
101
107
|
FinalEvent,
|
|
108
|
+
HealthAnomaly,
|
|
102
109
|
FirstOfTerminationCondition,
|
|
103
110
|
FirstOfTerminationConditions,
|
|
104
111
|
FirstOfTerminationInput,
|
|
@@ -147,6 +154,7 @@ export type {
|
|
|
147
154
|
RunEvaluator,
|
|
148
155
|
RunEventLog,
|
|
149
156
|
RunEvent,
|
|
157
|
+
RunHealthSummary,
|
|
150
158
|
RunMetadata,
|
|
151
159
|
RunResult,
|
|
152
160
|
RunUsage,
|
|
@@ -184,6 +192,13 @@ export type {
|
|
|
184
192
|
StreamOutputEvent,
|
|
185
193
|
StreamSubscription,
|
|
186
194
|
StopTerminationDecision,
|
|
195
|
+
SubRunBudgetClampedEvent,
|
|
196
|
+
SubRunCompletedEvent,
|
|
197
|
+
SubRunConcurrencyClampedEvent,
|
|
198
|
+
SubRunFailedEvent,
|
|
199
|
+
SubRunParentAbortedEvent,
|
|
200
|
+
SubRunQueuedEvent,
|
|
201
|
+
SubRunStartedEvent,
|
|
187
202
|
TerminationCondition,
|
|
188
203
|
TerminationDecision,
|
|
189
204
|
TerminationEvaluationContext,
|
|
@@ -37,6 +37,8 @@ export interface OpenAICompatibleProviderOptions {
|
|
|
37
37
|
readonly maxOutputTokens?: number;
|
|
38
38
|
readonly extraBody?: JsonObject;
|
|
39
39
|
readonly costEstimator?: OpenAICompatibleProviderCostEstimator;
|
|
40
|
+
/** Locality hint override; if omitted, auto-detected from baseURL. Explicit "local" always wins; explicit "remote" on a detected-local host throws. */
|
|
41
|
+
readonly locality?: "local" | "remote";
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface OpenAICompatibleChatCompletionResponse {
|
|
@@ -68,6 +70,10 @@ export function createOpenAICompatibleProvider(options: OpenAICompatibleProvider
|
|
|
68
70
|
|
|
69
71
|
const providerId = options.id ?? `openai-compatible:${options.model}`;
|
|
70
72
|
const fetchImplementation = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
73
|
+
const baseURLForLocality = new URL(String(options.baseURL ?? defaultBaseURL));
|
|
74
|
+
const detectedLocality = classifyHostLocality(baseURLForLocality.hostname);
|
|
75
|
+
const resolvedLocality: "local" | "remote" =
|
|
76
|
+
options.locality === "local" ? "local" : options.locality === "remote" ? "remote" : detectedLocality;
|
|
71
77
|
|
|
72
78
|
if (!fetchImplementation) {
|
|
73
79
|
throw new DogpileError({
|
|
@@ -85,6 +91,8 @@ export function createOpenAICompatibleProvider(options: OpenAICompatibleProvider
|
|
|
85
91
|
|
|
86
92
|
return {
|
|
87
93
|
id: providerId,
|
|
94
|
+
modelId: options.model,
|
|
95
|
+
metadata: { locality: resolvedLocality },
|
|
88
96
|
async generate(request: ModelRequest): Promise<ModelResponse> {
|
|
89
97
|
let response: Response;
|
|
90
98
|
|
|
@@ -99,12 +107,12 @@ export function createOpenAICompatibleProvider(options: OpenAICompatibleProvider
|
|
|
99
107
|
throw normalizeFetchError(error, providerId);
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
const payload = await readJson(response, providerId);
|
|
103
|
-
|
|
104
110
|
if (!response.ok) {
|
|
111
|
+
const payload = await readJsonLenient(response);
|
|
105
112
|
throw createProviderError(response, payload, providerId);
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
const payload = await readJson(response, providerId);
|
|
108
116
|
const completion = asChatCompletionResponse(payload, providerId);
|
|
109
117
|
const text = readAssistantText(completion, providerId);
|
|
110
118
|
const usage = normalizeUsage(completion.usage);
|
|
@@ -154,6 +162,27 @@ function validateOptions(options: OpenAICompatibleProviderOptions): void {
|
|
|
154
162
|
if (options.costEstimator !== undefined && typeof options.costEstimator !== "function") {
|
|
155
163
|
throwInvalid("costEstimator", "a function when provided");
|
|
156
164
|
}
|
|
165
|
+
if (options.locality !== undefined && options.locality !== "local" && options.locality !== "remote") {
|
|
166
|
+
throwInvalid("locality", "\"local\" | \"remote\" when provided");
|
|
167
|
+
}
|
|
168
|
+
if (options.locality === "remote") {
|
|
169
|
+
const baseURL = new URL(String(options.baseURL ?? defaultBaseURL));
|
|
170
|
+
const detected = classifyHostLocality(baseURL.hostname);
|
|
171
|
+
if (detected === "local") {
|
|
172
|
+
throw new DogpileError({
|
|
173
|
+
code: "invalid-configuration",
|
|
174
|
+
message: `locality "remote" cannot be set when baseURL resolves to a local host (${baseURL.hostname}).`,
|
|
175
|
+
retryable: false,
|
|
176
|
+
detail: {
|
|
177
|
+
kind: "configuration-validation",
|
|
178
|
+
path: "locality",
|
|
179
|
+
expected: "\"local\" (or omit to auto-detect)",
|
|
180
|
+
reason: "remote-override-on-local-host",
|
|
181
|
+
host: baseURL.hostname
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
157
186
|
}
|
|
158
187
|
|
|
159
188
|
function throwInvalid(path: string, expected: string): never {
|
|
@@ -169,6 +198,46 @@ function throwInvalid(path: string, expected: string): never {
|
|
|
169
198
|
});
|
|
170
199
|
}
|
|
171
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Classify a URL hostname as "local" or "remote" per Phase 3 D-02.
|
|
203
|
+
* Local: localhost, *.local mDNS, IPv4 loopback (127.0.0.0/8), RFC1918
|
|
204
|
+
* (10/8, 172.16/12, 192.168/16), link-local (169.254/16), IPv6 loopback (::1),
|
|
205
|
+
* IPv6 ULA (fc00::/7), IPv6 link-local (fe80::/10).
|
|
206
|
+
*
|
|
207
|
+
* Pure function: no I/O, no side effects. Exported for tests and future reuse.
|
|
208
|
+
*/
|
|
209
|
+
export function classifyHostLocality(host: string): "local" | "remote" {
|
|
210
|
+
const lower = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
211
|
+
const mappedIpv4 = ipv4MappedToDottedQuad(lower);
|
|
212
|
+
if (mappedIpv4 !== undefined) {
|
|
213
|
+
return classifyHostLocality(mappedIpv4);
|
|
214
|
+
}
|
|
215
|
+
if (lower === "localhost") return "local";
|
|
216
|
+
if (lower.endsWith(".local")) return "local";
|
|
217
|
+
if (/^127(?:\.\d{1,3}){3}$/.test(lower)) return "local";
|
|
218
|
+
if (/^10(?:\.\d{1,3}){3}$/.test(lower)) return "local";
|
|
219
|
+
if (/^172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}$/.test(lower)) return "local";
|
|
220
|
+
if (/^192\.168(?:\.\d{1,3}){2}$/.test(lower)) return "local";
|
|
221
|
+
if (/^169\.254(?:\.\d{1,3}){2}$/.test(lower)) return "local";
|
|
222
|
+
if (lower === "::1") return "local";
|
|
223
|
+
if (/^f[cd][0-9a-f]{2}:/.test(lower)) return "local";
|
|
224
|
+
if (/^fe[89ab][0-9a-f]?:/.test(lower)) return "local";
|
|
225
|
+
return "remote";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function ipv4MappedToDottedQuad(host: string): string | undefined {
|
|
229
|
+
const match = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
230
|
+
if (match === null) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const high = Number.parseInt(match[1] ?? "", 16);
|
|
234
|
+
const low = Number.parseInt(match[2] ?? "", 16);
|
|
235
|
+
if (!Number.isFinite(high) || !Number.isFinite(low)) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
172
241
|
function createURL(options: OpenAICompatibleProviderOptions): URL {
|
|
173
242
|
const baseURL = new URL(String(options.baseURL ?? defaultBaseURL));
|
|
174
243
|
const path = options.path ?? defaultPath;
|
|
@@ -232,6 +301,14 @@ async function readJson(response: Response, providerId: string): Promise<unknown
|
|
|
232
301
|
}
|
|
233
302
|
}
|
|
234
303
|
|
|
304
|
+
async function readJsonLenient(response: Response): Promise<unknown> {
|
|
305
|
+
try {
|
|
306
|
+
return await response.json();
|
|
307
|
+
} catch {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
235
312
|
function asChatCompletionResponse(payload: unknown, providerId: string): OpenAICompatibleChatCompletionResponse {
|
|
236
313
|
if (!isRecord(payload)) {
|
|
237
314
|
throw new DogpileError({
|
|
@@ -341,14 +418,17 @@ function responseMetadata(response: OpenAICompatibleChatCompletionResponse): Jso
|
|
|
341
418
|
}
|
|
342
419
|
|
|
343
420
|
function createProviderError(response: Response, payload: unknown, providerId: string): DogpileError {
|
|
421
|
+
const code = codeForStatus(response.status);
|
|
422
|
+
const timeoutSource = code === "provider-timeout" ? { source: "provider" as const } : {};
|
|
344
423
|
return new DogpileError({
|
|
345
|
-
code
|
|
424
|
+
code,
|
|
346
425
|
message: providerResponseErrorMessage(response, payload),
|
|
347
426
|
retryable: response.status === 408 || response.status === 429 || response.status >= 500,
|
|
348
427
|
providerId,
|
|
349
428
|
detail: removeUndefined({
|
|
350
429
|
statusCode: response.status,
|
|
351
430
|
statusText: response.statusText,
|
|
431
|
+
...timeoutSource,
|
|
352
432
|
response: isJsonValue(payload) ? payload : undefined
|
|
353
433
|
})
|
|
354
434
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Protocol, Tier, Trace } from "../types.js";
|
|
2
|
+
import type { BudgetStopEvent, FinalEvent, SubRunCompletedEvent, TurnEvent } from "../types/events.js";
|
|
3
|
+
|
|
4
|
+
export type AuditOutcomeStatus = "completed" | "budget-stopped" | "aborted";
|
|
5
|
+
|
|
6
|
+
export interface AuditOutcome {
|
|
7
|
+
readonly status: AuditOutcomeStatus;
|
|
8
|
+
readonly terminationCode?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AuditCost {
|
|
12
|
+
readonly usd: number;
|
|
13
|
+
readonly inputTokens: number;
|
|
14
|
+
readonly outputTokens: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AuditAgentRecord {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly role: string;
|
|
20
|
+
readonly turnCount: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AuditRecord {
|
|
24
|
+
readonly auditSchemaVersion: "1";
|
|
25
|
+
readonly runId: string;
|
|
26
|
+
readonly intent: string;
|
|
27
|
+
readonly startedAt: string;
|
|
28
|
+
readonly completedAt: string;
|
|
29
|
+
readonly protocol: Protocol;
|
|
30
|
+
readonly tier: Tier;
|
|
31
|
+
readonly modelProviderId: string;
|
|
32
|
+
readonly agentCount: number;
|
|
33
|
+
readonly turnCount: number;
|
|
34
|
+
readonly outcome: AuditOutcome;
|
|
35
|
+
readonly cost: AuditCost;
|
|
36
|
+
readonly agents: readonly AuditAgentRecord[];
|
|
37
|
+
readonly childRunIds?: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Derive a versioned, schema-stable audit record from a completed run trace.
|
|
42
|
+
*
|
|
43
|
+
* Pure function - no side effects, no I/O, no storage access. Deterministic:
|
|
44
|
+
* given the same trace, always produces the same AuditRecord.
|
|
45
|
+
*
|
|
46
|
+
* @param trace - Completed run trace (from RunResult.trace or a stored/replayed trace).
|
|
47
|
+
*/
|
|
48
|
+
export function createAuditRecord(trace: Trace): AuditRecord {
|
|
49
|
+
const finalEvent = trace.events.find((event): event is FinalEvent => event.type === "final");
|
|
50
|
+
const budgetStopEvent = trace.events.find((event): event is BudgetStopEvent => event.type === "budget-stop");
|
|
51
|
+
|
|
52
|
+
const outcome: AuditOutcome = budgetStopEvent
|
|
53
|
+
? { status: "budget-stopped", terminationCode: budgetStopEvent.reason }
|
|
54
|
+
: finalEvent
|
|
55
|
+
? { status: "completed" }
|
|
56
|
+
: { status: "aborted" };
|
|
57
|
+
|
|
58
|
+
const lastTurnCost = [...trace.events]
|
|
59
|
+
.reverse()
|
|
60
|
+
.find((event): event is TurnEvent => event.type === "agent-turn")?.cost;
|
|
61
|
+
const costSource = finalEvent?.cost ?? budgetStopEvent?.cost ?? lastTurnCost;
|
|
62
|
+
const cost: AuditCost = {
|
|
63
|
+
usd: costSource?.usd ?? 0,
|
|
64
|
+
inputTokens: costSource?.inputTokens ?? 0,
|
|
65
|
+
outputTokens: costSource?.outputTokens ?? 0
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const turnEvents = trace.events.filter((event): event is TurnEvent => event.type === "agent-turn");
|
|
69
|
+
const agentTurnMap = new Map<string, { role: string; count: number }>();
|
|
70
|
+
for (const event of turnEvents) {
|
|
71
|
+
const existing = agentTurnMap.get(event.agentId);
|
|
72
|
+
if (existing !== undefined) {
|
|
73
|
+
existing.count++;
|
|
74
|
+
} else {
|
|
75
|
+
agentTurnMap.set(event.agentId, { role: event.role, count: 1 });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const agents: AuditAgentRecord[] = [...agentTurnMap.entries()]
|
|
80
|
+
.map(([id, { role, count }]) => ({ id, role, turnCount: count }))
|
|
81
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
82
|
+
|
|
83
|
+
const childRunIds = trace.events
|
|
84
|
+
.filter((event): event is SubRunCompletedEvent => event.type === "sub-run-completed")
|
|
85
|
+
.map((event) => event.childRunId);
|
|
86
|
+
|
|
87
|
+
const startedAt = eventStartedAt(trace.events[0]);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
auditSchemaVersion: "1",
|
|
91
|
+
runId: trace.runId,
|
|
92
|
+
intent: trace.inputs.intent,
|
|
93
|
+
startedAt,
|
|
94
|
+
completedAt: trace.finalOutput.completedAt,
|
|
95
|
+
protocol: trace.protocol,
|
|
96
|
+
tier: trace.tier,
|
|
97
|
+
modelProviderId: trace.modelProviderId,
|
|
98
|
+
agentCount: agentTurnMap.size,
|
|
99
|
+
turnCount: turnEvents.length,
|
|
100
|
+
outcome,
|
|
101
|
+
cost,
|
|
102
|
+
agents,
|
|
103
|
+
...(childRunIds.length > 0 ? { childRunIds } : {})
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function eventStartedAt(event: Trace["events"][number] | undefined): string {
|
|
108
|
+
if (event === undefined) {
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if ("at" in event) {
|
|
113
|
+
return event.at;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if ("startedAt" in event) {
|
|
117
|
+
return event.startedAt;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return "";
|
|
121
|
+
}
|
package/src/runtime/broadcast.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
TerminationCondition,
|
|
18
18
|
TerminationStopRecord,
|
|
19
19
|
Tier,
|
|
20
|
+
Trace,
|
|
20
21
|
TranscriptEntry
|
|
21
22
|
} from "../types.js";
|
|
22
23
|
import { createRunId, elapsedMs, nowMs, providerCallIdFor } from "./ids.js";
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
createTranscriptLink,
|
|
36
37
|
emptyCost
|
|
37
38
|
} from "./defaults.js";
|
|
39
|
+
import { computeHealth, DEFAULT_HEALTH_THRESHOLDS } from "./health.js";
|
|
38
40
|
import { throwIfAborted } from "./cancellation.js";
|
|
39
41
|
import { parseAgentDecision } from "./decisions.js";
|
|
40
42
|
import { generateModelTurn } from "./model.js";
|
|
@@ -289,45 +291,46 @@ export async function runBroadcast(options: BroadcastRunOptions): Promise<RunRes
|
|
|
289
291
|
transcriptEntryCount: transcript.length
|
|
290
292
|
});
|
|
291
293
|
const finalEvent = events.at(-1);
|
|
294
|
+
const trace: Trace = {
|
|
295
|
+
schemaVersion: "1.0",
|
|
296
|
+
runId,
|
|
297
|
+
protocol: "broadcast",
|
|
298
|
+
tier: options.tier,
|
|
299
|
+
modelProviderId: options.model.id,
|
|
300
|
+
agentsUsed: options.agents,
|
|
301
|
+
inputs: createReplayTraceRunInputs({
|
|
302
|
+
intent: options.intent,
|
|
303
|
+
protocol: options.protocol,
|
|
304
|
+
tier: options.tier,
|
|
305
|
+
modelProviderId: options.model.id,
|
|
306
|
+
agents: options.agents,
|
|
307
|
+
temperature: options.temperature
|
|
308
|
+
}),
|
|
309
|
+
budget: createReplayTraceBudget({
|
|
310
|
+
tier: options.tier,
|
|
311
|
+
...(options.budget ? { caps: options.budget } : {}),
|
|
312
|
+
...(options.terminate ? { termination: options.terminate } : {})
|
|
313
|
+
}),
|
|
314
|
+
budgetStateChanges: createReplayTraceBudgetStateChanges(events),
|
|
315
|
+
seed: createReplayTraceSeed(options.seed),
|
|
316
|
+
protocolDecisions,
|
|
317
|
+
providerCalls,
|
|
318
|
+
finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
|
|
319
|
+
type: "final",
|
|
320
|
+
runId,
|
|
321
|
+
at: "",
|
|
322
|
+
output,
|
|
323
|
+
cost: totalCost,
|
|
324
|
+
transcript: createTranscriptLink(transcript)
|
|
325
|
+
}),
|
|
326
|
+
events,
|
|
327
|
+
transcript
|
|
328
|
+
};
|
|
292
329
|
|
|
293
330
|
return {
|
|
294
331
|
output,
|
|
295
332
|
eventLog: createRunEventLog(runId, "broadcast", events),
|
|
296
|
-
trace
|
|
297
|
-
schemaVersion: "1.0",
|
|
298
|
-
runId,
|
|
299
|
-
protocol: "broadcast",
|
|
300
|
-
tier: options.tier,
|
|
301
|
-
modelProviderId: options.model.id,
|
|
302
|
-
agentsUsed: options.agents,
|
|
303
|
-
inputs: createReplayTraceRunInputs({
|
|
304
|
-
intent: options.intent,
|
|
305
|
-
protocol: options.protocol,
|
|
306
|
-
tier: options.tier,
|
|
307
|
-
modelProviderId: options.model.id,
|
|
308
|
-
agents: options.agents,
|
|
309
|
-
temperature: options.temperature
|
|
310
|
-
}),
|
|
311
|
-
budget: createReplayTraceBudget({
|
|
312
|
-
tier: options.tier,
|
|
313
|
-
...(options.budget ? { caps: options.budget } : {}),
|
|
314
|
-
...(options.terminate ? { termination: options.terminate } : {})
|
|
315
|
-
}),
|
|
316
|
-
budgetStateChanges: createReplayTraceBudgetStateChanges(events),
|
|
317
|
-
seed: createReplayTraceSeed(options.seed),
|
|
318
|
-
protocolDecisions,
|
|
319
|
-
providerCalls,
|
|
320
|
-
finalOutput: createReplayTraceFinalOutput(output, finalEvent ?? {
|
|
321
|
-
type: "final",
|
|
322
|
-
runId,
|
|
323
|
-
at: "",
|
|
324
|
-
output,
|
|
325
|
-
cost: totalCost,
|
|
326
|
-
transcript: createTranscriptLink(transcript)
|
|
327
|
-
}),
|
|
328
|
-
events,
|
|
329
|
-
transcript
|
|
330
|
-
},
|
|
333
|
+
trace,
|
|
331
334
|
transcript,
|
|
332
335
|
usage: createRunUsage(totalCost),
|
|
333
336
|
metadata: createRunMetadata({
|
|
@@ -345,7 +348,8 @@ export async function runBroadcast(options: BroadcastRunOptions): Promise<RunRes
|
|
|
345
348
|
cost: totalCost,
|
|
346
349
|
events
|
|
347
350
|
}),
|
|
348
|
-
cost: totalCost
|
|
351
|
+
cost: totalCost,
|
|
352
|
+
health: computeHealth(trace, DEFAULT_HEALTH_THRESHOLDS)
|
|
349
353
|
};
|
|
350
354
|
|
|
351
355
|
function stopIfNeeded(): boolean {
|
|
@@ -440,4 +444,3 @@ function responseCost(response: ModelResponse): CostSummary {
|
|
|
440
444
|
totalTokens: response.usage?.totalTokens ?? 0
|
|
441
445
|
};
|
|
442
446
|
}
|
|
443
|
-
|
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
import { DogpileError, type JsonObject } from "../types.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Documented-convention vocabulary for `DogpileError({ code: "aborted" }).detail.reason`.
|
|
5
|
+
*
|
|
6
|
+
* Internal-only union (D-08): the literal strings are public-by-observation
|
|
7
|
+
* through `detail.reason` and locked via tests in `event-schema.test.ts` and
|
|
8
|
+
* `public-error-api.test.ts`, but we deliberately do NOT export the union
|
|
9
|
+
* type from `src/index.ts` to keep public-surface delta minimal.
|
|
10
|
+
*/
|
|
11
|
+
export type AbortReason = "parent-aborted" | "timeout";
|
|
12
|
+
export type ChildTimeoutSource = "provider" | "engine";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Classify an abort signal's reason into the BUDGET-01 / BUDGET-02
|
|
16
|
+
* `detail.reason` discriminator.
|
|
17
|
+
*
|
|
18
|
+
* - `"timeout"` when the reason is a {@link DogpileError} with `code === "timeout"`
|
|
19
|
+
* (matches the parent-deadline abort path in `engine.ts:createTimeoutAbortLifecycle`).
|
|
20
|
+
* - `"parent-aborted"` for every other reason — explicit caller abort, plain
|
|
21
|
+
* `Error`, `undefined`, or arbitrary primitive.
|
|
22
|
+
*/
|
|
23
|
+
export function classifyAbortReason(signalReasonOrError: unknown): AbortReason {
|
|
24
|
+
if (DogpileError.isInstance(signalReasonOrError) && signalReasonOrError.code === "timeout") {
|
|
25
|
+
return "timeout";
|
|
26
|
+
}
|
|
27
|
+
return "parent-aborted";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function classifyChildTimeoutSource(
|
|
31
|
+
_error: unknown,
|
|
32
|
+
context: {
|
|
33
|
+
readonly decisionTimeoutMs?: number;
|
|
34
|
+
readonly engineDefaultTimeoutMs?: number;
|
|
35
|
+
readonly isProviderError: boolean;
|
|
36
|
+
}
|
|
37
|
+
): ChildTimeoutSource {
|
|
38
|
+
if (context.isProviderError) {
|
|
39
|
+
return "provider";
|
|
40
|
+
}
|
|
41
|
+
if (context.decisionTimeoutMs !== undefined || context.engineDefaultTimeoutMs !== undefined) {
|
|
42
|
+
return "engine";
|
|
43
|
+
}
|
|
44
|
+
return "provider";
|
|
45
|
+
}
|
|
46
|
+
|
|
3
47
|
export function throwIfAborted(signal: AbortSignal | undefined, providerId: string): void {
|
|
4
48
|
if (!signal?.aborted) {
|
|
5
49
|
return;
|
|
@@ -24,7 +68,8 @@ export function createAbortErrorFromSignal(signal: AbortSignal, providerId: stri
|
|
|
24
68
|
return signal.reason;
|
|
25
69
|
}
|
|
26
70
|
|
|
27
|
-
|
|
71
|
+
const reason = classifyAbortReason(signal.reason);
|
|
72
|
+
return createAbortError(providerId, { reason }, signal.reason);
|
|
28
73
|
}
|
|
29
74
|
|
|
30
75
|
export function createTimeoutError(providerId: string, timeoutMs: number): DogpileError {
|
|
@@ -38,3 +83,16 @@ export function createTimeoutError(providerId: string, timeoutMs: number): Dogpi
|
|
|
38
83
|
}
|
|
39
84
|
});
|
|
40
85
|
}
|
|
86
|
+
|
|
87
|
+
export function createEngineDeadlineTimeoutError(providerId: string, timeoutMs: number): DogpileError {
|
|
88
|
+
return new DogpileError({
|
|
89
|
+
code: "provider-timeout",
|
|
90
|
+
message: `The child engine deadline expired after ${timeoutMs}ms.`,
|
|
91
|
+
retryable: true,
|
|
92
|
+
providerId,
|
|
93
|
+
detail: {
|
|
94
|
+
timeoutMs,
|
|
95
|
+
source: "engine"
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|