/** * Simplified Delegation Engine for WZRD Ecosystem * Adapted from opencode-background-agents with minimal dependencies */ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import { spawn, ChildProcess } from 'child_process'; // Readable ID generation function generateReadableId(): string { const adjectives = ['swift', 'quick', 'bright', 'clever', 'brave', 'calm', 'eager', 'fancy', 'gentle', 'happy']; const colors = ['red', 'blue', 'green', 'amber', 'violet', 'indigo', 'orange', 'yellow', 'purple', 'pink']; const animals = ['fox', 'wolf', 'eagle', 'lion', 'tiger', 'bear', 'hawk', 'owl', 'raven', 'falcon']; const randomElement = (arr: string[]) => arr[Math.floor(Math.random() * arr.length)]; return `${randomElement(adjectives)}-${randomElement(colors)}-${randomElement(animals)}`; } interface Delegation { id: string; sessionId: string; parentSessionId: string; prompt: string; agent: string; status: 'running' | 'complete' | 'error' | 'cancelled' | 'timeout'; startedAt: Date; completedAt?: Date; error?: string; title?: string; description?: string; result?: string; } interface DelegationOptions { parentSessionId: string; prompt: string; agent: string; timeoutMinutes?: number; storagePath?: string; } export class DelegationEngine { private delegations: Map = new Map(); private baseDir: string; private readonly MAX_RUN_TIME_MS: number; constructor(options?: { storagePath?: string; timeoutMinutes?: number }) { const timeoutMinutes = options?.timeoutMinutes || 15; this.MAX_RUN_TIME_MS = timeoutMinutes * 60 * 1000; this.baseDir = options?.storagePath || path.join(os.homedir(), '.wzrd', 'delegations'); } async initialize(): Promise { await fs.mkdir(this.baseDir, { recursive: true }); console.log(`Delegation engine initialized at: ${this.baseDir}`); } async delegate(options: DelegationOptions): Promise { const id = generateReadableId(); console.log(`Starting delegation: ${id}`); const delegation: Delegation = { id, sessionId: `session-${Date.now()}`, parentSessionId: options.parentSessionId, prompt: options.prompt, agent: options.agent, status: 'running', startedAt: new Date(), }; this.delegations.set(id, delegation); // Set timeout setTimeout(() => { const current = this.delegations.get(id); if (current && current.status === 'running') { this.handleTimeout(id); } }, this.MAX_RUN_TIME_MS + 5000); // Simulate async execution (in real implementation, spawn agent process) this.executeDelegation(delegation); return delegation; } private async executeDelegation(delegation: Delegation): Promise { try { // In real implementation, this would spawn the actual agent // For now, simulate with timeout await new Promise(resolve => setTimeout(resolve, 2000)); delegation.status = 'complete'; delegation.completedAt = new Date(); delegation.result = `Simulated result for: ${delegation.prompt}\n\nGenerated by delegation engine.`; delegation.title = delegation.prompt.slice(0, 30); delegation.description = delegation.prompt.slice(0, 150); await this.persistOutput(delegation); await this.notifyCompletion(delegation); } catch (error) { delegation.status = 'error'; delegation.completedAt = new Date(); delegation.error = error instanceof Error ? error.message : 'Unknown error'; await this.persistOutput(delegation); } } private async handleTimeout(delegationId: string): Promise { const delegation = this.delegations.get(delegationId); if (!delegation || delegation.status !== 'running') return; delegation.status = 'timeout'; delegation.completedAt = new Date(); delegation.error = `Delegation timed out after ${this.MAX_RUN_TIME_MS / 1000}s`; await this.persistOutput(delegation); } private async persistOutput(delegation: Delegation): Promise { try { const sessionDir = path.join(this.baseDir, delegation.parentSessionId); await fs.mkdir(sessionDir, { recursive: true }); const filePath = path.join(sessionDir, `${delegation.id}.md`); const content = `# ${delegation.title || delegation.id} ${delegation.description || 'Delegation result'} **ID:** ${delegation.id} **Agent:** ${delegation.agent} **Status:** ${delegation.status} **Started:** ${delegation.startedAt.toISOString()} **Completed:** ${delegation.completedAt?.toISOString() || 'N/A'} ${delegation.error ? `**Error:** ${delegation.error}` : ''} --- ${delegation.result || 'No result generated'} `; await fs.writeFile(filePath, content, 'utf8'); console.log(`Persisted delegation output to: ${filePath}`); } catch (error) { console.error('Failed to persist output:', error); } } private async notifyCompletion(delegation: Delegation): Promise { console.log(`Delegation ${delegation.id} completed with status: ${delegation.status}`); // In real implementation, this would send WebSocket notification // or trigger callback to Gateway V2 } async readOutput(sessionId: string, delegationId: string): Promise { // Check in-memory first const delegation = this.delegations.get(delegationId); if (delegation) { if (delegation.status === 'running') { return `Delegation ${delegationId} is still running...`; } return delegation.result || 'No result available'; } // Check filesystem try { const filePath = path.join(this.baseDir, sessionId, `${delegationId}.md`); return await fs.readFile(filePath, 'utf8'); } catch { throw new Error(`Delegation ${delegationId} not found`); } } async listDelegations(sessionId: string): Promise { const results: Delegation[] = []; // Add in-memory delegations for (const delegation of this.delegations.values()) { if (delegation.parentSessionId === sessionId) { results.push(delegation); } } // Add filesystem delegations try { const sessionDir = path.join(this.baseDir, sessionId); const files = await fs.readdir(sessionDir); for (const file of files) { if (file.endsWith('.md')) { const id = file.replace('.md', ''); if (!results.find(d => d.id === id)) { results.push({ id, sessionId: 'unknown', parentSessionId: sessionId, prompt: 'Loaded from storage', agent: 'unknown', status: 'complete', startedAt: new Date(), title: id, description: 'Delegation loaded from storage', }); } } } } catch { // Directory may not exist } return results; } getPendingCount(sessionId: string): number { return Array.from(this.delegations.values()) .filter(d => d.parentSessionId === sessionId && d.status === 'running') .length; } }