@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -73,8 +73,19 @@ export const BUILTIN_TOOL_DEFS = [
73
73
  },
74
74
  ];
75
75
 
76
- /** One line telling an agent what a local model is for and what it takes. */
77
- export function describeLocalModel(m) {
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
- return `- "${m.slug}" (${m.name}): takes ${takes}; ${gives}.`;
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, readModelManifest) {
110
- const lines = slugs.map((slug) => {
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, readModelManifestSync),
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
- return predictWithModel(model, record, input);
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 { describeLocalModel } from './builtin-tools-registry.js';
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(describeLocalModel(m));
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
- Object.assign(tools, getBuiltinTools(agentName, config));
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
- out[slug] = { hash: hashFiles(files), files };
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) return 'current';
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
+ }