@myvillage/cli 1.88.3 → 1.89.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/package.json +1 -1
- package/src/agent-runtime/runner.js +104 -3
package/package.json
CHANGED
|
@@ -23,7 +23,7 @@ import fs from 'fs';
|
|
|
23
23
|
import path from 'path';
|
|
24
24
|
import { getConfig } from '../utils/config.js';
|
|
25
25
|
import { getAccessToken } from '../utils/auth.js';
|
|
26
|
-
import { listLocalAgents, readAgentConfig } from '../utils/local-agent.js';
|
|
26
|
+
import { listLocalAgents, readAgentConfig, getAgentDir, writeAgentConfig } from '../utils/local-agent.js';
|
|
27
27
|
import { startDaemon, stopDaemon, isDaemonRunning, getDaemonInfo } from './daemon.js';
|
|
28
28
|
|
|
29
29
|
const RUNNER_STATE_FILE = path.join(os.homedir(), '.myvillage', 'runner.json');
|
|
@@ -101,6 +101,88 @@ async function request(networkBaseUrl, pathname, method, body, headers) {
|
|
|
101
101
|
return { status: res.status, ok: res.ok, json };
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// ── Config sync (materialise an assigned agent from the control plane) ───────
|
|
105
|
+
|
|
106
|
+
// Records which published config version this machine last wrote, so we only
|
|
107
|
+
// re-write files when the definition actually changed.
|
|
108
|
+
function syncedVersionPath(agentDir) {
|
|
109
|
+
return path.join(agentDir, '.config-version');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readSyncedVersion(agentDir) {
|
|
113
|
+
try {
|
|
114
|
+
return Number(fs.readFileSync(syncedVersionPath(agentDir), 'utf8').trim()) || 0;
|
|
115
|
+
} catch {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Write an assigned agent's files from its published definition, creating the
|
|
122
|
+
* agent directory if this machine has never run it. Returns true when the agent
|
|
123
|
+
* is ready to start.
|
|
124
|
+
*
|
|
125
|
+
* `.env` is never written here: secrets stay per-machine until they can be
|
|
126
|
+
* envelope-encrypted (see AgentConfig in the schema). An existing local `.env`
|
|
127
|
+
* is left untouched.
|
|
128
|
+
*/
|
|
129
|
+
async function syncAgentConfig(ctx, assignment) {
|
|
130
|
+
const { agentProfileId, handle, configVersion } = assignment;
|
|
131
|
+
const name = handle || agentProfileId;
|
|
132
|
+
const agentDir = getAgentDir(name);
|
|
133
|
+
const exists = fs.existsSync(path.join(agentDir, 'agent.config.yaml'));
|
|
134
|
+
|
|
135
|
+
// Nothing published: fall back to whatever is already on this machine.
|
|
136
|
+
if (!configVersion) return exists;
|
|
137
|
+
// Already current.
|
|
138
|
+
if (exists && readSyncedVersion(agentDir) === configVersion) return true;
|
|
139
|
+
|
|
140
|
+
const res = await ctx.cp(`/agents/${agentProfileId}/config`);
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
log(`could not fetch config for ${name} (${res.status}) — leaving local files alone`);
|
|
143
|
+
return exists;
|
|
144
|
+
}
|
|
145
|
+
const c = res.json.data;
|
|
146
|
+
|
|
147
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
148
|
+
if (typeof c.promptMd === 'string' && c.promptMd) {
|
|
149
|
+
fs.writeFileSync(path.join(agentDir, 'prompt.md'), c.promptMd);
|
|
150
|
+
}
|
|
151
|
+
if (typeof c.toolsYaml === 'string' && c.toolsYaml) {
|
|
152
|
+
fs.writeFileSync(path.join(agentDir, 'tools.yaml'), c.toolsYaml);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Compose agent.config.yaml from server-known identity + published schedule
|
|
156
|
+
// and brain, preserving anything already set locally that the control plane
|
|
157
|
+
// does not own.
|
|
158
|
+
let existingCfg = {};
|
|
159
|
+
try { existingCfg = readAgentConfig(name) || {}; } catch { /* first sync */ }
|
|
160
|
+
const cfg = {
|
|
161
|
+
...existingCfg,
|
|
162
|
+
name,
|
|
163
|
+
display_name: c.displayName || existingCfg.display_name || name,
|
|
164
|
+
description: c.description ?? existingCfg.description ?? '',
|
|
165
|
+
man: {
|
|
166
|
+
...(existingCfg.man || {}),
|
|
167
|
+
agent_id: c.agentId,
|
|
168
|
+
...(c.villageAgentId ? { village_agent_id: c.villageAgentId } : {}),
|
|
169
|
+
handle: c.handle || name,
|
|
170
|
+
},
|
|
171
|
+
...(c.schedule ? { schedule: c.schedule } : {}),
|
|
172
|
+
...(c.brain ? { brain: c.brain } : {}),
|
|
173
|
+
};
|
|
174
|
+
writeAgentConfig(name, cfg);
|
|
175
|
+
|
|
176
|
+
// Keep an empty .env so the daemon's loader and 0600 expectations hold, but
|
|
177
|
+
// never overwrite secrets a person put here.
|
|
178
|
+
const envPath = path.join(agentDir, '.env');
|
|
179
|
+
if (!fs.existsSync(envPath)) fs.writeFileSync(envPath, '', { mode: 0o600 });
|
|
180
|
+
|
|
181
|
+
fs.writeFileSync(syncedVersionPath(agentDir), String(configVersion));
|
|
182
|
+
log(`synced ${name} config v${configVersion}${exists ? '' : ' (new on this machine)'}`);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
104
186
|
// ── Local agent indexing ─────────────────────────────────────────────────────
|
|
105
187
|
|
|
106
188
|
// Map control-plane agentProfileId -> local agent { name, config } via the
|
|
@@ -238,9 +320,28 @@ async function reconcileOnce(ctx) {
|
|
|
238
320
|
|
|
239
321
|
// Start desired-running agents we don't yet own+run here.
|
|
240
322
|
for (const a of desiredRunning) {
|
|
241
|
-
|
|
323
|
+
// Materialise (or refresh) the agent from its published definition, so a
|
|
324
|
+
// machine that has never seen this agent can still run it.
|
|
325
|
+
let entry = local.get(a.agentProfileId);
|
|
326
|
+
try {
|
|
327
|
+
const ready = await syncAgentConfig(ctx, a);
|
|
328
|
+
if (ready && !entry) {
|
|
329
|
+
const name = a.handle || a.agentProfileId;
|
|
330
|
+
entry = { name, config: readAgentConfig(name) };
|
|
331
|
+
local.set(a.agentProfileId, entry);
|
|
332
|
+
} else if (ready && entry) {
|
|
333
|
+
// Pick up an edited prompt/tools on the next start.
|
|
334
|
+
entry.config = readAgentConfig(entry.name);
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
log(`config sync failed for ${a.handle || a.agentProfileId}`, err?.message || err);
|
|
338
|
+
}
|
|
339
|
+
|
|
242
340
|
if (!entry) {
|
|
243
|
-
log(
|
|
341
|
+
log(
|
|
342
|
+
`assigned agent ${a.handle || a.agentProfileId} has no config on this machine and none published — ` +
|
|
343
|
+
`open it in Studio to publish its setup`,
|
|
344
|
+
);
|
|
244
345
|
continue;
|
|
245
346
|
}
|
|
246
347
|
if (ctx.leases.has(a.agentProfileId) && isDaemonRunning(entry.name)) continue;
|