agentgui 1.0.438 → 1.0.440

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.
@@ -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 startAll() {
102
- log('starting ACP tools...');
103
- for (const tool of ACP_TOOLS) {
104
- const bin = resolveBinary(tool.cmd);
105
- if (bin === tool.cmd && !fs.existsSync(bin)) {
106
- log(tool.id + ' not found, skipping');
107
- continue;
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 Array.from(processes.values()).map(e => ({
135
- id: e.id, port: e.port, pid: e.pid, healthy: e.healthy,
136
- uptime: Date.now() - e.startedAt, restartCount: e.restarts.length,
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
- const entry = processes.get(agentId);
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 entry = processes.get(agentId);
162
- if (!entry?.healthy) return [];
204
+ const port = await ensureRunning(agentId);
205
+ if (!port) return [];
163
206
  try {
164
- const res = await fetch('http://127.0.0.1:' + entry.port + '/provider', {
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, queryModels } 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' || !agent.acpPort) return { subAgents: [] };
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(agent.acpPort, '/agents/search', {});
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,25 @@ 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: 'claude-haiku', label: 'Haiku' },
139
- { id: 'claude-sonnet', label: 'Sonnet' },
140
- { id: 'claude-opus', label: 'Opus' }
141
- ];
142
- } else {
143
- const agent = discoveredAgents.find(x => x.id === p.id);
144
- if (agent?.protocol === 'acp' && agent.acpPort) {
145
- try {
146
- const data = await acpFetch(agent.acpPort, '/models');
147
- const list = data?.data || data?.models || (Array.isArray(data) ? data : []);
148
- 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);
149
- } catch (_) {}
150
- }
151
- }
152
- if (models.length > 0) modelCache.set(p.id, { models, ts: Date.now() });
153
- return { models };
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
+ models = await queryModels(p.id);
149
+ }
150
+ }
151
+ if (models.length > 0) modelCache.set(p.id, { models, ts: Date.now() });
152
+ return { models };
153
+ });
155
154
 
156
155
  router.handle('agent.search', (p) => db.searchAgents(discoveredAgents, p.query || p));
157
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.438",
3
+ "version": "1.0.440",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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: 'claude-haiku', label: 'Haiku' },
437
- { id: 'claude-sonnet', label: 'Sonnet' },
438
- { id: 'claude-opus', label: 'Opus' }
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
- const acpPort = getACPPort(agentId) || agent.acpPort;
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) {