@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
|
@@ -1,18 +1,156 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
2
3
|
import { composeRuleset } from './compose.js';
|
|
4
|
+
import { applyPrimitiveAcceptance, validatePrimitiveAcceptancePolicy } from './acceptance.js';
|
|
3
5
|
import { DecisionPreDispatchError, decisionInvocationFingerprint, nextReceipt } from './receipts.js';
|
|
4
6
|
import { admitEntry, EntryAdmissionError } from './entry.js';
|
|
7
|
+
import { correlateAtomicBatch, decisionBatchQuestionId, planNativeDecisionBatches } from './batch.js';
|
|
8
|
+
import { AdmissionError, AdmissionProfileError, decisionAdmissionRegistry } from './admission.js';
|
|
9
|
+
import { runBoundedFair, SchedulerWaitError } from './scheduler.js';
|
|
10
|
+
import { allocateEstimatedUsage, batchAccountingTotals, batchEnforcementCostMicros, deriveCost } from './batch-receipts/accounting.js';
|
|
11
|
+
import { validOpaqueRequestId } from './batch-receipts/validate.js';
|
|
12
|
+
import { batchResultReference, newBatchReceipt, nextBatchReceipt } from './batch-receipts/receipt.js';
|
|
13
|
+
import { BatchRecordUnavailableError, BatchStoreIntegrityError, BatchStoreMigrationRequiredError } from './batch-receipts/protection.js';
|
|
14
|
+
import { prepareAdapterRequest } from './compile-cache/runtime.js';
|
|
15
|
+
import { providerPrefixEvidence } from './compile-cache/prefix.js';
|
|
16
|
+
import { digestCachedResult, RESULT_CACHE_KEY_VERSION } from './result-cache/index.js';
|
|
17
|
+
import { DecisionProjectionError, normalizeProjectionOrigin, projectDecisionState, } from './projection.js';
|
|
18
|
+
import { DecisionRuntimeTrace, runtimeTraceOf } from './telemetry/runtime.js';
|
|
19
|
+
import { assertContextQualified } from './context-qualification.js';
|
|
20
|
+
import { assertContextPlanCurrent, ContextPlanError, planDecisionContext, recordContextActualUsage, } from './context-plan.js';
|
|
5
21
|
import { DECISION_API_VERSION, DECISION_API_VERSION_STRUCTURED } from './types.js';
|
|
6
|
-
import { artifactPin, assertArtifactPin, DecisionValidationError, resolveJsonPointer, validateAgainstSchema, validateBinding, validateDecisionValue, validateDefinition, validateDistribution, validateRuleset, } from './validate.js';
|
|
22
|
+
import { artifactPin, assertArtifactPin, assertDecisionResultWriterVersion, DecisionValidationError, resolveJsonPointer, validateAgainstSchema, validateBinding, validateDecisionValue, validateDefinition, validateDistribution, validateRuleset, } from './validate.js';
|
|
7
23
|
const RETRIABLE = new Set([
|
|
8
24
|
'timeout', 'network-transient', 'rate-limited', 'overloaded', 'service-error',
|
|
9
25
|
]);
|
|
10
26
|
export async function evaluateDecisionRuleset(request) {
|
|
27
|
+
// Telemetry hooks below record live spans through runtimeTraceOf(request); none can change the result.
|
|
28
|
+
const { request: traced, trace } = DecisionRuntimeTrace.begin(request);
|
|
29
|
+
const result = traced.resultCache?.policy.enabled
|
|
30
|
+
? await evaluateWithResultCache(traced) : await evaluateDecisionRulesetInternal(traced);
|
|
31
|
+
await trace?.finish(result);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
/** A cache hit is a historical result accompanied by a NEW caller receipt, not a fresh adapter attempt. */
|
|
35
|
+
async function evaluateWithResultCache(request) {
|
|
36
|
+
const config = request.resultCache;
|
|
37
|
+
if (!config.policy.sideEffectFree || !request.receiptStore || !request.calibrationPin || !request.policyPin
|
|
38
|
+
|| !config.recordCallerReceipt) {
|
|
39
|
+
throw new Error('Result cache requires side-effect-free policy, durable receipts, policy and calibration pins');
|
|
40
|
+
}
|
|
41
|
+
// Exact invocation replay wins over semantic reuse, including fingerprint mismatch protection.
|
|
42
|
+
const projectId = request.receiptProjectId ?? 'default';
|
|
43
|
+
if (projectId !== config.actor.projectId)
|
|
44
|
+
throw new Error('Cache actor and receipt project must match');
|
|
45
|
+
if (await request.receiptStore.read(request.invocationId, projectId))
|
|
46
|
+
return evaluateDecisionRulesetInternal(request);
|
|
47
|
+
// The experimental integration is deliberately narrower than the cache service: no
|
|
48
|
+
// batching, fallback, retries or context-dependent scheduling that can vary per caller.
|
|
49
|
+
if (request.ruleset.spec.evaluations.length !== 1 || request.batching?.enabled || request.context || request.scheduler?.enabled) {
|
|
50
|
+
throw new Error('Result cache supports only a single unbatched, unscheduled evaluation');
|
|
51
|
+
}
|
|
52
|
+
const snapshot = { ...request, ruleset: structuredClone(request.ruleset), binding: structuredClone(request.binding),
|
|
53
|
+
definitions: structuredClone(request.definitions), input: structuredClone(request.input) };
|
|
54
|
+
admitEntry(snapshot.input);
|
|
55
|
+
validateRuleset(snapshot.ruleset);
|
|
56
|
+
validateBinding(snapshot.binding, snapshot.ruleset);
|
|
57
|
+
validateAgainstSchema(snapshot.ruleset.spec.inputSchema, snapshot.input, 'ruleset input');
|
|
58
|
+
const [item] = resolveDefinitions(snapshot);
|
|
59
|
+
const targets = snapshot.binding.spec.evaluations[item.alias]?.targets;
|
|
60
|
+
if (!targets || targets.length !== 1 || targets[0].retry.maxRetries !== 0) {
|
|
61
|
+
throw new Error('Result cache requires one target without retry or fallback');
|
|
62
|
+
}
|
|
63
|
+
const target = targets[0];
|
|
64
|
+
const projected = await projectRuntimeInput(snapshot, item.alias, target, item.input, snapshot.adapters[target.adapter]);
|
|
65
|
+
// The key normalizes Unicode. The adapter must receive that same normalized
|
|
66
|
+
// representation or two byte-distinct prompts could alias to one cache entry.
|
|
67
|
+
assertNormalizedCacheInput(projected.input);
|
|
68
|
+
const identity = config.identityFor({ alias: item.alias, definition: structuredClone(item.definition),
|
|
69
|
+
target: structuredClone(target), projectedInput: structuredClone(projected.input) });
|
|
70
|
+
assertRuntimeCacheIdentity(identity, snapshot, item.pin, target, projected.input);
|
|
71
|
+
const alias = identity.modelCompatibility.mode === 'alias' ? identity.modelCompatibility : null;
|
|
72
|
+
if (alias && !config.verifyAliasSnapshot)
|
|
73
|
+
throw new Error('Alias cache requires a registry compatibility verifier');
|
|
74
|
+
const approved = alias ? await config.verifyAliasSnapshot(structuredClone(alias)) : true;
|
|
75
|
+
const outcome = await config.service.evaluate({ actor: config.actor,
|
|
76
|
+
policy: approved ? config.policy : { ...config.policy, enabled: false }, identity,
|
|
77
|
+
callerInvocationId: snapshot.invocationId, nowEpochMs: snapshot.now?.() }, async () => {
|
|
78
|
+
const original = await evaluateDecisionRulesetInternal(snapshot);
|
|
79
|
+
const attempt = original.spec.evaluations[item.alias]?.spec.attempts[0];
|
|
80
|
+
const receipt = await snapshot.receiptStore.read(snapshot.invocationId, projectId);
|
|
81
|
+
// Never publish an incomplete or uncertain receipt, even if an adapter returned a value.
|
|
82
|
+
const successful = original.spec.status === 'completed' && original.spec.evaluations[item.alias]?.spec.status === 'success'
|
|
83
|
+
&& original.spec.evaluations[item.alias]?.spec.attempts.length === 1 && attempt?.actualModel
|
|
84
|
+
&& attempt.remoteExecution !== 'unknown' && receipt?.state === 'completed';
|
|
85
|
+
return { result: original, resultDigest: digestCachedResult(original),
|
|
86
|
+
sourceInvocationId: snapshot.invocationId, sourceReceiptId: snapshot.invocationId,
|
|
87
|
+
evaluatedAtEpochMs: receipt?.completedAtEpochMs ?? snapshot.now?.() ?? Date.now(),
|
|
88
|
+
actualModel: attempt?.actualModel ?? '', uncertainty: (original.spec.evaluations[item.alias]?.spec.uncertainty ?? null),
|
|
89
|
+
calibrationStatus: original.spec.evaluations[item.alias]?.spec.calibrationCompatibility ? 'compatible' : 'pinned',
|
|
90
|
+
durationMs: attempt?.durationMs ?? 0, usage: attempt?.usage ?? { inputTokens: null, outputTokens: null, costUsd: null },
|
|
91
|
+
status: successful ? 'success' : 'terminal-failure', failureReason: successful ? 'none' : original.spec.reason };
|
|
92
|
+
});
|
|
93
|
+
if (outcome.receipt.disposition === 'cache-hit') {
|
|
94
|
+
const source = await snapshot.receiptStore.read(outcome.receipt.sourceInvocationId, projectId);
|
|
95
|
+
if (source?.state !== 'completed' || !source.result
|
|
96
|
+
|| digestCachedResult(source.result) !== digestCachedResult(outcome.evidence.result)
|
|
97
|
+
|| source.completedAtEpochMs !== outcome.receipt.originalEvaluatedAtEpochMs) {
|
|
98
|
+
throw new Error('Cached source receipt could not be verified');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const historical = structuredClone(outcome.evidence.result);
|
|
102
|
+
const released = { ...historical, spec: { ...historical.spec, cache: structuredClone(outcome.receipt) } };
|
|
103
|
+
// `spec.cache` is D15 evidence defined only by the v1alpha2 schema. A historical
|
|
104
|
+
// v1alpha1 entry cannot carry it, so it is refused before the caller receipt is persisted.
|
|
105
|
+
assertDecisionResultWriterVersion(released);
|
|
106
|
+
await config.recordCallerReceipt(structuredClone(outcome.receipt));
|
|
107
|
+
return released;
|
|
108
|
+
}
|
|
109
|
+
function assertNormalizedCacheInput(value) {
|
|
110
|
+
if (typeof value === 'string' && value !== value.normalize('NFC'))
|
|
111
|
+
throw new Error('Cache input must be NFC-normalized before dispatch');
|
|
112
|
+
if (Array.isArray(value))
|
|
113
|
+
value.forEach(assertNormalizedCacheInput);
|
|
114
|
+
else if (value !== null && typeof value === 'object') {
|
|
115
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
116
|
+
if (key !== key.normalize('NFC'))
|
|
117
|
+
throw new Error('Cache input keys must be NFC-normalized before dispatch');
|
|
118
|
+
assertNormalizedCacheInput(entry);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function assertRuntimeCacheIdentity(identity, request, definition, target, projected) {
|
|
123
|
+
const sha = (value) => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
124
|
+
const pinMatches = (left, right) => digestCachedResult(left) === digestCachedResult(right);
|
|
125
|
+
if (identity.keyVersion !== RESULT_CACHE_KEY_VERSION || !pinMatches(identity.definition, definition)
|
|
126
|
+
|| !pinMatches(identity.ruleset, artifactPin(request.ruleset))
|
|
127
|
+
|| !pinMatches(identity.binding, artifactPin(request.binding))
|
|
128
|
+
|| identity.adapter.id !== target.adapter || identity.adapter.version !== target.adapterVersion
|
|
129
|
+
|| identity.requestedModel !== target.model || identity.primitive !== Object.values(request.definitions).find(value => value.metadata.id === definition.id)?.spec.answer.kind
|
|
130
|
+
|| identity.acceptancePolicyDigest !== digestCachedResult(target.acceptance)
|
|
131
|
+
|| identity.calibrationDigest !== request.calibrationPin?.digest
|
|
132
|
+
|| identity.runtimePolicyDigest !== request.policyPin?.digest
|
|
133
|
+
|| digestCachedResult(identity.projectedInput) !== digestCachedResult(projected)
|
|
134
|
+
|| ![identity.promptDigest, identity.subjectIdentityDigest, identity.projectionPolicyDigest,
|
|
135
|
+
identity.egressPolicyDigest].every(sha)
|
|
136
|
+
|| !identity.backend || !identity.capabilityMode
|
|
137
|
+
|| (identity.modelCompatibility.mode === 'pinned' && identity.modelCompatibility.actualModel !== target.model)
|
|
138
|
+
|| (identity.modelCompatibility.mode === 'alias' && identity.modelCompatibility.alias !== target.model)) {
|
|
139
|
+
throw new Error('Invalid semantic result-cache identity');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function evaluateDecisionRulesetInternal(request) {
|
|
143
|
+
const result = await evaluateDecisionRulesetUngated(request);
|
|
144
|
+
assertDecisionResultWriterVersion(result);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
async function evaluateDecisionRulesetUngated(request) {
|
|
11
148
|
let rulesetPin;
|
|
12
149
|
let bindingPin;
|
|
13
|
-
let base;
|
|
150
|
+
let base = invalidResultBase(request);
|
|
14
151
|
let resolved;
|
|
15
152
|
let admissionStage = 'artifact';
|
|
153
|
+
let contextPlan;
|
|
16
154
|
try {
|
|
17
155
|
admitEntry(request.ruleset);
|
|
18
156
|
admitEntry(request.binding);
|
|
@@ -20,6 +158,7 @@ export async function evaluateDecisionRuleset(request) {
|
|
|
20
158
|
admissionStage = 'input';
|
|
21
159
|
admitEntry(request.input);
|
|
22
160
|
admissionStage = 'artifact';
|
|
161
|
+
validateEgressBoundaryConfiguration(request);
|
|
23
162
|
validateRuleset(request.ruleset);
|
|
24
163
|
rulesetPin = artifactPin(request.ruleset);
|
|
25
164
|
bindingPin = artifactPin(request.binding);
|
|
@@ -31,16 +170,41 @@ export async function evaluateDecisionRuleset(request) {
|
|
|
31
170
|
};
|
|
32
171
|
base = resultBase(request, rulesetPin, bindingPin);
|
|
33
172
|
validateBinding(request.binding, request.ruleset);
|
|
173
|
+
validateSchedulerPolicy(request);
|
|
34
174
|
validateAgainstSchema(request.ruleset.spec.inputSchema, request.input, 'ruleset input');
|
|
35
175
|
resolved = resolveDefinitions(request);
|
|
176
|
+
contextPlan = prepareContextPlan(request, resolved);
|
|
177
|
+
for (const item of resolved) {
|
|
178
|
+
for (const target of request.binding.spec.evaluations[item.alias].targets) {
|
|
179
|
+
if (target.acceptance.mode === 'primitive-policy') {
|
|
180
|
+
if (request.binding.apiVersion !== DECISION_API_VERSION_STRUCTURED) {
|
|
181
|
+
throw new DecisionValidationError('primitive-aware acceptance requires decision.aiwg.io/v1alpha2');
|
|
182
|
+
}
|
|
183
|
+
validatePrimitiveAcceptancePolicy(target.acceptance, item.definition);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
36
187
|
}
|
|
37
188
|
catch (error) {
|
|
38
189
|
const reason = error instanceof EntryAdmissionError && admissionStage === 'input'
|
|
39
190
|
? 'invalid-input' : classifyValidationFailure(error);
|
|
40
|
-
|
|
191
|
+
const failed = failureResult(base, reason);
|
|
192
|
+
return error instanceof ContextPlanError && request.context
|
|
193
|
+
? { ...failed, spec: { ...failed.spec, contextFailure: contextFailureEvidence(error) } } : failed;
|
|
194
|
+
}
|
|
195
|
+
if (contextPlan)
|
|
196
|
+
base = withRulesetContext(base, contextPlan, []);
|
|
197
|
+
runtimeTraceOf(request)?.validated(false);
|
|
198
|
+
const batchPolicy = request.batchReceipts;
|
|
199
|
+
if (batchPolicy?.unknownCostBound && (!Number.isSafeInteger(batchPolicy.unknownCostBound.upperBoundMicros)
|
|
200
|
+
|| batchPolicy.unknownCostBound.upperBoundMicros < 0 || !batchPolicy.unknownCostBound.policyId
|
|
201
|
+
|| !batchPolicy.unknownCostBound.policyVersion))
|
|
202
|
+
return failureResult(base, 'invalid-definition');
|
|
203
|
+
if (batchPolicy?.maxCostMicros !== undefined && (!Number.isSafeInteger(batchPolicy.maxCostMicros)
|
|
204
|
+
|| batchPolicy.maxCostMicros < 0 || !batchPolicy.unknownCostBound
|
|
205
|
+
|| batchPolicy.unknownCostBound.upperBoundMicros > batchPolicy.maxCostMicros)) {
|
|
206
|
+
return failureResult(base, 'budget-exhausted');
|
|
41
207
|
}
|
|
42
|
-
if (!base)
|
|
43
|
-
return failureResult(invalidResultBase(request), 'invalid-definition');
|
|
44
208
|
const fingerprint = decisionInvocationFingerprint({
|
|
45
209
|
invocationId: request.invocationId,
|
|
46
210
|
value: request.input,
|
|
@@ -55,8 +219,10 @@ export async function evaluateDecisionRuleset(request) {
|
|
|
55
219
|
const projectId = request.receiptProjectId ?? 'default';
|
|
56
220
|
if (request.receiptStore) {
|
|
57
221
|
try {
|
|
58
|
-
const
|
|
222
|
+
const traceParent = runtimeTraceOf(request)?.traceParent;
|
|
223
|
+
const acquisition = await request.receiptStore.acquire(request.invocationId, projectId, fingerprint, traceParent ? { traceParent } : {});
|
|
59
224
|
if (!acquisition.owner) {
|
|
225
|
+
runtimeTraceOf(request)?.linkOrigin(acquisition.receipt.traceParent, 'continuation', {});
|
|
60
226
|
if (acquisition.receipt.fingerprint !== fingerprint)
|
|
61
227
|
return failureResult(base, 'replay-mismatch');
|
|
62
228
|
if (acquisition.receipt.state === 'completed')
|
|
@@ -120,7 +286,9 @@ export async function evaluateDecisionRuleset(request) {
|
|
|
120
286
|
const totalTimeout = AbortSignal.timeout(request.binding.spec.totalTimeoutMs);
|
|
121
287
|
const totalAbort = AbortSignal.any([request.signal ?? new AbortController().signal, totalTimeout]);
|
|
122
288
|
const evaluations = structuredClone(receipt?.evaluations ?? {});
|
|
289
|
+
const contextUsage = contextUsageFromEvaluations(evaluations);
|
|
123
290
|
let attemptsUsed = Object.values(evaluations).reduce((sum, result) => sum + result.spec.attempts.length, 0);
|
|
291
|
+
const attemptBudget = new AttemptBudget(request.binding.spec.maxAttempts, attemptsUsed);
|
|
124
292
|
try {
|
|
125
293
|
if (reconciled && receipt?.pending) {
|
|
126
294
|
const pending = receipt.pending;
|
|
@@ -129,8 +297,11 @@ export async function evaluateDecisionRuleset(request) {
|
|
|
129
297
|
if (!item || !target)
|
|
130
298
|
throw new RemoteUncertainError();
|
|
131
299
|
let observation;
|
|
300
|
+
let calibrationCompatibility;
|
|
132
301
|
try {
|
|
133
|
-
|
|
302
|
+
const calibrated = normalizeObservationForRuntime({ request, item, target, observation: reconciled, now });
|
|
303
|
+
observation = calibrated.observation;
|
|
304
|
+
calibrationCompatibility = calibrated.calibrationCompatibility;
|
|
134
305
|
}
|
|
135
306
|
catch {
|
|
136
307
|
throw new RemoteUncertainError();
|
|
@@ -138,46 +309,345 @@ export async function evaluateDecisionRuleset(request) {
|
|
|
138
309
|
if (observation.status !== 'success')
|
|
139
310
|
throw new RemoteUncertainError();
|
|
140
311
|
const context = { request, item, rulesetPin, bindingPin, totalDeadline, signal: totalAbort, totalTimeout,
|
|
141
|
-
|
|
312
|
+
attemptBudget, now, advance };
|
|
142
313
|
const attempts = [...pending.attempts, toAttempt(target, pending.ordinal, observation, 0)];
|
|
143
|
-
evaluations[item.alias] = decisionResult(context, observation, attempts);
|
|
314
|
+
evaluations[item.alias] = decisionResult(context, observation, attempts, calibrationCompatibility);
|
|
144
315
|
attemptsUsed += attempts.length;
|
|
316
|
+
attemptBudget.consume(attempts.length);
|
|
145
317
|
await advance('observation-received', { evaluations, pending: null });
|
|
146
318
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
319
|
+
// Native batching may coexist with the invocation receipt only when a batch
|
|
320
|
+
// receipt owns the shared dispatch and its accounting before transport begins.
|
|
321
|
+
if ((!request.receiptStore || request.batchReceipts) && request.batching?.enabled) {
|
|
322
|
+
let batchCostSpent = 0;
|
|
323
|
+
// A terminal receipt must never point at values that cannot be recovered on replay.
|
|
324
|
+
if (request.batchReceipts && !request.batchReceipts.resultStore)
|
|
325
|
+
throw new ReceiptPersistenceError();
|
|
326
|
+
const candidates = [];
|
|
327
|
+
for (const item of resolved) {
|
|
328
|
+
if (evaluations[item.alias])
|
|
329
|
+
continue;
|
|
330
|
+
const configured = request.binding.spec.evaluations[item.alias];
|
|
331
|
+
const target = configured?.targets.length && configured.targets.length <= 2
|
|
332
|
+
&& (configured.targets.length === 1 || request.batchReceipts)
|
|
333
|
+
&& (!configured.targets[1] || configured.targets[1].adapter === configured.targets[0]?.adapter
|
|
334
|
+
&& configured.targets[1].adapterVersion === configured.targets[0]?.adapterVersion)
|
|
335
|
+
? configured.targets[0] : undefined;
|
|
336
|
+
const adapter = target ? request.adapters[target.adapter] : undefined;
|
|
337
|
+
if (!target || !adapter)
|
|
338
|
+
continue;
|
|
339
|
+
const capabilityFailure = await checkCapabilities(adapter, target, item.definition);
|
|
340
|
+
if (capabilityFailure || configured.targets[1] && await checkCapabilities(adapter, configured.targets[1], item.definition))
|
|
341
|
+
continue;
|
|
342
|
+
candidates.push({ alias: item.alias, definition: item.definition, input: item.input, target, adapter,
|
|
343
|
+
capabilities: await adapter.capabilities() });
|
|
154
344
|
}
|
|
155
|
-
const
|
|
156
|
-
request,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
345
|
+
for (const plan of request.context?.rollout?.mode === 'observe-only' ? []
|
|
346
|
+
: contextPartitionedBatchPlans(planNativeDecisionBatches(candidates, request.batching), contextPlan)) {
|
|
347
|
+
if (plan.candidates.some(candidate => evaluations[candidate.alias]))
|
|
348
|
+
continue;
|
|
349
|
+
const fallbackTargets = plan.candidates.map(candidate => request.binding.spec.evaluations[candidate.alias].targets[1]);
|
|
350
|
+
if (fallbackTargets.some(Boolean) && (fallbackTargets.some(target => !target)
|
|
351
|
+
|| fallbackTargets.some(target => canonicalJson(batchTargetIdentity(target))
|
|
352
|
+
!== canonicalJson(batchTargetIdentity(fallbackTargets[0])))))
|
|
353
|
+
continue;
|
|
354
|
+
const fallbackTarget = fallbackTargets[0];
|
|
355
|
+
if (attemptsUsed + plan.candidates.length > request.binding.spec.maxAttempts)
|
|
356
|
+
continue;
|
|
357
|
+
if (request.batchReceipts?.maxCostMicros !== undefined && batchCostSpent
|
|
358
|
+
+ request.batchReceipts.unknownCostBound.upperBoundMicros > request.batchReceipts.maxCostMicros) {
|
|
359
|
+
for (const candidate of plan.candidates) {
|
|
360
|
+
const item = resolved.find(value => value.alias === candidate.alias);
|
|
361
|
+
evaluations[item.alias] = decisionResult({ request, item, rulesetPin, bindingPin, totalDeadline, signal: totalAbort,
|
|
362
|
+
totalTimeout, attemptBudget, now, advance, contextPlan, contextUsage }, observationFailure('budget-exhausted'), []);
|
|
363
|
+
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const started = now();
|
|
367
|
+
const deadline = Math.min(totalDeadline, ...plan.candidates.map(candidate => started + candidate.target.timeoutMs));
|
|
368
|
+
const questionIds = plan.candidates.map(candidate => decisionBatchQuestionId(candidate.alias));
|
|
369
|
+
let observations = new Map();
|
|
370
|
+
let durableReceipt;
|
|
371
|
+
let batchReferences = new Map();
|
|
372
|
+
let dispatchCount = 0;
|
|
373
|
+
const batchSpans = [];
|
|
374
|
+
try {
|
|
375
|
+
const adapter = plan.candidates[0].adapter;
|
|
376
|
+
const projectedCandidates = await Promise.all(plan.candidates.map(async (candidate) => ({
|
|
377
|
+
candidate,
|
|
378
|
+
projected: await projectRuntimeInput(request, candidate.alias, candidate.target, candidate.input, adapter),
|
|
379
|
+
})));
|
|
380
|
+
// Both egress projections must be authorized before the first transport call.
|
|
381
|
+
const fallbackProjectedCandidates = fallbackTarget ? await Promise.all(plan.candidates.map(async (candidate) => ({
|
|
382
|
+
candidate, projected: await projectRuntimeInput(request, candidate.alias, fallbackTarget, candidate.input, adapter),
|
|
383
|
+
}))) : null;
|
|
384
|
+
if (request.batchReceipts) {
|
|
385
|
+
const partition = request.batchReceipts.contextPlan.partitions.find(candidate => sameStringSet(candidate.questionIds, questionIds));
|
|
386
|
+
if (!partition)
|
|
387
|
+
throw new ReceiptPersistenceError();
|
|
388
|
+
const initial = newBatchReceipt({
|
|
389
|
+
tenantId: request.batchReceipts.tenantId,
|
|
390
|
+
projectId: request.batchReceipts.projectId,
|
|
391
|
+
batchId: durableBatchId(request.invocationId, plan.groupId, partition.id),
|
|
392
|
+
invocationId: request.invocationId,
|
|
393
|
+
runId: request.runId,
|
|
394
|
+
contextPlan: request.batchReceipts.contextPlan,
|
|
395
|
+
partition,
|
|
396
|
+
nativeBatchGroupId: plan.groupId,
|
|
397
|
+
subjectHash: request.batchReceipts.subjectHash,
|
|
398
|
+
executionEnvelope: plan.candidates[0].capabilities.batch.executionEnvelope,
|
|
399
|
+
nowEpochMs: started,
|
|
400
|
+
traceParent: runtimeTraceOf(request)?.traceParent,
|
|
401
|
+
});
|
|
402
|
+
const acquired = await request.batchReceipts.store.acquire(initial);
|
|
403
|
+
durableReceipt = acquired.receipt;
|
|
404
|
+
if (!acquired.owner) {
|
|
405
|
+
if (durableReceipt.status === 'completed') {
|
|
406
|
+
const replayed = await request.batchReceipts.resultStore?.readMany(durableReceipt);
|
|
407
|
+
if (!replayed || replayed.size !== questionIds.length)
|
|
408
|
+
throw new RemoteUncertainError();
|
|
409
|
+
batchReferences = new Map(questionIds.map(questionId => [questionId, batchResultReference(durableReceipt, questionId)]));
|
|
410
|
+
observations = replayed;
|
|
411
|
+
// The shared request already has an owner. Never dispatch it again.
|
|
412
|
+
throw new ReplayedBatchReceipt();
|
|
413
|
+
}
|
|
414
|
+
throw new RemoteUncertainError();
|
|
415
|
+
}
|
|
416
|
+
const running = nextBatchReceipt(durableReceipt, { status: 'running', updatedAtEpochMs: now() });
|
|
417
|
+
if (!await request.batchReceipts.store.compareAndSwap(durableReceipt, running)) {
|
|
418
|
+
throw new ReceiptPersistenceError();
|
|
419
|
+
}
|
|
420
|
+
durableReceipt = running;
|
|
421
|
+
}
|
|
422
|
+
let fallbackActive = false;
|
|
423
|
+
for (let retry = 0; retry <= (request.batchReceipts
|
|
424
|
+
? plan.candidates[0].target.retry.maxRetries + (fallbackTarget ? fallbackTarget.retry.maxRetries + 1 : 0) : 0); retry++) {
|
|
425
|
+
dispatchCount++;
|
|
426
|
+
const activeTarget = fallbackActive ? fallbackTarget : plan.candidates[0].target;
|
|
427
|
+
const preparedRequests = await Promise.all((fallbackActive ? fallbackProjectedCandidates : projectedCandidates)
|
|
428
|
+
.map(({ candidate, projected }, index) => prepareAdapterRequest({
|
|
429
|
+
alias: candidate.alias, questionId: questionIds[index], definition: structuredClone(candidate.definition),
|
|
430
|
+
input: structuredClone(projected.input), target: structuredClone(activeTarget),
|
|
431
|
+
invocationId: `${request.invocationId}:${plan.groupId}`, deadlineEpochMs: deadline,
|
|
432
|
+
signal: totalAbort, callerSignal: request.signal ?? new AbortController().signal, totalSignal: totalAbort,
|
|
433
|
+
resolveCredential: request.resolveCredential ?? unauthorizedCredential,
|
|
434
|
+
...(projected.evidence ? { projectionEvidence: projected.evidence } : {}),
|
|
435
|
+
}, adapter, request.compileCache)));
|
|
436
|
+
const dispatchedAt = now();
|
|
437
|
+
const batchSpan = runtimeTraceOf(request)?.startBatch(plan.candidates.map(candidate => candidate.alias), plan.groupId, durableReceipt ? durableReceipt.attempts.length + 1 : dispatchCount);
|
|
438
|
+
if (batchSpan)
|
|
439
|
+
batchSpans.push(batchSpan);
|
|
440
|
+
const response = await adapter.evaluateMany({ decisionSubject: plan.decisionSubject,
|
|
441
|
+
requests: preparedRequests, ...(batchSpan ? { traceContext: batchSpan.traceContext } : {}) });
|
|
442
|
+
if (batchSpan)
|
|
443
|
+
batchSpan.response = response;
|
|
444
|
+
if (contextPlan && response.sharedUsage.inputTokens !== null) {
|
|
445
|
+
recordRuntimeContextUsage(contextPlan, questionIds, response.sharedUsage.inputTokens, contextUsage);
|
|
446
|
+
}
|
|
447
|
+
observations = correlateAtomicBatch(questionIds, response.answers.map(answer => ({ questionId: answer.questionId, value: answer.observation })));
|
|
448
|
+
// Validate every sibling before publishing any result: the batch is atomic.
|
|
449
|
+
const normalized = new Map();
|
|
450
|
+
plan.candidates.forEach((candidate, index) => {
|
|
451
|
+
const item = resolved.find(value => value.alias === candidate.alias);
|
|
452
|
+
// Siblings share transport identity, not acceptance: each answer is judged by its own target's policy.
|
|
453
|
+
const ownTarget = fallbackActive ? request.binding.spec.evaluations[candidate.alias].targets[1] : candidate.target;
|
|
454
|
+
normalized.set(questionIds[index], normalizeObservationForRuntime({ request, item, target: ownTarget,
|
|
455
|
+
observation: observations.get(questionIds[index]), now }));
|
|
456
|
+
});
|
|
457
|
+
observations = new Map([...normalized].map(([id, value]) => [id, value.observation]));
|
|
458
|
+
if (durableReceipt && request.batchReceipts) {
|
|
459
|
+
const requestIds = uniqueNonNull(response.answers.map(answer => answer.observation.requestId));
|
|
460
|
+
if (requestIds.length > 1)
|
|
461
|
+
throw new Error('batch response contained multiple provider request IDs');
|
|
462
|
+
const models = uniqueNonNull(response.answers.map(answer => answer.observation.actualModel));
|
|
463
|
+
const derivedCost = request.batchReceipts.priceCatalog
|
|
464
|
+
? deriveCost(response.sharedUsage, request.batchReceipts.priceCatalog) : { kind: 'unknown' };
|
|
465
|
+
const cost = providerCost(response.sharedUsage.costUsd)
|
|
466
|
+
?? (derivedCost.kind !== 'unknown' ? derivedCost : request.batchReceipts.unknownCostBound
|
|
467
|
+
? { kind: 'bounded-unknown', currency: 'USD',
|
|
468
|
+
upperBoundMicros: request.batchReceipts.unknownCostBound.upperBoundMicros,
|
|
469
|
+
boundPolicyId: request.batchReceipts.unknownCostBound.policyId,
|
|
470
|
+
boundPolicyVersion: request.batchReceipts.unknownCostBound.policyVersion }
|
|
471
|
+
: { kind: 'unknown' });
|
|
472
|
+
const certainties = response.answers.map(answer => answer.observation.dispatchCertainty);
|
|
473
|
+
const attemptStatus = certainties.some(value => value === 'unknown')
|
|
474
|
+
? 'execution-uncertain' : certainties.every(value => value === 'not-sent') ? 'not-sent'
|
|
475
|
+
: [...observations.values()].every(value => value.status === 'success') ? 'succeeded' : 'failed';
|
|
476
|
+
const attempt = {
|
|
477
|
+
ordinal: durableReceipt.attempts.length + 1,
|
|
478
|
+
adapterId: adapter.id,
|
|
479
|
+
adapterVersion: adapter.version,
|
|
480
|
+
requestedModel: activeTarget.model,
|
|
481
|
+
actualModel: models.length === 1 ? models[0] : null,
|
|
482
|
+
providerRequestId: requestIds[0] && validOpaqueRequestId(requestIds[0]) ? requestIds[0] : null,
|
|
483
|
+
status: attemptStatus,
|
|
484
|
+
dispatchedAtEpochMs: attemptStatus === 'not-sent' ? null : dispatchedAt,
|
|
485
|
+
completedAtEpochMs: now(),
|
|
486
|
+
usage: { inputTokens: response.sharedUsage.inputTokens, outputTokens: response.sharedUsage.outputTokens },
|
|
487
|
+
cost,
|
|
488
|
+
fallbackFromAttemptOrdinal: fallbackActive ? durableReceipt.attempts.at(-1)?.ordinal ?? null : null,
|
|
489
|
+
};
|
|
490
|
+
const answerReferences = plan.candidates.map((candidate, index) => ({
|
|
491
|
+
questionId: questionIds[index],
|
|
492
|
+
answerId: durableAnswerId(durableReceipt.batchId, questionIds[index]),
|
|
493
|
+
resultId: `${request.invocationId}-${candidate.alias}`,
|
|
494
|
+
}));
|
|
495
|
+
const retriable = attemptStatus === 'failed'
|
|
496
|
+
&& [...observations.values()].every(value => RETRIABLE.has(value.reason));
|
|
497
|
+
const canRetry = retriable && (fallbackActive
|
|
498
|
+
? retry - plan.candidates[0].target.retry.maxRetries - 1 < fallbackTarget.retry.maxRetries
|
|
499
|
+
: retry < plan.candidates[0].target.retry.maxRetries)
|
|
500
|
+
&& attemptsUsed + (dispatchCount + 1) * plan.candidates.length <= request.binding.spec.maxAttempts
|
|
501
|
+
&& (request.batchReceipts.maxCostMicros === undefined
|
|
502
|
+
|| (batchEnforcementCostMicros({ ...durableReceipt, attempts: [...durableReceipt.attempts, attempt] }) ?? Infinity)
|
|
503
|
+
+ request.batchReceipts.unknownCostBound.upperBoundMicros <= request.batchReceipts.maxCostMicros)
|
|
504
|
+
&& now() < deadline;
|
|
505
|
+
const canFallback = attemptStatus === 'failed' && !canRetry && !fallbackActive && !!fallbackTarget
|
|
506
|
+
&& [...observations.values()].every(value => plan.candidates.every(candidate => request.binding.spec.evaluations[candidate.alias].fallbackOn.includes(value.reason)))
|
|
507
|
+
&& attemptsUsed + (dispatchCount + 1) * plan.candidates.length <= request.binding.spec.maxAttempts
|
|
508
|
+
&& (request.batchReceipts.maxCostMicros === undefined
|
|
509
|
+
|| (batchEnforcementCostMicros({ ...durableReceipt, attempts: [...durableReceipt.attempts, attempt] }) ?? Infinity)
|
|
510
|
+
+ request.batchReceipts.unknownCostBound.upperBoundMicros <= request.batchReceipts.maxCostMicros)
|
|
511
|
+
&& now() < deadline;
|
|
512
|
+
if (canRetry || canFallback) {
|
|
513
|
+
const pending = nextBatchReceipt(durableReceipt, { status: 'running', updatedAtEpochMs: now(),
|
|
514
|
+
attempts: [...durableReceipt.attempts, attempt] });
|
|
515
|
+
if (!await request.batchReceipts.store.compareAndSwap(durableReceipt, pending))
|
|
516
|
+
throw new ReceiptPersistenceError();
|
|
517
|
+
durableReceipt = pending;
|
|
518
|
+
if (canFallback)
|
|
519
|
+
fallbackActive = true;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const terminalStatus = attemptStatus === 'succeeded' ? 'completed'
|
|
523
|
+
: attemptStatus === 'execution-uncertain' ? 'execution-uncertain' : 'failed';
|
|
524
|
+
const terminal = nextBatchReceipt(durableReceipt, { status: terminalStatus, updatedAtEpochMs: now(),
|
|
525
|
+
attempts: [...durableReceipt.attempts, attempt],
|
|
526
|
+
...(terminalStatus === 'completed' ? { answerReferences,
|
|
527
|
+
allocations: allocateEstimatedUsage(questionIds, batchAccountingTotals({ ...durableReceipt,
|
|
528
|
+
attempts: [...durableReceipt.attempts, attempt] }).usage) } : {}) });
|
|
529
|
+
if (terminalStatus === 'completed') {
|
|
530
|
+
// Publish values before committing the terminal receipt: a crash between these
|
|
531
|
+
// writes leaves an uncertain receipt, never a successful but unreadable replay.
|
|
532
|
+
observations = resultOnlyBatchObservations(observations);
|
|
533
|
+
await request.batchReceipts.resultStore.writeMany(terminal, observations);
|
|
534
|
+
}
|
|
535
|
+
if (!await request.batchReceipts.store.compareAndSwap(durableReceipt, terminal)) {
|
|
536
|
+
throw new ReceiptPersistenceError();
|
|
537
|
+
}
|
|
538
|
+
durableReceipt = terminal;
|
|
539
|
+
if (terminalStatus === 'completed') {
|
|
540
|
+
batchReferences = new Map(questionIds.map(questionId => [questionId, batchResultReference(terminal, questionId)]));
|
|
541
|
+
}
|
|
542
|
+
else
|
|
543
|
+
observations = resultOnlyBatchObservations(new Map(questionIds.map(questionId => [questionId, observationFailure(terminalStatus === 'execution-uncertain' ? 'execution-uncertain' : 'service-error')])));
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
if (error instanceof ReplayedBatchReceipt || error.name === 'ReplayedBatchReceipt') {
|
|
550
|
+
// References and reconstructed result observations were prepared above.
|
|
551
|
+
}
|
|
552
|
+
else if (durableReceipt && request.batchReceipts && durableReceipt.status === 'running') {
|
|
553
|
+
const uncertain = nextBatchReceipt(durableReceipt, { status: 'execution-uncertain', updatedAtEpochMs: now(),
|
|
554
|
+
attempts: [...durableReceipt.attempts, uncertainBatchAttempt(plan, started, now())] });
|
|
555
|
+
if (!await request.batchReceipts.store.compareAndSwap(durableReceipt, uncertain)) {
|
|
556
|
+
throw new ReceiptPersistenceError();
|
|
557
|
+
}
|
|
558
|
+
observations = new Map(questionIds.map(id => [id, observationFailure('execution-uncertain')]));
|
|
559
|
+
}
|
|
560
|
+
else if (error instanceof BatchRecordUnavailableError || error instanceof BatchStoreIntegrityError
|
|
561
|
+
|| error instanceof BatchStoreMigrationRequiredError) {
|
|
562
|
+
// Erased, expired, tampered or unmigrated durable state is never re-dispatched and never
|
|
563
|
+
// falls back to another read of stored values. Only lifecycle unavailability gets the
|
|
564
|
+
// dedicated reason; integrity and migration failures stay persistence-error.
|
|
565
|
+
const reason = error instanceof BatchRecordUnavailableError ? 'batch-record-unavailable' : 'persistence-error';
|
|
566
|
+
observations = new Map(questionIds.map(id => [id, observationFailure(reason)]));
|
|
567
|
+
}
|
|
568
|
+
else if (error instanceof ReceiptPersistenceError || error instanceof RemoteUncertainError) {
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const replayed = durableReceipt?.status === 'completed'
|
|
573
|
+
? await request.batchReceipts?.resultStore?.readMany(durableReceipt) : undefined;
|
|
574
|
+
if (replayed && replayed.size === questionIds.length) {
|
|
575
|
+
batchReferences = new Map(questionIds.map(questionId => [questionId, batchResultReference(durableReceipt, questionId)]));
|
|
576
|
+
observations = replayed;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
observations = new Map(questionIds.map(id => [id, observationFailure(error instanceof DecisionProjectionError ? 'data-boundary-denied' : 'invalid-output')]));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
runtimeTraceOf(request)?.finishBatch(batchSpans, durableReceipt);
|
|
584
|
+
plan.candidates.forEach((candidate, index) => {
|
|
585
|
+
const item = resolved.find(value => value.alias === candidate.alias);
|
|
586
|
+
const evidence = { mode: 'native', groupId: plan.groupId, questionId: questionIds[index] };
|
|
587
|
+
const observation = observations.get(questionIds[index]);
|
|
588
|
+
const context = { request, item, rulesetPin, bindingPin, totalDeadline, signal: totalAbort, totalTimeout,
|
|
589
|
+
attemptBudget, now, advance, batchResult: batchReferences.get(questionIds[index]), contextPlan, contextUsage };
|
|
590
|
+
const calibrationCompatibility = request.calibrationCompatibility && observation.status === 'success'
|
|
591
|
+
? normalizeObservationForRuntime({ request, item, target: durableReceipt?.attempts.at(-1)?.fallbackFromAttemptOrdinal
|
|
592
|
+
? fallbackTarget : candidate.target, observation, now }).calibrationCompatibility
|
|
593
|
+
: undefined;
|
|
594
|
+
const historical = durableReceipt?.attempts.map(attempt => {
|
|
595
|
+
const failure = observationFailure(attempt.status === 'execution-uncertain'
|
|
596
|
+
? 'execution-uncertain' : 'service-error');
|
|
597
|
+
const value = attempt.status === 'succeeded' ? observation : resultOnlyBatchObservations(new Map([[questionIds[index], failure]])).get(questionIds[index]);
|
|
598
|
+
return toAttempt({ ...candidate.target, model: attempt.requestedModel }, attempt.ordinal, value, Math.max(0, (attempt.completedAtEpochMs ?? durableReceipt.updatedAtEpochMs)
|
|
599
|
+
- (attempt.dispatchedAtEpochMs ?? durableReceipt.createdAtEpochMs)), evidence);
|
|
600
|
+
});
|
|
601
|
+
evaluations[item.alias] = decisionResult(context, observation, historical?.length ? historical : [toAttempt(candidate.target, 1, observation, Math.max(0, now() - started), evidence)], calibrationCompatibility);
|
|
602
|
+
});
|
|
603
|
+
if (durableReceipt?.status === 'completed' || durableReceipt?.status === 'failed') {
|
|
604
|
+
batchCostSpent += batchEnforcementCostMicros(durableReceipt) ?? Infinity;
|
|
605
|
+
}
|
|
606
|
+
attemptsUsed += Math.max(1, dispatchCount) * plan.candidates.length;
|
|
607
|
+
attemptBudget.consume(Math.max(1, dispatchCount) * plan.candidates.length);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const concurrency = effectiveConcurrency(request);
|
|
611
|
+
const remainingItems = resolved.filter(item => !evaluations[item.alias]);
|
|
612
|
+
const waves = contextPlan
|
|
613
|
+
? [...new Set(contextPlan.partitions.map(partition => partition.wave))].sort((left, right) => left - right)
|
|
614
|
+
.map(wave => remainingItems.filter(item => contextPlan.partitions.some(partition => partition.wave === wave && partition.questionIds.includes(decisionBatchQuestionId(item.alias)))))
|
|
615
|
+
: [remainingItems];
|
|
616
|
+
for (const waveItems of waves) {
|
|
617
|
+
const scheduled = await runBoundedFair(waveItems.map(item => ({ value: item, lane: schedulerLane(request, item) })), concurrency, async (item, _index, slot) => evaluateOne({ request, item, rulesetPin, bindingPin, totalDeadline, signal: totalAbort, totalTimeout,
|
|
618
|
+
attemptBudget, now, advance, batchEvidence: singleBatchEvidence(request, item.alias), contextPlan, contextUsage,
|
|
619
|
+
// Receipt v2 keeps a strictly serial chronology, so only receipt-free scheduled runs yield during backoff.
|
|
620
|
+
...(request.scheduler?.enabled && !request.receiptStore ? { suspend: slot.suspend } : {}) }), { signal: totalAbort, deadlineEpochMs: totalDeadline, now });
|
|
621
|
+
scheduled.forEach((execution, index) => {
|
|
622
|
+
const item = waveItems[index];
|
|
623
|
+
if (execution instanceof SchedulerWaitError) {
|
|
624
|
+
const reason = execution.reason === 'cancelled' ? 'cancelled' : 'timeout';
|
|
625
|
+
evaluations[item.alias] = emptyDecisionResult(request, item, rulesetPin, bindingPin, reason === 'cancelled' ? 'cancelled' : 'error', reason);
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
evaluations[item.alias] = execution;
|
|
629
|
+
}
|
|
166
630
|
});
|
|
167
|
-
attemptsUsed += execution.spec.attempts.length;
|
|
168
|
-
evaluations[item.alias] = execution;
|
|
169
|
-
await advance('observation-received', { evaluations, pending: null });
|
|
170
631
|
}
|
|
171
|
-
|
|
632
|
+
// Receipt v2 has one pending slot; concurrency is forced to one when durable
|
|
633
|
+
// ownership is enabled, so this update remains an atomic chronology.
|
|
634
|
+
if (request.receiptStore)
|
|
635
|
+
await advance('observation-received', { evaluations, pending: null });
|
|
636
|
+
if (receipt?.state === 'execution-uncertain') {
|
|
637
|
+
releaseAdmissionBudget(request);
|
|
172
638
|
return failureResult(base, 'execution-uncertain', evaluations);
|
|
639
|
+
}
|
|
640
|
+
const orderedEvaluations = Object.fromEntries(resolved
|
|
641
|
+
.filter(item => evaluations[item.alias])
|
|
642
|
+
.map(item => [item.alias, evaluations[item.alias]]));
|
|
173
643
|
let result;
|
|
174
644
|
if (totalAbort.aborted) {
|
|
175
645
|
const reason = request.signal?.aborted ? 'cancelled' : 'timeout';
|
|
176
|
-
result = { ...base, spec: { ...base.spec, status: reason === 'cancelled' ? 'cancelled' : 'error', reason, matchedRules: [], evaluations } };
|
|
646
|
+
result = { ...base, spec: { ...base.spec, status: reason === 'cancelled' ? 'cancelled' : 'error', reason, matchedRules: [], evaluations: orderedEvaluations } };
|
|
177
647
|
}
|
|
178
648
|
else {
|
|
179
649
|
try {
|
|
180
|
-
const composition = composeRuleset(request.ruleset, request.input,
|
|
650
|
+
const composition = composeRuleset(request.ruleset, request.input, orderedEvaluations);
|
|
181
651
|
result = {
|
|
182
652
|
...base,
|
|
183
653
|
spec: {
|
|
@@ -186,16 +656,25 @@ export async function evaluateDecisionRuleset(request) {
|
|
|
186
656
|
reason: composition.reason,
|
|
187
657
|
...(composition.outcome !== undefined ? { outcome: composition.outcome } : {}),
|
|
188
658
|
matchedRules: composition.matchedRules,
|
|
189
|
-
evaluations,
|
|
659
|
+
evaluations: orderedEvaluations,
|
|
190
660
|
},
|
|
191
661
|
};
|
|
192
662
|
}
|
|
193
663
|
catch {
|
|
194
|
-
result = { ...base, spec: { ...base.spec, status: 'error', reason: 'invalid-output', matchedRules: [], evaluations } };
|
|
664
|
+
result = { ...base, spec: { ...base.spec, status: 'error', reason: 'invalid-output', matchedRules: [], evaluations: orderedEvaluations } };
|
|
195
665
|
}
|
|
196
666
|
}
|
|
667
|
+
// Either incomplete-context signal (context plan or projection evidence) prohibits automatic action.
|
|
668
|
+
if ((contextPlan && !contextPlan.automaticActionAllowed || projectionBlockedAutomaticAction.has(request))
|
|
669
|
+
&& (result.spec.status === 'completed' || result.spec.status === 'defaulted')) {
|
|
670
|
+
const { outcome: _outcome, ...withoutOutcome } = result.spec;
|
|
671
|
+
result = { ...result, spec: { ...withoutOutcome, status: 'review', reason: 'insufficient-information' } };
|
|
672
|
+
}
|
|
197
673
|
await advance('composed');
|
|
674
|
+
if (contextPlan)
|
|
675
|
+
result = withRulesetContext(result, contextPlan, contextUsage);
|
|
198
676
|
await advance('completed', { result });
|
|
677
|
+
releaseAdmissionBudget(request);
|
|
199
678
|
return result;
|
|
200
679
|
}
|
|
201
680
|
catch (error) {
|
|
@@ -206,8 +685,10 @@ export async function evaluateDecisionRuleset(request) {
|
|
|
206
685
|
catch {
|
|
207
686
|
return failureResult(base, 'persistence-error', evaluations);
|
|
208
687
|
}
|
|
688
|
+
releaseAdmissionBudget(request);
|
|
209
689
|
return failureResult(base, 'execution-uncertain', evaluations);
|
|
210
690
|
}
|
|
691
|
+
releaseAdmissionBudget(request);
|
|
211
692
|
return failureResult(base, 'persistence-error', evaluations);
|
|
212
693
|
}
|
|
213
694
|
}
|
|
@@ -215,25 +696,35 @@ class ReceiptPersistenceError extends Error {
|
|
|
215
696
|
}
|
|
216
697
|
class RemoteUncertainError extends Error {
|
|
217
698
|
}
|
|
699
|
+
class ReplayedBatchReceipt extends Error {
|
|
700
|
+
constructor() { super('replayed batch receipt'); this.name = 'ReplayedBatchReceipt'; }
|
|
701
|
+
}
|
|
218
702
|
async function evaluateOne(context) {
|
|
219
703
|
const evaluation = context.request.binding.spec.evaluations[context.item.alias];
|
|
220
704
|
const attempts = [];
|
|
221
705
|
let final = observationFailure('budget-exhausted');
|
|
706
|
+
let calibrationCompatibility;
|
|
707
|
+
const trace = runtimeTraceOf(context.request);
|
|
222
708
|
for (let targetIndex = 0; targetIndex < evaluation.targets.length; targetIndex += 1) {
|
|
223
709
|
const target = evaluation.targets[targetIndex];
|
|
224
710
|
if (context.signal.aborted || context.now() >= context.totalDeadline) {
|
|
225
711
|
final = interruption(context);
|
|
226
712
|
break;
|
|
227
713
|
}
|
|
228
|
-
if (
|
|
714
|
+
if (!context.attemptBudget.available) {
|
|
229
715
|
final = observationFailure('budget-exhausted');
|
|
230
716
|
break;
|
|
231
717
|
}
|
|
232
718
|
const adapter = context.request.adapters[target.adapter];
|
|
233
719
|
const capabilityFailure = await checkCapabilities(adapter, target, context.item.definition);
|
|
234
720
|
if (capabilityFailure) {
|
|
721
|
+
if (!context.attemptBudget.claim()) {
|
|
722
|
+
final = observationFailure('budget-exhausted');
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
235
725
|
final = capabilityFailure;
|
|
236
|
-
attempts.push(toAttempt(target, attempts.length + 1, final, 0));
|
|
726
|
+
attempts.push(toAttempt(target, attempts.length + 1, final, 0, context.batchEvidence));
|
|
727
|
+
trace?.endAttempt(trace.startAttempt(context.item.alias, target, attempts.length, targetIndex > 0), attempts.at(-1));
|
|
237
728
|
}
|
|
238
729
|
else {
|
|
239
730
|
for (let retry = 0; retry <= target.retry.maxRetries; retry += 1) {
|
|
@@ -241,16 +732,18 @@ async function evaluateOne(context) {
|
|
|
241
732
|
final = interruption(context);
|
|
242
733
|
break;
|
|
243
734
|
}
|
|
244
|
-
if (
|
|
735
|
+
if (!context.attemptBudget.claim()) {
|
|
245
736
|
final = observationFailure('budget-exhausted');
|
|
246
737
|
break;
|
|
247
738
|
}
|
|
248
739
|
const started = context.now();
|
|
740
|
+
const span = trace?.startAttempt(context.item.alias, target, attempts.length + 1, targetIndex > 0);
|
|
249
741
|
const attemptDeadline = Math.min(context.totalDeadline, started + target.timeoutMs);
|
|
250
742
|
try {
|
|
743
|
+
const projected = await projectRuntimeInput(context.request, context.item.alias, target, context.item.input, adapter, span);
|
|
251
744
|
await context.advance('dispatched', { pending: { alias: context.item.alias, targetIndex, ordinal: attempts.length + 1, attempts } });
|
|
252
745
|
try {
|
|
253
|
-
final = await invokeWithDeadline(context, adapter, target, attemptDeadline, attempts.length + 1);
|
|
746
|
+
final = await invokeWithDeadline(context, adapter, target, attemptDeadline, attempts.length + 1, projected, span?.traceContext);
|
|
254
747
|
}
|
|
255
748
|
catch (error) {
|
|
256
749
|
if (context.request.receiptStore && !(error instanceof DecisionPreDispatchError))
|
|
@@ -263,14 +756,22 @@ async function evaluateOne(context) {
|
|
|
263
756
|
throw new RemoteUncertainError();
|
|
264
757
|
}
|
|
265
758
|
await context.advance('observation-received');
|
|
266
|
-
|
|
759
|
+
const calibrated = normalizeObservationForRuntime({ request: context.request, item: context.item, target,
|
|
760
|
+
observation: final, now: context.now });
|
|
761
|
+
final = calibrated.observation;
|
|
762
|
+
calibrationCompatibility = calibrated.calibrationCompatibility;
|
|
763
|
+
if (context.contextPlan && final.usage.inputTokens !== null && context.contextUsage) {
|
|
764
|
+
recordRuntimeContextUsage(context.contextPlan, decisionBatchQuestionId(context.item.alias), final.usage.inputTokens, context.contextUsage);
|
|
765
|
+
}
|
|
267
766
|
}
|
|
268
767
|
catch (error) {
|
|
269
768
|
if (error instanceof ReceiptPersistenceError || error instanceof RemoteUncertainError)
|
|
270
769
|
throw error;
|
|
271
|
-
final = observationFailure(error instanceof
|
|
770
|
+
final = observationFailure(error instanceof DecisionProjectionError ? 'data-boundary-denied'
|
|
771
|
+
: error instanceof DecisionValidationError ? 'invalid-output' : 'service-error');
|
|
272
772
|
}
|
|
273
|
-
attempts.push(toAttempt(target, attempts.length + 1, final, Math.max(0, context.now() - started)));
|
|
773
|
+
attempts.push(toAttempt(target, attempts.length + 1, final, Math.max(0, context.now() - started), context.batchEvidence));
|
|
774
|
+
trace?.endAttempt(span, attempts.at(-1));
|
|
274
775
|
if (final.status === 'success')
|
|
275
776
|
break;
|
|
276
777
|
if (!RETRIABLE.has(final.reason) || retry === target.retry.maxRetries)
|
|
@@ -286,8 +787,10 @@ async function evaluateOne(context) {
|
|
|
286
787
|
break;
|
|
287
788
|
}
|
|
288
789
|
attempts[attempts.length - 1].retryDelayMs = delayMs;
|
|
790
|
+
trace?.retryScheduled(span, delayMs);
|
|
289
791
|
try {
|
|
290
|
-
|
|
792
|
+
const backoff = () => (context.request.delay ?? abortableDelay)(delayMs, context.signal);
|
|
793
|
+
await (context.suspend ? context.suspend(backoff) : backoff());
|
|
291
794
|
}
|
|
292
795
|
catch {
|
|
293
796
|
final = interruption(context);
|
|
@@ -305,14 +808,16 @@ async function evaluateOne(context) {
|
|
|
305
808
|
if (!hasNext || !evaluation.fallbackOn.includes(final.reason))
|
|
306
809
|
break;
|
|
307
810
|
}
|
|
308
|
-
return decisionResult(context, final, attempts);
|
|
811
|
+
return decisionResult(context, final, attempts, calibrationCompatibility);
|
|
309
812
|
}
|
|
310
|
-
async function invokeWithDeadline(context, adapter, target, deadlineEpochMs, ordinal) {
|
|
813
|
+
async function invokeWithDeadline(context, adapter, target, deadlineEpochMs, ordinal, projected, traceContext) {
|
|
311
814
|
const controller = new AbortController();
|
|
312
815
|
const signal = AbortSignal.any([context.signal, controller.signal]);
|
|
313
816
|
const timeoutMs = Math.max(1, deadlineEpochMs - context.now());
|
|
314
817
|
let timer;
|
|
315
818
|
let removeAbortListener = () => undefined;
|
|
819
|
+
let admissionEvidence;
|
|
820
|
+
let releaseAdmission;
|
|
316
821
|
const boundary = new Promise(resolve => {
|
|
317
822
|
timer = setTimeout(() => {
|
|
318
823
|
controller.abort(new DOMException('Decision target timed out', 'TimeoutError'));
|
|
@@ -327,39 +832,186 @@ async function invokeWithDeadline(context, adapter, target, deadlineEpochMs, ord
|
|
|
327
832
|
}
|
|
328
833
|
});
|
|
329
834
|
try {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
835
|
+
if (context.request.scheduler?.enabled) {
|
|
836
|
+
const policy = context.request.scheduler;
|
|
837
|
+
const controller = decisionAdmissionRegistry.controllerFor(policy, context.now);
|
|
838
|
+
const admissionStarted = context.now();
|
|
839
|
+
const trace = runtimeTraceOf(context.request);
|
|
840
|
+
try {
|
|
841
|
+
const lease = await controller.acquire({ budgetId: context.request.invocationId,
|
|
842
|
+
principalId: policy.principal.id, workspaceId: policy.workspace.id,
|
|
843
|
+
providerId: target.adapter, estimate: policy.estimate?.(context.item.alias, target, context.item.input) ?? { attempts: 1, batchSize: 1, items: 1 },
|
|
844
|
+
deadlineEpochMs, signal });
|
|
845
|
+
admissionEvidence = lease.evidence;
|
|
846
|
+
const admitSpan = trace?.startAdmission(context.item.alias, target.adapter, admissionStarted, lease.evidence);
|
|
847
|
+
releaseAdmission = outcome => {
|
|
848
|
+
lease.release(outcome);
|
|
849
|
+
trace?.endAdmission(admitSpan, lease.evidence);
|
|
850
|
+
};
|
|
851
|
+
policy.onEvidence?.(context.item.alias, lease.evidence);
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
if (error instanceof AdmissionError) {
|
|
855
|
+
trace?.startAdmission(context.item.alias, target.adapter, admissionStarted, error.evidence);
|
|
856
|
+
policy.onEvidence?.(context.item.alias, error.evidence);
|
|
857
|
+
// Nothing was sent. A target timeout that aborts a queued wait is a timeout, not a caller cancellation.
|
|
858
|
+
const failure = error.evidence.reason === 'cancelled' && !context.request.signal?.aborted
|
|
859
|
+
? interruption(context) : observationFailure(admissionReason(error));
|
|
860
|
+
return { ...failure, dispatchCertainty: 'not-sent', admission: error.evidence };
|
|
861
|
+
}
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const preparedRequest = await prepareAdapterRequest({
|
|
866
|
+
alias: context.item.alias,
|
|
867
|
+
definition: structuredClone(context.item.definition),
|
|
868
|
+
input: structuredClone(projected.input),
|
|
869
|
+
target: structuredClone(target),
|
|
870
|
+
invocationId: `${context.request.invocationId}:${context.item.alias}:${ordinal}`,
|
|
871
|
+
deadlineEpochMs,
|
|
872
|
+
signal,
|
|
873
|
+
callerSignal: context.request.signal ?? new AbortController().signal,
|
|
874
|
+
totalSignal: context.signal,
|
|
875
|
+
resolveCredential: context.request.resolveCredential ?? unauthorizedCredential,
|
|
876
|
+
...(projected.evidence ? { projectionEvidence: projected.evidence } : {}),
|
|
877
|
+
...(traceContext ? { traceContext } : {}),
|
|
878
|
+
onRemoteHandle: async (handle) => {
|
|
879
|
+
try {
|
|
880
|
+
const previous = await context.request.receiptStore?.read(context.request.invocationId, context.request.receiptProjectId ?? 'default');
|
|
881
|
+
if (previous && !previous.remoteHandles.includes(handle))
|
|
882
|
+
await context.advance('remote-handle-known', { remoteHandles: [...previous.remoteHandles, handle] });
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
throw new ReceiptPersistenceError();
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
}, adapter, context.request.compileCache);
|
|
889
|
+
const observedWithTransportMetadata = await Promise.race([
|
|
890
|
+
adapter.evaluate(preparedRequest),
|
|
353
891
|
boundary,
|
|
354
892
|
]);
|
|
355
|
-
|
|
893
|
+
const { providerPrefixReport, ...observedWithoutPrefixReport } = observedWithTransportMetadata;
|
|
894
|
+
let observed = observedWithoutPrefixReport;
|
|
895
|
+
if (context.request.providerPrefix) {
|
|
896
|
+
try {
|
|
897
|
+
const identity = context.request.providerPrefix.identityFor({
|
|
898
|
+
request: preparedRequest,
|
|
899
|
+
adapter,
|
|
900
|
+
observation: observedWithTransportMetadata,
|
|
901
|
+
});
|
|
902
|
+
const evidence = providerPrefixEvidence(identity, providerPrefixReport ?? { kind: 'unreported' }, context.now());
|
|
903
|
+
observed = { ...observed, providerPrefix: evidence };
|
|
904
|
+
context.request.providerPrefix.onEvidence?.({ alias: context.item.alias, evidence });
|
|
905
|
+
}
|
|
906
|
+
catch {
|
|
907
|
+
// Provider cache metadata is observability-only and cannot alter decision semantics.
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const result = context.request.signal?.aborted ? observationFailure('cancelled', 'cancelled', observed, 'caller-cancelled') : observed;
|
|
911
|
+
releaseAdmission?.({ success: result.status === 'success', ...(result.retryAfterMs === undefined ? {} : { retryAfterMs: result.retryAfterMs }) });
|
|
912
|
+
return admissionEvidence ? { ...result, admission: admissionEvidence } : result;
|
|
356
913
|
}
|
|
357
914
|
finally {
|
|
915
|
+
releaseAdmission?.({ success: false });
|
|
358
916
|
if (timer)
|
|
359
917
|
clearTimeout(timer);
|
|
360
918
|
removeAbortListener();
|
|
361
919
|
}
|
|
362
920
|
}
|
|
921
|
+
/** Requests whose projection evidence prohibited automatic action (allowed incomplete context). */
|
|
922
|
+
const projectionBlockedAutomaticAction = new WeakSet();
|
|
923
|
+
function isUnprojectedLocalOptOut(projection) {
|
|
924
|
+
return !!projection && projection.mode === 'unprojected-local';
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* The projection field is host code, never portable data: it must be either a
|
|
928
|
+
* resolver-backed policy or the exact opt-out object. Anything else (including a
|
|
929
|
+
* JSON-shaped policy lifted from input or definitions) is rejected before dispatch.
|
|
930
|
+
*/
|
|
931
|
+
function validateEgressBoundaryConfiguration(request) {
|
|
932
|
+
const projection = request.projection;
|
|
933
|
+
if (projection === undefined)
|
|
934
|
+
return;
|
|
935
|
+
if (!projection || typeof projection !== 'object' || Array.isArray(projection)) {
|
|
936
|
+
throw new DecisionValidationError('unsupported egress boundary configuration');
|
|
937
|
+
}
|
|
938
|
+
const record = projection;
|
|
939
|
+
if ('mode' in record) {
|
|
940
|
+
if (record.mode !== 'unprojected-local' || Object.keys(record).length !== 1) {
|
|
941
|
+
throw new DecisionValidationError('unsupported egress boundary configuration');
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (typeof record.resolve !== 'function')
|
|
946
|
+
throw new DecisionValidationError('unsupported egress boundary configuration');
|
|
947
|
+
}
|
|
948
|
+
/** Project one dispatch input and record a metadata-only `decision.project` span for it. */
|
|
949
|
+
async function projectRuntimeInput(request, alias, target, input, adapter, parentSpan) {
|
|
950
|
+
const trace = runtimeTraceOf(request);
|
|
951
|
+
const started = (request.now ?? Date.now)();
|
|
952
|
+
const mode = !request.projection ? 'none'
|
|
953
|
+
: isUnprojectedLocalOptOut(request.projection) ? 'unprojected-local' : 'policy';
|
|
954
|
+
try {
|
|
955
|
+
const projected = await projectRuntimeInputUntraced(request, alias, target, input, adapter);
|
|
956
|
+
// A no-egress passthrough with no projection configured applied no D10 boundary: no span.
|
|
957
|
+
if (mode !== 'none')
|
|
958
|
+
trace?.projected(alias, parentSpan, started, { mode, outcome: 'allowed', ...(projected.evidence ? {
|
|
959
|
+
fieldCount: projected.evidence.included.length, incompleteContext: projected.evidence.incompleteContext,
|
|
960
|
+
automaticActionAllowed: projected.evidence.automaticActionAllowed
|
|
961
|
+
} : {}) });
|
|
962
|
+
return projected;
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
trace?.projected(alias, parentSpan, started, { mode, outcome: 'denied',
|
|
966
|
+
...(error instanceof DecisionProjectionError ? { reason: error.reason } : {}) });
|
|
967
|
+
throw error;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function projectRuntimeInputUntraced(request, alias, target, input, adapter) {
|
|
971
|
+
// Read the adapter's egress declaration at dispatch time, not from planning,
|
|
972
|
+
// so a destination change after planning is still bound before credentials.
|
|
973
|
+
const egress = adapter ? (await adapter.capabilities()).egress : undefined;
|
|
974
|
+
const projection = request.projection;
|
|
975
|
+
if (!projection || isUnprojectedLocalOptOut(projection)) {
|
|
976
|
+
// Safe default: unprojected input may reach only a no-egress adapter, unless
|
|
977
|
+
// the host explicitly opted out (recorded in the result and receipt).
|
|
978
|
+
if (projection || egress?.mode === 'none')
|
|
979
|
+
return { input };
|
|
980
|
+
throw new DecisionProjectionError('data-boundary-denied', 'network-capable dispatch requires a projection policy');
|
|
981
|
+
}
|
|
982
|
+
const policy = projection.resolve({ alias, target: structuredClone(target) });
|
|
983
|
+
if (policy.provider !== target.adapter || policy.model !== target.model) {
|
|
984
|
+
throw new DecisionProjectionError('data-boundary-denied', 'projection destination does not match execution target');
|
|
985
|
+
}
|
|
986
|
+
if (egress?.mode !== 'none') {
|
|
987
|
+
// Bind the policy's declared destination to the adapter's effective transport.
|
|
988
|
+
// An adapter that cannot name its origin or region is an unknown destination.
|
|
989
|
+
if (egress?.mode !== 'network' || !egress.origin || typeof policy.origin !== 'string'
|
|
990
|
+
|| normalizeProjectionOrigin(policy.origin) !== normalizeProjectionOrigin(egress.origin)) {
|
|
991
|
+
throw new DecisionProjectionError('data-boundary-denied', 'projection origin does not match the adapter endpoint');
|
|
992
|
+
}
|
|
993
|
+
if (!egress.region || policy.region !== egress.region) {
|
|
994
|
+
throw new DecisionProjectionError('data-boundary-denied', 'projection region does not match the adapter deployment');
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const projected = await projectDecisionState(input, policy, {
|
|
998
|
+
incompleteContext: projection.incompleteContext,
|
|
999
|
+
});
|
|
1000
|
+
if (!projected.evidence.automaticActionAllowed)
|
|
1001
|
+
projectionBlockedAutomaticAction.add(request);
|
|
1002
|
+
if (projection.debugCapture) {
|
|
1003
|
+
const { scope, capture } = projection.debugCapture;
|
|
1004
|
+
try {
|
|
1005
|
+
await capture(scope, new TextEncoder().encode(canonicalJson(projected.state)));
|
|
1006
|
+
}
|
|
1007
|
+
catch {
|
|
1008
|
+
// Neither backend error text nor captured bytes belong in runtime results.
|
|
1009
|
+
throw new DecisionProjectionError('data-boundary-denied', 'debug capture unavailable');
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
projection.onEvidence?.({ alias, evidence: structuredClone(projected.evidence) });
|
|
1013
|
+
return { input: projected.state, evidence: projected.evidence };
|
|
1014
|
+
}
|
|
363
1015
|
function interruption(context) {
|
|
364
1016
|
if (context.request.signal?.aborted)
|
|
365
1017
|
return observationFailure('cancelled', 'cancelled', undefined, 'caller-cancelled');
|
|
@@ -412,6 +1064,8 @@ function normalizeObservation(definition, target, observation) {
|
|
|
412
1064
|
validateDistribution(definition, observation.uncertainty.distribution);
|
|
413
1065
|
if (target.acceptance.mode === 'typed-value')
|
|
414
1066
|
return observation;
|
|
1067
|
+
if (target.acceptance.mode === 'primitive-policy')
|
|
1068
|
+
return applyPrimitiveAcceptance(definition, target.acceptance, observation);
|
|
415
1069
|
if (!observation.uncertainty || observation.uncertainty.confidence === null)
|
|
416
1070
|
return observationFailure('missing-confidence', 'abstained', observation);
|
|
417
1071
|
if (observation.uncertainty.profile !== target.acceptance.profile)
|
|
@@ -420,32 +1074,91 @@ function normalizeObservation(definition, target, observation) {
|
|
|
420
1074
|
return observationFailure('low-confidence', 'abstained', observation);
|
|
421
1075
|
return observation;
|
|
422
1076
|
}
|
|
423
|
-
function
|
|
424
|
-
|
|
1077
|
+
function normalizeObservationForRuntime(input) {
|
|
1078
|
+
let observation = input.observation;
|
|
1079
|
+
const runtime = input.request.calibrationCompatibility;
|
|
1080
|
+
if (runtime && observation.status === 'success') {
|
|
1081
|
+
if (observation.actualModel === null) {
|
|
1082
|
+
observation = withoutCalibratedRisk(observation);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
const identity = runtime.identityFor({ alias: input.item.alias, definition: structuredClone(input.item.definition),
|
|
1086
|
+
target: structuredClone(input.target), actualModel: observation.actualModel });
|
|
1087
|
+
if (identity.actualModel !== observation.actualModel) {
|
|
1088
|
+
throw new DecisionValidationError('calibration identity actual model does not match provider evidence');
|
|
1089
|
+
}
|
|
1090
|
+
const calibrationCompatibility = runtime.registry.resolve({
|
|
1091
|
+
runId: `${input.request.runId}:${input.request.invocationId}:${input.item.alias}`,
|
|
1092
|
+
requestedAlias: input.target.model,
|
|
1093
|
+
actualIdentity: identity,
|
|
1094
|
+
calibrationArtifactId: runtime.calibrationArtifactId,
|
|
1095
|
+
at: new Date(input.now()).toISOString(),
|
|
1096
|
+
}, runtime.policy);
|
|
1097
|
+
observation = calibrationCompatibility.action === 'allow'
|
|
1098
|
+
? retainPinnedCalibratedRisk(observation, calibrationCompatibility)
|
|
1099
|
+
: withoutCalibratedRisk(observation);
|
|
1100
|
+
return { observation: normalizeObservation(input.item.definition, input.target, observation), calibrationCompatibility };
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return { observation: normalizeObservation(input.item.definition, input.target, observation) };
|
|
1104
|
+
}
|
|
1105
|
+
function withoutCalibratedRisk(observation) {
|
|
1106
|
+
if (!observation.uncertainty?.calibratedRisk)
|
|
1107
|
+
return observation;
|
|
1108
|
+
const uncertainty = structuredClone(observation.uncertainty);
|
|
1109
|
+
delete uncertainty.calibratedRisk;
|
|
1110
|
+
return { ...observation, uncertainty };
|
|
1111
|
+
}
|
|
1112
|
+
function retainPinnedCalibratedRisk(observation, pin) {
|
|
1113
|
+
const risk = observation.uncertainty?.calibratedRisk;
|
|
1114
|
+
if (!risk || !pin.artifactId || !pin.artifactDigest)
|
|
1115
|
+
return withoutCalibratedRisk(observation);
|
|
1116
|
+
const acceptedReferences = new Set([pin.artifactId, pin.artifactDigest, `${pin.artifactId}@${pin.artifactDigest}`]);
|
|
1117
|
+
return acceptedReferences.has(risk.calibrationRef) ? observation : withoutCalibratedRisk(observation);
|
|
1118
|
+
}
|
|
1119
|
+
function decisionResult(context, observation, attempts, calibrationCompatibility) {
|
|
1120
|
+
const result = {
|
|
425
1121
|
apiVersion: resultVersion(context.request), kind: 'DecisionResult',
|
|
426
1122
|
metadata: { id: `${context.request.invocationId}-${context.item.alias}`, version: '1.0.0', description: `Decision result for ${context.item.alias}` },
|
|
427
1123
|
spec: {
|
|
428
1124
|
decision: context.item.pin, ruleset: context.rulesetPin, binding: context.bindingPin,
|
|
429
1125
|
alias: context.item.alias, runId: context.request.runId, invocationId: context.request.invocationId,
|
|
430
1126
|
status: observation.status, ...(observation.status === 'success' ? { value: observation.value } : {}),
|
|
431
|
-
reason: observation.reason, uncertainty: observation.uncertainty,
|
|
1127
|
+
reason: observation.reason, uncertainty: observation.uncertainty,
|
|
1128
|
+
...(observation.acceptance ? { acceptance: observation.acceptance } : {}), attempts,
|
|
1129
|
+
...(calibrationCompatibility ? { calibrationCompatibility } : {}),
|
|
1130
|
+
...(context.batchResult ? { batchResult: context.batchResult } : {}),
|
|
1131
|
+
...(context.contextPlan ? { context: decisionContextEvidence(context.contextPlan, context.contextUsage ?? [], decisionBatchQuestionId(context.item.alias)) } : {}),
|
|
432
1132
|
},
|
|
433
1133
|
};
|
|
1134
|
+
runtimeTraceOf(context.request)?.decided(result);
|
|
1135
|
+
return result;
|
|
434
1136
|
}
|
|
435
1137
|
function emptyDecisionResult(request, item, rulesetPin, bindingPin, status, reason) {
|
|
436
1138
|
return decisionResult({ request, item, rulesetPin, bindingPin, totalDeadline: 0, signal: new AbortController().signal,
|
|
437
|
-
totalTimeout: new AbortController().signal,
|
|
1139
|
+
totalTimeout: new AbortController().signal, attemptBudget: new AttemptBudget(0), now: Date.now, advance: async () => undefined }, observationFailure(reason, status), []);
|
|
438
1140
|
}
|
|
439
1141
|
function resultBase(request, ruleset, binding) {
|
|
440
1142
|
return {
|
|
441
1143
|
apiVersion: resultVersion(request), kind: 'RulesetResult',
|
|
442
1144
|
metadata: { id: request.invocationId, version: '1.0.0', description: `Ruleset result for ${request.ruleset.metadata.id}` },
|
|
443
|
-
spec: { ruleset, binding, runId: request.runId, invocationId: request.invocationId, status: 'error', reason: 'evaluation-failed', matchedRules: [], evaluations: {}
|
|
1145
|
+
spec: { ruleset, binding, runId: request.runId, invocationId: request.invocationId, status: 'error', reason: 'evaluation-failed', matchedRules: [], evaluations: {},
|
|
1146
|
+
...(isUnprojectedLocalOptOut(request.projection) ? { projection: { mode: 'unprojected-local', authority: 'host' } } : {}) },
|
|
444
1147
|
};
|
|
445
1148
|
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Results that can carry D04/D07 batch provenance, D05 admission, D06 context,
|
|
1151
|
+
* D15 result-cache receipts or D30 provider-prefix evidence are written as v1alpha2 even from v1alpha1 inputs,
|
|
1152
|
+
* so every nested result and receipt payload of one invocation shares one version.
|
|
1153
|
+
*/
|
|
446
1154
|
function resultVersion(request) {
|
|
447
|
-
|
|
448
|
-
|
|
1155
|
+
// The projection opt-out record and an incomplete-projection downgrade are v1alpha2-only semantics.
|
|
1156
|
+
if (request.calibrationCompatibility || isUnprojectedLocalOptOut(request.projection)
|
|
1157
|
+
|| request.projection && !isUnprojectedLocalOptOut(request.projection) && request.projection.incompleteContext === true
|
|
1158
|
+
|| request.ruleset.apiVersion === DECISION_API_VERSION_STRUCTURED || request.binding.apiVersion === DECISION_API_VERSION_STRUCTURED
|
|
1159
|
+
|| Object.values(request.definitions).some(definition => definition.apiVersion === DECISION_API_VERSION_STRUCTURED)
|
|
1160
|
+
|| request.batching || request.batchReceipts || request.scheduler?.enabled || request.providerPrefix || request.context
|
|
1161
|
+
|| request.resultCache?.policy.enabled) {
|
|
449
1162
|
return DECISION_API_VERSION_STRUCTURED;
|
|
450
1163
|
}
|
|
451
1164
|
return DECISION_API_VERSION;
|
|
@@ -471,7 +1184,14 @@ function observationFailure(reason, status = reason === 'cancelled' ? 'cancelled
|
|
|
471
1184
|
...(termination ? { termination } : {}),
|
|
472
1185
|
};
|
|
473
1186
|
}
|
|
474
|
-
function
|
|
1187
|
+
function resultOnlyBatchObservations(observations) {
|
|
1188
|
+
return new Map([...observations].map(([questionId, observation]) => {
|
|
1189
|
+
const { requestIdSource: _requestIdSource, ...withoutRequestIdSource } = observation;
|
|
1190
|
+
return [questionId, { ...withoutRequestIdSource,
|
|
1191
|
+
usage: { inputTokens: null, outputTokens: null, costUsd: null }, requestId: null }];
|
|
1192
|
+
}));
|
|
1193
|
+
}
|
|
1194
|
+
function toAttempt(target, ordinal, observation, durationMs, batch) {
|
|
475
1195
|
return {
|
|
476
1196
|
ordinal, adapter: target.adapter, adapterVersion: target.adapterVersion, requestedModel: target.model,
|
|
477
1197
|
actualModel: observation.actualModel, subagent: target.subagent ?? null, status: observation.status,
|
|
@@ -480,9 +1200,235 @@ function toAttempt(target, ordinal, observation, durationMs) {
|
|
|
480
1200
|
...(observation.httpStatus !== undefined ? { httpStatus: observation.httpStatus } : {}),
|
|
481
1201
|
...(observation.termination ? { termination: observation.termination } : {}),
|
|
482
1202
|
...(observation.remoteExecution ? { remoteExecution: observation.remoteExecution } : {}),
|
|
1203
|
+
...(batch ? { batch } : {}),
|
|
1204
|
+
...(observation.admission ? { admission: observation.admission } : {}),
|
|
1205
|
+
...(observation.providerPrefix ? { providerPrefix: observation.providerPrefix } : {}),
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
function prepareContextPlan(request, resolved) {
|
|
1209
|
+
const runtime = request.context;
|
|
1210
|
+
if (!runtime)
|
|
1211
|
+
return undefined;
|
|
1212
|
+
const expectedIds = resolved.map(item => decisionBatchQuestionId(item.alias)).sort();
|
|
1213
|
+
const suppliedIds = runtime.input.questions.map(question => question.id).sort();
|
|
1214
|
+
if (!sameStringSet(expectedIds, suppliedIds)) {
|
|
1215
|
+
throw new ContextPlanError('invalid-input', 'context questions must exactly cover the resolved decision evaluations');
|
|
1216
|
+
}
|
|
1217
|
+
if (request.batching?.enabled) {
|
|
1218
|
+
for (const item of resolved) {
|
|
1219
|
+
const batching = request.batching.evaluations[item.alias];
|
|
1220
|
+
if (batching?.independent && batching.decisionSubject !== runtime.input.subject) {
|
|
1221
|
+
throw new ContextPlanError('invalid-input', `batch subject for '${item.alias}' differs from context subject`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const plan = runtime.plan ?? planDecisionContext(runtime.input, runtime.profile, runtime.estimator);
|
|
1226
|
+
assertContextPlanCurrent(plan, runtime.input, runtime.profile, runtime.estimator);
|
|
1227
|
+
if (runtime.rollout?.mode === 'enforce') {
|
|
1228
|
+
assertContextQualified(runtime.rollout.qualification, runtime.profile, runtime.estimator);
|
|
1229
|
+
}
|
|
1230
|
+
else if (!runtime.rollout && (request.batching?.enabled || request.batchReceipts)) {
|
|
1231
|
+
// D06 default: partitioned native batches stay closed until a rollout mode is chosen explicitly.
|
|
1232
|
+
throw new ContextPlanError('rollout-unqualified', 'native batching with context planning requires rollout observe-only or a qualified enforce mode');
|
|
1233
|
+
}
|
|
1234
|
+
if (request.batchReceipts && request.batchReceipts.contextPlan.planDigest !== plan.planDigest) {
|
|
1235
|
+
throw new ContextPlanError('stale-plan', 'batch receipt context plan differs from runtime context plan', {
|
|
1236
|
+
plannedDigest: request.batchReceipts.contextPlan.planDigest, currentDigest: plan.planDigest,
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
return plan;
|
|
1240
|
+
}
|
|
1241
|
+
function batchTargetIdentity(target) {
|
|
1242
|
+
return { adapter: target.adapter, adapterVersion: target.adapterVersion, model: target.model,
|
|
1243
|
+
credentialRef: target.credentialRef ?? null, subagent: target.subagent ?? null,
|
|
1244
|
+
timeoutMs: target.timeoutMs, retry: target.retry };
|
|
1245
|
+
}
|
|
1246
|
+
function contextPartitionedBatchPlans(plans, contextPlan) {
|
|
1247
|
+
if (!contextPlan)
|
|
1248
|
+
return plans;
|
|
1249
|
+
const split = [];
|
|
1250
|
+
for (const partition of [...contextPlan.partitions].sort((left, right) => left.wave - right.wave || left.id.localeCompare(right.id))) {
|
|
1251
|
+
const permitted = new Set(partition.questionIds);
|
|
1252
|
+
for (const plan of plans) {
|
|
1253
|
+
if (plan.decisionSubject !== partition.subject)
|
|
1254
|
+
continue;
|
|
1255
|
+
const candidates = plan.candidates.filter(candidate => permitted.has(decisionBatchQuestionId(candidate.alias)));
|
|
1256
|
+
if (candidates.length > 1)
|
|
1257
|
+
split.push({ ...plan, candidates });
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return split;
|
|
1261
|
+
}
|
|
1262
|
+
function recordRuntimeContextUsage(plan, questionIds, actualInputTokens, evidence) {
|
|
1263
|
+
const ids = typeof questionIds === 'string' ? [questionIds] : questionIds;
|
|
1264
|
+
const partition = plan.partitions.find(candidate => ids.every(id => candidate.questionIds.includes(id)));
|
|
1265
|
+
if (!partition)
|
|
1266
|
+
throw new ContextPlanError('invalid-input', 'request questions have no common context partition');
|
|
1267
|
+
const recorded = recordContextActualUsage(plan, partition.id, actualInputTokens, ids);
|
|
1268
|
+
const key = recorded.questionIds.join('\0');
|
|
1269
|
+
const index = evidence.findIndex(candidate => candidate.planDigest === plan.planDigest
|
|
1270
|
+
&& candidate.partitionId === partition.id && candidate.questionIds.join('\0') === key);
|
|
1271
|
+
if (index < 0)
|
|
1272
|
+
evidence.push(recorded);
|
|
1273
|
+
else
|
|
1274
|
+
evidence[index] = recorded;
|
|
1275
|
+
}
|
|
1276
|
+
function contextFailureEvidence(error) {
|
|
1277
|
+
const digestDetail = (value) => typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value) ? value : undefined;
|
|
1278
|
+
const plannedDigest = digestDetail(error.details.plannedDigest);
|
|
1279
|
+
const currentDigest = digestDetail(error.details.currentDigest);
|
|
1280
|
+
return { schemaVersion: 'decision-context-failure/v1', reason: error.reason,
|
|
1281
|
+
...(plannedDigest ? { plannedDigest } : {}), ...(currentDigest ? { currentDigest } : {}) };
|
|
1282
|
+
}
|
|
1283
|
+
function decisionContextEvidence(plan, actualUsage, questionId) {
|
|
1284
|
+
const partitionIds = questionId === undefined
|
|
1285
|
+
? new Set(plan.partitions.map(partition => partition.id))
|
|
1286
|
+
: new Set(plan.partitions.filter(partition => partition.questionIds.includes(questionId)).map(partition => partition.id));
|
|
1287
|
+
return { plan: structuredClone(plan), actualUsage: structuredClone(actualUsage.filter(item => partitionIds.has(item.partitionId))) };
|
|
1288
|
+
}
|
|
1289
|
+
function withRulesetContext(result, plan, usage) {
|
|
1290
|
+
return { ...result, spec: { ...result.spec, context: decisionContextEvidence(plan, usage) } };
|
|
1291
|
+
}
|
|
1292
|
+
function contextUsageFromEvaluations(evaluations) {
|
|
1293
|
+
const usage = new Map();
|
|
1294
|
+
for (const result of Object.values(evaluations)) {
|
|
1295
|
+
for (const item of result.spec.context?.actualUsage ?? []) {
|
|
1296
|
+
usage.set(`${item.planDigest}\0${item.partitionId}\0${item.questionIds.join('\0')}`, structuredClone(item));
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return [...usage.values()];
|
|
1300
|
+
}
|
|
1301
|
+
function durableBatchId(invocationId, groupId, partitionId) {
|
|
1302
|
+
return `batch_${createHash('sha256').update(`${invocationId}\0${groupId}\0${partitionId}`, 'utf8').digest('hex').slice(0, 32)}`;
|
|
1303
|
+
}
|
|
1304
|
+
function durableAnswerId(batchId, questionId) {
|
|
1305
|
+
return `answer_${createHash('sha256').update(`${batchId}\0${questionId}`, 'utf8').digest('hex').slice(0, 32)}`;
|
|
1306
|
+
}
|
|
1307
|
+
function sameStringSet(left, right) {
|
|
1308
|
+
return left.length === right.length && left.every(value => right.includes(value));
|
|
1309
|
+
}
|
|
1310
|
+
function uniqueNonNull(values) {
|
|
1311
|
+
return [...new Set(values.filter((value) => value !== null))];
|
|
1312
|
+
}
|
|
1313
|
+
function providerCost(costUsd) {
|
|
1314
|
+
if (costUsd === null)
|
|
1315
|
+
return null;
|
|
1316
|
+
const amountMicros = Math.round(costUsd * 1_000_000);
|
|
1317
|
+
return Number.isSafeInteger(amountMicros) && amountMicros >= 0
|
|
1318
|
+
? { kind: 'provider-authoritative', currency: 'USD', amountMicros }
|
|
1319
|
+
: null;
|
|
1320
|
+
}
|
|
1321
|
+
function uncertainBatchAttempt(plan, started, completed) {
|
|
1322
|
+
const target = plan.candidates[0].target;
|
|
1323
|
+
return { ordinal: 1, adapterId: target.adapter, adapterVersion: target.adapterVersion,
|
|
1324
|
+
requestedModel: target.model, actualModel: null, providerRequestId: null, status: 'execution-uncertain',
|
|
1325
|
+
dispatchedAtEpochMs: started, completedAtEpochMs: completed,
|
|
1326
|
+
usage: { inputTokens: null, outputTokens: null }, cost: { kind: 'unknown' }, fallbackFromAttemptOrdinal: null };
|
|
1327
|
+
}
|
|
1328
|
+
class AttemptBudget {
|
|
1329
|
+
maximum;
|
|
1330
|
+
consumed;
|
|
1331
|
+
constructor(maximum, used = 0) {
|
|
1332
|
+
this.maximum = maximum;
|
|
1333
|
+
this.consumed = used;
|
|
1334
|
+
}
|
|
1335
|
+
get available() { return this.consumed < this.maximum; }
|
|
1336
|
+
claim() { if (!this.available)
|
|
1337
|
+
return false; this.consumed += 1; return true; }
|
|
1338
|
+
consume(count) { this.consumed = Math.min(this.maximum, this.consumed + Math.max(0, count)); }
|
|
1339
|
+
}
|
|
1340
|
+
function effectiveConcurrency(request) {
|
|
1341
|
+
const policy = request.scheduler;
|
|
1342
|
+
// Receipt v2 serializes one pending dispatch. Do not weaken atomic ownership.
|
|
1343
|
+
if (!policy?.enabled || request.receiptStore)
|
|
1344
|
+
return 1;
|
|
1345
|
+
const ceilings = [request.binding.spec.concurrency, policy.callerConcurrency, policy.graphConcurrency,
|
|
1346
|
+
policy.workspace.limits.concurrency, policy.principal.limits.concurrency,
|
|
1347
|
+
...Object.values(policy.providers).map(limits => limits.concurrency)]
|
|
1348
|
+
.filter((value) => value !== undefined && Number.isInteger(value) && value > 0);
|
|
1349
|
+
return Math.max(1, Math.min(...ceilings));
|
|
1350
|
+
}
|
|
1351
|
+
function schedulerLane(request, item) {
|
|
1352
|
+
const policy = request.scheduler;
|
|
1353
|
+
if (!policy?.enabled)
|
|
1354
|
+
return 'serial';
|
|
1355
|
+
const target = request.binding.spec.evaluations[item.alias]?.targets[0];
|
|
1356
|
+
return `${target?.adapter ?? 'none'}\u0000${policy.workspace.id}\u0000${policy.principal.id}`;
|
|
1357
|
+
}
|
|
1358
|
+
function admissionReason(error) {
|
|
1359
|
+
if (error.evidence.reason === 'cancelled')
|
|
1360
|
+
return 'cancelled';
|
|
1361
|
+
if (error.evidence.reason === 'deadline-exceeded' || error.evidence.reason === 'queue-timeout')
|
|
1362
|
+
return 'timeout';
|
|
1363
|
+
if (error.evidence.reason === 'requests-per-minute' || error.evidence.reason === 'tokens-per-second'
|
|
1364
|
+
|| error.evidence.reason === 'retry-after')
|
|
1365
|
+
return 'rate-limited';
|
|
1366
|
+
if (error.evidence.reason === 'attempts' || error.evidence.reason === 'cost' || error.evidence.reason === 'unknown-cost'
|
|
1367
|
+
|| error.evidence.reason === 'tokens' || error.evidence.reason === 'unknown-tokens')
|
|
1368
|
+
return 'budget-exhausted';
|
|
1369
|
+
return 'overloaded';
|
|
1370
|
+
}
|
|
1371
|
+
function releaseAdmissionBudget(request) {
|
|
1372
|
+
if (!request.scheduler?.enabled)
|
|
1373
|
+
return;
|
|
1374
|
+
decisionAdmissionRegistry.controller(request.scheduler.workspace.id)?.releaseBudget(request.invocationId);
|
|
1375
|
+
}
|
|
1376
|
+
function validateSchedulerPolicy(request) {
|
|
1377
|
+
const policy = request.scheduler;
|
|
1378
|
+
if (!policy)
|
|
1379
|
+
return;
|
|
1380
|
+
if (!policy.profileVersion.trim() || !policy.workspace.id.trim() || !policy.principal.id.trim()) {
|
|
1381
|
+
throw new DecisionValidationError('scheduler profile version and trusted scope identities are required');
|
|
1382
|
+
}
|
|
1383
|
+
const limits = [policy.workspace.limits, policy.principal.limits, ...Object.values(policy.providers)];
|
|
1384
|
+
if (!Object.keys(policy.providers).length || limits.some(limit => !Number.isInteger(limit.concurrency) || limit.concurrency < 1)) {
|
|
1385
|
+
throw new DecisionValidationError('scheduler concurrency ceilings must be positive integers');
|
|
1386
|
+
}
|
|
1387
|
+
for (const value of [policy.callerConcurrency, policy.graphConcurrency]) {
|
|
1388
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 1))
|
|
1389
|
+
throw new DecisionValidationError('scheduler caller/graph ceilings must be positive integers');
|
|
1390
|
+
}
|
|
1391
|
+
for (const limit of limits) {
|
|
1392
|
+
for (const value of [limit.requestsPerMinute, limit.tokensPerSecond, limit.maxAttempts, limit.maxBatchSize,
|
|
1393
|
+
limit.maxQueueLength, limit.maxQueueWaitMs, limit.maxRequestBytes, limit.maxItems, limit.maxRetainedWork, limit.maxTokens]) {
|
|
1394
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0))
|
|
1395
|
+
throw new DecisionValidationError('scheduler admission limits must be finite and non-negative');
|
|
1396
|
+
}
|
|
1397
|
+
if (limit.maxCostUsd !== undefined && (!Number.isFinite(limit.maxCostUsd) || limit.maxCostUsd < 0)) {
|
|
1398
|
+
throw new DecisionValidationError('scheduler cost limit must be finite and non-negative');
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (!policy.enabled)
|
|
1402
|
+
return;
|
|
1403
|
+
try {
|
|
1404
|
+
decisionAdmissionRegistry.register(policy, request.now ?? Date.now);
|
|
1405
|
+
}
|
|
1406
|
+
catch (error) {
|
|
1407
|
+
if (error instanceof AdmissionProfileError)
|
|
1408
|
+
throw new DecisionValidationError(error.message);
|
|
1409
|
+
throw error;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function singleBatchEvidence(request, alias) {
|
|
1413
|
+
if (!request.batching)
|
|
1414
|
+
return undefined;
|
|
1415
|
+
const configured = request.batching.evaluations[alias];
|
|
1416
|
+
const target = request.binding.spec.evaluations[alias]?.targets[0];
|
|
1417
|
+
const adapter = target ? request.adapters[target.adapter] : undefined;
|
|
1418
|
+
const reason = !request.batching.enabled ? 'disabled'
|
|
1419
|
+
: configured?.independent && (!adapter?.evaluateMany || Boolean(request.receiptStore)) ? 'unsupported' : 'ineligible';
|
|
1420
|
+
return {
|
|
1421
|
+
mode: 'single',
|
|
1422
|
+
groupId: `single_${decisionBatchQuestionId(alias).slice(2)}`,
|
|
1423
|
+
questionId: decisionBatchQuestionId(alias),
|
|
1424
|
+
degradationReason: reason,
|
|
483
1425
|
};
|
|
484
1426
|
}
|
|
485
1427
|
function classifyValidationFailure(error) {
|
|
1428
|
+
if (error instanceof ContextPlanError) {
|
|
1429
|
+
return error.reason === 'stale-plan' ? 'context-plan-stale'
|
|
1430
|
+
: error.reason === 'rollout-unqualified' ? 'context-unqualified' : 'invalid-input';
|
|
1431
|
+
}
|
|
486
1432
|
if (!(error instanceof DecisionValidationError))
|
|
487
1433
|
return 'invalid-definition';
|
|
488
1434
|
if (/digest/.test(error.message))
|