@myvillage/cli 1.26.0 → 1.28.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
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Core agent loop using Vercel AI SDK. Reads prompt.md,
|
|
3
3
|
// gathers context, calls LLM with tools, logs results.
|
|
4
4
|
|
|
5
|
-
import { generateText } from 'ai';
|
|
5
|
+
import { generateText, stepCountIs } from 'ai';
|
|
6
6
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
7
7
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
8
8
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
@@ -176,7 +176,15 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
176
176
|
const instructionText = activeTask.instruction
|
|
177
177
|
|| (activeTask.input ? JSON.stringify(activeTask.input, null, 2) : '');
|
|
178
178
|
|
|
179
|
-
systemPrompt = `${systemPrompt}\n\n## ACTIVE TASK MODE
|
|
179
|
+
systemPrompt = `${systemPrompt}\n\n## ACTIVE TASK MODE
|
|
180
|
+
A client has assigned you the task below. Your job this iteration is to **execute it fully** using your available tools, not just acknowledge it.
|
|
181
|
+
|
|
182
|
+
Guidelines:
|
|
183
|
+
- Use as many tool calls as needed to complete the task. A single discovery call (e.g. listing directories) is usually NOT enough — follow up with the calls that actually produce the requested result.
|
|
184
|
+
- Do not stop after one tool call unless that call alone fulfills the entire instruction.
|
|
185
|
+
- Do not ask clarifying questions back to the user; make a reasonable interpretation and proceed.
|
|
186
|
+
- The feed context is provided only for situational awareness — do not let "no new feed activity" prevent you from carrying out the task.
|
|
187
|
+
- Your final text response should include the actual answer/output the user asked for (e.g. the list of files, the summary, the result), not just a description of what you did.`;
|
|
180
188
|
|
|
181
189
|
context = `## TASK (id=${activeTask.id}, type=${activeTask.taskType})\n${instructionText}\n\n---\n\n## FEED CONTEXT (for awareness only)\n${context}`;
|
|
182
190
|
|
|
@@ -193,13 +201,20 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
193
201
|
});
|
|
194
202
|
|
|
195
203
|
// Call LLM with tools
|
|
204
|
+
// AI SDK v6 removed `maxSteps` — the loop now uses `stopWhen` with
|
|
205
|
+
// a StopCondition predicate. The default is `stepCountIs(1)`, so
|
|
206
|
+
// without this the model gets exactly ONE round of tool calls and
|
|
207
|
+
// returns immediately. We give it up to 10 steps so a task like
|
|
208
|
+
// "find any README under the workspace" can do
|
|
209
|
+
// list_allowed_directories → list_directory → search_files → ...
|
|
210
|
+
// until it's actually done.
|
|
196
211
|
const result = await generateText({
|
|
197
212
|
model,
|
|
198
213
|
system: systemPrompt,
|
|
199
214
|
prompt: context,
|
|
200
215
|
tools,
|
|
201
|
-
|
|
202
|
-
maxTokens,
|
|
216
|
+
stopWhen: stepCountIs(10),
|
|
217
|
+
maxOutputTokens: maxTokens,
|
|
203
218
|
});
|
|
204
219
|
|
|
205
220
|
// Log LLM response
|
|
@@ -215,6 +230,10 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
215
230
|
|
|
216
231
|
// Log tool calls and count activity. Also audit action-tool success
|
|
217
232
|
// so we don't trust the model's final text about whether a task worked.
|
|
233
|
+
// `toolCallSummary` accumulates a structured per-call record that we
|
|
234
|
+
// attach to the task output so the Activity panel can show what the
|
|
235
|
+
// agent actually did — not just a tool-call count.
|
|
236
|
+
const toolCallSummary = [];
|
|
218
237
|
if (result.steps?.length) {
|
|
219
238
|
for (const step of result.steps) {
|
|
220
239
|
if (step.toolCalls?.length) {
|
|
@@ -228,25 +247,39 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
228
247
|
if (step.toolCalls?.length && step.toolResults?.length) {
|
|
229
248
|
for (let i = 0; i < step.toolResults.length; i++) {
|
|
230
249
|
const tr = step.toolResults[i];
|
|
231
|
-
|
|
250
|
+
// AI SDK v6 renamed `args` → `input`. Try v6 first.
|
|
251
|
+
const args = step.toolCalls[i]?.input ?? step.toolCalls[i]?.args;
|
|
232
252
|
const errored = isToolResultError(tr);
|
|
253
|
+
const resultSummary = summarizeToolResult(tr);
|
|
233
254
|
auditToolCall(taskActionAudit, tr.toolName, errored, tr);
|
|
255
|
+
toolCallSummary.push({
|
|
256
|
+
tool: tr.toolName,
|
|
257
|
+
args,
|
|
258
|
+
result: resultSummary,
|
|
259
|
+
ok: !errored,
|
|
260
|
+
});
|
|
234
261
|
logActivity(agentDir, {
|
|
235
262
|
type: 'tool_call',
|
|
236
263
|
tool: tr.toolName,
|
|
237
264
|
args,
|
|
238
|
-
result:
|
|
265
|
+
result: resultSummary,
|
|
239
266
|
ok: !errored,
|
|
240
267
|
});
|
|
241
268
|
}
|
|
242
269
|
} else if (step.toolResults?.length) {
|
|
243
270
|
for (const tr of step.toolResults) {
|
|
244
271
|
const errored = isToolResultError(tr);
|
|
272
|
+
const resultSummary = summarizeToolResult(tr);
|
|
245
273
|
auditToolCall(taskActionAudit, tr.toolName, errored, tr);
|
|
274
|
+
toolCallSummary.push({
|
|
275
|
+
tool: tr.toolName,
|
|
276
|
+
result: resultSummary,
|
|
277
|
+
ok: !errored,
|
|
278
|
+
});
|
|
246
279
|
logActivity(agentDir, {
|
|
247
280
|
type: 'tool_call',
|
|
248
281
|
tool: tr.toolName,
|
|
249
|
-
result:
|
|
282
|
+
result: resultSummary,
|
|
250
283
|
ok: !errored,
|
|
251
284
|
});
|
|
252
285
|
}
|
|
@@ -259,10 +292,16 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
259
292
|
if (tc.toolName === 'comment_create') activity.commentsCreated++;
|
|
260
293
|
if (tc.toolName === 'vote_cast') activity.votesGiven++;
|
|
261
294
|
// No paired result here — assume executed, can't audit.
|
|
295
|
+
const args = tc.input ?? tc.args;
|
|
296
|
+
toolCallSummary.push({
|
|
297
|
+
tool: tc.toolName,
|
|
298
|
+
args,
|
|
299
|
+
result: 'executed',
|
|
300
|
+
});
|
|
262
301
|
logActivity(agentDir, {
|
|
263
302
|
type: 'tool_call',
|
|
264
303
|
tool: tc.toolName,
|
|
265
|
-
args
|
|
304
|
+
args,
|
|
266
305
|
result: 'executed',
|
|
267
306
|
});
|
|
268
307
|
}
|
|
@@ -281,12 +320,12 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
281
320
|
if (result.steps?.length) {
|
|
282
321
|
for (const step of result.steps) {
|
|
283
322
|
for (const tc of step.toolCalls || []) {
|
|
284
|
-
collectActions(tc.toolName, tc.args);
|
|
323
|
+
collectActions(tc.toolName, tc.input ?? tc.args);
|
|
285
324
|
}
|
|
286
325
|
}
|
|
287
326
|
} else if (result.toolCalls?.length) {
|
|
288
327
|
for (const tc of result.toolCalls) {
|
|
289
|
-
collectActions(tc.toolName, tc.args);
|
|
328
|
+
collectActions(tc.toolName, tc.input ?? tc.args);
|
|
290
329
|
}
|
|
291
330
|
}
|
|
292
331
|
// Keep only last 50 actions to bound memory
|
|
@@ -310,7 +349,8 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
310
349
|
errorMessage,
|
|
311
350
|
output: {
|
|
312
351
|
text: result.text || '',
|
|
313
|
-
|
|
352
|
+
toolCallCount: activity.toolCalls,
|
|
353
|
+
toolCalls: toolCallSummary,
|
|
314
354
|
toolErrors: taskActionAudit.toolErrors,
|
|
315
355
|
note: 'Marked FAILED because the action tools did not succeed. The model\'s text may claim success but the underlying tool calls errored.',
|
|
316
356
|
},
|
|
@@ -326,7 +366,8 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
326
366
|
await completeAgentTask(config.man.village_agent_id, activeTask.id, {
|
|
327
367
|
output: {
|
|
328
368
|
text: result.text || '',
|
|
329
|
-
|
|
369
|
+
toolCallCount: activity.toolCalls,
|
|
370
|
+
toolCalls: toolCallSummary,
|
|
330
371
|
toolErrors: taskActionAudit.toolErrors.length > 0 ? taskActionAudit.toolErrors : undefined,
|
|
331
372
|
},
|
|
332
373
|
tokensUsed: result.usage?.totalTokens || 0,
|
|
@@ -452,7 +493,10 @@ const ACTION_TOOLS = new Set([
|
|
|
452
493
|
|
|
453
494
|
function flattenToolResultText(tr) {
|
|
454
495
|
if (!tr) return '';
|
|
455
|
-
|
|
496
|
+
// AI SDK v6 renamed `result` → `output`. Fall back to the old name so
|
|
497
|
+
// older SDK versions still work if anyone pins them.
|
|
498
|
+
const r = tr.output ?? tr.result;
|
|
499
|
+
if (r == null) return '';
|
|
456
500
|
if (typeof r === 'string') return r;
|
|
457
501
|
if (Array.isArray(r?.content)) {
|
|
458
502
|
return r.content
|
|
@@ -102,15 +102,23 @@ export async function getMCPTools(agentDir, agentConfig) {
|
|
|
102
102
|
},
|
|
103
103
|
}), `${name} (${transport} ${server.url})`);
|
|
104
104
|
} else {
|
|
105
|
-
// Local MCP servers via stdio transport
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
// Local MCP servers via stdio transport.
|
|
106
|
+
//
|
|
107
|
+
// @ai-sdk/mcp v1 doesn't accept a `{ type: 'stdio', ... }` config —
|
|
108
|
+
// its MCPTransportConfig is HTTP-only (`'sse' | 'http'`). For stdio
|
|
109
|
+
// you must instantiate the transport class explicitly from the
|
|
110
|
+
// `/mcp-stdio` subpath and pass the instance.
|
|
111
|
+
const { Experimental_StdioMCPTransport } =
|
|
112
|
+
await import('@ai-sdk/mcp/mcp-stdio');
|
|
113
|
+
const stdioTransport = new Experimental_StdioMCPTransport({
|
|
114
|
+
command: server.command,
|
|
115
|
+
args: server.args || [],
|
|
116
|
+
env: { ...process.env, ...resolveEnvVars(server.env || {}) },
|
|
117
|
+
});
|
|
118
|
+
client = await withTimeout(
|
|
119
|
+
createMCPClient({ transport: stdioTransport }),
|
|
120
|
+
`${name} (stdio ${server.command})`,
|
|
121
|
+
);
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
activeClients.push(client);
|
|
@@ -4,6 +4,7 @@ import { villageSpinner, brand } from '../utils/brand.js';
|
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
5
|
import { existsSync, readFileSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
7
8
|
import { execSync } from 'child_process';
|
|
8
9
|
import { isAuthenticated, getAccessToken } from '../utils/auth.js';
|
|
9
10
|
import { getConfig, setConfig } from '../utils/config.js';
|
|
@@ -41,7 +42,7 @@ import {
|
|
|
41
42
|
readAgentLogs,
|
|
42
43
|
getLogFilePath,
|
|
43
44
|
} from '../utils/local-agent.js';
|
|
44
|
-
import { scaffoldAgent, TOOL_CATALOG } from '../utils/agent-scaffolder.js';
|
|
45
|
+
import { scaffoldAgent, TOOL_CATALOG, resolveCatalogEntry } from '../utils/agent-scaffolder.js';
|
|
45
46
|
import { formatLocalAgentList, formatLocalAgentStatus } from '../utils/formatters.js';
|
|
46
47
|
|
|
47
48
|
// ── Create Local Agent (wizard) ─────────────────────────
|
|
@@ -117,11 +118,10 @@ export async function agentCreateLocalCommand() {
|
|
|
117
118
|
message: 'Which tools should your agent have?',
|
|
118
119
|
choices: [
|
|
119
120
|
{ name: 'MyVillageOS MCP (feed, posts, communities, wallet)', value: 'myvillage', checked: true, disabled: 'always enabled' },
|
|
120
|
-
{ name: 'Local Files (read/write
|
|
121
|
+
{ name: 'Local Files (read/write a sandboxed workspace)', value: 'filesystem', checked: true },
|
|
121
122
|
{ name: 'Gmail', value: 'gmail' },
|
|
122
123
|
{ name: 'Calendar', value: 'calendar' },
|
|
123
124
|
{ name: 'GitHub', value: 'github' },
|
|
124
|
-
{ name: 'Browser', value: 'browser' },
|
|
125
125
|
],
|
|
126
126
|
},
|
|
127
127
|
{
|
|
@@ -396,7 +396,7 @@ export async function agentStartCommand(name) {
|
|
|
396
396
|
const toolsConfig = readToolsYaml(name);
|
|
397
397
|
if (toolsConfig.servers?.['man-feed'] && !toolsConfig.servers?.['myvillage']) {
|
|
398
398
|
delete toolsConfig.servers['man-feed'];
|
|
399
|
-
toolsConfig.servers = { myvillage:
|
|
399
|
+
toolsConfig.servers = { myvillage: resolveCatalogEntry('myvillage', name), ...toolsConfig.servers };
|
|
400
400
|
writeToolsYaml(name, toolsConfig);
|
|
401
401
|
}
|
|
402
402
|
|
|
@@ -648,9 +648,22 @@ function printLogEntry(entry) {
|
|
|
648
648
|
console.log(` ${' '.repeat(10)} ${brand.teal(`Tokens: ${entry.tokensUsed.prompt || 0}p / ${entry.tokensUsed.completion || 0}c`)}`);
|
|
649
649
|
}
|
|
650
650
|
break;
|
|
651
|
-
case 'tool_call':
|
|
652
|
-
|
|
651
|
+
case 'tool_call': {
|
|
652
|
+
const arrow = entry.ok === false ? chalk.red('→') : brand.teal('→');
|
|
653
|
+
const statusLabel = entry.ok === false ? chalk.red('error') : '';
|
|
654
|
+
console.log(` ${time} ${chalk.yellow('TOOL')} ${entry.tool || '?'} ${arrow} ${statusLabel}`);
|
|
655
|
+
// Show args (if any) and the result content on indented sub-lines,
|
|
656
|
+
// so a developer tailing logs can actually see what the agent did.
|
|
657
|
+
if (entry.args && Object.keys(entry.args).length > 0) {
|
|
658
|
+
const argsStr = truncateLog(JSON.stringify(entry.args), 200);
|
|
659
|
+
console.log(` ${' '.repeat(10)} ${brand.teal('args:')} ${argsStr}`);
|
|
660
|
+
}
|
|
661
|
+
if (entry.result && entry.result !== 'ok' && entry.result !== 'executed') {
|
|
662
|
+
const resultStr = truncateLog(String(entry.result), 200);
|
|
663
|
+
console.log(` ${' '.repeat(10)} ${brand.teal('result:')} ${resultStr}`);
|
|
664
|
+
}
|
|
653
665
|
break;
|
|
666
|
+
}
|
|
654
667
|
case 'error':
|
|
655
668
|
console.log(` ${time} ${chalk.red('ERR')} ${entry.error || entry.message || 'Unknown error'}`);
|
|
656
669
|
break;
|
|
@@ -710,11 +723,15 @@ export async function agentAddToolCommand(name, tool) {
|
|
|
710
723
|
}
|
|
711
724
|
|
|
712
725
|
toolsConfig.servers = toolsConfig.servers || {};
|
|
713
|
-
toolsConfig.servers[tool] =
|
|
726
|
+
toolsConfig.servers[tool] = resolveCatalogEntry(tool, name);
|
|
714
727
|
writeToolsYaml(name, toolsConfig);
|
|
715
728
|
|
|
716
729
|
console.log(brand.green(` \u2713 Added "${tool}" to agent "${name}".`));
|
|
717
730
|
|
|
731
|
+
if (tool === 'filesystem') {
|
|
732
|
+
const workspace = join(homedir(), '.myvillage', 'agents', name, 'workspace');
|
|
733
|
+
console.log(brand.teal(` Workspace: ${workspace}`));
|
|
734
|
+
}
|
|
718
735
|
if (tool === 'github') {
|
|
719
736
|
console.log(brand.teal(' Note: Set GITHUB_TOKEN in your environment for GitHub access.'));
|
|
720
737
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
3
4
|
import { stringify as stringifyYaml } from 'yaml';
|
|
4
5
|
|
|
5
6
|
// ── MCP Tool Catalog ────────────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// Static-shape entries. The `filesystem` entry's allow-listed root is
|
|
9
|
+
// resolved per-agent at scaffold/add-tool time — see
|
|
10
|
+
// resolveCatalogEntry() — so each agent gets its own sandboxed workspace.
|
|
6
11
|
|
|
7
12
|
const TOOL_CATALOG = {
|
|
8
13
|
myvillage: {
|
|
@@ -12,8 +17,10 @@ const TOOL_CATALOG = {
|
|
|
12
17
|
},
|
|
13
18
|
filesystem: {
|
|
14
19
|
command: 'npx',
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
// Last arg is a placeholder; replaced with `<agentDir>/workspace`
|
|
21
|
+
// by resolveCatalogEntry().
|
|
22
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', '__AGENT_WORKSPACE__'],
|
|
23
|
+
description: 'Read/write files under the agent\'s sandboxed workspace',
|
|
17
24
|
},
|
|
18
25
|
gmail: {
|
|
19
26
|
command: 'npx',
|
|
@@ -31,15 +38,41 @@ const TOOL_CATALOG = {
|
|
|
31
38
|
env: { GITHUB_TOKEN: '${GITHUB_TOKEN}' },
|
|
32
39
|
description: 'GitHub repository access',
|
|
33
40
|
},
|
|
34
|
-
browser:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
description: 'Navigate web pages, extract content',
|
|
38
|
-
},
|
|
41
|
+
// browser: removed in v1.27.0 — the previously catalog'd
|
|
42
|
+
// `@anthropic/mcp-puppeteer` package is no longer published under that
|
|
43
|
+
// name. Add a working browser MCP back when one stabilizes.
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
export { TOOL_CATALOG };
|
|
42
47
|
|
|
48
|
+
// ── Catalog Resolution ──────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns a deep-copied catalog entry with per-agent placeholders
|
|
52
|
+
* resolved (e.g. the filesystem workspace path). Also creates any
|
|
53
|
+
* directories the entry depends on (the workspace dir for filesystem).
|
|
54
|
+
*
|
|
55
|
+
* Use this anywhere you'd otherwise do `{ ...TOOL_CATALOG[toolId] }`
|
|
56
|
+
* to write into a tools.yaml.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveCatalogEntry(toolId, agentName) {
|
|
59
|
+
const entry = TOOL_CATALOG[toolId];
|
|
60
|
+
if (!entry) return null;
|
|
61
|
+
const resolved = JSON.parse(JSON.stringify(entry));
|
|
62
|
+
|
|
63
|
+
if (toolId === 'filesystem') {
|
|
64
|
+
const workspace = join(homedir(), '.myvillage', 'agents', agentName, 'workspace');
|
|
65
|
+
if (!existsSync(workspace)) {
|
|
66
|
+
mkdirSync(workspace, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
resolved.args = resolved.args.map(a =>
|
|
69
|
+
a === '__AGENT_WORKSPACE__' ? workspace : a,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
// ── Check-in Interval Map ───────────────────────────────
|
|
44
77
|
|
|
45
78
|
const INTERVAL_MAP = {
|
|
@@ -73,7 +106,7 @@ export function scaffoldAgent(agentDir, options) {
|
|
|
73
106
|
// Write tools.yaml
|
|
74
107
|
writeFileSync(
|
|
75
108
|
join(agentDir, 'tools.yaml'),
|
|
76
|
-
generateToolsYaml(tools)
|
|
109
|
+
generateToolsYaml(tools, name)
|
|
77
110
|
);
|
|
78
111
|
|
|
79
112
|
// Write logs/.gitkeep
|
|
@@ -156,17 +189,16 @@ When you receive a TASK in your context, follow these rules:
|
|
|
156
189
|
|
|
157
190
|
// ── Tools YAML Generation ───────────────────────────────
|
|
158
191
|
|
|
159
|
-
function generateToolsYaml(selectedTools) {
|
|
192
|
+
function generateToolsYaml(selectedTools, agentName) {
|
|
160
193
|
const servers = {};
|
|
161
194
|
|
|
162
195
|
// myvillage is always included
|
|
163
|
-
servers['myvillage'] =
|
|
196
|
+
servers['myvillage'] = resolveCatalogEntry('myvillage', agentName);
|
|
164
197
|
|
|
165
198
|
for (const toolId of selectedTools) {
|
|
166
199
|
if (toolId === 'myvillage') continue;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
200
|
+
const entry = resolveCatalogEntry(toolId, agentName);
|
|
201
|
+
if (entry) servers[toolId] = entry;
|
|
170
202
|
}
|
|
171
203
|
|
|
172
204
|
return stringifyYaml({ servers }, { lineWidth: 0 });
|