@myvillage/cli 2.8.1 → 2.9.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
|
@@ -22,6 +22,14 @@ export const DATASET_APPEND_DESCRIPTION =
|
|
|
22
22
|
'regression provide a numeric `target`. Data stays local; provenance ' +
|
|
23
23
|
'is recorded automatically.';
|
|
24
24
|
|
|
25
|
+
export const DATASET_READ_DESCRIPTION =
|
|
26
|
+
'Read a LOCAL dataset on this machine: its description, columns, labels, ' +
|
|
27
|
+
'row counts, and a page of its rows (newest working set by default, or a ' +
|
|
28
|
+
'frozen version). Filter by split or label and page with offset/limit. ' +
|
|
29
|
+
'Table rows come back keyed by column name; image and sound rows come back ' +
|
|
30
|
+
'as absolute file paths (which model_predict accepts). Read-only — use ' +
|
|
31
|
+
'dataset_append to add rows.';
|
|
32
|
+
|
|
25
33
|
export const MODEL_PREDICT_DESCRIPTION =
|
|
26
34
|
'Run a prediction with a LOCAL trained model on this machine. `input` is ' +
|
|
27
35
|
'text (words models), the values of one table row as a list in column ' +
|
|
@@ -38,6 +46,12 @@ export const BUILTIN_TOOL_DEFS = [
|
|
|
38
46
|
tier: 'villager',
|
|
39
47
|
builtin: true,
|
|
40
48
|
},
|
|
49
|
+
{
|
|
50
|
+
name: 'dataset_read',
|
|
51
|
+
description: DATASET_READ_DESCRIPTION,
|
|
52
|
+
tier: 'villager',
|
|
53
|
+
builtin: true,
|
|
54
|
+
},
|
|
41
55
|
{
|
|
42
56
|
name: 'model_predict',
|
|
43
57
|
description: MODEL_PREDICT_DESCRIPTION,
|
|
@@ -10,16 +10,63 @@
|
|
|
10
10
|
* `myvillage dataset append` command uses — so agents and humans write
|
|
11
11
|
* identically. Raw data stays local; nothing leaves the machine.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* `dataset_read` is its read side: it wraps readRows() so agents can look at
|
|
14
|
+
* what's been collected (to answer questions about it, avoid duplicate
|
|
15
|
+
* appends, or feed rows to model_predict). Also local-only.
|
|
16
|
+
*
|
|
17
|
+
* Tool contracts mirror @myvillage-studio/dataset-schema DATASET_APPEND_TOOL
|
|
18
|
+
* and DATASET_READ_TOOL.
|
|
14
19
|
*/
|
|
15
20
|
import { tool } from 'ai';
|
|
16
21
|
import { z } from 'zod';
|
|
17
|
-
import {
|
|
18
|
-
|
|
22
|
+
import {
|
|
23
|
+
appendRows,
|
|
24
|
+
datasetExists,
|
|
25
|
+
readManifest,
|
|
26
|
+
readRows,
|
|
27
|
+
ingestAsset,
|
|
28
|
+
getAssetPath,
|
|
29
|
+
listDatasets,
|
|
30
|
+
} from '../utils/dataset-workspace.js';
|
|
31
|
+
import {
|
|
32
|
+
DATASET_APPEND_DESCRIPTION,
|
|
33
|
+
DATASET_READ_DESCRIPTION,
|
|
34
|
+
MODEL_PREDICT_DESCRIPTION,
|
|
35
|
+
} from './builtin-tools-registry.js';
|
|
19
36
|
import { readToolsYaml } from '../utils/local-agent.js';
|
|
20
37
|
|
|
21
38
|
export { BUILTIN_TOOL_DEFS } from './builtin-tools-registry.js';
|
|
22
39
|
|
|
40
|
+
const READ_DEFAULT_LIMIT = 50;
|
|
41
|
+
const READ_MAX_LIMIT = 200;
|
|
42
|
+
// Long text rows are clipped so one page can't blow out the model's context.
|
|
43
|
+
const READ_MAX_TEXT_CHARS = 2000;
|
|
44
|
+
|
|
45
|
+
/** Shape a stored row for the model: readable features, no internal noise. */
|
|
46
|
+
function presentRow(slug, manifest, row) {
|
|
47
|
+
let features = row.features;
|
|
48
|
+
let truncated = false;
|
|
49
|
+
if (features && typeof features === 'object' && !Array.isArray(features) && features.path) {
|
|
50
|
+
// AssetRef → absolute path, the same form model_predict / dataset_append take
|
|
51
|
+
features = getAssetPath(slug, features.path);
|
|
52
|
+
} else if (
|
|
53
|
+
Array.isArray(features) &&
|
|
54
|
+
manifest.featureNames?.length === features.length
|
|
55
|
+
) {
|
|
56
|
+
features = Object.fromEntries(manifest.featureNames.map((name, i) => [name, features[i]]));
|
|
57
|
+
} else if (typeof features === 'string' && features.length > READ_MAX_TEXT_CHARS) {
|
|
58
|
+
features = features.slice(0, READ_MAX_TEXT_CHARS);
|
|
59
|
+
truncated = true;
|
|
60
|
+
}
|
|
61
|
+
const out = { id: row.id, features, split: row.split };
|
|
62
|
+
if (truncated) out.truncated = true;
|
|
63
|
+
if (row.label != null) out.label = row.label;
|
|
64
|
+
if (row.target != null) out.target = row.target;
|
|
65
|
+
if (row.provenance?.source) out.source = row.provenance.source;
|
|
66
|
+
if (row.provenance?.ts) out.addedAt = row.provenance.ts;
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
23
70
|
/**
|
|
24
71
|
* Gate built-ins by the SAME allowlist as the MyVillageOS platform server
|
|
25
72
|
* (`allowed_tools` on whichever tools.yaml server points at a myvillageproject.ai
|
|
@@ -128,6 +175,77 @@ export function getBuiltinTools(agentName, config) {
|
|
|
128
175
|
},
|
|
129
176
|
}),
|
|
130
177
|
|
|
178
|
+
dataset_read: tool({
|
|
179
|
+
description: DATASET_READ_DESCRIPTION,
|
|
180
|
+
inputSchema: z.object({
|
|
181
|
+
datasetId: z.string().describe('Slug of the local dataset to read'),
|
|
182
|
+
version: z
|
|
183
|
+
.number()
|
|
184
|
+
.int()
|
|
185
|
+
.positive()
|
|
186
|
+
.optional()
|
|
187
|
+
.describe('Frozen version number to read; omit for the current working set'),
|
|
188
|
+
split: z.enum(['train', 'val']).optional().describe('Only rows in this split'),
|
|
189
|
+
label: z.string().optional().describe('Only rows with this class label (classification datasets)'),
|
|
190
|
+
offset: z.number().int().min(0).optional().describe('Rows to skip (default 0)'),
|
|
191
|
+
limit: z
|
|
192
|
+
.number()
|
|
193
|
+
.int()
|
|
194
|
+
.min(0)
|
|
195
|
+
.max(READ_MAX_LIMIT)
|
|
196
|
+
.optional()
|
|
197
|
+
.describe(`Rows to return (default ${READ_DEFAULT_LIMIT}, max ${READ_MAX_LIMIT}; 0 = summary only)`),
|
|
198
|
+
}),
|
|
199
|
+
execute: async ({ datasetId, version, split, label, offset = 0, limit = READ_DEFAULT_LIMIT }) => {
|
|
200
|
+
const manifest = readManifest(datasetId);
|
|
201
|
+
if (!manifest) {
|
|
202
|
+
let available = [];
|
|
203
|
+
try {
|
|
204
|
+
available = listDatasets().map((d) => d.slug);
|
|
205
|
+
} catch { /* store unavailable */ }
|
|
206
|
+
return {
|
|
207
|
+
error:
|
|
208
|
+
`Dataset "${datasetId}" not found locally.` +
|
|
209
|
+
(available.length ? ` Available: ${available.join(', ')}.` : ' No local datasets exist yet.'),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (version && !manifest.versions?.some((v) => v.version === version)) {
|
|
213
|
+
const known = (manifest.versions ?? []).map((v) => v.version);
|
|
214
|
+
return {
|
|
215
|
+
error:
|
|
216
|
+
`Dataset "${datasetId}" has no version ${version}.` +
|
|
217
|
+
(known.length ? ` Frozen versions: ${known.join(', ')}.` : ' It has no frozen versions yet.'),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
let rows = readRows(datasetId, { version, split });
|
|
222
|
+
if (label != null) rows = rows.filter((r) => r.label === label);
|
|
223
|
+
const page = rows.slice(offset, offset + limit);
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
datasetId: manifest.slug,
|
|
227
|
+
name: manifest.name,
|
|
228
|
+
...(manifest.description ? { description: manifest.description } : {}),
|
|
229
|
+
modality: manifest.modality,
|
|
230
|
+
taskType: manifest.taskType,
|
|
231
|
+
labels: manifest.labels ?? [],
|
|
232
|
+
...(manifest.featureNames ? { columns: manifest.featureNames } : {}),
|
|
233
|
+
...(manifest.featureTypes ? { columnTypes: manifest.featureTypes } : {}),
|
|
234
|
+
stats: manifest.stats,
|
|
235
|
+
versions: (manifest.versions ?? []).map((v) => ({ version: v.version, rowCount: v.rowCount, frozenAt: v.frozenAt })),
|
|
236
|
+
reading: version ? `version ${version}` : 'working set',
|
|
237
|
+
matchingRows: rows.length,
|
|
238
|
+
offset,
|
|
239
|
+
returned: page.length,
|
|
240
|
+
hasMore: offset + page.length < rows.length,
|
|
241
|
+
rows: page.map((r) => presentRow(datasetId, manifest, r)),
|
|
242
|
+
};
|
|
243
|
+
} catch (e) {
|
|
244
|
+
return { error: e.message };
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
|
|
131
249
|
model_predict: tool({
|
|
132
250
|
description: MODEL_PREDICT_DESCRIPTION,
|
|
133
251
|
inputSchema: z.object({
|
|
@@ -34,13 +34,14 @@ export async function gatherContext(agentConfig, lastCheckIn, recentActions = []
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Local Lab datasets & models, so the agent knows what it can
|
|
38
|
-
// predict with (and the exact slugs to pass to
|
|
37
|
+
// Local Lab datasets & models, so the agent knows what it can read / append
|
|
38
|
+
// to / predict with (and the exact slugs to pass to dataset_read /
|
|
39
|
+
// dataset_append / model_predict).
|
|
39
40
|
// Local-only, synchronous file reads; tfjs is NOT loaded by listing models.
|
|
40
41
|
try {
|
|
41
42
|
const datasets = listDatasets();
|
|
42
43
|
if (datasets.length > 0) {
|
|
43
|
-
parts.push('\nLocal datasets you can add to with dataset_append (use the slug as datasetId):');
|
|
44
|
+
parts.push('\nLocal datasets you can read with dataset_read or add to with dataset_append (use the slug as datasetId):');
|
|
44
45
|
for (const d of datasets.slice(0, 25)) {
|
|
45
46
|
parts.push(`- ${d.slug} — ${d.modality}/${d.taskType}, ${d.stats?.rowCount ?? 0} rows`);
|
|
46
47
|
}
|
|
@@ -141,7 +141,8 @@ function requestHostContext(timeoutMs) {
|
|
|
141
141
|
// dev server.
|
|
142
142
|
|
|
143
143
|
const DEFAULT_DEV_SERVER = 'https://myvillage-realtime-staging.myvillage-realtime.workers.dev';
|
|
144
|
-
|
|
144
|
+
// No vowels (or look-alikes), so random codes can't spell words.
|
|
145
|
+
const CODE_ALPHABET = 'BCDFGHJKMNPQRSTVWXZ23456789';
|
|
145
146
|
|
|
146
147
|
export class MultiplayerError extends Error {
|
|
147
148
|
constructor(code, message, status) {
|
|
@@ -185,7 +186,8 @@ class Multiplayer {
|
|
|
185
186
|
this._ticket = null;
|
|
186
187
|
/** The current match (see MatchView in the platform docs), or null. */
|
|
187
188
|
this.match = null;
|
|
188
|
-
|
|
189
|
+
// Digits only: random letters can spell words.
|
|
190
|
+
this.me = { displayName: ctx ? ctx.player.displayName : options.name || 'Player ' + (100 + Math.floor(Math.random() * 900)) };
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
// ── lobby plumbing ──
|
|
@@ -208,8 +210,14 @@ class Multiplayer {
|
|
|
208
210
|
async quickMatch({ lobby } = {}) {
|
|
209
211
|
this._requireAvailable();
|
|
210
212
|
// Finished match rooms can't be reused, so dev quick match rotates rooms
|
|
211
|
-
// every 10 minutes
|
|
212
|
-
|
|
213
|
+
// every 10 minutes, and connect() steps through numbered rooms in that
|
|
214
|
+
// window past any that are finished or full. Tabs follow the same
|
|
215
|
+
// sequence, so they still meet.
|
|
216
|
+
if (this.mode === 'dev') {
|
|
217
|
+
this._devQuickBase = 'QUICK' + Math.floor(Date.now() / 600000).toString(36).toUpperCase();
|
|
218
|
+
this._devQuickTry = 0;
|
|
219
|
+
return this._devMatch(this._devQuickBase, false, lobby);
|
|
220
|
+
}
|
|
213
221
|
return this._enter(await this._api('POST', `/api/v1/games/${this._game()}/matches/quick`, { platform: this._ctx.platform, lobby }));
|
|
214
222
|
}
|
|
215
223
|
|
|
@@ -294,10 +302,23 @@ class Multiplayer {
|
|
|
294
302
|
async connect() {
|
|
295
303
|
this._requireAvailable();
|
|
296
304
|
if (!this.match) throw new MultiplayerError('no_match', 'Host, join or quick match first.');
|
|
297
|
-
const ticket = await this._freshTicket();
|
|
298
305
|
const { ArenaClient } = await import('./myvillage-realtime.js');
|
|
299
|
-
|
|
300
|
-
|
|
306
|
+
let net;
|
|
307
|
+
for (;;) {
|
|
308
|
+
const ticket = await this._freshTicket();
|
|
309
|
+
net = new ArenaClient({ wsUrl: ticket.wsUrl, ticket: ticket.ticket });
|
|
310
|
+
try {
|
|
311
|
+
await net.connect();
|
|
312
|
+
break;
|
|
313
|
+
} catch (err) {
|
|
314
|
+
// Dev quick match: that room is finished or full — try the next one.
|
|
315
|
+
const devQuick = this.mode === 'dev' && this._devQuickBase && this.match.code.startsWith(this._devQuickBase);
|
|
316
|
+
if (!devQuick || this._devQuickTry >= 20 || !/finished|full/i.test(String(err && err.message))) throw err;
|
|
317
|
+
this._devQuickTry++;
|
|
318
|
+
this._ticket = null;
|
|
319
|
+
this._devMatch(this._devQuickBase + '-' + this._devQuickTry, false, this.match.lobby);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
301
322
|
this._ticket = null; // single use: a reconnect fetches a fresh one
|
|
302
323
|
this._net = net;
|
|
303
324
|
return net;
|
|
@@ -25,7 +25,7 @@ const TOOL_GROUPS = {
|
|
|
25
25
|
villages: ['village_search', 'village_view', 'village_create', 'village_update'],
|
|
26
26
|
moments: ['moment_create', 'moment_get', 'pulse_create', 'pulse_list'],
|
|
27
27
|
villagers: ['villager_me', 'villager_search', 'villager_update', 'agent_list'],
|
|
28
|
-
meetings: ['recall_send_bot', 'recall_meeting_attendance'],
|
|
28
|
+
meetings: ['recall_send_bot', 'recall_list_sessions', 'recall_meeting_attendance'],
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
// ─── Agentic App Template (Next.js) ─────────────────────────────────────────
|
|
@@ -756,7 +756,7 @@ const TOOL_GROUPS = {
|
|
|
756
756
|
villages: ['village_search', 'village_view', 'village_create', 'village_update'],
|
|
757
757
|
moments: ['moment_create', 'moment_get', 'pulse_create', 'pulse_list'],
|
|
758
758
|
villagers: ['villager_me', 'villager_search', 'villager_update', 'agent_list'],
|
|
759
|
-
meetings: ['recall_send_bot', 'recall_meeting_attendance'],
|
|
759
|
+
meetings: ['recall_send_bot', 'recall_list_sessions', 'recall_meeting_attendance'],
|
|
760
760
|
};
|
|
761
761
|
|
|
762
762
|
// Tool groups selected during project creation
|