@tangle-network/agent-eval 0.189.0 → 0.190.1
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 +94 -0
- package/README.md +3 -2
- package/dist/adapters/http.d.ts +10 -1
- package/dist/adapters/http.d.ts.map +1 -1
- package/dist/adapters/http.js +75 -7
- package/dist/adapters/http.js.map +1 -1
- package/dist/analyst/index.d.ts +66 -3
- package/dist/analyst/index.d.ts.map +1 -1
- package/dist/analyst/index.js +212 -7
- package/dist/analyst/index.js.map +1 -1
- package/dist/{benchmark-command-BhhAYstU.js → benchmark-command-BXIr_k8D.js} +12 -11
- package/dist/benchmark-command-BXIr_k8D.js.map +1 -0
- package/dist/benchmarks/index.d.ts +1 -1
- package/dist/benchmarks/index.js +3 -3
- package/dist/campaign/index.d.ts +4 -4
- package/dist/campaign/index.js +7 -7
- package/dist/{campaign-BWoSQPSU.js → campaign-B7q74uIh.js} +256 -11
- package/dist/campaign-B7q74uIh.js.map +1 -0
- package/dist/{campaign-evidence-CBlGJ3kC.js → campaign-evidence-D40-LHIL.js} +6 -5
- package/dist/campaign-evidence-D40-LHIL.js.map +1 -0
- package/dist/{chat-client-C4dg7Ng_.js → chat-client-B_ItsSBw.js} +2 -2
- package/dist/{chat-client-C4dg7Ng_.js.map → chat-client-B_ItsSBw.js.map} +1 -1
- package/dist/{chat-json-call-JVGbfx95.js → chat-json-call-CXHBvUz8.js} +2 -2
- package/dist/{chat-json-call-JVGbfx95.js.map → chat-json-call-CXHBvUz8.js.map} +1 -1
- package/dist/claim-BGOUqoMq.d.ts +38 -0
- package/dist/claim-BGOUqoMq.d.ts.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/contract/index.d.ts +4 -4
- package/dist/contract/index.js +8 -8
- package/dist/{default-registry-m2jhbG6Q.js → default-registry-16I-Bxlf.js} +2 -2
- package/dist/{default-registry-m2jhbG6Q.js.map → default-registry-16I-Bxlf.js.map} +1 -1
- package/dist/{define-agent-eval-98sgUaC1.js → define-agent-eval-DPizhHfy.js} +4 -4
- package/dist/{define-agent-eval-98sgUaC1.js.map → define-agent-eval-DPizhHfy.js.map} +1 -1
- package/dist/{define-agent-eval-q6890HU4.d.ts → define-agent-eval-DblGedQk.d.ts} +4 -3
- package/dist/define-agent-eval-DblGedQk.d.ts.map +1 -0
- package/dist/diagnosis/index.d.ts +14 -2
- package/dist/diagnosis/index.d.ts.map +1 -1
- package/dist/diagnosis/index.js +11 -4
- package/dist/diagnosis/index.js.map +1 -1
- package/dist/{dspy-rlm-engine-DLYttKBm.js → dspy-rlm-engine-Cs5LzJ-f.js} +3 -3
- package/dist/{dspy-rlm-engine-DLYttKBm.js.map → dspy-rlm-engine-Cs5LzJ-f.js.map} +1 -1
- package/dist/{eval-campaign-C0Q1Eeo_.js → eval-campaign-CwsBn0Gm.js} +2 -2
- package/dist/{eval-campaign-C0Q1Eeo_.js.map → eval-campaign-CwsBn0Gm.js.map} +1 -1
- package/dist/experiment/index.d.ts +2 -1
- package/dist/experiment/index.d.ts.map +1 -1
- package/dist/experiment/index.js +1 -1
- package/dist/exporters-XMCYJlqT.d.ts +161 -0
- package/dist/exporters-XMCYJlqT.d.ts.map +1 -0
- package/dist/{external-optimizer-process-C3quNeKV.js → external-optimizer-process-BnSmrkgL.js} +3 -3
- package/dist/{external-optimizer-process-C3quNeKV.js.map → external-optimizer-process-BnSmrkgL.js.map} +1 -1
- package/dist/{external-optimizer-subprocess-BDwgKLO1.js → external-optimizer-subprocess-kbcPu5po.js} +3 -3
- package/dist/{external-optimizer-subprocess-BDwgKLO1.js.map → external-optimizer-subprocess-kbcPu5po.js.map} +1 -1
- package/dist/{feedback-trajectory-B3ZSkfAq.d.ts → feedback-trajectory-BYjh7NEM.d.ts} +2 -2
- package/dist/{feedback-trajectory-B3ZSkfAq.d.ts.map → feedback-trajectory-BYjh7NEM.d.ts.map} +1 -1
- package/dist/{index-CnJOPR_E.d.ts → index-CJv56rhX.d.ts} +673 -244
- package/dist/index-CJv56rhX.d.ts.map +1 -0
- package/dist/{index-Dk1dOUfm.d.ts → index-CWoIJHmy.d.ts} +4 -4
- package/dist/{index-Dk1dOUfm.d.ts.map → index-CWoIJHmy.d.ts.map} +1 -1
- package/dist/{index-Bcc9PkOz.d.ts → index-D4pC7Tvd.d.ts} +3 -2
- package/dist/index-D4pC7Tvd.d.ts.map +1 -0
- package/dist/{index-BqDJ6fUK.d.ts → index-I4UJGRno.d.ts} +74 -2
- package/dist/index-I4UJGRno.d.ts.map +1 -0
- package/dist/index.d.ts +66 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +375 -31
- package/dist/index.js.map +1 -1
- package/dist/{journal-CDP0Vhx_.js → journal-c3qY0SDP.js} +246 -5
- package/dist/journal-c3qY0SDP.js.map +1 -0
- package/dist/{kind-factory-BGwN0nE6.js → kind-factory-ByZeXDqF.js} +2 -2
- package/dist/{kind-factory-BGwN0nE6.js.map → kind-factory-ByZeXDqF.js.map} +1 -1
- package/dist/ledger-core/index.d.ts +2 -2
- package/dist/ledger-core/index.js +2 -2
- package/dist/{llm-client-DF9bPNYA.js → llm-client-KYQBh9C8.js} +2 -2
- package/dist/{llm-client-DF9bPNYA.js.map → llm-client-KYQBh9C8.js.map} +1 -1
- package/dist/{llm-judge-M_l_WXw2.js → llm-judge-BnxAv7ed.js} +1892 -1378
- package/dist/llm-judge-BnxAv7ed.js.map +1 -0
- package/dist/mint-C9Lojt1V.d.ts +147 -0
- package/dist/mint-C9Lojt1V.d.ts.map +1 -0
- package/dist/openapi.json +1 -1
- package/dist/pipelines/index.d.ts +1 -1
- package/dist/pipelines/index.js +1 -1
- package/dist/pipelines/index.js.map +1 -1
- package/dist/{produced-state-BOY98IJO.js → produced-state-BXjXJHJT.js} +4 -4
- package/dist/{produced-state-BOY98IJO.js.map → produced-state-BXjXJHJT.js.map} +1 -1
- package/dist/{raw-provider-sink-C96CcqeG.js → raw-provider-sink-DYh4Bszw.js} +2 -2
- package/dist/{raw-provider-sink-C96CcqeG.js.map → raw-provider-sink-DYh4Bszw.js.map} +1 -1
- package/dist/{redact-DxwlUAvh.js → redact-B7rt9Zot.js} +130 -10
- package/dist/redact-B7rt9Zot.js.map +1 -0
- package/dist/{redact-DPoH1-OW.d.ts → redact-CopuMxll.d.ts} +7 -5
- package/dist/redact-CopuMxll.d.ts.map +1 -0
- package/dist/rl.d.ts +3 -2
- package/dist/rl.d.ts.map +1 -1
- package/dist/rl.js +1 -1
- package/dist/rollout/index.d.ts +3 -2
- package/dist/search-ledger-types-C9anU2DX.d.ts +489 -0
- package/dist/search-ledger-types-C9anU2DX.d.ts.map +1 -0
- package/dist/{semantic-concept-judge-Cm2K2wAo.js → semantic-concept-judge-ClB73cei.js} +6 -6
- package/dist/{semantic-concept-judge-Cm2K2wAo.js.map → semantic-concept-judge-ClB73cei.js.map} +1 -1
- package/dist/{server-C8XvqCg8.js → server-DtetGGoA.js} +2 -2
- package/dist/{server-C8XvqCg8.js.map → server-DtetGGoA.js.map} +1 -1
- package/dist/{skillopt-optimization-method-BVSB676o.js → skillopt-optimization-method-l2vDBtKp.js} +363 -18
- package/dist/skillopt-optimization-method-l2vDBtKp.js.map +1 -0
- package/dist/{spans-W0o8pbEg.d.ts → spans-BWtjk_9c.d.ts} +2 -2
- package/dist/{spans-W0o8pbEg.d.ts.map → spans-BWtjk_9c.d.ts.map} +1 -1
- package/dist/{statistical-heldout-plAHG82H.d.ts → statistical-heldout-CLpjYffJ.d.ts} +2 -36
- package/dist/statistical-heldout-CLpjYffJ.d.ts.map +1 -0
- package/dist/{store-otlp-D0AV4cvB.js → store-otlp-84DH0OEH.js} +2 -2
- package/dist/{store-otlp-D0AV4cvB.js.map → store-otlp-84DH0OEH.js.map} +1 -1
- package/dist/{store-tool-spans-8PAyhRqb.d.ts → store-tool-spans-BBXs8hDP.d.ts} +2 -2
- package/dist/{store-tool-spans-8PAyhRqb.d.ts.map → store-tool-spans-BBXs8hDP.d.ts.map} +1 -1
- package/dist/{store-tool-spans-B_0mXvYl.js → store-tool-spans-DUiqyHvR.js} +5 -5
- package/dist/{store-tool-spans-B_0mXvYl.js.map → store-tool-spans-DUiqyHvR.js.map} +1 -1
- package/dist/traces.d.ts +3 -63
- package/dist/traces.d.ts.map +1 -1
- package/dist/traces.js +6 -195
- package/dist/traces.js.map +1 -1
- package/dist/wire/index.d.ts +1 -1
- package/dist/wire/index.js +1 -1
- package/docs/adapters-observability.md +31 -27
- package/docs/charter.md +1 -1
- package/docs/design/mlbenchmarks-book-review.md +1 -1
- package/docs/distributed-driver.md +1 -5
- package/docs/experiment.md +15 -0
- package/docs/public-api.md +181 -71
- package/docs/redaction.md +7 -1
- package/docs/search-ledger.md +174 -0
- package/docs/trace-contracts.md +48 -3
- package/package.json +1 -1
- package/dist/benchmark-command-BhhAYstU.js.map +0 -1
- package/dist/campaign-BWoSQPSU.js.map +0 -1
- package/dist/campaign-evidence-CBlGJ3kC.js.map +0 -1
- package/dist/define-agent-eval-q6890HU4.d.ts.map +0 -1
- package/dist/index-Bcc9PkOz.d.ts.map +0 -1
- package/dist/index-BqDJ6fUK.d.ts.map +0 -1
- package/dist/index-CnJOPR_E.d.ts.map +0 -1
- package/dist/journal-CDP0Vhx_.js.map +0 -1
- package/dist/llm-judge-M_l_WXw2.js.map +0 -1
- package/dist/mint-DsS38v5c.d.ts +0 -305
- package/dist/mint-DsS38v5c.d.ts.map +0 -1
- package/dist/redact-DPoH1-OW.d.ts.map +0 -1
- package/dist/redact-DxwlUAvh.js.map +0 -1
- package/dist/search-ledger-types-D7KDVg-o.d.ts +0 -302
- package/dist/search-ledger-types-D7KDVg-o.d.ts.map +0 -1
- package/dist/skillopt-optimization-method-BVSB676o.js.map +0 -1
- package/dist/statistical-heldout-plAHG82H.d.ts.map +0 -1
- package/docs/search-history-receipts.md +0 -233
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
2
|
import { i as JudgeError, o as NotFoundError, r as ConfigError, s as ValidationError, t as AgentEvalError } from "./errors-Dngq5h35.js";
|
|
3
3
|
import { a as hashCanonical, r as canonicalString } from "./canonical-BsZVgBhW.js";
|
|
4
|
-
import { a as CODING_HARNESSES, c as agentProfileId, d as harnessAxisOf, i as verifyCompletion, l as agentProfileModelId, n as completionVerdict, o as HARNESS_NATIVE_MODEL, r as createLlmCorrectnessChecker, s as agentProfileHash, t as extractProducedState, u as expandProfileAxes } from "./produced-state-
|
|
4
|
+
import { a as CODING_HARNESSES, c as agentProfileId, d as harnessAxisOf, i as verifyCompletion, l as agentProfileModelId, n as completionVerdict, o as HARNESS_NATIVE_MODEL, r as createLlmCorrectnessChecker, s as agentProfileHash, t as extractProducedState, u as expandProfileAxes } from "./produced-state-BXjXJHJT.js";
|
|
5
5
|
import { c as groupRunsByAgentProfileCell, d as validateAgentProfileCell, f as verifyAgentProfileCell, h as manifestContentDigest, i as agentProfileCellKey, m as hashJson, r as agentProfileCellHashMaterial, s as buildAgentProfileCell, t as AGENT_PROFILE_KINDS, u as toAgentProfileJson } from "./agent-profile-cell-DcFW4vOc.js";
|
|
6
6
|
import { t as mulberry32 } from "./random-Dn5fPWkt.js";
|
|
7
7
|
import { a as spearmanR, c as weightedMean, i as ranks, n as partialCredit, o as summarizeNumberSeries, r as pearsonR, s as weightedComposite, t as confidenceInterval } from "./descriptive-1V17A-qa.js";
|
|
@@ -21,40 +21,40 @@ import { r as observedSplitScore, t as isRealnessGated } from "./reward-nw2xZGZG
|
|
|
21
21
|
import { a as parseRunRecordSafe, c as runTaskScore, i as modelHasSnapshot, l as searchCellRunId, n as UNKNOWN_MODEL, o as roundTripRunRecord, r as isRunRecord, s as runCostFloorUsd, t as RunRecordValidationError, u as validateRunRecord } from "./run-record-BRlM99rX.js";
|
|
22
22
|
import { a as summaryTable, n as gainHistogram, r as paretoChart } from "./summary-report-BMvou2oZ.js";
|
|
23
23
|
import { n as contentHash, r as fileVerdictCache, t as canonicalJson } from "./verdict-cache-BQEsVy9n.js";
|
|
24
|
-
import {
|
|
24
|
+
import { $ as scoreRedTeamOutput, Q as redTeamReport, U as dominates, W as paretoFrontier, X as DEFAULT_RED_TEAM_CORPUS, Z as redTeamDataset, a as transientDispatchFailure, et as runCanaries, i as quotaExhaustedUntil, o as aggregateRunScore, ot as runCampaign, s as clamp01, t as llmJudge } from "./llm-judge-BnxAv7ed.js";
|
|
25
25
|
import { a as resolveModelPricing, i as isModelPriced, n as estimateCost, r as estimateTokens, t as MODEL_PRICING } from "./metrics-DOMpkoeb.js";
|
|
26
26
|
import { a as CostLedgerPersistenceError, c as costForTokenPricing, i as CostLedger, l as costForUsage, n as CostCallConflictError, o as CostReceiptCaptureError, r as CostCeilingReachedError, s as CostReservationExceededError, t as CostAccountingIncompleteError, u as modelPriceKey } from "./cost-ledger-_IbkejCF.js";
|
|
27
|
-
import { D as parseReflectionResponse, E as buildReflectionPrompt, G as
|
|
28
|
-
import { d as redactForShare, f as redactText, n as REDACTION_VERSION, p as shareAllowed, r as assessShareSafety, u as redact } from "./redact-
|
|
29
|
-
import { r as selfImprove, t as defineAgentEval } from "./define-agent-eval-
|
|
27
|
+
import { B as surfaceContentHash, D as parseReflectionResponse, E as buildReflectionPrompt, G as assertRealBackend, K as summarizeBackendIntegrity, W as BackendIntegrityError, c as ATTESTATION_ALGORITHM, l as attest, u as verifyAttestation } from "./campaign-evidence-D40-LHIL.js";
|
|
28
|
+
import { d as redactForShare, f as redactText, n as REDACTION_VERSION, p as shareAllowed, r as assessShareSafety, u as redact } from "./redact-B7rt9Zot.js";
|
|
29
|
+
import { r as selfImprove, t as defineAgentEval } from "./define-agent-eval-DPizhHfy.js";
|
|
30
30
|
import { n as equivalenceVerdict, t as certificationEvidenceDigest } from "./verdict-DhPoXApk.js";
|
|
31
31
|
import { t as TraceEmitter } from "./emitter-DbMXUIVP.js";
|
|
32
32
|
import { t as buildTrajectory } from "./trajectory-D_7rLrvE.js";
|
|
33
33
|
import { t as runCounterfactual } from "./counterfactual-DtMWvn7D.js";
|
|
34
|
-
import {
|
|
35
|
-
import { _ as completedAnalystReviewQuality, b as validateAnalystReviewDecisions, c as FAILURE_MODE_KIND_SPEC, g as assertUniqueFindingIds, h as analystRunDigest, i as DEFAULT_TRACE_ANALYST_KINDS, m as analystFindingDigest, n as AnalystRegistry, t as buildDefaultAnalystRegistry, v as readAnalystReview, y as snapshotAnalystRun } from "./default-registry-
|
|
34
|
+
import { G as DEFAULT_TRACE_ANALYST_OUTPUT_TOKENS, U as parseFindingSubject, h as TRACE_ANALYST_TRUNCATION_MARKER_PREFIX, m as DEFAULT_TRACE_ANALYST_BUDGETS, t as createTraceAnalyst, u as createBoundedTraceAnalysisStore } from "./kind-factory-ByZeXDqF.js";
|
|
35
|
+
import { _ as completedAnalystReviewQuality, b as validateAnalystReviewDecisions, c as FAILURE_MODE_KIND_SPEC, g as assertUniqueFindingIds, h as analystRunDigest, i as DEFAULT_TRACE_ANALYST_KINDS, m as analystFindingDigest, n as AnalystRegistry, t as buildDefaultAnalystRegistry, v as readAnalystReview, y as snapshotAnalystRun } from "./default-registry-16I-Bxlf.js";
|
|
36
36
|
import { OUTPUT_VALUE } from "./trace-attributes.js";
|
|
37
37
|
import { r as extractUsageFromSse, t as extractUsage } from "./extract-usage-BrQ8mCLX.js";
|
|
38
|
-
import { n as InMemoryRawProviderSink, r as NoopRawProviderSink, t as FileSystemRawProviderSink } from "./raw-provider-sink-
|
|
39
|
-
import {
|
|
38
|
+
import { n as InMemoryRawProviderSink, r as NoopRawProviderSink, t as FileSystemRawProviderSink } from "./raw-provider-sink-DYh4Bszw.js";
|
|
39
|
+
import { S as exportRunAsOtlp, T as captureFetchToRawSink, _ as tokenizeDomainWords, d as inferDomainKeywords, g as scoreTraceInsightReadiness, l as describeTraceInsightScope, n as toolSpansToTraceAnalysisStore, o as buildTraceInsightContext, s as buildTraceInsightPrompt, u as domainEvidencePattern, v as analyzeTraces } from "./store-tool-spans-DUiqyHvR.js";
|
|
40
40
|
import { n as assertRunCaptured, t as RunIntegrityError } from "./integrity-BFP4kjGj.js";
|
|
41
41
|
import { n as InMemoryTraceStore, t as FileSystemTraceStore } from "./store-DNe_Uv1Q.js";
|
|
42
42
|
import { t as packageVersion$1 } from "./package-version-D7lQHt_-.js";
|
|
43
43
|
import { a as classifyFailure, i as INFRA_FAILURE_BLAMES, o as classifyFailureReason, r as FAILURE_BLAME, s as failureBlame, t as DEFAULT_FAILURE_REASON_RULES } from "./failure-taxonomy-CjabTATj.js";
|
|
44
44
|
import { n as runBoundedProcess, t as DEFAULT_BOUNDED_PROCESS_TIMEOUT_MS } from "./bounded-process-BBZob7vl.js";
|
|
45
|
-
import { t as runEvalCampaign } from "./eval-campaign-
|
|
45
|
+
import { t as runEvalCampaign } from "./eval-campaign-CwsBn0Gm.js";
|
|
46
46
|
import "./rollout-B-XBReTC.js";
|
|
47
47
|
import { t as mintRolloutRows } from "./mint-iCD-W43s.js";
|
|
48
48
|
import { n as pairedEvalueSequence, t as evaluateInterimReleaseConfidence } from "./sequential-CzK5DarL.js";
|
|
49
|
-
import { S as judgeFamily, _ as assertServedModels, b as CrossFamilyError, d as stripFencedJson, f as ModelSubstitutionError, h as assertServedModel, i as backoffMs, l as isTransientLlmError, m as assertCrossFamilyServed, o as costReceiptFromLlm, p as ServedCrossFamilyError, r as LlmResponseError, s as costReceiptFromLlmError, t as LlmCallError, u as maximumChargeForLlmRequest, v as checkServedModel, x as assertCrossFamily, y as servedModelAcceptable } from "./llm-client-
|
|
50
|
-
import { n as paidJsonChat } from "./chat-json-call-
|
|
49
|
+
import { S as judgeFamily, _ as assertServedModels, b as CrossFamilyError, d as stripFencedJson, f as ModelSubstitutionError, h as assertServedModel, i as backoffMs, l as isTransientLlmError, m as assertCrossFamilyServed, o as costReceiptFromLlm, p as ServedCrossFamilyError, r as LlmResponseError, s as costReceiptFromLlmError, t as LlmCallError, u as maximumChargeForLlmRequest, v as checkServedModel, x as assertCrossFamily, y as servedModelAcceptable } from "./llm-client-KYQBh9C8.js";
|
|
50
|
+
import { n as paidJsonChat } from "./chat-json-call-CXHBvUz8.js";
|
|
51
51
|
import { i as makeProposalFinding, n as computeFindingId, r as makeFinding } from "./types-CW5b90JH.js";
|
|
52
|
-
import { t as createDspyRlmTraceEngine } from "./dspy-rlm-engine-
|
|
53
|
-
import { a as diffFindings, n as runSemanticConceptJudge, o as createChatTraceEngine, r as FindingsStore, t as SEMANTIC_CONCEPT_JUDGE_VERSION } from "./semantic-concept-judge-
|
|
52
|
+
import { t as createDspyRlmTraceEngine } from "./dspy-rlm-engine-Cs5LzJ-f.js";
|
|
53
|
+
import { a as diffFindings, n as runSemanticConceptJudge, o as createChatTraceEngine, r as FindingsStore, t as SEMANTIC_CONCEPT_JUDGE_VERSION } from "./semantic-concept-judge-ClB73cei.js";
|
|
54
54
|
import { t as analyzeSeries } from "./series-convergence-CjO2QdRW.js";
|
|
55
|
-
import { i as otlpTextToTraceAnalysisStore, n as OtlpFileTraceStore } from "./store-otlp-
|
|
55
|
+
import { i as otlpTextToTraceAnalysisStore, n as OtlpFileTraceStore } from "./store-otlp-84DH0OEH.js";
|
|
56
56
|
import { n as evaluateReleaseConfidence, r as bootstrapCi } from "./release-confidence-XQm_xjhR.js";
|
|
57
|
-
import { t as createChatClient } from "./chat-client-
|
|
57
|
+
import { t as createChatClient } from "./chat-client-B_ItsSBw.js";
|
|
58
58
|
import { createHash } from "node:crypto";
|
|
59
59
|
import { accessSync, appendFileSync, constants, cpSync, existsSync, mkdirSync, promises, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
60
60
|
import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -1206,7 +1206,7 @@ function contractSpanKind(span) {
|
|
|
1206
1206
|
/**
|
|
1207
1207
|
* Tool name: `span.toolName` (typed ToolSpan) → the tool-name attributes
|
|
1208
1208
|
* (`gen_ai.tool.name`, `tool.name`, ..., `toolName`) → `span.name` when the
|
|
1209
|
-
* span is a TOOL span
|
|
1209
|
+
* span is a TOOL span.
|
|
1210
1210
|
*/
|
|
1211
1211
|
function contractSpanToolName(span) {
|
|
1212
1212
|
if (typeof span.toolName === "string") return span.toolName;
|
|
@@ -1219,12 +1219,23 @@ function spanModel(span) {
|
|
|
1219
1219
|
if (typeof span.model === "string" && span.model.length > 0) return span.model;
|
|
1220
1220
|
return firstStringAttr(span.attributes ?? {}, MODEL_ATTR_KEYS);
|
|
1221
1221
|
}
|
|
1222
|
-
/**
|
|
1222
|
+
/**
|
|
1223
|
+
* Input plus output tokens, or `undefined` when either side is unrecorded.
|
|
1224
|
+
* Input follows the OTel GenAI definition — prompt tokens plus any cache
|
|
1225
|
+
* read and cache write tokens the span records — because a cached call's
|
|
1226
|
+
* `INPUT_TOKEN_ATTR_KEYS` value (`llm.token_count.prompt` and its aliases)
|
|
1227
|
+
* carries only the uncached remainder, and a cache-heavy call can spend far
|
|
1228
|
+
* more on cache reads and writes than on that remainder. `llm.maxTotalTokens`
|
|
1229
|
+
* would otherwise pass runs at a small fraction of what they actually cost.
|
|
1230
|
+
*/
|
|
1223
1231
|
function spanTotalTokens(span) {
|
|
1224
1232
|
const attributes = span.attributes ?? {};
|
|
1225
1233
|
const input = finite(span.inputTokens) ?? firstNumberAttr(attributes, INPUT_TOKEN_ATTR_KEYS);
|
|
1226
1234
|
const output = finite(span.outputTokens) ?? firstNumberAttr(attributes, OUTPUT_TOKEN_ATTR_KEYS);
|
|
1227
|
-
|
|
1235
|
+
if (input === void 0 || output === void 0) return void 0;
|
|
1236
|
+
const cacheRead = firstNumberAttr(attributes, ["llm.token_count.prompt_cache_hit"]) ?? 0;
|
|
1237
|
+
const cacheWrite = firstNumberAttr(attributes, ["llm.token_count.prompt_cache_write"]) ?? 0;
|
|
1238
|
+
return input + output + cacheRead + cacheWrite;
|
|
1228
1239
|
}
|
|
1229
1240
|
const ARGUMENT_ATTR_KEYS = [
|
|
1230
1241
|
"gen_ai.tool.call.arguments",
|
|
@@ -1254,6 +1265,40 @@ function spanArguments(span) {
|
|
|
1254
1265
|
reason: "the span carries no argument evidence"
|
|
1255
1266
|
};
|
|
1256
1267
|
}
|
|
1268
|
+
/** The OTel GenAI attribute listing the tool definitions offered to the
|
|
1269
|
+
* model: an array (or its JSON string) of `{ name }`, `{ function: { name } }`
|
|
1270
|
+
* or plain names. */
|
|
1271
|
+
const TOOL_DEFINITIONS_ATTR = "gen_ai.tool.definitions";
|
|
1272
|
+
/** The tool names a span records as offered to the model. */
|
|
1273
|
+
function spanOfferedTools(span) {
|
|
1274
|
+
const raw = span.attributes?.[TOOL_DEFINITIONS_ATTR];
|
|
1275
|
+
if (raw === void 0 || raw === null) return { recorded: false };
|
|
1276
|
+
const value = parseJsonString(raw);
|
|
1277
|
+
if (!Array.isArray(value)) return {
|
|
1278
|
+
recorded: true,
|
|
1279
|
+
unreadable: `${TOOL_DEFINITIONS_ATTR} is not a list`
|
|
1280
|
+
};
|
|
1281
|
+
const names = [];
|
|
1282
|
+
for (const entry of value) {
|
|
1283
|
+
const name = typeof entry === "string" ? entry : definitionName(entry);
|
|
1284
|
+
if (name === void 0) return {
|
|
1285
|
+
recorded: true,
|
|
1286
|
+
unreadable: `${TOOL_DEFINITIONS_ATTR} has an entry without a tool name`
|
|
1287
|
+
};
|
|
1288
|
+
names.push(name);
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
recorded: true,
|
|
1292
|
+
names
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function definitionName(entry) {
|
|
1296
|
+
if (entry === null || typeof entry !== "object") return void 0;
|
|
1297
|
+
const record = entry;
|
|
1298
|
+
if (typeof record.name === "string" && record.name.length > 0) return record.name;
|
|
1299
|
+
const fn = record.function;
|
|
1300
|
+
return fn !== null && typeof fn === "object" && typeof fn.name === "string" && fn.name.length > 0 ? fn.name : void 0;
|
|
1301
|
+
}
|
|
1257
1302
|
function parseJsonString(value) {
|
|
1258
1303
|
if (typeof value !== "string") return value;
|
|
1259
1304
|
try {
|
|
@@ -1402,6 +1447,14 @@ function assertJsonPointer(pointer, where) {
|
|
|
1402
1447
|
if (typeof pointer !== "string" || pointer !== "" && !pointer.startsWith("/")) throw new ValidationError(`${where}: pointer must be an RFC 6901 JSON Pointer ('' or '/a/b')`);
|
|
1403
1448
|
if (/~(?![01])/.test(pointer)) throw new ValidationError(`${where}: pointer has an invalid escape; use ~0 for ~ and ~1 for /`);
|
|
1404
1449
|
}
|
|
1450
|
+
/** A non-empty list of distinct, non-empty tool names. */
|
|
1451
|
+
function assertNames(names, where) {
|
|
1452
|
+
if (!Array.isArray(names) || names.length === 0) throw new ValidationError(`${where} must be a non-empty array of tool names`);
|
|
1453
|
+
for (const name of names) if (typeof name !== "string" || name.length === 0) throw new ValidationError(`${where} must hold non-empty tool names`);
|
|
1454
|
+
const dup = names.find((n, i) => names.indexOf(n) !== i);
|
|
1455
|
+
if (dup !== void 0) throw new ValidationError(`${where} lists "${dup}" twice`);
|
|
1456
|
+
return names;
|
|
1457
|
+
}
|
|
1405
1458
|
function assertArgumentCheck(check, where) {
|
|
1406
1459
|
const c = check;
|
|
1407
1460
|
if (c === null || typeof c !== "object") throw new ValidationError(`${where}: check must be an object`);
|
|
@@ -1442,7 +1495,7 @@ function assertRule(rule, where) {
|
|
|
1442
1495
|
assertCount(rule.max, where);
|
|
1443
1496
|
return;
|
|
1444
1497
|
case "run":
|
|
1445
|
-
if (rule.requireCompleted
|
|
1498
|
+
if (!rule.requireCompleted && rule.allowedStatuses === void 0 && rule.maxDurationMs === void 0) throw new ValidationError(`${where}: run rule checks nothing`);
|
|
1446
1499
|
if (rule.requireCompleted !== void 0 && typeof rule.requireCompleted !== "boolean") throw new ValidationError(`${where}: requireCompleted must be a boolean`);
|
|
1447
1500
|
if (rule.allowedStatuses !== void 0) {
|
|
1448
1501
|
if (!Array.isArray(rule.allowedStatuses) || rule.allowedStatuses.length === 0) throw new ValidationError(`${where}: allowedStatuses must be a non-empty array`);
|
|
@@ -1456,6 +1509,25 @@ function assertRule(rule, where) {
|
|
|
1456
1509
|
assertArgumentCheck(rule.check, where);
|
|
1457
1510
|
if (rule.occurrence !== void 0 && !ARGUMENT_OCCURRENCES.includes(rule.occurrence)) throw new ValidationError(`${where}: occurrence must be one of ${ARGUMENT_OCCURRENCES.join(", ")}`);
|
|
1458
1511
|
return;
|
|
1512
|
+
case "toolsOffered":
|
|
1513
|
+
if (rule.declared !== void 0) assertNames(rule.declared, `${where}: declared`);
|
|
1514
|
+
return;
|
|
1515
|
+
case "retrySafe": {
|
|
1516
|
+
const reads = rule.reads === void 0 ? [] : assertNames(rule.reads, `${where}: reads`);
|
|
1517
|
+
if (rule.writes === void 0 && rule.reads === void 0) throw new ValidationError(`${where}: retrySafe needs reads or writes`);
|
|
1518
|
+
if (rule.writes !== void 0) {
|
|
1519
|
+
if (!Array.isArray(rule.writes) || rule.writes.length === 0) throw new ValidationError(`${where}: writes must be a non-empty array`);
|
|
1520
|
+
const writes = assertNames(rule.writes.map((w) => w?.tool), `${where}: writes[].tool`);
|
|
1521
|
+
for (const [i, w] of rule.writes.entries()) {
|
|
1522
|
+
if (w.idempotencyKey === void 0) continue;
|
|
1523
|
+
assertJsonPointer(w.idempotencyKey, `${where}: writes[${i}].idempotencyKey`);
|
|
1524
|
+
if (w.idempotencyKey === "") throw new ValidationError(`${where}: writes[${i}].idempotencyKey must name a member, not the whole arguments`);
|
|
1525
|
+
}
|
|
1526
|
+
const both = writes.find((t) => reads.includes(t));
|
|
1527
|
+
if (both !== void 0) throw new ValidationError(`${where}: "${both}" is both a read and a write`);
|
|
1528
|
+
}
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1459
1531
|
default: throw new ValidationError(`${where}: unknown rule kind "${String(rule.kind)}"`);
|
|
1460
1532
|
}
|
|
1461
1533
|
}
|
|
@@ -1525,12 +1597,19 @@ function describeRule(rule) {
|
|
|
1525
1597
|
case "tokensAtMost": return `spans matching ${describePredicate(rule.p)} use at most ${rule.max} tokens in total`;
|
|
1526
1598
|
case "run": {
|
|
1527
1599
|
const parts = [];
|
|
1528
|
-
if (rule.requireCompleted) parts.push("the run
|
|
1600
|
+
if (rule.requireCompleted) parts.push("the run completed and recorded its end");
|
|
1529
1601
|
if (rule.allowedStatuses) parts.push(`its status is one of ${rule.allowedStatuses.join(", ")}`);
|
|
1530
1602
|
if (rule.maxDurationMs !== void 0) parts.push(`it took at most ${rule.maxDurationMs} ms`);
|
|
1531
1603
|
return parts.join("; ");
|
|
1532
1604
|
}
|
|
1533
1605
|
case "argument": return `${rule.occurrence ?? "all"} call(s) matching ${describePredicate(rule.p)} have argument ${rule.pointer || "(whole value)"} that ${describeCheck(rule.check)}`;
|
|
1606
|
+
case "toolsOffered": return `the trace records the tools offered to the model, every tool call is one of them${rule.declared ? `, and every offered tool is one of ${rule.declared.join(", ")}` : ""}`;
|
|
1607
|
+
case "retrySafe": {
|
|
1608
|
+
const parts = ["no tool is called twice with the same arguments"];
|
|
1609
|
+
if (rule.reads) parts.push(`except the reads ${rule.reads.join(", ")}`);
|
|
1610
|
+
for (const w of rule.writes ?? []) parts.push(w.idempotencyKey === void 0 ? `write ${w.tool} may never repeat` : `write ${w.tool} may repeat only with one idempotency key at ${w.idempotencyKey}`);
|
|
1611
|
+
return parts.join("; ");
|
|
1612
|
+
}
|
|
1534
1613
|
}
|
|
1535
1614
|
}
|
|
1536
1615
|
/** Plain-language statement of everything a contract checks, one rule per line. */
|
|
@@ -1660,6 +1739,28 @@ var TraceContractBuilder = class TraceContractBuilder {
|
|
|
1660
1739
|
assertRule(rule, this.where("argument"));
|
|
1661
1740
|
return this.add(rule);
|
|
1662
1741
|
}
|
|
1742
|
+
/** The trace records the tools offered to the model, every call is one of
|
|
1743
|
+
* them, and, with `declared`, every offered tool is declared. */
|
|
1744
|
+
toolsOffered(declared, label = "toolsOffered") {
|
|
1745
|
+
const rule = {
|
|
1746
|
+
kind: "toolsOffered",
|
|
1747
|
+
label,
|
|
1748
|
+
...declared === void 0 ? {} : { declared: [...declared] }
|
|
1749
|
+
};
|
|
1750
|
+
assertRule(rule, this.where("toolsOffered"));
|
|
1751
|
+
return this.add(rule);
|
|
1752
|
+
}
|
|
1753
|
+
/** No call repeats a side effect that is not proven safe to repeat. */
|
|
1754
|
+
retrySafe(options, label = "retrySafe") {
|
|
1755
|
+
const rule = {
|
|
1756
|
+
kind: "retrySafe",
|
|
1757
|
+
label,
|
|
1758
|
+
...options.reads === void 0 ? {} : { reads: [...options.reads] },
|
|
1759
|
+
...options.writes === void 0 ? {} : { writes: options.writes.map((w) => ({ ...w })) }
|
|
1760
|
+
};
|
|
1761
|
+
assertRule(rule, this.where("retrySafe"));
|
|
1762
|
+
return this.add(rule);
|
|
1763
|
+
}
|
|
1663
1764
|
/** Evaluate only the subtree rooted at the one span matching `root`. */
|
|
1664
1765
|
scope(root) {
|
|
1665
1766
|
this.scopeRoot = normalizePredicate(root, this.where("scope"));
|
|
@@ -1868,9 +1969,9 @@ function checkRun(rule, context) {
|
|
|
1868
1969
|
}];
|
|
1869
1970
|
const run = context.run;
|
|
1870
1971
|
const out = [];
|
|
1871
|
-
if (rule.requireCompleted && (run.status
|
|
1972
|
+
if (rule.requireCompleted && (run.status !== "completed" || finite(run.endedAt) === void 0)) out.push({
|
|
1872
1973
|
rule: rule.label,
|
|
1873
|
-
detail:
|
|
1974
|
+
detail: run.status === "completed" ? "run status is completed but no end time was recorded" : `run status is ${run.status ?? "unknown"}, not completed`
|
|
1874
1975
|
});
|
|
1875
1976
|
if (rule.allowedStatuses && !rule.allowedStatuses.includes(run.status ?? "")) out.push({
|
|
1876
1977
|
rule: rule.label,
|
|
@@ -1890,6 +1991,188 @@ function checkRun(rule, context) {
|
|
|
1890
1991
|
}
|
|
1891
1992
|
return out;
|
|
1892
1993
|
}
|
|
1994
|
+
const listed = (names, max = 6) => names.length <= max ? names.join(", ") : `${names.slice(0, max).join(", ")} and ${names.length - max} more`;
|
|
1995
|
+
function toolSpans(spans) {
|
|
1996
|
+
const out = [];
|
|
1997
|
+
spans.forEach((span, i) => {
|
|
1998
|
+
if (contractSpanKind(span) === "TOOL") out.push({
|
|
1999
|
+
span,
|
|
2000
|
+
ref: spanRef(span, i)
|
|
2001
|
+
});
|
|
2002
|
+
});
|
|
2003
|
+
return out;
|
|
2004
|
+
}
|
|
2005
|
+
/** Declared, offered and called tools agree. */
|
|
2006
|
+
function checkToolsOffered(rule, spans) {
|
|
2007
|
+
const out = [];
|
|
2008
|
+
const offered = /* @__PURE__ */ new Set();
|
|
2009
|
+
let recorded = 0;
|
|
2010
|
+
const declared = rule.declared ? new Set(rule.declared) : void 0;
|
|
2011
|
+
spans.forEach((span, i) => {
|
|
2012
|
+
const tools = spanOfferedTools(span);
|
|
2013
|
+
if (!tools.recorded) return;
|
|
2014
|
+
recorded += 1;
|
|
2015
|
+
const ref = spanRef(span, i);
|
|
2016
|
+
if ("unreadable" in tools) {
|
|
2017
|
+
out.push({
|
|
2018
|
+
rule: rule.label,
|
|
2019
|
+
spanId: span.spanId,
|
|
2020
|
+
detail: `span ${ref}: ${tools.unreadable}`
|
|
2021
|
+
});
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
for (const name of tools.names) offered.add(name);
|
|
2025
|
+
const undeclared = declared ? [...new Set(tools.names)].filter((n) => !declared.has(n)) : [];
|
|
2026
|
+
if (undeclared.length > 0) out.push({
|
|
2027
|
+
rule: rule.label,
|
|
2028
|
+
spanId: span.spanId,
|
|
2029
|
+
detail: `span ${ref} offered the model ${undeclared.length} undeclared tool(s): ${listed(undeclared)}`
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
if (recorded === 0) return [{
|
|
2033
|
+
rule: rule.label,
|
|
2034
|
+
detail: `no span records the tools offered to the model (${TOOL_DEFINITIONS_ATTR}), so what the harness enforced is unknown`
|
|
2035
|
+
}];
|
|
2036
|
+
for (const { span, ref } of toolSpans(spans)) {
|
|
2037
|
+
const name = contractSpanToolName(span);
|
|
2038
|
+
if (name === void 0) out.push({
|
|
2039
|
+
rule: rule.label,
|
|
2040
|
+
spanId: span.spanId,
|
|
2041
|
+
detail: `span ${ref} has no tool name, so it cannot be matched to an offered tool`
|
|
2042
|
+
});
|
|
2043
|
+
else if (!offered.has(name)) out.push({
|
|
2044
|
+
rule: rule.label,
|
|
2045
|
+
spanId: span.spanId,
|
|
2046
|
+
detail: `span ${ref} called ${name}, which was never offered: the harness did not enforce its tool set`
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
return out;
|
|
2050
|
+
}
|
|
2051
|
+
/** The call's idempotency key, or why it has none. */
|
|
2052
|
+
function idempotencyKey(span, pointer) {
|
|
2053
|
+
const evidence = spanArguments(span);
|
|
2054
|
+
if (!evidence.known) return { missing: evidence.reason };
|
|
2055
|
+
const resolved = resolveJsonPointer(evidence.value, pointer);
|
|
2056
|
+
const value = resolved.value;
|
|
2057
|
+
if (resolved.found && typeof value === "string" && value.length > 0) return { key: value };
|
|
2058
|
+
if (resolved.found && typeof value === "number" && Number.isFinite(value)) return { key: String(value) };
|
|
2059
|
+
return { missing: `no idempotency key at ${pointer}` };
|
|
2060
|
+
}
|
|
2061
|
+
/** No call repeats a side effect that is not proven safe to repeat. */
|
|
2062
|
+
function checkRetrySafe(rule, spans) {
|
|
2063
|
+
const reads = new Set(rule.reads ?? []);
|
|
2064
|
+
const writes = new Map((rule.writes ?? []).map((w) => [w.tool, w]));
|
|
2065
|
+
const out = [];
|
|
2066
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2067
|
+
const unseen = /* @__PURE__ */ new Map();
|
|
2068
|
+
for (const call of toolSpans(spans)) {
|
|
2069
|
+
const name = contractSpanToolName(call.span);
|
|
2070
|
+
if (name === void 0) {
|
|
2071
|
+
out.push({
|
|
2072
|
+
rule: rule.label,
|
|
2073
|
+
spanId: call.span.spanId,
|
|
2074
|
+
detail: `span ${call.ref} has no tool name, so whether it repeats a call is unknown`
|
|
2075
|
+
});
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
2078
|
+
if (reads.has(name)) continue;
|
|
2079
|
+
const args = spanArguments(call.span);
|
|
2080
|
+
if (!args.known) {
|
|
2081
|
+
const list = unseen.get(name) ?? [];
|
|
2082
|
+
list.push(call);
|
|
2083
|
+
unseen.set(name, list);
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
const pointer = writes.get(name)?.idempotencyKey;
|
|
2087
|
+
const key = `${name}\u0000${canonicalString(pointer === void 0 ? args.value : withoutMember(args.value, pointer))}`;
|
|
2088
|
+
const list = groups.get(key) ?? [];
|
|
2089
|
+
list.push(call);
|
|
2090
|
+
groups.set(key, list);
|
|
2091
|
+
}
|
|
2092
|
+
for (const [name, calls] of unseen) {
|
|
2093
|
+
const total = calls.length + [...groups].reduce((n, [k, list]) => k.startsWith(`${name}\u0000`) ? n + list.length : n, 0);
|
|
2094
|
+
if (total > 1) out.push({
|
|
2095
|
+
rule: rule.label,
|
|
2096
|
+
spanId: calls[0].span.spanId,
|
|
2097
|
+
detail: `${name} was called ${total} times and span ${calls[0].ref} has no captured arguments, so whether a call repeats another is unknown`
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
for (const [key, calls] of groups) {
|
|
2101
|
+
if (calls.length < 2) continue;
|
|
2102
|
+
const name = key.slice(0, key.indexOf("\0"));
|
|
2103
|
+
const refs = listed(calls.map((c) => c.ref));
|
|
2104
|
+
const write = writes.get(name);
|
|
2105
|
+
if (!write) {
|
|
2106
|
+
out.push({
|
|
2107
|
+
rule: rule.label,
|
|
2108
|
+
spanId: calls[1].span.spanId,
|
|
2109
|
+
detail: `${name} was called ${calls.length} times with the same arguments (spans ${refs}) and its side effect is not declared, so the repeat is not proven safe`
|
|
2110
|
+
});
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
2113
|
+
const pointer = write.idempotencyKey;
|
|
2114
|
+
if (pointer === void 0) {
|
|
2115
|
+
out.push({
|
|
2116
|
+
rule: rule.label,
|
|
2117
|
+
spanId: calls[1].span.spanId,
|
|
2118
|
+
detail: `write ${name} was called ${calls.length} times with the same arguments (spans ${refs}) and declares no idempotency key, so it may have applied more than once`
|
|
2119
|
+
});
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
const keyed = calls.map((c) => ({
|
|
2123
|
+
c,
|
|
2124
|
+
k: idempotencyKey(c.span, pointer)
|
|
2125
|
+
}));
|
|
2126
|
+
const missing = keyed.find((x) => "missing" in x.k);
|
|
2127
|
+
const keys = new Set(keyed.flatMap((x) => "key" in x.k ? [x.k.key] : []));
|
|
2128
|
+
if (missing) out.push({
|
|
2129
|
+
rule: rule.label,
|
|
2130
|
+
spanId: missing.c.span.spanId,
|
|
2131
|
+
detail: `write ${name} was called ${calls.length} times with the same arguments (spans ${refs}); span ${missing.c.ref} has ${missing.k.missing}, so it may have applied more than once`
|
|
2132
|
+
});
|
|
2133
|
+
else if (keys.size > 1) out.push({
|
|
2134
|
+
rule: rule.label,
|
|
2135
|
+
spanId: calls[1].span.spanId,
|
|
2136
|
+
detail: `write ${name} was called ${calls.length} times with the same arguments (spans ${refs}) under ${keys.size} different idempotency keys, so the target cannot tell they are one operation`
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
const flagged = new Set(out.map((v) => v.spanId));
|
|
2140
|
+
for (const call of toolSpans(spans)) {
|
|
2141
|
+
const name = contractSpanToolName(call.span);
|
|
2142
|
+
if (name === void 0 || flagged.has(call.span.spanId)) continue;
|
|
2143
|
+
const write = writes.get(name);
|
|
2144
|
+
if (!write || !isSpanError(call.span)) continue;
|
|
2145
|
+
const pointer = write.idempotencyKey;
|
|
2146
|
+
if (!(pointer !== void 0 && "key" in idempotencyKey(call.span, pointer))) {
|
|
2147
|
+
out.push({
|
|
2148
|
+
rule: rule.label,
|
|
2149
|
+
spanId: call.span.spanId,
|
|
2150
|
+
detail: `write ${name} (span ${call.ref}) errored and ${pointer === void 0 ? "declares no idempotency key" : `has ${idempotencyKey(call.span, pointer).missing}`}; its outcome is unknown, so a retry cannot be proven safe`
|
|
2151
|
+
});
|
|
2152
|
+
flagged.add(call.span.spanId);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return out;
|
|
2156
|
+
}
|
|
2157
|
+
/** A copy of `value` without the member `pointer` names (RFC 6901). */
|
|
2158
|
+
function withoutMember(value, pointer) {
|
|
2159
|
+
const tokens = pointer.slice(1).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
2160
|
+
const strip = (node, depth) => {
|
|
2161
|
+
const token = tokens[depth];
|
|
2162
|
+
const last = depth === tokens.length - 1;
|
|
2163
|
+
if (Array.isArray(node)) {
|
|
2164
|
+
const index = /^(?:0|[1-9]\d*)$/.test(token) ? Number(token) : -1;
|
|
2165
|
+
if (index < 0 || index >= node.length) return node;
|
|
2166
|
+
return last ? node.filter((_, i) => i !== index) : node.map((v, i) => i === index ? strip(v, depth + 1) : v);
|
|
2167
|
+
}
|
|
2168
|
+
if (node === null || typeof node !== "object" || !Object.hasOwn(node, token)) return node;
|
|
2169
|
+
const copy = { ...node };
|
|
2170
|
+
if (last) delete copy[token];
|
|
2171
|
+
else copy[token] = strip(copy[token], depth + 1);
|
|
2172
|
+
return copy;
|
|
2173
|
+
};
|
|
2174
|
+
return strip(value, 0);
|
|
2175
|
+
}
|
|
1893
2176
|
function checkRule(rule, ctx) {
|
|
1894
2177
|
const { spans } = ctx;
|
|
1895
2178
|
switch (rule.kind) {
|
|
@@ -1947,6 +2230,8 @@ function checkRule(rule, ctx) {
|
|
|
1947
2230
|
}
|
|
1948
2231
|
case "run": return checkRun(rule, ctx.run);
|
|
1949
2232
|
case "argument": return checkArgument(rule, spans);
|
|
2233
|
+
case "toolsOffered": return checkToolsOffered(rule, spans);
|
|
2234
|
+
case "retrySafe": return checkRetrySafe(rule, spans);
|
|
1950
2235
|
}
|
|
1951
2236
|
}
|
|
1952
2237
|
function executeRule(rule, ctx) {
|
|
@@ -1969,12 +2254,16 @@ function executeRule(rule, ctx) {
|
|
|
1969
2254
|
/**
|
|
1970
2255
|
* A run derived from one span: the `run.status` attribute a run-anchor span
|
|
1971
2256
|
* carries (store-to-otlp writes it), else failed on an error status, else
|
|
1972
|
-
*
|
|
2257
|
+
* unknown. An ended span with an OK status and no declared `run.status` is
|
|
2258
|
+
* NOT inferred as completed — a span exporter can end its root span cleanly
|
|
2259
|
+
* without ever learning whether the underlying run actually finished (for
|
|
2260
|
+
* example, a trace reader that saw no terminal record). `requireCompleted`
|
|
2261
|
+
* and `allowedStatuses` must fail closed on that gap rather than pass it.
|
|
1973
2262
|
*/
|
|
1974
2263
|
function runOfSpan(span) {
|
|
1975
2264
|
const declared = span.attributes?.["run.status"];
|
|
1976
2265
|
return {
|
|
1977
|
-
status: typeof declared === "string" && RUN_STATUSES.includes(declared) ? declared : isSpanError(span) ? "failed" :
|
|
2266
|
+
status: typeof declared === "string" && RUN_STATUSES.includes(declared) ? declared : isSpanError(span) ? "failed" : void 0,
|
|
1978
2267
|
...finite(span.startedAt) === void 0 ? {} : { startedAt: span.startedAt },
|
|
1979
2268
|
...finite(span.endedAt) === void 0 ? {} : { endedAt: span.endedAt }
|
|
1980
2269
|
};
|
|
@@ -2008,8 +2297,8 @@ function statusOf(outcomes) {
|
|
|
2008
2297
|
}
|
|
2009
2298
|
/**
|
|
2010
2299
|
* Evaluate one contract over a span sequence. Pure and synchronous — works on
|
|
2011
|
-
* `Span[]` from a TraceStore, `
|
|
2012
|
-
*
|
|
2300
|
+
* `Span[]` from a TraceStore, `contractSpansFromOtlp` output, or any array
|
|
2301
|
+
* satisfying `ContractSpan`. Throws
|
|
2013
2302
|
* `ValidationError` for a malformed contract; a rule that cannot be evaluated
|
|
2014
2303
|
* is reported as `error` in the verdict instead.
|
|
2015
2304
|
*/
|
|
@@ -2108,7 +2397,9 @@ function rulePredicates(rule) {
|
|
|
2108
2397
|
switch (rule.kind) {
|
|
2109
2398
|
case "precedes": return [rule.a, rule.b];
|
|
2110
2399
|
case "neverUnless": return [rule.p, rule.prior];
|
|
2111
|
-
case "run":
|
|
2400
|
+
case "run":
|
|
2401
|
+
case "toolsOffered":
|
|
2402
|
+
case "retrySafe": return [];
|
|
2112
2403
|
default: return [rule.p];
|
|
2113
2404
|
}
|
|
2114
2405
|
}
|
|
@@ -2145,7 +2436,12 @@ function checkTraceContracts(spans, contracts, options = {}) {
|
|
|
2145
2436
|
* "allowed": ["lookup_order", "issue_refund", "send_email"],
|
|
2146
2437
|
* "maxCalls": 20,
|
|
2147
2438
|
* "requiredOrder": ["lookup_order", "issue_refund"],
|
|
2148
|
-
* "arguments": [{ "tool": "issue_refund", "pointer": "/amount", "type": "number" }]
|
|
2439
|
+
* "arguments": [{ "tool": "issue_refund", "pointer": "/amount", "type": "number" }],
|
|
2440
|
+
* "enforced": true
|
|
2441
|
+
* },
|
|
2442
|
+
* "retries": {
|
|
2443
|
+
* "reads": ["lookup_order"],
|
|
2444
|
+
* "writes": [{ "tool": "issue_refund", "idempotencyKey": "/idempotency_key" }]
|
|
2149
2445
|
* },
|
|
2150
2446
|
* "llm": { "maxCalls": 10, "maxTotalTokens": 50000, "allowedModels": ["claude-sonnet-4-5"] },
|
|
2151
2447
|
* "alternatives": { "anyOf": [{ "id": "cache-hit", "tools": { "required": ["read_cache"] } }] }
|
|
@@ -2162,6 +2458,7 @@ const PATH_KEYS = [
|
|
|
2162
2458
|
"run",
|
|
2163
2459
|
"tools",
|
|
2164
2460
|
"llm",
|
|
2461
|
+
"retries",
|
|
2165
2462
|
"rules"
|
|
2166
2463
|
];
|
|
2167
2464
|
const SPEC_KEYS = [
|
|
@@ -2185,8 +2482,11 @@ const TOOLS_KEYS = [
|
|
|
2185
2482
|
"maxCallsPerTool",
|
|
2186
2483
|
"requiredOrder",
|
|
2187
2484
|
"orderMode",
|
|
2188
|
-
"arguments"
|
|
2485
|
+
"arguments",
|
|
2486
|
+
"enforced"
|
|
2189
2487
|
];
|
|
2488
|
+
const RETRIES_KEYS = ["reads", "writes"];
|
|
2489
|
+
const WRITE_KEYS = ["tool", "idempotencyKey"];
|
|
2190
2490
|
const LLM_KEYS = [
|
|
2191
2491
|
"maxCalls",
|
|
2192
2492
|
"maxTotalTokens",
|
|
@@ -2274,6 +2574,17 @@ const RULE_KEYS = {
|
|
|
2274
2574
|
"pointer",
|
|
2275
2575
|
"check",
|
|
2276
2576
|
"occurrence"
|
|
2577
|
+
],
|
|
2578
|
+
toolsOffered: [
|
|
2579
|
+
"kind",
|
|
2580
|
+
"label",
|
|
2581
|
+
"declared"
|
|
2582
|
+
],
|
|
2583
|
+
retrySafe: [
|
|
2584
|
+
"kind",
|
|
2585
|
+
"label",
|
|
2586
|
+
"reads",
|
|
2587
|
+
"writes"
|
|
2277
2588
|
]
|
|
2278
2589
|
};
|
|
2279
2590
|
/**
|
|
@@ -2368,7 +2679,18 @@ function compilePath(path, prefix) {
|
|
|
2368
2679
|
...arg.occurrence === void 0 ? {} : { occurrence: arg.occurrence }
|
|
2369
2680
|
});
|
|
2370
2681
|
});
|
|
2682
|
+
if (tools.enforced) rules.push({
|
|
2683
|
+
kind: "toolsOffered",
|
|
2684
|
+
label: at("tools.enforced"),
|
|
2685
|
+
...tools.allowed === void 0 ? {} : { declared: [...tools.allowed] }
|
|
2686
|
+
});
|
|
2371
2687
|
}
|
|
2688
|
+
if (path.retries) rules.push({
|
|
2689
|
+
kind: "retrySafe",
|
|
2690
|
+
label: at("retries"),
|
|
2691
|
+
...path.retries.reads === void 0 ? {} : { reads: [...path.retries.reads] },
|
|
2692
|
+
...path.retries.writes === void 0 ? {} : { writes: path.retries.writes.map((w) => ({ ...w })) }
|
|
2693
|
+
});
|
|
2372
2694
|
const llm = path.llm;
|
|
2373
2695
|
if (llm) {
|
|
2374
2696
|
if (llm.maxCalls !== void 0) rules.push({
|
|
@@ -2442,6 +2764,7 @@ function parsePath(path, prefix) {
|
|
|
2442
2764
|
if (path.run !== void 0) {
|
|
2443
2765
|
const run = object(path.run, at("run"));
|
|
2444
2766
|
strictKeys(run, RUN_KEYS, at("run"));
|
|
2767
|
+
if (run.requireCompleted !== void 0 && run.requireCompleted !== true) throw new ValidationError(`${at("run.requireCompleted")}: must be true`);
|
|
2445
2768
|
if (Array.isArray(run.allowedStatuses)) run.allowedStatuses.forEach((status, i) => {
|
|
2446
2769
|
oneOfValues(status, RUN_STATUSES, at(`run.allowedStatuses[${i}]`));
|
|
2447
2770
|
});
|
|
@@ -2463,11 +2786,13 @@ function parsePath(path, prefix) {
|
|
|
2463
2786
|
if (tools.maxCalls !== void 0) count(tools.maxCalls, at("tools.maxCalls"));
|
|
2464
2787
|
if (tools.maxCallsPerTool !== void 0) {
|
|
2465
2788
|
const perTool = object(tools.maxCallsPerTool, at("tools.maxCallsPerTool"));
|
|
2789
|
+
if (Object.keys(perTool).length === 0) throw new ValidationError(`${at("tools.maxCallsPerTool")}: must not be empty`);
|
|
2466
2790
|
for (const [tool, max] of Object.entries(perTool)) {
|
|
2467
2791
|
nonEmptyString(tool, at("tools.maxCallsPerTool key"));
|
|
2468
2792
|
count(max, at(`tools.maxCallsPerTool.${tool}`));
|
|
2469
2793
|
}
|
|
2470
2794
|
}
|
|
2795
|
+
if (tools.enforced !== void 0 && tools.enforced !== true) throw new ValidationError(`${at("tools.enforced")}: must be true`);
|
|
2471
2796
|
if (tools.orderMode !== void 0) oneOfValues(tools.orderMode, ORDER_MODES, at("tools.orderMode"));
|
|
2472
2797
|
if (tools.orderMode !== void 0 && tools.requiredOrder === void 0) throw new ValidationError(`${at("tools.orderMode")}: set only with requiredOrder`);
|
|
2473
2798
|
if (tools.arguments !== void 0) nonEmptyArray(tools.arguments, at("tools.arguments")).forEach((arg, i) => {
|
|
@@ -2490,9 +2815,25 @@ function parsePath(path, prefix) {
|
|
|
2490
2815
|
}, where);
|
|
2491
2816
|
});
|
|
2492
2817
|
}
|
|
2818
|
+
if (path.retries !== void 0) {
|
|
2819
|
+
const retries = object(path.retries, at("retries"));
|
|
2820
|
+
strictKeys(retries, RETRIES_KEYS, at("retries"));
|
|
2821
|
+
if (retries.reads !== void 0) toolNames(retries.reads, at("retries.reads"));
|
|
2822
|
+
if (retries.writes !== void 0) nonEmptyArray(retries.writes, at("retries.writes")).forEach((w, i) => {
|
|
2823
|
+
const where = at(`retries.writes[${i}]`);
|
|
2824
|
+
strictKeys(object(w, where), WRITE_KEYS, where);
|
|
2825
|
+
nonEmptyString(w.tool, `${where}.tool`);
|
|
2826
|
+
});
|
|
2827
|
+
assertRule({
|
|
2828
|
+
kind: "retrySafe",
|
|
2829
|
+
label: "retries",
|
|
2830
|
+
...retries
|
|
2831
|
+
}, at("retries"));
|
|
2832
|
+
}
|
|
2493
2833
|
if (path.llm !== void 0) {
|
|
2494
2834
|
const llm = object(path.llm, at("llm"));
|
|
2495
2835
|
strictKeys(llm, LLM_KEYS, at("llm"));
|
|
2836
|
+
if (llm.maxCalls === void 0 && llm.maxTotalTokens === void 0 && llm.allowedModels === void 0) throw new ValidationError(`${at("llm")}: llm rule checks nothing`);
|
|
2496
2837
|
if (llm.maxCalls !== void 0) count(llm.maxCalls, at("llm.maxCalls"));
|
|
2497
2838
|
if (llm.maxTotalTokens !== void 0) count(llm.maxTotalTokens, at("llm.maxTotalTokens"));
|
|
2498
2839
|
if (llm.allowedModels !== void 0) toolNames(llm.allowedModels, at("llm.allowedModels"));
|
|
@@ -2645,12 +2986,15 @@ function lintPath(path, prefix, out) {
|
|
|
2645
2986
|
if (forbidden.has(arg.tool)) error("tools.argument-on-forbidden", `tools.arguments[${i}]`, `"${arg.tool}" is forbidden, so its arguments can never be checked`);
|
|
2646
2987
|
if (!needed.includes(arg.tool)) warn("tools.argument-without-required", `tools.arguments[${i}]`, `"${arg.tool}" is not required; a run that never calls it fails this check for missing evidence`);
|
|
2647
2988
|
}
|
|
2989
|
+
if (t.enforced && !allowed) warn("tools.enforced-without-allowed", "tools.enforced", "without allowed, only calls are checked against the offered tools; add allowed to also check what the harness offered");
|
|
2990
|
+
for (const [i, w] of (path.retries?.writes ?? []).entries()) if (forbidden.has(w.tool)) warn("retries.write-forbidden", `retries.writes[${i}]`, `"${w.tool}" is forbidden, so its retry rule never applies`);
|
|
2648
2991
|
const order = t.requiredOrder ?? [];
|
|
2649
2992
|
const repeated = order.find((tool, i) => order.indexOf(tool) !== i);
|
|
2650
2993
|
if (repeated !== void 0) error("tools.order-repeats", "tools.requiredOrder", `"${repeated}" appears twice, so the order contradicts itself`);
|
|
2651
2994
|
if (order.length > 1 && t.orderMode === void 0) warn("tools.order-default-mode", "tools.orderMode", "start-order passes when calls overlap; set finish-before-start if each step must finish first");
|
|
2652
2995
|
const run = path.run;
|
|
2653
|
-
if (run?.requireCompleted && run.allowedStatuses
|
|
2996
|
+
if (run?.requireCompleted && run.allowedStatuses && !run.allowedStatuses.includes("completed")) error("run.completed-not-allowed", "run", "requireCompleted needs status completed, which allowedStatuses excludes");
|
|
2997
|
+
else if (run?.requireCompleted && run.allowedStatuses?.some((s) => s !== "completed")) warn("run.statuses-beside-completed", "run.allowedStatuses", "requireCompleted accepts only completed, so the other allowed statuses never apply");
|
|
2654
2998
|
if (path.llm?.maxCalls === 0 && (path.llm.maxTotalTokens !== void 0 || path.llm.allowedModels)) warn("llm.no-calls", "llm", "maxCalls is 0, so the token and model limits never apply");
|
|
2655
2999
|
}
|
|
2656
3000
|
//#endregion
|