@aiwg/cli 2026.9.19 → 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.
Files changed (272) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +58 -6
  2. package/agentic/code/providers/model-capabilities.v1.json +34 -0
  3. package/agentic/code/providers/model-catalog.v1.json +55 -13
  4. package/bin/aiwg.mjs +2 -0
  5. package/dist/src/api/index.d.ts +1 -0
  6. package/dist/src/api/index.js +1 -0
  7. package/dist/src/artifacts/enrichment/cli.js +1 -1
  8. package/dist/src/artifacts/views/cli.js +1 -1
  9. package/dist/src/audit/operator-decision.js +10 -0
  10. package/dist/src/auth/credential-store.js +127 -27
  11. package/dist/src/catalog/builtin-models.json +272 -40
  12. package/dist/src/catalog/cli.js +1 -1
  13. package/dist/src/catalog/cli.mjs +1 -1
  14. package/dist/src/cli/handlers/effect.js +872 -0
  15. package/dist/src/cli/handlers/help.js +2 -0
  16. package/dist/src/cli/handlers/index.js +4 -2
  17. package/dist/src/cli/handlers/models.js +41 -10
  18. package/dist/src/cli/handlers/ralph.js +1 -1
  19. package/dist/src/cli/handlers/sessions.js +113 -37
  20. package/dist/src/cli/handlers/subcommands.js +31 -0
  21. package/dist/src/cli/handlers/use.js +46 -9
  22. package/dist/src/cli/handlers/utilities.js +10 -0
  23. package/dist/src/cli/handlers/workspace.js +3 -1
  24. package/dist/src/cli/scope-resolver.js +112 -9
  25. package/dist/src/cli/services/deployment-verification.js +5 -0
  26. package/dist/src/config/aiwg-config.js +3 -0
  27. package/dist/src/decision/acceptance-rollout.js +93 -0
  28. package/dist/src/decision/acceptance.js +175 -0
  29. package/dist/src/decision/adapters/jev.js +234 -13
  30. package/dist/src/decision/adapters/llm-subagent.js +60 -12
  31. package/dist/src/decision/admission.js +550 -0
  32. package/dist/src/decision/batch-benchmark.js +47 -0
  33. package/dist/src/decision/batch-receipts/accounting.js +75 -0
  34. package/dist/src/decision/batch-receipts/export.js +40 -0
  35. package/dist/src/decision/batch-receipts/index.js +9 -0
  36. package/dist/src/decision/batch-receipts/protection.js +40 -0
  37. package/dist/src/decision/batch-receipts/receipt.js +44 -0
  38. package/dist/src/decision/batch-receipts/result-store.js +341 -0
  39. package/dist/src/decision/batch-receipts/store.js +362 -0
  40. package/dist/src/decision/batch-receipts/types.js +2 -0
  41. package/dist/src/decision/batch-receipts/validate.js +127 -0
  42. package/dist/src/decision/batch.js +68 -0
  43. package/dist/src/decision/calibration/index.js +4 -0
  44. package/dist/src/decision/calibration/receipts.js +130 -0
  45. package/dist/src/decision/calibration/registry.js +258 -0
  46. package/dist/src/decision/calibration/types.js +2 -0
  47. package/dist/src/decision/compile-cache/benchmark.js +62 -0
  48. package/dist/src/decision/compile-cache/compatibility.js +65 -0
  49. package/dist/src/decision/compile-cache/file-store.js +280 -0
  50. package/dist/src/decision/compile-cache/identity.js +26 -0
  51. package/dist/src/decision/compile-cache/index.js +9 -0
  52. package/dist/src/decision/compile-cache/prefix.js +21 -0
  53. package/dist/src/decision/compile-cache/records.js +86 -0
  54. package/dist/src/decision/compile-cache/runtime.js +68 -0
  55. package/dist/src/decision/compile-cache/store.js +108 -0
  56. package/dist/src/decision/compile-cache/types.js +2 -0
  57. package/dist/src/decision/context-plan.js +234 -0
  58. package/dist/src/decision/context-qualification.js +60 -0
  59. package/dist/src/decision/ensemble/aggregate.js +215 -0
  60. package/dist/src/decision/ensemble/contract.js +316 -0
  61. package/dist/src/decision/ensemble/drift.js +51 -0
  62. package/dist/src/decision/ensemble/index.js +6 -0
  63. package/dist/src/decision/ensemble/report.js +71 -0
  64. package/dist/src/decision/ensemble/types.js +2 -0
  65. package/dist/src/decision/evaluate.js +1024 -78
  66. package/dist/src/decision/file-lifecycle-store.js +168 -0
  67. package/dist/src/decision/graph-beam.js +40 -0
  68. package/dist/src/decision/graph-budget.js +110 -0
  69. package/dist/src/decision/graph-decision-bridge.js +93 -0
  70. package/dist/src/decision/graph-evidence.js +123 -0
  71. package/dist/src/decision/graph-flow-adapter.js +56 -0
  72. package/dist/src/decision/graph-flow.js +105 -0
  73. package/dist/src/decision/graph-parallel.js +79 -0
  74. package/dist/src/decision/graph-patterns.js +20 -0
  75. package/dist/src/decision/graph-run-store.js +66 -0
  76. package/dist/src/decision/graph-run.js +166 -0
  77. package/dist/src/decision/graph-skill-bridge.js +91 -0
  78. package/dist/src/decision/graph-templates.js +56 -0
  79. package/dist/src/decision/graph.js +115 -0
  80. package/dist/src/decision/index.js +35 -0
  81. package/dist/src/decision/job-accounting.js +93 -0
  82. package/dist/src/decision/job-contract.js +186 -0
  83. package/dist/src/decision/job-effects.js +95 -0
  84. package/dist/src/decision/job-evaluate.js +80 -0
  85. package/dist/src/decision/job-gateway.js +121 -0
  86. package/dist/src/decision/job-http.js +194 -0
  87. package/dist/src/decision/job-payload-store.js +258 -0
  88. package/dist/src/decision/job-poll-limiter.js +92 -0
  89. package/dist/src/decision/job-quota.js +158 -0
  90. package/dist/src/decision/job-runtime.js +240 -0
  91. package/dist/src/decision/job-scheduler.js +26 -0
  92. package/dist/src/decision/job-service.js +117 -0
  93. package/dist/src/decision/job-store.js +264 -0
  94. package/dist/src/decision/job-worker.js +141 -0
  95. package/dist/src/decision/lifecycle.js +148 -0
  96. package/dist/src/decision/patterns/artifacts.js +173 -0
  97. package/dist/src/decision/patterns/catalog.js +108 -0
  98. package/dist/src/decision/patterns/durable-review.js +237 -0
  99. package/dist/src/decision/patterns/index.js +8 -0
  100. package/dist/src/decision/patterns/playground.js +401 -0
  101. package/dist/src/decision/patterns/recorded.js +43 -0
  102. package/dist/src/decision/patterns/specs.js +174 -0
  103. package/dist/src/decision/patterns/types.js +2 -0
  104. package/dist/src/decision/portable-secrets.js +53 -0
  105. package/dist/src/decision/projection.js +210 -0
  106. package/dist/src/decision/qualification/capture.js +67 -0
  107. package/dist/src/decision/qualification/drift.js +68 -0
  108. package/dist/src/decision/qualification/gate-evidence.js +172 -0
  109. package/dist/src/decision/qualification/gates.js +83 -0
  110. package/dist/src/decision/qualification/index.js +12 -0
  111. package/dist/src/decision/qualification/load.js +275 -0
  112. package/dist/src/decision/qualification/manifest.js +37 -0
  113. package/dist/src/decision/qualification/privacy.js +41 -0
  114. package/dist/src/decision/qualification/quality.js +213 -0
  115. package/dist/src/decision/qualification/release.js +116 -0
  116. package/dist/src/decision/qualification/runner.js +408 -0
  117. package/dist/src/decision/qualification/types.js +2 -0
  118. package/dist/src/decision/receipts.js +30 -7
  119. package/dist/src/decision/result-cache/index.js +6 -0
  120. package/dist/src/decision/result-cache/integrity.js +28 -0
  121. package/dist/src/decision/result-cache/key.js +28 -0
  122. package/dist/src/decision/result-cache/service.js +92 -0
  123. package/dist/src/decision/result-cache/store.js +327 -0
  124. package/dist/src/decision/result-cache/types.js +3 -0
  125. package/dist/src/decision/review/authorization.js +110 -0
  126. package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
  127. package/dist/src/decision/review/index.js +12 -0
  128. package/dist/src/decision/review/migrate.js +34 -0
  129. package/dist/src/decision/review/operator-audit.js +59 -0
  130. package/dist/src/decision/review/recovery-adapters.js +252 -0
  131. package/dist/src/decision/review/recovery.js +33 -0
  132. package/dist/src/decision/review/service.js +498 -0
  133. package/dist/src/decision/review/store.js +219 -0
  134. package/dist/src/decision/review/types.js +2 -0
  135. package/dist/src/decision/review/validate.js +205 -0
  136. package/dist/src/decision/scheduler.js +115 -0
  137. package/dist/src/decision/telemetry/context.js +42 -0
  138. package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
  139. package/dist/src/decision/telemetry/exporter.js +77 -0
  140. package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
  141. package/dist/src/decision/telemetry/index.js +13 -0
  142. package/dist/src/decision/telemetry/mapping.js +146 -0
  143. package/dist/src/decision/telemetry/metrics.js +164 -0
  144. package/dist/src/decision/telemetry/otlp-http.js +183 -0
  145. package/dist/src/decision/telemetry/redaction.js +93 -0
  146. package/dist/src/decision/telemetry/retention.js +109 -0
  147. package/dist/src/decision/telemetry/runtime.js +310 -0
  148. package/dist/src/decision/telemetry/trace.js +158 -0
  149. package/dist/src/decision/telemetry/types.js +3 -0
  150. package/dist/src/decision/validate.js +135 -5
  151. package/dist/src/effects/checkpoint-sinks.js +94 -0
  152. package/dist/src/effects/checkpoint.js +76 -0
  153. package/dist/src/effects/decision-links.js +54 -0
  154. package/dist/src/effects/errors.js +48 -0
  155. package/dist/src/effects/identity.js +178 -0
  156. package/dist/src/effects/index.js +25 -0
  157. package/dist/src/effects/keyring.js +160 -0
  158. package/dist/src/effects/keys.js +130 -0
  159. package/dist/src/effects/ledger.js +435 -0
  160. package/dist/src/effects/lifecycle.js +110 -0
  161. package/dist/src/effects/lock-recovery.js +269 -0
  162. package/dist/src/effects/reader.js +103 -0
  163. package/dist/src/effects/records.js +114 -0
  164. package/dist/src/effects/redaction.js +51 -0
  165. package/dist/src/effects/schema.js +73 -0
  166. package/dist/src/effects/store.js +239 -0
  167. package/dist/src/effects/types.js +49 -0
  168. package/dist/src/effects/verifiers/decision.js +168 -0
  169. package/dist/src/effects/verifiers/file.js +106 -0
  170. package/dist/src/effects/verifiers/git.js +206 -0
  171. package/dist/src/effects/verifiers/index.js +90 -0
  172. package/dist/src/effects/verifiers/registry.js +148 -0
  173. package/dist/src/effects/verifiers/review.js +123 -0
  174. package/dist/src/effects/verifiers/tracker.js +299 -0
  175. package/dist/src/effects/verifiers/types.js +21 -0
  176. package/dist/src/effects/verify.js +295 -0
  177. package/dist/src/evidence/bundle.js +2 -0
  178. package/dist/src/extensions/commands/definitions.js +28 -0
  179. package/dist/src/extensions/manifest.js +1 -0
  180. package/dist/src/marketplace/provenance.js +25 -27
  181. package/dist/src/mcp/cli.mjs +11 -1
  182. package/dist/src/mcp/grok-build-config.mjs +189 -0
  183. package/dist/src/mcp/registry.js +21 -1
  184. package/dist/src/mcp/registry.mjs +20 -1
  185. package/dist/src/models/grok-build-models.js +356 -0
  186. package/dist/src/models/index.js +1 -0
  187. package/dist/src/models/model-capabilities.v1.json +34 -0
  188. package/dist/src/models/model-catalog.v1.json +55 -13
  189. package/dist/src/models/model-discovery.js +41 -5
  190. package/dist/src/models/provider-policy.js +3 -2
  191. package/dist/src/plugin/skill-command-translator.js +1 -0
  192. package/dist/src/providers/capability-matrix.yaml +58 -6
  193. package/dist/src/providers/grok-build-acp.js +192 -0
  194. package/dist/src/providers/grok-build-dispatch.js +251 -0
  195. package/dist/src/providers/grok-build-headless.js +170 -0
  196. package/dist/src/providers/muse-paths.js +105 -0
  197. package/dist/src/providers/provider-definitions.js +93 -7
  198. package/dist/src/providers/provider-definitions.mjs +21 -1
  199. package/dist/src/providers/provider-inventory.js +3 -0
  200. package/dist/src/security/artifact-attestation.js +4 -8
  201. package/dist/src/security/signing.js +119 -0
  202. package/dist/src/sessions/adapters/grok-build.js +187 -0
  203. package/dist/src/sessions/adapters/muse.js +412 -0
  204. package/dist/src/sessions/contracts.js +1 -1
  205. package/dist/src/sessions/grok-build-cli.js +196 -0
  206. package/dist/src/sessions/index.js +3 -0
  207. package/dist/src/sessions/workspace-discovery.js +9 -5
  208. package/dist/src/skills/deployer.js +33 -5
  209. package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
  210. package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
  211. package/dist/src/smiths/context-pipeline/finalization.js +1 -32
  212. package/dist/src/smiths/context-pipeline/generator.js +12 -0
  213. package/dist/src/smiths/context-pipeline/index.js +1 -1
  214. package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
  215. package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
  216. package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
  217. package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
  218. package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
  219. package/dist/src/smiths/hook-bridge/index.js +3 -0
  220. package/dist/src/smiths/hook-bridge/shim.js +2 -0
  221. package/dist/src/smiths/hook-bridge/types.js +5 -0
  222. package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
  223. package/dist/src/storage/protected-files.js +102 -0
  224. package/dist/src/tracker/forge-http.js +371 -0
  225. package/dist/src/tracker/project-inputs.js +41 -0
  226. package/package.json +2 -1
  227. package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
  228. package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
  229. package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
  230. package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
  231. package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
  232. package/schemas/decision/DecisionBinding.schema.json +309 -0
  233. package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
  234. package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
  235. package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
  236. package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
  237. package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
  238. package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
  239. package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
  240. package/schemas/decision/DecisionDefinition.schema.json +159 -0
  241. package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
  242. package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
  243. package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
  244. package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
  245. package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
  246. package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
  247. package/schemas/decision/DecisionJob.v1.schema.json +71 -0
  248. package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
  249. package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
  250. package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
  251. package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
  252. package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
  253. package/schemas/decision/DecisionResult.schema.json +524 -0
  254. package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
  255. package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
  256. package/schemas/decision/DecisionReview.schema.json +29 -0
  257. package/schemas/decision/DecisionRuleset.schema.json +313 -0
  258. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
  259. package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
  260. package/schemas/decision/RulesetResult.schema.json +721 -0
  261. package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
  262. package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
  263. package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
  264. package/tools/agents/deploy-agents.mjs +21 -6
  265. package/tools/agents/providers/base.mjs +7 -4
  266. package/tools/agents/providers/codex.mjs +1 -1
  267. package/tools/agents/providers/grok-build.d.mts +7 -0
  268. package/tools/agents/providers/grok-build.mjs +249 -9
  269. package/tools/agents/providers/model-role.mjs +6 -3
  270. package/tools/agents/providers/muse-hooks.mjs +785 -0
  271. package/tools/agents/providers/muse-paths.mjs +112 -0
  272. 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
