ai-capability-framework 1.0.3 → 1.1.0
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/CHANGELOG.md +42 -0
- package/README.md +38 -30
- package/ROADMAP.md +14 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +32 -1
- package/dist/discovery/bindings.d.ts +7 -0
- package/dist/discovery/bindings.d.ts.map +1 -0
- package/dist/discovery/bindings.js +24 -0
- package/dist/discovery/budget.d.ts +17 -0
- package/dist/discovery/budget.d.ts.map +1 -0
- package/dist/discovery/budget.js +77 -0
- package/dist/discovery/configuration.d.ts +10 -0
- package/dist/discovery/configuration.d.ts.map +1 -0
- package/dist/discovery/configuration.js +63 -0
- package/dist/discovery/corpus.d.ts +8 -0
- package/dist/discovery/corpus.d.ts.map +1 -0
- package/dist/discovery/corpus.js +64 -0
- package/dist/discovery/diagnostics.d.ts +15 -0
- package/dist/discovery/diagnostics.d.ts.map +1 -0
- package/dist/discovery/diagnostics.js +11 -0
- package/dist/discovery/discoverer.d.ts +23 -0
- package/dist/discovery/discoverer.d.ts.map +1 -0
- package/dist/discovery/discoverer.js +261 -0
- package/dist/discovery/embedding-configuration.d.ts +12 -0
- package/dist/discovery/embedding-configuration.d.ts.map +1 -0
- package/dist/discovery/embedding-configuration.js +81 -0
- package/dist/discovery/embedding-types.d.ts +75 -0
- package/dist/discovery/embedding-types.d.ts.map +1 -0
- package/dist/discovery/embedding-types.js +1 -0
- package/dist/discovery/embeddings.d.ts +38 -0
- package/dist/discovery/embeddings.d.ts.map +1 -0
- package/dist/discovery/embeddings.js +71 -0
- package/dist/discovery/evaluation-validation.d.ts +8 -0
- package/dist/discovery/evaluation-validation.d.ts.map +1 -0
- package/dist/discovery/evaluation-validation.js +19 -0
- package/dist/discovery/evaluation.d.ts +84 -0
- package/dist/discovery/evaluation.d.ts.map +1 -0
- package/dist/discovery/evaluation.js +310 -0
- package/dist/discovery/export.d.ts +14 -0
- package/dist/discovery/export.d.ts.map +1 -0
- package/dist/discovery/export.js +81 -0
- package/dist/discovery/facade.d.ts +82 -0
- package/dist/discovery/facade.d.ts.map +1 -0
- package/dist/discovery/facade.js +266 -0
- package/dist/discovery/fingerprint.d.ts +71 -0
- package/dist/discovery/fingerprint.d.ts.map +1 -0
- package/dist/discovery/fingerprint.js +67 -0
- package/dist/discovery/index-builder.d.ts +3 -0
- package/dist/discovery/index-builder.d.ts.map +1 -0
- package/dist/discovery/index-builder.js +129 -0
- package/dist/discovery/index-validation.d.ts +12 -0
- package/dist/discovery/index-validation.d.ts.map +1 -0
- package/dist/discovery/index-validation.js +117 -0
- package/dist/discovery/index.d.ts +32 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +18 -0
- package/dist/discovery/json-snapshot.d.ts +3 -0
- package/dist/discovery/json-snapshot.d.ts.map +1 -0
- package/dist/discovery/json-snapshot.js +11 -0
- package/dist/discovery/lexical.d.ts +12 -0
- package/dist/discovery/lexical.d.ts.map +1 -0
- package/dist/discovery/lexical.js +26 -0
- package/dist/discovery/metadata.d.ts +4 -0
- package/dist/discovery/metadata.d.ts.map +1 -0
- package/dist/discovery/metadata.js +33 -0
- package/dist/discovery/ranking.d.ts +7 -0
- package/dist/discovery/ranking.d.ts.map +1 -0
- package/dist/discovery/ranking.js +68 -0
- package/dist/discovery/replay.d.ts +50 -0
- package/dist/discovery/replay.d.ts.map +1 -0
- package/dist/discovery/replay.js +175 -0
- package/dist/discovery/retrieval-policy.d.ts +5 -0
- package/dist/discovery/retrieval-policy.d.ts.map +1 -0
- package/dist/discovery/retrieval-policy.js +10 -0
- package/dist/discovery/router.d.ts +39 -0
- package/dist/discovery/router.d.ts.map +1 -0
- package/dist/discovery/router.js +98 -0
- package/dist/discovery/selection-attempt.d.ts +20 -0
- package/dist/discovery/selection-attempt.d.ts.map +1 -0
- package/dist/discovery/selection-attempt.js +39 -0
- package/dist/discovery/selection-cards.d.ts +12 -0
- package/dist/discovery/selection-cards.d.ts.map +1 -0
- package/dist/discovery/selection-cards.js +79 -0
- package/dist/discovery/selection-configuration.d.ts +18 -0
- package/dist/discovery/selection-configuration.d.ts.map +1 -0
- package/dist/discovery/selection-configuration.js +71 -0
- package/dist/discovery/selection-response.d.ts +13 -0
- package/dist/discovery/selection-response.d.ts.map +1 -0
- package/dist/discovery/selection-response.js +47 -0
- package/dist/discovery/selection-types.d.ts +18 -0
- package/dist/discovery/selection-types.d.ts.map +1 -0
- package/dist/discovery/selection-types.js +1 -0
- package/dist/discovery/selection.d.ts +6 -0
- package/dist/discovery/selection.d.ts.map +1 -0
- package/dist/discovery/selection.js +145 -0
- package/dist/discovery/types.d.ts +291 -0
- package/dist/discovery/types.d.ts.map +1 -0
- package/dist/discovery/types.js +1 -0
- package/dist/discovery/usage.d.ts +2 -0
- package/dist/discovery/usage.d.ts.map +1 -0
- package/dist/discovery/usage.js +1 -0
- package/dist/discovery/vectors.d.ts +5 -0
- package/dist/discovery/vectors.d.ts.map +1 -0
- package/dist/discovery/vectors.js +43 -0
- package/dist/discovery/versions.d.ts +37 -0
- package/dist/discovery/versions.d.ts.map +1 -0
- package/dist/discovery/versions.js +36 -0
- package/dist/evals-live/live-eval-runner.d.ts +1 -0
- package/dist/evals-live/live-eval-runner.d.ts.map +1 -1
- package/dist/evals-live/live-eval-runner.js +12 -1
- package/dist/evals-live/types.d.ts +3 -0
- package/dist/evals-live/types.d.ts.map +1 -1
- package/dist/generated/manifest-types.d.ts +3655 -0
- package/dist/generated/manifest-types.d.ts.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +8 -0
- package/dist/mcp-server/server.d.ts.map +1 -1
- package/dist/mcp-server/server.js +11 -2
- package/dist/mcp-server/types.d.ts +4 -0
- package/dist/mcp-server/types.d.ts.map +1 -1
- package/dist/observability/redaction.d.ts.map +1 -1
- package/dist/observability/redaction.js +6 -2
- package/dist/observability/types.d.ts +1 -1
- package/dist/observability/types.d.ts.map +1 -1
- package/dist/openai/index.d.ts +2 -0
- package/dist/openai/index.d.ts.map +1 -1
- package/dist/openai/index.js +1 -0
- package/dist/openai/responses-runner.d.ts +1 -1
- package/dist/openai/responses-runner.d.ts.map +1 -1
- package/dist/openai/responses-runner.js +134 -7
- package/dist/openai/selection-client.d.ts +28 -0
- package/dist/openai/selection-client.d.ts.map +1 -0
- package/dist/openai/selection-client.js +95 -0
- package/dist/openai/tool-loop.js +1 -1
- package/dist/openai/types.d.ts +19 -2
- package/dist/openai/types.d.ts.map +1 -1
- package/dist/replay/discovery-snapshot.d.ts +10 -0
- package/dist/replay/discovery-snapshot.d.ts.map +1 -0
- package/dist/replay/discovery-snapshot.js +17 -0
- package/dist/replay/recorder.d.ts.map +1 -1
- package/dist/replay/recorder.js +4 -0
- package/dist/replay/redaction.js +5 -1
- package/dist/replay/replayer.d.ts.map +1 -1
- package/dist/replay/replayer.js +14 -1
- package/dist/replay/types.d.ts +3 -0
- package/dist/replay/types.d.ts.map +1 -1
- package/dist/replay/validation.d.ts.map +1 -1
- package/dist/replay/validation.js +6 -0
- package/dist/runtime/bounded-json.d.ts +4 -0
- package/dist/runtime/bounded-json.d.ts.map +1 -0
- package/dist/runtime/bounded-json.js +74 -0
- package/dist/runtime/eligibility.d.ts +4 -0
- package/dist/runtime/eligibility.d.ts.map +1 -0
- package/dist/runtime/eligibility.js +128 -0
- package/dist/runtime/index.d.ts +5 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/provider-attempt.d.ts +11 -0
- package/dist/runtime/provider-attempt.d.ts.map +1 -0
- package/dist/runtime/provider-attempt.js +221 -0
- package/dist/runtime/provider-run.d.ts +10 -0
- package/dist/runtime/provider-run.d.ts.map +1 -0
- package/dist/runtime/provider-run.js +123 -0
- package/dist/runtime/provider-stop.d.ts +9 -0
- package/dist/runtime/provider-stop.d.ts.map +1 -0
- package/dist/runtime/provider-stop.js +28 -0
- package/dist/runtime/provider-types.d.ts +189 -0
- package/dist/runtime/provider-types.d.ts.map +1 -0
- package/dist/runtime/provider-types.js +1 -0
- package/dist/runtime/provider-usage.d.ts +9 -0
- package/dist/runtime/provider-usage.d.ts.map +1 -0
- package/dist/runtime/provider-usage.js +69 -0
- package/dist/runtime/router.d.ts.map +1 -1
- package/dist/runtime/router.js +3 -118
- package/dist/runtime/types.d.ts +9 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/validator.d.ts +3 -0
- package/dist/validator.d.ts.map +1 -1
- package/dist/validator.js +32 -0
- package/docs/api/public-api-policy.md +10 -0
- package/docs/api/stable-workflow.md +58 -0
- package/docs/api.md +7 -0
- package/docs/capability-manifests.md +24 -0
- package/docs/development.md +22 -0
- package/docs/discovery.md +186 -0
- package/docs/evals/discovery-live.md +199 -0
- package/docs/evals/discovery-qualification.md +158 -0
- package/docs/evals/overview.md +9 -0
- package/docs/getting-started/installation.md +11 -12
- package/docs/getting-started/openai-quickstart.md +16 -5
- package/docs/getting-started/provider-neutral-quickstart.md +247 -88
- package/docs/getting-started/quickstart.md +8 -30
- package/docs/index.md +10 -0
- package/docs/release.md +29 -5
- package/docs/runtime.md +27 -0
- package/docs/start-here.md +23 -134
- package/examples/12-capability-discovery-selection/README.md +346 -0
- package/examples/12-capability-discovery-selection/customer-cases.mjs +52 -0
- package/examples/12-capability-discovery-selection/customer-harness.mjs +91 -0
- package/examples/12-capability-discovery-selection/customer-request-example.mjs +320 -0
- package/examples/12-capability-discovery-selection/discovery-example.mjs +175 -0
- package/examples/12-capability-discovery-selection/discovery-fixtures/three-domains.suite.json +7887 -0
- package/examples/12-capability-discovery-selection/discovery-fixtures/three-domains.v2.suite.json +8167 -0
- package/examples/12-capability-discovery-selection/discovery-fixtures/three-domains.v3.suite.json +8342 -0
- package/examples/12-capability-discovery-selection/evaluation/application.mjs +305 -0
- package/examples/12-capability-discovery-selection/evaluation/campaign.mjs +248 -0
- package/examples/12-capability-discovery-selection/evaluation/clients.mjs +433 -0
- package/examples/12-capability-discovery-selection/evaluation/continuation.mjs +208 -0
- package/examples/12-capability-discovery-selection/evaluation/durable-budget.mjs +420 -0
- package/examples/12-capability-discovery-selection/evaluation/freeze.mjs +9 -0
- package/examples/12-capability-discovery-selection/evaluation/harness.mjs +618 -0
- package/examples/12-capability-discovery-selection/evaluation/mock-host.mjs +260 -0
- package/examples/12-capability-discovery-selection/evaluation/preflight.mjs +115 -0
- package/examples/12-capability-discovery-selection/evaluation/qualification-workflows.mjs +87 -0
- package/examples/12-capability-discovery-selection/evaluation/regrade.mjs +281 -0
- package/examples/12-capability-discovery-selection/evaluation/report.mjs +517 -0
- package/examples/12-capability-discovery-selection/evaluation/runner.mjs +613 -0
- package/examples/12-capability-discovery-selection/evaluation/saved-campaign.mjs +385 -0
- package/examples/12-capability-discovery-selection/evaluation/wording.mjs +115 -0
- package/examples/12-capability-discovery-selection/evaluation/workflow-application.mjs +378 -0
- package/examples/12-capability-discovery-selection/evaluation/workflow-cases.mjs +90 -0
- package/examples/12-capability-discovery-selection/evaluation-example.mjs +154 -0
- package/examples/12-capability-discovery-selection/hybrid-example.mjs +174 -0
- package/examples/12-capability-discovery-selection/mock-embeddings.mjs +101 -0
- package/examples/12-capability-discovery-selection/mock-selection.mjs +78 -0
- package/examples/12-capability-discovery-selection/pipeline-example.mjs +496 -0
- package/examples/12-capability-discovery-selection/retrieval-cases.mjs +31 -0
- package/examples/12-capability-discovery-selection/run-campaign-mock.mjs +40 -0
- package/examples/12-capability-discovery-selection/run-customer-mock.mjs +15 -0
- package/examples/12-capability-discovery-selection/run-evaluation-live.mjs +10 -0
- package/examples/12-capability-discovery-selection/run-evaluation-mock.mjs +3 -0
- package/examples/12-capability-discovery-selection/run-evaluation-regrade.mjs +71 -0
- package/examples/12-capability-discovery-selection/run-hybrid-mock.mjs +3 -0
- package/examples/12-capability-discovery-selection/run-mock.mjs +3 -0
- package/examples/12-capability-discovery-selection/run-pipeline-mock.mjs +2 -0
- package/examples/12-capability-discovery-selection/run-selector-mock.mjs +3 -0
- package/examples/12-capability-discovery-selection/selection-example.mjs +88 -0
- package/package.json +17 -4
- package/schemas/discovery/embedding-index.schema.json +71 -0
- package/schemas/discovery/eval-observations.schema.json +409 -0
- package/schemas/discovery/eval-suite.schema.json +375 -0
- package/schemas/discovery/metadata.schema.json +30 -0
- package/schemas/discovery/selection-response.schema.json +47 -0
- package/schemas/replay/discovery-snapshot.schema.json +335 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { assertProviderRunScope } from "../runtime/provider-run.js";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { DefaultContextBuilder } from "../runtime/context.js";
|
|
4
|
+
import { hasOnlyKeys, isRecord, nonblank, resolveDiscoveryConfiguration } from "./configuration.js";
|
|
5
|
+
import { registrySourceFingerprint } from "./corpus.js";
|
|
6
|
+
import { CapabilityDiscoverer, validateQueries } from "./discoverer.js";
|
|
7
|
+
import { DiscoveryError } from "./diagnostics.js";
|
|
8
|
+
import { positiveInteger, resolveIndexLimits } from "./embedding-configuration.js";
|
|
9
|
+
import { exportDiscoveryResult, exportSelectedResult, validateRouterLimit } from "./export.js";
|
|
10
|
+
import { boundedJsonSnapshot, deepFreeze } from "./json-snapshot.js";
|
|
11
|
+
import { ModelCapabilitySelector } from "./selection.js";
|
|
12
|
+
import { resolveSelectionLimits } from "./selection-configuration.js";
|
|
13
|
+
import { aggregateDiscoveryUsage } from "./usage.js";
|
|
14
|
+
import { SELECTION_HARD_LIMITS } from "./versions.js";
|
|
15
|
+
export const DISCOVERY_STANDARD_PROFILE = deepFreeze({
|
|
16
|
+
policyVersion: "aicf.discovery.standard.v1", timeoutMs: 30_000,
|
|
17
|
+
weights: { semantic: 0.75, lexical: 0.25 }, failurePolicy: "unavailable",
|
|
18
|
+
indexLimits: { maxArtifactBytes: 16 * 1024 * 1024, maxEntries: 10_000, maxVectorDimension: 4_096 },
|
|
19
|
+
maxCharactersPerText: 16_384, maxCharactersPerCall: 65_536
|
|
20
|
+
});
|
|
21
|
+
const eligibilityKeys = [
|
|
22
|
+
"allowedCapabilityTypes", "allowedDomains", "capabilityPermissions", "excludeCapabilityIds", "includeCapabilityIds",
|
|
23
|
+
"includeDeprecated", "includeDisabledForTests", "includeDraft", "includeExperimental", "includeRestricted",
|
|
24
|
+
"maxCapabilities", "maxRiskTier"
|
|
25
|
+
];
|
|
26
|
+
function validateEligibility(value) {
|
|
27
|
+
const invalid = () => new DiscoveryError({ code: "invalid_input", message: "Discovery eligibility options are invalid." });
|
|
28
|
+
for (const key of ["allowedCapabilityTypes", "allowedDomains", "excludeCapabilityIds", "includeCapabilityIds"]) {
|
|
29
|
+
if (value[key] !== undefined && (!Array.isArray(value[key]) || !value[key].every(nonblank)))
|
|
30
|
+
throw invalid();
|
|
31
|
+
}
|
|
32
|
+
for (const key of ["includeDeprecated", "includeDisabledForTests", "includeDraft", "includeExperimental", "includeRestricted"]) {
|
|
33
|
+
if (value[key] !== undefined && typeof value[key] !== "boolean")
|
|
34
|
+
throw invalid();
|
|
35
|
+
}
|
|
36
|
+
if (value.maxRiskTier !== undefined && !["low", "medium", "high", "critical"].includes(value.maxRiskTier))
|
|
37
|
+
throw invalid();
|
|
38
|
+
if (value.capabilityPermissions !== undefined) {
|
|
39
|
+
if (!isRecord(value.capabilityPermissions))
|
|
40
|
+
throw invalid();
|
|
41
|
+
for (const permission of Object.values(value.capabilityPermissions)) {
|
|
42
|
+
if (!isRecord(permission) || !hasOnlyKeys(permission, ["allowed", "permissions", "reason"])
|
|
43
|
+
|| typeof permission.allowed !== "boolean" || !Array.isArray(permission.permissions)
|
|
44
|
+
|| !permission.permissions.every(nonblank) || permission.reason !== undefined && typeof permission.reason !== "string")
|
|
45
|
+
throw invalid();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
validateRouterLimit(value.maxCapabilities);
|
|
49
|
+
}
|
|
50
|
+
/** Simple application-driven search. Model choice and execution stay in the ordinary runtime. */
|
|
51
|
+
export function createCapabilityDiscovery(options) {
|
|
52
|
+
const invalid = () => new DiscoveryError({ code: "invalid_configuration", message: "The discovery facade configuration is invalid." });
|
|
53
|
+
if (!isRecord(options) || !hasOnlyKeys(options, ["registry", "contextBuilder", "ranker", "policyVersion", "timeoutMs",
|
|
54
|
+
"limits", "aliasWeight", "minRetrievalScore", "mode", "embedding", "weights", "failurePolicy"])
|
|
55
|
+
|| Object.values(options).some(value => value === null)
|
|
56
|
+
|| !options.registry || options.contextBuilder !== undefined && typeof options.contextBuilder?.build !== "function")
|
|
57
|
+
throw invalid();
|
|
58
|
+
const timeoutMs = options.timeoutMs ?? DISCOVERY_STANDARD_PROFILE.timeoutMs;
|
|
59
|
+
if (!positiveInteger(timeoutMs))
|
|
60
|
+
throw invalid();
|
|
61
|
+
const registry = options.registry;
|
|
62
|
+
const contextBuilder = options.contextBuilder ?? new DefaultContextBuilder();
|
|
63
|
+
const base = { policyVersion: options.policyVersion ?? DISCOVERY_STANDARD_PROFILE.policyVersion,
|
|
64
|
+
limits: options.limits, aliasWeight: options.aliasWeight, minRetrievalScore: options.minRetrievalScore };
|
|
65
|
+
const configuration = options.mode === "hybrid"
|
|
66
|
+
? { ...base, mode: "hybrid", weights: options.weights ?? DISCOVERY_STANDARD_PROFILE.weights,
|
|
67
|
+
failurePolicy: options.failurePolicy ?? DISCOVERY_STANDARD_PROFILE.failurePolicy }
|
|
68
|
+
: { ...base, mode: options.mode ?? "lexical" };
|
|
69
|
+
const resolved = resolveDiscoveryConfiguration(configuration);
|
|
70
|
+
const ranker = options.ranker;
|
|
71
|
+
// Validate even before the first search; the low-level engine remains strict and explicit.
|
|
72
|
+
const lexicalEngine = new CapabilityDiscoverer({ ...base, mode: "lexical" }, { ranker });
|
|
73
|
+
let embedding;
|
|
74
|
+
let indexLimits;
|
|
75
|
+
if (options.mode === "hybrid") {
|
|
76
|
+
const value = options.embedding;
|
|
77
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["client", "index", "maxInputTokens", "indexLimits", "maxCharactersPerText",
|
|
78
|
+
"maxCharactersPerCall", "maxEstimatedCostPerCall", "estimateCost"])
|
|
79
|
+
|| !positiveInteger(value.maxInputTokens) || !value.client || typeof value.client.embed !== "function"
|
|
80
|
+
|| typeof value.client.mock !== "boolean" || value.indexLimits !== undefined && !isRecord(value.indexLimits))
|
|
81
|
+
throw invalid();
|
|
82
|
+
embedding = { ...value };
|
|
83
|
+
indexLimits = resolveIndexLimits({ ...DISCOVERY_STANDARD_PROFILE.indexLimits, ...value.indexLimits });
|
|
84
|
+
for (const key of ["maxCharactersPerText", "maxCharactersPerCall"]) {
|
|
85
|
+
if (value[key] !== undefined && !positiveInteger(value[key]))
|
|
86
|
+
throw invalid();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (options.embedding !== undefined || options.weights !== undefined || options.failurePolicy !== undefined)
|
|
90
|
+
throw invalid();
|
|
91
|
+
// Entries live only as long as the caller retains the issued result. This is not a retrieval cache.
|
|
92
|
+
const bindings = new WeakMap();
|
|
93
|
+
const batches = new WeakMap();
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
async search(request) {
|
|
96
|
+
if (!isRecord(request) || !hasOnlyKeys(request, ["tasks", "runtimeContext", "controls", "eligibility", "signal", "deadlineAt", "budget", "providerRun"])
|
|
97
|
+
|| !Array.isArray(request.tasks) || !request.runtimeContext
|
|
98
|
+
|| request.controls !== undefined && typeof request.controls?.evaluate !== "function"
|
|
99
|
+
|| request.deadlineAt !== undefined && !Number.isFinite(request.deadlineAt)
|
|
100
|
+
|| request.signal !== undefined && !(request.signal instanceof AbortSignal)
|
|
101
|
+
|| request.eligibility !== undefined && (!isRecord(request.eligibility) || !hasOnlyKeys(request.eligibility, eligibilityKeys))) {
|
|
102
|
+
throw new DiscoveryError({ code: "invalid_input", message: "Search requires tasks, runtime context, and valid request options." });
|
|
103
|
+
}
|
|
104
|
+
if (request.tasks.length > resolved.limits.maxQueries)
|
|
105
|
+
throw new DiscoveryError({ code: "input_limit_exceeded", message: "The task batch exceeds the configured limit." });
|
|
106
|
+
const queries = Array.from(request.tasks, task => {
|
|
107
|
+
if (!isRecord(task) || !hasOnlyKeys(task, ["id", "text"]) || !nonblank(task.id) || typeof task.text !== "string") {
|
|
108
|
+
throw new DiscoveryError({ code: "invalid_input", message: "Tasks require unique nonblank IDs and string text." });
|
|
109
|
+
}
|
|
110
|
+
return { id: task.id, fragments: [{ key: "task", text: task.text }] };
|
|
111
|
+
});
|
|
112
|
+
// Reuse the engine's bounds and snapshot validation before invoking application code.
|
|
113
|
+
validateQueries({ queries }, resolved);
|
|
114
|
+
if (!queries.length)
|
|
115
|
+
return Object.freeze([]);
|
|
116
|
+
const eligibility = request.eligibility ?? {};
|
|
117
|
+
validateEligibility(eligibility);
|
|
118
|
+
if (request.providerRun)
|
|
119
|
+
assertProviderRunScope(request.providerRun, request.runtimeContext.runId, request.budget);
|
|
120
|
+
const budget = request.budget ?? request.providerRun?.budget;
|
|
121
|
+
const signals = [request.signal, request.providerRun?.signal].filter((item) => !!item);
|
|
122
|
+
const signal = signals.length ? AbortSignal.any(signals) : undefined;
|
|
123
|
+
const deadlineAt = Math.min(Date.now() + timeoutMs, request.deadlineAt ?? Infinity, request.providerRun?.deadlineAt ?? Infinity);
|
|
124
|
+
let context;
|
|
125
|
+
try {
|
|
126
|
+
context = structuredClone(request.runtimeContext);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw new DiscoveryError({ code: "invalid_input", message: "The runtime context cannot be bound to discovery." });
|
|
130
|
+
}
|
|
131
|
+
const source = registrySourceFingerprint(registry);
|
|
132
|
+
const builtContext = await contextBuilder.build({ baseContext: request.runtimeContext, registry,
|
|
133
|
+
// This context-only sentinel never enters retrieval or provider input.
|
|
134
|
+
userInput: { text: queries.find(query => query.fragments[0].text.trim())?.fragments[0].text ?? "[empty discovery query]" } });
|
|
135
|
+
const routeRequest = { ...eligibility, controls: request.controls, registry, builtContext,
|
|
136
|
+
userInput: { text: queries[0].fragments[0].text } };
|
|
137
|
+
let engine = lexicalEngine;
|
|
138
|
+
if (embedding) {
|
|
139
|
+
if (!budget || !isRecord(budget) || !hasOnlyKeys(budget, ["ledger", "policy"])
|
|
140
|
+
|| !Number.isSafeInteger(budget.policy?.maxInputTokensPerRun)
|
|
141
|
+
|| budget.policy.maxInputTokensPerRun < 0)
|
|
142
|
+
throw invalid();
|
|
143
|
+
engine = new CapabilityDiscoverer(resolved, { ranker, embedding: {
|
|
144
|
+
client: embedding.client, index: embedding.index, indexLimits: indexLimits, execution: {
|
|
145
|
+
budgetPolicy: budget.policy, estimateCost: embedding.estimateCost,
|
|
146
|
+
limits: { maxInputTokens: embedding.maxInputTokens,
|
|
147
|
+
maxCharactersPerText: embedding.maxCharactersPerText ?? DISCOVERY_STANDARD_PROFILE.maxCharactersPerText,
|
|
148
|
+
maxCharactersPerCall: embedding.maxCharactersPerCall ?? DISCOVERY_STANDARD_PROFILE.maxCharactersPerCall,
|
|
149
|
+
...(embedding.maxEstimatedCostPerCall === undefined ? {} : { maxEstimatedCostPerCall: embedding.maxEstimatedCostPerCall }) }
|
|
150
|
+
}
|
|
151
|
+
} });
|
|
152
|
+
}
|
|
153
|
+
const results = await engine.discover({ routeRequest, queries, runId: context.runId, requestId: context.requestId,
|
|
154
|
+
deadlineAt, signal, budget: budget?.ledger, providerRun: request.providerRun,
|
|
155
|
+
maxCandidates: Math.min(eligibility.maxCapabilities ?? resolved.limits.maxCandidates, resolved.limits.maxCandidates) });
|
|
156
|
+
return Object.freeze(results.map((result, index) => {
|
|
157
|
+
const current = isDeepStrictEqual(context, request.runtimeContext) && isDeepStrictEqual(context, builtContext.runtimeContext);
|
|
158
|
+
const issued = deepFreeze(exportDiscoveryResult({ ...routeRequest, deadlineAt, signal }, result, source, current).discovery[0]);
|
|
159
|
+
bindings.set(issued, { source, taskText: queries[index].fragments[0].text, context,
|
|
160
|
+
liveContext: request.runtimeContext, eligibility, controls: request.controls, builtContext, signal, deadlineAt, providerRun: request.providerRun });
|
|
161
|
+
request.providerRun?.record("discovery.end", context.requestId, { queryId: issued.queryId, outcome: issued.status,
|
|
162
|
+
candidateCount: issued.candidates.length, reasonCode: issued.diagnostics[0]?.code, corpusFingerprint: issued.corpusFingerprint, indexFingerprint: issued.indexFingerprint,
|
|
163
|
+
retrievalPolicyFingerprint: issued.retrievalPolicyFingerprint, eligibilityFingerprint: issued.eligibilityFingerprint });
|
|
164
|
+
return issued;
|
|
165
|
+
}));
|
|
166
|
+
},
|
|
167
|
+
async select(request) {
|
|
168
|
+
const invalidInput = () => new DiscoveryError({ code: "invalid_input", message: "Selection requires issued results from one compatible invocation and scope." });
|
|
169
|
+
if (!isRecord(request) || !hasOnlyKeys(request, ["results", "client", "budget", "maxInputTokens", "maxOutputTokens",
|
|
170
|
+
"contextByQueryId", "limits", "maxEstimatedCostPerCall", "estimateCost", "signal", "deadlineAt", "providerRun"])
|
|
171
|
+
|| !Array.isArray(request.results) || request.signal !== undefined && !(request.signal instanceof AbortSignal)
|
|
172
|
+
|| request.deadlineAt !== undefined && !Number.isFinite(request.deadlineAt)
|
|
173
|
+
|| request.contextByQueryId !== undefined && !isRecord(request.contextByQueryId))
|
|
174
|
+
throw invalidInput();
|
|
175
|
+
const limits = resolveSelectionLimits({ ...request.limits, maxInputTokens: request.maxInputTokens, maxOutputTokens: request.maxOutputTokens });
|
|
176
|
+
if (request.limits !== undefined && (!isRecord(request.limits) || !hasOnlyKeys(request.limits, ["maxTasks", "maxInputCharacters", "maxAttempts"])))
|
|
177
|
+
throw invalidInput();
|
|
178
|
+
if (request.results.length > SELECTION_HARD_LIMITS.maxTasks)
|
|
179
|
+
throw new DiscoveryError({ code: "input_limit_exceeded", message: "The selection batch exceeds the hard task limit." });
|
|
180
|
+
const records = Array.from(request.results, result => { const record = bindings.get(result); if (!record)
|
|
181
|
+
throw invalidInput(); return record; });
|
|
182
|
+
const queryIds = new Set(request.results.map(result => result.queryId));
|
|
183
|
+
if (queryIds.size !== request.results.length || Reflect.ownKeys(request.contextByQueryId ?? {}).some(id => typeof id !== "string" || !queryIds.has(id)))
|
|
184
|
+
throw invalidInput();
|
|
185
|
+
const projections = Object.getOwnPropertyDescriptors(request.contextByQueryId ?? {});
|
|
186
|
+
if (Object.values(projections).some(property => !property.enumerable || !("value" in property)))
|
|
187
|
+
throw invalidInput();
|
|
188
|
+
if (!records.length)
|
|
189
|
+
return deepFreeze({ discovery: [], outcomes: [], diagnostics: [], usage: aggregateDiscoveryUsage([], 0) });
|
|
190
|
+
const first = records[0];
|
|
191
|
+
if (records.some(record => !isDeepStrictEqual(record.context, first.context) || !isDeepStrictEqual(record.eligibility, first.eligibility)
|
|
192
|
+
|| record.controls !== first.controls || record.providerRun !== first.providerRun))
|
|
193
|
+
throw invalidInput();
|
|
194
|
+
const providerRun = request.providerRun ?? first.providerRun;
|
|
195
|
+
if (request.providerRun && first.providerRun && request.providerRun !== first.providerRun)
|
|
196
|
+
throw invalidInput();
|
|
197
|
+
if (providerRun)
|
|
198
|
+
assertProviderRunScope(providerRun, first.context.runId, request.budget);
|
|
199
|
+
const budget = request.budget ?? providerRun?.budget;
|
|
200
|
+
if (!budget || !isRecord(budget) || !hasOnlyKeys(budget, ["ledger", "policy"]))
|
|
201
|
+
throw invalidInput();
|
|
202
|
+
const signals = [request.signal, providerRun?.signal, ...records.map(record => record.signal)].filter((signal) => signal !== undefined);
|
|
203
|
+
const signal = signals.length ? AbortSignal.any(signals) : undefined;
|
|
204
|
+
const deadlineAt = Math.min(request.deadlineAt ?? Infinity, providerRun?.deadlineAt ?? Infinity, ...records.map(record => record.deadlineAt));
|
|
205
|
+
const model = structuredClone(request.client.configuration), mock = request.client.mock, transportVersion = request.client.transportVersion;
|
|
206
|
+
let projectedContext;
|
|
207
|
+
try {
|
|
208
|
+
projectedContext = boundedJsonSnapshot(request.contextByQueryId ?? {}, SELECTION_HARD_LIMITS.maxInputCharacters * 4);
|
|
209
|
+
}
|
|
210
|
+
catch { /* The selector returns a bounded invalid-input outcome without reading accessors. */ }
|
|
211
|
+
const originalResults = [...request.results];
|
|
212
|
+
const current = () => records.every(record => isDeepStrictEqual(record.context, record.liveContext))
|
|
213
|
+
&& isDeepStrictEqual(model, request.client.configuration) && mock === request.client.mock && transportVersion === request.client.transportVersion
|
|
214
|
+
&& isDeepStrictEqual(projectedContext, request.contextByQueryId ?? {});
|
|
215
|
+
const batch = await new ModelCapabilitySelector().select({
|
|
216
|
+
tasks: request.results.map((result, index) => ({ queryId: result.queryId, requestText: records[index].taskText,
|
|
217
|
+
...(Object.hasOwn(projections, result.queryId) ? { context: projections[result.queryId].value } : {}), discovery: result })),
|
|
218
|
+
registry, routeRequest: { ...first.eligibility, controls: first.controls, registry, builtContext: first.builtContext,
|
|
219
|
+
userInput: { text: first.taskText } }, runId: first.context.runId, requestId: first.context.requestId,
|
|
220
|
+
deadlineAt, signal, providerRun,
|
|
221
|
+
client: request.client, budget: budget.ledger, budgetPolicy: budget.policy, limits,
|
|
222
|
+
maxEstimatedCostPerCall: request.maxEstimatedCostPerCall, estimateCost: request.estimateCost,
|
|
223
|
+
isCurrent: current
|
|
224
|
+
});
|
|
225
|
+
batches.set(batch, { results: originalResults, current, deadlineAt, signal, providerRun });
|
|
226
|
+
providerRun?.record("selection.end", first.context.requestId, { taskCount: batch.outcomes.length, reasonCode: batch.diagnostics[0]?.code, outcome: batch.diagnostics.length ? "unavailable" : "completed" });
|
|
227
|
+
return batch;
|
|
228
|
+
},
|
|
229
|
+
routerFor(value, queryId) {
|
|
230
|
+
const batch = queryId === undefined ? undefined : value;
|
|
231
|
+
const selectionBinding = batch && batches.get(batch);
|
|
232
|
+
if (batch && !selectionBinding)
|
|
233
|
+
throw new DiscoveryError({ code: "invalid_input", message: "The selection batch was not issued by this facade." });
|
|
234
|
+
const result = batch ? selectionBinding.results.find(result => result.queryId === queryId) : value;
|
|
235
|
+
const binding = result && bindings.get(result);
|
|
236
|
+
if (!binding)
|
|
237
|
+
throw new DiscoveryError({ code: "invalid_input", message: "The result was not issued by this discovery instance." });
|
|
238
|
+
const routeWithDiagnostics = (request) => {
|
|
239
|
+
validateRouterLimit(request.maxCapabilities);
|
|
240
|
+
// A runner may omit search filters, but may never replace them with broader ones.
|
|
241
|
+
let current = request.registry === registry && (selectionBinding?.current() ?? true) && isDeepStrictEqual(binding.context, request.builtContext.runtimeContext)
|
|
242
|
+
&& isDeepStrictEqual(binding.context, binding.liveContext) && request.userInput.text === binding.taskText
|
|
243
|
+
&& (!binding.controls || !request.controls || request.controls === binding.controls)
|
|
244
|
+
&& (!request.providerRun || request.providerRun === (selectionBinding?.providerRun ?? binding.providerRun));
|
|
245
|
+
for (const key of eligibilityKeys) {
|
|
246
|
+
if (key !== "maxCapabilities" && request[key] !== undefined && binding.eligibility[key] !== undefined
|
|
247
|
+
&& !isDeepStrictEqual(request[key], binding.eligibility[key]))
|
|
248
|
+
current = false;
|
|
249
|
+
}
|
|
250
|
+
const limit = Math.min(request.maxCapabilities ?? 20, binding.eligibility.maxCapabilities ?? 20);
|
|
251
|
+
const signals = [request.signal, selectionBinding?.signal ?? binding.signal].filter((item) => !!item);
|
|
252
|
+
const routeRequest = { ...request, maxCapabilities: limit,
|
|
253
|
+
controls: request.controls ?? binding.controls, signal: signals.length ? AbortSignal.any(signals) : undefined,
|
|
254
|
+
deadlineAt: Math.min(request.deadlineAt ?? Infinity, selectionBinding?.deadlineAt ?? binding.deadlineAt) };
|
|
255
|
+
for (const key of eligibilityKeys) {
|
|
256
|
+
if (key !== "maxCapabilities" && request[key] === undefined)
|
|
257
|
+
Object.assign(routeRequest, { [key]: binding.eligibility[key] });
|
|
258
|
+
}
|
|
259
|
+
return batch ? exportSelectedResult(routeRequest, result, batch, binding.source, current)
|
|
260
|
+
: exportDiscoveryResult(routeRequest, result, binding.source, current);
|
|
261
|
+
};
|
|
262
|
+
return Object.freeze({ providerRun: selectionBinding?.providerRun ?? binding.providerRun, routeWithDiagnostics, route: (request) => routeWithDiagnostics(request).slice,
|
|
263
|
+
revalidate: (request) => routeWithDiagnostics(request).slice });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { JsonObject } from "../types.js";
|
|
2
|
+
import type { CapabilityCandidateCard, DiscoveryConfiguration, EmbeddingConfiguration, ProviderModelConfiguration, SelectionLimits } from "./types.js";
|
|
3
|
+
export interface CapabilityVersionBinding {
|
|
4
|
+
capabilityId: string;
|
|
5
|
+
capabilityVersion: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DiscoveryFingerprintPayloads {
|
|
8
|
+
registry_source: {
|
|
9
|
+
capabilities: readonly (CapabilityVersionBinding & {
|
|
10
|
+
sourceContentHash: string;
|
|
11
|
+
})[];
|
|
12
|
+
};
|
|
13
|
+
corpus: {
|
|
14
|
+
registrySourceFingerprint: string;
|
|
15
|
+
projectionVersion: string;
|
|
16
|
+
projectionConfiguration: JsonObject;
|
|
17
|
+
documents: readonly (CapabilityVersionBinding & {
|
|
18
|
+
documentHash: string;
|
|
19
|
+
})[];
|
|
20
|
+
};
|
|
21
|
+
embedding_configuration: EmbeddingConfiguration;
|
|
22
|
+
index: {
|
|
23
|
+
schemaVersion: "1.0";
|
|
24
|
+
projectionVersion: string;
|
|
25
|
+
mock: boolean;
|
|
26
|
+
registrySourceFingerprint: string;
|
|
27
|
+
corpusFingerprint: string;
|
|
28
|
+
embedding: EmbeddingConfiguration;
|
|
29
|
+
embeddingConfigurationFingerprint: string;
|
|
30
|
+
vectorDimension: number;
|
|
31
|
+
entries: readonly (CapabilityVersionBinding & {
|
|
32
|
+
documentHash: string;
|
|
33
|
+
vectorHash: string;
|
|
34
|
+
})[];
|
|
35
|
+
};
|
|
36
|
+
eligibility: {
|
|
37
|
+
capabilities: readonly (CapabilityVersionBinding & {
|
|
38
|
+
exposedOperations: readonly ("select" | "prepare")[];
|
|
39
|
+
})[];
|
|
40
|
+
/** Only explicit, nonsecret routing policy belongs in this projection. */
|
|
41
|
+
policy: JsonObject;
|
|
42
|
+
};
|
|
43
|
+
retrieval_policy: {
|
|
44
|
+
tokenizerVersion: string;
|
|
45
|
+
rankerVersion: string;
|
|
46
|
+
configuration: DiscoveryConfiguration;
|
|
47
|
+
};
|
|
48
|
+
candidate_cards: {
|
|
49
|
+
projectionVersion: string;
|
|
50
|
+
cards: readonly (Pick<CapabilityCandidateCard, "capabilityId" | "capabilityVersion"> & {
|
|
51
|
+
cardHash: string;
|
|
52
|
+
})[];
|
|
53
|
+
};
|
|
54
|
+
selector: {
|
|
55
|
+
transportVersion?: string;
|
|
56
|
+
promptVersion: string;
|
|
57
|
+
cardProjectionVersion: string;
|
|
58
|
+
outputSchemaVersion: string;
|
|
59
|
+
model: ProviderModelConfiguration;
|
|
60
|
+
mock: boolean;
|
|
61
|
+
limits: SelectionLimits;
|
|
62
|
+
maxEstimatedCostPerCall?: number;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export type DiscoveryFingerprintKind = keyof DiscoveryFingerprintPayloads;
|
|
66
|
+
/** Strict JSON serialization with ordinal object-key order and unchanged arrays. */
|
|
67
|
+
export declare function canonicalizeDiscoveryValue(value: unknown): string;
|
|
68
|
+
/** Content digest for a source manifest or an explicitly projected retrieval document. */
|
|
69
|
+
export declare function hashDiscoveryContent(value: unknown): string;
|
|
70
|
+
export declare function fingerprintDiscoveryValue<K extends DiscoveryFingerprintKind>(kind: K, payload: DiscoveryFingerprintPayloads[K]): string;
|
|
71
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/discovery/fingerprint.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EACV,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EACtB,0BAA0B,EAC1B,eAAe,EAChB,MAAM,YAAY,CAAC;AAIpB,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,4BAA4B;IAC3C,eAAe,EAAE;QACf,YAAY,EAAE,SAAS,CAAC,wBAAwB,GAAG;YAAE,iBAAiB,EAAE,MAAM,CAAA;SAAE,CAAC,EAAE,CAAC;KACrF,CAAC;IACF,MAAM,EAAE;QACN,yBAAyB,EAAE,MAAM,CAAC;QAClC,iBAAiB,EAAE,MAAM,CAAC;QAC1B,uBAAuB,EAAE,UAAU,CAAC;QACpC,SAAS,EAAE,SAAS,CAAC,wBAAwB,GAAG;YAAE,YAAY,EAAE,MAAM,CAAA;SAAE,CAAC,EAAE,CAAC;KAC7E,CAAC;IACF,uBAAuB,EAAE,sBAAsB,CAAC;IAChD,KAAK,EAAE;QACL,aAAa,EAAE,KAAK,CAAC;QACrB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,IAAI,EAAE,OAAO,CAAC;QACd,yBAAyB,EAAE,MAAM,CAAC;QAClC,iBAAiB,EAAE,MAAM,CAAC;QAC1B,SAAS,EAAE,sBAAsB,CAAC;QAClC,iCAAiC,EAAE,MAAM,CAAC;QAC1C,eAAe,EAAE,MAAM,CAAC;QACxB,OAAO,EAAE,SAAS,CAAC,wBAAwB,GAAG;YAAE,YAAY,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC,EAAE,CAAC;KAC/F,CAAC;IACF,WAAW,EAAE;QACX,YAAY,EAAE,SAAS,CAAC,wBAAwB,GAAG;YAAE,iBAAiB,EAAE,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAA;SAAE,CAAC,EAAE,CAAC;QAC/G,0EAA0E;QAC1E,MAAM,EAAE,UAAU,CAAC;KACpB,CAAC;IACF,gBAAgB,EAAE;QAChB,gBAAgB,EAAE,MAAM,CAAC;QACzB,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,EAAE,sBAAsB,CAAC;KACvC,CAAC;IACF,eAAe,EAAE;QACf,iBAAiB,EAAE,MAAM,CAAC;QAC1B,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,uBAAuB,EAAE,cAAc,GAAG,mBAAmB,CAAC,GAAG;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC,EAAE,CAAC;KAChH,CAAC;IACF,QAAQ,EAAE;QACR,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,aAAa,EAAE,MAAM,CAAC;QACtB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,mBAAmB,EAAE,MAAM,CAAC;QAC5B,KAAK,EAAE,0BAA0B,CAAC;QAClC,IAAI,EAAE,OAAO,CAAC;QACd,MAAM,EAAE,eAAe,CAAC;QACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;KAClC,CAAC;CACH;AAED,MAAM,MAAM,wBAAwB,GAAG,MAAM,4BAA4B,CAAC;AAE1E,oFAAoF;AACpF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjE;AAED,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE3D;AAED,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,wBAAwB,EAC1E,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,4BAA4B,CAAC,CAAC,CAAC,GACvC,MAAM,CAMR"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { DiscoveryError } from "./diagnostics.js";
|
|
3
|
+
import { DISCOVERY_BINDING_VERSIONS } from "./versions.js";
|
|
4
|
+
/** Strict JSON serialization with ordinal object-key order and unchanged arrays. */
|
|
5
|
+
export function canonicalizeDiscoveryValue(value) {
|
|
6
|
+
return serialize(value, new Set());
|
|
7
|
+
}
|
|
8
|
+
/** Content digest for a source manifest or an explicitly projected retrieval document. */
|
|
9
|
+
export function hashDiscoveryContent(value) {
|
|
10
|
+
return "sha256:" + createHash("sha256").update(canonicalizeDiscoveryValue(value)).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
export function fingerprintDiscoveryValue(kind, payload) {
|
|
13
|
+
if (!Object.prototype.hasOwnProperty.call(DISCOVERY_BINDING_VERSIONS, kind)) {
|
|
14
|
+
throw invalidPayload();
|
|
15
|
+
}
|
|
16
|
+
const envelope = { kind, version: DISCOVERY_BINDING_VERSIONS[kind], payload };
|
|
17
|
+
return "sha256:" + createHash("sha256").update(canonicalizeDiscoveryValue(envelope)).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
function serialize(value, ancestors) {
|
|
20
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
}
|
|
26
|
+
if (typeof value !== "object" || value === null || ancestors.has(value)) {
|
|
27
|
+
throw invalidPayload();
|
|
28
|
+
}
|
|
29
|
+
const prototype = Object.getPrototypeOf(value);
|
|
30
|
+
const array = Array.isArray(value);
|
|
31
|
+
if ((!array && prototype !== Object.prototype && prototype !== null) || Object.getOwnPropertySymbols(value).length) {
|
|
32
|
+
throw invalidPayload();
|
|
33
|
+
}
|
|
34
|
+
ancestors.add(value);
|
|
35
|
+
try {
|
|
36
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
37
|
+
if (array) {
|
|
38
|
+
const elements = [];
|
|
39
|
+
if (Object.keys(descriptors).length !== value.length + 1)
|
|
40
|
+
throw invalidPayload();
|
|
41
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
42
|
+
const descriptor = descriptors[String(index)];
|
|
43
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable)
|
|
44
|
+
throw invalidPayload();
|
|
45
|
+
elements.push(serialize(descriptor.value, ancestors));
|
|
46
|
+
}
|
|
47
|
+
return "[" + elements.join(",") + "]";
|
|
48
|
+
}
|
|
49
|
+
const entries = [];
|
|
50
|
+
for (const key of Object.keys(descriptors).sort()) {
|
|
51
|
+
const descriptor = descriptors[key];
|
|
52
|
+
if (!("value" in descriptor) || !descriptor.enumerable)
|
|
53
|
+
throw invalidPayload();
|
|
54
|
+
entries.push(JSON.stringify(key) + ":" + serialize(descriptor.value, ancestors));
|
|
55
|
+
}
|
|
56
|
+
return "{" + entries.join(",") + "}";
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
ancestors.delete(value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function invalidPayload() {
|
|
63
|
+
return new DiscoveryError({
|
|
64
|
+
code: "invalid_fingerprint_payload",
|
|
65
|
+
message: "Discovery fingerprints require finite JSON data without accessors or cycles."
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CapabilityEmbeddingIndexBuildRequest, CapabilityEmbeddingIndexBuildResult } from "./embedding-types.js";
|
|
2
|
+
export declare function buildCapabilityEmbeddingIndex(request: CapabilityEmbeddingIndexBuildRequest): Promise<CapabilityEmbeddingIndexBuildResult>;
|
|
3
|
+
//# sourceMappingURL=index-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-builder.d.ts","sourceRoot":"","sources":["../../src/discovery/index-builder.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,oCAAoC,EAAE,mCAAmC,EAAE,MAAM,sBAAsB,CAAC;AAStH,wBAAsB,6BAA6B,CAAC,OAAO,EAAE,oCAAoC,GAAG,OAAO,CAAC,mCAAmC,CAAC,CAmE/I"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { CAPABILITY_CORPUS_PROJECTION_VERSION, ordinalCompare } from "./corpus.js";
|
|
3
|
+
import { hasOnlyKeys, isRecord } from "./configuration.js";
|
|
4
|
+
import { DiscoveryError } from "./diagnostics.js";
|
|
5
|
+
import { checkEmbeddingTexts, inputLimit, invalidEmbeddingConfiguration, positiveInteger, resolveEmbeddingConfiguration, resolveEmbeddingExecution, resolveIndexLimits } from "./embedding-configuration.js";
|
|
6
|
+
import { runEmbeddingAttempt, stopReason, validateProviderInvocation } from "./embeddings.js";
|
|
7
|
+
import { canonicalizeDiscoveryValue, fingerprintDiscoveryValue, hashDiscoveryContent } from "./fingerprint.js";
|
|
8
|
+
import { embeddingIndexFingerprint, validateCapabilityEmbeddingIndex } from "./index-validation.js";
|
|
9
|
+
import { boundedJsonSnapshot, deepFreeze } from "./json-snapshot.js";
|
|
10
|
+
import { LEXICAL_TOKENIZER_VERSION, tokenizeDiscoveryText } from "./lexical.js";
|
|
11
|
+
import { aggregateDiscoveryUsage } from "./usage.js";
|
|
12
|
+
export async function buildCapabilityEmbeddingIndex(request) {
|
|
13
|
+
validateProviderInvocation(request);
|
|
14
|
+
const started = performance.now();
|
|
15
|
+
const limits = resolveIndexLimits(request.limits);
|
|
16
|
+
const execution = resolveEmbeddingExecution(request.execution);
|
|
17
|
+
const configuration = resolveEmbeddingConfiguration(request.client.configuration);
|
|
18
|
+
if (typeof request.client.mock !== "boolean" || typeof request.client.embed !== "function"
|
|
19
|
+
|| !positiveInteger(request.batchSize) || !positiveInteger(request.maxProviderCalls))
|
|
20
|
+
throw invalidEmbeddingConfiguration();
|
|
21
|
+
const corpus = snapshotCorpus(request.corpus, limits.maxEntries, limits.maxArtifactBytes, execution.limits.maxCharactersPerText);
|
|
22
|
+
const stages = [];
|
|
23
|
+
const failed = (code, message) => ({
|
|
24
|
+
status: code === "cancelled" || code === "deadline_exceeded" ? "cancelled" : "unavailable",
|
|
25
|
+
index: null, usage: aggregateDiscoveryUsage(stages, performance.now() - started), diagnostics: [{ code, message }]
|
|
26
|
+
});
|
|
27
|
+
if (!corpus.documents.length)
|
|
28
|
+
return failed("invalid_input", "An empty corpus cannot produce an embedding index.");
|
|
29
|
+
const calls = Math.ceil(corpus.documents.length / request.batchSize);
|
|
30
|
+
if (calls > request.maxProviderCalls || calls > execution.budgetPolicy.maxProviderCallsPerRun)
|
|
31
|
+
throw inputLimit();
|
|
32
|
+
if (configuration.dimensions !== undefined && configuration.dimensions > limits.maxVectorDimension)
|
|
33
|
+
throw inputLimit();
|
|
34
|
+
for (let start = 0; start < corpus.documents.length; start += request.batchSize) {
|
|
35
|
+
checkEmbeddingTexts(corpus.documents.slice(start, start + request.batchSize).map(document => document.text), execution.limits);
|
|
36
|
+
}
|
|
37
|
+
const base = {
|
|
38
|
+
schemaVersion: "1.0", projectionVersion: corpus.projectionVersion,
|
|
39
|
+
registrySourceFingerprint: corpus.registrySourceFingerprint, corpusFingerprint: corpus.corpusFingerprint,
|
|
40
|
+
embedding: configuration, embeddingConfigurationFingerprint: fingerprintDiscoveryValue("embedding_configuration", configuration),
|
|
41
|
+
mock: request.client.mock, vectorDimension: configuration.dimensions ?? limits.maxVectorDimension,
|
|
42
|
+
builtAt: new Date().toISOString(), indexFingerprint: "sha256:" + "0".repeat(64),
|
|
43
|
+
entries: corpus.documents.map(document => ({
|
|
44
|
+
capabilityId: document.capabilityId, capabilityVersion: document.capabilityVersion,
|
|
45
|
+
documentHash: document.documentHash, vector: [], vectorHash: "sha256:" + "0".repeat(64)
|
|
46
|
+
}))
|
|
47
|
+
};
|
|
48
|
+
// Conservative complete-artifact bound: JSON finite-number spelling takes at most 25 bytes plus a comma.
|
|
49
|
+
if (Buffer.byteLength(canonicalizeDiscoveryValue(base))
|
|
50
|
+
+ corpus.documents.length * (base.vectorDimension * 26 + 1) > limits.maxArtifactBytes)
|
|
51
|
+
throw inputLimit();
|
|
52
|
+
let dimension = configuration.dimensions;
|
|
53
|
+
const entries = [];
|
|
54
|
+
for (let start = 0; start < corpus.documents.length; start += request.batchSize) {
|
|
55
|
+
const stopped = stopReason(request);
|
|
56
|
+
if (stopped)
|
|
57
|
+
return failed(stopped, "The embedding index build was cancelled or its deadline elapsed.");
|
|
58
|
+
const documents = corpus.documents.slice(start, start + request.batchSize);
|
|
59
|
+
const result = await runEmbeddingAttempt({
|
|
60
|
+
...request, configuration, mock: base.mock, execution, texts: documents.map(document => document.text),
|
|
61
|
+
stage: "embedding_build", maxVectorDimension: limits.maxVectorDimension, expectedDimension: dimension
|
|
62
|
+
});
|
|
63
|
+
stages.push(...result.usage.stages);
|
|
64
|
+
if (result.status !== "completed")
|
|
65
|
+
return {
|
|
66
|
+
status: result.status, index: null, usage: aggregateDiscoveryUsage(stages, performance.now() - started),
|
|
67
|
+
diagnostics: result.diagnostics
|
|
68
|
+
};
|
|
69
|
+
dimension = result.vectorDimension;
|
|
70
|
+
for (const [offset, document] of documents.entries()) {
|
|
71
|
+
const vector = [...result.vectors[offset]];
|
|
72
|
+
entries.push({
|
|
73
|
+
capabilityId: document.capabilityId, capabilityVersion: document.capabilityVersion,
|
|
74
|
+
documentHash: document.documentHash, vector, vectorHash: hashDiscoveryContent(vector)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const stopped = stopReason(request);
|
|
79
|
+
if (stopped)
|
|
80
|
+
return failed(stopped, "The embedding index build was cancelled or its deadline elapsed.");
|
|
81
|
+
const artifact = { ...base, vectorDimension: dimension, entries };
|
|
82
|
+
artifact.indexFingerprint = embeddingIndexFingerprint(artifact);
|
|
83
|
+
const validated = validateCapabilityEmbeddingIndex(artifact, { corpus, configuration, mock: base.mock, limits });
|
|
84
|
+
if (!validated.valid)
|
|
85
|
+
return { status: "unavailable", index: null, diagnostics: validated.diagnostics,
|
|
86
|
+
usage: aggregateDiscoveryUsage(stages, performance.now() - started) };
|
|
87
|
+
return { status: "built", index: validated.index, diagnostics: [], usage: aggregateDiscoveryUsage(stages, performance.now() - started) };
|
|
88
|
+
}
|
|
89
|
+
function snapshotCorpus(value, maxEntries, maxBytes, maxText) {
|
|
90
|
+
try {
|
|
91
|
+
if (!isRecord(value) || !Array.isArray(value.documents) || value.documents.length > maxEntries)
|
|
92
|
+
throw inputLimit();
|
|
93
|
+
const corpus = boundedJsonSnapshot(value, Math.min(Number.MAX_SAFE_INTEGER, maxBytes + maxEntries * maxText * 16));
|
|
94
|
+
if (corpus.schemaVersion !== "1.0" || corpus.projectionVersion !== CAPABILITY_CORPUS_PROJECTION_VERSION
|
|
95
|
+
|| !/^sha256:[a-f0-9]{64}$/.test(corpus.registrySourceFingerprint))
|
|
96
|
+
throw new Error();
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
for (const document of corpus.documents) {
|
|
99
|
+
if (!isRecord(document) || !hasOnlyKeys(document, ["capabilityId", "capabilityVersion", "riskTier", "name", "summary", "aliases", "text", "tokens", "documentHash"])
|
|
100
|
+
|| typeof document.capabilityId !== "string" || seen.has(document.capabilityId)
|
|
101
|
+
|| typeof document.name !== "string" || typeof document.summary !== "string"
|
|
102
|
+
|| !Array.isArray(document.aliases) || !document.aliases.every(alias => typeof alias === "string")
|
|
103
|
+
|| !["none", "low", "medium", "high", "critical"].includes(document.riskTier))
|
|
104
|
+
throw new Error();
|
|
105
|
+
seen.add(document.capabilityId);
|
|
106
|
+
const { capabilityId, capabilityVersion, riskTier, name, summary, aliases } = document;
|
|
107
|
+
const fields = [capabilityId, name, summary, ...aliases];
|
|
108
|
+
if (document.text.length > maxText)
|
|
109
|
+
throw inputLimit();
|
|
110
|
+
if (document.text !== fields.join("\n")
|
|
111
|
+
|| document.documentHash !== hashDiscoveryContent({ capabilityId, capabilityVersion, riskTier, name, summary, aliases })
|
|
112
|
+
|| canonicalizeDiscoveryValue(document.tokens) !== canonicalizeDiscoveryValue([...new Set(fields.flatMap(tokenizeDiscoveryText))]))
|
|
113
|
+
throw new Error();
|
|
114
|
+
}
|
|
115
|
+
const documents = [...corpus.documents].sort((a, b) => ordinalCompare(a.capabilityId, b.capabilityId));
|
|
116
|
+
if (corpus.corpusFingerprint !== fingerprintDiscoveryValue("corpus", {
|
|
117
|
+
registrySourceFingerprint: corpus.registrySourceFingerprint, projectionVersion: corpus.projectionVersion,
|
|
118
|
+
projectionConfiguration: { tokenizerVersion: LEXICAL_TOKENIZER_VERSION },
|
|
119
|
+
documents: documents.map(({ capabilityId, capabilityVersion, documentHash }) => ({ capabilityId, capabilityVersion, documentHash }))
|
|
120
|
+
}))
|
|
121
|
+
throw new Error();
|
|
122
|
+
return deepFreeze({ ...corpus, documents });
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error instanceof DiscoveryError && error.code === "input_limit_exceeded")
|
|
126
|
+
throw error;
|
|
127
|
+
throw new DiscoveryError({ code: "invalid_input", message: "An index build requires a current approved-field corpus snapshot." });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CapabilityEmbeddingIndexStore, EmbeddingIndexExpected, EmbeddingIndexValidationResult } from "./embedding-types.js";
|
|
2
|
+
import { type DiscoveryFingerprintPayloads } from "./fingerprint.js";
|
|
3
|
+
export declare function embeddingIndexFingerprint(index: DiscoveryFingerprintPayloads["index"]): string;
|
|
4
|
+
export declare function validateCapabilityEmbeddingIndex(value: unknown, expected: EmbeddingIndexExpected): EmbeddingIndexValidationResult;
|
|
5
|
+
export declare function serializeCapabilityEmbeddingIndex(value: unknown, expected: EmbeddingIndexExpected): string;
|
|
6
|
+
export declare function parseCapabilityEmbeddingIndex(text: string, expected: EmbeddingIndexExpected): EmbeddingIndexValidationResult;
|
|
7
|
+
export declare function loadCapabilityEmbeddingIndex(store: CapabilityEmbeddingIndexStore, key: string, expected: EmbeddingIndexExpected): Promise<EmbeddingIndexValidationResult>;
|
|
8
|
+
export declare function publishCapabilityEmbeddingIndex(store: CapabilityEmbeddingIndexStore, key: string, value: unknown, expected: EmbeddingIndexExpected): Promise<{
|
|
9
|
+
published: boolean;
|
|
10
|
+
diagnostics: readonly import("./diagnostics.js").DiscoveryDiagnostic[];
|
|
11
|
+
}>;
|
|
12
|
+
//# sourceMappingURL=index-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-validation.d.ts","sourceRoot":"","sources":["../../src/discovery/index-validation.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACgB,6BAA6B,EAAE,sBAAsB,EAAE,8BAA8B,EAChH,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAA+E,KAAK,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAOlJ,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,4BAA4B,CAAC,OAAO,CAAC,GAAG,MAAM,CAS9F;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,sBAAsB,GAAG,8BAA8B,CAkDjI;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,sBAAsB,GAAG,MAAM,CAI1G;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG,8BAA8B,CAQ5H;AAED,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,6BAA6B,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAClF,OAAO,CAAC,8BAA8B,CAAC,CAOzC;AAED,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,6BAA6B,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,sBAAsB,GAClG,OAAO,CAAC;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,SAAS,OAAO,kBAAkB,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC,CASzG"}
|