agentgui 1.0.438 → 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/lib/acp-manager.js +69 -19
- package/lib/ws-handlers-session.js +31 -25
- package/package.json +1 -1
- package/server.js +6 -21
package/lib/acp-manager.js
CHANGED
|
@@ -17,6 +17,7 @@ const MAX_RESTARTS = 10;
|
|
|
17
17
|
const RESTART_WINDOW_MS = 300000;
|
|
18
18
|
const HEALTH_INTERVAL_MS = 30000;
|
|
19
19
|
const STARTUP_GRACE_MS = 5000;
|
|
20
|
+
const IDLE_TIMEOUT_MS = 120000;
|
|
20
21
|
const processes = new Map();
|
|
21
22
|
let healthTimer = null;
|
|
22
23
|
let shuttingDown = false;
|
|
@@ -32,6 +33,9 @@ function resolveBinary(cmd) {
|
|
|
32
33
|
|
|
33
34
|
function startProcess(tool) {
|
|
34
35
|
if (shuttingDown) return null;
|
|
36
|
+
const existing = processes.get(tool.id);
|
|
37
|
+
if (existing?.process && !existing.process.killed) return existing;
|
|
38
|
+
|
|
35
39
|
const bin = resolveBinary(tool.cmd);
|
|
36
40
|
const args = [...tool.args, '--port', String(tool.port)];
|
|
37
41
|
const opts = { stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() };
|
|
@@ -44,6 +48,7 @@ function startProcess(tool) {
|
|
|
44
48
|
const entry = {
|
|
45
49
|
id: tool.id, port: tool.port, process: proc, pid: proc.pid,
|
|
46
50
|
startedAt: Date.now(), restarts: [], healthy: false, lastHealthCheck: 0,
|
|
51
|
+
lastUsed: Date.now(), idleTimer: null,
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
proc.stdout.on('data', () => {});
|
|
@@ -57,7 +62,7 @@ function startProcess(tool) {
|
|
|
57
62
|
|
|
58
63
|
proc.on('close', (code) => {
|
|
59
64
|
entry.healthy = false;
|
|
60
|
-
if (shuttingDown) return;
|
|
65
|
+
if (shuttingDown || entry._stopping) return;
|
|
61
66
|
log(tool.id + ' exited code ' + code);
|
|
62
67
|
scheduleRestart(tool, entry.restarts);
|
|
63
68
|
});
|
|
@@ -65,9 +70,29 @@ function startProcess(tool) {
|
|
|
65
70
|
processes.set(tool.id, entry);
|
|
66
71
|
log(tool.id + ' started port ' + tool.port + ' pid ' + proc.pid);
|
|
67
72
|
setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
|
|
73
|
+
resetIdleTimer(tool.id);
|
|
68
74
|
return entry;
|
|
69
75
|
}
|
|
70
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
|
+
|
|
71
96
|
function scheduleRestart(tool, prevRestarts = []) {
|
|
72
97
|
if (shuttingDown) return;
|
|
73
98
|
const now = Date.now();
|
|
@@ -98,16 +123,30 @@ async function checkHealth(toolId) {
|
|
|
98
123
|
entry.lastHealthCheck = Date.now();
|
|
99
124
|
}
|
|
100
125
|
|
|
101
|
-
export async function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
startProcess(tool);
|
|
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;
|
|
110
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)');
|
|
111
150
|
healthTimer = setInterval(() => {
|
|
112
151
|
for (const [id] of processes) checkHealth(id);
|
|
113
152
|
}, HEALTH_INTERVAL_MS);
|
|
@@ -118,6 +157,7 @@ export async function stopAll() {
|
|
|
118
157
|
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
|
119
158
|
const kills = [];
|
|
120
159
|
for (const [id, entry] of processes) {
|
|
160
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
121
161
|
log('stopping ' + id + ' pid ' + entry.pid);
|
|
122
162
|
kills.push(new Promise(resolve => {
|
|
123
163
|
const t = setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} resolve(); }, 5000);
|
|
@@ -131,10 +171,14 @@ export async function stopAll() {
|
|
|
131
171
|
}
|
|
132
172
|
|
|
133
173
|
export function getStatus() {
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
});
|
|
138
182
|
}
|
|
139
183
|
|
|
140
184
|
export function getPort(agentId) {
|
|
@@ -151,17 +195,16 @@ export function getRunningPorts() {
|
|
|
151
195
|
export async function restart(agentId) {
|
|
152
196
|
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
153
197
|
if (!tool) return false;
|
|
154
|
-
|
|
155
|
-
if (entry) { try { entry.process.kill('SIGTERM'); } catch (_) {} processes.delete(agentId); }
|
|
198
|
+
stopTool(agentId);
|
|
156
199
|
startProcess(tool);
|
|
157
200
|
return true;
|
|
158
201
|
}
|
|
159
202
|
|
|
160
203
|
export async function queryModels(agentId) {
|
|
161
|
-
const
|
|
162
|
-
if (!
|
|
204
|
+
const port = await ensureRunning(agentId);
|
|
205
|
+
if (!port) return [];
|
|
163
206
|
try {
|
|
164
|
-
const res = await fetch('http://127.0.0.1:' +
|
|
207
|
+
const res = await fetch('http://127.0.0.1:' + port + '/provider', {
|
|
165
208
|
signal: AbortSignal.timeout(5000), headers: { 'Accept': 'application/json' }
|
|
166
209
|
});
|
|
167
210
|
if (!res.ok) return [];
|
|
@@ -176,4 +219,11 @@ export async function queryModels(agentId) {
|
|
|
176
219
|
} catch (_) { return []; }
|
|
177
220
|
}
|
|
178
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
|
+
|
|
179
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
package/server.js
CHANGED
|
@@ -22,7 +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 } from './lib/acp-manager.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';
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
process.on('uncaughtException', (err, origin) => {
|
|
@@ -433,30 +433,14 @@ async function getModelsForAgent(agentId) {
|
|
|
433
433
|
let models = [];
|
|
434
434
|
if (agentId === 'claude-code') {
|
|
435
435
|
models = [
|
|
436
|
-
{ id: '
|
|
437
|
-
{ id: '
|
|
438
|
-
{ id: '
|
|
436
|
+
{ id: 'haiku', label: 'Haiku' },
|
|
437
|
+
{ id: 'sonnet', label: 'Sonnet' },
|
|
438
|
+
{ id: 'opus', label: 'Opus' }
|
|
439
439
|
];
|
|
440
440
|
} else {
|
|
441
441
|
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
442
442
|
if (agent?.protocol === 'acp') {
|
|
443
|
-
|
|
444
|
-
if (acpPort) {
|
|
445
|
-
try {
|
|
446
|
-
models = await queryACPModels(agentId);
|
|
447
|
-
if (!models.length) {
|
|
448
|
-
const res = await fetch(`http://localhost:${acpPort}/models`, {
|
|
449
|
-
headers: { 'Content-Type': 'application/json' },
|
|
450
|
-
signal: AbortSignal.timeout(3000)
|
|
451
|
-
});
|
|
452
|
-
if (res.ok) {
|
|
453
|
-
const data = await res.json();
|
|
454
|
-
const list = data?.data || data?.models || (Array.isArray(data) ? data : []);
|
|
455
|
-
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);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
} catch (_) {}
|
|
459
|
-
}
|
|
443
|
+
try { models = await queryACPModels(agentId); } catch (_) {}
|
|
460
444
|
}
|
|
461
445
|
}
|
|
462
446
|
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
@@ -3166,6 +3150,7 @@ function createChunkBatcher() {
|
|
|
3166
3150
|
|
|
3167
3151
|
async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, model) {
|
|
3168
3152
|
const startTime = Date.now();
|
|
3153
|
+
touchACP(agentId);
|
|
3169
3154
|
|
|
3170
3155
|
const conv = queries.getConversation(conversationId);
|
|
3171
3156
|
if (!conv) {
|