@try-works/dsh-model-as-tool 0.1.0 → 0.1.2
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/README.md +51 -0
- package/bin/dsh-tool-mode-mcp-stdio.mjs +80 -0
- package/catalog/desert-ant-models.json +77 -13
- package/lib/adapters/index.d.ts +19 -6
- package/lib/adapters/index.d.ts.map +1 -1
- package/lib/adapters/index.js +28 -13
- package/lib/adapters/tongue.d.ts +11 -3
- package/lib/adapters/tongue.d.ts.map +1 -1
- package/lib/adapters/tongue.js +12 -4
- package/lib/adapters/types.d.ts +11 -0
- package/lib/adapters/types.d.ts.map +1 -1
- package/lib/adapters/voz.d.ts +7 -1
- package/lib/adapters/voz.d.ts.map +1 -1
- package/lib/adapters/voz.js +16 -4
- package/lib/asset-acquisition.d.ts +137 -0
- package/lib/asset-acquisition.d.ts.map +1 -0
- package/lib/asset-acquisition.js +161 -0
- package/lib/authority.d.ts +153 -0
- package/lib/authority.d.ts.map +1 -0
- package/lib/authority.js +194 -0
- package/lib/bindings.d.ts +115 -0
- package/lib/bindings.d.ts.map +1 -0
- package/lib/bindings.js +239 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +57 -0
- package/lib/contract-payload.d.ts +47 -0
- package/lib/contract-payload.d.ts.map +1 -0
- package/lib/contract-payload.js +137 -0
- package/lib/deployment-coordinator.d.ts +213 -0
- package/lib/deployment-coordinator.d.ts.map +1 -0
- package/lib/deployment-coordinator.js +274 -0
- package/lib/harness-budgets.d.ts +158 -0
- package/lib/harness-budgets.d.ts.map +1 -0
- package/lib/harness-budgets.js +152 -0
- package/lib/index.d.ts +41 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +424 -16
- package/lib/licensing.d.ts +221 -0
- package/lib/licensing.d.ts.map +1 -0
- package/lib/licensing.js +188 -0
- package/lib/mcp/host-binding.d.ts +91 -0
- package/lib/mcp/host-binding.d.ts.map +1 -0
- package/lib/mcp/host-binding.js +133 -0
- package/lib/mcp/http.d.ts +130 -0
- package/lib/mcp/http.d.ts.map +1 -0
- package/lib/mcp/http.js +274 -0
- package/lib/mcp/protocol.d.ts +261 -0
- package/lib/mcp/protocol.d.ts.map +1 -0
- package/lib/mcp/protocol.js +387 -0
- package/lib/mcp/server.d.ts +115 -0
- package/lib/mcp/server.d.ts.map +1 -0
- package/lib/mcp/server.js +200 -0
- package/lib/mcp/stdio.d.ts +80 -0
- package/lib/mcp/stdio.d.ts.map +1 -0
- package/lib/mcp/stdio.js +176 -0
- package/lib/mcp/subscriptions.d.ts +135 -0
- package/lib/mcp/subscriptions.d.ts.map +1 -0
- package/lib/mcp/subscriptions.js +207 -0
- package/lib/provider-bridge.d.ts +69 -0
- package/lib/provider-bridge.d.ts.map +1 -0
- package/lib/provider-bridge.js +218 -0
- package/lib/publication.d.ts +160 -0
- package/lib/publication.d.ts.map +1 -0
- package/lib/publication.js +173 -0
- package/lib/receipt.d.ts +208 -0
- package/lib/receipt.d.ts.map +1 -0
- package/lib/receipt.js +150 -0
- package/lib/registrar.d.ts +13 -0
- package/lib/registrar.d.ts.map +1 -1
- package/lib/registrar.js +20 -3
- package/lib/resource-governance.d.ts +204 -0
- package/lib/resource-governance.d.ts.map +1 -0
- package/lib/resource-governance.js +213 -0
- package/lib/server-lifecycle.d.ts +175 -0
- package/lib/server-lifecycle.d.ts.map +1 -0
- package/lib/server-lifecycle.js +269 -0
- package/lib/types.d.ts +21 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +9 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation from a model's native result to a contract's payload.
|
|
3
|
+
*
|
|
4
|
+
* The spec puts this responsibility on adapters: they "translate between the
|
|
5
|
+
* capability's canonical representation and the model's native interface". It is
|
|
6
|
+
* a separate module from the invoke adapters because the two change for different
|
|
7
|
+
* reasons — an adapter changes when the vendor SDK does, a translator changes when
|
|
8
|
+
* the contract does — and conflating them is how a contract silently drifts to
|
|
9
|
+
* match whatever the SDK happened to return.
|
|
10
|
+
*
|
|
11
|
+
* The contract is the authority. When a translation cannot supply a field the
|
|
12
|
+
* contract requires, that is reported rather than faked; TOOL-01 is explicit that
|
|
13
|
+
* an incomplete clip must not be presented as fully transcribed.
|
|
14
|
+
*
|
|
15
|
+
* @module @try-works/dsh-model-as-tool/contract-payload
|
|
16
|
+
*/
|
|
17
|
+
import type { JsonValue } from './adapters/index.ts';
|
|
18
|
+
/** Raised when a native result cannot be expressed under its contract. */
|
|
19
|
+
export declare class PayloadTranslationError extends Error {
|
|
20
|
+
readonly name = "PayloadTranslationError";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Translate a contract's validated arguments into an adapter's native shape.
|
|
24
|
+
*
|
|
25
|
+
* This is the input half of the same seam as {@link toContractPayload}. An adapter
|
|
26
|
+
* speaks its SDK's vocabulary — `audio_path` — while the contract publishes the
|
|
27
|
+
* caller's vocabulary — `audio.path`. Without this translation the contract layer
|
|
28
|
+
* would be cosmetic: the model would advertise one shape and the implementation
|
|
29
|
+
* would read another, and every call would fail on a name that no longer exists.
|
|
30
|
+
*
|
|
31
|
+
* The contract is the authority here too, so the mapping is explicit and total
|
|
32
|
+
* rather than a spread that happens to overlap.
|
|
33
|
+
* @param contractId - the contract the call was validated against.
|
|
34
|
+
* @param contractArgs - the validated contract arguments.
|
|
35
|
+
* @returns the arguments the selected adapter expects.
|
|
36
|
+
*/
|
|
37
|
+
export declare function toNativeArgs(contractId: string, contractArgs: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Translate a native implementation result into the payload its contract declares.
|
|
40
|
+
* @param contractId - the contract the implementation was selected for.
|
|
41
|
+
* @param native - the adapter's native result.
|
|
42
|
+
* @param args - the validated arguments, for contract-conditional translation.
|
|
43
|
+
* @returns the contract payload.
|
|
44
|
+
* @throws PayloadTranslationError when the native result cannot satisfy the contract.
|
|
45
|
+
*/
|
|
46
|
+
export declare function toContractPayload(contractId: string, native: Record<string, JsonValue>, args?: Readonly<Record<string, unknown>>): Record<string, JsonValue>;
|
|
47
|
+
//# sourceMappingURL=contract-payload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-payload.d.ts","sourceRoot":"","sources":["../src/contract-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,0EAA0E;AAC1E,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,SAAkB,IAAI,6BAA4B;CACnD;AAyED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC9C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiBzB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EACjC,IAAI,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAM,GAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAW3B"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation from a model's native result to a contract's payload.
|
|
3
|
+
*
|
|
4
|
+
* The spec puts this responsibility on adapters: they "translate between the
|
|
5
|
+
* capability's canonical representation and the model's native interface". It is
|
|
6
|
+
* a separate module from the invoke adapters because the two change for different
|
|
7
|
+
* reasons — an adapter changes when the vendor SDK does, a translator changes when
|
|
8
|
+
* the contract does — and conflating them is how a contract silently drifts to
|
|
9
|
+
* match whatever the SDK happened to return.
|
|
10
|
+
*
|
|
11
|
+
* The contract is the authority. When a translation cannot supply a field the
|
|
12
|
+
* contract requires, that is reported rather than faked; TOOL-01 is explicit that
|
|
13
|
+
* an incomplete clip must not be presented as fully transcribed.
|
|
14
|
+
*
|
|
15
|
+
* @module @try-works/dsh-model-as-tool/contract-payload
|
|
16
|
+
*/
|
|
17
|
+
/** Raised when a native result cannot be expressed under its contract. */
|
|
18
|
+
export class PayloadTranslationError extends Error {
|
|
19
|
+
name = 'PayloadTranslationError';
|
|
20
|
+
}
|
|
21
|
+
/** Best-effort read of a numeric field. */
|
|
22
|
+
function numberOf(value) {
|
|
23
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Translate a native language-detection result into the `text.language_detect`
|
|
27
|
+
* payload.
|
|
28
|
+
*
|
|
29
|
+
* The only rename is the vendor's `isTooCloseToCall` to the contract's
|
|
30
|
+
* `tooCloseToCall`; every other field already matches, which is what makes this a
|
|
31
|
+
* translation rather than a reshape.
|
|
32
|
+
* @param native - the adapter's result.
|
|
33
|
+
* @returns the contract payload, without the `meta` envelope the host stamps.
|
|
34
|
+
*/
|
|
35
|
+
function languageDetectPayload(native) {
|
|
36
|
+
const candidates = Array.isArray(native.candidates) ? native.candidates : [];
|
|
37
|
+
return {
|
|
38
|
+
// A model that found no language signal returns null rather than a guess.
|
|
39
|
+
language: typeof native.language === 'string' ? native.language : null,
|
|
40
|
+
reliability: typeof native.reliability === 'string' ? native.reliability : 'unknown',
|
|
41
|
+
tooCloseToCall: native.isTooCloseToCall === true,
|
|
42
|
+
candidates: candidates,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Translate a native transcription result into the `stt.transcribe` payload.
|
|
47
|
+
*
|
|
48
|
+
* `coverage` is the interesting field. Voz streams the whole file and fails loudly
|
|
49
|
+
* on a short decode, so a returned transcript covers all the audio it read;
|
|
50
|
+
* `transcribedSeconds` is taken from the last word's end time, which is a lower
|
|
51
|
+
* bound on the audio actually processed. When no words are reported, coverage is
|
|
52
|
+
* reported as zero seconds with `complete: true` only because the engine ran to
|
|
53
|
+
* completion — an empty transcript is a valid result (TOOL-01), not a failure.
|
|
54
|
+
* @param native - the adapter's result.
|
|
55
|
+
* @param requestedWords - whether the caller asked for word timings.
|
|
56
|
+
* @returns the contract payload, without the `meta` envelope the host stamps.
|
|
57
|
+
*/
|
|
58
|
+
function transcribePayload(native, requestedWords) {
|
|
59
|
+
const words = Array.isArray(native.words) ? native.words : null;
|
|
60
|
+
if (requestedWords && words === null) {
|
|
61
|
+
// TOOL-01: an unsupported timestamp request is an error, never a silent
|
|
62
|
+
// omission the caller might read as "there were no word timings".
|
|
63
|
+
throw new PayloadTranslationError('word timestamps were requested but the implementation returned none; '
|
|
64
|
+
+ 'this is reported as an error rather than an empty list');
|
|
65
|
+
}
|
|
66
|
+
let transcribedSeconds = 0;
|
|
67
|
+
if (words !== null && words.length > 0) {
|
|
68
|
+
const last = words[words.length - 1];
|
|
69
|
+
transcribedSeconds = numberOf(last.end) ?? 0;
|
|
70
|
+
}
|
|
71
|
+
const truncated = native.wordsTruncated === true;
|
|
72
|
+
return {
|
|
73
|
+
text: typeof native.text === 'string' ? native.text : '',
|
|
74
|
+
coverage: {
|
|
75
|
+
complete: !truncated,
|
|
76
|
+
transcribedSeconds,
|
|
77
|
+
totalSeconds: null,
|
|
78
|
+
reason: truncated ? 'word list truncated for transport; the transcript text is complete' : null,
|
|
79
|
+
},
|
|
80
|
+
words: words === null ? null : words,
|
|
81
|
+
language: typeof native.language === 'string' ? native.language : null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Translate a contract's validated arguments into an adapter's native shape.
|
|
86
|
+
*
|
|
87
|
+
* This is the input half of the same seam as {@link toContractPayload}. An adapter
|
|
88
|
+
* speaks its SDK's vocabulary — `audio_path` — while the contract publishes the
|
|
89
|
+
* caller's vocabulary — `audio.path`. Without this translation the contract layer
|
|
90
|
+
* would be cosmetic: the model would advertise one shape and the implementation
|
|
91
|
+
* would read another, and every call would fail on a name that no longer exists.
|
|
92
|
+
*
|
|
93
|
+
* The contract is the authority here too, so the mapping is explicit and total
|
|
94
|
+
* rather than a spread that happens to overlap.
|
|
95
|
+
* @param contractId - the contract the call was validated against.
|
|
96
|
+
* @param contractArgs - the validated contract arguments.
|
|
97
|
+
* @returns the arguments the selected adapter expects.
|
|
98
|
+
*/
|
|
99
|
+
export function toNativeArgs(contractId, contractArgs) {
|
|
100
|
+
switch (contractId) {
|
|
101
|
+
case 'stt.transcribe': {
|
|
102
|
+
const audio = contractArgs.audio;
|
|
103
|
+
return {
|
|
104
|
+
audio_path: audio?.path,
|
|
105
|
+
// The contract defaults `timestamps` to none, while the adapter defaults
|
|
106
|
+
// its own flag to true. Mapping explicitly keeps the contract's default
|
|
107
|
+
// authoritative instead of letting the adapter's leak through.
|
|
108
|
+
include_words: contractArgs.timestamps === 'word',
|
|
109
|
+
...contractArgs.language === undefined ? {} : { language: contractArgs.language },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
// `text.language_detect` already shares its argument names with the adapter.
|
|
114
|
+
return { ...contractArgs };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Translate a native implementation result into the payload its contract declares.
|
|
119
|
+
* @param contractId - the contract the implementation was selected for.
|
|
120
|
+
* @param native - the adapter's native result.
|
|
121
|
+
* @param args - the validated arguments, for contract-conditional translation.
|
|
122
|
+
* @returns the contract payload.
|
|
123
|
+
* @throws PayloadTranslationError when the native result cannot satisfy the contract.
|
|
124
|
+
*/
|
|
125
|
+
export function toContractPayload(contractId, native, args = {}) {
|
|
126
|
+
switch (contractId) {
|
|
127
|
+
case 'text.language_detect':
|
|
128
|
+
return languageDetectPayload(native);
|
|
129
|
+
case 'stt.transcribe':
|
|
130
|
+
return transcribePayload(native, args.timestamps === 'word');
|
|
131
|
+
default:
|
|
132
|
+
// A contract with no translator returns the native payload unchanged, which
|
|
133
|
+
// the output schema then accepts or rejects on its own terms.
|
|
134
|
+
return native;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=contract-payload.js.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment coordination: preparation, single-flight loading and reference counting
|
|
3
|
+
* (RES-02, RES-03, RES-05).
|
|
4
|
+
*
|
|
5
|
+
* This is the module that makes readiness states PRODUCED rather than assigned. Before
|
|
6
|
+
* it, `prepared-cold` and `ready` were values a test could write; here they are values
|
|
7
|
+
* this coordinator derives from what actually happened.
|
|
8
|
+
*
|
|
9
|
+
* Three rules shape the design:
|
|
10
|
+
*
|
|
11
|
+
* - **One operation per EXACT deployment** (RES-05). The key that identifies a
|
|
12
|
+
* deployment includes the model revision, engine version, quantization and
|
|
13
|
+
* preprocessing recipe, not just a model name — two builds of "the same model" that
|
|
14
|
+
* differ in quantization are different deployments and loading one must not satisfy
|
|
15
|
+
* a request for the other.
|
|
16
|
+
* - **Concurrent callers JOIN** (RES-05). A second caller during a load awaits the
|
|
17
|
+
* same promise rather than starting a second load, which is the difference between
|
|
18
|
+
* sharing 349 MB and allocating it twice.
|
|
19
|
+
* - **A failed or interrupted preparation cannot become ready** (RES-03). Failure is
|
|
20
|
+
* recorded as failure, and a later caller retries rather than inheriting a
|
|
21
|
+
* half-prepared deployment that reports success.
|
|
22
|
+
*
|
|
23
|
+
* @module @try-works/dsh-model-as-tool/deployment-coordinator
|
|
24
|
+
*/
|
|
25
|
+
import type { ModelReadiness } from '@try-works/tool-mode-extension-sdk';
|
|
26
|
+
/**
|
|
27
|
+
* The identity of an EXACT provider deployment (RES-02, RES-05).
|
|
28
|
+
*
|
|
29
|
+
* Every field participates in the deployment key. Pinning only the model name would
|
|
30
|
+
* let a caller be served by a differently-quantized build than the one it requested.
|
|
31
|
+
*/
|
|
32
|
+
export interface DeploymentIdentity {
|
|
33
|
+
/** The extension package that provides it. */
|
|
34
|
+
readonly extensionId: string;
|
|
35
|
+
/** The model inside that package. */
|
|
36
|
+
readonly modelId: string;
|
|
37
|
+
/** The exact model revision, not a branch or a floating tag. */
|
|
38
|
+
readonly modelRevision: string;
|
|
39
|
+
/** The engine or SDK version the assets were converted for. */
|
|
40
|
+
readonly engineVersion: string;
|
|
41
|
+
/** The quantization, which changes outputs and is therefore part of identity. */
|
|
42
|
+
readonly quantization: string;
|
|
43
|
+
/** The tokenizer identity. */
|
|
44
|
+
readonly tokenizer: string;
|
|
45
|
+
/** The preprocessing recipe version. */
|
|
46
|
+
readonly preprocessingRecipe: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The stable key for an exact deployment (RES-05).
|
|
50
|
+
*
|
|
51
|
+
* A delimiter that cannot appear in a component, so two different field combinations
|
|
52
|
+
* cannot collide into one key.
|
|
53
|
+
* @param identity - the deployment identity.
|
|
54
|
+
* @returns the key.
|
|
55
|
+
*/
|
|
56
|
+
export declare function deploymentKey(identity: DeploymentIdentity): string;
|
|
57
|
+
/** One asset a preparation must obtain (RES-02, RES-03). */
|
|
58
|
+
export interface PlannedAsset {
|
|
59
|
+
/** The asset id from the lock file. */
|
|
60
|
+
readonly assetId: string;
|
|
61
|
+
/** The download size in bytes, for the pre-approval disclosure. */
|
|
62
|
+
readonly bytes: number;
|
|
63
|
+
/** The pinned source. */
|
|
64
|
+
readonly source: string;
|
|
65
|
+
/** The expected content digest. */
|
|
66
|
+
readonly digest: string;
|
|
67
|
+
/**
|
|
68
|
+
* Whether this asset is an EXECUTABLE.
|
|
69
|
+
*
|
|
70
|
+
* RES-02: prebuilt engine downloads count as executable installation, so they
|
|
71
|
+
* require the separate executable-preparation approval rather than riding along
|
|
72
|
+
* with a model download.
|
|
73
|
+
*/
|
|
74
|
+
readonly executable: boolean;
|
|
75
|
+
}
|
|
76
|
+
/** What a preparation would do, shown before approval (RES-02). */
|
|
77
|
+
export interface PreparationPlan {
|
|
78
|
+
readonly identity: DeploymentIdentity;
|
|
79
|
+
readonly assets: readonly PlannedAsset[];
|
|
80
|
+
/** Total download size, so an operator approves a number rather than a hope. */
|
|
81
|
+
readonly totalBytes: number;
|
|
82
|
+
/** Total bytes retained after installation. */
|
|
83
|
+
readonly storageBytes: number;
|
|
84
|
+
/** Whether any asset is executable, which changes which approval is needed. */
|
|
85
|
+
readonly requiresExecutableApproval: boolean;
|
|
86
|
+
/** The pinned license terms, surfaced before the download rather than after. */
|
|
87
|
+
readonly license: {
|
|
88
|
+
readonly name: string;
|
|
89
|
+
readonly url: string;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Build the pre-approval disclosure (RES-02).
|
|
94
|
+
*
|
|
95
|
+
* An operator approving a preparation is approving a specific download of a specific
|
|
96
|
+
* size under specific terms, so all three are computed here rather than discovered
|
|
97
|
+
* during the install.
|
|
98
|
+
* @param identity - the exact deployment.
|
|
99
|
+
* @param assets - the assets the lock resolves to.
|
|
100
|
+
* @param license - the pinned license.
|
|
101
|
+
* @returns the plan.
|
|
102
|
+
*/
|
|
103
|
+
export declare function planPreparation(identity: DeploymentIdentity, assets: readonly PlannedAsset[], license: {
|
|
104
|
+
readonly name: string;
|
|
105
|
+
readonly url: string;
|
|
106
|
+
}): PreparationPlan;
|
|
107
|
+
/** A preparation that has not yet completed. */
|
|
108
|
+
export interface DeploymentRecord {
|
|
109
|
+
/** The exact deployment key. */
|
|
110
|
+
readonly key: string;
|
|
111
|
+
/** The identity it belongs to. */
|
|
112
|
+
readonly identity: DeploymentIdentity;
|
|
113
|
+
/** The current state, using the RES-04 vocabulary. */
|
|
114
|
+
readiness: ModelReadiness;
|
|
115
|
+
/** How many callers currently hold a reference. */
|
|
116
|
+
activeUsers: number;
|
|
117
|
+
/** When the current load began, if one is in progress. */
|
|
118
|
+
loadingSince: number | null;
|
|
119
|
+
/** Why it failed, when it did. */
|
|
120
|
+
failureReason: string | null;
|
|
121
|
+
}
|
|
122
|
+
/** What a caller receives when it acquires a deployment. */
|
|
123
|
+
export interface DeploymentLease {
|
|
124
|
+
/** The deployment key. */
|
|
125
|
+
readonly key: string;
|
|
126
|
+
/** The state at acquisition. */
|
|
127
|
+
readonly readiness: ModelReadiness;
|
|
128
|
+
/** Whether the caller waited for a load to finish. */
|
|
129
|
+
readonly joined: boolean;
|
|
130
|
+
/** Release this caller's reference. Idempotent. */
|
|
131
|
+
release(): void;
|
|
132
|
+
}
|
|
133
|
+
/** Raised when a deployment cannot be prepared. */
|
|
134
|
+
export declare class DeploymentError extends Error {
|
|
135
|
+
readonly name = "DeploymentError";
|
|
136
|
+
readonly key: string;
|
|
137
|
+
constructor(key: string, message: string);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Coordinates preparation, loading and reference counting per exact deployment.
|
|
141
|
+
*
|
|
142
|
+
* Not thread-safe in the language sense — Node is single-threaded — but
|
|
143
|
+
* single-flight with respect to `await`: two callers that reach `acquire` before the
|
|
144
|
+
* load settles both await ONE promise.
|
|
145
|
+
*/
|
|
146
|
+
export declare class DeploymentCoordinator {
|
|
147
|
+
#private;
|
|
148
|
+
/** Every tracked deployment, for diagnostics. */
|
|
149
|
+
get deployments(): readonly DeploymentRecord[];
|
|
150
|
+
/** One deployment's record. */
|
|
151
|
+
record(key: string): DeploymentRecord | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* The state to report for a deployment that has not been touched yet.
|
|
154
|
+
* @param identity - the exact deployment.
|
|
155
|
+
* @returns the readiness.
|
|
156
|
+
*/
|
|
157
|
+
readinessOf(identity: DeploymentIdentity): ModelReadiness;
|
|
158
|
+
/**
|
|
159
|
+
* Prepare and load a deployment, joining an in-flight load if there is one.
|
|
160
|
+
*
|
|
161
|
+
* RES-05: "One preparation/load operation per exact provider deployment; concurrent
|
|
162
|
+
* callers join or queue."
|
|
163
|
+
* @param identity - the exact deployment.
|
|
164
|
+
* @param load - performs the load. Called AT MOST ONCE per concurrent group.
|
|
165
|
+
* @param options - cancellation for THIS caller.
|
|
166
|
+
* @returns the lease.
|
|
167
|
+
* @throws DeploymentError when the load fails.
|
|
168
|
+
*/
|
|
169
|
+
acquire(identity: DeploymentIdentity, load: () => Promise<void>, options?: {
|
|
170
|
+
readonly signal?: AbortSignal;
|
|
171
|
+
}): Promise<DeploymentLease>;
|
|
172
|
+
/**
|
|
173
|
+
* Unload a deployment, but only when it is IDLE (RES-05).
|
|
174
|
+
*
|
|
175
|
+
* "Unload only idle resources or an explicitly revoked deployment after coordinated
|
|
176
|
+
* cleanup." Unloading a busy deployment would discard another caller's work.
|
|
177
|
+
* @param key - the deployment key.
|
|
178
|
+
* @param force - set only for an explicitly revoked deployment.
|
|
179
|
+
* @returns whether it was unloaded, and why not when it was not.
|
|
180
|
+
*/
|
|
181
|
+
unload(key: string, force?: boolean): {
|
|
182
|
+
readonly unloaded: boolean;
|
|
183
|
+
readonly reason?: string;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Revoke a deployment, discarding in-flight work after coordinated cleanup (RES-05).
|
|
187
|
+
* @param key - the deployment key.
|
|
188
|
+
* @returns whether it was revoked.
|
|
189
|
+
*/
|
|
190
|
+
revoke(key: string): boolean;
|
|
191
|
+
}
|
|
192
|
+
/** An asset ownership record (RES-03). */
|
|
193
|
+
export interface AssetOwnership {
|
|
194
|
+
/** The content digest, which is what makes sharing safe. */
|
|
195
|
+
readonly digest: string;
|
|
196
|
+
/** The holders of this asset, by owner id. */
|
|
197
|
+
readonly owners: readonly string[];
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Decide which assets may be deleted (RES-03).
|
|
201
|
+
*
|
|
202
|
+
* RES-03: "Package uninstall and asset purge are separate operations; delete only
|
|
203
|
+
* unreferenced host-owned assets." So removing an owner does not delete anything, and
|
|
204
|
+
* an asset with another owner is never deleted.
|
|
205
|
+
* @param ownership - the current records.
|
|
206
|
+
* @param removedOwner - the owner being uninstalled.
|
|
207
|
+
* @returns the digests that are now unreferenced, and those still held.
|
|
208
|
+
*/
|
|
209
|
+
export declare function purgeCandidates(ownership: readonly AssetOwnership[], removedOwner: string): {
|
|
210
|
+
readonly deletable: readonly string[];
|
|
211
|
+
readonly retained: readonly string[];
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=deployment-coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-coordinator.d.ts","sourceRoot":"","sources":["../src/deployment-coordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment coordination: preparation, single-flight loading and reference counting
|
|
3
|
+
* (RES-02, RES-03, RES-05).
|
|
4
|
+
*
|
|
5
|
+
* This is the module that makes readiness states PRODUCED rather than assigned. Before
|
|
6
|
+
* it, `prepared-cold` and `ready` were values a test could write; here they are values
|
|
7
|
+
* this coordinator derives from what actually happened.
|
|
8
|
+
*
|
|
9
|
+
* Three rules shape the design:
|
|
10
|
+
*
|
|
11
|
+
* - **One operation per EXACT deployment** (RES-05). The key that identifies a
|
|
12
|
+
* deployment includes the model revision, engine version, quantization and
|
|
13
|
+
* preprocessing recipe, not just a model name — two builds of "the same model" that
|
|
14
|
+
* differ in quantization are different deployments and loading one must not satisfy
|
|
15
|
+
* a request for the other.
|
|
16
|
+
* - **Concurrent callers JOIN** (RES-05). A second caller during a load awaits the
|
|
17
|
+
* same promise rather than starting a second load, which is the difference between
|
|
18
|
+
* sharing 349 MB and allocating it twice.
|
|
19
|
+
* - **A failed or interrupted preparation cannot become ready** (RES-03). Failure is
|
|
20
|
+
* recorded as failure, and a later caller retries rather than inheriting a
|
|
21
|
+
* half-prepared deployment that reports success.
|
|
22
|
+
*
|
|
23
|
+
* @module @try-works/dsh-model-as-tool/deployment-coordinator
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The stable key for an exact deployment (RES-05).
|
|
27
|
+
*
|
|
28
|
+
* A delimiter that cannot appear in a component, so two different field combinations
|
|
29
|
+
* cannot collide into one key.
|
|
30
|
+
* @param identity - the deployment identity.
|
|
31
|
+
* @returns the key.
|
|
32
|
+
*/
|
|
33
|
+
export function deploymentKey(identity) {
|
|
34
|
+
return [
|
|
35
|
+
identity.extensionId,
|
|
36
|
+
identity.modelId,
|
|
37
|
+
identity.modelRevision,
|
|
38
|
+
identity.engineVersion,
|
|
39
|
+
identity.quantization,
|
|
40
|
+
identity.tokenizer,
|
|
41
|
+
identity.preprocessingRecipe,
|
|
42
|
+
// A unit separator is used rather than a printable character because a model id can
|
|
43
|
+
// legitimately contain most printable ones.
|
|
44
|
+
].join('\u001f');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the pre-approval disclosure (RES-02).
|
|
48
|
+
*
|
|
49
|
+
* An operator approving a preparation is approving a specific download of a specific
|
|
50
|
+
* size under specific terms, so all three are computed here rather than discovered
|
|
51
|
+
* during the install.
|
|
52
|
+
* @param identity - the exact deployment.
|
|
53
|
+
* @param assets - the assets the lock resolves to.
|
|
54
|
+
* @param license - the pinned license.
|
|
55
|
+
* @returns the plan.
|
|
56
|
+
*/
|
|
57
|
+
export function planPreparation(identity, assets, license) {
|
|
58
|
+
const totalBytes = assets.reduce((sum, asset) => sum + asset.bytes, 0);
|
|
59
|
+
return {
|
|
60
|
+
identity,
|
|
61
|
+
assets,
|
|
62
|
+
totalBytes,
|
|
63
|
+
// Storage is stated separately from download because an operator approving
|
|
64
|
+
// bandwidth is not thereby approving disk.
|
|
65
|
+
storageBytes: totalBytes,
|
|
66
|
+
requiresExecutableApproval: assets.some(asset => asset.executable),
|
|
67
|
+
license,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Raised when a deployment cannot be prepared. */
|
|
71
|
+
export class DeploymentError extends Error {
|
|
72
|
+
name = 'DeploymentError';
|
|
73
|
+
key;
|
|
74
|
+
constructor(key, message) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.key = key;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Coordinates preparation, loading and reference counting per exact deployment.
|
|
81
|
+
*
|
|
82
|
+
* Not thread-safe in the language sense — Node is single-threaded — but
|
|
83
|
+
* single-flight with respect to `await`: two callers that reach `acquire` before the
|
|
84
|
+
* load settles both await ONE promise.
|
|
85
|
+
*/
|
|
86
|
+
export class DeploymentCoordinator {
|
|
87
|
+
#records = new Map();
|
|
88
|
+
/** The in-flight load per key, so concurrent callers join rather than duplicate. */
|
|
89
|
+
#loading = new Map();
|
|
90
|
+
/** Per-caller release handlers, so releasing is idempotent. */
|
|
91
|
+
#leases = new WeakSet();
|
|
92
|
+
/** Every tracked deployment, for diagnostics. */
|
|
93
|
+
get deployments() {
|
|
94
|
+
return [...this.#records.values()];
|
|
95
|
+
}
|
|
96
|
+
/** One deployment's record. */
|
|
97
|
+
record(key) {
|
|
98
|
+
return this.#records.get(key);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The state to report for a deployment that has not been touched yet.
|
|
102
|
+
* @param identity - the exact deployment.
|
|
103
|
+
* @returns the readiness.
|
|
104
|
+
*/
|
|
105
|
+
readinessOf(identity) {
|
|
106
|
+
return this.#records.get(deploymentKey(identity))?.readiness ?? 'needs-preparation';
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Prepare and load a deployment, joining an in-flight load if there is one.
|
|
110
|
+
*
|
|
111
|
+
* RES-05: "One preparation/load operation per exact provider deployment; concurrent
|
|
112
|
+
* callers join or queue."
|
|
113
|
+
* @param identity - the exact deployment.
|
|
114
|
+
* @param load - performs the load. Called AT MOST ONCE per concurrent group.
|
|
115
|
+
* @param options - cancellation for THIS caller.
|
|
116
|
+
* @returns the lease.
|
|
117
|
+
* @throws DeploymentError when the load fails.
|
|
118
|
+
*/
|
|
119
|
+
async acquire(identity, load, options = {}) {
|
|
120
|
+
const key = deploymentKey(identity);
|
|
121
|
+
let record = this.#records.get(key);
|
|
122
|
+
if (record === undefined) {
|
|
123
|
+
record = {
|
|
124
|
+
key,
|
|
125
|
+
identity,
|
|
126
|
+
readiness: 'needs-preparation',
|
|
127
|
+
activeUsers: 0,
|
|
128
|
+
loadingSince: null,
|
|
129
|
+
failureReason: null,
|
|
130
|
+
};
|
|
131
|
+
this.#records.set(key, record);
|
|
132
|
+
}
|
|
133
|
+
// A previous failure must not be inherited as a success: RES-03 says a failed or
|
|
134
|
+
// interrupted preparation cannot become ready, so the next caller RETRIES.
|
|
135
|
+
if (record.readiness === 'failed') {
|
|
136
|
+
record.readiness = 'needs-preparation';
|
|
137
|
+
record.failureReason = null;
|
|
138
|
+
}
|
|
139
|
+
let joined = false;
|
|
140
|
+
if (record.readiness === 'ready') {
|
|
141
|
+
// Already loaded: nothing to join.
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
let pending = this.#loading.get(key);
|
|
145
|
+
if (pending === undefined) {
|
|
146
|
+
pending = this.#startLoad(record, load);
|
|
147
|
+
this.#loading.set(key, pending);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
joined = true;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
await pending;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// The caller is told WHY, and the record keeps the failure so a later caller
|
|
157
|
+
// retries rather than seeing a stale success.
|
|
158
|
+
throw new DeploymentError(key, `${identity.extensionId}/${identity.modelId} could not be prepared: `
|
|
159
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (options.signal?.aborted === true) {
|
|
163
|
+
// Cancellation is PER INVOCATION (RES-05): this caller gives up, and the
|
|
164
|
+
// deployment it just waited for is left loaded and available to whoever else
|
|
165
|
+
// wants it. Killing it here would destroy another caller's legitimate work.
|
|
166
|
+
throw new DeploymentError(key, 'the caller cancelled before the deployment could be used');
|
|
167
|
+
}
|
|
168
|
+
record.activeUsers += 1;
|
|
169
|
+
const lease = {
|
|
170
|
+
key,
|
|
171
|
+
readiness: record.readiness,
|
|
172
|
+
joined,
|
|
173
|
+
release: () => this.#release(key, lease),
|
|
174
|
+
};
|
|
175
|
+
return lease;
|
|
176
|
+
}
|
|
177
|
+
/** Start the single load for a key. */
|
|
178
|
+
#startLoad(record, load) {
|
|
179
|
+
record.readiness = 'loading';
|
|
180
|
+
record.loadingSince = Date.now();
|
|
181
|
+
const pending = (async () => {
|
|
182
|
+
try {
|
|
183
|
+
await load();
|
|
184
|
+
// A load that threw is not ready; a load that returned is.
|
|
185
|
+
record.readiness = 'ready';
|
|
186
|
+
record.failureReason = null;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
// RES-03: a failed or interrupted preparation CANNOT become ready.
|
|
190
|
+
record.readiness = 'failed';
|
|
191
|
+
record.failureReason = error instanceof Error ? error.message : String(error);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
record.loadingSince = null;
|
|
196
|
+
this.#loading.delete(record.key);
|
|
197
|
+
}
|
|
198
|
+
})();
|
|
199
|
+
return pending;
|
|
200
|
+
}
|
|
201
|
+
/** Decrement a caller's reference. */
|
|
202
|
+
#release(key, lease) {
|
|
203
|
+
if (this.#leases.has(lease))
|
|
204
|
+
return;
|
|
205
|
+
this.#leases.add(lease);
|
|
206
|
+
const record = this.#records.get(key);
|
|
207
|
+
if (record === undefined)
|
|
208
|
+
return;
|
|
209
|
+
record.activeUsers = Math.max(0, record.activeUsers - 1);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Unload a deployment, but only when it is IDLE (RES-05).
|
|
213
|
+
*
|
|
214
|
+
* "Unload only idle resources or an explicitly revoked deployment after coordinated
|
|
215
|
+
* cleanup." Unloading a busy deployment would discard another caller's work.
|
|
216
|
+
* @param key - the deployment key.
|
|
217
|
+
* @param force - set only for an explicitly revoked deployment.
|
|
218
|
+
* @returns whether it was unloaded, and why not when it was not.
|
|
219
|
+
*/
|
|
220
|
+
unload(key, force = false) {
|
|
221
|
+
const record = this.#records.get(key);
|
|
222
|
+
if (record === undefined)
|
|
223
|
+
return { unloaded: false, reason: 'no such deployment' };
|
|
224
|
+
if (this.#loading.has(key)) {
|
|
225
|
+
return { unloaded: false, reason: 'a load is in progress' };
|
|
226
|
+
}
|
|
227
|
+
if (record.activeUsers > 0 && !force) {
|
|
228
|
+
// The count is the whole point: another caller is using this right now.
|
|
229
|
+
return { unloaded: false, reason: `${record.activeUsers} caller(s) are still using it` };
|
|
230
|
+
}
|
|
231
|
+
record.readiness = 'prepared-cold';
|
|
232
|
+
return { unloaded: true };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Revoke a deployment, discarding in-flight work after coordinated cleanup (RES-05).
|
|
236
|
+
* @param key - the deployment key.
|
|
237
|
+
* @returns whether it was revoked.
|
|
238
|
+
*/
|
|
239
|
+
revoke(key) {
|
|
240
|
+
const record = this.#records.get(key);
|
|
241
|
+
if (record === undefined)
|
|
242
|
+
return false;
|
|
243
|
+
// Revocation is the one case that may discard a busy deployment, and it is
|
|
244
|
+
// explicit: the operator asked for it rather than a policy inferring it.
|
|
245
|
+
record.activeUsers = 0;
|
|
246
|
+
record.readiness = 'prepared-cold';
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Decide which assets may be deleted (RES-03).
|
|
252
|
+
*
|
|
253
|
+
* RES-03: "Package uninstall and asset purge are separate operations; delete only
|
|
254
|
+
* unreferenced host-owned assets." So removing an owner does not delete anything, and
|
|
255
|
+
* an asset with another owner is never deleted.
|
|
256
|
+
* @param ownership - the current records.
|
|
257
|
+
* @param removedOwner - the owner being uninstalled.
|
|
258
|
+
* @returns the digests that are now unreferenced, and those still held.
|
|
259
|
+
*/
|
|
260
|
+
export function purgeCandidates(ownership, removedOwner) {
|
|
261
|
+
const deletable = [];
|
|
262
|
+
const retained = [];
|
|
263
|
+
for (const asset of ownership) {
|
|
264
|
+
const remaining = asset.owners.filter(owner => owner !== removedOwner);
|
|
265
|
+
// An asset with a remaining owner is RETAINED, because deleting it would break a
|
|
266
|
+
// deployment nobody uninstalled.
|
|
267
|
+
if (remaining.length === 0)
|
|
268
|
+
deletable.push(asset.digest);
|
|
269
|
+
else
|
|
270
|
+
retained.push(asset.digest);
|
|
271
|
+
}
|
|
272
|
+
return { deletable, retained };
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=deployment-coordinator.js.map
|