@aiwg/cli 2026.9.20 → 2026.9.24
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/cli.js +23 -18
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -2
- package/dist/src/artifacts/query-engine.js +14 -4
- 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 +282 -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,168 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, mkdir, open } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { DECISION_LIFECYCLE_SURFACES } from './lifecycle.js';
|
|
5
|
+
const OPAQUE = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
6
|
+
/** Single-writer local lifecycle journal. Content erasure is delegated per surface. */
|
|
7
|
+
export class FileDecisionLifecycleStore {
|
|
8
|
+
directory;
|
|
9
|
+
erasers;
|
|
10
|
+
constructor(directory, erasers) {
|
|
11
|
+
this.directory = directory;
|
|
12
|
+
this.erasers = erasers;
|
|
13
|
+
if (!directory)
|
|
14
|
+
throw new Error('Lifecycle storage root required');
|
|
15
|
+
}
|
|
16
|
+
async path() {
|
|
17
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
18
|
+
const info = await lstat(this.directory);
|
|
19
|
+
if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 0o077) !== 0) {
|
|
20
|
+
throw new Error('Lifecycle storage root must be private');
|
|
21
|
+
}
|
|
22
|
+
return join(this.directory, 'lifecycle.jsonl');
|
|
23
|
+
}
|
|
24
|
+
valid(subject, reference) {
|
|
25
|
+
if (typeof subject !== 'string' || !OPAQUE.test(subject) || reference &&
|
|
26
|
+
(!DECISION_LIFECYCLE_SURFACES.includes(reference.surface) || typeof reference.opaqueId !== 'string'
|
|
27
|
+
|| !OPAQUE.test(reference.opaqueId)))
|
|
28
|
+
throw new Error('Lifecycle reference must be opaque');
|
|
29
|
+
}
|
|
30
|
+
async append(entry) {
|
|
31
|
+
const fd = await open(await this.path(), constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
32
|
+
try {
|
|
33
|
+
await fd.writeFile(`${JSON.stringify(entry)}\n`);
|
|
34
|
+
await fd.sync();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await fd.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async entries() {
|
|
41
|
+
let fd;
|
|
42
|
+
try {
|
|
43
|
+
fd = await open(await this.path(), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code === 'ENOENT')
|
|
47
|
+
return [];
|
|
48
|
+
throw new Error('Lifecycle journal unavailable');
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const text = await fd.readFile({ encoding: 'utf8' });
|
|
52
|
+
const records = text.trim() ? text.trim().split('\n').map(line => JSON.parse(line)) : [];
|
|
53
|
+
if (records.some(entry => !this.validEntry(entry)))
|
|
54
|
+
throw new Error();
|
|
55
|
+
return records;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error('Lifecycle journal invalid');
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await fd.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
validEntry(entry) {
|
|
65
|
+
if (!entry || !['link', 'tombstone', 'hold', 'release'].includes(entry.kind))
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
if (entry.kind === 'link') {
|
|
69
|
+
if (!entry.reference)
|
|
70
|
+
return false;
|
|
71
|
+
this.valid(entry.subject, entry.reference);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (entry.kind === 'tombstone') {
|
|
75
|
+
if (!entry.value?.reference)
|
|
76
|
+
return false;
|
|
77
|
+
this.valid(entry.value.subject, entry.value.reference);
|
|
78
|
+
return Number.isSafeInteger(entry.value.deletedAt) && entry.value.deletedAt >= 0;
|
|
79
|
+
}
|
|
80
|
+
const hold = entry.kind === 'hold' ? entry.value : entry.hold;
|
|
81
|
+
this.valid(hold?.subject);
|
|
82
|
+
if (!hold.reason || !hold.authorizedBy || !Number.isSafeInteger(hold.expiresAt)
|
|
83
|
+
|| !Array.isArray(hold.scope) || !hold.scope.length
|
|
84
|
+
|| hold.scope.some(surface => !DECISION_LIFECYCLE_SURFACES.includes(surface)))
|
|
85
|
+
return false;
|
|
86
|
+
if (entry.kind === 'release') {
|
|
87
|
+
return entry.subject === hold.subject && !!entry.actor && !!entry.reason
|
|
88
|
+
&& Number.isSafeInteger(entry.at) && entry.at >= 0;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async register(subject, reference) {
|
|
97
|
+
this.valid(subject, reference);
|
|
98
|
+
await this.append({ kind: 'link', subject, reference });
|
|
99
|
+
}
|
|
100
|
+
async links(subject) {
|
|
101
|
+
this.valid(subject);
|
|
102
|
+
const records = await this.entries();
|
|
103
|
+
const registered = records.filter((entry) => entry.kind === 'link' && entry.subject === subject);
|
|
104
|
+
return [...new Map(registered.map(entry => [`${entry.reference.surface}:${entry.reference.opaqueId}`, entry.reference])).values()];
|
|
105
|
+
}
|
|
106
|
+
async tombstone(value) {
|
|
107
|
+
this.valid(value.subject, value.reference);
|
|
108
|
+
await this.append({ kind: 'tombstone', value });
|
|
109
|
+
}
|
|
110
|
+
async tombstones(subject) {
|
|
111
|
+
this.valid(subject);
|
|
112
|
+
return (await this.entries()).flatMap(entry => entry.kind === 'tombstone' && entry.value.subject === subject ? [entry.value] : []);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a cross-surface reference held by any record. A reference whose target was erased
|
|
116
|
+
* resolves to an explicit tombstone (without the erased subject) rather than a dangling link.
|
|
117
|
+
*/
|
|
118
|
+
async resolveReference(reference) {
|
|
119
|
+
this.valid('opaque-subject', reference);
|
|
120
|
+
const same = (value) => value.surface === reference.surface && value.opaqueId === reference.opaqueId;
|
|
121
|
+
const records = await this.entries();
|
|
122
|
+
const deleted = records.find((entry) => entry.kind === 'tombstone' && same(entry.value.reference));
|
|
123
|
+
if (deleted)
|
|
124
|
+
return { state: 'tombstoned', reference: { ...reference }, deletedAt: deleted.value.deletedAt };
|
|
125
|
+
return records.some(entry => entry.kind === 'link' && same(entry.reference))
|
|
126
|
+
? { state: 'linked', reference: { ...reference } } : { state: 'unknown', reference: { ...reference } };
|
|
127
|
+
}
|
|
128
|
+
async erase(reference) {
|
|
129
|
+
this.valid('opaque-subject', reference);
|
|
130
|
+
const erase = this.erasers[reference.surface];
|
|
131
|
+
if (!erase)
|
|
132
|
+
throw new Error('Lifecycle eraser unavailable');
|
|
133
|
+
try {
|
|
134
|
+
await erase(reference.opaqueId);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw new Error('Lifecycle erasure failed');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async holds(subject) {
|
|
141
|
+
this.valid(subject);
|
|
142
|
+
const active = [];
|
|
143
|
+
for (const entry of await this.entries()) {
|
|
144
|
+
if (entry.kind === 'hold' && entry.value.subject === subject)
|
|
145
|
+
active.push(entry.value);
|
|
146
|
+
if (entry.kind === 'release' && entry.subject === subject) {
|
|
147
|
+
const index = active.findIndex(hold => JSON.stringify(hold) === JSON.stringify(entry.hold));
|
|
148
|
+
if (index >= 0)
|
|
149
|
+
active.splice(index, 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return active;
|
|
153
|
+
}
|
|
154
|
+
async recordHold(hold) {
|
|
155
|
+
if (!this.validEntry({ kind: 'hold', value: hold }))
|
|
156
|
+
throw new Error('Lifecycle hold invalid');
|
|
157
|
+
await this.append({ kind: 'hold', value: hold });
|
|
158
|
+
}
|
|
159
|
+
async releaseHold(hold, actor, reason, at) {
|
|
160
|
+
this.valid(hold.subject);
|
|
161
|
+
const release = { kind: 'release', subject: hold.subject, hold, actor, reason, at };
|
|
162
|
+
if (!this.validEntry(release) || !(await this.holds(hold.subject)).some(active => JSON.stringify(active) === JSON.stringify(hold))) {
|
|
163
|
+
throw new Error('Lifecycle release invalid');
|
|
164
|
+
}
|
|
165
|
+
await this.append(release);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=file-lifecycle-store.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DecisionGraphError } from './graph.js';
|
|
2
|
+
import { effectiveGraphCeilings } from './graph-evidence.js';
|
|
3
|
+
import { selectDecisionBeam } from './graph-patterns.js';
|
|
4
|
+
/** A deterministic host-side selector at the existing Flow skill invocation seam.
|
|
5
|
+
* It cannot create nodes, change pins, dispatch inference or authorize actions.
|
|
6
|
+
* The host still wraps this with admittedDecisionFlowAdapter for uniform accounting.
|
|
7
|
+
*/
|
|
8
|
+
export function graphBeamFlowInvoker(graph, delegate, ceilings = []) {
|
|
9
|
+
const nodeStage = graph.nodes.find(n => n.id === 'select')?.stage;
|
|
10
|
+
const stageWidth = graph.stageBudgets?.find(item => item.stage === nodeStage)?.limits.beamWidth;
|
|
11
|
+
const width = Math.min(effectiveGraphCeilings(graph, ...ceilings).beamWidth, stageWidth ?? Infinity);
|
|
12
|
+
return async (request) => {
|
|
13
|
+
const node = graph.nodes.find(n => n.id === request.node.id);
|
|
14
|
+
if (node?.target !== 'beam')
|
|
15
|
+
return delegate(request);
|
|
16
|
+
if (graph.pattern !== 'taxonomy-beam' || node.id !== 'select' || request.node.kind !== 'skill') {
|
|
17
|
+
throw new DecisionGraphError('unauthorized beam selector');
|
|
18
|
+
}
|
|
19
|
+
const candidates = node.input.map(id => {
|
|
20
|
+
const score = request.inputs[id];
|
|
21
|
+
if (typeof score !== 'number' || !Number.isFinite(score))
|
|
22
|
+
throw new DecisionGraphError('invalid beam score');
|
|
23
|
+
return { id, score };
|
|
24
|
+
});
|
|
25
|
+
if (!candidates.length)
|
|
26
|
+
throw new DecisionGraphError('empty beam shortlist');
|
|
27
|
+
const ranked = selectDecisionBeam(candidates, Math.min(width, candidates.length), width).map(item => item.id);
|
|
28
|
+
const selected = new Set(ranked);
|
|
29
|
+
const output = { result: ranked };
|
|
30
|
+
for (const id of node.input) {
|
|
31
|
+
const flag = `chosen-${id}`;
|
|
32
|
+
if (node.output.includes(flag))
|
|
33
|
+
output[flag] = selected.has(id);
|
|
34
|
+
}
|
|
35
|
+
if (node.output.some(key => !Object.hasOwn(output, key)))
|
|
36
|
+
throw new DecisionGraphError('beam selector output mismatch');
|
|
37
|
+
return { outputs: output, attempts: 1, usage: { tokens: 0, costUsd: 0, timeMs: 0 } };
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=graph-beam.js.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { DecisionGraphError, planDecisionGraph } from './graph.js';
|
|
2
|
+
import { effectiveGraphCeilings } from './graph-evidence.js';
|
|
3
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
4
|
+
const empty = () => ({ attempts: 0, tokens: 0, costMicros: 0 });
|
|
5
|
+
const fields = ['attempts', 'tokens', 'costMicros'];
|
|
6
|
+
const safe = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
7
|
+
/** Synchronous reservations protect cumulative and per-stage capacity before adapter calls.
|
|
8
|
+
* Host estimates must be conservative bounds, including unknown cost. This is not a
|
|
9
|
+
* network dispatcher; callers retain Flow identities, transport and authorization.
|
|
10
|
+
*/
|
|
11
|
+
export class GraphBudgetLedger {
|
|
12
|
+
graph;
|
|
13
|
+
plan;
|
|
14
|
+
clock;
|
|
15
|
+
total = empty();
|
|
16
|
+
stages = new Map();
|
|
17
|
+
active = new Map();
|
|
18
|
+
stageStarted = new Map();
|
|
19
|
+
cancelled = false;
|
|
20
|
+
started;
|
|
21
|
+
limits;
|
|
22
|
+
constructor(graph, plan, ceilings = [], clock = Date.now) {
|
|
23
|
+
this.graph = graph;
|
|
24
|
+
this.plan = plan;
|
|
25
|
+
this.clock = clock;
|
|
26
|
+
const pins = new Set(graph.nodes.flatMap(n => [n.definition.digest, n.binding.digest]));
|
|
27
|
+
if (canonicalJson(plan) !== canonicalJson(planDecisionGraph(graph, pins)))
|
|
28
|
+
throw new DecisionGraphError('graph/plan mismatch');
|
|
29
|
+
this.limits = effectiveGraphCeilings(graph, ...ceilings);
|
|
30
|
+
this.started = clock();
|
|
31
|
+
if (!Number.isFinite(this.started))
|
|
32
|
+
throw new DecisionGraphError('invalid clock');
|
|
33
|
+
}
|
|
34
|
+
cancel() { this.cancelled = true; }
|
|
35
|
+
/** All-or-nothing synchronous batch claim; no remote call occurs before every
|
|
36
|
+
* member passes stage, graph and concurrency capacity checks. */
|
|
37
|
+
reserveBatch(stage, estimates) {
|
|
38
|
+
if (!estimates.length || !estimates.every(e => fields.every(field => safe(e[field])) && e.attempts >= 1)) {
|
|
39
|
+
throw new DecisionGraphError('invalid graph batch estimate');
|
|
40
|
+
}
|
|
41
|
+
const now = this.clock();
|
|
42
|
+
if (this.cancelled)
|
|
43
|
+
throw new DecisionGraphError('graph cancelled');
|
|
44
|
+
if (!Number.isFinite(now) || now < this.started || now - this.started >= this.limits.deadlineMs ||
|
|
45
|
+
!this.plan.stages.some(s => s.stage === stage))
|
|
46
|
+
throw new DecisionGraphError('graph batch deadline or stage invalid');
|
|
47
|
+
const limit = this.graph.stageBudgets?.find(item => item.stage === stage)?.limits ?? this.limits;
|
|
48
|
+
if (now - (this.stageStarted.get(stage) ?? now) >= limit.deadlineMs)
|
|
49
|
+
throw new DecisionGraphError('stage deadline exhausted');
|
|
50
|
+
const total = Object.fromEntries(fields.map(field => [field, estimates.reduce((sum, e) => sum + e[field], 0)]));
|
|
51
|
+
const spent = this.stages.get(stage) ?? empty();
|
|
52
|
+
if (estimates.length + (this.active.get(stage) ?? 0) > Math.min(limit.concurrency, this.limits.concurrency) ||
|
|
53
|
+
fields.some(field => !safe(total[field]) || this.total[field] + total[field] > this.limits[field] ||
|
|
54
|
+
spent[field] + total[field] > limit[field]))
|
|
55
|
+
throw new DecisionGraphError('graph batch budget exhausted');
|
|
56
|
+
return estimates.map(estimate => this.reserveAt(stage, estimate, now));
|
|
57
|
+
}
|
|
58
|
+
reserve(stage, estimate) {
|
|
59
|
+
return this.reserveAt(stage, estimate, this.clock());
|
|
60
|
+
}
|
|
61
|
+
reserveAt(stage, estimate, now) {
|
|
62
|
+
if (this.cancelled)
|
|
63
|
+
throw new DecisionGraphError('graph cancelled');
|
|
64
|
+
if (!Number.isFinite(now) || now < this.started || now - this.started >= this.limits.deadlineMs) {
|
|
65
|
+
throw new DecisionGraphError('graph deadline exhausted');
|
|
66
|
+
}
|
|
67
|
+
if (!this.plan.stages.some(s => s.stage === stage))
|
|
68
|
+
throw new DecisionGraphError('unknown graph stage');
|
|
69
|
+
if (!fields.every(field => safe(estimate[field])) || estimate.attempts < 1)
|
|
70
|
+
throw new DecisionGraphError('invalid graph estimate');
|
|
71
|
+
const stageLimit = this.graph.stageBudgets?.find(item => item.stage === stage)?.limits ?? this.limits;
|
|
72
|
+
const stageStart = this.stageStarted.get(stage) ?? now;
|
|
73
|
+
if (now - stageStart >= stageLimit.deadlineMs)
|
|
74
|
+
throw new DecisionGraphError('stage deadline exhausted');
|
|
75
|
+
const used = this.stages.get(stage) ?? empty();
|
|
76
|
+
if ([this.total, used].some((spent, index) => fields.some(field => spent[field] + estimate[field] > (index === 0 ? this.limits : stageLimit)[field]))) {
|
|
77
|
+
throw new DecisionGraphError('graph budget exhausted');
|
|
78
|
+
}
|
|
79
|
+
const count = this.active.get(stage) ?? 0;
|
|
80
|
+
if (count >= Math.min(this.limits.concurrency, stageLimit.concurrency))
|
|
81
|
+
throw new DecisionGraphError('graph concurrency exhausted');
|
|
82
|
+
for (const field of fields) {
|
|
83
|
+
this.total[field] += estimate[field];
|
|
84
|
+
used[field] += estimate[field];
|
|
85
|
+
}
|
|
86
|
+
this.stages.set(stage, used);
|
|
87
|
+
this.active.set(stage, count + 1);
|
|
88
|
+
this.stageStarted.set(stage, stageStart);
|
|
89
|
+
let settled = false;
|
|
90
|
+
return (actual) => {
|
|
91
|
+
if (settled)
|
|
92
|
+
throw new DecisionGraphError('graph reservation already settled');
|
|
93
|
+
settled = true;
|
|
94
|
+
this.active.set(stage, this.active.get(stage) - 1);
|
|
95
|
+
// Never refund an attempt: even a failed/uncertain dispatch consumes it.
|
|
96
|
+
const finished = this.clock();
|
|
97
|
+
if (!fields.every(field => safe(actual[field])) || fields.some(field => actual[field] > estimate[field]) ||
|
|
98
|
+
!Number.isFinite(finished) || finished < now || finished - this.started > this.limits.deadlineMs ||
|
|
99
|
+
finished - stageStart > stageLimit.deadlineMs) {
|
|
100
|
+
this.cancelled = true;
|
|
101
|
+
throw new DecisionGraphError('graph usage exceeded reservation');
|
|
102
|
+
}
|
|
103
|
+
for (const field of ['tokens', 'costMicros']) {
|
|
104
|
+
this.total[field] -= estimate[field] - actual[field];
|
|
105
|
+
used[field] -= estimate[field] - actual[field];
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=graph-budget.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { evaluateDecisionRuleset } from './evaluate.js';
|
|
2
|
+
import { artifactPin } from './validate.js';
|
|
3
|
+
import { DecisionGraphError } from './graph.js';
|
|
4
|
+
import { admitEntry } from './entry.js';
|
|
5
|
+
/** Default node status: an unsupported capability, or every evaluation abstaining,
|
|
6
|
+
* yields no evidence. Hosts with a ruleset-specific policy supply their own. */
|
|
7
|
+
export function decisionResultNodeStatus(result) {
|
|
8
|
+
const statuses = Object.values(result.spec.evaluations).map(item => item.spec.status);
|
|
9
|
+
if (result.spec.reason === 'unsupported-capability' || (statuses.length && statuses.every(s => s === 'unsupported')))
|
|
10
|
+
return 'unsupported';
|
|
11
|
+
if (statuses.length && statuses.every(s => s === 'abstained'))
|
|
12
|
+
return 'abstained';
|
|
13
|
+
return 'ok';
|
|
14
|
+
}
|
|
15
|
+
/** Validate a Flow skill request and return the graph node and its declared projected input. */
|
|
16
|
+
export function decisionFlowNode(graph, flow) {
|
|
17
|
+
const node = graph.nodes.find(n => n.id === flow.node.id);
|
|
18
|
+
if (!node || flow.node.kind !== 'skill' || !flow.runId || !flow.invocationKey)
|
|
19
|
+
throw new DecisionGraphError('invalid Flow decision invocation');
|
|
20
|
+
const input = {};
|
|
21
|
+
for (const name of node.input) {
|
|
22
|
+
if (!Object.hasOwn(flow.inputs, name))
|
|
23
|
+
throw new DecisionGraphError('missing projected Flow input');
|
|
24
|
+
input[name] = flow.inputs[name];
|
|
25
|
+
}
|
|
26
|
+
return { node, input };
|
|
27
|
+
}
|
|
28
|
+
export function assertDecisionFlowPins(node, binding, definitions) {
|
|
29
|
+
if (artifactPin(binding).digest !== node.binding.digest ||
|
|
30
|
+
!definitions.some(def => artifactPin(def).digest === node.definition.digest)) {
|
|
31
|
+
throw new DecisionGraphError('Flow decision pin mismatch');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Project a ruleset result through the host projector and account every attempt. */
|
|
35
|
+
export function decisionFlowResponse(graph, node, result, host) {
|
|
36
|
+
const status = (host.status ?? ((_, value) => decisionResultNodeStatus(value)))(node.id, result);
|
|
37
|
+
if (!['ok', 'abstained', 'unsupported'].includes(status))
|
|
38
|
+
throw new DecisionGraphError('invalid decision node status');
|
|
39
|
+
const output = status === 'ok' ? host.project(node.id, result) : {};
|
|
40
|
+
try {
|
|
41
|
+
admitEntry(output);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new DecisionGraphError('invalid projected decision result');
|
|
45
|
+
}
|
|
46
|
+
if (status === 'ok' && (!output || typeof output !== 'object' || Array.isArray(output) ||
|
|
47
|
+
Object.keys(output).some(key => !node.output.includes(key)) ||
|
|
48
|
+
node.output.some(key => !Object.hasOwn(output, key)) ||
|
|
49
|
+
graph.edges.some(edge => edge.from === node.id && edge.when && typeof output[edge.when.source] !== 'boolean'))) {
|
|
50
|
+
throw new DecisionGraphError('invalid projected decision outputs');
|
|
51
|
+
}
|
|
52
|
+
const attempts = Object.values(result.spec.evaluations).flatMap(e => e.spec.attempts);
|
|
53
|
+
let tokens = 0, costUsd = 0, timeMs = 0;
|
|
54
|
+
for (const attempt of attempts) {
|
|
55
|
+
const inputTokens = attempt.usage.inputTokens;
|
|
56
|
+
const outputTokens = attempt.usage.outputTokens;
|
|
57
|
+
if (inputTokens === null || outputTokens === null || !Number.isSafeInteger(inputTokens) || !Number.isSafeInteger(outputTokens)) {
|
|
58
|
+
throw new DecisionGraphError('unknown graph token usage');
|
|
59
|
+
}
|
|
60
|
+
tokens += inputTokens + outputTokens;
|
|
61
|
+
costUsd += attempt.usage.costUsd ?? host.unknownCostBoundUsd;
|
|
62
|
+
timeMs += attempt.durationMs;
|
|
63
|
+
}
|
|
64
|
+
if (!Number.isSafeInteger(tokens) || !Number.isFinite(costUsd) || !Number.isSafeInteger(timeMs))
|
|
65
|
+
throw new DecisionGraphError('invalid graph usage');
|
|
66
|
+
return { outputs: output, attempts: attempts.length || 1, usage: { tokens, costUsd, timeMs },
|
|
67
|
+
...(status === 'ok' ? {} : { status }) };
|
|
68
|
+
}
|
|
69
|
+
export function assertUnknownCostBound(host) {
|
|
70
|
+
if (!Number.isFinite(host.unknownCostBoundUsd) || host.unknownCostBoundUsd <= 0) {
|
|
71
|
+
throw new DecisionGraphError('unknown cost requires conservative price bound');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Embedded-host bridge: calls the same evaluateDecisionRuleset that the shipped
|
|
75
|
+
* decision-evaluate skill script wraps, in process. Use decisionEvaluateSkillFlowInvoker
|
|
76
|
+
* to run the skill's own entrypoint and request contract.
|
|
77
|
+
* Host owns authenticated artifact resolution, adapters, receipt store and price bound.
|
|
78
|
+
* The graph artifact supplies no credentials, executable adapters or permissions.
|
|
79
|
+
*/
|
|
80
|
+
export function decisionRulesetFlowInvoker(graph, host) {
|
|
81
|
+
assertUnknownCostBound(host);
|
|
82
|
+
return async (flow) => {
|
|
83
|
+
const { node, input } = decisionFlowNode(graph, flow);
|
|
84
|
+
const resolved = host.resolve(node.id);
|
|
85
|
+
assertDecisionFlowPins(node, resolved.binding, Object.values(resolved.definitions));
|
|
86
|
+
// The entry has no dependency: only the trusted host input is used.
|
|
87
|
+
const result = await (host.evaluate ?? evaluateDecisionRuleset)({ ...resolved,
|
|
88
|
+
input: node.id === graph.entry ? resolved.input : input,
|
|
89
|
+
runId: flow.runId, invocationId: flow.invocationKey });
|
|
90
|
+
return decisionFlowResponse(graph, node, result, host);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=graph-decision-bridge.js.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
3
|
+
import { admitEntry } from './entry.js';
|
|
4
|
+
import { DecisionGraphError, planDecisionGraph } from './graph.js';
|
|
5
|
+
const digest = (value) => `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
|
|
6
|
+
const fields = ['attempts', 'deadlineMs', 'tokens', 'costMicros', 'fanOut', 'beamWidth', 'depth', 'concurrency'];
|
|
7
|
+
/** Caller, workspace and provider ceilings may only narrow the graph's authored ceiling. */
|
|
8
|
+
export function effectiveGraphCeilings(graph, ...ceilings) {
|
|
9
|
+
const result = { ...graph.budget };
|
|
10
|
+
for (const layer of ceilings) {
|
|
11
|
+
for (const field of fields) {
|
|
12
|
+
const limit = layer[field];
|
|
13
|
+
if (limit === undefined)
|
|
14
|
+
continue;
|
|
15
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
16
|
+
throw new DecisionGraphError('invalid graph ceiling');
|
|
17
|
+
result[field] = Math.min(result[field], limit);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
/** Audit complete immutable observations against an already validated trusted plan.
|
|
23
|
+
* No partial receipt is returned on malformed observations. Unknown cost fails closed.
|
|
24
|
+
*/
|
|
25
|
+
export function auditGraphEvidence(graph, plan, observations, ceilings = [], unknownCostBoundMicros) {
|
|
26
|
+
try {
|
|
27
|
+
admitEntry({ graph, plan, observations, ceilings, ...(unknownCostBoundMicros === undefined ? {} : { unknownCostBoundMicros }) });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new DecisionGraphError('graph evidence admission denied');
|
|
31
|
+
}
|
|
32
|
+
// Revalidate topology and reject forged plan groups/edges. This does not resolve pins;
|
|
33
|
+
// trusted pin resolution belongs to the caller that created the original plan.
|
|
34
|
+
const pins = new Set(graph.nodes.flatMap(node => [node.definition.digest, node.binding.digest]));
|
|
35
|
+
const expected = planDecisionGraph(graph, pins);
|
|
36
|
+
if (canonicalJson(plan) !== canonicalJson(expected))
|
|
37
|
+
throw new DecisionGraphError('graph/plan mismatch');
|
|
38
|
+
const limits = effectiveGraphCeilings(graph, ...ceilings);
|
|
39
|
+
const byId = new Map();
|
|
40
|
+
for (const obs of observations) {
|
|
41
|
+
if (byId.has(obs.node) || !graph.nodes.some(n => n.id === obs.node))
|
|
42
|
+
throw new DecisionGraphError('duplicate or unknown observation');
|
|
43
|
+
for (const field of ['attempts', 'tokens', 'durationMs']) {
|
|
44
|
+
if (!Number.isSafeInteger(obs[field]) || obs[field] < 0)
|
|
45
|
+
throw new DecisionGraphError('invalid usage');
|
|
46
|
+
}
|
|
47
|
+
if (obs.costMicros !== null && (!Number.isSafeInteger(obs.costMicros) || obs.costMicros < 0))
|
|
48
|
+
throw new DecisionGraphError('invalid cost');
|
|
49
|
+
if (obs.costMicros === null && (!Number.isSafeInteger(unknownCostBoundMicros) || unknownCostBoundMicros < 1))
|
|
50
|
+
throw new DecisionGraphError('unknown cost without bound');
|
|
51
|
+
if (typeof obs.used !== 'boolean' || !['ok', 'abstained', 'error', 'unsupported', 'cancelled', 'skipped'].includes(obs.status))
|
|
52
|
+
throw new DecisionGraphError('invalid observation');
|
|
53
|
+
const names = graph.nodes.find(n => n.id === obs.node).output;
|
|
54
|
+
if (!obs.output || Array.isArray(obs.output) || typeof obs.output !== 'object' ||
|
|
55
|
+
Object.keys(obs.output).some(name => !names.includes(name)))
|
|
56
|
+
throw new DecisionGraphError('undeclared observation output');
|
|
57
|
+
if (obs.status === 'skipped' && (obs.used || obs.attempts !== 0 || obs.tokens !== 0 || obs.costMicros !== 0 || obs.durationMs !== 0 || Object.keys(obs.output).length)) {
|
|
58
|
+
throw new DecisionGraphError('skipped node has resource usage');
|
|
59
|
+
}
|
|
60
|
+
byId.set(obs.node, obs);
|
|
61
|
+
}
|
|
62
|
+
const totals = { attempts: 0, tokens: 0, costMicros: 0, durationMs: 0 };
|
|
63
|
+
let outcome = 'complete';
|
|
64
|
+
const stages = plan.stages.map(stage => ({ stage: stage.stage, nodes: stage.nodes.map(id => {
|
|
65
|
+
const obs = byId.get(id);
|
|
66
|
+
if (!obs)
|
|
67
|
+
throw new DecisionGraphError('missing graph observation');
|
|
68
|
+
const input = {};
|
|
69
|
+
for (const edge of plan.edges.filter(e => e.to === id)) {
|
|
70
|
+
const source = byId.get(edge.from);
|
|
71
|
+
if (!source || source.status !== 'ok' || (!source.used && obs.used) || !Object.hasOwn(source.output, edge.source)) {
|
|
72
|
+
// Keep the earliest explicit cause (for example an abstained predecessor).
|
|
73
|
+
if (outcome === 'complete')
|
|
74
|
+
outcome = 'incomplete-evidence';
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
input[edge.destination] = { sourceNode: edge.from, sourceResultDigest: digest(source.output), value: source.output[edge.source] };
|
|
78
|
+
}
|
|
79
|
+
const node = graph.nodes.find(n => n.id === id);
|
|
80
|
+
if (outcome === 'complete' && obs.status !== 'skipped' && node.input.some(key => !Object.hasOwn(input, key)))
|
|
81
|
+
outcome = 'incomplete-evidence';
|
|
82
|
+
const costMicros = obs.costMicros ?? unknownCostBoundMicros;
|
|
83
|
+
totals.attempts += obs.attempts;
|
|
84
|
+
totals.tokens += obs.tokens;
|
|
85
|
+
totals.costMicros += costMicros;
|
|
86
|
+
totals.durationMs += obs.durationMs;
|
|
87
|
+
if (![totals.attempts, totals.tokens, totals.costMicros, totals.durationMs].every(Number.isSafeInteger))
|
|
88
|
+
throw new DecisionGraphError('usage overflow');
|
|
89
|
+
if (obs.status === 'cancelled')
|
|
90
|
+
outcome = 'cancelled';
|
|
91
|
+
else if (outcome === 'complete' && obs.status !== 'ok' && obs.status !== 'skipped')
|
|
92
|
+
outcome = obs.status;
|
|
93
|
+
return { id, status: obs.status, used: obs.used, input, resultDigest: digest(obs.output),
|
|
94
|
+
attempts: obs.attempts, tokens: obs.tokens, costMicros, durationMs: obs.durationMs,
|
|
95
|
+
...(obs.flow ? { flow: obs.flow } : {}) };
|
|
96
|
+
}) }));
|
|
97
|
+
if (stages.some(stage => {
|
|
98
|
+
const stageLimit = graph.stageBudgets?.find(b => b.stage === stage.stage)?.limits;
|
|
99
|
+
if (!stageLimit)
|
|
100
|
+
return false;
|
|
101
|
+
const spent = { attempts: 0, tokens: 0, costMicros: 0, durationMs: 0 };
|
|
102
|
+
for (const node of stage.nodes) {
|
|
103
|
+
spent.attempts += node.attempts;
|
|
104
|
+
spent.tokens += node.tokens;
|
|
105
|
+
spent.costMicros += node.costMicros;
|
|
106
|
+
spent.durationMs += node.durationMs;
|
|
107
|
+
}
|
|
108
|
+
return spent.attempts > stageLimit.attempts || spent.tokens > stageLimit.tokens ||
|
|
109
|
+
spent.costMicros > stageLimit.costMicros || spent.durationMs > stageLimit.deadlineMs ||
|
|
110
|
+
stage.nodes.length > stageLimit.fanOut;
|
|
111
|
+
}))
|
|
112
|
+
outcome = 'budget-exhausted';
|
|
113
|
+
if (totals.attempts > limits.attempts || totals.tokens > limits.tokens || totals.costMicros > limits.costMicros ||
|
|
114
|
+
totals.durationMs > limits.deadlineMs || stages.length > limits.depth ||
|
|
115
|
+
stages.some(stage => stage.nodes.length > limits.fanOut))
|
|
116
|
+
outcome = 'budget-exhausted';
|
|
117
|
+
if (observations.some(obs => obs.status === 'cancelled'))
|
|
118
|
+
outcome = 'cancelled';
|
|
119
|
+
const receipt = { schemaVersion: 'decision-graph-evidence/v1', graphDigest: plan.graphDigest,
|
|
120
|
+
planDigest: plan.planDigest, limits, stages, totals, outcome };
|
|
121
|
+
return { ...receipt, receiptDigest: digest(receipt) };
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=graph-evidence.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DecisionGraphError } from './graph.js';
|
|
2
|
+
/** Attach trusted host admission to Flow's existing invokeNode seam. The estimator
|
|
3
|
+
* must bound a full decision-evaluate call including retries/fallback and unknown
|
|
4
|
+
* cost; without a conservative estimate, reject before invoking any adapter.
|
|
5
|
+
*/
|
|
6
|
+
export function admittedDecisionFlowAdapter(ledger, estimate, invoke, signal, onObservation) {
|
|
7
|
+
return async (request) => {
|
|
8
|
+
if (signal?.aborted) {
|
|
9
|
+
ledger.cancel();
|
|
10
|
+
throw new DecisionGraphError('graph cancelled');
|
|
11
|
+
}
|
|
12
|
+
// Flow invokes a gate only after the host approved it. The gate dispatches
|
|
13
|
+
// nothing, spends no decision budget and is not a decision observation.
|
|
14
|
+
if (request.node.kind === 'gate' && request.node.sideEffectMode === 'none' && request.node.ref === undefined) {
|
|
15
|
+
return { outputs: {}, attempts: 1, usage: { tokens: 0, costUsd: 0, timeMs: 0 } };
|
|
16
|
+
}
|
|
17
|
+
if (request.node.kind !== 'skill' || request.node.sideEffectMode !== 'none' || request.node.retry?.limit !== 0 ||
|
|
18
|
+
!/^stage-(0|[1-9]\d*)$/.test(request.node.phase ?? ''))
|
|
19
|
+
throw new DecisionGraphError('unauthorized graph Flow node');
|
|
20
|
+
const stage = Number(request.node.phase.slice(6));
|
|
21
|
+
const bound = estimate(request);
|
|
22
|
+
if (!bound || !Number.isSafeInteger(bound.tokens) || bound.tokens < 1 ||
|
|
23
|
+
!Number.isSafeInteger(bound.costMicros) || bound.costMicros < 1)
|
|
24
|
+
throw new DecisionGraphError('missing conservative graph estimate');
|
|
25
|
+
const reservation = ledger.reserve(stage, bound);
|
|
26
|
+
try {
|
|
27
|
+
const response = await invoke(request);
|
|
28
|
+
if (!response || !response.usage || !Number.isSafeInteger(response.usage.tokens) || response.usage.tokens < 0 ||
|
|
29
|
+
!Number.isFinite(response.usage.costUsd) || response.usage.costUsd < 0 ||
|
|
30
|
+
!Number.isSafeInteger(response.usage.timeMs) || response.usage.timeMs < 0 ||
|
|
31
|
+
!Number.isSafeInteger(response.attempts) || response.attempts < 1 ||
|
|
32
|
+
![undefined, 'ok', 'abstained', 'unsupported'].includes(response.status) ||
|
|
33
|
+
(response.status !== undefined && response.status !== 'ok' && Object.keys(response.outputs ?? {}).length)) {
|
|
34
|
+
ledger.cancel();
|
|
35
|
+
throw new DecisionGraphError('invalid graph usage');
|
|
36
|
+
}
|
|
37
|
+
const micros = Math.ceil(response.usage.costUsd * 1_000_000);
|
|
38
|
+
reservation({ attempts: response.attempts, tokens: response.usage.tokens, costMicros: micros });
|
|
39
|
+
if (signal?.aborted) {
|
|
40
|
+
ledger.cancel();
|
|
41
|
+
throw new DecisionGraphError('graph cancelled');
|
|
42
|
+
}
|
|
43
|
+
onObservation?.({ request, response });
|
|
44
|
+
// Record the spent attempt, then fail the Flow node so no dependent runs.
|
|
45
|
+
if (response.status === 'abstained' || response.status === 'unsupported') {
|
|
46
|
+
throw new DecisionGraphError(`decision node ${response.status}`);
|
|
47
|
+
}
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
ledger.cancel();
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=graph-flow-adapter.js.map
|