apteva 0.4.3 → 0.4.5
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/dist/App.y11xqt9m.js +227 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/db.ts +93 -19
- package/src/integrations/agentdojo.ts +350 -0
- package/src/openapi.ts +195 -0
- package/src/providers.ts +78 -7
- package/src/routes/api/agent-utils.ts +638 -0
- package/src/routes/api/agents.ts +743 -0
- package/src/routes/api/helpers.ts +12 -0
- package/src/routes/api/integrations.ts +608 -0
- package/src/routes/api/mcp.ts +377 -0
- package/src/routes/api/meta-agent.ts +145 -0
- package/src/routes/api/projects.ts +95 -0
- package/src/routes/api/providers.ts +269 -0
- package/src/routes/api/skills.ts +538 -0
- package/src/routes/api/system.ts +215 -0
- package/src/routes/api/telemetry.ts +142 -0
- package/src/routes/api/users.ts +148 -0
- package/src/routes/api.ts +32 -3474
- package/src/server.ts +1 -1
- package/src/web/components/api/ApiDocsPage.tsx +259 -0
- package/src/web/components/mcp/IntegrationsPanel.tsx +15 -8
- package/src/web/components/mcp/McpPage.tsx +458 -174
- package/src/web/components/settings/SettingsPage.tsx +275 -36
- package/src/web/components/skills/SkillsPage.tsx +330 -1
- package/src/web/components/tasks/TasksPage.tsx +187 -58
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/hooks/useAgents.ts +9 -0
- package/src/web/types.ts +22 -4
- package/dist/App.mbp9atpm.js +0 -227
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { spawn } from "bun";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { mkdirSync, existsSync, rmSync } from "fs";
|
|
5
|
+
import { agentProcesses, agentsStarting, getBinaryPathForAgent, getBinaryStatus, BIN_DIR, telemetryBroadcaster, type TelemetryEvent } from "../../server";
|
|
6
|
+
import { AgentDB, McpServerDB, SkillDB, TelemetryDB, generateId, getMultiAgentConfig, type Agent, type Project } from "../../db";
|
|
7
|
+
import { ProviderKeys, PROVIDERS, type ProviderId } from "../../providers";
|
|
8
|
+
import { binaryExists } from "../../binary";
|
|
9
|
+
|
|
10
|
+
// Data directory for agent instances (in ~/.apteva/agents/)
|
|
11
|
+
export const AGENTS_DATA_DIR = process.env.DATA_DIR
|
|
12
|
+
? join(process.env.DATA_DIR, "agents")
|
|
13
|
+
: join(homedir(), ".apteva", "agents");
|
|
14
|
+
|
|
15
|
+
// Meta Agent configuration
|
|
16
|
+
export const META_AGENT_ENABLED = process.env.META_AGENT_ENABLED === "true";
|
|
17
|
+
export const META_AGENT_ID = "apteva-assistant";
|
|
18
|
+
|
|
19
|
+
// Update agent status + emit telemetry event + broadcast to SSE
|
|
20
|
+
export function setAgentStatus(agentId: string, status: "running" | "stopped", reason?: string): Agent | null {
|
|
21
|
+
const agent = AgentDB.setStatus(agentId, status);
|
|
22
|
+
const event: TelemetryEvent = {
|
|
23
|
+
id: generateId(),
|
|
24
|
+
agent_id: agentId,
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
category: "system",
|
|
27
|
+
type: status === "running" ? "agent_started" : "agent_stopped",
|
|
28
|
+
level: "info",
|
|
29
|
+
data: { reason: reason || status },
|
|
30
|
+
};
|
|
31
|
+
TelemetryDB.insertBatch(agentId, [event]);
|
|
32
|
+
telemetryBroadcaster.broadcast([event]);
|
|
33
|
+
return agent;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Wait for agent to be healthy (with timeout)
|
|
37
|
+
// Note: /health endpoint is whitelisted in agent, no auth needed
|
|
38
|
+
export async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200): Promise<boolean> {
|
|
39
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(`http://localhost:${port}/health`, {
|
|
42
|
+
signal: AbortSignal.timeout(1000),
|
|
43
|
+
});
|
|
44
|
+
if (res.ok) return true;
|
|
45
|
+
} catch {
|
|
46
|
+
// Not ready yet
|
|
47
|
+
}
|
|
48
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if a port is free by trying to connect
|
|
54
|
+
export async function checkPortFree(port: number): Promise<boolean> {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const net = require("net");
|
|
57
|
+
const server = net.createServer();
|
|
58
|
+
server.once("error", () => {
|
|
59
|
+
resolve(false); // Port in use
|
|
60
|
+
});
|
|
61
|
+
server.once("listening", () => {
|
|
62
|
+
server.close();
|
|
63
|
+
resolve(true); // Port is free
|
|
64
|
+
});
|
|
65
|
+
server.listen(port, "127.0.0.1");
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Make authenticated request to agent
|
|
70
|
+
export async function agentFetch(
|
|
71
|
+
agentId: string,
|
|
72
|
+
port: number,
|
|
73
|
+
endpoint: string,
|
|
74
|
+
options: RequestInit = {}
|
|
75
|
+
): Promise<Response> {
|
|
76
|
+
const apiKey = AgentDB.getApiKey(agentId);
|
|
77
|
+
const headers: Record<string, string> = {
|
|
78
|
+
...(options.headers as Record<string, string> || {}),
|
|
79
|
+
};
|
|
80
|
+
if (apiKey) {
|
|
81
|
+
headers["X-API-Key"] = apiKey;
|
|
82
|
+
}
|
|
83
|
+
return fetch(`http://localhost:${port}${endpoint}`, {
|
|
84
|
+
...options,
|
|
85
|
+
headers,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Build agent config from apteva agent data
|
|
90
|
+
// Note: POST /config expects flat structure WITHOUT "agent" wrapper
|
|
91
|
+
export function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
92
|
+
const features = agent.features;
|
|
93
|
+
|
|
94
|
+
// Get MCP server details for the agent's selected servers
|
|
95
|
+
const mcpServers: Array<{ name: string; type: "http"; url: string; headers: Record<string, string>; enabled: boolean }> = [];
|
|
96
|
+
|
|
97
|
+
// Get skill definitions for the agent's selected skills
|
|
98
|
+
const skillDefinitions: Array<{
|
|
99
|
+
name: string;
|
|
100
|
+
description: string;
|
|
101
|
+
instructions: string;
|
|
102
|
+
icon: string;
|
|
103
|
+
category: string;
|
|
104
|
+
tags: string[];
|
|
105
|
+
tools: string[];
|
|
106
|
+
enabled: boolean;
|
|
107
|
+
}> = [];
|
|
108
|
+
|
|
109
|
+
for (const skillId of agent.skills || []) {
|
|
110
|
+
const skill = SkillDB.findById(skillId);
|
|
111
|
+
if (!skill || !skill.enabled) continue;
|
|
112
|
+
|
|
113
|
+
skillDefinitions.push({
|
|
114
|
+
name: skill.name,
|
|
115
|
+
description: skill.description,
|
|
116
|
+
instructions: skill.content,
|
|
117
|
+
icon: "",
|
|
118
|
+
category: "",
|
|
119
|
+
tags: [],
|
|
120
|
+
tools: skill.allowed_tools || [],
|
|
121
|
+
enabled: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const id of agent.mcp_servers || []) {
|
|
126
|
+
const server = McpServerDB.findById(id);
|
|
127
|
+
if (!server) continue;
|
|
128
|
+
|
|
129
|
+
if (server.type === "http" && server.url) {
|
|
130
|
+
// Remote HTTP server (Composio, Smithery, or custom)
|
|
131
|
+
mcpServers.push({
|
|
132
|
+
name: server.name,
|
|
133
|
+
type: "http",
|
|
134
|
+
url: server.url,
|
|
135
|
+
headers: server.headers || {},
|
|
136
|
+
enabled: true,
|
|
137
|
+
});
|
|
138
|
+
} else if (server.status === "running" && server.port) {
|
|
139
|
+
// Local MCP server (npm, github, custom)
|
|
140
|
+
mcpServers.push({
|
|
141
|
+
name: server.name,
|
|
142
|
+
type: "http",
|
|
143
|
+
url: `http://localhost:${server.port}/mcp`,
|
|
144
|
+
headers: {},
|
|
145
|
+
enabled: true,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
id: agent.id,
|
|
152
|
+
name: agent.name,
|
|
153
|
+
description: agent.system_prompt,
|
|
154
|
+
public_url: `http://localhost:${agent.port}`,
|
|
155
|
+
llm: {
|
|
156
|
+
provider: agent.provider,
|
|
157
|
+
model: agent.model,
|
|
158
|
+
max_tokens: 4000,
|
|
159
|
+
temperature: 0.7,
|
|
160
|
+
system_prompt: agent.system_prompt,
|
|
161
|
+
vision: {
|
|
162
|
+
enabled: features.vision,
|
|
163
|
+
max_images: 20,
|
|
164
|
+
max_image_size: 5242880,
|
|
165
|
+
allowed_types: ["jpeg", "png", "gif", "webp"],
|
|
166
|
+
resize_images: true,
|
|
167
|
+
max_dimension: 1568,
|
|
168
|
+
pdf: {
|
|
169
|
+
enabled: features.vision,
|
|
170
|
+
max_file_size: 33554432,
|
|
171
|
+
max_pages: 100,
|
|
172
|
+
allow_urls: true,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
parallel_tools: {
|
|
176
|
+
enabled: true,
|
|
177
|
+
max_concurrent: 10,
|
|
178
|
+
},
|
|
179
|
+
tools: [], // Clear any old tool whitelist - agent uses all registered tools
|
|
180
|
+
builtin_tools: [
|
|
181
|
+
...(features.builtinTools?.webSearch ? [{ type: "web_search_20250305", name: "web_search" }] : []),
|
|
182
|
+
...(features.builtinTools?.webFetch ? [{ type: "web_fetch_20250910", name: "web_fetch" }] : []),
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
tasks: {
|
|
186
|
+
enabled: features.tasks,
|
|
187
|
+
allow_scheduling: true,
|
|
188
|
+
allow_recurring: true,
|
|
189
|
+
max_tasks: 100,
|
|
190
|
+
auto_execute: false,
|
|
191
|
+
},
|
|
192
|
+
scheduler: {
|
|
193
|
+
enabled: features.tasks,
|
|
194
|
+
interval: "1m",
|
|
195
|
+
max_tasks: 100,
|
|
196
|
+
},
|
|
197
|
+
memory: {
|
|
198
|
+
enabled: features.memory,
|
|
199
|
+
embedding_model: "text-embedding-3-small",
|
|
200
|
+
decision_model: "gpt-4o-mini",
|
|
201
|
+
max_memories_per_query: 20,
|
|
202
|
+
min_importance: 0.3,
|
|
203
|
+
min_similarity: 0.3,
|
|
204
|
+
auto_prune: true,
|
|
205
|
+
max_memories: 10000,
|
|
206
|
+
embedding_provider: "openai",
|
|
207
|
+
auto_extract_memories: features.memory ? true : null,
|
|
208
|
+
auto_ingest_files: true,
|
|
209
|
+
},
|
|
210
|
+
operator: {
|
|
211
|
+
enabled: features.operator,
|
|
212
|
+
virtual_browser: "http://localhost:8098",
|
|
213
|
+
display_width: 1024,
|
|
214
|
+
display_height: 768,
|
|
215
|
+
max_actions_per_turn: 5,
|
|
216
|
+
},
|
|
217
|
+
mcp: {
|
|
218
|
+
enabled: features.mcp,
|
|
219
|
+
base_url: "http://localhost:3000/mcp",
|
|
220
|
+
timeout: "30s",
|
|
221
|
+
retry_count: 3,
|
|
222
|
+
cache_ttl: "15m",
|
|
223
|
+
servers: mcpServers,
|
|
224
|
+
},
|
|
225
|
+
realtime: {
|
|
226
|
+
enabled: features.realtime,
|
|
227
|
+
provider: "openai",
|
|
228
|
+
model: "gpt-4o-realtime-preview",
|
|
229
|
+
voice: "alloy",
|
|
230
|
+
},
|
|
231
|
+
context: {
|
|
232
|
+
max_messages: 30,
|
|
233
|
+
max_tokens: 0,
|
|
234
|
+
keep_images: 5,
|
|
235
|
+
},
|
|
236
|
+
filesystem: {
|
|
237
|
+
enabled: true,
|
|
238
|
+
max_file_size: 10485760,
|
|
239
|
+
max_total_size: 104857600,
|
|
240
|
+
auto_extract: true,
|
|
241
|
+
auto_cleanup: true,
|
|
242
|
+
retention_days: 7,
|
|
243
|
+
},
|
|
244
|
+
telemetry: {
|
|
245
|
+
enabled: true,
|
|
246
|
+
endpoint: `http://localhost:${process.env.PORT || 4280}/api/telemetry`,
|
|
247
|
+
batch_size: 1,
|
|
248
|
+
flush_interval: 1, // Every 1 second
|
|
249
|
+
categories: [], // Empty = all categories
|
|
250
|
+
},
|
|
251
|
+
skills: {
|
|
252
|
+
enabled: skillDefinitions.length > 0,
|
|
253
|
+
definitions: skillDefinitions,
|
|
254
|
+
},
|
|
255
|
+
agents: (() => {
|
|
256
|
+
const multiAgentConfig = getMultiAgentConfig(features, agent.project_id);
|
|
257
|
+
const baseUrl = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 4280}`;
|
|
258
|
+
return {
|
|
259
|
+
enabled: multiAgentConfig.enabled,
|
|
260
|
+
mode: multiAgentConfig.mode || "worker",
|
|
261
|
+
group: multiAgentConfig.group || agent.project_id || undefined,
|
|
262
|
+
// This agent's reachable URL for peer communication
|
|
263
|
+
url: `http://localhost:${agent.port}`,
|
|
264
|
+
// Discovery endpoint to find peer agents in the same group
|
|
265
|
+
discovery_url: `${baseUrl}/api/discovery/agents`,
|
|
266
|
+
};
|
|
267
|
+
})(),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Push config to running agent (with authentication)
|
|
272
|
+
export async function pushConfigToAgent(agentId: string, port: number, config: any): Promise<{ success: boolean; error?: string }> {
|
|
273
|
+
try {
|
|
274
|
+
const res = await agentFetch(agentId, port, "/config", {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: { "Content-Type": "application/json" },
|
|
277
|
+
body: JSON.stringify(config),
|
|
278
|
+
signal: AbortSignal.timeout(5000),
|
|
279
|
+
});
|
|
280
|
+
if (res.ok) {
|
|
281
|
+
return { success: true };
|
|
282
|
+
}
|
|
283
|
+
const data = await res.json().catch(() => ({}));
|
|
284
|
+
return { success: false, error: data.error || `HTTP ${res.status}` };
|
|
285
|
+
} catch (err) {
|
|
286
|
+
return { success: false, error: String(err) };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Push skills to running agent via /skills endpoint (not config)
|
|
291
|
+
export async function pushSkillsToAgent(agentId: string, port: number, skills: Array<{
|
|
292
|
+
name: string;
|
|
293
|
+
description: string;
|
|
294
|
+
instructions: string;
|
|
295
|
+
icon?: string;
|
|
296
|
+
category?: string;
|
|
297
|
+
tags?: string[];
|
|
298
|
+
tools?: string[];
|
|
299
|
+
enabled: boolean;
|
|
300
|
+
}>): Promise<{ success: boolean; error?: string }> {
|
|
301
|
+
if (skills.length === 0) {
|
|
302
|
+
return { success: true };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// Push each skill - try PUT first (update), then POST (create) if not found
|
|
307
|
+
for (const skill of skills) {
|
|
308
|
+
// First try PUT to update existing skill
|
|
309
|
+
let res = await agentFetch(agentId, port, "/skills", {
|
|
310
|
+
method: "PUT",
|
|
311
|
+
headers: { "Content-Type": "application/json" },
|
|
312
|
+
body: JSON.stringify(skill),
|
|
313
|
+
signal: AbortSignal.timeout(5000),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// If skill doesn't exist (404), create it with POST
|
|
317
|
+
if (res.status === 404) {
|
|
318
|
+
res = await agentFetch(agentId, port, "/skills", {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: { "Content-Type": "application/json" },
|
|
321
|
+
body: JSON.stringify(skill),
|
|
322
|
+
signal: AbortSignal.timeout(5000),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!res.ok) {
|
|
327
|
+
const data = await res.json().catch(() => ({}));
|
|
328
|
+
console.error(`[pushSkillsToAgent] Failed to push skill ${skill.name}:`, data.error || res.status);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Enable skills globally via POST /skills/status
|
|
333
|
+
const statusRes = await agentFetch(agentId, port, "/skills/status", {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers: { "Content-Type": "application/json" },
|
|
336
|
+
body: JSON.stringify({ enabled: true }),
|
|
337
|
+
signal: AbortSignal.timeout(5000),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if (!statusRes.ok) {
|
|
341
|
+
const data = await statusRes.json().catch(() => ({}));
|
|
342
|
+
return { success: false, error: data.error || `HTTP ${statusRes.status}` };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(`[pushSkillsToAgent] Pushed ${skills.length} skill(s) to agent`);
|
|
346
|
+
return { success: true };
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return { success: false, error: String(err) };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Exported helper to start an agent process (used by API route and auto-restart)
|
|
353
|
+
export async function startAgentProcess(
|
|
354
|
+
agent: Agent,
|
|
355
|
+
options: { silent?: boolean; cleanData?: boolean } = {}
|
|
356
|
+
): Promise<{ success: boolean; port?: number; error?: string }> {
|
|
357
|
+
const { silent = false, cleanData = false } = options;
|
|
358
|
+
|
|
359
|
+
// Check if binary exists
|
|
360
|
+
if (!binaryExists(BIN_DIR)) {
|
|
361
|
+
return { success: false, error: "Agent binary not available" };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check if already running (process map)
|
|
365
|
+
if (agentProcesses.has(agent.id)) {
|
|
366
|
+
return { success: false, error: "Agent already running" };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check if already being started (race condition prevention)
|
|
370
|
+
if (agentsStarting.has(agent.id)) {
|
|
371
|
+
return { success: false, error: "Agent is already starting" };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Mark as starting
|
|
375
|
+
agentsStarting.add(agent.id);
|
|
376
|
+
|
|
377
|
+
// Get the API key for the agent's provider
|
|
378
|
+
const providerKey = ProviderKeys.getDecrypted(agent.provider);
|
|
379
|
+
if (!providerKey) {
|
|
380
|
+
agentsStarting.delete(agent.id);
|
|
381
|
+
return { success: false, error: `No API key for provider: ${agent.provider}` };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Get provider config for env var name
|
|
385
|
+
const providerConfig = PROVIDERS[agent.provider as ProviderId];
|
|
386
|
+
if (!providerConfig) {
|
|
387
|
+
agentsStarting.delete(agent.id);
|
|
388
|
+
return { success: false, error: `Unknown provider: ${agent.provider}` };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Use agent's permanently assigned port
|
|
392
|
+
const port = agent.port;
|
|
393
|
+
if (!port) {
|
|
394
|
+
agentsStarting.delete(agent.id);
|
|
395
|
+
return { success: false, error: "Agent has no assigned port" };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Get or create API key for the agent
|
|
399
|
+
const agentApiKey = AgentDB.ensureApiKey(agent.id);
|
|
400
|
+
if (!agentApiKey) {
|
|
401
|
+
agentsStarting.delete(agent.id);
|
|
402
|
+
return { success: false, error: "Failed to get/create agent API key" };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
// Check if something is already running on this port (orphaned process)
|
|
407
|
+
try {
|
|
408
|
+
const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(500) });
|
|
409
|
+
if (res.ok) {
|
|
410
|
+
// Something is running - try to shut it down
|
|
411
|
+
if (!silent) {
|
|
412
|
+
console.log(` Port ${port} in use, stopping orphaned process...`);
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(1000) });
|
|
416
|
+
} catch {
|
|
417
|
+
// Shutdown failed - process might not support it
|
|
418
|
+
}
|
|
419
|
+
// Wait longer for port to be released
|
|
420
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// No HTTP response - but port might still be bound by zombie process
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Double-check port is actually free by trying to connect
|
|
427
|
+
const isPortFree = await checkPortFree(port);
|
|
428
|
+
if (!isPortFree) {
|
|
429
|
+
if (!silent) {
|
|
430
|
+
console.log(` Port ${port} still in use, trying to kill process...`);
|
|
431
|
+
}
|
|
432
|
+
// Try to kill process using the port (Linux/Mac)
|
|
433
|
+
try {
|
|
434
|
+
const { execSync } = await import("child_process");
|
|
435
|
+
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { stdio: "ignore" });
|
|
436
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
437
|
+
} catch {
|
|
438
|
+
// Ignore errors
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Final check
|
|
442
|
+
const stillInUse = !(await checkPortFree(port));
|
|
443
|
+
if (stillInUse) {
|
|
444
|
+
agentsStarting.delete(agent.id);
|
|
445
|
+
return { success: false, error: `Port ${port} is still in use` };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Handle data directory
|
|
450
|
+
const agentDataDir = join(AGENTS_DATA_DIR, agent.id);
|
|
451
|
+
if (cleanData && existsSync(agentDataDir)) {
|
|
452
|
+
// Clean old data if requested
|
|
453
|
+
rmSync(agentDataDir, { recursive: true, force: true });
|
|
454
|
+
if (!silent) {
|
|
455
|
+
console.log(` Cleaned old data directory`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (!existsSync(agentDataDir)) {
|
|
459
|
+
mkdirSync(agentDataDir, { recursive: true });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!silent) {
|
|
463
|
+
console.log(`Starting agent ${agent.name} on port ${port}...`);
|
|
464
|
+
console.log(` Provider: ${agent.provider}`);
|
|
465
|
+
console.log(` Data dir: ${agentDataDir}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Build environment with provider key and agent API key
|
|
469
|
+
// CONFIG_PATH ensures each agent has its own config file (prevents sharing)
|
|
470
|
+
const agentConfigPath = join(agentDataDir, "agent-config.json");
|
|
471
|
+
const env: Record<string, string> = {
|
|
472
|
+
...process.env as Record<string, string>,
|
|
473
|
+
PORT: String(port),
|
|
474
|
+
DATA_DIR: agentDataDir,
|
|
475
|
+
CONFIG_PATH: agentConfigPath,
|
|
476
|
+
AGENT_API_KEY: agentApiKey,
|
|
477
|
+
[providerConfig.envVar]: providerKey,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// If memory is enabled and agent doesn't use OpenAI, also pass OpenAI key for embeddings
|
|
481
|
+
if (agent.features.memory && agent.provider !== "openai") {
|
|
482
|
+
const openaiKey = ProviderKeys.getDecrypted("openai");
|
|
483
|
+
if (openaiKey) {
|
|
484
|
+
env.OPENAI_API_KEY = openaiKey;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Get binary path dynamically (allows hot-reload of new binary versions)
|
|
489
|
+
const binaryPath = getBinaryPathForAgent();
|
|
490
|
+
|
|
491
|
+
const proc = spawn({
|
|
492
|
+
cmd: [binaryPath],
|
|
493
|
+
env,
|
|
494
|
+
stdout: "inherit",
|
|
495
|
+
stderr: "inherit",
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Store process with port for tracking
|
|
499
|
+
agentProcesses.set(agent.id, { proc, port });
|
|
500
|
+
|
|
501
|
+
// Detect unexpected process exits (crashes)
|
|
502
|
+
proc.exited.then((code) => {
|
|
503
|
+
if (agentProcesses.has(agent.id)) {
|
|
504
|
+
agentProcesses.delete(agent.id);
|
|
505
|
+
setAgentStatus(agent.id, "stopped", code === 0 ? "exited" : "crashed");
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Wait for agent to be healthy
|
|
510
|
+
if (!silent) {
|
|
511
|
+
console.log(` Waiting for agent to be ready...`);
|
|
512
|
+
}
|
|
513
|
+
const isHealthy = await waitForAgentHealth(port);
|
|
514
|
+
if (!isHealthy) {
|
|
515
|
+
if (!silent) {
|
|
516
|
+
console.error(` Agent failed to start (health check timeout)`);
|
|
517
|
+
}
|
|
518
|
+
proc.kill();
|
|
519
|
+
agentProcesses.delete(agent.id);
|
|
520
|
+
agentsStarting.delete(agent.id);
|
|
521
|
+
return { success: false, error: "Health check timeout" };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Push configuration to the agent
|
|
525
|
+
if (!silent) {
|
|
526
|
+
console.log(` Pushing configuration...`);
|
|
527
|
+
}
|
|
528
|
+
const config = buildAgentConfig(agent, providerKey);
|
|
529
|
+
const configResult = await pushConfigToAgent(agent.id, port, config);
|
|
530
|
+
if (!configResult.success) {
|
|
531
|
+
if (!silent) {
|
|
532
|
+
console.error(` Failed to configure agent: ${configResult.error}`);
|
|
533
|
+
}
|
|
534
|
+
// Agent is running but not configured - still usable but log warning
|
|
535
|
+
} else if (!silent) {
|
|
536
|
+
console.log(` Configuration applied successfully`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Push skills via /skills endpoint (separate from config)
|
|
540
|
+
if (config.skills?.definitions?.length > 0) {
|
|
541
|
+
const skillsResult = await pushSkillsToAgent(agent.id, port, config.skills.definitions);
|
|
542
|
+
if (!skillsResult.success && !silent) {
|
|
543
|
+
console.error(` Failed to push skills: ${skillsResult.error}`);
|
|
544
|
+
} else if (!silent) {
|
|
545
|
+
console.log(` Skills pushed successfully (${config.skills.definitions.length} skills)`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Update status in database + emit telemetry event
|
|
550
|
+
setAgentStatus(agent.id, "running");
|
|
551
|
+
|
|
552
|
+
if (!silent) {
|
|
553
|
+
console.log(`Agent ${agent.name} started on port ${port} (pid: ${proc.pid})`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
agentsStarting.delete(agent.id);
|
|
557
|
+
return { success: true, port };
|
|
558
|
+
} catch (err) {
|
|
559
|
+
agentsStarting.delete(agent.id);
|
|
560
|
+
if (!silent) {
|
|
561
|
+
console.error(`Failed to start agent: ${err}`);
|
|
562
|
+
}
|
|
563
|
+
return { success: false, error: String(err) };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Transform DB agent to API response format (camelCase for frontend compatibility)
|
|
568
|
+
export function toApiAgent(agent: Agent) {
|
|
569
|
+
// Look up MCP server details
|
|
570
|
+
const mcpServerDetails = (agent.mcp_servers || [])
|
|
571
|
+
.map(id => McpServerDB.findById(id))
|
|
572
|
+
.filter((s): s is NonNullable<typeof s> => s !== null)
|
|
573
|
+
.map(s => ({
|
|
574
|
+
id: s.id,
|
|
575
|
+
name: s.name,
|
|
576
|
+
type: s.type,
|
|
577
|
+
status: s.status,
|
|
578
|
+
port: s.port,
|
|
579
|
+
url: s.url, // Include URL for HTTP servers
|
|
580
|
+
}));
|
|
581
|
+
|
|
582
|
+
// Look up skill details
|
|
583
|
+
const skillDetails = (agent.skills || [])
|
|
584
|
+
.map(id => SkillDB.findById(id))
|
|
585
|
+
.filter((s): s is NonNullable<typeof s> => s !== null)
|
|
586
|
+
.map(s => ({
|
|
587
|
+
id: s.id,
|
|
588
|
+
name: s.name,
|
|
589
|
+
description: s.description,
|
|
590
|
+
version: s.version,
|
|
591
|
+
enabled: s.enabled,
|
|
592
|
+
}));
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
id: agent.id,
|
|
596
|
+
name: agent.name,
|
|
597
|
+
model: agent.model,
|
|
598
|
+
provider: agent.provider,
|
|
599
|
+
systemPrompt: agent.system_prompt,
|
|
600
|
+
status: agent.status,
|
|
601
|
+
port: agent.port,
|
|
602
|
+
features: agent.features,
|
|
603
|
+
mcpServers: agent.mcp_servers, // Keep IDs for backwards compatibility
|
|
604
|
+
mcpServerDetails, // Include full details
|
|
605
|
+
skills: agent.skills, // Skill IDs
|
|
606
|
+
skillDetails, // Include full details
|
|
607
|
+
projectId: agent.project_id,
|
|
608
|
+
createdAt: agent.created_at,
|
|
609
|
+
updatedAt: agent.updated_at,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Transform DB project to API response format
|
|
614
|
+
export function toApiProject(project: Project) {
|
|
615
|
+
return {
|
|
616
|
+
id: project.id,
|
|
617
|
+
name: project.name,
|
|
618
|
+
description: project.description,
|
|
619
|
+
color: project.color,
|
|
620
|
+
createdAt: project.created_at,
|
|
621
|
+
updatedAt: project.updated_at,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Helper to fetch from a running agent (with authentication)
|
|
626
|
+
export async function fetchFromAgent(agentId: string, port: number, endpoint: string): Promise<any> {
|
|
627
|
+
try {
|
|
628
|
+
const response = await agentFetch(agentId, port, endpoint, {
|
|
629
|
+
headers: { "Accept": "application/json" },
|
|
630
|
+
});
|
|
631
|
+
if (response.ok) {
|
|
632
|
+
return await response.json();
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
} catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|