agentgui 1.0.437 → 1.0.439
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/CLAUDE.md +10 -0
- package/build-portable.js +28 -11
- package/lib/acp-manager.js +229 -0
- package/lib/ws-handlers-session.js +31 -25
- package/package.json +2 -2
- package/server.js +32 -16
package/CLAUDE.md
CHANGED
|
@@ -17,6 +17,7 @@ Server starts on `http://localhost:3000`, redirects to `/gm/`.
|
|
|
17
17
|
server.js HTTP server + WebSocket + all API routes (raw http.createServer)
|
|
18
18
|
database.js SQLite setup (WAL mode), schema, query functions
|
|
19
19
|
lib/claude-runner.js Agent framework - spawns CLI processes, parses stream-json output
|
|
20
|
+
lib/acp-manager.js ACP tool lifecycle - auto-starts opencode/kilo HTTP servers, restart on crash
|
|
20
21
|
lib/speech.js Speech-to-text and text-to-speech via @huggingface/transformers
|
|
21
22
|
bin/gmgui.cjs CLI entry point (npx agentgui / bunx agentgui)
|
|
22
23
|
static/index.html Main HTML shell
|
|
@@ -49,6 +50,14 @@ static/templates/ 31 HTML template fragments for event rendering
|
|
|
49
50
|
- `STARTUP_CWD` - Working directory passed to agents
|
|
50
51
|
- `HOT_RELOAD` - Set to "false" to disable watch mode
|
|
51
52
|
|
|
53
|
+
## ACP Tool Lifecycle
|
|
54
|
+
|
|
55
|
+
On startup, agentgui auto-launches bundled ACP tools (opencode, kilo) as HTTP servers:
|
|
56
|
+
- OpenCode: port 18100 (`opencode acp --port 18100`)
|
|
57
|
+
- Kilo: port 18101 (`kilo acp --port 18101`)
|
|
58
|
+
|
|
59
|
+
Managed by `lib/acp-manager.js`. Features: crash restart with exponential backoff (max 10 in 5min), health checks every 30s via `GET /provider`, clean shutdown on SIGTERM. The `acpPort` field on discovered agents is set automatically once healthy. Models are queried from the running ACP HTTP servers via their `/provider` endpoint.
|
|
60
|
+
|
|
52
61
|
## REST API
|
|
53
62
|
|
|
54
63
|
All routes are prefixed with `BASE_URL` (default `/gm`).
|
|
@@ -68,6 +77,7 @@ All routes are prefixed with `BASE_URL` (default `/gm`).
|
|
|
68
77
|
- `GET /api/sessions/:id/chunks` - Get session chunks (query: since)
|
|
69
78
|
- `GET /api/sessions/:id/execution` - Get execution events (query: limit, offset, filterType)
|
|
70
79
|
- `GET /api/agents` - List discovered agents
|
|
80
|
+
- `GET /api/acp/status` - ACP tool lifecycle status (ports, health, PIDs, restart counts)
|
|
71
81
|
- `GET /api/home` - Get home directory
|
|
72
82
|
- `POST /api/stt` - Speech-to-text (raw audio body)
|
|
73
83
|
- `POST /api/tts` - Text-to-speech (body: text)
|
package/build-portable.js
CHANGED
|
@@ -122,6 +122,32 @@ const audioDeps = new Set();
|
|
|
122
122
|
collectDeps('audio-decode', audioDeps);
|
|
123
123
|
for (const dep of audioDeps) copyPkg(dep);
|
|
124
124
|
|
|
125
|
+
log('Copying ACP tools (opencode, kilo, gemini) for portable...');
|
|
126
|
+
const acpNativeTools = [
|
|
127
|
+
{ wrapper: 'opencode-ai', platformPrefix: 'opencode-windows-x64' },
|
|
128
|
+
{ wrapper: '@kilocode/cli', platformPrefix: '@kilocode/cli-windows-x64' },
|
|
129
|
+
];
|
|
130
|
+
for (const tool of acpNativeTools) {
|
|
131
|
+
const wrapperSrc = path.join(nm, tool.wrapper);
|
|
132
|
+
if (fs.existsSync(wrapperSrc)) copyDir(wrapperSrc, path.join(destNm, tool.wrapper));
|
|
133
|
+
for (const suffix of ['', '-baseline']) {
|
|
134
|
+
const platPkg = tool.platformPrefix + suffix;
|
|
135
|
+
const platSrc = path.join(nm, platPkg);
|
|
136
|
+
if (fs.existsSync(platSrc)) {
|
|
137
|
+
copyDir(platSrc, path.join(destNm, platPkg));
|
|
138
|
+
log(` ${platPkg} (${Math.round(sizeOf(platSrc) / 1024 / 1024)}MB)`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const geminiSrc = path.join(nm, '@google', 'gemini-cli');
|
|
143
|
+
if (fs.existsSync(geminiSrc)) {
|
|
144
|
+
copyDir(geminiSrc, path.join(destNm, '@google', 'gemini-cli'));
|
|
145
|
+
const geminiDeps = new Set();
|
|
146
|
+
collectDeps('@google/gemini-cli', geminiDeps);
|
|
147
|
+
for (const dep of geminiDeps) copyPkg(dep);
|
|
148
|
+
log(` gemini-cli + ${geminiDeps.size} deps`);
|
|
149
|
+
}
|
|
150
|
+
|
|
125
151
|
log('Copying @anthropic-ai/claude-code ripgrep (win32)...');
|
|
126
152
|
const claudeSrc = path.join(nm, '@anthropic-ai', 'claude-code');
|
|
127
153
|
const claudeDest = path.join(destNm, '@anthropic-ai', 'claude-code');
|
|
@@ -157,17 +183,8 @@ if (process.env.NO_BUNDLE_MODELS === 'true') {
|
|
|
157
183
|
}
|
|
158
184
|
}
|
|
159
185
|
|
|
160
|
-
fs.writeFileSync(path.join(out, 'README.txt'),
|
|
161
|
-
'# AgentGUI Portable'
|
|
162
|
-
'',
|
|
163
|
-
'No installation required. Double-click agentgui.exe to start.',
|
|
164
|
-
'',
|
|
165
|
-
'Web interface: http://localhost:3000/gm/',
|
|
166
|
-
'',
|
|
167
|
-
'Data is stored in the data/ folder next to the executable.',
|
|
168
|
-
'',
|
|
169
|
-
'Requirements: None - fully self-contained.',
|
|
170
|
-
].join('\n'));
|
|
186
|
+
fs.writeFileSync(path.join(out, 'README.txt'),
|
|
187
|
+
'# AgentGUI Portable\n\nDouble-click agentgui.exe to start.\nWeb interface: http://localhost:3000/gm/\nData stored in data/ folder. Fully self-contained.\n');
|
|
171
188
|
|
|
172
189
|
const totalMB = Math.round(sizeOf(out) / 1024 / 1024);
|
|
173
190
|
log(`Build complete! Total: ${totalMB}MB Output: ${out}`);
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
9
|
+
const isWindows = os.platform() === 'win32';
|
|
10
|
+
|
|
11
|
+
const ACP_TOOLS = [
|
|
12
|
+
{ id: 'opencode', cmd: 'opencode', args: ['acp'], port: 18100, npxPkg: 'opencode-ai' },
|
|
13
|
+
{ id: 'kilo', cmd: 'kilo', args: ['acp'], port: 18101, npxPkg: '@kilocode/cli' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const MAX_RESTARTS = 10;
|
|
17
|
+
const RESTART_WINDOW_MS = 300000;
|
|
18
|
+
const HEALTH_INTERVAL_MS = 30000;
|
|
19
|
+
const STARTUP_GRACE_MS = 5000;
|
|
20
|
+
const IDLE_TIMEOUT_MS = 120000;
|
|
21
|
+
const processes = new Map();
|
|
22
|
+
let healthTimer = null;
|
|
23
|
+
let shuttingDown = false;
|
|
24
|
+
|
|
25
|
+
function log(msg) { console.log('[ACP] ' + msg); }
|
|
26
|
+
|
|
27
|
+
function resolveBinary(cmd) {
|
|
28
|
+
const ext = isWindows ? '.cmd' : '';
|
|
29
|
+
const localBin = path.join(projectRoot, 'node_modules', '.bin', cmd + ext);
|
|
30
|
+
if (fs.existsSync(localBin)) return localBin;
|
|
31
|
+
return cmd;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function startProcess(tool) {
|
|
35
|
+
if (shuttingDown) return null;
|
|
36
|
+
const existing = processes.get(tool.id);
|
|
37
|
+
if (existing?.process && !existing.process.killed) return existing;
|
|
38
|
+
|
|
39
|
+
const bin = resolveBinary(tool.cmd);
|
|
40
|
+
const args = [...tool.args, '--port', String(tool.port)];
|
|
41
|
+
const opts = { stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() };
|
|
42
|
+
if (isWindows) opts.shell = true;
|
|
43
|
+
|
|
44
|
+
let proc;
|
|
45
|
+
try { proc = spawn(bin, args, opts); }
|
|
46
|
+
catch (err) { log(tool.id + ' spawn failed: ' + err.message); return null; }
|
|
47
|
+
|
|
48
|
+
const entry = {
|
|
49
|
+
id: tool.id, port: tool.port, process: proc, pid: proc.pid,
|
|
50
|
+
startedAt: Date.now(), restarts: [], healthy: false, lastHealthCheck: 0,
|
|
51
|
+
lastUsed: Date.now(), idleTimer: null,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
proc.stdout.on('data', () => {});
|
|
55
|
+
proc.stderr.on('data', (d) => {
|
|
56
|
+
const t = d.toString().trim();
|
|
57
|
+
if (t) log(tool.id + ': ' + t.substring(0, 200));
|
|
58
|
+
});
|
|
59
|
+
proc.stdout.on('error', () => {});
|
|
60
|
+
proc.stderr.on('error', () => {});
|
|
61
|
+
proc.on('error', (err) => { log(tool.id + ' error: ' + err.message); entry.healthy = false; });
|
|
62
|
+
|
|
63
|
+
proc.on('close', (code) => {
|
|
64
|
+
entry.healthy = false;
|
|
65
|
+
if (shuttingDown || entry._stopping) return;
|
|
66
|
+
log(tool.id + ' exited code ' + code);
|
|
67
|
+
scheduleRestart(tool, entry.restarts);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
processes.set(tool.id, entry);
|
|
71
|
+
log(tool.id + ' started port ' + tool.port + ' pid ' + proc.pid);
|
|
72
|
+
setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
|
|
73
|
+
resetIdleTimer(tool.id);
|
|
74
|
+
return entry;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resetIdleTimer(toolId) {
|
|
78
|
+
const entry = processes.get(toolId);
|
|
79
|
+
if (!entry) return;
|
|
80
|
+
entry.lastUsed = Date.now();
|
|
81
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
82
|
+
entry.idleTimer = setTimeout(() => stopTool(toolId), IDLE_TIMEOUT_MS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stopTool(toolId) {
|
|
86
|
+
const entry = processes.get(toolId);
|
|
87
|
+
if (!entry) return;
|
|
88
|
+
log(toolId + ' idle, stopping to free RAM');
|
|
89
|
+
entry._stopping = true;
|
|
90
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
91
|
+
try { entry.process.kill('SIGTERM'); } catch (_) {}
|
|
92
|
+
setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} }, 5000);
|
|
93
|
+
processes.delete(toolId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function scheduleRestart(tool, prevRestarts = []) {
|
|
97
|
+
if (shuttingDown) return;
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const recent = prevRestarts.filter(t => now - t < RESTART_WINDOW_MS);
|
|
100
|
+
if (recent.length >= MAX_RESTARTS) {
|
|
101
|
+
log(tool.id + ' exceeded restart limit, giving up');
|
|
102
|
+
processes.delete(tool.id);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const delay = Math.min(1000 * Math.pow(2, recent.length), 30000);
|
|
106
|
+
log(tool.id + ' restarting in ' + delay + 'ms');
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
if (shuttingDown) return;
|
|
109
|
+
const entry = startProcess(tool);
|
|
110
|
+
if (entry) entry.restarts = [...recent, Date.now()];
|
|
111
|
+
}, delay);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function checkHealth(toolId) {
|
|
115
|
+
const entry = processes.get(toolId);
|
|
116
|
+
if (!entry || shuttingDown) return;
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch('http://127.0.0.1:' + entry.port + '/provider', {
|
|
119
|
+
signal: AbortSignal.timeout(3000), headers: { 'Accept': 'application/json' }
|
|
120
|
+
});
|
|
121
|
+
entry.healthy = res.ok;
|
|
122
|
+
} catch (_) { entry.healthy = false; }
|
|
123
|
+
entry.lastHealthCheck = Date.now();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function ensureRunning(agentId) {
|
|
127
|
+
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
128
|
+
if (!tool) return null;
|
|
129
|
+
let entry = processes.get(agentId);
|
|
130
|
+
if (entry?.healthy) { resetIdleTimer(agentId); return entry.port; }
|
|
131
|
+
if (!entry || entry._stopping) {
|
|
132
|
+
entry = startProcess(tool);
|
|
133
|
+
if (!entry) return null;
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < 20; i++) {
|
|
136
|
+
await new Promise(r => setTimeout(r, 500));
|
|
137
|
+
await checkHealth(agentId);
|
|
138
|
+
if (processes.get(agentId)?.healthy) { resetIdleTimer(agentId); return tool.port; }
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function touch(agentId) {
|
|
144
|
+
const entry = processes.get(agentId);
|
|
145
|
+
if (entry) resetIdleTimer(agentId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function startAll() {
|
|
149
|
+
log('ACP tools available (on-demand start)');
|
|
150
|
+
healthTimer = setInterval(() => {
|
|
151
|
+
for (const [id] of processes) checkHealth(id);
|
|
152
|
+
}, HEALTH_INTERVAL_MS);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function stopAll() {
|
|
156
|
+
shuttingDown = true;
|
|
157
|
+
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
|
158
|
+
const kills = [];
|
|
159
|
+
for (const [id, entry] of processes) {
|
|
160
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
161
|
+
log('stopping ' + id + ' pid ' + entry.pid);
|
|
162
|
+
kills.push(new Promise(resolve => {
|
|
163
|
+
const t = setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} resolve(); }, 5000);
|
|
164
|
+
entry.process.on('close', () => { clearTimeout(t); resolve(); });
|
|
165
|
+
try { entry.process.kill('SIGTERM'); } catch (_) {}
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
await Promise.all(kills);
|
|
169
|
+
processes.clear();
|
|
170
|
+
log('all stopped');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getStatus() {
|
|
174
|
+
return ACP_TOOLS.map(tool => {
|
|
175
|
+
const e = processes.get(tool.id);
|
|
176
|
+
return {
|
|
177
|
+
id: tool.id, port: tool.port, running: !!e, healthy: e?.healthy || false,
|
|
178
|
+
pid: e?.pid, uptime: e ? Date.now() - e.startedAt : 0,
|
|
179
|
+
restartCount: e?.restarts.length || 0, idleMs: e ? Date.now() - e.lastUsed : 0,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function getPort(agentId) {
|
|
185
|
+
const e = processes.get(agentId);
|
|
186
|
+
return e?.healthy ? e.port : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getRunningPorts() {
|
|
190
|
+
const ports = {};
|
|
191
|
+
for (const [id, e] of processes) if (e.healthy) ports[id] = e.port;
|
|
192
|
+
return ports;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function restart(agentId) {
|
|
196
|
+
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
197
|
+
if (!tool) return false;
|
|
198
|
+
stopTool(agentId);
|
|
199
|
+
startProcess(tool);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function queryModels(agentId) {
|
|
204
|
+
const port = await ensureRunning(agentId);
|
|
205
|
+
if (!port) return [];
|
|
206
|
+
try {
|
|
207
|
+
const res = await fetch('http://127.0.0.1:' + port + '/provider', {
|
|
208
|
+
signal: AbortSignal.timeout(5000), headers: { 'Accept': 'application/json' }
|
|
209
|
+
});
|
|
210
|
+
if (!res.ok) return [];
|
|
211
|
+
const data = await res.json();
|
|
212
|
+
const models = [];
|
|
213
|
+
for (const prov of (data.all || [])) {
|
|
214
|
+
for (const m of Object.values(prov.models || {})) {
|
|
215
|
+
models.push({ id: m.id, label: m.name || m.id, provider: prov.name || prov.id });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return models;
|
|
219
|
+
} catch (_) { return []; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function isAvailable(agentId) {
|
|
223
|
+
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
224
|
+
if (!tool) return false;
|
|
225
|
+
const bin = resolveBinary(tool.cmd);
|
|
226
|
+
return bin !== tool.cmd || fs.existsSync(bin);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const ACP_TOOL_CONFIGS = ACP_TOOLS;
|
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { execSync, spawn } from 'child_process';
|
|
5
|
+
import { ensureRunning, touch } from './acp-manager.js';
|
|
5
6
|
|
|
6
7
|
function spawnScript(cmd, args, convId, scriptName, agentId, deps) {
|
|
7
8
|
const { activeScripts, broadcastSync, modelCache } = deps;
|
|
@@ -109,9 +110,11 @@ export function register(router, deps) {
|
|
|
109
110
|
|
|
110
111
|
router.handle('agent.subagents', async (p) => {
|
|
111
112
|
const agent = discoveredAgents.find(x => x.id === p.id);
|
|
112
|
-
if (!agent || agent.protocol !== 'acp'
|
|
113
|
+
if (!agent || agent.protocol !== 'acp') return { subAgents: [] };
|
|
114
|
+
const port = await ensureRunning(p.id);
|
|
115
|
+
if (!port) return { subAgents: [] };
|
|
113
116
|
try {
|
|
114
|
-
const data = await acpFetch(
|
|
117
|
+
const data = await acpFetch(port, '/agents/search', {});
|
|
115
118
|
const list = Array.isArray(data) ? data : (data?.agents || []);
|
|
116
119
|
return { subAgents: list.map(a => ({ id: a.agent_id || a.id, name: a.metadata?.ref?.name || a.name || a.agent_id || a.id })) };
|
|
117
120
|
} catch (_) { return { subAgents: [] }; }
|
|
@@ -129,29 +132,32 @@ export function register(router, deps) {
|
|
|
129
132
|
return d;
|
|
130
133
|
});
|
|
131
134
|
|
|
132
|
-
router.handle('agent.models', async (p) => {
|
|
133
|
-
const cached = modelCache.get(p.id);
|
|
134
|
-
if (cached && (Date.now() - cached.ts) < 300000) return { models: cached.models };
|
|
135
|
-
let models = [];
|
|
136
|
-
if (p.id === 'claude-code') {
|
|
137
|
-
models = [
|
|
138
|
-
{ id: '
|
|
139
|
-
{ id: '
|
|
140
|
-
{ id: '
|
|
141
|
-
];
|
|
142
|
-
} else {
|
|
143
|
-
const agent = discoveredAgents.find(x => x.id === p.id);
|
|
144
|
-
if (agent?.protocol === 'acp'
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
135
|
+
router.handle('agent.models', async (p) => {
|
|
136
|
+
const cached = modelCache.get(p.id);
|
|
137
|
+
if (cached && (Date.now() - cached.ts) < 300000) return { models: cached.models };
|
|
138
|
+
let models = [];
|
|
139
|
+
if (p.id === 'claude-code') {
|
|
140
|
+
models = [
|
|
141
|
+
{ id: 'haiku', label: 'Haiku' },
|
|
142
|
+
{ id: 'sonnet', label: 'Sonnet' },
|
|
143
|
+
{ id: 'opus', label: 'Opus' }
|
|
144
|
+
];
|
|
145
|
+
} else {
|
|
146
|
+
const agent = discoveredAgents.find(x => x.id === p.id);
|
|
147
|
+
if (agent?.protocol === 'acp') {
|
|
148
|
+
const port = await ensureRunning(p.id);
|
|
149
|
+
if (port) {
|
|
150
|
+
try {
|
|
151
|
+
const data = await acpFetch(port, '/models');
|
|
152
|
+
const list = data?.data || data?.models || (Array.isArray(data) ? data : []);
|
|
153
|
+
models = list.map(m => ({ id: m.id || m.model_id, label: m.name || m.display_name || m.id || m.model_id })).filter(m => m.id);
|
|
154
|
+
} catch (_) {}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (models.length > 0) modelCache.set(p.id, { models, ts: Date.now() });
|
|
159
|
+
return { models };
|
|
160
|
+
});
|
|
155
161
|
|
|
156
162
|
router.handle('agent.search', (p) => db.searchAgents(discoveredAgents, p.query || p));
|
|
157
163
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.439",
|
|
4
4
|
"description": "Multi-agent ACP client with real-time communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"fsbrowse": "^0.2.18",
|
|
34
34
|
"google-auth-library": "^10.5.0",
|
|
35
35
|
"onnxruntime-node": "1.21.0",
|
|
36
|
-
"opencode-ai": "
|
|
36
|
+
"opencode-ai": "^1.2.15",
|
|
37
37
|
"puppeteer-core": "^24.37.5",
|
|
38
38
|
"webtalk": "^1.0.31",
|
|
39
39
|
"ws": "^8.14.2"
|
package/server.js
CHANGED
|
@@ -22,6 +22,7 @@ import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
|
|
|
22
22
|
import { register as registerSessionHandlers } from './lib/ws-handlers-session.js';
|
|
23
23
|
import { register as registerRunHandlers } from './lib/ws-handlers-run.js';
|
|
24
24
|
import { register as registerUtilHandlers } from './lib/ws-handlers-util.js';
|
|
25
|
+
import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, queryModels as queryACPModels, touch as touchACP } from './lib/acp-manager.js';
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
process.on('uncaughtException', (err, origin) => {
|
|
@@ -432,24 +433,14 @@ async function getModelsForAgent(agentId) {
|
|
|
432
433
|
let models = [];
|
|
433
434
|
if (agentId === 'claude-code') {
|
|
434
435
|
models = [
|
|
435
|
-
{ id: '
|
|
436
|
-
{ id: '
|
|
437
|
-
{ id: '
|
|
436
|
+
{ id: 'haiku', label: 'Haiku' },
|
|
437
|
+
{ id: 'sonnet', label: 'Sonnet' },
|
|
438
|
+
{ id: 'opus', label: 'Opus' }
|
|
438
439
|
];
|
|
439
440
|
} else {
|
|
440
441
|
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
441
|
-
if (agent?.protocol === 'acp'
|
|
442
|
-
try {
|
|
443
|
-
const res = await fetch(`http://localhost:${agent.acpPort}/models`, {
|
|
444
|
-
headers: { 'Content-Type': 'application/json' },
|
|
445
|
-
signal: AbortSignal.timeout(3000)
|
|
446
|
-
});
|
|
447
|
-
if (res.ok) {
|
|
448
|
-
const data = await res.json();
|
|
449
|
-
const list = data?.data || data?.models || (Array.isArray(data) ? data : []);
|
|
450
|
-
models = list.map(m => ({ id: m.id || m.model_id, label: m.name || m.display_name || m.id || m.model_id })).filter(m => m.id);
|
|
451
|
-
}
|
|
452
|
-
} catch (_) {}
|
|
442
|
+
if (agent?.protocol === 'acp') {
|
|
443
|
+
try { models = await queryACPModels(agentId); } catch (_) {}
|
|
453
444
|
}
|
|
454
445
|
}
|
|
455
446
|
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
@@ -1747,6 +1738,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
1747
1738
|
return;
|
|
1748
1739
|
}
|
|
1749
1740
|
|
|
1741
|
+
if (pathOnly === '/api/acp/status' && req.method === 'GET') {
|
|
1742
|
+
sendJSON(req, res, 200, { tools: getACPStatus() });
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1750
1746
|
if (pathOnly === '/api/ws-stats' && req.method === 'GET') {
|
|
1751
1747
|
const stats = wsOptimizer.getStats();
|
|
1752
1748
|
sendJSON(req, res, 200, stats);
|
|
@@ -3154,6 +3150,7 @@ function createChunkBatcher() {
|
|
|
3154
3150
|
|
|
3155
3151
|
async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, model) {
|
|
3156
3152
|
const startTime = Date.now();
|
|
3153
|
+
touchACP(agentId);
|
|
3157
3154
|
|
|
3158
3155
|
const conv = queries.getConversation(conversationId);
|
|
3159
3156
|
if (!conv) {
|
|
@@ -3965,7 +3962,11 @@ if (watch) {
|
|
|
3965
3962
|
|
|
3966
3963
|
process.on('SIGTERM', () => {
|
|
3967
3964
|
console.log('[SIGNAL] SIGTERM received - graceful shutdown');
|
|
3968
|
-
|
|
3965
|
+
stopACPTools().then(() => {
|
|
3966
|
+
wss.close(() => server.close(() => process.exit(0)));
|
|
3967
|
+
}).catch(() => {
|
|
3968
|
+
wss.close(() => server.close(() => process.exit(0)));
|
|
3969
|
+
});
|
|
3969
3970
|
});
|
|
3970
3971
|
|
|
3971
3972
|
server.on('error', (err) => {
|
|
@@ -4168,6 +4169,21 @@ function onServerReady() {
|
|
|
4168
4169
|
|
|
4169
4170
|
resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
|
|
4170
4171
|
|
|
4172
|
+
startACPTools().then(() => {
|
|
4173
|
+
setTimeout(() => {
|
|
4174
|
+
const acpStatus = getACPStatus();
|
|
4175
|
+
for (const s of acpStatus) {
|
|
4176
|
+
if (s.healthy) {
|
|
4177
|
+
const agent = discoveredAgents.find(a => a.id === s.id);
|
|
4178
|
+
if (agent) { agent.acpPort = s.port; }
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
if (acpStatus.length > 0) {
|
|
4182
|
+
console.log(`[ACP] Tools ready: ${acpStatus.filter(s => s.healthy).map(s => s.id + ':' + s.port).join(', ') || 'none healthy yet'}`);
|
|
4183
|
+
}
|
|
4184
|
+
}, 6000);
|
|
4185
|
+
}).catch(err => console.error('[ACP] Startup error:', err.message));
|
|
4186
|
+
|
|
4171
4187
|
ensureModelsDownloaded().then(async ok => {
|
|
4172
4188
|
if (ok) console.log('[MODELS] Speech models ready');
|
|
4173
4189
|
else console.log('[MODELS] Speech model download failed');
|