@try-works/dsh-tool-mode 0.5.44 → 0.5.46
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/LICENSE +205 -0
- package/README.md +6 -2
- package/client/index.js +119 -0
- package/lib/extensions-service.d.ts.map +1 -1
- package/lib/extensions-service.js +47 -2
- package/lib/headless.d.ts.map +1 -1
- package/lib/headless.js +3 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +84 -8
- package/lib/model-runtime-lane.d.ts +69 -0
- package/lib/model-runtime-lane.d.ts.map +1 -0
- package/lib/model-runtime-lane.js +122 -0
- package/lib/model-runtime-recipe.d.ts +132 -0
- package/lib/model-runtime-recipe.d.ts.map +1 -0
- package/lib/model-runtime-recipe.js +291 -0
- package/node_modules/@try-works/tool-mode-contracts/package.json +1 -1
- package/node_modules/@try-works/tool-mode-extension-sdk/package.json +1 -1
- package/node_modules/@try-works/tool-mode-recipes/package.json +1 -1
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -44,6 +44,8 @@ import { captureToolSchemaSnapshot, describePlanningRejections, filterPlanningLi
|
|
|
44
44
|
import { discoverExtension, loadExtensionModule, resolveDeclarativeImplementations, resolveExtensionEntry } from '@try-works/tool-mode-extension-sdk';
|
|
45
45
|
import { ExtensionStore, ExtensionLifecycle } from '@try-works/tool-mode-extension-sdk';
|
|
46
46
|
import { authorizeTool, createGrant } from "./authority.js";
|
|
47
|
+
import { isModelRuntimeRecipe, parseModelRuntimeRecipe } from "./model-runtime-recipe.js";
|
|
48
|
+
import { createModelRuntimeTool } from "./model-runtime-lane.js";
|
|
47
49
|
import { ProviderBridge } from "./provider-bridge.js";
|
|
48
50
|
import { createUpdateHostSurface } from "./update-surface.js";
|
|
49
51
|
import { SubscriptionRegistry, deliverCatalogChange, MemoryDeliverySink } from "./mcp/subscriptions.js";
|
|
@@ -4518,16 +4520,93 @@ export async function apply(ctx, config = {}) {
|
|
|
4518
4520
|
// catalogue binds it, and there is nothing to register.
|
|
4519
4521
|
return () => { };
|
|
4520
4522
|
}
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4523
|
+
/**
|
|
4524
|
+
* THE RECIPES ARE RESOLVED BEFORE ANY PROVIDER DECISION, because their `kind`
|
|
4525
|
+
* decides which lane serves them (docs/runtime/PHASE2.md).
|
|
4526
|
+
*
|
|
4527
|
+
* The harness lane needs a composed `LlmProvider`; the model-runtime lane must NOT
|
|
4528
|
+
* (its engine is a factory the package ships and the recipe names). Checking for a
|
|
4529
|
+
* harness provider first — which is what this function did before the lane existed —
|
|
4530
|
+
* would refuse a model-runtime package for lacking something it never needs.
|
|
4531
|
+
*/
|
|
4526
4532
|
const resolved = resolveDeclarativeImplementations(record.installPath, manifest.implementations ?? []);
|
|
4527
4533
|
if (!resolved.ok) {
|
|
4528
4534
|
throw new Error(`${contribution.extensionId} is declarative but its recipe could not be resolved: `
|
|
4529
4535
|
+ `${resolved.id} — ${resolved.detail}`);
|
|
4530
4536
|
}
|
|
4537
|
+
const rawEntry = resolved.resolved.find(entry => entry.id === contribution.name);
|
|
4538
|
+
if (rawEntry === undefined)
|
|
4539
|
+
return () => { };
|
|
4540
|
+
/**
|
|
4541
|
+
* THE MODEL-RUNTIME LANE (AT-25, ARCH-02).
|
|
4542
|
+
*
|
|
4543
|
+
* A `kind: "model-runtime"` recipe is served WITHOUT a harness model: the host
|
|
4544
|
+
* parses the recipe, resolves the provider factory FROM THE EXTENSION'S OWN PACKAGE
|
|
4545
|
+
* (never from the host's module graph, which is why the host can declare no engine),
|
|
4546
|
+
* and registers the tool the recipe names with the recipe's schema files.
|
|
4547
|
+
*
|
|
4548
|
+
* Every failure here is a NAMED refusal thrown from registration, so a package that
|
|
4549
|
+
* cannot run is quarantined rather than published as a tool that fails at call time.
|
|
4550
|
+
*/
|
|
4551
|
+
if (isModelRuntimeRecipe(rawEntry.recipe)) {
|
|
4552
|
+
const parsedRecipe = parseModelRuntimeRecipe(rawEntry.recipe);
|
|
4553
|
+
if (!parsedRecipe.ok) {
|
|
4554
|
+
throw new Error(`${contribution.extensionId} declares a model-runtime recipe for ${contribution.name} that did not validate: `
|
|
4555
|
+
+ `${parsedRecipe.code} — ${parsedRecipe.detail}`);
|
|
4556
|
+
}
|
|
4557
|
+
/**
|
|
4558
|
+
* The authority is the SAME one the extension-tool path uses, so a model-runtime
|
|
4559
|
+
* provider resolves caller paths through the host's grants rather than opening
|
|
4560
|
+
* whatever it is handed (SEC-06).
|
|
4561
|
+
*/
|
|
4562
|
+
const authority = createInvocationAuthority(grant, 'interactive');
|
|
4563
|
+
const lane = createModelRuntimeTool({
|
|
4564
|
+
extensionId: contribution.extensionId,
|
|
4565
|
+
implementationId: contribution.name,
|
|
4566
|
+
installPath: record.installPath,
|
|
4567
|
+
recipe: parsedRecipe.recipe,
|
|
4568
|
+
assets: () => prepareAssetsSurface?.assetMapFor(contribution.extensionId) ?? { assetIds: [], get: () => undefined },
|
|
4569
|
+
invocationContext: exec => ({
|
|
4570
|
+
invocationId: `inv_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`,
|
|
4571
|
+
signal: exec?.signal ?? new AbortController().signal,
|
|
4572
|
+
limits: authority.limits,
|
|
4573
|
+
resolvePath: candidate => authority.resolvePath(candidate),
|
|
4574
|
+
}),
|
|
4575
|
+
onError: message => ctx.logger.warn(`model-tools: ${message}`),
|
|
4576
|
+
});
|
|
4577
|
+
if (!lane.ok) {
|
|
4578
|
+
throw new Error(`${contribution.extensionId} declares a model-runtime recipe for ${contribution.name} that cannot be served: `
|
|
4579
|
+
+ `${lane.code} — ${lane.detail}`);
|
|
4580
|
+
}
|
|
4581
|
+
const disposeTool = ctx.tools.register(defineTool({
|
|
4582
|
+
name: lane.tool.name,
|
|
4583
|
+
description: lane.tool.description,
|
|
4584
|
+
parameters: lane.tool.parameters,
|
|
4585
|
+
output: {
|
|
4586
|
+
schema: { type: 'json' },
|
|
4587
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
4588
|
+
},
|
|
4589
|
+
async execute(args, exec) {
|
|
4590
|
+
return await lane.tool.execute(args, exec);
|
|
4591
|
+
},
|
|
4592
|
+
}));
|
|
4593
|
+
exposeExtensionToolToMcp(lane.tool.name, {
|
|
4594
|
+
description: lane.tool.description,
|
|
4595
|
+
inputSchema: lane.tool.inputSchema,
|
|
4596
|
+
outputSchema: lane.tool.outputSchema,
|
|
4597
|
+
});
|
|
4598
|
+
return () => {
|
|
4599
|
+
// Withdraw first so no new call can start, then release the provider.
|
|
4600
|
+
withdrawExtensionToolFromMcp(lane.tool.name);
|
|
4601
|
+
disposeTool();
|
|
4602
|
+
void lane.tool.dispose();
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
if (harnessProvider === undefined && config.harnessProfiles === undefined) {
|
|
4606
|
+
throw new Error(`${contribution.extensionId} is declarative and its recipes resolved, but they cannot be served: `
|
|
4607
|
+
+ 'this deployment composes no harness LlmProvider, so a declarative package\'s recipes '
|
|
4608
|
+
+ 'cannot be executed. Set config.harnessModel (or supply harnessProvider/harnessProfiles) to serve them.');
|
|
4609
|
+
}
|
|
4531
4610
|
const served = prepareDeclarative({
|
|
4532
4611
|
...harnessProvider === undefined ? {} : { provider: harnessProvider },
|
|
4533
4612
|
...config.harnessProfiles === undefined ? {} : { profiles: config.harnessProfiles },
|
|
@@ -4537,9 +4616,6 @@ export async function apply(ctx, config = {}) {
|
|
|
4537
4616
|
throw new Error(`${contribution.extensionId} is declarative and its ${resolved.resolved.length} recipe(s) resolved, `
|
|
4538
4617
|
+ `but they cannot be served: ${served.detail}`);
|
|
4539
4618
|
}
|
|
4540
|
-
const rawEntry = resolved.resolved.find(entry => entry.id === contribution.name);
|
|
4541
|
-
if (rawEntry === undefined)
|
|
4542
|
-
return () => { };
|
|
4543
4619
|
const parsed = parseRecipe(rawEntry.recipe);
|
|
4544
4620
|
if (!parsed.ok) {
|
|
4545
4621
|
throw new Error(`${contribution.extensionId} declares a recipe that did not validate: ${JSON.stringify(parsed).slice(0, 300)}`);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host lane that turns a parsed `model-runtime` recipe into a callable tool
|
|
3
|
+
* (docs/runtime/PHASE2.md § Host lane).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS MODULE OWNS, and what it deliberately does not:
|
|
6
|
+
*
|
|
7
|
+
* - It owns the CONTRACT between the recipe and DSH: the tool's name and description, the
|
|
8
|
+
* projection of the recipe's JSON-Schema input into what DSH can advertise, the FULL
|
|
9
|
+
* schema enforced at call time (the projection is intentionally looser), and the output
|
|
10
|
+
* check that stops a malformed payload from being published as a result.
|
|
11
|
+
* - It does NOT own the engine. The provider is a `ModelProviderV1` created by the factory
|
|
12
|
+
* the recipe names, which was resolved from the extension's own package (AT-25).
|
|
13
|
+
*
|
|
14
|
+
* TIMING IS PART OF THE DESIGN. Creating a tool resolves the provider module but does not
|
|
15
|
+
* import or run it; the import and the provider's `load` happen on the FIRST CALL. That keeps
|
|
16
|
+
* activation lightweight (EXT-19) while still refusing — at registration — a package whose
|
|
17
|
+
* engine is not installed, because resolution is synchronous.
|
|
18
|
+
*
|
|
19
|
+
* @module @try-works/dsh-tool-mode/model-runtime-lane
|
|
20
|
+
*/
|
|
21
|
+
import type { InvocationContextV1, PreparedAssetMapV1 } from '@try-works/tool-mode-extension-sdk';
|
|
22
|
+
import { type ModelRuntimeRecipe, type ModelRuntimeRefusal } from './model-runtime-recipe.ts';
|
|
23
|
+
/** What the host hands the lane when it registers one implementation's tool. */
|
|
24
|
+
export interface ModelRuntimeToolDeps {
|
|
25
|
+
/** The extension that owns the tool, for diagnostics. */
|
|
26
|
+
readonly extensionId: string;
|
|
27
|
+
/** The implementation id the recipe belongs to (the manifest's, not the tool name). */
|
|
28
|
+
readonly implementationId: string;
|
|
29
|
+
/** The installed package directory. */
|
|
30
|
+
readonly installPath: string;
|
|
31
|
+
/** The parsed recipe. */
|
|
32
|
+
readonly recipe: ModelRuntimeRecipe;
|
|
33
|
+
/** The host-verified prepared assets for this extension; empty until prepared. */
|
|
34
|
+
readonly assets: () => Readonly<PreparedAssetMapV1>;
|
|
35
|
+
/** Build the call-scoped context the SDK contract requires. */
|
|
36
|
+
readonly invocationContext: (exec: {
|
|
37
|
+
readonly signal?: AbortSignal;
|
|
38
|
+
} | undefined) => InvocationContextV1;
|
|
39
|
+
/** Record a teardown failure without throwing out of a disposer. */
|
|
40
|
+
readonly onError?: (message: string) => void;
|
|
41
|
+
}
|
|
42
|
+
/** A registered tool's host-side half: what DSH advertises and what it runs. */
|
|
43
|
+
export interface ModelRuntimeToolHandle {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly description: string;
|
|
46
|
+
/** The DSH-advertisable parameters projected from the recipe's input schema. */
|
|
47
|
+
readonly parameters: Readonly<Record<string, unknown>>;
|
|
48
|
+
/** The recipe's own input schema, for the MCP advertisement (enforced in full above). */
|
|
49
|
+
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
50
|
+
/** The recipe's own output schema, for the MCP advertisement. */
|
|
51
|
+
readonly outputSchema: Readonly<Record<string, unknown>>;
|
|
52
|
+
readonly execute: (args: unknown, exec?: {
|
|
53
|
+
readonly signal?: AbortSignal;
|
|
54
|
+
}) => Promise<unknown>;
|
|
55
|
+
readonly dispose: () => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/** The lane's outcome: a tool, or a named refusal the host turns into a refusal at registration. */
|
|
58
|
+
export type ModelRuntimeToolResult = {
|
|
59
|
+
readonly ok: true;
|
|
60
|
+
readonly tool: ModelRuntimeToolHandle;
|
|
61
|
+
} | ModelRuntimeRefusal;
|
|
62
|
+
/**
|
|
63
|
+
* Build the tool for one model-runtime implementation.
|
|
64
|
+
*
|
|
65
|
+
* @param deps - the installed package, the parsed recipe and the host's context/asset hooks.
|
|
66
|
+
* @returns the tool handle or a named refusal.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createModelRuntimeTool(deps: ModelRuntimeToolDeps): ModelRuntimeToolResult;
|
|
69
|
+
//# sourceMappingURL=model-runtime-lane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-runtime-lane.d.ts","sourceRoot":"","sources":["../src/model-runtime-lane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAA+B,kBAAkB,EAAE,MAAM,oCAAoC,CAAA;AAE9H,OAAO,EAIL,KAAK,kBAAkB,EAEvB,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAA;AAElC,gFAAgF;AAChF,MAAM,WAAW,oBAAoB;IACnC,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,uCAAuC;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;IACnC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;IACnD,+DAA+D;IAC/D,QAAQ,CAAC,iBAAiB,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,SAAS,KAAK,mBAAmB,CAAA;IACxG,oEAAoE;IACpE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7C;AAED,gFAAgF;AAChF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACtD,yFAAyF;IACzF,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACvD,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/F,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAED,oGAAoG;AACpG,MAAM,MAAM,sBAAsB,GAC9B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAA;CAAE,GAC5D,mBAAmB,CAAA;AASvB;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,GAAG,sBAAsB,CAkFzF"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host lane that turns a parsed `model-runtime` recipe into a callable tool
|
|
3
|
+
* (docs/runtime/PHASE2.md § Host lane).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS MODULE OWNS, and what it deliberately does not:
|
|
6
|
+
*
|
|
7
|
+
* - It owns the CONTRACT between the recipe and DSH: the tool's name and description, the
|
|
8
|
+
* projection of the recipe's JSON-Schema input into what DSH can advertise, the FULL
|
|
9
|
+
* schema enforced at call time (the projection is intentionally looser), and the output
|
|
10
|
+
* check that stops a malformed payload from being published as a result.
|
|
11
|
+
* - It does NOT own the engine. The provider is a `ModelProviderV1` created by the factory
|
|
12
|
+
* the recipe names, which was resolved from the extension's own package (AT-25).
|
|
13
|
+
*
|
|
14
|
+
* TIMING IS PART OF THE DESIGN. Creating a tool resolves the provider module but does not
|
|
15
|
+
* import or run it; the import and the provider's `load` happen on the FIRST CALL. That keeps
|
|
16
|
+
* activation lightweight (EXT-19) while still refusing — at registration — a package whose
|
|
17
|
+
* engine is not installed, because resolution is synchronous.
|
|
18
|
+
*
|
|
19
|
+
* @module @try-works/dsh-tool-mode/model-runtime-lane
|
|
20
|
+
*/
|
|
21
|
+
import { assertAgainstSchema, compileSchema, projectSchemaToDsh } from '@try-works/tool-mode-contracts';
|
|
22
|
+
import { importProviderFactory, readModelRuntimeSchema, resolveProviderModule, } from "./model-runtime-recipe.js";
|
|
23
|
+
/** DSH tool arguments are JSON; this only satisfies the index-signature boundary type. */
|
|
24
|
+
function asJsonObject(value) {
|
|
25
|
+
// SAFETY: the arguments arrived from the DSH registry (JSON by construction) and were just
|
|
26
|
+
// validated against the recipe's schema, so a JSON round-trip is lossless.
|
|
27
|
+
return JSON.parse(JSON.stringify(value));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the tool for one model-runtime implementation.
|
|
31
|
+
*
|
|
32
|
+
* @param deps - the installed package, the parsed recipe and the host's context/asset hooks.
|
|
33
|
+
* @returns the tool handle or a named refusal.
|
|
34
|
+
*/
|
|
35
|
+
export function createModelRuntimeTool(deps) {
|
|
36
|
+
const inputSchema = readModelRuntimeSchema(deps.installPath, deps.recipe.tool.inputSchema);
|
|
37
|
+
if (!inputSchema.ok)
|
|
38
|
+
return inputSchema;
|
|
39
|
+
const outputSchema = readModelRuntimeSchema(deps.installPath, deps.recipe.tool.outputSchema);
|
|
40
|
+
if (!outputSchema.ok)
|
|
41
|
+
return outputSchema;
|
|
42
|
+
// Resolve (not import) the engine now: a package whose factory is missing is refused at
|
|
43
|
+
// registration rather than at the first caller's expense.
|
|
44
|
+
const providerModule = resolveProviderModule({ installPath: deps.installPath, recipe: deps.recipe });
|
|
45
|
+
if (!providerModule.ok)
|
|
46
|
+
return providerModule;
|
|
47
|
+
const name = deps.recipe.tool.name;
|
|
48
|
+
const inputValidator = compileSchema(inputSchema.schema);
|
|
49
|
+
const outputValidator = compileSchema(outputSchema.schema);
|
|
50
|
+
const parameters = projectSchemaToDsh(inputSchema.schema).parameters;
|
|
51
|
+
let factoryPromise;
|
|
52
|
+
let providerPromise;
|
|
53
|
+
let disposed = false;
|
|
54
|
+
/** Import the factory once; a failure is retried on the next call rather than cached forever. */
|
|
55
|
+
const ensureFactory = async () => {
|
|
56
|
+
factoryPromise ??= (async () => {
|
|
57
|
+
const imported = await importProviderFactory({ installPath: deps.installPath, recipe: deps.recipe });
|
|
58
|
+
if (!imported.ok)
|
|
59
|
+
throw new Error(`${imported.code}: ${imported.detail}`);
|
|
60
|
+
return imported.factory;
|
|
61
|
+
})();
|
|
62
|
+
return factoryPromise;
|
|
63
|
+
};
|
|
64
|
+
/** Create and LOAD the provider once; a failed load is retried rather than reported as ready. */
|
|
65
|
+
const ensureProvider = async (signal) => {
|
|
66
|
+
providerPromise ??= (async () => {
|
|
67
|
+
const factory = await ensureFactory();
|
|
68
|
+
const assets = deps.assets();
|
|
69
|
+
const provider = factory(deps.recipe, assets);
|
|
70
|
+
await provider.load({ assets, signal: signal ?? new AbortController().signal });
|
|
71
|
+
return provider;
|
|
72
|
+
})();
|
|
73
|
+
const pending = providerPromise;
|
|
74
|
+
pending.catch(() => {
|
|
75
|
+
if (providerPromise === pending)
|
|
76
|
+
providerPromise = undefined;
|
|
77
|
+
});
|
|
78
|
+
return pending;
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
tool: {
|
|
83
|
+
name,
|
|
84
|
+
description: deps.recipe.tool.description,
|
|
85
|
+
parameters,
|
|
86
|
+
inputSchema: inputSchema.schema,
|
|
87
|
+
outputSchema: outputSchema.schema,
|
|
88
|
+
async execute(args, exec) {
|
|
89
|
+
if (disposed)
|
|
90
|
+
throw new Error(`${name}: the owning extension was disposed`);
|
|
91
|
+
// The FULL schema, before any engine work: the DSH advertisement is a projection.
|
|
92
|
+
assertAgainstSchema(`${name} input`, inputValidator, args ?? {});
|
|
93
|
+
const provider = await ensureProvider(exec?.signal);
|
|
94
|
+
const payload = await provider.invoke({
|
|
95
|
+
implementationId: deps.implementationId,
|
|
96
|
+
input: asJsonObject(args ?? {}),
|
|
97
|
+
context: deps.invocationContext(exec),
|
|
98
|
+
});
|
|
99
|
+
assertAgainstSchema(`${name} output`, outputValidator, payload);
|
|
100
|
+
return payload;
|
|
101
|
+
},
|
|
102
|
+
async dispose() {
|
|
103
|
+
if (disposed)
|
|
104
|
+
return;
|
|
105
|
+
disposed = true;
|
|
106
|
+
const pending = providerPromise;
|
|
107
|
+
providerPromise = undefined;
|
|
108
|
+
if (pending === undefined)
|
|
109
|
+
return;
|
|
110
|
+
try {
|
|
111
|
+
const provider = await pending;
|
|
112
|
+
await provider.dispose();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// A disposer must not throw: deactivation continues, and the reason is recorded.
|
|
116
|
+
deps.onError?.(`${deps.extensionId}: ${name} teardown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=model-runtime-lane.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `model-runtime` recipe: parsing, schema files and the provider factory binding
|
|
3
|
+
* (docs/runtime/PHASE2.md).
|
|
4
|
+
*
|
|
5
|
+
* WHY THE HOST OWNS THE PARSER. The recipe decides what the host will register and which
|
|
6
|
+
* module it will import, so it is a TRUST BOUNDARY. Validating it with code imported from
|
|
7
|
+
* the extension would let the extension define what counts as valid; importing a dedicated
|
|
8
|
+
* runtime-recipes package would put a Phase-2 runtime package in the host's dependency
|
|
9
|
+
* graph, which AT-25 and ARCH-02 exist to prevent. The rules are therefore structural and
|
|
10
|
+
* live here: data only, no engine, no model, no vendor SDK.
|
|
11
|
+
*
|
|
12
|
+
* WHAT THE HOST DOES DELEGATE — and the whole point of the recipe — is the ENGINE. The
|
|
13
|
+
* factory is resolved with `createRequire` anchored at the EXTENSION's own package.json, so
|
|
14
|
+
* a provider package that the extension installed is found, and one that only the host
|
|
15
|
+
* happens to have is not treated as available. The host declares no runtime; the package
|
|
16
|
+
* ships one and names it.
|
|
17
|
+
*
|
|
18
|
+
* @module @try-works/dsh-tool-mode/model-runtime-recipe
|
|
19
|
+
*/
|
|
20
|
+
import type { ModelProviderV1, PreparedAssetMapV1 } from '@try-works/tool-mode-extension-sdk';
|
|
21
|
+
/** The accelerators a recipe may request; the host decides which are actually available. */
|
|
22
|
+
export type ModelRuntimeAccelerator = 'cpu' | 'dml' | 'webgpu';
|
|
23
|
+
/** The artifact formats the lane knows how to name. */
|
|
24
|
+
export type ModelRuntimeFormat = 'onnx' | 'tflite' | 'gguf';
|
|
25
|
+
/** A parsed, validated `model-runtime` recipe. */
|
|
26
|
+
export interface ModelRuntimeRecipe {
|
|
27
|
+
readonly kind: 'model-runtime';
|
|
28
|
+
readonly schemaVersion: 1;
|
|
29
|
+
readonly provider: {
|
|
30
|
+
readonly package: string;
|
|
31
|
+
readonly export: string;
|
|
32
|
+
readonly accelerators?: readonly ModelRuntimeAccelerator[];
|
|
33
|
+
};
|
|
34
|
+
readonly model: {
|
|
35
|
+
readonly assetSet: string;
|
|
36
|
+
readonly fileName: string;
|
|
37
|
+
readonly format: ModelRuntimeFormat;
|
|
38
|
+
};
|
|
39
|
+
readonly processor: {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
};
|
|
42
|
+
readonly limits: {
|
|
43
|
+
readonly maxInputBytes: number;
|
|
44
|
+
readonly timeoutMs: number;
|
|
45
|
+
};
|
|
46
|
+
readonly tool: {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly description: string;
|
|
49
|
+
readonly inputSchema: string;
|
|
50
|
+
readonly outputSchema: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Why a recipe or its binding was refused. Each is a NAMED refusal, never a silent no-op. */
|
|
54
|
+
export type ModelRuntimeRefusalCode = 'not-model-runtime' | 'unknown-key' | 'bad-schema-version' | 'bad-provider' | 'bad-model' | 'bad-processor' | 'bad-limits' | 'bad-tool' | 'schema-unreadable' | 'factory-unresolved' | 'factory-import-failed' | 'factory-not-function';
|
|
55
|
+
/** A refusal with the exact reason the operator needs. */
|
|
56
|
+
export interface ModelRuntimeRefusal {
|
|
57
|
+
readonly ok: false;
|
|
58
|
+
readonly code: ModelRuntimeRefusalCode;
|
|
59
|
+
readonly detail: string;
|
|
60
|
+
}
|
|
61
|
+
/** A parsed recipe, or a refusal. */
|
|
62
|
+
export type ModelRuntimeRecipeResult = {
|
|
63
|
+
readonly ok: true;
|
|
64
|
+
readonly recipe: ModelRuntimeRecipe;
|
|
65
|
+
} | ModelRuntimeRefusal;
|
|
66
|
+
/** The engine binding a recipe names. */
|
|
67
|
+
export type ModelRuntimeProviderFactory = (recipe: ModelRuntimeRecipe, assets: Readonly<PreparedAssetMapV1>) => ModelProviderV1;
|
|
68
|
+
/**
|
|
69
|
+
* Whether an unvalidated recipe claims to be a `model-runtime` recipe.
|
|
70
|
+
*
|
|
71
|
+
* The host branches on this BEFORE any harness behaviour: a harness recipe (or one with no
|
|
72
|
+
* `kind`) keeps the existing path untouched.
|
|
73
|
+
* @param raw - the recipe as read from disk.
|
|
74
|
+
* @returns whether this is the model-runtime lane's business.
|
|
75
|
+
*/
|
|
76
|
+
export declare function isModelRuntimeRecipe(raw: unknown): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Parse and validate a `model-runtime` recipe (PHASE2 rules).
|
|
79
|
+
* @param raw - the recipe as read from disk.
|
|
80
|
+
* @returns the typed recipe or a named refusal.
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseModelRuntimeRecipe(raw: unknown): ModelRuntimeRecipeResult;
|
|
83
|
+
/** The result of reading one schema file. */
|
|
84
|
+
export type ModelRuntimeSchemaResult = {
|
|
85
|
+
readonly ok: true;
|
|
86
|
+
readonly schema: Readonly<Record<string, unknown>>;
|
|
87
|
+
} | ModelRuntimeRefusal;
|
|
88
|
+
/**
|
|
89
|
+
* Read one package-relative schema file, refusing anything that could leave the package.
|
|
90
|
+
* @param installPath - the installed package directory.
|
|
91
|
+
* @param relative - the recipe's `./...` path.
|
|
92
|
+
* @returns the parsed schema or a named refusal.
|
|
93
|
+
*/
|
|
94
|
+
export declare function readModelRuntimeSchema(installPath: string, relative: string): ModelRuntimeSchemaResult;
|
|
95
|
+
/** The result of resolving the recipe's provider module (a path, not an import). */
|
|
96
|
+
export type ProviderModuleResult = {
|
|
97
|
+
readonly ok: true;
|
|
98
|
+
readonly resolved: string;
|
|
99
|
+
} | ModelRuntimeRefusal;
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the recipe's provider module from the EXTENSION's own package, without importing it.
|
|
102
|
+
*
|
|
103
|
+
* Resolution is synchronous, which is what lets `registerDeclarative` — a synchronous host
|
|
104
|
+
* hook — refuse a package that cannot be resolved AT REGISTRATION, instead of registering a
|
|
105
|
+
* tool that cannot run. The import itself stays deferred to first call, so activation remains
|
|
106
|
+
* lightweight (EXT-19).
|
|
107
|
+
* @param options - the install directory and the parsed recipe.
|
|
108
|
+
* @returns the resolved module path or a named refusal.
|
|
109
|
+
*/
|
|
110
|
+
export declare function resolveProviderModule(options: {
|
|
111
|
+
readonly installPath: string;
|
|
112
|
+
readonly recipe: ModelRuntimeRecipe;
|
|
113
|
+
}): ProviderModuleResult;
|
|
114
|
+
/** The result of importing the recipe's provider factory. */
|
|
115
|
+
export type ProviderFactoryResult = {
|
|
116
|
+
readonly ok: true;
|
|
117
|
+
readonly factory: ModelRuntimeProviderFactory;
|
|
118
|
+
} | ModelRuntimeRefusal;
|
|
119
|
+
/**
|
|
120
|
+
* Import the provider factory the recipe names, resolving from the EXTENSION's own package.
|
|
121
|
+
*
|
|
122
|
+
* The anchor is the extension's `package.json`, not the host's module graph: a package that
|
|
123
|
+
* declares an engine dependency resolves it from its own install, and the host never
|
|
124
|
+
* carries one (AT-25, ARCH-02).
|
|
125
|
+
* @param options - the install directory and the parsed recipe.
|
|
126
|
+
* @returns the factory or a named refusal.
|
|
127
|
+
*/
|
|
128
|
+
export declare function importProviderFactory(options: {
|
|
129
|
+
readonly installPath: string;
|
|
130
|
+
readonly recipe: ModelRuntimeRecipe;
|
|
131
|
+
}): Promise<ProviderFactoryResult>;
|
|
132
|
+
//# sourceMappingURL=model-runtime-recipe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-runtime-recipe.d.ts","sourceRoot":"","sources":["../src/model-runtime-recipe.ts"],"names":[],"mappings":"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"}
|