@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/runtime/defaults.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DogpileError } from "../types.js";
|
|
1
2
|
import type {
|
|
2
3
|
AgentSpec,
|
|
3
4
|
Budget,
|
|
@@ -18,7 +19,9 @@ import type {
|
|
|
18
19
|
RunEventLog,
|
|
19
20
|
RunMetadata,
|
|
20
21
|
RunUsage,
|
|
22
|
+
OnChildFailureMode,
|
|
21
23
|
Tier,
|
|
24
|
+
Trace,
|
|
22
25
|
TranscriptEntry,
|
|
23
26
|
TranscriptLink
|
|
24
27
|
} from "../types.js";
|
|
@@ -129,6 +132,38 @@ export function addCost(left: CostSummary, right: CostSummary): CostSummary {
|
|
|
129
132
|
};
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
export function resolveOnChildFailure(
|
|
136
|
+
runOption: OnChildFailureMode | undefined,
|
|
137
|
+
engineOption: OnChildFailureMode | undefined
|
|
138
|
+
): OnChildFailureMode {
|
|
139
|
+
// onChildFailure precedence: per-run option > engine option > default.
|
|
140
|
+
return runOption ?? engineOption ?? "continue";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Walk a parent's events and accumulate the cost contributed by every
|
|
145
|
+
* sub-run (BUDGET-03 / D-06). Internal helper — not part of the public surface.
|
|
146
|
+
*
|
|
147
|
+
* - `sub-run-completed` events contribute `event.subResult.cost`.
|
|
148
|
+
* - `sub-run-failed` events contribute `event.partialCost` (real provider
|
|
149
|
+
* spend captured before the throw).
|
|
150
|
+
*
|
|
151
|
+
* Used by the `parent-rollup-drift` parity check in
|
|
152
|
+
* {@link recomputeAccountingFromTrace} to verify the parent's recorded
|
|
153
|
+
* accounting equals `localOnly + Σ children` recursively.
|
|
154
|
+
*/
|
|
155
|
+
export function accumulateSubRunCost(events: readonly RunEvent[]): CostSummary {
|
|
156
|
+
let total = emptyCost();
|
|
157
|
+
for (const event of events) {
|
|
158
|
+
if (event.type === "sub-run-completed") {
|
|
159
|
+
total = addCost(total, event.subResult.cost);
|
|
160
|
+
} else if (event.type === "sub-run-failed") {
|
|
161
|
+
total = addCost(total, event.partialCost);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return total;
|
|
165
|
+
}
|
|
166
|
+
|
|
132
167
|
export function createTranscriptLink(transcript: readonly TranscriptEntry[]): TranscriptLink {
|
|
133
168
|
return {
|
|
134
169
|
kind: "trace-transcript",
|
|
@@ -201,8 +236,8 @@ export function createRunMetadata(options: {
|
|
|
201
236
|
tier: options.tier,
|
|
202
237
|
modelProviderId: options.modelProviderId,
|
|
203
238
|
agentsUsed: options.agentsUsed,
|
|
204
|
-
startedAt: firstEvent
|
|
205
|
-
completedAt: lastEvent
|
|
239
|
+
startedAt: eventTimestamp(firstEvent) ?? "",
|
|
240
|
+
completedAt: eventTimestamp(lastEvent) ?? ""
|
|
206
241
|
};
|
|
207
242
|
}
|
|
208
243
|
|
|
@@ -274,6 +309,13 @@ export function createReplayTraceBudgetStateChanges(
|
|
|
274
309
|
case "model-output-chunk":
|
|
275
310
|
case "tool-call":
|
|
276
311
|
case "tool-result":
|
|
312
|
+
case "sub-run-started":
|
|
313
|
+
case "sub-run-completed":
|
|
314
|
+
case "sub-run-failed":
|
|
315
|
+
case "sub-run-parent-aborted":
|
|
316
|
+
case "sub-run-budget-clamped":
|
|
317
|
+
case "sub-run-queued":
|
|
318
|
+
case "sub-run-concurrency-clamped":
|
|
277
319
|
return [];
|
|
278
320
|
}
|
|
279
321
|
});
|
|
@@ -323,7 +365,7 @@ export function createReplayTraceProtocolDecision(
|
|
|
323
365
|
eventType: event.type,
|
|
324
366
|
protocol,
|
|
325
367
|
decision: options.decision ?? defaultProtocolDecision(event),
|
|
326
|
-
at: event
|
|
368
|
+
at: eventTimestamp(event),
|
|
327
369
|
...(options.turn !== undefined ? { turn: options.turn } : {}),
|
|
328
370
|
...(options.phase !== undefined ? { phase: options.phase } : {}),
|
|
329
371
|
...(options.round !== undefined ? { round: options.round } : {}),
|
|
@@ -408,6 +450,39 @@ export function createReplayTraceProtocolDecision(
|
|
|
408
450
|
output: event.output,
|
|
409
451
|
cost: event.cost
|
|
410
452
|
};
|
|
453
|
+
case "sub-run-started":
|
|
454
|
+
return {
|
|
455
|
+
...base,
|
|
456
|
+
input: event.intent
|
|
457
|
+
};
|
|
458
|
+
case "sub-run-completed":
|
|
459
|
+
return {
|
|
460
|
+
...base,
|
|
461
|
+
output: event.subResult.output,
|
|
462
|
+
cost: event.subResult.cost
|
|
463
|
+
};
|
|
464
|
+
case "sub-run-failed":
|
|
465
|
+
return {
|
|
466
|
+
...base
|
|
467
|
+
};
|
|
468
|
+
case "sub-run-parent-aborted":
|
|
469
|
+
return {
|
|
470
|
+
...base
|
|
471
|
+
};
|
|
472
|
+
case "sub-run-budget-clamped":
|
|
473
|
+
return {
|
|
474
|
+
...base
|
|
475
|
+
};
|
|
476
|
+
case "sub-run-queued":
|
|
477
|
+
return {
|
|
478
|
+
...base,
|
|
479
|
+
childRunId: event.childRunId,
|
|
480
|
+
queuePosition: event.queuePosition
|
|
481
|
+
};
|
|
482
|
+
case "sub-run-concurrency-clamped":
|
|
483
|
+
return {
|
|
484
|
+
...base
|
|
485
|
+
};
|
|
411
486
|
}
|
|
412
487
|
}
|
|
413
488
|
|
|
@@ -433,6 +508,20 @@ function defaultProtocolDecision(event: RunEvent): ReplayTraceProtocolDecisionTy
|
|
|
433
508
|
return "stop-for-budget";
|
|
434
509
|
case "final":
|
|
435
510
|
return "finalize-output";
|
|
511
|
+
case "sub-run-started":
|
|
512
|
+
return "start-sub-run";
|
|
513
|
+
case "sub-run-completed":
|
|
514
|
+
return "complete-sub-run";
|
|
515
|
+
case "sub-run-failed":
|
|
516
|
+
return "fail-sub-run";
|
|
517
|
+
case "sub-run-parent-aborted":
|
|
518
|
+
return "mark-sub-run-parent-aborted";
|
|
519
|
+
case "sub-run-budget-clamped":
|
|
520
|
+
return "mark-sub-run-budget-clamped";
|
|
521
|
+
case "sub-run-queued":
|
|
522
|
+
return "queue-sub-run";
|
|
523
|
+
case "sub-run-concurrency-clamped":
|
|
524
|
+
return "mark-sub-run-concurrency-clamped";
|
|
436
525
|
}
|
|
437
526
|
}
|
|
438
527
|
|
|
@@ -461,7 +550,7 @@ export function createReplayTraceFinalOutput(output: string, event: RunEvent): R
|
|
|
461
550
|
kind: "replay-trace-final-output",
|
|
462
551
|
output,
|
|
463
552
|
cost: emptyCost(),
|
|
464
|
-
completedAt: event
|
|
553
|
+
completedAt: eventTimestamp(event),
|
|
465
554
|
transcript: {
|
|
466
555
|
kind: "trace-transcript",
|
|
467
556
|
entryCount: 0,
|
|
@@ -470,6 +559,14 @@ export function createReplayTraceFinalOutput(output: string, event: RunEvent): R
|
|
|
470
559
|
};
|
|
471
560
|
}
|
|
472
561
|
|
|
562
|
+
function eventTimestamp(event: RunEvent): string;
|
|
563
|
+
function eventTimestamp(event: RunEvent | undefined): string | undefined;
|
|
564
|
+
function eventTimestamp(event: RunEvent | undefined): string | undefined {
|
|
565
|
+
if (event === undefined) return undefined;
|
|
566
|
+
if ("at" in event) return event.at;
|
|
567
|
+
return event.type === "model-response" ? event.completedAt : event.startedAt;
|
|
568
|
+
}
|
|
569
|
+
|
|
473
570
|
export function nextProviderCallId(
|
|
474
571
|
runId: string,
|
|
475
572
|
providerCalls: readonly ReplayTraceProviderCall[]
|
|
@@ -500,6 +597,7 @@ export function canonicalizeRunResult(result: RunResult): RunResult {
|
|
|
500
597
|
cost: canonicalizeSerializable(result.cost),
|
|
501
598
|
...(result.evaluation !== undefined ? { evaluation: canonicalizeSerializable(result.evaluation) } : {}),
|
|
502
599
|
eventLog,
|
|
600
|
+
health: canonicalizeSerializable(result.health),
|
|
503
601
|
metadata: canonicalizeSerializable(result.metadata),
|
|
504
602
|
output: result.output,
|
|
505
603
|
...(result.quality !== undefined ? { quality: canonicalizeSerializable(result.quality) } : {}),
|
|
@@ -515,6 +613,293 @@ export function stableJsonStringify(value: unknown): string {
|
|
|
515
613
|
return JSON.stringify(canonicalizeSerializable(value));
|
|
516
614
|
}
|
|
517
615
|
|
|
616
|
+
/**
|
|
617
|
+
* The eight numeric fields recursively verified by `recomputeAccountingFromTrace`.
|
|
618
|
+
*
|
|
619
|
+
* These are the only summable scalars on `RunAccounting`. Non-numeric fields
|
|
620
|
+
* (`kind`, `tier`, `budget`, `termination`, `budgetStateChanges`) and derived
|
|
621
|
+
* ratios (`usdCapUtilization`, `totalTokenCapUtilization`) are NOT in this set.
|
|
622
|
+
*/
|
|
623
|
+
const RECOMPUTE_FIELD_ORDER: readonly [
|
|
624
|
+
"cost.usd",
|
|
625
|
+
"cost.inputTokens",
|
|
626
|
+
"cost.outputTokens",
|
|
627
|
+
"cost.totalTokens",
|
|
628
|
+
"usage.usd",
|
|
629
|
+
"usage.inputTokens",
|
|
630
|
+
"usage.outputTokens",
|
|
631
|
+
"usage.totalTokens"
|
|
632
|
+
] = [
|
|
633
|
+
"cost.usd",
|
|
634
|
+
"cost.inputTokens",
|
|
635
|
+
"cost.outputTokens",
|
|
636
|
+
"cost.totalTokens",
|
|
637
|
+
"usage.usd",
|
|
638
|
+
"usage.inputTokens",
|
|
639
|
+
"usage.outputTokens",
|
|
640
|
+
"usage.totalTokens"
|
|
641
|
+
];
|
|
642
|
+
|
|
643
|
+
const USD_FIELDS: ReadonlySet<string> = new Set(["cost.usd", "usage.usd"]);
|
|
644
|
+
const FLOAT_EPSILON = 1e-9;
|
|
645
|
+
|
|
646
|
+
function readNumericField(accounting: RunAccounting, field: (typeof RECOMPUTE_FIELD_ORDER)[number]): number {
|
|
647
|
+
switch (field) {
|
|
648
|
+
case "cost.usd":
|
|
649
|
+
return accounting.cost.usd;
|
|
650
|
+
case "cost.inputTokens":
|
|
651
|
+
return accounting.cost.inputTokens;
|
|
652
|
+
case "cost.outputTokens":
|
|
653
|
+
return accounting.cost.outputTokens;
|
|
654
|
+
case "cost.totalTokens":
|
|
655
|
+
return accounting.cost.totalTokens;
|
|
656
|
+
case "usage.usd":
|
|
657
|
+
return accounting.usage.usd;
|
|
658
|
+
case "usage.inputTokens":
|
|
659
|
+
return accounting.usage.inputTokens;
|
|
660
|
+
case "usage.outputTokens":
|
|
661
|
+
return accounting.usage.outputTokens;
|
|
662
|
+
case "usage.totalTokens":
|
|
663
|
+
return accounting.usage.totalTokens;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function fieldsEqual(field: (typeof RECOMPUTE_FIELD_ORDER)[number], a: number, b: number): boolean {
|
|
668
|
+
if (USD_FIELDS.has(field)) {
|
|
669
|
+
return Math.abs(a - b) < FLOAT_EPSILON;
|
|
670
|
+
}
|
|
671
|
+
return a === b;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function firstDifferingField(
|
|
675
|
+
recorded: RunAccounting,
|
|
676
|
+
recomputed: RunAccounting
|
|
677
|
+
): { readonly field: (typeof RECOMPUTE_FIELD_ORDER)[number]; readonly recorded: number; readonly recomputed: number } | null {
|
|
678
|
+
for (const field of RECOMPUTE_FIELD_ORDER) {
|
|
679
|
+
const a = readNumericField(recorded, field);
|
|
680
|
+
const b = readNumericField(recomputed, field);
|
|
681
|
+
if (!fieldsEqual(field, a, b)) {
|
|
682
|
+
return { field, recorded: a, recomputed: b };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function buildLocalAccounting(trace: Trace): RunAccounting {
|
|
689
|
+
return createRunAccounting({
|
|
690
|
+
tier: trace.tier,
|
|
691
|
+
...(trace.budget.caps ? { budget: trace.budget.caps } : {}),
|
|
692
|
+
...(trace.budget.termination ? { termination: trace.budget.termination } : {}),
|
|
693
|
+
cost: trace.finalOutput.cost,
|
|
694
|
+
events: trace.events
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function lastCostBearingEventCost(events: readonly RunEvent[]): CostSummary | null {
|
|
699
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
700
|
+
const event = events[index];
|
|
701
|
+
if (event === undefined) continue;
|
|
702
|
+
if (
|
|
703
|
+
event.type === "final" ||
|
|
704
|
+
event.type === "agent-turn" ||
|
|
705
|
+
event.type === "broadcast" ||
|
|
706
|
+
event.type === "budget-stop"
|
|
707
|
+
) {
|
|
708
|
+
return event.cost;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Recompute a parent's `RunAccounting` from a saved `Trace` for replay-time
|
|
716
|
+
* tamper detection.
|
|
717
|
+
*
|
|
718
|
+
* @remarks
|
|
719
|
+
* Returns the parent's local `RunAccounting` (built the same way `replay()`
|
|
720
|
+
* builds it today, from `trace.finalOutput.cost` and `trace.events`). While
|
|
721
|
+
* walking events, every `sub-run-completed` is recursed into and the
|
|
722
|
+
* recomputed child accounting is compared field-by-field to the recorded
|
|
723
|
+
* `event.subResult.accounting`. A mismatch on any of the eight enumerated
|
|
724
|
+
* numeric fields throws `DogpileError({ code: "invalid-configuration" })`
|
|
725
|
+
* with `detail.reason: "trace-accounting-mismatch"` and a concrete
|
|
726
|
+
* `detail.field` identifying the first differing numeric.
|
|
727
|
+
*
|
|
728
|
+
* Pure: no provider calls, no I/O, no clock reads.
|
|
729
|
+
*
|
|
730
|
+
* Non-summed fields (`kind`, `tier`, `budget`, `termination`,
|
|
731
|
+
* `budgetStateChanges`) and derived ratios (`usdCapUtilization`,
|
|
732
|
+
* `totalTokenCapUtilization`) are not in the comparison set.
|
|
733
|
+
*/
|
|
734
|
+
export function recomputeAccountingFromTrace(trace: Trace): RunAccounting {
|
|
735
|
+
const local = buildLocalAccounting(trace);
|
|
736
|
+
|
|
737
|
+
// Parent-level integrity: the recorded `trace.finalOutput.cost` must match
|
|
738
|
+
// the cost on the last cost-bearing event. On a clean trace this holds by
|
|
739
|
+
// construction (every protocol writes `totalCost` into the final event).
|
|
740
|
+
// On a trace where `finalOutput.cost` was mutated without updating the
|
|
741
|
+
// events (or vice versa), this catches the drift.
|
|
742
|
+
const lastEventCost = lastCostBearingEventCost(trace.events);
|
|
743
|
+
if (lastEventCost !== null) {
|
|
744
|
+
const reconstructedFromEvents: RunAccounting = createRunAccounting({
|
|
745
|
+
tier: trace.tier,
|
|
746
|
+
...(trace.budget.caps ? { budget: trace.budget.caps } : {}),
|
|
747
|
+
...(trace.budget.termination ? { termination: trace.budget.termination } : {}),
|
|
748
|
+
cost: lastEventCost,
|
|
749
|
+
events: trace.events
|
|
750
|
+
});
|
|
751
|
+
const drift = firstDifferingField(local, reconstructedFromEvents);
|
|
752
|
+
if (drift !== null) {
|
|
753
|
+
throw new DogpileError({
|
|
754
|
+
code: "invalid-configuration",
|
|
755
|
+
message: `Trace accounting mismatch at parent run ${trace.runId}: field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed}.`,
|
|
756
|
+
retryable: false,
|
|
757
|
+
detail: {
|
|
758
|
+
kind: "trace-validation",
|
|
759
|
+
reason: "trace-accounting-mismatch",
|
|
760
|
+
eventIndex: -1,
|
|
761
|
+
childRunId: trace.runId,
|
|
762
|
+
field: drift.field,
|
|
763
|
+
recorded: drift.recorded,
|
|
764
|
+
recomputed: drift.recomputed
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// BUDGET-03 / D-04: parent-rollup-drift parity check. Runs BEFORE the
|
|
771
|
+
// child recurse loop so a tampered child cost surfaces with the dedicated
|
|
772
|
+
// `subReason: "parent-rollup-drift"` rather than the generic
|
|
773
|
+
// `trace-accounting-mismatch` from the recurse check.
|
|
774
|
+
//
|
|
775
|
+
// The discriminator: each sub-run-completed event stores cost in TWO places
|
|
776
|
+
// (`subResult.cost` and `subResult.accounting.cost`). They must agree
|
|
777
|
+
// field-by-field — they are the parent-side roll-up source vs the
|
|
778
|
+
// child-side accounting source. Drift indicates someone mutated one without
|
|
779
|
+
// the other. For sub-run-failed events, `partialCost` must equal the cost
|
|
780
|
+
// implied by the partial trace's last cost-bearing event.
|
|
781
|
+
//
|
|
782
|
+
// Plus: Σ children must not exceed the parent's recorded total — cost is
|
|
783
|
+
// monotonic. A child total > parent total is unambiguous tampering.
|
|
784
|
+
for (let eventIndex = 0; eventIndex < trace.events.length; eventIndex += 1) {
|
|
785
|
+
const event = trace.events[eventIndex];
|
|
786
|
+
if (event === undefined) continue;
|
|
787
|
+
if (event.type === "sub-run-completed") {
|
|
788
|
+
const childRecordedRollup = createRunAccounting({
|
|
789
|
+
tier: trace.tier,
|
|
790
|
+
cost: event.subResult.cost,
|
|
791
|
+
events: []
|
|
792
|
+
});
|
|
793
|
+
const childRecordedAccounting = event.subResult.accounting;
|
|
794
|
+
const drift = firstDifferingField(childRecordedAccounting, childRecordedRollup);
|
|
795
|
+
if (drift !== null) {
|
|
796
|
+
throw new DogpileError({
|
|
797
|
+
code: "invalid-configuration",
|
|
798
|
+
message: `Trace parent-rollup mismatch at sub-run ${event.childRunId}: field "${drift.field}" recorded ${drift.recorded} on accounting, ${drift.recomputed} on subResult.cost.`,
|
|
799
|
+
retryable: false,
|
|
800
|
+
detail: {
|
|
801
|
+
kind: "trace-validation",
|
|
802
|
+
reason: "trace-accounting-mismatch",
|
|
803
|
+
subReason: "parent-rollup-drift",
|
|
804
|
+
eventIndex,
|
|
805
|
+
childRunId: event.childRunId,
|
|
806
|
+
field: drift.field,
|
|
807
|
+
recorded: drift.recorded,
|
|
808
|
+
recomputed: drift.recomputed
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
} else if (event.type === "sub-run-failed") {
|
|
813
|
+
const partialFromTrace = lastCostBearingEventCost(event.partialTrace.events) ?? emptyCost();
|
|
814
|
+
const recordedAccounting = createRunAccounting({
|
|
815
|
+
tier: trace.tier,
|
|
816
|
+
cost: event.partialCost,
|
|
817
|
+
events: []
|
|
818
|
+
});
|
|
819
|
+
const recomputedAccounting = createRunAccounting({
|
|
820
|
+
tier: trace.tier,
|
|
821
|
+
cost: partialFromTrace,
|
|
822
|
+
events: []
|
|
823
|
+
});
|
|
824
|
+
const drift = firstDifferingField(recordedAccounting, recomputedAccounting);
|
|
825
|
+
if (drift !== null) {
|
|
826
|
+
throw new DogpileError({
|
|
827
|
+
code: "invalid-configuration",
|
|
828
|
+
message: `Trace parent-rollup mismatch at sub-run ${event.childRunId}: partialCost field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed} from partialTrace events.`,
|
|
829
|
+
retryable: false,
|
|
830
|
+
detail: {
|
|
831
|
+
kind: "trace-validation",
|
|
832
|
+
reason: "trace-accounting-mismatch",
|
|
833
|
+
subReason: "parent-rollup-drift",
|
|
834
|
+
eventIndex,
|
|
835
|
+
childRunId: event.childRunId,
|
|
836
|
+
field: drift.field,
|
|
837
|
+
recorded: drift.recorded,
|
|
838
|
+
recomputed: drift.recomputed
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Tree-level monotonicity: Σ children must be ≤ parent's recorded total
|
|
846
|
+
// across all 8 fields. Cost is non-negative and monotonic.
|
|
847
|
+
const subRunTotal = accumulateSubRunCost(trace.events);
|
|
848
|
+
const parentTotal = trace.finalOutput.cost;
|
|
849
|
+
for (const field of RECOMPUTE_FIELD_ORDER) {
|
|
850
|
+
if (field.startsWith("usage.")) continue; // usage mirrors cost; one check is enough.
|
|
851
|
+
const [, key] = field.split(".") as [string, keyof CostSummary];
|
|
852
|
+
const parentValue = parentTotal[key];
|
|
853
|
+
const childValue = subRunTotal[key];
|
|
854
|
+
if (childValue - parentValue > FLOAT_EPSILON) {
|
|
855
|
+
throw new DogpileError({
|
|
856
|
+
code: "invalid-configuration",
|
|
857
|
+
message: `Trace parent-rollup mismatch at run ${trace.runId}: field "${field}" Σ children ${childValue} exceeds parent recorded ${parentValue}.`,
|
|
858
|
+
retryable: false,
|
|
859
|
+
detail: {
|
|
860
|
+
kind: "trace-validation",
|
|
861
|
+
reason: "trace-accounting-mismatch",
|
|
862
|
+
subReason: "parent-rollup-drift",
|
|
863
|
+
eventIndex: -1,
|
|
864
|
+
childRunId: trace.runId,
|
|
865
|
+
field,
|
|
866
|
+
recorded: parentValue,
|
|
867
|
+
recomputed: childValue
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Child-level integrity: recurse into every sub-run-completed and verify
|
|
874
|
+
// its recorded `subResult.accounting` matches what the child trace recomputes.
|
|
875
|
+
for (let eventIndex = 0; eventIndex < trace.events.length; eventIndex += 1) {
|
|
876
|
+
const event = trace.events[eventIndex];
|
|
877
|
+
if (event === undefined || event.type !== "sub-run-completed") continue;
|
|
878
|
+
|
|
879
|
+
const childRecomputed = recomputeAccountingFromTrace(event.subResult.trace);
|
|
880
|
+
const childRecorded = event.subResult.accounting;
|
|
881
|
+
const drift = firstDifferingField(childRecorded, childRecomputed);
|
|
882
|
+
if (drift !== null) {
|
|
883
|
+
throw new DogpileError({
|
|
884
|
+
code: "invalid-configuration",
|
|
885
|
+
message: `Trace accounting mismatch at sub-run ${event.childRunId}: field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed}.`,
|
|
886
|
+
retryable: false,
|
|
887
|
+
detail: {
|
|
888
|
+
kind: "trace-validation",
|
|
889
|
+
reason: "trace-accounting-mismatch",
|
|
890
|
+
eventIndex,
|
|
891
|
+
childRunId: event.childRunId,
|
|
892
|
+
field: drift.field,
|
|
893
|
+
recorded: drift.recorded,
|
|
894
|
+
recomputed: drift.recomputed
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return local;
|
|
901
|
+
}
|
|
902
|
+
|
|
518
903
|
export function canonicalizeSerializable<T>(value: T): T {
|
|
519
904
|
if (Array.isArray(value)) {
|
|
520
905
|
return value.map((item) => canonicalizeSerializable(item)) as T;
|