From 8ed1a0840b70f3e48a6245a89d4b4d468093ee97 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Tue, 4 Nov 2025 14:29:28 -0500 Subject: [PATCH 01/24] feat: improve template selection reliability and prompt clarity - Added retry mechanism with configurable retry count to handle transient failures - Enhanced prompt formatting with clearer template list presentation and validation rules - Improved error handling to retry on failures before falling back to null selection --- worker/agents/planning/templateSelector.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/worker/agents/planning/templateSelector.ts b/worker/agents/planning/templateSelector.ts index 69adf546..c2bea73f 100644 --- a/worker/agents/planning/templateSelector.ts +++ b/worker/agents/planning/templateSelector.ts @@ -20,14 +20,14 @@ interface SelectTemplateArgs { /** * Uses AI to select the most suitable template for a given query. */ -export async function selectTemplate({ env, query, availableTemplates, inferenceContext, images }: SelectTemplateArgs): Promise { +export async function selectTemplate({ env, query, availableTemplates, inferenceContext, images }: SelectTemplateArgs, retryCount: number = 3): Promise { if (availableTemplates.length === 0) { logger.info("No templates available for selection."); return { selectedTemplateName: null, reasoning: "No templates were available to choose from.", useCase: null, complexity: null, styleSelection: null, projectName: '' }; } try { - logger.info("Asking AI to select a template", { + logger.info(`Asking AI to select a template for the ${retryCount} time`, { query, queryLength: query.length, imagesCount: images?.length || 0, @@ -35,8 +35,10 @@ export async function selectTemplate({ env, query, availableTemplates, inference templateCount: availableTemplates.length }); + const validTemplateNames = availableTemplates.map(t => t.name); + const templateDescriptions = availableTemplates.map((t, index) => - `- Template #${index + 1} \n Name - ${t.name} \n Language: ${t.language}, Frameworks: ${t.frameworks?.join(', ') || 'None'}\n ${t.description.selection}` + `### Template #${index + 1} \n Name - ${t.name} \n Language: ${t.language}, Frameworks: ${t.frameworks?.join(', ') || 'None'}\n Description: \`\`\`${t.description.selection}\`\`\`` ).join('\n\n'); const systemPrompt = `You are an Expert Software Architect at Cloudflare specializing in template selection for rapid development. Your task is to select the most suitable starting template based on user requirements. @@ -81,13 +83,16 @@ Reasoning: "Social template provides user interactions, content sharing, and com ## RULES: - ALWAYS select a template (never return null) - Ignore misleading template names - analyze actual features +- **ONLY** Choose from the list of available templates - Focus on functionality over naming conventions - Provide clear, specific reasoning for selection` const userPrompt = `**User Request:** "${query}" -**Available Templates:** -${templateDescriptions} +## **Available Templates:** +**ONLY** These template names are available for selection: ${validTemplateNames.join(', ')} + +Template detail: ${templateDescriptions} **Task:** Select the most suitable template and provide: 1. Template name (exact match from list) @@ -122,7 +127,6 @@ ENTROPY SEED: ${generateSecureToken(64)} - for unique results`; maxTokens: 2000, }); - logger.info(`AI template selection result: ${selection.selectedTemplateName || 'None'}, Reasoning: ${selection.reasoning}`); return selection; @@ -131,6 +135,10 @@ ENTROPY SEED: ${generateSecureToken(64)} - for unique results`; if (error instanceof RateLimitExceededError || error instanceof SecurityError) { throw error; } + + if (retryCount > 0) { + return selectTemplate({ env, query, availableTemplates, inferenceContext, images }, retryCount - 1); + } // Fallback to no template selection in case of error return { selectedTemplateName: null, reasoning: "An error occurred during the template selection process.", useCase: null, complexity: null, styleSelection: null, projectName: '' }; } From 834cffa6968bc71513640e423a8f504f17d4ef9a Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Tue, 4 Nov 2025 17:18:28 -0500 Subject: [PATCH 02/24] fix: better stale comment removal + correct YAML string escaping --- .github/workflows/claude-issue-triage.yml | 4 +- .github/workflows/claude-reviews.yml | 75 ++++++++++++----------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/.github/workflows/claude-issue-triage.yml b/.github/workflows/claude-issue-triage.yml index a2683c99..bf30dc25 100644 --- a/.github/workflows/claude-issue-triage.yml +++ b/.github/workflows/claude-issue-triage.yml @@ -165,7 +165,7 @@ jobs: ### STAGE 3: Solution Proposal (CONDITIONAL)${{ steps.analyze.outputs.severity == 'high' && '\n\n**⚠️ REQUIRED** - Critical issue, must propose fix' || '\n\n**OPTIONAL** - Propose fix if solution is clear and straightforward' }} - ${{ steps.analyze.outputs.severity == 'high' && '**You MUST propose a fix for this critical issue:**' || '**Propose a fix only if:**\n- Root cause is clear\n- Fix is straightforward (< 50 lines changed)\n- You\'re confident it won\'t introduce regressions' }} + ${{ steps.analyze.outputs.severity == 'high' && '**You MUST propose a fix for this critical issue:**' || '**Propose a fix only if:**\n- Root cause is clear\n- Fix is straightforward (< 50 lines changed)\n- You''re confident it won''t introduce regressions' }} #### 3.1 Design the Fix @@ -249,7 +249,7 @@ jobs: **Comment Structure** (adapt based on investigation depth): - ${{ steps.analyze.outputs.should_investigate == 'true' && '**For investigated issues:**\n\n```markdown\n## Investigation Results\n\nThanks for reporting this, @${{ github.event.issue.user.login }}! I\'ve performed a thorough analysis.\n\n### Classification\n- **Type:** [bug/feature/etc]\n- **Severity:** [P0-critical/P1-high/etc]\n- **Component:** [worker/ui/etc]\n\n### Root Cause Analysis\n[Detailed explanation of what\'s causing the issue]\n\n**Location:** `path/to/file.ts:123`\n\n**Introduced in:** PR #XXX (if found) or Commit ABC123\n\n### Proposed Solution\n[Explain the fix approach]\n\n### Status\n- ✅ PR #XXX created with proposed fix (if applicable)\n- ⏳ Requires manual review and testing\n- 📋 Added to backlog for team review (if no PR)\n\n### Next Steps\n[What happens next - PR review, team discussion, etc.]\n```' || '**For standard triage:**\n\n```markdown\nThanks for reporting this, @${{ github.event.issue.user.login }}!\n\n### Classification\n- **Labels:** [list applied labels]\n- **Priority:** [explanation of priority]\n\n[Context-specific response based on issue type]\n\n### Next Steps\nThe team will review this and provide updates.\n```' }} + ${{ steps.analyze.outputs.should_investigate == 'true' && '**For investigated issues:**\n\n```markdown\n## Investigation Results\n\nThanks for reporting this, @${{ github.event.issue.user.login }}! I''ve performed a thorough analysis.\n\n### Classification\n- **Type:** [bug/feature/etc]\n- **Severity:** [P0-critical/P1-high/etc]\n- **Component:** [worker/ui/etc]\n\n### Root Cause Analysis\n[Detailed explanation of what''s causing the issue]\n\n**Location:** `path/to/file.ts:123`\n\n**Introduced in:** PR #XXX (if found) or Commit ABC123\n\n### Proposed Solution\n[Explain the fix approach]\n\n### Status\n- ✅ PR #XXX created with proposed fix (if applicable)\n- ⏳ Requires manual review and testing\n- 📋 Added to backlog for team review (if no PR)\n\n### Next Steps\n[What happens next - PR review, team discussion, etc.]\n```' || '**For standard triage:**\n\n```markdown\nThanks for reporting this, @${{ github.event.issue.user.login }}!\n\n### Classification\n- **Labels:** [list applied labels]\n- **Priority:** [explanation of priority]\n\n[Context-specific response based on issue type]\n\n### Next Steps\nThe team will review this and provide updates.\n```' }} --- diff --git a/.github/workflows/claude-reviews.yml b/.github/workflows/claude-reviews.yml index 0117ff8f..f85694c1 100644 --- a/.github/workflows/claude-reviews.yml +++ b/.github/workflows/claude-reviews.yml @@ -45,32 +45,6 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Minimize Old Review Comments - run: | - echo "Collapsing previous review comments from github-actions[bot]..." - - # Get all comments from github-actions[bot] on this PR - OLD_REVIEWS=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \ - --jq '[.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("## Code Review") or contains("🔒 **CRITICAL PATH SECURITY REVIEW**") or contains("## 🔍 Code Quality & Security Review")) | .id]') - - if [ -n "$OLD_REVIEWS" ] && [ "$OLD_REVIEWS" != "[]" ]; then - echo "Found old review comments to collapse" - echo "$OLD_REVIEWS" | jq -r '.[]' | while read comment_id; do - echo "Collapsing comment $comment_id" - gh api repos/${{ github.repository }}/issues/comments/$comment_id -X PATCH \ - -f body="
🔒 Previous review (outdated) - - This review has been superseded by a newer review. -
" || echo "Failed to collapse comment $comment_id" - done - echo "✓ Old comments collapsed" - else - echo "No old review comments found" - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - continue-on-error: true - - name: Run Comprehensive Review uses: anthropics/claude-code-action@v1 with: @@ -137,16 +111,8 @@ jobs: [Overall assessment with approval/disapproval reasoning] ``` - 5. Post review (MANDATORY FINAL STEP - use single efficient command) + 5. Post review (MANDATORY FINAL STEP) ```bash - # Post new review (collapse old ones first if any exist) - OLD_IDS=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments --jq '[.[] | select(.user.login == "github-actions[bot]") | select(.body | startswith("## Code & Security Review")) | .id] | @csv' | tr -d '"') - if [ -n "$OLD_IDS" ]; then - for id in ${OLD_IDS//,/ }; do - gh api -X PATCH repos/${{ github.repository }}/issues/comments/$id -f body="
Outdated
" & - done - wait - fi gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "YOUR_REVIEW_HERE" ``` @@ -180,3 +146,42 @@ jobs: --allowed-tools "mcp__github_inline_comment__create_inline_comment,Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh search:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr edit:*),Bash(gh api:*)" --max-turns ${{ steps.critical_paths.outputs.is_critical == 'true' && '90' || '65' }} --model claude-sonnet-4-5-20250929 + + - name: Intelligent Comment Cleanup + uses: anthropics/claude-code-action@v1 + if: always() + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + Clean up stale bot comments on PR #${{ github.event.pull_request.number }}. + + **Task:** + 1. Fetch all comments on this PR + 2. Identify bot comments (users ending in [bot]) that are stale/outdated: + - Old reviews superseded by newer ones + - Old PR description suggestions + - Previously collapsed/outdated markers + - Progress/status comments from previous workflow runs + 3. Keep only the most recent comment per category per bot + 4. DELETE all stale comments (do not collapse) + + **Get all comments:** + ```bash + gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments --jq '.[] | {id, user: .user.login, body, created_at}' + ``` + + **Delete a comment:** + ```bash + gh api repos/${{ github.repository }}/issues/comments/COMMENT_ID -X DELETE + ``` + + Be intelligent: + - Preserve the newest useful comment in each category + - Delete everything else that's redundant or stale + - If unsure, keep the comment (conservative approach) + + claude_args: | + --allowed-tools "Bash(gh api repos/*/issues/*/comments:*),Bash(gh api repos/*/issues/comments/*:*)" + --max-turns 8 + --model claude-haiku-4-5-20251001 From 718b00e3644af181e7501ca53dbf3b831eb6382b Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Wed, 5 Nov 2025 22:01:22 -0500 Subject: [PATCH 03/24] refactor: improve generation state handling on reconnect and cancellation - Move auto-resume logic to initial state restoration for more reliable reconnection handling - Update agent state handler to only manage UI state, not trigger generation - Add proper phase cancellation tracking when generation is stopped by user --- .../chat/utils/handle-websocket-message.ts | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/routes/chat/utils/handle-websocket-message.ts b/src/routes/chat/utils/handle-websocket-message.ts index d37f37e5..327fceb8 100644 --- a/src/routes/chat/utils/handle-websocket-message.ts +++ b/src/routes/chat/utils/handle-websocket-message.ts @@ -251,6 +251,12 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { } setIsInitialStateRestored(true); + + if (state.shouldBeGenerating && !isGenerating) { + logger.debug('🔄 Reconnected with shouldBeGenerating=true, auto-resuming generation'); + updateStage('code', { status: 'active' }); + sendWebSocketMessage(websocket, 'generate_all'); + } } break; } @@ -258,13 +264,10 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { const { state } = message; logger.debug('🔄 Agent state update received:', state); - if (state.shouldBeGenerating) { - logger.debug('🔄 shouldBeGenerating=true detected, auto-resuming generation'); + if (state.shouldBeGenerating && !isGenerating) { + logger.debug('🔄 shouldBeGenerating=true, updating UI to active state'); updateStage('code', { status: 'active' }); - - logger.debug('📡 Sending auto-resume generate_all message'); - sendWebSocketMessage(websocket, 'generate_all'); - } else { + } else if (!state.shouldBeGenerating) { const codeStage = projectStages.find((stage: any) => stage.id === 'code'); if (codeStage?.status === 'active' && !isGenerating) { if (state.generatedFilesMap && Object.keys(state.generatedFilesMap).length > 0) { @@ -684,6 +687,23 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { setIsPhaseProgressActive(false); setIsThinking(false); + // Mark any active phases as cancelled (not completed, since they were interrupted) + setPhaseTimeline((prev) => + prev.map(phase => + (phase.status === 'generating' || phase.status === 'validating') + ? { + ...phase, + status: 'cancelled' as const, + files: phase.files.map(file => + file.status === 'generating' || file.status === 'validating' + ? { ...file, status: 'cancelled' as const } + : file + ) + } + : phase + ) + ); + // Show toast notification for user-initiated stop toast.info('Generation stopped', { description: message.message || 'Code generation has been stopped' From d3f984c3fd8e5ce4909f5849b97c3ecd3b900dd2 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Wed, 5 Nov 2025 22:15:44 -0500 Subject: [PATCH 04/24] fix: typeScript features configurable to reduce memory - Fixed model disposal to prevent memory leaks - Improved value updates using pushEditOperations for better undo/redo support --- .../monaco-editor/monaco-editor.tsx | 114 ++++++++++++------ src/routes/app/index.tsx | 2 +- src/routes/chat/chat.tsx | 2 +- 3 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/components/monaco-editor/monaco-editor.tsx b/src/components/monaco-editor/monaco-editor.tsx index 5777877e..b4900047 100644 --- a/src/components/monaco-editor/monaco-editor.tsx +++ b/src/components/monaco-editor/monaco-editor.tsx @@ -36,7 +36,7 @@ self.MonacoEnvironment = { }; // From GitHub Dark theme -monaco.editor.defineTheme('v1-dev-dark', { +monaco.editor.defineTheme('vibesdk-dark', { base: 'vs-dark', inherit: true, rules: [ @@ -77,7 +77,7 @@ monaco.editor.defineTheme('v1-dev-dark', { }, }); -monaco.editor.defineTheme('v1-dev', { +monaco.editor.defineTheme('vibesdk', { base: 'vs', inherit: true, rules: [ @@ -116,43 +116,13 @@ monaco.editor.defineTheme('v1-dev', { }, }); -monaco.editor.setTheme('v1-dev'); - -monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: true, - noSyntaxValidation: true, -}); - -// Configure TypeScript defaults for JSX support -monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ - jsx: monaco.languages.typescript.JsxEmit.React, - allowJs: true, - allowSyntheticDefaultImports: true, - esModuleInterop: true, - moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, - module: monaco.languages.typescript.ModuleKind.ESNext, - target: monaco.languages.typescript.ScriptTarget.ESNext, - jsxFactory: 'React.createElement', - jsxFragmentFactory: 'React.Fragment', -}); - -// Configure JavaScript defaults for JSX support -monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - allowJs: true, - allowSyntheticDefaultImports: true, - esModuleInterop: true, - jsx: monaco.languages.typescript.JsxEmit.React, - moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, - module: monaco.languages.typescript.ModuleKind.ESNext, - target: monaco.languages.typescript.ScriptTarget.ESNext, - jsxFactory: 'React.createElement', - jsxFragmentFactory: 'React.Fragment', -}); +monaco.editor.setTheme('vibesdk'); export type MonacoEditorProps = React.ComponentProps<'div'> & { createOptions?: monaco.editor.IStandaloneEditorConstructionOptions; find?: string; replace?: string; + enableTypeScriptFeatures?: 'auto' | boolean; }; /** @@ -163,6 +133,7 @@ export const MonacoEditor = memo(function MonacoEditor({ createOptions = {}, find, replace, + enableTypeScriptFeatures = 'auto', ...props }) { const containerRef = useRef(null); @@ -171,6 +142,63 @@ export const MonacoEditor = memo(function MonacoEditor({ const stickyScroll = useRef(true); const { theme } = useTheme(); + const shouldEnableTypeScript = React.useMemo(() => { + if (enableTypeScriptFeatures === 'auto') { + return !createOptions.readOnly; + } + return enableTypeScriptFeatures; + }, [enableTypeScriptFeatures, createOptions.readOnly]); + + // Configure TypeScript diagnostics based on mode + useEffect(() => { + const tsDefaults = monaco.languages.typescript.typescriptDefaults; + const jsDefaults = monaco.languages.typescript.javascriptDefaults; + + if (shouldEnableTypeScript) { + // Enable full IntelliSense for editing + tsDefaults.setDiagnosticsOptions({ + noSemanticValidation: false, + noSyntaxValidation: false, + }); + tsDefaults.setCompilerOptions({ + jsx: monaco.languages.typescript.JsxEmit.React, + allowJs: true, + allowSyntheticDefaultImports: true, + esModuleInterop: true, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + module: monaco.languages.typescript.ModuleKind.ESNext, + target: monaco.languages.typescript.ScriptTarget.ESNext, + jsxFactory: 'React.createElement', + jsxFragmentFactory: 'React.Fragment', + }); + jsDefaults.setCompilerOptions({ + allowJs: true, + allowSyntheticDefaultImports: true, + esModuleInterop: true, + jsx: monaco.languages.typescript.JsxEmit.React, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + module: monaco.languages.typescript.ModuleKind.ESNext, + target: monaco.languages.typescript.ScriptTarget.ESNext, + jsxFactory: 'React.createElement', + jsxFragmentFactory: 'React.Fragment', + }); + } else { + // Disable expensive features for viewing + tsDefaults.setDiagnosticsOptions({ + noSemanticValidation: true, + noSyntaxValidation: true, + }); + tsDefaults.setCompilerOptions({ + jsx: monaco.languages.typescript.JsxEmit.React, + target: monaco.languages.typescript.ScriptTarget.ESNext, + }); + jsDefaults.setCompilerOptions({ + jsx: monaco.languages.typescript.JsxEmit.React, + target: monaco.languages.typescript.ScriptTarget.ESNext, + }); + } + }, [shouldEnableTypeScript]); + useEffect(() => { let configuredTheme = theme; @@ -180,7 +208,7 @@ export const MonacoEditor = memo(function MonacoEditor({ editor.current = monaco.editor.create(containerRef.current!, { language: createOptions.language || 'typescript', minimap: { enabled: false }, - theme: configuredTheme === 'dark' ? 'v1-dev-dark' : 'v1-dev', + theme: configuredTheme === 'dark' ? 'vibesdk-dark' : 'vibesdk', automaticLayout: true, value: defaultCode, fontSize: 13, @@ -207,6 +235,10 @@ export const MonacoEditor = memo(function MonacoEditor({ } return () => { + const model = editor.current?.getModel(); + if (model) { + model.dispose(); + } editor.current?.dispose(); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -217,10 +249,16 @@ export const MonacoEditor = memo(function MonacoEditor({ const model = editor.current.getModel(); if (!model) return; - editor.current.setValue(createOptions.value || ''); + model.pushEditOperations( + [], + [{ + range: model.getFullModelRange(), + text: createOptions.value || '' + }], + () => null + ); if (stickyScroll.current) { - // Scroll to bottom const lineCount = model.getLineCount(); editor.current.revealLine(lineCount); } @@ -293,7 +331,7 @@ export const MonacoEditor = memo(function MonacoEditor({ // Update theme when app theme changes useEffect(() => { if (editor.current) { - monaco.editor.setTheme(theme === 'dark' ? 'v1-dev-dark' : 'v1-dev'); + monaco.editor.setTheme(theme === 'dark' ? 'vibesdk-dark' : 'vibesdk'); } }, [theme]); diff --git a/src/routes/app/index.tsx b/src/routes/app/index.tsx index b576d1f9..03a9cdc1 100644 --- a/src/routes/app/index.tsx +++ b/src/routes/app/index.tsx @@ -1000,7 +1000,7 @@ export default function AppView() { 'on', scrollBeyondLastLine: false, fontSize: 13, - theme: 'v1-dev', + theme: 'vibesdk', automaticLayout: true, }} /> diff --git a/src/routes/chat/chat.tsx b/src/routes/chat/chat.tsx index 71803cea..a7dda364 100644 --- a/src/routes/chat/chat.tsx +++ b/src/routes/chat/chat.tsx @@ -1183,7 +1183,7 @@ export default function Chat() { lineNumbers: 'on', scrollBeyondLastLine: false, fontSize: 13, - theme: 'v1-dev', + theme: 'vibesdk', automaticLayout: true, }} find={ From 0dd3e7ef357a0193d6f8f5de9771306ab13754bd Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Wed, 5 Nov 2025 22:23:22 -0500 Subject: [PATCH 05/24] fix: don't wait for runtime errors if app not deployed --- worker/agents/core/simpleGeneratorAgent.ts | 13 ++++++++----- .../services/implementations/DeploymentManager.ts | 5 ++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/worker/agents/core/simpleGeneratorAgent.ts b/worker/agents/core/simpleGeneratorAgent.ts index d36e4437..3346814a 100644 --- a/worker/agents/core/simpleGeneratorAgent.ts +++ b/worker/agents/core/simpleGeneratorAgent.ts @@ -7,7 +7,7 @@ import { FileOutputType, PhaseImplementationSchemaType, } from '../schemas'; -import { ExecuteCommandsResponse, GitHubPushRequest, PreviewType, StaticAnalysisResponse, TemplateDetails } from '../../services/sandbox/sandboxTypes'; +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'; @@ -1435,8 +1435,10 @@ export class SimpleCodeGeneratorAgent extends Agent { return this.ctx.getWebSockets(); } - async fetchRuntimeErrors(clear: boolean = true) { - await this.deploymentManager.waitForPreview(); + async fetchRuntimeErrors(clear: boolean = true, shouldWait: boolean = true): Promise { + if (shouldWait) { + await this.deploymentManager.waitForPreview(); + } try { const errors = await this.deploymentManager.fetchRuntimeErrors(clear); @@ -1454,7 +1456,8 @@ export class SimpleCodeGeneratorAgent extends Agent { this.logger().error("Exception fetching runtime errors:", error); // If fetch fails, initiate redeploy this.deployToSandbox(); - return []; + const message = ""; + return [{ message, timestamp: new Date().toISOString(), level: 0, rawOutput: message }]; } } @@ -2429,7 +2432,7 @@ export class SimpleCodeGeneratorAgent extends Agent { await this.ensureTemplateDetails(); // Just fetch runtime errors - const errors = await this.fetchRuntimeErrors(false); + const errors = await this.fetchRuntimeErrors(false, false); const projectUpdates = await this.getAndResetProjectUpdates(); this.logger().info('Passing context to user conversation processor', { errors, projectUpdates }); diff --git a/worker/agents/services/implementations/DeploymentManager.ts b/worker/agents/services/implementations/DeploymentManager.ts index 41a49260..48c1c668 100644 --- a/worker/agents/services/implementations/DeploymentManager.ts +++ b/worker/agents/services/implementations/DeploymentManager.ts @@ -258,12 +258,11 @@ export class DeploymentManager extends BaseAgentService implements IDeploymentMa */ async fetchRuntimeErrors(clear: boolean = true): Promise { const { sandboxInstanceId } = this.getState(); - const logger = this.getLog(); - const client = this.getClient(); - if (!sandboxInstanceId) { throw new Error('No sandbox instance available for runtime error fetching'); } + const logger = this.getLog(); + const client = this.getClient(); const resp = await client.getInstanceErrors(sandboxInstanceId, clear); From d51310b77723c87749a34c20b78a415054298eaa Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Wed, 5 Nov 2025 22:34:42 -0500 Subject: [PATCH 06/24] fix: unescape commit messages before git operations - Process escaped newlines and tabs in commit messages to preserve proper formatting - Apply unescaping in both FileManager and git tool for consistency --- worker/agents/services/implementations/FileManager.ts | 5 +++-- worker/agents/tools/toolkit/git.ts | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/worker/agents/services/implementations/FileManager.ts b/worker/agents/services/implementations/FileManager.ts index fea5878b..3ad856d4 100644 --- a/worker/agents/services/implementations/FileManager.ts +++ b/worker/agents/services/implementations/FileManager.ts @@ -133,8 +133,9 @@ export class FileManager implements IFileManager { if (shouldCommit) { // If commit message is available, commit, else stage if (commitMessage) { - console.log(`[FileManager] Committing ${fileStates.length} files:`, commitMessage); - await this.git.commit(fileStates, commitMessage); + const unescapedMessage = commitMessage.replace(/\\n/g, '\n').replace(/\\t/g, '\t'); + console.log(`[FileManager] Committing ${fileStates.length} files:`, unescapedMessage); + await this.git.commit(fileStates, unescapedMessage); console.log(`[FileManager] Commit successful`); } else { console.log(`[FileManager] Staging ${fileStates.length} files`); diff --git a/worker/agents/tools/toolkit/git.ts b/worker/agents/tools/toolkit/git.ts index aa82f27d..7ca91246 100644 --- a/worker/agents/tools/toolkit/git.ts +++ b/worker/agents/tools/toolkit/git.ts @@ -76,8 +76,10 @@ export function createGitTool( }; } - logger.info('Git commit', { message }); - const commitOid = await gitInstance.commit([], message); + const unescapedMessage = message.replace(/\\n/g, '\n').replace(/\\t/g, '\t'); + + logger.info('Git commit', { message: unescapedMessage }); + const commitOid = await gitInstance.commit([], unescapedMessage); return { success: true, From 188ba0d1bdf7f30fd258275804e311921457d5e9 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Wed, 5 Nov 2025 22:35:16 -0500 Subject: [PATCH 07/24] fix: prevent race condition in code generation state - Set isGenerating flag immediately before triggering generation to avoid UI state inconsistencies - Added flag update for both new chat and reconnection scenarios --- src/routes/chat/hooks/use-chat.ts | 1 + src/routes/chat/utils/handle-websocket-message.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/routes/chat/hooks/use-chat.ts b/src/routes/chat/hooks/use-chat.ts index 4194426e..05b254b6 100644 --- a/src/routes/chat/hooks/use-chat.ts +++ b/src/routes/chat/hooks/use-chat.ts @@ -294,6 +294,7 @@ export function useChat({ // Request file generation for new chats only if (!disableGenerate && urlChatId === 'new') { logger.debug('🔄 Starting code generation for new chat'); + setIsGenerating(true); sendWebSocketMessage(ws, 'generate_all'); } }); diff --git a/src/routes/chat/utils/handle-websocket-message.ts b/src/routes/chat/utils/handle-websocket-message.ts index 327fceb8..3636aef7 100644 --- a/src/routes/chat/utils/handle-websocket-message.ts +++ b/src/routes/chat/utils/handle-websocket-message.ts @@ -254,6 +254,7 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { if (state.shouldBeGenerating && !isGenerating) { logger.debug('🔄 Reconnected with shouldBeGenerating=true, auto-resuming generation'); + setIsGenerating(true); updateStage('code', { status: 'active' }); sendWebSocketMessage(websocket, 'generate_all'); } From fc340772bb53e14eff3f8914e9f588aece32245f Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Fri, 7 Nov 2025 03:52:22 -0500 Subject: [PATCH 08/24] refactor: generalize coding agent to behavior + business; part 1 - Split monolithic simpleCodeGeneratorAgent into baseAgentBehavior and PhasicAgentBehavior - CodeGeneratorAgent now inherits Agent class (DO) and uses behavior - AgentBehavior relies on AgentInfrastructure (provided by DO) to work, allows to write non-DO coding agents (like cli based) - Phasic for legacy state machine app builder, Agentic for agent driven loop - Agentic would support multiple project types - 'app', 'workflow', 'slides/docs' --- worker/agents/assistants/codeDebugger.ts | 4 +- .../{simpleGeneratorAgent.ts => baseAgent.ts} | 1027 ++++------------- worker/agents/core/phasic/behavior.ts | 852 ++++++++++++++ worker/agents/core/smartGeneratorAgent.ts | 107 +- worker/agents/core/state.ts | 105 +- worker/agents/core/types.ts | 37 +- worker/agents/core/websocket.ts | 9 +- .../agents/domain/values/GenerationContext.ts | 129 ++- worker/agents/index.ts | 3 - worker/agents/operations/FileRegeneration.ts | 5 +- worker/agents/operations/PhaseGeneration.ts | 5 +- .../agents/operations/PhaseImplementation.ts | 62 +- .../agents/operations/PostPhaseCodeFixer.ts | 5 +- .../agents/operations/ScreenshotAnalysis.ts | 134 --- .../agents/operations/SimpleCodeGeneration.ts | 280 +++++ .../operations/UserConversationProcessor.ts | 5 +- worker/agents/operations/common.ts | 34 +- worker/agents/planning/blueprint.ts | 145 ++- worker/agents/prompts.ts | 34 +- worker/agents/schemas.ts | 26 +- .../services/implementations/CodingAgent.ts | 2 +- .../services/interfaces/ICodingAgent.ts | 92 +- worker/agents/tools/customTools.ts | 4 +- .../agents/tools/toolkit/alter-blueprint.ts | 4 +- worker/agents/tools/toolkit/deep-debugger.ts | 4 +- worker/agents/tools/toolkit/deploy-preview.ts | 4 +- worker/agents/tools/toolkit/exec-commands.ts | 4 +- worker/agents/tools/toolkit/generate-files.ts | 4 +- worker/agents/tools/toolkit/get-logs.ts | 4 +- .../tools/toolkit/get-runtime-errors.ts | 4 +- worker/agents/tools/toolkit/git.ts | 4 +- worker/agents/tools/toolkit/queue-request.ts | 6 +- worker/agents/tools/toolkit/read-files.ts | 4 +- .../agents/tools/toolkit/regenerate-file.ts | 4 +- worker/agents/tools/toolkit/rename-project.ts | 4 +- worker/agents/tools/toolkit/run-analysis.ts | 4 +- worker/agents/tools/toolkit/wait-for-debug.ts | 4 +- .../tools/toolkit/wait-for-generation.ts | 4 +- 38 files changed, 1889 insertions(+), 1279 deletions(-) rename worker/agents/core/{simpleGeneratorAgent.ts => baseAgent.ts} (68%) create mode 100644 worker/agents/core/phasic/behavior.ts delete mode 100644 worker/agents/operations/ScreenshotAnalysis.ts create mode 100644 worker/agents/operations/SimpleCodeGeneration.ts diff --git a/worker/agents/assistants/codeDebugger.ts b/worker/agents/assistants/codeDebugger.ts index f31f8b75..45157ad8 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. @@ -544,7 +544,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 68% rename from worker/agents/core/simpleGeneratorAgent.ts rename to worker/agents/core/baseAgent.ts index 3975d570..60daad52 100644 --- a/worker/agents/core/simpleGeneratorAgent.ts +++ b/worker/agents/core/baseAgent.ts @@ -1,32 +1,23 @@ -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'; @@ -34,137 +25,126 @@ import { InferenceContext, AgentActionKey } from '../inferutils/config.types'; import { AGENT_CONFIG } from '../inferutils/config'; 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[] = [] - private generationPromise: Promise | null = null; - private currentAbortController?: AbortController; - private deepDebugPromise: Promise<{ transcript: string } | { error: string }> | null = null; - private deepDebugConversationId: string | null = null; + protected pendingUserImages: ProcessedImageAttachment[] = [] + protected generationPromise: Promise | null = null; + protected currentAbortController?: AbortController; + protected deepDebugPromise: Promise<{ transcript: string } | { error: string }> | null = null; + protected deepDebugConversationId: string | 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( @@ -187,110 +167,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(), @@ -329,29 +218,15 @@ 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`); } - private async gitInit() { + protected async gitInit() { try { await this.git.init(); this.logger().info("Git initialized successfully"); @@ -374,19 +249,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 }); @@ -427,7 +290,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.'); @@ -559,7 +422,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); @@ -568,10 +431,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(), @@ -619,38 +482,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 @@ -701,36 +533,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, { @@ -752,7 +555,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"); @@ -764,7 +567,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] @@ -777,7 +579,7 @@ export class SimpleCodeGeneratorAgent extends Agent { } } - private fetchPendingUserRequests(): string[] { + protected fetchPendingUserRequests(): string[] { const inputs = this.state.pendingUserInputs; if (inputs.length > 0) { this.setState({ @@ -792,7 +594,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; @@ -801,11 +603,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() @@ -814,69 +616,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 staticAnalysisCache: StaticAnalysisResponse | undefined; - 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; - 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"); + await this.build(); } catch (error) { if (error instanceof RateLimitExceededError) { this.logger().error("Error in state machine:", error); @@ -904,203 +645,10 @@ export class SimpleCodeGeneratorAgent extends Agent { } /** - * 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 + * 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 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; - } + abstract build(): Promise async executeDeepDebug( issue: string, @@ -1128,7 +676,7 @@ export class SimpleCodeGeneratorAgent extends Agent { const out = await dbg.run( { issue, previousTranscript }, - { filesIndex, agent: this.codingAgent, runtimeErrors }, + { filesIndex, agent: this, runtimeErrors }, streamCb, toolRenderer, ); @@ -1155,192 +703,6 @@ export class SimpleCodeGeneratorAgent extends Agent { return await debugPromise; } - /** - * 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 @@ -1404,7 +766,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 { @@ -1416,25 +778,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(); @@ -1482,41 +837,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(); @@ -1601,7 +925,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 @@ -1633,13 +957,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', @@ -1649,25 +977,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; } @@ -1764,32 +1099,54 @@ export class SimpleCodeGeneratorAgent extends Agent { requirementsCount: requirements.length, filesCount: files.length }); + + // Broadcast file generation started + this.broadcast(WebSocketMessageResponses.PHASE_IMPLEMENTING, { + message: `Generating files: ${phaseName}`, + phaseName + }); - // 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, + const operation = new SimpleCodeGenerationOperation(); + const result = await operation.execute( { - runtimeErrors: [], - staticAnalysis: { - success: true, - lint: { issues: [] }, - typecheck: { issues: [] } + phaseName, + phaseDescription, + requirements, + files, + fileGeneratingCallback: (filePath: string, filePurpose: string) => { + this.broadcast(WebSocketMessageResponses.FILE_GENERATING, { + message: `Generating file: ${filePath}`, + filePath, + filePurpose + }); }, + fileChunkGeneratedCallback: (filePath: string, chunk: string, format: 'full_content' | 'unified_diff') => { + this.broadcast(WebSocketMessageResponses.FILE_CHUNK_GENERATED, { + message: `Generating file: ${filePath}`, + filePath, + chunk, + format + }); + }, + fileClosedCallback: (file, message) => { + this.broadcast(WebSocketMessageResponses.FILE_GENERATED, { + message, + file + }); + } }, - { suggestions: requirements }, - true, // streamChunks - false // postPhaseFixing = false (skip auto-fixes) + this.getOperationOptions() + ); + + await this.fileManager.saveGeneratedFiles( + result.files, + `feat: ${phaseName}\n\n${phaseDescription}` ); + this.logger().info('Files generated and saved', { + fileCount: result.files.length + }); + // Return files with diffs from FileState return { files: result.files.map(f => ({ @@ -1800,6 +1157,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 { // Call deployment manager with callbacks for broadcasting at the right times const result = await this.deploymentManager.deployToSandbox( @@ -1979,14 +1346,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, @@ -2006,14 +1373,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 @@ -2059,7 +1426,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'); @@ -2193,7 +1560,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 4cca09c9..07855801 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; @@ -186,10 +187,10 @@ const userPromptFormatter = (issues: IssueReport, userSuggestions?: string[], is 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 } = inputs; const { env, logger, context } = options; diff --git a/worker/agents/operations/PhaseImplementation.ts b/worker/agents/operations/PhaseImplementation.ts index 4de9f8ba..76611f58 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 @@ -396,30 +397,6 @@ Goal: Thoroughly review the entire codebase generated in previous phases. Identi This phase prepares the code for final deployment.`; -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 ''; @@ -456,10 +433,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; @@ -580,37 +557,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 new file mode 100644 index 00000000..fb572ea5 --- /dev/null +++ b/worker/agents/operations/SimpleCodeGeneration.ts @@ -0,0 +1,280 @@ +import { FileConceptType, FileOutputType } from '../schemas'; +import { createUserMessage, createSystemMessage } from '../inferutils/common'; +import { executeInference } from '../inferutils/infer'; +import { PROMPT_UTILS } from '../prompts'; +import { AgentOperation, getSystemPromptWithProjectContext, OperationOptions } from './common'; +import { SCOFFormat, SCOFParsingState } from '../output-formats/streaming-formats/scof'; +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 { + phaseName: string; + phaseDescription: string; + requirements: string[]; + files: FileConceptType[]; + fileGeneratingCallback?: (filePath: string, filePurpose: string) => void; + fileChunkGeneratedCallback?: (filePath: string, chunk: string, format: 'full_content' | 'unified_diff') => void; + fileClosedCallback?: (file: FileOutputType, message: string) => void; +} + +export interface SimpleCodeGenerationOutputs { + files: FileOutputType[]; +} + +const SYSTEM_PROMPT = `You are an expert Cloudflare developer specializing in Cloudflare Workers and Workflows. + +Your task is to generate production-ready code based on the provided specifications. + +## Original User Request +{{userQuery}} + +## Project Context +{{projectContext}} + +## Template Information +{{template}} + +## Previously Generated Files +{{existingFiles}} + +## Critical Guidelines +- Write clean, type-safe TypeScript code +- Follow best practices for the specific project type +- For Workflows: use WorkflowEntrypoint, step.do(), step.sleep() patterns +- For Workers: use standard Worker patterns with Request/Response +- Ensure all imports are correct +- Add proper error handling +- Include JSDoc comments where helpful +- Consider the context of existing files when generating new code +- Ensure new code integrates well with previously generated files`; + +const USER_PROMPT = `Generate code for the following phase: + +**Phase Name:** {{phaseName}} +**Description:** {{phaseDescription}} + +**Requirements:** +{{requirements}} + +**Files to Generate:** +{{files}} + +Generate complete, production-ready code for all specified files.`; + +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 formatRequirements = (requirements: string[]): string => { + return requirements.map((req, index) => `${index + 1}. ${req}`).join('\n'); +}; + +const formatFiles = (files: FileConceptType[]): string => { + return files.map((file, index) => { + return `${index + 1}. **${file.path}** + Purpose: ${file.purpose} + ${file.changes ? `Changes needed: ${file.changes}` : 'Create new file'}`; + }).join('\n\n'); +}; + +const formatExistingFiles = (allFiles: FileState[]): string => { + if (!allFiles || allFiles.length === 0) { + return 'No files generated yet. This is the first generation phase.'; + } + + // Convert FileState[] to FileOutputType[] format for serializer + const filesForSerializer: FileOutputType[] = allFiles.map(file => ({ + filePath: file.filePath, + fileContents: file.fileContents, + filePurpose: file.filePurpose || 'Previously generated file' + })); + + // Use existing serializer from PROMPT_UTILS + return PROMPT_UTILS.serializeFiles(filesForSerializer, CodeSerializerType.SIMPLE); +}; + +export class SimpleCodeGenerationOperation extends AgentOperation< + GenerationContext, + SimpleCodeGenerationInputs, + SimpleCodeGenerationOutputs +> { + async execute( + inputs: SimpleCodeGenerationInputs, + options: OperationOptions + ): Promise { + const { phaseName, phaseDescription, requirements, files } = inputs; + const { env, logger, context, inferenceContext } = options; + + logger.info('Generating code via simple code generation', { + phaseName, + phaseDescription, + fileCount: files.length, + requirementCount: requirements.length, + existingFilesCount: context.allFiles.length, + hasUserQuery: !!context.query, + hasTemplateDetails: !!context.templateDetails + }); + + // Build project context + const projectContext = context.templateDetails + ? PROMPT_UTILS.serializeTemplate(context.templateDetails) + : 'No template context available'; + + // Format existing files for context + const existingFilesContext = formatExistingFiles(context.allFiles); + + // Build system message with full context + const systemPrompt = PROMPT_UTILS.replaceTemplateVariables(SYSTEM_PROMPT, { + userQuery: context.query || 'No specific user query available', + projectContext, + template: context.templateDetails ? PROMPT_UTILS.serializeTemplate(context.templateDetails) : 'No template information', + existingFiles: existingFilesContext + }); + + // Build user message with requirements + const userPrompt = PROMPT_UTILS.replaceTemplateVariables(USER_PROMPT, { + phaseName, + phaseDescription, + requirements: formatRequirements(requirements), + files: formatFiles(files) + }); + + const codeGenerationFormat = new SCOFFormat(); + const messages = [ + createSystemMessage(systemPrompt), + createUserMessage(userPrompt + codeGenerationFormat.formatInstructions()) + ]; + + // Initialize streaming state + const streamingState: CodeGenerationStreamingState = { + accumulator: '', + completedFiles: new Map(), + parsingState: {} as SCOFParsingState + }; + + const generatedFiles: FileOutputType[] = []; + + // Execute inference with streaming + await executeInference({ + env, + context: inferenceContext, + agentActionName: 'phaseImplementation', // Use existing phase implementation config + messages, + stream: { + chunk_size: 256, + onChunk: (chunk: string) => { + codeGenerationFormat.parseStreamingChunks( + chunk, + streamingState, + // File generation started + (filePath: string) => { + logger.info(`Starting generation of file: ${filePath}`); + if (inputs.fileGeneratingCallback) { + const purpose = files.find(f => f.path === filePath)?.purpose || 'Generated file'; + inputs.fileGeneratingCallback(filePath, purpose); + } + }, + // Stream file content chunks + (filePath: string, fileChunk: string, format: 'full_content' | 'unified_diff') => { + if (inputs.fileChunkGeneratedCallback) { + inputs.fileChunkGeneratedCallback(filePath, fileChunk, format); + } + }, + // onFileClose callback + (filePath: string) => { + logger.info(`Completed generation of file: ${filePath}`); + const completedFile = streamingState.completedFiles.get(filePath); + if (!completedFile) { + logger.error(`Completed file not found: ${filePath}`); + return; + } + + // Process the file contents + const originalContents = context.allFiles.find(f => f.filePath === filePath)?.fileContents || ''; + completedFile.fileContents = FileProcessing.processGeneratedFileContents( + completedFile, + originalContents, + logger + ); + + const generatedFile: FileOutputType = { + ...completedFile, + filePurpose: files.find(f => f.path === filePath)?.purpose || 'Generated file' + }; + + generatedFiles.push(generatedFile); + + if (inputs.fileClosedCallback) { + inputs.fileClosedCallback(generatedFile, `Completed generation of ${filePath}`); + } + } + ); + } + } + }); + + logger.info('Code generation completed', { + fileCount: generatedFiles.length + }); + + return { + files: generatedFiles + }; + } + + async generateReadme(options: OperationOptions): Promise { + 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/UserConversationProcessor.ts b/worker/agents/operations/UserConversationProcessor.ts index 43ebaafe..915b3e25 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 5f3148f9..e319aff6 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. @@ -158,15 +225,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; @@ -174,26 +238,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, @@ -201,7 +285,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 @@ -231,21 +315,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 ce292c87..3410b6ae 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"; @@ -1312,8 +1312,8 @@ FRONTEND_FIRST_CODING: ` export interface GeneralSystemPromptBuilderParams { query: string, - templateDetails: TemplateDetails, - dependencies: Record, + templateDetails?: TemplateDetails, + dependencies?: Record, blueprint?: Blueprint, language?: string, frameworks?: string[], @@ -1327,19 +1327,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 { From d6407e1de150471e2aeaa7efdf2ebf5c7aef5e0c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:21:06 +0000 Subject: [PATCH 09/24] chore(main): release 1.0.0 --- CHANGELOG.md | 283 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..53bf67fc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,283 @@ +# Changelog + +## 1.0.0 (2025-11-03) + + +### Features + +* add abort signal support to inference operations ([f3e545e](https://github.com/cloudflare/vibesdk/commit/f3e545e742b7b4cedb4c74b3af9501327ee8a8b3)) +* add actionKey tracking for inference calls and rate limiting ([226441e](https://github.com/cloudflare/vibesdk/commit/226441eda591b94cbf259e04f5b216eb74eae7bf)) +* add app visibility toggle and shareable link functionality to deployment controls ([692215d](https://github.com/cloudflare/vibesdk/commit/692215d388ab19ecd1871ac7e1e0303555660b6a)) +* add auto-scroll behavior to chat messages container ([6919820](https://github.com/cloudflare/vibesdk/commit/69198204c7d972261b17e4177980b5602d9a5ff5)) +* add automated PR description generation workflow ([3c81d5d](https://github.com/cloudflare/vibesdk/commit/3c81d5d486a244ca9dfcf40495641dedea3d453a)) +* add automated PR workflows and commit validation ([5ba22fc](https://github.com/cloudflare/vibesdk/commit/5ba22fce6f707c9b05f39bdeee291f7464d8b0bb)) +* add better streaming tool call accumulation and debug logging ([67360fe](https://github.com/cloudflare/vibesdk/commit/67360fef74a033fc898b7cf2e425218e0278d269)) +* add bootstrap script and template customization ([1fa893f](https://github.com/cloudflare/vibesdk/commit/1fa893fe72f709587c7a6080d37076015dacd7c2)) +* add conciseness requirement to agent instructions ([262d48f](https://github.com/cloudflare/vibesdk/commit/262d48fdeddeca349430e36035a5a8da2ec6c662)) +* add conversation reset functionality with confirmation dialog ([5ebb714](https://github.com/cloudflare/vibesdk/commit/5ebb714087367724785f97991c9c5fbe9587e83a)) +* add Custom style option to template selector ([76a4960](https://github.com/cloudflare/vibesdk/commit/76a4960fe1ed5cba209be4618408748d7bff2d78)) +* add deep debug session management with state persistence and wait tools ([e60694f](https://github.com/cloudflare/vibesdk/commit/e60694f6334b2c05acd1c7147650fc88c9adbd61)) +* add deep debugger transcript UI with collapsible tool results ([95ed92f](https://github.com/cloudflare/vibesdk/commit/95ed92f0111bab5f78318a810c9c965afc38a388)) +* add deploy to Cloudflare button in phase timeline header ([bea0bcd](https://github.com/cloudflare/vibesdk/commit/bea0bcdc09a408a0fdea6f5e27cd308bc57f2fb2)) +* add diff toggle option to git show command ([2657ba9](https://github.com/cloudflare/vibesdk/commit/2657ba9706a65cd0c6bb7e7dfeaf23265c24e035)) +* add discover nudge UI with arrow and font styling ([a33f29b](https://github.com/cloudflare/vibesdk/commit/a33f29b153b44246a3c74479ce204193bb8a8ea4)) +* add duration filter option for log retrieval in sandbox ([4579da2](https://github.com/cloudflare/vibesdk/commit/4579da2530a45d91294fd108d9937254d93e68de)) +* add dynamic platform status banner with changelog dialog in global header ([2f24af2](https://github.com/cloudflare/vibesdk/commit/2f24af2cedf0b2f0e6701838295dbd97d394f5b5)) +* add ESM module support and improve error handling in worker deployment ([dc2ee30](https://github.com/cloudflare/vibesdk/commit/dc2ee30a13f32bdb47434bcdde0397a9ead3a5e2)) +* add exponential backoff and max retries constant for deployment failures ([cbf1220](https://github.com/cloudflare/vibesdk/commit/cbf12206a12a45e400a64fbc278d5cbbf3521241)) +* add force refresh mechanism for preview deployments ([796e34d](https://github.com/cloudflare/vibesdk/commit/796e34d3459657502599bdbfdeddc1144289c0e3)) +* add generate_files tool for creating new files and full rewrites ([52d917f](https://github.com/cloudflare/vibesdk/commit/52d917f2f6ff39f2ea047f494cf579e28dab9a31)) +* add Git clone functionality for repositories ([d7532f2](https://github.com/cloudflare/vibesdk/commit/d7532f225d6ff6fb4e9439ea02619338ac5e5207)) +* add git clone protocol support with template rebasing ([6005c4d](https://github.com/cloudflare/vibesdk/commit/6005c4dea9fe15ee03f8a77edbba950081dd176e)) +* add git version control tools and enhance debugging system ([f6b6b00](https://github.com/cloudflare/vibesdk/commit/f6b6b0076d0ef57ef120a0a15b6493ecb616f020)) +* add GitHub Actions for issue triage + improved code-review ([d42f805](https://github.com/cloudflare/vibesdk/commit/d42f8050b3b3a53a4f2e5e612e156aea534cb71d)) +* add in-memory caching for git clone operations with 5s TTL ([263ac37](https://github.com/cloudflare/vibesdk/commit/263ac37b3d0289f5b0ca3c1eda8273a001088321)) +* add incremental GitHub sync with commit history preservation ([4785472](https://github.com/cloudflare/vibesdk/commit/4785472f0664b946714ef180727bc6427cbc8843)) +* add indexes for app views and stars by app_id and timestamp ([87dedbe](https://github.com/cloudflare/vibesdk/commit/87dedbeef5379e1023e09e33749b5895d6204ddd)) +* add inference cancellation support with AbortController ([6e752c2](https://github.com/cloudflare/vibesdk/commit/6e752c214197ec755c988ee0308721d1f04b50fb)) +* add issues field to DeterministicCodeFixCompletedMessage interface ([f7689fd](https://github.com/cloudflare/vibesdk/commit/f7689fdc1c2b53427928a6c155dd0b1f11ac3d74)) +* add latest Gemini model variants ([134f518](https://github.com/cloudflare/vibesdk/commit/134f518cb42fc77ff2e402dc684afd02f18c607a)) +* add latest Gemini model variants and reorganize AI model enum groupings ([a6a2b5d](https://github.com/cloudflare/vibesdk/commit/a6a2b5df3d8698b28957cdc21a5f6c0ba12663aa)) +* add llm.md and update claude.md using it ([a1e90dd](https://github.com/cloudflare/vibesdk/commit/a1e90ddfe398714ca9f4af75e57ea935b184060b)) +* add localhost:8787 to allowed CORS origins in dev environment ([88c1919](https://github.com/cloudflare/vibesdk/commit/88c1919e7ff52f986acfa1553a00f9b00e873d1e)) +* add log file extractor tool and nanoid dependency ([0b12d76](https://github.com/cloudflare/vibesdk/commit/0b12d76b7fbea3d7ea9e7863e9bb6be0d0cde27f)) +* add option to suppress error toasts in API client requests ([e490280](https://github.com/cloudflare/vibesdk/commit/e4902806e5f12019d2980c2bbbc14cbf1321dfa7)) +* add persistent command execution in sandbox environment ([7d02a43](https://github.com/cloudflare/vibesdk/commit/7d02a43669414185bed002ddc3baff463e15753d)) +* add preview expiration handling and update deployment messaging ([74b16da](https://github.com/cloudflare/vibesdk/commit/74b16daee96dc86dd72091ea49c19b79f252f8a4)) +* add previous debug session context and improve error handling in debugging tools ([45ac910](https://github.com/cloudflare/vibesdk/commit/45ac9105ad920e36cd7b284a050a414c1913a2a6)) +* add project name generation and config file updates ([a948e62](https://github.com/cloudflare/vibesdk/commit/a948e62b29999f3dca31b0262438fa3608c9df35)) +* add pulsing edge animation to indicate active phase progress ([49daa1d](https://github.com/cloudflare/vibesdk/commit/49daa1ddd4ad7a6b66d23c1b0adc78677478ec6f)) +* add read-only mode for agent initialization ([7f83e19](https://github.com/cloudflare/vibesdk/commit/7f83e1975f72a7e1df90a6b661a45f553a882176)) +* add retry logic for process health check in sandbox service ([3284afe](https://github.com/cloudflare/vibesdk/commit/3284afe0201f591776e348ffb0666bf9e4a5c494)) +* add retry mechanism for sandbox deployment failures with max 3 attempts ([1c110e0](https://github.com/cloudflare/vibesdk/commit/1c110e0cbf443b74f01a8a9dd4aa8772ed3f479e)) +* add safe JSON stringification with circular reference and Error object handling ([6e6dcd2](https://github.com/cloudflare/vibesdk/commit/6e6dcd2d155ef3064bc60c33879c32334f76b2c0)) +* add Sentry frontend integration ([72455ff](https://github.com/cloudflare/vibesdk/commit/72455ffc53b2cb527bcd317f4bae89404795c674)) +* add Sentry frontend integration ([c6755a2](https://github.com/cloudflare/vibesdk/commit/c6755a2e85a7b3c49e1f73aa9f9a9570c9fd09eb)) +* add sort persistence and auto-collapse sidebar on new build ([332041a](https://github.com/cloudflare/vibesdk/commit/332041acc160128a015e65719590fea64e11b16e)) +* add stop generation button and handle cancelled state in phase timeline ([3cd7cab](https://github.com/cloudflare/vibesdk/commit/3cd7cabed64f8cbd012f12f63b5be6355b9a51e4)) +* add streaming support to deep debugger and enhance logs tool with truncation ([1bdb1e7](https://github.com/cloudflare/vibesdk/commit/1bdb1e7e97c6a18ae3c3a2c69b8c14e6fdd65a19)) +* add support for backward compatibility flags in templates ([506a7ca](https://github.com/cloudflare/vibesdk/commit/506a7cabb6578583a11ca4b3c1a14fcc1bca02b1)) +* add support for separate preview domain in deployment system ([057ffa5](https://github.com/cloudflare/vibesdk/commit/057ffa5bbd6d2969d83381531e6f49b20f5ed263)) +* add support for user-suggested phase generation and forbid image generation ([0ab6501](https://github.com/cloudflare/vibesdk/commit/0ab6501eb3a074efb37967b3d2a92ad8e27f74fb)) +* add timeout handling and force redeploy option ([d2543d5](https://github.com/cloudflare/vibesdk/commit/d2543d5e575c45b141bb8728eeb10e3674fceb13)) +* add tool depth limit and conversation context compactification ([5ba30b9](https://github.com/cloudflare/vibesdk/commit/5ba30b9cb384bc2c519462c436f37dc2c7536f72)) +* add tool result field to websocket messages and update event handling ([41fab81](https://github.com/cloudflare/vibesdk/commit/41fab814b6991895da1cd9eccb2acac00a6f79a4)) +* add uploads routes + fix filename in r2 ([6a41be1](https://github.com/cloudflare/vibesdk/commit/6a41be1d0d19317146c07ac2bbaab90d46bed0df)) +* add wait_for_generation tool and enhance tool call result handling ([92f4e0e](https://github.com/cloudflare/vibesdk/commit/92f4e0e5192efd8ca0154e7d28ba6fd1f046a8da)) +* add WebSocket support for sandbox proxy ([9d9d60b](https://github.com/cloudflare/vibesdk/commit/9d9d60b3313eee2f58e2e50fe19fdfd44ab99511)) +* add WebSocket support for sandbox proxy ([72538be](https://github.com/cloudflare/vibesdk/commit/72538be52c99345a3fa20ec36e91a0eb274ce718)) +* add word counter and 4000 word limit to chat input ([33f5f50](https://github.com/cloudflare/vibesdk/commit/33f5f50cc23fe6a5e812ba865b30c968b6663a9d)) +* added cloudflared tunnels for local dev + docs ([745cd2a](https://github.com/cloudflare/vibesdk/commit/745cd2ac8893c44e29f5e0cabc74938ea0004c3e)) +* added guardrails that impact on multiple tiers for policy ([cf455b5](https://github.com/cloudflare/vibesdk/commit/cf455b5f465eaba4766008467a2141efed82f91b)) +* added guardrails that impact on multiple tiers for policy ([6c1d1ec](https://github.com/cloudflare/vibesdk/commit/6c1d1ec23c789fcc47d40087185bb061c560890c)) +* allow CORS for localhost during development ([c575baa](https://github.com/cloudflare/vibesdk/commit/c575baae7465200b02cea2bbd229280351c58607)) +* alter template schema to return map instead of list of files ([fad85df](https://github.com/cloudflare/vibesdk/commit/fad85df3b1385426fb3892d74fb97f9f4dd2d52d)) +* always upload images to r2, best-try on CF Images ([5797f13](https://github.com/cloudflare/vibesdk/commit/5797f135679ba5c6ea0e14a73b79eee9b6567d56)) +* build lint/tsc cache in background on sandbox init ([c89e655](https://github.com/cloudflare/vibesdk/commit/c89e6552899b591295a7969032cb58ff05881900)) +* bump wrangler versions and default to sandbox-3 ([20f1be9](https://github.com/cloudflare/vibesdk/commit/20f1be99f83d1d3731ccee5c9c1a2460fc51fec1)) +* cleaner sandbox instanceId generation + toast notifs ([7a667a7](https://github.com/cloudflare/vibesdk/commit/7a667a7ff8d29bcfadeea60d6e2e7c067c875d37)) +* code deep debugger prompt tweaks ([394a720](https://github.com/cloudflare/vibesdk/commit/394a720fadb9db067f343b42e344c65d471da67d)) +* disabled app remixing for now ([2ae671c](https://github.com/cloudflare/vibesdk/commit/2ae671cc79c424e6c1adc02a3fd45c97c98e6c09)) +* disabled byok and secrets service ([6926d2e](https://github.com/cloudflare/vibesdk/commit/6926d2e437c6892bf8d81ee34c33aa57176665a1)) +* disabled byok and secrets service ([a01e82b](https://github.com/cloudflare/vibesdk/commit/a01e82bf44ccded4882507fc84549001d07a94c5)) +* display completion message when entering finalization phase ([1365eeb](https://github.com/cloudflare/vibesdk/commit/1365eebae9fc150a91d048bae2e6b5cf11cda350)) +* display tool execution status in chat messages with live updates ([efef517](https://github.com/cloudflare/vibesdk/commit/efef517abe2c2256edcd40a65c95f09911c20588)) +* DO based git, fs storage, commit every saved file ([2156e89](https://github.com/cloudflare/vibesdk/commit/2156e89a22280fc7d16483e9d50ec635f0bfb2a0)) +* doc and script for local setup + local fixes ([1bcb83b](https://github.com/cloudflare/vibesdk/commit/1bcb83b65633a35f254d306ae184d4f7d9703ff9)) +* enable Sentry error reporting with CF Access auth and improved logging integration ([fa24bfb](https://github.com/cloudflare/vibesdk/commit/fa24bfbb1968f11e9b66fac32ed78366ae51e8dd)) +* enhance app list animations and infinite scroll loading experience ([70f0adc](https://github.com/cloudflare/vibesdk/commit/70f0adceec88ce948d8bc53659809ad9e30052d4)) +* enhance bootstrap command validation and safety ([ed1c006](https://github.com/cloudflare/vibesdk/commit/ed1c00642d452632d2aebdbc22d1c855f7bd7692)) +* enhance debugging tools with user interaction handling and runtime error detection ([b144e2c](https://github.com/cloudflare/vibesdk/commit/b144e2c8b63bdf803f28b0ad85f8183ae6281316)) +* enhance deep debug session management and UI ([2188c90](https://github.com/cloudflare/vibesdk/commit/2188c90b1fecc5571ec011696f6d3f4b1e5fbd4f)) +* enhance git integration with safer command access ([3c9696f](https://github.com/cloudflare/vibesdk/commit/3c9696f96faa27e89e025c0bd7d13c230e73ff13)) +* enhance git show command with diff support and optimize message handling ([fd70b1c](https://github.com/cloudflare/vibesdk/commit/fd70b1c7673c3b7d84f3056167e95625098c10da)) +* enhance phase timeline with debugging status and issue tracking ([79fb107](https://github.com/cloudflare/vibesdk/commit/79fb1071524cd654750f978332f6acc0d1ac4c2a)) +* enhance PR description workflow with review capabilities ([433c398](https://github.com/cloudflare/vibesdk/commit/433c398b4ab9e7afb55d2c602a62c1a00ea0d896)) +* enhance PR review automation with issue linking and feedback ([04720aa](https://github.com/cloudflare/vibesdk/commit/04720aa280c36725479941212d69c4b6305bbc17)) +* enhance PR review workflow with unified analysis ([b321376](https://github.com/cloudflare/vibesdk/commit/b3213764e02fcebd19858d2036bc3db245ed0bde)) +* enhance prompts for react errors ([9674413](https://github.com/cloudflare/vibesdk/commit/96744135e23124294874ef483bfadf1602676692)) +* enhance template file filtering with pattern matching and add nanoid generator ([68678ac](https://github.com/cloudflare/vibesdk/commit/68678ac3a06b71d936a8b5f8e9e146b093d6d9a0)) +* extend PR review trigger to include synchronize events ([16e18ba](https://github.com/cloudflare/vibesdk/commit/16e18ba8e3514377e75011bd36e1861a6839829d)) +* fix frontend types + proper load convo state ([3d6cc16](https://github.com/cloudflare/vibesdk/commit/3d6cc16432e96a6b7a5b2e2bbc8084bb5fe419c0)) +* fortification against rerender loops ([fbda105](https://github.com/cloudflare/vibesdk/commit/fbda10566cc717c8a9628e260de390154bbf38f7)) +* General cleanups and patches ([8133d22](https://github.com/cloudflare/vibesdk/commit/8133d2218f774c6f6daf126a5626c63a09dd283e)) +* git repos finally working ([d2e4594](https://github.com/cloudflare/vibesdk/commit/d2e4594771869cbc0f3c8140445bb39ac677ddbb)) +* implement collapsible tool result viewer with JSON formatting support ([9ae4640](https://github.com/cloudflare/vibesdk/commit/9ae464050cad6a55aa10f87df01f0739ccc00364)) +* implement D1 read replicas for optimized database queries ([2e7110a](https://github.com/cloudflare/vibesdk/commit/2e7110af58201ef92181e126bee072006df3a73e)) +* implement daily rate limit for API and LLM calls ([d8f0ba0](https://github.com/cloudflare/vibesdk/commit/d8f0ba017b0a41ba7727ca7546c2fdd5d18fbb7f)) +* implement file tree builder and pass in project context ([2dd4b2a](https://github.com/cloudflare/vibesdk/commit/2dd4b2a66a2f03ac9adc7845b6e2f5945d5b792d)) +* implement message deduplication to prevent duplicate assistant responses ([0256634](https://github.com/cloudflare/vibesdk/commit/0256634a6a042d7136a6b9bcda80ea92a8d1478c)) +* implement model-specific rate limit increments for LLM calls ([cc1de09](https://github.com/cloudflare/vibesdk/commit/cc1de09e4428e4456e9a6d26fd556fffce1a8285)) +* implement project name and blueprint update tools ([871548f](https://github.com/cloudflare/vibesdk/commit/871548febfdcf6efd5d3fade563f8a7738f64e08)) +* improve AI Gateway token validation and handling ([f0bce24](https://github.com/cloudflare/vibesdk/commit/f0bce241777d772a9542ad4336da41d9eded6664)) +* improve chat message handling and reconnection flow ([b7e1ae2](https://github.com/cloudflare/vibesdk/commit/b7e1ae27b1c1c837abf919be1ad53b899e3420ba)) +* improve chat message handling and reconnection flow ([d853266](https://github.com/cloudflare/vibesdk/commit/d8532668ec9ff595522c9fe9169b20a74c55baa7)) +* improve code regen ([c3baadf](https://github.com/cloudflare/vibesdk/commit/c3baadff98b875a8ba8d434cedeba464ab79daa3)) +* improve error handling and message display for debugging ([472688d](https://github.com/cloudflare/vibesdk/commit/472688dd38f406bad1e6784901ec7124910a11c5)) +* improve file tree serialization and error formatting ([ddcfa3a](https://github.com/cloudflare/vibesdk/commit/ddcfa3ab315cec5c363553395b6bb67ca5402b6d)) +* improve package.json synchronization with sandbox ([a960bc2](https://github.com/cloudflare/vibesdk/commit/a960bc2836bfa05da3ee465df7d4d4955f581661)) +* improve sandbox and git integration ([8044881](https://github.com/cloudflare/vibesdk/commit/8044881621661f681a045b6f5af65826c2fd09c1)) +* improve sandbox directory handling and error recovery ([eaf5bd5](https://github.com/cloudflare/vibesdk/commit/eaf5bd57b78e5a9f2a102f08c2f72ea95e66955e)) +* improve tab layout and git clone button placement ([8b6f908](https://github.com/cloudflare/vibesdk/commit/8b6f9086cf7e319fe7e0e7e758baa371bfd63490)) +* improve tab layout and git clone button placement ([c1b8a02](https://github.com/cloudflare/vibesdk/commit/c1b8a02c7188789036f1a964a9f80787394d80ee)) +* improve tool calling depth handling for debugging ([73af22b](https://github.com/cloudflare/vibesdk/commit/73af22b1a43fdb0ed1ebbfbe06df3288b38e25ea)) +* in memory get template details ([e01f067](https://github.com/cloudflare/vibesdk/commit/e01f067969e1ad414257a312d7c887216c06fe46)) +* include runtime errors in conversation context ([e71b9f5](https://github.com/cloudflare/vibesdk/commit/e71b9f5bf0a7713017428b5c6204214473316fac)) +* increase instance resources and improve GitHub file export handling ([bfa4855](https://github.com/cloudflare/vibesdk/commit/bfa48552cb8e03123dc341ac79cfab5d307e1120)) +* initial draft of ai gateway proxy for user apps ([79505a6](https://github.com/cloudflare/vibesdk/commit/79505a674b5a8f5a1979a0d1d0e60b1d28b2da8b)) +* initial implementation of deep debugging agent ([520c2f6](https://github.com/cloudflare/vibesdk/commit/520c2f61fc5d6a012c45a5cb487fc728e2be3fb4)) +* initial integration of images support in build/chat ([62d024f](https://github.com/cloudflare/vibesdk/commit/62d024f92e20043eaee8433e91c321f789eed821)) +* isolated deepdebug window ([6605ecd](https://github.com/cloudflare/vibesdk/commit/6605ecdf51aa6f2c7feacc91ad2f3b1cdfb91127)) +* limit deep_debug tool to one call per conversation turn ([1b13c71](https://github.com/cloudflare/vibesdk/commit/1b13c71b719871fc5c60e1a5e45a08bc90db2456)) +* make deterministic code fixer completely sync ([23f0261](https://github.com/cloudflare/vibesdk/commit/23f0261d9079c9c2991074180c8b5468af0e085a)) +* more prompt tweaks for react/frontend ([866c881](https://github.com/cloudflare/vibesdk/commit/866c881612adcbf1f658d63d20caeddee42c6602)) +* more prompt tweaks, clarifications and build fix ([e54ea92](https://github.com/cloudflare/vibesdk/commit/e54ea92c4f23e2c65214293cce23376d1162c0bf)) +* Only enable ai proxy if jwt secret is set ([c40cb7c](https://github.com/cloudflare/vibesdk/commit/c40cb7c958c072fbb1f2b5a9aef4611044407a5c)) +* optimize GitHub push with per-commit trees and blob deduplication ([c73b698](https://github.com/cloudflare/vibesdk/commit/c73b698057e7648c4b318f0dc83908797096756e)) +* origin validation for openai proxy ([297b918](https://github.com/cloudflare/vibesdk/commit/297b918e09d9beee4ba236f01951f956530a599b)) +* pass user suggestions to phase impl too ([5b359e7](https://github.com/cloudflare/vibesdk/commit/5b359e749fc1f7cefcce57a6d7517a55a7949cec)) +* phase bar on chat scroll + phase timeline consolidation ([e6c3f26](https://github.com/cloudflare/vibesdk/commit/e6c3f2629f821baf670ca5568dd5cf86680e1060)) +* prompt optimizations and alignment ([ac99a15](https://github.com/cloudflare/vibesdk/commit/ac99a15994bb882deff6e0bde6d3388401961bf6)) +* prompt refinements for zustand ([ff604c6](https://github.com/cloudflare/vibesdk/commit/ff604c6fafa42be76431d3dd8038edc9b4a71aa5)) +* provide git tools to deep_debugger + reset functionality ([0c0a777](https://github.com/cloudflare/vibesdk/commit/0c0a7776203fbf728a9ab281206bb5e99f03d35a)) +* purely in-isolate template unzip + inmemory storage ([3cd14ed](https://github.com/cloudflare/vibesdk/commit/3cd14ed58a4fe786523a4e5c63348c0b2671ee78)) +* redact older phase details to optimize context length in prompts ([2cd720e](https://github.com/cloudflare/vibesdk/commit/2cd720ea26e6c03dcdeb250b4d93546da1f52d91)) +* redesign app tabs + fixed dark mode banner color ([981c18d](https://github.com/cloudflare/vibesdk/commit/981c18d7243d96c840814c7f5d135d6ee8748333)) +* reimplement tool calling pipeline for dynamic tools ([56ca8c7](https://github.com/cloudflare/vibesdk/commit/56ca8c7f372a071f77ef69c0c22640c84789a125)) +* remove code-review system in favor of deep-debugger + user prompt ([bafe0d9](https://github.com/cloudflare/vibesdk/commit/bafe0d9fe63312895f8df4cfd6ae035d9199cf11)) +* replace chat input with auto-resizing textarea supporting multiline messages ([8e295e6](https://github.com/cloudflare/vibesdk/commit/8e295e6f265b113b2b1061e622cddbb00929c3f3)) +* replace fork count with view count in app card stats display ([f8a4823](https://github.com/cloudflare/vibesdk/commit/f8a4823c401df1220c9942490846bf094b4f06d9)) +* replace XML parsing with direct tool-based conversation processing ([53afb15](https://github.com/cloudflare/vibesdk/commit/53afb15d25765f2c7ccae534811b5b25939b5a86)) +* restrict Sentry error tracking to API endpoints only ([f53267d](https://github.com/cloudflare/vibesdk/commit/f53267dc042f1e8cb683a4961b36ae46783a89ec)) +* reworked github flow, pure DO based + cache token to avoid oauth ([7f33d02](https://github.com/cloudflare/vibesdk/commit/7f33d02d4b9cf3408f546fe4efd46b9038b79297)) +* rewritten process monitoring - simpler, more reliable ([cf99657](https://github.com/cloudflare/vibesdk/commit/cf996574f73c0b991d19079c4ea9b0dd531af2d7)) +* sandboxsdk 0.4.3 port + in-memory template details zip extraction ([fd63813](https://github.com/cloudflare/vibesdk/commit/fd638136bc7261869fefbef2692ec488adabc5d9)) +* show discoverable apps preview on homepage ([3f3b052](https://github.com/cloudflare/vibesdk/commit/3f3b0522ba2790eceb0e2197c9d42fdf0867d186)) +* show discoverable apps preview on homepage ([5e05841](https://github.com/cloudflare/vibesdk/commit/5e05841eaec965c4b57340560e440d8f5879fa99)) +* show toast on agent session fail ([9a9e401](https://github.com/cloudflare/vibesdk/commit/9a9e401c57c34612d833ace72e1d86b3533a2305)) +* simplify GitHub push by passing file content directly instead of reading from sandbox ([66911fd](https://github.com/cloudflare/vibesdk/commit/66911fd49e5c738670bc7c090b7c2961ade4d369)) +* store and pass last diff to allow revert ([95f739e](https://github.com/cloudflare/vibesdk/commit/95f739eebc1bb09340395cffedaa950ad1d77c7d)) +* store full histories in separate DO table ([429b03b](https://github.com/cloudflare/vibesdk/commit/429b03b0d9ff1963c0f03d141c7a4f14726a113e)) +* strip system context tags from user messages and UI ([87a44df](https://github.com/cloudflare/vibesdk/commit/87a44df29dfb70e26541262cd77fb1b88da7bff2)) +* stronger auth enforcement, by default all routes are authentica… ([3228f0f](https://github.com/cloudflare/vibesdk/commit/3228f0f0d086be73c1417b81cd7fabf7754ff45f)) +* true convo compactification + archive to r2 ([eb81e9a](https://github.com/cloudflare/vibesdk/commit/eb81e9aca3ec52cca319fc3cbe8abb2945658a23)) +* uncomment fast code fixers for optional use ([cff2213](https://github.com/cloudflare/vibesdk/commit/cff2213d310e2d4f7267d81310bbc1616c6dbaa7)) +* update to latest packages ([9ce7c55](https://github.com/cloudflare/vibesdk/commit/9ce7c5509b2055e88e1f546b82563eb4c8825e59)) +* use custom isomorphic-git fork + fix fs adapter and integration ([27640b8](https://github.com/cloudflare/vibesdk/commit/27640b8020a767cd3e2ccbe0282c8cc961284408)) +* wrap app cards and menu items in anchor tags for proper URL routing ([89a40f4](https://github.com/cloudflare/vibesdk/commit/89a40f41d596c49dd66d8b9031bae1e0215f3c41)) + + +### Bug Fixes + +* add ESM type declarations for isomorphic-git and update imports ([5bff523](https://github.com/cloudflare/vibesdk/commit/5bff5235f941fa198d93b482b74afcce64a4b716)) +* add missing await for async getFullState call in getAgentState function ([b0afd15](https://github.com/cloudflare/vibesdk/commit/b0afd1533f736976367e8a3585a7d260d76fd8c4)) +* add ownership/access checks in git protocol handlers ([ff9643a](https://github.com/cloudflare/vibesdk/commit/ff9643afb7d79ac6f58e2b2862b0681e7f94b6f2)) +* add setup commands execution after sandbox deployment ([5c471ff](https://github.com/cloudflare/vibesdk/commit/5c471ff8ae4d21d678a592e3047b76d5b5153e47)) +* Add template migration directly into onStart ([57ee53f](https://github.com/cloudflare/vibesdk/commit/57ee53f670178c0e350919d12376e164966a0bf9)) +* adjust rate limit period from 1 hour to 10 minutes for better request throttling ([6aadd5f](https://github.com/cloudflare/vibesdk/commit/6aadd5f082479b8b8138b94963a24b06eb609887)) +* align prompts to adhere better to user suggestions ([b7eff13](https://github.com/cloudflare/vibesdk/commit/b7eff133fd5af21959eb52f42ceea26a8f53abaf)) +* allow websockets now in prompts ([406761c](https://github.com/cloudflare/vibesdk/commit/406761c89679b2a9998861f3eeef6d2b350a2cb8)) +* better deduplication of commands for bootstrap file ([27efc11](https://github.com/cloudflare/vibesdk/commit/27efc11290e995187daf0a5f3d5b9b63af779aba)) +* big asset fetching corruption + unicode issue ([7e1d70e](https://github.com/cloudflare/vibesdk/commit/7e1d70ea92df0435a432987111fcd621d90cfffb)) +* big asset fetching corruption + unicode issue ([b2a317d](https://github.com/cloudflare/vibesdk/commit/b2a317d5dd0063006251ad16b6b83e38a7cc079d)) +* build errors ([ddd5639](https://github.com/cloudflare/vibesdk/commit/ddd56394887abb248f51948d65895539e6736eba)) +* build errors ([3898da4](https://github.com/cloudflare/vibesdk/commit/3898da495ec78526b36bf8c6657d5b89d45cefac)) +* check pending user inputs before finalizing phase generation ([595f4f2](https://github.com/cloudflare/vibesdk/commit/595f4f231932659764744f802d1b3e0f2324cca8)) +* check repo exists before export ([89ace7f](https://github.com/cloudflare/vibesdk/commit/89ace7f6a440de8e7f455d433644da26d260126d)) +* clarify Cloudflare WARP note in setup documentation for local development ([49c5dd2](https://github.com/cloudflare/vibesdk/commit/49c5dd222efe1e89c1ffbc89684183e106e86b41)) +* clarify docs and align ([5e1df23](https://github.com/cloudflare/vibesdk/commit/5e1df2364181d98adab392e207a6b74aefe8a669)) +* clarify instructions around homepage rewriting and file modification restrictions ([1e24baa](https://github.com/cloudflare/vibesdk/commit/1e24baaa2157381735d76d1c580097eca9b4ba32)) +* clean up error stack traces by filtering out non-typescript lines ([ef4f174](https://github.com/cloudflare/vibesdk/commit/ef4f17497fa1f622006b428606f7a6378b55a9c8)) +* clear user suggestions after phase execution to prevent duplicate processing ([40f39ab](https://github.com/cloudflare/vibesdk/commit/40f39abb42db2d982fe245dabc316b4538aa1bb1)) +* code debugger prompt improvements ([06375ce](https://github.com/cloudflare/vibesdk/commit/06375ce09891d311acc8b283391d2378a50f35f0)) +* concurrent deploy + use nanoid + stalestate ([ee61d46](https://github.com/cloudflare/vibesdk/commit/ee61d464fbcd712a20ff822bdb76e0cf881e1aa3)) +* copy URL button was not working ([1ff6122](https://github.com/cloudflare/vibesdk/commit/1ff612202a83672ecf8e3415b4778357c7662837)) +* copy URL button was not working ([9a722e3](https://github.com/cloudflare/vibesdk/commit/9a722e3c3b2b9b99d6c6e0fef62fe13469b6071b)) +* disabled tracing causing deployment failures for non-GA ([1fa292e](https://github.com/cloudflare/vibesdk/commit/1fa292ea92642992c867ee0a004fe1ed71a80c79)) +* enhance code analysis and fix strategies for deterministic code-fixer ([589ca8f](https://github.com/cloudflare/vibesdk/commit/589ca8f4175434b723c3a324046a0e8c2e3c38e6)) +* enhance deterministic code fixer to handle external module installation and improve tool execution feedback ([5baba34](https://github.com/cloudflare/vibesdk/commit/5baba3437ad6ff5272684bdd29d112f071abd6ed)) +* ensure tool messages are preserved with their matching assistant tool_calls during conversation compactification ([78fe339](https://github.com/cloudflare/vibesdk/commit/78fe33936649039cb5adcd98dd01f0987f3c0891)) +* escape single quotes in GitHub workflow commands ([d7e534a](https://github.com/cloudflare/vibesdk/commit/d7e534a20696fd07192852e6755fb49aca6dacf6)) +* file patch extension check for undefined ([de455e6](https://github.com/cloudflare/vibesdk/commit/de455e66d03f015a4e32629c3367f58af45087be)) +* fileRegeneration was using realtimeCodeFixer's model config ([989aa1b](https://github.com/cloudflare/vibesdk/commit/989aa1bf9c649558bd4aa959c906cf438be29ef6)) +* filter websocket failed errors ([6df1d30](https://github.com/cloudflare/vibesdk/commit/6df1d30cbfc4fe27a7febc5fb72f40e19b9dfbea)) +* finish posting to files map ([b73557e](https://github.com/cloudflare/vibesdk/commit/b73557e4ed141191aba31a1fd6dceb7cbb86ec14)) +* fix all structured format failing tests ([5d6e74f](https://github.com/cloudflare/vibesdk/commit/5d6e74fff7abcaa78e06afbb7e57cf260503a4c0)) +* fixed build errors ([d9f865b](https://github.com/cloudflare/vibesdk/commit/d9f865be2a9759119690c78190b24e70a36b80b1)) +* Fixed filtering important files ([d3d4776](https://github.com/cloudflare/vibesdk/commit/d3d47769a7331523c2e2cb4c07b5408e3b7c862f)) +* fixed importing template selection directly from code file ([6fcb004](https://github.com/cloudflare/vibesdk/commit/6fcb0045ca5c905c88b6af35e49504759e187a7a)) +* fixed setup project broken ([6a48871](https://github.com/cloudflare/vibesdk/commit/6a4887103e388784def70ee12b15c916ee2b3eb2)) +* fixed wrangler configs ([fabde00](https://github.com/cloudflare/vibesdk/commit/fabde007be799f94c0c24b8891bf90b22198eed3)) +* generation context template details passing ([2b761d0](https://github.com/cloudflare/vibesdk/commit/2b761d0d3d45d43206212f1166b98fb8dbcf3732)) +* get images from url param and pass it ([0be1f25](https://github.com/cloudflare/vibesdk/commit/0be1f25c1d8d6e205b68897909bf421ac797acc7)) +* GitHub repository handling for exports to existing repo ([d7c4261](https://github.com/cloudflare/vibesdk/commit/d7c4261e527c8b54291d8cc3d703492bf8e19ebc)) +* handle missing projectUpdatesAccumulator in state and add missing deployer config options ([43da2c8](https://github.com/cloudflare/vibesdk/commit/43da2c86545d0a69a98dd7b1739c8bb79d2b774a)) +* handle missing sandbox instance by returning empty issues object ([1259176](https://github.com/cloudflare/vibesdk/commit/1259176c3b77500c4d145207f33d4a29cad20d74)) +* handle parent dir creation in sandbox ([262bf69](https://github.com/cloudflare/vibesdk/commit/262bf691ace3b66212128f18278e8f9fe348a0ec)) +* handle zone detection failure and preserve existing zone info in wildcard routes ([a79629c](https://github.com/cloudflare/vibesdk/commit/a79629c369f2d4de5bfe58267eca76fa960206ec)) +* improve rate limit error handling and logging across API client and WebSocket ([662095f](https://github.com/cloudflare/vibesdk/commit/662095f68e823bbf0462684a8a6f9b3936082b0c)) +* improved user convo agent's prompt + reduced its max_tokens ([51db49e](https://github.com/cloudflare/vibesdk/commit/51db49ec1f8ba62784791dfd957f1de762475293)) +* increase deployment retry interval and remove redundant 422 retry logic ([d8c65d0](https://github.com/cloudflare/vibesdk/commit/d8c65d0f4a968ff3249c06b7189fca65c2d97f59)) +* log results object correctly and suppress init error propagation in generator agent ([bb0d97e](https://github.com/cloudflare/vibesdk/commit/bb0d97e3236bf048f2bb5aa545a1b50fae86e81e)) +* minor dark mode fixes, change homepage header ([30dabff](https://github.com/cloudflare/vibesdk/commit/30dabff4b227922d27568c242830e587fb769c07)) +* minor dark mode fixes, change homepage header ([273c079](https://github.com/cloudflare/vibesdk/commit/273c079736c6e5fd84198174c0288b79e3fa356e)) +* move security errors to shared to avoid import issues in frontend ([ceea941](https://github.com/cloudflare/vibesdk/commit/ceea94128694b49f4b4f9bdc94ab22a4f915db6d)) +* move template selection back to the worker for stability ([7025f43](https://github.com/cloudflare/vibesdk/commit/7025f43051121dc3e1f60e2beb4cef39128fbf27)) +* only generate AI proxy vars when JWT secret is configured ([701fe81](https://github.com/cloudflare/vibesdk/commit/701fe8171b7098f0d470f8a56a9ae50b394f924f)) +* Only update dependencies during package.json sync + migrations ([52fdf38](https://github.com/cloudflare/vibesdk/commit/52fdf385c4a25ed7e1df8adc1b273ac28c7f7770)) +* pagination not working on mode change + consolidated fetches ([c4a86b5](https://github.com/cloudflare/vibesdk/commit/c4a86b56bb74e4cf64236c24f53c1e2c97831c72)) +* pass noToast param when retrying CSRF token refresh and add rate limit toast ([d79763a](https://github.com/cloudflare/vibesdk/commit/d79763af9241d4e92a550504420b60d44bde148d)) +* pass user suggestions to phase implementation too for better reliability ([f8e027f](https://github.com/cloudflare/vibesdk/commit/f8e027fc7656ab9b06354b80e3cdce31e53ff764)) +* prevent concurrent sandbox deployments and improve instance cleanup ([2eb27b1](https://github.com/cloudflare/vibesdk/commit/2eb27b1b15d7529ed83d99819230d7edc4ee8d17)) +* prevent further review cycles by tracking review state ([b1c749c](https://github.com/cloudflare/vibesdk/commit/b1c749c0b726ccdd204ff36dacba4918098fd12e)) +* prevent null ruleId in ESLint diagnostic messages by providing empty string fallback ([b76d000](https://github.com/cloudflare/vibesdk/commit/b76d000b919ddf4cdeb36891afa6078325f8e59c)) +* prevent reuse of aborted controllers and improve sandbox deployment resilience ([d5092b0](https://github.com/cloudflare/vibesdk/commit/d5092b0c1757071efedd9c5a10677691e5497040)) +* prevent unnecessary status resets when cached status exists ([625e015](https://github.com/cloudflare/vibesdk/commit/625e015bb5ee5f4a746f6a01abe43ccd3e01cd24)) +* prompt react fixes ([0c3a841](https://github.com/cloudflare/vibesdk/commit/0c3a841b72497bd97169de907a806d8527ff0abd)) +* properly sanitize projectName causing unresolve issues ([3ef0ce9](https://github.com/cloudflare/vibesdk/commit/3ef0ce9a72b5182f7e23213d022f08b0711a0c83)) +* reduce enhanced sandbox instance resources to 4cpu 4gb ([788fdcd](https://github.com/cloudflare/vibesdk/commit/788fdcd003886629c6e5c57e54d1efdb24c4a1e5)) +* refresh to csrf token after register/login ([a1225c8](https://github.com/cloudflare/vibesdk/commit/a1225c8b86643280a2b6ac4f0fd73753a8b2c08f)) +* remove any type assertion in chat and reduce API rate limit to 1000 requests per minute ([1e41df3](https://github.com/cloudflare/vibesdk/commit/1e41df329758c2aff857fbddcc26ac8aaed0b868)) +* remove background static analysis from setupInstance ([0744b12](https://github.com/cloudflare/vibesdk/commit/0744b12a4b750f4c3c315dd361ef741c27a0f34a)) +* remove experimental remote bindings from vite config ([4a180b5](https://github.com/cloudflare/vibesdk/commit/4a180b50849a63e35316b458f6e114b7944b648f)) +* remove hardcoded models ([b98f862](https://github.com/cloudflare/vibesdk/commit/b98f86280dd802cb226dd2d7e836e6ee8b713b5e)) +* remove unnecessary quotes in Cloudflare deploy URL ([c57606a](https://github.com/cloudflare/vibesdk/commit/c57606a30cc308ffc1a7c713ae46a769f8385157)) +* remove unnecessary quotes in Cloudflare deploy URL ([f96be72](https://github.com/cloudflare/vibesdk/commit/f96be72c1eb3dedb54dac7a064f8c42a777e90ad)) +* remove unused forCodegen parameter from project setup template ([4b24ea2](https://github.com/cloudflare/vibesdk/commit/4b24ea27ab07b1c91b01f5f8942bfba32eec369b)) +* remove unused util giving SAST error ([577ff98](https://github.com/cloudflare/vibesdk/commit/577ff983e43dfa8959067b67314cdc7e0d9bf092)) +* rendering and migration for template details change ([af1ba31](https://github.com/cloudflare/vibesdk/commit/af1ba3155e458320abc6d67c60ca7e4276885ec4)) +* reset sandbox sessionId when container service disconnects ([849872a](https://github.com/cloudflare/vibesdk/commit/849872abd7ec844edd21e380ebeb011dbfd83909)) +* resolved command history growth issue ([0d23ca8](https://github.com/cloudflare/vibesdk/commit/0d23ca8db76ffa6ce8254f72b9b739460caa9508)) +* restrict agent preview endpoint to authenticated users only ([4eef652](https://github.com/cloudflare/vibesdk/commit/4eef652cd26e2f2aa05eb948c0e8f896a3522abd)) +* Return correct csrf validation error format ([db4d162](https://github.com/cloudflare/vibesdk/commit/db4d16279c2bf2390eade27ab7ffeb4ab808c30c)) +* reverted some changes - quality decreased ([5e43ab5](https://github.com/cloudflare/vibesdk/commit/5e43ab54af58924fc7045757e0cf02f7a1192213)) +* simply use git push from isomorphic git to sync with github ([f13fab7](https://github.com/cloudflare/vibesdk/commit/f13fab7f97d6f64e6899aeebe22bd51d41d46152)) +* simply use images rest api for uploading images ([b97fb68](https://github.com/cloudflare/vibesdk/commit/b97fb683f62016658cc9bb332af0c636132fd4d7)) +* standardize isomorphic-git imports and resolve ESM compatibility issues ([9e1a98b](https://github.com/cloudflare/vibesdk/commit/9e1a98bb5dcb170b7ccba2c6430dbfeaeea7405d)) +* store history reliably + user images in r2 ([47ae9e4](https://github.com/cloudflare/vibesdk/commit/47ae9e48c825ea2d88f55a4c9122d10214ac57f1)) +* temp overflow fix ([5825fa4](https://github.com/cloudflare/vibesdk/commit/5825fa4a132fe1843147f9ebadcd9fe90a9b03de)) +* truncate project names to 20 chars and handle LLM rate limits ([7ee2291](https://github.com/cloudflare/vibesdk/commit/7ee2291038a61c3f3dd3928fe84c3e26f180c3f9)) +* try use github_token instead of odic ([cdc68c5](https://github.com/cloudflare/vibesdk/commit/cdc68c569c9595abaa6ead331845058e0672021c)) +* tune deep debug config ([6d361eb](https://github.com/cloudflare/vibesdk/commit/6d361eb1cd4218a4ec7a2c1ec5d1bcc276d1b64b)) +* typo in provider ([439f3ab](https://github.com/cloudflare/vibesdk/commit/439f3abc18726919e27649058f44ef03974a25ce)) +* typo in setup.ts ([64f7e99](https://github.com/cloudflare/vibesdk/commit/64f7e99ff8ca542469c882dec872260055b4f36e)) +* typo of precedence in phase generation prompt ([8485637](https://github.com/cloudflare/vibesdk/commit/8485637c151d9acec0d879b973a91a0bb53dbc4b)) +* update default data directory path to use ./.data instead of ./data ([a4eb812](https://github.com/cloudflare/vibesdk/commit/a4eb812e1d3dc6322af208b5091b3ea84d3a9380)) +* update dockerfile with cloudflared installation ([0d18e0c](https://github.com/cloudflare/vibesdk/commit/0d18e0c51b01d279e2eb803c9922725e02e88258)) +* update migration command to include local migrations ([78929ef](https://github.com/cloudflare/vibesdk/commit/78929ef12b8a280d7ce8aeda4c51f3175a687b55)) +* update setup documentation and script to address Cloudflare WARP issues and add tunnel preview option ([84c3f2d](https://github.com/cloudflare/vibesdk/commit/84c3f2d04d6942bbb185463758e7467745f2fb2a)) +* use correct error field in deployment failure toast message ([dcf98a4](https://github.com/cloudflare/vibesdk/commit/dcf98a4240447f79d5612d8f92284902fc144846)) +* use CUSTOM_DOMAIN env var instead of hostname for r2 screenshot urls ([e27c47f](https://github.com/cloudflare/vibesdk/commit/e27c47fa9943691f3aa4c55ec451c7df6f09cd6f)) +* use CUSTOM_DOMAIN env var instead of hostname for r2 screenshot URLs ([33eb6fc](https://github.com/cloudflare/vibesdk/commit/33eb6fc4bfb4bdc9eaae3d98050986311f53ab1d)) +* use instance metadata from memory cache! ([fb8cd1b](https://github.com/cloudflare/vibesdk/commit/fb8cd1b6506cc602ae7560116081da77d932fa1b)) +* use templateName check instead of isInitialized() for agent start validation ([d9c7a30](https://github.com/cloudflare/vibesdk/commit/d9c7a30732b5dcc5baf6c39635332ebe1030f9cd)) + + +### Performance Improvements + +* optimize app analytics queries by combining multiple queries into two batched operations ([101e495](https://github.com/cloudflare/vibesdk/commit/101e495004295c28bcffef7578d9f185571d35c1)) +* optimize binary file detection and base64 encoding in zip extractor ([a6c67d0](https://github.com/cloudflare/vibesdk/commit/a6c67d01be38e235207a647b83dd5dde0167dd49)) +* optimize file operations and reduce unnecessary commits ([650b967](https://github.com/cloudflare/vibesdk/commit/650b967bbbd760a91a29f97a9700801bdecb5539)) +* optimize file writing with batched shell script to reduce API requests ([f4f4f7b](https://github.com/cloudflare/vibesdk/commit/f4f4f7ba2ac441612f6cbae7a20e3c54da882231)) +* optimize git clone by only including reachable objects ([4716f4e](https://github.com/cloudflare/vibesdk/commit/4716f4e49e483ae71f4e3e5513ad310c1429d3a8)) +* reuse image bytes buffer for both Cloudflare Images and R2 uploads ([bb3de20](https://github.com/cloudflare/vibesdk/commit/bb3de207437079569435492d7be02d552b799628)) diff --git a/package.json b/package.json index 9267b0da..ff244568 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vibesdk", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "setup": "tsx scripts/setup.ts", From 89508523afada3f9522cdc19b994006b4e878a15 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Sun, 2 Nov 2025 21:58:43 -0500 Subject: [PATCH 10/24] feat: enhance changelog generation with detailed PR and commit data --- .../workflows/release-with-ai-changelog.yml | 157 +++++++++++------- 1 file changed, 99 insertions(+), 58 deletions(-) diff --git a/.github/workflows/release-with-ai-changelog.yml b/.github/workflows/release-with-ai-changelog.yml index b8192c16..c03fd469 100644 --- a/.github/workflows/release-with-ai-changelog.yml +++ b/.github/workflows/release-with-ai-changelog.yml @@ -45,82 +45,123 @@ jobs: echo "current_tag=$CURRENT_TAG" >> $GITHUB_OUTPUT echo "previous_tag=$PREV_TAG" >> $GITHUB_OUTPUT - - name: Generate changelog with Claude + - name: Generate enhanced changelog with Claude Code Action uses: anthropics/claude-code-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} prompt: | - Generate a professional changelog for this release. + Generate an enhanced changelog for release ${{ needs.release-please.outputs.tag_name }}. - Compare tags: - - From: ${{ steps.prev_tag.outputs.previous_tag }} - - To: ${{ steps.prev_tag.outputs.current_tag }} + **Step 1: Collect data** - Get commits and changes: + Get release-please's generated changelog: ```bash - git log "${{ steps.prev_tag.outputs.previous_tag }}...${{ steps.prev_tag.outputs.current_tag }}" --pretty=format:"%s" | head -100 > /tmp/commits.txt - git diff "${{ steps.prev_tag.outputs.previous_tag }}...${{ steps.prev_tag.outputs.current_tag }}" --shortstat > /tmp/stats.txt - cat /tmp/commits.txt /tmp/stats.txt + gh release view "${{ needs.release-please.outputs.tag_name }}" --json body -q .body > /tmp/release-please-changelog.txt + cat /tmp/release-please-changelog.txt ``` - Generate changelog: - 1. Categorize: Added, Changed, Fixed, Security, Performance, Documentation - 2. Write clear, user-focused descriptions - 3. Explain impact and breaking changes - 4. Use concise bullet points - 5. Professional tone, no emojis - 6. Focus on what matters to users + Get all commits with full messages (not just titles): + ```bash + PREV_TAG="${{ steps.prev_tag.outputs.previous_tag }}" + if [ -n "$PREV_TAG" ]; then + git log "$PREV_TAG...${{ needs.release-please.outputs.tag_name }}" --pretty=format:"### Commit: %s%n%b%n---" | head -c 50000 > /tmp/commits.txt + else + git log "${{ needs.release-please.outputs.tag_name }}" --pretty=format:"### Commit: %s%n%b%n---" | head -c 50000 > /tmp/commits.txt + fi + cat /tmp/commits.txt + ``` + + Get merged PRs with descriptions: + ```bash + PREV_TAG="${{ steps.prev_tag.outputs.previous_tag }}" + if [ -n "$PREV_TAG" ]; then + PREV_DATE=$(git log -1 --format=%aI "$PREV_TAG" 2>/dev/null || echo "2024-01-01T00:00:00Z") + else + PREV_DATE="2024-01-01T00:00:00Z" + fi + + gh pr list \ + --repo ${{ github.repository }} \ + --state merged \ + --json number,title,body,mergedAt \ + --limit 50 \ + --search "merged:>=$PREV_DATE" \ + --jq '.[] | "## PR #\(.number): \(.title)\n\(.body)\n---\n"' > /tmp/prs.txt 2>/dev/null || echo "No PRs found" > /tmp/prs.txt + cat /tmp/prs.txt + ``` + + Get diff statistics: + ```bash + PREV_TAG="${{ steps.prev_tag.outputs.previous_tag }}" + if [ -n "$PREV_TAG" ]; then + git diff "$PREV_TAG...${{ needs.release-please.outputs.tag_name }}" --shortstat > /tmp/stats.txt + else + git log --shortstat --oneline | head -1 > /tmp/stats.txt + fi + cat /tmp/stats.txt + ``` + + **Step 2: Generate enhanced changelog** + + Using the data above (release-please changelog, commit messages, PR descriptions, stats): + + 1. Use release-please's changelog as the base structure + 2. Enhance with details from commit bodies and PR descriptions + 3. Categorize clearly: Features, Bug Fixes, Performance, Documentation, Breaking Changes + 4. Write user-focused descriptions (not just commit titles) + 5. If a PR/commit has detailed description, use it to provide context + 6. Be concise but informative + 7. Professional tone, no emojis in the changelog itself + 8. Highlight important changes and impacts + + **Format:** + ```markdown + ## What's Changed + + ### ✨ Features + - **[Feature Title]**: Description with context from PR/commit body + + ### 🐛 Bug Fixes + - **[Fix Title]**: What was broken and how it's fixed + + ### ⚡ Performance + - **[Improvement Title]**: Impact description + + ### 📚 Documentation + - List updates - Save result to file: + ### 💥 Breaking Changes + - **[Change Title]**: Migration steps if any + + ### 📊 Statistics + - Files changed, insertions, deletions from git diff stats + ``` + + **Step 3: Save the enhanced changelog** ```bash - cat << 'CHANGELOG' > /tmp/changelog.md - [YOUR_GENERATED_CHANGELOG_HERE] - CHANGELOG + cat > /tmp/enhanced-changelog.md << 'CHANGELOG_EOF' + [PASTE YOUR GENERATED CHANGELOG HERE] + CHANGELOG_EOF + ``` + + **Step 4: Update the release** + ```bash + gh release edit "${{ needs.release-please.outputs.tag_name }}" --notes-file /tmp/enhanced-changelog.md + echo "✅ Release notes updated successfully" ``` claude_args: | - --allowed-tools "Bash(git:*),Bash(cat:*),Bash(echo:*)" - --max-turns 10 + --allowed-tools "Bash(gh release view:*),Bash(gh release edit:*),Bash(gh pr list:*),Bash(git log:*),Bash(git diff:*),Bash(cat:*),Bash(echo:*),Bash(head:*)" + --max-turns 20 --model claude-haiku-4-5-20251001 - - name: Check if changelog was generated - id: check_changelog - run: | - if [ -f /tmp/changelog.md ]; then - echo "exists=true" >> $GITHUB_OUTPUT - else - echo "exists=false" >> $GITHUB_OUTPUT - fi - - - name: Enhance release notes - if: steps.check_changelog.outputs.exists == 'true' + - name: Verify changelog updated + if: always() env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG_NAME="${{ needs.release-please.outputs.tag_name }}" - CURRENT_BODY=$(gh release view "$TAG_NAME" --json body -q .body) - - # Create new release notes file - cat > /tmp/release-notes.md << 'EOF' - ## Summary - - EOF - - # Append AI-generated changelog - cat /tmp/changelog.md >> /tmp/release-notes.md - - # Add separator and original notes - cat >> /tmp/release-notes.md << 'EOF' - - --- - - ## Detailed Changes - - EOF - - # Append current body without variable expansion - echo "$CURRENT_BODY" >> /tmp/release-notes.md - - # Update release - gh release edit "$TAG_NAME" --notes-file /tmp/release-notes.md + echo "Verifying release notes for $TAG_NAME..." + gh release view "$TAG_NAME" --json body -q .body | head -20 + echo "✅ Changelog verification complete" From 774ee428a3d26bf731f736803c744d04d30cad0b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 07:04:09 +0000 Subject: [PATCH 11/24] chore(main): release 1.1.0 --- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53bf67fc..8241811d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [1.1.0](https://github.com/cloudflare/vibesdk/compare/v1.0.0...v1.1.0) (2025-11-04) + + +### Features + +* enhance changelog generation with detailed PR and commit data ([7850fc6](https://github.com/cloudflare/vibesdk/commit/7850fc620cf56372ab364711b1479a65279e5e32)) +* enhance changelog generation with detailed PR and commit data ([01afb74](https://github.com/cloudflare/vibesdk/commit/01afb7410b3cd5e0a5bb228d9aad238e9b1d3985)) + + +### Bug Fixes + +* change phase not found from error to warning ([2b78809](https://github.com/cloudflare/vibesdk/commit/2b78809a0751f21a1f9637c91cc4238ebfb2bcf1)) +* client and logger management in DeploymentManager ([2a19261](https://github.com/cloudflare/vibesdk/commit/2a192612e0c9c37efabd8712f6804cb184c80745)) +* correct metadata file path and instance ID generation ([4e932f8](https://github.com/cloudflare/vibesdk/commit/4e932f889e7e1630dc1f0693af0b3514c63d69e2)) + ## 1.0.0 (2025-11-03) diff --git a/package.json b/package.json index ff244568..c1719b0d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vibesdk", "private": true, - "version": "1.0.0", + "version": "1.1.0", "type": "module", "scripts": { "setup": "tsx scripts/setup.ts", From a840c745b90cf220e21319d27639b3ef5f1f7f70 Mon Sep 17 00:00:00 2001 From: Justin Gray Date: Sat, 8 Nov 2025 14:20:30 -0700 Subject: [PATCH 12/24] Fix typos --- scripts/setup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/setup.ts b/scripts/setup.ts index c2734a68..f68f62cf 100755 --- a/scripts/setup.ts +++ b/scripts/setup.ts @@ -60,7 +60,7 @@ class SetupManager { }); constructor() { - console.log('🚀 VibSDK Development Setup'); + console.log('🚀 VibeSDK Development Setup'); console.log('============================\n'); } @@ -1788,7 +1788,7 @@ class SetupManager { } } - console.log('\n✨ Happy coding with VibSDK! ✨'); + console.log('\n✨ Happy coding with VibeSDK! ✨'); } private async updateWorkerConfiguration(): Promise { From 354403896f81573316f4ec5ec4fb1cae898fb255 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Mon, 17 Nov 2025 11:03:21 +0530 Subject: [PATCH 13/24] fix: added instructions for token creation --- .dev.vars.example | 6 ++++++ README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/.dev.vars.example b/.dev.vars.example index 2d29072b..ddf421e2 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -2,6 +2,12 @@ #CUSTOM_DOMAIN="your-domain.com" # Your custom domain for CORS #ENVIRONMENT="prod" # Options: dev, staging, prod +# Cloudflare Credentials (Required for manual deployment via `bun run deploy`) +# These are automatically provided when using "Deploy to Cloudflare" button +# Get these from: https://dash.cloudflare.com/profile/api-tokens +#CLOUDFLARE_API_TOKEN="" # API token with Account-level permissions (see README for required permissions) +#CLOUDFLARE_ACCOUNT_ID="" # Your Cloudflare account ID + # Essential Secrets: #CLOUDFLARE_AI_GATEWAY_TOKEN="" # If this has read and edit permissions, the AI Gateway will be created automatically. run is required at the least diff --git a/README.md b/README.md index 6bdf3085..e43bd81e 100644 --- a/README.md +++ b/README.md @@ -332,12 +332,53 @@ The setup script will guide you through: After setup, start the development server: +#### Required for Manual Deployment + +If you're deploying manually using `bun run deploy`, you **must** set these environment variables: + +**Cloudflare API Token & Account ID:** + +1. **Get your Account ID**: + - Go to [Cloudflare Dashboard -> Workers and Pages](https://dash.cloudflare.com/?to=/:account/workers-and-pages) + - Copy your Account ID from the right sidebar or URL + +2. **Create an API Token**: + - Go to [Cloudflare Dashboard -> API Tokens](https://dash.cloudflare.com/?to=/:account/api-tokens) + - Click "Create Token" → Use custom token + - Configure with these **minimum required permissions**: + - **Account** → **Containers** → **Edit** + - **Account** → **Secrets Store** → **Edit** + - **Account** → **D1** → **Edit** + - **Account** → **Workers R2 Storage** → **Edit** + - **Account** → **Workers KV Storage** → **Edit** + - **Account** → **Workers Scripts** → **Edit** + - **Account** → **Account Settings** → **Read** + - **Zone** → **Workers Routes** → **Edit** + - Under "Zone Resources": Select "All zones from an account" → Choose your account + - Click "Continue to summary" → "Create Token" + - Copy the token immediately (you won't see it again) + +3. **Set the environment variables**: + ```bash + export CLOUDFLARE_API_TOKEN="your-api-token-here" + export CLOUDFLARE_ACCOUNT_ID="your-account-id-here" + ``` + +> **Note**: These credentials are automatically provided when using the "Deploy to Cloudflare" button, but are required for manual `bun run deploy`. + +**For Local Development (.dev.vars):** ```bash bun run dev ``` Visit `http://localhost:5173` to access VibSDK locally. +**For Production Deployment (.prod.vars):** +```bash +cp .dev.vars.example .prod.vars +# Edit .prod.vars with your production API keys and tokens +``` + ### Production Deployment Deploy to Cloudflare Workers: From b74a645202937d0e8538265c9b7c6467f084655e Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Mon, 17 Nov 2025 14:08:40 -0500 Subject: [PATCH 14/24] fix: add additional ownership verification checks --- worker/api/controllers/analytics/controller.ts | 14 ++++++++------ .../controllers/githubExporter/controller.ts | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/worker/api/controllers/analytics/controller.ts b/worker/api/controllers/analytics/controller.ts index dec3df7d..4028cd35 100644 --- a/worker/api/controllers/analytics/controller.ts +++ b/worker/api/controllers/analytics/controller.ts @@ -38,9 +38,13 @@ export class AnalyticsController extends BaseController { ); } - // TODO: Add ownership verification - users should only see their own analytics - // For now, allow authenticated users to query any user analytics - // Later: if (authUser.id !== userId && !authUser.isAdmin) { return 403; } + // Verify user can only access their own analytics + if (authUser.id !== userId) { + return AnalyticsController.createErrorResponse( + 'You can only access your own analytics', + 403, + ); + } // Parse query parameters const url = new URL(request.url); @@ -114,9 +118,7 @@ export class AnalyticsController extends BaseController { ); } - // TODO: Add ownership verification - users should only see analytics for their own agents - // This would require checking if the agent/chat belongs to the authenticated user - // For now, allow authenticated users to query any agent analytics + // Ownership verification is handled by AuthConfig.ownerOnly middleware // Parse query parameters const url = new URL(request.url); diff --git a/worker/api/controllers/githubExporter/controller.ts b/worker/api/controllers/githubExporter/controller.ts index ae8057f1..5149d07e 100644 --- a/worker/api/controllers/githubExporter/controller.ts +++ b/worker/api/controllers/githubExporter/controller.ts @@ -316,6 +316,15 @@ export class GitHubExporterController extends BaseController { agentId: string; }; + const appService = new AppService(env); + const ownershipResult = await appService.checkAppOwnership(body.agentId, context.user.id); + if (!ownershipResult.isOwner) { + return GitHubExporterController.createErrorResponse( + 'You do not have permission to access this app', + 403 + ); + } + this.logger.info('Export initiated', { userId: context.user.id, agentId: body.agentId }); if (!body.repositoryName) { @@ -432,6 +441,15 @@ export class GitHubExporterController extends BaseController { ); } + const appService = new AppService(env); + const ownershipResult = await appService.checkAppOwnership(body.agentId, context.user.id); + if (!ownershipResult.isOwner) { + return GitHubExporterController.createErrorResponse( + 'You do not have permission to access this app', + 403 + ); + } + const agentStub = await getAgentStub(env, body.agentId); // Try to get cached token From 14219b0779f2be3aab6fc36bf79e8e919ec857d9 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Mon, 17 Nov 2025 15:24:16 -0500 Subject: [PATCH 15/24] refactor: always use httpOnly cookie, no need for arg --- worker/utils/authUtils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/worker/utils/authUtils.ts b/worker/utils/authUtils.ts index b84a68e0..128a70de 100644 --- a/worker/utils/authUtils.ts +++ b/worker/utils/authUtils.ts @@ -155,7 +155,6 @@ export function createSecureCookie(options: CookieOptions): string { name, value, maxAge = 7 * 24 * 60 * 60, // 7 days default - httpOnly = true, secure = true, sameSite = 'Lax', path = '/', @@ -167,10 +166,11 @@ export function createSecureCookie(options: CookieOptions): string { if (maxAge > 0) parts.push(`Max-Age=${maxAge}`); if (path) parts.push(`Path=${path}`); if (domain) parts.push(`Domain=${domain}`); - if (httpOnly) parts.push('HttpOnly'); if (secure) parts.push('Secure'); if (sameSite) parts.push(`SameSite=${sameSite}`); + parts.push('HttpOnly'); + return parts.join('; '); } @@ -196,7 +196,6 @@ export function setSecureAuthCookies( name: 'accessToken', value: accessToken, maxAge: accessTokenExpiry, - httpOnly: true, sameSite: 'Lax', }), ); From 5358219479e82f22803f2c45393e8036a28915f8 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Mon, 17 Nov 2025 15:24:40 -0500 Subject: [PATCH 16/24] chore: remove useless hardcoded ai message on bootstrap --- src/routes/chat/chat.tsx | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/routes/chat/chat.tsx b/src/routes/chat/chat.tsx index a7dda364..1dcf949c 100644 --- a/src/routes/chat/chat.tsx +++ b/src/routes/chat/chat.tsx @@ -413,28 +413,7 @@ export default function Chat() { setView('editor'); } }, [isGeneratingBlueprint, view]); - - useEffect(() => { - // Only show bootstrap completion message for NEW chats, not when reloading existing ones - if (doneStreaming && !isGeneratingBlueprint && !blueprint && urlChatId === 'new') { - onCompleteBootstrap(); - sendAiMessage( - createAIMessage( - 'creating-blueprint', - 'Bootstrapping complete, now creating a blueprint for you...', - true, - ), - ); - } - }, [ - doneStreaming, - isGeneratingBlueprint, - sendAiMessage, - blueprint, - onCompleteBootstrap, - urlChatId, - ]); - + const isRunning = useMemo(() => { return ( isBootstrapping || isGeneratingBlueprint // || codeGenState === 'active' From 1eda73ef5a57aa6203d9b7dac9d39bf160b2af00 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Mon, 17 Nov 2025 15:25:00 -0500 Subject: [PATCH 17/24] fix: dont clear runtime errors on executeDeepDebug --- worker/agents/core/baseAgent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worker/agents/core/baseAgent.ts b/worker/agents/core/baseAgent.ts index 60daad52..b98d4031 100644 --- a/worker/agents/core/baseAgent.ts +++ b/worker/agents/core/baseAgent.ts @@ -667,7 +667,7 @@ export abstract class BaseAgentBehavior impleme focusPaths.some((p) => f.filePath.includes(p)), ); - const runtimeErrors = await this.fetchRuntimeErrors(true); + const runtimeErrors = await this.fetchRuntimeErrors(false); const dbg = new DeepCodeDebugger( operationOptions.env, From a28bf9fbaa2036d6c1547397cf61a10bc4d0abb5 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Mon, 17 Nov 2025 15:26:30 -0500 Subject: [PATCH 18/24] chore: updated rate limit configs --- worker/services/rate-limit/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worker/services/rate-limit/config.ts b/worker/services/rate-limit/config.ts index d677a8d5..f84cd6ed 100644 --- a/worker/services/rate-limit/config.ts +++ b/worker/services/rate-limit/config.ts @@ -81,9 +81,9 @@ export const DEFAULT_RATE_LIMIT_SETTINGS: RateLimitSettings = { llmCalls: { enabled: true, store: RateLimitStore.DURABLE_OBJECT, - limit: 800, + limit: 500, period: 60 * 60, // 1 hour - dailyLimit: 2000, + dailyLimit: 1700, excludeBYOKUsers: true, }, }; From 9d181cf190cbc1c65e07a065ed85685df4cb9b1e Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Tue, 18 Nov 2025 15:07:34 -0500 Subject: [PATCH 19/24] Revert "refactor: generalize coding agent to behavior + business; part 1" This reverts commit fc340772bb53e14eff3f8914e9f588aece32245f. --- worker/agents/assistants/codeDebugger.ts | 4 +- worker/agents/core/phasic/behavior.ts | 852 -------------- .../{baseAgent.ts => simpleGeneratorAgent.ts} | 1027 +++++++++++++---- worker/agents/core/smartGeneratorAgent.ts | 107 +- worker/agents/core/state.ts | 105 +- worker/agents/core/types.ts | 37 +- worker/agents/core/websocket.ts | 9 +- .../agents/domain/values/GenerationContext.ts | 129 +-- worker/agents/index.ts | 3 + worker/agents/operations/FileRegeneration.ts | 5 +- worker/agents/operations/PhaseGeneration.ts | 5 +- .../agents/operations/PhaseImplementation.ts | 62 +- .../agents/operations/PostPhaseCodeFixer.ts | 5 +- .../agents/operations/ScreenshotAnalysis.ts | 134 +++ .../agents/operations/SimpleCodeGeneration.ts | 280 ----- .../operations/UserConversationProcessor.ts | 5 +- worker/agents/operations/common.ts | 34 +- worker/agents/planning/blueprint.ts | 145 +-- worker/agents/prompts.ts | 34 +- worker/agents/schemas.ts | 26 +- .../services/implementations/CodingAgent.ts | 2 +- .../services/interfaces/ICodingAgent.ts | 92 +- worker/agents/tools/customTools.ts | 4 +- .../agents/tools/toolkit/alter-blueprint.ts | 4 +- worker/agents/tools/toolkit/deep-debugger.ts | 4 +- worker/agents/tools/toolkit/deploy-preview.ts | 4 +- worker/agents/tools/toolkit/exec-commands.ts | 4 +- worker/agents/tools/toolkit/generate-files.ts | 4 +- worker/agents/tools/toolkit/get-logs.ts | 4 +- .../tools/toolkit/get-runtime-errors.ts | 4 +- worker/agents/tools/toolkit/git.ts | 4 +- worker/agents/tools/toolkit/queue-request.ts | 6 +- worker/agents/tools/toolkit/read-files.ts | 4 +- .../agents/tools/toolkit/regenerate-file.ts | 4 +- worker/agents/tools/toolkit/rename-project.ts | 4 +- worker/agents/tools/toolkit/run-analysis.ts | 4 +- worker/agents/tools/toolkit/wait-for-debug.ts | 4 +- .../tools/toolkit/wait-for-generation.ts | 4 +- 38 files changed, 1279 insertions(+), 1889 deletions(-) delete mode 100644 worker/agents/core/phasic/behavior.ts rename worker/agents/core/{baseAgent.ts => simpleGeneratorAgent.ts} (68%) create mode 100644 worker/agents/operations/ScreenshotAnalysis.ts delete mode 100644 worker/agents/operations/SimpleCodeGeneration.ts diff --git a/worker/agents/assistants/codeDebugger.ts b/worker/agents/assistants/codeDebugger.ts index 45157ad8..f31f8b75 100644 --- a/worker/agents/assistants/codeDebugger.ts +++ b/worker/agents/assistants/codeDebugger.ts @@ -10,6 +10,7 @@ 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'; @@ -18,7 +19,6 @@ 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. @@ -544,7 +544,7 @@ type LoopDetectionState = { export type DebugSession = { filesIndex: FileState[]; - agent: ICodingAgent; + agent: CodingAgentInterface; runtimeErrors?: RuntimeError[]; }; diff --git a/worker/agents/core/phasic/behavior.ts b/worker/agents/core/phasic/behavior.ts deleted file mode 100644 index cf69d48e..00000000 --- a/worker/agents/core/phasic/behavior.ts +++ /dev/null @@ -1,852 +0,0 @@ -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/baseAgent.ts b/worker/agents/core/simpleGeneratorAgent.ts similarity index 68% rename from worker/agents/core/baseAgent.ts rename to worker/agents/core/simpleGeneratorAgent.ts index b98d4031..441fbacf 100644 --- a/worker/agents/core/baseAgent.ts +++ b/worker/agents/core/simpleGeneratorAgent.ts @@ -1,23 +1,32 @@ -import { Connection, ConnectionContext } from 'agents'; +import { Agent, AgentContext, Connection, ConnectionContext } from 'agents'; import { + Blueprint, + PhaseConceptGenerationSchemaType, + PhaseConceptType, FileConceptType, FileOutputType, - Blueprint, + PhaseImplementationSchemaType, } 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 { BaseProjectState } from './state'; -import { AllIssues, AgentSummary, AgentInitArgs, BehaviorType } from './types'; +import { CodeGenState, CurrentDevState, MAX_PHASES } from './state'; +import { AllIssues, AgentSummary, AgentInitArgs, PhaseExecutionResult, UserContext } from './types'; import { PREVIEW_EXPIRED_ERROR, WebSocketMessageResponses } from '../constants'; import { broadcastToConnections, handleWebSocketClose, handleWebSocketMessage, sendToConnection } from './websocket'; -import { StructuredLogger } from '../../logger'; +import { createObjectLogger, 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'; @@ -25,126 +34,137 @@ import { InferenceContext, AgentActionKey } from '../inferutils/config.types'; import { AGENT_CONFIG } from '../inferutils/config'; import { ModelConfigService } from '../../database/services/ModelConfigService'; import { fixProjectIssues } from '../../services/code-fixer'; -import { GitVersionControl, SqlExecutor } from '../git'; +import { GitVersionControl } from '../git'; import { FastCodeFixerOperation } from '../operations/PostPhaseCodeFixer'; import { looksLikeCommand, validateAndCleanBootstrapCommands } from '../utils/common'; -import { customizeTemplateFiles, generateBootstrapScript } from '../utils/templateCustomizer'; +import { customizePackageJson, customizeTemplateFiles, generateBootstrapScript, generateProjectName } from '../utils/templateCustomizer'; +import { generateBlueprint } from '../planning/blueprint'; 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 { ICodingAgent } from '../services/interfaces/ICodingAgent'; -import { SimpleCodeGenerationOperation } from '../operations/SimpleCodeGeneration'; +import { IdGenerator } from '../utils/idGenerator'; -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 { +interface Operations { regenerateFile: FileRegenerationOperation; + generateNextPhase: PhaseGenerationOperation; + analyzeScreenshot: ScreenshotAnalysisOperation; + implementPhase: PhaseImplementationOperation; fastCodeFixer: FastCodeFixerOperation; processUserMessage: UserConversationProcessor; - simpleGenerateFiles: SimpleCodeGenerationOperation; } -export abstract class BaseAgentBehavior implements ICodingAgent { - protected static readonly MAX_COMMANDS_HISTORY = 10; - protected static readonly PROJECT_NAME_PREFIX_MAX_LENGTH = 20; +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; protected projectSetupAssistant: ProjectSetupAssistant | undefined; protected stateManager!: StateManager; protected fileManager!: FileManager; + protected codingAgent: CodingAgentInterface = new CodingAgentInterface(this); protected deploymentManager!: DeploymentManager; protected git: GitVersionControl; - protected previewUrlCache: string = ''; - protected templateDetailsCache: TemplateDetails | null = null; + private previewUrlCache: string = ''; + private templateDetailsCache: TemplateDetails | null = null; // In-memory storage for user-uploaded images (not persisted in DO state) - protected pendingUserImages: ProcessedImageAttachment[] = [] - protected generationPromise: Promise | null = null; - protected currentAbortController?: AbortController; - protected deepDebugPromise: Promise<{ transcript: string } | { error: string }> | null = null; - protected deepDebugConversationId: string | null = null; + private pendingUserImages: ProcessedImageAttachment[] = [] + private generationPromise: Promise | null = null; + private currentAbortController?: AbortController; + private deepDebugPromise: Promise<{ transcript: string } | { error: string }> | null = null; + private deepDebugConversationId: string | null = null; // GitHub token cache (ephemeral, lost on DO eviction) - protected githubTokenCache: { + private githubTokenCache: { token: string; username: string; expiresAt: number; } | null = null; - protected operations: BaseAgentOperations = { + + protected operations: Operations = { regenerateFile: new FileRegenerationOperation(), + generateNextPhase: new PhaseGenerationOperation(), + analyzeScreenshot: new ScreenshotAnalysisOperation(), + implementPhase: new PhaseImplementationOperation(), fastCodeFixer: new FastCodeFixerOperation(), - processUserMessage: new UserConversationProcessor(), - simpleGenerateFiles: new SimpleCodeGenerationOperation(), + processUserMessage: new UserConversationProcessor() }; - - protected _boundSql: SqlExecutor; - - logger(): StructuredLogger { - return this.infrastructure.logger(); - } - getAgentId(): string { - return this.infrastructure.getAgentId(); - } - - get sql(): SqlExecutor { - return this._boundSql; - } - - 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; + 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; } - /** - * Update state with partial changes (type-safe) - */ - updateState(updates: Partial): void { - this.setState({ ...this.state, ...updates } as TState); - } + 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, + }; - constructor(public readonly infrastructure: AgentInfrastructure) { - this._boundSql = this.infrastructure.sql.bind(this.infrastructure); + 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)`; // Initialize StateManager this.stateManager = new StateManager( @@ -167,19 +187,110 @@ export abstract class BaseAgentBehavior impleme getLogger: () => this.logger(), env: this.env }, - BaseAgentBehavior.MAX_COMMANDS_HISTORY + SimpleCodeGeneratorAgent.MAX_COMMANDS_HISTORY ); } - public async initialize( - _initArgs: AgentInitArgs, + /** + * Initialize the code generator with project blueprint and template + * Sets up services and begins deployment process + */ + async initialize( + initArgs: AgentInitArgs, ..._args: unknown[] - ): Promise { - this.logger().info("Initializing agent"); + ): 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(); return this.state; } - protected async initializeAsync(): Promise { + private async initializeAsync(): Promise { try { const [, setupCommands] = await Promise.all([ this.deployToSandbox(), @@ -218,15 +329,29 @@ export abstract class BaseAgentBehavior impleme 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'); + } - // Just in case + // Full initialization for read-write operations 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`); } - protected async gitInit() { + private async gitInit() { try { await this.git.init(); this.logger().info("Git initialized successfully"); @@ -249,7 +374,19 @@ export abstract class BaseAgentBehavior impleme } } - onStateUpdate(_state: TState, _source: "server" | Connection) {} + 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) + }); + } + } onConnect(connection: Connection, ctx: ConnectionContext) { this.logger().info(`Agent connected for agent ${this.getAgentId()}`, { connection, ctx }); @@ -290,7 +427,7 @@ export abstract class BaseAgentBehavior impleme return this.templateDetailsCache; } - protected getTemplateDetails(): TemplateDetails { + private getTemplateDetails(): TemplateDetails { if (!this.templateDetailsCache) { this.ensureTemplateDetails(); throw new Error('Template details not loaded. Call ensureTemplateDetails() first.'); @@ -422,7 +559,7 @@ export abstract class BaseAgentBehavior impleme this.setConversationState(conversationState); } - protected async saveToDatabase() { + private 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); @@ -431,10 +568,10 @@ export abstract class BaseAgentBehavior impleme userId: this.state.inferenceContext.userId, sessionToken: null, title: this.state.blueprint.title || this.state.query.substring(0, 100), - description: this.state.blueprint.description, + description: this.state.blueprint.description || null, originalPrompt: this.state.query, finalPrompt: this.state.query, - framework: this.state.blueprint.frameworks.join(','), + framework: this.state.blueprint.frameworks?.[0], visibility: 'private', status: 'generating', createdAt: new Date(), @@ -482,7 +619,38 @@ export abstract class BaseAgentBehavior impleme return this.generationPromise !== null; } - abstract getOperationOptions(): OperationOptions; + 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 + }; + } /** * Gets or creates an abort controller for the current operation @@ -533,7 +701,36 @@ export abstract class BaseAgentBehavior impleme }; } - protected broadcastError(context: string, error: unknown): void { + 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 { const errorMessage = error instanceof Error ? error.message : String(error); this.logger().error(`${context}:`, error); this.broadcast(WebSocketMessageResponses.ERROR, { @@ -555,7 +752,7 @@ export abstract class BaseAgentBehavior impleme filePurpose: 'Project documentation and setup instructions' }); - const readme = await this.operations.simpleGenerateFiles.generateReadme(this.getOperationOptions()); + const readme = await this.operations.implementPhase.generateReadme(this.getOperationOptions()); await this.fileManager.saveGeneratedFile(readme, "feat: README.md"); @@ -567,6 +764,7 @@ export abstract class BaseAgentBehavior impleme } async queueUserRequest(request: string, images?: ProcessedImageAttachment[]): Promise { + this.rechargePhasesCounter(3); this.setState({ ...this.state, pendingUserInputs: [...this.state.pendingUserInputs, request] @@ -579,7 +777,7 @@ export abstract class BaseAgentBehavior impleme } } - protected fetchPendingUserRequests(): string[] { + private fetchPendingUserRequests(): string[] { const inputs = this.state.pendingUserInputs; if (inputs.length > 0) { this.setState({ @@ -594,7 +792,7 @@ export abstract class BaseAgentBehavior impleme * State machine controller for code generation with user interaction support * Executes phases sequentially with review cycles and proper state transitions */ - async generateAllFiles(): Promise { + async generateAllFiles(reviewCycles: number = 5): Promise { if (this.state.mvpGenerated && this.state.pendingUserInputs.length === 0) { this.logger().info("Code generation already completed and no user inputs pending"); return; @@ -603,11 +801,11 @@ export abstract class BaseAgentBehavior impleme this.logger().info("Code generation already in progress"); return; } - this.generationPromise = this.buildWrapper(); + this.generationPromise = this.launchStateMachine(reviewCycles); await this.generationPromise; } - private async buildWrapper() { + private async launchStateMachine(reviewCycles: number) { this.broadcast(WebSocketMessageResponses.GENERATION_STARTED, { message: 'Starting code generation', totalFiles: this.getTotalFiles() @@ -616,8 +814,69 @@ export abstract class BaseAgentBehavior impleme 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 staticAnalysisCache: StaticAnalysisResponse | undefined; + let userContext: UserContext | undefined; + + // Store review cycles for later use + this.setState({ + ...this.state, + reviewCycles: reviewCycles + }); + try { - await this.build(); + 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) { if (error instanceof RateLimitExceededError) { this.logger().error("Error in state machine:", error); @@ -645,10 +904,203 @@ export abstract class BaseAgentBehavior impleme } /** - * Abstract method to be implemented by subclasses - * Contains the main logic for code generation and review process + * 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 */ - abstract build(): Promise + 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; + } async executeDeepDebug( issue: string, @@ -676,7 +1128,7 @@ export abstract class BaseAgentBehavior impleme const out = await dbg.run( { issue, previousTranscript }, - { filesIndex, agent: this, runtimeErrors }, + { filesIndex, agent: this.codingAgent, runtimeErrors }, streamCb, toolRenderer, ); @@ -703,6 +1155,192 @@ export abstract class BaseAgentBehavior impleme return await debugPromise; } + /** + * 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 @@ -766,7 +1404,7 @@ export abstract class BaseAgentBehavior impleme } getTotalFiles(): number { - return this.fileManager.getGeneratedFilePaths().length + return this.fileManager.getGeneratedFilePaths().length + ((this.state.currentPhase || this.state.blueprint.initialPhase)?.files?.length || 0); } getSummary(): Promise { @@ -778,18 +1416,25 @@ export abstract class BaseAgentBehavior impleme return Promise.resolve(summaryData); } - async getFullState(): Promise { + async getFullState(): Promise { return this.state; } - protected migrateStateIfNeeded(): void { - // no-op, only older phasic agents need this, for now. + private migrateStateIfNeeded(): void { + const migratedState = StateMigration.migrateIfNeeded(this.state, this.logger()); + if (migratedState) { + this.setState(migratedState); + } } 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(); @@ -837,10 +1482,41 @@ export abstract class BaseAgentBehavior impleme } } + 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 */ - protected async applyDeterministicCodeFixes() : Promise { + private async applyDeterministicCodeFixes() : Promise { try { // Get static analysis and do deterministic fixes const staticAnalysis = await this.runStaticAnalysisCode(); @@ -925,7 +1601,7 @@ export abstract class BaseAgentBehavior impleme try { const valid = /^[a-z0-9-_]{3,50}$/.test(newName); if (!valid) return false; - const updatedBlueprint = { ...this.state.blueprint, projectName: newName }; + const updatedBlueprint = { ...this.state.blueprint, projectName: newName } as Blueprint; this.setState({ ...this.state, blueprint: updatedBlueprint @@ -957,17 +1633,13 @@ export abstract class BaseAgentBehavior impleme } } - /** - * Update user-facing blueprint fields - * Only allows updating safe, cosmetic fields - not internal generation state - */ async updateBlueprint(patch: Partial): Promise { - // Fields that are safe to update after generation starts - // Excludes: initialPhase (breaks generation), plan (internal state) - const safeUpdatableFields = new Set([ + const keys = Object.keys(patch) as (keyof Blueprint)[]; + const allowed = new Set([ 'title', - 'description', + 'projectName', 'detailedDescription', + 'description', 'colorPalette', 'views', 'userFlow', @@ -977,32 +1649,25 @@ export abstract class BaseAgentBehavior impleme 'frameworks', 'implementationRoadmap' ]); - - // 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; + 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]; } } - - // projectName requires sandbox update, handle separately - if ('projectName' in patch && typeof patch.projectName === 'string') { - await this.updateProjectName(patch.projectName); + if (typeof filtered.projectName === 'string' && filtered.projectName) { + await this.updateProjectName(filtered.projectName); + delete (filtered as any).projectName; } - - // Merge and update state - const updated = { ...this.state.blueprint, ...filtered } as Blueprint; + const updated: Blueprint = { ...this.state.blueprint, ...(filtered as Blueprint) } as Blueprint; this.setState({ ...this.state, blueprint: updated }); - this.broadcast(WebSocketMessageResponses.BLUEPRINT_UPDATED, { message: 'Blueprint updated', updatedKeys: Object.keys(filtered) }); - return updated; } @@ -1099,54 +1764,32 @@ export abstract class BaseAgentBehavior impleme requirementsCount: requirements.length, filesCount: files.length }); - - // Broadcast file generation started - this.broadcast(WebSocketMessageResponses.PHASE_IMPLEMENTING, { - message: `Generating files: ${phaseName}`, - phaseName - }); - const operation = new SimpleCodeGenerationOperation(); - const result = await operation.execute( + // 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, { - phaseName, - phaseDescription, - requirements, - files, - fileGeneratingCallback: (filePath: string, filePurpose: string) => { - this.broadcast(WebSocketMessageResponses.FILE_GENERATING, { - message: `Generating file: ${filePath}`, - filePath, - filePurpose - }); + runtimeErrors: [], + staticAnalysis: { + success: true, + lint: { issues: [] }, + typecheck: { issues: [] } }, - fileChunkGeneratedCallback: (filePath: string, chunk: string, format: 'full_content' | 'unified_diff') => { - this.broadcast(WebSocketMessageResponses.FILE_CHUNK_GENERATED, { - message: `Generating file: ${filePath}`, - filePath, - chunk, - format - }); - }, - fileClosedCallback: (file, message) => { - this.broadcast(WebSocketMessageResponses.FILE_GENERATED, { - message, - file - }); - } }, - this.getOperationOptions() - ); - - await this.fileManager.saveGeneratedFiles( - result.files, - `feat: ${phaseName}\n\n${phaseDescription}` + { suggestions: requirements }, + true, // streamChunks + false // postPhaseFixing = false (skip auto-fixes) ); - this.logger().info('Files generated and saved', { - fileCount: result.files.length - }); - // Return files with diffs from FileState return { files: result.files.map(f => ({ @@ -1157,16 +1800,6 @@ export abstract class BaseAgentBehavior impleme }; } - // 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 { // Call deployment manager with callbacks for broadcasting at the right times const result = await this.deploymentManager.deployToSandbox( @@ -1346,14 +1979,14 @@ export abstract class BaseAgentBehavior impleme handleWebSocketClose(connection); } - protected async onProjectUpdate(message: string): Promise { + private async onProjectUpdate(message: string): Promise { this.setState({ ...this.state, projectUpdatesAccumulator: [...this.state.projectUpdatesAccumulator, message] }); } - protected async getAndResetProjectUpdates() { + private async getAndResetProjectUpdates() { const projectUpdates = this.state.projectUpdatesAccumulator || []; this.setState({ ...this.state, @@ -1373,14 +2006,14 @@ export abstract class BaseAgentBehavior impleme broadcastToConnections(this, msg, data || {} as WebSocketMessageData); } - protected getBootstrapCommands() { + private getBootstrapCommands() { const bootstrapCommands = this.state.commandsHistory || []; // Validate, deduplicate, and clean const { validCommands } = validateAndCleanBootstrapCommands(bootstrapCommands); return validCommands; } - protected async saveExecutedCommands(commands: string[]) { + private async saveExecutedCommands(commands: string[]) { this.logger().info('Saving executed commands', { commands }); // Merge with existing history @@ -1426,7 +2059,7 @@ export abstract class BaseAgentBehavior impleme * Execute commands with retry logic * Chunks commands and retries failed ones with AI assistance */ - protected async executeCommands(commands: string[], shouldRetry: boolean = true, chunkSize: number = 5): Promise { + private 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'); @@ -1560,7 +2193,7 @@ export abstract class BaseAgentBehavior impleme * Sync package.json from sandbox to agent's git repository * Called after install/add/remove commands to keep dependencies in sync */ - protected async syncPackageJsonFromSandbox(): Promise { + private 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/smartGeneratorAgent.ts b/worker/agents/core/smartGeneratorAgent.ts index 6b9f0998..88af36a1 100644 --- a/worker/agents/core/smartGeneratorAgent.ts +++ b/worker/agents/core/smartGeneratorAgent.ts @@ -1,89 +1,40 @@ -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"; +import { SimpleCodeGeneratorAgent } from "./simpleGeneratorAgent"; +import { CodeGenState } from "./state"; +import { AgentInitArgs } from "./types"; -export class CodeGeneratorAgent extends Agent implements AgentInfrastructure { - public _logger: StructuredLogger | undefined; - private behavior: BaseAgentBehavior; - private onStartDeferred?: { props?: Record; resolve: () => void }; +/** + * 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 { - - 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); - } - } - + /** + * Initialize the smart code generator with project blueprint and template + * Sets up services and begins deployment process + */ async initialize( - initArgs: AgentInitArgs, - ..._args: unknown[] - ): Promise { - const { inferenceContext } = initArgs; - this.initLogger(inferenceContext.agentId, inferenceContext.userId); - - await this.behavior.initialize(initArgs); - return this.behavior.state; - } - - private initLogger(agentId: string, userId: string, sessionId?: string) { - this._logger = createObjectLogger(this, 'CodeGeneratorAgent'); - this._logger.setObjectId(agentId); - this._logger.setFields({ - agentId, - userId, + initArgs: AgentInitArgs, + agentMode: 'deterministic' | 'smart' + ): Promise { + this.logger().info('🧠 Initializing SmartCodeGeneratorAgent with enhanced AI orchestration', { + queryLength: initArgs.query.length, + agentType: agentMode }); - if (sessionId) { - this._logger.setField('sessionId', sessionId); - } - return this._logger; + + // Call the parent initialization + return await super.initialize(initArgs); } - logger(): StructuredLogger { - if (!this._logger) { - this._logger = this.initLogger(this.getAgentId(), this.state.inferenceContext.userId, this.state.sessionId); + async generateAllFiles(reviewCycles: number = 10): Promise { + if (this.state.agentMode === 'deterministic') { + return super.generateAllFiles(reviewCycles); + } else { + return this.builderLoop(); } - return this._logger; } - getAgentId() { - return this.state.inferenceContext.agentId; + async builderLoop() { + // TODO } } \ No newline at end of file diff --git a/worker/agents/core/state.ts b/worker/agents/core/state.ts index f8c3a5e3..2840747b 100644 --- a/worker/agents/core/state.ts +++ b/worker/agents/core/state.ts @@ -1,11 +1,9 @@ -import type { PhasicBlueprint, AgenticBlueprint, PhaseConceptType , +import type { Blueprint, 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; @@ -26,92 +24,33 @@ export enum CurrentDevState { export const MAX_PHASES = 12; -/** Common state fields for all agent behaviors */ -export interface BaseProjectState { - behaviorType: BehaviorType; - // Identity - projectName: string; - query: string; - sessionId: string; - hostname: string; - +export interface CodeGenState { 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 + projectName: string, + query: string; generatedFilesMap: Record; - - // Common infrastructure + generatedPhases: PhaseState[]; + commandsHistory?: string[]; // History of commands run + lastPackageJson?: string; // Last package.json file contents + templateName: string; sandboxInstanceId?: string; - commandsHistory?: string[]; - lastPackageJson?: string; - pendingUserInputs: string[]; - projectUpdatesAccumulator: string[]; - // Deep debug - lastDeepDebugTranscript: string | null; - + shouldBeGenerating: boolean; // Persistent flag indicating generation should be active mvpGenerated: boolean; reviewingInitiated: boolean; -} - -/** Phasic agent state */ -export interface PhasicState extends BaseProjectState { - behaviorType: 'phasic'; - blueprint: PhasicBlueprint; - generatedPhases: PhaseState[]; - + agentMode: 'deterministic' | 'smart'; + sessionId: string; + hostname: string; phasesCounter: number; - currentDevState: CurrentDevState; - reviewCycles?: number; - currentPhase?: PhaseConceptType; -} - -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; -} + pendingUserInputs: string[]; + currentDevState: CurrentDevState; + reviewCycles?: number; // Number of review cycles for code review phase + currentPhase?: PhaseConceptType; // Current phase being worked on + + conversationMessages: ConversationMessage[]; + projectUpdatesAccumulator: string[]; + inferenceContext: InferenceContext; -export type AgentState = PhasicState | AgenticState; + lastDeepDebugTranscript: string | null; +} diff --git a/worker/agents/core/types.ts b/worker/agents/core/types.ts index 4ab3ba65..af7ecc27 100644 --- a/worker/agents/core/types.ts +++ b/worker/agents/core/types.ts @@ -5,46 +5,23 @@ 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, PhasicState, AgenticState } from './state'; +import { CurrentDevState } from './state'; import { ProcessedImageAttachment } from 'worker/types/image-attachment'; -export type BehaviorType = 'phasic' | 'agentic'; - -/** Base initialization arguments shared by all agents */ -interface BaseAgentInitArgs { +export interface AgentInitArgs { query: string; - hostname: string; - inferenceContext: InferenceContext; language?: string; frameworks?: string[]; - images?: ProcessedImageAttachment[]; - onBlueprintChunk: (chunk: string) => void; -} - -/** Phasic agent initialization arguments */ -interface PhasicAgentInitArgs extends BaseAgentInitArgs { + hostname: string; + inferenceContext: InferenceContext; templateInfo: { templateDetails: TemplateDetails; selection: TemplateSelection; - }; -} - -/** Agentic agent initialization arguments */ -interface AgenticAgentInitArgs extends BaseAgentInitArgs { - templateInfo?: { - templateDetails: TemplateDetails; - selection: TemplateSelection; - }; + } + images?: ProcessedImageAttachment[]; + onBlueprintChunk: (chunk: string) => void; } -/** 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 be655701..4428f690 100644 --- a/worker/agents/core/websocket.ts +++ b/worker/agents/core/websocket.ts @@ -1,18 +1,13 @@ 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: BaseAgentBehavior, - connection: Connection, - message: string -): void { +export function handleWebSocketMessage(agent: SimpleCodeGeneratorAgent, 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 788ea8d8..39176ab9 100644 --- a/worker/agents/domain/values/GenerationContext.ts +++ b/worker/agents/domain/values/GenerationContext.ts @@ -1,47 +1,36 @@ -import { PhasicBlueprint, AgenticBlueprint } from '../../schemas'; +import { Blueprint } from '../../schemas'; import { FileTreeNode, TemplateDetails } from '../../../services/sandbox/sandboxTypes'; -import { FileState, PhaseState, PhasicState, AgenticState } from '../../core/state'; +import { CodeGenState, FileState, PhaseState } 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; -} /** - * Discriminated union of generation contexts - * - * Discriminate using: `'generatedPhases' in context` or `GenerationContext.isPhasic(context)` + * Immutable context for code generation operations + * Contains all necessary data for generating code */ -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 { +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 { const dependencies = DependencyManagement.mergeDependencies( templateDetails.deps || {}, state.lastPackageJson, @@ -53,69 +42,35 @@ export namespace GenerationContext { state.generatedFilesMap ); - const base = { - query: state.query, - allFiles, + return new GenerationContext( + state.query, + state.blueprint, templateDetails, dependencies, - 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 }); - } - - /** Type guard for phasic context */ - export function isPhasic(context: GenerationContext): context is PhasicGenerationContext { - return 'generatedPhases' in context; + allFiles, + state.generatedPhases, + state.commandsHistory || [] + ); } - /** Type guard for agentic context */ - export function isAgentic(context: GenerationContext): context is AgenticGenerationContext { - return 'currentPlan' in context; + /** + * Get formatted phases for prompt generation + */ + getCompletedPhases() { + return Object.values(this.generatedPhases.filter(phase => phase.completed)); } - /** Get completed phases (empty array for agentic contexts) */ - export function getCompletedPhases(context: GenerationContext): PhaseState[] { - return isPhasic(context) - ? context.generatedPhases.filter(phase => phase.completed) - : []; - } + getFileTree(): FileTreeNode { + const builder = new FileTreeBuilder(this.templateDetails?.fileTree); - /** 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) { + for (const { filePath } of this.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, - }; + return builder.build(); } } diff --git a/worker/agents/index.ts b/worker/agents/index.ts index 102298c0..17101673 100644 --- a/worker/agents/index.ts +++ b/worker/agents/index.ts @@ -1,4 +1,7 @@ + +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 30edf3eb..ff05c24d 100644 --- a/worker/agents/operations/FileRegeneration.ts +++ b/worker/agents/operations/FileRegeneration.ts @@ -2,7 +2,6 @@ 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; @@ -100,10 +99,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 07855801..4cca09c9 100644 --- a/worker/agents/operations/PhaseGeneration.ts +++ b/worker/agents/operations/PhaseGeneration.ts @@ -8,7 +8,6 @@ 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; @@ -187,10 +186,10 @@ const userPromptFormatter = (issues: IssueReport, userSuggestions?: string[], is 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 } = inputs; const { env, logger, context } = options; diff --git a/worker/agents/operations/PhaseImplementation.ts b/worker/agents/operations/PhaseImplementation.ts index 76611f58..4de9f8ba 100644 --- a/worker/agents/operations/PhaseImplementation.ts +++ b/worker/agents/operations/PhaseImplementation.ts @@ -13,7 +13,6 @@ 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 @@ -397,6 +396,30 @@ Goal: Thoroughly review the entire codebase generated in previous phases. Identi This phase prepares the code for final deployment.`; +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 ''; @@ -433,10 +456,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; @@ -557,4 +580,37 @@ 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 242abd94..bb3de4e2 100644 --- a/worker/agents/operations/PostPhaseCodeFixer.ts +++ b/worker/agents/operations/PostPhaseCodeFixer.ts @@ -6,7 +6,6 @@ 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; @@ -72,10 +71,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 new file mode 100644 index 00000000..b562908b --- /dev/null +++ b/worker/agents/operations/ScreenshotAnalysis.ts @@ -0,0 +1,134 @@ +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 deleted file mode 100644 index fb572ea5..00000000 --- a/worker/agents/operations/SimpleCodeGeneration.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { FileConceptType, FileOutputType } from '../schemas'; -import { createUserMessage, createSystemMessage } from '../inferutils/common'; -import { executeInference } from '../inferutils/infer'; -import { PROMPT_UTILS } from '../prompts'; -import { AgentOperation, getSystemPromptWithProjectContext, OperationOptions } from './common'; -import { SCOFFormat, SCOFParsingState } from '../output-formats/streaming-formats/scof'; -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 { - phaseName: string; - phaseDescription: string; - requirements: string[]; - files: FileConceptType[]; - fileGeneratingCallback?: (filePath: string, filePurpose: string) => void; - fileChunkGeneratedCallback?: (filePath: string, chunk: string, format: 'full_content' | 'unified_diff') => void; - fileClosedCallback?: (file: FileOutputType, message: string) => void; -} - -export interface SimpleCodeGenerationOutputs { - files: FileOutputType[]; -} - -const SYSTEM_PROMPT = `You are an expert Cloudflare developer specializing in Cloudflare Workers and Workflows. - -Your task is to generate production-ready code based on the provided specifications. - -## Original User Request -{{userQuery}} - -## Project Context -{{projectContext}} - -## Template Information -{{template}} - -## Previously Generated Files -{{existingFiles}} - -## Critical Guidelines -- Write clean, type-safe TypeScript code -- Follow best practices for the specific project type -- For Workflows: use WorkflowEntrypoint, step.do(), step.sleep() patterns -- For Workers: use standard Worker patterns with Request/Response -- Ensure all imports are correct -- Add proper error handling -- Include JSDoc comments where helpful -- Consider the context of existing files when generating new code -- Ensure new code integrates well with previously generated files`; - -const USER_PROMPT = `Generate code for the following phase: - -**Phase Name:** {{phaseName}} -**Description:** {{phaseDescription}} - -**Requirements:** -{{requirements}} - -**Files to Generate:** -{{files}} - -Generate complete, production-ready code for all specified files.`; - -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 formatRequirements = (requirements: string[]): string => { - return requirements.map((req, index) => `${index + 1}. ${req}`).join('\n'); -}; - -const formatFiles = (files: FileConceptType[]): string => { - return files.map((file, index) => { - return `${index + 1}. **${file.path}** - Purpose: ${file.purpose} - ${file.changes ? `Changes needed: ${file.changes}` : 'Create new file'}`; - }).join('\n\n'); -}; - -const formatExistingFiles = (allFiles: FileState[]): string => { - if (!allFiles || allFiles.length === 0) { - return 'No files generated yet. This is the first generation phase.'; - } - - // Convert FileState[] to FileOutputType[] format for serializer - const filesForSerializer: FileOutputType[] = allFiles.map(file => ({ - filePath: file.filePath, - fileContents: file.fileContents, - filePurpose: file.filePurpose || 'Previously generated file' - })); - - // Use existing serializer from PROMPT_UTILS - return PROMPT_UTILS.serializeFiles(filesForSerializer, CodeSerializerType.SIMPLE); -}; - -export class SimpleCodeGenerationOperation extends AgentOperation< - GenerationContext, - SimpleCodeGenerationInputs, - SimpleCodeGenerationOutputs -> { - async execute( - inputs: SimpleCodeGenerationInputs, - options: OperationOptions - ): Promise { - const { phaseName, phaseDescription, requirements, files } = inputs; - const { env, logger, context, inferenceContext } = options; - - logger.info('Generating code via simple code generation', { - phaseName, - phaseDescription, - fileCount: files.length, - requirementCount: requirements.length, - existingFilesCount: context.allFiles.length, - hasUserQuery: !!context.query, - hasTemplateDetails: !!context.templateDetails - }); - - // Build project context - const projectContext = context.templateDetails - ? PROMPT_UTILS.serializeTemplate(context.templateDetails) - : 'No template context available'; - - // Format existing files for context - const existingFilesContext = formatExistingFiles(context.allFiles); - - // Build system message with full context - const systemPrompt = PROMPT_UTILS.replaceTemplateVariables(SYSTEM_PROMPT, { - userQuery: context.query || 'No specific user query available', - projectContext, - template: context.templateDetails ? PROMPT_UTILS.serializeTemplate(context.templateDetails) : 'No template information', - existingFiles: existingFilesContext - }); - - // Build user message with requirements - const userPrompt = PROMPT_UTILS.replaceTemplateVariables(USER_PROMPT, { - phaseName, - phaseDescription, - requirements: formatRequirements(requirements), - files: formatFiles(files) - }); - - const codeGenerationFormat = new SCOFFormat(); - const messages = [ - createSystemMessage(systemPrompt), - createUserMessage(userPrompt + codeGenerationFormat.formatInstructions()) - ]; - - // Initialize streaming state - const streamingState: CodeGenerationStreamingState = { - accumulator: '', - completedFiles: new Map(), - parsingState: {} as SCOFParsingState - }; - - const generatedFiles: FileOutputType[] = []; - - // Execute inference with streaming - await executeInference({ - env, - context: inferenceContext, - agentActionName: 'phaseImplementation', // Use existing phase implementation config - messages, - stream: { - chunk_size: 256, - onChunk: (chunk: string) => { - codeGenerationFormat.parseStreamingChunks( - chunk, - streamingState, - // File generation started - (filePath: string) => { - logger.info(`Starting generation of file: ${filePath}`); - if (inputs.fileGeneratingCallback) { - const purpose = files.find(f => f.path === filePath)?.purpose || 'Generated file'; - inputs.fileGeneratingCallback(filePath, purpose); - } - }, - // Stream file content chunks - (filePath: string, fileChunk: string, format: 'full_content' | 'unified_diff') => { - if (inputs.fileChunkGeneratedCallback) { - inputs.fileChunkGeneratedCallback(filePath, fileChunk, format); - } - }, - // onFileClose callback - (filePath: string) => { - logger.info(`Completed generation of file: ${filePath}`); - const completedFile = streamingState.completedFiles.get(filePath); - if (!completedFile) { - logger.error(`Completed file not found: ${filePath}`); - return; - } - - // Process the file contents - const originalContents = context.allFiles.find(f => f.filePath === filePath)?.fileContents || ''; - completedFile.fileContents = FileProcessing.processGeneratedFileContents( - completedFile, - originalContents, - logger - ); - - const generatedFile: FileOutputType = { - ...completedFile, - filePurpose: files.find(f => f.path === filePath)?.purpose || 'Generated file' - }; - - generatedFiles.push(generatedFile); - - if (inputs.fileClosedCallback) { - inputs.fileClosedCallback(generatedFile, `Completed generation of ${filePath}`); - } - } - ); - } - } - }); - - logger.info('Code generation completed', { - fileCount: generatedFiles.length - }); - - return { - files: generatedFiles - }; - } - - async generateReadme(options: OperationOptions): Promise { - 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/UserConversationProcessor.ts b/worker/agents/operations/UserConversationProcessor.ts index 915b3e25..43ebaafe 100644 --- a/worker/agents/operations/UserConversationProcessor.ts +++ b/worker/agents/operations/UserConversationProcessor.ts @@ -18,7 +18,6 @@ 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; @@ -326,7 +325,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 */ @@ -334,7 +333,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 baae2fc2..1ed88286 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 { ICodingAgent } from "../services/interfaces/ICodingAgent"; +import { CodingAgentInterface } from "../services/implementations/CodingAgent"; export function getSystemPromptWithProjectContext( systemPrompt: string, @@ -23,9 +23,9 @@ export function getSystemPromptWithProjectContext( })), createUserMessage( USER_PROMPT_FORMATTER.PROJECT_CONTEXT( - GenerationContext.getCompletedPhases(context), + context.getCompletedPhases(), allFiles, - GenerationContext.getFileTree(context), + context.getFileTree(), commandsHistory, serializerType ) @@ -35,32 +35,18 @@ export function getSystemPromptWithProjectContext( return messages; } -/** - * Operation options with context type constraint - * @template TContext - Context type (defaults to GenerationContext for universal operations) - */ -export interface OperationOptions { +export interface OperationOptions { env: Env; agentId: string; - context: TContext; + context: GenerationContext; logger: StructuredLogger; inferenceContext: InferenceContext; - agent: ICodingAgent; + agent: CodingAgentInterface; } -/** - * 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 -> { +export abstract class AgentOperation { abstract execute( - inputs: TInput, - options: OperationOptions - ): Promise; + inputs: InputType, + options: OperationOptions + ): Promise; } \ No newline at end of file diff --git a/worker/agents/planning/blueprint.ts b/worker/agents/planning/blueprint.ts index e319aff6..5f3148f9 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 { PhasicBlueprint, AgenticBlueprint, PhasicBlueprintSchema, AgenticBlueprintSchema, TemplateSelection, Blueprint } from '../schemas'; +import { Blueprint, BlueprintSchema, TemplateSelection } from '../schemas'; import { createLogger } from '../../logger'; import { createSystemMessage, createUserMessage, createMultiModalUserMessage } from '../inferutils/common'; import { InferenceContext } from '../inferutils/config.types'; @@ -13,74 +13,7 @@ import { getTemplateImportantFiles } from 'worker/services/sandbox/utils'; const logger = createLogger('Blueprint'); -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 = ` +const 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. @@ -225,12 +158,15 @@ Preinstalled dependencies: {{dependencies}} `; -interface BaseBlueprintGenerationArgs { +export interface BlueprintGenerationArgs { env: Env; inferenceContext: InferenceContext; query: string; language: string; frameworks: string[]; + // Add optional template info + templateDetails: TemplateDetails; + templateMetaInfo: TemplateSelection; images?: ProcessedImageAttachment[]; stream?: { chunk_size: number; @@ -238,46 +174,26 @@ interface BaseBlueprintGenerationArgs { }; } -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 */ -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; - +// Update function signature and system prompt +export async function generateBlueprint({ env, inferenceContext, query, language, frameworks, templateDetails, templateMetaInfo, images, stream }: BlueprintGenerationArgs): Promise { try { - logger.info(`Generating ${isAgentic ? 'agentic' : 'phasic'} blueprint`, { query, queryLength: query.length, imagesCount: images?.length || 0 }); - if (templateDetails) logger.info(`Using template: ${templateDetails.name}`); + logger.info("Generating application blueprint", { query, queryLength: query.length, imagesCount: images?.length || 0 }); + logger.info(templateDetails ? `Using template: ${templateDetails.name}` : "Not using a template."); - // 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); - } - + // --------------------------------------------------------------------------- + // 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); const systemPromptMessage = createSystemMessage(generalSystemPromptBuilder(systemPrompt, { query, templateDetails, @@ -285,7 +201,7 @@ export async function generateBlueprint( templateMetaInfo, blueprint: undefined, language, - dependencies: templateDetails?.deps, + dependencies: templateDetails.deps, })); const userMessage = images && images.length > 0 @@ -315,18 +231,21 @@ export async function generateBlueprint( env, messages, agentActionName: "blueprint", - schema, + schema: BlueprintSchema, context: inferenceContext, - stream, + stream: stream, }); - // 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')); + if (results) { + // Filter and remove any pdf files + results.initialPhase.files = results.initialPhase.files.filter(f => !f.path.endsWith('.pdf')); } - return results as PhasicBlueprint | AgenticBlueprint; + // // A hack + // if (results?.initialPhase) { + // results.initialPhase.lastPhase = false; + // } + return results as Blueprint; } catch (error) { logger.error("Error generating blueprint:", error); throw error; diff --git a/worker/agents/prompts.ts b/worker/agents/prompts.ts index 3410b6ae..ce292c87 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 { PhasicBlueprint, AgenticBlueprint, BlueprintSchemaLite, AgenticBlueprintSchema, FileOutputType, PhaseConceptLiteSchema, PhaseConceptSchema, PhaseConceptType, TemplateSelection, Blueprint } from "./schemas"; +import { Blueprint, BlueprintSchemaLite, FileOutputType, PhaseConceptLiteSchema, PhaseConceptSchema, PhaseConceptType, TemplateSelection } from "./schemas"; import { IssueReport } from "./domain/values/IssueReport"; import { FileState, MAX_PHASES } from "./core/state"; import { CODE_SERIALIZERS, CodeSerializerType } from "./utils/codeSerializers"; @@ -1312,8 +1312,8 @@ FRONTEND_FIRST_CODING: ` export interface GeneralSystemPromptBuilderParams { query: string, - templateDetails?: TemplateDetails, - dependencies?: Record, + templateDetails: TemplateDetails, + dependencies: Record, blueprint?: Blueprint, language?: string, frameworks?: string[], @@ -1327,29 +1327,19 @@ 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 ?? {}); - } - // Blueprint variables - discriminate by type + // Optional blueprint variables if (params.blueprint) { - 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'); + // Redact the initial phase information from blueprint + const blueprint = { + ...params.blueprint, + initialPhase: undefined, } + 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 7ba540fe..55344e32 100644 --- a/worker/agents/schemas.ts +++ b/worker/agents/schemas.ts @@ -75,16 +75,12 @@ 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 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({ +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.'), 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'), @@ -105,13 +101,10 @@ export const PhasicBlueprintSchema = SimpleBlueprintSchema.extend({ 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 AgenticBlueprintSchema = SimpleBlueprintSchema.extend({ - plan: z.array(z.string()).describe('Step by step plan for implementing the project'), -}); - -export const BlueprintSchemaLite = PhasicBlueprintSchema.omit({ +export const BlueprintSchemaLite = BlueprintSchema.omit({ initialPhase: true, }); @@ -131,8 +124,7 @@ export const ScreenshotAnalysisSchema = z.object({ }); export type TemplateSelection = z.infer; -export type PhasicBlueprint = z.infer; -export type AgenticBlueprint = z.infer; +export type Blueprint = z.infer; export type FileConceptType = z.infer; export type PhaseConceptType = z.infer; export type PhaseConceptLiteType = z.infer; @@ -153,4 +145,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 93f11150..d13e1a5c 100644 --- a/worker/agents/services/implementations/CodingAgent.ts +++ b/worker/agents/services/implementations/CodingAgent.ts @@ -44,7 +44,7 @@ export class CodingAgentInterface { } } - queueUserRequest(request: string, images?: ProcessedImageAttachment[]): void { + queueRequest(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 547dba07..48db5f4d 100644 --- a/worker/agents/services/interfaces/ICodingAgent.ts +++ b/worker/agents/services/interfaces/ICodingAgent.ts @@ -1,69 +1,65 @@ -import { FileOutputType, FileConceptType, Blueprint } from "worker/agents/schemas"; +import { FileOutputType, Blueprint, FileConceptType } 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 { BehaviorType, DeepDebugResult } from "worker/agents/core/types"; +import { OperationOptions } from "worker/agents/operations/common"; +import { 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 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; - - 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; +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; - updateBlueprint(patch: Partial): Promise; + 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; - generateFiles( + abstract regenerateFileByPath(path: string, issues: string[]): Promise<{ path: string; diff: string }>; + + abstract generateFiles( phaseName: string, phaseDescription: string, requirements: string[], files: FileConceptType[] ): Promise<{ files: Array<{ path: string; purpose: string; diff: string }> }>; - regenerateFileByPath(path: string, issues: string[]): Promise<{ path: string; diff: string }>; - - isCodeGenerating(): boolean; - - waitForGeneration(): Promise; - - isDeepDebugging(): boolean; - - waitForDeepDebug(): Promise; - - executeDeepDebug( + 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( 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 92fc9940..51c96d4c 100644 --- a/worker/agents/tools/customTools.ts +++ b/worker/agents/tools/customTools.ts @@ -6,6 +6,7 @@ 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'; @@ -20,7 +21,6 @@ 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: ICodingAgent, + agent: CodingAgentInterface, 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 96d5dec1..d4e5faa5 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; import { Blueprint } from 'worker/agents/schemas'; type AlterBlueprintArgs = { @@ -10,7 +10,7 @@ type AlterBlueprintArgs = { }; export function createAlterBlueprintTool( - agent: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/deep-debugger.ts b/worker/agents/tools/toolkit/deep-debugger.ts index f9e3e154..bf15de96 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; import { RenderToolCall } from 'worker/agents/operations/UserConversationProcessor'; export function createDeepDebuggerTool( - agent: ICodingAgent, + agent: CodingAgentInterface, 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 36995c79..c6be2788 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; type DeployPreviewArgs = Record; type DeployPreviewResult = { message: string } | ErrorResult; export function createDeployPreviewTool( - agent: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/exec-commands.ts b/worker/agents/tools/toolkit/exec-commands.ts index 3e947455..c9548d05 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; 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: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger, ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/generate-files.ts b/worker/agents/tools/toolkit/generate-files.ts index db513d78..7091a818 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; import { FileConceptType } from 'worker/agents/schemas'; export type GenerateFilesArgs = { @@ -18,7 +18,7 @@ export type GenerateFilesResult = | ErrorResult; export function createGenerateFilesTool( - agent: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger, ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/get-logs.ts b/worker/agents/tools/toolkit/get-logs.ts index b2214201..a5401058 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; type GetLogsArgs = { reset?: boolean; @@ -11,7 +11,7 @@ type GetLogsArgs = { type GetLogsResult = { logs: string } | ErrorResult; export function createGetLogsTool( - agent: ICodingAgent, + agent: CodingAgentInterface, 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 c3e35277..5c75374d 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; 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: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/git.ts b/worker/agents/tools/toolkit/git.ts index f1f5206c..7ca91246 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; type GitCommand = 'commit' | 'log' | 'show' | 'reset'; @@ -13,7 +13,7 @@ interface GitToolArgs { } export function createGitTool( - agent: ICodingAgent, + agent: CodingAgentInterface, 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 4fd90f5e..486bd809 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; type QueueRequestArgs = { modificationRequest: string; }; export function createQueueRequestTool( - agent: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger ): ToolDefinition { return { @@ -34,7 +34,7 @@ export function createQueueRequestTool( logger.info('Received app edit request', { modificationRequest: args.modificationRequest, }); - agent.queueUserRequest(args.modificationRequest); + agent.queueRequest(args.modificationRequest); return null; }, }; diff --git a/worker/agents/tools/toolkit/read-files.ts b/worker/agents/tools/toolkit/read-files.ts index 15cb28c4..fa8dc0f3 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; export type ReadFilesArgs = { paths: string[]; @@ -12,7 +12,7 @@ export type ReadFilesResult = | ErrorResult; export function createReadFilesTool( - agent: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger, ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/regenerate-file.ts b/worker/agents/tools/toolkit/regenerate-file.ts index 2fcde525..7afca870 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; export type RegenerateFileArgs = { path: string; @@ -12,7 +12,7 @@ export type RegenerateFileResult = | ErrorResult; export function createRegenerateFileTool( - agent: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger, ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/rename-project.ts b/worker/agents/tools/toolkit/rename-project.ts index 850161b5..be7ab416 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; type RenameArgs = { newName: string; @@ -9,7 +9,7 @@ type RenameArgs = { type RenameResult = { projectName: string }; export function createRenameProjectTool( - agent: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger ): ToolDefinition { return { diff --git a/worker/agents/tools/toolkit/run-analysis.ts b/worker/agents/tools/toolkit/run-analysis.ts index b95a38c8..52e67c78 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; 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: ICodingAgent, + agent: CodingAgentInterface, 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 59e31185..2852ea5a 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; export function createWaitForDebugTool( - agent: ICodingAgent, + agent: CodingAgentInterface, 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 a599f8ff..d34224c7 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 { ICodingAgent } from 'worker/agents/services/interfaces/ICodingAgent'; +import { CodingAgentInterface } from 'worker/agents/services/implementations/CodingAgent'; export function createWaitForGenerationTool( - agent: ICodingAgent, + agent: CodingAgentInterface, logger: StructuredLogger ): ToolDefinition, { status: string } | { error: string }> { return { From 670c8d6c241d38335676440ecaa6225795ba6f2a Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Tue, 18 Nov 2025 15:12:01 -0500 Subject: [PATCH 20/24] fix: cleanup chat.tsx for build --- src/routes/chat/chat.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/routes/chat/chat.tsx b/src/routes/chat/chat.tsx index 1dcf949c..df1a3c47 100644 --- a/src/routes/chat/chat.tsx +++ b/src/routes/chat/chat.tsx @@ -36,7 +36,6 @@ import { useAutoScroll } from '@/hooks/use-auto-scroll'; import { useImageUpload } from '@/hooks/use-image-upload'; import { useDragDrop } from '@/hooks/use-drag-drop'; import { ImageAttachmentPreview } from '@/components/image-attachment-preview'; -import { createAIMessage } from './utils/message-helpers'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; @@ -113,14 +112,12 @@ export default function Chat() { totalFiles, websocket, sendUserMessage, - sendAiMessage, blueprint, previewUrl, clearEdit, projectStages, phaseTimeline, isThinking, - onCompleteBootstrap, // Deployment and generation control isDeploying, cloudflareDeploymentUrl, @@ -235,7 +232,7 @@ export default function Chat() { const imageInputRef = useRef(null); // Fake stream bootstrap files - const { streamedFiles: streamedBootstrapFiles, doneStreaming } = + const { streamedFiles: streamedBootstrapFiles } = useFileContentStream(bootstrapFiles, { tps: 600, enabled: isBootstrapping, From 3f9ccf7b3b1b137d6255493da14f8463ce8a66f3 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Fri, 21 Nov 2025 03:49:50 -0500 Subject: [PATCH 21/24] feat: add agent-specific model constraints and refactor generation - Add agent-specific model constraints and filtering - Refactor centralize AI model configuration with credit-based system - Add SimpleCodeGeneration operation - Update model configurations and rate limits - Improve model selector and config UI --- .husky/commit-msg | 11 +- .husky/pre-commit | 11 +- SandboxDockerfile | 1 - src/components/config-modal.tsx | 86 ++- src/components/model-config-tabs.tsx | 6 +- src/components/ui/model-selector.tsx | 4 +- src/lib/api-client.ts | 11 +- src/routes/chat/components/preview-iframe.tsx | 2 + worker-configuration.d.ts | 18 +- worker/agents/assistants/codeDebugger.ts | 5 +- worker/agents/assistants/projectsetup.ts | 1 + worker/agents/core/simpleGeneratorAgent.ts | 117 ++-- worker/agents/inferutils/config.ts | 209 +++----- worker/agents/inferutils/config.types.ts | 367 +++++++++++-- worker/agents/inferutils/core.ts | 53 +- worker/agents/inferutils/infer.ts | 66 ++- worker/agents/operations/PhaseGeneration.ts | 2 +- .../agents/operations/SimpleCodeGeneration.ts | 276 ++++++++++ worker/agents/planning/templateSelector.ts | 1 + worker/agents/prompts.ts | 17 +- .../implementations/DeploymentManager.ts | 10 +- worker/api/controllers/agent/controller.ts | 16 +- .../api/controllers/modelConfig/byokHelper.ts | 7 + .../modelConfig/constraintHelper.ts | 69 +++ .../api/controllers/modelConfig/controller.ts | 90 +++- .../database/services/ModelConfigService.ts | 500 ++++++++++-------- worker/database/services/ModelTestService.ts | 2 +- worker/services/aigateway-proxy/controller.ts | 3 +- worker/services/rate-limit/config.ts | 22 +- worker/services/rate-limit/rateLimits.ts | 16 +- worker/services/sandbox/BaseSandboxService.ts | 28 +- worker/services/sandbox/sandboxTypes.ts | 9 +- 32 files changed, 1435 insertions(+), 601 deletions(-) create mode 100644 worker/agents/operations/SimpleCodeGeneration.ts create mode 100644 worker/api/controllers/modelConfig/constraintHelper.ts diff --git a/.husky/commit-msg b/.husky/commit-msg index 3b9e0aa4..2e99098d 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1 +1,10 @@ -bunx commitlint --edit $1 +# Add common bun installation paths to PATH +export PATH="/opt/homebrew/bin:$HOME/.bun/bin:$PATH" + +# Run commitlint if bunx is available +if command -v bunx >/dev/null 2>&1; then + bunx commitlint --edit $1 +else + echo "⚠️ bunx not found in PATH, skipping commitlint" + exit 0 +fi diff --git a/.husky/pre-commit b/.husky/pre-commit index 4dfead07..eac0326d 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,10 @@ -bun test +# Add common bun installation paths to PATH +export PATH="/opt/homebrew/bin:$HOME/.bun/bin:$PATH" + +# Run tests with bun +if command -v bun >/dev/null 2>&1; then + bun test +else + echo "⚠️ bun not found in PATH, skipping tests" + exit 0 +fi diff --git a/SandboxDockerfile b/SandboxDockerfile index 9bd45e9d..10daa45e 100644 --- a/SandboxDockerfile +++ b/SandboxDockerfile @@ -52,7 +52,6 @@ RUN chmod 755 /workspace/data # Set environment variable to indicate Docker container environment ENV CONTAINER_ENV=docker ENV VITE_LOGGER_TYPE=json - # # Set environment variable to indicate Docker container environment # ENV CONTAINER_ENV=docker diff --git a/src/components/config-modal.tsx b/src/components/config-modal.tsx index d7c7d980..e4092f96 100644 --- a/src/components/config-modal.tsx +++ b/src/components/config-modal.tsx @@ -68,18 +68,18 @@ const getModelDisplayName = (model: AIModels | string): string => { // Model recommendations by agent const getModelRecommendation = (agentAction: string) => { const recommendations: Record = { - templateSelection: '💡 Recommended: Fast models for quick template selection', - blueprint: '🏗️ Recommended: Creative models for architecture design', - projectSetup: '⚙️ Recommended: Reliable models for precise setup', - phaseGeneration: '📋 Recommended: Large context models for comprehensive planning', - firstPhaseImplementation: '🏁 Recommended: High-capability models for foundation development', - phaseImplementation: '⚡ Recommended: Strong coding models for implementation', - realtimeCodeFixer: '🚀 Recommended: Fast debugging models', - fastCodeFixer: '⚡ Recommended: Ultra-fast models for quick fixes', - conversationalResponse: '💬 Recommended: Balanced models for natural conversation', - codeReview: '🔍 Recommended: Analytical models with large context', - fileRegeneration: '📝 Recommended: Pure coding models', - screenshotAnalysis: '👁️ Recommended: Vision-capable models for image analysis' + templateSelection: 'Recommended: Fast models for quick template selection', + blueprint: 'Recommended: Creative models for architecture design', + projectSetup: 'Recommended: Reliable models for precise setup', + phaseGeneration: 'Recommended: Large context models for comprehensive planning', + firstPhaseImplementation: 'Recommended: High-capability models for foundation development', + phaseImplementation: 'Recommended: Strong coding models for implementation', + realtimeCodeFixer: 'Recommended: Fast debugging models', + fastCodeFixer: 'Recommended: Ultra-fast models for quick fixes', + conversationalResponse: 'Recommended: Balanced models for natural conversation', + codeReview: 'Recommended: Analytical models with large context', + fileRegeneration: 'Recommended: Pure coding models', + screenshotAnalysis: 'Recommended: Vision-capable models for image analysis' }; return recommendations[agentAction] || ''; }; @@ -98,7 +98,6 @@ export function ConfigModal({ // Form state const [formData, setFormData] = useState({ modelName: userConfig?.name || 'default', - maxTokens: userConfig?.max_tokens?.toString() || '', temperature: userConfig?.temperature?.toString() || '', reasoningEffort: userConfig?.reasoning_effort || 'default', fallbackModel: userConfig?.fallbackModel || 'default' @@ -115,11 +114,12 @@ export function ConfigModal({ const [byokData, setByokData] = useState(null); const [loadingByok, setLoadingByok] = useState(false); - // Load BYOK data + // Load BYOK data (filtered by agent constraints) const loadByokData = async () => { try { setLoadingByok(true); - const response = await apiClient.getByokProviders(); + // Pass agent key to get constraint-filtered models + const response = await apiClient.getByokProviders(agentConfig.key); if (response.success && response.data) { setByokData(response.data); } @@ -136,7 +136,6 @@ export function ConfigModal({ // First time opening - reset everything and load data setFormData({ modelName: userConfig?.name || 'default', - maxTokens: userConfig?.max_tokens?.toString() || '', temperature: userConfig?.temperature?.toString() || '', reasoningEffort: userConfig?.reasoning_effort || 'default', fallbackModel: userConfig?.fallbackModel || 'default' @@ -162,7 +161,6 @@ export function ConfigModal({ useEffect(() => { const originalFormData = { modelName: userConfig?.name || 'default', - maxTokens: userConfig?.max_tokens?.toString() || '', temperature: userConfig?.temperature?.toString() || '', reasoningEffort: userConfig?.reasoning_effort || 'default', fallbackModel: userConfig?.fallbackModel || 'default' @@ -246,7 +244,6 @@ export function ConfigModal({ const buildCurrentConfig = (): ModelConfigUpdate => { return { ...(formData.modelName !== 'default' && { modelName: formData.modelName }), - ...(formData.maxTokens && { maxTokens: parseInt(formData.maxTokens) }), ...(formData.temperature && { temperature: parseFloat(formData.temperature) }), ...(formData.reasoningEffort !== 'default' && { reasoningEffort: formData.reasoningEffort }), ...(formData.fallbackModel !== 'default' && { fallbackModel: formData.fallbackModel }), @@ -289,19 +286,31 @@ export function ConfigModal({ Configure {agentConfig.name} - -

{agentConfig.description}

- {getModelRecommendation(agentConfig.key) && ( - - - - {getModelRecommendation(agentConfig.key)} - - - )} + + {agentConfig.description} + {/* Alerts outside DialogDescription to avoid nested p/div issues */} +
+ {agentConfig.constraint?.enabled && ( + + + + Model selection limited to {agentConfig.constraint.allowedModels.length} allowed model{agentConfig.constraint.allowedModels.length !== 1 ? 's' : ''} for this operation. + + + )} + {getModelRecommendation(agentConfig.key) && ( + + + + {getModelRecommendation(agentConfig.key)} + + + )} +
+
{/* Current Status */}
@@ -428,7 +437,7 @@ export function ConfigModal({

Parameters

-
+
{/* Temperature */}
@@ -449,25 +458,6 @@ export function ConfigModal({ )}
- {/* Max Tokens */} -
- - setFormData({...formData, maxTokens: e.target.value})} - className="h-10" - /> - {defaultConfig?.max_tokens && ( -

- 🔧 Default: {defaultConfig.max_tokens?.toLocaleString()} -

- )} -
- {/* Reasoning Effort */}
diff --git a/src/components/model-config-tabs.tsx b/src/components/model-config-tabs.tsx index 1b957a7d..5e2b7b38 100644 --- a/src/components/model-config-tabs.tsx +++ b/src/components/model-config-tabs.tsx @@ -117,11 +117,15 @@ const categorizeAgent = (agentKey: string): string => { return 'advanced'; }; -// Frontend-specific agent display interface +// Frontend-specific agent display interface export interface AgentDisplayConfig { key: string; name: string; description: string; + constraint?: { + enabled: boolean; + allowedModels: string[]; + }; } interface ModelConfigTabsProps { diff --git a/src/components/ui/model-selector.tsx b/src/components/ui/model-selector.tsx index 0677d92d..7fbc6155 100644 --- a/src/components/ui/model-selector.tsx +++ b/src/components/ui/model-selector.tsx @@ -111,7 +111,7 @@ export function ModelSelector({ className="w-full justify-between" disabled={disabled} > - {getSelectedDisplay() || placeholder} + {getSelectedDisplay() || placeholder} @@ -203,7 +203,7 @@ export function ModelSelector({ {/* System default display */} {systemDefault && ( -

+

🔧 System default: {getModelDisplayName(systemDefault)}

)} diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index a6ea0952..ae87ebe9 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -684,11 +684,14 @@ class ApiClient { /** * Get BYOK providers and available models + * @param agentAction - Optional agent action to filter models by constraints */ - async getByokProviders(): Promise> { - return this.request( - '/api/model-configs/byok-providers', - ); + async getByokProviders(agentAction?: string): Promise> { + const endpoint = agentAction + ? `/api/model-configs/byok-providers?agentAction=${encodeURIComponent(agentAction)}` + : '/api/model-configs/byok-providers'; + + return this.request(endpoint); } /** diff --git a/src/routes/chat/components/preview-iframe.tsx b/src/routes/chat/components/preview-iframe.tsx index eae8d601..1b1986af 100644 --- a/src/routes/chat/components/preview-iframe.tsx +++ b/src/routes/chat/components/preview-iframe.tsx @@ -336,6 +336,7 @@ export const PreviewIframe = forwardRef( if (loadState.status === 'loaded' && loadState.loadedSrc) { return (