@myvillage/cli 2.15.0 → 2.16.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,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myvillage/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "MyVillageOS CLI for community developers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"myvillage": "./bin/myvillage.js"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.js"
|
|
11
|
+
},
|
|
9
12
|
"engines": {
|
|
10
13
|
"node": ">=22.0.0"
|
|
11
14
|
},
|
|
@@ -14,13 +14,26 @@
|
|
|
14
14
|
import { listDatasets } from '../utils/dataset-workspace.js';
|
|
15
15
|
import { listModels } from '../utils/model-workspace.js';
|
|
16
16
|
import { readAgentModels, describeAgentModel } from '../utils/agent-models.js';
|
|
17
|
+
import { fetchAgentMemories, formatMemorySection, readEnvSecrets } from './memory-context.js';
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
// `opts.agentDir` lets saved notes be checked against the agent's .env so a
|
|
20
|
+
// secret never reaches the context; `opts.listMemory` swaps the memory fetch
|
|
21
|
+
// (tests).
|
|
22
|
+
export async function gatherContext(agentConfig, lastCheckIn, recentActions = [], agentName = agentConfig?.name, opts = {}) {
|
|
19
23
|
const parts = [];
|
|
20
24
|
|
|
21
25
|
parts.push(`Current time: ${new Date().toISOString()}`);
|
|
22
26
|
parts.push(`Last check-in: ${lastCheckIn || 'never'}`);
|
|
23
27
|
|
|
28
|
+
// What the agent saved in earlier runs. Read by the runtime every loop, not
|
|
29
|
+
// left to the model to look up. Empty (and silent) if it can't be fetched.
|
|
30
|
+
const memories = await fetchAgentMemories(
|
|
31
|
+
agentConfig?.man?.agent_id,
|
|
32
|
+
opts.listMemory ? { list: opts.listMemory } : undefined,
|
|
33
|
+
);
|
|
34
|
+
const memorySection = formatMemorySection(memories, { secrets: readEnvSecrets(opts.agentDir) });
|
|
35
|
+
if (memorySection) parts.push(memorySection);
|
|
36
|
+
|
|
24
37
|
// Include recent actions so the agent knows what it already did
|
|
25
38
|
if (recentActions.length > 0) {
|
|
26
39
|
parts.push('\nYour recent actions:');
|
|
@@ -76,5 +89,5 @@ export async function gatherContext(agentConfig, lastCheckIn, recentActions = []
|
|
|
76
89
|
parts.push('You may post, comment, vote, read files, run your tools, or do nothing if appropriate.');
|
|
77
90
|
parts.push('Be thoughtful — quality over quantity. Avoid repeating yourself.');
|
|
78
91
|
|
|
79
|
-
return { text: parts.join('\n') };
|
|
92
|
+
return { text: parts.join('\n'), memoryCount: memories.length };
|
|
80
93
|
}
|
|
@@ -12,6 +12,7 @@ import { getMCPTools, cleanupMCPClients } from './mcp-client.js';
|
|
|
12
12
|
import { getBuiltinTools } from './builtin-tools.js';
|
|
13
13
|
import { readAgentModels } from '../utils/agent-models.js';
|
|
14
14
|
import { gatherContext } from './context.js';
|
|
15
|
+
import { RUN_NOTES_INSTRUCTION } from './memory-context.js';
|
|
15
16
|
import { isWithinActiveHours, getNextCheckInMs } from './scheduler.js';
|
|
16
17
|
import { parse as parseYaml } from 'yaml';
|
|
17
18
|
import { postAgentHeartbeat, listAgentTasks, claimAgentTask, completeAgentTask, refreshAccessToken } from '../utils/api.js';
|
|
@@ -308,6 +309,10 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
308
309
|
logActivity(agentDir, { type: 'wisdom_loaded', count: wisdom.length, names: wisdom.map(w => w.name) });
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
// Ask the agent to leave notes for its next run. Only when it can
|
|
313
|
+
// actually save them; the notes come back in the context every loop.
|
|
314
|
+
if (tools.agent_memory_set) systemPrompt = `${systemPrompt}\n\n${RUN_NOTES_INSTRUCTION}`;
|
|
315
|
+
|
|
311
316
|
// Try to pull an assigned task first. Tasks take priority over ambient
|
|
312
317
|
// feed-monitoring. If nothing is queued, fall through to the default.
|
|
313
318
|
if (resuming) {
|
|
@@ -320,7 +325,7 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
320
325
|
// no longer auto-fetched here — the agent pulls them on demand via its
|
|
321
326
|
// tools — so these activity counters stay at 0 unless a future signal
|
|
322
327
|
// repopulates them.
|
|
323
|
-
const contextResult = await gatherContext(config, lastCheckIn, recentActions, agentName);
|
|
328
|
+
const contextResult = await gatherContext(config, lastCheckIn, recentActions, agentName, { agentDir });
|
|
324
329
|
let context = contextResult.text;
|
|
325
330
|
const eventsFound = 0;
|
|
326
331
|
|
|
@@ -360,6 +365,7 @@ Guidelines:
|
|
|
360
365
|
feedItems: feedItemsRead,
|
|
361
366
|
mentions: mentionsFound,
|
|
362
367
|
events: eventsFound,
|
|
368
|
+
memories: contextResult.memoryCount,
|
|
363
369
|
});
|
|
364
370
|
|
|
365
371
|
const conversation = resuming
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// ── Memory Context ──────────────────────────────────────
|
|
2
|
+
// Every loop starts a fresh conversation, so an agent forgets what it worked
|
|
3
|
+
// out last run (a login selector that worked, an id it looked up) unless the
|
|
4
|
+
// model happens to call agent_memory_list. Relying on that choice failed in
|
|
5
|
+
// practice: an agent found the right selectors in run 1, never re-read them,
|
|
6
|
+
// and spent run 2 guessing again.
|
|
7
|
+
//
|
|
8
|
+
// So the runtime reads the agent's saved memories itself, at the start of
|
|
9
|
+
// every loop, and puts a compact "what you remember" section into the context.
|
|
10
|
+
// It reads the same store the hosted agent_memory_* MCP tools use
|
|
11
|
+
// (GET /api/network/agents/{agentProfileId}/memory), straight from the
|
|
12
|
+
// platform API, so it works whether or not the agent was granted those tools.
|
|
13
|
+
//
|
|
14
|
+
// Notes saved under RUN_NOTES_CATEGORY are shown first: that is the category
|
|
15
|
+
// the agent is asked to use for "things my next run should know".
|
|
16
|
+
|
|
17
|
+
import { readFileSync, existsSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { listAgentMemory } from '../utils/api.js';
|
|
20
|
+
|
|
21
|
+
export const RUN_NOTES_CATEGORY = 'run_notes';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_MAX_ENTRIES = 30;
|
|
24
|
+
const DEFAULT_MAX_CHARS = 4000;
|
|
25
|
+
const MAX_VALUE_CHARS = 500;
|
|
26
|
+
const FETCH_TIMEOUT_MS = 8000;
|
|
27
|
+
|
|
28
|
+
// Shown in the system prompt when the agent can call agent_memory_set. Kept
|
|
29
|
+
// constant so it doesn't break the cached system-prompt prefix.
|
|
30
|
+
export const RUN_NOTES_INSTRUCTION =
|
|
31
|
+
'## Notes for your next run\n' +
|
|
32
|
+
'Each run starts fresh: the only things you carry over are the notes you save. ' +
|
|
33
|
+
'Before you finish, if you worked something out that would save time next run (a selector, button or URL that worked, ' +
|
|
34
|
+
'an id you looked up, a step that failed and why), save it with agent_memory_set using ' +
|
|
35
|
+
`category "${RUN_NOTES_CATEGORY}" and a short descriptive key (e.g. "login_selectors"). ` +
|
|
36
|
+
'Update the same key rather than adding near-duplicates. ' +
|
|
37
|
+
'Never save a password or secret value; write its ${NAME} placeholder instead.';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetch this agent's saved memories, newest first. Never throws: any failure
|
|
41
|
+
* (no agent id yet, offline, signed out, slow server) returns [] so the loop
|
|
42
|
+
* runs exactly as it did before.
|
|
43
|
+
*/
|
|
44
|
+
export async function fetchAgentMemories(agentId, { list = listAgentMemory, timeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
45
|
+
if (!agentId) return [];
|
|
46
|
+
let timer;
|
|
47
|
+
try {
|
|
48
|
+
const res = await Promise.race([
|
|
49
|
+
list(agentId),
|
|
50
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(null), timeoutMs); }),
|
|
51
|
+
]);
|
|
52
|
+
const entries = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
|
|
53
|
+
return entries.filter((e) => e && typeof e.key === 'string');
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Values from the agent's .env, so a secret the agent mistakenly saved to
|
|
63
|
+
* memory is shown back as its ${NAME} placeholder instead of the real value.
|
|
64
|
+
*/
|
|
65
|
+
export function readEnvSecrets(agentDir) {
|
|
66
|
+
const out = [];
|
|
67
|
+
const path = agentDir && join(agentDir, '.env');
|
|
68
|
+
if (!path || !existsSync(path)) return out;
|
|
69
|
+
let text;
|
|
70
|
+
try { text = readFileSync(path, 'utf-8'); } catch { return out; }
|
|
71
|
+
for (const raw of text.split('\n')) {
|
|
72
|
+
const line = raw.trim();
|
|
73
|
+
if (!line || line.startsWith('#')) continue;
|
|
74
|
+
const eq = line.indexOf('=');
|
|
75
|
+
if (eq <= 0) continue;
|
|
76
|
+
const name = line.slice(0, eq).trim().replace(/^export\s+/, '');
|
|
77
|
+
let value = line.slice(eq + 1).trim();
|
|
78
|
+
if (value.length >= 2 && (value[0] === '"' || value[0] === "'") && value.endsWith(value[0])) {
|
|
79
|
+
value = value.slice(1, -1);
|
|
80
|
+
}
|
|
81
|
+
if (/^\w+$/.test(name) && value.length >= 3) out.push([name, value]);
|
|
82
|
+
}
|
|
83
|
+
// Longest first, so a value that contains another is replaced whole.
|
|
84
|
+
return out.sort((a, b) => b[1].length - a[1].length);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function redact(text, secrets) {
|
|
88
|
+
let out = text;
|
|
89
|
+
for (const [name, value] of secrets) out = out.split(value).join(`\${${name}}`);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function oneLine(value) {
|
|
94
|
+
const s = (typeof value === 'string' ? value : JSON.stringify(value) ?? '').replace(/\s+/g, ' ').trim();
|
|
95
|
+
return s.length > MAX_VALUE_CHARS ? `${s.slice(0, MAX_VALUE_CHARS)}…` : s;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function updatedTime(e) {
|
|
99
|
+
const t = Date.parse(e.updatedAt || e.createdAt || '');
|
|
100
|
+
return Number.isNaN(t) ? 0 : t;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render memories as a compact context section, or '' when there are none.
|
|
105
|
+
* Run notes come first, then everything else; newest first within each, capped
|
|
106
|
+
* at `maxEntries` lines and roughly `maxChars` characters.
|
|
107
|
+
*/
|
|
108
|
+
export function formatMemorySection(
|
|
109
|
+
entries,
|
|
110
|
+
{ maxEntries = DEFAULT_MAX_ENTRIES, maxChars = DEFAULT_MAX_CHARS, secrets = [] } = {},
|
|
111
|
+
) {
|
|
112
|
+
if (!Array.isArray(entries) || entries.length === 0) return '';
|
|
113
|
+
const byNewest = [...entries].sort((a, b) => updatedTime(b) - updatedTime(a));
|
|
114
|
+
const ordered = [
|
|
115
|
+
...byNewest.filter((e) => e.category === RUN_NOTES_CATEGORY),
|
|
116
|
+
...byNewest.filter((e) => e.category !== RUN_NOTES_CATEGORY),
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const header = [
|
|
120
|
+
'\nWhat you remember from earlier runs (your own saved notes, newest first; use them instead of working things out again):',
|
|
121
|
+
];
|
|
122
|
+
const lines = [];
|
|
123
|
+
let used = header[0].length;
|
|
124
|
+
for (const e of ordered) {
|
|
125
|
+
if (lines.length >= maxEntries) break;
|
|
126
|
+
const date = e.updatedAt ? ` (saved ${String(e.updatedAt).slice(0, 10)})` : '';
|
|
127
|
+
const cat = e.category && e.category !== RUN_NOTES_CATEGORY ? ` [${e.category}]` : '';
|
|
128
|
+
const line = redact(`- ${e.key}${cat}: ${oneLine(e.value)}${date}`, secrets);
|
|
129
|
+
if (lines.length > 0 && used + line.length + 1 > maxChars) break;
|
|
130
|
+
lines.push(line);
|
|
131
|
+
used += line.length + 1;
|
|
132
|
+
}
|
|
133
|
+
const hidden = ordered.length - lines.length;
|
|
134
|
+
if (hidden > 0) lines.push(`(${hidden} older note${hidden === 1 ? '' : 's'} not shown; agent_memory_list has them all.)`);
|
|
135
|
+
return [...header, ...lines].join('\n');
|
|
136
|
+
}
|