diff --git a/worker/agents/assistants/codeDebugger.ts b/worker/agents/assistants/codeDebugger.ts index 64bcbbcb..4d9acf6e 100644 --- a/worker/agents/assistants/codeDebugger.ts +++ b/worker/agents/assistants/codeDebugger.ts @@ -10,7 +10,6 @@ import { executeInference } from '../inferutils/infer'; import { InferenceContext, ModelConfig } from '../inferutils/config.types'; import { createObjectLogger } from '../../logger'; import type { ToolDefinition } from '../tools/types'; -import { CodingAgentInterface } from '../services/implementations/CodingAgent'; import { AGENT_CONFIG } from '../inferutils/config'; import { buildDebugTools } from '../tools/customTools'; import { RenderToolCall } from '../operations/UserConversationProcessor'; @@ -19,6 +18,7 @@ import { PROMPT_UTILS } from '../prompts'; import { RuntimeError } from 'worker/services/sandbox/sandboxTypes'; import { FileState } from '../core/state'; import { InferError } from '../inferutils/core'; +import { ICodingAgent } from '../services/interfaces/ICodingAgent'; const SYSTEM_PROMPT = `You are an elite autonomous code debugging specialist with deep expertise in root-cause analysis, modern web frameworks (React, Vite, Cloudflare Workers), TypeScript/JavaScript, build tools, and runtime environments. @@ -543,7 +543,7 @@ type LoopDetectionState = { export type DebugSession = { filesIndex: FileState[]; - agent: CodingAgentInterface; + agent: ICodingAgent; runtimeErrors?: RuntimeError[]; }; diff --git a/worker/agents/core/simpleGeneratorAgent.ts b/worker/agents/core/baseAgent.ts similarity index 69% rename from worker/agents/core/simpleGeneratorAgent.ts rename to worker/agents/core/baseAgent.ts index badd16db..85e0432a 100644 --- a/worker/agents/core/simpleGeneratorAgent.ts +++ b/worker/agents/core/baseAgent.ts @@ -1,92 +1,84 @@ -import { Agent, AgentContext, Connection, ConnectionContext } from 'agents'; +import { Connection, ConnectionContext } from 'agents'; import { - Blueprint, - PhaseConceptGenerationSchemaType, - PhaseConceptType, FileConceptType, FileOutputType, - PhaseImplementationSchemaType, + Blueprint, } from '../schemas'; import { ExecuteCommandsResponse, GitHubPushRequest, PreviewType, RuntimeError, StaticAnalysisResponse, TemplateDetails } from '../../services/sandbox/sandboxTypes'; import { GitHubExportResult } from '../../services/github/types'; import { GitHubService } from '../../services/github/GitHubService'; -import { CodeGenState, CurrentDevState, MAX_PHASES } from './state'; -import { AllIssues, AgentSummary, AgentInitArgs, PhaseExecutionResult, UserContext } from './types'; +import { BaseProjectState } from './state'; +import { AllIssues, AgentSummary, AgentInitArgs, BehaviorType } from './types'; import { PREVIEW_EXPIRED_ERROR, WebSocketMessageResponses } from '../constants'; import { broadcastToConnections, handleWebSocketClose, handleWebSocketMessage, sendToConnection } from './websocket'; -import { createObjectLogger, StructuredLogger } from '../../logger'; +import { StructuredLogger } from '../../logger'; import { ProjectSetupAssistant } from '../assistants/projectsetup'; import { UserConversationProcessor, RenderToolCall } from '../operations/UserConversationProcessor'; import { FileManager } from '../services/implementations/FileManager'; import { StateManager } from '../services/implementations/StateManager'; import { DeploymentManager } from '../services/implementations/DeploymentManager'; -// import { WebSocketBroadcaster } from '../services/implementations/WebSocketBroadcaster'; -import { GenerationContext } from '../domain/values/GenerationContext'; -import { IssueReport } from '../domain/values/IssueReport'; -import { PhaseImplementationOperation } from '../operations/PhaseImplementation'; import { FileRegenerationOperation } from '../operations/FileRegeneration'; -import { PhaseGenerationOperation } from '../operations/PhaseGeneration'; -import { ScreenshotAnalysisOperation } from '../operations/ScreenshotAnalysis'; // Database schema imports removed - using zero-storage OAuth flow import { BaseSandboxService } from '../../services/sandbox/BaseSandboxService'; import { WebSocketMessageData, WebSocketMessageType } from '../../api/websocketTypes'; import { InferenceContext, ModelConfig } from '../inferutils/config.types'; import { ModelConfigService } from '../../database/services/ModelConfigService'; import { fixProjectIssues } from '../../services/code-fixer'; -import { GitVersionControl } from '../git'; +import { GitVersionControl, SqlExecutor } from '../git'; import { FastCodeFixerOperation } from '../operations/PostPhaseCodeFixer'; import { looksLikeCommand, validateAndCleanBootstrapCommands } from '../utils/common'; -import { customizePackageJson, customizeTemplateFiles, generateBootstrapScript, generateProjectName } from '../utils/templateCustomizer'; -import { generateBlueprint } from '../planning/blueprint'; +import { customizeTemplateFiles, generateBootstrapScript } from '../utils/templateCustomizer'; import { AppService } from '../../database'; import { RateLimitExceededError } from 'shared/types/errors'; import { ImageAttachment, type ProcessedImageAttachment } from '../../types/image-attachment'; import { OperationOptions } from '../operations/common'; -import { CodingAgentInterface } from '../services/implementations/CodingAgent'; import { ImageType, uploadImage } from 'worker/utils/images'; import { ConversationMessage, ConversationState } from '../inferutils/common'; import { DeepCodeDebugger } from '../assistants/codeDebugger'; import { DeepDebugResult } from './types'; -import { StateMigration } from './stateMigration'; -import { generateNanoId } from 'worker/utils/idGenerator'; import { updatePackageJson } from '../utils/packageSyncer'; -import { IdGenerator } from '../utils/idGenerator'; +import { ICodingAgent } from '../services/interfaces/ICodingAgent'; import { SimpleCodeGenerationOperation } from '../operations/SimpleCodeGeneration'; -interface Operations { +const DEFAULT_CONVERSATION_SESSION_ID = 'default'; + +/** + * Infrastructure interface for agent implementations. + * Enables portability across different backends: + * - Durable Objects (current) + * - In-memory (testing) + * - Custom implementations + */ +export interface AgentInfrastructure { + readonly state: TState; + setState(state: TState): void; + readonly sql: SqlExecutor; + getWebSockets(): WebSocket[]; + getAgentId(): string; + logger(): StructuredLogger; + readonly env: Env; +} + +export interface BaseAgentOperations { regenerateFile: FileRegenerationOperation; - generateNextPhase: PhaseGenerationOperation; - analyzeScreenshot: ScreenshotAnalysisOperation; - implementPhase: PhaseImplementationOperation; fastCodeFixer: FastCodeFixerOperation; processUserMessage: UserConversationProcessor; + simpleGenerateFiles: SimpleCodeGenerationOperation; } -const DEFAULT_CONVERSATION_SESSION_ID = 'default'; - -/** - * SimpleCodeGeneratorAgent - Deterministically orchestrated agent - * - * Manages the lifecycle of code generation including: - * - Blueprint, phase generation, phase implementation, review cycles orchestrations - * - File streaming with WebSocket updates - * - Code validation and error correction - * - Deployment to sandbox service - */ -export class SimpleCodeGeneratorAgent extends Agent { - private static readonly MAX_COMMANDS_HISTORY = 10; - private static readonly PROJECT_NAME_PREFIX_MAX_LENGTH = 20; +export abstract class BaseAgentBehavior implements ICodingAgent { + protected static readonly MAX_COMMANDS_HISTORY = 10; + protected static readonly PROJECT_NAME_PREFIX_MAX_LENGTH = 20; protected projectSetupAssistant: ProjectSetupAssistant | undefined; protected stateManager!: StateManager; protected fileManager!: FileManager; - protected codingAgent: CodingAgentInterface = new CodingAgentInterface(this); protected deploymentManager!: DeploymentManager; protected git: GitVersionControl; - private previewUrlCache: string = ''; - private templateDetailsCache: TemplateDetails | null = null; + protected previewUrlCache: string = ''; + protected templateDetailsCache: TemplateDetails | null = null; // In-memory storage for user-uploaded images (not persisted in DO state) private pendingUserImages: ProcessedImageAttachment[] = [] @@ -98,75 +90,62 @@ export class SimpleCodeGeneratorAgent extends Agent { private staticAnalysisCache: StaticAnalysisResponse | null = null; // GitHub token cache (ephemeral, lost on DO eviction) - private githubTokenCache: { + protected githubTokenCache: { token: string; username: string; expiresAt: number; } | null = null; - - protected operations: Operations = { + protected operations: BaseAgentOperations = { regenerateFile: new FileRegenerationOperation(), - generateNextPhase: new PhaseGenerationOperation(), - analyzeScreenshot: new ScreenshotAnalysisOperation(), - implementPhase: new PhaseImplementationOperation(), fastCodeFixer: new FastCodeFixerOperation(), - processUserMessage: new UserConversationProcessor() + processUserMessage: new UserConversationProcessor(), + simpleGenerateFiles: new SimpleCodeGenerationOperation(), }; + + protected _boundSql: SqlExecutor; + + logger(): StructuredLogger { + return this.infrastructure.logger(); + } - public _logger: StructuredLogger | undefined; - - private initLogger(agentId: string, sessionId: string, userId: string) { - this._logger = createObjectLogger(this, 'CodeGeneratorAgent'); - this._logger.setObjectId(agentId); - this._logger.setFields({ - sessionId, - agentId, - userId, - }); - return this._logger; + getAgentId(): string { + return this.infrastructure.getAgentId(); } - logger(): StructuredLogger { - if (!this._logger) { - this._logger = this.initLogger(this.getAgentId(), this.state.sessionId, this.state.inferenceContext.userId); - } - return this._logger; - } - - getAgentId() { - return this.state.inferenceContext.agentId; - } - - initialState: CodeGenState = { - blueprint: {} as Blueprint, - projectName: "", - query: "", - generatedPhases: [], - generatedFilesMap: {}, - agentMode: 'deterministic', - sandboxInstanceId: undefined, - templateName: '', - commandsHistory: [], - lastPackageJson: '', - pendingUserInputs: [], - inferenceContext: {} as InferenceContext, - sessionId: '', - hostname: '', - conversationMessages: [], - currentDevState: CurrentDevState.IDLE, - phasesCounter: MAX_PHASES, - mvpGenerated: false, - shouldBeGenerating: false, - reviewingInitiated: false, - projectUpdatesAccumulator: [], - lastDeepDebugTranscript: null, - }; + get sql(): SqlExecutor { + return this._boundSql; + } - constructor(ctx: AgentContext, env: Env) { - super(ctx, env); - this.sql`CREATE TABLE IF NOT EXISTS full_conversations (id TEXT PRIMARY KEY, messages TEXT)`; - this.sql`CREATE TABLE IF NOT EXISTS compact_conversations (id TEXT PRIMARY KEY, messages TEXT)`; + get env(): Env { + return this.infrastructure.env; + } + + get state(): TState { + return this.infrastructure.state; + } + + setState(state: TState): void { + this.infrastructure.setState(state); + } + + getWebSockets(): WebSocket[] { + return this.infrastructure.getWebSockets(); + } + + getBehavior(): BehaviorType { + return this.state.behaviorType; + } + + /** + * Update state with partial changes (type-safe) + */ + updateState(updates: Partial): void { + this.setState({ ...this.state, ...updates } as TState); + } + + constructor(public readonly infrastructure: AgentInfrastructure) { + this._boundSql = this.infrastructure.sql.bind(this.infrastructure); // Initialize StateManager this.stateManager = new StateManager( @@ -189,110 +168,19 @@ export class SimpleCodeGeneratorAgent extends Agent { getLogger: () => this.logger(), env: this.env }, - SimpleCodeGeneratorAgent.MAX_COMMANDS_HISTORY + BaseAgentBehavior.MAX_COMMANDS_HISTORY ); } - /** - * Initialize the code generator with project blueprint and template - * Sets up services and begins deployment process - */ - async initialize( - initArgs: AgentInitArgs, + public async initialize( + _initArgs: AgentInitArgs, ..._args: unknown[] - ): Promise { - - const { query, language, frameworks, hostname, inferenceContext, templateInfo } = initArgs; - const sandboxSessionId = DeploymentManager.generateNewSessionId(); - this.initLogger(inferenceContext.agentId, sandboxSessionId, inferenceContext.userId); - - // Generate a blueprint - this.logger().info('Generating blueprint', { query, queryLength: query.length, imagesCount: initArgs.images?.length || 0 }); - this.logger().info(`Using language: ${language}, frameworks: ${frameworks ? frameworks.join(", ") : "none"}`); - - const blueprint = await generateBlueprint({ - env: this.env, - inferenceContext, - query, - language: language!, - frameworks: frameworks!, - templateDetails: templateInfo.templateDetails, - templateMetaInfo: templateInfo.selection, - images: initArgs.images, - stream: { - chunk_size: 256, - onChunk: (chunk) => { - // initArgs.writer.write({chunk}); - initArgs.onBlueprintChunk(chunk); - } - } - }) - - const packageJson = templateInfo.templateDetails?.allFiles['package.json']; - - this.templateDetailsCache = templateInfo.templateDetails; - - const projectName = generateProjectName( - blueprint?.projectName || templateInfo.templateDetails.name, - generateNanoId(), - SimpleCodeGeneratorAgent.PROJECT_NAME_PREFIX_MAX_LENGTH - ); - - this.logger().info('Generated project name', { projectName }); - - this.setState({ - ...this.initialState, - projectName, - query, - blueprint, - templateName: templateInfo.templateDetails.name, - sandboxInstanceId: undefined, - generatedPhases: [], - commandsHistory: [], - lastPackageJson: packageJson, - sessionId: sandboxSessionId, - hostname, - inferenceContext, - }); - - await this.gitInit(); - - // Customize template files (package.json, wrangler.jsonc, .bootstrap.js, .gitignore) - const customizedFiles = customizeTemplateFiles( - templateInfo.templateDetails.allFiles, - { - projectName, - commandsHistory: [] // Empty initially, will be updated later - } - ); - - this.logger().info('Customized template files', { - files: Object.keys(customizedFiles) - }); - - // Save customized files to git - const filesToSave = Object.entries(customizedFiles).map(([filePath, content]) => ({ - filePath, - fileContents: content, - filePurpose: 'Project configuration file' - })); - - await this.fileManager.saveGeneratedFiles( - filesToSave, - 'Initialize project configuration files' - ); - - this.logger().info('Committed customized template files to git'); - - this.initializeAsync().catch((error: unknown) => { - this.broadcastError("Initialization failed", error); - }); - this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} initialized successfully`); - await this.saveToDatabase(); + ): Promise { + this.logger().info("Initializing agent"); return this.state; } - private async initializeAsync(): Promise { + protected async initializeAsync(): Promise { try { const [, setupCommands] = await Promise.all([ this.deployToSandbox(), @@ -331,24 +219,10 @@ export class SimpleCodeGeneratorAgent extends Agent { this.logger().info(`Agent ${this.getAgentId()} starting in READ-ONLY mode - skipping expensive initialization`); return; } - - // migrate overwritten package.jsons - const oldPackageJson = this.fileManager.getFile('package.json')?.fileContents || this.state.lastPackageJson; - if (oldPackageJson) { - const packageJson = customizePackageJson(oldPackageJson, this.state.projectName); - this.fileManager.saveGeneratedFiles([ - { - filePath: 'package.json', - fileContents: packageJson, - filePurpose: 'Project configuration file' - } - ], 'chore: fix overwritten package.json'); - } - // Full initialization for read-write operations + // Just in case await this.gitInit(); - this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} onStart being processed, template name: ${this.state.templateName}`); - // Fill the template cache + await this.ensureTemplateDetails(); this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} onStart processed successfully`); @@ -373,7 +247,7 @@ export class SimpleCodeGeneratorAgent extends Agent { this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} onStart: User configs loaded successfully`, {userModelConfigs}); } - private async gitInit() { + protected async gitInit() { try { await this.git.init(); this.logger().info("Git initialized successfully"); @@ -396,19 +270,7 @@ export class SimpleCodeGeneratorAgent extends Agent { } } - onStateUpdate(_state: CodeGenState, _source: "server" | Connection) {} - - setState(state: CodeGenState): void { - try { - super.setState(state); - } catch (error) { - this.broadcastError("Error setting state", error); - this.logger().error("State details:", { - originalState: JSON.stringify(this.state, null, 2), - newState: JSON.stringify(state, null, 2) - }); - } - } + onStateUpdate(_state: TState, _source: "server" | Connection) {} onConnect(connection: Connection, ctx: ConnectionContext) { this.logger().info(`Agent connected for agent ${this.getAgentId()}`, { connection, ctx }); @@ -449,7 +311,7 @@ export class SimpleCodeGeneratorAgent extends Agent { return this.templateDetailsCache; } - private getTemplateDetails(): TemplateDetails { + protected getTemplateDetails(): TemplateDetails { if (!this.templateDetailsCache) { this.ensureTemplateDetails(); throw new Error('Template details not loaded. Call ensureTemplateDetails() first.'); @@ -581,7 +443,7 @@ export class SimpleCodeGeneratorAgent extends Agent { this.setConversationState(conversationState); } - private async saveToDatabase() { + protected async saveToDatabase() { this.logger().info(`Blueprint generated successfully for agent ${this.getAgentId()}`); // Save the app to database (authenticated users only) const appService = new AppService(this.env); @@ -590,10 +452,10 @@ export class SimpleCodeGeneratorAgent extends Agent { userId: this.state.inferenceContext.userId, sessionToken: null, title: this.state.blueprint.title || this.state.query.substring(0, 100), - description: this.state.blueprint.description || null, + description: this.state.blueprint.description, originalPrompt: this.state.query, finalPrompt: this.state.query, - framework: this.state.blueprint.frameworks?.[0], + framework: this.state.blueprint.frameworks.join(','), visibility: 'private', status: 'generating', createdAt: new Date(), @@ -641,38 +503,7 @@ export class SimpleCodeGeneratorAgent extends Agent { return this.generationPromise !== null; } - rechargePhasesCounter(max_phases: number = MAX_PHASES): void { - if (this.getPhasesCounter() <= max_phases) { - this.setState({ - ...this.state, - phasesCounter: max_phases - }); - } - } - - decrementPhasesCounter(): number { - const counter = this.getPhasesCounter() - 1; - this.setState({ - ...this.state, - phasesCounter: counter - }); - return counter; - } - - getPhasesCounter(): number { - return this.state.phasesCounter; - } - - getOperationOptions(): OperationOptions { - return { - env: this.env, - agentId: this.getAgentId(), - context: GenerationContext.from(this.state, this.getTemplateDetails(), this.logger()), - logger: this.logger(), - inferenceContext: this.getInferenceContext(), - agent: this.codingAgent - }; - } + abstract getOperationOptions(): OperationOptions; /** * Gets or creates an abort controller for the current operation @@ -723,36 +554,7 @@ export class SimpleCodeGeneratorAgent extends Agent { }; } - private createNewIncompletePhase(phaseConcept: PhaseConceptType) { - this.setState({ - ...this.state, - generatedPhases: [...this.state.generatedPhases, { - ...phaseConcept, - completed: false - }] - }) - - this.logger().info("Created new incomplete phase:", JSON.stringify(this.state.generatedPhases, null, 2)); - } - - private markPhaseComplete(phaseName: string) { - // First find the phase - const phases = this.state.generatedPhases; - if (!phases.some(p => p.name === phaseName)) { - this.logger().warn(`Phase ${phaseName} not found in generatedPhases array, skipping save`); - return; - } - - // Update the phase - this.setState({ - ...this.state, - generatedPhases: phases.map(p => p.name === phaseName ? { ...p, completed: true } : p) - }); - - this.logger().info("Completed phases:", JSON.stringify(phases, null, 2)); - } - - private broadcastError(context: string, error: unknown): void { + protected broadcastError(context: string, error: unknown): void { const errorMessage = error instanceof Error ? error.message : String(error); this.logger().error(`${context}:`, error); this.broadcast(WebSocketMessageResponses.ERROR, { @@ -774,7 +576,7 @@ export class SimpleCodeGeneratorAgent extends Agent { filePurpose: 'Project documentation and setup instructions' }); - const readme = await this.operations.implementPhase.generateReadme(this.getOperationOptions()); + const readme = await this.operations.simpleGenerateFiles.generateReadme(this.getOperationOptions()); await this.fileManager.saveGeneratedFile(readme, "feat: README.md"); @@ -786,7 +588,6 @@ export class SimpleCodeGeneratorAgent extends Agent { } async queueUserRequest(request: string, images?: ProcessedImageAttachment[]): Promise { - this.rechargePhasesCounter(3); this.setState({ ...this.state, pendingUserInputs: [...this.state.pendingUserInputs, request] @@ -799,7 +600,7 @@ export class SimpleCodeGeneratorAgent extends Agent { } } - private fetchPendingUserRequests(): string[] { + protected fetchPendingUserRequests(): string[] { const inputs = this.state.pendingUserInputs; if (inputs.length > 0) { this.setState({ @@ -814,7 +615,7 @@ export class SimpleCodeGeneratorAgent extends Agent { * State machine controller for code generation with user interaction support * Executes phases sequentially with review cycles and proper state transitions */ - async generateAllFiles(reviewCycles: number = 5): Promise { + async generateAllFiles(): Promise { if (this.state.mvpGenerated && this.state.pendingUserInputs.length === 0) { this.logger().info("Code generation already completed and no user inputs pending"); return; @@ -823,11 +624,11 @@ export class SimpleCodeGeneratorAgent extends Agent { this.logger().info("Code generation already in progress"); return; } - this.generationPromise = this.launchStateMachine(reviewCycles); + this.generationPromise = this.buildWrapper(); await this.generationPromise; } - private async launchStateMachine(reviewCycles: number) { + private async buildWrapper() { this.broadcast(WebSocketMessageResponses.GENERATION_STARTED, { message: 'Starting code generation', totalFiles: this.getTotalFiles() @@ -836,66 +637,8 @@ export class SimpleCodeGeneratorAgent extends Agent { totalFiles: this.getTotalFiles() }); await this.ensureTemplateDetails(); - - let currentDevState = CurrentDevState.PHASE_IMPLEMENTING; - const generatedPhases = this.state.generatedPhases; - const incompletedPhases = generatedPhases.filter(phase => !phase.completed); - let phaseConcept : PhaseConceptType | undefined; - if (incompletedPhases.length > 0) { - phaseConcept = incompletedPhases[incompletedPhases.length - 1]; - this.logger().info('Resuming code generation from incompleted phase', { - phase: phaseConcept - }); - } else if (generatedPhases.length > 0) { - currentDevState = CurrentDevState.PHASE_GENERATING; - this.logger().info('Resuming code generation after generating all phases', { - phase: generatedPhases[generatedPhases.length - 1] - }); - } else { - phaseConcept = this.state.blueprint.initialPhase; - this.logger().info('Starting code generation from initial phase', { - phase: phaseConcept - }); - this.createNewIncompletePhase(phaseConcept); - } - - let userContext: UserContext | undefined; - - // Store review cycles for later use - this.setState({ - ...this.state, - reviewCycles: reviewCycles - }); - try { - let executionResults: PhaseExecutionResult; - // State machine loop - continues until IDLE state - while (currentDevState !== CurrentDevState.IDLE) { - this.logger().info(`[generateAllFiles] Executing state: ${currentDevState}`); - switch (currentDevState) { - case CurrentDevState.PHASE_GENERATING: - executionResults = await this.executePhaseGeneration(); - currentDevState = executionResults.currentDevState; - phaseConcept = executionResults.result; - userContext = executionResults.userContext; - break; - case CurrentDevState.PHASE_IMPLEMENTING: - executionResults = await this.executePhaseImplementation(phaseConcept, userContext); - currentDevState = executionResults.currentDevState; - userContext = undefined; - break; - case CurrentDevState.REVIEWING: - currentDevState = await this.executeReviewCycle(); - break; - case CurrentDevState.FINALIZING: - currentDevState = await this.executeFinalizing(); - break; - default: - break; - } - } - - this.logger().info("State machine completed successfully"); + await this.build(); } catch (error) { if (error instanceof RateLimitExceededError) { this.logger().error("Error in state machine:", error); @@ -923,182 +666,10 @@ export class SimpleCodeGeneratorAgent extends Agent { } /** - * Execute phase generation state - generate next phase with user suggestions - */ - async executePhaseGeneration(isFinal?: boolean): Promise { - this.logger().info("Executing PHASE_GENERATING state"); - try { - const currentIssues = await this.fetchAllIssues(); - - // Generate next phase with user suggestions if available - - // Get stored images if user suggestions are present - const pendingUserInputs = this.fetchPendingUserRequests(); - const userContext = (pendingUserInputs.length > 0) - ? { - suggestions: pendingUserInputs, - images: this.pendingUserImages - } as UserContext - : undefined; - - if (userContext && userContext?.suggestions && userContext.suggestions.length > 0) { - // Only reset pending user inputs if user suggestions were read - this.logger().info("Resetting pending user inputs", { - userSuggestions: userContext.suggestions, - hasImages: !!userContext.images, - imageCount: userContext.images?.length || 0 - }); - - // Clear images after they're passed to phase generation - if (userContext?.images && userContext.images.length > 0) { - this.logger().info('Clearing stored user images after passing to phase generation'); - this.pendingUserImages = []; - } - } - - const nextPhase = await this.generateNextPhase(currentIssues, userContext, isFinal); - - if (!nextPhase) { - this.logger().info("No more phases to implement, transitioning to FINALIZING"); - return { - currentDevState: CurrentDevState.FINALIZING, - }; - } - - // Store current phase and transition to implementation - this.setState({ - ...this.state, - currentPhase: nextPhase - }); - - return { - currentDevState: CurrentDevState.PHASE_IMPLEMENTING, - result: nextPhase, - userContext: userContext, - }; - } catch (error) { - if (error instanceof RateLimitExceededError) { - throw error; - } - this.broadcastError("Error generating phase", error); - return { - currentDevState: CurrentDevState.IDLE, - }; - } - } - - /** - * Execute phase implementation state - implement current phase - */ - async executePhaseImplementation(phaseConcept?: PhaseConceptType, userContext?: UserContext): Promise<{currentDevState: CurrentDevState, staticAnalysis?: StaticAnalysisResponse}> { - try { - this.logger().info("Executing PHASE_IMPLEMENTING state"); - - if (phaseConcept === undefined) { - phaseConcept = this.state.currentPhase; - if (phaseConcept === undefined) { - this.logger().error("No phase concept provided to implement, will call phase generation"); - const results = await this.executePhaseGeneration(); - phaseConcept = results.result; - if (phaseConcept === undefined) { - this.logger().error("No phase concept provided to implement, will return"); - return {currentDevState: CurrentDevState.FINALIZING}; - } - } - } - - this.setState({ - ...this.state, - currentPhase: undefined // reset current phase - }); - - // Prepare issues for implementation - const currentIssues = await this.fetchAllIssues(true); - - // Implement the phase with user context (suggestions and images) - await this.implementPhase(phaseConcept, currentIssues, userContext); - - this.logger().info(`Phase ${phaseConcept.name} completed, generating next phase`); - - const phasesCounter = this.decrementPhasesCounter(); - - if ((phaseConcept.lastPhase || phasesCounter <= 0) && this.state.pendingUserInputs.length === 0) return {currentDevState: CurrentDevState.FINALIZING}; - return {currentDevState: CurrentDevState.PHASE_GENERATING}; - } catch (error) { - this.logger().error("Error implementing phase", error); - if (error instanceof RateLimitExceededError) { - throw error; - } - return {currentDevState: CurrentDevState.IDLE}; - } - } - - /** - * Execute review cycle state - review and cleanup + * Abstract method to be implemented by subclasses + * Contains the main logic for code generation and review process */ - async executeReviewCycle(): Promise { - this.logger().info("Executing REVIEWING state - review and cleanup"); - if (this.state.reviewingInitiated) { - this.logger().info("Reviewing already initiated, skipping"); - return CurrentDevState.IDLE; - } - this.setState({ - ...this.state, - reviewingInitiated: true - }); - - // If issues/errors found, prompt user if they want to review and cleanup - const issues = await this.fetchAllIssues(false); - if (issues.runtimeErrors.length > 0 || issues.staticAnalysis.typecheck.issues.length > 0) { - this.logger().info("Reviewing stage - issues found, prompting user to review and cleanup"); - const message : ConversationMessage = { - role: "assistant", - content: `If the user responds with yes, launch the 'deep_debug' tool with the prompt to fix all the issues in the app\nThere might be some bugs in the app. Do you want me to try to fix them?`, - conversationId: IdGenerator.generateConversationId(), - } - // Store the message in the conversation history so user's response can trigger the deep debug tool - this.addConversationMessage(message); - - this.broadcast(WebSocketMessageResponses.CONVERSATION_RESPONSE, { - message: message.content, - conversationId: message.conversationId, - isStreaming: false, - }); - } - - return CurrentDevState.IDLE; - } - - /** - * Execute finalizing state - final review and cleanup (runs only once) - */ - async executeFinalizing(): Promise { - this.logger().info("Executing FINALIZING state - final review and cleanup"); - - // Only do finalizing stage if it wasn't done before - if (this.state.mvpGenerated) { - this.logger().info("Finalizing stage already done"); - return CurrentDevState.REVIEWING; - } - this.setState({ - ...this.state, - mvpGenerated: true - }); - - const { result: phaseConcept, userContext } = await this.executePhaseGeneration(true); - if (!phaseConcept) { - this.logger().warn("Phase concept not generated, skipping final review"); - return CurrentDevState.REVIEWING; - } - - await this.executePhaseImplementation(phaseConcept, userContext); - - const numFilesGenerated = this.fileManager.getGeneratedFilePaths().length; - this.logger().info(`Finalization complete. Generated ${numFilesGenerated}/${this.getTotalFiles()} files.`); - - // Transition to IDLE - generation complete - return CurrentDevState.REVIEWING; - } + abstract build(): Promise async executeDeepDebug( issue: string, @@ -1126,7 +697,7 @@ export class SimpleCodeGeneratorAgent extends Agent { const out = await dbg.run( { issue, previousTranscript }, - { filesIndex, agent: this.codingAgent, runtimeErrors }, + { filesIndex, agent: this, runtimeErrors }, streamCb, toolRenderer, ); @@ -1153,195 +724,6 @@ export class SimpleCodeGeneratorAgent extends Agent { return await debugPromise; } - /** - * Generate next phase with user context (suggestions and images) - */ - async generateNextPhase(currentIssues: AllIssues, userContext?: UserContext, isFinal?: boolean): Promise { - const issues = IssueReport.from(currentIssues); - - // Build notification message - let notificationMsg = "Generating next phase"; - if (isFinal) { - notificationMsg = "Generating final phase"; - } - if (userContext?.suggestions && userContext.suggestions.length > 0) { - notificationMsg = `Generating next phase incorporating ${userContext.suggestions.length} user suggestion(s)`; - } - if (userContext?.images && userContext.images.length > 0) { - notificationMsg += ` with ${userContext.images.length} image(s)`; - } - - // Notify phase generation start - this.broadcast(WebSocketMessageResponses.PHASE_GENERATING, { - message: notificationMsg, - issues: issues, - userSuggestions: userContext?.suggestions, - }); - - const result = await this.operations.generateNextPhase.execute( - { - issues, - userContext, - isUserSuggestedPhase: userContext?.suggestions && userContext.suggestions.length > 0 && this.state.mvpGenerated, - isFinal: isFinal ?? false, - }, - this.getOperationOptions() - ) - // Execute install commands if any - if (result.installCommands && result.installCommands.length > 0) { - this.executeCommands(result.installCommands); - } - - // Execute delete commands if any - const filesToDelete = result.files.filter(f => f.changes?.toLowerCase().trim() === 'delete'); - if (filesToDelete.length > 0) { - this.logger().info(`Deleting ${filesToDelete.length} files: ${filesToDelete.map(f => f.path).join(", ")}`); - this.deleteFiles(filesToDelete.map(f => f.path)); - } - - if (result.files.length === 0) { - this.logger().info("No files generated for next phase"); - // Notify phase generation complete - this.broadcast(WebSocketMessageResponses.PHASE_GENERATED, { - message: `No files generated for next phase`, - phase: undefined - }); - return undefined; - } - - this.createNewIncompletePhase(result); - // Notify phase generation complete - this.broadcast(WebSocketMessageResponses.PHASE_GENERATED, { - message: `Generated next phase: ${result.name}`, - phase: result - }); - - return result; - } - - /** - * Implement a single phase of code generation - * Streams file generation with real-time updates and incorporates technical instructions - */ - async implementPhase(phase: PhaseConceptType, currentIssues: AllIssues, userContext?: UserContext, streamChunks: boolean = true, postPhaseFixing: boolean = true): Promise { - const issues = IssueReport.from(currentIssues); - - const implementationMsg = userContext?.suggestions && userContext.suggestions.length > 0 - ? `Implementing phase: ${phase.name} with ${userContext.suggestions.length} user suggestion(s)` - : `Implementing phase: ${phase.name}`; - const msgWithImages = userContext?.images && userContext.images.length > 0 - ? `${implementationMsg} and ${userContext.images.length} image(s)` - : implementationMsg; - - this.broadcast(WebSocketMessageResponses.PHASE_IMPLEMENTING, { - message: msgWithImages, - phase: phase, - issues: issues, - }); - - - const result = await this.operations.implementPhase.execute( - { - phase, - issues, - isFirstPhase: this.state.generatedPhases.filter(p => p.completed).length === 0, - fileGeneratingCallback: (filePath: string, filePurpose: string) => { - this.broadcast(WebSocketMessageResponses.FILE_GENERATING, { - message: `Generating file: ${filePath}`, - filePath: filePath, - filePurpose: filePurpose - }); - }, - userContext, - shouldAutoFix: this.state.inferenceContext.enableRealtimeCodeFix, - fileChunkGeneratedCallback: streamChunks ? (filePath: string, chunk: string, format: 'full_content' | 'unified_diff') => { - this.broadcast(WebSocketMessageResponses.FILE_CHUNK_GENERATED, { - message: `Generating file: ${filePath}`, - filePath: filePath, - chunk, - format, - }); - } : (_filePath: string, _chunk: string, _format: 'full_content' | 'unified_diff') => {}, - fileClosedCallback: (file: FileOutputType, message: string) => { - this.broadcast(WebSocketMessageResponses.FILE_GENERATED, { - message, - file, - }); - } - }, - this.getOperationOptions() - ); - - this.broadcast(WebSocketMessageResponses.PHASE_VALIDATING, { - message: `Validating files for phase: ${phase.name}`, - phase: phase, - }); - - // Await the already-created realtime code fixer promises - const finalFiles = await Promise.allSettled(result.fixedFilePromises).then((results: PromiseSettledResult[]) => { - return results.map((result) => { - if (result.status === 'fulfilled') { - return result.value; - } else { - return null; - } - }).filter((f): f is FileOutputType => f !== null); - }); - - // Update state with completed phase - await this.fileManager.saveGeneratedFiles(finalFiles, `feat: ${phase.name}\n\n${phase.description}`); - - this.logger().info("Files generated for phase:", phase.name, finalFiles.map(f => f.filePath)); - - // Execute commands if provided - if (result.commands && result.commands.length > 0) { - this.logger().info("Phase implementation suggested install commands:", result.commands); - await this.executeCommands(result.commands, false); - } - - // Deploy generated files - if (finalFiles.length > 0) { - await this.deployToSandbox(finalFiles, false, phase.name, true); - if (postPhaseFixing) { - await this.applyDeterministicCodeFixes(); - if (this.state.inferenceContext.enableFastSmartCodeFix) { - await this.applyFastSmartCodeFixes(); - } - } - } - - // Validation complete - this.broadcast(WebSocketMessageResponses.PHASE_VALIDATED, { - message: `Files validated for phase: ${phase.name}`, - phase: phase - }); - - this.logger().info("Files generated for phase:", phase.name, finalFiles.map(f => f.filePath)); - - this.logger().info(`Validation complete for phase: ${phase.name}`); - - // Notify phase completion - this.broadcast(WebSocketMessageResponses.PHASE_IMPLEMENTED, { - phase: { - name: phase.name, - files: finalFiles.map(f => ({ - path: f.filePath, - purpose: f.filePurpose, - contents: f.fileContents - })), - description: phase.description - }, - message: "Files generated successfully for phase" - }); - - this.markPhaseComplete(phase.name); - - return { - files: finalFiles, - deploymentNeeded: result.deploymentNeeded, - commands: result.commands - }; - } getModelConfigsInfo() { const modelService = new ModelConfigService(this.env); @@ -1349,7 +731,7 @@ export class SimpleCodeGeneratorAgent extends Agent { } getTotalFiles(): number { - return this.fileManager.getGeneratedFilePaths().length + ((this.state.currentPhase || this.state.blueprint.initialPhase)?.files?.length || 0); + return this.fileManager.getGeneratedFilePaths().length } getSummary(): Promise { @@ -1361,25 +743,18 @@ export class SimpleCodeGeneratorAgent extends Agent { return Promise.resolve(summaryData); } - async getFullState(): Promise { + async getFullState(): Promise { return this.state; } - private migrateStateIfNeeded(): void { - const migratedState = StateMigration.migrateIfNeeded(this.state, this.logger()); - if (migratedState) { - this.setState(migratedState); - } + protected migrateStateIfNeeded(): void { + // no-op, only older phasic agents need this, for now. } getFileGenerated(filePath: string) { return this.fileManager!.getGeneratedFile(filePath) || null; } - getWebSockets(): WebSocket[] { - return this.ctx.getWebSockets(); - } - async fetchRuntimeErrors(clear: boolean = true, shouldWait: boolean = true): Promise { if (shouldWait) { await this.deploymentManager.waitForPreview(); @@ -1433,41 +808,10 @@ export class SimpleCodeGeneratorAgent extends Agent { } } - private async applyFastSmartCodeFixes() : Promise { - try { - const startTime = Date.now(); - this.logger().info("Applying fast smart code fixes"); - // Get static analysis and do deterministic fixes - const staticAnalysis = await this.runStaticAnalysisCode(); - if (staticAnalysis.typecheck.issues.length + staticAnalysis.lint.issues.length == 0) { - this.logger().info("No issues found, skipping fast smart code fixes"); - return; - } - const issues = staticAnalysis.typecheck.issues.concat(staticAnalysis.lint.issues); - const allFiles = this.fileManager.getAllRelevantFiles(); - - const fastCodeFixer = await this.operations.fastCodeFixer.execute({ - query: this.state.query, - issues, - allFiles, - }, this.getOperationOptions()); - - if (fastCodeFixer.length > 0) { - await this.fileManager.saveGeneratedFiles(fastCodeFixer, "fix: Fast smart code fixes"); - await this.deployToSandbox(fastCodeFixer); - this.logger().info("Fast smart code fixes applied successfully"); - } - this.logger().info(`Fast smart code fixes applied in ${Date.now() - startTime}ms`); - } catch (error) { - this.broadcastError("Failed to apply fast smart code fixes", error); - return; - } - } - /** * Apply deterministic code fixes for common TypeScript errors */ - private async applyDeterministicCodeFixes() : Promise { + protected async applyDeterministicCodeFixes() : Promise { try { // Get static analysis and do deterministic fixes const staticAnalysis = await this.runStaticAnalysisCode(); @@ -1555,7 +899,7 @@ export class SimpleCodeGeneratorAgent extends Agent { try { const valid = /^[a-z0-9-_]{3,50}$/.test(newName); if (!valid) return false; - const updatedBlueprint = { ...this.state.blueprint, projectName: newName } as Blueprint; + const updatedBlueprint = { ...this.state.blueprint, projectName: newName }; this.setState({ ...this.state, blueprint: updatedBlueprint @@ -1587,13 +931,17 @@ export class SimpleCodeGeneratorAgent extends Agent { } } + /** + * Update user-facing blueprint fields + * Only allows updating safe, cosmetic fields - not internal generation state + */ async updateBlueprint(patch: Partial): Promise { - const keys = Object.keys(patch) as (keyof Blueprint)[]; - const allowed = new Set([ + // Fields that are safe to update after generation starts + // Excludes: initialPhase (breaks generation), plan (internal state) + const safeUpdatableFields = new Set([ 'title', - 'projectName', - 'detailedDescription', 'description', + 'detailedDescription', 'colorPalette', 'views', 'userFlow', @@ -1603,25 +951,32 @@ export class SimpleCodeGeneratorAgent extends Agent { 'frameworks', 'implementationRoadmap' ]); - const filtered: Partial = {}; - for (const k of keys) { - if (allowed.has(k) && typeof (patch as any)[k] !== 'undefined') { - (filtered as any)[k] = (patch as any)[k]; + + // Filter to only safe fields + const filtered: Record = {}; + for (const [key, value] of Object.entries(patch)) { + if (safeUpdatableFields.has(key) && value !== undefined) { + filtered[key] = value; } } - if (typeof filtered.projectName === 'string' && filtered.projectName) { - await this.updateProjectName(filtered.projectName); - delete (filtered as any).projectName; + + // projectName requires sandbox update, handle separately + if ('projectName' in patch && typeof patch.projectName === 'string') { + await this.updateProjectName(patch.projectName); } - const updated: Blueprint = { ...this.state.blueprint, ...(filtered as Blueprint) } as Blueprint; + + // Merge and update state + const updated = { ...this.state.blueprint, ...filtered } as Blueprint; this.setState({ ...this.state, blueprint: updated }); + this.broadcast(WebSocketMessageResponses.BLUEPRINT_UPDATED, { message: 'Blueprint updated', updatedKeys: Object.keys(filtered) }); + return updated; } @@ -1775,6 +1130,16 @@ export class SimpleCodeGeneratorAgent extends Agent { }) }; } + // A wrapper for LLM tool to deploy to sandbox + async deployPreview(clearLogs: boolean = true, forceRedeploy: boolean = false): Promise { + const response = await this.deployToSandbox([], forceRedeploy, undefined, clearLogs); + if (response && response.previewURL) { + this.broadcast(WebSocketMessageResponses.PREVIEW_FORCE_REFRESH, {}); + return `Deployment successful: ${response.previewURL}`; + } + return `Failed to deploy: ${response?.tunnelURL}`; + } + async deployToSandbox(files: FileOutputType[] = [], redeploy: boolean = false, commitMessage?: string, clearLogs: boolean = false): Promise { // Invalidate static analysis cache this.staticAnalysisCache = null; @@ -1957,14 +1322,14 @@ export class SimpleCodeGeneratorAgent extends Agent { handleWebSocketClose(connection); } - private async onProjectUpdate(message: string): Promise { + protected async onProjectUpdate(message: string): Promise { this.setState({ ...this.state, projectUpdatesAccumulator: [...this.state.projectUpdatesAccumulator, message] }); } - private async getAndResetProjectUpdates() { + protected async getAndResetProjectUpdates() { const projectUpdates = this.state.projectUpdatesAccumulator || []; this.setState({ ...this.state, @@ -1984,14 +1349,14 @@ export class SimpleCodeGeneratorAgent extends Agent { broadcastToConnections(this, msg, data || {} as WebSocketMessageData); } - private getBootstrapCommands() { + protected getBootstrapCommands() { const bootstrapCommands = this.state.commandsHistory || []; // Validate, deduplicate, and clean const { validCommands } = validateAndCleanBootstrapCommands(bootstrapCommands); return validCommands; } - private async saveExecutedCommands(commands: string[]) { + protected async saveExecutedCommands(commands: string[]) { this.logger().info('Saving executed commands', { commands }); // Merge with existing history @@ -2037,7 +1402,7 @@ export class SimpleCodeGeneratorAgent extends Agent { * Execute commands with retry logic * Chunks commands and retries failed ones with AI assistance */ - private async executeCommands(commands: string[], shouldRetry: boolean = true, chunkSize: number = 5): Promise { + protected async executeCommands(commands: string[], shouldRetry: boolean = true, chunkSize: number = 5): Promise { const state = this.state; if (!state.sandboxInstanceId) { this.logger().warn('No sandbox instance available for executing commands'); @@ -2171,7 +1536,7 @@ export class SimpleCodeGeneratorAgent extends Agent { * Sync package.json from sandbox to agent's git repository * Called after install/add/remove commands to keep dependencies in sync */ - private async syncPackageJsonFromSandbox(): Promise { + protected async syncPackageJsonFromSandbox(): Promise { try { this.logger().info('Fetching current package.json from sandbox'); const results = await this.readFiles(['package.json']); diff --git a/worker/agents/core/phasic/behavior.ts b/worker/agents/core/phasic/behavior.ts new file mode 100644 index 00000000..cf69d48e --- /dev/null +++ b/worker/agents/core/phasic/behavior.ts @@ -0,0 +1,852 @@ +import { + PhaseConceptGenerationSchemaType, + PhaseConceptType, + FileConceptType, + FileOutputType, + PhaseImplementationSchemaType, +} from '../../schemas'; +import { StaticAnalysisResponse } from '../../../services/sandbox/sandboxTypes'; +import { CurrentDevState, MAX_PHASES, PhasicState } from '../state'; +import { AllIssues, AgentInitArgs, PhaseExecutionResult, UserContext } from '../types'; +import { WebSocketMessageResponses } from '../../constants'; +import { UserConversationProcessor } from '../../operations/UserConversationProcessor'; +import { DeploymentManager } from '../../services/implementations/DeploymentManager'; +// import { WebSocketBroadcaster } from '../services/implementations/WebSocketBroadcaster'; +import { GenerationContext, PhasicGenerationContext } from '../../domain/values/GenerationContext'; +import { IssueReport } from '../../domain/values/IssueReport'; +import { PhaseImplementationOperation } from '../../operations/PhaseImplementation'; +import { FileRegenerationOperation } from '../../operations/FileRegeneration'; +import { PhaseGenerationOperation } from '../../operations/PhaseGeneration'; +// Database schema imports removed - using zero-storage OAuth flow +import { AgentActionKey } from '../../inferutils/config.types'; +import { AGENT_CONFIG } from '../../inferutils/config'; +import { ModelConfigService } from '../../../database/services/ModelConfigService'; +import { FastCodeFixerOperation } from '../../operations/PostPhaseCodeFixer'; +import { customizePackageJson, customizeTemplateFiles, generateProjectName } from '../../utils/templateCustomizer'; +import { generateBlueprint } from '../../planning/blueprint'; +import { RateLimitExceededError } from 'shared/types/errors'; +import { type ProcessedImageAttachment } from '../../../types/image-attachment'; +import { OperationOptions } from '../../operations/common'; +import { ConversationMessage } from '../../inferutils/common'; +import { generateNanoId } from 'worker/utils/idGenerator'; +import { IdGenerator } from '../../utils/idGenerator'; +import { BaseAgentBehavior, BaseAgentOperations } from '../baseAgent'; +import { ICodingAgent } from '../../services/interfaces/ICodingAgent'; +import { SimpleCodeGenerationOperation } from '../../operations/SimpleCodeGeneration'; + +interface PhasicOperations extends BaseAgentOperations { + generateNextPhase: PhaseGenerationOperation; + implementPhase: PhaseImplementationOperation; +} + +/** + * PhasicAgentBehavior - Deterministically orchestrated agent + * + * Manages the lifecycle of code generation including: + * - Blueprint, phase generation, phase implementation, review cycles orchestrations + * - File streaming with WebSocket updates + * - Code validation and error correction + * - Deployment to sandbox service + */ +export class PhasicAgentBehavior extends BaseAgentBehavior implements ICodingAgent { + protected operations: PhasicOperations = { + regenerateFile: new FileRegenerationOperation(), + fastCodeFixer: new FastCodeFixerOperation(), + processUserMessage: new UserConversationProcessor(), + simpleGenerateFiles: new SimpleCodeGenerationOperation(), + generateNextPhase: new PhaseGenerationOperation(), + implementPhase: new PhaseImplementationOperation(), + }; + + /** + * Initialize the code generator with project blueprint and template + * Sets up services and begins deployment process + */ + async initialize( + initArgs: AgentInitArgs, + ..._args: unknown[] + ): Promise { + await super.initialize(initArgs); + + const { query, language, frameworks, hostname, inferenceContext, templateInfo } = initArgs; + const sandboxSessionId = DeploymentManager.generateNewSessionId(); + + // Generate a blueprint + this.logger().info('Generating blueprint', { query, queryLength: query.length, imagesCount: initArgs.images?.length || 0 }); + this.logger().info(`Using language: ${language}, frameworks: ${frameworks ? frameworks.join(", ") : "none"}`); + + const blueprint = await generateBlueprint({ + env: this.env, + inferenceContext, + query, + language: language!, + frameworks: frameworks!, + templateDetails: templateInfo.templateDetails, + templateMetaInfo: templateInfo.selection, + images: initArgs.images, + stream: { + chunk_size: 256, + onChunk: (chunk) => { + // initArgs.writer.write({chunk}); + initArgs.onBlueprintChunk(chunk); + } + } + }) + + const packageJson = templateInfo.templateDetails?.allFiles['package.json']; + + this.templateDetailsCache = templateInfo.templateDetails; + + const projectName = generateProjectName( + blueprint?.projectName || templateInfo.templateDetails.name, + generateNanoId(), + PhasicAgentBehavior.PROJECT_NAME_PREFIX_MAX_LENGTH + ); + + this.logger().info('Generated project name', { projectName }); + + this.setState({ + ...this.state, + projectName, + query, + blueprint, + templateName: templateInfo.templateDetails.name, + sandboxInstanceId: undefined, + generatedPhases: [], + commandsHistory: [], + lastPackageJson: packageJson, + sessionId: sandboxSessionId, + hostname, + inferenceContext, + }); + + await this.gitInit(); + + // Customize template files (package.json, wrangler.jsonc, .bootstrap.js, .gitignore) + const customizedFiles = customizeTemplateFiles( + templateInfo.templateDetails.allFiles, + { + projectName, + commandsHistory: [] // Empty initially, will be updated later + } + ); + + this.logger().info('Customized template files', { + files: Object.keys(customizedFiles) + }); + + // Save customized files to git + const filesToSave = Object.entries(customizedFiles).map(([filePath, content]) => ({ + filePath, + fileContents: content, + filePurpose: 'Project configuration file' + })); + + await this.fileManager.saveGeneratedFiles( + filesToSave, + 'Initialize project configuration files' + ); + + this.logger().info('Committed customized template files to git'); + + this.initializeAsync().catch((error: unknown) => { + this.broadcastError("Initialization failed", error); + }); + this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} initialized successfully`); + await this.saveToDatabase(); + return this.state; + } + + async onStart(props?: Record | undefined): Promise { + await super.onStart(props); + + // migrate overwritten package.jsons + const oldPackageJson = this.fileManager.getFile('package.json')?.fileContents || this.state.lastPackageJson; + if (oldPackageJson) { + const packageJson = customizePackageJson(oldPackageJson, this.state.projectName); + this.fileManager.saveGeneratedFiles([ + { + filePath: 'package.json', + fileContents: packageJson, + filePurpose: 'Project configuration file' + } + ], 'chore: fix overwritten package.json'); + } + } + + setState(state: PhasicState): void { + try { + super.setState(state); + } catch (error) { + this.broadcastError("Error setting state", error); + this.logger().error("State details:", { + originalState: JSON.stringify(this.state, null, 2), + newState: JSON.stringify(state, null, 2) + }); + } + } + + rechargePhasesCounter(max_phases: number = MAX_PHASES): void { + if (this.getPhasesCounter() <= max_phases) { + this.setState({ + ...this.state, + phasesCounter: max_phases + }); + } + } + + decrementPhasesCounter(): number { + const counter = this.getPhasesCounter() - 1; + this.setState({ + ...this.state, + phasesCounter: counter + }); + return counter; + } + + getPhasesCounter(): number { + return this.state.phasesCounter; + } + + getOperationOptions(): OperationOptions { + return { + env: this.env, + agentId: this.getAgentId(), + context: GenerationContext.from(this.state, this.getTemplateDetails(), this.logger()) as PhasicGenerationContext, + logger: this.logger(), + inferenceContext: this.getInferenceContext(), + agent: this + }; + } + + private createNewIncompletePhase(phaseConcept: PhaseConceptType) { + this.setState({ + ...this.state, + generatedPhases: [...this.state.generatedPhases, { + ...phaseConcept, + completed: false + }] + }) + + this.logger().info("Created new incomplete phase:", JSON.stringify(this.state.generatedPhases, null, 2)); + } + + private markPhaseComplete(phaseName: string) { + // First find the phase + const phases = this.state.generatedPhases; + if (!phases.some(p => p.name === phaseName)) { + this.logger().warn(`Phase ${phaseName} not found in generatedPhases array, skipping save`); + return; + } + + // Update the phase + this.setState({ + ...this.state, + generatedPhases: phases.map(p => p.name === phaseName ? { ...p, completed: true } : p) + }); + + this.logger().info("Completed phases:", JSON.stringify(phases, null, 2)); + } + + async queueUserRequest(request: string, images?: ProcessedImageAttachment[]): Promise { + this.rechargePhasesCounter(3); + await super.queueUserRequest(request, images); + } + + async build(): Promise { + await this.launchStateMachine(); + } + + private async launchStateMachine() { + this.logger().info("Launching state machine"); + + let currentDevState = CurrentDevState.PHASE_IMPLEMENTING; + const generatedPhases = this.state.generatedPhases; + const incompletedPhases = generatedPhases.filter(phase => !phase.completed); + let phaseConcept : PhaseConceptType | undefined; + if (incompletedPhases.length > 0) { + phaseConcept = incompletedPhases[incompletedPhases.length - 1]; + this.logger().info('Resuming code generation from incompleted phase', { + phase: phaseConcept + }); + } else if (generatedPhases.length > 0) { + currentDevState = CurrentDevState.PHASE_GENERATING; + this.logger().info('Resuming code generation after generating all phases', { + phase: generatedPhases[generatedPhases.length - 1] + }); + } else { + phaseConcept = this.state.blueprint.initialPhase; + this.logger().info('Starting code generation from initial phase', { + phase: phaseConcept + }); + this.createNewIncompletePhase(phaseConcept); + } + + let staticAnalysisCache: StaticAnalysisResponse | undefined; + let userContext: UserContext | undefined; + + try { + let executionResults: PhaseExecutionResult; + // State machine loop - continues until IDLE state + while (currentDevState !== CurrentDevState.IDLE) { + this.logger().info(`[generateAllFiles] Executing state: ${currentDevState}`); + switch (currentDevState) { + case CurrentDevState.PHASE_GENERATING: + executionResults = await this.executePhaseGeneration(); + currentDevState = executionResults.currentDevState; + phaseConcept = executionResults.result; + staticAnalysisCache = executionResults.staticAnalysis; + userContext = executionResults.userContext; + break; + case CurrentDevState.PHASE_IMPLEMENTING: + executionResults = await this.executePhaseImplementation(phaseConcept, staticAnalysisCache, userContext); + currentDevState = executionResults.currentDevState; + staticAnalysisCache = executionResults.staticAnalysis; + userContext = undefined; + break; + case CurrentDevState.REVIEWING: + currentDevState = await this.executeReviewCycle(); + break; + case CurrentDevState.FINALIZING: + currentDevState = await this.executeFinalizing(); + break; + default: + break; + } + } + + this.logger().info("State machine completed successfully"); + } catch (error) { + this.logger().error("Error in state machine:", error); + } + } + + /** + * Execute phase generation state - generate next phase with user suggestions + */ + async executePhaseGeneration(): Promise { + this.logger().info("Executing PHASE_GENERATING state"); + try { + const currentIssues = await this.fetchAllIssues(); + + // Generate next phase with user suggestions if available + + // Get stored images if user suggestions are present + const pendingUserInputs = this.fetchPendingUserRequests(); + const userContext = (pendingUserInputs.length > 0) + ? { + suggestions: pendingUserInputs, + images: this.pendingUserImages + } as UserContext + : undefined; + + if (userContext && userContext?.suggestions && userContext.suggestions.length > 0) { + // Only reset pending user inputs if user suggestions were read + this.logger().info("Resetting pending user inputs", { + userSuggestions: userContext.suggestions, + hasImages: !!userContext.images, + imageCount: userContext.images?.length || 0 + }); + + // Clear images after they're passed to phase generation + if (userContext?.images && userContext.images.length > 0) { + this.logger().info('Clearing stored user images after passing to phase generation'); + this.pendingUserImages = []; + } + } + + const nextPhase = await this.generateNextPhase(currentIssues, userContext); + + if (!nextPhase) { + this.logger().info("No more phases to implement, transitioning to FINALIZING"); + return { + currentDevState: CurrentDevState.FINALIZING, + }; + } + + // Store current phase and transition to implementation + this.setState({ + ...this.state, + currentPhase: nextPhase + }); + + return { + currentDevState: CurrentDevState.PHASE_IMPLEMENTING, + result: nextPhase, + staticAnalysis: currentIssues.staticAnalysis, + userContext: userContext, + }; + } catch (error) { + if (error instanceof RateLimitExceededError) { + throw error; + } + this.broadcastError("Error generating phase", error); + return { + currentDevState: CurrentDevState.IDLE, + }; + } + } + + /** + * Execute phase implementation state - implement current phase + */ + async executePhaseImplementation(phaseConcept?: PhaseConceptType, staticAnalysis?: StaticAnalysisResponse, userContext?: UserContext): Promise<{currentDevState: CurrentDevState, staticAnalysis?: StaticAnalysisResponse}> { + try { + this.logger().info("Executing PHASE_IMPLEMENTING state"); + + if (phaseConcept === undefined) { + phaseConcept = this.state.currentPhase; + if (phaseConcept === undefined) { + this.logger().error("No phase concept provided to implement, will call phase generation"); + const results = await this.executePhaseGeneration(); + phaseConcept = results.result; + if (phaseConcept === undefined) { + this.logger().error("No phase concept provided to implement, will return"); + return {currentDevState: CurrentDevState.FINALIZING}; + } + } + } + + this.setState({ + ...this.state, + currentPhase: undefined // reset current phase + }); + + let currentIssues : AllIssues; + if (this.state.sandboxInstanceId) { + if (staticAnalysis) { + // If have cached static analysis, fetch everything else fresh + currentIssues = { + runtimeErrors: await this.fetchRuntimeErrors(true), + staticAnalysis: staticAnalysis, + }; + } else { + currentIssues = await this.fetchAllIssues(true) + } + } else { + currentIssues = { + runtimeErrors: [], + staticAnalysis: { success: true, lint: { issues: [] }, typecheck: { issues: [] } }, + } + } + // Implement the phase with user context (suggestions and images) + await this.implementPhase(phaseConcept, currentIssues, userContext); + + this.logger().info(`Phase ${phaseConcept.name} completed, generating next phase`); + + const phasesCounter = this.decrementPhasesCounter(); + + if ((phaseConcept.lastPhase || phasesCounter <= 0) && this.state.pendingUserInputs.length === 0) return {currentDevState: CurrentDevState.FINALIZING, staticAnalysis: staticAnalysis}; + return {currentDevState: CurrentDevState.PHASE_GENERATING, staticAnalysis: staticAnalysis}; + } catch (error) { + this.logger().error("Error implementing phase", error); + if (error instanceof RateLimitExceededError) { + throw error; + } + return {currentDevState: CurrentDevState.IDLE}; + } + } + + /** + * Execute review cycle state - review and cleanup + */ + async executeReviewCycle(): Promise { + this.logger().info("Executing REVIEWING state - review and cleanup"); + if (this.state.reviewingInitiated) { + this.logger().info("Reviewing already initiated, skipping"); + return CurrentDevState.IDLE; + } + this.setState({ + ...this.state, + reviewingInitiated: true + }); + + // If issues/errors found, prompt user if they want to review and cleanup + const issues = await this.fetchAllIssues(false); + if (issues.runtimeErrors.length > 0 || issues.staticAnalysis.typecheck.issues.length > 0) { + this.logger().info("Reviewing stage - issues found, prompting user to review and cleanup"); + const message : ConversationMessage = { + role: "assistant", + content: `If the user responds with yes, launch the 'deep_debug' tool with the prompt to fix all the issues in the app\nThere might be some bugs in the app. Do you want me to try to fix them?`, + conversationId: IdGenerator.generateConversationId(), + } + // Store the message in the conversation history so user's response can trigger the deep debug tool + this.addConversationMessage(message); + + this.broadcast(WebSocketMessageResponses.CONVERSATION_RESPONSE, { + message: message.content, + conversationId: message.conversationId, + isStreaming: false, + }); + } + + return CurrentDevState.IDLE; + } + + /** + * Execute finalizing state - final review and cleanup (runs only once) + */ + async executeFinalizing(): Promise { + this.logger().info("Executing FINALIZING state - final review and cleanup"); + + // Only do finalizing stage if it wasn't done before + if (this.state.mvpGenerated) { + this.logger().info("Finalizing stage already done"); + return CurrentDevState.REVIEWING; + } + this.setState({ + ...this.state, + mvpGenerated: true + }); + + const phaseConcept: PhaseConceptType = { + name: "Finalization and Review", + description: "Full polishing and final review of the application", + files: [], + lastPhase: true + } + + this.createNewIncompletePhase(phaseConcept); + + const currentIssues = await this.fetchAllIssues(true); + + // Run final review and cleanup phase + await this.implementPhase(phaseConcept, currentIssues); + + const numFilesGenerated = this.fileManager.getGeneratedFilePaths().length; + this.logger().info(`Finalization complete. Generated ${numFilesGenerated}/${this.getTotalFiles()} files.`); + + // Transition to IDLE - generation complete + return CurrentDevState.REVIEWING; + } + + /** + * Generate next phase with user context (suggestions and images) + */ + async generateNextPhase(currentIssues: AllIssues, userContext?: UserContext): Promise { + const issues = IssueReport.from(currentIssues); + + // Build notification message + let notificationMsg = "Generating next phase"; + if (userContext?.suggestions && userContext.suggestions.length > 0) { + notificationMsg = `Generating next phase incorporating ${userContext.suggestions.length} user suggestion(s)`; + } + if (userContext?.images && userContext.images.length > 0) { + notificationMsg += ` with ${userContext.images.length} image(s)`; + } + + // Notify phase generation start + this.broadcast(WebSocketMessageResponses.PHASE_GENERATING, { + message: notificationMsg, + issues: issues, + userSuggestions: userContext?.suggestions, + }); + + const result = await this.operations.generateNextPhase.execute( + { + issues, + userContext, + isUserSuggestedPhase: userContext?.suggestions && userContext.suggestions.length > 0 && this.state.mvpGenerated, + }, + this.getOperationOptions() + ) + // Execute install commands if any + if (result.installCommands && result.installCommands.length > 0) { + this.executeCommands(result.installCommands); + } + + // Execute delete commands if any + const filesToDelete = result.files.filter(f => f.changes?.toLowerCase().trim() === 'delete'); + if (filesToDelete.length > 0) { + this.logger().info(`Deleting ${filesToDelete.length} files: ${filesToDelete.map(f => f.path).join(", ")}`); + this.deleteFiles(filesToDelete.map(f => f.path)); + } + + if (result.files.length === 0) { + this.logger().info("No files generated for next phase"); + // Notify phase generation complete + this.broadcast(WebSocketMessageResponses.PHASE_GENERATED, { + message: `No files generated for next phase`, + phase: undefined + }); + return undefined; + } + + this.createNewIncompletePhase(result); + // Notify phase generation complete + this.broadcast(WebSocketMessageResponses.PHASE_GENERATED, { + message: `Generated next phase: ${result.name}`, + phase: result + }); + + return result; + } + + /** + * Implement a single phase of code generation + * Streams file generation with real-time updates and incorporates technical instructions + */ + async implementPhase(phase: PhaseConceptType, currentIssues: AllIssues, userContext?: UserContext, streamChunks: boolean = true, postPhaseFixing: boolean = true): Promise { + const issues = IssueReport.from(currentIssues); + + const implementationMsg = userContext?.suggestions && userContext.suggestions.length > 0 + ? `Implementing phase: ${phase.name} with ${userContext.suggestions.length} user suggestion(s)` + : `Implementing phase: ${phase.name}`; + const msgWithImages = userContext?.images && userContext.images.length > 0 + ? `${implementationMsg} and ${userContext.images.length} image(s)` + : implementationMsg; + + this.broadcast(WebSocketMessageResponses.PHASE_IMPLEMENTING, { + message: msgWithImages, + phase: phase, + issues: issues, + }); + + + const result = await this.operations.implementPhase.execute( + { + phase, + issues, + isFirstPhase: this.state.generatedPhases.filter(p => p.completed).length === 0, + fileGeneratingCallback: (filePath: string, filePurpose: string) => { + this.broadcast(WebSocketMessageResponses.FILE_GENERATING, { + message: `Generating file: ${filePath}`, + filePath: filePath, + filePurpose: filePurpose + }); + }, + userContext, + shouldAutoFix: this.state.inferenceContext.enableRealtimeCodeFix, + fileChunkGeneratedCallback: streamChunks ? (filePath: string, chunk: string, format: 'full_content' | 'unified_diff') => { + this.broadcast(WebSocketMessageResponses.FILE_CHUNK_GENERATED, { + message: `Generating file: ${filePath}`, + filePath: filePath, + chunk, + format, + }); + } : (_filePath: string, _chunk: string, _format: 'full_content' | 'unified_diff') => {}, + fileClosedCallback: (file: FileOutputType, message: string) => { + this.broadcast(WebSocketMessageResponses.FILE_GENERATED, { + message, + file, + }); + } + }, + this.getOperationOptions() + ); + + this.broadcast(WebSocketMessageResponses.PHASE_VALIDATING, { + message: `Validating files for phase: ${phase.name}`, + phase: phase, + }); + + // Await the already-created realtime code fixer promises + const finalFiles = await Promise.allSettled(result.fixedFilePromises).then((results: PromiseSettledResult[]) => { + return results.map((result) => { + if (result.status === 'fulfilled') { + return result.value; + } else { + return null; + } + }).filter((f): f is FileOutputType => f !== null); + }); + + // Update state with completed phase + await this.fileManager.saveGeneratedFiles(finalFiles, `feat: ${phase.name}\n\n${phase.description}`); + + this.logger().info("Files generated for phase:", phase.name, finalFiles.map(f => f.filePath)); + + // Execute commands if provided + if (result.commands && result.commands.length > 0) { + this.logger().info("Phase implementation suggested install commands:", result.commands); + await this.executeCommands(result.commands, false); + } + + // Deploy generated files + if (finalFiles.length > 0) { + await this.deployToSandbox(finalFiles, false, phase.name, true); + if (postPhaseFixing) { + await this.applyDeterministicCodeFixes(); + if (this.state.inferenceContext.enableFastSmartCodeFix) { + await this.applyFastSmartCodeFixes(); + } + } + } + + // Validation complete + this.broadcast(WebSocketMessageResponses.PHASE_VALIDATED, { + message: `Files validated for phase: ${phase.name}`, + phase: phase + }); + + this.logger().info("Files generated for phase:", phase.name, finalFiles.map(f => f.filePath)); + + this.logger().info(`Validation complete for phase: ${phase.name}`); + + // Notify phase completion + this.broadcast(WebSocketMessageResponses.PHASE_IMPLEMENTED, { + phase: { + name: phase.name, + files: finalFiles.map(f => ({ + path: f.filePath, + purpose: f.filePurpose, + contents: f.fileContents + })), + description: phase.description + }, + message: "Files generated successfully for phase" + }); + + this.markPhaseComplete(phase.name); + + return { + files: finalFiles, + deploymentNeeded: result.deploymentNeeded, + commands: result.commands + }; + } + + /** + * Get current model configurations (defaults + user overrides) + * Used by WebSocket to provide configuration info to frontend + */ + async getModelConfigsInfo() { + const userId = this.state.inferenceContext.userId; + if (!userId) { + throw new Error('No user session available for model configurations'); + } + + try { + const modelConfigService = new ModelConfigService(this.env); + + // Get all user configs + const userConfigsRecord = await modelConfigService.getUserModelConfigs(userId); + + // Transform to match frontend interface + const agents = Object.entries(AGENT_CONFIG).map(([key, config]) => ({ + key, + name: config.name, + description: config.description + })); + + const userConfigs: Record = {}; + const defaultConfigs: Record = {}; + + for (const [actionKey, mergedConfig] of Object.entries(userConfigsRecord)) { + if (mergedConfig.isUserOverride) { + userConfigs[actionKey] = { + name: mergedConfig.name, + max_tokens: mergedConfig.max_tokens, + temperature: mergedConfig.temperature, + reasoning_effort: mergedConfig.reasoning_effort, + fallbackModel: mergedConfig.fallbackModel, + isUserOverride: true + }; + } + + // Always include default config + const defaultConfig = AGENT_CONFIG[actionKey as AgentActionKey]; + if (defaultConfig) { + defaultConfigs[actionKey] = { + name: defaultConfig.name, + max_tokens: defaultConfig.max_tokens, + temperature: defaultConfig.temperature, + reasoning_effort: defaultConfig.reasoning_effort, + fallbackModel: defaultConfig.fallbackModel + }; + } + } + + return { + agents, + userConfigs, + defaultConfigs + }; + } catch (error) { + this.logger().error('Error fetching model configs info:', error); + throw error; + } + } + + getTotalFiles(): number { + return this.fileManager.getGeneratedFilePaths().length + ((this.state.currentPhase || this.state.blueprint.initialPhase)?.files?.length || 0); + } + + private async applyFastSmartCodeFixes() : Promise { + try { + const startTime = Date.now(); + this.logger().info("Applying fast smart code fixes"); + // Get static analysis and do deterministic fixes + const staticAnalysis = await this.runStaticAnalysisCode(); + if (staticAnalysis.typecheck.issues.length + staticAnalysis.lint.issues.length == 0) { + this.logger().info("No issues found, skipping fast smart code fixes"); + return; + } + const issues = staticAnalysis.typecheck.issues.concat(staticAnalysis.lint.issues); + const allFiles = this.fileManager.getAllRelevantFiles(); + + const fastCodeFixer = await this.operations.fastCodeFixer.execute({ + query: this.state.query, + issues, + allFiles, + }, this.getOperationOptions()); + + if (fastCodeFixer.length > 0) { + await this.fileManager.saveGeneratedFiles(fastCodeFixer, "fix: Fast smart code fixes"); + await this.deployToSandbox(fastCodeFixer); + this.logger().info("Fast smart code fixes applied successfully"); + } + this.logger().info(`Fast smart code fixes applied in ${Date.now() - startTime}ms`); + } catch (error) { + this.broadcastError("Failed to apply fast smart code fixes", error); + return; + } + } + + async generateFiles( + phaseName: string, + phaseDescription: string, + requirements: string[], + files: FileConceptType[] + ): Promise<{ files: Array<{ path: string; purpose: string; diff: string }> }> { + this.logger().info('Generating files for deep debugger', { + phaseName, + requirementsCount: requirements.length, + filesCount: files.length + }); + + // Create phase structure with explicit files + const phase: PhaseConceptType = { + name: phaseName, + description: phaseDescription, + files: files, + lastPhase: true + }; + + // Call existing implementPhase with postPhaseFixing=false + // This skips deterministic fixes and fast smart fixes + const result = await this.implementPhase( + phase, + { + runtimeErrors: [], + staticAnalysis: { + success: true, + lint: { issues: [] }, + typecheck: { issues: [] } + }, + }, + { suggestions: requirements }, + true, // streamChunks + false // postPhaseFixing = false (skip auto-fixes) + ); + + // Return files with diffs from FileState + return { + files: result.files.map(f => ({ + path: f.filePath, + purpose: f.filePurpose || '', + diff: (f as any).lastDiff || '' // FileState has lastDiff + })) + }; + } +} diff --git a/worker/agents/core/smartGeneratorAgent.ts b/worker/agents/core/smartGeneratorAgent.ts index 88af36a1..6b9f0998 100644 --- a/worker/agents/core/smartGeneratorAgent.ts +++ b/worker/agents/core/smartGeneratorAgent.ts @@ -1,40 +1,89 @@ -import { SimpleCodeGeneratorAgent } from "./simpleGeneratorAgent"; -import { CodeGenState } from "./state"; -import { AgentInitArgs } from "./types"; +import { Agent, AgentContext } from "agents"; +import { AgentInitArgs, BehaviorType } from "./types"; +import { AgentState, CurrentDevState, MAX_PHASES } from "./state"; +import { AgentInfrastructure, BaseAgentBehavior } from "./baseAgent"; +import { createObjectLogger, StructuredLogger } from '../../logger'; +import { Blueprint } from "../schemas"; +import { InferenceContext } from "../inferutils/config.types"; -/** - * SmartCodeGeneratorAgent - Smartly orchestrated AI-powered code generation - * using an LLM orchestrator instead of state machine based orchestrator. - * TODO: NOT YET IMPLEMENTED, CURRENTLY Just uses SimpleCodeGeneratorAgent - */ -export class SmartCodeGeneratorAgent extends SimpleCodeGeneratorAgent { +export class CodeGeneratorAgent extends Agent implements AgentInfrastructure { + public _logger: StructuredLogger | undefined; + private behavior: BaseAgentBehavior; + private onStartDeferred?: { props?: Record; resolve: () => void }; - /** - * Initialize the smart code generator with project blueprint and template - * Sets up services and begins deployment process - */ + + initialState: AgentState = { + blueprint: {} as Blueprint, + projectName: "", + query: "", + generatedPhases: [], + generatedFilesMap: {}, + behaviorType: 'phasic', + sandboxInstanceId: undefined, + templateName: '', + commandsHistory: [], + lastPackageJson: '', + pendingUserInputs: [], + inferenceContext: {} as InferenceContext, + sessionId: '', + hostname: '', + conversationMessages: [], + currentDevState: CurrentDevState.IDLE, + phasesCounter: MAX_PHASES, + mvpGenerated: false, + shouldBeGenerating: false, + reviewingInitiated: false, + projectUpdatesAccumulator: [], + lastDeepDebugTranscript: null, + }; + + constructor(ctx: AgentContext, env: Env) { + super(ctx, env); + + this.sql`CREATE TABLE IF NOT EXISTS full_conversations (id TEXT PRIMARY KEY, messages TEXT)`; + this.sql`CREATE TABLE IF NOT EXISTS compact_conversations (id TEXT PRIMARY KEY, messages TEXT)`; + + const behaviorTypeProp = (ctx.props as Record)?.behaviorType as BehaviorType | undefined; + const behaviorType = this.state.behaviorType || behaviorTypeProp || 'phasic'; + if (behaviorType === 'phasic') { + this.behavior = new PhasicAgentBehavior(this); + } else { + this.behavior = new AgenticAgentBehavior(this); + } + } + async initialize( - initArgs: AgentInitArgs, - agentMode: 'deterministic' | 'smart' - ): Promise { - this.logger().info('🧠 Initializing SmartCodeGeneratorAgent with enhanced AI orchestration', { - queryLength: initArgs.query.length, - agentType: agentMode - }); + initArgs: AgentInitArgs, + ..._args: unknown[] + ): Promise { + const { inferenceContext } = initArgs; + this.initLogger(inferenceContext.agentId, inferenceContext.userId); + + await this.behavior.initialize(initArgs); + return this.behavior.state; + } - // Call the parent initialization - return await super.initialize(initArgs); + private initLogger(agentId: string, userId: string, sessionId?: string) { + this._logger = createObjectLogger(this, 'CodeGeneratorAgent'); + this._logger.setObjectId(agentId); + this._logger.setFields({ + agentId, + userId, + }); + if (sessionId) { + this._logger.setField('sessionId', sessionId); + } + return this._logger; } - async generateAllFiles(reviewCycles: number = 10): Promise { - if (this.state.agentMode === 'deterministic') { - return super.generateAllFiles(reviewCycles); - } else { - return this.builderLoop(); + logger(): StructuredLogger { + if (!this._logger) { + this._logger = this.initLogger(this.getAgentId(), this.state.inferenceContext.userId, this.state.sessionId); } + return this._logger; } - async builderLoop() { - // TODO + getAgentId() { + return this.state.inferenceContext.agentId; } } \ No newline at end of file diff --git a/worker/agents/core/state.ts b/worker/agents/core/state.ts index 2840747b..f8c3a5e3 100644 --- a/worker/agents/core/state.ts +++ b/worker/agents/core/state.ts @@ -1,9 +1,11 @@ -import type { Blueprint, PhaseConceptType , +import type { PhasicBlueprint, AgenticBlueprint, PhaseConceptType , FileOutputType, + Blueprint, } from '../schemas'; // import type { ScreenshotData } from './types'; import type { ConversationMessage } from '../inferutils/common'; import type { InferenceContext } from '../inferutils/config.types'; +import { BehaviorType, Plan } from './types'; export interface FileState extends FileOutputType { lastDiff: string; @@ -24,33 +26,92 @@ export enum CurrentDevState { export const MAX_PHASES = 12; -export interface CodeGenState { - blueprint: Blueprint; - projectName: string, +/** Common state fields for all agent behaviors */ +export interface BaseProjectState { + behaviorType: BehaviorType; + // Identity + projectName: string; query: string; + sessionId: string; + hostname: string; + + blueprint: Blueprint; + + templateName: string | 'custom'; + + // Conversation + conversationMessages: ConversationMessage[]; + + // Inference context + inferenceContext: InferenceContext; + + // Generation control + shouldBeGenerating: boolean; + // agentMode: 'deterministic' | 'smart'; // Would be migrated and mapped to behaviorType + + // Common file storage generatedFilesMap: Record; - generatedPhases: PhaseState[]; - commandsHistory?: string[]; // History of commands run - lastPackageJson?: string; // Last package.json file contents - templateName: string; + + // Common infrastructure sandboxInstanceId?: string; + commandsHistory?: string[]; + lastPackageJson?: string; + pendingUserInputs: string[]; + projectUpdatesAccumulator: string[]; - shouldBeGenerating: boolean; // Persistent flag indicating generation should be active + // Deep debug + lastDeepDebugTranscript: string | null; + mvpGenerated: boolean; reviewingInitiated: boolean; - agentMode: 'deterministic' | 'smart'; - sessionId: string; - hostname: string; - phasesCounter: number; +} - pendingUserInputs: string[]; - currentDevState: CurrentDevState; - reviewCycles?: number; // Number of review cycles for code review phase - currentPhase?: PhaseConceptType; // Current phase being worked on +/** Phasic agent state */ +export interface PhasicState extends BaseProjectState { + behaviorType: 'phasic'; + blueprint: PhasicBlueprint; + generatedPhases: PhaseState[]; - conversationMessages: ConversationMessage[]; - projectUpdatesAccumulator: string[]; - inferenceContext: InferenceContext; + phasesCounter: number; + currentDevState: CurrentDevState; + reviewCycles?: number; + currentPhase?: PhaseConceptType; +} - lastDeepDebugTranscript: string | null; -} +export interface WorkflowMetadata { + name: string; + description: string; + params: Record; + bindings?: { + envVars?: Record; + secrets?: Record; + resources?: Record; + }; +} + +/** Agentic agent state */ +export interface AgenticState extends BaseProjectState { + behaviorType: 'agentic'; + blueprint: AgenticBlueprint; + currentPlan: Plan; +} + +export type AgentState = PhasicState | AgenticState; diff --git a/worker/agents/core/types.ts b/worker/agents/core/types.ts index af7ecc27..4ab3ba65 100644 --- a/worker/agents/core/types.ts +++ b/worker/agents/core/types.ts @@ -5,23 +5,46 @@ import type { ConversationMessage } from '../inferutils/common'; import type { InferenceContext } from '../inferutils/config.types'; import type { TemplateDetails } from '../../services/sandbox/sandboxTypes'; import { TemplateSelection } from '../schemas'; -import { CurrentDevState } from './state'; +import { CurrentDevState, PhasicState, AgenticState } from './state'; import { ProcessedImageAttachment } from 'worker/types/image-attachment'; -export interface AgentInitArgs { +export type BehaviorType = 'phasic' | 'agentic'; + +/** Base initialization arguments shared by all agents */ +interface BaseAgentInitArgs { query: string; - language?: string; - frameworks?: string[]; hostname: string; inferenceContext: InferenceContext; + language?: string; + frameworks?: string[]; + images?: ProcessedImageAttachment[]; + onBlueprintChunk: (chunk: string) => void; +} + +/** Phasic agent initialization arguments */ +interface PhasicAgentInitArgs extends BaseAgentInitArgs { templateInfo: { templateDetails: TemplateDetails; selection: TemplateSelection; - } - images?: ProcessedImageAttachment[]; - onBlueprintChunk: (chunk: string) => void; + }; +} + +/** Agentic agent initialization arguments */ +interface AgenticAgentInitArgs extends BaseAgentInitArgs { + templateInfo?: { + templateDetails: TemplateDetails; + selection: TemplateSelection; + }; } +/** Generic initialization arguments based on state type */ +export type AgentInitArgs = + TState extends PhasicState ? PhasicAgentInitArgs : + TState extends AgenticState ? AgenticAgentInitArgs : + PhasicAgentInitArgs | AgenticAgentInitArgs; + +export type Plan = string; + export interface AllIssues { runtimeErrors: RuntimeError[]; staticAnalysis: StaticAnalysisResponse; diff --git a/worker/agents/core/websocket.ts b/worker/agents/core/websocket.ts index 4428f690..be655701 100644 --- a/worker/agents/core/websocket.ts +++ b/worker/agents/core/websocket.ts @@ -1,13 +1,18 @@ import { Connection } from 'agents'; import { createLogger } from '../../logger'; import { WebSocketMessageRequests, WebSocketMessageResponses } from '../constants'; -import { SimpleCodeGeneratorAgent } from './simpleGeneratorAgent'; import { WebSocketMessage, WebSocketMessageData, WebSocketMessageType } from '../../api/websocketTypes'; import { MAX_IMAGES_PER_MESSAGE, MAX_IMAGE_SIZE_BYTES } from '../../types/image-attachment'; +import { BaseProjectState } from './state'; +import { BaseAgentBehavior } from './baseAgent'; const logger = createLogger('CodeGeneratorWebSocket'); -export function handleWebSocketMessage(agent: SimpleCodeGeneratorAgent, connection: Connection, message: string): void { +export function handleWebSocketMessage( + agent: BaseAgentBehavior, + connection: Connection, + message: string +): void { try { logger.info(`Received WebSocket message from ${connection.id}: ${message}`); const parsedMessage = JSON.parse(message); diff --git a/worker/agents/domain/values/GenerationContext.ts b/worker/agents/domain/values/GenerationContext.ts index 39176ab9..788ea8d8 100644 --- a/worker/agents/domain/values/GenerationContext.ts +++ b/worker/agents/domain/values/GenerationContext.ts @@ -1,36 +1,47 @@ -import { Blueprint } from '../../schemas'; +import { PhasicBlueprint, AgenticBlueprint } from '../../schemas'; import { FileTreeNode, TemplateDetails } from '../../../services/sandbox/sandboxTypes'; -import { CodeGenState, FileState, PhaseState } from '../../core/state'; +import { FileState, PhaseState, PhasicState, AgenticState } from '../../core/state'; import { DependencyManagement } from '../pure/DependencyManagement'; import type { StructuredLogger } from '../../../logger'; import { FileProcessing } from '../pure/FileProcessing'; +import { Plan } from '../../core/types'; + +/** Common fields shared by all generation contexts */ +interface BaseGenerationContext { + readonly query: string; + readonly allFiles: FileState[]; + readonly templateDetails: TemplateDetails; + readonly dependencies: Record; + readonly commandsHistory: string[]; +} + +/** Phase-based generation context with detailed blueprint */ +export interface PhasicGenerationContext extends BaseGenerationContext { + readonly blueprint: PhasicBlueprint; + readonly generatedPhases: PhaseState[]; +} + +/** Plan-based generation context with simple blueprint */ +export interface AgenticGenerationContext extends BaseGenerationContext { + readonly blueprint: AgenticBlueprint; + readonly currentPlan: Plan; +} /** - * Immutable context for code generation operations - * Contains all necessary data for generating code + * Discriminated union of generation contexts + * + * Discriminate using: `'generatedPhases' in context` or `GenerationContext.isPhasic(context)` */ -export class GenerationContext { - constructor( - public readonly query: string, - public readonly blueprint: Blueprint, - public readonly templateDetails: TemplateDetails, - public readonly dependencies: Record, - public readonly allFiles: FileState[], - public readonly generatedPhases: PhaseState[], - public readonly commandsHistory: string[] - ) { - // Freeze to ensure immutability - Object.freeze(this); - Object.freeze(this.dependencies); - Object.freeze(this.allFiles); - Object.freeze(this.generatedPhases); - Object.freeze(this.commandsHistory); - } - - /** - * Create context from current state - */ - static from(state: CodeGenState, templateDetails: TemplateDetails, logger?: Pick): GenerationContext { +export type GenerationContext = PhasicGenerationContext | AgenticGenerationContext; + +/** Generation context utility functions */ +export namespace GenerationContext { + /** Create immutable context from agent state */ + export function from( + state: PhasicState | AgenticState, + templateDetails: TemplateDetails, + logger?: Pick + ): GenerationContext { const dependencies = DependencyManagement.mergeDependencies( templateDetails.deps || {}, state.lastPackageJson, @@ -42,36 +53,70 @@ export class GenerationContext { state.generatedFilesMap ); - return new GenerationContext( - state.query, - state.blueprint, + const base = { + query: state.query, + allFiles, templateDetails, dependencies, - allFiles, - state.generatedPhases, - state.commandsHistory || [] - ); + commandsHistory: state.commandsHistory || [], + }; + + return state.behaviorType === 'phasic' + ? Object.freeze({ ...base, blueprint: (state as PhasicState).blueprint, generatedPhases: (state as PhasicState).generatedPhases }) + : Object.freeze({ ...base, blueprint: (state as AgenticState).blueprint, currentPlan: (state as AgenticState).currentPlan }); } - /** - * Get formatted phases for prompt generation - */ - getCompletedPhases() { - return Object.values(this.generatedPhases.filter(phase => phase.completed)); + /** Type guard for phasic context */ + export function isPhasic(context: GenerationContext): context is PhasicGenerationContext { + return 'generatedPhases' in context; } - getFileTree(): FileTreeNode { - const builder = new FileTreeBuilder(this.templateDetails?.fileTree); + /** Type guard for agentic context */ + export function isAgentic(context: GenerationContext): context is AgenticGenerationContext { + return 'currentPlan' in context; + } + + /** Get completed phases (empty array for agentic contexts) */ + export function getCompletedPhases(context: GenerationContext): PhaseState[] { + return isPhasic(context) + ? context.generatedPhases.filter(phase => phase.completed) + : []; + } - for (const { filePath } of this.allFiles) { + /** Build file tree from context files */ + export function getFileTree(context: GenerationContext): FileTreeNode { + const builder = new FileTreeBuilder(context.templateDetails?.fileTree); + + for (const { filePath } of context.allFiles) { const normalized = FileTreeBuilder.normalizePath(filePath); if (normalized) { builder.addFile(normalized); } } - + return builder.build(); } + + /** Get phasic blueprint if available */ + export function getPhasicBlueprint(context: GenerationContext): PhasicBlueprint | undefined { + return isPhasic(context) ? context.blueprint : undefined; + } + + /** Get agentic blueprint if available */ + export function getAgenticBlueprint(context: GenerationContext): AgenticBlueprint | undefined { + return isAgentic(context) ? context.blueprint : undefined; + } + + /** Get common blueprint data */ + export function getCommonBlueprintData(context: GenerationContext) { + return { + title: context.blueprint.title, + projectName: context.blueprint.projectName, + description: context.blueprint.description, + frameworks: context.blueprint.frameworks, + colorPalette: context.blueprint.colorPalette, + }; + } } class FileTreeBuilder { diff --git a/worker/agents/index.ts b/worker/agents/index.ts index 17101673..102298c0 100644 --- a/worker/agents/index.ts +++ b/worker/agents/index.ts @@ -1,7 +1,4 @@ - -import { SmartCodeGeneratorAgent } from './core/smartGeneratorAgent'; import { getAgentByName } from 'agents'; -import { CodeGenState } from './core/state'; import { generateId } from '../utils/idGenerator'; import { StructuredLogger } from '../logger'; import { InferenceContext } from './inferutils/config.types'; diff --git a/worker/agents/operations/FileRegeneration.ts b/worker/agents/operations/FileRegeneration.ts index ff05c24d..30edf3eb 100644 --- a/worker/agents/operations/FileRegeneration.ts +++ b/worker/agents/operations/FileRegeneration.ts @@ -2,6 +2,7 @@ import { FileGenerationOutputType } from '../schemas'; import { AgentOperation, OperationOptions } from '../operations/common'; import { RealtimeCodeFixer } from '../assistants/realtimeCodeFixer'; import { FileOutputType } from '../schemas'; +import { GenerationContext } from '../domain/values/GenerationContext'; export interface FileRegenerationInputs { file: FileOutputType; @@ -99,10 +100,10 @@ useEffect(() => { - If an issue cannot be fixed surgically, explain why instead of forcing a fix `; -export class FileRegenerationOperation extends AgentOperation { +export class FileRegenerationOperation extends AgentOperation { async execute( inputs: FileRegenerationInputs, - options: OperationOptions + options: OperationOptions ): Promise { try { // Use realtime code fixer to fix the file with enhanced surgical fix prompts diff --git a/worker/agents/operations/PhaseGeneration.ts b/worker/agents/operations/PhaseGeneration.ts index 1a824e4c..7889f998 100644 --- a/worker/agents/operations/PhaseGeneration.ts +++ b/worker/agents/operations/PhaseGeneration.ts @@ -8,6 +8,7 @@ import { AgentOperation, getSystemPromptWithProjectContext, OperationOptions } f import { AGENT_CONFIG } from '../inferutils/config'; import type { UserContext } from '../core/types'; import { imagesToBase64 } from 'worker/utils/images'; +import { PhasicGenerationContext } from '../domain/values/GenerationContext'; export interface PhaseGenerationInputs { issues: IssueReport; @@ -251,10 +252,10 @@ const userPromptFormatter = (isFinal: Boolean, issues: IssueReport, userSuggesti return PROMPT_UTILS.verifyPrompt(prompt); } -export class PhaseGenerationOperation extends AgentOperation { +export class PhaseGenerationOperation extends AgentOperation { async execute( inputs: PhaseGenerationInputs, - options: OperationOptions + options: OperationOptions ): Promise { const { issues, userContext, isUserSuggestedPhase, isFinal } = inputs; const { env, logger, context } = options; diff --git a/worker/agents/operations/PhaseImplementation.ts b/worker/agents/operations/PhaseImplementation.ts index 9786a09b..85b3e505 100644 --- a/worker/agents/operations/PhaseImplementation.ts +++ b/worker/agents/operations/PhaseImplementation.ts @@ -13,6 +13,7 @@ import { IsRealtimeCodeFixerEnabled, RealtimeCodeFixer } from '../assistants/rea import { CodeSerializerType } from '../utils/codeSerializers'; import type { UserContext } from '../core/types'; import { imagesToBase64 } from 'worker/utils/images'; +import { PhasicGenerationContext } from '../domain/values/GenerationContext'; export interface PhaseImplementationInputs { phase: PhaseConceptType @@ -336,30 +337,6 @@ ${PROMPT_UTILS.COMMON_DEP_DOCUMENTATION} // */ // \`\`\` -const README_GENERATION_PROMPT = ` -Generate a comprehensive README.md file for this project based on the provided blueprint and template information. -The README should be professional, well-structured, and provide clear instructions for users and developers. - - - -- Create a professional README with proper markdown formatting -- Do not add any images or screenshots -- Include project title, description, and key features from the blueprint -- Add technology stack section based on the template dependencies -- Include setup/installation instructions using bun (not npm/yarn) -- Add usage examples and development instructions -- Include a deployment section with Cloudflare-specific instructions -- **IMPORTANT**: Add a \`[cloudflarebutton]\` placeholder near the top and another in the deployment section for the Cloudflare deploy button. Write the **EXACT** string except the backticks and DON'T enclose it in any other button or anything. We will replace it with https://deploy.workers.cloudflare.com/?url=\${repositoryUrl\} when the repository is created. -- Structure the content clearly with appropriate headers and sections -- Be concise but comprehensive - focus on essential information -- Use professional tone suitable for open source projects - - -Generate the complete README.md content in markdown format. -Do not provide any additional text or explanation. -All your output will be directly saved in the README.md file. -Do not provide and markdown fence \`\`\` \`\`\` around the content either! Just pure raw markdown content!`; - const formatUserSuggestions = (suggestions?: string[] | null): string => { if (!suggestions || suggestions.length === 0) { return ''; @@ -392,10 +369,10 @@ const userPromptFormatter = (phaseConcept: PhaseConceptType, issues: IssueReport return PROMPT_UTILS.verifyPrompt(prompt); } -export class PhaseImplementationOperation extends AgentOperation { +export class PhaseImplementationOperation extends AgentOperation { async execute( inputs: PhaseImplementationInputs, - options: OperationOptions + options: OperationOptions ): Promise { const { phase, issues, userContext } = inputs; const { env, logger, context } = options; @@ -516,37 +493,4 @@ export class PhaseImplementationOperation extends AgentOperation { - const { env, logger, context } = options; - logger.info("Generating README.md for the project"); - - try { - let readmePrompt = README_GENERATION_PROMPT; - const messages = [...getSystemPromptWithProjectContext(SYSTEM_PROMPT, context, CodeSerializerType.SCOF), createUserMessage(readmePrompt)]; - - const results = await executeInference({ - env: env, - messages, - agentActionName: "projectSetup", - context: options.inferenceContext, - }); - - if (!results || !results.string) { - logger.error('Failed to generate README.md content'); - throw new Error('Failed to generate README.md content'); - } - - logger.info('Generated README.md content successfully'); - - return { - filePath: 'README.md', - fileContents: results.string, - filePurpose: 'Project documentation and setup instructions' - }; - } catch (error) { - logger.error("Error generating README:", error); - throw error; - } - } } diff --git a/worker/agents/operations/PostPhaseCodeFixer.ts b/worker/agents/operations/PostPhaseCodeFixer.ts index bb3de4e2..242abd94 100644 --- a/worker/agents/operations/PostPhaseCodeFixer.ts +++ b/worker/agents/operations/PostPhaseCodeFixer.ts @@ -6,6 +6,7 @@ import { FileOutputType, PhaseConceptType } from '../schemas'; import { SCOFFormat } from '../output-formats/streaming-formats/scof'; import { CodeIssue } from '../../services/sandbox/sandboxTypes'; import { CodeSerializerType } from '../utils/codeSerializers'; +import { PhasicGenerationContext } from '../domain/values/GenerationContext'; export interface FastCodeFixerInputs { query: string; @@ -71,10 +72,10 @@ const userPromptFormatter = (query: string, issues: CodeIssue[], allFiles: FileO return PROMPT_UTILS.verifyPrompt(prompt); } -export class FastCodeFixerOperation extends AgentOperation { +export class FastCodeFixerOperation extends AgentOperation { async execute( inputs: FastCodeFixerInputs, - options: OperationOptions + options: OperationOptions ): Promise { const { query, issues, allFiles, allPhases } = inputs; const { env, logger } = options; diff --git a/worker/agents/operations/ScreenshotAnalysis.ts b/worker/agents/operations/ScreenshotAnalysis.ts deleted file mode 100644 index b562908b..00000000 --- a/worker/agents/operations/ScreenshotAnalysis.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Blueprint, ScreenshotAnalysisSchema, ScreenshotAnalysisType } from '../schemas'; -import { createSystemMessage, createMultiModalUserMessage } from '../inferutils/common'; -import { executeInference } from '../inferutils/infer'; -import { PROMPT_UTILS } from '../prompts'; -import { ScreenshotData } from '../core/types'; -import { AgentOperation, OperationOptions } from './common'; -import { OperationError } from '../utils/operationError'; - -export interface ScreenshotAnalysisInput { - screenshotData: ScreenshotData, -} - -const SYSTEM_PROMPT = `You are a UI/UX Quality Assurance Specialist at Cloudflare. Your task is to analyze application screenshots against blueprint specifications and identify visual issues. - -## ANALYSIS PRIORITIES: -1. **Missing Elements** - Blueprint components not visible -2. **Layout Issues** - Misaligned, overlapping, or broken layouts -3. **Responsive Problems** - Mobile/desktop rendering issues -4. **Visual Bugs** - Broken styling, incorrect colors, missing images - -## EXAMPLE ANALYSES: - -**Example 1 - Game UI:** -Blueprint: "Score display in top-right, game board centered, control buttons below" -Screenshot: Shows score in top-left, buttons missing -Analysis: -- hasIssues: true -- issues: ["Score positioned incorrectly", "Control buttons not visible"] -- matchesBlueprint: false -- deviations: ["Score placement", "Missing controls"] - -**Example 2 - Dashboard:** -Blueprint: "3-column layout with sidebar, main content, and metrics panel" -Screenshot: Shows proper 3-column layout, all elements visible -Analysis: -- hasIssues: false -- issues: [] -- matchesBlueprint: true -- deviations: [] - -## OUTPUT FORMAT: -Return JSON with exactly these fields: -- hasIssues: boolean -- issues: string[] (specific problems found) -- uiCompliance: { matchesBlueprint: boolean, deviations: string[] } -- suggestions: string[] (improvement recommendations)`; - -const USER_PROMPT = `Analyze this screenshot against the blueprint requirements. - -**Blueprint Context:** -{{blueprint}} - -**Viewport:** {{viewport}} - -**Analysis Required:** -- Compare visible elements against blueprint specifications -- Check layout, spacing, and component positioning -- Identify any missing or broken UI elements -- Assess responsive design for the given viewport size -- Note any visual bugs or rendering issues - -Provide specific, actionable feedback focused on blueprint compliance.` - -const userPromptFormatter = (screenshotData: { viewport: { width: number; height: number }; }, blueprint: Blueprint) => { - const prompt = PROMPT_UTILS.replaceTemplateVariables(USER_PROMPT, { - blueprint: JSON.stringify(blueprint, null, 2), - viewport: `${screenshotData.viewport.width}x${screenshotData.viewport.height}` - }); - return PROMPT_UTILS.verifyPrompt(prompt); -} - -export class ScreenshotAnalysisOperation extends AgentOperation { - async execute( - input: ScreenshotAnalysisInput, - options: OperationOptions - ): Promise { - const { screenshotData } = input; - const { env, context, logger } = options; - try { - logger.info('Analyzing screenshot from preview', { - url: screenshotData.url, - viewport: screenshotData.viewport, - hasScreenshotData: !!screenshotData.screenshot, - screenshotDataLength: screenshotData.screenshot?.length || 0 - }); - - if (!screenshotData.screenshot) { - throw new Error('No screenshot data available for analysis'); - } - - // Create multi-modal messages - const messages = [ - createSystemMessage(SYSTEM_PROMPT), - createMultiModalUserMessage( - userPromptFormatter(screenshotData, context.blueprint), - screenshotData.screenshot, // The base64 data URL or image URL - 'high' // Use high detail for better analysis - ) - ]; - - const { object: analysisResult } = await executeInference({ - env: env, - messages, - schema: ScreenshotAnalysisSchema, - agentActionName: 'screenshotAnalysis', - context: options.inferenceContext, - retryLimit: 3 - }); - - if (!analysisResult) { - logger.warn('Screenshot analysis returned no result'); - throw new Error('No analysis result'); - } - - logger.info('Screenshot analysis completed', { - hasIssues: analysisResult.hasIssues, - issueCount: analysisResult.issues.length, - matchesBlueprint: analysisResult.uiCompliance.matchesBlueprint - }); - - // Log detected UI issues - if (analysisResult.hasIssues) { - logger.warn('UI issues detected in screenshot', { - issues: analysisResult.issues, - deviations: analysisResult.uiCompliance.deviations - }); - } - - return analysisResult; - } catch (error) { - OperationError.logAndThrow(logger, "screenshot analysis", error); - } - } -} \ No newline at end of file diff --git a/worker/agents/operations/SimpleCodeGeneration.ts b/worker/agents/operations/SimpleCodeGeneration.ts index df377bfa..3cacdf24 100644 --- a/worker/agents/operations/SimpleCodeGeneration.ts +++ b/worker/agents/operations/SimpleCodeGeneration.ts @@ -7,6 +7,7 @@ import { SCOFFormat, SCOFParsingState } from '../output-formats/streaming-format import { CodeGenerationStreamingState } from '../output-formats/streaming-formats/base'; import { FileProcessing } from '../domain/pure/FileProcessing'; import { CodeSerializerType } from '../utils/codeSerializers'; +import { GenerationContext } from '../domain/values/GenerationContext'; import { FileState } from '../core/state'; export interface SimpleCodeGenerationInputs { @@ -120,12 +121,13 @@ const formatExistingFiles = (allFiles: FileState[]): string => { }; export class SimpleCodeGenerationOperation extends AgentOperation< + GenerationContext, SimpleCodeGenerationInputs, SimpleCodeGenerationOutputs > { async execute( inputs: SimpleCodeGenerationInputs, - options: OperationOptions + options: OperationOptions ): Promise { const { phaseName, phaseDescription, requirements, files } = inputs; const { env, logger, context, inferenceContext } = options; diff --git a/worker/agents/operations/UserConversationProcessor.ts b/worker/agents/operations/UserConversationProcessor.ts index f04e5572..7715da42 100644 --- a/worker/agents/operations/UserConversationProcessor.ts +++ b/worker/agents/operations/UserConversationProcessor.ts @@ -18,6 +18,7 @@ import { ConversationState } from "../inferutils/common"; import { downloadR2Image, imagesToBase64, imageToBase64 } from "worker/utils/images"; import { ProcessedImageAttachment } from "worker/types/image-attachment"; import { AbortError, InferResponseString } from "../inferutils/core"; +import { GenerationContext } from "../domain/values/GenerationContext"; // Constants const CHUNK_SIZE = 64; @@ -325,7 +326,7 @@ async function prepareMessagesForInference(env: Env, messages: ConversationMessa return processedMessages; } -export class UserConversationProcessor extends AgentOperation { +export class UserConversationProcessor extends AgentOperation { /** * Remove system context tags from message content */ @@ -333,7 +334,7 @@ export class UserConversationProcessor extends AgentOperation[\s\S]*?<\/system_context>\n?/gi, '').trim(); } - async execute(inputs: UserConversationInputs, options: OperationOptions): Promise { + async execute(inputs: UserConversationInputs, options: OperationOptions): Promise { const { env, logger, context, agent } = options; const { userMessage, conversationState, errors, images, projectUpdates } = inputs; logger.info("Processing user message", { diff --git a/worker/agents/operations/common.ts b/worker/agents/operations/common.ts index 1ed88286..baae2fc2 100644 --- a/worker/agents/operations/common.ts +++ b/worker/agents/operations/common.ts @@ -5,7 +5,7 @@ import { InferenceContext } from "../inferutils/config.types"; import { createUserMessage, createSystemMessage, createAssistantMessage } from "../inferutils/common"; import { generalSystemPromptBuilder, USER_PROMPT_FORMATTER } from "../prompts"; import { CodeSerializerType } from "../utils/codeSerializers"; -import { CodingAgentInterface } from "../services/implementations/CodingAgent"; +import { ICodingAgent } from "../services/interfaces/ICodingAgent"; export function getSystemPromptWithProjectContext( systemPrompt: string, @@ -23,9 +23,9 @@ export function getSystemPromptWithProjectContext( })), createUserMessage( USER_PROMPT_FORMATTER.PROJECT_CONTEXT( - context.getCompletedPhases(), + GenerationContext.getCompletedPhases(context), allFiles, - context.getFileTree(), + GenerationContext.getFileTree(context), commandsHistory, serializerType ) @@ -35,18 +35,32 @@ export function getSystemPromptWithProjectContext( return messages; } -export interface OperationOptions { +/** + * Operation options with context type constraint + * @template TContext - Context type (defaults to GenerationContext for universal operations) + */ +export interface OperationOptions { env: Env; agentId: string; - context: GenerationContext; + context: TContext; logger: StructuredLogger; inferenceContext: InferenceContext; - agent: CodingAgentInterface; + agent: ICodingAgent; } -export abstract class AgentOperation { +/** + * Base class for agent operations with type-safe context enforcement + * @template TContext - Required context type (defaults to GenerationContext) + * @template TInput - Operation input type + * @template TOutput - Operation output type + */ +export abstract class AgentOperation< + TContext extends GenerationContext = GenerationContext, + TInput = unknown, + TOutput = unknown +> { abstract execute( - inputs: InputType, - options: OperationOptions - ): Promise; + inputs: TInput, + options: OperationOptions + ): Promise; } \ No newline at end of file diff --git a/worker/agents/planning/blueprint.ts b/worker/agents/planning/blueprint.ts index f347cd7d..bc8406b1 100644 --- a/worker/agents/planning/blueprint.ts +++ b/worker/agents/planning/blueprint.ts @@ -1,7 +1,7 @@ import { TemplateDetails, TemplateFileSchema } from '../../services/sandbox/sandboxTypes'; // Import the type import { STRATEGIES, PROMPT_UTILS, generalSystemPromptBuilder } from '../prompts'; import { executeInference } from '../inferutils/infer'; -import { Blueprint, BlueprintSchema, TemplateSelection } from '../schemas'; +import { PhasicBlueprint, AgenticBlueprint, PhasicBlueprintSchema, AgenticBlueprintSchema, TemplateSelection, Blueprint } from '../schemas'; import { createLogger } from '../../logger'; import { createSystemMessage, createUserMessage, createMultiModalUserMessage } from '../inferutils/common'; import { InferenceContext } from '../inferutils/config.types'; @@ -13,7 +13,74 @@ import { getTemplateImportantFiles } from 'worker/services/sandbox/utils'; const logger = createLogger('Blueprint'); -const SYSTEM_PROMPT = ` +const SIMPLE_SYSTEM_PROMPT = ` + You are a Senior Software Architect at Cloudflare with expertise in rapid prototyping and modern web development. + Your expertise lies in creating concise, actionable blueprints for building web applications quickly and efficiently. + + + + Create a high-level blueprint for a web application based on the client's request. + The project will be built on Cloudflare Workers and will start from a provided template. + Focus on a clear, concise design that captures the core requirements without over-engineering. + Enhance the user's request thoughtfully - be creative but practical. + + + + Design the product described by the client and provide: + - A professional, memorable project name + - A brief but clear description of what the application does + - A simple color palette (2-3 base colors) for visual identity + - Essential frameworks and libraries needed (beyond the template) + - A high-level step-by-step implementation plan + + Keep it concise - this is a simplified blueprint focused on rapid development. + Build upon the provided template's existing structure and components. + + + + ## Core Principles + • **Simplicity First:** Keep the design straightforward and achievable + • **Template-Aware:** Leverage existing components and patterns from the template + • **Essential Only:** Include only the frameworks/libraries that are truly needed + • **Clear Plan:** Provide a logical step-by-step implementation sequence + + ## Color Palette + • Choose 2-3 base RGB colors that work well together + • Consider the application's purpose and mood + • Ensure good contrast for accessibility + • Only specify base colors, not shades + + ## Frameworks & Dependencies + • Build on the template's existing dependencies + • Only add libraries that are essential for the requested features + • Prefer batteries-included libraries that work out-of-the-box + • No libraries requiring API keys or complex configuration + + ## Implementation Plan + • Break down the work into 5-8 logical steps + • Each step should be a clear, achievable milestone + • Order steps by dependency and priority + • Keep descriptions brief but actionable + + + +{{template}} + + +**SHADCN COMPONENTS, Error boundary components and use-toast hook ARE PRESENT AND INSTALLED BUT EXCLUDED FROM THESE FILES DUE TO CONTEXT SPAM** +{{filesText}} + + + +**Use these files as a reference for the file structure, components and hooks that are present** +{{fileTreeText}} + + +Preinstalled dependencies: +{{dependencies}} +`; + +const PHASIC_SYSTEM_PROMPT = ` You are a meticulous and forward-thinking Senior Software Architect and Product Manager at Cloudflare with extensive expertise in modern UI/UX design and visual excellence. Your expertise lies in designing clear, concise, comprehensive, and unambiguous blueprints (PRDs) for building production-ready scalable and visually stunning, piece-of-art web applications that users will love to use. @@ -161,15 +228,12 @@ Preinstalled dependencies: {{dependencies}} `; -export interface BlueprintGenerationArgs { +interface BaseBlueprintGenerationArgs { env: Env; inferenceContext: InferenceContext; query: string; language: string; frameworks: string[]; - // Add optional template info - templateDetails: TemplateDetails; - templateMetaInfo: TemplateSelection; images?: ProcessedImageAttachment[]; stream?: { chunk_size: number; @@ -177,26 +241,46 @@ export interface BlueprintGenerationArgs { }; } +export interface PhasicBlueprintGenerationArgs extends BaseBlueprintGenerationArgs { + templateDetails: TemplateDetails; + templateMetaInfo: TemplateSelection; +} + +export interface AgenticBlueprintGenerationArgs extends BaseBlueprintGenerationArgs { + templateDetails?: TemplateDetails; + templateMetaInfo?: TemplateSelection; +} + /** * Generate a blueprint for the application based on user prompt */ -// Update function signature and system prompt -export async function generateBlueprint({ env, inferenceContext, query, language, frameworks, templateDetails, templateMetaInfo, images, stream }: BlueprintGenerationArgs): Promise { +export async function generateBlueprint(args: PhasicBlueprintGenerationArgs): Promise; +export async function generateBlueprint(args: AgenticBlueprintGenerationArgs): Promise; +export async function generateBlueprint( + args: PhasicBlueprintGenerationArgs | AgenticBlueprintGenerationArgs +): Promise { + const { env, inferenceContext, query, language, frameworks, templateDetails, templateMetaInfo, images, stream } = args; + const isAgentic = !templateDetails || !templateMetaInfo; + try { - logger.info("Generating application blueprint", { query, queryLength: query.length, imagesCount: images?.length || 0 }); - logger.info(templateDetails ? `Using template: ${templateDetails.name}` : "Not using a template."); + logger.info(`Generating ${isAgentic ? 'agentic' : 'phasic'} blueprint`, { query, queryLength: query.length, imagesCount: images?.length || 0 }); + if (templateDetails) logger.info(`Using template: ${templateDetails.name}`); - // --------------------------------------------------------------------------- - // Build the SYSTEM prompt for blueprint generation - // --------------------------------------------------------------------------- - - const filesText = TemplateRegistry.markdown.serialize( - { files: getTemplateImportantFiles(templateDetails).filter(f => !f.filePath.includes('package.json')) }, - z.object({ files: z.array(TemplateFileSchema) }) - ); - - const fileTreeText = PROMPT_UTILS.serializeTreeNodes(templateDetails.fileTree); - const systemPrompt = SYSTEM_PROMPT.replace('{{filesText}}', filesText).replace('{{fileTreeText}}', fileTreeText); + // Select prompt and schema based on behavior type + const systemPromptTemplate = isAgentic ? SIMPLE_SYSTEM_PROMPT : PHASIC_SYSTEM_PROMPT; + const schema = isAgentic ? AgenticBlueprintSchema : PhasicBlueprintSchema; + + // Build system prompt with template context (if provided) + let systemPrompt = systemPromptTemplate; + if (templateDetails) { + const filesText = TemplateRegistry.markdown.serialize( + { files: getTemplateImportantFiles(templateDetails).filter(f => !f.filePath.includes('package.json')) }, + z.object({ files: z.array(TemplateFileSchema) }) + ); + const fileTreeText = PROMPT_UTILS.serializeTreeNodes(templateDetails.fileTree); + systemPrompt = systemPrompt.replace('{{filesText}}', filesText).replace('{{fileTreeText}}', fileTreeText); + } + const systemPromptMessage = createSystemMessage(generalSystemPromptBuilder(systemPrompt, { query, templateDetails, @@ -204,7 +288,7 @@ export async function generateBlueprint({ env, inferenceContext, query, language templateMetaInfo, blueprint: undefined, language, - dependencies: templateDetails.deps, + dependencies: templateDetails?.deps, })); const userMessage = images && images.length > 0 @@ -234,21 +318,18 @@ export async function generateBlueprint({ env, inferenceContext, query, language env, messages, agentActionName: "blueprint", - schema: BlueprintSchema, + schema, context: inferenceContext, - stream: stream, + stream, }); - if (results) { - // Filter and remove any pdf files - results.initialPhase.files = results.initialPhase.files.filter(f => !f.path.endsWith('.pdf')); + // Filter out PDF files from phasic blueprints + if (results && !isAgentic) { + const phasicResults = results as PhasicBlueprint; + phasicResults.initialPhase.files = phasicResults.initialPhase.files.filter(f => !f.path.endsWith('.pdf')); } - // // A hack - // if (results?.initialPhase) { - // results.initialPhase.lastPhase = false; - // } - return results as Blueprint; + return results as PhasicBlueprint | AgenticBlueprint; } catch (error) { logger.error("Error generating blueprint:", error); throw error; diff --git a/worker/agents/prompts.ts b/worker/agents/prompts.ts index 42dd8d6a..583a7e6e 100644 --- a/worker/agents/prompts.ts +++ b/worker/agents/prompts.ts @@ -1,7 +1,7 @@ import { FileTreeNode, RuntimeError, StaticAnalysisResponse, TemplateDetails } from "../services/sandbox/sandboxTypes"; import { TemplateRegistry } from "./inferutils/schemaFormatters"; import z from 'zod'; -import { Blueprint, BlueprintSchemaLite, FileOutputType, PhaseConceptLiteSchema, PhaseConceptSchema, PhaseConceptType, TemplateSelection } from "./schemas"; +import { PhasicBlueprint, AgenticBlueprint, BlueprintSchemaLite, AgenticBlueprintSchema, FileOutputType, PhaseConceptLiteSchema, PhaseConceptSchema, PhaseConceptType, TemplateSelection, Blueprint } from "./schemas"; import { IssueReport } from "./domain/values/IssueReport"; import { FileState, MAX_PHASES } from "./core/state"; import { CODE_SERIALIZERS, CodeSerializerType } from "./utils/codeSerializers"; @@ -1315,8 +1315,8 @@ FRONTEND_FIRST_CODING: ` export interface GeneralSystemPromptBuilderParams { query: string, - templateDetails: TemplateDetails, - dependencies: Record, + templateDetails?: TemplateDetails, + dependencies?: Record, blueprint?: Blueprint, language?: string, frameworks?: string[], @@ -1330,19 +1330,29 @@ export function generalSystemPromptBuilder( // Base variables always present const variables: Record = { query: params.query, - template: PROMPT_UTILS.serializeTemplate(params.templateDetails), - dependencies: JSON.stringify(params.dependencies ?? {}) }; + + // Template context (optional) + if (params.templateDetails) { + variables.template = PROMPT_UTILS.serializeTemplate(params.templateDetails); + variables.dependencies = JSON.stringify(params.dependencies ?? {}); + } - // Optional blueprint variables + // Blueprint variables - discriminate by type if (params.blueprint) { - // Redact the initial phase information from blueprint - const blueprint = { - ...params.blueprint, - initialPhase: undefined, + if ('implementationRoadmap' in params.blueprint) { + // Phasic blueprint + const phasicBlueprint = params.blueprint as PhasicBlueprint; + const blueprintForPrompt = { ...phasicBlueprint, initialPhase: undefined }; + variables.blueprint = TemplateRegistry.markdown.serialize(blueprintForPrompt, BlueprintSchemaLite); + variables.blueprintDependencies = phasicBlueprint.frameworks?.join(', ') ?? ''; + } else { + // Agentic blueprint + const agenticBlueprint = params.blueprint as AgenticBlueprint; + variables.blueprint = TemplateRegistry.markdown.serialize(agenticBlueprint, AgenticBlueprintSchema); + variables.blueprintDependencies = agenticBlueprint.frameworks?.join(', ') ?? ''; + variables.agenticPlan = agenticBlueprint.plan.map((step, i) => `${i + 1}. ${step}`).join('\n'); } - variables.blueprint = TemplateRegistry.markdown.serialize(blueprint, BlueprintSchemaLite); - variables.blueprintDependencies = params.blueprint.frameworks?.join(', ') ?? ''; } // Optional language and frameworks diff --git a/worker/agents/schemas.ts b/worker/agents/schemas.ts index 55344e32..7ba540fe 100644 --- a/worker/agents/schemas.ts +++ b/worker/agents/schemas.ts @@ -75,12 +75,16 @@ export const CodeReviewOutput = z.object({ commands: z.array(z.string()).describe('Commands that might be needed to run for fixing an issue. Empty array if no commands are needed'), }); -export const BlueprintSchema = z.object({ - title: z.string().describe('Title of the application'), - projectName: z.string().describe('Name of the project, in small case, no special characters, no spaces, no dots. Only letters, numbers, hyphens, underscores are allowed.'), +export const SimpleBlueprintSchema = z.object({ + title: z.string().describe('Title for the project'), + projectName: z.string().describe('Name for the project, in small case, no special characters, no spaces, no dots. Only letters, numbers, hyphens, underscores are allowed.'), + description: z.string().describe('Short, brief, concise description of the project in a single sentence'), + colorPalette: z.array(z.string()).describe('Color palette RGB codes to be used in the project, only base colors and not their shades, max 3 colors'), + frameworks: z.array(z.string()).describe('Essential Frameworks, libraries and dependencies to be used in the project, with only major versions optionally specified'), +}); + +export const PhasicBlueprintSchema = SimpleBlueprintSchema.extend({ detailedDescription: z.string().describe('Enhanced and detailed description of what the application does and how its supposed to work. Break down the project into smaller components and describe each component in detail.'), - description: z.string().describe('Short, brief, concise description of the application in a single sentence'), - colorPalette: z.array(z.string()).describe('Color palette RGB codes to be used in the application, only base colors and not their shades, max 3 colors'), views: z.array(z.object({ name: z.string().describe('Name of the view'), description: z.string().describe('Description of the view'), @@ -101,10 +105,13 @@ export const BlueprintSchema = z.object({ description: z.string().describe('Description of the phase'), })).describe('Phases of the implementation roadmap'), initialPhase: PhaseConceptSchema.describe('The first phase to be implemented, in **STRICT** accordance with '), - // commands: z.array(z.string()).describe('Commands to set up the development environment and install all dependencies not already in the template. These will run before code generation starts.'), }); -export const BlueprintSchemaLite = BlueprintSchema.omit({ +export const AgenticBlueprintSchema = SimpleBlueprintSchema.extend({ + plan: z.array(z.string()).describe('Step by step plan for implementing the project'), +}); + +export const BlueprintSchemaLite = PhasicBlueprintSchema.omit({ initialPhase: true, }); @@ -124,7 +131,8 @@ export const ScreenshotAnalysisSchema = z.object({ }); export type TemplateSelection = z.infer; -export type Blueprint = z.infer; +export type PhasicBlueprint = z.infer; +export type AgenticBlueprint = z.infer; export type FileConceptType = z.infer; export type PhaseConceptType = z.infer; export type PhaseConceptLiteType = z.infer; @@ -145,4 +153,4 @@ export const ConversationalResponseSchema = z.object({ export type ConversationalResponseType = z.infer; - +export type Blueprint = z.infer | z.infer; diff --git a/worker/agents/services/implementations/CodingAgent.ts b/worker/agents/services/implementations/CodingAgent.ts index d13e1a5c..93f11150 100644 --- a/worker/agents/services/implementations/CodingAgent.ts +++ b/worker/agents/services/implementations/CodingAgent.ts @@ -44,7 +44,7 @@ export class CodingAgentInterface { } } - queueRequest(request: string, images?: ProcessedImageAttachment[]): void { + queueUserRequest(request: string, images?: ProcessedImageAttachment[]): void { this.agentStub.queueUserRequest(request, images); } diff --git a/worker/agents/services/interfaces/ICodingAgent.ts b/worker/agents/services/interfaces/ICodingAgent.ts index 48db5f4d..547dba07 100644 --- a/worker/agents/services/interfaces/ICodingAgent.ts +++ b/worker/agents/services/interfaces/ICodingAgent.ts @@ -1,65 +1,69 @@ -import { FileOutputType, Blueprint, FileConceptType } from "worker/agents/schemas"; +import { FileOutputType, FileConceptType, Blueprint } from "worker/agents/schemas"; import { BaseSandboxService } from "worker/services/sandbox/BaseSandboxService"; import { ExecuteCommandsResponse, PreviewType, StaticAnalysisResponse, RuntimeError } from "worker/services/sandbox/sandboxTypes"; import { ProcessedImageAttachment } from "worker/types/image-attachment"; -import { OperationOptions } from "worker/agents/operations/common"; -import { DeepDebugResult } from "worker/agents/core/types"; +import { BehaviorType, DeepDebugResult } from "worker/agents/core/types"; import { RenderToolCall } from "worker/agents/operations/UserConversationProcessor"; import { WebSocketMessageType, WebSocketMessageData } from "worker/api/websocketTypes"; import { GitVersionControl } from "worker/agents/git/git"; +import { OperationOptions } from "worker/agents/operations/common"; -export abstract class ICodingAgent { - abstract getSandboxServiceClient(): BaseSandboxService; - - abstract getGit(): GitVersionControl; - - abstract deployToSandbox(files: FileOutputType[], redeploy: boolean, commitMessage?: string, clearLogs?: boolean): Promise; - - abstract deployToCloudflare(): Promise<{ deploymentUrl?: string; workersUrl?: string } | null>; - - abstract getLogs(reset?: boolean, durationSeconds?: number): Promise; - - abstract queueUserRequest(request: string, images?: ProcessedImageAttachment[]): void; - - abstract clearConversation(): void; - - abstract updateProjectName(newName: string): Promise; - - abstract updateBlueprint(patch: Partial): Promise; - - abstract getOperationOptions(): OperationOptions; - - abstract readFiles(paths: string[]): Promise<{ files: { path: string; content: string }[] }>; - - abstract runStaticAnalysisCode(files?: string[]): Promise; - - abstract execCommands(commands: string[], shouldSave: boolean, timeout?: number): Promise; +export interface ICodingAgent { + getBehavior(): BehaviorType; + + getLogs(reset?: boolean, durationSeconds?: number): Promise; + + fetchRuntimeErrors(clear?: boolean): Promise; + + deployToSandbox(files?: FileOutputType[], redeploy?: boolean, commitMessage?: string, clearLogs?: boolean): Promise; + + broadcast(msg: T, data?: WebSocketMessageData): void; + + deployToCloudflare(): Promise<{ deploymentUrl?: string; workersUrl?: string } | null>; + + queueUserRequest(request: string, images?: ProcessedImageAttachment[]): void; - abstract regenerateFileByPath(path: string, issues: string[]): Promise<{ path: string; diff: string }>; + deployPreview(clearLogs?: boolean, forceRedeploy?: boolean): Promise; + + clearConversation(): void; + + updateProjectName(newName: string): Promise; + + getOperationOptions(): OperationOptions; + + readFiles(paths: string[]): Promise<{ files: { path: string; content: string }[] }>; + + runStaticAnalysisCode(files?: string[]): Promise; + + execCommands(commands: string[], shouldSave: boolean, timeout?: number): Promise; - abstract generateFiles( + updateBlueprint(patch: Partial): Promise; + + generateFiles( phaseName: string, phaseDescription: string, requirements: string[], files: FileConceptType[] ): Promise<{ files: Array<{ path: string; purpose: string; diff: string }> }>; - abstract fetchRuntimeErrors(clear?: boolean): Promise; - - abstract isCodeGenerating(): boolean; - - abstract waitForGeneration(): Promise; - - abstract isDeepDebugging(): boolean; - - abstract waitForDeepDebug(): Promise; - - abstract broadcast(message: T, data?: WebSocketMessageData): void; - - abstract executeDeepDebug( + regenerateFileByPath(path: string, issues: string[]): Promise<{ path: string; diff: string }>; + + isCodeGenerating(): boolean; + + waitForGeneration(): Promise; + + isDeepDebugging(): boolean; + + waitForDeepDebug(): Promise; + + executeDeepDebug( issue: string, toolRenderer: RenderToolCall, streamCb: (chunk: string) => void, focusPaths?: string[], ): Promise; + + getGit(): GitVersionControl; + + getSandboxServiceClient(): BaseSandboxService; } diff --git a/worker/agents/tools/customTools.ts b/worker/agents/tools/customTools.ts index 51c96d4c..92fc9940 100644 --- a/worker/agents/tools/customTools.ts +++ b/worker/agents/tools/customTools.ts @@ -6,7 +6,6 @@ import { toolFeedbackDefinition } from './toolkit/feedback'; import { createQueueRequestTool } from './toolkit/queue-request'; import { createGetLogsTool } from './toolkit/get-logs'; import { createDeployPreviewTool } from './toolkit/deploy-preview'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; import { createDeepDebuggerTool } from "./toolkit/deep-debugger"; import { createRenameProjectTool } from './toolkit/rename-project'; import { createAlterBlueprintTool } from './toolkit/alter-blueprint'; @@ -21,6 +20,7 @@ import { createGetRuntimeErrorsTool } from './toolkit/get-runtime-errors'; import { createWaitForGenerationTool } from './toolkit/wait-for-generation'; import { createWaitForDebugTool } from './toolkit/wait-for-debug'; import { createGitTool } from './toolkit/git'; +import { ICodingAgent } from '../services/interfaces/ICodingAgent'; export async function executeToolWithDefinition( toolDef: ToolDefinition, @@ -37,7 +37,7 @@ export async function executeToolWithDefinition( * Add new tools here - they're automatically included in the conversation */ export function buildTools( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger, toolRenderer: RenderToolCall, streamCb: (chunk: string) => void, diff --git a/worker/agents/tools/toolkit/alter-blueprint.ts b/worker/agents/tools/toolkit/alter-blueprint.ts index d4e5faa5..96d5dec1 100644 --- a/worker/agents/tools/toolkit/alter-blueprint.ts +++ b/worker/agents/tools/toolkit/alter-blueprint.ts @@ -1,6 +1,6 @@ import { ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; import { Blueprint } from 'worker/agents/schemas'; type AlterBlueprintArgs = { @@ -10,7 +10,7 @@ type AlterBlueprintArgs = { }; export function createAlterBlueprintTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/deep-debugger.ts b/worker/agents/tools/toolkit/deep-debugger.ts index bf15de96..f9e3e154 100644 --- a/worker/agents/tools/toolkit/deep-debugger.ts +++ b/worker/agents/tools/toolkit/deep-debugger.ts @@ -1,10 +1,10 @@ import { ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; import { RenderToolCall } from 'worker/agents/operations/UserConversationProcessor'; export function createDeepDebuggerTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger, toolRenderer: RenderToolCall, streamCb: (chunk: string) => void, diff --git a/worker/agents/tools/toolkit/deploy-preview.ts b/worker/agents/tools/toolkit/deploy-preview.ts index c6be2788..36995c79 100644 --- a/worker/agents/tools/toolkit/deploy-preview.ts +++ b/worker/agents/tools/toolkit/deploy-preview.ts @@ -1,13 +1,13 @@ import { ErrorResult, ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; type DeployPreviewArgs = Record; type DeployPreviewResult = { message: string } | ErrorResult; export function createDeployPreviewTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/exec-commands.ts b/worker/agents/tools/toolkit/exec-commands.ts index c9548d05..3e947455 100644 --- a/worker/agents/tools/toolkit/exec-commands.ts +++ b/worker/agents/tools/toolkit/exec-commands.ts @@ -1,6 +1,6 @@ import { ToolDefinition, ErrorResult } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; import { ExecuteCommandsResponse } from 'worker/services/sandbox/sandboxTypes'; export type ExecCommandsArgs = { @@ -12,7 +12,7 @@ export type ExecCommandsArgs = { export type ExecCommandsResult = ExecuteCommandsResponse | ErrorResult; export function createExecCommandsTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger, ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/generate-files.ts b/worker/agents/tools/toolkit/generate-files.ts index 7091a818..db513d78 100644 --- a/worker/agents/tools/toolkit/generate-files.ts +++ b/worker/agents/tools/toolkit/generate-files.ts @@ -1,6 +1,6 @@ import { ToolDefinition, ErrorResult } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; import { FileConceptType } from 'worker/agents/schemas'; export type GenerateFilesArgs = { @@ -18,7 +18,7 @@ export type GenerateFilesResult = | ErrorResult; export function createGenerateFilesTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger, ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/get-logs.ts b/worker/agents/tools/toolkit/get-logs.ts index a5401058..b2214201 100644 --- a/worker/agents/tools/toolkit/get-logs.ts +++ b/worker/agents/tools/toolkit/get-logs.ts @@ -1,6 +1,6 @@ import { ErrorResult, ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; type GetLogsArgs = { reset?: boolean; @@ -11,7 +11,7 @@ type GetLogsArgs = { type GetLogsResult = { logs: string } | ErrorResult; export function createGetLogsTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/get-runtime-errors.ts b/worker/agents/tools/toolkit/get-runtime-errors.ts index 5c75374d..c3e35277 100644 --- a/worker/agents/tools/toolkit/get-runtime-errors.ts +++ b/worker/agents/tools/toolkit/get-runtime-errors.ts @@ -1,6 +1,6 @@ import { ErrorResult, ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; import { RuntimeError } from 'worker/services/sandbox/sandboxTypes'; type GetRuntimeErrorsArgs = Record; @@ -8,7 +8,7 @@ type GetRuntimeErrorsArgs = Record; type GetRuntimeErrorsResult = { errors: RuntimeError[] } | ErrorResult; export function createGetRuntimeErrorsTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/git.ts b/worker/agents/tools/toolkit/git.ts index 7ca91246..f1f5206c 100644 --- a/worker/agents/tools/toolkit/git.ts +++ b/worker/agents/tools/toolkit/git.ts @@ -1,6 +1,6 @@ import { ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; type GitCommand = 'commit' | 'log' | 'show' | 'reset'; @@ -13,7 +13,7 @@ interface GitToolArgs { } export function createGitTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger, options?: { excludeCommands?: GitCommand[] } ): ToolDefinition { diff --git a/worker/agents/tools/toolkit/queue-request.ts b/worker/agents/tools/toolkit/queue-request.ts index 486bd809..4fd90f5e 100644 --- a/worker/agents/tools/toolkit/queue-request.ts +++ b/worker/agents/tools/toolkit/queue-request.ts @@ -1,13 +1,13 @@ import { ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; type QueueRequestArgs = { modificationRequest: string; }; export function createQueueRequestTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger ): ToolDefinition { return { @@ -34,7 +34,7 @@ export function createQueueRequestTool( logger.info('Received app edit request', { modificationRequest: args.modificationRequest, }); - agent.queueRequest(args.modificationRequest); + agent.queueUserRequest(args.modificationRequest); return null; }, }; diff --git a/worker/agents/tools/toolkit/read-files.ts b/worker/agents/tools/toolkit/read-files.ts index fa8dc0f3..15cb28c4 100644 --- a/worker/agents/tools/toolkit/read-files.ts +++ b/worker/agents/tools/toolkit/read-files.ts @@ -1,6 +1,6 @@ import { ToolDefinition, ErrorResult } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; export type ReadFilesArgs = { paths: string[]; @@ -12,7 +12,7 @@ export type ReadFilesResult = | ErrorResult; export function createReadFilesTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger, ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/regenerate-file.ts b/worker/agents/tools/toolkit/regenerate-file.ts index 7afca870..2fcde525 100644 --- a/worker/agents/tools/toolkit/regenerate-file.ts +++ b/worker/agents/tools/toolkit/regenerate-file.ts @@ -1,6 +1,6 @@ import { ToolDefinition, ErrorResult } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; export type RegenerateFileArgs = { path: string; @@ -12,7 +12,7 @@ export type RegenerateFileResult = | ErrorResult; export function createRegenerateFileTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger, ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/rename-project.ts b/worker/agents/tools/toolkit/rename-project.ts index be7ab416..850161b5 100644 --- a/worker/agents/tools/toolkit/rename-project.ts +++ b/worker/agents/tools/toolkit/rename-project.ts @@ -1,6 +1,6 @@ import { ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; type RenameArgs = { newName: string; @@ -9,7 +9,7 @@ type RenameArgs = { type RenameResult = { projectName: string }; export function createRenameProjectTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/run-analysis.ts b/worker/agents/tools/toolkit/run-analysis.ts index 52e67c78..b95a38c8 100644 --- a/worker/agents/tools/toolkit/run-analysis.ts +++ b/worker/agents/tools/toolkit/run-analysis.ts @@ -1,6 +1,6 @@ import { ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; import { StaticAnalysisResponse } from 'worker/services/sandbox/sandboxTypes'; export type RunAnalysisArgs = { @@ -10,7 +10,7 @@ export type RunAnalysisArgs = { export type RunAnalysisResult = StaticAnalysisResponse; export function createRunAnalysisTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger, ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/wait-for-debug.ts b/worker/agents/tools/toolkit/wait-for-debug.ts index 2852ea5a..59e31185 100644 --- a/worker/agents/tools/toolkit/wait-for-debug.ts +++ b/worker/agents/tools/toolkit/wait-for-debug.ts @@ -1,9 +1,9 @@ import { ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; export function createWaitForDebugTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger ): ToolDefinition, { status: string } | { error: string }> { return { diff --git a/worker/agents/tools/toolkit/wait-for-generation.ts b/worker/agents/tools/toolkit/wait-for-generation.ts index d34224c7..a599f8ff 100644 --- a/worker/agents/tools/toolkit/wait-for-generation.ts +++ b/worker/agents/tools/toolkit/wait-for-generation.ts @@ -1,9 +1,9 @@ import { ToolDefinition } from '../types'; import { StructuredLogger } from '../../../logger'; -import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; +import { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; export function createWaitForGenerationTool( - agent: CodingAgentInterface, + agent: ICodingAgent, logger: StructuredLogger ): ToolDefinition, { status: string } | { error: string }> { return {