@aiwg/cli 2026.9.20 → 2026.9.21
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/agentic/code/providers/capability-matrix.yaml +58 -6
- package/agentic/code/providers/model-capabilities.v1.json +34 -0
- package/agentic/code/providers/model-catalog.v1.json +55 -13
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/views/cli.js +1 -1
- package/dist/src/audit/operator-decision.js +10 -0
- package/dist/src/auth/credential-store.js +127 -27
- package/dist/src/catalog/builtin-models.json +272 -40
- package/dist/src/catalog/cli.js +1 -1
- package/dist/src/catalog/cli.mjs +1 -1
- package/dist/src/cli/handlers/effect.js +872 -0
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/models.js +41 -10
- package/dist/src/cli/handlers/ralph.js +1 -1
- package/dist/src/cli/handlers/sessions.js +113 -37
- package/dist/src/cli/handlers/subcommands.js +31 -0
- package/dist/src/cli/handlers/use.js +46 -9
- package/dist/src/cli/handlers/utilities.js +10 -0
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/scope-resolver.js +112 -9
- package/dist/src/cli/services/deployment-verification.js +5 -0
- package/dist/src/config/aiwg-config.js +3 -0
- package/dist/src/decision/acceptance-rollout.js +93 -0
- package/dist/src/decision/acceptance.js +175 -0
- package/dist/src/decision/adapters/jev.js +234 -13
- package/dist/src/decision/adapters/llm-subagent.js +60 -12
- package/dist/src/decision/admission.js +550 -0
- package/dist/src/decision/batch-benchmark.js +47 -0
- package/dist/src/decision/batch-receipts/accounting.js +75 -0
- package/dist/src/decision/batch-receipts/export.js +40 -0
- package/dist/src/decision/batch-receipts/index.js +9 -0
- package/dist/src/decision/batch-receipts/protection.js +40 -0
- package/dist/src/decision/batch-receipts/receipt.js +44 -0
- package/dist/src/decision/batch-receipts/result-store.js +341 -0
- package/dist/src/decision/batch-receipts/store.js +362 -0
- package/dist/src/decision/batch-receipts/types.js +2 -0
- package/dist/src/decision/batch-receipts/validate.js +127 -0
- package/dist/src/decision/batch.js +68 -0
- package/dist/src/decision/calibration/index.js +4 -0
- package/dist/src/decision/calibration/receipts.js +130 -0
- package/dist/src/decision/calibration/registry.js +258 -0
- package/dist/src/decision/calibration/types.js +2 -0
- package/dist/src/decision/compile-cache/benchmark.js +62 -0
- package/dist/src/decision/compile-cache/compatibility.js +65 -0
- package/dist/src/decision/compile-cache/file-store.js +280 -0
- package/dist/src/decision/compile-cache/identity.js +26 -0
- package/dist/src/decision/compile-cache/index.js +9 -0
- package/dist/src/decision/compile-cache/prefix.js +21 -0
- package/dist/src/decision/compile-cache/records.js +86 -0
- package/dist/src/decision/compile-cache/runtime.js +68 -0
- package/dist/src/decision/compile-cache/store.js +108 -0
- package/dist/src/decision/compile-cache/types.js +2 -0
- package/dist/src/decision/context-plan.js +234 -0
- package/dist/src/decision/context-qualification.js +60 -0
- package/dist/src/decision/ensemble/aggregate.js +215 -0
- package/dist/src/decision/ensemble/contract.js +316 -0
- package/dist/src/decision/ensemble/drift.js +51 -0
- package/dist/src/decision/ensemble/index.js +6 -0
- package/dist/src/decision/ensemble/report.js +71 -0
- package/dist/src/decision/ensemble/types.js +2 -0
- package/dist/src/decision/evaluate.js +1024 -78
- package/dist/src/decision/file-lifecycle-store.js +168 -0
- package/dist/src/decision/graph-beam.js +40 -0
- package/dist/src/decision/graph-budget.js +110 -0
- package/dist/src/decision/graph-decision-bridge.js +93 -0
- package/dist/src/decision/graph-evidence.js +123 -0
- package/dist/src/decision/graph-flow-adapter.js +56 -0
- package/dist/src/decision/graph-flow.js +105 -0
- package/dist/src/decision/graph-parallel.js +79 -0
- package/dist/src/decision/graph-patterns.js +20 -0
- package/dist/src/decision/graph-run-store.js +66 -0
- package/dist/src/decision/graph-run.js +166 -0
- package/dist/src/decision/graph-skill-bridge.js +91 -0
- package/dist/src/decision/graph-templates.js +56 -0
- package/dist/src/decision/graph.js +115 -0
- package/dist/src/decision/index.js +35 -0
- package/dist/src/decision/job-accounting.js +93 -0
- package/dist/src/decision/job-contract.js +186 -0
- package/dist/src/decision/job-effects.js +95 -0
- package/dist/src/decision/job-evaluate.js +80 -0
- package/dist/src/decision/job-gateway.js +121 -0
- package/dist/src/decision/job-http.js +194 -0
- package/dist/src/decision/job-payload-store.js +258 -0
- package/dist/src/decision/job-poll-limiter.js +92 -0
- package/dist/src/decision/job-quota.js +158 -0
- package/dist/src/decision/job-runtime.js +240 -0
- package/dist/src/decision/job-scheduler.js +26 -0
- package/dist/src/decision/job-service.js +117 -0
- package/dist/src/decision/job-store.js +264 -0
- package/dist/src/decision/job-worker.js +141 -0
- package/dist/src/decision/lifecycle.js +148 -0
- package/dist/src/decision/patterns/artifacts.js +173 -0
- package/dist/src/decision/patterns/catalog.js +108 -0
- package/dist/src/decision/patterns/durable-review.js +237 -0
- package/dist/src/decision/patterns/index.js +8 -0
- package/dist/src/decision/patterns/playground.js +401 -0
- package/dist/src/decision/patterns/recorded.js +43 -0
- package/dist/src/decision/patterns/specs.js +174 -0
- package/dist/src/decision/patterns/types.js +2 -0
- package/dist/src/decision/portable-secrets.js +53 -0
- package/dist/src/decision/projection.js +210 -0
- package/dist/src/decision/qualification/capture.js +67 -0
- package/dist/src/decision/qualification/drift.js +68 -0
- package/dist/src/decision/qualification/gate-evidence.js +172 -0
- package/dist/src/decision/qualification/gates.js +83 -0
- package/dist/src/decision/qualification/index.js +12 -0
- package/dist/src/decision/qualification/load.js +275 -0
- package/dist/src/decision/qualification/manifest.js +37 -0
- package/dist/src/decision/qualification/privacy.js +41 -0
- package/dist/src/decision/qualification/quality.js +213 -0
- package/dist/src/decision/qualification/release.js +116 -0
- package/dist/src/decision/qualification/runner.js +408 -0
- package/dist/src/decision/qualification/types.js +2 -0
- package/dist/src/decision/receipts.js +30 -7
- package/dist/src/decision/result-cache/index.js +6 -0
- package/dist/src/decision/result-cache/integrity.js +28 -0
- package/dist/src/decision/result-cache/key.js +28 -0
- package/dist/src/decision/result-cache/service.js +92 -0
- package/dist/src/decision/result-cache/store.js +327 -0
- package/dist/src/decision/result-cache/types.js +3 -0
- package/dist/src/decision/review/authorization.js +110 -0
- package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
- package/dist/src/decision/review/index.js +12 -0
- package/dist/src/decision/review/migrate.js +34 -0
- package/dist/src/decision/review/operator-audit.js +59 -0
- package/dist/src/decision/review/recovery-adapters.js +252 -0
- package/dist/src/decision/review/recovery.js +33 -0
- package/dist/src/decision/review/service.js +498 -0
- package/dist/src/decision/review/store.js +219 -0
- package/dist/src/decision/review/types.js +2 -0
- package/dist/src/decision/review/validate.js +205 -0
- package/dist/src/decision/scheduler.js +115 -0
- package/dist/src/decision/telemetry/context.js +42 -0
- package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
- package/dist/src/decision/telemetry/exporter.js +77 -0
- package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
- package/dist/src/decision/telemetry/index.js +13 -0
- package/dist/src/decision/telemetry/mapping.js +146 -0
- package/dist/src/decision/telemetry/metrics.js +164 -0
- package/dist/src/decision/telemetry/otlp-http.js +183 -0
- package/dist/src/decision/telemetry/redaction.js +93 -0
- package/dist/src/decision/telemetry/retention.js +109 -0
- package/dist/src/decision/telemetry/runtime.js +310 -0
- package/dist/src/decision/telemetry/trace.js +158 -0
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/validate.js +135 -5
- package/dist/src/effects/checkpoint-sinks.js +94 -0
- package/dist/src/effects/checkpoint.js +76 -0
- package/dist/src/effects/decision-links.js +54 -0
- package/dist/src/effects/errors.js +48 -0
- package/dist/src/effects/identity.js +178 -0
- package/dist/src/effects/index.js +25 -0
- package/dist/src/effects/keyring.js +160 -0
- package/dist/src/effects/keys.js +130 -0
- package/dist/src/effects/ledger.js +435 -0
- package/dist/src/effects/lifecycle.js +110 -0
- package/dist/src/effects/lock-recovery.js +269 -0
- package/dist/src/effects/reader.js +103 -0
- package/dist/src/effects/records.js +114 -0
- package/dist/src/effects/redaction.js +51 -0
- package/dist/src/effects/schema.js +73 -0
- package/dist/src/effects/store.js +239 -0
- package/dist/src/effects/types.js +49 -0
- package/dist/src/effects/verifiers/decision.js +168 -0
- package/dist/src/effects/verifiers/file.js +106 -0
- package/dist/src/effects/verifiers/git.js +206 -0
- package/dist/src/effects/verifiers/index.js +90 -0
- package/dist/src/effects/verifiers/registry.js +148 -0
- package/dist/src/effects/verifiers/review.js +123 -0
- package/dist/src/effects/verifiers/tracker.js +299 -0
- package/dist/src/effects/verifiers/types.js +21 -0
- package/dist/src/effects/verify.js +295 -0
- package/dist/src/evidence/bundle.js +2 -0
- package/dist/src/extensions/commands/definitions.js +28 -0
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/marketplace/provenance.js +25 -27
- package/dist/src/mcp/cli.mjs +11 -1
- package/dist/src/mcp/grok-build-config.mjs +189 -0
- package/dist/src/mcp/registry.js +21 -1
- package/dist/src/mcp/registry.mjs +20 -1
- package/dist/src/models/grok-build-models.js +356 -0
- package/dist/src/models/index.js +1 -0
- package/dist/src/models/model-capabilities.v1.json +34 -0
- package/dist/src/models/model-catalog.v1.json +55 -13
- package/dist/src/models/model-discovery.js +41 -5
- package/dist/src/models/provider-policy.js +3 -2
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +58 -6
- package/dist/src/providers/grok-build-acp.js +192 -0
- package/dist/src/providers/grok-build-dispatch.js +251 -0
- package/dist/src/providers/grok-build-headless.js +170 -0
- package/dist/src/providers/muse-paths.js +105 -0
- package/dist/src/providers/provider-definitions.js +93 -7
- package/dist/src/providers/provider-definitions.mjs +21 -1
- package/dist/src/providers/provider-inventory.js +3 -0
- package/dist/src/security/artifact-attestation.js +4 -8
- package/dist/src/security/signing.js +119 -0
- package/dist/src/sessions/adapters/grok-build.js +187 -0
- package/dist/src/sessions/adapters/muse.js +412 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/grok-build-cli.js +196 -0
- package/dist/src/sessions/index.js +3 -0
- package/dist/src/sessions/workspace-discovery.js +9 -5
- package/dist/src/skills/deployer.js +33 -5
- package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
- package/dist/src/smiths/context-pipeline/finalization.js +1 -32
- package/dist/src/smiths/context-pipeline/generator.js +12 -0
- package/dist/src/smiths/context-pipeline/index.js +1 -1
- package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
- package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
- package/dist/src/smiths/hook-bridge/index.js +3 -0
- package/dist/src/smiths/hook-bridge/shim.js +2 -0
- package/dist/src/smiths/hook-bridge/types.js +5 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
- package/dist/src/storage/protected-files.js +102 -0
- package/dist/src/tracker/forge-http.js +371 -0
- package/dist/src/tracker/project-inputs.js +41 -0
- package/package.json +2 -1
- package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
- package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
- package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
- package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
- package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
- package/schemas/decision/DecisionBinding.schema.json +309 -0
- package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
- package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
- package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
- package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
- package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
- package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
- package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
- package/schemas/decision/DecisionDefinition.schema.json +159 -0
- package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
- package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
- package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
- package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
- package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
- package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
- package/schemas/decision/DecisionJob.v1.schema.json +71 -0
- package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
- package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
- package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
- package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
- package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
- package/schemas/decision/DecisionResult.schema.json +524 -0
- package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
- package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
- package/schemas/decision/DecisionReview.schema.json +29 -0
- package/schemas/decision/DecisionRuleset.schema.json +313 -0
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
- package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
- package/schemas/decision/RulesetResult.schema.json +721 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
- package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
- package/tools/agents/deploy-agents.mjs +21 -6
- package/tools/agents/providers/base.mjs +7 -4
- package/tools/agents/providers/codex.mjs +1 -1
- package/tools/agents/providers/grok-build.d.mts +7 -0
- package/tools/agents/providers/grok-build.mjs +249 -9
- package/tools/agents/providers/model-role.mjs +6 -3
- package/tools/agents/providers/muse-hooks.mjs +785 -0
- package/tools/agents/providers/muse-paths.mjs +112 -0
- package/tools/agents/providers/muse.mjs +336 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { extractTraceContext, transportTraceContext } from './context.js';
|
|
2
|
+
import { mapAdmissionEvidence, mapDecisionAttempt, mapDecisionResult, mapRulesetResult } from './mapping.js';
|
|
3
|
+
import { DecisionTraceBuilder, recordBatchReceiptTrace } from './trace.js';
|
|
4
|
+
import { recordDecisionSpanMetrics } from './metrics.js';
|
|
5
|
+
import { sanitizeAttributes } from './redaction.js';
|
|
6
|
+
// Keyed by the per-invocation telemetry object, which request snapshots copy by reference.
|
|
7
|
+
const TRACES = new WeakMap();
|
|
8
|
+
const USAGE_KEYS = ['gen_ai.usage.input_tokens', 'gen_ai.usage.output_tokens', 'aiwg.usage.cost_usd',
|
|
9
|
+
'aiwg.usage.cost_provenance', 'aiwg.provider.request_id', 'aiwg.provider.request_id_source'];
|
|
10
|
+
/**
|
|
11
|
+
* Records metadata-only spans while one evaluator invocation runs. Spans carry
|
|
12
|
+
* live start/end times, are buffered in start order and flushed once after the
|
|
13
|
+
* terminal result. Every failure disables the trace; none reaches the decision.
|
|
14
|
+
*/
|
|
15
|
+
export class DecisionRuntimeTrace {
|
|
16
|
+
telemetry;
|
|
17
|
+
now;
|
|
18
|
+
builder;
|
|
19
|
+
root;
|
|
20
|
+
validate;
|
|
21
|
+
batchSpans = new Map();
|
|
22
|
+
disabled = false;
|
|
23
|
+
constructor(telemetry, now) {
|
|
24
|
+
this.telemetry = telemetry;
|
|
25
|
+
this.now = now;
|
|
26
|
+
this.builder = new DecisionTraceBuilder(telemetry.ids, now);
|
|
27
|
+
this.root = this.builder.startSpan('decision.workflow', telemetry.parent ? { parent: telemetry.parent } : {});
|
|
28
|
+
}
|
|
29
|
+
/** Start the root span and bind the trace to a request copy. Invalid ID sources disable telemetry. */
|
|
30
|
+
static begin(request) {
|
|
31
|
+
if (!request.telemetry)
|
|
32
|
+
return { request, trace: null };
|
|
33
|
+
try {
|
|
34
|
+
const telemetry = { ...request.telemetry };
|
|
35
|
+
const trace = new DecisionRuntimeTrace(telemetry, request.now ?? Date.now);
|
|
36
|
+
TRACES.set(telemetry, trace);
|
|
37
|
+
return { request: { ...request, telemetry }, trace };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return { request, trace: null };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** W3C traceparent of the workflow span, for durable receipts created by this invocation. */
|
|
44
|
+
get traceParent() {
|
|
45
|
+
return this.disabled ? undefined : transportTraceContext(this.root.context).traceparent;
|
|
46
|
+
}
|
|
47
|
+
validated(rejected) {
|
|
48
|
+
this.guard(() => {
|
|
49
|
+
if (this.validate)
|
|
50
|
+
return;
|
|
51
|
+
const outcome = rejected ? 'rejected' : 'accepted';
|
|
52
|
+
this.validate = this.builder.startSpan('decision.validate', { parent: this.root.context,
|
|
53
|
+
startTimeUnixMs: this.root.startTimeUnixMs,
|
|
54
|
+
attributes: { 'aiwg.validation.outcome': outcome }, provenance: { 'aiwg.validation.outcome': 'client-derived' } });
|
|
55
|
+
this.builder.endSpan(this.validate, rejected ? 'error' : 'ok');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
startAttempt(alias, target, ordinal, fallback = false) {
|
|
59
|
+
return this.guard(() => {
|
|
60
|
+
// Known before dispatch, so an attempt whose outcome is never observed still names its route.
|
|
61
|
+
const attributes = { 'aiwg.decision.alias': alias, 'aiwg.adapter.id': target.adapter,
|
|
62
|
+
'aiwg.adapter.version': target.adapterVersion, 'gen_ai.request.model': target.model,
|
|
63
|
+
'aiwg.attempt.ordinal': ordinal, 'aiwg.route.fallback': fallback };
|
|
64
|
+
return this.open(this.builder.startSpan('decision.attempt', { parent: this.root.context, attributes,
|
|
65
|
+
provenance: Object.fromEntries(Object.keys(attributes).map(key => [key, 'client-derived'])) }), ordinal);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
endAttempt(handle, attempt) {
|
|
69
|
+
this.guard(() => {
|
|
70
|
+
if (!handle)
|
|
71
|
+
return;
|
|
72
|
+
const mapped = mapDecisionAttempt(attempt);
|
|
73
|
+
this.builder.annotate(handle.span, mapped.attributes, mapped.provenance);
|
|
74
|
+
if (attempt.termination)
|
|
75
|
+
handle.span.events.push({ name: 'attempt.terminated', timeUnixMs: this.now(),
|
|
76
|
+
attributes: { 'aiwg.attempt.termination': attempt.termination } });
|
|
77
|
+
this.builder.endSpan(handle.span, attempt.status === 'success' ? 'ok' : 'error');
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Open the live D05 `decision.admit` span for one admission decision. A granted
|
|
82
|
+
* lease stays open until release, so breaker transitions caused by the attempt
|
|
83
|
+
* land inside the span; a rejection or deferral closes it at once. Evidence is
|
|
84
|
+
* metadata-only by contract: principal and workspace IDs never reach the span.
|
|
85
|
+
*/
|
|
86
|
+
startAdmission(alias, adapterId, startedAtMs, evidence) {
|
|
87
|
+
const handle = this.guard(() => {
|
|
88
|
+
const mapped = mapAdmissionEvidence(evidence);
|
|
89
|
+
return { closed: false, span: this.builder.startSpan('decision.admit', { parent: this.root.context, startTimeUnixMs: startedAtMs,
|
|
90
|
+
attributes: { ...mapped.attributes, 'aiwg.decision.alias': alias, 'aiwg.adapter.id': adapterId },
|
|
91
|
+
provenance: { ...mapped.provenance, 'aiwg.decision.alias': 'client-derived', 'aiwg.adapter.id': 'client-derived' } }) };
|
|
92
|
+
});
|
|
93
|
+
if (evidence.decision !== 'admit')
|
|
94
|
+
this.endAdmission(handle, evidence);
|
|
95
|
+
return handle;
|
|
96
|
+
}
|
|
97
|
+
/** Close an admission span once; later calls are ignored. */
|
|
98
|
+
endAdmission(handle, evidence) {
|
|
99
|
+
this.guard(() => {
|
|
100
|
+
if (!handle || handle.closed)
|
|
101
|
+
return;
|
|
102
|
+
handle.closed = true;
|
|
103
|
+
for (const change of evidence.breakerTransitions ?? []) {
|
|
104
|
+
handle.span.events.push({ name: 'breaker.transition', timeUnixMs: this.now(),
|
|
105
|
+
attributes: { 'aiwg.breaker.from': change.from, 'aiwg.breaker.to': change.to } });
|
|
106
|
+
}
|
|
107
|
+
this.builder.endSpan(handle.span, evidence.decision === 'admit' ? 'ok' : evidence.decision === 'reject' ? 'error' : 'unset');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** Record one D10 projection step, under its attempt span when there is one. */
|
|
111
|
+
projected(alias, parent, startedAtMs, result) {
|
|
112
|
+
this.guard(() => {
|
|
113
|
+
const attributes = { 'aiwg.decision.alias': alias, 'aiwg.projection.mode': result.mode,
|
|
114
|
+
'aiwg.projection.outcome': result.outcome };
|
|
115
|
+
if (result.reason)
|
|
116
|
+
attributes['aiwg.projection.reason'] = result.reason;
|
|
117
|
+
if (result.fieldCount !== undefined)
|
|
118
|
+
attributes['aiwg.projection.field_count'] = result.fieldCount;
|
|
119
|
+
if (result.incompleteContext !== undefined)
|
|
120
|
+
attributes['aiwg.projection.incomplete_context'] = result.incompleteContext;
|
|
121
|
+
if (result.automaticActionAllowed !== undefined) {
|
|
122
|
+
attributes['aiwg.projection.automatic_action_allowed'] = result.automaticActionAllowed;
|
|
123
|
+
}
|
|
124
|
+
const span = this.builder.startSpan('decision.project', { parent: (parent?.span ?? this.root).context,
|
|
125
|
+
startTimeUnixMs: startedAtMs, attributes,
|
|
126
|
+
provenance: Object.fromEntries(Object.keys(attributes).map(key => [key, 'client-derived'])) });
|
|
127
|
+
this.builder.endSpan(span, result.outcome === 'allowed' ? 'ok' : 'error');
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
retryScheduled(handle, delayMs) {
|
|
131
|
+
this.guard(() => {
|
|
132
|
+
if (!handle)
|
|
133
|
+
return;
|
|
134
|
+
this.builder.annotate(handle.span, { 'aiwg.retry.delay_ms': delayMs }, { 'aiwg.retry.delay_ms': 'client-derived' });
|
|
135
|
+
handle.span.events.push({ name: 'retry.scheduled', timeUnixMs: this.now(), attributes: { 'aiwg.retry.delay_ms': delayMs } });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Open the shared request span before dispatch; its context is what the transport receives. */
|
|
139
|
+
startBatch(aliases, groupId, ordinal) {
|
|
140
|
+
return this.guard(() => {
|
|
141
|
+
const span = this.builder.startSpan('decision.batch.request', { parent: this.root.context,
|
|
142
|
+
attributes: { 'aiwg.batch.id': groupId, 'aiwg.batch.mode': 'native', 'aiwg.batch.item_count': aliases.length,
|
|
143
|
+
'aiwg.attempt.ordinal': ordinal },
|
|
144
|
+
provenance: { 'aiwg.batch.id': 'client-derived', 'aiwg.batch.mode': 'client-derived',
|
|
145
|
+
'aiwg.batch.item_count': 'client-derived', 'aiwg.attempt.ordinal': 'client-derived' } });
|
|
146
|
+
for (const alias of aliases)
|
|
147
|
+
this.batchSpans.set(alias, [...this.batchSpans.get(alias) ?? [], span]);
|
|
148
|
+
return this.open(span, ordinal);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Close one batch plan. A durable receipt is the accounting authority and is
|
|
153
|
+
* bridged through `recordBatchReceiptTrace`; without one, shared usage comes
|
|
154
|
+
* from the single transport response. A replayed receipt dispatched nothing
|
|
155
|
+
* here, so it only links back to the trace that created it.
|
|
156
|
+
*/
|
|
157
|
+
finishBatch(handles, receipt) {
|
|
158
|
+
this.guard(() => {
|
|
159
|
+
if (receipt && handles.length === 0) {
|
|
160
|
+
this.linkOrigin(receipt.traceParent, 'batch', { 'aiwg.batch.id': receipt.batchId });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (receipt) {
|
|
164
|
+
recordBatchReceiptTrace(this.builder, receipt, this.root.context, new Map(handles.map(handle => [handle.ordinal, handle.span])));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const handle of handles) {
|
|
168
|
+
if (!handle.response || handle.span.status !== 'unset')
|
|
169
|
+
continue;
|
|
170
|
+
const answers = handle.response.answers.map(answer => answer.observation);
|
|
171
|
+
const requestIds = [...new Set(answers.map(answer => answer.requestId).filter((id) => id !== null))];
|
|
172
|
+
const models = [...new Set(answers.map(answer => answer.actualModel).filter((model) => model !== null))];
|
|
173
|
+
this.builder.recordBatchUsage(handle.span, String(handle.span.attributes['aiwg.batch.id']), handle.response.sharedUsage, { requestId: requestIds.length === 1 ? requestIds[0] : null, actualModel: models.length === 1 ? models[0] : null }, { accountingKey: handle.span.context.spanId });
|
|
174
|
+
this.builder.endSpan(handle.span, answers.every(answer => answer.status === 'success') ? 'ok' : 'error');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Link the workflow to the trace that created a durable receipt this invocation is replaying. */
|
|
179
|
+
linkOrigin(traceParent, relationship, attributes) {
|
|
180
|
+
this.guard(() => {
|
|
181
|
+
const origin = traceParent ? extractTraceContext({ traceparent: traceParent }) : null;
|
|
182
|
+
if (!origin || (origin.traceId === this.root.context.traceId && origin.spanId === this.root.context.spanId)
|
|
183
|
+
|| this.root.links.some(link => link.traceId === origin.traceId && link.spanId === origin.spanId))
|
|
184
|
+
return;
|
|
185
|
+
this.root.links.push({ traceId: origin.traceId, spanId: origin.spanId, relationship,
|
|
186
|
+
...(Object.keys(attributes).length ? { attributes: sanitizeAttributes(attributes) } : {}) });
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** Record normalize/accept for one finished evaluation, linked to any shared batch request. */
|
|
190
|
+
decided(result) {
|
|
191
|
+
this.guard(() => {
|
|
192
|
+
const batches = this.batchSpans.get(result.spec.alias) ?? [];
|
|
193
|
+
const links = batches.map((span) => ({ traceId: span.context.traceId, spanId: span.context.spanId,
|
|
194
|
+
relationship: 'batch' }));
|
|
195
|
+
result.spec.attempts.forEach((attempt, index) => {
|
|
196
|
+
const request = batches[index];
|
|
197
|
+
// Native answers without a request span from this invocation are replayed evidence, not new work.
|
|
198
|
+
if (attempt.batch?.mode !== 'native' || !request)
|
|
199
|
+
return;
|
|
200
|
+
const mapped = withoutUsage(mapDecisionAttempt(attempt));
|
|
201
|
+
const span = this.builder.startSpan('decision.attempt', { parent: this.root.context,
|
|
202
|
+
startTimeUnixMs: request.startTimeUnixMs, links: [links[index]],
|
|
203
|
+
attributes: { ...mapped.attributes, 'aiwg.decision.alias': result.spec.alias, 'aiwg.usage.scope': 'batch-answer' },
|
|
204
|
+
provenance: { ...mapped.provenance, 'aiwg.decision.alias': 'client-derived', 'aiwg.usage.scope': 'client-derived' } });
|
|
205
|
+
this.builder.endSpan(span, attempt.status === 'success' ? 'ok' : 'error', request.endTimeUnixMs);
|
|
206
|
+
});
|
|
207
|
+
const mapped = mapDecisionResult(result);
|
|
208
|
+
const normalize = this.builder.startSpan('decision.normalize', { parent: this.root.context, links,
|
|
209
|
+
attributes: mapped.attributes, provenance: mapped.provenance });
|
|
210
|
+
this.builder.endSpan(normalize, result.spec.reason === 'invalid-output' ? 'error' : 'ok');
|
|
211
|
+
const accept = this.builder.startSpan('decision.accept', { parent: this.root.context, links,
|
|
212
|
+
attributes: mapped.attributes, provenance: mapped.provenance });
|
|
213
|
+
this.builder.endSpan(accept, result.spec.status === 'error' || result.spec.status === 'cancelled' ? 'error' : 'ok');
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/** Close the workflow and flush. Exporter and metric failures are isolated per span. */
|
|
217
|
+
async finish(result) {
|
|
218
|
+
if (this.disabled)
|
|
219
|
+
return;
|
|
220
|
+
let spans;
|
|
221
|
+
try {
|
|
222
|
+
const failed = result.spec.status === 'error' || result.spec.status === 'cancelled';
|
|
223
|
+
const mapped = mapRulesetResult(result);
|
|
224
|
+
this.builder.annotate(this.root, mapped.attributes, mapped.provenance);
|
|
225
|
+
if (result.spec.cache?.disposition === 'cache-hit') {
|
|
226
|
+
// Historical attempts are evidence, not attempts by this caller. Emitting
|
|
227
|
+
// their provider usage again would double-count tokens and fabricate work.
|
|
228
|
+
this.builder.annotate(this.root, { 'aiwg.invocation.id': result.spec.cache.callerInvocationId,
|
|
229
|
+
'aiwg.cache.layer': 'result', 'aiwg.cache.result': 'hit' }, { 'aiwg.invocation.id': 'client-derived',
|
|
230
|
+
'aiwg.cache.layer': 'client-derived', 'aiwg.cache.result': 'client-derived' });
|
|
231
|
+
const hit = this.builder.startSpan('decision.cache', { parent: this.root.context,
|
|
232
|
+
attributes: { 'aiwg.cache.layer': 'result', 'aiwg.cache.result': 'hit' },
|
|
233
|
+
provenance: { 'aiwg.cache.layer': 'client-derived', 'aiwg.cache.result': 'client-derived' } });
|
|
234
|
+
this.builder.endSpan(hit, 'ok');
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
this.validated(result.spec.reason === 'invalid-input' || result.spec.reason === 'invalid-definition');
|
|
238
|
+
// A span still open here never observed its outcome, e.g. an uncertain remote dispatch.
|
|
239
|
+
for (const span of this.builder.openSpans()) {
|
|
240
|
+
if (span === this.root)
|
|
241
|
+
continue;
|
|
242
|
+
this.builder.annotate(span, { 'aiwg.decision.reason': result.spec.reason }, { 'aiwg.decision.reason': 'client-derived' });
|
|
243
|
+
this.builder.endSpan(span, 'error');
|
|
244
|
+
}
|
|
245
|
+
const compose = this.builder.startSpan('decision.compose', { parent: this.root.context,
|
|
246
|
+
attributes: mapped.attributes, provenance: mapped.provenance });
|
|
247
|
+
this.builder.endSpan(compose, failed ? 'error' : 'ok');
|
|
248
|
+
if (result.spec.status === 'review') {
|
|
249
|
+
const review = this.builder.startSpan('decision.review', { parent: this.root.context,
|
|
250
|
+
attributes: { 'aiwg.decision.reason': result.spec.reason }, provenance: { 'aiwg.decision.reason': 'client-derived' } });
|
|
251
|
+
this.builder.endSpan(review, 'ok');
|
|
252
|
+
}
|
|
253
|
+
const persist = this.builder.startSpan('decision.persist', { parent: this.root.context,
|
|
254
|
+
attributes: { 'aiwg.persistence.result': result.spec.reason === 'persistence-error' ? 'failed' : 'persisted' },
|
|
255
|
+
provenance: { 'aiwg.persistence.result': 'client-derived' } });
|
|
256
|
+
this.builder.endSpan(persist, result.spec.reason === 'persistence-error' ? 'error' : 'ok');
|
|
257
|
+
}
|
|
258
|
+
this.builder.endSpan(this.root, failed ? 'error' : 'ok');
|
|
259
|
+
spans = this.builder.build().spans;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Observability must never alter, reject, or mask the authoritative result.
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
this.disabled = true;
|
|
267
|
+
}
|
|
268
|
+
if (this.telemetry.metrics)
|
|
269
|
+
for (const span of spans) {
|
|
270
|
+
try {
|
|
271
|
+
recordDecisionSpanMetrics(span, this.telemetry.metrics);
|
|
272
|
+
}
|
|
273
|
+
catch { /* metrics cannot change a decision */ }
|
|
274
|
+
}
|
|
275
|
+
for (const span of spans) {
|
|
276
|
+
try {
|
|
277
|
+
await this.telemetry.hook.emit(span);
|
|
278
|
+
}
|
|
279
|
+
catch { /* isolate individual exporter failures */ }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
open(span, ordinal) {
|
|
283
|
+
return { span, ordinal, traceContext: transportTraceContext(span.context) };
|
|
284
|
+
}
|
|
285
|
+
guard(record) {
|
|
286
|
+
if (this.disabled)
|
|
287
|
+
return undefined;
|
|
288
|
+
try {
|
|
289
|
+
return record();
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
this.disabled = true;
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** The live trace bound to this evaluator request, if telemetry is configured and healthy. */
|
|
298
|
+
export function runtimeTraceOf(request) {
|
|
299
|
+
return request.telemetry ? TRACES.get(request.telemetry) ?? null : null;
|
|
300
|
+
}
|
|
301
|
+
function withoutUsage(mapping) {
|
|
302
|
+
const attributes = { ...mapping.attributes };
|
|
303
|
+
const provenance = { ...mapping.provenance };
|
|
304
|
+
for (const key of USAGE_KEYS) {
|
|
305
|
+
delete attributes[key];
|
|
306
|
+
delete provenance[key];
|
|
307
|
+
}
|
|
308
|
+
return { attributes, provenance };
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createTelemetryContext } from './context.js';
|
|
2
|
+
import { sanitizeAttributes, sanitizeOpaqueValue } from './redaction.js';
|
|
3
|
+
import { DECISION_TELEMETRY_SCHEMA_VERSION } from './types.js';
|
|
4
|
+
export class DecisionTraceBuilder {
|
|
5
|
+
ids;
|
|
6
|
+
now;
|
|
7
|
+
spans = [];
|
|
8
|
+
batchUsage = new Set();
|
|
9
|
+
root;
|
|
10
|
+
constructor(ids, now = Date.now) {
|
|
11
|
+
this.ids = ids;
|
|
12
|
+
this.now = now;
|
|
13
|
+
}
|
|
14
|
+
startSpan(name, options = {}) {
|
|
15
|
+
const parent = options.parent ?? this.root;
|
|
16
|
+
const context = createTelemetryContext(this.ids, parent);
|
|
17
|
+
if (!this.root)
|
|
18
|
+
this.root = context;
|
|
19
|
+
const attributes = sanitizeAttributes(options.attributes ?? {});
|
|
20
|
+
const span = {
|
|
21
|
+
schemaVersion: DECISION_TELEMETRY_SCHEMA_VERSION, name, context,
|
|
22
|
+
parentSpanId: parent?.spanId ?? null,
|
|
23
|
+
startTimeUnixMs: options.startTimeUnixMs ?? this.now(), endTimeUnixMs: options.startTimeUnixMs ?? this.now(),
|
|
24
|
+
status: 'unset', attributes,
|
|
25
|
+
provenance: Object.fromEntries(Object.entries(options.provenance ?? {}).filter(([key]) => Object.hasOwn(attributes, key))),
|
|
26
|
+
links: structuredClone(options.links ?? []), events: [],
|
|
27
|
+
};
|
|
28
|
+
this.spans.push(span);
|
|
29
|
+
return span;
|
|
30
|
+
}
|
|
31
|
+
/** Merge allowlisted metadata into a recorded span; unknown or protected keys are dropped. */
|
|
32
|
+
annotate(span, attributes, provenance = {}) {
|
|
33
|
+
const safe = sanitizeAttributes(attributes);
|
|
34
|
+
Object.assign(span.attributes, safe);
|
|
35
|
+
for (const [key, value] of Object.entries(provenance))
|
|
36
|
+
if (Object.hasOwn(safe, key))
|
|
37
|
+
span.provenance[key] = value;
|
|
38
|
+
}
|
|
39
|
+
/** Spans that were started but not yet ended, in start order. */
|
|
40
|
+
openSpans() {
|
|
41
|
+
return this.spans.filter(span => span.status === 'unset');
|
|
42
|
+
}
|
|
43
|
+
endSpan(span, status = 'ok', endTimeUnixMs = this.now()) {
|
|
44
|
+
span.endTimeUnixMs = Math.max(span.startTimeUnixMs, endTimeUnixMs);
|
|
45
|
+
span.status = status;
|
|
46
|
+
}
|
|
47
|
+
/** Shared provider usage belongs to exactly one batch request span. */
|
|
48
|
+
recordBatchUsage(span, batchId, usage, provider = {}, options = {}) {
|
|
49
|
+
if (span.name !== 'decision.batch.request')
|
|
50
|
+
throw new Error('Batch usage can only be recorded on a batch request span');
|
|
51
|
+
const accountingKey = options.accountingKey ?? batchId;
|
|
52
|
+
if (this.batchUsage.has(accountingKey))
|
|
53
|
+
throw new Error(`Batch usage already recorded: ${accountingKey}`);
|
|
54
|
+
this.batchUsage.add(accountingKey);
|
|
55
|
+
span.attributes['aiwg.batch.id'] = batchId;
|
|
56
|
+
span.attributes['gen_ai.usage.input_tokens'] = usage.inputTokens;
|
|
57
|
+
span.attributes['gen_ai.usage.output_tokens'] = usage.outputTokens;
|
|
58
|
+
span.attributes['aiwg.usage.cost_usd'] = usage.costUsd;
|
|
59
|
+
span.attributes['aiwg.usage.scope'] = 'shared-request';
|
|
60
|
+
span.attributes['aiwg.provider.request_id'] = provider.requestId === null || provider.requestId === undefined
|
|
61
|
+
? null : sanitizeOpaqueValue(provider.requestId, 128);
|
|
62
|
+
span.attributes['gen_ai.response.model'] = provider.actualModel === null || provider.actualModel === undefined
|
|
63
|
+
? null : sanitizeOpaqueValue(provider.actualModel, 128);
|
|
64
|
+
for (const key of ['gen_ai.usage.input_tokens', 'gen_ai.usage.output_tokens']) {
|
|
65
|
+
span.provenance[key] = span.attributes[key] === null ? 'unknown' : 'provider-fact';
|
|
66
|
+
}
|
|
67
|
+
span.provenance['aiwg.usage.cost_usd'] = span.attributes['aiwg.usage.cost_usd'] === null
|
|
68
|
+
? 'unknown' : (options.costProvenance ?? 'provider-fact');
|
|
69
|
+
span.provenance['aiwg.provider.request_id'] = provider.requestId === null || provider.requestId === undefined ? 'unknown' : 'provider-fact';
|
|
70
|
+
span.provenance['gen_ai.response.model'] = provider.actualModel === null || provider.actualModel === undefined ? 'unknown' : 'provider-fact';
|
|
71
|
+
}
|
|
72
|
+
build() {
|
|
73
|
+
if (!this.root)
|
|
74
|
+
throw new Error('Cannot build an empty decision trace');
|
|
75
|
+
return { schemaVersion: DECISION_TELEMETRY_SCHEMA_VERSION, traceId: this.root.traceId, spans: structuredClone(this.spans) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Emit transport-attempt spans from the durable accounting authority. Each
|
|
80
|
+
* consumed provider attempt owns its usage exactly once, including failed
|
|
81
|
+
* retry/fallback attempts. Per-answer allocations remain off these spans.
|
|
82
|
+
*
|
|
83
|
+
* `started` holds live `decision.batch.request` spans keyed by attempt ordinal.
|
|
84
|
+
* The evaluator opens them before dispatch, so the propagated `traceparent`
|
|
85
|
+
* and the live start time belong to the span that later receives the receipt's
|
|
86
|
+
* accounting. Attempts without a live span are reconstructed from the receipt.
|
|
87
|
+
*/
|
|
88
|
+
export function recordBatchReceiptTrace(builder, receipt, parent, started = new Map()) {
|
|
89
|
+
const spans = [];
|
|
90
|
+
for (const attempt of receipt.attempts) {
|
|
91
|
+
const live = started.get(attempt.ordinal);
|
|
92
|
+
if (attempt.status === 'not-sent') {
|
|
93
|
+
// Nothing crossed the transport, so no usage belongs to this span.
|
|
94
|
+
if (live) {
|
|
95
|
+
builder.annotate(live, { 'aiwg.decision.status': attempt.status }, { 'aiwg.decision.status': 'client-derived' });
|
|
96
|
+
builder.endSpan(live, 'error', attempt.completedAtEpochMs ?? receipt.updatedAtEpochMs);
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const cost = batchCost(attempt.cost);
|
|
101
|
+
const attributes = {
|
|
102
|
+
'aiwg.batch.id': receipt.batchId,
|
|
103
|
+
'aiwg.batch.plan_digest': receipt.plan.planDigest,
|
|
104
|
+
'aiwg.batch.partition_id': receipt.plan.partitionId,
|
|
105
|
+
'aiwg.batch.item_count': receipt.questionIds.length,
|
|
106
|
+
'aiwg.batch.result_count': receipt.answerReferences.length,
|
|
107
|
+
'aiwg.attempt.ordinal': attempt.ordinal,
|
|
108
|
+
'aiwg.adapter.id': attempt.adapterId,
|
|
109
|
+
'aiwg.adapter.version': attempt.adapterVersion,
|
|
110
|
+
'gen_ai.request.model': attempt.requestedModel,
|
|
111
|
+
'aiwg.decision.status': attempt.status,
|
|
112
|
+
'aiwg.route.fallback': attempt.fallbackFromAttemptOrdinal !== null,
|
|
113
|
+
};
|
|
114
|
+
const provenance = Object.fromEntries(Object.keys(attributes).map(key => [key, 'client-derived']));
|
|
115
|
+
const span = live ?? builder.startSpan('decision.batch.request', {
|
|
116
|
+
parent, startTimeUnixMs: attempt.dispatchedAtEpochMs ?? receipt.createdAtEpochMs,
|
|
117
|
+
});
|
|
118
|
+
builder.annotate(span, attributes, provenance);
|
|
119
|
+
builder.recordBatchUsage(span, receipt.batchId, {
|
|
120
|
+
inputTokens: attempt.usage.inputTokens,
|
|
121
|
+
outputTokens: attempt.usage.outputTokens,
|
|
122
|
+
costUsd: cost.amountUsd,
|
|
123
|
+
}, { requestId: attempt.providerRequestId, actualModel: attempt.actualModel }, {
|
|
124
|
+
accountingKey: `${receipt.batchId}:${attempt.ordinal}`,
|
|
125
|
+
costProvenance: cost.provenance,
|
|
126
|
+
});
|
|
127
|
+
builder.endSpan(span, attempt.status === 'succeeded' ? 'ok' : 'error', attempt.completedAtEpochMs ?? attempt.dispatchedAtEpochMs ?? receipt.updatedAtEpochMs);
|
|
128
|
+
spans.push(span);
|
|
129
|
+
}
|
|
130
|
+
return spans;
|
|
131
|
+
}
|
|
132
|
+
function batchCost(cost) {
|
|
133
|
+
switch (cost.kind) {
|
|
134
|
+
case 'provider-authoritative': return { amountUsd: cost.amountMicros / 1_000_000, provenance: 'provider-fact' };
|
|
135
|
+
case 'client-derived': return { amountUsd: cost.amountMicros / 1_000_000, provenance: 'client-derived' };
|
|
136
|
+
case 'bounded-unknown':
|
|
137
|
+
case 'unknown': return { amountUsd: null, provenance: 'unknown' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function estimatedUsageAllocation(total, weights, method, version) {
|
|
141
|
+
if (!Number.isSafeInteger(total) || total < 0 || weights.length === 0 || weights.some(weight => !Number.isFinite(weight) || weight < 0))
|
|
142
|
+
throw new Error('Invalid usage allocation input');
|
|
143
|
+
const sum = weights.reduce((left, right) => left + right, 0);
|
|
144
|
+
if (sum <= 0 || !method || !version)
|
|
145
|
+
throw new Error('Usage allocation requires positive weights and versioned method');
|
|
146
|
+
const raw = weights.map(weight => total * weight / sum);
|
|
147
|
+
const values = raw.map(Math.floor);
|
|
148
|
+
let remainder = total - values.reduce((left, right) => left + right, 0);
|
|
149
|
+
const order = raw.map((value, index) => ({ index, fraction: value - values[index] }))
|
|
150
|
+
.sort((left, right) => right.fraction - left.fraction || left.index - right.index);
|
|
151
|
+
for (let index = 0; index < remainder; index += 1)
|
|
152
|
+
values[order[index].index] += 1;
|
|
153
|
+
remainder = total - values.reduce((left, right) => left + right, 0);
|
|
154
|
+
if (remainder !== 0)
|
|
155
|
+
throw new Error('Usage allocation did not reconcile');
|
|
156
|
+
return values.map(value => ({ value, provenance: 'estimate', method, version }));
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=trace.js.map
|
|
@@ -41,6 +41,12 @@ function getValidators() {
|
|
|
41
41
|
if (validators)
|
|
42
42
|
return validators;
|
|
43
43
|
const ajv = new Ajv2020({ strict: false, allErrors: true, validateFormats: false });
|
|
44
|
+
const admissionSchema = JSON.parse(readFileSync(resolve(schemaRoot(), 'DecisionAdmissionEvidence.v1.schema.json'), 'utf8'));
|
|
45
|
+
const contextEvidenceSchema = JSON.parse(readFileSync(resolve(schemaRoot(), 'DecisionContextEvidence.v1.schema.json'), 'utf8'));
|
|
46
|
+
const providerPrefixSchema = JSON.parse(readFileSync(resolve(schemaRoot(), 'DecisionProviderPrefixEvidence.v1.schema.json'), 'utf8'));
|
|
47
|
+
ajv.addSchema(admissionSchema);
|
|
48
|
+
ajv.addSchema(contextEvidenceSchema);
|
|
49
|
+
ajv.addSchema(providerPrefixSchema);
|
|
44
50
|
validators = new Map();
|
|
45
51
|
for (const [kind, filename] of Object.entries(schemaFiles)) {
|
|
46
52
|
const schema = JSON.parse(readFileSync(resolve(schemaRoot(), filename), 'utf8'));
|
|
@@ -103,6 +109,83 @@ export function assertDecisionWriterVersion(value, semantic) {
|
|
|
103
109
|
throw new DecisionValidationError(`${semantic} requires ${DECISION_API_VERSION_STRUCTURED}`);
|
|
104
110
|
}
|
|
105
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Result evidence that exists only in the v1alpha2 DecisionResult and RulesetResult
|
|
114
|
+
* schemas, with the contract addition that owns each field. v1alpha1 results never
|
|
115
|
+
* carry these fields; writers must label any result that does as v1alpha2.
|
|
116
|
+
*/
|
|
117
|
+
export const DECISION_RESULT_V1ALPHA2_FIELDS = [
|
|
118
|
+
{ field: 'batchResult', scope: 'decision', owner: 'D07', semantic: 'batch-receipt' },
|
|
119
|
+
{ field: 'batch', scope: 'attempt', owner: 'D04', semantic: 'batch-provenance' },
|
|
120
|
+
{ field: 'admission', scope: 'attempt', owner: 'D05', semantic: 'admission-evidence' },
|
|
121
|
+
{ field: 'context', scope: 'decision', owner: 'D06', semantic: 'context-evidence' },
|
|
122
|
+
{ field: 'context', scope: 'ruleset', owner: 'D06', semantic: 'context-evidence' },
|
|
123
|
+
{ field: 'contextFailure', scope: 'ruleset', owner: 'D06', semantic: 'context-evidence' },
|
|
124
|
+
{ field: 'providerPrefix', scope: 'attempt', owner: 'D30', semantic: 'provider-prefix-evidence' },
|
|
125
|
+
{ field: 'acceptance', scope: 'decision', owner: 'D08', semantic: 'acceptance-uncertainty' },
|
|
126
|
+
{ field: 'calibrationCompatibility', scope: 'decision', owner: 'D09', semantic: 'calibration-pin' },
|
|
127
|
+
{ field: 'cache', scope: 'ruleset', owner: 'D15', semantic: 'result-cache-receipt' },
|
|
128
|
+
{ field: 'projection', scope: 'ruleset', owner: 'D10', semantic: 'projection-opt-out' },
|
|
129
|
+
];
|
|
130
|
+
/** JSON paths of every v1alpha2-only field present in a DecisionResult or RulesetResult. */
|
|
131
|
+
export function decisionResultV1Alpha2Fields(value) {
|
|
132
|
+
const found = [];
|
|
133
|
+
const spec = (document) => {
|
|
134
|
+
const inner = document && typeof document === 'object' ? document.spec : undefined;
|
|
135
|
+
return inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : undefined;
|
|
136
|
+
};
|
|
137
|
+
const visitDecision = (document, path) => {
|
|
138
|
+
const decision = spec(document);
|
|
139
|
+
if (!decision)
|
|
140
|
+
return;
|
|
141
|
+
for (const entry of DECISION_RESULT_V1ALPHA2_FIELDS) {
|
|
142
|
+
if (entry.scope === 'decision' && Object.hasOwn(decision, entry.field))
|
|
143
|
+
found.push(`${path}.spec.${entry.field}`);
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(decision.attempts))
|
|
146
|
+
return;
|
|
147
|
+
decision.attempts.forEach((attempt, index) => {
|
|
148
|
+
if (!attempt || typeof attempt !== 'object')
|
|
149
|
+
return;
|
|
150
|
+
for (const entry of DECISION_RESULT_V1ALPHA2_FIELDS) {
|
|
151
|
+
if (entry.scope === 'attempt' && Object.hasOwn(attempt, entry.field))
|
|
152
|
+
found.push(`${path}.spec.attempts[${index}].${entry.field}`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
const kind = value && typeof value === 'object' ? value.kind : undefined;
|
|
157
|
+
if (kind === 'DecisionResult')
|
|
158
|
+
visitDecision(value, '$');
|
|
159
|
+
if (kind === 'RulesetResult') {
|
|
160
|
+
const ruleset = spec(value);
|
|
161
|
+
for (const entry of DECISION_RESULT_V1ALPHA2_FIELDS) {
|
|
162
|
+
if (entry.scope === 'ruleset' && ruleset && Object.hasOwn(ruleset, entry.field))
|
|
163
|
+
found.push(`$.spec.${entry.field}`);
|
|
164
|
+
}
|
|
165
|
+
const evaluations = ruleset?.evaluations;
|
|
166
|
+
if (evaluations && typeof evaluations === 'object' && !Array.isArray(evaluations)) {
|
|
167
|
+
for (const [alias, evaluation] of Object.entries(evaluations))
|
|
168
|
+
visitDecision(evaluation, `$.spec.evaluations.${alias}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return found;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Writer gate for every runtime DecisionResult/RulesetResult writer, including
|
|
175
|
+
* invocation receipt payloads. A result that carries v1alpha2-only evidence must be
|
|
176
|
+
* labelled v1alpha2; a v1alpha1 label on such a result is rejected before validation.
|
|
177
|
+
*/
|
|
178
|
+
export function assertDecisionResultWriterVersion(value) {
|
|
179
|
+
const kind = value && typeof value === 'object' ? value.kind : undefined;
|
|
180
|
+
if (kind !== 'DecisionResult' && kind !== 'RulesetResult') {
|
|
181
|
+
throw new DecisionValidationError('Result writer accepts only DecisionResult or RulesetResult documents');
|
|
182
|
+
}
|
|
183
|
+
const fields = decisionResultV1Alpha2Fields(value);
|
|
184
|
+
if (fields.length && value.apiVersion !== DECISION_API_VERSION_STRUCTURED) {
|
|
185
|
+
throw new DecisionValidationError(`${kind} carrying ${fields.join(', ')} requires ${DECISION_API_VERSION_STRUCTURED}`);
|
|
186
|
+
}
|
|
187
|
+
validateDecisionDocument(value);
|
|
188
|
+
}
|
|
106
189
|
/** A rollback reader may inspect v1alpha2, but cannot execute or rewrite it. */
|
|
107
190
|
export function readDecisionDocumentForRollback(value, mode) {
|
|
108
191
|
validateDecisionDocument(value);
|
|
@@ -121,12 +204,50 @@ function deepFreeze(value) {
|
|
|
121
204
|
deepFreeze(child);
|
|
122
205
|
Object.freeze(value);
|
|
123
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Compiled caller-schema validators, keyed by the sha256 of the schema's
|
|
209
|
+
* canonical JSON, so equal content shares a validator and different content
|
|
210
|
+
* never does. Each miss compiles a detached clone in a fresh Ajv instance, so
|
|
211
|
+
* later mutation of the caller's object or another schema's `$id` cannot leak
|
|
212
|
+
* into a cached entry. Bounded LRU; failed compiles are never cached.
|
|
213
|
+
*/
|
|
214
|
+
const SCHEMA_CACHE_LIMIT = 256;
|
|
215
|
+
const strictSchemaDigests = new Map();
|
|
216
|
+
const permissiveValidators = new Map();
|
|
217
|
+
function schemaDigest(schema) {
|
|
218
|
+
try {
|
|
219
|
+
return createHash('sha256').update(canonicalJson(schema)).digest('hex');
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
} // Not canonical JSON (for example an undefined member): compile uncached.
|
|
224
|
+
}
|
|
225
|
+
/** Only a cacheable (canonical JSON) schema needs a private copy. */
|
|
226
|
+
function detached(schema, digest) {
|
|
227
|
+
return digest === null ? schema : structuredClone(schema);
|
|
228
|
+
}
|
|
229
|
+
function cached(cache, digest, build) {
|
|
230
|
+
if (digest !== null) {
|
|
231
|
+
const hit = cache.get(digest);
|
|
232
|
+
if (hit !== undefined) {
|
|
233
|
+
cache.delete(digest);
|
|
234
|
+
cache.set(digest, hit);
|
|
235
|
+
return hit;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const value = build();
|
|
239
|
+
if (digest !== null) {
|
|
240
|
+
cache.set(digest, value);
|
|
241
|
+
if (cache.size > SCHEMA_CACHE_LIMIT)
|
|
242
|
+
cache.delete(cache.keys().next().value);
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
124
246
|
export function validateAgainstSchema(schema, value, label) {
|
|
125
|
-
assertLocalSchema(schema, label);
|
|
126
|
-
const ajv = new Ajv2020({ strict: false, allErrors: true, validateFormats: false });
|
|
247
|
+
const digest = assertLocalSchema(schema, label);
|
|
127
248
|
let validate;
|
|
128
249
|
try {
|
|
129
|
-
validate =
|
|
250
|
+
validate = cached(permissiveValidators, digest, () => new Ajv2020({ strict: false, allErrors: true, validateFormats: false }).compile(detached(schema, digest)));
|
|
130
251
|
}
|
|
131
252
|
catch (error) {
|
|
132
253
|
throw new DecisionValidationError(`${label} is not a supported local draft 2020-12 schema: ${errorMessage(error)}`);
|
|
@@ -182,6 +303,9 @@ export function validateBinding(binding, ruleset) {
|
|
|
182
303
|
}
|
|
183
304
|
for (const [alias, evaluation] of Object.entries(binding.spec.evaluations)) {
|
|
184
305
|
for (const target of evaluation.targets) {
|
|
306
|
+
if (target.acceptance.mode === 'primitive-policy' && binding.apiVersion !== DECISION_API_VERSION_STRUCTURED) {
|
|
307
|
+
throw new DecisionValidationError(`${alias} primitive-aware acceptance requires ${DECISION_API_VERSION_STRUCTURED}`);
|
|
308
|
+
}
|
|
185
309
|
if (target.retry.maxDelayMs < target.retry.initialDelayMs) {
|
|
186
310
|
throw new DecisionValidationError(`${alias} retry maxDelayMs must be >= initialDelayMs`);
|
|
187
311
|
}
|
|
@@ -267,13 +391,19 @@ function assertLocalSchema(schema, label) {
|
|
|
267
391
|
stack.delete(value);
|
|
268
392
|
};
|
|
269
393
|
visit(schema, new Set(), 0);
|
|
394
|
+
// The structural walk above runs on every call; only the strict compile is cached.
|
|
395
|
+
const digest = schemaDigest(schema);
|
|
270
396
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
397
|
+
cached(strictSchemaDigests, digest, () => {
|
|
398
|
+
new Ajv2020({ strictSchema: true, strictTypes: false, strictTuples: false,
|
|
399
|
+
strictRequired: false, validateFormats: false }).compile(detached(schema, digest));
|
|
400
|
+
return true;
|
|
401
|
+
});
|
|
273
402
|
}
|
|
274
403
|
catch (error) {
|
|
275
404
|
throw new DecisionValidationError(`${label} uses an unsupported schema construct: ${errorMessage(error)}`);
|
|
276
405
|
}
|
|
406
|
+
return digest;
|
|
277
407
|
}
|
|
278
408
|
function assertPredicateAliases(predicate, aliases) {
|
|
279
409
|
if ('all' in predicate)
|