259 lines
7.9 KiB
JavaScript
259 lines
7.9 KiB
JavaScript
/**
|
|
* Ephemeral Agent - Short-lived agent with federated memory access
|
|
*
|
|
* Features:
|
|
* - Automatic lifecycle management (spawn → execute → learn → destroy)
|
|
* - Federated memory sync via QUIC
|
|
* - Tenant isolation with JWT authentication
|
|
* - Memory persistence after agent destruction
|
|
*/
|
|
import Database from 'better-sqlite3';
|
|
import { FederationHub } from './FederationHub.js';
|
|
import { SecurityManager } from './SecurityManager.js';
|
|
import { logger } from '../utils/logger.js';
|
|
export class EphemeralAgent {
|
|
config;
|
|
context;
|
|
hub;
|
|
security;
|
|
cleanupTimer;
|
|
syncTimer;
|
|
constructor(config) {
|
|
this.config = {
|
|
lifetime: 300, // 5 minutes default
|
|
syncInterval: 5000, // 5 seconds default
|
|
enableEncryption: true,
|
|
...config
|
|
};
|
|
this.security = new SecurityManager();
|
|
}
|
|
/**
|
|
* Spawn a new ephemeral agent with federated memory access
|
|
*/
|
|
static async spawn(config) {
|
|
const agent = new EphemeralAgent(config);
|
|
await agent.initialize();
|
|
return agent;
|
|
}
|
|
/**
|
|
* Initialize agent: setup DB, connect to hub, start lifecycle timers
|
|
*/
|
|
async initialize() {
|
|
const agentId = `eph-${this.config.tenantId}-${Date.now()}`;
|
|
const spawnTime = Date.now();
|
|
const expiresAt = spawnTime + ((this.config.lifetime || 300) * 1000);
|
|
logger.info('Spawning ephemeral agent', {
|
|
agentId,
|
|
tenantId: this.config.tenantId,
|
|
lifetime: this.config.lifetime,
|
|
expiresAt: new Date(expiresAt).toISOString()
|
|
});
|
|
// Initialize local database instance
|
|
const memoryPath = this.config.memoryPath || `:memory:`;
|
|
// Use better-sqlite3 for now (AgentDB integration planned)
|
|
const db = new Database(memoryPath);
|
|
// Create JWT token for authentication
|
|
const token = await this.security.createAgentToken({
|
|
agentId,
|
|
tenantId: this.config.tenantId,
|
|
expiresAt
|
|
});
|
|
// Connect to federation hub if endpoint provided
|
|
if (this.config.hubEndpoint) {
|
|
this.hub = new FederationHub({
|
|
endpoint: this.config.hubEndpoint,
|
|
agentId,
|
|
tenantId: this.config.tenantId,
|
|
token
|
|
});
|
|
await this.hub.connect();
|
|
// Start periodic sync
|
|
if (this.config.syncInterval) {
|
|
this.syncTimer = setInterval(async () => {
|
|
await this.syncWithHub();
|
|
}, this.config.syncInterval);
|
|
}
|
|
}
|
|
// Store context
|
|
this.context = {
|
|
agentId,
|
|
tenantId: this.config.tenantId,
|
|
db,
|
|
spawnTime,
|
|
expiresAt
|
|
};
|
|
// Schedule automatic cleanup at expiration
|
|
const timeUntilExpiry = expiresAt - Date.now();
|
|
this.cleanupTimer = setTimeout(async () => {
|
|
logger.warn('Agent lifetime expired, auto-destroying', { agentId });
|
|
await this.destroy();
|
|
}, timeUntilExpiry);
|
|
logger.info('Ephemeral agent spawned successfully', {
|
|
agentId,
|
|
hubConnected: !!this.hub,
|
|
timeUntilExpiry
|
|
});
|
|
}
|
|
/**
|
|
* Execute a task within the agent context
|
|
* Automatically syncs memory before and after execution
|
|
*/
|
|
async execute(task) {
|
|
if (!this.context) {
|
|
throw new Error('Agent not initialized. Call spawn() first.');
|
|
}
|
|
const { agentId, db } = this.context;
|
|
// Check if agent has expired
|
|
if (Date.now() >= this.context.expiresAt) {
|
|
throw new Error(`Agent ${agentId} has expired and cannot execute tasks`);
|
|
}
|
|
logger.info('Executing task', { agentId });
|
|
try {
|
|
// Pre-execution sync: pull latest memories from hub
|
|
if (this.hub) {
|
|
await this.syncWithHub();
|
|
}
|
|
// Execute user task
|
|
const result = await task(db, this.context);
|
|
// Post-execution sync: push new memories to hub
|
|
if (this.hub) {
|
|
await this.syncWithHub();
|
|
}
|
|
logger.info('Task execution completed', { agentId });
|
|
return result;
|
|
}
|
|
catch (error) {
|
|
logger.error('Task execution failed', {
|
|
agentId,
|
|
error: error.message
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
/**
|
|
* Query memories from federated database
|
|
*/
|
|
async queryMemories(task, k = 5) {
|
|
if (!this.context) {
|
|
throw new Error('Agent not initialized');
|
|
}
|
|
const { db, tenantId } = this.context;
|
|
// Query using ReasoningBank pattern search
|
|
const patterns = await db.patternSearch({
|
|
task,
|
|
k,
|
|
tenantId // Apply tenant isolation
|
|
});
|
|
return patterns || [];
|
|
}
|
|
/**
|
|
* Store a learning episode to persistent memory
|
|
*/
|
|
async storeEpisode(episode) {
|
|
if (!this.context) {
|
|
throw new Error('Agent not initialized');
|
|
}
|
|
const { db, agentId, tenantId } = this.context;
|
|
// Store episode with tenant isolation
|
|
await db.patternStore({
|
|
sessionId: agentId,
|
|
task: episode.task,
|
|
input: episode.input,
|
|
output: episode.output,
|
|
reward: episode.reward,
|
|
critique: episode.critique || '',
|
|
success: episode.reward >= 0.7,
|
|
tokensUsed: 0,
|
|
latencyMs: 0,
|
|
tenantId // Ensure tenant isolation
|
|
});
|
|
logger.info('Episode stored', {
|
|
agentId,
|
|
task: episode.task,
|
|
reward: episode.reward
|
|
});
|
|
}
|
|
/**
|
|
* Sync local memory with federation hub
|
|
*/
|
|
async syncWithHub() {
|
|
if (!this.hub || !this.context) {
|
|
return;
|
|
}
|
|
try {
|
|
await this.hub.sync(this.context.db);
|
|
}
|
|
catch (error) {
|
|
logger.error('Federation sync failed', {
|
|
agentId: this.context.agentId,
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Get remaining lifetime in seconds
|
|
*/
|
|
getRemainingLifetime() {
|
|
if (!this.context) {
|
|
return 0;
|
|
}
|
|
return Math.max(0, Math.floor((this.context.expiresAt - Date.now()) / 1000));
|
|
}
|
|
/**
|
|
* Destroy agent and cleanup resources
|
|
* Memory persists in federation hub
|
|
*/
|
|
async destroy() {
|
|
if (!this.context) {
|
|
return;
|
|
}
|
|
const { agentId, db } = this.context;
|
|
logger.info('Destroying ephemeral agent', { agentId });
|
|
// Clear timers
|
|
if (this.cleanupTimer) {
|
|
clearTimeout(this.cleanupTimer);
|
|
}
|
|
if (this.syncTimer) {
|
|
clearInterval(this.syncTimer);
|
|
}
|
|
// Final sync to persist any pending changes
|
|
if (this.hub) {
|
|
try {
|
|
await this.syncWithHub();
|
|
await this.hub.disconnect();
|
|
}
|
|
catch (error) {
|
|
logger.error('Final sync failed', {
|
|
agentId,
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
// Close local database
|
|
try {
|
|
await db.close?.();
|
|
}
|
|
catch (error) {
|
|
// Ignore close errors for in-memory databases
|
|
}
|
|
// Clear context
|
|
this.context = undefined;
|
|
logger.info('Ephemeral agent destroyed', { agentId });
|
|
}
|
|
/**
|
|
* Check if agent is still alive
|
|
*/
|
|
isAlive() {
|
|
if (!this.context) {
|
|
return false;
|
|
}
|
|
return Date.now() < this.context.expiresAt;
|
|
}
|
|
/**
|
|
* Get agent info
|
|
*/
|
|
getInfo() {
|
|
return this.context || null;
|
|
}
|
|
}
|
|
//# sourceMappingURL=EphemeralAgent.js.map
|