agentgui 1.0.533 → 1.0.535
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-http-client.js +125 -0
- package/lib/acp-manager.js +20 -89
- package/lib/acp-process-lifecycle.js +65 -0
- package/lib/tool-manager.js +4 -4
- package/package.json +1 -1
- package/server.js +19 -33
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP HTTP Client with comprehensive request/response logging
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function logACPCall(method, url, requestData, responseData, error = null) {
|
|
6
|
+
const timestamp = new Date().toISOString();
|
|
7
|
+
const logEntry = {
|
|
8
|
+
timestamp,
|
|
9
|
+
method,
|
|
10
|
+
url,
|
|
11
|
+
request: requestData,
|
|
12
|
+
response: responseData,
|
|
13
|
+
error: error ? error.message : null
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
console.log('[ACP-HTTP]', JSON.stringify(logEntry, null, 2));
|
|
17
|
+
return logEntry;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function fetchACPProvider(baseUrl, port) {
|
|
21
|
+
const url = baseUrl + ':' + port + '/provider';
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
console.log('[ACP-HTTP] → GET ' + url);
|
|
26
|
+
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: 'GET',
|
|
29
|
+
headers: { 'Accept': 'application/json' },
|
|
30
|
+
signal: AbortSignal.timeout(3000)
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const data = response.ok ? await response.json() : null;
|
|
34
|
+
const duration = Date.now() - startTime;
|
|
35
|
+
|
|
36
|
+
logACPCall('GET', url, {
|
|
37
|
+
headers: { 'Accept': 'application/json' },
|
|
38
|
+
timeout: 3000
|
|
39
|
+
}, {
|
|
40
|
+
status: response.status,
|
|
41
|
+
statusText: response.statusText,
|
|
42
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
43
|
+
body: data,
|
|
44
|
+
duration_ms: duration
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { ok: response.ok, status: response.status, data };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logACPCall('GET', url, { headers: { 'Accept': 'application/json' } }, null, error);
|
|
50
|
+
return { ok: false, status: 0, data: null, error: error.message };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function fetchACPAgents(baseUrl) {
|
|
55
|
+
const endpoint = baseUrl.endsWith('/') ? baseUrl + 'agents/search' : baseUrl + '/agents/search';
|
|
56
|
+
const requestBody = {};
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
console.log('[ACP-HTTP] → POST ' + endpoint);
|
|
61
|
+
console.log('[ACP-HTTP] Request body: ' + JSON.stringify(requestBody));
|
|
62
|
+
|
|
63
|
+
const response = await fetch(endpoint, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'Accept': 'application/json'
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify(requestBody),
|
|
70
|
+
signal: AbortSignal.timeout(5000)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const data = response.ok ? await response.json() : null;
|
|
74
|
+
const duration = Date.now() - startTime;
|
|
75
|
+
|
|
76
|
+
logACPCall('POST', endpoint, {
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
'Accept': 'application/json'
|
|
80
|
+
},
|
|
81
|
+
body: requestBody,
|
|
82
|
+
timeout: 5000
|
|
83
|
+
}, {
|
|
84
|
+
status: response.status,
|
|
85
|
+
statusText: response.statusText,
|
|
86
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
87
|
+
body: data,
|
|
88
|
+
duration_ms: duration
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { ok: response.ok, status: response.status, data };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logACPCall('POST', endpoint, { body: requestBody }, null, error);
|
|
94
|
+
return { ok: false, status: 0, data: null, error: error.message };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function extractCompleteAgentData(agent) {
|
|
99
|
+
return {
|
|
100
|
+
id: agent.agent_id || agent.id,
|
|
101
|
+
name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
|
|
102
|
+
metadata: {
|
|
103
|
+
ref: {
|
|
104
|
+
name: agent.metadata?.ref?.name,
|
|
105
|
+
version: agent.metadata?.ref?.version,
|
|
106
|
+
url: agent.metadata?.ref?.url,
|
|
107
|
+
tags: agent.metadata?.ref?.tags
|
|
108
|
+
},
|
|
109
|
+
description: agent.metadata?.description,
|
|
110
|
+
author: agent.metadata?.author,
|
|
111
|
+
license: agent.metadata?.license
|
|
112
|
+
},
|
|
113
|
+
specs: agent.specs ? {
|
|
114
|
+
capabilities: agent.specs.capabilities,
|
|
115
|
+
input_schema: agent.specs.input_schema || agent.specs.input,
|
|
116
|
+
output_schema: agent.specs.output_schema || agent.specs.output,
|
|
117
|
+
thread_state_schema: agent.specs.thread_state_schema || agent.specs.thread_state,
|
|
118
|
+
config_schema: agent.specs.config_schema || agent.specs.config,
|
|
119
|
+
custom_streaming_update_schema: agent.specs.custom_streaming_update_schema || agent.specs.custom_streaming_update
|
|
120
|
+
} : null,
|
|
121
|
+
custom_data: agent.custom_data,
|
|
122
|
+
icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
|
|
123
|
+
protocol: 'acp'
|
|
124
|
+
};
|
|
125
|
+
}
|
package/lib/acp-manager.js
CHANGED
|
@@ -1,74 +1,35 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
1
|
+
import { startProcess as startProc, scheduleRestart as scheduleRestart, MAX_RESTARTS, RESTART_WINDOW_MS, IDLE_TIMEOUT_MS } from './acp-process-lifecycle.js';
|
|
10
2
|
|
|
11
3
|
const ACP_TOOLS = [
|
|
12
4
|
{ id: 'opencode', cmd: 'opencode', args: ['acp'], port: 18100, npxPkg: 'opencode-ai' },
|
|
13
5
|
{ id: 'kilo', cmd: 'kilo', args: ['acp'], port: 18101, npxPkg: '@kilocode/cli' },
|
|
14
6
|
];
|
|
15
7
|
|
|
16
|
-
const MAX_RESTARTS = 10;
|
|
17
|
-
const RESTART_WINDOW_MS = 300000;
|
|
18
8
|
const HEALTH_INTERVAL_MS = 30000;
|
|
19
9
|
const STARTUP_GRACE_MS = 5000;
|
|
20
|
-
const IDLE_TIMEOUT_MS = 120000;
|
|
21
10
|
const processes = new Map();
|
|
22
11
|
let healthTimer = null;
|
|
23
12
|
let shuttingDown = false;
|
|
24
13
|
|
|
25
14
|
function log(msg) { console.log('[ACP] ' + msg); }
|
|
26
15
|
|
|
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
16
|
function startProcess(tool) {
|
|
35
17
|
if (shuttingDown) return null;
|
|
36
18
|
const existing = processes.get(tool.id);
|
|
37
19
|
if (existing?.process && !existing.process.killed) return existing;
|
|
38
20
|
|
|
39
|
-
const
|
|
40
|
-
|
|
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; });
|
|
21
|
+
const entry = startProc(tool, log);
|
|
22
|
+
if (!entry) return null;
|
|
62
23
|
|
|
63
|
-
|
|
24
|
+
entry.process.on('close', (code) => {
|
|
64
25
|
entry.healthy = false;
|
|
65
26
|
if (shuttingDown || entry._stopping) return;
|
|
66
27
|
log(tool.id + ' exited code ' + code);
|
|
67
|
-
scheduleRestart(tool, entry.restarts);
|
|
28
|
+
scheduleRestart(tool, entry.restarts, log, startProcess, () => shuttingDown);
|
|
68
29
|
});
|
|
69
30
|
|
|
70
31
|
processes.set(tool.id, entry);
|
|
71
|
-
log(tool.id + ' started port ' + tool.port + ' pid ' +
|
|
32
|
+
log(tool.id + ' started port ' + tool.port + ' pid ' + entry.process.pid);
|
|
72
33
|
setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
|
|
73
34
|
resetIdleTimer(tool.id);
|
|
74
35
|
return entry;
|
|
@@ -93,34 +54,19 @@ function stopTool(toolId) {
|
|
|
93
54
|
processes.delete(toolId);
|
|
94
55
|
}
|
|
95
56
|
|
|
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
57
|
async function checkHealth(toolId) {
|
|
115
58
|
const entry = processes.get(toolId);
|
|
116
59
|
if (!entry || shuttingDown) return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
} catch (_) { entry.healthy = false; }
|
|
60
|
+
|
|
61
|
+
const { fetchACPProvider } = await import('./acp-http-client.js');
|
|
62
|
+
const result = await fetchACPProvider('http://127.0.0.1', entry.port);
|
|
63
|
+
|
|
64
|
+
entry.healthy = result.ok;
|
|
123
65
|
entry.lastHealthCheck = Date.now();
|
|
66
|
+
|
|
67
|
+
if (result.data) {
|
|
68
|
+
entry.providerInfo = result.data;
|
|
69
|
+
}
|
|
124
70
|
}
|
|
125
71
|
|
|
126
72
|
export async function ensureRunning(agentId) {
|
|
@@ -177,6 +123,7 @@ export function getStatus() {
|
|
|
177
123
|
id: tool.id, port: tool.port, running: !!e, healthy: e?.healthy || false,
|
|
178
124
|
pid: e?.pid, uptime: e ? Date.now() - e.startedAt : 0,
|
|
179
125
|
restartCount: e?.restarts.length || 0, idleMs: e ? Date.now() - e.lastUsed : 0,
|
|
126
|
+
providerInfo: e?.providerInfo || null,
|
|
180
127
|
};
|
|
181
128
|
});
|
|
182
129
|
}
|
|
@@ -201,33 +148,17 @@ export async function restart(agentId) {
|
|
|
201
148
|
}
|
|
202
149
|
|
|
203
150
|
export async function queryModels(agentId) {
|
|
204
|
-
const port =
|
|
151
|
+
const port = getPort(agentId);
|
|
205
152
|
if (!port) return [];
|
|
206
153
|
try {
|
|
207
|
-
const res = await fetch('http://127.0.0.1:' + port + '/
|
|
208
|
-
signal: AbortSignal.timeout(5000), headers: { 'Accept': 'application/json' }
|
|
209
|
-
});
|
|
154
|
+
const res = await fetch('http://127.0.0.1:' + port + '/models');
|
|
210
155
|
if (!res.ok) return [];
|
|
211
156
|
const data = await res.json();
|
|
212
|
-
|
|
213
|
-
const providers = (data.all || []).filter(p => connected.has(p.id));
|
|
214
|
-
const seen = new Map();
|
|
215
|
-
for (const prov of providers) {
|
|
216
|
-
for (const m of Object.values(prov.models || {})) {
|
|
217
|
-
if (!seen.has(m.id)) {
|
|
218
|
-
seen.set(m.id, { id: m.id, label: m.name || m.id, provider: prov.name || prov.id });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return Array.from(seen.values());
|
|
157
|
+
return data.models || [];
|
|
223
158
|
} catch (_) { return []; }
|
|
224
159
|
}
|
|
225
160
|
|
|
226
161
|
export function isAvailable(agentId) {
|
|
227
162
|
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
228
|
-
|
|
229
|
-
const bin = resolveBinary(tool.cmd);
|
|
230
|
-
return bin !== tool.cmd || fs.existsSync(bin);
|
|
163
|
+
return !!tool;
|
|
231
164
|
}
|
|
232
|
-
|
|
233
|
-
export const ACP_TOOL_CONFIGS = ACP_TOOLS;
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
export const MAX_RESTARTS = 10;
|
|
12
|
+
export const RESTART_WINDOW_MS = 300000;
|
|
13
|
+
export const IDLE_TIMEOUT_MS = 120000;
|
|
14
|
+
|
|
15
|
+
export function resolveBinary(cmd) {
|
|
16
|
+
const ext = isWindows ? '.cmd' : '';
|
|
17
|
+
const localBin = path.join(projectRoot, 'node_modules', '.bin', cmd + ext);
|
|
18
|
+
if (fs.existsSync(localBin)) return localBin;
|
|
19
|
+
return cmd;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function startProcess(tool, log) {
|
|
23
|
+
const bin = resolveBinary(tool.cmd);
|
|
24
|
+
const args = [...tool.args, '--port', String(tool.port)];
|
|
25
|
+
const opts = { stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() };
|
|
26
|
+
if (isWindows) opts.shell = true;
|
|
27
|
+
|
|
28
|
+
let proc;
|
|
29
|
+
try { proc = spawn(bin, args, opts); }
|
|
30
|
+
catch (err) { log(tool.id + ' spawn failed: ' + err.message); return null; }
|
|
31
|
+
|
|
32
|
+
const entry = {
|
|
33
|
+
id: tool.id, port: tool.port, process: proc, pid: proc.pid,
|
|
34
|
+
startedAt: Date.now(), restarts: [], healthy: false, lastHealthCheck: 0,
|
|
35
|
+
lastUsed: Date.now(), idleTimer: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
proc.stdout.on('data', () => {});
|
|
39
|
+
proc.stderr.on('data', (d) => {
|
|
40
|
+
const t = d.toString().trim();
|
|
41
|
+
if (t) log(tool.id + ': ' + t.substring(0, 200));
|
|
42
|
+
});
|
|
43
|
+
proc.stdout.on('error', () => {});
|
|
44
|
+
proc.stderr.on('error', () => {});
|
|
45
|
+
proc.on('error', (err) => { log(tool.id + ' error: ' + err.message); entry.healthy = false; });
|
|
46
|
+
|
|
47
|
+
return entry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function scheduleRestart(tool, prevRestarts, log, startProcessFn, shuttingDown) {
|
|
51
|
+
if (shuttingDown()) return;
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const recent = prevRestarts.filter(t => now - t < RESTART_WINDOW_MS);
|
|
54
|
+
if (recent.length >= MAX_RESTARTS) {
|
|
55
|
+
log(tool.id + ' exceeded restart limit, giving up');
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const delay = Math.min(1000 * Math.pow(2, recent.length), 30000);
|
|
59
|
+
log(tool.id + ' restarting in ' + delay + 'ms');
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
if (shuttingDown()) return;
|
|
62
|
+
const entry = startProcessFn(tool);
|
|
63
|
+
if (entry) entry.restarts = [...recent, Date.now()];
|
|
64
|
+
}, delay);
|
|
65
|
+
}
|
package/lib/tool-manager.js
CHANGED
|
@@ -5,10 +5,10 @@ import path from 'path';
|
|
|
5
5
|
|
|
6
6
|
const isWindows = os.platform() === 'win32';
|
|
7
7
|
const TOOLS = [
|
|
8
|
-
{ id: 'gm-cc', name: 'gm-cc', pkg: '
|
|
9
|
-
{ id: 'gm-oc', name: 'gm-oc', pkg: '
|
|
10
|
-
{ id: 'gm-gc', name: 'gm-gc', pkg: '
|
|
11
|
-
{ id: 'gm-kilo', name: 'gm-kilo', pkg: '
|
|
8
|
+
{ id: 'gm-cc', name: 'gm-cc', pkg: 'gm-cc', pluginId: 'gm-cc' },
|
|
9
|
+
{ id: 'gm-oc', name: 'gm-oc', pkg: 'gm-oc', pluginId: 'gm-oc' },
|
|
10
|
+
{ id: 'gm-gc', name: 'gm-gc', pkg: 'gm-gc', pluginId: 'gm' },
|
|
11
|
+
{ id: 'gm-kilo', name: 'gm-kilo', pkg: 'gm-kilo', pluginId: 'gm-kilo' },
|
|
12
12
|
];
|
|
13
13
|
|
|
14
14
|
const statusCache = new Map();
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -397,41 +397,27 @@ function findCommand(cmd) {
|
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
async function queryACPServerAgents(baseUrl) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (!response.ok) {
|
|
412
|
-
console.error(`Failed to query ACP agents from ${baseUrl}: ${response.status}`);
|
|
413
|
-
return [];
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const data = await response.json();
|
|
417
|
-
if (!data.agents || !Array.isArray(data.agents)) {
|
|
418
|
-
console.error(`Invalid agents response from ${baseUrl}`);
|
|
419
|
-
return [];
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Convert ACP agent format to our internal format
|
|
423
|
-
return data.agents.map(agent => ({
|
|
424
|
-
id: agent.agent_id || agent.id,
|
|
425
|
-
name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
|
|
426
|
-
icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
|
|
427
|
-
path: baseUrl,
|
|
428
|
-
protocol: 'acp',
|
|
429
|
-
description: agent.metadata?.description || '',
|
|
430
|
-
}));
|
|
431
|
-
} catch (error) {
|
|
432
|
-
console.error(`Error querying ACP server ${baseUrl}:`, error.message);
|
|
400
|
+
const { fetchACPAgents, extractCompleteAgentData } = await import('./lib/acp-http-client.js');
|
|
401
|
+
|
|
402
|
+
const result = await fetchACPAgents(baseUrl);
|
|
403
|
+
|
|
404
|
+
if (!result.ok) {
|
|
405
|
+
console.error(`Failed to query ACP agents from ${baseUrl}: ${result.status} ${result.error || ''}`);
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!result.data?.agents || !Array.isArray(result.data.agents)) {
|
|
410
|
+
console.error(`Invalid agents response from ${baseUrl}`);
|
|
433
411
|
return [];
|
|
434
412
|
}
|
|
413
|
+
|
|
414
|
+
return result.data.agents.map(agent => {
|
|
415
|
+
const complete = extractCompleteAgentData(agent);
|
|
416
|
+
return {
|
|
417
|
+
...complete,
|
|
418
|
+
path: baseUrl
|
|
419
|
+
};
|
|
420
|
+
});
|
|
435
421
|
}
|
|
436
422
|
|
|
437
423
|
function discoverAgents() {
|