@agenticmail/enterprise 0.5.136 → 0.5.138
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/chunk-3HF4ADUM.js +16412 -0
- package/dist/chunk-7AD6QYGX.js +898 -0
- package/dist/chunk-DNYMU2RT.js +1912 -0
- package/dist/chunk-EGCPX7IL.js +1917 -0
- package/dist/chunk-FSKYI7ZY.js +2195 -0
- package/dist/chunk-H2JHIF7S.js +2195 -0
- package/dist/chunk-UMVZFTHO.js +898 -0
- package/dist/chunk-X2TDFOHX.js +16413 -0
- package/dist/cli-agent-QWTLGC23.js +631 -0
- package/dist/cli-agent-UQNMEU4D.js +631 -0
- package/dist/cli-serve-GMOK6GCB.js +34 -0
- package/dist/cli-serve-LYLE6GIP.js +34 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +4 -4
- package/dist/lifecycle-FRIBFHCN.js +10 -0
- package/dist/lifecycle-T2MQOTWQ.js +10 -0
- package/dist/routes-ETJCOKHZ.js +6960 -0
- package/dist/routes-WEKIB55E.js +6960 -0
- package/dist/runtime-H4HXYQQE.js +49 -0
- package/dist/runtime-ZR2ZCK5Y.js +49 -0
- package/dist/server-6P7YEIAT.js +12 -0
- package/dist/server-XLKMHCZB.js +12 -0
- package/dist/setup-EXMRCDA2.js +20 -0
- package/dist/setup-ZUTCS6M7.js +20 -0
- package/package.json +1 -1
- package/src/engine/lifecycle.ts +8 -0
- package/src/runtime/llm-client.ts +1 -0
|
@@ -0,0 +1,1912 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withRetry
|
|
3
|
+
} from "./chunk-JLSQOQ5L.js";
|
|
4
|
+
import {
|
|
5
|
+
PermissionEngine
|
|
6
|
+
} from "./chunk-T6FM7KNN.js";
|
|
7
|
+
|
|
8
|
+
// src/engine/agent-config.ts
|
|
9
|
+
var AgentConfigGenerator = class _AgentConfigGenerator {
|
|
10
|
+
/**
|
|
11
|
+
* Generate the complete workspace files for an agent
|
|
12
|
+
*/
|
|
13
|
+
generateWorkspace(config) {
|
|
14
|
+
return {
|
|
15
|
+
"SOUL.md": this.generateSoul(config),
|
|
16
|
+
"USER.md": this.generateUser(config),
|
|
17
|
+
"AGENTS.md": this.generateAgents(config),
|
|
18
|
+
"IDENTITY.md": this.generateIdentity(config),
|
|
19
|
+
"HEARTBEAT.md": this.generateHeartbeat(config),
|
|
20
|
+
"TOOLS.md": this.generateTools(config),
|
|
21
|
+
"MEMORY.md": `# MEMORY.md \u2014 ${config.displayName}'s Long-Term Memory
|
|
22
|
+
|
|
23
|
+
_Created ${(/* @__PURE__ */ new Date()).toISOString()}_
|
|
24
|
+
`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate the gateway config for this agent
|
|
29
|
+
*/
|
|
30
|
+
generateGatewayConfig(config) {
|
|
31
|
+
const channels = {};
|
|
32
|
+
for (const ch of config.channels?.enabled || []) {
|
|
33
|
+
if (!ch.enabled) continue;
|
|
34
|
+
channels[ch.type] = ch.config;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
model: `${config.model.provider}/${config.model.modelId}`,
|
|
38
|
+
thinking: config.model.thinkingLevel,
|
|
39
|
+
temperature: config.model.temperature,
|
|
40
|
+
maxTokens: config.model.maxTokens,
|
|
41
|
+
channels,
|
|
42
|
+
heartbeat: config.heartbeat?.enabled ? {
|
|
43
|
+
intervalMinutes: config.heartbeat.intervalMinutes
|
|
44
|
+
} : void 0,
|
|
45
|
+
workspace: config.workspace?.workingDirectory
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generate a docker-compose.yml for this agent
|
|
50
|
+
*/
|
|
51
|
+
generateDockerCompose(config) {
|
|
52
|
+
const dc = config.deployment.config.docker;
|
|
53
|
+
if (!dc) throw new Error("No Docker config");
|
|
54
|
+
const env = { ...dc.env };
|
|
55
|
+
env["AGENTICMAIL_MODEL"] = `${config.model.provider}/${config.model.modelId}`;
|
|
56
|
+
env["AGENTICMAIL_THINKING"] = config.model.thinkingLevel;
|
|
57
|
+
if (config.email?.enabled && config.email.address) {
|
|
58
|
+
env["AGENTICMAIL_EMAIL"] = config.email.address;
|
|
59
|
+
}
|
|
60
|
+
const envLines = Object.entries(env).map(([k, v]) => ` ${k}: "${v}"`).join("\n");
|
|
61
|
+
const volumes = dc.volumes.map((v) => ` - ${v}`).join("\n");
|
|
62
|
+
const ports = dc.ports.map((p) => ` - "${p}:${p}"`).join("\n");
|
|
63
|
+
return `version: "3.8"
|
|
64
|
+
|
|
65
|
+
services:
|
|
66
|
+
${config.name}:
|
|
67
|
+
image: ${dc.image}:${dc.tag}
|
|
68
|
+
container_name: agenticmail-${config.name}
|
|
69
|
+
restart: ${dc.restart}
|
|
70
|
+
ports:
|
|
71
|
+
${ports}
|
|
72
|
+
volumes:
|
|
73
|
+
${volumes}
|
|
74
|
+
environment:
|
|
75
|
+
${envLines}
|
|
76
|
+
deploy:
|
|
77
|
+
resources:
|
|
78
|
+
limits:
|
|
79
|
+
cpus: "${dc.resources.cpuLimit}"
|
|
80
|
+
memory: ${dc.resources.memoryLimit}
|
|
81
|
+
${dc.network ? ` networks:
|
|
82
|
+
- ${dc.network}
|
|
83
|
+
|
|
84
|
+
networks:
|
|
85
|
+
${dc.network}:
|
|
86
|
+
external: true` : ""}
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Generate a systemd service file for VPS deployment
|
|
91
|
+
*/
|
|
92
|
+
generateSystemdUnit(config) {
|
|
93
|
+
const vps = config.deployment.config.vps;
|
|
94
|
+
if (!vps) throw new Error("No VPS config");
|
|
95
|
+
return `[Unit]
|
|
96
|
+
Description=AgenticMail Agent: ${config.displayName}
|
|
97
|
+
After=network.target
|
|
98
|
+
|
|
99
|
+
[Service]
|
|
100
|
+
Type=simple
|
|
101
|
+
User=${vps.user}
|
|
102
|
+
WorkingDirectory=${vps.installPath}
|
|
103
|
+
ExecStart=/usr/bin/env node ${vps.installPath}/node_modules/.bin/agenticmail-enterprise start
|
|
104
|
+
Restart=always
|
|
105
|
+
RestartSec=10
|
|
106
|
+
Environment=NODE_ENV=production
|
|
107
|
+
Environment=AGENTICMAIL_MODEL=${config.model.provider}/${config.model.modelId}
|
|
108
|
+
Environment=AGENTICMAIL_THINKING=${config.model.thinkingLevel}
|
|
109
|
+
|
|
110
|
+
# Security hardening
|
|
111
|
+
NoNewPrivileges=true
|
|
112
|
+
ProtectSystem=strict
|
|
113
|
+
ProtectHome=read-only
|
|
114
|
+
ReadWritePaths=${vps.installPath}
|
|
115
|
+
PrivateTmp=true
|
|
116
|
+
|
|
117
|
+
[Install]
|
|
118
|
+
WantedBy=multi-user.target
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Generate a deployment script for VPS
|
|
123
|
+
*/
|
|
124
|
+
generateVPSDeployScript(config) {
|
|
125
|
+
const vps = config.deployment.config.vps;
|
|
126
|
+
if (!vps) throw new Error("No VPS config");
|
|
127
|
+
return `#!/bin/bash
|
|
128
|
+
set -euo pipefail
|
|
129
|
+
|
|
130
|
+
# AgenticMail Agent Deployment Script
|
|
131
|
+
# Agent: ${config.displayName}
|
|
132
|
+
# Target: ${vps.host}
|
|
133
|
+
|
|
134
|
+
echo "\u{1F680} Deploying ${config.displayName} to ${vps.host}..."
|
|
135
|
+
|
|
136
|
+
INSTALL_PATH="${vps.installPath}"
|
|
137
|
+
${vps.sudo ? 'SUDO="sudo"' : 'SUDO=""'}
|
|
138
|
+
|
|
139
|
+
# 1. Install Node.js if needed
|
|
140
|
+
if ! command -v node &> /dev/null; then
|
|
141
|
+
echo "\u{1F4E6} Installing Node.js..."
|
|
142
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | $SUDO bash -
|
|
143
|
+
$SUDO apt-get install -y nodejs
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# 2. Create workspace
|
|
147
|
+
mkdir -p "$INSTALL_PATH/workspace"
|
|
148
|
+
cd "$INSTALL_PATH"
|
|
149
|
+
|
|
150
|
+
# 3. Install AgenticMail Enterprise
|
|
151
|
+
echo "\u{1F4E6} Installing packages..."
|
|
152
|
+
npm init -y 2>/dev/null || true
|
|
153
|
+
npm install @agenticmail/enterprise @agenticmail/core agenticmail
|
|
154
|
+
|
|
155
|
+
# 4. Write workspace files
|
|
156
|
+
echo "\u{1F4DD} Writing agent configuration..."
|
|
157
|
+
${Object.entries(this.generateWorkspace(config)).map(
|
|
158
|
+
([file, content]) => `cat > "$INSTALL_PATH/workspace/${file}" << 'WORKSPACE_EOF'
|
|
159
|
+
${content}
|
|
160
|
+
WORKSPACE_EOF`
|
|
161
|
+
).join("\n\n")}
|
|
162
|
+
|
|
163
|
+
# 5. Write gateway config
|
|
164
|
+
cat > "$INSTALL_PATH/config.yaml" << 'CONFIG_EOF'
|
|
165
|
+
${JSON.stringify(this.generateGatewayConfig(config), null, 2)}
|
|
166
|
+
CONFIG_EOF
|
|
167
|
+
|
|
168
|
+
# 6. Install systemd service
|
|
169
|
+
echo "\u2699\uFE0F Installing systemd service..."
|
|
170
|
+
$SUDO tee /etc/systemd/system/agenticmail-${config.name}.service > /dev/null << 'SERVICE_EOF'
|
|
171
|
+
${this.generateSystemdUnit(config)}
|
|
172
|
+
SERVICE_EOF
|
|
173
|
+
|
|
174
|
+
$SUDO systemctl daemon-reload
|
|
175
|
+
$SUDO systemctl enable agenticmail-${config.name}
|
|
176
|
+
$SUDO systemctl start agenticmail-${config.name}
|
|
177
|
+
|
|
178
|
+
echo "\u2705 ${config.displayName} deployed and running!"
|
|
179
|
+
echo " Status: systemctl status agenticmail-${config.name}"
|
|
180
|
+
echo " Logs: journalctl -u agenticmail-${config.name} -f"
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
// ─── Private Generators ─────────────────────────────
|
|
184
|
+
generateSoul(config) {
|
|
185
|
+
const id = config.identity;
|
|
186
|
+
if (id.personality && id.personality.length > 200) {
|
|
187
|
+
const personaBlock = this.buildPersonaBlock(config);
|
|
188
|
+
return personaBlock ? id.personality + "\n\n" + personaBlock : id.personality;
|
|
189
|
+
}
|
|
190
|
+
const toneMap = {
|
|
191
|
+
formal: "Be professional and precise. Use proper grammar. Avoid slang or casual language.",
|
|
192
|
+
casual: "Be relaxed and conversational. Use contractions. Feel free to be informal.",
|
|
193
|
+
professional: "Be competent and clear. Direct communication without being stiff.",
|
|
194
|
+
friendly: "Be warm and approachable. Show genuine interest. Use a positive tone.",
|
|
195
|
+
custom: id.customTone || ""
|
|
196
|
+
};
|
|
197
|
+
const personaSection = this.buildPersonaBlock(config);
|
|
198
|
+
return `# SOUL.md \u2014 Who You Are
|
|
199
|
+
|
|
200
|
+
## Role
|
|
201
|
+
You are **${config.displayName}**, a ${id.role}.
|
|
202
|
+
|
|
203
|
+
## Communication Style
|
|
204
|
+
${toneMap[id.tone] || toneMap["professional"]}
|
|
205
|
+
|
|
206
|
+
## Language
|
|
207
|
+
Primary language: ${id.language}
|
|
208
|
+
${personaSection ? "\n" + personaSection + "\n" : ""}
|
|
209
|
+
## Core Principles
|
|
210
|
+
- Be genuinely helpful, not performatively helpful
|
|
211
|
+
- Be resourceful \u2014 try to figure things out before asking
|
|
212
|
+
- Earn trust through competence
|
|
213
|
+
- Keep private information private
|
|
214
|
+
|
|
215
|
+
## Boundaries
|
|
216
|
+
- Never share confidential company information
|
|
217
|
+
- Ask before taking irreversible actions
|
|
218
|
+
- Stay within your assigned role and permissions
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
/** Calculate current age from a date-of-birth string */
|
|
222
|
+
static deriveAge(dateOfBirth) {
|
|
223
|
+
const dob = new Date(dateOfBirth);
|
|
224
|
+
const today = /* @__PURE__ */ new Date();
|
|
225
|
+
let age = today.getFullYear() - dob.getFullYear();
|
|
226
|
+
const monthDiff = today.getMonth() - dob.getMonth();
|
|
227
|
+
if (monthDiff < 0 || monthDiff === 0 && today.getDate() < dob.getDate()) age--;
|
|
228
|
+
return age;
|
|
229
|
+
}
|
|
230
|
+
buildPersonaBlock(config) {
|
|
231
|
+
const id = config.identity;
|
|
232
|
+
const lines = [];
|
|
233
|
+
const liveAge = id.dateOfBirth ? _AgentConfigGenerator.deriveAge(id.dateOfBirth) : id.age;
|
|
234
|
+
if (id.gender || liveAge || id.ageRange || id.maritalStatus || id.culturalBackground || id.traits) {
|
|
235
|
+
lines.push("## Persona");
|
|
236
|
+
lines.push(`Your name is **${config.displayName}**. This is your real identity \u2014 introduce yourself by this name and respond to it naturally. You embody the following personal characteristics. These are core to who you are \u2014 reflect them naturally in your communication style, references, perspectives, and behavior.
|
|
237
|
+
`);
|
|
238
|
+
}
|
|
239
|
+
if (id.gender) {
|
|
240
|
+
const genderMap = {
|
|
241
|
+
male: "You present as male. Use he/him pronouns when referring to yourself in the third person.",
|
|
242
|
+
female: "You present as female. Use she/her pronouns when referring to yourself in the third person.",
|
|
243
|
+
"non-binary": "You present as non-binary. Use they/them pronouns when referring to yourself in the third person."
|
|
244
|
+
};
|
|
245
|
+
if (genderMap[id.gender]) lines.push(genderMap[id.gender]);
|
|
246
|
+
}
|
|
247
|
+
if (id.dateOfBirth) {
|
|
248
|
+
const dob = new Date(id.dateOfBirth);
|
|
249
|
+
const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
250
|
+
lines.push(`You were born on ${monthNames[dob.getMonth()]} ${dob.getDate()}, ${dob.getFullYear()}. You are currently ${liveAge} years old. This is your real birthday and age \u2014 let it naturally inform your frame of reference, cultural touchstones, energy level, and life experience.`);
|
|
251
|
+
} else if (liveAge) {
|
|
252
|
+
lines.push(`You are ${liveAge} years old. This is your actual age \u2014 let it naturally inform your frame of reference, cultural touchstones, energy level, and life experience that you draw from.`);
|
|
253
|
+
} else if (id.ageRange) {
|
|
254
|
+
const ageMap = {
|
|
255
|
+
young: "You are a young professional in your 20s-30s. You bring fresh perspectives, are comfortable with modern technology and trends, and communicate with energy and adaptability. Your references and examples tend to be contemporary.",
|
|
256
|
+
"mid-career": "You are a mid-career professional in your 30s-40s. You balance youthful energy with substantial experience. You have seen enough to know what works, and you bring both confidence and openness to your interactions.",
|
|
257
|
+
senior: "You are a senior veteran professional in your 50s+. You draw on decades of experience and wisdom. You communicate with gravitas, patience, and deep institutional knowledge. You mentor naturally and reference time-tested principles."
|
|
258
|
+
};
|
|
259
|
+
if (ageMap[id.ageRange]) lines.push(ageMap[id.ageRange]);
|
|
260
|
+
}
|
|
261
|
+
if (id.maritalStatus) {
|
|
262
|
+
const maritalMap = {
|
|
263
|
+
single: "You are single. This shapes your availability mindset \u2014 you are flexible with your time and can relate to independent lifestyles.",
|
|
264
|
+
married: "You are married. You understand partnership, compromise, and family-oriented priorities. You naturally reference teamwork and shared responsibility.",
|
|
265
|
+
divorced: "You have been through a divorce. This gives you resilience, empathy for difficult transitions, and a pragmatic outlook on change.",
|
|
266
|
+
widowed: "You are widowed. You carry deep empathy, emotional maturity, and an appreciation for what matters most.",
|
|
267
|
+
"in-a-relationship": "You are in a relationship. You bring warmth and relational awareness to your interactions."
|
|
268
|
+
};
|
|
269
|
+
if (maritalMap[id.maritalStatus]) lines.push(maritalMap[id.maritalStatus]);
|
|
270
|
+
}
|
|
271
|
+
if (id.culturalBackground) {
|
|
272
|
+
const cultureMap = {
|
|
273
|
+
"north-american": "Your cultural background is North American. You communicate directly, value efficiency and individual initiative, use informal greetings, and are comfortable with straightforward feedback.",
|
|
274
|
+
"british-european": "Your cultural background is British/European. You communicate with polite understatement, appreciate structured formality, use measured and precise language, and value diplomatic phrasing.",
|
|
275
|
+
"latin-american": "Your cultural background is Latin American. You communicate with warmth, prioritize building personal relationships before business, are expressive and personable, and value hospitality in interactions.",
|
|
276
|
+
"middle-eastern": "Your cultural background is Middle Eastern. You communicate with respect and hospitality, are context-aware in formality, value honor and trust-building, and show generous courtesy in your interactions.",
|
|
277
|
+
"east-asian": "Your cultural background is East Asian. You communicate with indirect harmony, show respect for hierarchy and seniority, exercise thoughtful precision in your words, and value group consensus over individual assertion.",
|
|
278
|
+
"south-asian": "Your cultural background is South Asian. You communicate with adaptable formality, are relationship-aware and respectful of elders and authority, blend traditional values with modern pragmatism, and show warmth through attentiveness.",
|
|
279
|
+
"southeast-asian": "Your cultural background is Southeast Asian. You communicate gently and diplomatically, seek consensus in group settings, show deference and politeness, and value harmony over confrontation.",
|
|
280
|
+
"african": "Your cultural background is African. You communicate with community orientation, warmth, and storytelling richness. You value collective wisdom, show respect for elders and tradition, and bring vibrant energy to interactions.",
|
|
281
|
+
"caribbean": "Your cultural background is Caribbean. You communicate with friendly warmth, approachable energy, and resilient optimism. You value community, bring vibrant personality to interactions, and balance professionalism with genuine connection.",
|
|
282
|
+
"australian-pacific": "Your cultural background is Australian/Pacific. You communicate casually and straightforwardly, value egalitarianism, use humor naturally, and keep interactions grounded and unpretentious."
|
|
283
|
+
};
|
|
284
|
+
if (cultureMap[id.culturalBackground]) lines.push(cultureMap[id.culturalBackground]);
|
|
285
|
+
}
|
|
286
|
+
if (id.traits) {
|
|
287
|
+
const traitLines = [];
|
|
288
|
+
if (id.traits.communication === "direct") traitLines.push("You are direct and straightforward \u2014 you say what you mean clearly without excessive hedging.");
|
|
289
|
+
if (id.traits.communication === "diplomatic") traitLines.push("You are diplomatic \u2014 you frame feedback constructively, soften disagreements, and prioritize preserving relationships.");
|
|
290
|
+
if (id.traits.detail === "big-picture") traitLines.push("You focus on the big picture \u2014 you lead with strategy, outcomes, and high-level thinking before diving into specifics.");
|
|
291
|
+
if (id.traits.detail === "detail-oriented") traitLines.push("You are detail-oriented \u2014 you are thorough, precise, and ensure nothing falls through the cracks.");
|
|
292
|
+
if (id.traits.energy === "enthusiastic") traitLines.push("You are enthusiastic \u2014 you bring visible energy, optimism, and momentum to every interaction.");
|
|
293
|
+
if (id.traits.energy === "calm") traitLines.push("You are calm and measured \u2014 you project steadiness, think before responding, and bring a grounding presence.");
|
|
294
|
+
if (id.traits.humor === "witty") traitLines.push("You have a witty sense of humor \u2014 you use clever wordplay, quick observations, and well-timed remarks to keep interactions engaging.");
|
|
295
|
+
if (id.traits.humor === "dry") traitLines.push("You have a dry sense of humor \u2014 you deliver deadpan observations and understated irony that reward attentive listeners.");
|
|
296
|
+
if (id.traits.humor === "warm") traitLines.push("You have a warm sense of humor \u2014 you use gentle, inclusive humor that puts people at ease and builds rapport.");
|
|
297
|
+
if (id.traits.humor === "none") traitLines.push("You keep things strictly professional \u2014 you avoid humor and focus entirely on substance and clarity.");
|
|
298
|
+
if (id.traits.formality === "formal") traitLines.push("You maintain a formal communication style \u2014 you use proper titles, structured language, and professional conventions.");
|
|
299
|
+
if (id.traits.formality === "casual") traitLines.push("You are casual and approachable \u2014 you use conversational language, contractions, and a friendly, relaxed tone.");
|
|
300
|
+
if (id.traits.formality === "adaptive") traitLines.push("You adapt your formality to context \u2014 you match the tone of whoever you are speaking with, formal when needed, relaxed when appropriate.");
|
|
301
|
+
if (id.traits.empathy === "high") traitLines.push("You are highly empathetic \u2014 you actively acknowledge emotions, validate feelings, and prioritize emotional connection alongside the task at hand.");
|
|
302
|
+
if (id.traits.empathy === "moderate") traitLines.push("You show moderate empathy \u2014 you are aware of and responsive to emotions without letting them overshadow practical outcomes.");
|
|
303
|
+
if (id.traits.empathy === "reserved") traitLines.push("You are emotionally reserved \u2014 you focus on facts and logic, offering support through competence rather than emotional expression.");
|
|
304
|
+
if (id.traits.patience === "patient") traitLines.push("You are exceptionally patient \u2014 you take time to explain, repeat when needed, and never rush or show frustration.");
|
|
305
|
+
if (id.traits.patience === "efficient") traitLines.push("You prioritize efficiency \u2014 you get to the point quickly, value brevity, and keep interactions focused and productive.");
|
|
306
|
+
if (id.traits.creativity === "creative") traitLines.push("You are creative and inventive \u2014 you suggest novel approaches, think outside the box, and bring fresh perspectives to problems.");
|
|
307
|
+
if (id.traits.creativity === "conventional") traitLines.push("You favor proven approaches \u2014 you rely on established best practices, standard methods, and time-tested solutions.");
|
|
308
|
+
if (traitLines.length) lines.push(traitLines.join(" "));
|
|
309
|
+
}
|
|
310
|
+
return lines.length > 1 ? lines.join("\n") : "";
|
|
311
|
+
}
|
|
312
|
+
generateUser(config) {
|
|
313
|
+
return config.context?.userInfo || `# USER.md \u2014 About Your Organization
|
|
314
|
+
|
|
315
|
+
_Configure this from the admin dashboard._
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
generateAgents(config) {
|
|
319
|
+
const customInstructions = config.context?.customInstructions || "";
|
|
320
|
+
return `# AGENTS.md \u2014 Your Workspace
|
|
321
|
+
|
|
322
|
+
## Every Session
|
|
323
|
+
1. Read SOUL.md \u2014 this is who you are
|
|
324
|
+
2. Read USER.md \u2014 this is who you're helping
|
|
325
|
+
3. Check memory/ for recent context
|
|
326
|
+
|
|
327
|
+
## Memory
|
|
328
|
+
- Daily notes: memory/YYYY-MM-DD.md
|
|
329
|
+
- Long-term: MEMORY.md
|
|
330
|
+
|
|
331
|
+
## Safety
|
|
332
|
+
- Don't exfiltrate private data
|
|
333
|
+
- Don't run destructive commands without asking
|
|
334
|
+
- When in doubt, ask
|
|
335
|
+
|
|
336
|
+
${customInstructions}
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
generateIdentity(config) {
|
|
340
|
+
const id = config.identity;
|
|
341
|
+
const lines = [
|
|
342
|
+
"# IDENTITY.md\n",
|
|
343
|
+
`- **Name:** ${config.displayName}`,
|
|
344
|
+
`- **Role:** ${id.role}`,
|
|
345
|
+
`- **Tone:** ${id.tone}`,
|
|
346
|
+
`- **Language:** ${id.language || "en"}`
|
|
347
|
+
];
|
|
348
|
+
if (id.gender) lines.push(`- **Gender:** ${id.gender}`);
|
|
349
|
+
if (id.dateOfBirth) {
|
|
350
|
+
const age = _AgentConfigGenerator.deriveAge(id.dateOfBirth);
|
|
351
|
+
lines.push(`- **Date of Birth:** ${id.dateOfBirth}`);
|
|
352
|
+
lines.push(`- **Age:** ${age}`);
|
|
353
|
+
} else if (id.age) {
|
|
354
|
+
lines.push(`- **Age:** ${id.age}`);
|
|
355
|
+
}
|
|
356
|
+
if (id.ageRange) lines.push(`- **Age Range:** ${{ young: "Young Professional (20s-30s)", "mid-career": "Mid-Career (30s-40s)", senior: "Senior / Veteran (50s+)" }[id.ageRange] || id.ageRange}`);
|
|
357
|
+
if (id.maritalStatus) lines.push(`- **Marital Status:** ${id.maritalStatus.replace(/-/g, " ")}`);
|
|
358
|
+
if (id.culturalBackground) lines.push(`- **Cultural Background:** ${id.culturalBackground.replace(/-/g, " ")}`);
|
|
359
|
+
if (id.traits) {
|
|
360
|
+
if (id.traits.communication) lines.push(`- **Communication Style:** ${id.traits.communication}`);
|
|
361
|
+
if (id.traits.detail) lines.push(`- **Focus:** ${id.traits.detail}`);
|
|
362
|
+
if (id.traits.energy) lines.push(`- **Energy:** ${id.traits.energy}`);
|
|
363
|
+
if (id.traits.humor) lines.push(`- **Humor:** ${id.traits.humor}`);
|
|
364
|
+
if (id.traits.formality) lines.push(`- **Formality:** ${id.traits.formality}`);
|
|
365
|
+
if (id.traits.empathy) lines.push(`- **Empathy:** ${id.traits.empathy}`);
|
|
366
|
+
if (id.traits.patience) lines.push(`- **Patience:** ${id.traits.patience}`);
|
|
367
|
+
if (id.traits.creativity) lines.push(`- **Creativity:** ${id.traits.creativity}`);
|
|
368
|
+
}
|
|
369
|
+
return lines.join("\n") + "\n";
|
|
370
|
+
}
|
|
371
|
+
generateHeartbeat(config) {
|
|
372
|
+
if (!config.heartbeat?.enabled) return "# HEARTBEAT.md\n# Heartbeat disabled\n";
|
|
373
|
+
const checks = (config.heartbeat.checks || []).map((c) => `- Check ${c}`).join("\n");
|
|
374
|
+
return `# HEARTBEAT.md
|
|
375
|
+
|
|
376
|
+
## Periodic Checks
|
|
377
|
+
${checks}
|
|
378
|
+
|
|
379
|
+
## Schedule
|
|
380
|
+
Check every ${config.heartbeat.intervalMinutes} minutes during active hours.
|
|
381
|
+
`;
|
|
382
|
+
}
|
|
383
|
+
generateTools(config) {
|
|
384
|
+
return `# TOOLS.md \u2014 Local Notes
|
|
385
|
+
|
|
386
|
+
_Add environment-specific notes here (camera names, SSH hosts, etc.)_
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/engine/deployer.ts
|
|
392
|
+
var DeploymentEngine = class {
|
|
393
|
+
configGen = new AgentConfigGenerator();
|
|
394
|
+
deployments = /* @__PURE__ */ new Map();
|
|
395
|
+
liveStatus = /* @__PURE__ */ new Map();
|
|
396
|
+
/**
|
|
397
|
+
* Deploy an agent to its configured target
|
|
398
|
+
*/
|
|
399
|
+
async deploy(config, onEvent) {
|
|
400
|
+
const events = [];
|
|
401
|
+
const emit = (phase, status, message, details) => {
|
|
402
|
+
const event = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), phase, status, message, details };
|
|
403
|
+
events.push(event);
|
|
404
|
+
onEvent?.(event);
|
|
405
|
+
};
|
|
406
|
+
try {
|
|
407
|
+
emit("validate", "started", "Validating agent configuration...");
|
|
408
|
+
this.validateConfig(config);
|
|
409
|
+
emit("validate", "completed", "Configuration valid");
|
|
410
|
+
let result;
|
|
411
|
+
switch (config.deployment.target) {
|
|
412
|
+
case "docker":
|
|
413
|
+
result = await this.deployDocker(config, emit);
|
|
414
|
+
break;
|
|
415
|
+
case "vps":
|
|
416
|
+
result = await this.deployVPS(config, emit);
|
|
417
|
+
break;
|
|
418
|
+
case "fly":
|
|
419
|
+
result = await this.deployFly(config, emit);
|
|
420
|
+
break;
|
|
421
|
+
case "railway":
|
|
422
|
+
result = await this.deployRailway(config, emit);
|
|
423
|
+
break;
|
|
424
|
+
case "local":
|
|
425
|
+
result = await this.deployLocal(config, emit);
|
|
426
|
+
break;
|
|
427
|
+
default:
|
|
428
|
+
throw new Error(`Unsupported deployment target: ${config.deployment.target}`);
|
|
429
|
+
}
|
|
430
|
+
result.events = events;
|
|
431
|
+
this.deployments.set(config.id, result);
|
|
432
|
+
return result;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
emit("complete", "failed", `Deployment failed: ${error.message}`);
|
|
435
|
+
const result = { success: false, events, error: error.message };
|
|
436
|
+
this.deployments.set(config.id, result);
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Stop a running agent
|
|
442
|
+
*/
|
|
443
|
+
async stop(config) {
|
|
444
|
+
switch (config.deployment.target) {
|
|
445
|
+
case "docker":
|
|
446
|
+
return this.execCommand(`docker stop agenticmail-${config.name} && docker rm agenticmail-${config.name}`);
|
|
447
|
+
case "vps":
|
|
448
|
+
return this.execSSH(config, `sudo systemctl stop agenticmail-${config.name}`);
|
|
449
|
+
case "fly":
|
|
450
|
+
return this.flyMachineAction(config, "stop");
|
|
451
|
+
case "local":
|
|
452
|
+
return { success: true, message: "Local agent stopped" };
|
|
453
|
+
default:
|
|
454
|
+
return { success: false, message: `Cannot stop: unsupported target ${config.deployment.target}` };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Restart a running agent
|
|
459
|
+
*/
|
|
460
|
+
async restart(config) {
|
|
461
|
+
switch (config.deployment.target) {
|
|
462
|
+
case "docker":
|
|
463
|
+
return this.execCommand(`docker restart agenticmail-${config.name}`);
|
|
464
|
+
case "vps":
|
|
465
|
+
return this.execSSH(config, `sudo systemctl restart agenticmail-${config.name}`);
|
|
466
|
+
case "fly":
|
|
467
|
+
return this.flyMachineAction(config, "restart");
|
|
468
|
+
case "local":
|
|
469
|
+
return { success: true, message: "Local agent restarted" };
|
|
470
|
+
default:
|
|
471
|
+
return { success: false, message: `Cannot restart: unsupported target ${config.deployment.target}` };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get live status of a deployed agent
|
|
476
|
+
*/
|
|
477
|
+
async getStatus(config) {
|
|
478
|
+
const base = {
|
|
479
|
+
agentId: config.id,
|
|
480
|
+
name: config.displayName,
|
|
481
|
+
status: "not-deployed",
|
|
482
|
+
healthStatus: "unknown"
|
|
483
|
+
};
|
|
484
|
+
try {
|
|
485
|
+
switch (config.deployment.target) {
|
|
486
|
+
case "docker":
|
|
487
|
+
return await this.getDockerStatus(config, base);
|
|
488
|
+
case "vps":
|
|
489
|
+
return await this.getVPSStatus(config, base);
|
|
490
|
+
case "fly":
|
|
491
|
+
return await this.getCloudStatus(config, base);
|
|
492
|
+
case "local":
|
|
493
|
+
return { ...base, status: "running", healthStatus: "healthy", uptime: Math.floor((Date.now() - new Date(config.deployment?.config?.deployedAt || Date.now()).getTime()) / 1e3) };
|
|
494
|
+
default:
|
|
495
|
+
return base;
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
return { ...base, status: "error", healthStatus: "unhealthy" };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Stream logs from a deployed agent
|
|
503
|
+
*/
|
|
504
|
+
async getLogs(config, lines = 100) {
|
|
505
|
+
switch (config.deployment.target) {
|
|
506
|
+
case "docker":
|
|
507
|
+
return (await this.execCommand(`docker logs --tail ${lines} agenticmail-${config.name}`)).message;
|
|
508
|
+
case "vps":
|
|
509
|
+
return (await this.execSSH(config, `journalctl -u agenticmail-${config.name} --no-pager -n ${lines}`)).message;
|
|
510
|
+
case "fly":
|
|
511
|
+
return `Logs available at: https://fly.io/apps/${config.deployment.config.cloud?.appName || "unknown"}/monitoring`;
|
|
512
|
+
default:
|
|
513
|
+
return "Log streaming not supported for this target";
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Update a deployed agent's configuration without full redeployment
|
|
518
|
+
*/
|
|
519
|
+
async updateConfig(config) {
|
|
520
|
+
const workspace = this.configGen.generateWorkspace(config);
|
|
521
|
+
const gatewayConfig = this.configGen.generateGatewayConfig(config);
|
|
522
|
+
switch (config.deployment.target) {
|
|
523
|
+
case "docker": {
|
|
524
|
+
for (const [file, content] of Object.entries(workspace)) {
|
|
525
|
+
const escaped = content.replace(/'/g, "'\\''");
|
|
526
|
+
await this.execCommand(`docker exec agenticmail-${config.name} sh -c 'echo "${Buffer.from(content).toString("base64")}" | base64 -d > /workspace/${file}'`);
|
|
527
|
+
}
|
|
528
|
+
await this.execCommand(`docker exec agenticmail-${config.name} agenticmail-enterprise restart`);
|
|
529
|
+
return { success: true, message: "Configuration updated and gateway restarted" };
|
|
530
|
+
}
|
|
531
|
+
case "vps": {
|
|
532
|
+
const vps = config.deployment.config.vps;
|
|
533
|
+
for (const [file, content] of Object.entries(workspace)) {
|
|
534
|
+
await this.execSSH(config, `cat > ${vps.installPath}/workspace/${file} << 'EOF'
|
|
535
|
+
${content}
|
|
536
|
+
EOF`);
|
|
537
|
+
}
|
|
538
|
+
await this.execSSH(config, `sudo systemctl restart agenticmail-${config.name}`);
|
|
539
|
+
return { success: true, message: "Configuration updated and service restarted" };
|
|
540
|
+
}
|
|
541
|
+
default:
|
|
542
|
+
return { success: false, message: "Hot config update not supported for this target" };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// ─── Docker Deployment ────────────────────────────────
|
|
546
|
+
async deployDocker(config, emit) {
|
|
547
|
+
const dc = config.deployment.config.docker;
|
|
548
|
+
if (!dc) throw new Error("Docker config missing");
|
|
549
|
+
emit("provision", "started", "Generating Docker configuration...");
|
|
550
|
+
const compose = this.configGen.generateDockerCompose(config);
|
|
551
|
+
emit("provision", "completed", "Docker Compose generated");
|
|
552
|
+
emit("configure", "started", "Generating agent workspace...");
|
|
553
|
+
const workspace = this.configGen.generateWorkspace(config);
|
|
554
|
+
emit("configure", "completed", `Generated ${Object.keys(workspace).length} workspace files`);
|
|
555
|
+
emit("install", "started", `Pulling image ${dc.image}:${dc.tag}...`);
|
|
556
|
+
await this.execCommand(`docker pull ${dc.image}:${dc.tag}`);
|
|
557
|
+
emit("install", "completed", "Image pulled");
|
|
558
|
+
emit("start", "started", "Starting container...");
|
|
559
|
+
const envArgs = Object.entries(dc.env).map(([k, v]) => `-e ${k}="${v}"`).join(" ");
|
|
560
|
+
const volumeArgs = dc.volumes.map((v) => `-v ${v}`).join(" ");
|
|
561
|
+
const portArgs = dc.ports.map((p) => `-p ${p}:${p}`).join(" ");
|
|
562
|
+
const runCmd = `docker run -d --name agenticmail-${config.name} --restart ${dc.restart} ${portArgs} ${volumeArgs} ${envArgs} ${dc.resources ? `--cpus="${dc.resources.cpuLimit}" --memory="${dc.resources.memoryLimit}"` : ""} ${dc.image}:${dc.tag}`;
|
|
563
|
+
const runResult = await this.execCommand(runCmd);
|
|
564
|
+
if (!runResult.success) {
|
|
565
|
+
throw new Error(`Container failed to start: ${runResult.message}`);
|
|
566
|
+
}
|
|
567
|
+
const containerId = runResult.message.trim().substring(0, 12);
|
|
568
|
+
emit("start", "completed", `Container ${containerId} running`);
|
|
569
|
+
emit("upload", "started", "Writing workspace files...");
|
|
570
|
+
for (const [file, content] of Object.entries(workspace)) {
|
|
571
|
+
await this.execCommand(`docker exec agenticmail-${config.name} sh -c 'echo "${Buffer.from(content).toString("base64")}" | base64 -d > /workspace/${file}'`);
|
|
572
|
+
}
|
|
573
|
+
emit("upload", "completed", "Workspace configured");
|
|
574
|
+
emit("healthcheck", "started", "Checking agent health...");
|
|
575
|
+
let healthy = false;
|
|
576
|
+
for (let i = 0; i < 10; i++) {
|
|
577
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
578
|
+
const check = await this.execCommand(`docker exec agenticmail-${config.name} agenticmail-enterprise status 2>/dev/null || echo "not ready"`);
|
|
579
|
+
if (check.success && !check.message.includes("not ready")) {
|
|
580
|
+
healthy = true;
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (healthy) {
|
|
585
|
+
emit("healthcheck", "completed", "Agent is healthy");
|
|
586
|
+
emit("complete", "completed", `Agent "${config.displayName}" deployed successfully`);
|
|
587
|
+
} else {
|
|
588
|
+
emit("healthcheck", "failed", "Agent did not become healthy within 30s");
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
success: healthy,
|
|
592
|
+
containerId,
|
|
593
|
+
url: `http://localhost:${dc.ports[0]}`,
|
|
594
|
+
events: []
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
// ─── VPS Deployment ───────────────────────────────────
|
|
598
|
+
async deployVPS(config, emit) {
|
|
599
|
+
const vps = config.deployment.config.vps;
|
|
600
|
+
if (!vps) throw new Error("VPS config missing");
|
|
601
|
+
emit("provision", "started", `Connecting to ${vps.host}...`);
|
|
602
|
+
const script = this.configGen.generateVPSDeployScript(config);
|
|
603
|
+
emit("provision", "completed", "Deploy script generated");
|
|
604
|
+
emit("configure", "started", "Testing SSH connection...");
|
|
605
|
+
const sshTest = await this.execSSH(config, 'echo "ok"');
|
|
606
|
+
if (!sshTest.success) {
|
|
607
|
+
throw new Error(`SSH connection failed: ${sshTest.message}`);
|
|
608
|
+
}
|
|
609
|
+
emit("configure", "completed", "SSH connection verified");
|
|
610
|
+
emit("upload", "started", "Uploading deployment script...");
|
|
611
|
+
const scriptB64 = Buffer.from(script).toString("base64");
|
|
612
|
+
await this.execSSH(config, `echo "${scriptB64}" | base64 -d > /tmp/deploy-agenticmail.sh && chmod +x /tmp/deploy-agenticmail.sh`);
|
|
613
|
+
emit("upload", "completed", "Script uploaded");
|
|
614
|
+
emit("install", "started", "Running deployment (this may take a few minutes)...");
|
|
615
|
+
const deployResult = await this.execSSH(config, "bash /tmp/deploy-agenticmail.sh");
|
|
616
|
+
if (!deployResult.success) {
|
|
617
|
+
throw new Error(`Deployment script failed: ${deployResult.message}`);
|
|
618
|
+
}
|
|
619
|
+
emit("install", "completed", "Installation complete");
|
|
620
|
+
emit("healthcheck", "started", "Verifying service status...");
|
|
621
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
622
|
+
const statusCheck = await this.execSSH(config, `systemctl is-active agenticmail-${config.name}`);
|
|
623
|
+
const isActive = statusCheck.success && statusCheck.message.trim() === "active";
|
|
624
|
+
if (isActive) {
|
|
625
|
+
emit("healthcheck", "completed", "Service is active");
|
|
626
|
+
emit("complete", "completed", `Agent deployed to ${vps.host}`);
|
|
627
|
+
} else {
|
|
628
|
+
emit("healthcheck", "failed", "Service not active");
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
success: isActive,
|
|
632
|
+
sshCommand: `ssh ${vps.user}@${vps.host}${vps.port !== 22 ? ` -p ${vps.port}` : ""}`,
|
|
633
|
+
events: []
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
// ─── Fly.io Deployment ────────────────────────────────
|
|
637
|
+
/**
|
|
638
|
+
* Local/in-process deployment — agent runs within the same enterprise server.
|
|
639
|
+
* No container or VM needed. Just mark as running.
|
|
640
|
+
*/
|
|
641
|
+
async deployLocal(config, emit) {
|
|
642
|
+
emit("provision", "started", "Preparing local agent runtime...");
|
|
643
|
+
emit("provision", "completed", "Local runtime ready");
|
|
644
|
+
emit("configure", "started", "Applying agent configuration...");
|
|
645
|
+
emit("configure", "completed", "Configuration applied");
|
|
646
|
+
emit("start", "started", "Starting agent...");
|
|
647
|
+
emit("start", "completed", "Agent started in local runtime");
|
|
648
|
+
emit("healthcheck", "started", "Running health check...");
|
|
649
|
+
emit("healthcheck", "completed", "Agent is healthy");
|
|
650
|
+
emit("complete", "completed", "Local deployment successful");
|
|
651
|
+
return {
|
|
652
|
+
success: true,
|
|
653
|
+
url: void 0,
|
|
654
|
+
events: []
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Deploy agent to Fly.io using the Machines API (HTTP).
|
|
659
|
+
* No flyctl CLI needed — works from inside containers.
|
|
660
|
+
*
|
|
661
|
+
* Flow:
|
|
662
|
+
* 1. Create app (if it doesn't exist)
|
|
663
|
+
* 2. Create a Machine with the @agenticmail/enterprise Docker image
|
|
664
|
+
* 3. Set secrets (API keys, DB URL, etc.)
|
|
665
|
+
* 4. Wait for machine to start
|
|
666
|
+
*/
|
|
667
|
+
async deployFly(config, emit) {
|
|
668
|
+
const cloud = config.deployment.config.cloud;
|
|
669
|
+
if (!cloud) throw new Error("Fly.io config missing");
|
|
670
|
+
const apiToken = cloud.apiToken;
|
|
671
|
+
if (!apiToken) throw new Error("Fly.io API token is required");
|
|
672
|
+
const appName = cloud.appName || config.deployment.config.flyAppName || `am-agent-${config.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 30)}`;
|
|
673
|
+
const region = cloud.region || "iad";
|
|
674
|
+
const size = cloud.size || "shared-cpu-1x";
|
|
675
|
+
const FLY_API = "https://api.machines.dev/v1";
|
|
676
|
+
const authHeader = apiToken.startsWith("FlyV1 ") || apiToken.startsWith("fm2_") ? apiToken.startsWith("FlyV1 ") ? apiToken : `FlyV1 ${apiToken}` : `Bearer ${apiToken}`;
|
|
677
|
+
const flyFetch = async (path, method = "GET", body) => {
|
|
678
|
+
const res = await fetch(`${FLY_API}${path}`, {
|
|
679
|
+
method,
|
|
680
|
+
headers: {
|
|
681
|
+
"Authorization": authHeader,
|
|
682
|
+
"Content-Type": "application/json"
|
|
683
|
+
},
|
|
684
|
+
body: body ? JSON.stringify(body) : void 0
|
|
685
|
+
});
|
|
686
|
+
const text = await res.text();
|
|
687
|
+
let data;
|
|
688
|
+
try {
|
|
689
|
+
data = JSON.parse(text);
|
|
690
|
+
} catch {
|
|
691
|
+
data = { raw: text };
|
|
692
|
+
}
|
|
693
|
+
if (!res.ok) throw new Error(`Fly API ${method} ${path}: ${res.status} \u2014 ${data.error || text}`);
|
|
694
|
+
return data;
|
|
695
|
+
};
|
|
696
|
+
emit("provision", "started", `Creating Fly.io app "${appName}"...`);
|
|
697
|
+
try {
|
|
698
|
+
let flyOrg = cloud.org && cloud.org.length < 40 && /^[a-z0-9-]+$/.test(cloud.org) ? cloud.org : "";
|
|
699
|
+
if (!flyOrg) {
|
|
700
|
+
try {
|
|
701
|
+
const orgsRes = await flyFetch("/apps?org_slug=personal");
|
|
702
|
+
if (orgsRes?.apps?.length > 0 && orgsRes.apps[0].organization?.slug) {
|
|
703
|
+
flyOrg = orgsRes.apps[0].organization.slug;
|
|
704
|
+
}
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
if (!flyOrg) flyOrg = "personal";
|
|
708
|
+
}
|
|
709
|
+
await flyFetch("/apps", "POST", {
|
|
710
|
+
app_name: appName,
|
|
711
|
+
org_slug: flyOrg
|
|
712
|
+
});
|
|
713
|
+
emit("provision", "completed", `App "${appName}" created`);
|
|
714
|
+
} catch (e) {
|
|
715
|
+
if (e.message.includes("already exists") || e.message.includes("already been taken")) {
|
|
716
|
+
emit("provision", "completed", `App "${appName}" already exists, reusing`);
|
|
717
|
+
} else {
|
|
718
|
+
throw e;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
emit("configure", "started", "Preparing agent configuration...");
|
|
722
|
+
const env = {
|
|
723
|
+
NODE_ENV: "production",
|
|
724
|
+
AGENTICMAIL_AGENT_ID: config.id,
|
|
725
|
+
AGENTICMAIL_AGENT_NAME: config.displayName || config.name,
|
|
726
|
+
AGENTICMAIL_MODEL: `${config.model?.provider || "anthropic"}/${config.model?.modelId || "claude-sonnet-4-20250514"}`,
|
|
727
|
+
PORT: "3000"
|
|
728
|
+
};
|
|
729
|
+
if (config.model?.thinkingLevel) env.AGENTICMAIL_THINKING = config.model.thinkingLevel;
|
|
730
|
+
if (process.env.DATABASE_URL) env.DATABASE_URL = process.env.DATABASE_URL;
|
|
731
|
+
if (process.env.JWT_SECRET) env.JWT_SECRET = process.env.JWT_SECRET;
|
|
732
|
+
emit("configure", "completed", "Configuration ready");
|
|
733
|
+
emit("install", "started", "Deploying machine...");
|
|
734
|
+
const existingMachines = await flyFetch(`/apps/${appName}/machines`);
|
|
735
|
+
const machineConfig = {
|
|
736
|
+
image: "node:22-slim",
|
|
737
|
+
env,
|
|
738
|
+
services: [{
|
|
739
|
+
ports: [
|
|
740
|
+
{ port: 443, handlers: ["tls", "http"] },
|
|
741
|
+
{ port: 80, handlers: ["http"], force_https: true }
|
|
742
|
+
],
|
|
743
|
+
protocol: "tcp",
|
|
744
|
+
internal_port: 3e3
|
|
745
|
+
}],
|
|
746
|
+
guest: {
|
|
747
|
+
cpu_kind: size.includes("performance") ? "performance" : "shared",
|
|
748
|
+
cpus: size.includes("2x") ? 2 : 1,
|
|
749
|
+
memory_mb: size.includes("2x") ? 1024 : 512
|
|
750
|
+
},
|
|
751
|
+
init: {
|
|
752
|
+
cmd: ["sh", "-c", "rm -rf /root/.npm && mkdir -p /tmp/agent && cd /tmp/agent && npm init -y > /dev/null 2>&1 && npm install --no-save @agenticmail/enterprise pg && npx @agenticmail/enterprise agent"]
|
|
753
|
+
},
|
|
754
|
+
auto_destroy: false,
|
|
755
|
+
restart: { policy: "always" }
|
|
756
|
+
};
|
|
757
|
+
let machineId;
|
|
758
|
+
const liveMachines = (existingMachines || []).filter((m) => m.state !== "destroyed");
|
|
759
|
+
if (liveMachines.length > 0) {
|
|
760
|
+
const existing = liveMachines[0];
|
|
761
|
+
machineId = existing.id;
|
|
762
|
+
emit("install", "started", `Reusing existing machine ${machineId} (state: ${existing.state})...`);
|
|
763
|
+
if (existing.state === "started" || existing.state === "running") {
|
|
764
|
+
try {
|
|
765
|
+
await flyFetch(`/apps/${appName}/machines/${machineId}/stop`, "POST");
|
|
766
|
+
await flyFetch(`/apps/${appName}/machines/${machineId}/wait?state=stopped&timeout=30`);
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
await flyFetch(`/apps/${appName}/machines/${machineId}`, "POST", {
|
|
771
|
+
config: machineConfig,
|
|
772
|
+
region
|
|
773
|
+
});
|
|
774
|
+
emit("install", "completed", `Machine ${machineId} updated and restarting`);
|
|
775
|
+
} else {
|
|
776
|
+
const machine = await flyFetch(`/apps/${appName}/machines`, "POST", {
|
|
777
|
+
name: `agent-${config.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 20)}`,
|
|
778
|
+
region,
|
|
779
|
+
config: machineConfig
|
|
780
|
+
});
|
|
781
|
+
machineId = machine.id;
|
|
782
|
+
emit("install", "completed", `Machine ${machineId} created in ${region}`);
|
|
783
|
+
}
|
|
784
|
+
emit("start", "started", "Waiting for machine to start...");
|
|
785
|
+
try {
|
|
786
|
+
await flyFetch(`/apps/${appName}/machines/${machineId}/wait?state=started&timeout=60`);
|
|
787
|
+
emit("start", "completed", "Machine is running");
|
|
788
|
+
} catch {
|
|
789
|
+
emit("start", "completed", "Machine starting (health check pending)");
|
|
790
|
+
}
|
|
791
|
+
config.deployment.config.cloud = {
|
|
792
|
+
...cloud,
|
|
793
|
+
provider: "fly",
|
|
794
|
+
appName,
|
|
795
|
+
region,
|
|
796
|
+
size
|
|
797
|
+
};
|
|
798
|
+
config.deployment.config.flyAppName = appName;
|
|
799
|
+
config.deployment.config.flyMachineId = machineId;
|
|
800
|
+
config.deployment.config.deployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
801
|
+
const url = cloud.customDomain || `https://${appName}.fly.dev`;
|
|
802
|
+
emit("complete", "completed", `Agent live at ${url}`);
|
|
803
|
+
return {
|
|
804
|
+
success: true,
|
|
805
|
+
url,
|
|
806
|
+
appId: appName,
|
|
807
|
+
events: []
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
// ─── Railway Deployment ───────────────────────────────
|
|
811
|
+
async deployRailway(config, emit) {
|
|
812
|
+
const cloud = config.deployment.config.cloud;
|
|
813
|
+
if (!cloud || cloud.provider !== "railway") throw new Error("Railway config missing");
|
|
814
|
+
emit("provision", "started", "Creating Railway project...");
|
|
815
|
+
const appName = cloud.appName || `agenticmail-${config.name}`;
|
|
816
|
+
const result = await this.execCommand(`railway init --name ${appName}`, { RAILWAY_TOKEN: cloud.apiToken });
|
|
817
|
+
emit("provision", result.success ? "completed" : "failed", result.message);
|
|
818
|
+
return {
|
|
819
|
+
success: result.success,
|
|
820
|
+
url: `https://${appName}.up.railway.app`,
|
|
821
|
+
appId: appName,
|
|
822
|
+
events: []
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
// ─── Status Checkers ──────────────────────────────────
|
|
826
|
+
async getDockerStatus(config, base) {
|
|
827
|
+
const inspect = await this.execCommand(`docker inspect agenticmail-${config.name} --format '{{.State.Status}} {{.State.StartedAt}}'`);
|
|
828
|
+
if (!inspect.success) return { ...base, status: "not-deployed" };
|
|
829
|
+
const [status, startedAt] = inspect.message.trim().split(" ");
|
|
830
|
+
const running = status === "running";
|
|
831
|
+
const uptime = running ? Math.floor((Date.now() - new Date(startedAt).getTime()) / 1e3) : 0;
|
|
832
|
+
let metrics = void 0;
|
|
833
|
+
if (running) {
|
|
834
|
+
const stats = await this.execCommand(`docker stats agenticmail-${config.name} --no-stream --format '{{.CPUPerc}} {{.MemUsage}}'`);
|
|
835
|
+
if (stats.success) {
|
|
836
|
+
const parts = stats.message.trim().split(" ");
|
|
837
|
+
metrics = {
|
|
838
|
+
cpuPercent: parseFloat(parts[0]) || 0,
|
|
839
|
+
memoryMb: parseFloat(parts[1]) || 0,
|
|
840
|
+
toolCallsToday: 0,
|
|
841
|
+
activeSessionCount: 0,
|
|
842
|
+
errorRate: 0
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
...base,
|
|
848
|
+
status: running ? "running" : "stopped",
|
|
849
|
+
uptime,
|
|
850
|
+
healthStatus: running ? "healthy" : "unhealthy",
|
|
851
|
+
lastHealthCheck: (/* @__PURE__ */ new Date()).toISOString(),
|
|
852
|
+
metrics
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
async getVPSStatus(config, base) {
|
|
856
|
+
const result = await this.execSSH(config, `systemctl is-active agenticmail-${config.name}`);
|
|
857
|
+
const active = result.success && result.message.trim() === "active";
|
|
858
|
+
let uptime = 0;
|
|
859
|
+
if (active) {
|
|
860
|
+
const uptimeResult = await this.execSSH(config, `systemctl show agenticmail-${config.name} --property=ActiveEnterTimestamp --value`);
|
|
861
|
+
if (uptimeResult.success) {
|
|
862
|
+
uptime = Math.floor((Date.now() - new Date(uptimeResult.message.trim()).getTime()) / 1e3);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
...base,
|
|
867
|
+
status: active ? "running" : "stopped",
|
|
868
|
+
uptime,
|
|
869
|
+
healthStatus: active ? "healthy" : "unhealthy",
|
|
870
|
+
lastHealthCheck: (/* @__PURE__ */ new Date()).toISOString()
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
async getCloudStatus(config, base) {
|
|
874
|
+
const cloud = config.deployment.config.cloud;
|
|
875
|
+
if (!cloud || !cloud.apiToken) return base;
|
|
876
|
+
const appName = cloud.appName || `am-agent-${config.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 30)}`;
|
|
877
|
+
const FLY_API = "https://api.machines.dev/v1";
|
|
878
|
+
const t = cloud.apiToken;
|
|
879
|
+
const auth = t.startsWith("FlyV1 ") ? t : t.startsWith("fm2_") ? `FlyV1 ${t}` : `Bearer ${t}`;
|
|
880
|
+
try {
|
|
881
|
+
const res = await fetch(`${FLY_API}/apps/${appName}/machines`, {
|
|
882
|
+
headers: { "Authorization": auth }
|
|
883
|
+
});
|
|
884
|
+
if (!res.ok) return { ...base, status: "error", healthStatus: "unhealthy" };
|
|
885
|
+
const machines = await res.json();
|
|
886
|
+
if (machines.length === 0) return { ...base, status: "stopped" };
|
|
887
|
+
const machine = machines[0];
|
|
888
|
+
const state = machine.state;
|
|
889
|
+
const isRunning = state === "started" || state === "replacing";
|
|
890
|
+
return {
|
|
891
|
+
...base,
|
|
892
|
+
status: isRunning ? "running" : state === "stopped" ? "stopped" : "error",
|
|
893
|
+
healthStatus: isRunning ? "healthy" : "unhealthy",
|
|
894
|
+
endpoint: `https://${appName}.fly.dev`,
|
|
895
|
+
version: machine.image_ref?.tag,
|
|
896
|
+
uptime: machine.created_at ? Math.floor((Date.now() - new Date(machine.created_at).getTime()) / 1e3) : void 0
|
|
897
|
+
};
|
|
898
|
+
} catch {
|
|
899
|
+
return { ...base, status: "error", healthStatus: "unhealthy" };
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
// ─── Helpers ──────────────────────────────────────────
|
|
903
|
+
/** Stop or restart a Fly.io machine via the Machines API */
|
|
904
|
+
async flyMachineAction(config, action) {
|
|
905
|
+
const cloud = config.deployment.config.cloud;
|
|
906
|
+
if (!cloud || !cloud.apiToken) return { success: false, message: "Fly.io config missing" };
|
|
907
|
+
const appName = cloud.appName || `am-agent-${config.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 30)}`;
|
|
908
|
+
const FLY_API = "https://api.machines.dev/v1";
|
|
909
|
+
const t = cloud.apiToken;
|
|
910
|
+
const auth = t.startsWith("FlyV1 ") ? t : t.startsWith("fm2_") ? `FlyV1 ${t}` : `Bearer ${t}`;
|
|
911
|
+
try {
|
|
912
|
+
const res = await fetch(`${FLY_API}/apps/${appName}/machines`, {
|
|
913
|
+
headers: { "Authorization": auth }
|
|
914
|
+
});
|
|
915
|
+
if (!res.ok) return { success: false, message: `Failed to list machines: ${res.status}` };
|
|
916
|
+
const machines = await res.json();
|
|
917
|
+
if (machines.length === 0) return { success: false, message: "No machines found" };
|
|
918
|
+
const machineId = machines[0].id;
|
|
919
|
+
const actionRes = await fetch(`${FLY_API}/apps/${appName}/machines/${machineId}/${action}`, {
|
|
920
|
+
method: "POST",
|
|
921
|
+
headers: { "Authorization": auth }
|
|
922
|
+
});
|
|
923
|
+
if (!actionRes.ok) {
|
|
924
|
+
const err = await actionRes.text();
|
|
925
|
+
return { success: false, message: `${action} failed: ${err}` };
|
|
926
|
+
}
|
|
927
|
+
return { success: true, message: `Machine ${machineId} ${action}ed` };
|
|
928
|
+
} catch (e) {
|
|
929
|
+
return { success: false, message: e.message };
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
validateConfig(config) {
|
|
933
|
+
if (!config.name) throw new Error("Agent name is required");
|
|
934
|
+
if (!config.identity.role) throw new Error("Agent role is required");
|
|
935
|
+
if (!config.model.modelId) throw new Error("Model ID is required");
|
|
936
|
+
if (!config.deployment.target) throw new Error("Deployment target is required");
|
|
937
|
+
switch (config.deployment.target) {
|
|
938
|
+
case "docker":
|
|
939
|
+
if (!config.deployment.config.docker) throw new Error("Docker configuration missing");
|
|
940
|
+
break;
|
|
941
|
+
case "vps":
|
|
942
|
+
if (!config.deployment.config.vps?.host) throw new Error("VPS host is required");
|
|
943
|
+
break;
|
|
944
|
+
case "fly":
|
|
945
|
+
case "railway":
|
|
946
|
+
if (!config.deployment.config.cloud?.apiToken) throw new Error("Cloud API token is required");
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
generateDockerfile(config) {
|
|
951
|
+
return `FROM node:22-slim
|
|
952
|
+
|
|
953
|
+
WORKDIR /app
|
|
954
|
+
|
|
955
|
+
RUN npm install -g @agenticmail/enterprise @agenticmail/core agenticmail
|
|
956
|
+
|
|
957
|
+
COPY workspace/ /workspace/
|
|
958
|
+
|
|
959
|
+
ENV NODE_ENV=production
|
|
960
|
+
ENV AGENTICMAIL_MODEL=${config.model.provider}/${config.model.modelId}
|
|
961
|
+
ENV AGENTICMAIL_THINKING=${config.model.thinkingLevel}
|
|
962
|
+
|
|
963
|
+
EXPOSE 3000
|
|
964
|
+
|
|
965
|
+
CMD ["agenticmail-enterprise", "start"]
|
|
966
|
+
`;
|
|
967
|
+
}
|
|
968
|
+
async execCommand(cmd, env) {
|
|
969
|
+
const { exec } = await import("child_process");
|
|
970
|
+
const { promisify } = await import("util");
|
|
971
|
+
const execAsync = promisify(exec);
|
|
972
|
+
try {
|
|
973
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
974
|
+
timeout: 3e5,
|
|
975
|
+
// 5 min max
|
|
976
|
+
env: { ...process.env, ...env }
|
|
977
|
+
});
|
|
978
|
+
return { success: true, message: stdout || stderr };
|
|
979
|
+
} catch (error) {
|
|
980
|
+
return { success: false, message: error.stderr || error.message };
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async execSSH(config, command) {
|
|
984
|
+
const vps = config.deployment.config.vps;
|
|
985
|
+
if (!vps) return { success: false, message: "No VPS config" };
|
|
986
|
+
const sshArgs = [
|
|
987
|
+
"-o StrictHostKeyChecking=no",
|
|
988
|
+
`-p ${vps.port || 22}`,
|
|
989
|
+
vps.sshKeyPath ? `-i ${vps.sshKeyPath}` : "",
|
|
990
|
+
`${vps.user}@${vps.host}`,
|
|
991
|
+
`"${command.replace(/"/g, '\\"')}"`
|
|
992
|
+
].filter(Boolean).join(" ");
|
|
993
|
+
return this.execCommand(`ssh ${sshArgs}`);
|
|
994
|
+
}
|
|
995
|
+
async writeFile(path, content) {
|
|
996
|
+
const { writeFile } = await import("fs/promises");
|
|
997
|
+
const { dirname } = await import("path");
|
|
998
|
+
const { mkdir } = await import("fs/promises");
|
|
999
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1000
|
+
await writeFile(path, content, "utf-8");
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
// src/engine/lifecycle.ts
|
|
1005
|
+
var AgentLifecycleManager = class {
|
|
1006
|
+
agents = /* @__PURE__ */ new Map();
|
|
1007
|
+
healthCheckIntervals = /* @__PURE__ */ new Map();
|
|
1008
|
+
deployer = new DeploymentEngine();
|
|
1009
|
+
configGen = new AgentConfigGenerator();
|
|
1010
|
+
permissions;
|
|
1011
|
+
engineDb;
|
|
1012
|
+
eventListeners = [];
|
|
1013
|
+
dirtyAgents = /* @__PURE__ */ new Set();
|
|
1014
|
+
flushTimer = null;
|
|
1015
|
+
/** Track which budget alert thresholds have already fired per agent per day to avoid duplicates */
|
|
1016
|
+
firedAlerts = /* @__PURE__ */ new Map();
|
|
1017
|
+
budgetAlerts = [];
|
|
1018
|
+
birthdayTimer = null;
|
|
1019
|
+
lastBirthdayCheck = "";
|
|
1020
|
+
/** External callback for sending birthday messages (set via setBirthdaySender) */
|
|
1021
|
+
birthdaySender = null;
|
|
1022
|
+
/** Vault for decrypting deploy credentials */
|
|
1023
|
+
vault;
|
|
1024
|
+
constructor(opts) {
|
|
1025
|
+
this.engineDb = opts?.db;
|
|
1026
|
+
this.permissions = opts?.permissions || new PermissionEngine();
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Set the database adapter and load existing agents from DB
|
|
1030
|
+
*/
|
|
1031
|
+
async setDb(db) {
|
|
1032
|
+
this.engineDb = db;
|
|
1033
|
+
await this.loadFromDb();
|
|
1034
|
+
}
|
|
1035
|
+
setVault(vault) {
|
|
1036
|
+
this.vault = vault;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Load all agents from DB into memory
|
|
1040
|
+
*/
|
|
1041
|
+
async loadFromDb() {
|
|
1042
|
+
if (!this.engineDb) return;
|
|
1043
|
+
try {
|
|
1044
|
+
const agents = await this.engineDb.getAllManagedAgents();
|
|
1045
|
+
for (const agent of agents) {
|
|
1046
|
+
this.agents.set(agent.id, agent);
|
|
1047
|
+
if (agent.state === "running" || agent.state === "degraded") {
|
|
1048
|
+
this.startHealthCheckLoop(agent);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// ─── Agent CRUD ─────────────────────────────────────
|
|
1055
|
+
/**
|
|
1056
|
+
* Create a new managed agent (starts in 'draft' state)
|
|
1057
|
+
*/
|
|
1058
|
+
async createAgent(orgId, config, createdBy) {
|
|
1059
|
+
const agent = {
|
|
1060
|
+
id: config.id || crypto.randomUUID(),
|
|
1061
|
+
orgId,
|
|
1062
|
+
config,
|
|
1063
|
+
state: "draft",
|
|
1064
|
+
stateHistory: [],
|
|
1065
|
+
health: {
|
|
1066
|
+
status: "unknown",
|
|
1067
|
+
lastCheck: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1068
|
+
uptime: 0,
|
|
1069
|
+
consecutiveFailures: 0,
|
|
1070
|
+
checks: []
|
|
1071
|
+
},
|
|
1072
|
+
usage: this.emptyUsage(),
|
|
1073
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1074
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1075
|
+
version: 1
|
|
1076
|
+
};
|
|
1077
|
+
this.agents.set(agent.id, agent);
|
|
1078
|
+
await this.persistAgent(agent);
|
|
1079
|
+
this.emitEvent(agent, "created", { createdBy });
|
|
1080
|
+
return agent;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Update agent configuration (must be in draft, ready, stopped, or error state)
|
|
1084
|
+
*/
|
|
1085
|
+
async updateConfig(agentId, updates, updatedBy) {
|
|
1086
|
+
const agent = this.getAgent(agentId);
|
|
1087
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1088
|
+
const mutableStates = ["draft", "ready", "stopped", "error"];
|
|
1089
|
+
if (!mutableStates.includes(agent.state)) {
|
|
1090
|
+
throw new Error(`Cannot update config in state "${agent.state}". Stop the agent first.`);
|
|
1091
|
+
}
|
|
1092
|
+
const merged = { ...agent.config, ...updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1093
|
+
if (updates.identity && agent.config.identity) {
|
|
1094
|
+
merged.identity = { ...agent.config.identity, ...updates.identity };
|
|
1095
|
+
}
|
|
1096
|
+
if (updates.model && agent.config.model) {
|
|
1097
|
+
merged.model = { ...agent.config.model, ...updates.model };
|
|
1098
|
+
}
|
|
1099
|
+
if (updates.deployment && agent.config.deployment) {
|
|
1100
|
+
merged.deployment = { ...agent.config.deployment, ...updates.deployment };
|
|
1101
|
+
}
|
|
1102
|
+
agent.config = merged;
|
|
1103
|
+
agent.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1104
|
+
agent.version++;
|
|
1105
|
+
if (agent.state === "draft" && this.isConfigComplete(agent.config)) {
|
|
1106
|
+
this.transition(agent, "ready", "Configuration complete", updatedBy);
|
|
1107
|
+
} else if (agent.state !== "draft") {
|
|
1108
|
+
this.transition(agent, "ready", "Configuration updated", updatedBy);
|
|
1109
|
+
}
|
|
1110
|
+
await this.persistAgent(agent);
|
|
1111
|
+
this.emitEvent(agent, "configured", { updatedBy, changes: Object.keys(updates) });
|
|
1112
|
+
return agent;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Deploy an agent to its target environment
|
|
1116
|
+
*/
|
|
1117
|
+
async deploy(agentId, deployedBy) {
|
|
1118
|
+
const agent = this.getAgent(agentId);
|
|
1119
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1120
|
+
if (!["ready", "stopped", "error"].includes(agent.state)) {
|
|
1121
|
+
throw new Error(`Cannot deploy from state "${agent.state}"`);
|
|
1122
|
+
}
|
|
1123
|
+
if (!this.isConfigComplete(agent.config)) {
|
|
1124
|
+
throw new Error("Agent configuration is incomplete");
|
|
1125
|
+
}
|
|
1126
|
+
this.transition(agent, "provisioning", "Deployment initiated", deployedBy);
|
|
1127
|
+
await this.persistAgent(agent);
|
|
1128
|
+
try {
|
|
1129
|
+
await this.resolveDeployCredentials(agent);
|
|
1130
|
+
this.transition(agent, "deploying", "Pushing configuration", "system");
|
|
1131
|
+
const result = await this.deployer.deploy(agent.config, (event) => {
|
|
1132
|
+
this.emitEvent(agent, "deployed", { phase: event.phase, status: event.status, message: event.message });
|
|
1133
|
+
});
|
|
1134
|
+
if (result.success) {
|
|
1135
|
+
this.transition(agent, "starting", "Deployment successful, agent starting", "system");
|
|
1136
|
+
agent.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1137
|
+
const healthy = await this.waitForHealthy(agent, 6e4);
|
|
1138
|
+
if (healthy) {
|
|
1139
|
+
this.transition(agent, "running", "Agent is healthy and running", "system");
|
|
1140
|
+
this.emitEvent(agent, "started", { deployedBy });
|
|
1141
|
+
this.emitEvent(agent, "onboarding_required", { message: "Agent should complete onboarding: read org policies, acknowledge each, and internalize knowledge." });
|
|
1142
|
+
this.startHealthCheckLoop(agent);
|
|
1143
|
+
} else {
|
|
1144
|
+
this.transition(agent, "degraded", "Agent started but health check failed", "system");
|
|
1145
|
+
this.startHealthCheckLoop(agent);
|
|
1146
|
+
}
|
|
1147
|
+
} else {
|
|
1148
|
+
this.transition(agent, "error", `Deployment failed: ${result.error}`, "system");
|
|
1149
|
+
}
|
|
1150
|
+
await this.persistAgent(agent);
|
|
1151
|
+
return agent;
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
this.transition(agent, "error", `Deployment error: ${error.message}`, "system");
|
|
1154
|
+
await this.persistAgent(agent);
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Stop a running agent
|
|
1160
|
+
*/
|
|
1161
|
+
async stop(agentId, stoppedBy, reason) {
|
|
1162
|
+
const agent = this.getAgent(agentId);
|
|
1163
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1164
|
+
if (!["running", "degraded", "starting", "error"].includes(agent.state)) {
|
|
1165
|
+
throw new Error(`Cannot stop from state "${agent.state}"`);
|
|
1166
|
+
}
|
|
1167
|
+
this.stopHealthCheckLoop(agentId);
|
|
1168
|
+
try {
|
|
1169
|
+
await this.deployer.stop(agent.config);
|
|
1170
|
+
this.transition(agent, "stopped", reason || "Stopped by user", stoppedBy);
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
this.transition(agent, "stopped", `Stopped with error: ${error.message}`, stoppedBy);
|
|
1173
|
+
}
|
|
1174
|
+
await this.persistAgent(agent);
|
|
1175
|
+
this.emitEvent(agent, "stopped", { stoppedBy, reason });
|
|
1176
|
+
return agent;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Restart a running agent
|
|
1180
|
+
*/
|
|
1181
|
+
async restart(agentId, restartedBy) {
|
|
1182
|
+
const agent = this.getAgent(agentId);
|
|
1183
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1184
|
+
this.transition(agent, "updating", "Restarting", restartedBy);
|
|
1185
|
+
try {
|
|
1186
|
+
await this.deployer.restart(agent.config);
|
|
1187
|
+
const healthy = await this.waitForHealthy(agent, 3e4);
|
|
1188
|
+
this.transition(agent, healthy ? "running" : "degraded", "Restarted", "system");
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
this.transition(agent, "error", `Restart failed: ${error.message}`, "system");
|
|
1191
|
+
}
|
|
1192
|
+
await this.persistAgent(agent);
|
|
1193
|
+
this.emitEvent(agent, "restarted", { restartedBy });
|
|
1194
|
+
return agent;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Hot-update config on a running agent (no full redeploy)
|
|
1198
|
+
*/
|
|
1199
|
+
async hotUpdate(agentId, updates, updatedBy) {
|
|
1200
|
+
const agent = this.getAgent(agentId);
|
|
1201
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1202
|
+
if (agent.state !== "running" && agent.state !== "degraded") {
|
|
1203
|
+
throw new Error(`Hot update only works on running agents (current: "${agent.state}")`);
|
|
1204
|
+
}
|
|
1205
|
+
const prevState = agent.state;
|
|
1206
|
+
this.transition(agent, "updating", "Hot config update", updatedBy);
|
|
1207
|
+
const merged = { ...agent.config, ...updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1208
|
+
if (updates.identity && agent.config.identity) {
|
|
1209
|
+
merged.identity = { ...agent.config.identity, ...updates.identity };
|
|
1210
|
+
}
|
|
1211
|
+
if (updates.model && agent.config.model) {
|
|
1212
|
+
merged.model = { ...agent.config.model, ...updates.model };
|
|
1213
|
+
}
|
|
1214
|
+
if (updates.deployment && agent.config.deployment) {
|
|
1215
|
+
merged.deployment = { ...agent.config.deployment, ...updates.deployment };
|
|
1216
|
+
}
|
|
1217
|
+
agent.config = merged;
|
|
1218
|
+
agent.version++;
|
|
1219
|
+
try {
|
|
1220
|
+
await this.deployer.updateConfig(agent.config);
|
|
1221
|
+
this.transition(agent, prevState, "Config updated successfully", "system");
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
this.transition(agent, "degraded", `Config update failed: ${error.message}`, "system");
|
|
1224
|
+
}
|
|
1225
|
+
await this.persistAgent(agent);
|
|
1226
|
+
this.emitEvent(agent, "updated", { updatedBy, hotUpdate: true });
|
|
1227
|
+
return agent;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Destroy an agent completely (stop + delete all resources)
|
|
1231
|
+
*/
|
|
1232
|
+
async destroy(agentId, destroyedBy) {
|
|
1233
|
+
const agent = this.getAgent(agentId);
|
|
1234
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1235
|
+
this.transition(agent, "destroying", "Agent being destroyed", destroyedBy);
|
|
1236
|
+
this.stopHealthCheckLoop(agentId);
|
|
1237
|
+
if (["running", "degraded", "starting"].includes(agent.state)) {
|
|
1238
|
+
try {
|
|
1239
|
+
await this.deployer.stop(agent.config);
|
|
1240
|
+
} catch {
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
this.emitEvent(agent, "destroyed", { destroyedBy });
|
|
1244
|
+
this.agents.delete(agentId);
|
|
1245
|
+
try {
|
|
1246
|
+
await this.engineDb?.deleteManagedAgent(agentId);
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
console.error(`[lifecycle] Failed to delete agent ${agentId} from DB:`, err);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
// ─── Monitoring ─────────────────────────────────────
|
|
1252
|
+
/**
|
|
1253
|
+
* Record LLM usage (tokens + cost) from an agent session turn.
|
|
1254
|
+
*/
|
|
1255
|
+
recordLLMUsage(agentId, opts) {
|
|
1256
|
+
const agent = this.agents.get(agentId);
|
|
1257
|
+
if (!agent) return;
|
|
1258
|
+
const totalTokens = (opts.inputTokens || 0) + (opts.outputTokens || 0);
|
|
1259
|
+
const usage = agent.usage;
|
|
1260
|
+
if (totalTokens > 0) {
|
|
1261
|
+
usage.tokensToday += totalTokens;
|
|
1262
|
+
usage.tokensThisWeek += totalTokens;
|
|
1263
|
+
usage.tokensThisMonth += totalTokens;
|
|
1264
|
+
usage.tokensThisYear += totalTokens;
|
|
1265
|
+
}
|
|
1266
|
+
if (opts.costUsd) {
|
|
1267
|
+
usage.costToday += opts.costUsd;
|
|
1268
|
+
usage.costThisWeek += opts.costUsd;
|
|
1269
|
+
usage.costThisMonth += opts.costUsd;
|
|
1270
|
+
usage.costThisYear += opts.costUsd;
|
|
1271
|
+
}
|
|
1272
|
+
usage.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1273
|
+
this.dirtyAgents.add(agentId);
|
|
1274
|
+
this.scheduleUsageFlush();
|
|
1275
|
+
const budget = agent.budgetConfig;
|
|
1276
|
+
if (budget) {
|
|
1277
|
+
if (budget.dailyCostCap > 0 && usage.costToday >= budget.dailyCostCap) {
|
|
1278
|
+
this.fireBudgetAlert(agent, "daily_exceeded", "cost", usage.costToday, budget.dailyCostCap);
|
|
1279
|
+
this.stop(agentId, "system", "Daily cost budget exceeded").catch(() => {
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Record a tool call for usage tracking with per-agent budget controls
|
|
1286
|
+
*/
|
|
1287
|
+
recordToolCall(agentId, toolId, opts) {
|
|
1288
|
+
const agent = this.agents.get(agentId);
|
|
1289
|
+
if (!agent) return;
|
|
1290
|
+
const usage = agent.usage;
|
|
1291
|
+
usage.toolCallsToday++;
|
|
1292
|
+
usage.toolCallsThisMonth++;
|
|
1293
|
+
if (opts?.tokensUsed) {
|
|
1294
|
+
usage.tokensToday += opts.tokensUsed;
|
|
1295
|
+
usage.tokensThisWeek += opts.tokensUsed;
|
|
1296
|
+
usage.tokensThisMonth += opts.tokensUsed;
|
|
1297
|
+
usage.tokensThisYear += opts.tokensUsed;
|
|
1298
|
+
}
|
|
1299
|
+
if (opts?.costUsd) {
|
|
1300
|
+
usage.costToday += opts.costUsd;
|
|
1301
|
+
usage.costThisWeek += opts.costUsd;
|
|
1302
|
+
usage.costThisMonth += opts.costUsd;
|
|
1303
|
+
usage.costThisYear += opts.costUsd;
|
|
1304
|
+
}
|
|
1305
|
+
if (opts?.isExternalAction) {
|
|
1306
|
+
usage.externalActionsToday++;
|
|
1307
|
+
usage.externalActionsThisMonth++;
|
|
1308
|
+
}
|
|
1309
|
+
if (opts?.error) {
|
|
1310
|
+
usage.errorsToday++;
|
|
1311
|
+
}
|
|
1312
|
+
usage.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1313
|
+
const budget = agent.budgetConfig;
|
|
1314
|
+
if (budget) {
|
|
1315
|
+
if (budget.dailyCostCap > 0 && usage.costToday >= budget.dailyCostCap) {
|
|
1316
|
+
this.fireBudgetAlert(agent, "daily_exceeded", "cost", usage.costToday, budget.dailyCostCap);
|
|
1317
|
+
this.stop(agentId, "system", "Daily cost budget exceeded").catch(() => {
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
if (budget.monthlyCostCap > 0 && usage.costThisMonth >= budget.monthlyCostCap) {
|
|
1321
|
+
this.fireBudgetAlert(agent, "exceeded", "cost", usage.costThisMonth, budget.monthlyCostCap);
|
|
1322
|
+
this.stop(agentId, "system", "Monthly cost budget exceeded").catch(() => {
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
if (budget.dailyTokenCap > 0 && usage.tokensToday >= budget.dailyTokenCap) {
|
|
1326
|
+
this.fireBudgetAlert(agent, "daily_exceeded", "tokens", usage.tokensToday, budget.dailyTokenCap);
|
|
1327
|
+
this.stop(agentId, "system", "Daily token budget exceeded").catch(() => {
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
if (budget.monthlyTokenCap > 0 && usage.tokensThisMonth >= budget.monthlyTokenCap) {
|
|
1331
|
+
this.fireBudgetAlert(agent, "exceeded", "tokens", usage.tokensThisMonth, budget.monthlyTokenCap);
|
|
1332
|
+
this.stop(agentId, "system", "Monthly token budget exceeded").catch(() => {
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
if (budget.weeklyCostCap > 0 && usage.costThisWeek >= budget.weeklyCostCap) {
|
|
1336
|
+
this.fireBudgetAlert(agent, "weekly_exceeded", "cost", usage.costThisWeek, budget.weeklyCostCap);
|
|
1337
|
+
this.stop(agentId, "system", "Weekly cost budget exceeded").catch(() => {
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
if (budget.weeklyTokenCap > 0 && usage.tokensThisWeek >= budget.weeklyTokenCap) {
|
|
1341
|
+
this.fireBudgetAlert(agent, "weekly_exceeded", "tokens", usage.tokensThisWeek, budget.weeklyTokenCap);
|
|
1342
|
+
this.stop(agentId, "system", "Weekly token budget exceeded").catch(() => {
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
if (budget.annualCostCap > 0 && usage.costThisYear >= budget.annualCostCap) {
|
|
1346
|
+
this.fireBudgetAlert(agent, "annual_exceeded", "cost", usage.costThisYear, budget.annualCostCap);
|
|
1347
|
+
this.stop(agentId, "system", "Annual cost budget exceeded").catch(() => {
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
if (budget.annualTokenCap > 0 && usage.tokensThisYear >= budget.annualTokenCap) {
|
|
1351
|
+
this.fireBudgetAlert(agent, "annual_exceeded", "tokens", usage.tokensThisYear, budget.annualTokenCap);
|
|
1352
|
+
this.stop(agentId, "system", "Annual token budget exceeded").catch(() => {
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
const thresholds = budget.warningThresholds || [50, 80, 95];
|
|
1356
|
+
for (const pct of thresholds) {
|
|
1357
|
+
if (budget.monthlyCostCap > 0) {
|
|
1358
|
+
const ratio = usage.costThisMonth / budget.monthlyCostCap * 100;
|
|
1359
|
+
if (ratio >= pct) {
|
|
1360
|
+
this.fireBudgetAlert(agent, `warning_${pct}`, "cost", usage.costThisMonth, budget.monthlyCostCap);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (budget.monthlyTokenCap > 0) {
|
|
1364
|
+
const ratio = usage.tokensThisMonth / budget.monthlyTokenCap * 100;
|
|
1365
|
+
if (ratio >= pct) {
|
|
1366
|
+
this.fireBudgetAlert(agent, `warning_${pct}`, "tokens", usage.tokensThisMonth, budget.monthlyTokenCap);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (budget.weeklyCostCap > 0) {
|
|
1370
|
+
const ratio = usage.costThisWeek / budget.weeklyCostCap * 100;
|
|
1371
|
+
if (ratio >= pct) {
|
|
1372
|
+
this.fireBudgetAlert(agent, `weekly_warning_${pct}`, "cost", usage.costThisWeek, budget.weeklyCostCap);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (budget.weeklyTokenCap > 0) {
|
|
1376
|
+
const ratio = usage.tokensThisWeek / budget.weeklyTokenCap * 100;
|
|
1377
|
+
if (ratio >= pct) {
|
|
1378
|
+
this.fireBudgetAlert(agent, `weekly_warning_${pct}`, "tokens", usage.tokensThisWeek, budget.weeklyTokenCap);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (budget.annualCostCap > 0) {
|
|
1382
|
+
const ratio = usage.costThisYear / budget.annualCostCap * 100;
|
|
1383
|
+
if (ratio >= pct) {
|
|
1384
|
+
this.fireBudgetAlert(agent, `annual_warning_${pct}`, "cost", usage.costThisYear, budget.annualCostCap);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (budget.annualTokenCap > 0) {
|
|
1388
|
+
const ratio = usage.tokensThisYear / budget.annualTokenCap * 100;
|
|
1389
|
+
if (ratio >= pct) {
|
|
1390
|
+
this.fireBudgetAlert(agent, `annual_warning_${pct}`, "tokens", usage.tokensThisYear, budget.annualTokenCap);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
} else {
|
|
1395
|
+
if (usage.tokenBudgetMonthly > 0 && usage.tokensThisMonth >= usage.tokenBudgetMonthly) {
|
|
1396
|
+
this.emitEvent(agent, "budget_exceeded", { type: "tokens", used: usage.tokensThisMonth, budget: usage.tokenBudgetMonthly });
|
|
1397
|
+
this.stop(agentId, "system", "Monthly token budget exceeded").catch(() => {
|
|
1398
|
+
});
|
|
1399
|
+
} else if (usage.tokenBudgetMonthly > 0 && usage.tokensThisMonth >= usage.tokenBudgetMonthly * 0.8) {
|
|
1400
|
+
this.emitEvent(agent, "budget_warning", { type: "tokens", used: usage.tokensThisMonth, budget: usage.tokenBudgetMonthly, percent: 80 });
|
|
1401
|
+
}
|
|
1402
|
+
if (usage.costBudgetMonthly > 0 && usage.costThisMonth >= usage.costBudgetMonthly) {
|
|
1403
|
+
this.emitEvent(agent, "budget_exceeded", { type: "cost", used: usage.costThisMonth, budget: usage.costBudgetMonthly });
|
|
1404
|
+
this.stop(agentId, "system", "Monthly cost budget exceeded").catch(() => {
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
this.emitEvent(agent, "tool_call", { toolId, ...opts });
|
|
1409
|
+
this.dirtyAgents.add(agentId);
|
|
1410
|
+
this.scheduleUsageFlush();
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Get all agents for an org
|
|
1414
|
+
*/
|
|
1415
|
+
getAgentsByOrg(orgId) {
|
|
1416
|
+
return Array.from(this.agents.values()).filter((a) => a.orgId === orgId);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Get a single agent
|
|
1420
|
+
*/
|
|
1421
|
+
getAgent(agentId) {
|
|
1422
|
+
return this.agents.get(agentId);
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Get org-wide usage summary
|
|
1426
|
+
*/
|
|
1427
|
+
getOrgUsage(orgId) {
|
|
1428
|
+
const agents = this.getAgentsByOrg(orgId);
|
|
1429
|
+
return {
|
|
1430
|
+
totalAgents: agents.length,
|
|
1431
|
+
runningAgents: agents.filter((a) => a.state === "running").length,
|
|
1432
|
+
totalTokensToday: agents.reduce((sum, a) => sum + a.usage.tokensToday, 0),
|
|
1433
|
+
totalCostToday: agents.reduce((sum, a) => sum + a.usage.costToday, 0),
|
|
1434
|
+
totalToolCallsToday: agents.reduce((sum, a) => sum + a.usage.toolCallsToday, 0),
|
|
1435
|
+
totalErrorsToday: agents.reduce((sum, a) => sum + a.usage.errorsToday, 0),
|
|
1436
|
+
agents: agents.map((a) => ({ id: a.id, name: a.config.displayName, state: a.state, usage: a.usage }))
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Subscribe to lifecycle events (for dashboard real-time updates)
|
|
1441
|
+
*/
|
|
1442
|
+
onEvent(listener) {
|
|
1443
|
+
this.eventListeners.push(listener);
|
|
1444
|
+
return () => {
|
|
1445
|
+
this.eventListeners = this.eventListeners.filter((l) => l !== listener);
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Reset daily counters (call at midnight via cron)
|
|
1450
|
+
*/
|
|
1451
|
+
resetDailyCounters() {
|
|
1452
|
+
for (const agent of this.agents.values()) {
|
|
1453
|
+
agent.usage.tokensToday = 0;
|
|
1454
|
+
agent.usage.toolCallsToday = 0;
|
|
1455
|
+
agent.usage.externalActionsToday = 0;
|
|
1456
|
+
agent.usage.costToday = 0;
|
|
1457
|
+
agent.usage.errorsToday = 0;
|
|
1458
|
+
agent.usage.totalSessionsToday = 0;
|
|
1459
|
+
this.dirtyAgents.add(agent.id);
|
|
1460
|
+
}
|
|
1461
|
+
this.firedAlerts.clear();
|
|
1462
|
+
this.scheduleUsageFlush();
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Reset monthly counters (call on 1st of month)
|
|
1466
|
+
*/
|
|
1467
|
+
resetMonthlyCounters() {
|
|
1468
|
+
for (const agent of this.agents.values()) {
|
|
1469
|
+
agent.usage.tokensThisMonth = 0;
|
|
1470
|
+
agent.usage.toolCallsThisMonth = 0;
|
|
1471
|
+
agent.usage.externalActionsThisMonth = 0;
|
|
1472
|
+
agent.usage.costThisMonth = 0;
|
|
1473
|
+
this.dirtyAgents.add(agent.id);
|
|
1474
|
+
}
|
|
1475
|
+
this.scheduleUsageFlush();
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Reset weekly counters (call on Monday via workforce scheduler)
|
|
1479
|
+
*/
|
|
1480
|
+
resetWeeklyCounters() {
|
|
1481
|
+
for (const agent of this.agents.values()) {
|
|
1482
|
+
agent.usage.tokensThisWeek = 0;
|
|
1483
|
+
agent.usage.costThisWeek = 0;
|
|
1484
|
+
this.dirtyAgents.add(agent.id);
|
|
1485
|
+
}
|
|
1486
|
+
this.scheduleUsageFlush();
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Reset annual counters (call on Jan 1 via workforce scheduler)
|
|
1490
|
+
*/
|
|
1491
|
+
resetAnnualCounters() {
|
|
1492
|
+
for (const agent of this.agents.values()) {
|
|
1493
|
+
agent.usage.tokensThisYear = 0;
|
|
1494
|
+
agent.usage.costThisYear = 0;
|
|
1495
|
+
this.dirtyAgents.add(agent.id);
|
|
1496
|
+
}
|
|
1497
|
+
this.scheduleUsageFlush();
|
|
1498
|
+
}
|
|
1499
|
+
// ─── Budget Management ─────────────────────────────────
|
|
1500
|
+
/**
|
|
1501
|
+
* Set per-agent budget configuration
|
|
1502
|
+
*/
|
|
1503
|
+
async setBudgetConfig(agentId, config) {
|
|
1504
|
+
const agent = this.agents.get(agentId);
|
|
1505
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1506
|
+
agent.budgetConfig = config;
|
|
1507
|
+
if (!agent.config) agent.config = {};
|
|
1508
|
+
agent.config.budgetConfig = config;
|
|
1509
|
+
agent.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1510
|
+
await this.persistAgent(agent);
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Get per-agent budget configuration
|
|
1514
|
+
*/
|
|
1515
|
+
getBudgetConfig(agentId) {
|
|
1516
|
+
const agent = this.agents.get(agentId);
|
|
1517
|
+
if (!agent) return void 0;
|
|
1518
|
+
if (!agent.budgetConfig && agent.config?.budgetConfig) {
|
|
1519
|
+
agent.budgetConfig = agent.config.budgetConfig;
|
|
1520
|
+
}
|
|
1521
|
+
return agent.budgetConfig;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Get budget alerts (optionally filtered)
|
|
1525
|
+
*/
|
|
1526
|
+
getBudgetAlerts(opts) {
|
|
1527
|
+
let alerts = [...this.budgetAlerts];
|
|
1528
|
+
if (opts?.orgId) alerts = alerts.filter((a) => a.orgId === opts.orgId);
|
|
1529
|
+
if (opts?.agentId) alerts = alerts.filter((a) => a.agentId === opts.agentId);
|
|
1530
|
+
if (opts?.acknowledged !== void 0) alerts = alerts.filter((a) => a.acknowledged === opts.acknowledged);
|
|
1531
|
+
return alerts.slice(0, opts?.limit || 100);
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Acknowledge a budget alert
|
|
1535
|
+
*/
|
|
1536
|
+
async acknowledgeBudgetAlert(alertId) {
|
|
1537
|
+
const alert = this.budgetAlerts.find((a) => a.id === alertId);
|
|
1538
|
+
if (alert) {
|
|
1539
|
+
alert.acknowledged = true;
|
|
1540
|
+
this.engineDb?.execute(
|
|
1541
|
+
"UPDATE budget_alerts SET acknowledged = 1 WHERE id = ?",
|
|
1542
|
+
[alertId]
|
|
1543
|
+
).catch((err) => {
|
|
1544
|
+
console.error(`[lifecycle] Failed to acknowledge alert ${alertId}:`, err);
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Get org-wide budget summary
|
|
1550
|
+
*/
|
|
1551
|
+
getBudgetSummary(orgId) {
|
|
1552
|
+
const agents = this.getAgentsByOrg(orgId);
|
|
1553
|
+
return {
|
|
1554
|
+
totalDailyCost: agents.reduce((s, a) => s + a.usage.costToday, 0),
|
|
1555
|
+
totalWeeklyCost: agents.reduce((s, a) => s + a.usage.costThisWeek, 0),
|
|
1556
|
+
totalMonthlyCost: agents.reduce((s, a) => s + a.usage.costThisMonth, 0),
|
|
1557
|
+
totalAnnualCost: agents.reduce((s, a) => s + a.usage.costThisYear, 0),
|
|
1558
|
+
totalDailyTokens: agents.reduce((s, a) => s + a.usage.tokensToday, 0),
|
|
1559
|
+
totalWeeklyTokens: agents.reduce((s, a) => s + a.usage.tokensThisWeek, 0),
|
|
1560
|
+
totalMonthlyTokens: agents.reduce((s, a) => s + a.usage.tokensThisMonth, 0),
|
|
1561
|
+
totalAnnualTokens: agents.reduce((s, a) => s + a.usage.tokensThisYear, 0),
|
|
1562
|
+
agentBudgets: agents.map((a) => ({
|
|
1563
|
+
id: a.id,
|
|
1564
|
+
name: a.config.displayName,
|
|
1565
|
+
budget: a.budgetConfig,
|
|
1566
|
+
usage: {
|
|
1567
|
+
costToday: a.usage.costToday,
|
|
1568
|
+
costThisWeek: a.usage.costThisWeek,
|
|
1569
|
+
costThisMonth: a.usage.costThisMonth,
|
|
1570
|
+
costThisYear: a.usage.costThisYear,
|
|
1571
|
+
tokensToday: a.usage.tokensToday,
|
|
1572
|
+
tokensThisWeek: a.usage.tokensThisWeek,
|
|
1573
|
+
tokensThisMonth: a.usage.tokensThisMonth,
|
|
1574
|
+
tokensThisYear: a.usage.tokensThisYear
|
|
1575
|
+
}
|
|
1576
|
+
})),
|
|
1577
|
+
recentAlerts: this.budgetAlerts.filter((a) => a.orgId === orgId).slice(0, 20)
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
fireBudgetAlert(agent, alertType, budgetType, currentValue, limitValue) {
|
|
1581
|
+
const key = `${agent.id}:${alertType}:${budgetType}`;
|
|
1582
|
+
if (!this.firedAlerts.has(agent.id)) this.firedAlerts.set(agent.id, /* @__PURE__ */ new Set());
|
|
1583
|
+
const fired = this.firedAlerts.get(agent.id);
|
|
1584
|
+
if (fired.has(key)) return;
|
|
1585
|
+
fired.add(key);
|
|
1586
|
+
const alert = {
|
|
1587
|
+
id: crypto.randomUUID(),
|
|
1588
|
+
orgId: agent.orgId,
|
|
1589
|
+
agentId: agent.id,
|
|
1590
|
+
alertType,
|
|
1591
|
+
budgetType,
|
|
1592
|
+
currentValue,
|
|
1593
|
+
limitValue,
|
|
1594
|
+
acknowledged: false,
|
|
1595
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1596
|
+
};
|
|
1597
|
+
this.budgetAlerts.push(alert);
|
|
1598
|
+
if (this.budgetAlerts.length > 500) this.budgetAlerts = this.budgetAlerts.slice(-500);
|
|
1599
|
+
this.engineDb?.execute(
|
|
1600
|
+
"INSERT INTO budget_alerts (id, org_id, agent_id, alert_type, budget_type, current_value, limit_value, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
1601
|
+
[alert.id, alert.orgId, alert.agentId, alert.alertType, alert.budgetType, alert.currentValue, alert.limitValue, alert.createdAt]
|
|
1602
|
+
).catch((err) => {
|
|
1603
|
+
console.error(`[lifecycle] Failed to persist budget alert:`, err);
|
|
1604
|
+
});
|
|
1605
|
+
const eventType = alertType.startsWith("warning") ? "budget_warning" : "budget_exceeded";
|
|
1606
|
+
this.emitEvent(agent, eventType, { alertType, budgetType, currentValue, limitValue, percent: Math.round(currentValue / limitValue * 100) });
|
|
1607
|
+
}
|
|
1608
|
+
// ─── Health Check Loop ────────────────────────────────
|
|
1609
|
+
startHealthCheckLoop(agent) {
|
|
1610
|
+
this.stopHealthCheckLoop(agent.id);
|
|
1611
|
+
const interval = setInterval(async () => {
|
|
1612
|
+
try {
|
|
1613
|
+
const status = await this.deployer.getStatus(agent.config);
|
|
1614
|
+
agent.lastHealthCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1615
|
+
const check = {
|
|
1616
|
+
name: "deployment_status",
|
|
1617
|
+
status: status.status === "running" ? "pass" : "fail",
|
|
1618
|
+
message: `Status: ${status.status}, Health: ${status.healthStatus}`,
|
|
1619
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1620
|
+
durationMs: 0
|
|
1621
|
+
};
|
|
1622
|
+
agent.health.checks = [check, ...agent.health.checks].slice(0, 10);
|
|
1623
|
+
if (status.status === "running" && status.healthStatus === "healthy") {
|
|
1624
|
+
agent.health.status = "healthy";
|
|
1625
|
+
agent.health.consecutiveFailures = 0;
|
|
1626
|
+
if (status.uptime) agent.health.uptime = status.uptime;
|
|
1627
|
+
if (status.metrics) {
|
|
1628
|
+
agent.usage.activeSessionCount = status.metrics.activeSessionCount;
|
|
1629
|
+
}
|
|
1630
|
+
if (agent.state === "degraded") {
|
|
1631
|
+
this.transition(agent, "running", "Health restored", "system");
|
|
1632
|
+
this.emitEvent(agent, "auto_recovered", {});
|
|
1633
|
+
}
|
|
1634
|
+
} else {
|
|
1635
|
+
agent.health.consecutiveFailures++;
|
|
1636
|
+
agent.health.status = agent.health.consecutiveFailures >= 3 ? "unhealthy" : "degraded";
|
|
1637
|
+
if (agent.state === "running" && agent.health.consecutiveFailures >= 2) {
|
|
1638
|
+
this.transition(agent, "degraded", `Health degraded: ${agent.health.consecutiveFailures} consecutive failures`, "system");
|
|
1639
|
+
}
|
|
1640
|
+
if (agent.health.consecutiveFailures >= 5 && agent.state !== "error") {
|
|
1641
|
+
this.emitEvent(agent, "auto_recovered", { action: "restart", failures: agent.health.consecutiveFailures });
|
|
1642
|
+
agent.health.consecutiveFailures = 0;
|
|
1643
|
+
try {
|
|
1644
|
+
await this.deployer.restart(agent.config);
|
|
1645
|
+
this.transition(agent, "starting", "Auto-restarted after health failures", "system");
|
|
1646
|
+
} catch {
|
|
1647
|
+
this.transition(agent, "error", "Auto-restart failed", "system");
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
agent.health.lastCheck = (/* @__PURE__ */ new Date()).toISOString();
|
|
1652
|
+
await this.persistAgent(agent);
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
agent.health.consecutiveFailures++;
|
|
1655
|
+
agent.health.status = "unhealthy";
|
|
1656
|
+
}
|
|
1657
|
+
}, 3e4);
|
|
1658
|
+
this.healthCheckIntervals.set(agent.id, interval);
|
|
1659
|
+
}
|
|
1660
|
+
stopHealthCheckLoop(agentId) {
|
|
1661
|
+
const interval = this.healthCheckIntervals.get(agentId);
|
|
1662
|
+
if (interval) {
|
|
1663
|
+
clearInterval(interval);
|
|
1664
|
+
this.healthCheckIntervals.delete(agentId);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
// ─── Private Helpers ──────────────────────────────────
|
|
1668
|
+
transition(agent, to, reason, triggeredBy) {
|
|
1669
|
+
const from = agent.state;
|
|
1670
|
+
const transition = {
|
|
1671
|
+
from,
|
|
1672
|
+
to,
|
|
1673
|
+
reason,
|
|
1674
|
+
triggeredBy,
|
|
1675
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1676
|
+
};
|
|
1677
|
+
agent.stateHistory.push(transition);
|
|
1678
|
+
if (agent.stateHistory.length > 50) agent.stateHistory = agent.stateHistory.slice(-50);
|
|
1679
|
+
agent.state = to;
|
|
1680
|
+
agent.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1681
|
+
this.engineDb?.addStateTransition(agent.id, transition).catch((err) => {
|
|
1682
|
+
console.error(`[lifecycle] Failed to persist state transition for ${agent.id}:`, err);
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Resolve org-level deploy credentials and merge into agent config.
|
|
1687
|
+
* If the agent's deployment config is missing an API token, look up
|
|
1688
|
+
* the org's deploy_credentials table for a matching target type.
|
|
1689
|
+
* Also sanitizes app names to be valid for the target platform.
|
|
1690
|
+
*/
|
|
1691
|
+
async resolveDeployCredentials(agent) {
|
|
1692
|
+
const target = agent.config?.deployment?.target;
|
|
1693
|
+
if (!target) return;
|
|
1694
|
+
if (!agent.config.deployment.config) agent.config.deployment.config = {};
|
|
1695
|
+
if (target === "fly" || target === "railway") {
|
|
1696
|
+
if (!agent.config.deployment.config.cloud) agent.config.deployment.config.cloud = {};
|
|
1697
|
+
const cloud = agent.config.deployment.config.cloud;
|
|
1698
|
+
if (cloud.appName) {
|
|
1699
|
+
cloud.appName = cloud.appName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
|
|
1700
|
+
}
|
|
1701
|
+
if (!cloud.provider) cloud.provider = target;
|
|
1702
|
+
if (cloud.apiToken) return;
|
|
1703
|
+
if (this.engineDb) {
|
|
1704
|
+
try {
|
|
1705
|
+
const orgId = agent.orgId || "default";
|
|
1706
|
+
const creds = await this.engineDb.getDeployCredentialsByType(orgId, target);
|
|
1707
|
+
if (creds.length > 0) {
|
|
1708
|
+
let credConfig = creds[0].config;
|
|
1709
|
+
if (credConfig?._encrypted && this.vault) {
|
|
1710
|
+
try {
|
|
1711
|
+
credConfig = JSON.parse(this.vault.decrypt(credConfig._encrypted));
|
|
1712
|
+
} catch (decErr) {
|
|
1713
|
+
console.error("[lifecycle] Failed to decrypt deploy credential:", decErr);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (credConfig?.apiToken || credConfig?.token) {
|
|
1717
|
+
cloud.apiToken = credConfig.apiToken || credConfig.token;
|
|
1718
|
+
if (credConfig.region && !cloud.region) cloud.region = credConfig.region;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
} catch (err) {
|
|
1722
|
+
console.error("[lifecycle] Failed to resolve deploy credentials:", err);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
isConfigComplete(config) {
|
|
1728
|
+
return !!(config.name && config.displayName && config.identity?.role && config.model?.modelId && config.deployment?.target && config.permissionProfileId);
|
|
1729
|
+
}
|
|
1730
|
+
async waitForHealthy(agent, timeoutMs) {
|
|
1731
|
+
const start = Date.now();
|
|
1732
|
+
while (Date.now() - start < timeoutMs) {
|
|
1733
|
+
try {
|
|
1734
|
+
const status = await this.deployer.getStatus(agent.config);
|
|
1735
|
+
if (status.status === "running") return true;
|
|
1736
|
+
} catch {
|
|
1737
|
+
}
|
|
1738
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
1739
|
+
}
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
/** Public persist — for use by routes that mutate agent config directly (e.g. email config) */
|
|
1743
|
+
async saveAgent(agentId) {
|
|
1744
|
+
const agent = this.agents.get(agentId);
|
|
1745
|
+
if (agent) await this.persistAgent(agent);
|
|
1746
|
+
}
|
|
1747
|
+
async persistAgent(agent) {
|
|
1748
|
+
if (!agent.name) agent.name = agent.id;
|
|
1749
|
+
this.agents.set(agent.id, agent);
|
|
1750
|
+
if (!this.engineDb) return;
|
|
1751
|
+
try {
|
|
1752
|
+
await withRetry(
|
|
1753
|
+
() => this.engineDb.upsertManagedAgent(agent),
|
|
1754
|
+
{ maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 2e3 }
|
|
1755
|
+
);
|
|
1756
|
+
} catch (err) {
|
|
1757
|
+
console.error(`[lifecycle] Failed to persist agent ${agent.id}:`, err);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
scheduleUsageFlush() {
|
|
1761
|
+
if (this.flushTimer) return;
|
|
1762
|
+
this.flushTimer = setTimeout(async () => {
|
|
1763
|
+
this.flushTimer = null;
|
|
1764
|
+
const agentIds = [...this.dirtyAgents];
|
|
1765
|
+
this.dirtyAgents.clear();
|
|
1766
|
+
for (const id of agentIds) {
|
|
1767
|
+
const agent = this.agents.get(id);
|
|
1768
|
+
if (agent) {
|
|
1769
|
+
await this.persistAgent(agent);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}, 5e3);
|
|
1773
|
+
}
|
|
1774
|
+
emitEvent(agent, type, data) {
|
|
1775
|
+
const event = {
|
|
1776
|
+
id: crypto.randomUUID(),
|
|
1777
|
+
agentId: agent.id,
|
|
1778
|
+
orgId: agent.orgId,
|
|
1779
|
+
type,
|
|
1780
|
+
data,
|
|
1781
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1782
|
+
};
|
|
1783
|
+
for (const listener of this.eventListeners) {
|
|
1784
|
+
try {
|
|
1785
|
+
listener(event);
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
emptyUsage() {
|
|
1791
|
+
return {
|
|
1792
|
+
tokensToday: 0,
|
|
1793
|
+
tokensThisWeek: 0,
|
|
1794
|
+
tokensThisMonth: 0,
|
|
1795
|
+
tokensThisYear: 0,
|
|
1796
|
+
tokenBudgetMonthly: 0,
|
|
1797
|
+
toolCallsToday: 0,
|
|
1798
|
+
toolCallsThisMonth: 0,
|
|
1799
|
+
externalActionsToday: 0,
|
|
1800
|
+
externalActionsThisMonth: 0,
|
|
1801
|
+
costToday: 0,
|
|
1802
|
+
costThisWeek: 0,
|
|
1803
|
+
costThisMonth: 0,
|
|
1804
|
+
costThisYear: 0,
|
|
1805
|
+
costBudgetMonthly: 0,
|
|
1806
|
+
activeSessionCount: 0,
|
|
1807
|
+
totalSessionsToday: 0,
|
|
1808
|
+
errorsToday: 0,
|
|
1809
|
+
errorRate1h: 0,
|
|
1810
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
// ─── Birthday Automation ────────────────────────────
|
|
1814
|
+
/**
|
|
1815
|
+
* Register a callback to send birthday messages to agents.
|
|
1816
|
+
* Called once during server startup with access to the communication bus.
|
|
1817
|
+
*/
|
|
1818
|
+
setBirthdaySender(sender) {
|
|
1819
|
+
this.birthdaySender = sender;
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Start the daily birthday check loop.
|
|
1823
|
+
* Runs every hour, but only triggers once per calendar day.
|
|
1824
|
+
*/
|
|
1825
|
+
startBirthdayScheduler() {
|
|
1826
|
+
this.checkBirthdays();
|
|
1827
|
+
this.birthdayTimer = setInterval(() => this.checkBirthdays(), 60 * 60 * 1e3);
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Check all agents for birthdays and send greetings.
|
|
1831
|
+
* Only fires once per calendar day.
|
|
1832
|
+
*/
|
|
1833
|
+
async checkBirthdays() {
|
|
1834
|
+
const today = /* @__PURE__ */ new Date();
|
|
1835
|
+
const dateKey = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
|
|
1836
|
+
if (this.lastBirthdayCheck === dateKey) return;
|
|
1837
|
+
this.lastBirthdayCheck = dateKey;
|
|
1838
|
+
const todayMonth = today.getMonth() + 1;
|
|
1839
|
+
const todayDay = today.getDate();
|
|
1840
|
+
for (const agent of this.agents.values()) {
|
|
1841
|
+
const dob = agent.config?.identity?.dateOfBirth;
|
|
1842
|
+
if (!dob) continue;
|
|
1843
|
+
const dobDate = new Date(dob);
|
|
1844
|
+
if (dobDate.getMonth() + 1 === todayMonth && dobDate.getDate() === todayDay) {
|
|
1845
|
+
const age = AgentConfigGenerator.deriveAge(dob);
|
|
1846
|
+
this.emitEvent(agent, "birthday", { dateOfBirth: dob, age, name: agent.config.displayName });
|
|
1847
|
+
if (this.birthdaySender) {
|
|
1848
|
+
try {
|
|
1849
|
+
await this.birthdaySender(agent);
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
console.error(`[lifecycle] Failed to send birthday message to ${agent.config.displayName}:`, err);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
/** Get agents with upcoming birthdays (next N days) */
|
|
1858
|
+
getUpcomingBirthdays(days = 30) {
|
|
1859
|
+
const today = /* @__PURE__ */ new Date();
|
|
1860
|
+
const results = [];
|
|
1861
|
+
for (const agent of this.agents.values()) {
|
|
1862
|
+
const dob = agent.config?.identity?.dateOfBirth;
|
|
1863
|
+
if (!dob) continue;
|
|
1864
|
+
const dobDate = new Date(dob);
|
|
1865
|
+
const thisYearBday = new Date(today.getFullYear(), dobDate.getMonth(), dobDate.getDate());
|
|
1866
|
+
if (thisYearBday < today) {
|
|
1867
|
+
thisYearBday.setFullYear(today.getFullYear() + 1);
|
|
1868
|
+
}
|
|
1869
|
+
const diffMs = thisYearBday.getTime() - today.getTime();
|
|
1870
|
+
const daysUntil = Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
|
|
1871
|
+
if (daysUntil <= days) {
|
|
1872
|
+
results.push({
|
|
1873
|
+
agent,
|
|
1874
|
+
dateOfBirth: dob,
|
|
1875
|
+
age: AgentConfigGenerator.deriveAge(dob) + (daysUntil > 0 ? 1 : 0),
|
|
1876
|
+
daysUntil
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return results.sort((a, b) => a.daysUntil - b.daysUntil);
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Cleanup: stop all health check loops
|
|
1884
|
+
*/
|
|
1885
|
+
shutdown() {
|
|
1886
|
+
if (this.flushTimer) {
|
|
1887
|
+
clearTimeout(this.flushTimer);
|
|
1888
|
+
this.flushTimer = null;
|
|
1889
|
+
}
|
|
1890
|
+
if (this.birthdayTimer) {
|
|
1891
|
+
clearInterval(this.birthdayTimer);
|
|
1892
|
+
this.birthdayTimer = null;
|
|
1893
|
+
}
|
|
1894
|
+
for (const id of this.dirtyAgents) {
|
|
1895
|
+
const agent = this.agents.get(id);
|
|
1896
|
+
if (agent) {
|
|
1897
|
+
this.engineDb?.upsertManagedAgent(agent).catch(() => {
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
this.dirtyAgents.clear();
|
|
1902
|
+
for (const [id] of this.healthCheckIntervals) {
|
|
1903
|
+
this.stopHealthCheckLoop(id);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
export {
|
|
1909
|
+
AgentConfigGenerator,
|
|
1910
|
+
DeploymentEngine,
|
|
1911
|
+
AgentLifecycleManager
|
|
1912
|
+
};
|