@agenticmail/enterprise 0.5.95 → 0.5.98

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.
@@ -126,7 +126,7 @@ export class DeploymentEngine {
126
126
  case 'vps':
127
127
  return this.execSSH(config, `sudo systemctl stop agenticmail-${config.name}`);
128
128
  case 'fly':
129
- return this.execCommand(`fly apps destroy agenticmail-${config.name} --yes`);
129
+ return this.flyMachineAction(config, 'stop');
130
130
  case 'local':
131
131
  return { success: true, message: 'Local agent stopped' };
132
132
  default:
@@ -144,7 +144,7 @@ export class DeploymentEngine {
144
144
  case 'vps':
145
145
  return this.execSSH(config, `sudo systemctl restart agenticmail-${config.name}`);
146
146
  case 'fly':
147
- return this.execCommand(`fly apps restart agenticmail-${config.name}`);
147
+ return this.flyMachineAction(config, 'restart');
148
148
  case 'local':
149
149
  return { success: true, message: 'Local agent restarted' };
150
150
  default:
@@ -191,7 +191,7 @@ export class DeploymentEngine {
191
191
  case 'vps':
192
192
  return (await this.execSSH(config, `journalctl -u agenticmail-${config.name} --no-pager -n ${lines}`)).message;
193
193
  case 'fly':
194
- return (await this.execCommand(`fly logs -a agenticmail-${config.name} -n ${lines}`)).message;
194
+ return `Logs available at: https://fly.io/apps/${config.deployment.config.cloud?.appName || 'unknown'}/monitoring`;
195
195
  default:
196
196
  return 'Log streaming not supported for this target';
197
197
  }
@@ -382,67 +382,151 @@ export class DeploymentEngine {
382
382
  };
383
383
  }
384
384
 
385
+ /**
386
+ * Deploy agent to Fly.io using the Machines API (HTTP).
387
+ * No flyctl CLI needed — works from inside containers.
388
+ *
389
+ * Flow:
390
+ * 1. Create app (if it doesn't exist)
391
+ * 2. Create a Machine with the @agenticmail/enterprise Docker image
392
+ * 3. Set secrets (API keys, DB URL, etc.)
393
+ * 4. Wait for machine to start
394
+ */
385
395
  private async deployFly(config: AgentConfig, emit: Function): Promise<DeploymentResult> {
386
396
  const cloud = config.deployment.config.cloud;
387
- if (!cloud || cloud.provider !== 'fly') throw new Error('Fly.io config missing');
388
-
389
- const appName = cloud.appName || `agenticmail-${config.name}`;
397
+ if (!cloud) throw new Error('Fly.io config missing');
398
+
399
+ const apiToken = cloud.apiToken;
400
+ if (!apiToken) throw new Error('Fly.io API token is required');
401
+
402
+ const appName = cloud.appName || `am-agent-${config.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 30)}`;
403
+ const region = cloud.region || 'iad';
404
+ const size = cloud.size || 'shared-cpu-1x';
405
+ const FLY_API = 'https://api.machines.dev/v1';
406
+
407
+ const flyFetch = async (path: string, method: string = 'GET', body?: any): Promise<any> => {
408
+ const res = await fetch(`${FLY_API}${path}`, {
409
+ method,
410
+ headers: {
411
+ 'Authorization': `Bearer ${apiToken}`,
412
+ 'Content-Type': 'application/json',
413
+ },
414
+ body: body ? JSON.stringify(body) : undefined,
415
+ });
416
+ const text = await res.text();
417
+ let data;
418
+ try { data = JSON.parse(text); } catch { data = { raw: text }; }
419
+ if (!res.ok) throw new Error(`Fly API ${method} ${path}: ${res.status} — ${data.error || text}`);
420
+ return data;
421
+ };
390
422
 
391
- emit('provision', 'started', `Creating Fly.io app ${appName}...`);
392
- await this.execCommand(`fly apps create ${appName} --org personal`, { FLY_API_TOKEN: cloud.apiToken });
393
- emit('provision', 'completed', `App ${appName} created`);
423
+ // 1. Create app (ignore "already exists" errors)
424
+ emit('provision', 'started', `Creating Fly.io app "${appName}"...`);
425
+ try {
426
+ await flyFetch('/apps', 'POST', {
427
+ app_name: appName,
428
+ org_slug: cloud.org || 'personal',
429
+ });
430
+ emit('provision', 'completed', `App "${appName}" created`);
431
+ } catch (e: any) {
432
+ if (e.message.includes('already exists')) {
433
+ emit('provision', 'completed', `App "${appName}" already exists, reusing`);
434
+ } else {
435
+ throw e;
436
+ }
437
+ }
394
438
 
395
- // Generate Dockerfile
396
- emit('configure', 'started', 'Generating Dockerfile...');
397
- const dockerfile = this.generateDockerfile(config);
439
+ // 2. Generate workspace files and encode as base64 init script
440
+ emit('configure', 'started', 'Preparing agent configuration...');
398
441
  const workspace = this.configGen.generateWorkspace(config);
442
+ const initCommands = Object.entries(workspace).map(([file, content]) => {
443
+ const b64 = Buffer.from(content).toString('base64');
444
+ return `echo "${b64}" | base64 -d > /workspace/${file}`;
445
+ }).join(' && ');
446
+
447
+ // Build environment variables for the agent
448
+ const env: Record<string, string> = {
449
+ NODE_ENV: 'production',
450
+ AGENTICMAIL_MODEL: `${config.model.provider}/${config.model.modelId}`,
451
+ AGENTICMAIL_AGENT_ID: config.id,
452
+ AGENTICMAIL_AGENT_NAME: config.displayName || config.name,
453
+ PORT: '3000',
454
+ };
455
+ if (config.model.thinkingLevel) env.AGENTICMAIL_THINKING = config.model.thinkingLevel;
456
+ emit('configure', 'completed', 'Configuration ready');
457
+
458
+ // 3. Check for existing machines — update if found, create if not
459
+ emit('install', 'started', 'Deploying machine...');
460
+ const existingMachines = await flyFetch(`/apps/${appName}/machines`);
461
+ const machineConfig = {
462
+ image: 'node:22-slim',
463
+ env,
464
+ services: [{
465
+ ports: [
466
+ { port: 443, handlers: ['tls', 'http'], force_https: true },
467
+ { port: 80, handlers: ['http'] },
468
+ ],
469
+ protocol: 'tcp',
470
+ internal_port: 3000,
471
+ }],
472
+ guest: {
473
+ cpu_kind: size.includes('performance') ? 'performance' : 'shared',
474
+ cpus: size.includes('2x') ? 2 : 1,
475
+ memory_mb: size.includes('2x') ? 1024 : 512,
476
+ },
477
+ init: {
478
+ cmd: ['sh', '-c', `mkdir -p /workspace && ${initCommands || 'true'} && npx @agenticmail/enterprise start`],
479
+ },
480
+ auto_destroy: false,
481
+ restart: { policy: 'always' },
482
+ };
399
483
 
400
- // Write temp build context
401
- const buildDir = `/tmp/agenticmail-build-${config.name}`;
402
- await this.execCommand(`mkdir -p ${buildDir}/workspace`);
403
- await this.writeFile(`${buildDir}/Dockerfile`, dockerfile);
404
- for (const [file, content] of Object.entries(workspace)) {
405
- await this.writeFile(`${buildDir}/workspace/${file}`, content);
484
+ let machineId: string;
485
+ if (existingMachines.length > 0) {
486
+ // Update existing machine
487
+ const existing = existingMachines[0];
488
+ machineId = existing.id;
489
+ await flyFetch(`/apps/${appName}/machines/${machineId}`, 'POST', {
490
+ config: machineConfig,
491
+ region,
492
+ });
493
+ emit('install', 'completed', `Machine ${machineId} updated`);
494
+ } else {
495
+ // Create new machine
496
+ const machine = await flyFetch(`/apps/${appName}/machines`, 'POST', {
497
+ name: `agent-${config.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 20)}`,
498
+ region,
499
+ config: machineConfig,
500
+ });
501
+ machineId = machine.id;
502
+ emit('install', 'completed', `Machine ${machineId} created in ${region}`);
406
503
  }
407
504
 
408
- // Write fly.toml
409
- const flyToml = `
410
- app = "${appName}"
411
- primary_region = "${cloud.region || 'iad'}"
412
-
413
- [build]
414
- dockerfile = "Dockerfile"
415
-
416
- [http_service]
417
- internal_port = 3000
418
- force_https = true
419
- auto_stop_machines = true
420
- auto_start_machines = true
421
- min_machines_running = 1
422
-
423
- [[vm]]
424
- size = "${cloud.size || 'shared-cpu-1x'}"
425
- memory = "512mb"
426
- `;
427
- await this.writeFile(`${buildDir}/fly.toml`, flyToml);
428
- emit('configure', 'completed', 'Build context ready');
429
-
430
- // Deploy
431
- emit('install', 'started', 'Deploying to Fly.io (building + pushing)...');
432
- const deployResult = await this.execCommand(`cd ${buildDir} && fly deploy --now`, { FLY_API_TOKEN: cloud.apiToken });
433
- emit('install', deployResult.success ? 'completed' : 'failed', deployResult.message);
505
+ // 4. Wait for machine to start
506
+ emit('start', 'started', 'Waiting for machine to start...');
507
+ try {
508
+ await flyFetch(`/apps/${appName}/machines/${machineId}/wait?state=started&timeout=60`);
509
+ emit('start', 'completed', 'Machine is running');
510
+ } catch {
511
+ emit('start', 'completed', 'Machine starting (health check pending)');
512
+ }
434
513
 
435
- // Cleanup
436
- await this.execCommand(`rm -rf ${buildDir}`);
514
+ // Store deployment metadata
515
+ config.deployment.config.cloud = {
516
+ ...cloud,
517
+ provider: 'fly',
518
+ appName,
519
+ region,
520
+ size,
521
+ };
522
+ (config.deployment.config as any).flyMachineId = machineId;
523
+ (config.deployment.config as any).deployedAt = new Date().toISOString();
437
524
 
438
525
  const url = cloud.customDomain || `https://${appName}.fly.dev`;
439
-
440
- if (deployResult.success) {
441
- emit('complete', 'completed', `Agent live at ${url}`);
442
- }
526
+ emit('complete', 'completed', `Agent live at ${url}`);
443
527
 
444
528
  return {
445
- success: deployResult.success,
529
+ success: true,
446
530
  url,
447
531
  appId: appName,
448
532
  events: [],
@@ -528,29 +612,70 @@ primary_region = "${cloud.region || 'iad'}"
528
612
 
529
613
  private async getCloudStatus(config: AgentConfig, base: LiveAgentStatus): Promise<LiveAgentStatus> {
530
614
  const cloud = config.deployment.config.cloud;
531
- if (!cloud) return base;
615
+ if (!cloud || !cloud.apiToken) return base;
532
616
 
533
- const appName = cloud.appName || `agenticmail-${config.name}`;
534
- const result = await this.execCommand(`fly status -a ${appName} --json`, { FLY_API_TOKEN: cloud.apiToken });
535
-
536
- if (!result.success) return { ...base, status: 'error' };
617
+ const appName = cloud.appName || `am-agent-${config.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 30)}`;
618
+ const FLY_API = 'https://api.machines.dev/v1';
537
619
 
538
620
  try {
539
- const status = JSON.parse(result.message);
621
+ const res = await fetch(`${FLY_API}/apps/${appName}/machines`, {
622
+ headers: { 'Authorization': `Bearer ${cloud.apiToken}` },
623
+ });
624
+ if (!res.ok) return { ...base, status: 'error', healthStatus: 'unhealthy' };
625
+
626
+ const machines = await res.json() as any[];
627
+ if (machines.length === 0) return { ...base, status: 'stopped' };
628
+
629
+ const machine = machines[0];
630
+ const state = machine.state;
631
+ const isRunning = state === 'started' || state === 'replacing';
632
+
540
633
  return {
541
634
  ...base,
542
- status: status.Deployed ? 'running' : 'stopped',
543
- healthStatus: status.Deployed ? 'healthy' : 'unhealthy',
635
+ status: isRunning ? 'running' : state === 'stopped' ? 'stopped' : 'error',
636
+ healthStatus: isRunning ? 'healthy' : 'unhealthy',
544
637
  endpoint: `https://${appName}.fly.dev`,
545
- version: status.Version?.toString(),
638
+ version: machine.image_ref?.tag,
639
+ uptime: machine.created_at ? Math.floor((Date.now() - new Date(machine.created_at).getTime()) / 1000) : undefined,
546
640
  };
547
641
  } catch {
548
- return { ...base, status: 'error' };
642
+ return { ...base, status: 'error', healthStatus: 'unhealthy' };
549
643
  }
550
644
  }
551
645
 
552
646
  // ─── Helpers ──────────────────────────────────────────
553
647
 
648
+ /** Stop or restart a Fly.io machine via the Machines API */
649
+ private async flyMachineAction(config: AgentConfig, action: 'stop' | 'restart'): Promise<{ success: boolean; message: string }> {
650
+ const cloud = config.deployment.config.cloud;
651
+ if (!cloud || !cloud.apiToken) return { success: false, message: 'Fly.io config missing' };
652
+
653
+ const appName = cloud.appName || `am-agent-${config.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 30)}`;
654
+ const FLY_API = 'https://api.machines.dev/v1';
655
+
656
+ try {
657
+ const res = await fetch(`${FLY_API}/apps/${appName}/machines`, {
658
+ headers: { 'Authorization': `Bearer ${cloud.apiToken}` },
659
+ });
660
+ if (!res.ok) return { success: false, message: `Failed to list machines: ${res.status}` };
661
+ const machines = await res.json() as any[];
662
+ if (machines.length === 0) return { success: false, message: 'No machines found' };
663
+
664
+ const machineId = machines[0].id;
665
+ const actionRes = await fetch(`${FLY_API}/apps/${appName}/machines/${machineId}/${action}`, {
666
+ method: 'POST',
667
+ headers: { 'Authorization': `Bearer ${cloud.apiToken}` },
668
+ });
669
+ if (!actionRes.ok) {
670
+ const err = await actionRes.text();
671
+ return { success: false, message: `${action} failed: ${err}` };
672
+ }
673
+ return { success: true, message: `Machine ${machineId} ${action}ed` };
674
+ } catch (e: any) {
675
+ return { success: false, message: e.message };
676
+ }
677
+ }
678
+
554
679
  private validateConfig(config: AgentConfig) {
555
680
  if (!config.name) throw new Error('Agent name is required');
556
681
  if (!config.identity.role) throw new Error('Agent role is required');
@@ -137,6 +137,8 @@ export class AgentRuntime {
137
137
  agentId,
138
138
  workspaceDir: process.cwd(),
139
139
  agenticmailManager: this.config.agenticmailManager,
140
+ agentMemoryManager: this.config.agentMemoryManager,
141
+ orgId: 'default', // TODO: resolve from agent's org
140
142
  };
141
143
  if (this.config.getEmailConfig) {
142
144
  const ec = this.config.getEmailConfig(agentId);
@@ -258,7 +260,12 @@ export class AgentRuntime {
258
260
  // Build agent config
259
261
  var tools = opts.tools || await createAllTools(this.buildToolOptions(agentId));
260
262
 
261
- var systemPrompt = opts.systemPrompt || buildDefaultSystemPrompt(agentId);
263
+ // Inject persistent memory context into system prompt
264
+ var memoryContext = '';
265
+ if (this.config.agentMemoryManager) {
266
+ try { memoryContext = await this.config.agentMemoryManager.generateMemoryContext(agentId); } catch {}
267
+ }
268
+ var systemPrompt = opts.systemPrompt || buildDefaultSystemPrompt(agentId, memoryContext);
262
269
 
263
270
  var agentConfig: AgentConfig = {
264
271
  agentId,
@@ -300,11 +307,16 @@ export class AgentRuntime {
300
307
 
301
308
  var tools = await createAllTools(this.buildToolOptions(session.agentId));
302
309
 
310
+ var memoryContext = '';
311
+ if (this.config.agentMemoryManager) {
312
+ try { memoryContext = await this.config.agentMemoryManager.generateMemoryContext(session.agentId); } catch {}
313
+ }
314
+
303
315
  var agentConfig: AgentConfig = {
304
316
  agentId: session.agentId,
305
317
  orgId: session.orgId,
306
318
  model,
307
- systemPrompt: buildDefaultSystemPrompt(session.agentId),
319
+ systemPrompt: buildDefaultSystemPrompt(session.agentId, memoryContext),
308
320
  tools,
309
321
  };
310
322
 
@@ -591,11 +603,16 @@ export class AgentRuntime {
591
603
 
592
604
  var tools = await createAllTools(this.buildToolOptions(session.agentId));
593
605
 
606
+ var mc = '';
607
+ if (this.config.agentMemoryManager) {
608
+ try { mc = await this.config.agentMemoryManager.generateMemoryContext(session.agentId); } catch {}
609
+ }
610
+
594
611
  var agentConfig: AgentConfig = {
595
612
  agentId: session.agentId,
596
613
  orgId: session.orgId,
597
614
  model,
598
- systemPrompt: buildDefaultSystemPrompt(session.agentId),
615
+ systemPrompt: buildDefaultSystemPrompt(session.agentId, mc),
599
616
  tools,
600
617
  };
601
618
 
@@ -678,8 +695,8 @@ export function createAgentRuntime(config: RuntimeConfig): AgentRuntime {
678
695
 
679
696
  // ─── Default System Prompt ───────────────────────────────
680
697
 
681
- function buildDefaultSystemPrompt(agentId: string): string {
682
- return `You are an AI agent managed by AgenticMail Enterprise (agent: ${agentId}).
698
+ function buildDefaultSystemPrompt(agentId: string, memoryContext?: string): string {
699
+ var base = `You are an AI agent managed by AgenticMail Enterprise (agent: ${agentId}).
683
700
 
684
701
  You have access to a comprehensive set of tools for completing tasks. Use them effectively.
685
702
 
@@ -691,6 +708,15 @@ Guidelines:
691
708
  - Respect organization policies and permissions
692
709
  - Keep responses concise unless detail is requested
693
710
  - For long tasks, work systematically and report progress
711
+ - ACTIVELY USE YOUR MEMORY: After corrections, lessons, or insights, call memory_reflect to record them
712
+ - Before complex tasks, call memory_context to recall relevant knowledge
713
+ - Your memory persists across conversations — it's how you grow as an expert
694
714
 
695
715
  Current time: ${new Date().toISOString()}`;
716
+
717
+ if (memoryContext) {
718
+ base += '\n\n' + memoryContext;
719
+ }
720
+
721
+ return base;
696
722
  }
@@ -118,6 +118,8 @@ export interface RuntimeConfig {
118
118
  gatewayEnabled?: boolean;
119
119
  /** AgenticMail manager for org email access (optional — enables agenticmail_* tools) */
120
120
  agenticmailManager?: import('../agent-tools/tools/agenticmail.js').AgenticMailManagerRef;
121
+ /** Agent memory manager for persistent DB-backed memory (optional — enables enhanced memory tools) */
122
+ agentMemoryManager?: import('../engine/agent-memory.js').AgentMemoryManager;
121
123
  /** Get OAuth email config for an agent (enables Google/Microsoft Workspace tools) */
122
124
  getEmailConfig?: (agentId: string) => any;
123
125
  /** Callback to persist refreshed OAuth tokens */
package/src/server.ts CHANGED
@@ -268,8 +268,10 @@ export function createServer(config: ServerConfig): ServerInstance {
268
268
  // Import lifecycle for email config access
269
269
  let getEmailConfig: ((agentId: string) => any) | undefined;
270
270
  let onTokenRefresh: ((agentId: string, tokens: any) => void) | undefined;
271
+ let agentMemoryMgr: any;
271
272
  try {
272
- const { lifecycle: lc } = await import('./engine/routes.js');
273
+ const { lifecycle: lc, memoryManager: mm } = await import('./engine/routes.js');
274
+ agentMemoryMgr = mm;
273
275
  if (lc) {
274
276
  getEmailConfig = (agentId: string) => {
275
277
  const managed = lc.getAgent(agentId);
@@ -294,6 +296,7 @@ export function createServer(config: ServerConfig): ServerInstance {
294
296
  gatewayEnabled: true,
295
297
  getEmailConfig,
296
298
  onTokenRefresh,
299
+ agentMemoryManager: agentMemoryMgr,
297
300
  });
298
301
  await runtime.start();
299
302
  const runtimeApp = runtime.getApp();