- return failureResult(base ?? invalidResultBase(request), reason);
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 acquisition = await request.receiptStore.acquire(request.invocationId, projectId, fingerprint);
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
- observation = normalizeObservation(item.definition, target, reconciled);
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
- attemptsAvailable: request.binding.spec.maxAttempts, now, advance };
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
- for (const item of resolved) {
148
- if (evaluations[item.alias])
149
- continue;
150
- if (totalAbort.aborted) {
151
- const reason = request.signal?.aborted ? 'cancelled' : 'timeout';
152
- evaluations[item.alias] = emptyDecisionResult(request, item, rulesetPin, bindingPin, reason === 'cancelled' ? 'cancelled' : 'error', reason);
153
- continue;
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 execution = await evaluateOne({
156
- request,
157
- item,
158
- rulesetPin,
159
- bindingPin,
160
- totalDeadline,
161
- signal: totalAbort,
162
- totalTimeout,
163
- attemptsAvailable: request.binding.spec.maxAttempts - attemptsUsed,
164
- now,
165
- advance,
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
- if (receipt?.state === 'execution-uncertain')
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, evaluations);
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 (attempts.length >= context.attemptsAvailable) {
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 (attempts.length >= context.attemptsAvailable) {
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
- final = normalizeObservation(context.item.definition, target, final);
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 DecisionValidationError ? 'invalid-output' : 'service-error');
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
- await (context.request.delay ?? abortableDelay)(delayMs, context.signal);
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
- const observed = await Promise.race([
331
- adapter.evaluate({
332
- alias: context.item.alias,
333
- definition: structuredClone(context.item.definition),
334
- input: structuredClone(context.item.input),
335
- target: structuredClone(target),
336
- invocationId: `${context.request.invocationId}:${context.item.alias}:${ordinal}`,
337
- deadlineEpochMs,
338
- signal,
339
- callerSignal: context.request.signal ?? new AbortController().signal,
340
- totalSignal: context.signal,
341
- resolveCredential: context.request.resolveCredential ?? unauthorizedCredential,
342
- onRemoteHandle: async (handle) => {
343
- try {
344
- const previous = await context.request.receiptStore?.read(context.request.invocationId, context.request.receiptProjectId ?? 'default');
345
- if (previous && !previous.remoteHandles.includes(handle))
346
- await context.advance('remote-handle-known', { remoteHandles: [...previous.remoteHandles, handle] });
347
- }
348
- catch {
349
- throw new ReceiptPersistenceError();
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
- return context.request.signal?.aborted ? observationFailure('cancelled', 'cancelled', observed, 'caller-cancelled') : observed;
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 decisionResult(context, observation, attempts) {
424
- return {
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, attempts,
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, attemptsAvailable: 0, now: Date.now, advance: async () => undefined }, observationFailure(reason, status), []);
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
- if (request.ruleset.apiVersion === DECISION_API_VERSION_STRUCTURED || request.binding.apiVersion === DECISION_API_VERSION_STRUCTURED
448
- || Object.values(request.definitions).some(definition => definition.apiVersion === DECISION_API_VERSION_STRUCTURED)) {
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 toAttempt(target, ordinal, observation, durationMs) {
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))