@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4
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/bin/cliOperations.ts +25 -3
- package/bin/run.ts +18 -0
- package/components/Application.ts +52 -78
- package/components/DESIGN.md +76 -3
- package/components/Scope.ts +10 -4
- package/components/anthropic/index.ts +10 -1
- package/components/bedrock/index.ts +10 -1
- package/components/componentLoader.ts +23 -14
- package/components/mcp/tools/operations.ts +3 -1
- package/components/ollama/index.ts +10 -1
- package/components/openai/index.ts +265 -18
- package/components/operations.js +40 -1
- package/config/configUtils.ts +3 -19
- package/dataLayer/DESIGN.md +29 -5
- package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
- package/dataLayer/restoreMarker.ts +250 -14
- package/dataLayer/schema.ts +2 -7
- package/dataLayer/schemaDescribe.ts +10 -0
- package/dist/bin/cliOperations.js +28 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/run.js +19 -0
- package/dist/bin/run.js.map +1 -1
- package/dist/components/Application.d.ts +8 -2
- package/dist/components/Application.js +43 -69
- package/dist/components/Application.js.map +1 -1
- package/dist/components/Scope.d.ts +1 -1
- package/dist/components/Scope.js +9 -4
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +0 -10
- package/dist/components/anthropic/index.js +9 -1
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +0 -12
- package/dist/components/bedrock/index.js +9 -1
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.js +23 -14
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +1 -1
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/ollama/index.d.ts +0 -13
- package/dist/components/ollama/index.js +9 -1
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +11 -15
- package/dist/components/openai/index.js +224 -12
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +9 -0
- package/dist/components/operations.js +32 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +1 -2
- package/dist/config/configUtils.js +7 -22
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +25 -3
- package/dist/dataLayer/restoreMarker.js +213 -13
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/schema.js +1 -4
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +10 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +28 -0
- package/dist/resources/DatabaseTransaction.d.ts +21 -1
- package/dist/resources/DatabaseTransaction.js +147 -8
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +1 -1
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/Table.d.ts +31 -3
- package/dist/resources/Table.js +397 -79
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +16 -10
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +1 -0
- package/dist/resources/blob.js +14 -5
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.js +3 -3
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databaseDropPreparation.d.ts +20 -0
- package/dist/resources/databaseDropPreparation.js +106 -0
- package/dist/resources/databaseDropPreparation.js.map +1 -0
- package/dist/resources/databases.d.ts +26 -21
- package/dist/resources/databases.js +1040 -204
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +23 -0
- package/dist/resources/derivedIndexRegistry.js +92 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -1
- package/dist/resources/derivedIndexRuntime.d.ts +10 -0
- package/dist/resources/derivedIndexRuntime.js +136 -25
- package/dist/resources/derivedIndexRuntime.js.map +1 -1
- package/dist/resources/derivedIndexes.d.ts +39 -0
- package/dist/resources/derivedIndexes.js +733 -0
- package/dist/resources/derivedIndexes.js.map +1 -0
- package/dist/resources/fullTextSchema.d.ts +8 -0
- package/dist/resources/fullTextSchema.js +42 -0
- package/dist/resources/fullTextSchema.js.map +1 -1
- package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
- package/dist/resources/fullTextSchemaLifecycle.js +0 -9
- package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
- package/dist/resources/graphql.js +101 -9
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
- package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
- package/dist/resources/models/Models.d.ts +26 -3
- package/dist/resources/models/Models.js +256 -13
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/TestBackend.d.ts +3 -1
- package/dist/resources/models/TestBackend.js +35 -1
- package/dist/resources/models/TestBackend.js.map +1 -1
- package/dist/resources/models/analyticsTable.d.ts +2 -2
- package/dist/resources/models/analyticsTable.js +4 -2
- package/dist/resources/models/analyticsTable.js.map +1 -1
- package/dist/resources/models/backendHelpers.d.ts +24 -0
- package/dist/resources/models/backendHelpers.js +18 -1
- package/dist/resources/models/backendHelpers.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +10 -21
- package/dist/resources/models/backendRegistry.js +74 -18
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +7 -0
- package/dist/resources/models/bootstrap.js +36 -4
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/decideHook.d.ts +33 -0
- package/dist/resources/models/decideHook.js +105 -0
- package/dist/resources/models/decideHook.js.map +1 -0
- package/dist/resources/models/decision.d.ts +64 -0
- package/dist/resources/models/decision.js +507 -0
- package/dist/resources/models/decision.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +134 -0
- package/dist/resources/models/decisionStore.js +350 -0
- package/dist/resources/models/decisionStore.js.map +1 -0
- package/dist/resources/models/embedHook.d.ts +66 -8
- package/dist/resources/models/embedHook.js +228 -61
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/generativeDecision.d.ts +58 -0
- package/dist/resources/models/generativeDecision.js +315 -0
- package/dist/resources/models/generativeDecision.js.map +1 -0
- package/dist/resources/models/types.d.ts +232 -4
- package/dist/security/authn/oidc/tokenExchange.js +3 -33
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
- package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
- package/dist/security/authn/oidc/tokenUseTable.js +28 -0
- package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
- package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
- package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
- package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
- package/dist/security/certificateVerification/crlVerification.js +77 -165
- package/dist/security/certificateVerification/crlVerification.js.map +1 -1
- package/dist/security/certificateVerification/types.d.ts +0 -3
- package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
- package/dist/security/certificateVerification/verificationTables.js +125 -0
- package/dist/security/certificateVerification/verificationTables.js.map +1 -0
- package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
- package/dist/security/certificateVerification/verificationUtils.js +6 -31
- package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
- package/dist/security/jsLoader.d.ts +5 -0
- package/dist/security/jsLoader.js +8 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/Server.d.ts +1 -0
- package/dist/server/Server.js.map +1 -1
- package/dist/server/http.js +2 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +21 -2
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobOwnership.d.ts +25 -0
- package/dist/server/jobs/jobOwnership.js +127 -0
- package/dist/server/jobs/jobOwnership.js.map +1 -0
- package/dist/server/jobs/jobProcess.js +9 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +18 -5
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/loadRootComponents.js +10 -0
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +11 -1
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +12 -2
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/threads/itc.d.ts +4 -1
- package/dist/server/threads/itc.js +29 -6
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +12 -4
- package/dist/server/threads/manageThreads.js +241 -119
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +1 -0
- package/dist/server/threads/threadServer.js +15 -1
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/server/threads/windowsProcessTree.d.ts +91 -0
- package/dist/server/threads/windowsProcessTree.js +361 -0
- package/dist/server/threads/windowsProcessTree.js.map +1 -0
- package/dist/server/throttle.d.ts +1 -1
- package/dist/server/throttle.js +8 -1
- package/dist/server/throttle.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +53 -46
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +10 -1
- package/dist/utility/errors/hdbError.js +55 -11
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/fsync.d.ts +2 -0
- package/dist/utility/fsync.js +20 -0
- package/dist/utility/fsync.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +3 -0
- package/dist/utility/hdbTerms.js +3 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/signalling.d.ts +7 -1
- package/dist/utility/signalling.js +27 -3
- package/dist/utility/signalling.js.map +1 -1
- package/dist/utility/when.d.ts +1 -0
- package/dist/utility/when.js +21 -0
- package/dist/utility/when.js.map +1 -1
- package/dist/validation/configValidator.js +30 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +13 -0
- package/json/systemSchema.json +28 -0
- package/npm-shrinkwrap.json +151 -55
- package/package.json +14 -13
- package/resources/DESIGN.md +80 -100
- package/resources/DatabaseTransaction.ts +157 -10
- package/resources/PrimaryRocksDatabase.ts +2 -2
- package/resources/Table.ts +494 -101
- package/resources/auditStore.ts +18 -10
- package/resources/blob.ts +13 -6
- package/resources/branchDatabase.ts +3 -3
- package/resources/databaseDropPreparation.ts +120 -0
- package/resources/databases.ts +1155 -205
- package/resources/derivedIndexRegistry.ts +122 -0
- package/resources/derivedIndexRuntime.ts +157 -30
- package/resources/derivedIndexes.ts +856 -0
- package/resources/fullTextSchema.ts +55 -0
- package/resources/fullTextSchemaLifecycle.ts +0 -15
- package/resources/graphql.ts +107 -12
- package/resources/indexes/DESIGN.md +7 -0
- package/resources/indexes/hnswDerivedIndex.ts +0 -260
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
- package/resources/models/DESIGN.md +39 -0
- package/resources/models/Models.ts +361 -18
- package/resources/models/TestBackend.ts +56 -1
- package/resources/models/analyticsTable.ts +6 -4
- package/resources/models/backendHelpers.ts +17 -0
- package/resources/models/backendRegistry.ts +76 -21
- package/resources/models/bootstrap.ts +57 -7
- package/resources/models/decideHook.ts +135 -0
- package/resources/models/decision.ts +505 -0
- package/resources/models/decisionStore.ts +419 -0
- package/resources/models/embedHook.ts +272 -55
- package/resources/models/generativeDecision.ts +459 -0
- package/resources/models/types.ts +231 -4
- package/security/DESIGN.md +22 -2
- package/security/authn/oidc/tokenExchange.ts +6 -34
- package/security/authn/oidc/tokenUseTable.ts +27 -0
- package/security/certificateVerification/certificateVerificationSource.ts +2 -3
- package/security/certificateVerification/crlVerification.ts +96 -174
- package/security/certificateVerification/types.ts +0 -3
- package/security/certificateVerification/verificationTables.ts +124 -0
- package/security/certificateVerification/verificationUtils.ts +8 -32
- package/security/jsLoader.ts +9 -1
- package/server/DESIGN.md +27 -1
- package/server/Server.ts +2 -1
- package/server/http.ts +4 -2
- package/server/itc/serverHandlers.js +29 -1
- package/server/jobs/jobOwnership.ts +89 -0
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +15 -4
- package/server/loadRootComponents.js +11 -0
- package/server/serverHelpers/contentTypes.ts +13 -1
- package/server/serverHelpers/serverHandlers.js +11 -2
- package/server/threads/itc.js +32 -6
- package/server/threads/manageThreads.js +268 -122
- package/server/threads/threadServer.js +13 -1
- package/server/threads/windowsProcessTree.ts +409 -0
- package/server/throttle.ts +11 -1
- package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
- package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
- package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
- package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
- package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
- package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
- package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
- package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
- package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
- package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
- package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
- package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
- package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
- package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
- package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
- package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
- package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
- package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
- package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
- package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
- package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
- package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
- package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
- package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
- package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
- package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +61 -54
- package/utility/DESIGN.md +5 -1
- package/utility/errors/hdbError.ts +56 -8
- package/utility/fsync.ts +16 -0
- package/utility/hdbTerms.ts +3 -0
- package/utility/signalling.ts +33 -4
- package/utility/when.ts +22 -0
- package/validation/configValidator.ts +36 -7
|
@@ -20,7 +20,7 @@ export interface ModelCallRecord {
|
|
|
20
20
|
app?: string;
|
|
21
21
|
model?: string;
|
|
22
22
|
backend: string;
|
|
23
|
-
method: 'embed' | 'generate' | 'generateStream';
|
|
23
|
+
method: 'embed' | 'generate' | 'generateStream' | 'decide' | 'scoreChoices';
|
|
24
24
|
adapter?: string;
|
|
25
25
|
conversation_id?: string;
|
|
26
26
|
prompt_tokens?: number;
|
|
@@ -134,15 +134,17 @@ export class ModelCallAnalyticsWriter {
|
|
|
134
134
|
this.#cleanupTimer.unref?.();
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
write(record: ModelCallRecord):
|
|
138
|
-
|
|
137
|
+
write(record: ModelCallRecord): number {
|
|
138
|
+
const id = getNextMonotonicTime();
|
|
139
|
+
if (this.#stopped) return id;
|
|
139
140
|
// id last so a record that accidentally carries an `id` field can't override
|
|
140
141
|
// the monotonic primary key.
|
|
141
|
-
this.#buffer.push({ ...record, id
|
|
142
|
+
this.#buffer.push({ ...record, id });
|
|
142
143
|
if (this.#buffer.length >= this.#maxBufferSize) {
|
|
143
144
|
// Out-of-cadence flush; swallow errors so a failing flush doesn't escape into the caller.
|
|
144
145
|
this.flush().catch((err) => log.warn?.(`Model-call analytics flush failed: ${err?.message ?? err}`));
|
|
145
146
|
}
|
|
147
|
+
return id;
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
async flush(): Promise<void> {
|
|
@@ -10,12 +10,29 @@
|
|
|
10
10
|
* in via a constructor type so the thrown errors carry the backend's name
|
|
11
11
|
* for `instanceof` matching in tests.
|
|
12
12
|
*/
|
|
13
|
+
import { ServerError } from '../../utility/errors/hdbError.ts';
|
|
13
14
|
import { isUnresolvedEnvVarPlaceholder } from '../../utility/expandEnvVar.ts';
|
|
14
15
|
import type { TokenUsage } from './types.ts';
|
|
15
16
|
|
|
16
17
|
/** Constructor signature for backend-specific error classes. */
|
|
17
18
|
export type BackendErrorCtor = new (message: string) => Error;
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Thrown by a backend's `scoreChoices` for a call it cannot score (#2838), as distinct from a call
|
|
22
|
+
* that failed: the decision adapter votes instead in `auto` mode, and the facade records the
|
|
23
|
+
* attempt as `scoring_unsupported`. `usage` carries the tokens the attempt did consume (a
|
|
24
|
+
* completion that came back without log-probabilities is still billed), so the failure row
|
|
25
|
+
* accounts for them.
|
|
26
|
+
*/
|
|
27
|
+
export class ChoiceScoringUnsupportedError extends ServerError {
|
|
28
|
+
declare usage?: TokenUsage;
|
|
29
|
+
constructor(message: string, usage?: TokenUsage) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'ChoiceScoringUnsupportedError';
|
|
32
|
+
if (usage) this.usage = usage;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
/**
|
|
20
37
|
* Combine a caller-supplied AbortSignal with a per-call timeout via
|
|
21
38
|
* `AbortSignal.any`. Returns the caller signal directly when no timeout is
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { ServerError } from '../../utility/errors/hdbError.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
DefineBackendSpec,
|
|
5
|
+
GenerateResult,
|
|
6
|
+
ModelBackend,
|
|
7
|
+
ModelCapabilities,
|
|
8
|
+
ModelKind,
|
|
9
|
+
ToolCall,
|
|
10
|
+
} from './types.ts';
|
|
4
11
|
|
|
5
12
|
/**
|
|
6
13
|
* Process-wide model backend registry.
|
|
@@ -20,10 +27,11 @@ import type { DefineBackendSpec, GenerateResult, ModelBackend, ModelCapabilities
|
|
|
20
27
|
* step) lands in Phase 2 alongside the first real backend.
|
|
21
28
|
*/
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
const registries: Record<ModelKind, Map<string, ModelBackend>> = {
|
|
31
|
+
embedding: new Map(),
|
|
32
|
+
generative: new Map(),
|
|
33
|
+
decision: new Map(),
|
|
34
|
+
};
|
|
27
35
|
|
|
28
36
|
/** A registration a factory made during construction, deferred to the caller. */
|
|
29
37
|
export interface CapturedInstall {
|
|
@@ -54,7 +62,7 @@ function install(kind: ModelKind, logicalName: string, backend: ModelBackend): v
|
|
|
54
62
|
else slot.extras.push({ kind, logicalName, backend });
|
|
55
63
|
return;
|
|
56
64
|
}
|
|
57
|
-
|
|
65
|
+
registries[kind].set(logicalName, backend);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
/** Map `logicalName` to a backend for embedding calls. Re-set replaces. */
|
|
@@ -67,6 +75,11 @@ export function setGenerative(logicalName: string, backend: ModelBackend): void
|
|
|
67
75
|
install('generative', logicalName, backend);
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
/** Map `logicalName` to a backend for decide calls. Re-set replaces. */
|
|
79
|
+
export function setDecision(logicalName: string, backend: ModelBackend): void {
|
|
80
|
+
install('decision', logicalName, backend);
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
/**
|
|
71
84
|
* Build a backend through its normal registration path but return it instead of installing it, so a
|
|
72
85
|
* config reload can install it conditionally. A scratch logical name would be briefly visible
|
|
@@ -99,7 +112,7 @@ export function replaceIfCurrent(
|
|
|
99
112
|
expected: ModelBackend | undefined,
|
|
100
113
|
next: ModelBackend
|
|
101
114
|
): boolean {
|
|
102
|
-
const map = kind
|
|
115
|
+
const map = registries[kind];
|
|
103
116
|
if (map.get(logicalName) !== expected) return false;
|
|
104
117
|
map.set(logicalName, next);
|
|
105
118
|
return true;
|
|
@@ -110,7 +123,7 @@ export function replaceIfCurrent(
|
|
|
110
123
|
* removed. Withdrawing a credential must not delete a slot another writer has since taken over.
|
|
111
124
|
*/
|
|
112
125
|
export function removeIfCurrent(kind: ModelKind, logicalName: string, expected: ModelBackend | undefined): boolean {
|
|
113
|
-
const map = kind
|
|
126
|
+
const map = registries[kind];
|
|
114
127
|
if (map.get(logicalName) !== expected) return false;
|
|
115
128
|
map.delete(logicalName);
|
|
116
129
|
return true;
|
|
@@ -118,7 +131,7 @@ export function removeIfCurrent(kind: ModelKind, logicalName: string, expected:
|
|
|
118
131
|
|
|
119
132
|
/** Non-throwing lookup of the backend mapped to `logicalName` for `kind`, or `undefined`. Used by the router to assemble + filter candidate lists without exceptions. */
|
|
120
133
|
export function getBackend(kind: ModelKind, logicalName: string): ModelBackend | undefined {
|
|
121
|
-
return
|
|
134
|
+
return registries[kind].get(logicalName);
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
/**
|
|
@@ -127,8 +140,7 @@ export function getBackend(kind: ModelKind, logicalName: string): ModelBackend |
|
|
|
127
140
|
* caller passes as `opts.model`, not the backend's own `.name`.
|
|
128
141
|
*/
|
|
129
142
|
export function listBackends(kind: ModelKind): Array<{ logicalName: string; backend: ModelBackend }> {
|
|
130
|
-
|
|
131
|
-
return [...map.entries()].map(([logicalName, backend]) => ({ logicalName, backend }));
|
|
143
|
+
return [...registries[kind].entries()].map(([logicalName, backend]) => ({ logicalName, backend }));
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
/**
|
|
@@ -136,7 +148,7 @@ export function listBackends(kind: ModelKind): Array<{ logicalName: string; back
|
|
|
136
148
|
* Throws `ModelBackendNotFoundError` if no backend is mapped.
|
|
137
149
|
*/
|
|
138
150
|
export function resolveEmbedding(logicalName: string = 'default'): ModelBackend {
|
|
139
|
-
const backend = embedding.get(logicalName);
|
|
151
|
+
const backend = registries.embedding.get(logicalName);
|
|
140
152
|
if (!backend) throw new ModelBackendNotFoundError('embedding', logicalName);
|
|
141
153
|
return backend;
|
|
142
154
|
}
|
|
@@ -146,11 +158,21 @@ export function resolveEmbedding(logicalName: string = 'default'): ModelBackend
|
|
|
146
158
|
* Throws `ModelBackendNotFoundError` if no backend is mapped.
|
|
147
159
|
*/
|
|
148
160
|
export function resolveGenerative(logicalName: string = 'default'): ModelBackend {
|
|
149
|
-
const backend = generative.get(logicalName);
|
|
161
|
+
const backend = registries.generative.get(logicalName);
|
|
150
162
|
if (!backend) throw new ModelBackendNotFoundError('generative', logicalName);
|
|
151
163
|
return backend;
|
|
152
164
|
}
|
|
153
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Resolve the decision backend mapped to `logicalName` (default: `'default'`).
|
|
168
|
+
* Throws `ModelBackendNotFoundError` if no backend is mapped.
|
|
169
|
+
*/
|
|
170
|
+
export function resolveDecision(logicalName: string = 'default'): ModelBackend {
|
|
171
|
+
const backend = registries.decision.get(logicalName);
|
|
172
|
+
if (!backend) throw new ModelBackendNotFoundError('decision', logicalName);
|
|
173
|
+
return backend;
|
|
174
|
+
}
|
|
175
|
+
|
|
154
176
|
/**
|
|
155
177
|
* Public registration API (#1325).
|
|
156
178
|
*
|
|
@@ -170,8 +192,10 @@ export function resolveGenerative(logicalName: string = 'default'): ModelBackend
|
|
|
170
192
|
* derives them for you, so prefer it.
|
|
171
193
|
*/
|
|
172
194
|
export function registerBackend(kind: ModelKind, id: string, backend: ModelBackend): void {
|
|
173
|
-
if (kind !== 'embedding' && kind !== 'generative')
|
|
174
|
-
throw new ModelBackendRegistrationError(
|
|
195
|
+
if (kind !== 'embedding' && kind !== 'generative' && kind !== 'decision')
|
|
196
|
+
throw new ModelBackendRegistrationError(
|
|
197
|
+
`kind must be 'embedding', 'generative' or 'decision', got '${String(kind)}'`
|
|
198
|
+
);
|
|
175
199
|
if (typeof id !== 'string' || id.length === 0)
|
|
176
200
|
throw new ModelBackendRegistrationError('backend id must be a non-empty string');
|
|
177
201
|
if (
|
|
@@ -185,6 +209,10 @@ export function registerBackend(kind: ModelKind, id: string, backend: ModelBacke
|
|
|
185
209
|
if (typeof backend.embed !== 'function')
|
|
186
210
|
throw new ModelBackendRegistrationError(`embedding backend '${id}' must implement embed()`);
|
|
187
211
|
setEmbedding(id, backend);
|
|
212
|
+
} else if (kind === 'decision') {
|
|
213
|
+
if (typeof backend.decide !== 'function')
|
|
214
|
+
throw new ModelBackendRegistrationError(`decision backend '${id}' must implement decide()`);
|
|
215
|
+
setDecision(id, backend);
|
|
188
216
|
} else {
|
|
189
217
|
if (typeof backend.generate !== 'function' && typeof backend.generateStream !== 'function')
|
|
190
218
|
throw new ModelBackendRegistrationError(
|
|
@@ -196,7 +224,8 @@ export function registerBackend(kind: ModelKind, id: string, backend: ModelBacke
|
|
|
196
224
|
|
|
197
225
|
/**
|
|
198
226
|
* Build a `ModelBackend` from just the methods it implements. `capabilities()`
|
|
199
|
-
* is derived from which of `embed` / `generate` / `generateStream`
|
|
227
|
+
* is derived from which of `embed` / `generate` / `generateStream` / `decide` /
|
|
228
|
+
* `scoreChoices` are present;
|
|
200
229
|
* `tools` and `adapters` aren't inferable from method presence, so pass them
|
|
201
230
|
* explicitly (both default `false`). Lowers the bar from authoring a class to
|
|
202
231
|
* supplying a function — pair with `registerBackend`. See #1325.
|
|
@@ -204,16 +233,34 @@ export function registerBackend(kind: ModelKind, id: string, backend: ModelBacke
|
|
|
204
233
|
export function defineBackend(spec: DefineBackendSpec): ModelBackend {
|
|
205
234
|
if (!spec || typeof spec.name !== 'string' || spec.name.length === 0)
|
|
206
235
|
throw new ModelBackendRegistrationError('defineBackend requires a non-empty name');
|
|
207
|
-
const {
|
|
236
|
+
const {
|
|
237
|
+
name,
|
|
238
|
+
embed,
|
|
239
|
+
generate,
|
|
240
|
+
generateStream,
|
|
241
|
+
decide,
|
|
242
|
+
scoreChoices,
|
|
243
|
+
tools = false,
|
|
244
|
+
adapters = false,
|
|
245
|
+
calibrated = false,
|
|
246
|
+
structuredOutput = false,
|
|
247
|
+
noMatch = false,
|
|
248
|
+
calibratedNoMatch = false,
|
|
249
|
+
maxScoredChoices,
|
|
250
|
+
} = spec;
|
|
208
251
|
// Gate on function-ness, not truthiness: a non-function value (`generate: 'oops'`)
|
|
209
252
|
// must be rejected at definition time, not assigned and crash at call time.
|
|
210
253
|
const hasEmbed = typeof embed === 'function';
|
|
211
254
|
const hasGenerate = typeof generate === 'function';
|
|
212
255
|
const hasStream = typeof generateStream === 'function';
|
|
213
|
-
|
|
256
|
+
const hasDecide = typeof decide === 'function';
|
|
257
|
+
const hasScore = typeof scoreChoices === 'function';
|
|
258
|
+
if (!hasEmbed && !hasGenerate && !hasStream && !hasDecide && !hasScore)
|
|
214
259
|
throw new ModelBackendRegistrationError(
|
|
215
|
-
`backend '${name}' must implement at least one of embed / generate / generateStream (as functions)`
|
|
260
|
+
`backend '${name}' must implement at least one of embed / generate / generateStream / decide / scoreChoices (as functions)`
|
|
216
261
|
);
|
|
262
|
+
if (maxScoredChoices !== undefined && !(Number.isSafeInteger(maxScoredChoices) && maxScoredChoices > 0))
|
|
263
|
+
throw new ModelBackendRegistrationError(`backend '${name}': maxScoredChoices must be a positive integer`);
|
|
217
264
|
const capabilities: ModelCapabilities = Object.freeze({
|
|
218
265
|
embed: hasEmbed,
|
|
219
266
|
// A stream-only backend gains generate() via the synthesis below.
|
|
@@ -221,11 +268,20 @@ export function defineBackend(spec: DefineBackendSpec): ModelBackend {
|
|
|
221
268
|
stream: hasStream,
|
|
222
269
|
tools,
|
|
223
270
|
adapters,
|
|
271
|
+
decide: hasDecide,
|
|
272
|
+
calibrated: hasDecide && calibrated,
|
|
273
|
+
scoreChoices: hasScore,
|
|
274
|
+
structuredOutput: (hasGenerate || hasStream) && structuredOutput,
|
|
275
|
+
noMatch: hasDecide && noMatch,
|
|
276
|
+
calibratedNoMatch: hasDecide && noMatch && calibratedNoMatch,
|
|
277
|
+
...(hasScore && maxScoredChoices !== undefined ? { maxScoredChoices } : {}),
|
|
224
278
|
});
|
|
225
279
|
const backend: ModelBackend = { name, capabilities: () => capabilities };
|
|
226
280
|
if (hasEmbed) backend.embed = embed;
|
|
227
281
|
if (hasGenerate) backend.generate = generate;
|
|
228
282
|
if (hasStream) backend.generateStream = generateStream;
|
|
283
|
+
if (hasDecide) backend.decide = decide;
|
|
284
|
+
if (hasScore) backend.scoreChoices = scoreChoices;
|
|
229
285
|
// Stream-only generative backend: synthesize generate() by draining the stream,
|
|
230
286
|
// so a plain models.generate() works without the backend implementing both.
|
|
231
287
|
if (hasStream && !hasGenerate) backend.generate = synthesizeGenerateFromStream(generateStream!);
|
|
@@ -261,8 +317,7 @@ function synthesizeGenerateFromStream(
|
|
|
261
317
|
|
|
262
318
|
/** Remove all registrations. Test-only hygiene. */
|
|
263
319
|
export function clearRegistry(): void {
|
|
264
|
-
|
|
265
|
-
generative.clear();
|
|
320
|
+
for (const map of Object.values(registries)) map.clear();
|
|
266
321
|
}
|
|
267
322
|
|
|
268
323
|
export class ModelBackendNotFoundError extends ServerError {
|
|
@@ -34,6 +34,7 @@ import { registerOllamaBackend, type OllamaBackendConfig } from '../../component
|
|
|
34
34
|
import { registerOpenAIBackend, type OpenAIBackendConfig } from '../../components/openai/index.ts';
|
|
35
35
|
import { registerAnthropicBackend, type AnthropicBackendConfig } from '../../components/anthropic/index.ts';
|
|
36
36
|
import { registerBedrockBackend, type BedrockBackendConfig } from '../../components/bedrock/index.ts';
|
|
37
|
+
import { setModelsConfigHash } from './decisionStore.ts';
|
|
37
38
|
import { isAbsolute, resolve as resolvePath } from 'node:path';
|
|
38
39
|
import { pathToFileURL } from 'node:url';
|
|
39
40
|
import { createRequire } from 'node:module';
|
|
@@ -48,7 +49,8 @@ import {
|
|
|
48
49
|
} from './backendRegistry.ts';
|
|
49
50
|
import { getSharedRootConfigWatcher, RootConfigWatcher } from '../../config/RootConfigWatcher.ts';
|
|
50
51
|
import { validateModelsBlock } from '../../validation/configValidator.ts';
|
|
51
|
-
import type
|
|
52
|
+
import { registerGenerativeDecisionBackend, type GenerativeDecisionConfig } from './generativeDecision.ts';
|
|
53
|
+
import type { ModelBackend, ModelKind } from './types.ts';
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Field names treated as credentials. When present in config as a literal
|
|
@@ -58,8 +60,6 @@ import type { ModelBackend } from './types.ts';
|
|
|
58
60
|
*/
|
|
59
61
|
const CREDENTIAL_FIELDS = new Set(['apiKey']);
|
|
60
62
|
|
|
61
|
-
type ModelKind = 'embedding' | 'generative';
|
|
62
|
-
|
|
63
63
|
interface ModelEntry {
|
|
64
64
|
backend?: string;
|
|
65
65
|
host?: string;
|
|
@@ -71,6 +71,13 @@ interface ModelEntry {
|
|
|
71
71
|
organization?: string;
|
|
72
72
|
// bedrock
|
|
73
73
|
region?: string;
|
|
74
|
+
// generative decision adapter
|
|
75
|
+
generative?: string;
|
|
76
|
+
samples?: number;
|
|
77
|
+
concurrency?: number;
|
|
78
|
+
temperature?: number;
|
|
79
|
+
scoring?: 'auto' | 'vote' | 'score';
|
|
80
|
+
requireStructuredOutput?: boolean;
|
|
74
81
|
/** Ordered fallback group: other logical names tried, in order, after this one (#1326). */
|
|
75
82
|
fallback?: string[];
|
|
76
83
|
}
|
|
@@ -78,6 +85,7 @@ interface ModelEntry {
|
|
|
78
85
|
interface ModelsConfig {
|
|
79
86
|
embedding?: Record<string, ModelEntry>;
|
|
80
87
|
generative?: Record<string, ModelEntry>;
|
|
88
|
+
decision?: Record<string, ModelEntry>;
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
interface RootConfig {
|
|
@@ -85,14 +93,33 @@ interface RootConfig {
|
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
type BackendRegisterFn = (args: { logicalName: string; kind: ModelKind; config: object }) => void | Promise<void>;
|
|
96
|
+
type ProviderKind = Exclude<ModelKind, 'decision'>;
|
|
88
97
|
|
|
98
|
+
// The provider casts hold because `builtinServesKind` refuses a `decision` entry before any factory runs.
|
|
89
99
|
const FACTORIES: Record<string, BackendRegisterFn> = {
|
|
90
|
-
ollama: (args) =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
ollama: (args) =>
|
|
101
|
+
registerOllamaBackend({ ...args, kind: args.kind as ProviderKind, config: args.config as OllamaBackendConfig }),
|
|
102
|
+
openai: (args) =>
|
|
103
|
+
registerOpenAIBackend({ ...args, kind: args.kind as ProviderKind, config: args.config as OpenAIBackendConfig }),
|
|
104
|
+
anthropic: (args) =>
|
|
105
|
+
registerAnthropicBackend({
|
|
106
|
+
...args,
|
|
107
|
+
kind: args.kind as ProviderKind,
|
|
108
|
+
config: args.config as AnthropicBackendConfig,
|
|
109
|
+
}),
|
|
110
|
+
bedrock: (args) =>
|
|
111
|
+
registerBedrockBackend({ ...args, kind: args.kind as ProviderKind, config: args.config as BedrockBackendConfig }),
|
|
112
|
+
generative: (args) => registerGenerativeDecisionBackend({ ...args, config: args.config as GenerativeDecisionConfig }),
|
|
94
113
|
};
|
|
95
114
|
|
|
115
|
+
// The provider factories treat every non-embedding kind as generative, so a `decision` entry naming
|
|
116
|
+
// one would silently register a backend with no `decide`; the adapter is the only built-in for that kind.
|
|
117
|
+
const DECISION_BUILTINS = new Set(['generative']);
|
|
118
|
+
|
|
119
|
+
function builtinServesKind(backend: string, kind: ModelKind): boolean {
|
|
120
|
+
return (kind === 'decision') === DECISION_BUILTINS.has(backend);
|
|
121
|
+
}
|
|
122
|
+
|
|
96
123
|
// What the config projection currently has installed, per kind+logicalName slot. `backend` is the
|
|
97
124
|
// exact instance this module put in the registry: reload swaps compare against it, so a
|
|
98
125
|
// programmatic `registerBackend` that overrode a config entry keeps its documented precedence, and
|
|
@@ -106,6 +133,7 @@ interface InstalledSlot {
|
|
|
106
133
|
entryJson: string;
|
|
107
134
|
/** The entry's fallback group as applied, so a retained backend keeps its routing. */
|
|
108
135
|
fallback?: string[];
|
|
136
|
+
configJson?: string;
|
|
109
137
|
/** Whether the entry's backend is a built-in. Module-backed entries are restart-managed: reload
|
|
110
138
|
* refuses to add, change, OR remove them, so a rename cannot half-apply as a bare removal. */
|
|
111
139
|
builtin?: boolean;
|
|
@@ -223,6 +251,7 @@ interface DesiredEntry {
|
|
|
223
251
|
logicalName: string;
|
|
224
252
|
entry: ModelEntry;
|
|
225
253
|
entryJson: string;
|
|
254
|
+
configJson?: string;
|
|
226
255
|
}
|
|
227
256
|
|
|
228
257
|
function collectKind(
|
|
@@ -291,6 +320,7 @@ function publishEntry(
|
|
|
291
320
|
const builtin = Boolean(FACTORIES[entry.backend as string]);
|
|
292
321
|
if (replaceIfCurrent(kind, logicalName, expected, backend)) {
|
|
293
322
|
installedSlots.set(key, {
|
|
323
|
+
configJson: desiredEntry.configJson,
|
|
294
324
|
kind,
|
|
295
325
|
logicalName,
|
|
296
326
|
backend,
|
|
@@ -303,6 +333,7 @@ function publishEntry(
|
|
|
303
333
|
// Record the ask with no installed instance, so unchanged reloads skip instead of
|
|
304
334
|
// re-losing this swap every apply; installed helpers stay recorded and removable.
|
|
305
335
|
installedSlots.set(key, {
|
|
336
|
+
configJson: desiredEntry.configJson,
|
|
306
337
|
kind,
|
|
307
338
|
logicalName,
|
|
308
339
|
entryJson,
|
|
@@ -339,6 +370,7 @@ async function applyModels(block: ModelsConfig | null | undefined, isBoot: boole
|
|
|
339
370
|
const presentKeys = new Set<string>();
|
|
340
371
|
collectKind('embedding', block?.embedding, desired, presentKeys);
|
|
341
372
|
collectKind('generative', block?.generative, desired, presentKeys);
|
|
373
|
+
collectKind('decision', block?.decision, desired, presentKeys);
|
|
342
374
|
|
|
343
375
|
const staged = new Map<string, { desiredEntry: DesiredEntry; backend: ModelBackend; extras: CapturedInstall[] }>();
|
|
344
376
|
const failedKeys = new Set<string>();
|
|
@@ -358,6 +390,13 @@ async function applyModels(block: ModelsConfig | null | undefined, isBoot: boole
|
|
|
358
390
|
// the incoming backend AND a currently-installed module-backed slot: a module→built-in rewrite
|
|
359
391
|
// is still a change of a restart-managed entry, so it must not live-replace the module (which
|
|
360
392
|
// would drop its helpers with none of the disposal a restart performs).
|
|
393
|
+
if (FACTORIES[entry.backend as string] && !builtinServesKind(entry.backend as string, kind)) {
|
|
394
|
+
harperLogger.error(
|
|
395
|
+
`models.${kind}.${logicalName}: backend '${entry.backend}' cannot serve ${kind} entries; skipping`
|
|
396
|
+
);
|
|
397
|
+
failedKeys.add(key);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
361
400
|
if (!isBoot && (!FACTORIES[entry.backend as string] || slot?.builtin === false)) {
|
|
362
401
|
harperLogger.warn(
|
|
363
402
|
`models.${kind}.${logicalName}: module-backed entries require a restart to add or change; ` +
|
|
@@ -378,6 +417,7 @@ async function applyModels(block: ModelsConfig | null | undefined, isBoot: boole
|
|
|
378
417
|
// (env var unset) pass through unchanged — backend's required-field
|
|
379
418
|
// validation catches them with a meaningful error.
|
|
380
419
|
const config = expandEnvVarsDeep(entry);
|
|
420
|
+
desiredEntry.configJson = JSON.stringify(config);
|
|
381
421
|
const { backend, extras } = await constructBackend(kind, logicalName, async () => {
|
|
382
422
|
const builtin = FACTORIES[entry.backend as string];
|
|
383
423
|
if (builtin) {
|
|
@@ -462,6 +502,16 @@ async function applyModels(block: ModelsConfig | null | undefined, isBoot: boole
|
|
|
462
502
|
setFallbackGroup(slot.kind, slot.logicalName, slot.fallback);
|
|
463
503
|
}
|
|
464
504
|
}
|
|
505
|
+
setModelsConfigHash(installedConfiguration());
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function installedConfiguration(): Record<string, unknown> | undefined {
|
|
509
|
+
const configuration: Record<string, unknown> = {};
|
|
510
|
+
for (const [key, slot] of installedSlots) {
|
|
511
|
+
if (slot.backend)
|
|
512
|
+
configuration[key] = JSON.parse(slot.configJson ?? JSON.stringify(expandEnvVarsDeep(JSON.parse(slot.entryJson))));
|
|
513
|
+
}
|
|
514
|
+
return Object.keys(configuration).length > 0 ? configuration : undefined;
|
|
465
515
|
}
|
|
466
516
|
|
|
467
517
|
// ── Hot reload wiring ─────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ClientError } from '../../utility/errors/hdbError.ts';
|
|
2
|
+
import { isAllowedValue } from './decision.ts';
|
|
3
|
+
import {
|
|
4
|
+
runWriteJobs,
|
|
5
|
+
sanitizedHookError,
|
|
6
|
+
sourceState,
|
|
7
|
+
writeHookApplies,
|
|
8
|
+
type WriteHook,
|
|
9
|
+
type WriteHookContext,
|
|
10
|
+
} from './embedHook.ts';
|
|
11
|
+
import type { DecideInput, DecideOpts, Decision, DecisionLeaf } from './types.ts';
|
|
12
|
+
|
|
13
|
+
export type DecideConfig = {
|
|
14
|
+
source: string;
|
|
15
|
+
model: string;
|
|
16
|
+
confidence?: string;
|
|
17
|
+
/** A nullable `String` attribute that receives the durable `Decision.id`; the decision is recorded only when this is named (#2852). */
|
|
18
|
+
decision?: string;
|
|
19
|
+
instructions?: string;
|
|
20
|
+
/** Resolved from the attribute type and the directive arguments, and validated, when the schema loads. */
|
|
21
|
+
schema: DecisionLeaf;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type DecideAttribute = {
|
|
25
|
+
name: string;
|
|
26
|
+
decide: DecideConfig;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `null` clears the attribute, its confidence and its decision id; `probability` is required when the
|
|
31
|
+
* directive names a confidence attribute. `id` must come from a `models.decide` call; an override for a
|
|
32
|
+
* directive with a `decision` attribute gets it from a `models.decide` call made with `persist: true`.
|
|
33
|
+
*/
|
|
34
|
+
export type DeciderResult = { value: unknown; probability?: number; id?: string };
|
|
35
|
+
export type Decider = (record: any, hook?: WriteHookContext) => Promise<DeciderResult | null | undefined>;
|
|
36
|
+
|
|
37
|
+
type DecideFn = (state: DecideInput, schema: DecisionLeaf, opts: DecideOpts) => Promise<Decision>;
|
|
38
|
+
|
|
39
|
+
// Lazy-imported so this module can be unit-tested without loading the transaction
|
|
40
|
+
// stack `Models.ts` pulls in. Overridable via `__setDecideFnForTest`.
|
|
41
|
+
let _decideFn: DecideFn | undefined;
|
|
42
|
+
function resolveDecideFn(): DecideFn {
|
|
43
|
+
if (_decideFn) return _decideFn;
|
|
44
|
+
const { Models } = require('#src/resources/models/Models'); // eslint-disable-line @typescript-eslint/no-var-requires
|
|
45
|
+
const models = new Models();
|
|
46
|
+
_decideFn = (state, schema, opts) => models.decide(state, schema, opts);
|
|
47
|
+
return _decideFn;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Test seam: override the decide function. Pass `undefined` to reset to `Models.decide`. */
|
|
51
|
+
export function __setDecideFnForTest(fn: DecideFn | undefined): void {
|
|
52
|
+
_decideFn = fn;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createDefaultDecider(config: DecideConfig): Decider {
|
|
56
|
+
const { source, model, instructions, schema } = config;
|
|
57
|
+
const persist = Boolean(config.decision);
|
|
58
|
+
return async (record: any, hook?: WriteHookContext): Promise<DeciderResult | null> => {
|
|
59
|
+
const sourceValue = record?.[source];
|
|
60
|
+
if (sourceValue == null) return null;
|
|
61
|
+
// An object source is program state the primitive serializes itself; anything else is text.
|
|
62
|
+
const state: DecideInput = typeof sourceValue === 'object' ? sourceValue : String(sourceValue);
|
|
63
|
+
const decision = await resolveDecideFn()(state, schema, { model, instructions, signal: hook?.signal, persist });
|
|
64
|
+
return { value: decision.value, probability: decision.probability, id: decision.id };
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildDecideBefore(
|
|
69
|
+
record: any,
|
|
70
|
+
context: any,
|
|
71
|
+
options: any,
|
|
72
|
+
decideAttributes: DecideAttribute[] | undefined,
|
|
73
|
+
deciders: Record<string, Decider>
|
|
74
|
+
): WriteHook | undefined {
|
|
75
|
+
if (!decideAttributes || decideAttributes.length === 0) return undefined;
|
|
76
|
+
const applies = writeHookApplies(record, context, options);
|
|
77
|
+
if (record && typeof record === 'object' && options?.isNotification !== true && context?.alreadyLogged !== true)
|
|
78
|
+
for (const attr of decideAttributes) {
|
|
79
|
+
const { decision, source } = attr.decide;
|
|
80
|
+
if (!decision || !(decision in record)) continue;
|
|
81
|
+
const state = applies ? sourceState(record, source) : 'absent';
|
|
82
|
+
// An outcome report trusts this id, so only a write that derives or clears it may carry it.
|
|
83
|
+
if (state !== 'value' && state !== 'null')
|
|
84
|
+
throw new ClientError(
|
|
85
|
+
`"${decision}" is written by @decide on "${attr.name}"; a write may carry it only with a value for "${source}"`,
|
|
86
|
+
400
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!applies) return undefined;
|
|
90
|
+
let present = false;
|
|
91
|
+
for (const attr of decideAttributes) if (sourceState(record, attr.decide?.source) !== 'absent') present = true;
|
|
92
|
+
if (!present) return undefined;
|
|
93
|
+
return (signal) =>
|
|
94
|
+
runWriteJobs(
|
|
95
|
+
decideAttributes.map((attr) => async (jobSignal) => {
|
|
96
|
+
const { source, confidence, decision, schema } = attr.decide;
|
|
97
|
+
const state = sourceState(record, source);
|
|
98
|
+
if (state === 'absent' || state === 'op') return;
|
|
99
|
+
const clear = () => {
|
|
100
|
+
record[attr.name] = null;
|
|
101
|
+
if (confidence) record[confidence] = null;
|
|
102
|
+
if (decision) record[decision] = null;
|
|
103
|
+
};
|
|
104
|
+
if (state === 'null') return clear();
|
|
105
|
+
const decider = deciders[attr.name];
|
|
106
|
+
// Committing the source without its pair would be indistinguishable from a decision later.
|
|
107
|
+
if (!decider) throw new Error(`No decider is registered for the @decide attribute "${attr.name}"`);
|
|
108
|
+
let result: DeciderResult | null | undefined;
|
|
109
|
+
try {
|
|
110
|
+
result = await decider(record, { signal: jobSignal });
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// An aborted job failed because a sibling did; only that failure is worth a log line.
|
|
113
|
+
throw sanitizedHookError('Decider', 'decision', attr.name, err, !jobSignal.aborted);
|
|
114
|
+
}
|
|
115
|
+
if (result == null) return clear();
|
|
116
|
+
// The facade validates `models.decide` output; an author override is checked here so the
|
|
117
|
+
// stored pair keeps the closed-set guarantee a query relies on.
|
|
118
|
+
if (!isAllowedValue(schema, result.value))
|
|
119
|
+
throw new Error(`Decider for attribute "${attr.name}" returned a value outside its @decide set`);
|
|
120
|
+
if (confidence) {
|
|
121
|
+
const probability = result.probability;
|
|
122
|
+
if (!(typeof probability === 'number' && probability >= 0 && probability <= 1))
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Decider for attribute "${attr.name}" must return a probability in [0, 1] for its confidence attribute "${confidence}"`
|
|
125
|
+
);
|
|
126
|
+
record[attr.name] = result.value;
|
|
127
|
+
record[confidence] = probability;
|
|
128
|
+
} else {
|
|
129
|
+
record[attr.name] = result.value;
|
|
130
|
+
}
|
|
131
|
+
if (decision) record[decision] = typeof result.id === 'string' && result.id !== '' ? result.id : null;
|
|
132
|
+
}),
|
|
133
|
+
signal
|
|
134
|
+
);
|
|
135
|
+
}
|