ai-agentmanager 1.12.0 → 1.14.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/CHANGELOG.md +20 -0
- package/README.md +4 -1
- package/docs/agentkit.md +15 -1
- package/package.json +1 -1
- package/public/app.js +66 -7
- package/public/built.js +147 -22
- package/public/index.html +9 -3
- package/public/menu.js +18 -0
- package/public/style.css +12 -0
- package/src/agentkit/engines.js +14 -1
- package/src/builder.js +207 -82
- package/src/cli.js +33 -0
- package/src/manager.js +80 -4
- package/src/rules.js +161 -0
- package/src/server.js +27 -5
- package/src/stuck.js +9 -2
package/public/style.css
CHANGED
|
@@ -968,8 +968,20 @@ body.remote .settings-links .local-only { display: none; }
|
|
|
968
968
|
.built-body details > summary { cursor: pointer; padding: 4px 0; }
|
|
969
969
|
.built-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
970
970
|
.built-exe { margin: 10px 0; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; }
|
|
971
|
+
.built-session, .built-panel { margin: 0 0 10px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel, transparent); }
|
|
972
|
+
.built-session .line1, .built-panel .line1 { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
973
|
+
.built-panel { margin: 10px 0; }
|
|
974
|
+
.built-panel p { margin: 6px 0 0; }
|
|
975
|
+
.built-change { display: flex; gap: 6px; margin-top: 6px; align-items: flex-end; }
|
|
976
|
+
.built-change textarea { flex: 1; min-width: 0; }
|
|
971
977
|
.built-exe-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
|
|
972
978
|
.built-tests li { margin-bottom: 8px; } .built-tests li.pass b { color: var(--green); } .built-tests li.fail b { color: var(--red); }
|
|
973
979
|
.built-tests pre, .built-runs div { white-space: pre-wrap; font-size: 12.5px; }
|
|
974
980
|
.code-area { width: 100%; font: 12.5px/1.45 var(--mono); margin-top: 6px; }
|
|
975
981
|
.built-runs { list-style: none; padding: 0; } .built-runs li { padding: 6px 0; border-bottom: 1px solid var(--border); }
|
|
982
|
+
|
|
983
|
+
/* Approval rules (Settings) and agents waiting on the same command (inbox) */
|
|
984
|
+
.rules-list { list-style: none; padding: 0; margin: 6px 0 0; display: grid; gap: 6px; }
|
|
985
|
+
.rules-list li { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
986
|
+
.rules-list li button { margin-left: auto; }
|
|
987
|
+
.need.group { border-style: dashed; }
|
package/src/agentkit/engines.js
CHANGED
|
@@ -58,6 +58,19 @@ export function approverServer({ url, token }) {
|
|
|
58
58
|
return { command: process.execPath, args: isSea() ? ['approver'] : [AGENTKIT_BIN, 'approver'], env: { AGENTKIT_APPROVAL_URL: url, AGENTKIT_APPROVAL_TOKEN: token } };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Claude Code scopes file writes with Edit(<path>) rules (they cover Write too); a Write(<path>)
|
|
63
|
+
* rule is ignored. Each Write(<path>) also gets its Edit(<path>), so writing there is allowed.
|
|
64
|
+
*/
|
|
65
|
+
export function claudeRules(allow) {
|
|
66
|
+
const out = [...allow];
|
|
67
|
+
for (const p of allow) {
|
|
68
|
+
const m = /^Write\((.+)\)$/.exec(p);
|
|
69
|
+
if (m && !out.includes(`Edit(${m[1]})`)) out.push(`Edit(${m[1]})`);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
/**
|
|
62
75
|
* One turn. `session`: the engine session to continue (null starts one); returns
|
|
63
76
|
* { text, session, costUsd }. `system` goes in as an extra system prompt where the engine has one,
|
|
@@ -77,7 +90,7 @@ export async function turn(agent, { prompt, system = '', session = null, mcpFile
|
|
|
77
90
|
args.push('--setting-sources', 'project', '--strict-mcp-config');
|
|
78
91
|
if (mcpFile) args.push('--mcp-config', mcpFile);
|
|
79
92
|
// The approve list wins: an allow pattern that also covers an approval tool goes to the approver.
|
|
80
|
-
const allow = approver ? allowedOutright(agent) : agent.allow;
|
|
93
|
+
const allow = claudeRules(approver ? allowedOutright(agent) : agent.allow);
|
|
81
94
|
if (allow.length) args.push('--allowedTools', ...allow);
|
|
82
95
|
// With an approver, Claude asks it before anything not allowed outright (it says no to tools
|
|
83
96
|
// off the approve list); without one, approval tools are simply not available.
|
package/src/builder.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// The agent builder: a task or workflow in words → an
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The agent builder: a task or workflow in words → an agentkit agent (docs/agentkit.md), developed
|
|
2
|
+
// in an agent session that works in the agent's folder: it writes the analysis, spec, instructions
|
|
3
|
+
// and tests, and makes the changes the person asks for. The person tests, runs, packages and
|
|
4
|
+
// downloads it here. Built agents live in ~/.agentmanager/built/<name>/ as ordinary agentkit folders.
|
|
4
5
|
import { spawn } from 'node:child_process';
|
|
5
6
|
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
6
8
|
import path from 'node:path';
|
|
7
9
|
import { fileURLToPath } from 'node:url';
|
|
8
10
|
import YAML from 'yaml';
|
|
@@ -19,79 +21,139 @@ const err = (status, message) => Object.assign(new Error(message), { status });
|
|
|
19
21
|
const now = () => new Date().toISOString();
|
|
20
22
|
const slug = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'agent';
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
const DOCS = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'docs', 'agentkit.md');
|
|
25
|
+
const ENGINES = ['claude', 'codex', 'muse'];
|
|
26
|
+
|
|
27
|
+
/** The agent's name from the task: its first few words. */
|
|
28
|
+
export function nameFrom(task) {
|
|
29
|
+
const words = String(task).toLowerCase().replace(/https?:\/\/\S+/g, ' ').split(/[^a-z0-9]+/).filter((w) => w && !STOP.has(w));
|
|
30
|
+
const n = slug(words.slice(0, 4).join('-')).slice(0, 32).replace(/-+$/, '');
|
|
31
|
+
return /^[a-z][a-z0-9-]{1,}$/.test(n) ? n : `agent-${n}`.replace(/-+$/, '').slice(0, 32);
|
|
32
|
+
}
|
|
33
|
+
const STOP = new Set(['a', 'an', 'the', 'to', 'of', 'for', 'and', 'or', 'in', 'on', 'by', 'with', 'from', 'that', 'this', 'it', 'is', 'are', 'be', 'i', 'we', 'my', 'our', 'want', 'need', 'agent', 'build', 'make', 'create', 'please', 'which', 'who']);
|
|
34
|
+
|
|
35
|
+
/** The commands its session uses to check and try the agent. */
|
|
36
|
+
export function agentkitCommands(dir) {
|
|
37
|
+
return { check: `${process.execPath} ${AGENTKIT} check`, run: `${process.execPath} ${AGENTKIT} run ${dir} --once` };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What its session may do without a permission prompt: check the agent, read the reference. */
|
|
41
|
+
export function builderAllowedTools(dir) {
|
|
42
|
+
return [`Bash(${agentkitCommands(dir).check}:*)`, `Read(/${DOCS})`];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What a session that builds an agent is told (appended to its system prompt on every launch):
|
|
47
|
+
* the folder is the agent, the files it keeps, the format, and how to check and try it.
|
|
48
|
+
*/
|
|
49
|
+
export function builderPrompt({ dir, name, engine = 'claude' }) {
|
|
50
|
+
const cmd = agentkitCommands(dir);
|
|
23
51
|
return [
|
|
24
|
-
|
|
25
|
-
`${engine === 'claude' ? 'Claude Code' : engine} as its engine, uses tools through MCP servers, and is reached by people (chat) and other agents (A2A, MCP).`,
|
|
26
|
-
'From the task below, write the analysis and the agent. Be concrete; do not invent systems the task does not mention.',
|
|
52
|
+
`You are building an autonomous agent together with the person: an agentkit agent that runs on its own, continuously, and does one job well. This folder (${dir}) is the agent. Its name is "${name}"; keep it (it is the folder's name).`,
|
|
27
53
|
'',
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
' "instructions": "<Markdown: how to do the job, step by step; what to never do; when to ask>",',
|
|
34
|
-
' "tools": [{"name": "<short id>", "why": "<what it needs it for>", "command": "<MCP server command, e.g. npx>", "args": ["…"], "env": {"VAR": "${VAR}"}}],',
|
|
35
|
-
' "allow": ["<engine tool names it may use without asking, e.g. Read, Grep, mcp__<tool>__*>"],',
|
|
36
|
-
' "approve": ["<tools that change things in the outside world and need a person\'s yes>"],',
|
|
37
|
-
' "triggers": [{"every": "30m"} | {"cron": "0 9 * * 1-5"} | {"webhook": "/hooks/<name>"} | {"watch": "./inbox"}],',
|
|
38
|
-
' "tests": [{"input": "<a realistic input>", "expect": "<what a good result does or contains>"}]',
|
|
39
|
-
'}',
|
|
54
|
+
'Keep these files current:',
|
|
55
|
+
'- agent.yaml: the spec (below).',
|
|
56
|
+
'- instructions.md: how to do the job, step by step; what it must never do; when it asks a person.',
|
|
57
|
+
'- tests.yaml: 3 to 8 test cases, a YAML list of {input, expect}: the normal case, an edge case, and a case it should refuse or escalate. The person runs them from the Built tab; each result is judged against `expect`.',
|
|
58
|
+
'- analysis.json: {"analysis": {"problem": "<the job and why>", "inputs": [...], "outputs": [...], "risks": ["<what could go wrong, what needs a person>"]}, "tools": [{"name": "<MCP server id>", "why": "<what for>"}]}. The person reads it in the Built tab; update it when the design changes.',
|
|
40
59
|
'',
|
|
41
|
-
|
|
42
|
-
'
|
|
43
|
-
'
|
|
60
|
+
`agent.yaml has: name, title, description (one line), goal (what is true when it has done its job, for each input), instructions: instructions.md, engine: ${engine},`,
|
|
61
|
+
' tools (MCP servers by id: {command, args, env}; only servers that really exist; secrets only as "${VAR}"),',
|
|
62
|
+
' allow (engine tools it may use without asking, e.g. Read, Grep, "mcp__<id>__*"; to let it write files only under a folder: "Edit(./out/**)", which covers Write), approve (tools that change things in the outside world and need a person\'s yes),',
|
|
63
|
+
' triggers (only when the task implies running on its own: {every: 30m}, {cron: "0 9 * * 1-5"}, {webhook: /hooks/<name>}, {watch: ./inbox}; a chat-only agent has none),',
|
|
64
|
+
' loop: {plan: true, check: true, maxSteps: 4}, memory: true, interfaces: {http: {port: 0}, a2a: true, acp: true, mcp: true}.',
|
|
65
|
+
`The full reference: ${DOCS}.`,
|
|
44
66
|
'',
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
'Write and edit these files with your file tools (Write, Edit), not with shell commands.',
|
|
68
|
+
`After every change, check it loads: ${cmd.check} ${dir}`,
|
|
69
|
+
`To try one input for real (it runs with its allowed tools; tools that need approval are refused): ${cmd.run} "<input>". Only with inputs that are safe to run for real.`,
|
|
70
|
+
'Be concrete; do not invent systems the task does not mention. When something only the person knows is missing (an account, a team name, a label set), ask.',
|
|
71
|
+
'The person tests, starts, packages and downloads it from the Built tab; you do not start it continuously or package it. When they ask for a change, make it in these files, check, and say in a sentence what changed.',
|
|
49
72
|
].join('\n');
|
|
50
73
|
}
|
|
51
74
|
|
|
52
|
-
|
|
75
|
+
/** The first message of a build session. */
|
|
76
|
+
export function buildIntent({ task, name, engine }) {
|
|
77
|
+
return `Build the agent "${name}" (engine: ${engine}) for this task:\n\n${task}\n\nWrite analysis.json, agent.yaml, instructions.md and tests.yaml in this folder, check it, then tell me what it does and what I should decide or provide.`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function judgePrompt({ goal, input, expect, result, files = [] }) {
|
|
53
81
|
return [
|
|
54
82
|
"You check one test of an autonomous agent. Decide if the agent's result meets the expectation for the input.",
|
|
83
|
+
'The files it created or changed during the run are shown too: judge what it did from them, not only from what it says.',
|
|
55
84
|
'Be strict about substance, lenient about wording. Reply with JSON only: {"pass": true|false, "why": "<one sentence>"}.',
|
|
56
85
|
`<goal>\n${goal}\n</goal>`,
|
|
57
86
|
`<input>\n${input}\n</input>`,
|
|
58
87
|
`<expectation>\n${expect}\n</expectation>`,
|
|
59
88
|
`<result>\n${result}\n</result>`,
|
|
89
|
+
...(files.length ? ['<files_written>', ...files.map((f) => `--- ${f.path}${f.truncated ? ' (first part)' : ''}\n${f.text}`), '</files_written>'] : ['<files_written>\n(none)\n</files_written>']),
|
|
60
90
|
].join('\n');
|
|
61
91
|
}
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
93
|
+
const SKIP = new Set(['.agentkit', '.git', 'node_modules', 'dist']);
|
|
94
|
+
/** Files under `dir` (not its own state, git or build output) with when each last changed. */
|
|
95
|
+
export function snapshot(dir, { max = 5000 } = {}) {
|
|
96
|
+
const seen = new Map();
|
|
97
|
+
const walk = (d) => {
|
|
98
|
+
let entries = [];
|
|
99
|
+
try {
|
|
100
|
+
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const e of entries) {
|
|
105
|
+
if (seen.size >= max) return;
|
|
106
|
+
const p = path.join(d, e.name);
|
|
107
|
+
if (e.isDirectory()) {
|
|
108
|
+
if (!SKIP.has(e.name)) walk(p);
|
|
109
|
+
} else if (e.isFile()) {
|
|
110
|
+
const st = fs.statSync(p, { throwIfNoEntry: false });
|
|
111
|
+
if (st) seen.set(p, `${st.mtimeMs}:${st.size}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
80
114
|
};
|
|
81
|
-
|
|
115
|
+
walk(dir);
|
|
116
|
+
return seen;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The text files created or changed since `before` (a snapshot), for the judge; a few, each capped. */
|
|
120
|
+
export function filesWritten(dir, before, { root = dir, limit = 6, bytes = 4000 } = {}) {
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const [p, sig] of snapshot(dir)) {
|
|
123
|
+
if (before.get(p) === sig || out.length >= limit) continue;
|
|
124
|
+
const buf = fs.readFileSync(p);
|
|
125
|
+
if (buf.subarray(0, 2000).includes(0)) continue; // binary
|
|
126
|
+
out.push({ path: path.relative(root, p), text: buf.subarray(0, bytes).toString('utf8'), truncated: buf.length > bytes });
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A throwaway copy of the agent's folder for one test run, so what a run writes (outputs, memory,
|
|
133
|
+
* changed fixtures) doesn't change the agent or the next test. It keeps the agent's memory, not its
|
|
134
|
+
* engine sessions, run logs, executables, dependencies or git data.
|
|
135
|
+
*/
|
|
136
|
+
export function testCopy(dir) {
|
|
137
|
+
const copy = fs.mkdtempSync(path.join(os.tmpdir(), `agentkit-test-${path.basename(dir)}-`));
|
|
138
|
+
const state = path.join(dir, '.agentkit');
|
|
139
|
+
const keep = (src) => src === dir || (path.dirname(src) === state ? path.basename(src) === 'memory.md' : src === state || !SKIP.has(path.basename(src)));
|
|
140
|
+
fs.cpSync(dir, copy, { recursive: true, filter: keep });
|
|
141
|
+
return copy;
|
|
82
142
|
}
|
|
83
143
|
|
|
84
144
|
export class Builder {
|
|
85
|
-
/** `oneShot(prompt)` answers a
|
|
145
|
+
/** `oneShot(prompt)` answers a judge prompt (the daemon's writer engine). */
|
|
146
|
+
/** `sessions`: the daemon's agent sessions ({ create, get, tell, stop }); each built agent is developed in one. */
|
|
86
147
|
/** `reportEnv()`: settings that make an agent started here report to this daemon. */
|
|
87
|
-
constructor({ oneShot, env = process.env, runTurn = engineTurn, pack = packageAgent, reportEnv = () => ({}) }) {
|
|
148
|
+
constructor({ oneShot, sessions, env = process.env, runTurn = engineTurn, pack = packageAgent, reportEnv = () => ({}) }) {
|
|
88
149
|
this.reportEnv = reportEnv;
|
|
89
150
|
this.oneShot = oneShot;
|
|
151
|
+
this.sessions = sessions;
|
|
90
152
|
this.env = env;
|
|
91
153
|
this.runTurn = runTurn;
|
|
92
154
|
this.pack = pack;
|
|
93
155
|
this.procs = new Map(); // name -> child process of `agentkit run`
|
|
94
|
-
this.pending = new Map(); // name -> what's in progress (
|
|
156
|
+
this.pending = new Map(); // name -> what's in progress (testing, packaging)
|
|
95
157
|
}
|
|
96
158
|
|
|
97
159
|
dir(name) {
|
|
@@ -128,59 +190,108 @@ export class Builder {
|
|
|
128
190
|
const read = (f) => (fs.existsSync(path.join(dir, f)) ? fs.readFileSync(path.join(dir, f), 'utf8') : '');
|
|
129
191
|
let spec = null;
|
|
130
192
|
let specError = null;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
193
|
+
const yaml = read('agent.yaml');
|
|
194
|
+
if (yaml) {
|
|
195
|
+
try {
|
|
196
|
+
spec = loadAgent(dir, { env: this.env });
|
|
197
|
+
} catch (e) {
|
|
198
|
+
specError = e.message;
|
|
199
|
+
}
|
|
135
200
|
}
|
|
136
201
|
const p = this.procs.get(name);
|
|
137
202
|
let runs = [];
|
|
138
203
|
if (spec) runs = new Agent(spec, { env: this.env }).runs(20);
|
|
204
|
+
// Its session keeps analysis.json and tests.yaml current; read them as they are now.
|
|
205
|
+
let design = {};
|
|
206
|
+
try {
|
|
207
|
+
design = JSON.parse(read('analysis.json') || '{}');
|
|
208
|
+
} catch {}
|
|
209
|
+
let tests = m.tests ?? [];
|
|
210
|
+
try {
|
|
211
|
+
tests = (YAML.parse(read('tests.yaml') || '[]') ?? []).filter((t) => t?.input).map((t) => ({ input: String(t.input), expect: String(t.expect ?? '') }));
|
|
212
|
+
} catch {}
|
|
213
|
+
// Until its spec loads, the status follows the files: designing, then designed or needs fixing.
|
|
214
|
+
let status = m.status;
|
|
215
|
+
if (!yaml) status = 'designing';
|
|
216
|
+
else if (specError) status = 'needs-fixing';
|
|
217
|
+
else if (['designing', 'needs-fixing'].includes(status)) status = 'designed';
|
|
218
|
+
const s = m.session ? this.sessions?.get(m.session) : null;
|
|
219
|
+
// When its files last changed (its session edits them): results and executables from before are out of date.
|
|
220
|
+
const changedAt = new Date(Math.max(0, ...['agent.yaml', 'instructions.md', 'tests.yaml'].map((f) => fs.statSync(path.join(dir, f), { throwIfNoEntry: false })?.mtimeMs ?? 0))).toISOString();
|
|
139
221
|
return {
|
|
140
222
|
...m,
|
|
141
223
|
name,
|
|
224
|
+
status,
|
|
142
225
|
title: spec?.title ?? name,
|
|
143
226
|
description: spec?.description ?? '',
|
|
144
227
|
dir,
|
|
145
|
-
yaml
|
|
228
|
+
yaml,
|
|
146
229
|
instructionsText: read('instructions.md'),
|
|
230
|
+
analysis: design.analysis ?? m.analysis ?? null,
|
|
231
|
+
tools: design.tools ?? m.tools ?? [],
|
|
232
|
+
tests,
|
|
147
233
|
specError,
|
|
148
|
-
engine: spec?.engine ?? null,
|
|
234
|
+
engine: spec?.engine ?? m.engine ?? null,
|
|
149
235
|
triggers: spec?.triggers.map((t) => t.label) ?? [],
|
|
150
236
|
missingEnv: spec?.missingEnv ?? [],
|
|
151
237
|
pending: this.pending.get(name) ?? null,
|
|
152
238
|
running: p ? { pid: p.child.pid, url: p.url, since: p.since } : null,
|
|
239
|
+
session: s ? { id: s.id, name: s.name, status: s.status, running: s.runtime.running, busy: s.runtime.busy, attention: s.runtime.attention ?? s.runtime.waiting?.text ?? null } : null,
|
|
153
240
|
runs,
|
|
154
241
|
platforms: PLATFORMS.filter((x) => process.platform === 'darwin' || !x.startsWith('macos-')),
|
|
155
242
|
host: HOST,
|
|
243
|
+
changedAt,
|
|
156
244
|
// Executables built since the last edit that are still there.
|
|
157
245
|
executables: Object.fromEntries(Object.entries(m.executables ?? {}).filter(([, x]) => fs.existsSync(path.join(dir, 'dist', x.file)))),
|
|
158
246
|
};
|
|
159
247
|
}
|
|
160
248
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Start building an agent for a task: its folder, and an agent session working in it that
|
|
251
|
+
* analyses the task and writes the agent (analysis, spec, instructions, tests). The person
|
|
252
|
+
* keeps developing it in that session.
|
|
253
|
+
*/
|
|
254
|
+
design({ task, engine = 'claude', name, attachments, permissionMode, model }) {
|
|
163
255
|
task = String(task ?? '').trim();
|
|
164
256
|
if (!task) throw err(400, 'Describe the task or workflow');
|
|
165
|
-
if (!
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
257
|
+
if (!ENGINES.includes(engine) && !/^acp:[a-z0-9-]+$/.test(engine)) throw err(400, 'engine: claude, codex, muse or acp:<name>');
|
|
258
|
+
if (!this.sessions) throw err(503, 'Agent sessions are not available');
|
|
259
|
+
const want = name ? slug(name) : nameFrom(task);
|
|
260
|
+
if (!/^[a-z][a-z0-9-]{1,40}$/.test(want)) throw err(400, 'name: lowercase letters, digits and dashes, starting with a letter');
|
|
261
|
+
let n = want;
|
|
262
|
+
for (let i = 2; fs.existsSync(path.join(root(), n)); i++) n = `${want.slice(0, 36)}-${i}`;
|
|
263
|
+
const dir = path.join(root(), n);
|
|
171
264
|
fs.mkdirSync(dir, { recursive: true });
|
|
172
|
-
|
|
173
|
-
fs.writeFileSync(path.join(dir, 'instructions.md'), String(d.instructions ?? '').trim() + '\n');
|
|
174
|
-
const tests = (d.tests ?? []).filter((t) => t?.input).slice(0, 8).map((t) => ({ input: String(t.input), expect: String(t.expect ?? '') }));
|
|
175
|
-
fs.writeFileSync(path.join(dir, 'tests.yaml'), YAML.stringify(tests));
|
|
176
|
-
// Check it loads; a broken design is kept so the person can fix it.
|
|
177
|
-
let specError = null;
|
|
265
|
+
this.saveMeta(n, { task, engine, results: null, status: 'designing', createdAt: now() });
|
|
178
266
|
try {
|
|
179
|
-
|
|
267
|
+
const s = this.sessions.create({ name: n, dir, intent: buildIntent({ task, name: n, engine }), builds: { name: n, engine }, attachments, permissionMode, model });
|
|
268
|
+
this.saveMeta(n, { session: s.id });
|
|
180
269
|
} catch (e) {
|
|
181
|
-
|
|
270
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
271
|
+
throw e;
|
|
272
|
+
}
|
|
273
|
+
return this.view(n);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Ask its session for a change (typed in when it's idle; started if it's closed). An agent with
|
|
278
|
+
* no session yet (built before sessions, or its session deleted) gets a new one.
|
|
279
|
+
*/
|
|
280
|
+
change(name, text) {
|
|
281
|
+
const dir = this.dir(name);
|
|
282
|
+
const m = this.meta(name);
|
|
283
|
+
if (!m) throw err(404, `No built agent "${name}"`);
|
|
284
|
+
text = String(text ?? '').trim();
|
|
285
|
+
const s = m.session ? this.sessions.get(m.session) : null;
|
|
286
|
+
if (s) {
|
|
287
|
+
if (!text) throw err(400, 'Say what to change');
|
|
288
|
+
this.sessions.tell(s.id, text);
|
|
289
|
+
} else {
|
|
290
|
+
const engine = this.view(name).engine ?? 'claude';
|
|
291
|
+
const intent = `This folder is the agent "${name}"${m.task ? `, built for this task:\n\n${m.task}\n\n` : '. '}Read agent.yaml, instructions.md and tests.yaml${fs.existsSync(path.join(dir, 'analysis.json')) ? ' and analysis.json' : ' and write analysis.json'}. ${text || 'Then tell me briefly what it does and wait for what to change.'}`;
|
|
292
|
+
const created = this.sessions.create({ name, dir, intent, builds: { name, engine } });
|
|
293
|
+
this.saveMeta(name, { session: created.id });
|
|
182
294
|
}
|
|
183
|
-
this.saveMeta(name, { task, analysis: d.analysis ?? null, tools: (d.tools ?? []).map((t) => ({ name: t.name, why: t.why ?? '' })), tests, results: null, status: specError ? 'needs-fixing' : 'designed', createdAt: now() });
|
|
184
295
|
return this.view(name);
|
|
185
296
|
}
|
|
186
297
|
|
|
@@ -227,21 +338,32 @@ export class Builder {
|
|
|
227
338
|
this.pending.set(name, 'testing');
|
|
228
339
|
this.saveMeta(name, { status: 'testing' });
|
|
229
340
|
try {
|
|
230
|
-
//
|
|
231
|
-
|
|
341
|
+
// Each test runs in its own copy of the agent's folder (a workdir outside it is used in place),
|
|
342
|
+
// and the judge sees what the run wrote there, not only what it says it did. Tests don't wait
|
|
343
|
+
// for people: approval tools are simply not available in them.
|
|
232
344
|
const results = [];
|
|
233
345
|
for (const t of tests) {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
346
|
+
const copy = testCopy(dir);
|
|
347
|
+
try {
|
|
348
|
+
const run = loadAgent(copy, { env: this.env });
|
|
349
|
+
const agent = new Agent({ ...run, triggers: [], report: null }, { env: this.env, engineTurn: this.runTurn, approvals: 'deny' });
|
|
350
|
+
const roots = [...new Set([copy, run.workdir])].filter((d, i, all) => !all.some((o, j) => j !== i && d.startsWith(o + path.sep)));
|
|
351
|
+
const before = roots.map((d) => snapshot(d));
|
|
352
|
+
const r = await agent.run(t.input, { source: 'test' });
|
|
353
|
+
const files = roots.flatMap((d, i) => filesWritten(d, before[i], { root: copy })).slice(0, 6);
|
|
354
|
+
let verdict = { pass: false, why: 'The run failed' };
|
|
355
|
+
if (r.status !== 'failed') {
|
|
356
|
+
try {
|
|
357
|
+
const v = parseJsonReply(await this.oneShot(judgePrompt({ goal: spec.goal, input: t.input, expect: t.expect, result: r.result, files })));
|
|
358
|
+
verdict = { pass: v.pass === true, why: String(v.why ?? '') };
|
|
359
|
+
} catch (e) {
|
|
360
|
+
verdict = { pass: false, why: `Could not judge: ${e.message}` };
|
|
361
|
+
}
|
|
242
362
|
}
|
|
363
|
+
results.push({ input: t.input, expect: t.expect, result: r.result, status: r.status, costUsd: r.costUsd, files: files.map((f) => f.path), ...verdict });
|
|
364
|
+
} finally {
|
|
365
|
+
fs.rmSync(copy, { recursive: true, force: true });
|
|
243
366
|
}
|
|
244
|
-
results.push({ input: t.input, expect: t.expect, result: r.result, status: r.status, costUsd: r.costUsd, ...verdict });
|
|
245
367
|
}
|
|
246
368
|
const passed = results.filter((r) => r.pass).length;
|
|
247
369
|
this.saveMeta(name, { results: { at: now(), passed, total: results.length, list: results }, status: passed === results.length ? 'tested' : 'failing' });
|
|
@@ -326,6 +448,9 @@ export class Builder {
|
|
|
326
448
|
|
|
327
449
|
remove(name) {
|
|
328
450
|
this.stop(name);
|
|
451
|
+
// Its session works in the folder about to go; close it (the conversation is kept).
|
|
452
|
+
const sid = this.meta(name)?.session;
|
|
453
|
+
if (sid && this.sessions?.get(sid)) this.sessions.stop(sid);
|
|
329
454
|
fs.rmSync(this.dir(name), { recursive: true, force: true });
|
|
330
455
|
return { ok: true };
|
|
331
456
|
}
|
package/src/cli.js
CHANGED
|
@@ -879,6 +879,8 @@ export async function main(argv) {
|
|
|
879
879
|
await attach(ref, { kind: `shell:${n}` });
|
|
880
880
|
});
|
|
881
881
|
|
|
882
|
+
const repoLabel = (k) => String(k).replace(/^dir:/, '').split('/').filter(Boolean).slice(-2).join('/');
|
|
883
|
+
|
|
882
884
|
program
|
|
883
885
|
.command('inbox')
|
|
884
886
|
.description('Everything waiting on you: prompts, questions, stuck agents, overlapping changes')
|
|
@@ -888,6 +890,11 @@ export async function main(argv) {
|
|
|
888
890
|
if (!items.length) return console.log(dim('Nothing needs you right now.'));
|
|
889
891
|
const label = { prompt: red('needs input'), waiting: red('waiting'), question: cyan('question'), review: cyan('AI review'), stuck: yellow('stuck?'), conflict: yellow('overlap') };
|
|
890
892
|
for (const it of items) {
|
|
893
|
+
if (it.kind === 'rule') {
|
|
894
|
+
console.log(`${cyan('always allow?')} ${bold(repoLabel(it.repo))} ${it.title} ${dim(`(allowed ${it.n} times)`)}`);
|
|
895
|
+
console.log(dim(' agentmgr rules'));
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
891
898
|
console.log(`${label[it.kind]} ${bold(it.agentName)} ${dim(it.agentId)} ${it.title}`);
|
|
892
899
|
if (it.kind === 'prompt') console.log(dim(` ${(it.prompt?.choices ?? []).map((c) => `${c.key}. ${c.label}`).join(' ')} → agentmgr attach ${it.agentId}`));
|
|
893
900
|
if (it.kind === 'question') {
|
|
@@ -901,6 +908,32 @@ export async function main(argv) {
|
|
|
901
908
|
}
|
|
902
909
|
});
|
|
903
910
|
|
|
911
|
+
program
|
|
912
|
+
.command('rules')
|
|
913
|
+
.description('Approval rules: commands always allowed per repo, and the ones the inbox suggests')
|
|
914
|
+
.argument('[action]', 'allow <n> | no <n> (suggestions), rm <n> (allowed)')
|
|
915
|
+
.argument('[n]')
|
|
916
|
+
.action(async (action, n) => {
|
|
917
|
+
await daemon();
|
|
918
|
+
let r = await api('GET', '/rules');
|
|
919
|
+
const pick = (list) => {
|
|
920
|
+
const x = list[Number(n) - 1];
|
|
921
|
+
if (!x) throw new Error(`No rule ${n ?? ''}; see agentmgr rules`);
|
|
922
|
+
return { repo: x.repo, rule: x.rule };
|
|
923
|
+
};
|
|
924
|
+
if (action === 'allow') r = await api('POST', '/rules/accept', pick(r.suggestions));
|
|
925
|
+
else if (action === 'no') r = await api('POST', '/rules/dismiss', pick(r.suggestions));
|
|
926
|
+
else if (action === 'rm') r = await api('POST', '/rules/remove', pick(r.allowed));
|
|
927
|
+
else if (action) throw new Error('Use: agentmgr rules [allow <n> | no <n> | rm <n>]');
|
|
928
|
+
if (r.suggestions.length) {
|
|
929
|
+
console.log(bold('Suggested') + dim(` (you allowed these ${r.suggestAfter}+ times; agentmgr rules allow <n> | no <n>)`));
|
|
930
|
+
r.suggestions.forEach((x, i) => console.log(` ${i + 1}. ${x.label} ${dim(`in ${repoLabel(x.repo)}, allowed ${x.n} times`)}`));
|
|
931
|
+
}
|
|
932
|
+
console.log(bold('Allowed') + dim(' (agents in the repo run these without asking; agentmgr rules rm <n>)'));
|
|
933
|
+
if (!r.allowed.length) console.log(dim(' none yet'));
|
|
934
|
+
r.allowed.forEach((x, i) => console.log(` ${i + 1}. ${x.label} ${dim(`in ${repoLabel(x.repo)}${x.uses ? `, answered ${x.uses} prompts` : ''}`)}`));
|
|
935
|
+
});
|
|
936
|
+
|
|
904
937
|
program
|
|
905
938
|
.command('answer')
|
|
906
939
|
.description('Answer a question an agent asked in the inbox')
|