@dogpile/sdk 0.3.0 → 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 +145 -0
- package/README.md +1 -0
- package/dist/browser/index.js +2270 -507
- package/dist/browser/index.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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 +87 -2
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/runtime/broadcast.d.ts.map +1 -1
- package/dist/runtime/broadcast.js +1 -13
- 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 +74 -1
- package/dist/runtime/coordinator.d.ts.map +1 -1
- package/dist/runtime/coordinator.js +929 -34
- 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/ids.d.ts +19 -0
- package/dist/runtime/ids.d.ts.map +1 -0
- package/dist/runtime/ids.js +36 -0
- package/dist/runtime/ids.js.map +1 -0
- package/dist/runtime/logger.d.ts +61 -0
- package/dist/runtime/logger.d.ts.map +1 -0
- package/dist/runtime/logger.js +114 -0
- package/dist/runtime/logger.js.map +1 -0
- package/dist/runtime/retry.d.ts +99 -0
- package/dist/runtime/retry.d.ts.map +1 -0
- package/dist/runtime/retry.js +181 -0
- package/dist/runtime/retry.js.map +1 -0
- package/dist/runtime/sequential.d.ts.map +1 -1
- package/dist/runtime/sequential.js +9 -11
- package/dist/runtime/sequential.js.map +1 -1
- package/dist/runtime/shared.d.ts.map +1 -1
- package/dist/runtime/shared.js +1 -13
- package/dist/runtime/shared.js.map +1 -1
- package/dist/runtime/tools/built-in.d.ts +99 -0
- package/dist/runtime/tools/built-in.d.ts.map +1 -0
- package/dist/runtime/tools/built-in.js +577 -0
- package/dist/runtime/tools/built-in.js.map +1 -0
- package/dist/runtime/tools/vercel-ai.d.ts +67 -0
- package/dist/runtime/tools/vercel-ai.d.ts.map +1 -0
- package/dist/runtime/tools/vercel-ai.js +148 -0
- package/dist/runtime/tools/vercel-ai.js.map +1 -0
- package/dist/runtime/tools.d.ts +5 -268
- package/dist/runtime/tools.d.ts.map +1 -1
- package/dist/runtime/tools.js +7 -770
- package/dist/runtime/tools.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/benchmark.d.ts +276 -0
- package/dist/types/benchmark.d.ts.map +1 -0
- package/dist/types/benchmark.js +2 -0
- package/dist/types/benchmark.js.map +1 -0
- package/dist/types/events.d.ts +816 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +2 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/replay.d.ts +173 -0
- package/dist/types/replay.d.ts.map +1 -0
- package/dist/types/replay.js +2 -0
- package/dist/types/replay.js.map +1 -0
- package/dist/types.d.ts +135 -938
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +27 -1
- package/src/index.ts +14 -0
- package/src/providers/openai-compatible.ts +82 -3
- package/src/runtime/broadcast.ts +1 -16
- package/src/runtime/cancellation.ts +59 -1
- package/src/runtime/coordinator.ts +1164 -34
- package/src/runtime/decisions.ts +307 -4
- package/src/runtime/defaults.ts +376 -0
- package/src/runtime/engine.ts +363 -24
- package/src/runtime/ids.ts +41 -0
- package/src/runtime/logger.ts +152 -0
- package/src/runtime/retry.ts +270 -0
- package/src/runtime/sequential.ts +10 -13
- package/src/runtime/shared.ts +1 -16
- package/src/runtime/tools/built-in.ts +875 -0
- package/src/runtime/tools/vercel-ai.ts +269 -0
- package/src/runtime/tools.ts +60 -1255
- package/src/runtime/validation.ts +81 -0
- package/src/types/benchmark.ts +300 -0
- package/src/types/events.ts +895 -0
- package/src/types/replay.ts +212 -0
- package/src/types.ts +251 -997
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",
|
|
@@ -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
|
});
|
|
@@ -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
|
|
|
@@ -515,6 +604,293 @@ export function stableJsonStringify(value: unknown): string {
|
|
|
515
604
|
return JSON.stringify(canonicalizeSerializable(value));
|
|
516
605
|
}
|
|
517
606
|
|
|
607
|
+
/**
|
|
608
|
+
* The eight numeric fields recursively verified by `recomputeAccountingFromTrace`.
|
|
609
|
+
*
|
|
610
|
+
* These are the only summable scalars on `RunAccounting`. Non-numeric fields
|
|
611
|
+
* (`kind`, `tier`, `budget`, `termination`, `budgetStateChanges`) and derived
|
|
612
|
+
* ratios (`usdCapUtilization`, `totalTokenCapUtilization`) are NOT in this set.
|
|
613
|
+
*/
|
|
614
|
+
const RECOMPUTE_FIELD_ORDER: readonly [
|
|
615
|
+
"cost.usd",
|
|
616
|
+
"cost.inputTokens",
|
|
617
|
+
"cost.outputTokens",
|
|
618
|
+
"cost.totalTokens",
|
|
619
|
+
"usage.usd",
|
|
620
|
+
"usage.inputTokens",
|
|
621
|
+
"usage.outputTokens",
|
|
622
|
+
"usage.totalTokens"
|
|
623
|
+
] = [
|
|
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
|
+
|
|
634
|
+
const USD_FIELDS: ReadonlySet<string> = new Set(["cost.usd", "usage.usd"]);
|
|
635
|
+
const FLOAT_EPSILON = 1e-9;
|
|
636
|
+
|
|
637
|
+
function readNumericField(accounting: RunAccounting, field: (typeof RECOMPUTE_FIELD_ORDER)[number]): number {
|
|
638
|
+
switch (field) {
|
|
639
|
+
case "cost.usd":
|
|
640
|
+
return accounting.cost.usd;
|
|
641
|
+
case "cost.inputTokens":
|
|
642
|
+
return accounting.cost.inputTokens;
|
|
643
|
+
case "cost.outputTokens":
|
|
644
|
+
return accounting.cost.outputTokens;
|
|
645
|
+
case "cost.totalTokens":
|
|
646
|
+
return accounting.cost.totalTokens;
|
|
647
|
+
case "usage.usd":
|
|
648
|
+
return accounting.usage.usd;
|
|
649
|
+
case "usage.inputTokens":
|
|
650
|
+
return accounting.usage.inputTokens;
|
|
651
|
+
case "usage.outputTokens":
|
|
652
|
+
return accounting.usage.outputTokens;
|
|
653
|
+
case "usage.totalTokens":
|
|
654
|
+
return accounting.usage.totalTokens;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function fieldsEqual(field: (typeof RECOMPUTE_FIELD_ORDER)[number], a: number, b: number): boolean {
|
|
659
|
+
if (USD_FIELDS.has(field)) {
|
|
660
|
+
return Math.abs(a - b) < FLOAT_EPSILON;
|
|
661
|
+
}
|
|
662
|
+
return a === b;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function firstDifferingField(
|
|
666
|
+
recorded: RunAccounting,
|
|
667
|
+
recomputed: RunAccounting
|
|
668
|
+
): { readonly field: (typeof RECOMPUTE_FIELD_ORDER)[number]; readonly recorded: number; readonly recomputed: number } | null {
|
|
669
|
+
for (const field of RECOMPUTE_FIELD_ORDER) {
|
|
670
|
+
const a = readNumericField(recorded, field);
|
|
671
|
+
const b = readNumericField(recomputed, field);
|
|
672
|
+
if (!fieldsEqual(field, a, b)) {
|
|
673
|
+
return { field, recorded: a, recomputed: b };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function buildLocalAccounting(trace: Trace): RunAccounting {
|
|
680
|
+
return createRunAccounting({
|
|
681
|
+
tier: trace.tier,
|
|
682
|
+
...(trace.budget.caps ? { budget: trace.budget.caps } : {}),
|
|
683
|
+
...(trace.budget.termination ? { termination: trace.budget.termination } : {}),
|
|
684
|
+
cost: trace.finalOutput.cost,
|
|
685
|
+
events: trace.events
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function lastCostBearingEventCost(events: readonly RunEvent[]): CostSummary | null {
|
|
690
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
691
|
+
const event = events[index];
|
|
692
|
+
if (event === undefined) continue;
|
|
693
|
+
if (
|
|
694
|
+
event.type === "final" ||
|
|
695
|
+
event.type === "agent-turn" ||
|
|
696
|
+
event.type === "broadcast" ||
|
|
697
|
+
event.type === "budget-stop"
|
|
698
|
+
) {
|
|
699
|
+
return event.cost;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Recompute a parent's `RunAccounting` from a saved `Trace` for replay-time
|
|
707
|
+
* tamper detection.
|
|
708
|
+
*
|
|
709
|
+
* @remarks
|
|
710
|
+
* Returns the parent's local `RunAccounting` (built the same way `replay()`
|
|
711
|
+
* builds it today, from `trace.finalOutput.cost` and `trace.events`). While
|
|
712
|
+
* walking events, every `sub-run-completed` is recursed into and the
|
|
713
|
+
* recomputed child accounting is compared field-by-field to the recorded
|
|
714
|
+
* `event.subResult.accounting`. A mismatch on any of the eight enumerated
|
|
715
|
+
* numeric fields throws `DogpileError({ code: "invalid-configuration" })`
|
|
716
|
+
* with `detail.reason: "trace-accounting-mismatch"` and a concrete
|
|
717
|
+
* `detail.field` identifying the first differing numeric.
|
|
718
|
+
*
|
|
719
|
+
* Pure: no provider calls, no I/O, no clock reads.
|
|
720
|
+
*
|
|
721
|
+
* Non-summed fields (`kind`, `tier`, `budget`, `termination`,
|
|
722
|
+
* `budgetStateChanges`) and derived ratios (`usdCapUtilization`,
|
|
723
|
+
* `totalTokenCapUtilization`) are not in the comparison set.
|
|
724
|
+
*/
|
|
725
|
+
export function recomputeAccountingFromTrace(trace: Trace): RunAccounting {
|
|
726
|
+
const local = buildLocalAccounting(trace);
|
|
727
|
+
|
|
728
|
+
// Parent-level integrity: the recorded `trace.finalOutput.cost` must match
|
|
729
|
+
// the cost on the last cost-bearing event. On a clean trace this holds by
|
|
730
|
+
// construction (every protocol writes `totalCost` into the final event).
|
|
731
|
+
// On a trace where `finalOutput.cost` was mutated without updating the
|
|
732
|
+
// events (or vice versa), this catches the drift.
|
|
733
|
+
const lastEventCost = lastCostBearingEventCost(trace.events);
|
|
734
|
+
if (lastEventCost !== null) {
|
|
735
|
+
const reconstructedFromEvents: RunAccounting = createRunAccounting({
|
|
736
|
+
tier: trace.tier,
|
|
737
|
+
...(trace.budget.caps ? { budget: trace.budget.caps } : {}),
|
|
738
|
+
...(trace.budget.termination ? { termination: trace.budget.termination } : {}),
|
|
739
|
+
cost: lastEventCost,
|
|
740
|
+
events: trace.events
|
|
741
|
+
});
|
|
742
|
+
const drift = firstDifferingField(local, reconstructedFromEvents);
|
|
743
|
+
if (drift !== null) {
|
|
744
|
+
throw new DogpileError({
|
|
745
|
+
code: "invalid-configuration",
|
|
746
|
+
message: `Trace accounting mismatch at parent run ${trace.runId}: field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed}.`,
|
|
747
|
+
retryable: false,
|
|
748
|
+
detail: {
|
|
749
|
+
kind: "trace-validation",
|
|
750
|
+
reason: "trace-accounting-mismatch",
|
|
751
|
+
eventIndex: -1,
|
|
752
|
+
childRunId: trace.runId,
|
|
753
|
+
field: drift.field,
|
|
754
|
+
recorded: drift.recorded,
|
|
755
|
+
recomputed: drift.recomputed
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// BUDGET-03 / D-04: parent-rollup-drift parity check. Runs BEFORE the
|
|
762
|
+
// child recurse loop so a tampered child cost surfaces with the dedicated
|
|
763
|
+
// `subReason: "parent-rollup-drift"` rather than the generic
|
|
764
|
+
// `trace-accounting-mismatch` from the recurse check.
|
|
765
|
+
//
|
|
766
|
+
// The discriminator: each sub-run-completed event stores cost in TWO places
|
|
767
|
+
// (`subResult.cost` and `subResult.accounting.cost`). They must agree
|
|
768
|
+
// field-by-field — they are the parent-side roll-up source vs the
|
|
769
|
+
// child-side accounting source. Drift indicates someone mutated one without
|
|
770
|
+
// the other. For sub-run-failed events, `partialCost` must equal the cost
|
|
771
|
+
// implied by the partial trace's last cost-bearing event.
|
|
772
|
+
//
|
|
773
|
+
// Plus: Σ children must not exceed the parent's recorded total — cost is
|
|
774
|
+
// monotonic. A child total > parent total is unambiguous tampering.
|
|
775
|
+
for (let eventIndex = 0; eventIndex < trace.events.length; eventIndex += 1) {
|
|
776
|
+
const event = trace.events[eventIndex];
|
|
777
|
+
if (event === undefined) continue;
|
|
778
|
+
if (event.type === "sub-run-completed") {
|
|
779
|
+
const childRecordedRollup = createRunAccounting({
|
|
780
|
+
tier: trace.tier,
|
|
781
|
+
cost: event.subResult.cost,
|
|
782
|
+
events: []
|
|
783
|
+
});
|
|
784
|
+
const childRecordedAccounting = event.subResult.accounting;
|
|
785
|
+
const drift = firstDifferingField(childRecordedAccounting, childRecordedRollup);
|
|
786
|
+
if (drift !== null) {
|
|
787
|
+
throw new DogpileError({
|
|
788
|
+
code: "invalid-configuration",
|
|
789
|
+
message: `Trace parent-rollup mismatch at sub-run ${event.childRunId}: field "${drift.field}" recorded ${drift.recorded} on accounting, ${drift.recomputed} on subResult.cost.`,
|
|
790
|
+
retryable: false,
|
|
791
|
+
detail: {
|
|
792
|
+
kind: "trace-validation",
|
|
793
|
+
reason: "trace-accounting-mismatch",
|
|
794
|
+
subReason: "parent-rollup-drift",
|
|
795
|
+
eventIndex,
|
|
796
|
+
childRunId: event.childRunId,
|
|
797
|
+
field: drift.field,
|
|
798
|
+
recorded: drift.recorded,
|
|
799
|
+
recomputed: drift.recomputed
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
} else if (event.type === "sub-run-failed") {
|
|
804
|
+
const partialFromTrace = lastCostBearingEventCost(event.partialTrace.events) ?? emptyCost();
|
|
805
|
+
const recordedAccounting = createRunAccounting({
|
|
806
|
+
tier: trace.tier,
|
|
807
|
+
cost: event.partialCost,
|
|
808
|
+
events: []
|
|
809
|
+
});
|
|
810
|
+
const recomputedAccounting = createRunAccounting({
|
|
811
|
+
tier: trace.tier,
|
|
812
|
+
cost: partialFromTrace,
|
|
813
|
+
events: []
|
|
814
|
+
});
|
|
815
|
+
const drift = firstDifferingField(recordedAccounting, recomputedAccounting);
|
|
816
|
+
if (drift !== null) {
|
|
817
|
+
throw new DogpileError({
|
|
818
|
+
code: "invalid-configuration",
|
|
819
|
+
message: `Trace parent-rollup mismatch at sub-run ${event.childRunId}: partialCost field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed} from partialTrace events.`,
|
|
820
|
+
retryable: false,
|
|
821
|
+
detail: {
|
|
822
|
+
kind: "trace-validation",
|
|
823
|
+
reason: "trace-accounting-mismatch",
|
|
824
|
+
subReason: "parent-rollup-drift",
|
|
825
|
+
eventIndex,
|
|
826
|
+
childRunId: event.childRunId,
|
|
827
|
+
field: drift.field,
|
|
828
|
+
recorded: drift.recorded,
|
|
829
|
+
recomputed: drift.recomputed
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Tree-level monotonicity: Σ children must be ≤ parent's recorded total
|
|
837
|
+
// across all 8 fields. Cost is non-negative and monotonic.
|
|
838
|
+
const subRunTotal = accumulateSubRunCost(trace.events);
|
|
839
|
+
const parentTotal = trace.finalOutput.cost;
|
|
840
|
+
for (const field of RECOMPUTE_FIELD_ORDER) {
|
|
841
|
+
if (field.startsWith("usage.")) continue; // usage mirrors cost; one check is enough.
|
|
842
|
+
const [, key] = field.split(".") as [string, keyof CostSummary];
|
|
843
|
+
const parentValue = parentTotal[key];
|
|
844
|
+
const childValue = subRunTotal[key];
|
|
845
|
+
if (childValue - parentValue > FLOAT_EPSILON) {
|
|
846
|
+
throw new DogpileError({
|
|
847
|
+
code: "invalid-configuration",
|
|
848
|
+
message: `Trace parent-rollup mismatch at run ${trace.runId}: field "${field}" Σ children ${childValue} exceeds parent recorded ${parentValue}.`,
|
|
849
|
+
retryable: false,
|
|
850
|
+
detail: {
|
|
851
|
+
kind: "trace-validation",
|
|
852
|
+
reason: "trace-accounting-mismatch",
|
|
853
|
+
subReason: "parent-rollup-drift",
|
|
854
|
+
eventIndex: -1,
|
|
855
|
+
childRunId: trace.runId,
|
|
856
|
+
field,
|
|
857
|
+
recorded: parentValue,
|
|
858
|
+
recomputed: childValue
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Child-level integrity: recurse into every sub-run-completed and verify
|
|
865
|
+
// its recorded `subResult.accounting` matches what the child trace recomputes.
|
|
866
|
+
for (let eventIndex = 0; eventIndex < trace.events.length; eventIndex += 1) {
|
|
867
|
+
const event = trace.events[eventIndex];
|
|
868
|
+
if (event === undefined || event.type !== "sub-run-completed") continue;
|
|
869
|
+
|
|
870
|
+
const childRecomputed = recomputeAccountingFromTrace(event.subResult.trace);
|
|
871
|
+
const childRecorded = event.subResult.accounting;
|
|
872
|
+
const drift = firstDifferingField(childRecorded, childRecomputed);
|
|
873
|
+
if (drift !== null) {
|
|
874
|
+
throw new DogpileError({
|
|
875
|
+
code: "invalid-configuration",
|
|
876
|
+
message: `Trace accounting mismatch at sub-run ${event.childRunId}: field "${drift.field}" recorded ${drift.recorded}, recomputed ${drift.recomputed}.`,
|
|
877
|
+
retryable: false,
|
|
878
|
+
detail: {
|
|
879
|
+
kind: "trace-validation",
|
|
880
|
+
reason: "trace-accounting-mismatch",
|
|
881
|
+
eventIndex,
|
|
882
|
+
childRunId: event.childRunId,
|
|
883
|
+
field: drift.field,
|
|
884
|
+
recorded: drift.recorded,
|
|
885
|
+
recomputed: drift.recomputed
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return local;
|
|
892
|
+
}
|
|
893
|
+
|
|
518
894
|
export function canonicalizeSerializable<T>(value: T): T {
|
|
519
895
|
if (Array.isArray(value)) {
|
|
520
896
|
return value.map((item) => canonicalizeSerializable(item)) as T;
|