@try-works/dsh-model-as-tool 0.0.0-stage → 0.1.1
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 +245 -2
- package/catalog/desert-ant-models.json +629 -0
- package/catalog/playbooks.json +531 -0
- package/cordis.patch.yml +39 -0
- package/lib/adapters/index.d.ts +48 -0
- package/lib/adapters/index.d.ts.map +1 -0
- package/lib/adapters/index.js +67 -0
- package/lib/adapters/tongue.d.ts +23 -0
- package/lib/adapters/tongue.d.ts.map +1 -0
- package/lib/adapters/tongue.js +85 -0
- package/lib/adapters/types.d.ts +95 -0
- package/lib/adapters/types.d.ts.map +1 -0
- package/lib/adapters/types.js +85 -0
- package/lib/adapters/voz.d.ts +22 -0
- package/lib/adapters/voz.d.ts.map +1 -0
- package/lib/adapters/voz.js +90 -0
- package/lib/capability.d.ts +58 -0
- package/lib/capability.d.ts.map +1 -0
- package/lib/capability.js +175 -0
- package/lib/catalog.d.ts +37 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +217 -0
- package/lib/index.d.ts +120 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +391 -0
- package/lib/playbook.d.ts +42 -0
- package/lib/playbook.d.ts.map +1 -0
- package/lib/playbook.js +65 -0
- package/lib/registrar.d.ts +59 -0
- package/lib/registrar.d.ts.map +1 -0
- package/lib/registrar.js +136 -0
- package/lib/skills.d.ts +25 -0
- package/lib/skills.d.ts.map +1 -0
- package/lib/skills.js +102 -0
- package/lib/state.d.ts +100 -0
- package/lib/state.d.ts.map +1 -0
- package/lib/state.js +160 -0
- package/lib/transport.d.ts +158 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +332 -0
- package/lib/types.d.ts +111 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +8 -0
- package/package.json +83 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-model-as-tool — install on-device models as DSH tools and skills.
|
|
3
|
+
*
|
|
4
|
+
* The plugin composes five layers: a JSON catalog describing every model, a
|
|
5
|
+
* capability resolver that decides which of them can run in this process, an
|
|
6
|
+
* adapter per runnable model, a registrar that turns each one into a DSH tool,
|
|
7
|
+
* and a skill per tool that teaches the agent when to reach for it.
|
|
8
|
+
*
|
|
9
|
+
* Nothing is imported from a model package at plugin load, so a deployment that
|
|
10
|
+
* installs none of them still starts cleanly and reports exactly what is
|
|
11
|
+
* missing. A model only becomes a tool when its adapter can really invoke it.
|
|
12
|
+
*
|
|
13
|
+
* @module @try-works/dsh-model-as-tool
|
|
14
|
+
*/
|
|
15
|
+
import z from '@deepseek-ai/schemastery';
|
|
16
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
17
|
+
import { loadCatalog } from "./catalog.js";
|
|
18
|
+
import { resolveCapability } from "./capability.js";
|
|
19
|
+
import { createAdapter } from "./adapters/index.js";
|
|
20
|
+
import { registerModelTool } from "./registrar.js";
|
|
21
|
+
import { renderSkillBody } from "./skills.js";
|
|
22
|
+
import { loadPlaybooks } from "./playbook.js";
|
|
23
|
+
import { commandExists, DEFAULT_CLI_COMMAND } from "./transport.js";
|
|
24
|
+
import { createFileStateStore, createMemoryStateStore, defaultStateFile, resolveSelection, withChoice, } from "./state.js";
|
|
25
|
+
export const name = 'model-tools';
|
|
26
|
+
export const inject = ['tools'];
|
|
27
|
+
/** Validate and default the plugin configuration. */
|
|
28
|
+
export const Config = z.object({
|
|
29
|
+
catalogFile: z.string(),
|
|
30
|
+
playbookFile: z.string(),
|
|
31
|
+
stateFile: z.string(),
|
|
32
|
+
persist: z.boolean().default(true),
|
|
33
|
+
models: z.array(z.string()).default([]),
|
|
34
|
+
exclude: z.array(z.string()).default([]),
|
|
35
|
+
exposeUnavailable: z.boolean().default(false),
|
|
36
|
+
registerSkills: z.boolean().default(true),
|
|
37
|
+
registerControlTool: z.boolean().default(true),
|
|
38
|
+
cli: z.object({
|
|
39
|
+
enabled: z.boolean().default(true),
|
|
40
|
+
command: z.string().default(DEFAULT_CLI_COMMAND),
|
|
41
|
+
}),
|
|
42
|
+
remote: z.object({
|
|
43
|
+
endpoint: z.string(),
|
|
44
|
+
token: z.string(),
|
|
45
|
+
}),
|
|
46
|
+
allowNpm: z.boolean().default(true),
|
|
47
|
+
});
|
|
48
|
+
/** Render the model list the control tool returns, and the plugin logs. */
|
|
49
|
+
function describeModels(resolved) {
|
|
50
|
+
return resolved.map(({ entry, verdict }) => ({
|
|
51
|
+
id: entry.id,
|
|
52
|
+
name: entry.name,
|
|
53
|
+
summary: entry.summary,
|
|
54
|
+
capability: entry.capability,
|
|
55
|
+
input: entry.input,
|
|
56
|
+
tool: entry.tool.name,
|
|
57
|
+
skill: entry.skill.name,
|
|
58
|
+
runnable: verdict.runnable,
|
|
59
|
+
route: verdict.route,
|
|
60
|
+
reason: verdict.reason,
|
|
61
|
+
languages: entry.languages,
|
|
62
|
+
license: entry.license,
|
|
63
|
+
docs: entry.docsUrl,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Register the Desert Ant model tools and skills.
|
|
68
|
+
* @param ctx - the plugin context whose tool registry receives the definitions.
|
|
69
|
+
* @param config - deployment configuration for which models to expose.
|
|
70
|
+
*/
|
|
71
|
+
export function apply(ctx, config = {}) {
|
|
72
|
+
const entries = loadCatalog(config.catalogFile);
|
|
73
|
+
const playbooks = loadPlaybooks(config.playbookFile);
|
|
74
|
+
const installed = new Map();
|
|
75
|
+
// An empty allowlist means "no explicit selection" rather than "select
|
|
76
|
+
// nothing": the config schema materializes a declared array to `[]`.
|
|
77
|
+
const wanted = config.models !== undefined && config.models.length > 0
|
|
78
|
+
? new Set(config.models)
|
|
79
|
+
: undefined;
|
|
80
|
+
const excluded = new Set(config.exclude ?? []);
|
|
81
|
+
const exposeUnavailable = config.exposeUnavailable ?? false;
|
|
82
|
+
const registerSkills = config.registerSkills ?? true;
|
|
83
|
+
// Persistence: the config states the deployment default, the store records the
|
|
84
|
+
// user's choices on top of it, so enable/disable outlives the session.
|
|
85
|
+
const persist = config.persist ?? true;
|
|
86
|
+
const stateStore = persist
|
|
87
|
+
? createFileStateStore(config.stateFile ?? defaultStateFile())
|
|
88
|
+
: createMemoryStateStore();
|
|
89
|
+
let persisted = stateStore.read();
|
|
90
|
+
const cliCommand = config.cli?.command ?? DEFAULT_CLI_COMMAND;
|
|
91
|
+
const cliEnabled = config.cli?.enabled ?? true;
|
|
92
|
+
// Established once: probing PATH per model would repeat identical work and
|
|
93
|
+
// make the reported reasons depend on when they were read.
|
|
94
|
+
const cliAvailable = cliEnabled && commandExists(cliCommand);
|
|
95
|
+
const remoteEndpoint = config.remote?.endpoint;
|
|
96
|
+
const transportOptions = {
|
|
97
|
+
cliCommand,
|
|
98
|
+
...remoteEndpoint !== undefined ? { endpoint: remoteEndpoint } : {},
|
|
99
|
+
...config.remote?.token !== undefined ? { token: config.remote.token } : {},
|
|
100
|
+
};
|
|
101
|
+
const resolved = entries.map(entry => ({
|
|
102
|
+
entry,
|
|
103
|
+
verdict: resolveCapability(entry, {
|
|
104
|
+
cliAvailable,
|
|
105
|
+
remoteEndpoint,
|
|
106
|
+
...config.allowNpm !== undefined ? { allowNpm: config.allowNpm } : {},
|
|
107
|
+
}),
|
|
108
|
+
}));
|
|
109
|
+
const byId = new Map(resolved.map(item => [item.entry.id, item]));
|
|
110
|
+
/**
|
|
111
|
+
* Register one model's tool and skill.
|
|
112
|
+
*
|
|
113
|
+
* Idempotent: registering an already-installed model is a no-op, so the
|
|
114
|
+
* control tool can call it without first checking.
|
|
115
|
+
* @param item - the resolved model to install.
|
|
116
|
+
* @returns whether the model is installed after the call.
|
|
117
|
+
*/
|
|
118
|
+
const install = (item) => {
|
|
119
|
+
const { entry, verdict } = item;
|
|
120
|
+
if (installed.has(entry.id))
|
|
121
|
+
return true;
|
|
122
|
+
if (!verdict.runnable && !exposeUnavailable)
|
|
123
|
+
return false;
|
|
124
|
+
const adapter = createAdapter(entry, { route: verdict.route, ...transportOptions });
|
|
125
|
+
if (adapter === undefined) {
|
|
126
|
+
// Runnable but no adapter yet: report rather than register a dead tool.
|
|
127
|
+
ctx.logger.info(`model-tools: ${entry.name} can run here (${verdict.route}) but no adapter ships yet; skipping ${entry.tool.name}`);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const registration = registerModelTool(ctx, entry, adapter);
|
|
131
|
+
const record = { ...registration, entry };
|
|
132
|
+
if (registerSkills) {
|
|
133
|
+
const skills = ctx.get('skills');
|
|
134
|
+
if (skills !== undefined) {
|
|
135
|
+
const body = renderSkillBody(entry, playbooks.get(entry.id));
|
|
136
|
+
record.disposeSkill = skills.register({
|
|
137
|
+
name: entry.skill.name,
|
|
138
|
+
description: entry.skill.description,
|
|
139
|
+
...entry.skill.whenToUse !== undefined ? { whenToUse: entry.skill.whenToUse } : {},
|
|
140
|
+
content: body,
|
|
141
|
+
source: 'bundled',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
installed.set(entry.id, record);
|
|
146
|
+
ctx.logger.info(`model-tools: registered ${entry.tool.name} for ${entry.name} (${verdict.route})`);
|
|
147
|
+
return true;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Unregister one model's tool and skill.
|
|
151
|
+
* @param modelId - the catalog id to remove.
|
|
152
|
+
* @returns whether the model was installed.
|
|
153
|
+
*/
|
|
154
|
+
const uninstall = (modelId) => {
|
|
155
|
+
const record = installed.get(modelId);
|
|
156
|
+
if (record === undefined)
|
|
157
|
+
return false;
|
|
158
|
+
record.disposeSkill?.();
|
|
159
|
+
record.dispose();
|
|
160
|
+
installed.delete(modelId);
|
|
161
|
+
ctx.logger.info(`model-tools: unregistered ${record.toolName}`);
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Record one user choice durably.
|
|
166
|
+
*
|
|
167
|
+
* A failed write is reported rather than swallowed: the caller asked for a
|
|
168
|
+
* change that is supposed to survive a restart, and quietly keeping it
|
|
169
|
+
* in-memory would make the next session silently disagree.
|
|
170
|
+
* @param modelId - the model the user acted on.
|
|
171
|
+
* @param enabled - true to turn on, false to turn off.
|
|
172
|
+
* @returns whether the choice was written to the durable store.
|
|
173
|
+
*/
|
|
174
|
+
const remember = (modelId, enabled) => {
|
|
175
|
+
persisted = withChoice(persisted, modelId, enabled);
|
|
176
|
+
if (!persist)
|
|
177
|
+
return false;
|
|
178
|
+
stateStore.write(persisted);
|
|
179
|
+
return true;
|
|
180
|
+
};
|
|
181
|
+
/** The configuration's own selection, before the user's choices are layered on. */
|
|
182
|
+
const configuredIds = resolved
|
|
183
|
+
.filter(item => (wanted === undefined || wanted.has(item.entry.id)) && !excluded.has(item.entry.id))
|
|
184
|
+
.map(item => item.entry.id);
|
|
185
|
+
for (const item of resolved) {
|
|
186
|
+
const { install: selection, disabled } = resolveSelection(configuredIds, persisted);
|
|
187
|
+
if (!selection.has(item.entry.id)) {
|
|
188
|
+
if (disabled.has(item.entry.id) && item.verdict.runnable) {
|
|
189
|
+
ctx.logger.info(`model-tools: ${item.entry.name} stays uninstalled (disabled by the user)`);
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// A model the user enabled that cannot run here is reported, not installed:
|
|
194
|
+
// the persisted choice is kept so it takes effect once a route appears.
|
|
195
|
+
install(item);
|
|
196
|
+
}
|
|
197
|
+
if (config.registerControlTool ?? true) {
|
|
198
|
+
ctx.tools.register(defineTool({
|
|
199
|
+
name: 'model_tools',
|
|
200
|
+
description: 'Inspect and control the on-device models available to this agent: list what each one does and whether it '
|
|
201
|
+
+ 'can run on this machine, install a model\'s tool, or remove one. Installs and removals are remembered '
|
|
202
|
+
+ 'across restarts. Use it before calling a model tool when unsure which one fits, to install a model the '
|
|
203
|
+
+ 'user asked for, or to explain why a model is unavailable here. Models that cannot run in this process '
|
|
204
|
+
+ 'report the exact reason.',
|
|
205
|
+
parameters: {
|
|
206
|
+
action: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
required: true,
|
|
209
|
+
enum: ['list', 'installed', 'describe', 'enable', 'disable', 'reset'],
|
|
210
|
+
description: 'list: every catalogued model and whether it runs here. installed: only the models whose tools are '
|
|
211
|
+
+ 'registered. describe: full detail for one model. enable: install a model\'s tool and remember it. '
|
|
212
|
+
+ 'disable: remove a model\'s tool and remember it. reset: forget the remembered choice for one model, '
|
|
213
|
+
+ 'or for every model when no id is given.',
|
|
214
|
+
},
|
|
215
|
+
model: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
description: 'Model id, required for describe, enable and disable, and optional for reset (for example "tongue" or "title").',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
output: {
|
|
221
|
+
schema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
additionalProperties: false,
|
|
224
|
+
properties: {
|
|
225
|
+
models: {
|
|
226
|
+
type: 'array',
|
|
227
|
+
required: true,
|
|
228
|
+
items: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
properties: {
|
|
232
|
+
id: { type: 'string', required: true },
|
|
233
|
+
name: { type: 'string', required: true },
|
|
234
|
+
summary: { type: 'string', required: true },
|
|
235
|
+
capability: { type: 'string', required: true },
|
|
236
|
+
input: { type: 'string', required: true },
|
|
237
|
+
tool: { type: 'string', required: true },
|
|
238
|
+
skill: { type: 'string', required: true },
|
|
239
|
+
runnable: { type: 'boolean', required: true },
|
|
240
|
+
installed: { type: 'boolean', required: true },
|
|
241
|
+
/** The user's remembered choice for this model. */
|
|
242
|
+
choice: { type: 'string', required: true, enum: ['none', 'enabled', 'disabled'] },
|
|
243
|
+
route: { type: 'string', required: true },
|
|
244
|
+
reason: { type: 'string', required: true },
|
|
245
|
+
languages: {
|
|
246
|
+
required: true,
|
|
247
|
+
oneOf: [
|
|
248
|
+
{ type: 'integer' },
|
|
249
|
+
{ type: 'null' },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
license: { type: 'string', required: true },
|
|
253
|
+
docs: { type: 'string', required: true },
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
installed: { type: 'integer', required: true },
|
|
258
|
+
runnable: { type: 'integer', required: true },
|
|
259
|
+
/** Set by enable/disable/reset: whether the requested change took effect. */
|
|
260
|
+
changed: { type: 'boolean' },
|
|
261
|
+
/** Set by enable/disable/reset: what the call did to the tool surface. */
|
|
262
|
+
action: { type: 'string' },
|
|
263
|
+
/** Set by enable/disable/reset: whether the choice was written to the durable store. */
|
|
264
|
+
persisted: { type: 'boolean' },
|
|
265
|
+
/** Where choices are stored, so a caller can tell the user what to edit. */
|
|
266
|
+
stateFile: { type: 'string' },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
render: (_args, value) => {
|
|
270
|
+
const headline = value.action !== undefined
|
|
271
|
+
? `${value.action}: ${value.changed === true ? 'done' : 'no change'}`
|
|
272
|
+
+ `${value.persisted === true ? ', remembered across restarts' : ''}. `
|
|
273
|
+
: '';
|
|
274
|
+
const lines = value.models.map(model => `- [${model.installed ? 'installed' : model.runnable ? 'ready' : 'unavailable'}] `
|
|
275
|
+
+ `${model.id} (\`${model.tool}\`)${model.choice === 'none' ? '' : ` (${model.choice} by you)`}: `
|
|
276
|
+
+ `${model.summary}`
|
|
277
|
+
+ (model.runnable || model.installed ? '' : `\n why not: ${model.reason}`));
|
|
278
|
+
return [{
|
|
279
|
+
type: 'text',
|
|
280
|
+
text: `${headline}${value.installed} of ${value.models.length} catalogued models are installed as tools; `
|
|
281
|
+
+ `${value.runnable} can run on this machine.\n\n${lines.join('\n')}`,
|
|
282
|
+
}];
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
execute(args, exec) {
|
|
286
|
+
void exec;
|
|
287
|
+
const action = args.action;
|
|
288
|
+
const modelId = args.model;
|
|
289
|
+
/** The user's remembered choice for one model. */
|
|
290
|
+
const choiceOf = (id) => persisted.enabled.includes(id) ? 'enabled' : persisted.disabled.includes(id) ? 'disabled' : 'none';
|
|
291
|
+
/** The current snapshot, with per-model installed and remembered state. */
|
|
292
|
+
const snapshot = (items) => items.map(item => ({
|
|
293
|
+
...describeModels([item])[0],
|
|
294
|
+
installed: installed.has(item.entry.id),
|
|
295
|
+
choice: choiceOf(item.entry.id),
|
|
296
|
+
}));
|
|
297
|
+
const counts = () => ({
|
|
298
|
+
installed: installed.size,
|
|
299
|
+
runnable: resolved.filter(item => item.verdict.runnable).length,
|
|
300
|
+
...persist ? { stateFile: config.stateFile ?? defaultStateFile() } : {},
|
|
301
|
+
});
|
|
302
|
+
if (action === 'reset') {
|
|
303
|
+
// Forgetting a choice is what makes the config authoritative again.
|
|
304
|
+
if (modelId !== undefined) {
|
|
305
|
+
if (!byId.has(modelId))
|
|
306
|
+
throw new Error(`unknown model "${modelId}"`);
|
|
307
|
+
persisted = {
|
|
308
|
+
enabled: persisted.enabled.filter(id => id !== modelId),
|
|
309
|
+
disabled: persisted.disabled.filter(id => id !== modelId),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
persisted = { enabled: [], disabled: [] };
|
|
314
|
+
}
|
|
315
|
+
if (persist)
|
|
316
|
+
stateStore.write(persisted);
|
|
317
|
+
// Re-apply the config's own selection, so a reset takes effect now
|
|
318
|
+
// rather than only at the next start.
|
|
319
|
+
for (const item of resolved) {
|
|
320
|
+
if (configuredIds.includes(item.entry.id))
|
|
321
|
+
install(item);
|
|
322
|
+
else
|
|
323
|
+
uninstall(item.entry.id);
|
|
324
|
+
}
|
|
325
|
+
const models = modelId === undefined ? resolved : [byId.get(modelId)];
|
|
326
|
+
return Promise.resolve({
|
|
327
|
+
models: snapshot(models),
|
|
328
|
+
...counts(),
|
|
329
|
+
changed: true,
|
|
330
|
+
action,
|
|
331
|
+
persisted: persist,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (action === 'describe' || action === 'enable' || action === 'disable') {
|
|
335
|
+
if (modelId === undefined)
|
|
336
|
+
throw new Error(`${action} requires a "model" id`);
|
|
337
|
+
const item = byId.get(modelId);
|
|
338
|
+
if (item === undefined)
|
|
339
|
+
throw new Error(`unknown model "${modelId}"`);
|
|
340
|
+
if (action === 'describe') {
|
|
341
|
+
return Promise.resolve({ models: snapshot([item]), ...counts() });
|
|
342
|
+
}
|
|
343
|
+
const changed = action === 'enable' ? install(item) : uninstall(modelId);
|
|
344
|
+
if (action === 'enable' && !changed) {
|
|
345
|
+
// Say why rather than reporting a silent no-op: the caller asked for
|
|
346
|
+
// a tool and did not get one.
|
|
347
|
+
throw new Error(`${item.entry.name} cannot be installed here. ${item.verdict.reason}`);
|
|
348
|
+
}
|
|
349
|
+
// Only remember a choice that actually took effect: persisting a
|
|
350
|
+
// failed enable would install it silently on the next start.
|
|
351
|
+
const remembered = remember(modelId, action === 'enable');
|
|
352
|
+
return Promise.resolve({
|
|
353
|
+
models: snapshot([item]),
|
|
354
|
+
...counts(),
|
|
355
|
+
changed,
|
|
356
|
+
action,
|
|
357
|
+
persisted: remembered,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
const models = action === 'installed'
|
|
361
|
+
? snapshot(resolved.filter(item => installed.has(item.entry.id)))
|
|
362
|
+
: snapshot(resolved);
|
|
363
|
+
return Promise.resolve({ models, ...counts() });
|
|
364
|
+
},
|
|
365
|
+
presentCall(args) {
|
|
366
|
+
return { card: 'generic', title: `Model tools: ${args.action}`, kind: 'read', rawInput: args.model ?? args.action };
|
|
367
|
+
},
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
// The registry disposes each registration with the plugin fiber, but doing it
|
|
371
|
+
// explicitly keeps model resources (a loaded ONNX session, a cached weight
|
|
372
|
+
// file) bounded by the plugin's own lifetime rather than the process's.
|
|
373
|
+
ctx.effect(() => () => {
|
|
374
|
+
for (const record of installed.values()) {
|
|
375
|
+
record.disposeSkill?.();
|
|
376
|
+
record.dispose();
|
|
377
|
+
}
|
|
378
|
+
installed.clear();
|
|
379
|
+
}, 'model-tools teardown');
|
|
380
|
+
const runnable = resolved.filter(item => item.verdict.runnable).length;
|
|
381
|
+
ctx.logger.info(`model-tools: ${installed.size} model tool(s) registered from ${entries.length} catalogued models `
|
|
382
|
+
+ `(${runnable} runnable in this process)`);
|
|
383
|
+
}
|
|
384
|
+
export { loadCatalog, CatalogError, DEFAULT_CATALOG_FILE } from "./catalog.js";
|
|
385
|
+
export { resolveCapability, resolveCatalog, canResolve, packageSpecifier } from "./capability.js";
|
|
386
|
+
export { createAdapter, ADAPTER_FACTORIES } from "./adapters/index.js";
|
|
387
|
+
export { buildArgv, commandExists, createCliTransport, createRemoteTransport, parseBridgeResponse, parseCliOutcome, splitArguments, BRIDGE_PATH, DEFAULT_CLI_COMMAND, } from "./transport.js";
|
|
388
|
+
export { renderSkillBody } from "./skills.js";
|
|
389
|
+
export { loadPlaybooks, DEFAULT_PLAYBOOK_FILE } from "./playbook.js";
|
|
390
|
+
export { createFileStateStore, createMemoryStateStore, defaultStateFile, parseState, resolveSelection, withChoice, EMPTY_STATE, StateError, } from "./state.js";
|
|
391
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbooks: the per-model knowledge that turns a tool into a usable capability.
|
|
3
|
+
*
|
|
4
|
+
* A catalog entry says what a model *is*; a playbook says what it is *for* — the
|
|
5
|
+
* concrete jobs an agent should reach for it on, the jobs it should not, and how
|
|
6
|
+
* the model composes with its neighbours. Playbooks live in their own document
|
|
7
|
+
* so they can be revised without touching the catalog or the code.
|
|
8
|
+
*
|
|
9
|
+
* @module @try-works/dsh-model-as-tool/playbook
|
|
10
|
+
*/
|
|
11
|
+
/** The authored playbook for one model. */
|
|
12
|
+
export interface ModelPlaybook {
|
|
13
|
+
/** Concrete jobs this model is the right tool for. */
|
|
14
|
+
readonly useCases: readonly string[];
|
|
15
|
+
/** Trigger conditions: when the agent should reach for this tool. */
|
|
16
|
+
readonly whenToUse: readonly string[];
|
|
17
|
+
/** Anti-triggers: when this tool is the wrong move. */
|
|
18
|
+
readonly whenNotToUse: readonly string[];
|
|
19
|
+
/** Ordered steps describing how to drive the model well. */
|
|
20
|
+
readonly workflow: readonly string[];
|
|
21
|
+
/** How this model composes with the others, by model id or tool name. */
|
|
22
|
+
readonly pairsWith: readonly string[];
|
|
23
|
+
/** Limits, cost and failure modes worth knowing before calling. */
|
|
24
|
+
readonly caveats: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
/** Absolute path of the playbook document shipped with this package. */
|
|
27
|
+
export declare const DEFAULT_PLAYBOOK_FILE: string;
|
|
28
|
+
/** Raised when the playbook document is present but malformed. */
|
|
29
|
+
export declare class PlaybookError extends Error {
|
|
30
|
+
readonly name = "PlaybookError";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse a playbook document into a per-model map.
|
|
34
|
+
*
|
|
35
|
+
* An absent document is not an error: playbooks are editorial content, and the
|
|
36
|
+
* skill renderer falls back to the catalog's own routing description.
|
|
37
|
+
* @param file - absolute path of the playbook document.
|
|
38
|
+
* @returns the playbooks by model id, empty when the document does not exist.
|
|
39
|
+
* @throws PlaybookError when the document exists but is not a valid playbook map.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadPlaybooks(file?: string): Map<string, ModelPlaybook>;
|
|
42
|
+
//# sourceMappingURL=playbook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playbook.d.ts","sourceRoot":"","sources":["../src/playbook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,2CAA2C;AAC3C,MAAM,WAAW,aAAa;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,uDAAuD;IACvD,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IACxC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AAED,wEAAwE;AACxE,eAAO,MAAM,qBAAqB,QAEjC,CAAA;AAED,kEAAkE;AAClE,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAkB,IAAI,mBAAkB;CACzC;AAID;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,GAAE,MAA8B,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAiC9F"}
|
package/lib/playbook.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbooks: the per-model knowledge that turns a tool into a usable capability.
|
|
3
|
+
*
|
|
4
|
+
* A catalog entry says what a model *is*; a playbook says what it is *for* — the
|
|
5
|
+
* concrete jobs an agent should reach for it on, the jobs it should not, and how
|
|
6
|
+
* the model composes with its neighbours. Playbooks live in their own document
|
|
7
|
+
* so they can be revised without touching the catalog or the code.
|
|
8
|
+
*
|
|
9
|
+
* @module @try-works/dsh-model-as-tool/playbook
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
/** Absolute path of the playbook document shipped with this package. */
|
|
14
|
+
export const DEFAULT_PLAYBOOK_FILE = fileURLToPath(new URL('../catalog/playbooks.json', import.meta.url));
|
|
15
|
+
/** Raised when the playbook document is present but malformed. */
|
|
16
|
+
export class PlaybookError extends Error {
|
|
17
|
+
name = 'PlaybookError';
|
|
18
|
+
}
|
|
19
|
+
const STRING_ARRAY_FIELDS = ['useCases', 'whenToUse', 'whenNotToUse', 'workflow', 'pairsWith', 'caveats'];
|
|
20
|
+
/**
|
|
21
|
+
* Parse a playbook document into a per-model map.
|
|
22
|
+
*
|
|
23
|
+
* An absent document is not an error: playbooks are editorial content, and the
|
|
24
|
+
* skill renderer falls back to the catalog's own routing description.
|
|
25
|
+
* @param file - absolute path of the playbook document.
|
|
26
|
+
* @returns the playbooks by model id, empty when the document does not exist.
|
|
27
|
+
* @throws PlaybookError when the document exists but is not a valid playbook map.
|
|
28
|
+
*/
|
|
29
|
+
export function loadPlaybooks(file = DEFAULT_PLAYBOOK_FILE) {
|
|
30
|
+
let text;
|
|
31
|
+
try {
|
|
32
|
+
text = readFileSync(file, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return new Map();
|
|
36
|
+
}
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new PlaybookError(`${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
45
|
+
throw new PlaybookError(`${file} must contain a JSON object keyed by model id`);
|
|
46
|
+
}
|
|
47
|
+
const playbooks = new Map();
|
|
48
|
+
for (const [modelId, raw] of Object.entries(parsed)) {
|
|
49
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
50
|
+
throw new PlaybookError(`${file}: playbook for "${modelId}" must be an object`);
|
|
51
|
+
}
|
|
52
|
+
const record = raw;
|
|
53
|
+
const playbook = {};
|
|
54
|
+
for (const field of STRING_ARRAY_FIELDS) {
|
|
55
|
+
const value = record[field] ?? [];
|
|
56
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
|
|
57
|
+
throw new PlaybookError(`${file}: playbook "${modelId}" field "${field}" must be an array of strings`);
|
|
58
|
+
}
|
|
59
|
+
playbook[field] = value;
|
|
60
|
+
}
|
|
61
|
+
playbooks.set(modelId, playbook);
|
|
62
|
+
}
|
|
63
|
+
return playbooks;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=playbook.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registrar: turns each runnable catalog model into a DSH tool.
|
|
3
|
+
*
|
|
4
|
+
* The registrar owns the translation from the catalog's declarative tool spec
|
|
5
|
+
* into a `defineTool` definition, including the parameter schema, the canonical
|
|
6
|
+
* output contract and the model-facing rendering. Registration goes through
|
|
7
|
+
* `ctx.tools.register`, whose returned disposer is the only unregister API, so
|
|
8
|
+
* the plugin can add or drop a model's tool at runtime as capability changes.
|
|
9
|
+
*
|
|
10
|
+
* @module @try-works/dsh-model-as-tool/registrar
|
|
11
|
+
*/
|
|
12
|
+
import { type ParameterSchemaSpec } from '@deepseek-ai/dsh-tools';
|
|
13
|
+
import type { ModelCatalogEntry } from './types.ts';
|
|
14
|
+
import type { ModelAdapter } from './adapters/index.ts';
|
|
15
|
+
/** The tool spec a catalog entry declares, resolved into a registration. */
|
|
16
|
+
export interface RegisteredModelTool {
|
|
17
|
+
readonly modelId: string;
|
|
18
|
+
readonly toolName: string;
|
|
19
|
+
readonly dispose: () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build the parameter schema for one model's tool.
|
|
23
|
+
*
|
|
24
|
+
* Every catalog parameter is optional except the model's primary input, which
|
|
25
|
+
* the adapter validates as a required non-empty value. Types come from the
|
|
26
|
+
* catalog declaration, so a numeric or list parameter reaches the model as the
|
|
27
|
+
* type it actually is rather than as a string it has to re-parse.
|
|
28
|
+
* @param entry - the catalog entry.
|
|
29
|
+
* @returns the DSH parameter schema spec.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildParameters(entry: ModelCatalogEntry): ParameterSchemaSpec;
|
|
32
|
+
/**
|
|
33
|
+
* Render the tool result for the model.
|
|
34
|
+
*
|
|
35
|
+
* The canonical value is the source of truth; this projection only chooses how
|
|
36
|
+
* to present it. Tongue's shaped result gets a sentence the model can act on
|
|
37
|
+
* directly, while everything else falls back to stable JSON so no information
|
|
38
|
+
* is lost to a lossy humanization.
|
|
39
|
+
* @param entry - the catalog entry.
|
|
40
|
+
* @param value - the canonical value the adapter returned.
|
|
41
|
+
* @returns the model-facing content blocks.
|
|
42
|
+
*/
|
|
43
|
+
export declare function renderResult(entry: ModelCatalogEntry, value: Record<string, unknown>): {
|
|
44
|
+
type: 'text';
|
|
45
|
+
text: string;
|
|
46
|
+
}[];
|
|
47
|
+
/**
|
|
48
|
+
* Register one model's tool on the calling context.
|
|
49
|
+
* @param ctx - the context whose tool registry receives the definition; its scope decides visibility.
|
|
50
|
+
* @param entry - the catalog entry to expose.
|
|
51
|
+
* @param adapter - the adapter that invokes the model.
|
|
52
|
+
* @returns the model id, the tool name, and the disposer returned by the registry.
|
|
53
|
+
*/
|
|
54
|
+
export declare function registerModelTool(ctx: {
|
|
55
|
+
tools: {
|
|
56
|
+
register(definition: unknown): () => void;
|
|
57
|
+
};
|
|
58
|
+
}, entry: ModelCatalogEntry, adapter: ModelAdapter): RegisteredModelTool;
|
|
59
|
+
//# sourceMappingURL=registrar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registrar.d.ts","sourceRoot":"","sources":["../src/registrar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAA0C,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACzG,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,YAAY,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B;AAwBD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,mBAAmB,CAY7E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CA0BlC;AAcD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE;IAAE,KAAK,EAAE;QAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,IAAI,CAAA;KAAE,CAAA;CAAE,EAC7D,KAAK,EAAE,iBAAiB,EACxB,OAAO,EAAE,YAAY,GACpB,mBAAmB,CA6BrB"}
|