← vibesdk
worker/agents/operations/AgenticProjectBuilder.ts
12.0 KB
Download file
import {
createSystemMessage,
createUserMessage,
Message,
ConversationMessage,
} from '../inferutils/common';
import { AgentActionKey } from '../inferutils/config.types';
import { withRenderer } from '../tools/customTools';
import { RenderToolCall } from './UserConversationProcessor';
import { PROMPT_UTILS } from '../prompts';
import { FileState } from '../core/state';
import { ProjectType } from '../core/types';
import { Blueprint, AgenticBlueprint } from '../schemas';
import { prepareMessagesForInference } from '../utils/common';
import { createMarkGenerationCompleteTool } from '../tools/toolkit/completion-signals';
import { AgentOperationWithTools, OperationOptions, ToolSession, ToolCallbacks } from './common';
import { GenerationContext } from '../domain/values/GenerationContext';
import getSystemPrompt from './prompts/agenticBuilderPrompts';
import { ToolDefinition } from '../tools/types';
import { InferResponseString } from '../inferutils/core';
import { createGenerateBlueprintTool } from '../tools/toolkit/generate-blueprint';
import { createAlterBlueprintTool } from '../tools/toolkit/alter-blueprint';
import { createInitSuitableTemplateTool } from '../tools/toolkit/init-suitable-template';
import { createVirtualFilesystemTool } from '../tools/toolkit/virtual-filesystem';
import { createGenerateFilesTool } from '../tools/toolkit/generate-files';
import { createRegenerateFileTool } from '../tools/toolkit/regenerate-file';
import { createRunAnalysisTool } from '../tools/toolkit/run-analysis';
import { createDeployPreviewTool } from '../tools/toolkit/deploy-preview';
import { createGetRuntimeErrorsTool } from '../tools/toolkit/get-runtime-errors';
import { createGetLogsTool } from '../tools/toolkit/get-logs';
import { createExecCommandsTool } from '../tools/toolkit/exec-commands';
import { createWaitTool } from '../tools/toolkit/wait';
import { createGitTool } from '../tools/toolkit/git';
import { createGenerateImagesTool } from '../tools/toolkit/generate-images';
export interface AgenticProjectBuilderInputs {
query: string;
projectName: string;
blueprint?: Blueprint;
filesIndex: FileState[];
projectType: ProjectType;
selectedTemplate?: string;
operationalMode: 'initial' | 'followup';
conversationHistory?: ConversationMessage[];
streamCb?: (chunk: string) => void;
reasoningCb?: (delta: string) => void;
toolRenderer: RenderToolCall;
onToolComplete?: (message: Message) => Promise<void>;
onAssistantMessage?: (message: Message) => Promise<void>;
}
export interface AgenticProjectBuilderOutputs {
output: string;
}
export interface AgenticBuilderSession extends ToolSession {
templateInfo?: string;
dynamicHints: string;
fileSummaries: string;
hasFiles: boolean;
hasPlan: boolean;
}
/**
* Build user prompt with all context
*/
const getUserPrompt = (
query: string,
projectName: string,
fileSummaries: string,
templateInfo?: string
): string => {
return `## Build Task
**Project Name**: ${projectName}
**User Request**: ${query}
${
// blueprint ? `## Project Blueprint
// The following blueprint defines the structure, features, and requirements for this project:
// \`\`\`json
// ${JSON.stringify(blueprint, null, 2)}
// \`\`\`
// **Use this blueprint to guide your implementation.** It outlines what needs to be built.` : `## Note
// No blueprint provided. Design the project structure based on the user request above.`
''
}
${templateInfo ? `## Template Context
This project uses a preconfigured template:
${templateInfo}
**IMPORTANT:** Leverage existing components, utilities, and APIs from the template. Do not recreate what already exists.` : ''}
${fileSummaries ? `## Current Codebase
${fileSummaries}` : `## Starting Fresh
This is a new project. Start from the template or scratch.`}
Begin building.`;
};
export class AgenticProjectBuilderOperation extends AgentOperationWithTools<
GenerationContext,
AgenticProjectBuilderInputs,
AgenticProjectBuilderOutputs,
AgenticBuilderSession
> {
protected getCallbacks(
inputs: AgenticProjectBuilderInputs,
_options: OperationOptions<GenerationContext>
): ToolCallbacks {
const { streamCb, reasoningCb, toolRenderer, onToolComplete, onAssistantMessage } = inputs;
return {
streamCb,
reasoningCb,
toolRenderer,
onToolComplete,
onAssistantMessage,
};
}
protected buildSession(
inputs: AgenticProjectBuilderInputs,
options: OperationOptions<GenerationContext>
): AgenticBuilderSession {
const { logger, agent, context } = options;
const {
projectName,
projectType,
blueprint,
filesIndex,
selectedTemplate,
} = inputs;
logger.info('Starting project build', {
projectName,
projectType,
hasBlueprint: !!blueprint,
});
const fileSummaries = PROMPT_UTILS.summarizeFiles(filesIndex);
const templateInfo = context.templateDetails
? PROMPT_UTILS.serializeTemplate(context.templateDetails)
: undefined;
const hasFiles = (filesIndex || []).length > 0;
const isAgenticBlueprint = (bp?: Blueprint): bp is AgenticBlueprint => {
if (!bp) return false;
return 'plan' in bp && Array.isArray(bp.plan);
};
const hasTSX = filesIndex?.some(f => /\.(t|j)sx$/i.test(f.filePath)) || false;
const hasMD = filesIndex?.some(f => /\.(md|mdx)$/i.test(f.filePath)) || false;
const hasPlan = isAgenticBlueprint(blueprint) && blueprint.plan.length > 0;
const hasTemplate = !!selectedTemplate;
const isPresentationProject = projectType === 'presentation';
const needsSandbox = !isPresentationProject && (hasTSX || projectType === 'app');
const dynamicHints = [
!hasPlan
? '- No plan detected: Start with generate_blueprint (optionally with prompt parameter) to establish PRD (title, projectName, description, colorPalette, frameworks, plan).'
: '- Plan detected: proceed to implement milestones using generate_files/regenerate_file.',
needsSandbox && !hasTemplate
? '- Interactive project without template: Use init_suitable_template() to let AI select and import best matching template before first deploy.'
: '',
isPresentationProject && !hasTemplate
? '- Presentation project detected: Use init_suitable_template() to select presentation template, then create stunning slides with unique design.'
: '',
hasTSX && !isPresentationProject
? '- UI detected: Use deploy_preview to verify runtime; then run_analysis for quick feedback.'
: '',
isPresentationProject
? '- Presentation mode: Use deploy_preview to sync slides. NO run_analysis needed. Focus on beautiful JSON slides, ask user for feedback.'
: '',
hasMD && !hasTSX
? '- Documents detected without UI: This is STATIC content - generate files in docs/, NO deploy_preview needed.'
: '',
!hasFiles && hasPlan
? '- Plan ready, no files yet: Scaffold initial structure with generate_files.'
: '',
]
.filter(Boolean)
.join('\n');
return {
agent,
templateInfo,
dynamicHints,
fileSummaries,
hasFiles,
hasPlan,
};
}
protected async buildMessages(
inputs: AgenticProjectBuilderInputs,
options: OperationOptions<GenerationContext>,
session: AgenticBuilderSession
): Promise<Message[]> {
const { env, logger } = options;
let historyMessages: Message[] = [];
if (inputs.conversationHistory && inputs.conversationHistory.length > 0) {
const prepared = await prepareMessagesForInference(env, inputs.conversationHistory);
historyMessages = prepared as Message[];
logger.info('Loaded conversation history', {
messageCount: historyMessages.length,
});
}
let systemPrompt = getSystemPrompt(inputs.projectType, session.dynamicHints);
if (historyMessages.length > 0) {
systemPrompt += `\n\n# Conversation History\nYou are being provided with the full conversation history from your previous interactions. Review it to understand context and avoid repeating work.`;
}
const userPrompt = getUserPrompt(
inputs.query,
inputs.projectName,
session.fileSummaries,
session.templateInfo,
);
const system = createSystemMessage(systemPrompt);
const user = createUserMessage(userPrompt);
return [system, user, ...historyMessages];
}
protected buildTools(
inputs: AgenticProjectBuilderInputs,
options: OperationOptions<GenerationContext>,
session: AgenticBuilderSession,
callbacks: ToolCallbacks
): ToolDefinition<unknown, unknown>[] {
const { logger } = options;
const toolRenderer = callbacks.toolRenderer;
const onToolComplete = callbacks.onToolComplete;
let rawTools : ToolDefinition<any, any>[] = [
// PRD generation + refinement
createGenerateBlueprintTool(session.agent, logger),
createAlterBlueprintTool(session.agent, logger),
// Virtual filesystem operations (list + read from Durable Object storage)
createVirtualFilesystemTool(session.agent, logger),
// Build + analysis toolchain
createGenerateFilesTool(session.agent, logger),
createRegenerateFileTool(session.agent, logger),
createRunAnalysisTool(session.agent, logger),
// Runtime + deploy
createDeployPreviewTool(session.agent, logger),
createGetRuntimeErrorsTool(session.agent, logger),
createGetLogsTool(session.agent, logger),
// Utilities
createExecCommandsTool(session.agent, logger),
createWaitTool(logger),
createGitTool(session.agent, logger),
// WIP: images
createGenerateImagesTool(session.agent, logger),
];
if (!inputs.selectedTemplate || inputs.selectedTemplate === 'scratch') {
rawTools.push(createInitSuitableTemplateTool(session.agent, logger));
}
rawTools = withRenderer(rawTools, toolRenderer, onToolComplete);
rawTools.push(createMarkGenerationCompleteTool(session.agent, logger));
return rawTools;
}
protected getAgentConfig(
inputs: AgenticProjectBuilderInputs,
options: OperationOptions<GenerationContext>,
session: AgenticBuilderSession
) {
const { logger } = options;
const { hasFiles, hasPlan } = session;
logger.info('Agentic builder mode', {
mode: inputs.operationalMode,
hasFiles,
hasPlan,
});
return {
agentActionName: 'agenticProjectBuilder' as AgentActionKey,
completionSignalName: 'mark_generation_complete',
operationalMode: inputs.operationalMode,
allowWarningInjection: inputs.operationalMode === 'initial',
};
}
protected mapResultToOutput(
_inputs: AgenticProjectBuilderInputs,
options: OperationOptions<GenerationContext>,
_session: AgenticBuilderSession,
result: InferResponseString
): AgenticProjectBuilderOutputs {
const output = result?.string || '';
options.logger.info('Project build completed', {
outputLength: output.length,
});
return { output };
}
}