Leon4gr45/builder
0
1// lib/server-generate/server-orchestrator-runner.ts2import { MultiAgentOrchestrator } from '@/lib/llm/multi-agent-orchestrator';3import { ServerConfigManager } from './server-config-manager';4import { runWithVFS } from './vfs-context';5import type { SSEEventBus } from './sse-event-bus';6import type { TaskManager } from './task-manager';7import type { ServerGenerationParams, ServerOrchestratorContext, StartGenerationRequest, BuildResult } from './types';8import { VirtualFileSystem } from '@/lib/vfs';9import type { VirtualFile } from '@/lib/vfs/types';10 11interface RunnerDeps {12 taskManager: TaskManager;13 eventBus: SSEEventBus;14 createVFS: (projectId: string) => Promise<VirtualFileSystem>;15 apiBaseUrl: string;16}17 18function trackVFSMutations(vfs: VirtualFileSystem, dirtyPaths: Set<string>): VirtualFileSystem {19 const origCreate = vfs.createFile.bind(vfs);20 const origUpdate = vfs.updateFile.bind(vfs);21 const origDelete = vfs.deleteFile.bind(vfs);22 const origRename = vfs.renameFile.bind(vfs);23 const origMove = vfs.moveFile.bind(vfs);24 const origDeleteDir = vfs.deleteDirectory.bind(vfs);25 const origCreateDir = vfs.createDirectory.bind(vfs);26 27 vfs.createFile = async (projectId, path, content, opts?) => {28 const result = await origCreate(projectId, path, content, opts);29 dirtyPaths.add(path);30 return result;31 };32 vfs.updateFile = async (projectId, path, content, opts?) => {33 const result = await origUpdate(projectId, path, content, opts);34 dirtyPaths.add(path);35 return result;36 };37 vfs.deleteFile = async (projectId, path, opts?) => {38 await origDelete(projectId, path, opts);39 dirtyPaths.add(path);40 };41 vfs.renameFile = async (projectId, oldPath, newPath) => {42 const result = await origRename(projectId, oldPath, newPath);43 dirtyPaths.add(oldPath);44 dirtyPaths.add(newPath);45 return result;46 };47 vfs.moveFile = async (projectId, oldPath, newPath) => {48 const result = await origMove(projectId, oldPath, newPath);49 dirtyPaths.add(oldPath);50 dirtyPaths.add(newPath);51 return result;52 };53 vfs.deleteDirectory = async (projectId, path) => {54 await origDeleteDir(projectId, path);55 dirtyPaths.add(path);56 };57 vfs.createDirectory = async (projectId, path) => {58 await origCreateDir(projectId, path);59 dirtyPaths.add(path);60 };61 62 return vfs;63}64 65export async function runServerGeneration(66 taskId: string,67 request: StartGenerationRequest,68 deps: RunnerDeps,69): Promise<void> {70 const { taskManager, eventBus, createVFS, apiBaseUrl } = deps;71 const task = taskManager.getTask(taskId);72 if (!task) throw new Error(`Task ${taskId} not found`);73 74 const params: ServerGenerationParams = {75 provider: request.providerConfig?.provider ?? 'openai',76 model: request.model,77 apiKey: request.apiKey,78 providerBaseUrl: request.providerConfig?.baseUrl,79 ...request.generationParams,80 };81 82 const serverConfig = new ServerConfigManager(params, taskId);83 const dirtyPaths = new Set<string>();84 const serverVFS = trackVFSMutations(await createVFS(request.projectId), dirtyPaths);85 86 const serverContext: ServerOrchestratorContext = {87 apiBaseUrl,88 vfs: serverVFS,89 config: serverConfig as any,90 onEvent: (event, data) => {91 eventBus.emit(taskId, request.projectId, event, data, task.sessionId);92 },93 dirtyPaths,94 };95 96 const flushDirtyPaths = () => {97 if (dirtyPaths.size === 0) return;98 const paths = Array.from(dirtyPaths);99 dirtyPaths.clear();100 eventBus.emit(taskId, request.projectId, 'files_changed', { paths, taskId }, task.sessionId);101 };102 103 const progressCallback = (event: string, data?: unknown) => {104 const eventData = (data && typeof data === 'object' ? data : {}) as Record<string, unknown>;105 eventBus.emit(taskId, request.projectId, event, eventData, task.sessionId);106 107 if (event === 'tool_status' && eventData.status === 'completed') {108 flushDirtyPaths();109 }110 111 if (event === 'usage' && eventData.cost != null) {112 serverConfig.updateSessionCost(113 { promptTokens: (eventData as any).promptTokens, completionTokens: (eventData as any).completionTokens },114 eventData.cost as number,115 );116 }117 };118 119 await runWithVFS(serverVFS, async () => {120 const orchestrator = new MultiAgentOrchestrator(121 request.projectId,122 'orchestrator',123 progressCallback,124 {125 model: request.model,126 serverContext,127 permissionMode: request.permissionMode,128 permissionOverrides: request.permissionOverrides,129 // Server-side has no UI to prompt, so gated commands are declined.130 // Auto mode never gates, so this only affects Ask/Custom users.131 onApprovalNeeded: async () => 'deny' as const,132 },133 );134 135 task.orchestrator = orchestrator;136 137 try {138 if (request.conversationHistory?.length) {139 orchestrator.importConversation(request.conversationHistory as any[]);140 }141 142 const result = await orchestrator.execute(request.prompt, request.executeOptions);143 144 flushDirtyPaths();145 146 const wasStopped = task.status === 'cancelled' || task.status === 'stopping' || task.status === 'paused'147 || result.exitReason === 'stopped' || result.exitReason === 'error_stop';148 const session = serverConfig.getSessionCost();149 const finalResult = wasStopped ? 'stopped' : (result.success ? 'success' : 'failed');150 151 eventBus.emit(taskId, request.projectId, 'task_complete', {152 result: finalResult,153 ...(finalResult === 'failed' ? { error: result.summary } : {}),154 tokens: session.totalPromptTokens + session.totalCompletionTokens,155 cost: session.totalCost,156 }, task.sessionId);157 158 taskManager.completeTask(taskId, wasStopped ? 'cancelled' : (result.success ? 'completed' : 'failed'));159 } catch (error) {160 if (task.status === 'cancelled' || task.status === 'stopping' || task.status === 'paused') {161 flushDirtyPaths();162 const session = serverConfig.getSessionCost();163 eventBus.emit(taskId, request.projectId, 'task_complete', {164 result: 'stopped',165 tokens: session.totalPromptTokens + session.totalCompletionTokens,166 cost: session.totalCost,167 }, task.sessionId);168 taskManager.completeTask(taskId, 'cancelled');169 return;170 }171 172 const message = error instanceof Error ? error.message : String(error);173 eventBus.emit(taskId, request.projectId, 'error', { message, fatal: true }, task.sessionId);174 175 const session = serverConfig.getSessionCost();176 eventBus.emit(taskId, request.projectId, 'task_complete', {177 result: 'failed',178 tokens: session.totalPromptTokens + session.totalCompletionTokens,179 cost: session.totalCost,180 error: message,181 }, task.sessionId);182 183 taskManager.completeTask(taskId, 'failed');184 }185 });186}187 188export async function awaitBuildResult(taskId: string, deps: RunnerDeps): Promise<BuildResult> {189 const { taskManager, eventBus } = deps;190 const task = taskManager.getTask(taskId);191 if (!task) return { success: false, errors: ['Task not found'] };192 193 const allEntries = await deps.createVFS(task.projectId).then((v) => v.getAllFilesAndDirectories(task.projectId));194 const manifest: Record<string, number> = {};195 for (const f of allEntries) {196 if ('id' in f) {197 const file = f as VirtualFile;198 manifest[file.path] = file.updatedAt ? new Date(file.updatedAt).getTime() : Date.now();199 }200 }201 202 eventBus.emit(taskId, task.projectId, 'build_requested', { taskId, fileManifest: manifest }, task.sessionId);203 204 let timeoutId: ReturnType<typeof setTimeout>;205 206 const result = await Promise.race<BuildResult>([207 new Promise<BuildResult>((resolve) => {208 task.pendingBuildResolve = (r: BuildResult) => {209 clearTimeout(timeoutId);210 resolve(r);211 };212 }),213 new Promise<BuildResult>((resolve) => {214 timeoutId = setTimeout(() => {215 task.pendingBuildResolve = null;216 task.buildDeferred = true;217 resolve({ success: true, errors: ['Build deferred — client disconnected'] });218 }, 30_000);219 }),220 ]);221 222 return result;223}224 