@myvillage/cli 2.12.0 → 2.13.0
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/package.json
CHANGED
|
@@ -73,8 +73,19 @@ export const BUILTIN_TOOL_DEFS = [
|
|
|
73
73
|
},
|
|
74
74
|
];
|
|
75
75
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
76
|
+
/** Longest example input shown to an agent, so a model's description stays short. */
|
|
77
|
+
const MAX_EXAMPLE_CHARS = 400;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Tell an agent what a local model is for, what it takes, and what it gives
|
|
81
|
+
* back, so it can decide whether the model fits the job.
|
|
82
|
+
*
|
|
83
|
+
* `extras` come from the dataset the model learned from: `labelNames` (the
|
|
84
|
+
* villager's names for each label id, e.g. waiting → "Spam and Junk"),
|
|
85
|
+
* `learnedFrom` (the dataset's description) and `example` (one training input,
|
|
86
|
+
* so the agent sends text shaped the same way).
|
|
87
|
+
*/
|
|
88
|
+
export function describeLocalModel(m, extras = {}) {
|
|
78
89
|
const f = m.featurization ?? {};
|
|
79
90
|
const modality = f.modality ?? m.modality;
|
|
80
91
|
let takes;
|
|
@@ -88,9 +99,18 @@ export function describeLocalModel(m) {
|
|
|
88
99
|
} else {
|
|
89
100
|
takes = `a list of ${m.inputDim ?? 'some'} numbers`;
|
|
90
101
|
}
|
|
102
|
+
const names = extras.labelNames ?? {};
|
|
103
|
+
const label = (l) => (names[l] && names[l] !== l ? `"${l}" (${names[l]})` : `"${l}"`);
|
|
91
104
|
const gives =
|
|
92
105
|
m.taskType === 'classification' && Array.isArray(m.labels) && m.labels.length
|
|
93
|
-
? `sorts it into: ${m.labels.join(', ')}`
|
|
106
|
+
? `sorts it into: ${m.labels.map(label).join(', ')}`
|
|
94
107
|
: 'gives back a number';
|
|
95
|
-
|
|
108
|
+
const lines = [`- "${m.slug}" (${m.name}): takes ${takes}; ${gives}.`];
|
|
109
|
+
if (extras.learnedFrom) lines.push(` Learned from: ${extras.learnedFrom}`);
|
|
110
|
+
if (extras.example != null && modality !== 'image' && modality !== 'audio') {
|
|
111
|
+
let ex = typeof extras.example === 'string' ? extras.example : JSON.stringify(extras.example);
|
|
112
|
+
if (ex.length > MAX_EXAMPLE_CHARS) ex = ex.slice(0, MAX_EXAMPLE_CHARS) + '…';
|
|
113
|
+
lines.push(` Send input shaped like this training example: ${JSON.stringify(ex)}`);
|
|
114
|
+
}
|
|
115
|
+
return lines.join('\n');
|
|
96
116
|
}
|
|
@@ -37,10 +37,8 @@ import {
|
|
|
37
37
|
DATASET_READ_DESCRIPTION,
|
|
38
38
|
DATASET_SPLIT_DESCRIPTION,
|
|
39
39
|
MODEL_PREDICT_DESCRIPTION,
|
|
40
|
-
describeLocalModel,
|
|
41
40
|
} from './builtin-tools-registry.js';
|
|
42
|
-
import { readAgentModels } from '../utils/agent-models.js';
|
|
43
|
-
import { readModelManifest as readModelManifestSync } from '../utils/model-workspace.js';
|
|
41
|
+
import { readAgentModels, describeAgentModel, modelDatasetExtras } from '../utils/agent-models.js';
|
|
44
42
|
import { readToolsYaml } from '../utils/local-agent.js';
|
|
45
43
|
|
|
46
44
|
export { BUILTIN_TOOL_DEFS } from './builtin-tools-registry.js';
|
|
@@ -106,11 +104,8 @@ function applyBuiltinAllowlist(builtins, agentName) {
|
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
/** model_predict's description, naming the models this agent was given. */
|
|
109
|
-
function modelPredictDescription(slugs
|
|
110
|
-
const lines = slugs.map(
|
|
111
|
-
const m = readModelManifest(slug);
|
|
112
|
-
return m ? describeLocalModel(m) : `- "${slug}": not on this computer yet.`;
|
|
113
|
-
});
|
|
107
|
+
function modelPredictDescription(slugs) {
|
|
108
|
+
const lines = slugs.map(describeAgentModel);
|
|
114
109
|
return `${MODEL_PREDICT_DESCRIPTION} Use one whenever its job matches what you are doing. The models you can use (the quoted slug is the modelId):\n${lines.join('\n')}`;
|
|
115
110
|
}
|
|
116
111
|
|
|
@@ -292,7 +287,7 @@ export function getBuiltinTools(agentName, config) {
|
|
|
292
287
|
}),
|
|
293
288
|
|
|
294
289
|
model_predict: tool({
|
|
295
|
-
description: modelPredictDescription(grantedModels
|
|
290
|
+
description: modelPredictDescription(grantedModels),
|
|
296
291
|
inputSchema: z.object({
|
|
297
292
|
modelId: z.string().describe('Slug of the local trained model'),
|
|
298
293
|
input: z
|
|
@@ -331,7 +326,10 @@ export function getBuiltinTools(agentName, config) {
|
|
|
331
326
|
const { predictWithModel } = await import('../utils/model-trainer.js');
|
|
332
327
|
const record = readModelManifest(modelId);
|
|
333
328
|
const model = await loadModel(modelId);
|
|
334
|
-
|
|
329
|
+
const result = await predictWithModel(model, record, input);
|
|
330
|
+
// Name the label the way the villager named it (e.g. "Spam and Junk").
|
|
331
|
+
const name = result?.label != null ? modelDatasetExtras(record).labelNames?.[result.label] : undefined;
|
|
332
|
+
return name && name !== result.label ? { ...result, labelName: name } : result;
|
|
335
333
|
} catch (e) {
|
|
336
334
|
return { error: e.message };
|
|
337
335
|
}
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { listDatasets } from '../utils/dataset-workspace.js';
|
|
15
15
|
import { listModels } from '../utils/model-workspace.js';
|
|
16
|
-
import {
|
|
17
|
-
import { readAgentModels } from '../utils/agent-models.js';
|
|
16
|
+
import { readAgentModels, describeAgentModel } from '../utils/agent-models.js';
|
|
18
17
|
|
|
19
18
|
export async function gatherContext(agentConfig, lastCheckIn, recentActions = [], agentName = agentConfig?.name) {
|
|
20
19
|
const parts = [];
|
|
@@ -57,7 +56,7 @@ export async function gatherContext(agentConfig, lastCheckIn, recentActions = []
|
|
|
57
56
|
const models = listModels().filter((m) => granted.has(m.slug));
|
|
58
57
|
if (models.length > 0) {
|
|
59
58
|
parts.push('\nModels you can run with model_predict (the quoted slug is the modelId). Use one whenever its job matches what you are doing:');
|
|
60
|
-
for (const m of models.slice(0, 25)) parts.push(
|
|
59
|
+
for (const m of models.slice(0, 25)) parts.push(describeAgentModel(m.slug));
|
|
61
60
|
}
|
|
62
61
|
} catch {
|
|
63
62
|
/* no local models / store unavailable — skip */
|
|
@@ -10,6 +10,7 @@ import { join } from 'path';
|
|
|
10
10
|
import { homedir } from 'os';
|
|
11
11
|
import { getMCPTools, cleanupMCPClients } from './mcp-client.js';
|
|
12
12
|
import { getBuiltinTools } from './builtin-tools.js';
|
|
13
|
+
import { readAgentModels } from '../utils/agent-models.js';
|
|
13
14
|
import { gatherContext } from './context.js';
|
|
14
15
|
import { isWithinActiveHours, getNextCheckInMs } from './scheduler.js';
|
|
15
16
|
import { parse as parseYaml } from 'yaml';
|
|
@@ -140,7 +141,8 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
140
141
|
const result = await getMCPTools(agentDir, config);
|
|
141
142
|
tools = result.tools;
|
|
142
143
|
// Merge native built-in tools (in-process; no MCP server). e.g. dataset_append.
|
|
143
|
-
|
|
144
|
+
const builtins = getBuiltinTools(agentName, config);
|
|
145
|
+
Object.assign(tools, builtins);
|
|
144
146
|
const servers = result.servers;
|
|
145
147
|
const totalTools = Object.keys(tools).length;
|
|
146
148
|
const failed = servers.filter(s => s.status === 'failed');
|
|
@@ -150,6 +152,9 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
150
152
|
model: modelId,
|
|
151
153
|
totalTools,
|
|
152
154
|
servers,
|
|
155
|
+
// Named so the log shows what the agent could use, models included.
|
|
156
|
+
builtinTools: Object.keys(builtins),
|
|
157
|
+
models: readAgentModels(agentName),
|
|
153
158
|
});
|
|
154
159
|
|
|
155
160
|
// Surface failed MCP connections as an error event too, so they show up
|
|
@@ -16,10 +16,13 @@ import { createHash } from 'crypto';
|
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { readToolsYaml } from './local-agent.js';
|
|
19
|
-
import { getModelDir, modelExists } from './model-workspace.js';
|
|
19
|
+
import { getModelDir, modelExists, readModelManifest } from './model-workspace.js';
|
|
20
|
+
import { readManifest as readDatasetManifest, readRows } from './dataset-workspace.js';
|
|
21
|
+
import { describeLocalModel } from '../agent-runtime/builtin-tools-registry.js';
|
|
20
22
|
|
|
21
23
|
const MODEL_FILES = ['manifest.json', 'model.json', 'weights.b64'];
|
|
22
24
|
const SYNCED_MARKER = '.synced-hash';
|
|
25
|
+
const ABOUT_FILE = 'about.json';
|
|
23
26
|
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,127}$/;
|
|
24
27
|
|
|
25
28
|
/** Slugs listed under `models:` in an agent's tools.yaml (deduped, in order). */
|
|
@@ -46,7 +49,9 @@ export function bundleAgentModels(agentName) {
|
|
|
46
49
|
const dir = getModelDir(slug);
|
|
47
50
|
try {
|
|
48
51
|
const files = Object.fromEntries(MODEL_FILES.map((f) => [f, readFileSync(join(dir, f), 'utf-8')]));
|
|
49
|
-
|
|
52
|
+
const manifest = JSON.parse(files['manifest.json']);
|
|
53
|
+
// What the dataset says about the model, since its dataset stays here.
|
|
54
|
+
out[slug] = { hash: hashFiles(files), files, about: modelDatasetExtras(manifest) };
|
|
50
55
|
} catch {
|
|
51
56
|
/* half-written or partly deleted model: skip it */
|
|
52
57
|
}
|
|
@@ -75,7 +80,10 @@ export function writeSyncedModel(slug, entry) {
|
|
|
75
80
|
const markerPath = join(dir, SYNCED_MARKER);
|
|
76
81
|
if (modelExists(slug)) {
|
|
77
82
|
if (!existsSync(markerPath)) return 'kept-local';
|
|
78
|
-
if (readFileSync(markerPath, 'utf-8').trim() === hash)
|
|
83
|
+
if (readFileSync(markerPath, 'utf-8').trim() === hash) {
|
|
84
|
+
writeAbout(dir, entry.about); // label names can change without retraining
|
|
85
|
+
return 'current';
|
|
86
|
+
}
|
|
79
87
|
}
|
|
80
88
|
mkdirSync(dir, { recursive: true });
|
|
81
89
|
// Weights first and the manifest last: a model counts as present once its
|
|
@@ -84,5 +92,50 @@ export function writeSyncedModel(slug, entry) {
|
|
|
84
92
|
writeFileSync(join(dir, 'model.json'), files['model.json']);
|
|
85
93
|
writeFileSync(join(dir, 'manifest.json'), files['manifest.json']);
|
|
86
94
|
writeFileSync(markerPath, hash);
|
|
95
|
+
writeAbout(dir, entry.about);
|
|
87
96
|
return 'written';
|
|
88
97
|
}
|
|
98
|
+
|
|
99
|
+
/** The synced copy of what a model's dataset says about it (see modelDatasetExtras). */
|
|
100
|
+
function writeAbout(dir, about) {
|
|
101
|
+
if (about && typeof about === 'object') writeFileSync(join(dir, ABOUT_FILE), JSON.stringify(about));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What the model's dataset adds to its description: the villager's names for
|
|
106
|
+
* its labels, what the dataset is, and one training input as an example.
|
|
107
|
+
* Falls back to the copy synced with the model, else empty; the model still
|
|
108
|
+
* works without it.
|
|
109
|
+
*/
|
|
110
|
+
export function modelDatasetExtras(m) {
|
|
111
|
+
try {
|
|
112
|
+
const ds = m.datasetSlug ? readDatasetManifest(m.datasetSlug) : null;
|
|
113
|
+
if (!ds) return syncedAbout(m.slug);
|
|
114
|
+
let example;
|
|
115
|
+
try {
|
|
116
|
+
example = readRows(m.datasetSlug, { split: 'train', limit: 1 })[0]?.features;
|
|
117
|
+
} catch {
|
|
118
|
+
/* no rows to show */
|
|
119
|
+
}
|
|
120
|
+
return { labelNames: ds.labelNames ?? undefined, learnedFrom: ds.description || undefined, example };
|
|
121
|
+
} catch {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** On a machine without the dataset: what came with the synced model. */
|
|
127
|
+
function syncedAbout(slug) {
|
|
128
|
+
try {
|
|
129
|
+
const about = JSON.parse(readFileSync(join(getModelDir(slug), ABOUT_FILE), 'utf-8'));
|
|
130
|
+
return about && typeof about === 'object' ? about : {};
|
|
131
|
+
} catch {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** One model as an agent should see it (see describeLocalModel). */
|
|
137
|
+
export function describeAgentModel(slug) {
|
|
138
|
+
const m = readModelManifest(slug);
|
|
139
|
+
if (!m) return `- "${slug}": not on this computer yet.`;
|
|
140
|
+
return describeLocalModel(m, modelDatasetExtras(m));
|
|
141
|
+
}
|