@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.
- package/agentic/code/providers/capability-matrix.yaml +58 -6
- package/agentic/code/providers/model-capabilities.v1.json +34 -0
- package/agentic/code/providers/model-catalog.v1.json +55 -13
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/views/cli.js +1 -1
- package/dist/src/audit/operator-decision.js +10 -0
- package/dist/src/auth/credential-store.js +127 -27
- package/dist/src/catalog/builtin-models.json +272 -40
- package/dist/src/catalog/cli.js +1 -1
- package/dist/src/catalog/cli.mjs +1 -1
- package/dist/src/cli/handlers/effect.js +872 -0
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/models.js +41 -10
- package/dist/src/cli/handlers/ralph.js +1 -1
- package/dist/src/cli/handlers/sessions.js +113 -37
- package/dist/src/cli/handlers/subcommands.js +31 -0
- package/dist/src/cli/handlers/use.js +46 -9
- package/dist/src/cli/handlers/utilities.js +10 -0
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/scope-resolver.js +112 -9
- package/dist/src/cli/services/deployment-verification.js +5 -0
- package/dist/src/config/aiwg-config.js +3 -0
- package/dist/src/decision/acceptance-rollout.js +93 -0
- package/dist/src/decision/acceptance.js +175 -0
- package/dist/src/decision/adapters/jev.js +234 -13
- package/dist/src/decision/adapters/llm-subagent.js +60 -12
- package/dist/src/decision/admission.js +550 -0
- package/dist/src/decision/batch-benchmark.js +47 -0
- package/dist/src/decision/batch-receipts/accounting.js +75 -0
- package/dist/src/decision/batch-receipts/export.js +40 -0
- package/dist/src/decision/batch-receipts/index.js +9 -0
- package/dist/src/decision/batch-receipts/protection.js +40 -0
- package/dist/src/decision/batch-receipts/receipt.js +44 -0
- package/dist/src/decision/batch-receipts/result-store.js +341 -0
- package/dist/src/decision/batch-receipts/store.js +362 -0
- package/dist/src/decision/batch-receipts/types.js +2 -0
- package/dist/src/decision/batch-receipts/validate.js +127 -0
- package/dist/src/decision/batch.js +68 -0
- package/dist/src/decision/calibration/index.js +4 -0
- package/dist/src/decision/calibration/receipts.js +130 -0
- package/dist/src/decision/calibration/registry.js +258 -0
- package/dist/src/decision/calibration/types.js +2 -0
- package/dist/src/decision/compile-cache/benchmark.js +62 -0
- package/dist/src/decision/compile-cache/compatibility.js +65 -0
- package/dist/src/decision/compile-cache/file-store.js +280 -0
- package/dist/src/decision/compile-cache/identity.js +26 -0
- package/dist/src/decision/compile-cache/index.js +9 -0
- package/dist/src/decision/compile-cache/prefix.js +21 -0
- package/dist/src/decision/compile-cache/records.js +86 -0
- package/dist/src/decision/compile-cache/runtime.js +68 -0
- package/dist/src/decision/compile-cache/store.js +108 -0
- package/dist/src/decision/compile-cache/types.js +2 -0
- package/dist/src/decision/context-plan.js +234 -0
- package/dist/src/decision/context-qualification.js +60 -0
- package/dist/src/decision/ensemble/aggregate.js +215 -0
- package/dist/src/decision/ensemble/contract.js +316 -0
- package/dist/src/decision/ensemble/drift.js +51 -0
- package/dist/src/decision/ensemble/index.js +6 -0
- package/dist/src/decision/ensemble/report.js +71 -0
- package/dist/src/decision/ensemble/types.js +2 -0
- package/dist/src/decision/evaluate.js +1024 -78
- package/dist/src/decision/file-lifecycle-store.js +168 -0
- package/dist/src/decision/graph-beam.js +40 -0
- package/dist/src/decision/graph-budget.js +110 -0
- package/dist/src/decision/graph-decision-bridge.js +93 -0
- package/dist/src/decision/graph-evidence.js +123 -0
- package/dist/src/decision/graph-flow-adapter.js +56 -0
- package/dist/src/decision/graph-flow.js +105 -0
- package/dist/src/decision/graph-parallel.js +79 -0
- package/dist/src/decision/graph-patterns.js +20 -0
- package/dist/src/decision/graph-run-store.js +66 -0
- package/dist/src/decision/graph-run.js +166 -0
- package/dist/src/decision/graph-skill-bridge.js +91 -0
- package/dist/src/decision/graph-templates.js +56 -0
- package/dist/src/decision/graph.js +115 -0
- package/dist/src/decision/index.js +35 -0
- package/dist/src/decision/job-accounting.js +93 -0
- package/dist/src/decision/job-contract.js +186 -0
- package/dist/src/decision/job-effects.js +95 -0
- package/dist/src/decision/job-evaluate.js +80 -0
- package/dist/src/decision/job-gateway.js +121 -0
- package/dist/src/decision/job-http.js +194 -0
- package/dist/src/decision/job-payload-store.js +258 -0
- package/dist/src/decision/job-poll-limiter.js +92 -0
- package/dist/src/decision/job-quota.js +158 -0
- package/dist/src/decision/job-runtime.js +240 -0
- package/dist/src/decision/job-scheduler.js +26 -0
- package/dist/src/decision/job-service.js +117 -0
- package/dist/src/decision/job-store.js +264 -0
- package/dist/src/decision/job-worker.js +141 -0
- package/dist/src/decision/lifecycle.js +148 -0
- package/dist/src/decision/patterns/artifacts.js +173 -0
- package/dist/src/decision/patterns/catalog.js +108 -0
- package/dist/src/decision/patterns/durable-review.js +237 -0
- package/dist/src/decision/patterns/index.js +8 -0
- package/dist/src/decision/patterns/playground.js +401 -0
- package/dist/src/decision/patterns/recorded.js +43 -0
- package/dist/src/decision/patterns/specs.js +174 -0
- package/dist/src/decision/patterns/types.js +2 -0
- package/dist/src/decision/portable-secrets.js +53 -0
- package/dist/src/decision/projection.js +210 -0
- package/dist/src/decision/qualification/capture.js +67 -0
- package/dist/src/decision/qualification/drift.js +68 -0
- package/dist/src/decision/qualification/gate-evidence.js +172 -0
- package/dist/src/decision/qualification/gates.js +83 -0
- package/dist/src/decision/qualification/index.js +12 -0
- package/dist/src/decision/qualification/load.js +275 -0
- package/dist/src/decision/qualification/manifest.js +37 -0
- package/dist/src/decision/qualification/privacy.js +41 -0
- package/dist/src/decision/qualification/quality.js +213 -0
- package/dist/src/decision/qualification/release.js +116 -0
- package/dist/src/decision/qualification/runner.js +408 -0
- package/dist/src/decision/qualification/types.js +2 -0
- package/dist/src/decision/receipts.js +30 -7
- package/dist/src/decision/result-cache/index.js +6 -0
- package/dist/src/decision/result-cache/integrity.js +28 -0
- package/dist/src/decision/result-cache/key.js +28 -0
- package/dist/src/decision/result-cache/service.js +92 -0
- package/dist/src/decision/result-cache/store.js +327 -0
- package/dist/src/decision/result-cache/types.js +3 -0
- package/dist/src/decision/review/authorization.js +110 -0
- package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
- package/dist/src/decision/review/index.js +12 -0
- package/dist/src/decision/review/migrate.js +34 -0
- package/dist/src/decision/review/operator-audit.js +59 -0
- package/dist/src/decision/review/recovery-adapters.js +252 -0
- package/dist/src/decision/review/recovery.js +33 -0
- package/dist/src/decision/review/service.js +498 -0
- package/dist/src/decision/review/store.js +219 -0
- package/dist/src/decision/review/types.js +2 -0
- package/dist/src/decision/review/validate.js +205 -0
- package/dist/src/decision/scheduler.js +115 -0
- package/dist/src/decision/telemetry/context.js +42 -0
- package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
- package/dist/src/decision/telemetry/exporter.js +77 -0
- package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
- package/dist/src/decision/telemetry/index.js +13 -0
- package/dist/src/decision/telemetry/mapping.js +146 -0
- package/dist/src/decision/telemetry/metrics.js +164 -0
- package/dist/src/decision/telemetry/otlp-http.js +183 -0
- package/dist/src/decision/telemetry/redaction.js +93 -0
- package/dist/src/decision/telemetry/retention.js +109 -0
- package/dist/src/decision/telemetry/runtime.js +310 -0
- package/dist/src/decision/telemetry/trace.js +158 -0
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/validate.js +135 -5
- package/dist/src/effects/checkpoint-sinks.js +94 -0
- package/dist/src/effects/checkpoint.js +76 -0
- package/dist/src/effects/decision-links.js +54 -0
- package/dist/src/effects/errors.js +48 -0
- package/dist/src/effects/identity.js +178 -0
- package/dist/src/effects/index.js +25 -0
- package/dist/src/effects/keyring.js +160 -0
- package/dist/src/effects/keys.js +130 -0
- package/dist/src/effects/ledger.js +435 -0
- package/dist/src/effects/lifecycle.js +110 -0
- package/dist/src/effects/lock-recovery.js +269 -0
- package/dist/src/effects/reader.js +103 -0
- package/dist/src/effects/records.js +114 -0
- package/dist/src/effects/redaction.js +51 -0
- package/dist/src/effects/schema.js +73 -0
- package/dist/src/effects/store.js +239 -0
- package/dist/src/effects/types.js +49 -0
- package/dist/src/effects/verifiers/decision.js +168 -0
- package/dist/src/effects/verifiers/file.js +106 -0
- package/dist/src/effects/verifiers/git.js +206 -0
- package/dist/src/effects/verifiers/index.js +90 -0
- package/dist/src/effects/verifiers/registry.js +148 -0
- package/dist/src/effects/verifiers/review.js +123 -0
- package/dist/src/effects/verifiers/tracker.js +299 -0
- package/dist/src/effects/verifiers/types.js +21 -0
- package/dist/src/effects/verify.js +295 -0
- package/dist/src/evidence/bundle.js +2 -0
- package/dist/src/extensions/commands/definitions.js +28 -0
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/marketplace/provenance.js +25 -27
- package/dist/src/mcp/cli.mjs +11 -1
- package/dist/src/mcp/grok-build-config.mjs +189 -0
- package/dist/src/mcp/registry.js +21 -1
- package/dist/src/mcp/registry.mjs +20 -1
- package/dist/src/models/grok-build-models.js +356 -0
- package/dist/src/models/index.js +1 -0
- package/dist/src/models/model-capabilities.v1.json +34 -0
- package/dist/src/models/model-catalog.v1.json +55 -13
- package/dist/src/models/model-discovery.js +41 -5
- package/dist/src/models/provider-policy.js +3 -2
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +58 -6
- package/dist/src/providers/grok-build-acp.js +192 -0
- package/dist/src/providers/grok-build-dispatch.js +251 -0
- package/dist/src/providers/grok-build-headless.js +170 -0
- package/dist/src/providers/muse-paths.js +105 -0
- package/dist/src/providers/provider-definitions.js +93 -7
- package/dist/src/providers/provider-definitions.mjs +21 -1
- package/dist/src/providers/provider-inventory.js +3 -0
- package/dist/src/security/artifact-attestation.js +4 -8
- package/dist/src/security/signing.js +119 -0
- package/dist/src/sessions/adapters/grok-build.js +187 -0
- package/dist/src/sessions/adapters/muse.js +412 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/grok-build-cli.js +196 -0
- package/dist/src/sessions/index.js +3 -0
- package/dist/src/sessions/workspace-discovery.js +9 -5
- package/dist/src/skills/deployer.js +33 -5
- package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
- package/dist/src/smiths/context-pipeline/finalization.js +1 -32
- package/dist/src/smiths/context-pipeline/generator.js +12 -0
- package/dist/src/smiths/context-pipeline/index.js +1 -1
- package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
- package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
- package/dist/src/smiths/hook-bridge/index.js +3 -0
- package/dist/src/smiths/hook-bridge/shim.js +2 -0
- package/dist/src/smiths/hook-bridge/types.js +5 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
- package/dist/src/storage/protected-files.js +102 -0
- package/dist/src/tracker/forge-http.js +371 -0
- package/dist/src/tracker/project-inputs.js +41 -0
- package/package.json +2 -1
- package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
- package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
- package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
- package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
- package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
- package/schemas/decision/DecisionBinding.schema.json +309 -0
- package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
- package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
- package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
- package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
- package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
- package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
- package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
- package/schemas/decision/DecisionDefinition.schema.json +159 -0
- package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
- package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
- package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
- package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
- package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
- package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
- package/schemas/decision/DecisionJob.v1.schema.json +71 -0
- package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
- package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
- package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
- package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
- package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
- package/schemas/decision/DecisionResult.schema.json +524 -0
- package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
- package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
- package/schemas/decision/DecisionReview.schema.json +29 -0
- package/schemas/decision/DecisionRuleset.schema.json +313 -0
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
- package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
- package/schemas/decision/RulesetResult.schema.json +721 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
- package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
- package/tools/agents/deploy-agents.mjs +21 -6
- package/tools/agents/providers/base.mjs +7 -4
- package/tools/agents/providers/codex.mjs +1 -1
- package/tools/agents/providers/grok-build.d.mts +7 -0
- package/tools/agents/providers/grok-build.mjs +249 -9
- package/tools/agents/providers/model-role.mjs +6 -3
- package/tools/agents/providers/muse-hooks.mjs +785 -0
- package/tools/agents/providers/muse-paths.mjs +112 -0
- package/tools/agents/providers/muse.mjs +336 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
6
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
7
|
+
import { admitEntry } from './entry.js';
|
|
8
|
+
export class DecisionGraphError extends Error {
|
|
9
|
+
constructor(message) { super(message); this.name = 'DecisionGraphError'; }
|
|
10
|
+
}
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const schemaDir = [resolve(here, '../../schemas/decision'), resolve(here, '../../../schemas/decision')]
|
|
13
|
+
.find(path => existsSync(resolve(path, 'DecisionGraph.v1.schema.json')));
|
|
14
|
+
if (!schemaDir)
|
|
15
|
+
throw new Error('Decision graph schema directory is unavailable');
|
|
16
|
+
const schema = JSON.parse(readFileSync(resolve(schemaDir, 'DecisionGraph.v1.schema.json'), 'utf8'));
|
|
17
|
+
const check = new Ajv2020({ strict: false, allErrors: true }).compile(schema);
|
|
18
|
+
const digest = (value) => `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
|
|
19
|
+
const fail = (message) => { throw new DecisionGraphError(message); };
|
|
20
|
+
/** resolvedPins must be supplied by the trusted caller; neither evidence nor model state can supply it. */
|
|
21
|
+
export function planDecisionGraph(value, resolvedPins) {
|
|
22
|
+
try {
|
|
23
|
+
admitEntry(value);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return fail('graph admission denied');
|
|
27
|
+
}
|
|
28
|
+
if (!check(value))
|
|
29
|
+
return fail('invalid decision graph schema');
|
|
30
|
+
const graph = value;
|
|
31
|
+
const nodes = new Map();
|
|
32
|
+
for (const node of graph.nodes) {
|
|
33
|
+
if (nodes.has(node.id))
|
|
34
|
+
return fail('duplicate graph node');
|
|
35
|
+
nodes.set(node.id, node);
|
|
36
|
+
if (!resolvedPins.has(node.definition.digest) || !resolvedPins.has(node.binding.digest))
|
|
37
|
+
return fail('unresolved graph pin');
|
|
38
|
+
}
|
|
39
|
+
const entry = nodes.get(graph.entry);
|
|
40
|
+
if (!entry || entry.stage !== 0 || entry.input.length !== 0)
|
|
41
|
+
return fail('invalid graph entry');
|
|
42
|
+
if (new Set(graph.terminals).size !== graph.terminals.length || graph.terminals.some(id => !nodes.has(id)))
|
|
43
|
+
return fail('invalid graph terminal');
|
|
44
|
+
const inputs = new Map();
|
|
45
|
+
const outgoing = new Map();
|
|
46
|
+
for (const node of graph.nodes) {
|
|
47
|
+
inputs.set(node.id, new Set());
|
|
48
|
+
outgoing.set(node.id, []);
|
|
49
|
+
}
|
|
50
|
+
const edgeKeys = new Set();
|
|
51
|
+
for (const edge of graph.edges) {
|
|
52
|
+
const from = nodes.get(edge.from);
|
|
53
|
+
const to = nodes.get(edge.to);
|
|
54
|
+
if (!from || !to || from.id === to.id || from.stage >= to.stage)
|
|
55
|
+
return fail('invalid graph dependency or cycle');
|
|
56
|
+
if (!from.output.includes(edge.source) || !to.input.includes(edge.destination) ||
|
|
57
|
+
(edge.when && !from.output.includes(edge.when.source)))
|
|
58
|
+
return fail('illegal evidence projection');
|
|
59
|
+
const key = canonicalJson(edge);
|
|
60
|
+
if (edgeKeys.has(key) || inputs.get(to.id).has(edge.destination))
|
|
61
|
+
return fail('ambiguous evidence projection');
|
|
62
|
+
edgeKeys.add(key);
|
|
63
|
+
inputs.get(to.id).add(edge.destination);
|
|
64
|
+
outgoing.get(from.id).push(to.id);
|
|
65
|
+
}
|
|
66
|
+
const reached = new Set([graph.entry]);
|
|
67
|
+
const queue = [graph.entry];
|
|
68
|
+
for (let i = 0; i < queue.length; i++) {
|
|
69
|
+
for (const id of outgoing.get(queue[i]))
|
|
70
|
+
if (!reached.has(id)) {
|
|
71
|
+
reached.add(id);
|
|
72
|
+
queue.push(id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const node of graph.nodes) {
|
|
76
|
+
if (!reached.has(node.id) || (node.id !== graph.entry && (!node.input.length || node.input.some(name => !inputs.get(node.id).has(name)))) ||
|
|
77
|
+
(outgoing.get(node.id).length === 0 ? !graph.terminals.includes(node.id) :
|
|
78
|
+
graph.terminals.includes(node.id) && graph.edges.some(edge => edge.from === node.id && !edge.when))) {
|
|
79
|
+
return fail('ambiguous root, input, or terminal');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const sortedNodes = [...graph.nodes].sort((a, b) => a.stage - b.stage || a.id.localeCompare(b.id));
|
|
83
|
+
const edges = [...graph.edges].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
|
|
84
|
+
const stages = [...new Set(sortedNodes.map(node => node.stage))].map(stage => {
|
|
85
|
+
const stageNodes = sortedNodes.filter(node => node.stage === stage);
|
|
86
|
+
const groups = new Map();
|
|
87
|
+
for (const node of stageNodes) {
|
|
88
|
+
const projections = edges.filter(edge => edge.to === node.id)
|
|
89
|
+
.map(edge => [edge.from, edge.source, edge.destination]);
|
|
90
|
+
const key = canonicalJson([node.subject, node.target, node.model, node.egress, node.stateDigest,
|
|
91
|
+
node.definition.digest, node.binding.digest, projections]);
|
|
92
|
+
const group = groups.get(key) ?? [];
|
|
93
|
+
group.push(node.id);
|
|
94
|
+
groups.set(key, group);
|
|
95
|
+
}
|
|
96
|
+
const batches = [...groups.values()].filter(group => group.length > 1);
|
|
97
|
+
return { stage, nodes: stageNodes.map(node => node.id), batches,
|
|
98
|
+
fanOut: [...groups.values()].filter(group => group.length === 1).flat() };
|
|
99
|
+
});
|
|
100
|
+
if (stages.some((s, i) => s.stage !== i) || stages.length > graph.budget.depth || stages.some(s => s.nodes.length > graph.budget.fanOut) ||
|
|
101
|
+
stages.some(s => s.batches.some(group => group.length > graph.budget.beamWidth)))
|
|
102
|
+
return fail('graph budget exceeded');
|
|
103
|
+
if (graph.stageBudgets && (new Set(graph.stageBudgets.map(item => item.stage)).size !== graph.stageBudgets.length ||
|
|
104
|
+
graph.stageBudgets.some(item => {
|
|
105
|
+
const stage = stages.find(s => s.stage === item.stage);
|
|
106
|
+
return !stage || stage.nodes.length > item.limits.fanOut ||
|
|
107
|
+
stage.batches.some(group => group.length > item.limits.beamWidth) || stage.stage >= item.limits.depth;
|
|
108
|
+
})))
|
|
109
|
+
return fail('invalid stage budget');
|
|
110
|
+
const canonicalGraph = { ...graph, ...(graph.stageBudgets ? { stageBudgets: [...graph.stageBudgets].sort((a, b) => a.stage - b.stage) } : {}),
|
|
111
|
+
terminals: [...graph.terminals].sort(), nodes: sortedNodes.map(node => ({ ...node, input: [...node.input].sort(), output: [...node.output].sort() })), edges };
|
|
112
|
+
const plan = { schemaVersion: 'decision-graph-plan/v1', graphDigest: digest(canonicalGraph), stages, edges };
|
|
113
|
+
return { ...plan, planDigest: digest(plan) };
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -4,9 +4,44 @@ export * from './entry.js';
|
|
|
4
4
|
export * from './convert.js';
|
|
5
5
|
export * from './predicates.js';
|
|
6
6
|
export * from './compose.js';
|
|
7
|
+
export * from './acceptance.js';
|
|
8
|
+
export * from './acceptance-rollout.js';
|
|
9
|
+
export * from './batch.js';
|
|
10
|
+
export * from './job-contract.js';
|
|
11
|
+
export * from './job-store.js';
|
|
12
|
+
export * from './job-runtime.js';
|
|
13
|
+
export * from './job-worker.js';
|
|
14
|
+
export * from './job-evaluate.js';
|
|
15
|
+
export * from './job-accounting.js';
|
|
16
|
+
export * from './job-scheduler.js';
|
|
17
|
+
export * from './job-gateway.js';
|
|
18
|
+
export * from './job-quota.js';
|
|
19
|
+
export * from './job-poll-limiter.js';
|
|
20
|
+
export * from './job-service.js';
|
|
21
|
+
export * from './job-payload-store.js';
|
|
22
|
+
export * from './job-http.js';
|
|
23
|
+
export * from './job-effects.js';
|
|
24
|
+
export * from './batch-benchmark.js';
|
|
25
|
+
export * from './scheduler.js';
|
|
26
|
+
export * from './admission.js';
|
|
27
|
+
export * from './batch-receipts/index.js';
|
|
28
|
+
export * from './context-plan.js';
|
|
29
|
+
export * from './context-qualification.js';
|
|
30
|
+
export * from './calibration/index.js';
|
|
31
|
+
export * from './projection.js';
|
|
32
|
+
export * from './lifecycle.js';
|
|
33
|
+
export * from './file-lifecycle-store.js';
|
|
7
34
|
export * from './receipts.js';
|
|
35
|
+
export * from './portable-secrets.js';
|
|
8
36
|
export * from './evaluate.js';
|
|
9
37
|
export * from './export.js';
|
|
10
38
|
export * from './adapters/jev.js';
|
|
11
39
|
export * from './adapters/llm-subagent.js';
|
|
40
|
+
export * from './review/index.js';
|
|
41
|
+
export * from './qualification/index.js';
|
|
42
|
+
export * from './telemetry/index.js';
|
|
43
|
+
export * from './patterns/index.js';
|
|
44
|
+
export * from './compile-cache/index.js';
|
|
45
|
+
export * from './result-cache/index.js';
|
|
46
|
+
export * from './ensemble/index.js';
|
|
12
47
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { artifactDigest } from './validate.js';
|
|
2
|
+
import { batchAccountingTotals } from './batch-receipts/accounting.js';
|
|
3
|
+
import { JobConflictError } from './job-store.js';
|
|
4
|
+
const empty = () => ({ inputTokens: 0, outputTokens: 0, knownCostMicros: 0,
|
|
5
|
+
unknownUsage: false, unknownCost: false, costProvenance: 'client-derived', complete: true });
|
|
6
|
+
function add(target, input, output, costMicros) {
|
|
7
|
+
if (input === null || output === null)
|
|
8
|
+
target.unknownUsage = true;
|
|
9
|
+
if (costMicros === null) {
|
|
10
|
+
target.unknownCost = true;
|
|
11
|
+
target.costProvenance = 'unknown';
|
|
12
|
+
}
|
|
13
|
+
for (const [field, value] of [['inputTokens', input], ['outputTokens', output], ['knownCostMicros', costMicros]]) {
|
|
14
|
+
if (value === null)
|
|
15
|
+
continue;
|
|
16
|
+
if (!Number.isSafeInteger(value) || value < 0 || !Number.isSafeInteger(target[field] + value))
|
|
17
|
+
throw new JobConflictError('Invalid job usage total');
|
|
18
|
+
target[field] += value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Read only: receipt ownership is authoritative; per-answer native batch allocations are ignored. */
|
|
22
|
+
export async function accountDecisionJob(job, receipts, batches) {
|
|
23
|
+
const total = empty();
|
|
24
|
+
const items = Object.create(null);
|
|
25
|
+
const seenBatches = new Map();
|
|
26
|
+
for (const item of job.items) {
|
|
27
|
+
const usage = empty();
|
|
28
|
+
items[item.id] = usage;
|
|
29
|
+
for (const attempt of item.attempts) {
|
|
30
|
+
if (!attempt.receiptDigest) {
|
|
31
|
+
usage.complete = false;
|
|
32
|
+
usage.unknownUsage = true;
|
|
33
|
+
usage.unknownCost = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const receipt = await receipts.read(attempt.id, job.scope.projectId);
|
|
37
|
+
if (!receipt || receipt.invocationId !== attempt.id || receipt.projectId !== job.scope.projectId ||
|
|
38
|
+
artifactDigest(receipt) !== attempt.receiptDigest || receipt.state !== 'completed' || !receipt.result ||
|
|
39
|
+
receipt.result.spec.invocationId !== attempt.id ||
|
|
40
|
+
Object.values(receipt.result.spec.evaluations).some(evaluation => evaluation.spec.invocationId !== attempt.id)) {
|
|
41
|
+
throw new JobConflictError('Job receipt unavailable or changed');
|
|
42
|
+
}
|
|
43
|
+
for (const evaluation of Object.values(receipt.result.spec.evaluations)) {
|
|
44
|
+
for (const providerAttempt of evaluation.spec.attempts) {
|
|
45
|
+
if (providerAttempt.batch?.mode === 'native') {
|
|
46
|
+
const ref = evaluation.spec.batchResult;
|
|
47
|
+
if (!ref || !batches) {
|
|
48
|
+
usage.complete = false;
|
|
49
|
+
usage.unknownUsage = true;
|
|
50
|
+
usage.unknownCost = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const owningItem = seenBatches.get(ref.batchId);
|
|
54
|
+
if (owningItem && owningItem !== item.id)
|
|
55
|
+
throw new JobConflictError('Native batch shared across job subjects');
|
|
56
|
+
if (owningItem)
|
|
57
|
+
continue;
|
|
58
|
+
const batch = await batches.read(ref.batchId, job.scope.tenantId, job.scope.projectId);
|
|
59
|
+
if (!batch || batch.tenantId !== job.scope.tenantId || batch.projectId !== job.scope.projectId ||
|
|
60
|
+
batch.invocationId !== attempt.id || batch.status !== 'completed' ||
|
|
61
|
+
batch.revision < ref.receiptRevision || !batch.questionIds.includes(ref.questionId) ||
|
|
62
|
+
!batch.answerReferences.some(answer => answer.questionId === ref.questionId && answer.answerId === ref.answerId))
|
|
63
|
+
throw new JobConflictError('Native batch receipt unavailable or outside item invocation');
|
|
64
|
+
seenBatches.set(ref.batchId, item.id);
|
|
65
|
+
const owner = batchAccountingTotals(batch);
|
|
66
|
+
add(usage, owner.usage.inputTokens, owner.usage.outputTokens, owner.cost.unknownAttemptCount === 0 ? owner.cost.knownAmountMicros : owner.cost.conservativeUpperBoundMicros);
|
|
67
|
+
if (owner.cost.unknownAttemptCount) {
|
|
68
|
+
usage.unknownCost = owner.cost.conservativeUpperBoundMicros === null;
|
|
69
|
+
usage.costProvenance = usage.unknownCost ? 'unknown' : 'estimate';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const providerUsage = providerAttempt.usage;
|
|
74
|
+
const amount = providerUsage.costUsd;
|
|
75
|
+
add(usage, providerUsage.inputTokens, providerUsage.outputTokens, amount === null || !Number.isFinite(amount) || amount < 0 ? null : Math.round(amount * 1_000_000));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
add(total, usage.inputTokens, usage.outputTokens, usage.knownCostMicros);
|
|
81
|
+
total.complete &&= usage.complete;
|
|
82
|
+
total.unknownCost ||= usage.unknownCost;
|
|
83
|
+
total.unknownUsage ||= usage.unknownUsage;
|
|
84
|
+
if (usage.costProvenance === 'estimate' && total.costProvenance !== 'unknown')
|
|
85
|
+
total.costProvenance = 'estimate';
|
|
86
|
+
}
|
|
87
|
+
if (total.unknownCost)
|
|
88
|
+
total.costProvenance = 'unknown';
|
|
89
|
+
return { ...total, items,
|
|
90
|
+
overTokenBudget: total.unknownUsage || total.inputTokens + total.outputTokens > job.budget.maxTokens,
|
|
91
|
+
overCostBudget: total.unknownCost || total.knownCostMicros > job.budget.maxCostMicros };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=job-accounting.js.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
6
|
+
import { admitEntry } from './entry.js';
|
|
7
|
+
/** Versioned offline contract. Provider dispatch is not enabled by the job layer. */
|
|
8
|
+
export const ITEM_STATES = [
|
|
9
|
+
'queued', 'running', 'succeeded', 'abstained', 'review', 'unsupported', 'retryable-failed',
|
|
10
|
+
'permanent-failed', 'canceled', 'expired', 'execution-unknown',
|
|
11
|
+
];
|
|
12
|
+
export class DecisionJobContractError extends Error {
|
|
13
|
+
constructor(message) { super(message); this.name = 'DecisionJobContractError'; }
|
|
14
|
+
}
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const dir = [resolve(here, '../../schemas/decision'), resolve(here, '../../../schemas/decision')]
|
|
17
|
+
.find(path => existsSync(resolve(path, 'DecisionJob.v1.schema.json')));
|
|
18
|
+
if (!dir)
|
|
19
|
+
throw new Error('Decision job schema directory is unavailable');
|
|
20
|
+
const schema = JSON.parse(readFileSync(resolve(dir, 'DecisionJob.v1.schema.json'), 'utf8'));
|
|
21
|
+
const check = new Ajv2020({ strict: false, allErrors: true }).compile(schema);
|
|
22
|
+
const reject = (message) => { throw new DecisionJobContractError(message); };
|
|
23
|
+
const transitions = {
|
|
24
|
+
validating: ['queued', 'failed', 'canceled', 'expired'],
|
|
25
|
+
queued: ['running', 'cancel-requested', 'expired', 'failed'],
|
|
26
|
+
running: ['running', 'partially-completed', 'completed', 'cancel-requested', 'expired', 'failed'],
|
|
27
|
+
'partially-completed': ['partially-completed', 'completed', 'cancel-requested', 'expired', 'failed'],
|
|
28
|
+
'cancel-requested': ['canceled', 'partially-completed', 'failed', 'expired'],
|
|
29
|
+
completed: [], canceled: [], expired: [], failed: [],
|
|
30
|
+
};
|
|
31
|
+
const itemTransitions = {
|
|
32
|
+
queued: ['running', 'canceled', 'expired'],
|
|
33
|
+
running: ['succeeded', 'abstained', 'review', 'unsupported', 'retryable-failed',
|
|
34
|
+
'permanent-failed', 'execution-unknown', 'canceled', 'expired'],
|
|
35
|
+
'retryable-failed': ['queued', 'running', 'canceled', 'expired'],
|
|
36
|
+
succeeded: [], abstained: [], review: [], unsupported: [], 'permanent-failed': [],
|
|
37
|
+
canceled: [], expired: [], 'execution-unknown': [],
|
|
38
|
+
};
|
|
39
|
+
const RESULT_STATES = ['succeeded', 'abstained', 'review'];
|
|
40
|
+
function validResolution(attempt) {
|
|
41
|
+
const resolution = attempt.resolution;
|
|
42
|
+
return !resolution || (resolution.method === 'effect-ledger' && resolution.reason === 'digest-match' &&
|
|
43
|
+
attempt.outcome === 'succeeded' && !!attempt.receiptDigest && resolution.receiptDigest === attempt.receiptDigest);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The one gated transition out of `execution-unknown` (D16 opt-in resolver,
|
|
47
|
+
* #2722): the latest attempt moves from `execution-unknown` to `succeeded`
|
|
48
|
+
* with a receipt digest bound by a `digest-match` resolution, the item gains
|
|
49
|
+
* its result digest, and nothing else changes.
|
|
50
|
+
*/
|
|
51
|
+
export function isExecutionUnknownResolution(previous, item) {
|
|
52
|
+
const before = previous.attempts.at(-1);
|
|
53
|
+
const after = item.attempts.at(-1);
|
|
54
|
+
if (previous.state !== 'execution-unknown' || !RESULT_STATES.includes(item.state) || !before || !after ||
|
|
55
|
+
item.attempts.length !== previous.attempts.length || before.outcome !== 'execution-unknown' ||
|
|
56
|
+
before.receiptDigest !== undefined || before.resolution !== undefined ||
|
|
57
|
+
previous.resultDigest !== undefined || !item.resultDigest || item.errorCode !== previous.errorCode)
|
|
58
|
+
return false;
|
|
59
|
+
if (previous.attempts.slice(0, -1).some((attempt, index) => canonicalJson(attempt) !== canonicalJson(item.attempts[index])))
|
|
60
|
+
return false;
|
|
61
|
+
const { receiptDigest, resolution, outcome, ...rest } = after;
|
|
62
|
+
const { outcome: _previousOutcome, ...previousRest } = before;
|
|
63
|
+
return outcome === 'succeeded' && !!receiptDigest && !!resolution && validResolution(after) &&
|
|
64
|
+
canonicalJson(rest) === canonicalJson(previousRest);
|
|
65
|
+
}
|
|
66
|
+
function sameItem(previous, item) {
|
|
67
|
+
return !!item && canonicalJson(previous) === canonicalJson(item);
|
|
68
|
+
}
|
|
69
|
+
export function validateDecisionJob(value) {
|
|
70
|
+
try {
|
|
71
|
+
admitEntry(value);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return reject('job admission denied');
|
|
75
|
+
}
|
|
76
|
+
if (!check(value))
|
|
77
|
+
return reject('invalid decision job schema');
|
|
78
|
+
const job = value;
|
|
79
|
+
if (job.expiresAtEpochMs <= job.createdAtEpochMs)
|
|
80
|
+
return reject('invalid job expiry');
|
|
81
|
+
const ids = new Set();
|
|
82
|
+
let reservedTokens = 0;
|
|
83
|
+
let reservedCostMicros = 0;
|
|
84
|
+
const actual = Object.fromEntries(ITEM_STATES.map(state => [state, 0]));
|
|
85
|
+
for (const item of job.items) {
|
|
86
|
+
if (ids.has(item.id))
|
|
87
|
+
return reject('duplicate job item ID');
|
|
88
|
+
ids.add(item.id);
|
|
89
|
+
actual[item.state]++;
|
|
90
|
+
if (item.attempts.length > job.budget.maxAttempts ||
|
|
91
|
+
new Set(item.attempts.map(attempt => attempt.id)).size !== item.attempts.length)
|
|
92
|
+
return reject('invalid item attempts');
|
|
93
|
+
if (['succeeded', 'abstained', 'review'].includes(item.state) &&
|
|
94
|
+
(!item.resultDigest || !item.attempts.some(attempt => attempt.outcome === 'succeeded' && attempt.receiptDigest)))
|
|
95
|
+
return reject('result item lacks validated receipt');
|
|
96
|
+
if (item.resultDigest && !['succeeded', 'abstained', 'review'].includes(item.state))
|
|
97
|
+
return reject('invalid item result');
|
|
98
|
+
if (item.state === 'execution-unknown' && !item.attempts.some(attempt => attempt.outcome === 'execution-unknown'))
|
|
99
|
+
return reject('unreconciled item missing attempt');
|
|
100
|
+
for (const attempt of item.attempts) {
|
|
101
|
+
if ((attempt.reservedTokens === undefined) !== (attempt.reservedCostMicros === undefined))
|
|
102
|
+
return reject('incomplete job reservation');
|
|
103
|
+
reservedTokens += attempt.reservedTokens ?? 0;
|
|
104
|
+
reservedCostMicros += attempt.reservedCostMicros ?? 0;
|
|
105
|
+
if (!Number.isSafeInteger(reservedTokens) || !Number.isSafeInteger(reservedCostMicros))
|
|
106
|
+
return reject('job reservation overflow');
|
|
107
|
+
if (!validResolution(attempt))
|
|
108
|
+
return reject('unbound execution-unknown resolution');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (reservedTokens > job.budget.maxTokens || reservedCostMicros > job.budget.maxCostMicros)
|
|
112
|
+
return reject('job budget reservation exceeded');
|
|
113
|
+
if (Object.keys(job.summary).length !== ITEM_STATES.length ||
|
|
114
|
+
ITEM_STATES.some(state => job.summary[state] !== actual[state]))
|
|
115
|
+
return reject('job summary mismatch');
|
|
116
|
+
if (['validating', 'queued'].includes(job.state) && (actual.running || actual.succeeded || actual['execution-unknown']))
|
|
117
|
+
return reject('unstarted job contains dispatched items');
|
|
118
|
+
if (job.state === 'canceled' && (actual.running || actual.queued || actual['execution-unknown']))
|
|
119
|
+
return reject('canceled job conceals in-flight ambiguity');
|
|
120
|
+
if (job.state === 'completed' && (actual.running || actual.queued || actual['retryable-failed'] || actual['execution-unknown']))
|
|
121
|
+
return reject('completed job conceals unfinished items');
|
|
122
|
+
}
|
|
123
|
+
/** A same-state job revision whose only changes are gated `execution-unknown` resolutions. */
|
|
124
|
+
function resolutionOnly(before, after) {
|
|
125
|
+
if (before.state !== after.state || before.items.length !== after.items.length)
|
|
126
|
+
return false;
|
|
127
|
+
let resolved = 0;
|
|
128
|
+
for (const [index, previous] of before.items.entries()) {
|
|
129
|
+
const item = after.items[index];
|
|
130
|
+
if (!item || item.id !== previous.id)
|
|
131
|
+
return false;
|
|
132
|
+
if (isExecutionUnknownResolution(previous, item))
|
|
133
|
+
resolved++;
|
|
134
|
+
else if (!sameItem(previous, item))
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return resolved > 0;
|
|
138
|
+
}
|
|
139
|
+
export function assertJobTransition(before, after) {
|
|
140
|
+
validateDecisionJob(before);
|
|
141
|
+
validateDecisionJob(after);
|
|
142
|
+
if (before.id !== after.id || before.fingerprint !== after.fingerprint ||
|
|
143
|
+
canonicalJson(before.scope) !== canonicalJson(after.scope) ||
|
|
144
|
+
before.createdAtEpochMs !== after.createdAtEpochMs || before.expiresAtEpochMs !== after.expiresAtEpochMs ||
|
|
145
|
+
canonicalJson(before.budget) !== canonicalJson(after.budget) ||
|
|
146
|
+
!(transitions[before.state].includes(after.state) || resolutionOnly(before, after)))
|
|
147
|
+
return reject('illegal job transition or changed identity');
|
|
148
|
+
const next = new Map(after.items.map(item => [item.id, item]));
|
|
149
|
+
if (next.size !== before.items.length || after.items.length !== before.items.length)
|
|
150
|
+
return reject('job items changed');
|
|
151
|
+
for (const [index, previous] of before.items.entries()) {
|
|
152
|
+
const item = after.items[index];
|
|
153
|
+
if (item?.id !== previous.id)
|
|
154
|
+
return reject('job item order changed');
|
|
155
|
+
if (!item || previous.fingerprint !== item.fingerprint || previous.subjectDigest !== item.subjectDigest ||
|
|
156
|
+
previous.definitionDigest !== item.definitionDigest || previous.bindingDigest !== item.bindingDigest ||
|
|
157
|
+
previous.rulesetDigest !== item.rulesetDigest)
|
|
158
|
+
return reject('illegal item transition, mutated pins or attempt history');
|
|
159
|
+
if (isExecutionUnknownResolution(previous, item))
|
|
160
|
+
continue;
|
|
161
|
+
if ((previous.state !== item.state && !itemTransitions[previous.state].includes(item.state)) ||
|
|
162
|
+
item.attempts.length < previous.attempts.length || item.attempts.length > previous.attempts.length + 1 ||
|
|
163
|
+
(item.attempts.length > previous.attempts.length &&
|
|
164
|
+
!(item.state === 'running' && ['queued', 'retryable-failed'].includes(previous.state) &&
|
|
165
|
+
item.attempts.at(-1)?.outcome === 'dispatched')) ||
|
|
166
|
+
(['succeeded', 'abstained', 'review', 'unsupported', 'permanent-failed', 'canceled', 'expired', 'execution-unknown'].includes(previous.state) &&
|
|
167
|
+
item.attempts.length !== previous.attempts.length) ||
|
|
168
|
+
(previous.state === 'running' && item.state === 'canceled' && previous.attempts.some(attempt => attempt.outcome === 'dispatched')) ||
|
|
169
|
+
(previous.resultDigest !== undefined && item.resultDigest !== previous.resultDigest) ||
|
|
170
|
+
(previous.errorCode !== undefined && item.errorCode !== previous.errorCode) ||
|
|
171
|
+
(previous.state === item.state &&
|
|
172
|
+
(previous.resultDigest !== item.resultDigest || previous.errorCode !== item.errorCode)) ||
|
|
173
|
+
previous.attempts.some((attempt, index) => canonicalJson(attempt) !== canonicalJson(item.attempts[index]) &&
|
|
174
|
+
!(previous.state === 'running' && index === previous.attempts.length - 1 &&
|
|
175
|
+
attempt.outcome === 'dispatched' &&
|
|
176
|
+
(item.attempts[index]?.outcome === 'execution-unknown' && item.state === 'execution-unknown' ||
|
|
177
|
+
item.attempts[index]?.outcome === 'succeeded' && ['succeeded', 'abstained', 'review'].includes(item.state) && !!item.attempts[index]?.receiptDigest ||
|
|
178
|
+
item.attempts[index]?.outcome === 'failed' && ['retryable-failed', 'permanent-failed', 'unsupported'].includes(item.state)) &&
|
|
179
|
+
attempt.id === item.attempts[index]?.id && attempt.requestDigest === item.attempts[index]?.requestDigest &&
|
|
180
|
+
attempt.reservedTokens === item.attempts[index]?.reservedTokens &&
|
|
181
|
+
attempt.reservedCostMicros === item.attempts[index]?.reservedCostMicros &&
|
|
182
|
+
(item.attempts[index]?.outcome === 'execution-unknown' ? !item.attempts[index]?.receiptDigest : true))))
|
|
183
|
+
return reject('illegal item transition, mutated pins or attempt history');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=job-contract.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D16 adoption of the AIWG effect ledger (#2722).
|
|
3
|
+
*
|
|
4
|
+
* - `ledgerJobEffectRecorder` records a `decision.receipt` effect for a job
|
|
5
|
+
* item attempt once the admitted evaluator has written its D03 receipt and
|
|
6
|
+
* before the job record is updated. The effect's payload digest is the D03
|
|
7
|
+
* receipt digest, so the ledger holds digest references only.
|
|
8
|
+
* - `ledgerExecutionUnknownResolver` is the opt-in resolver for
|
|
9
|
+
* `DecisionJobRuntime.reconcile(..., { resolveUnknown })`. It resolves an
|
|
10
|
+
* `execution-unknown` item only when the `decision.receipt` verifier reports
|
|
11
|
+
* `present` / `digest-match` against the digest the ledger recorded, and the
|
|
12
|
+
* D03 receipt still has that digest. `state-match`, `absent` and `unknown`
|
|
13
|
+
* never resolve, and nothing is ever dispatched or replayed.
|
|
14
|
+
*
|
|
15
|
+
* Effect identity: kind `decision.receipt`, target
|
|
16
|
+
* `decision:job/<jobId>/<itemId>`, context the job scope plus `attemptId`,
|
|
17
|
+
* subsystem `job`. The ledger's tenant and project are the job's.
|
|
18
|
+
*
|
|
19
|
+
* @see docs/decision/async-jobs.md "Resolving execution-unknown"
|
|
20
|
+
* @see docs/contracts/effect-ledger.v1.md "Composition"
|
|
21
|
+
*/
|
|
22
|
+
import { createVerifierRegistry, decisionReceiptVerifier, effectId as deriveEffectId, lookupEffect, reconcileEffect, recordIntent, } from '../effects/index.js';
|
|
23
|
+
import { decisionResultJobState } from './job-evaluate.js';
|
|
24
|
+
import { artifactDigest } from './validate.js';
|
|
25
|
+
export const JOB_EFFECT_KIND = 'decision.receipt';
|
|
26
|
+
/** The ledger identity (kind, target, context) of one job item attempt. */
|
|
27
|
+
export function jobItemEffectIdentity(scope, jobId, itemId, attemptId) {
|
|
28
|
+
return {
|
|
29
|
+
kind: JOB_EFFECT_KIND,
|
|
30
|
+
target: `decision:job/${jobId}/${itemId}`,
|
|
31
|
+
context: { tenantId: scope.tenantId, projectId: scope.projectId, workspaceId: scope.workspaceId, principalId: scope.principalId, attemptId },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** The effect ID of one job item attempt in a `job` ledger for the job's tenant and project. */
|
|
35
|
+
export function jobItemEffectId(scope, jobId, itemId, attemptId) {
|
|
36
|
+
const identity = jobItemEffectIdentity(scope, jobId, itemId, attemptId);
|
|
37
|
+
return deriveEffectId({ scope: { tenant: scope.tenantId, project: scope.projectId, subsystem: 'job' }, ...identity });
|
|
38
|
+
}
|
|
39
|
+
function assertJobLedger(options) {
|
|
40
|
+
if (options.ledger.scope.subsystem !== 'job')
|
|
41
|
+
throw new Error('Job effect ledger must use the job subsystem');
|
|
42
|
+
}
|
|
43
|
+
const inScope = (options, scope) => options.ledger.scope.tenant === scope.tenantId && options.ledger.scope.project === scope.projectId;
|
|
44
|
+
function registry(options) {
|
|
45
|
+
return createVerifierRegistry([decisionReceiptVerifier({ receipts: options.receipts, jobs: options.jobs })]);
|
|
46
|
+
}
|
|
47
|
+
/** Records the D03 receipt of a finished attempt: a signed intent carrying the receipt digest, then a verified `completed`. */
|
|
48
|
+
export function ledgerJobEffectRecorder(options) {
|
|
49
|
+
assertJobLedger(options);
|
|
50
|
+
return {
|
|
51
|
+
async recordReceipt({ scope, jobId, itemId, attemptId, receiptDigest }) {
|
|
52
|
+
if (!inScope(options, scope))
|
|
53
|
+
throw new Error('Job effect ledger scope mismatch');
|
|
54
|
+
const identity = jobItemEffectIdentity(scope, jobId, itemId, attemptId);
|
|
55
|
+
const intent = await recordIntent(options.ledger, { ...identity, payloadDigest: receiptDigest, links: options.links });
|
|
56
|
+
await reconcileEffect(options.ledger, intent.effectId, {
|
|
57
|
+
verifiers: registry(options), expected: { digest: receiptDigest }, links: options.links,
|
|
58
|
+
...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The opt-in D16 resolver backed by the effect ledger. Resolves only on `digest-match`. */
|
|
64
|
+
export function ledgerExecutionUnknownResolver(options) {
|
|
65
|
+
assertJobLedger(options);
|
|
66
|
+
return async ({ job, item, attempt }) => {
|
|
67
|
+
if (!inScope(options, job.scope) || item.state !== 'execution-unknown' || attempt.outcome !== 'execution-unknown')
|
|
68
|
+
return null;
|
|
69
|
+
const id = jobItemEffectId(job.scope, job.id, item.id, attempt.id);
|
|
70
|
+
const lookup = await lookupEffect(options.ledger, id);
|
|
71
|
+
const intent = lookup.records.find(record => record.phase === 'intent');
|
|
72
|
+
// Without a recorded digest there is nothing to match: the item stays execution-unknown.
|
|
73
|
+
if (!intent || lookup.tombstoned)
|
|
74
|
+
return null;
|
|
75
|
+
const recorded = intent.payloadDigest;
|
|
76
|
+
const outcome = await reconcileEffect(options.ledger, id, {
|
|
77
|
+
verifiers: registry(options), expected: { digest: recorded }, links: options.links,
|
|
78
|
+
...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
|
|
79
|
+
});
|
|
80
|
+
const verification = outcome.result.verification;
|
|
81
|
+
if (verification.result !== 'present' || verification.reason !== 'digest-match')
|
|
82
|
+
return null;
|
|
83
|
+
const receipt = await options.receipts.read(attempt.id, job.scope.projectId);
|
|
84
|
+
if (!receipt || receipt.state !== 'completed' || !receipt.result || artifactDigest(receipt) !== recorded)
|
|
85
|
+
return null;
|
|
86
|
+
const state = decisionResultJobState(receipt.result);
|
|
87
|
+
if (state !== 'succeeded' && state !== 'review')
|
|
88
|
+
return null;
|
|
89
|
+
return {
|
|
90
|
+
state, effectId: id, reason: 'digest-match',
|
|
91
|
+
receiptDigest: recorded, resultDigest: artifactDigest(receipt.result),
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=job-effects.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
2
|
+
import { artifactDigest } from './validate.js';
|
|
3
|
+
import { evaluateDecisionRuleset } from './evaluate.js';
|
|
4
|
+
import { JobConflictError } from './job-store.js';
|
|
5
|
+
/** Job item state for a completed D03 decision result; `null` for an unsupported status. */
|
|
6
|
+
export function decisionResultJobState(result) {
|
|
7
|
+
const status = result?.spec?.status;
|
|
8
|
+
if (status === 'review')
|
|
9
|
+
return 'review';
|
|
10
|
+
if (status === 'completed' || status === 'defaulted')
|
|
11
|
+
return 'succeeded';
|
|
12
|
+
if (status === 'error' || status === 'cancelled')
|
|
13
|
+
return 'permanent-failed';
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Host-only bridge to the synchronous evaluator. Never pass model-authored scope or adapters.
|
|
18
|
+
* Each job item receives an independent invocation/receipt; native same-state batches remain
|
|
19
|
+
* inside that invocation, never across two subjects.
|
|
20
|
+
*/
|
|
21
|
+
export function admittedJobItemExecutor(job, requestFor) {
|
|
22
|
+
const execute = async (item, signal) => {
|
|
23
|
+
const attempt = item.attempts.at(-1);
|
|
24
|
+
if (!attempt || attempt.outcome !== 'dispatched')
|
|
25
|
+
throw new JobConflictError('Missing durable dispatch fence');
|
|
26
|
+
const request = requestFor(item, signal);
|
|
27
|
+
const scheduler = request.scheduler;
|
|
28
|
+
if (!scheduler?.enabled || scheduler.workspace.id !== job.scope.workspaceId ||
|
|
29
|
+
scheduler.principal.id !== job.scope.principalId || !request.receiptStore ||
|
|
30
|
+
request.receiptProjectId !== job.scope.projectId || request.invocationId !== attempt.id ||
|
|
31
|
+
request.signal !== signal || scheduler.principal.limits.allowUnknownCost !== false ||
|
|
32
|
+
scheduler.principal.limits.maxCostUsd === undefined ||
|
|
33
|
+
scheduler.principal.limits.maxCostUsd > job.budget.maxCostMicros / 1_000_000 ||
|
|
34
|
+
scheduler.principal.limits.maxAttempts === undefined ||
|
|
35
|
+
scheduler.principal.limits.maxAttempts > job.budget.maxAttempts ||
|
|
36
|
+
!scheduler.estimate || request.binding.spec.maxAttempts > job.budget.maxAttempts)
|
|
37
|
+
throw new JobConflictError('Job evaluation requires authenticated bounded admission and durable receipts');
|
|
38
|
+
let plannedTokens = 0;
|
|
39
|
+
let plannedCostMicros = 0;
|
|
40
|
+
try {
|
|
41
|
+
for (const [alias, plan] of Object.entries(request.binding.spec.evaluations)) {
|
|
42
|
+
for (const target of plan.targets) {
|
|
43
|
+
const estimate = scheduler.estimate(alias, target, request.input);
|
|
44
|
+
if (!Number.isSafeInteger(estimate.tokens) || estimate.tokens < 0 ||
|
|
45
|
+
!Number.isFinite(estimate.costUsd) || estimate.costUsd < 0)
|
|
46
|
+
throw new JobConflictError('Unknown job estimate');
|
|
47
|
+
plannedTokens += estimate.tokens;
|
|
48
|
+
plannedCostMicros += Math.ceil(estimate.costUsd * 1_000_000);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!Number.isSafeInteger(plannedTokens) || !Number.isSafeInteger(plannedCostMicros) ||
|
|
52
|
+
attempt.reservedTokens === undefined || attempt.reservedCostMicros === undefined ||
|
|
53
|
+
plannedTokens > attempt.reservedTokens || plannedCostMicros > attempt.reservedCostMicros)
|
|
54
|
+
throw new JobConflictError('Item admission exceeds durable reservation');
|
|
55
|
+
if (!item.rulesetDigest || artifactDigest(request.ruleset) !== item.rulesetDigest ||
|
|
56
|
+
artifactDigest(request.input) !== item.subjectDigest ||
|
|
57
|
+
artifactDigest(request.definitions) !== item.definitionDigest ||
|
|
58
|
+
artifactDigest(request.binding) !== item.bindingDigest)
|
|
59
|
+
throw new JobConflictError('Job item pins changed');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new JobConflictError('Job item pins changed');
|
|
63
|
+
}
|
|
64
|
+
const result = await evaluateDecisionRuleset(request);
|
|
65
|
+
const receipt = await request.receiptStore.read(request.invocationId, job.scope.projectId);
|
|
66
|
+
if (!receipt || receipt.state !== 'completed' || canonicalJson(receipt.result) !== canonicalJson(result))
|
|
67
|
+
throw new JobConflictError('Unvalidated or incomplete decision receipt');
|
|
68
|
+
const receiptDigest = artifactDigest(receipt);
|
|
69
|
+
const resultDigest = artifactDigest(result);
|
|
70
|
+
const state = decisionResultJobState(result);
|
|
71
|
+
if (state === 'review' || state === 'succeeded')
|
|
72
|
+
return { state, receiptDigest, resultDigest };
|
|
73
|
+
if (state === 'permanent-failed')
|
|
74
|
+
return { state, errorCode: 'decision-failed' };
|
|
75
|
+
throw new JobConflictError('Unsupported decision outcome');
|
|
76
|
+
};
|
|
77
|
+
execute.requiresReservation = true;
|
|
78
|
+
return execute;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=job-evaluate.js.map
|