CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
server-orchestrator-runner.ts224 linesDownload Raw Back to server-generate
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