@try-works/dsh-model-as-tool 0.1.1 → 0.1.3
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/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +85 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +260 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +23 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.js +24 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +104 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +55 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +81 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +163 -0
- package/node_modules/@try-works/tool-mode-contracts/package.json +35 -0
- package/package.json +17 -5
package/lib/index.js
CHANGED
|
@@ -18,18 +18,41 @@ import { loadCatalog } from "./catalog.js";
|
|
|
18
18
|
import { resolveCapability } from "./capability.js";
|
|
19
19
|
import { createAdapter } from "./adapters/index.js";
|
|
20
20
|
import { registerModelTool } from "./registrar.js";
|
|
21
|
+
import { registerContractTool } from "./bindings.js";
|
|
22
|
+
import { createGrant } from "./authority.js";
|
|
23
|
+
import { ProviderBridge } from "./provider-bridge.js";
|
|
24
|
+
import { buildSurface } from "./publication.js";
|
|
25
|
+
import { createMcpHost } from "./mcp/host-binding.js";
|
|
26
|
+
import { ToolCallId } from '@deepseek-ai/dsh-llm';
|
|
27
|
+
import { STANDARD_CONTRACTS } from '@try-works/tool-mode-contracts';
|
|
21
28
|
import { renderSkillBody } from "./skills.js";
|
|
22
29
|
import { loadPlaybooks } from "./playbook.js";
|
|
23
30
|
import { commandExists, DEFAULT_CLI_COMMAND } from "./transport.js";
|
|
24
31
|
import { createFileStateStore, createMemoryStateStore, defaultStateFile, resolveSelection, withChoice, } from "./state.js";
|
|
25
32
|
export const name = 'model-tools';
|
|
26
33
|
export const inject = ['tools'];
|
|
34
|
+
/**
|
|
35
|
+
* This package's version, stamped into every result as `extensionVersion`.
|
|
36
|
+
* Kept as a literal so the runtime does not depend on reading its own manifest.
|
|
37
|
+
*/
|
|
38
|
+
export /** How many invocation receipts are retained in memory (EXT-35). */ const MAX_RETAINED_RECEIPTS = 200;
|
|
39
|
+
const HOST_VERSION = '0.1.1';
|
|
27
40
|
/** Validate and default the plugin configuration. */
|
|
28
41
|
export const Config = z.object({
|
|
29
42
|
catalogFile: z.string(),
|
|
30
43
|
playbookFile: z.string(),
|
|
31
44
|
stateFile: z.string(),
|
|
32
45
|
persist: z.boolean().default(true),
|
|
46
|
+
providers: z.dict(z.string()).default({}),
|
|
47
|
+
legacyAliases: z.boolean().default(false),
|
|
48
|
+
approvedRoots: z.array(z.string()).default([]),
|
|
49
|
+
grantId: z.string().default('local-interactive'),
|
|
50
|
+
recordReceipts: z.boolean().default(true),
|
|
51
|
+
limits: z.object({
|
|
52
|
+
maxInputBytes: z.number(),
|
|
53
|
+
maxAudioSeconds: z.number(),
|
|
54
|
+
maxResultBytes: z.number(),
|
|
55
|
+
}),
|
|
33
56
|
models: z.array(z.string()).default([]),
|
|
34
57
|
exclude: z.array(z.string()).default([]),
|
|
35
58
|
exposeUnavailable: z.boolean().default(false),
|
|
@@ -107,11 +130,175 @@ export function apply(ctx, config = {}) {
|
|
|
107
130
|
}),
|
|
108
131
|
}));
|
|
109
132
|
const byId = new Map(resolved.map(item => [item.entry.id, item]));
|
|
133
|
+
/**
|
|
134
|
+
* Choose the implementation that serves each standard contract (BASE-02).
|
|
135
|
+
*
|
|
136
|
+
* A contract is registered once. Selection is host configuration: an explicit
|
|
137
|
+
* `providers` override wins only if that model can actually run here, and
|
|
138
|
+
* otherwise the first eligible implementation in catalog order is used. The
|
|
139
|
+
* alternatives are kept so discovery can report them without a caller having to
|
|
140
|
+
* choose a model.
|
|
141
|
+
*/
|
|
142
|
+
// The host owns provenance and authority: `extension`/`version` name THIS
|
|
143
|
+
// package, the receipt counter lives here, and the grant is built from operator
|
|
144
|
+
// configuration, so an implementation can forge neither (BASE-05, DECL-03).
|
|
145
|
+
let receiptCounter = 0;
|
|
146
|
+
/** The most recent invocation receipts (EXT-35), bounded so retention cannot grow without limit. */
|
|
147
|
+
const invocationReceipts = [];
|
|
148
|
+
const grant = createGrant({
|
|
149
|
+
grantId: config.grantId ?? 'local-interactive',
|
|
150
|
+
// Every registered tool is granted to the interactive operator; an external
|
|
151
|
+
// caller gets a narrower grant of its own rather than reusing this one.
|
|
152
|
+
allowedTools: [...STANDARD_CONTRACTS.map(contract => contract.toolName), 'model_tools'],
|
|
153
|
+
approvedRoots: config.approvedRoots ?? [],
|
|
154
|
+
hostedCode: false,
|
|
155
|
+
recordingPolicy: config.recordReceipts === false ? 'none' : 'receipts',
|
|
156
|
+
...config.limits === undefined ? {} : { limits: config.limits },
|
|
157
|
+
});
|
|
158
|
+
const host = {
|
|
159
|
+
extension: '@try-works/dsh-model-as-tool',
|
|
160
|
+
extensionVersion: HOST_VERSION,
|
|
161
|
+
activationGeneration: 1,
|
|
162
|
+
nextReceiptId: () => `rcpt_${(++receiptCounter).toString(36).padStart(6, '0')}`,
|
|
163
|
+
grant,
|
|
164
|
+
callerKind: 'interactive',
|
|
165
|
+
// EXT-35: bounded in-memory retention. A receipt is provenance, not a log of
|
|
166
|
+
// content, so retaining the last N costs little and makes the record
|
|
167
|
+
// recoverable after the call rather than only during it.
|
|
168
|
+
recordReceipt: receipt => {
|
|
169
|
+
invocationReceipts.push(receipt);
|
|
170
|
+
if (invocationReceipts.length > MAX_RETAINED_RECEIPTS)
|
|
171
|
+
invocationReceipts.shift();
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Extension providers, reached only through the public extension API (BASE-09).
|
|
176
|
+
*
|
|
177
|
+
* The host holds no adapter for any model: a catalog entry names the package
|
|
178
|
+
* that implements it, and this bridge loads it. One bridge per activation, so an
|
|
179
|
+
* extension serving several models is imported once.
|
|
180
|
+
*/
|
|
181
|
+
const bridge = new ProviderBridge({
|
|
182
|
+
grant,
|
|
183
|
+
activationGeneration: host.activationGeneration,
|
|
184
|
+
log: (message, fields) => ctx.logger?.debug?.(`[model-as-tool] ${message}`, fields),
|
|
185
|
+
});
|
|
186
|
+
const contractById = new Map(STANDARD_CONTRACTS.map(contract => [contract.contract, contract]));
|
|
187
|
+
const bindings = new Map();
|
|
188
|
+
for (const contract of STANDARD_CONTRACTS) {
|
|
189
|
+
const eligible = resolved.filter(item => item.entry.contract === contract.contract && item.verdict.runnable);
|
|
190
|
+
if (eligible.length === 0)
|
|
191
|
+
continue;
|
|
192
|
+
const override = config.providers?.[contract.contract];
|
|
193
|
+
const chosen = eligible.find(item => item.entry.id === override) ?? eligible[0];
|
|
194
|
+
if (chosen === undefined)
|
|
195
|
+
continue;
|
|
196
|
+
const adapter = createAdapter(chosen.entry, { route: chosen.verdict.route, ...transportOptions }, undefined, bridge);
|
|
197
|
+
if (adapter === undefined)
|
|
198
|
+
continue;
|
|
199
|
+
bindings.set(contract.contract, {
|
|
200
|
+
contract,
|
|
201
|
+
implementation: chosen.entry,
|
|
202
|
+
adapter,
|
|
203
|
+
route: chosen.verdict.route,
|
|
204
|
+
alternatives: eligible.filter(item => item.entry.id !== chosen.entry.id).map(item => item.entry.id),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** Contracts already registered, so a second implementation joins rather than duplicates. */
|
|
208
|
+
const registeredContracts = new Map();
|
|
209
|
+
/**
|
|
210
|
+
* The MCP surface over the tools this activation actually published (MCP-08/09).
|
|
211
|
+
*
|
|
212
|
+
* It reads `registeredContracts`, so a tool appears over MCP only if it was
|
|
213
|
+
* REGISTERED here — the publication surface already decided that, and re-deriving
|
|
214
|
+
* it would let MCP advertise something the host had withheld.
|
|
215
|
+
*/
|
|
216
|
+
const mcpHost = createMcpHost({
|
|
217
|
+
serverInfo: { name: '@try-works/dsh-model-as-tool', version: HOST_VERSION },
|
|
218
|
+
surface: {
|
|
219
|
+
get published() {
|
|
220
|
+
const out = [];
|
|
221
|
+
for (const [contractId, registered] of registeredContracts) {
|
|
222
|
+
const contract = contractById.get(contractId);
|
|
223
|
+
if (contract === undefined)
|
|
224
|
+
continue;
|
|
225
|
+
out.push({ toolName: registered.toolName, contract });
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
},
|
|
229
|
+
async execute({ name, arguments: args, signal, callId }) {
|
|
230
|
+
// MCP-08: routed through the DSH tool runtime with a SERVER-ISSUED call
|
|
231
|
+
// identity and the CALLER's signal, never by resolving a definition and
|
|
232
|
+
// calling it directly — that path would bypass the grant, the receipt and
|
|
233
|
+
// the result policy the runtime applies.
|
|
234
|
+
const result = await ctx.tools.execute({
|
|
235
|
+
signal,
|
|
236
|
+
callId: ToolCallId(callId),
|
|
237
|
+
name,
|
|
238
|
+
arguments: args,
|
|
239
|
+
});
|
|
240
|
+
return result.isError
|
|
241
|
+
? { isError: true, message: result.error.message }
|
|
242
|
+
: { isError: false, value: result.value };
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
onWithheld: tool => ctx.logger.info(`model-tools: ${tool.toolName} is not exported over MCP: ${tool.notExportableReason ?? 'no reason given'}`),
|
|
246
|
+
});
|
|
247
|
+
/**
|
|
248
|
+
* The publication decision for one catalog entry (EXT-24, EXT-32).
|
|
249
|
+
*
|
|
250
|
+
* ONE function decides what may be published, used by the installer and by the
|
|
251
|
+
* discovery surface. Two call sites computing this separately is how a tool ends
|
|
252
|
+
* up registered while discovery reports it withheld, or the reverse.
|
|
253
|
+
*/
|
|
254
|
+
const surfaceFor = (entry) => {
|
|
255
|
+
const binding = entry.contract === null ? undefined : bindings.get(entry.contract);
|
|
256
|
+
if (binding === undefined)
|
|
257
|
+
return undefined;
|
|
258
|
+
return buildSurface({
|
|
259
|
+
generation: host.activationGeneration,
|
|
260
|
+
tools: [{
|
|
261
|
+
name: binding.contract.toolName,
|
|
262
|
+
contractId: binding.contract.contract,
|
|
263
|
+
selectedModelId: binding.implementation.id,
|
|
264
|
+
implementations: [
|
|
265
|
+
{
|
|
266
|
+
extensionId: binding.implementation.provider?.extension
|
|
267
|
+
?? binding.implementation.npmPackage
|
|
268
|
+
?? 'unknown',
|
|
269
|
+
modelId: binding.implementation.id,
|
|
270
|
+
// There is no preparation lifecycle yet, so a model the capability
|
|
271
|
+
// resolver proved runnable is treated as ready. When preparation exists
|
|
272
|
+
// this reads the deployment's real readiness instead, and the surface
|
|
273
|
+
// already withholds the tool when it is not ready.
|
|
274
|
+
readiness: 'ready',
|
|
275
|
+
runnable: true,
|
|
276
|
+
},
|
|
277
|
+
...binding.alternatives.map(alternativeId => {
|
|
278
|
+
// The alternative's own extension, looked up rather than assumed: an
|
|
279
|
+
// alternative served by a different package is exactly the case a
|
|
280
|
+
// second implementation exists for (EXT-22).
|
|
281
|
+
const alternative = resolved.find(item => item.entry.id === alternativeId);
|
|
282
|
+
return {
|
|
283
|
+
extensionId: alternative?.entry.provider?.extension
|
|
284
|
+
?? alternative?.entry.npmPackage
|
|
285
|
+
?? 'unknown',
|
|
286
|
+
modelId: alternativeId,
|
|
287
|
+
readiness: 'ready',
|
|
288
|
+
runnable: true,
|
|
289
|
+
};
|
|
290
|
+
}),
|
|
291
|
+
],
|
|
292
|
+
}],
|
|
293
|
+
skills: [],
|
|
294
|
+
});
|
|
295
|
+
};
|
|
110
296
|
/**
|
|
111
297
|
* Register one model's tool and skill.
|
|
112
298
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
299
|
+
* A model bound to a standard contract contributes to that CONTRACT's single
|
|
300
|
+
* tool; a model with no contract keeps its own per-model tool. Idempotent in
|
|
301
|
+
* both cases, so the control tool can call it without first checking.
|
|
115
302
|
* @param item - the resolved model to install.
|
|
116
303
|
* @returns whether the model is installed after the call.
|
|
117
304
|
*/
|
|
@@ -121,14 +308,62 @@ export function apply(ctx, config = {}) {
|
|
|
121
308
|
return true;
|
|
122
309
|
if (!verdict.runnable && !exposeUnavailable)
|
|
123
310
|
return false;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
311
|
+
let toolName = entry.tool.name;
|
|
312
|
+
let dispose;
|
|
313
|
+
if (entry.contract !== null && contractById.has(entry.contract)) {
|
|
314
|
+
const binding = bindings.get(entry.contract);
|
|
315
|
+
if (binding === undefined) {
|
|
316
|
+
// The model implements a contract but lost provider selection, or cannot
|
|
317
|
+
// run. Report rather than register a second tool for the same contract.
|
|
318
|
+
ctx.logger.info(`model-tools: ${entry.name} implements ${entry.contract} but is not the selected provider; `
|
|
319
|
+
+ 'no separate tool is registered (BASE-02)');
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* EXT-24: decide what may be published BEFORE registering anything.
|
|
324
|
+
*
|
|
325
|
+
* The surface is built whole and consulted first, so a tool is registered
|
|
326
|
+
* only when the surface actually published it. Registering first and
|
|
327
|
+
* reconciling afterwards is the state EXT-24 forbids: a caller would observe
|
|
328
|
+
* an available tool whose binding was not yet in place.
|
|
329
|
+
*
|
|
330
|
+
* One helper serves the installer and the discovery surface, because two call
|
|
331
|
+
* sites computing this separately is how a tool gets registered while
|
|
332
|
+
* discovery reports it withheld, or the reverse.
|
|
333
|
+
*/
|
|
334
|
+
const surface = surfaceFor(entry);
|
|
335
|
+
const decision = surface?.tools[0];
|
|
336
|
+
if (decision === undefined) {
|
|
337
|
+
const reason = surface?.withheld[0];
|
|
338
|
+
ctx.logger.info(`model-tools: ${entry.name} is active but its tool is withheld (${reason?.code ?? 'unknown'}): `
|
|
339
|
+
+ `${reason?.message ?? 'no reason reported'}`);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
const existing = registeredContracts.get(entry.contract);
|
|
343
|
+
if (existing !== undefined) {
|
|
344
|
+
// Already served by another implementation: this model joins that tool.
|
|
345
|
+
toolName = existing.toolName;
|
|
346
|
+
dispose = () => { };
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
const registered = registerContractTool(ctx, binding, host);
|
|
350
|
+
registeredContracts.set(entry.contract, registered);
|
|
351
|
+
toolName = registered.toolName;
|
|
352
|
+
dispose = registered.dispose;
|
|
353
|
+
}
|
|
129
354
|
}
|
|
130
|
-
|
|
131
|
-
|
|
355
|
+
else {
|
|
356
|
+
const adapter = createAdapter(entry, { route: verdict.route, ...transportOptions }, undefined, bridge);
|
|
357
|
+
if (adapter === undefined) {
|
|
358
|
+
// Runnable but no adapter yet: report rather than register a dead tool.
|
|
359
|
+
ctx.logger.info(`model-tools: ${entry.name} can run here (${verdict.route}) but no adapter ships yet; skipping ${entry.tool.name}`);
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
const registration = registerModelTool(ctx, entry, adapter);
|
|
363
|
+
toolName = registration.toolName;
|
|
364
|
+
dispose = registration.dispose;
|
|
365
|
+
}
|
|
366
|
+
const record = { entry, toolName, dispose };
|
|
132
367
|
if (registerSkills) {
|
|
133
368
|
const skills = ctx.get('skills');
|
|
134
369
|
if (skills !== undefined) {
|
|
@@ -142,8 +377,19 @@ export function apply(ctx, config = {}) {
|
|
|
142
377
|
});
|
|
143
378
|
}
|
|
144
379
|
}
|
|
380
|
+
// BASE-03: the deprecated alias is opt-in, so the default catalog shows one
|
|
381
|
+
// transcription tool. It keeps its own name and adapter, never the new
|
|
382
|
+
// contract's schema, so an old caller's arguments are not reinterpreted.
|
|
383
|
+
if (config.legacyAliases === true && entry.contract !== null && entry.tool.name !== toolName) {
|
|
384
|
+
const aliasAdapter = createAdapter(entry, { route: verdict.route, ...transportOptions }, undefined, bridge);
|
|
385
|
+
if (aliasAdapter !== undefined) {
|
|
386
|
+
const alias = registerModelTool(ctx, entry, aliasAdapter);
|
|
387
|
+
record.disposeAlias = alias.dispose;
|
|
388
|
+
ctx.logger.info(`model-tools: registered deprecated alias ${alias.toolName} (BASE-03)`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
145
391
|
installed.set(entry.id, record);
|
|
146
|
-
ctx.logger.info(`model-tools: registered ${
|
|
392
|
+
ctx.logger.info(`model-tools: registered ${toolName} for ${entry.name} (${verdict.route})`);
|
|
147
393
|
return true;
|
|
148
394
|
};
|
|
149
395
|
/**
|
|
@@ -156,7 +402,16 @@ export function apply(ctx, config = {}) {
|
|
|
156
402
|
if (record === undefined)
|
|
157
403
|
return false;
|
|
158
404
|
record.disposeSkill?.();
|
|
405
|
+
record.disposeAlias?.();
|
|
159
406
|
record.dispose();
|
|
407
|
+
// A contract-served model's registration is owned by the contract; dropping
|
|
408
|
+
// the last model that used it must also release the contract's tool, or a
|
|
409
|
+
// removed implementation would keep answering calls.
|
|
410
|
+
if (record.entry.contract !== null) {
|
|
411
|
+
const stillInstalled = [...installed.values()].some(other => other.entry.id !== modelId && other.entry.contract === record.entry.contract);
|
|
412
|
+
if (!stillInstalled)
|
|
413
|
+
registeredContracts.delete(record.entry.contract);
|
|
414
|
+
}
|
|
160
415
|
installed.delete(modelId);
|
|
161
416
|
ctx.logger.info(`model-tools: unregistered ${record.toolName}`);
|
|
162
417
|
return true;
|
|
@@ -206,7 +461,7 @@ export function apply(ctx, config = {}) {
|
|
|
206
461
|
action: {
|
|
207
462
|
type: 'string',
|
|
208
463
|
required: true,
|
|
209
|
-
enum: ['list', 'installed', 'describe', 'enable', 'disable', 'reset'],
|
|
464
|
+
enum: ['list', 'installed', 'describe', 'enable', 'disable', 'reset', 'receipts', 'mcp'],
|
|
210
465
|
description: 'list: every catalogued model and whether it runs here. installed: only the models whose tools are '
|
|
211
466
|
+ 'registered. describe: full detail for one model. enable: install a model\'s tool and remember it. '
|
|
212
467
|
+ 'disable: remove a model\'s tool and remember it. reset: forget the remembered choice for one model, '
|
|
@@ -251,11 +506,88 @@ export function apply(ctx, config = {}) {
|
|
|
251
506
|
},
|
|
252
507
|
license: { type: 'string', required: true },
|
|
253
508
|
docs: { type: 'string', required: true },
|
|
509
|
+
/**
|
|
510
|
+
* Public extension/implementation provenance (EXT-32).
|
|
511
|
+
*
|
|
512
|
+
* Declared rather than left open: a discovery surface that could
|
|
513
|
+
* return anything would let the shape drift unnoticed, and the
|
|
514
|
+
* output schema refusing undeclared fields is what caught this
|
|
515
|
+
* addition in the first place.
|
|
516
|
+
*/
|
|
517
|
+
provenance: {
|
|
518
|
+
type: 'object',
|
|
519
|
+
additionalProperties: false,
|
|
520
|
+
properties: {
|
|
521
|
+
contract: {
|
|
522
|
+
required: true,
|
|
523
|
+
oneOf: [{ type: 'string' }, { type: 'null' }],
|
|
524
|
+
},
|
|
525
|
+
extension: {
|
|
526
|
+
required: true,
|
|
527
|
+
oneOf: [{ type: 'string' }, { type: 'null' }],
|
|
528
|
+
},
|
|
529
|
+
modelId: {
|
|
530
|
+
required: true,
|
|
531
|
+
oneOf: [{ type: 'string' }, { type: 'null' }],
|
|
532
|
+
},
|
|
533
|
+
publishedTool: {
|
|
534
|
+
required: true,
|
|
535
|
+
oneOf: [{ type: 'string' }, { type: 'null' }],
|
|
536
|
+
},
|
|
537
|
+
alternatives: { type: 'array', required: true, items: { type: 'string' } },
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
/** The deployment's readiness, so an absent tool is explicable. */
|
|
541
|
+
readiness: { type: 'string', required: true },
|
|
542
|
+
/** Whether the tool is genuinely callable right now. */
|
|
543
|
+
callable: { type: 'boolean', required: true },
|
|
544
|
+
/** Why the tool is absent, when it is. */
|
|
545
|
+
withheld: {
|
|
546
|
+
type: 'object',
|
|
547
|
+
additionalProperties: false,
|
|
548
|
+
properties: {
|
|
549
|
+
code: { type: 'string', required: true },
|
|
550
|
+
message: { type: 'string', required: true },
|
|
551
|
+
},
|
|
552
|
+
},
|
|
254
553
|
},
|
|
255
554
|
},
|
|
256
555
|
},
|
|
257
556
|
installed: { type: 'integer', required: true },
|
|
258
557
|
runnable: { type: 'integer', required: true },
|
|
558
|
+
/** Set by the mcp action: what this host would export over MCP (MCP-09). */
|
|
559
|
+
mcp: {
|
|
560
|
+
type: 'array',
|
|
561
|
+
items: {
|
|
562
|
+
type: 'object',
|
|
563
|
+
additionalProperties: false,
|
|
564
|
+
properties: {
|
|
565
|
+
name: { type: 'string', required: true },
|
|
566
|
+
hasOutputSchema: { type: 'boolean', required: true },
|
|
567
|
+
title: {
|
|
568
|
+
required: true,
|
|
569
|
+
oneOf: [{ type: 'string' }, { type: 'null' }],
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
/** Set by the receipts action: recent invocation provenance (EXT-35). */
|
|
575
|
+
receipts: {
|
|
576
|
+
type: 'array',
|
|
577
|
+
items: {
|
|
578
|
+
type: 'object',
|
|
579
|
+
additionalProperties: false,
|
|
580
|
+
properties: {
|
|
581
|
+
receiptId: { type: 'string', required: true },
|
|
582
|
+
contract: { type: 'string', required: true },
|
|
583
|
+
implementationId: { type: 'string', required: true },
|
|
584
|
+
extension: { type: 'string', required: true },
|
|
585
|
+
modelIdentity: { type: 'string', required: true },
|
|
586
|
+
activationGeneration: { type: 'integer', required: true },
|
|
587
|
+
executionLocation: { type: 'string', required: true },
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
},
|
|
259
591
|
/** Set by enable/disable/reset: whether the requested change took effect. */
|
|
260
592
|
changed: { type: 'boolean' },
|
|
261
593
|
/** Set by enable/disable/reset: what the call did to the tool surface. */
|
|
@@ -288,17 +620,92 @@ export function apply(ctx, config = {}) {
|
|
|
288
620
|
const modelId = args.model;
|
|
289
621
|
/** The user's remembered choice for one model. */
|
|
290
622
|
const choiceOf = (id) => persisted.enabled.includes(id) ? 'enabled' : persisted.disabled.includes(id) ? 'disabled' : 'none';
|
|
291
|
-
/**
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
623
|
+
/**
|
|
624
|
+
* The provenance and readiness a discovery surface must expose (EXT-32).
|
|
625
|
+
*
|
|
626
|
+
* ALWAYS returns the full shape, including for a model with no contract
|
|
627
|
+
* binding: the fields are declared required in the output schema, so a
|
|
628
|
+
* partial object would be refused — and a caller reading `readiness` should
|
|
629
|
+
* get an answer for every model rather than having to handle its absence.
|
|
630
|
+
*/
|
|
631
|
+
const provenanceOf = (entry) => {
|
|
632
|
+
const surface = surfaceFor(entry);
|
|
633
|
+
const published = surface?.tools[0];
|
|
634
|
+
const notice = surface?.readiness.find(item => item.modelId === entry.id);
|
|
635
|
+
const withheld = surface === undefined
|
|
636
|
+
? undefined
|
|
637
|
+
: surface.withheld.find(item => item.subject === published?.name) ?? surface.withheld[0];
|
|
638
|
+
return {
|
|
639
|
+
provenance: {
|
|
640
|
+
contract: entry.contract,
|
|
641
|
+
// Which PACKAGE implements this model, and which model inside it. Read
|
|
642
|
+
// from the catalog binding, so it is reported even for a model that is
|
|
643
|
+
// not the selected provider and therefore has no published tool.
|
|
644
|
+
extension: entry.provider?.extension ?? null,
|
|
645
|
+
modelId: entry.provider?.modelId ?? null,
|
|
646
|
+
publishedTool: published?.name ?? null,
|
|
647
|
+
// Copied into a mutable array: the surface's own list is readonly, and
|
|
648
|
+
// a discovery result is a plain JSON value.
|
|
649
|
+
alternatives: [...(published?.alternatives ?? [])],
|
|
650
|
+
},
|
|
651
|
+
readiness: notice?.readiness ?? 'unknown',
|
|
652
|
+
callable: notice?.callable ?? false,
|
|
653
|
+
...withheld === undefined ? {} : {
|
|
654
|
+
withheld: { code: withheld.code, message: withheld.message },
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
};
|
|
658
|
+
/**
|
|
659
|
+
* The current snapshot, with per-model installed and remembered state.
|
|
660
|
+
*
|
|
661
|
+
* `tool` reports the name actually REGISTERED, not the catalog's per-model
|
|
662
|
+
* name: under a contract two implementations share one tool, and reporting
|
|
663
|
+
* the catalog name would tell a caller to call something that does not exist.
|
|
664
|
+
*/
|
|
665
|
+
const snapshot = (items) => items.map(item => {
|
|
666
|
+
const record = installed.get(item.entry.id);
|
|
667
|
+
return {
|
|
668
|
+
...describeModels([item])[0],
|
|
669
|
+
...record === undefined ? {} : { tool: record.toolName },
|
|
670
|
+
...provenanceOf(item.entry),
|
|
671
|
+
installed: record !== undefined,
|
|
672
|
+
choice: choiceOf(item.entry.id),
|
|
673
|
+
};
|
|
674
|
+
});
|
|
675
|
+
/**
|
|
676
|
+
* The most recent invocation receipts (EXT-35).
|
|
677
|
+
*
|
|
678
|
+
* Exposed through discovery so the provenance is observable rather than
|
|
679
|
+
* merely recorded: a receipt nobody can read is indistinguishable from no
|
|
680
|
+
* receipt at all.
|
|
681
|
+
*/
|
|
682
|
+
const receipts = () => invocationReceipts.map(receipt => ({
|
|
683
|
+
receiptId: receipt.receiptId,
|
|
684
|
+
contract: receipt.contract,
|
|
685
|
+
implementationId: receipt.implementationId,
|
|
686
|
+
extension: receipt.extension,
|
|
687
|
+
modelIdentity: receipt.modelIdentity,
|
|
688
|
+
activationGeneration: receipt.activationGeneration,
|
|
689
|
+
executionLocation: receipt.executionLocation,
|
|
296
690
|
}));
|
|
297
691
|
const counts = () => ({
|
|
298
692
|
installed: installed.size,
|
|
299
693
|
runnable: resolved.filter(item => item.verdict.runnable).length,
|
|
300
694
|
...persist ? { stateFile: config.stateFile ?? defaultStateFile() } : {},
|
|
301
695
|
});
|
|
696
|
+
if (action === 'mcp') {
|
|
697
|
+
return { models: [], ...counts(), mcp: mcpHost.listTools().map(tool => ({
|
|
698
|
+
name: tool.name,
|
|
699
|
+
// Presence of the OUTPUT schema is the MCP-09 fact worth exposing: DSH
|
|
700
|
+
// model-facing projection omits it, so this is the evidence that the
|
|
701
|
+
// export is built from the contract instead.
|
|
702
|
+
hasOutputSchema: tool.outputSchema !== undefined,
|
|
703
|
+
title: tool.title ?? null,
|
|
704
|
+
})) };
|
|
705
|
+
}
|
|
706
|
+
if (action === 'receipts') {
|
|
707
|
+
return { models: [], ...counts(), receipts: receipts() };
|
|
708
|
+
}
|
|
302
709
|
if (action === 'reset') {
|
|
303
710
|
// Forgetting a choice is what makes the config authoritative again.
|
|
304
711
|
if (modelId !== undefined) {
|
|
@@ -373,6 +780,7 @@ export function apply(ctx, config = {}) {
|
|
|
373
780
|
ctx.effect(() => () => {
|
|
374
781
|
for (const record of installed.values()) {
|
|
375
782
|
record.disposeSkill?.();
|
|
783
|
+
record.disposeAlias?.();
|
|
376
784
|
record.dispose();
|
|
377
785
|
}
|
|
378
786
|
installed.clear();
|