ai-agentmanager 1.12.0 → 1.13.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 +13 -0
- package/docs/agentkit.md +15 -1
- package/package.json +1 -1
- package/public/app.js +11 -2
- package/public/built.js +147 -22
- package/public/index.html +4 -3
- package/public/style.css +6 -0
- package/src/agentkit/engines.js +14 -1
- package/src/builder.js +177 -73
- package/src/manager.js +20 -2
- package/src/server.js +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.0 (2026-11-01)
|
|
4
|
+
|
|
5
|
+
- **Agents are built in an agent session.** **+ Build an agent** now starts an agent session that works in the new agent's folder (`~/.agentmanager/built/<name>/`) and goes straight to it. The session writes the analysis (`analysis.json`), `agent.yaml`, `instructions.md` and `tests.yaml`, and checks that the agent loads. You keep shaping the agent in that conversation, like any other session (terminal, resume, inbox).
|
|
6
|
+
- The session's page has a panel for its agent: status, tests, **Run tests**, **Start**/**Stop**, its chat, **Details**. Failing tests go back to the session with **Ask this session to fix them**.
|
|
7
|
+
- In the Built tab, each agent links to its session. The details dialog has a box that sends a change request to the session. Agents built before this get **Open a session for it**.
|
|
8
|
+
- Test results and executables built before the agent's latest change are marked as out of date. Deleting a built agent closes its session; the conversation is kept.
|
|
9
|
+
- **Fixes from building a real agent:**
|
|
10
|
+
- A permission prompt is now flagged when newer Claude Code blinks a ⏺ beside the pending tool call. The blinking used to keep the session looking busy, so the prompt never reached the Inbox. This affects every session, not just build sessions.
|
|
11
|
+
- agentkit agents can write files under a folder they're allowed: Claude Code ignores `Write(<path>)` allow rules, so each one now also gets its `Edit(<path>)` rule.
|
|
12
|
+
- The test judge sees the files each test run created or changed, not only the agent's reply, and the Tests list shows them.
|
|
13
|
+
- Build sessions read the agentkit reference without asking, and write the agent's files with their file tools.
|
|
14
|
+
- The build dialog takes an optional name; otherwise the name comes from the task's first words. `POST /api/builder/:name/change` sends a change request to the session from scripts.
|
|
15
|
+
|
|
3
16
|
## 1.12.0 (2026-11-01)
|
|
4
17
|
|
|
5
18
|
- **agentkit (phase 1): agents you build and run.** An agent is a folder with `agent.yaml` (goal, instructions, engine, MCP tools, allow/approve lists, triggers, loop, memory, interfaces); `agentkit run <agent>` runs it continuously. Each input is a run of plan → act → check (acting again on what's left, up to `maxSteps`) → reflect into memory, through an engine's CLI (Claude Code, Codex, Muse Code or an ACP agent), self-contained (only its own MCP servers; none of your personal Claude settings). Triggers: `every`, `cron`, `webhook`, `watch`. Interfaces: a chat page and HTTP API, A2A (agent card and `message/send` / `tasks/get`), ACP on stdio (`agentkit acp`), and an MCP tool (`agentkit mcp`, one tool `ask_<name>`). `agentkit init`, `check`, `run --once`. See [docs/agentkit.md](docs/agentkit.md).
|
package/docs/agentkit.md
CHANGED
|
@@ -27,7 +27,8 @@ tools: # MCP servers the engine gets
|
|
|
27
27
|
command: npx
|
|
28
28
|
args: [-y, "@acme/jira-mcp"]
|
|
29
29
|
env: { JIRA_TOKEN: "${JIRA_TOKEN}" } # "${VAR}" (quoted) comes from the environment, never stored
|
|
30
|
-
allow: [Read, Grep, "mcp__jira__*"] # tools it may use without asking (engine names)
|
|
30
|
+
allow: [Read, Grep, "mcp__jira__*"] # tools it may use without asking (engine names);
|
|
31
|
+
# "Edit(./out/**)" lets it write files under ./out only
|
|
31
32
|
approve: ["mcp__jira__transition"] # tools that need a person's yes (asked in chat / Inbox)
|
|
32
33
|
approveTimeout: 30m # no answer by then counts as no
|
|
33
34
|
report: # report to AI Agent Manager (or set AGENTKIT_REPORT_*)
|
|
@@ -55,6 +56,19 @@ interfaces:
|
|
|
55
56
|
mcp: true # `<agent> mcp` exposes it as an MCP tool
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
## Building one in AI Agent Manager
|
|
60
|
+
|
|
61
|
+
**Built → + Build an agent** takes a task or workflow (attachments welcome), the engine it runs on and an optional name. It starts an **agent session** whose folder is the new agent (`~/.agentmanager/built/<name>/`), and opens that session. The session writes the agent and checks that it loads (`agentkit check`):
|
|
62
|
+
|
|
63
|
+
- `analysis.json`: the problem, inputs, outputs, risks and the MCP tools it chose
|
|
64
|
+
- `agent.yaml`
|
|
65
|
+
- `instructions.md`
|
|
66
|
+
- `tests.yaml`: 3 to 8 `{input, expect}` cases
|
|
67
|
+
|
|
68
|
+
It asks you what only you know: accounts, labels, teams. You keep developing the agent in that conversation.
|
|
69
|
+
|
|
70
|
+
The session's page shows a panel for its agent: status and test results, with **Run tests** (each test runs through the real runtime and is judged against `expect`), **Start**/**Stop** (runs it continuously on this computer, with its chat page and API) and **Details**. Failing tests can go straight back to the session. The Built tab lists every built agent with a link to its session. Its details dialog sends change requests to the session and has the spec, executables and downloads. Results and executables from before the latest change are marked out of date.
|
|
71
|
+
|
|
58
72
|
## A run
|
|
59
73
|
|
|
60
74
|
1. **Input** arrives: a trigger fires (with its payload), someone chats, an A2A task or an MCP
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -127,7 +127,14 @@ $('#new-playbook').onclick = () => {
|
|
|
127
127
|
};
|
|
128
128
|
$('#triggers-open').onclick = () => triggersUi.open();
|
|
129
129
|
let builtReports = []; // agents built with agentkit that report here (runs, approvals)
|
|
130
|
-
const builtUi = initBuilt({
|
|
130
|
+
const builtUi = initBuilt({
|
|
131
|
+
api, esc, toast, $,
|
|
132
|
+
isOpen: () => sideTab === 'built',
|
|
133
|
+
setCount: (n) => ($('#built-count').textContent = n),
|
|
134
|
+
reports: () => builtReports,
|
|
135
|
+
openSession: (id) => (showTab('agents'), select(id)),
|
|
136
|
+
onPanel: () => agents.get(selectedId)?.builds && renderDetail(),
|
|
137
|
+
});
|
|
131
138
|
const landUi = initLand({ api, esc, toast, agents, $, scoreBadge, select: (id) => (showTab('agents'), select(id)), openReview: (id) => reviewUi.open(id), isOpen: () => sideTab === 'land' });
|
|
132
139
|
|
|
133
140
|
function statusOf(a) {
|
|
@@ -713,7 +720,7 @@ function agentItem(a) {
|
|
|
713
720
|
const idle = !isActive(a);
|
|
714
721
|
return `<li data-id="${a.id}" ${a.kind === 'general' || a.kind === 'assistant' ? '' : 'draggable="true" '}class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
|
|
715
722
|
<div class="line1">${prioChip(a.priority ?? 2)}${a.results?.outcome === 'blocked' ? '<span class="outcome-mark" title="Blocked">⛔</span>' : a.results?.outcome === 'done' ? '<span class="outcome-mark" title="Done">✅</span>' : ''}<span class="name">${esc(a.name)}</span>${flags(a)}<span class="pill ${st}">${pillText(st)}</span></div>
|
|
716
|
-
<div class="intent" title="${esc(describeTitle(a) ?? '')}">${a.kind === 'general' ? '<span class="tag">general</span> ' : a.kind === 'assistant' ? '<span class="tag assistant">assistant</span> ' : a.startedBy ? '<span class="tag assistant" title="Started by the assistant">🤖 by assistant</span> ' : a.trigger ? `<span class="tag assistant" title="Started from ${a.trigger.source === 'github' ? 'a labeled GitHub issue' : 'the webhook'}${a.trigger.url ? `: ${esc(a.trigger.url)}` : ''}">⚡ ${a.trigger.source === 'github' ? 'GitHub' : 'webhook'}</span> ` : a.playbook ? `<span class="tag assistant" title="Started by the playbook ${esc(a.playbook.name)}">▶ ${esc(a.playbook.name)}</span> ` : ''}${a.engine && a.engine !== 'claude' ? `<span class="tag engine">${esc(ENGINE_LABEL[a.engine] ?? a.engine)}</span> ` : ''}${a.isolation === 'container' ? '<span class="tag engine" title="Runs in its own Docker container: it sees only its own folder">📦 container</span> ' : ''}${esc(describe(a) || (a.kind === 'assistant' ? 'Finds new work and starts agents for it' : 'General session'))}</div>
|
|
723
|
+
<div class="intent" title="${esc(describeTitle(a) ?? '')}">${a.builds ? `<span class="tag assistant" title="Develops the agent ${esc(a.builds.name)} (Built tab)">⚙ builds agent</span> ` : ''}${a.kind === 'general' ? '<span class="tag">general</span> ' : a.kind === 'assistant' ? '<span class="tag assistant">assistant</span> ' : a.startedBy ? '<span class="tag assistant" title="Started by the assistant">🤖 by assistant</span> ' : a.trigger ? `<span class="tag assistant" title="Started from ${a.trigger.source === 'github' ? 'a labeled GitHub issue' : 'the webhook'}${a.trigger.url ? `: ${esc(a.trigger.url)}` : ''}">⚡ ${a.trigger.source === 'github' ? 'GitHub' : 'webhook'}</span> ` : a.playbook ? `<span class="tag assistant" title="Started by the playbook ${esc(a.playbook.name)}">▶ ${esc(a.playbook.name)}</span> ` : ''}${a.engine && a.engine !== 'claude' ? `<span class="tag engine">${esc(ENGINE_LABEL[a.engine] ?? a.engine)}</span> ` : ''}${a.isolation === 'container' ? '<span class="tag engine" title="Runs in its own Docker container: it sees only its own folder">📦 container</span> ' : ''}${esc(describe(a) || (a.kind === 'assistant' ? 'Finds new work and starts agents for it' : 'General session'))}</div>
|
|
717
724
|
${a.status === 'preparing' && a.progress ? `<div class="progress-line">${esc(progressText(a.progress))}</div>` : ''}
|
|
718
725
|
<div class="meta">
|
|
719
726
|
<span class="mono">${a.id}</span>
|
|
@@ -1743,6 +1750,7 @@ function renderDetail() {
|
|
|
1743
1750
|
${a.runtime.attention ? `<div class="error-box">${esc(a.runtime.attention)}: the agent is waiting for an answer. Open its terminal to respond.</div>` : ''}
|
|
1744
1751
|
${raceBanner(a)}
|
|
1745
1752
|
${teamBanner(a)}
|
|
1753
|
+
${a.builds ? builtUi.panel(a) : ''}
|
|
1746
1754
|
${a.forkOf ? `<div class="bl-banner">⑂ Forked from <a href="#${a.forkOf.agentId}" data-goto="${a.forkOf.agentId}">${esc(a.forkOf.name)}</a>${a.forkOf.checkpoint ? ` at checkpoint ${a.forkOf.checkpoint}` : ''} ${ago(a.forkOf.at)}</div>` : ''}
|
|
1747
1755
|
${(() => {
|
|
1748
1756
|
const items = needsFor(a, { both: true });
|
|
@@ -3641,6 +3649,7 @@ function handleEvent(msg) {
|
|
|
3641
3649
|
agents.set(msg.agent.id, msg.agent);
|
|
3642
3650
|
watchDocs(msg.agent);
|
|
3643
3651
|
agentChanged(msg.agent);
|
|
3652
|
+
if (msg.agent.builds) builtUi.sessionChanged(msg.agent);
|
|
3644
3653
|
} else if (msg.t === 'removed') {
|
|
3645
3654
|
agents.delete(msg.id);
|
|
3646
3655
|
if (selectedId === msg.id) select(null);
|
package/public/built.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
// The Built tab: agents made with the builder (agentkit agents, docs/agentkit.md).
|
|
2
|
-
//
|
|
1
|
+
// The Built tab: agents made with the builder (agentkit agents, docs/agentkit.md). Each one is
|
|
2
|
+
// developed in an agent session working in its folder: building one starts that session, and the
|
|
3
|
+
// person keeps shaping the agent there. Here (and in a panel on that session) they review its
|
|
4
|
+
// analysis, spec and tests, test it, run it here, open its chat, package and download it.
|
|
3
5
|
import { attachBox } from '/attach.js';
|
|
4
6
|
|
|
5
|
-
const STATUS = { designed: 'designed', 'needs-fixing': 'needs fixing', testing: 'testing…', tested: 'tests pass', failing: 'tests fail', running: 'running' };
|
|
7
|
+
const STATUS = { designing: 'designing…', designed: 'designed', 'needs-fixing': 'needs fixing', testing: 'testing…', tested: 'tests pass', failing: 'tests fail', running: 'running' };
|
|
6
8
|
|
|
7
9
|
const ago = (iso) => {
|
|
8
10
|
const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
|
|
9
11
|
return s < 60 ? 'just now' : s < 3600 ? `${Math.round(s / 60)} min ago` : s < 86400 ? `${Math.round(s / 3600)} h ago` : `${Math.round(s / 86400)} d ago`;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
const sessionText = (s) => (!s ? 'no session' : s.attention ? 'session waiting for you' : s.busy ? 'session working' : s.running ? 'session idle' : 'session closed');
|
|
15
|
+
const tagClass = (b) => (b.status === 'tested' ? 'open' : b.status === 'failing' || b.status === 'needs-fixing' ? 'fail' : '');
|
|
16
|
+
const later = (a, b) => Boolean(a && b && Date.parse(a) > Date.parse(b) + 1000);
|
|
17
|
+
|
|
18
|
+
/** `openSession(id)`: show an agent session (the agents view). `onPanel()`: a session's panel has new data. */
|
|
19
|
+
export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () => [], openSession = () => {}, onPanel = () => {} }) {
|
|
13
20
|
const view = $('#built-view');
|
|
14
21
|
const dlg = $('#built-dialog');
|
|
15
22
|
const body = $('#built-body');
|
|
@@ -28,9 +35,9 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
function render() {
|
|
31
|
-
view.innerHTML = `<div class="built-head"><button class="small primary" data-bt="new">+ Build an agent</button><span class="hint">An agent for a task or workflow: it plans, acts, checks itself and keeps working in the background.</span></div>
|
|
32
|
-
${list.length ? `<ul class="built-list">${list.map((b) => `<li data-bt-open="${esc(b.name)}"><div class="line1"><b>${esc(b.title ?? b.name)}</b>${b.running ? ' <span class="dot-run" title="Running">●</span>' : ''}<span class="spacer"></span
|
|
33
|
-
<div class="hint">${esc(b.name)} · ${esc(b.engine ?? '')}${b.results?.total ? ` · tests ${b.results.passed}/${b.results.total}` : ''}${b.triggers.length ? ` · ${esc(b.triggers.join(', '))}` : ''}</div></li>`).join('')}</ul>` : '<p class="hint built-empty">No agents built yet.</p>'}
|
|
38
|
+
view.innerHTML = `<div class="built-head"><button class="small primary" data-bt="new">+ Build an agent</button><span class="hint">An agent for a task or workflow: it plans, acts, checks itself and keeps working in the background. Each is developed in its own agent session.</span></div>
|
|
39
|
+
${list.length ? `<ul class="built-list">${list.map((b) => `<li data-bt-open="${esc(b.name)}"><div class="line1"><b>${esc(b.title ?? b.name)}</b>${b.running ? ' <span class="dot-run" title="Running">●</span>' : ''}<span class="spacer"></span>${b.session ? `<a href="#${esc(b.session.id)}" class="button small ghost" data-bt-session="${esc(b.session.id)}" title="Its session: where you shape it">Session${b.session.busy ? ' ●' : ''}</a>` : ''}<span class="tag ${tagClass(b)}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span></div>
|
|
40
|
+
<div class="hint">${esc(b.name)} · ${esc(b.engine ?? '')} · ${sessionText(b.session)}${b.results?.total ? ` · tests ${b.results.passed}/${b.results.total}` : ''}${b.triggers.length ? ` · ${esc(b.triggers.join(', '))}` : ''}</div></li>`).join('')}</ul>` : '<p class="hint built-empty">No agents built yet.</p>'}
|
|
34
41
|
${reporting()}`;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -85,20 +92,23 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
85
92
|
if (files.pending()) return toast('Still uploading the attached files');
|
|
86
93
|
const btn = f.querySelector('button[type=submit]');
|
|
87
94
|
btn.disabled = true;
|
|
88
|
-
btn.textContent = '
|
|
95
|
+
btn.textContent = 'Starting its session…';
|
|
89
96
|
try {
|
|
90
|
-
const b = await api('POST', '/builder', { task: f.task.value, engine: f.engine.value, attachments: files.paths().length ? files.paths() : undefined });
|
|
97
|
+
const b = await api('POST', '/builder', { task: f.task.value, engine: f.engine.value, name: f.name.value.trim() || undefined, attachments: files.paths().length ? files.paths() : undefined });
|
|
91
98
|
buildDlg.close();
|
|
92
99
|
f.reset();
|
|
93
100
|
files.clear();
|
|
101
|
+
cache.set(b.name, b);
|
|
94
102
|
await load();
|
|
95
|
-
|
|
103
|
+
// Building happens in its session: go there.
|
|
104
|
+
if (b.session) openSession(b.session.id);
|
|
105
|
+
else show(b);
|
|
96
106
|
} catch (err) {
|
|
97
107
|
$('#build-error').textContent = err.message;
|
|
98
108
|
$('#build-error').hidden = false;
|
|
99
109
|
} finally {
|
|
100
110
|
btn.disabled = false;
|
|
101
|
-
btn.textContent = '
|
|
111
|
+
btn.textContent = 'Start building';
|
|
102
112
|
}
|
|
103
113
|
});
|
|
104
114
|
$('#build-cancel').onclick = () => buildDlg.close();
|
|
@@ -110,28 +120,32 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
110
120
|
const a = b.analysis ?? {};
|
|
111
121
|
const li = (xs) => (xs?.length ? `<ul>${xs.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` : '<p class="hint">—</p>');
|
|
112
122
|
const r = b.results;
|
|
123
|
+
const failed = (r?.list ?? []).filter((t) => t.pass === false);
|
|
113
124
|
body.innerHTML = `
|
|
125
|
+
${sessionBlock(b)}
|
|
114
126
|
<div class="built-actions">
|
|
115
|
-
<span class="tag ${b
|
|
127
|
+
<span class="tag ${tagClass(b)}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span>
|
|
116
128
|
${b.running ? `<span class="hint">running since ${new Date(b.running.since).toLocaleTimeString()}</span>${b.running.url ? ` <a href="${esc(b.running.url)}" target="_blank" rel="noopener">Open its chat ↗</a>` : ''}` : ''}
|
|
117
129
|
<span class="spacer"></span>
|
|
118
|
-
<button class="small" data-bt="test" ${b.pending ? 'disabled' : ''} title="Run each test through the real engine and judge the results">${b.pending === 'testing' ? 'Testing…' : 'Run tests'}</button>
|
|
119
|
-
<button class="small ${b.running ? '' : 'primary'}" data-bt="${b.running ? 'stop' : 'start'}" ${b.specError ? 'disabled' : ''} title="${b.running ? 'Stop it' : 'Run it continuously on this computer: its triggers, chat page and API'}">${b.running ? 'Stop' : 'Start'}</button>
|
|
130
|
+
<button class="small" data-bt="test" ${b.pending || b.specError || b.status === 'designing' ? 'disabled' : ''} title="Run each test through the real engine and judge the results">${b.pending === 'testing' ? 'Testing…' : 'Run tests'}</button>
|
|
131
|
+
<button class="small ${b.running ? '' : 'primary'}" data-bt="${b.running ? 'stop' : 'start'}" ${b.specError || b.status === 'designing' ? 'disabled' : ''} title="${b.running ? 'Stop it' : 'Run it continuously on this computer: its triggers, chat page and API'}">${b.running ? 'Stop' : 'Start'}</button>
|
|
120
132
|
<a class="button small ghost" href="/api/builder/${esc(b.name)}/download" title="Its folder (agent.yaml, instructions, tests) to run anywhere with agentkit">Download folder</a>
|
|
121
133
|
<button class="small ghost" data-bt="delete">Delete</button>
|
|
122
134
|
</div>
|
|
123
135
|
<div class="built-exe"><b>Single executable</b> <span class="hint">one file that runs this agent anywhere, no Node or agentkit needed (its engine's CLI must be installed and signed in)</span>
|
|
124
136
|
<div class="built-exe-row"><select id="built-platform">${(b.platforms ?? []).map((p) => `<option value="${esc(p)}" ${p === b.host ? 'selected' : ''}>${esc(p)}${p === b.host ? ' (this computer)' : ''}</option>`).join('')}</select>
|
|
125
|
-
<button class="small" data-bt="package" ${b.pending || b.specError ? 'disabled' : ''}>${b.pending?.startsWith('packaging') ? 'Building… (a minute or two)' : 'Build executable'}</button>
|
|
126
|
-
${Object.entries(b.executables ?? {}).map(([p, x]) => `<a class="button small ghost" href="/api/builder/${esc(b.name)}/executable/${esc(p)}" title="Built ${esc(new Date(x.at).toLocaleString())}">⬇ ${esc(p)} · ${Math.round(x.size / 1e6)} MB</a>`).join('')}</div>
|
|
137
|
+
<button class="small" data-bt="package" ${b.pending || b.specError || b.status === 'designing' ? 'disabled' : ''}>${b.pending?.startsWith('packaging') ? 'Building… (a minute or two)' : 'Build executable'}</button>
|
|
138
|
+
${Object.entries(b.executables ?? {}).map(([p, x]) => `<a class="button small ghost" href="/api/builder/${esc(b.name)}/executable/${esc(p)}" title="Built ${esc(new Date(x.at).toLocaleString())}${later(b.changedAt, x.at) ? ', before its latest change: build it again' : ''}">⬇ ${esc(p)} · ${Math.round(x.size / 1e6)} MB${later(b.changedAt, x.at) ? ' (older)' : ''}</a>`).join('')}</div>
|
|
127
139
|
${b.packageError ? `<p class="error">${esc(b.packageError)}</p>` : ''}</div>
|
|
128
140
|
${b.specError ? `<p class="error">${esc(b.specError)}</p>` : ''}
|
|
129
141
|
${b.missingEnv.length ? `<p class="hint warn">Set these in the environment before running it: ${esc(b.missingEnv.join(', '))}</p>` : ''}
|
|
130
|
-
<details open><summary><b>Analysis</b></summary
|
|
142
|
+
<details open><summary><b>Analysis</b></summary>${b.status === 'designing' && !a.problem ? '<p class="hint">Its session is working it out; this fills in as it writes the files.</p>' : ''}<p>${esc(a.problem ?? '')}</p><div class="row three"><div><b>Inputs</b>${li(a.inputs)}</div><div><b>Outputs</b>${li(a.outputs)}</div><div><b>Risks</b>${li(a.risks)}</div></div>
|
|
131
143
|
${b.tools?.length ? `<p><b>Tools</b></p><ul>${b.tools.map((t) => `<li><code>${esc(t.name)}</code> ${esc(t.why)}</li>`).join('')}</ul>` : ''}</details>
|
|
132
144
|
<details ${r ? 'open' : ''}><summary><b>Tests</b> ${r?.total ? `<span class="hint">${r.passed}/${r.total} pass · ${new Date(r.at).toLocaleString()}</span>` : '<span class="hint">not run yet</span>'}</summary>
|
|
133
145
|
${r?.error ? `<p class="error">${esc(r.error)}</p>` : ''}
|
|
134
|
-
|
|
146
|
+
${r?.at && later(b.changedAt, r.at) ? '<p class="hint warn">The agent changed after these results; run the tests again.</p>' : ''}
|
|
147
|
+
${failed.length && b.session ? `<button class="small" data-bt="fix">Ask its session to fix the ${failed.length} failing test${failed.length > 1 ? 's' : ''}</button>` : ''}
|
|
148
|
+
<ol class="built-tests">${(r?.list?.length ? r.list : b.tests ?? []).map((t) => `<li class="${t.pass === true ? 'pass' : t.pass === false ? 'fail' : ''}"><div><b>${t.pass === true ? '✓' : t.pass === false ? '✗' : '·'}</b> ${esc(t.input)}</div><div class="hint">Expect: ${esc(t.expect)}</div>${t.why ? `<div class="hint">${esc(t.why)}</div>` : ''}${t.files?.length ? `<div class="hint">Wrote: ${t.files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</div>` : ''}${t.result ? `<details><summary class="hint">Result</summary><pre>${esc(t.result)}</pre></details>` : ''}</li>`).join('')}</ol></details>
|
|
135
149
|
<details><summary><b>agent.yaml</b> and instructions</summary>
|
|
136
150
|
<textarea id="built-yaml" class="code-area" rows="16" spellcheck="false">${esc(b.yaml)}</textarea>
|
|
137
151
|
<textarea id="built-instr" class="code-area" rows="10" spellcheck="false">${esc(b.instructionsText)}</textarea>
|
|
@@ -140,15 +154,31 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
140
154
|
<p class="hint">Task: ${esc(b.task ?? '')}</p>`;
|
|
141
155
|
if (!dlg.open) dlg.showModal();
|
|
142
156
|
clearInterval(poll);
|
|
143
|
-
if (b.pending) poll = setInterval(refresh, 3000);
|
|
157
|
+
if (b.pending || b.status === 'designing') poll = setInterval(refresh, 3000);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Where the agent is developed: its session, and a box to ask it for a change.
|
|
161
|
+
function sessionBlock(b) {
|
|
162
|
+
if (!b.session)
|
|
163
|
+
return `<div class="built-session"><b>No session</b> <span class="hint">Open an agent session in its folder to keep shaping it with you.</span><span class="spacer"></span><button class="small primary" data-bt="session-new">Open a session for it</button></div>`;
|
|
164
|
+
return `<div class="built-session"><div class="line1"><b>Developed in its session</b> <span class="hint">${sessionText(b.session)}</span><span class="spacer"></span><a href="#${esc(b.session.id)}" class="button small primary" data-bt-session="${esc(b.session.id)}">Open session</a></div>
|
|
165
|
+
<div class="built-change"><textarea id="built-change" rows="2" placeholder="Ask its session for a change: “also post a summary to #support every evening”, “never reassign tickets”…"></textarea><button class="small" data-bt="change">Send</button></div></div>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function failuresText(b) {
|
|
169
|
+
const failed = (b.results?.list ?? []).filter((t) => t.pass === false);
|
|
170
|
+
return `These tests failed. Change the agent so they pass (or fix a test if its expectation is wrong), check it, and tell me what you changed.\n\n${failed.map((t, i) => `${i + 1}. Input: ${t.input}\n Expected: ${t.expect}\n Judge: ${t.why}\n Result: ${String(t.result ?? '').slice(0, 600)}`).join('\n\n')}`;
|
|
144
171
|
}
|
|
145
172
|
|
|
146
173
|
async function refresh() {
|
|
147
174
|
if (!open || !dlg.open) return clearInterval(poll);
|
|
148
175
|
try {
|
|
149
176
|
const b = await api('GET', `/builder/${open.name}`);
|
|
150
|
-
|
|
151
|
-
|
|
177
|
+
cache.set(b.name, b);
|
|
178
|
+
onPanel();
|
|
179
|
+
const typing = document.activeElement?.closest?.('#built-body textarea');
|
|
180
|
+
if (!typing && JSON.stringify(b) !== JSON.stringify(open)) show(b);
|
|
181
|
+
if (!b.pending && b.status !== 'designing') clearInterval(poll), load();
|
|
152
182
|
} catch {}
|
|
153
183
|
}
|
|
154
184
|
|
|
@@ -161,6 +191,16 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
161
191
|
show(await api('POST', `/builder/${name}/start`));
|
|
162
192
|
setTimeout(refresh, 1500);
|
|
163
193
|
} else if (what === 'stop') show(await api('POST', `/builder/${name}/stop`));
|
|
194
|
+
else if (what === 'change' || what === 'fix') {
|
|
195
|
+
const text = what === 'fix' ? failuresText(open) : $('#built-change').value.trim();
|
|
196
|
+
if (!text) return toast('Say what to change');
|
|
197
|
+
show(await api('POST', `/builder/${name}/change`, { text }));
|
|
198
|
+
toast('Sent to its session');
|
|
199
|
+
} else if (what === 'session-new') {
|
|
200
|
+
const b = await api('POST', `/builder/${name}/change`, {});
|
|
201
|
+
dlg.close();
|
|
202
|
+
if (b.session) openSession(b.session.id);
|
|
203
|
+
}
|
|
164
204
|
else if (what === 'save') {
|
|
165
205
|
show(await api('PUT', `/builder/${name}`, { yaml: $('#built-yaml').value, instructions: $('#built-instr').value }));
|
|
166
206
|
toast('Saved');
|
|
@@ -193,15 +233,100 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
193
233
|
}
|
|
194
234
|
return;
|
|
195
235
|
}
|
|
236
|
+
const ses = e.target.closest('[data-bt-session]');
|
|
237
|
+
if (ses) return e.preventDefault(), openSession(ses.dataset.btSession);
|
|
196
238
|
const li = e.target.closest('[data-bt-open]');
|
|
197
239
|
if (li) show(await api('GET', `/builder/${li.dataset.btOpen}`));
|
|
198
240
|
});
|
|
199
241
|
body.addEventListener('click', (e) => {
|
|
242
|
+
const ses = e.target.closest('[data-bt-session]');
|
|
243
|
+
if (ses) return e.preventDefault(), dlg.close(), openSession(ses.dataset.btSession);
|
|
200
244
|
const b = e.target.closest('[data-bt]');
|
|
201
245
|
if (b) act(b.dataset.bt);
|
|
202
246
|
});
|
|
203
247
|
$('#built-close').onclick = () => (clearInterval(poll), dlg.close());
|
|
204
248
|
|
|
249
|
+
// ------------------------------------------------------------ the panel on its session
|
|
250
|
+
const cache = new Map(); // name -> its latest view (or { missing: true })
|
|
251
|
+
const fetching = new Set();
|
|
252
|
+
async function fetchOne(name) {
|
|
253
|
+
if (fetching.has(name)) return;
|
|
254
|
+
fetching.add(name);
|
|
255
|
+
try {
|
|
256
|
+
cache.set(name, await api('GET', `/builder/${encodeURIComponent(name)}`));
|
|
257
|
+
} catch (err) {
|
|
258
|
+
cache.set(name, /No built agent|Unknown agent/.test(err.message) ? { missing: true, name } : { error: err.message, name });
|
|
259
|
+
} finally {
|
|
260
|
+
fetching.delete(name);
|
|
261
|
+
onPanel();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The panel shown on a session that builds an agent (agent `a`, with a.builds). */
|
|
266
|
+
function panel(a) {
|
|
267
|
+
const name = a.builds.name;
|
|
268
|
+
const b = cache.get(name);
|
|
269
|
+
if (!b) {
|
|
270
|
+
fetchOne(name);
|
|
271
|
+
return `<div class="built-panel"><b>⚙ Builds the agent ${esc(name)}</b> <span class="hint">loading…</span></div>`;
|
|
272
|
+
}
|
|
273
|
+
if (b.error) return `<div class="built-panel"><b>⚙ Builds the agent ${esc(name)}</b> <span class="hint">· ${esc(b.error)}</span></div>`;
|
|
274
|
+
if (b.missing) return `<div class="built-panel"><b>⚙ Built the agent ${esc(name)}</b> <span class="hint">· deleted from the Built tab</span></div>`;
|
|
275
|
+
const r = b.results;
|
|
276
|
+
const failed = (r?.list ?? []).filter((t) => t.pass === false);
|
|
277
|
+
const stale = r?.at && later(b.changedAt, r.at);
|
|
278
|
+
return `<div class="built-panel" data-bp-name="${esc(name)}">
|
|
279
|
+
<div class="line1"><b>⚙ ${esc(b.title ?? name)}</b> <span class="tag ${tagClass(b)}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span>
|
|
280
|
+
<span class="hint">${esc(b.engine ?? '')}${b.triggers.length ? ` · ${esc(b.triggers.join(', '))}` : ''}${r?.total ? ` · tests ${r.passed}/${r.total}${stale ? ' (before the latest change)' : ''}` : b.tests.length ? ` · ${b.tests.length} tests, not run` : ''}${b.running ? ' · running here' : ''}</span>
|
|
281
|
+
<span class="spacer"></span>
|
|
282
|
+
<button class="small" data-bp="test" ${b.pending || b.specError || b.status === 'designing' ? 'disabled' : ''} title="Run each test through the real engine and judge the results">${b.pending === 'testing' ? 'Testing…' : 'Run tests'}</button>
|
|
283
|
+
<button class="small" data-bp="${b.running ? 'stop' : 'start'}" ${b.specError || b.status === 'designing' ? 'disabled' : ''} title="${b.running ? 'Stop it' : 'Run it continuously on this computer: its triggers, chat page and API'}">${b.running ? 'Stop' : 'Start'}</button>
|
|
284
|
+
${b.running?.url ? `<a class="button small ghost" href="${esc(b.running.url)}" target="_blank" rel="noopener">Chat ↗</a>` : ''}
|
|
285
|
+
<button class="small ghost" data-bp="open" title="Analysis, spec, tests, executables, downloads">Details</button></div>
|
|
286
|
+
${b.specError ? `<p class="error">${esc(b.specError)}</p>` : ''}
|
|
287
|
+
${b.missingEnv.length ? `<p class="hint warn">Needs in the environment before it runs: ${esc(b.missingEnv.join(', '))}</p>` : ''}
|
|
288
|
+
${b.description ? `<div class="hint">${esc(b.description)}</div>` : ''}
|
|
289
|
+
${failed.length && !stale ? `<div class="hint">Failing: ${failed.map((t) => esc(t.input.slice(0, 60))).join(' · ')} <button class="small" data-bp="fix">Ask this session to fix them</button></div>` : ''}
|
|
290
|
+
</div>`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
document.addEventListener('click', async (e) => {
|
|
294
|
+
const el = e.target.closest('[data-bp]');
|
|
295
|
+
if (!el) return;
|
|
296
|
+
const name = el.closest('[data-bp-name]').dataset.bpName;
|
|
297
|
+
try {
|
|
298
|
+
if (el.dataset.bp === 'open') return show(await api('GET', `/builder/${name}`));
|
|
299
|
+
if (el.dataset.bp === 'fix') {
|
|
300
|
+
await api('POST', `/builder/${name}/change`, { text: failuresText(cache.get(name)) });
|
|
301
|
+
return toast('Sent to this session');
|
|
302
|
+
}
|
|
303
|
+
el.disabled = true;
|
|
304
|
+
cache.set(name, await api('POST', `/builder/${name}/${el.dataset.bp}`));
|
|
305
|
+
onPanel();
|
|
306
|
+
// Tests and starting finish in the background; follow them.
|
|
307
|
+
const follow = setInterval(async () => {
|
|
308
|
+
await fetchOne(name);
|
|
309
|
+
const b = cache.get(name);
|
|
310
|
+
if (!b || b.missing || (!b.pending && (el.dataset.bp !== 'start' || b.running?.url || !b.running))) clearInterval(follow), load();
|
|
311
|
+
}, 2000);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
toast(`Error: ${err.message}`, 6000);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Its session changed (it edits the files as it goes): fetch the agent again, a moment later.
|
|
318
|
+
const timers = new Map();
|
|
319
|
+
function sessionChanged(a) {
|
|
320
|
+
const name = a.builds?.name;
|
|
321
|
+
if (!name) return;
|
|
322
|
+
clearTimeout(timers.get(name));
|
|
323
|
+
timers.set(name, setTimeout(() => {
|
|
324
|
+
fetchOne(name);
|
|
325
|
+
if (open?.name === name && dlg.open) refresh();
|
|
326
|
+
if (isOpen()) load();
|
|
327
|
+
}, 1000));
|
|
328
|
+
}
|
|
329
|
+
|
|
205
330
|
load();
|
|
206
|
-
return { load, render };
|
|
331
|
+
return { load, render, panel, sessionChanged, openAgent: async (name) => show(await api('GET', `/builder/${name}`)) };
|
|
207
332
|
}
|
package/public/index.html
CHANGED
|
@@ -382,14 +382,15 @@
|
|
|
382
382
|
<dialog id="build-dialog">
|
|
383
383
|
<form id="build-form" method="dialog">
|
|
384
384
|
<h2>Build an agent</h2>
|
|
385
|
-
<p class="hint">Describe the task or workflow it takes over: what comes in, what it should do, what it must never do.
|
|
385
|
+
<p class="hint">Describe the task or workflow it takes over: what comes in, what it should do, what it must never do. An agent session works it out with you in the agent's own folder (analysis, spec, instructions, test cases); you keep shaping it there, then test, run and package it.</p>
|
|
386
386
|
<label>Task or workflow
|
|
387
387
|
<textarea name="task" rows="8" required placeholder="Every morning, look at new support tickets in Jira, label each by product area, route it to the right team, and draft a first reply for a person to send. Never close or reply to tickets on its own."></textarea>
|
|
388
388
|
</label>
|
|
389
389
|
<div id="build-attach"></div>
|
|
390
|
-
<label>Engine <select name="engine"><option value="claude">Claude Code</option></select></label>
|
|
390
|
+
<div class="row"><label>Engine it runs on <select name="engine"><option value="claude">Claude Code</option></select></label>
|
|
391
|
+
<label>Name <input name="name" placeholder="from the task" pattern="[a-z][a-z0-9-]{1,40}" title="lowercase letters, digits and dashes"></label></div>
|
|
391
392
|
<p id="build-error" class="error" hidden></p>
|
|
392
|
-
<div class="dialog-actions"><button type="button" id="build-cancel" class="ghost">Cancel</button><button type="submit" class="primary">
|
|
393
|
+
<div class="dialog-actions"><button type="button" id="build-cancel" class="ghost">Cancel</button><button type="submit" class="primary">Start building</button></div>
|
|
393
394
|
</form>
|
|
394
395
|
</dialog>
|
|
395
396
|
|
package/public/style.css
CHANGED
|
@@ -968,6 +968,12 @@ 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; }
|
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,6 +1,7 @@
|
|
|
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';
|
|
6
7
|
import path from 'node:path';
|
|
@@ -19,79 +20,126 @@ const err = (status, message) => Object.assign(new Error(message), { status });
|
|
|
19
20
|
const now = () => new Date().toISOString();
|
|
20
21
|
const slug = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'agent';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
const DOCS = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'docs', 'agentkit.md');
|
|
24
|
+
const ENGINES = ['claude', 'codex', 'muse'];
|
|
25
|
+
|
|
26
|
+
/** The agent's name from the task: its first few words. */
|
|
27
|
+
export function nameFrom(task) {
|
|
28
|
+
const words = String(task).toLowerCase().replace(/https?:\/\/\S+/g, ' ').split(/[^a-z0-9]+/).filter((w) => w && !STOP.has(w));
|
|
29
|
+
const n = slug(words.slice(0, 4).join('-')).slice(0, 32).replace(/-+$/, '');
|
|
30
|
+
return /^[a-z][a-z0-9-]{1,}$/.test(n) ? n : `agent-${n}`.replace(/-+$/, '').slice(0, 32);
|
|
31
|
+
}
|
|
32
|
+
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']);
|
|
33
|
+
|
|
34
|
+
/** The commands its session uses to check and try the agent. */
|
|
35
|
+
export function agentkitCommands(dir) {
|
|
36
|
+
return { check: `${process.execPath} ${AGENTKIT} check`, run: `${process.execPath} ${AGENTKIT} run ${dir} --once` };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What its session may do without a permission prompt: check the agent, read the reference. */
|
|
40
|
+
export function builderAllowedTools(dir) {
|
|
41
|
+
return [`Bash(${agentkitCommands(dir).check}:*)`, `Read(/${DOCS})`];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What a session that builds an agent is told (appended to its system prompt on every launch):
|
|
46
|
+
* the folder is the agent, the files it keeps, the format, and how to check and try it.
|
|
47
|
+
*/
|
|
48
|
+
export function builderPrompt({ dir, name, engine = 'claude' }) {
|
|
49
|
+
const cmd = agentkitCommands(dir);
|
|
23
50
|
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.',
|
|
51
|
+
`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
52
|
'',
|
|
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
|
-
'}',
|
|
53
|
+
'Keep these files current:',
|
|
54
|
+
'- agent.yaml: the spec (below).',
|
|
55
|
+
'- instructions.md: how to do the job, step by step; what it must never do; when it asks a person.',
|
|
56
|
+
'- 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`.',
|
|
57
|
+
'- 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
58
|
'',
|
|
41
|
-
|
|
42
|
-
'
|
|
43
|
-
'
|
|
59
|
+
`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},`,
|
|
60
|
+
' tools (MCP servers by id: {command, args, env}; only servers that really exist; secrets only as "${VAR}"),',
|
|
61
|
+
' 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),',
|
|
62
|
+
' 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),',
|
|
63
|
+
' loop: {plan: true, check: true, maxSteps: 4}, memory: true, interfaces: {http: {port: 0}, a2a: true, acp: true, mcp: true}.',
|
|
64
|
+
`The full reference: ${DOCS}.`,
|
|
44
65
|
'',
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
'Write and edit these files with your file tools (Write, Edit), not with shell commands.',
|
|
67
|
+
`After every change, check it loads: ${cmd.check} ${dir}`,
|
|
68
|
+
`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.`,
|
|
69
|
+
'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.',
|
|
70
|
+
'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
71
|
].join('\n');
|
|
50
72
|
}
|
|
51
73
|
|
|
52
|
-
|
|
74
|
+
/** The first message of a build session. */
|
|
75
|
+
export function buildIntent({ task, name, engine }) {
|
|
76
|
+
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.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function judgePrompt({ goal, input, expect, result, files = [] }) {
|
|
53
80
|
return [
|
|
54
81
|
"You check one test of an autonomous agent. Decide if the agent's result meets the expectation for the input.",
|
|
82
|
+
'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
83
|
'Be strict about substance, lenient about wording. Reply with JSON only: {"pass": true|false, "why": "<one sentence>"}.',
|
|
56
84
|
`<goal>\n${goal}\n</goal>`,
|
|
57
85
|
`<input>\n${input}\n</input>`,
|
|
58
86
|
`<expectation>\n${expect}\n</expectation>`,
|
|
59
87
|
`<result>\n${result}\n</result>`,
|
|
88
|
+
...(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
89
|
].join('\n');
|
|
61
90
|
}
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
const SKIP = new Set(['.agentkit', '.git', 'node_modules', 'dist']);
|
|
93
|
+
/** Files under `dir` (not its own state, git or build output) with when each last changed. */
|
|
94
|
+
export function snapshot(dir, { max = 5000 } = {}) {
|
|
95
|
+
const seen = new Map();
|
|
96
|
+
const walk = (d) => {
|
|
97
|
+
let entries = [];
|
|
98
|
+
try {
|
|
99
|
+
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const e of entries) {
|
|
104
|
+
if (seen.size >= max) return;
|
|
105
|
+
const p = path.join(d, e.name);
|
|
106
|
+
if (e.isDirectory()) {
|
|
107
|
+
if (!SKIP.has(e.name)) walk(p);
|
|
108
|
+
} else if (e.isFile()) {
|
|
109
|
+
const st = fs.statSync(p, { throwIfNoEntry: false });
|
|
110
|
+
if (st) seen.set(p, `${st.mtimeMs}:${st.size}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
80
113
|
};
|
|
81
|
-
|
|
114
|
+
walk(dir);
|
|
115
|
+
return seen;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The text files created or changed since `before` (a snapshot), for the judge; a few, each capped. */
|
|
119
|
+
export function filesWritten(dir, before, { root = dir, limit = 6, bytes = 4000 } = {}) {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const [p, sig] of snapshot(dir)) {
|
|
122
|
+
if (before.get(p) === sig || out.length >= limit) continue;
|
|
123
|
+
const buf = fs.readFileSync(p);
|
|
124
|
+
if (buf.subarray(0, 2000).includes(0)) continue; // binary
|
|
125
|
+
out.push({ path: path.relative(root, p), text: buf.subarray(0, bytes).toString('utf8'), truncated: buf.length > bytes });
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
82
128
|
}
|
|
83
129
|
|
|
84
130
|
export class Builder {
|
|
85
|
-
/** `oneShot(prompt)` answers a
|
|
131
|
+
/** `oneShot(prompt)` answers a judge prompt (the daemon's writer engine). */
|
|
132
|
+
/** `sessions`: the daemon's agent sessions ({ create, get, tell, stop }); each built agent is developed in one. */
|
|
86
133
|
/** `reportEnv()`: settings that make an agent started here report to this daemon. */
|
|
87
|
-
constructor({ oneShot, env = process.env, runTurn = engineTurn, pack = packageAgent, reportEnv = () => ({}) }) {
|
|
134
|
+
constructor({ oneShot, sessions, env = process.env, runTurn = engineTurn, pack = packageAgent, reportEnv = () => ({}) }) {
|
|
88
135
|
this.reportEnv = reportEnv;
|
|
89
136
|
this.oneShot = oneShot;
|
|
137
|
+
this.sessions = sessions;
|
|
90
138
|
this.env = env;
|
|
91
139
|
this.runTurn = runTurn;
|
|
92
140
|
this.pack = pack;
|
|
93
141
|
this.procs = new Map(); // name -> child process of `agentkit run`
|
|
94
|
-
this.pending = new Map(); // name -> what's in progress (
|
|
142
|
+
this.pending = new Map(); // name -> what's in progress (testing, packaging)
|
|
95
143
|
}
|
|
96
144
|
|
|
97
145
|
dir(name) {
|
|
@@ -128,59 +176,108 @@ export class Builder {
|
|
|
128
176
|
const read = (f) => (fs.existsSync(path.join(dir, f)) ? fs.readFileSync(path.join(dir, f), 'utf8') : '');
|
|
129
177
|
let spec = null;
|
|
130
178
|
let specError = null;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
179
|
+
const yaml = read('agent.yaml');
|
|
180
|
+
if (yaml) {
|
|
181
|
+
try {
|
|
182
|
+
spec = loadAgent(dir, { env: this.env });
|
|
183
|
+
} catch (e) {
|
|
184
|
+
specError = e.message;
|
|
185
|
+
}
|
|
135
186
|
}
|
|
136
187
|
const p = this.procs.get(name);
|
|
137
188
|
let runs = [];
|
|
138
189
|
if (spec) runs = new Agent(spec, { env: this.env }).runs(20);
|
|
190
|
+
// Its session keeps analysis.json and tests.yaml current; read them as they are now.
|
|
191
|
+
let design = {};
|
|
192
|
+
try {
|
|
193
|
+
design = JSON.parse(read('analysis.json') || '{}');
|
|
194
|
+
} catch {}
|
|
195
|
+
let tests = m.tests ?? [];
|
|
196
|
+
try {
|
|
197
|
+
tests = (YAML.parse(read('tests.yaml') || '[]') ?? []).filter((t) => t?.input).map((t) => ({ input: String(t.input), expect: String(t.expect ?? '') }));
|
|
198
|
+
} catch {}
|
|
199
|
+
// Until its spec loads, the status follows the files: designing, then designed or needs fixing.
|
|
200
|
+
let status = m.status;
|
|
201
|
+
if (!yaml) status = 'designing';
|
|
202
|
+
else if (specError) status = 'needs-fixing';
|
|
203
|
+
else if (['designing', 'needs-fixing'].includes(status)) status = 'designed';
|
|
204
|
+
const s = m.session ? this.sessions?.get(m.session) : null;
|
|
205
|
+
// When its files last changed (its session edits them): results and executables from before are out of date.
|
|
206
|
+
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
207
|
return {
|
|
140
208
|
...m,
|
|
141
209
|
name,
|
|
210
|
+
status,
|
|
142
211
|
title: spec?.title ?? name,
|
|
143
212
|
description: spec?.description ?? '',
|
|
144
213
|
dir,
|
|
145
|
-
yaml
|
|
214
|
+
yaml,
|
|
146
215
|
instructionsText: read('instructions.md'),
|
|
216
|
+
analysis: design.analysis ?? m.analysis ?? null,
|
|
217
|
+
tools: design.tools ?? m.tools ?? [],
|
|
218
|
+
tests,
|
|
147
219
|
specError,
|
|
148
|
-
engine: spec?.engine ?? null,
|
|
220
|
+
engine: spec?.engine ?? m.engine ?? null,
|
|
149
221
|
triggers: spec?.triggers.map((t) => t.label) ?? [],
|
|
150
222
|
missingEnv: spec?.missingEnv ?? [],
|
|
151
223
|
pending: this.pending.get(name) ?? null,
|
|
152
224
|
running: p ? { pid: p.child.pid, url: p.url, since: p.since } : null,
|
|
225
|
+
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
226
|
runs,
|
|
154
227
|
platforms: PLATFORMS.filter((x) => process.platform === 'darwin' || !x.startsWith('macos-')),
|
|
155
228
|
host: HOST,
|
|
229
|
+
changedAt,
|
|
156
230
|
// Executables built since the last edit that are still there.
|
|
157
231
|
executables: Object.fromEntries(Object.entries(m.executables ?? {}).filter(([, x]) => fs.existsSync(path.join(dir, 'dist', x.file)))),
|
|
158
232
|
};
|
|
159
233
|
}
|
|
160
234
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Start building an agent for a task: its folder, and an agent session working in it that
|
|
237
|
+
* analyses the task and writes the agent (analysis, spec, instructions, tests). The person
|
|
238
|
+
* keeps developing it in that session.
|
|
239
|
+
*/
|
|
240
|
+
design({ task, engine = 'claude', name, attachments, permissionMode, model }) {
|
|
163
241
|
task = String(task ?? '').trim();
|
|
164
242
|
if (!task) throw err(400, 'Describe the task or workflow');
|
|
165
|
-
if (!
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
243
|
+
if (!ENGINES.includes(engine) && !/^acp:[a-z0-9-]+$/.test(engine)) throw err(400, 'engine: claude, codex, muse or acp:<name>');
|
|
244
|
+
if (!this.sessions) throw err(503, 'Agent sessions are not available');
|
|
245
|
+
const want = name ? slug(name) : nameFrom(task);
|
|
246
|
+
if (!/^[a-z][a-z0-9-]{1,40}$/.test(want)) throw err(400, 'name: lowercase letters, digits and dashes, starting with a letter');
|
|
247
|
+
let n = want;
|
|
248
|
+
for (let i = 2; fs.existsSync(path.join(root(), n)); i++) n = `${want.slice(0, 36)}-${i}`;
|
|
249
|
+
const dir = path.join(root(), n);
|
|
171
250
|
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;
|
|
251
|
+
this.saveMeta(n, { task, engine, results: null, status: 'designing', createdAt: now() });
|
|
178
252
|
try {
|
|
179
|
-
|
|
253
|
+
const s = this.sessions.create({ name: n, dir, intent: buildIntent({ task, name: n, engine }), builds: { name: n, engine }, attachments, permissionMode, model });
|
|
254
|
+
this.saveMeta(n, { session: s.id });
|
|
180
255
|
} catch (e) {
|
|
181
|
-
|
|
256
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
257
|
+
throw e;
|
|
258
|
+
}
|
|
259
|
+
return this.view(n);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Ask its session for a change (typed in when it's idle; started if it's closed). An agent with
|
|
264
|
+
* no session yet (built before sessions, or its session deleted) gets a new one.
|
|
265
|
+
*/
|
|
266
|
+
change(name, text) {
|
|
267
|
+
const dir = this.dir(name);
|
|
268
|
+
const m = this.meta(name);
|
|
269
|
+
if (!m) throw err(404, `No built agent "${name}"`);
|
|
270
|
+
text = String(text ?? '').trim();
|
|
271
|
+
const s = m.session ? this.sessions.get(m.session) : null;
|
|
272
|
+
if (s) {
|
|
273
|
+
if (!text) throw err(400, 'Say what to change');
|
|
274
|
+
this.sessions.tell(s.id, text);
|
|
275
|
+
} else {
|
|
276
|
+
const engine = this.view(name).engine ?? 'claude';
|
|
277
|
+
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.'}`;
|
|
278
|
+
const created = this.sessions.create({ name, dir, intent, builds: { name, engine } });
|
|
279
|
+
this.saveMeta(name, { session: created.id });
|
|
182
280
|
}
|
|
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
281
|
return this.view(name);
|
|
185
282
|
}
|
|
186
283
|
|
|
@@ -230,18 +327,22 @@ export class Builder {
|
|
|
230
327
|
// Tests don't wait for people: approval tools are simply not available in them.
|
|
231
328
|
const agent = new Agent({ ...spec, triggers: [], report: null }, { env: this.env, engineTurn: this.runTurn, approvals: 'deny' });
|
|
232
329
|
const results = [];
|
|
330
|
+
// The judge sees what each run wrote in the agent's folders, not only what it says it did.
|
|
331
|
+
const roots = [...new Set([dir, spec.workdir])].filter((d, i, all) => !all.some((o, j) => j !== i && d.startsWith(o + path.sep)));
|
|
233
332
|
for (const t of tests) {
|
|
333
|
+
const before = roots.map((d) => snapshot(d));
|
|
234
334
|
const r = await agent.run(t.input, { source: 'test' });
|
|
335
|
+
const files = roots.flatMap((d, i) => filesWritten(d, before[i], { root: dir })).slice(0, 6);
|
|
235
336
|
let verdict = { pass: false, why: 'The run failed' };
|
|
236
337
|
if (r.status !== 'failed') {
|
|
237
338
|
try {
|
|
238
|
-
const v = parseJsonReply(await this.oneShot(judgePrompt({ goal: spec.goal, input: t.input, expect: t.expect, result: r.result })));
|
|
339
|
+
const v = parseJsonReply(await this.oneShot(judgePrompt({ goal: spec.goal, input: t.input, expect: t.expect, result: r.result, files })));
|
|
239
340
|
verdict = { pass: v.pass === true, why: String(v.why ?? '') };
|
|
240
341
|
} catch (e) {
|
|
241
342
|
verdict = { pass: false, why: `Could not judge: ${e.message}` };
|
|
242
343
|
}
|
|
243
344
|
}
|
|
244
|
-
results.push({ input: t.input, expect: t.expect, result: r.result, status: r.status, costUsd: r.costUsd, ...verdict });
|
|
345
|
+
results.push({ input: t.input, expect: t.expect, result: r.result, status: r.status, costUsd: r.costUsd, files: files.map((f) => f.path), ...verdict });
|
|
245
346
|
}
|
|
246
347
|
const passed = results.filter((r) => r.pass).length;
|
|
247
348
|
this.saveMeta(name, { results: { at: now(), passed, total: results.length, list: results }, status: passed === results.length ? 'tested' : 'failing' });
|
|
@@ -326,6 +427,9 @@ export class Builder {
|
|
|
326
427
|
|
|
327
428
|
remove(name) {
|
|
328
429
|
this.stop(name);
|
|
430
|
+
// Its session works in the folder about to go; close it (the conversation is kept).
|
|
431
|
+
const sid = this.meta(name)?.session;
|
|
432
|
+
if (sid && this.sessions?.get(sid)) this.sessions.stop(sid);
|
|
329
433
|
fs.rmSync(this.dir(name), { recursive: true, force: true });
|
|
330
434
|
return { ok: true };
|
|
331
435
|
}
|
package/src/manager.js
CHANGED
|
@@ -113,6 +113,7 @@ import { leadPrompt, subagents, teamDir, teamPrompt } from './teams.js';
|
|
|
113
113
|
import { assistantDir, assistantPrompt, assistantSettings } from './assistant.js';
|
|
114
114
|
import { learnCommand, memoryKey, memoryPrompt, memoryText } from './memory.js';
|
|
115
115
|
import { runningClaude, scanClaudeSessions } from './sessions.js';
|
|
116
|
+
import { builderAllowedTools, builderPrompt } from './builder.js';
|
|
116
117
|
import {
|
|
117
118
|
bugFromRef,
|
|
118
119
|
extractRefs,
|
|
@@ -596,6 +597,7 @@ export class AgentManager extends EventEmitter {
|
|
|
596
597
|
trigger,
|
|
597
598
|
attachments,
|
|
598
599
|
isolation,
|
|
600
|
+
builds,
|
|
599
601
|
}) {
|
|
600
602
|
intent = (intent ?? '').trim();
|
|
601
603
|
const staged = stagedFiles(attachments);
|
|
@@ -648,6 +650,8 @@ export class AgentManager extends EventEmitter {
|
|
|
648
650
|
...(startedBy ? { startedBy } : {}),
|
|
649
651
|
...(trigger ? { trigger } : {}),
|
|
650
652
|
...(isolation ? { isolation } : {}),
|
|
653
|
+
// A session that develops an agent built here (builder.js): { name, engine }.
|
|
654
|
+
...(builds ? { builds: { name: String(builds.name), engine: String(builds.engine ?? 'claude') } } : {}),
|
|
651
655
|
name: name?.trim() || jiraKey || slug,
|
|
652
656
|
slug,
|
|
653
657
|
intent,
|
|
@@ -664,7 +668,7 @@ export class AgentManager extends EventEmitter {
|
|
|
664
668
|
model: model || null,
|
|
665
669
|
// Review the implementation plan before the agent implements it: null = no review.
|
|
666
670
|
// Team members get their review from the team instead.
|
|
667
|
-
planReview: !general && !team && (planReview ?? this.config.planReview ?? false) ? { status: 'writing' } : null,
|
|
671
|
+
planReview: !general && !team && !builds && (planReview ?? this.config.planReview ?? false) ? { status: 'writing' } : null,
|
|
668
672
|
status: 'preparing',
|
|
669
673
|
desired: start ? 'running' : 'stopped',
|
|
670
674
|
error: null,
|
|
@@ -1042,7 +1046,8 @@ export class AgentManager extends EventEmitter {
|
|
|
1042
1046
|
if (this.config.inbox !== false) prompts.push(questionsPrompt(path.join(dir, path.basename(questionsPath(agent.id)))));
|
|
1043
1047
|
}
|
|
1044
1048
|
// SPEC.md before changing code, and PLAN.md kept as it goes (unless the plan review asks for the plan).
|
|
1045
|
-
|
|
1049
|
+
// A session building an agent keeps its analysis in the agent's folder instead.
|
|
1050
|
+
if (task && this.config.specs !== false && !agent.builds) {
|
|
1046
1051
|
fs.mkdirSync(briefDir(agent.id), { recursive: true });
|
|
1047
1052
|
const dir = fs.realpathSync(briefDir(agent.id));
|
|
1048
1053
|
writableDir ??= dir;
|
|
@@ -1061,6 +1066,11 @@ export class AgentManager extends EventEmitter {
|
|
|
1061
1066
|
prompts.push(memoryPrompt(memoryText(memoryKey(agent))));
|
|
1062
1067
|
allowedTools = [`Bash(${learnCommand()}:*)`];
|
|
1063
1068
|
}
|
|
1069
|
+
if (agent.builds && agent.workdir) {
|
|
1070
|
+
prompts.push(builderPrompt({ dir: agent.workdir, name: agent.builds.name, engine: agent.builds.engine }));
|
|
1071
|
+
// Checking the agent loads and reading the reference need no permission prompt.
|
|
1072
|
+
allowedTools = [...(allowedTools ?? []), ...builderAllowedTools(agent.workdir)];
|
|
1073
|
+
}
|
|
1064
1074
|
if (agent.kind === 'assistant') {
|
|
1065
1075
|
fs.mkdirSync(assistantDir(), { recursive: true });
|
|
1066
1076
|
const dir = fs.realpathSync(assistantDir());
|
|
@@ -1153,6 +1163,14 @@ export class AgentManager extends EventEmitter {
|
|
|
1153
1163
|
this.runtimes.set(agent.id, rt);
|
|
1154
1164
|
|
|
1155
1165
|
proc.onData((data) => {
|
|
1166
|
+
// Newer Claude Code blinks a ⏺ beside a pending tool call (while its permission prompt is
|
|
1167
|
+
// up): drawn, not work. It goes to the terminal but doesn't keep the agent busy or fill the
|
|
1168
|
+
// screen text the prompt is read from.
|
|
1169
|
+
if (!data.replace(ANSI, '').replace(/[\s⏺]/g, '')) {
|
|
1170
|
+
this.appendOutput(rt, data);
|
|
1171
|
+
this.broadcast(rt, { t: 'o', d: data });
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1156
1174
|
rt.linkScan = true;
|
|
1157
1175
|
rt.lastOutputAt = Date.now();
|
|
1158
1176
|
rt.tail = (rt.tail + data).slice(-4000);
|
package/src/server.js
CHANGED
|
@@ -525,9 +525,19 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
525
525
|
api.get('/agents/:id/memory', wrap((req) => memory.read(req.params.id)));
|
|
526
526
|
api.put('/agents/:id/memory', wrap((req) => memory.write(req.params.id, req.body?.text)));
|
|
527
527
|
api.post('/agents/:id/learn', wrap((req) => memory.learn(req.params.id, req.body?.text, { by: req.headers['x-agentmgr-agent'] ? undefined : 'you' })));
|
|
528
|
-
// The agent builder (agentkit agents
|
|
529
|
-
//
|
|
530
|
-
|
|
528
|
+
// The agent builder (agentkit agents built from a task, each developed in an agent session
|
|
529
|
+
// working in its folder): build, change, test, run, package, download. Agents started here
|
|
530
|
+
// report to this daemon (runs, cost, approvals).
|
|
531
|
+
const sessions = {
|
|
532
|
+
create: (opts) => manager.create({ ...opts, worktree: false }),
|
|
533
|
+
get: (id) => {
|
|
534
|
+
const a = manager.get(id);
|
|
535
|
+
return a ? manager.view(a) : null;
|
|
536
|
+
},
|
|
537
|
+
tell: (id, text) => manager.tell(id, text),
|
|
538
|
+
stop: (id) => manager.stop(id).catch(() => {}),
|
|
539
|
+
};
|
|
540
|
+
const builder = new Builder({ sessions, oneShot: (prompt) => manager.oneShot(null, prompt, { ...manager.writerFor({}), timeoutMs: 300_000 }), reportEnv: () => ({ AGENTKIT_REPORT_URL: `http://127.0.0.1:${port}`, AGENTKIT_REPORT_TOKEN: reports.reportToken() }) });
|
|
531
541
|
api.post('/agentkit/report', wrap((req) => reports.report(req.body ?? {})));
|
|
532
542
|
api.get('/agentkit/agents', wrap(() => reports.list()));
|
|
533
543
|
api.post('/agentkit/agents/:key/approvals/:id', wrap((req) => reports.decide(req.params.key, req.params.id, req.body?.allow === true, req.device ? `${req.device.name ?? 'a signed-in device'}` : 'you')));
|
|
@@ -535,7 +545,8 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
535
545
|
// What an agent elsewhere needs to report here (this computer only: it shows the token).
|
|
536
546
|
api.get('/agentkit/connect', wrap(() => ({ token: reports.reportToken(), local: `http://127.0.0.1:${port}`, remote: remoteServer ? remote.remoteUrls(remotePort).map((u) => u.url) : [], cert: remoteServer ? remote.certificateFingerprint() : null })));
|
|
537
547
|
api.get('/builder', wrap(() => builder.list()));
|
|
538
|
-
api.post('/builder', wrap((req) => builder.design({ task: req.body?.task, engine: req.body?.engine || 'claude', attachments: req.body?.attachments
|
|
548
|
+
api.post('/builder', wrap((req) => builder.design({ task: req.body?.task, engine: req.body?.engine || 'claude', name: req.body?.name || undefined, attachments: req.body?.attachments, permissionMode: req.body?.permissionMode || undefined, model: req.body?.model || undefined })));
|
|
549
|
+
api.post('/builder/:name/change', wrap((req) => builder.change(req.params.name, req.body?.text)));
|
|
539
550
|
api.get('/builder/:name', wrap((req) => builder.view(req.params.name)));
|
|
540
551
|
api.put('/builder/:name', wrap((req) => builder.update(req.params.name, req.body ?? {})));
|
|
541
552
|
api.post('/builder/:name/test', wrap((req) => {
|