Conversation
Co-authored-by: otdoges <otdoges@proton.me>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
CodeCapy Review ₍ᐢ•(ܫ)•ᐢ₎
Codebase SummaryZapDev is an AI-powered development platform that allows users to create and iterate on web applications using real-time AI agents within sandboxes. It features code generation, file management, project persistence, and integrates various services such as Vercel AI Gateway, Clerk authentication, and background job processing. PR ChangesThis pull request migrates the agent functionality from the Inngest Gateway to Vercel's AI Gateway by replacing the '@inngest/agent-kit' references with '@ai-sdk/gateway' and related modules. The changes include updates in jest configuration, dependency changes in package.json, and modifications in the agent functions and tools to work with the new AI SDK. Setup Instructions
Generated Test Cases1: Agent Request Flow Using AI SDK Gateway ❗️❗️❗️Description: Verifies that when a user submits a new development request, the request is processed through the updated AI SDK Gateway and the generated code preview appears. This tests the full end-to-end flow of initiating an AI agent call and displaying outputs. Prerequisites:
Steps:
Expected Result: The user sees a smooth transition from loading to displaying the generated code files and a summary description of the generated project, indicating that the AI request was routed correctly through the new AI SDK Gateway. 2: Error Handling When AI Gateway Request Fails ❗️❗️❗️Description: Tests that if the AI Gateway returns an error (e.g., due to an incorrect API key), the UI correctly displays an error message informing the user of the issue. Prerequisites:
Steps:
Expected Result: The UI presents an error notification indicating a failure in connecting to the AI Gateway, with clear instructions or hints for configuration. Once the configuration is corrected, the error message should be resolved. 3: Display of Updated File Explorer After AI Tool Execution ❗️❗️Description: Ensures that when an AI agent generates or updates files via the new code agent tools, the file explorer in the UI is updated correctly to reflect the new/modified files. Prerequisites:
Steps:
Expected Result: The file explorer dynamically reflects the changes made by the AI agent. The new or updated files are visible with correct file names and content, and a summary description is available elsewhere on the page that confirms the agent’s action. Raw Changes AnalyzedFile: jest.config.js
Changes:
@@ -11,7 +11,8 @@ module.exports = {
'^@/convex/_generated/dataModel$': '<rootDir>/tests/mocks/convex-generated-dataModel.ts',
'^@/convex/(.*)$': '<rootDir>/convex/$1',
'^@/(.*)$': '<rootDir>/src/$1',
- '^@inngest/agent-kit$': '<rootDir>/tests/mocks/inngest-agent-kit.ts',
+ '^@ai-sdk/gateway$': '<rootDir>/tests/mocks/ai-sdk-gateway.ts',
+ '^ai$': '<rootDir>/tests/mocks/ai.ts',
'^@e2b/code-interpreter$': '<rootDir>/tests/mocks/e2b-code-interpreter.ts',
'^convex/browser$': '<rootDir>/tests/mocks/convex-browser.ts',
},
File: package.json
Changes:
@@ -12,11 +12,11 @@
"convex:deploy": "bunx convex deploy"
},
"dependencies": {
+ "@ai-sdk/gateway": "^0.0.9",
"@databuddy/sdk": "^2.2.1",
"@e2b/code-interpreter": "^1.5.1",
"@hookform/resolvers": "^3.3.4",
- "@inngest/agent-kit": "^0.13.1",
- "@inngest/realtime": "^0.4.4",
+ "ai": "^4.3.16",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^2.2.0",
"@opentelemetry/resources": "^2.2.0",
File: src/ai-sdk/gateway.ts
Changes:
@@ -0,0 +1,8 @@
+import { createGateway } from "@ai-sdk/gateway";
+
+export const gateway = createGateway({
+ baseURL: process.env.AI_GATEWAY_BASE_URL || "https://gateway.ai.vercel.sh/v1",
+ headers: {
+ Authorization: `Bearer ${process.env.AI_GATEWAY_API_KEY}`,
+ },
+});
File: src/ai-sdk/index.ts
Changes:
@@ -0,0 +1,9 @@
+export { gateway } from "./gateway";
+export { createCodeAgentTools } from "./tools";
+export type {
+ AgentState,
+ AgentResult,
+ Framework,
+ AgentContext,
+ ToolResult,
+} from "./types";
File: src/ai-sdk/tools.ts
Changes:
@@ -0,0 +1,137 @@
+import { tool } from "ai";
+import { z } from "zod";
+import { Sandbox } from "@e2b/code-interpreter";
+import type { AgentState, ToolResult } from "./types";
+
+async function getSandboxFromId(sandboxId: string): Promise<Sandbox> {
+ return await Sandbox.connect(sandboxId, {
+ apiKey: process.env.E2B_API_KEY,
+ });
+}
+
+export function createCodeAgentTools(
+ sandboxId: string,
+ stateRef: { current: AgentState }
+) {
+ const terminalTool = tool({
+ description: "Run a terminal command in the sandbox environment",
+ parameters: z.object({
+ command: z.string().describe("The command to execute in the terminal"),
+ }),
+ execute: async ({ command }): Promise<ToolResult> => {
+ const buffers = { stdout: "", stderr: "" };
+
+ try {
+ const sandbox = await getSandboxFromId(sandboxId);
+ const result = await sandbox.commands.run(command, {
+ onStdout: (data: string) => {
+ buffers.stdout += data;
+ },
+ onStderr: (data: string) => {
+ buffers.stderr += data;
+ },
+ });
+ return {
+ success: true,
+ data: {
+ stdout: result.stdout || buffers.stdout,
+ stderr: buffers.stderr,
+ exitCode: result.exitCode,
+ },
+ };
+ } catch (e) {
+ const errorMessage = e instanceof Error ? e.message : String(e);
+ return {
+ success: false,
+ error: `Command failed: ${errorMessage}\nstdout: ${buffers.stdout}\nstderr: ${buffers.stderr}`,
+ };
+ }
+ },
+ });
+
+ const createOrUpdateFilesTool = tool({
+ description:
+ "Create or update files in the sandbox. Use this to write code files.",
+ parameters: z.object({
+ files: z
+ .array(
+ z.object({
+ path: z.string().describe("The file path relative to the sandbox"),
+ content: z.string().describe("The content to write to the file"),
+ })
+ )
+ .describe("Array of files to create or update"),
+ }),
+ execute: async ({ files }): Promise<ToolResult> => {
+ try {
+ const sandbox = await getSandboxFromId(sandboxId);
+ const updatedFiles = stateRef.current.files || {};
+
+ for (const file of files) {
+ await sandbox.files.write(file.path, file.content);
+ updatedFiles[file.path] = file.content;
+ }
+
+ stateRef.current.files = updatedFiles;
+
+ return {
+ success: true,
+ data: {
+ filesWritten: files.map((f) => f.path),
+ totalFiles: Object.keys(updatedFiles).length,
+ },
+ };
+ } catch (e) {
+ const errorMessage = e instanceof Error ? e.message : String(e);
+ return {
+ success: false,
+ error: `Failed to write files: ${errorMessage}`,
+ };
+ }
+ },
+ });
+
+ const readFilesTool = tool({
+ description: "Read files from the sandbox to understand existing code",
+ parameters: z.object({
+ files: z
+ .array(z.string())
+ .describe("Array of file paths to read from the sandbox"),
+ }),
+ execute: async ({ files }): Promise<ToolResult> => {
+ try {
+ const sandbox = await getSandboxFromId(sandboxId);
+ const contents: Array<{ path: string; content: string }> = [];
+
+ for (const filePath of files) {
+ try {
+ const content = await sandbox.files.read(filePath);
+ contents.push({ path: filePath, content });
+ } catch {
+ contents.push({
+ path: filePath,
+ content: `[Error: Could not read file ${filePath}]`,
+ });
+ }
+ }
+
+ return {
+ success: true,
+ data: contents,
+ };
+ } catch (e) {
+ const errorMessage = e instanceof Error ? e.message : String(e);
+ return {
+ success: false,
+ error: `Failed to read files: ${errorMessage}`,
+ };
+ }
+ },
+ });
+
+ return {
+ terminal: terminalTool,
+ createOrUpdateFiles: createOrUpdateFilesTool,
+ readFiles: readFilesTool,
+ };
+}
File: src/ai-sdk/types.ts
Changes:
@@ -0,0 +1,28 @@
+import type { CoreMessage } from "ai";
+
+export type Framework = "nextjs" | "angular" | "react" | "vue" | "svelte";
+
+export interface AgentState {
+ summary: string;
+ files: Record<string, string>;
+ selectedFramework?: Framework;
+ summaryRetryCount: number;
+}
+
+export interface AgentContext {
+ sandboxId: string;
+ state: AgentState;
+ messages: CoreMessage[];
+}
+
+export interface AgentResult {
+ state: AgentState;
+ messages: CoreMessage[];
+ output: string;
+}
+
+export interface ToolResult {
+ success: boolean;
+ data?: unknown;
+ error?: string;
+}
File: src/app/api/agent/token/route.ts
Changes:
@@ -11,8 +11,8 @@ export async function POST() {
);
}
- // Realtime token generation is not available without @inngest/realtime middleware
- // TODO: Install @inngest/realtime if needed
+ // Realtime token generation is handled via AI SDK streaming
+ // This endpoint is a placeholder for future realtime features
return Response.json(
{ error: "Realtime feature not configured" },
{ status: 503 }
File: src/inngest/functions.ts
Changes:
@@ -1,21 +1,13 @@
-import { z } from "zod";
import { Sandbox } from "@e2b/code-interpreter";
-import {
- openai,
- gemini,
- createAgent,
- createTool,
- createNetwork,
- type Tool,
- type Message,
- createState,
- type NetworkRun,
-} from "@inngest/agent-kit";
+import { generateText, type CoreMessage } from "ai";
import { ConvexHttpClient } from "convex/browser";
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import { inspect } from "util";
+import { gateway } from "@/ai-sdk/gateway";
+import { createCodeAgentTools as createAISDKTools } from "@/ai-sdk/tools";
+import type { AgentState } from "@/ai-sdk/types";
import { crawlUrl, type CrawledContent } from "@/lib/firecrawl";
// Get Convex client lazily to avoid build-time errors
@@ -49,11 +41,9 @@ import {
} from "@/prompt";
import { inngest } from "./client";
-import { SANDBOX_TIMEOUT, type Framework, type AgentState } from "./types";
+import { SANDBOX_TIMEOUT, type Framework } from "./types";
import {
getSandbox,
- lastAssistantTextMessageContent,
- parseAgentOutput,
createSandboxWithRetry,
validateSandboxHealth,
} from "./utils";
@@ -213,67 +203,6 @@ export function selectModelForTask(
return chosenModel;
}
-/**
- * Returns the appropriate AI adapter based on model provider
- */
-function getModelAdapter(
- modelId: keyof typeof MODEL_CONFIGS | string,
- temperature?: number,
-) {
- const config =
- modelId in MODEL_CONFIGS
- ? MODEL_CONFIGS[modelId as keyof typeof MODEL_CONFIGS]
- : null;
-
- const commonConfig = {
- model: modelId,
- apiKey: process.env.AI_GATEWAY_API_KEY!,
- baseUrl:
- process.env.AI_GATEWAY_BASE_URL || "https://ai-gateway.vercel.sh/v1",
- defaultParameters: {
- temperature: temperature ?? config?.temperature ?? 0.7,
- },
- };
-
- // Use native Gemini adapter for Google models (detect by model ID or provider)
- const isGoogleModel =
- config?.provider === "google" ||
- modelId.startsWith("google/") ||
- modelId.includes("gemini");
-
- if (isGoogleModel) {
- return gemini(commonConfig);
- }
-
- // Use OpenAI adapter for all other models (OpenAI, Anthropic, Moonshot, xAI, etc.)
- return openai(commonConfig);
-}
-
-/**
- * Converts screenshot URLs to AI-compatible image messages
- */
-async function createImageMessages(screenshots: string[]): Promise<Message[]> {
- const imageMessages: Message[] = [];
-
- for (const screenshotUrl of screenshots) {
- try {
- // For URL-based images (OpenAI and Gemini support this)
- imageMessages.push({
- type: "image",
- role: "user",
- content: screenshotUrl,
- });
- } catch (error) {
- console.error(
- `[ERROR] Failed to create image message for ${screenshotUrl}:`,
- error,
- );
- }
- }
-
- return imageMessages;
-}
-
const AUTO_FIX_ERROR_PATTERNS = [
/Error:/i,
/\[ERROR\]/i,
@@ -360,17 +289,23 @@ const extractSummaryText = (value: string): string => {
};
const getLastAssistantMessage = (
- networkRun: NetworkRun<AgentState>,
+ messages: CoreMessage[],
): string | undefined => {
- const results = networkRun.state.results;
-
- if (results.length === 0) {
- return undefined;
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const msg = messages[i];
+ if (msg.role === "assistant") {
+ if (typeof msg.content === "string") {
+ return msg.content;
+ }
+ if (Array.isArray(msg.content)) {
+ return msg.content
+ .filter((part) => part.type === "text")
+ .map((part) => (part as { type: "text"; text: string }).text)
+ .join("");
+ }
+ }
}
-
- const latestResult = results[results.length - 1];
-
- return lastAssistantTextMessageContent(latestResult);
+ return undefined;
};
const runLintCheck = async (sandboxId: string): Promise<string | null> => {
@@ -787,98 +722,7 @@ const validateMergeStrategy = (
};
};
-const createCodeAgentTools = (sandboxId: string) => [
- createTool({
- name: "terminal",
- description: "Use the terminal to run commands",
- parameters: z.object({
- command: z.string(),
- }),
- handler: async (
- { command }: { command: string },
- opts: Tool.Options<AgentState>,
- ) => {
- return await opts.step?.run("terminal", async () => {
- const buffers: { stdout: string; stderr: string } = {
- stdout: "",
- stderr: "",
- };
-
- try {
- const sandbox = await getSandbox(sandboxId);
- const result = await sandbox.commands.run(command, {
- onStdout: (data: string) => {
- buffers.stdout += data;
- },
- onStderr: (data: string) => {
- buffers.stderr += data;
- },
- });
- return result.stdout;
- } catch (e) {
- console.error(
- `Command failed: ${e} \nstdout: ${buffers.stdout}\nstderror: ${buffers.stderr}`,
- );
- return `Command failed: ${e} \nstdout: ${buffers.stdout}\nstderr: ${buffers.stderr}`;
- }
- });
- },
- }),
- createTool({
- name: "createOrUpdateFiles",
- description: "Create or update files in the sandbox",
- parameters: z.object({
- files: z.array(
- z.object({
- path: z.string(),
- content: z.string(),
- }),
- ),
- }),
- handler: async ({ files }, { step, network }: Tool.Options<AgentState>) => {
- const newFiles = await step?.run("createOrUpdateFiles", async () => {
- try {
- const updatedFiles = network.state.data.files || {};
- const sandbox = await getSandbox(sandboxId);
- for (const file of files) {
- await sandbox.files.write(file.path, file.content);
- updatedFiles[file.path] = file.content;
- }
-
- return updatedFiles;
- } catch (e) {
- return "Error: " + e;
- }
- });
-
- if (typeof newFiles === "object") {
- network.state.data.files = newFiles;
- }
- },
- }),
- createTool({
- name: "readFiles",
- description: "Read files from the sandbox",
- parameters: z.object({
- files: z.array(z.string()),
- }),
- handler: async ({ files }, { step }) => {
- return await step?.run("readFiles", async () => {
- try {
- const sandbox = await getSandbox(sandboxId);
- const contents = [];
- for (const file of files) {
- const content = await sandbox.files.read(file);
- contents.push({ path: file, content });
- }
- return JSON.stringify(contents);
- } catch (e) {
- return "Error: " + e;
- }
- });
- },
- }),
-];
+// Tools are now created using @ai-sdk tools via createAISDKTools from "@/ai-sdk/tools"
export const codeAgentFunction = inngest.createFunction(
{ id: "code-agent" },
@@ -913,36 +757,22 @@ export const codeAgentFunction = inngest.createFunction(
if (!project?.framework) {
console.log("[DEBUG] No framework set, running framework selector...");
- const frameworkSelectorAgent = createAgent({
- name: "framework-selector",
- description: "Determines the best framework for the user's request",
+ const frameworkResult = await generateText({
+ model: gateway("google/gemini-2.5-flash-lite"),
system: FRAMEWORK_SELECTOR_PROMPT,
- model: getModelAdapter("google/gemini-2.5-flash-lite", 0.3),
+ messages: [{ role: "user", content: event.data.value }],
+ temperature: 0.3,
});
- const frameworkResult = await frameworkSelectorAgent.run(
- event.data.value,
- );
- const frameworkOutput = frameworkResult.output[0];
+ const detectedFramework = frameworkResult.text.trim().toLowerCase();
+ console.log("[DEBUG] Framework selector output:", detectedFramework);
- if (frameworkOutput.type === "text") {
- const detectedFramework = (
- typeof frameworkOutput.content === "string"
- ? frameworkOutput.content
- : frameworkOutput.content.map((c) => c.text).join("")
+ if (
+ ["nextjs", "angular", "react", "vue", "svelte"].includes(
+ detectedFramework,
)
- .trim()
- .toLowerCase();
-
- console.log("[DEBUG] Framework selector output:", detectedFramework);
-
- if (
- ["nextjs", "angular", "react", "vue", "svelte"].includes(
- detectedFramework,
- )
- ) {
- selectedFramework = detectedFramework as Framework;
- }
+ ) {
+ selectedFramework = detectedFramework as Framework;
}
console.log("[DEBUG] Selected framework:", selectedFramework);
@@ -1176,7 +1006,7 @@ export const codeAgentFunction = inngest.createFunction(
"[DEBUG] Fetching previous messages for project:",
event.data.projectId,
);
- const formattedMessages: Message[] = [];
+ const formattedMessages: CoreMessage[] = [];
try {
const allMessages = await convex.query(api.messages.listForUser, {
@@ -1191,7 +1021,6 @@ export const codeAgentFunction = inngest.createFunction(
for (const message of messages) {
formattedMessages.push({
- type: "text",
role: message.role === "ASSISTANT" ? "assistant" : "user",
content: message.content,
});
@@ -1278,27 +1107,23 @@ export const codeAgentFunction = inngest.createFunction(
}
});
- const contextMessages: Message[] = (crawledContexts ?? []).map(
+ const contextMessages: CoreMessage[] = (crawledContexts ?? []).map(
(context) => ({
- type: "text",
- role: "user",
+ role: "user" as const,
content: `Crawled context from ${context.url}:\n${context.content}`,
}),
);
- const initialMessages = [...contextMessages, ...previousMessages];
+ const initialMessages: CoreMessage[] = [...contextMessages, ...previousMessages];
- const state = createState<AgentState>(
- {
- summary: "",
- files: {},
- selectedFramework,
- summaryRetryCount: 0,
- },
- {
- messages: initialMessages,
- },
- );
+ // Agent state for tracking files and summary
+ const agentState: AgentState = {
+ summary: "",
+ files: {},
+ selectedFramework,
+ summaryRetryCount: 0,
+ };
+ const stateRef = { current: agentState };
// Check if this message has an approved spec
const currentMessage = await step.run("get-current-message", async () => {
@@ -1355,101 +1180,116 @@ Generate code that matches the approved specification.`;
}
);
- const codeAgent = createAgent<AgentState>({
- name: `${selectedFramework}-code-agent`,
- description: `An expert ${selectedFramework} coding agent powered by ${modelConfig.name}`,
- system: frameworkPrompt,
- model: getModelAdapter(selectedModel, modelConfig.temperature),
- tools: createCodeAgentTools(sandboxId),
- lifecycle: {
- onResponse: async ({ result, network }) => {
- const lastAssistantMessageText =
- lastAssistantTextMessageContent(result);
-
- if (lastAssistantMessageText && network) {
- const containsSummaryTag =
- lastAssistantMessageText.includes("<task_summary>");
- console.log(
- `[DEBUG] Agent response received (contains summary tag: ${containsSummaryTag})`,
- );
- if (containsSummaryTag) {
- network.state.data.summary = extractSummaryText(
- lastAssistantMessageText,
- );
- network.state.data.summaryRetryCount = 0;
- }
- }
+ // Create AI SDK tools for the sandbox
+ const tools = createAISDKTools(sandboxId, stateRef);
+ const MAX_ITERATIONS = 8;
- return result;
- },
- },
- });
+ // Build conversation messages
+ let messages: CoreMessage[] = [
+ ...initialMessages,
+ { role: "user", content: event.data.value },
+ ];
- const network = createNetwork<AgentState>({
- name: "coding-agent-network",
- agents: [codeAgent],
- maxIter: 8,
- defaultState: state,
- router: async ({ network }) => {
- const summaryText = extractSummaryText(
- network.state.data.summary ?? "",
- );
- const fileEntries = network.state.data.files ?? {};
- const fileCount = Object.keys(fileEntries).length;
+ console.log("[DEBUG] Running AI SDK agent with input:", event.data.value);
+
+ // Main agent loop using AI SDK
+ let iteration = 0;
+ let lastOutput = "";
+
+ while (iteration < MAX_ITERATIONS) {
+ iteration++;
+ console.log(`[AI-SDK] Running iteration ${iteration}/${MAX_ITERATIONS}`);
- if (summaryText.length > 0) {
- return;
- }
+ try {
+ const result = await generateText({
+ model: gateway(selectedModel),
+ system: frameworkPrompt,
+ messages,
+ tools,
+ maxSteps: 10,
+ temperature: modelConfig.temperature,
+ onStepFinish: async ({ text }) => {
+ if (text) {
+ lastOutput = text;
+ const summary = extractSummaryText(text);
+ if (summary) {
+ stateRef.current.summary = summary;
+ stateRef.current.summaryRetryCount = 0;
+ }
+ }
+ },
+ });
+
+ if (result.text) {
+ lastOutput = result.text;
+ messages.push({ role: "assistant", content: result.text });
- if (fileCount === 0) {
- network.state.data.summaryRetryCount = 0;
- return codeAgent;
+ const summary = extractSummaryText(result.text);
+ if (summary) {
+ stateRef.current.summary = summary;
+ console.log(`[AI-SDK] Summary extracted (length: ${summary.length})`);
+ break;
+ }
}
- const currentRetry = network.state.data.summaryRetryCount ?? 0;
- if (currentRetry >= 2) {
- console.warn(
- "[WARN] Missing <task_summary> after multiple attempts despite generated files; proceeding with fallback handling.",
- );
- return;
+ const hasFiles = Object.keys(stateRef.current.files).length > 0;
+ const hasSummary = stateRef.current.summary.length > 0;
+
+ if (hasSummary) {
+ console.log("[AI-SDK] Task complete with summary");
+ break;
}
- const nextRetry = currentRetry + 1;
- network.state.data.summaryRetryCount = nextRetry;
- console.log(
- `[DEBUG] No <task_summary> yet; retrying agent to request summary (attempt ${nextRetry}).`,
- );
-
- return codeAgent;
- },
- });
+ if (hasFiles && !hasSummary) {
+ stateRef.current.summaryRetryCount++;
+ if (stateRef.current.summaryRetryCount >= 2) {
+ console.warn("[AI-SDK] Missing summary after multiple attempts, proceeding");
+ break;
+ }
- console.log("[DEBUG] Running network with input:", event.data.value);
- let result = await network.run(event.data.value, { state });
+ messages.push({
+ role: "user",
+ content: "IMPORTANT: You have generated files but forgot to provide the <task_summary> tag. Please provide it now with a brief description of what you built.",
+ });
+ }
+ } catch (error) {
+ console.error(`[AI-SDK] Error in iteration ${iteration}:`, error);
+ throw error;
+ }
+ }
// Post-network fallback: If no summary but files exist, make one more explicit request
- let summaryText = extractSummaryText(result.state.data.summary ?? "");
- const hasGeneratedFiles = Object.keys(result.state.data.files || {}).length > 0;
+ let summaryText = extractSummaryText(stateRef.current.summary ?? "");
+ const hasGeneratedFiles = Object.keys(stateRef.current.files || {}).length > 0;
if (!summaryText && hasGeneratedFiles) {
- console.log("[DEBUG] No summary detected after network run, requesting explicitly...");
- result = await network.run(
- "IMPORTANT: You have successfully generated files, but you forgot to provide the <task_summary> tag. Please provide it now with a brief description of what you built. This is required to complete the task.",
- { state: result.state }
- );
+ console.log("[DEBUG] No summary detected after agent run, requesting explicitly...");
- // Re-extract summary after explicit request
- summaryText = extractSummaryText(result.state.data.summary ?? "");
+ messages.push({
+ role: "user",
+ content: "IMPORTANT: You have successfully generated files, but you forgot to provide the <task_summary> tag. Please provide it now with a brief description of what you built. This is required to complete the task.",
+ });
+
+ const summaryResult = await generateText({
+ model: gateway(selectedModel),
+ system: frameworkPrompt,
+ messages,
+ temperature: modelConfig.temperature,
+ });
- if (summaryText) {
- console.log("[DEBUG] Summary successfully extracted after explicit request");
- } else {
- console.warn("[WARN] Summary still missing after explicit request, will use fallback");
+ if (summaryResult.text) {
+ summaryText = extractSummaryText(summaryResult.text);
+ if (summaryText) {
+ stateRef.current.summary = summaryText;
+ console.log("[DEBUG] Summary successfully extracted after explicit request");
+ } else {
+ console.warn("[WARN] Summary still missing after explicit request, will use fallback");
+ }
}
}
// Post-execution validation: Check if expected entry point file was modified
- const generatedFiles = result.state.data.files || {};
+ const generatedFiles = stateRef.current.files || {};
const fileKeys = Object.keys(generatedFiles);
// Define expected entry points by framework
@@ -1509,10 +1349,10 @@ Generate code that matches the approved specification.`;
]);
let autoFixAttempts = 0;
- let lastAssistantMessage = getLastAssistantMessage(result);
+ let lastAssistantMessage = getLastAssistantMessage(messages);
if (selectedFramework === "nextjs") {
- const currentFiles = (result.state.data.files || {}) as Record<
+ const currentFiles = (stateRef.current.files || {}) as Record<
string,
string
>;
@@ -1564,8 +1404,7 @@ Generate code that matches the approved specification.`;
`\n[DEBUG] Auto-fix triggered (attempt ${autoFixAttempts}). Errors detected.\n${errorDetails}\n`,
);
- result = await network.run(
- `CRITICAL ERROR DETECTED - IMMEDIATE FIX REQUIRED
+ const autoFixPrompt = `CRITICAL ERROR DETECTED - IMMEDIATE FIX REQUIRED
The previous attempt encountered an error that must be corrected before proceeding.
@@ -1587,11 +1426,32 @@ REQUIRED ACTIONS:
6. Rerun any commands that failed and verify they now succeed
7. Provide an updated <task_summary> only after the error is fully resolved
-DO NOT proceed until the error is completely fixed. The fix must be thorough and address the root cause, not just mask the symptoms.`,
- { state: result.state },
- );
+DO NOT proceed until the error is completely fixed. The fix must be thorough and address the root cause, not just mask the symptoms.`;
+
+ messages.push({ role: "user", content: autoFixPrompt });
- lastAssistantMessage = getLastAssistantMessage(result);
+ const fixResult = await generateText({
+ model: gateway(selectedModel),
+ system: frameworkPrompt,
+ messages,
+ tools,
+ maxSteps: 10,
+ temperature: modelConfig.temperature,
+ onStepFinish: async ({ text }) => {
+ if (text) {
+ const summary = extractSummaryText(text);
+ if (summary) {
+ stateRef.current.summary = summary;
+ }
+ }
+ },
+ });
+
+ if (fixResult.text) {
+ messages.push({ role: "assistant", content: fixResult.text });
+ }
+
+ lastAssistantMessage = getLastAssistantMessage(messages);
// Re-run validation checks to verify if errors are actually fixed
console.log(
@@ -1629,15 +1489,15 @@ DO NOT proceed until the error is completely fixed. The fix must be thorough and
}
}
- lastAssistantMessage = getLastAssistantMessage(result);
+ lastAssistantMessage = getLastAssistantMessage(messages);
- const files = (result.state.data.files || {}) as Record<string, string>;
+ const files = (stateRef.current.files || {}) as Record<string, string>;
const filePaths = Object.keys(files);
const hasFiles = filePaths.length > 0;
summaryText = extractSummaryText(
- typeof result.state.data.summary === "string"
- ? result.state.data.summary
+ typeof stateRef.current.summary === "string"
+ ? stateRef.current.summary
: "",
);
const agentProvidedSummary = summaryText.length > 0;
@@ -1652,7 +1512,7 @@ DO NOT proceed until the error is completely fixed. The fix must be thorough and
);
}
- result.state.data.summary = summaryText;
+ stateRef.current.summary = summaryText;
const hasSummary = summaryText.length > 0;
@@ -1722,34 +1582,28 @@ DO NOT proceed until the error is completely fixed. The fix must be thorough and
return fallbackHost;
});
- let fragmentTitleOutput: Message[] | undefined;
- let responseOutput: Message[] | undefined;
+ let fragmentTitleOutput: string | undefined;
+ let responseOutput: string | undefined;
if (!isError && hasSummary && hasFiles) {
try {
- const titleModel = getModelAdapter("google/gemini-2.5-flash-lite", 0.3);
-
- const fragmentTitleGenerator = createAgent({
- name: "fragment-title-generator",
- description: "A fragment title generator",
- system: FRAGMENT_TITLE_PROMPT,
- model: titleModel,
- });
-
- const responseGenerator = createAgent({
- name: "response-generator",
- description: "A response generator",
- system: RESPONSE_PROMPT,
- model: titleModel,
- });
-
const [titleResult, responseResult] = await Promise.all([
- fragmentTitleGenerator.run(summaryText),
- responseGenerator.run(summaryText),
+ generateText({
+ model: gateway("google/gemini-2.5-flash-lite"),
+ system: FRAGMENT_TITLE_PROMPT,
+ messages: [{ role: "user", content: summaryText }],
+ temperature: 0.3,
+ }),
+ generateText({
+ model: gateway("google/gemini-2.5-flash-lite"),
+ system: RESPONSE_PROMPT,
+ messages: [{ role: "user", content: summaryText }],
+ temperature: 0.3,
+ }),
]);
- fragmentTitleOutput = titleResult.output;
- responseOutput = responseResult.output;
+ fragmentTitleOutput = titleResult.text;
+ responseOutput = responseResult.text;
} catch (gatewayError) {
console.error(
"[ERROR] Failed to generate fragment metadata:",
@@ -2008,10 +1862,7 @@ DO NOT proceed until the error is completely fixed. The fix must be thorough and
});
}
- const parsedResponse = parseAgentOutput(responseOutput);
- const parsedTitle = parseAgentOutput(fragmentTitleOutput);
-
- const sanitizedResponse = sanitizeTextForDatabase(parsedResponse ?? "");
+ const sanitizedResponse = sanitizeTextForDatabase(responseOutput ?? "");
const baseResponseContent =
sanitizedResponse.length > 0
? sanitizedResponse
@@ -2026,7 +1877,7 @@ DO NOT proceed until the error is completely fixed. The fix must be thorough and
`${baseResponseContent}${warningsNote}`,
);
- const sanitizedTitle = sanitizeTextForDatabase(parsedTitle ?? "");
+ const sanitizedTitle = sanitizeTextForDatabase(fragmentTitleOutput ?? "");
const fragmentTitle =
sanitizedTitle.length > 0 ? sanitizedTitle : "Generated Fragment";
@@ -2074,7 +1925,7 @@ DO NOT proceed until the error is completely fixed. The fix must be thorough and
url: sandboxUrl,
title: "Fragment",
files: finalFiles,
- summary: result.state.data.summary,
+ summary: stateRef.current.summary,
};
},
);
@@ -2286,20 +2137,16 @@ export const errorFixFunction = inngest.createFunction(
console.log("[DEBUG] Errors detected, running fix agent...");
- // Create a minimal state with existing files
- const state = createState<AgentState>(
- {
- summary:
- ((fragmentRecord.metadata as Record<string, unknown>)
- ?.summary as string) ?? "",
- files: fragmentFiles,
- selectedFramework: fragmentFramework,
- summaryRetryCount: 0,
- },
- {
- messages: [],
- },
- );
+ // Create agent state with existing files
+ const errorFixState: AgentState = {
+ summary:
+ ((fragmentRecord.metadata as Record<string, unknown>)
+ ?.summary as string) ?? "",
+ files: fragmentFiles,
+ selectedFramework: fragmentFramework,
+ summaryRetryCount: 0,
+ };
+ const errorFixStateRef = { current: errorFixState };
const frameworkPrompt = getFrameworkPrompt(fragmentFramework);
const errorFixModelConfig = MODEL_CONFIGS[fragmentModel];
@@ -2310,80 +2157,8 @@ export const errorFixFunction = inngest.createFunction(
errorFixModelConfig,
);
- const codeAgent = createAgent<AgentState>({
- name: `${fragmentFramework}-error-fix-agent`,
- description: `An expert ${fragmentFramework} coding agent for fixing errors powered by ${errorFixModelConfig.name}`,
- system: frameworkPrompt,
- model: openai({
- model: fragmentModel,
- apiKey: process.env.AI_GATEWAY_API_KEY!,
- baseUrl:
- process.env.AI_GATEWAY_BASE_URL || "https://ai-gateway.vercel.sh/v1",
- defaultParameters: {
- temperature: errorFixModelConfig.temperature,
- },
- }),
- tools: createCodeAgentTools(sandboxId),
- lifecycle: {
- onResponse: async ({ result, network }) => {
- const lastAssistantMessageText =
- lastAssistantTextMessageContent(result);
- if (lastAssistantMessageText && network) {
- const containsSummaryTag =
- lastAssistantMessageText.includes("<task_summary>");
- console.log(
- `[DEBUG] Error-fix agent response received (contains summary tag: ${containsSummaryTag})`,
- );
- if (containsSummaryTag) {
- network.state.data.summary = extractSummaryText(
- lastAssistantMessageText,
- );
- network.state.data.summaryRetryCount = 0;
- }
- }
- return result;
- },
- },
- });
-
- const network = createNetwork<AgentState>({
- name: "error-fix-network",
- agents: [codeAgent],
- maxIter: 10,
- defaultState: state,
- router: async ({ network }) => {
- const summaryText = extractSummaryText(
- network.state.data.summary ?? "",
- );
- const fileEntries = network.state.data.files ?? {};
- const fileCount = Object.keys(fileEntries).length;
-
- if (summaryText.length > 0) {
- return;
- }
-
- if (fileCount === 0) {
- network.state.data.summaryRetryCount = 0;
- return codeAgent;
- }
-
- const currentRetry = network.state.data.summaryRetryCount ?? 0;
- if (currentRetry >= 3) {
- console.warn(
- "[WARN] Error-fix agent missing <task_summary> after multiple retries; proceeding with collected fixes.",
- );
- return;
- }
-
- const nextRetry = currentRetry + 1;
- network.state.data.summaryRetryCount = nextRetry;
- console.log(
- `[DEBUG] Error-fix agent missing <task_summary>; retrying (attempt ${nextRetry}).`,
- );
-
- return codeAgent;
- },
- });
+ // Create AI SDK tools for the sandbox
+ const errorFixTools = createAISDKTools(sandboxId, errorFixStateRef);
const fixPrompt = `CRITICAL ERROR FIX REQUEST
@@ -2406,26 +2181,63 @@ REQUIRED ACTIONS:
DO NOT proceed until all errors are completely resolved. Focus on fixing the root cause, not just masking symptoms.`;
try {
- let result = await network.run(fixPrompt, { state });
+ // Run the error fix using AI SDK
+ const errorFixMessages: CoreMessage[] = [
+ { role: "user", content: fixPrompt },
+ ];
+
+ const fixResult = await generateText({
+ model: gateway(fragmentModel),
+ system: frameworkPrompt,
+ messages: errorFixMessages,
+ tools: errorFixTools,
+ maxSteps: 10,
+ temperature: errorFixModelConfig.temperature,
+ onStepFinish: async ({ text }) => {
+ if (text) {
+ const summary = extractSummaryText(text);
+ if (summary) {
+ errorFixStateRef.current.summary = summary;
+ }
+ }
+ },
+ });
- // Post-network fallback: If no summary but files were modified, make one more explicit request
- let summaryText = extractSummaryText(result.state.data.summary ?? "");
- const hasModifiedFiles = Object.keys(result.state.data.files || {}).length > 0;
+ if (fixResult.text) {
+ errorFixMessages.push({ role: "assistant", content: fixResult.text });
+ const summary = extractSummaryText(fixResult.text);
+ if (summary) {
+ errorFixStateRef.current.summary = summary;
+ }
+ }
+
+ // Post-fix fallback: If no summary but files were modified, make one more explicit request
+ let summaryText = extractSummaryText(errorFixStateRef.current.summary ?? "");
+ const hasModifiedFiles = Object.keys(errorFixStateRef.current.files || {}).length > 0;
if (!summaryText && hasModifiedFiles) {
console.log("[DEBUG] No summary detected after error-fix, requesting explicitly...");
- result = await network.run(
- "IMPORTANT: You have successfully fixed the errors, but you forgot to provide the <task_summary> tag. Please provide it now with a brief description of what errors you fixed. This is required to complete the task.",
- { state: result.state }
- );
- // Re-extract summary after explicit request
- summaryText = extractSummaryText(result.state.data.summary ?? "");
+ errorFixMessages.push({
+ role: "user",
+ content: "IMPORTANT: You have successfully fixed the errors, but you forgot to provide the <task_summary> tag. Please provide it now with a brief description of what errors you fixed. This is required to complete the task.",
+ });
+
+ const summaryResult = await generateText({
+ model: gateway(fragmentModel),
+ system: frameworkPrompt,
+ messages: errorFixMessages,
+ temperature: errorFixModelConfig.temperature,
+ });
- if (summaryText) {
- console.log("[DEBUG] Summary successfully extracted after explicit request");
- } else {
- console.warn("[WARN] Summary still missing after explicit request, will use fallback");
+ if (summaryResult.text) {
+ summaryText = extractSummaryText(summaryResult.text);
+ if (summaryText) {
+ errorFixStateRef.current.summary = summaryText;
+ console.log("[DEBUG] Summary successfully extracted after explicit request");
+ } else {
+ console.warn("[WARN] Summary still missing after explicit request, will use fallback");
+ }
}
}
@@ -2455,7 +2267,7 @@ DO NOT proceed until all errors are completely resolved. Focus on fixing the roo
// Ensure all fixed files are written back to the sandbox
await step.run("sync-fixed-files-to-sandbox", async () => {
- const fixedFiles = result.state.data.files || {};
+ const fixedFiles = errorFixStateRef.current.files || {};
const sandbox = await getSandbox(sandboxId);
console.log(
@@ -2520,7 +2332,7 @@ DO NOT proceed until all errors are completely resolved. Focus on fixing the roo
previousFiles: originalFiles,
fixedAt: new Date().toISOString(),
lastFixSuccess: {
- summary: result.state.data.summary,
+ summary: errorFixStateRef.current.summary,
occurredAt: new Date().toISOString(),
},
}
@@ -2532,7 +2344,7 @@ DO NOT proceed until all errors are completely resolved. Focus on fixing the roo
sandboxId: fragment.sandboxId || undefined,
sandboxUrl: fragment.sandboxUrl,
title: fragment.title,
- files: result.state.data.files,
+ files: errorFixStateRef.current.files,
framework: frameworkToConvexEnum(fragmentFramework),
metadata: metadataUpdate || fragment.metadata,
});
@@ -2545,7 +2357,7 @@ DO NOT proceed until all errors are completely resolved. Focus on fixing the roo
message: remainingErrors
? "Some errors may remain. Please check the sandbox."
: "Errors fixed successfully",
- summary: result.state.data.summary,
+ summary: errorFixStateRef.current.summary,
remainingErrors: remainingErrors || undefined,
};
} catch (error) {
@@ -2630,32 +2442,16 @@ DO NOT proceed until all errors are completely resolved. Focus on fixing the roo
},
);
-// Helper function to extract spec content from agent response
-const extractSpecContent = (output: Message[]): string => {
- const textContent = output
- .filter((msg) => msg.type === "text")
- .map((msg) => {
- if (typeof msg.content === "string") {
- return msg.content;
- }
- if (Array.isArray(msg.content)) {
- return msg.content
- .filter((c) => c.type === "text")
- .map((c) => c.text)
- .join("\n");
- }
- return "";
- })
- .join("\n");
-
+// Helper function to extract spec content from text response
+const extractSpecContent = (text: string): string => {
// Extract content between <spec>...</spec> tags
- const specMatch = /<spec>([\s\S]*?)<\/spec>/i.exec(textContent);
- if (specMatch && specMatch[1]) {
+ const specMatch = /<spec>([\s\S]*?)<\/spec>/i.exec(text);
+ if (specMatch?.[1]) {
return specMatch[1].trim();
}
// If no tags found, return the entire response
- return textContent.trim();
+ return text.trim();
};
// Spec Planning Agent Function
@@ -2696,34 +2492,21 @@ export const specPlanningAgentFunction = inngest.createFunction(
if (!project?.framework) {
console.log("[DEBUG] No framework set, running framework selector...");
- const frameworkSelectorAgent = createAgent({
- name: "framework-selector",
- description: "Determines the best framework for the user's request",
+ const frameworkResult = await generateText({
+ model: gateway("google/gemini-2.5-flash-lite"),
system: FRAMEWORK_SELECTOR_PROMPT,
- model: getModelAdapter("google/gemini-2.5-flash-lite", 0.3),
+ messages: [{ role: "user", content: event.data.value }],
+ temperature: 0.3,
});
- const frameworkResult = await frameworkSelectorAgent.run(
- event.data.value,
- );
- const frameworkOutput = frameworkResult.output[0];
+ const detectedFramework = frameworkResult.text.trim().toLowerCase();
- if (frameworkOutput.type === "text") {
- const detectedFramework = (
- typeof frameworkOutput.content === "string"
- ? frameworkOutput.content
- : frameworkOutput.content.map((c) => c.text).join("")
+ if (
+ ["nextjs", "angular", "react", "vue", "svelte"].includes(
+ detectedFramework,
)
- .trim()
- .toLowerCase();
-
- if (
- ["nextjs", "angular", "react", "vue", "svelte"].includes(
- detectedFramework,
- )
- ) {
- selectedFramework = detectedFramework as Framework;
- }
+ ) {
+ selectedFramework = detectedFramework as Framework;
}
// Update project with selected framework
@@ -2751,14 +2534,6 @@ ${frameworkPrompt}
Remember to wrap your complete specification in <spec>...</spec> tags.`;
- // Create planning agent with GPT-5.1 Codex
- const planningAgent = createAgent({
- name: "spec-planning-agent",
- description: "Creates detailed implementation specifications",
- system: enhancedSpecPrompt,
- model: getModelAdapter("openai/gpt-5.1-codex", 0.7),
- });
-
console.log("[DEBUG] Running planning agent with user request");
// Get previous messages for context
@@ -2774,40 +2549,32 @@ Remember to wrap your complete specification in <spec>...</spec> tags.`;
// Take last 3 messages for context (excluding current one)
const messages = allMessages.slice(-4, -1);
- const formattedMessages: Message[] = messages.map((msg) => ({
- type: "text",
+ const formattedMessages: CoreMessage[] = messages.map((msg) => ({
role: msg.role === "ASSISTANT" ? "assistant" : "user",
content: msg.content,
}));
return formattedMessages;
} catch (error) {
console.error("[ERROR] Failed to fetch previous messages:", error);
- return [];
+ return [] as CoreMessage[];
}
},
);
- // Run the planning agent
+ // Run the planning agent using AI SDK
const result = await step.run("generate-spec", async () => {
- const state = createState<AgentState>(
- {
- summary: "",
- files: {},
- selectedFramework,
- summaryRetryCount: 0,
- },
- {
- messages: previousMessages,
- },
- );
-
- const planResult = await planningAgent.run(event.data.value, { state });
+ const planResult = await generateText({
+ model: gateway("openai/gpt-5.1-codex"),
+ system: enhancedSpecPrompt,
+ messages: [...previousMessages, { role: "user", content: event.data.value }],
+ temperature: 0.7,
+ });
return planResult;
});
// Extract spec content from response
- const specContent = extractSpecContent(result.output);
+ const specContent = extractSpecContent(result.text);
console.log("[DEBUG] Spec generated, length:", specContent.length);
File: src/inngest/types.ts
Changes:
@@ -2,14 +2,10 @@ export const SANDBOX_TIMEOUT = 30 * 60 * 1000; // 30 minutes in MS (reduced from
export type Framework = 'nextjs' | 'angular' | 'react' | 'vue' | 'svelte';
-export interface AgentState {
- summary: string;
- files: Record<string, string>;
- selectedFramework?: Framework;
- summaryRetryCount: number;
-}
-
export interface ClientState {
projectId: string;
userId?: string;
}
+
+// Re-export AgentState from ai-sdk for backward compatibility
+export type { AgentState } from "@/ai-sdk/types";
File: src/inngest/utils.ts
Changes:
@@ -1,5 +1,4 @@
import { Sandbox } from "@e2b/code-interpreter";
-import { AgentResult, Message, TextMessage } from "@inngest/agent-kit";
import { SANDBOX_TIMEOUT } from "./types";
@@ -214,37 +213,3 @@ export async function readFilesFromSandbox(
return [];
}
}
-
-export function lastAssistantTextMessageContent(result: AgentResult) {
- const lastAssistantTextMessageIndex = result.output.findLastIndex(
- (message) => message.role === "assistant",
- );
-
- const message = result.output[lastAssistantTextMessageIndex] as
- | TextMessage
- | undefined;
-
- return message?.content
- ? typeof message.content === "string"
- ? message.content
- : message.content.map((c) => c.text).join("")
- : undefined;
-}
-
-export const parseAgentOutput = (value?: Message[]) => {
- if (!value || value.length === 0) {
- return "Fragment";
- }
-
- const output = value[0];
-
- if (output.type !== "text") {
- return "Fragment";
- }
-
- if (Array.isArray(output.content)) {
- return output.content.map((txt) => (typeof txt === "string" ? txt : txt.text ?? "")).join("")
- } else {
- return output.content
- }
-};
File: tests/mocks/ai-sdk-gateway.ts
Changes:
@@ -0,0 +1,6 @@
+export const createGateway = jest.fn(() => {
+ return jest.fn((modelId: string) => ({
+ modelId,
+ provider: "mock-gateway",
+ }));
+});
File: tests/mocks/ai.ts
Changes:
@@ -0,0 +1,17 @@
+export const generateText = jest.fn(async () => ({
+ text: "Mock AI response",
+ toolCalls: [],
+ toolResults: [],
+ finishReason: "stop",
+ usage: { promptTokens: 0, completionTokens: 0 },
+}));
+
+export const tool = jest.fn((config) => ({
+ ...config,
+ execute: config.execute,
+}));
+
+export type CoreMessage = {
+ role: "user" | "assistant" | "system";
+ content: string | Array<{ type: string; text?: string; image?: string }>;
+};
File: tests/mocks/inngest-agent-kit.ts
Changes:
@@ -1,18 +0,0 @@
-export const openai = () => ({
- model: '',
- apiKey: '',
- baseUrl: '',
- defaultParameters: {},
-});
-
-export const createAgent = () => ({
- run: async () => ({ output: [] }),
-});
-
-export const createTool = () => ({ handler: async () => ({}) });
-export const createNetwork = () => ({ run: async () => ({ output: [] }) });
-export const createState = () => ({ get: () => ({}), set: () => undefined });
-
-export type Tool = Record<string, unknown>;
-export type Message = { type: string; content: unknown };
-export type NetworkRun = Record<string, unknown>;
|
WalkthroughMigrates the codebase from Inngest's Changes
Sequence DiagramsequenceDiagram
participant Inngest as Inngest Engine
participant AgentFn as Agent Function
participant AI as AI SDK (Gateway)
participant Tools as Agent Tools
participant Sandbox as Sandbox API
participant State as stateRef
Inngest->>AgentFn: Trigger codeAgentFunction
AgentFn->>State: Initialize stateRef with AgentState
AgentFn->>Tools: createCodeAgentTools(sandboxId, stateRef)
Tools->>Tools: Setup terminal, createOrUpdateFiles, readFiles
loop Agent Loop
AgentFn->>AI: generateText with context + tools
AI-->>AgentFn: Response (text/toolCalls)
alt Tool Call Needed
AgentFn->>Tools: Execute tool (terminal/file operation)
Tools->>Sandbox: Send request (exec/read/write)
Sandbox-->>Tools: Return output
Tools->>State: Update stateRef.current
Tools-->>AgentFn: Return ToolResult
else No Tool Call
AgentFn->>State: Extract summary from AI response
AgentFn->>AgentFn: Update messages (CoreMessage[])
end
end
AgentFn->>Sandbox: Finalize & sync files from stateRef.current.files
AgentFn-->>Inngest: Return AgentResult with final state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
🚀 Scrapybara Ubuntu instance started! |
|
🔧 Setting up test environment... Agent Steps |
|
❌ Something went wrong: |
PR Review: Migrate to AI SDK GatewayI've completed a thorough review of this migration from Inngest Agent Kit to Vercel's AI SDK. Here's my feedback: ✅ Strengths1. Clean Architecture
2. Tool Implementation QualityThe new AI SDK tools in `src/ai-sdk/tools.ts` are well-structured:
3. Test Mocking Updated
🔴 Critical Issues1. Sandbox Connection Inefficiency (src/ai-sdk/tools.ts)Problem: Each tool execution creates a new sandbox connection via `Sandbox.connect()`. With multiple tool calls per agent iteration, this creates unnecessary overhead. Recommendation: Implement connection caching: async function getSandboxFromId(sandboxId: string): Promise { 2. Missing Error Context in File Read (src/ai-sdk/tools.ts:110-115)Problem: Swallowed exceptions provide no debugging information: ```typescript Fix: 3. State Mutation Without Validation (src/ai-sdk/tools.ts:75)Problem: Direct mutation of `stateRef.current.files` without checks. Recommendation: Add validation:
|
There was a problem hiding this comment.
💡 Codex Review
https://github.com/otdoges/zapdev/blob/e9a605062d5543f3c4d0542c6878686aac800d83/src/inngest/functions.ts#L1756
Undefined agent result prevents fragment save
After migrating from Inngest agent-kit, the merge phase still reads result.state.data.files, but result is never defined in this function. When the code agent runs, this line will throw a ReferenceError before any files can be merged or saved, so the function will fail every time the merge step is reached regardless of inputs. The agent likely intended to use the AI SDK state (e.g., stateRef.current.files) instead.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Greptile OverviewGreptile SummaryReplaces Inngest Agent Kit with Vercel AI SDK for agent orchestration, moving from a network-based agent system to manual iteration loops with Key Changes:
Impact:
Confidence Score: 4/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant User
participant Inngest as Inngest Function
participant Gateway as AI SDK Gateway
participant AI as AI Model (via Gateway)
participant Tools as AI SDK Tools
participant Sandbox as E2B Sandbox
User->>Inngest: code-agent/run event
Inngest->>Inngest: Framework detection with generateText()
Inngest->>Gateway: gateway(model)
Gateway->>AI: HTTP request with auth
AI-->>Gateway: Framework result
Gateway-->>Inngest: Framework selected
Inngest->>Inngest: Create agent state & tools
Inngest->>Inngest: Build messages array
loop Agent Iteration (max 8)
Inngest->>Gateway: generateText(model, messages, tools)
Gateway->>AI: Send messages + tool definitions
alt Tool Call Requested
AI-->>Gateway: Tool call response
Gateway-->>Inngest: Tool execution needed
Inngest->>Tools: Execute tool (terminal/files)
Tools->>Sandbox: Connect & execute
Sandbox-->>Tools: Result
Tools-->>Inngest: ToolResult
Inngest->>Inngest: Update state & messages
else Text Response
AI-->>Gateway: Text completion
Gateway-->>Inngest: result.text
Inngest->>Inngest: Extract summary if present
end
alt Summary Found
Inngest->>Inngest: Break loop
else No Summary But Has Files
Inngest->>Inngest: Retry prompt for summary
end
end
Inngest->>Sandbox: Run lint & build checks
Sandbox-->>Inngest: Validation results
alt Errors Detected
loop Auto-fix (max 2)
Inngest->>Gateway: generateText with error context
Gateway->>AI: Fix request
AI-->>Gateway: Fixed code
Gateway-->>Inngest: Fix result
Inngest->>Tools: Update files
Tools->>Sandbox: Write fixed files
Inngest->>Sandbox: Re-validate
end
end
Inngest->>Inngest: Save fragments to Convex
Inngest-->>User: Completion
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/inngest/functions.ts (1)
1756-1781: Fix staleresult.state.data.filesreference incodeAgentFunctionHere:
const agentFiles = result.state.data.files || {};
resultis not in scope at this point incodeAgentFunction(it only exists as aconstinside the earlierwhileloop), and the AI SDKgenerateTextresult no longer exposes astate.data.filestree anyway. This will fail type-checking and at runtime.You already have the agent’s file map tracked via
stateRef.current.filesand aliased asgeneratedFilesearlier in the function, soagentFilesshould be derived from that instead. A minimal fix:- const agentFiles = result.state.data.files || {}; + const agentFiles = generatedFiles;This keeps the merge/validation logic unchanged while correctly using the new state source.
🧹 Nitpick comments (7)
src/app/api/agent/token/route.ts (1)
14-25: Documented placeholder is clear; consider Sentry for error trackingThe new comments correctly explain that realtime is now handled via AI SDK streaming and this endpoint is a stub. To align with the Sentry error-handling guideline, you could also capture exceptions in the catch block:
-import { getUser } from "@/lib/auth-server"; +import { getUser } from "@/lib/auth-server"; +import * as Sentry from "@sentry/nextjs"; export async function POST() { try { const user = await getUser(); // ... } catch (error) { - console.error("[ERROR] Failed to generate realtime token:", error); + Sentry.captureException(error); + console.error("[ERROR] Failed to generate realtime token:", error); return Response.json( { error: "Failed to generate token" }, { status: 500 } ); } }As per coding guidelines, this keeps production errors visible without changing the current API surface.
src/inngest/types.ts (1)
10-11: AgentState re-export keeps inngest types backward compatibleRe-exporting
AgentStatefrom@/ai-sdk/typesis a clean way to keep existingsrc/inngestconsumers compiling after the migration.One small follow-up you may consider later: if
Frameworkis also defined in@/ai-sdk/types, re-exporting it here instead of maintaining a parallel union would avoid type drift between the two modules.src/ai-sdk/gateway.ts (1)
1-8: Avoid sendingBearer undefinedwhen AI_GATEWAY_API_KEY is missingIf
AI_GATEWAY_API_KEYis not set, this code will still send anAuthorization: Bearer undefinedheader, which obscures misconfiguration and produces less helpful failures.Consider guarding the header on presence of the key (and optionally logging/throwing early):
export const gateway = createGateway({ baseURL: process.env.AI_GATEWAY_BASE_URL || "https://gateway.ai.vercel.sh/v1", - headers: { - Authorization: `Bearer ${process.env.AI_GATEWAY_API_KEY}`, - }, + headers: process.env.AI_GATEWAY_API_KEY + ? { Authorization: `Bearer ${process.env.AI_GATEWAY_API_KEY}` } + : {}, });This aligns with the env-variable expectations in
AGENTS.mdwhile failing fast in a clearer way when configuration is incomplete.src/ai-sdk/tools.ts (1)
52-92: Validate and sanitize file paths before sandbox read/write
createOrUpdateFilesToolandreadFilesToolaccept arbitrary paths and pass them directly tosandbox.files.write/sandbox.files.read. Elsewhere (e.g.,isValidFilePathandreadFileWithTimeoutinsrc/inngest/functions.ts), you already enforce strict path validation to prevent traversal and keep everything under known workspace roots.To keep behavior consistent and align with the repo’s “sanitize file paths to prevent directory traversal attacks” guideline, consider:
- Reusing a shared
isValidFilePath-style helper (moved to a small shared module) in these tools, and- Skipping or explicitly erroring on invalid paths rather than blindly attempting the operation.
This keeps the agent from accidentally touching unexpected locations even inside the sandbox and makes file handling semantics uniform across the codebase.
Also applies to: 94-129
src/ai-sdk/types.ts (1)
3-28: Avoid duplicating theFrameworkunion typeThis file defines
Frameworkwhilesrc/inngest/types.tsalready exports an identical union. Keeping two independent definitions risks subtle drift if a new framework is added or one side is updated.Consider centralizing
Frameworkin a small shared types module (e.g.,src/shared/framework-types.ts) and re-exporting it from bothsrc/ai-sdk/types.tsandsrc/inngest/types.tsso there’s a single source of truth.src/inngest/functions.ts (2)
291-309: MakegetLastAssistantMessageresilient to non-text content variants
getLastAssistantMessageassumes that any non-stringmsg.contentis an array of objects with{ type: "text"; text: string }and blindly casts each part. If the underlyingCoreMessagetype gains new shapes (e.g., tool calls, images, or other non-text parts without atextfield), this cast could throw at runtime.You can make this more defensive by checking the shape before access, e.g.:
if (Array.isArray(msg.content)) { return msg.content .filter( (part): part is { type: "text"; text: string } => part.type === "text" && typeof (part as any).text === "string", ) .map((part) => part.text) .join(""); }This keeps the helper robust to future
CoreMessageevolutions while still extracting the text you care about.
2141-2213: Harden model lookup inerrorFixFunctionto handle legacy/unknown valuesIn
errorFixFunction,fragmentModelis derived frominitialMetadata.modeland cast tokeyof typeof MODEL_CONFIGS, then used for bothMODEL_CONFIGS[fragmentModel]andgateway(fragmentModel):const fragmentModel = (initialMetadata.model as keyof typeof MODEL_CONFIGS) || "anthropic/claude-haiku-4.5"; const errorFixModelConfig = MODEL_CONFIGS[fragmentModel];If any existing fragment has a
modelvalue not present inMODEL_CONFIGS(e.g., older data written before this enum was introduced),errorFixModelConfigwill beundefinedand accessingerrorFixModelConfig.temperaturewill throw.Consider:
- Validating
fragmentModelagainstMODEL_CONFIGSand falling back when missing, e.g.:const defaultModel: keyof typeof MODEL_CONFIGS = "anthropic/claude-haiku-4.5"; const isKnownModel = fragmentModel in MODEL_CONFIGS; const effectiveModel = (isKnownModel ? fragmentModel : defaultModel) as keyof typeof MODEL_CONFIGS; const errorFixModelConfig = MODEL_CONFIGS[effectiveModel];
- Using
effectiveModelconsistently for bothgateway(effectiveModel)and metadata.This keeps error-fix robust even if older fragments carry unexpected
modelstrings.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (13)
jest.config.js(1 hunks)package.json(1 hunks)src/ai-sdk/gateway.ts(1 hunks)src/ai-sdk/index.ts(1 hunks)src/ai-sdk/tools.ts(1 hunks)src/ai-sdk/types.ts(1 hunks)src/app/api/agent/token/route.ts(1 hunks)src/inngest/functions.ts(27 hunks)src/inngest/types.ts(1 hunks)src/inngest/utils.ts(0 hunks)tests/mocks/ai-sdk-gateway.ts(1 hunks)tests/mocks/ai.ts(1 hunks)tests/mocks/inngest-agent-kit.ts(0 hunks)
💤 Files with no reviewable changes (2)
- src/inngest/utils.ts
- tests/mocks/inngest-agent-kit.ts
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
**/*.{ts,tsx}: Use Strict TypeScript - avoidanytypes
Use proper error handling with Sentry integration
**/*.{ts,tsx}: Avoidanytype in TypeScript - use proper typing orunknownfor uncertain types
Define interfaces/types for all data structures in TypeScript
Use Sentry to capture exceptions in production withSentry.captureException()and re-throw errors for proper handling
Sanitize file paths to prevent directory traversal attacks
Never expose secrets client-side; only use NEXT_PUBLIC_ prefix for public environment variables
**/*.{ts,tsx}: Use TypeScript strict mode with end-to-end type safety across frontend and backend
Prefer tRPC for type-safe API definitions between frontend and backend
**/*.{ts,tsx}: Use strict TypeScript withoutanytypes in AI agent code
Use modern framework patterns including Next.js App Router and React hooks
Implement accessibility and responsive design in UI components
Files:
src/ai-sdk/index.tssrc/app/api/agent/token/route.tstests/mocks/ai-sdk-gateway.tssrc/ai-sdk/gateway.tstests/mocks/ai.tssrc/ai-sdk/tools.tssrc/ai-sdk/types.tssrc/inngest/types.tssrc/inngest/functions.ts
src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/zapdev_rules.mdc)
src/**/*.{ts,tsx}: Use tRPC hooks for type-safe API calls with proper imports from@/trpc/client
Use functional components with TypeScript interfaces for props in React
Use React Query for server state management; use useState/useReducer for local state only
Always validate user inputs with Zod schemas
src/**/*.{ts,tsx}: Validate all user inputs using Zod schemas and sanitize file paths to prevent directory traversal attacks
Keep OAuth tokens encrypted in Convex and never expose API keys in client-side code (use NEXT_PUBLIC_ prefix only for public values)
Files:
src/ai-sdk/index.tssrc/app/api/agent/token/route.tssrc/ai-sdk/gateway.tssrc/ai-sdk/tools.tssrc/ai-sdk/types.tssrc/inngest/types.tssrc/inngest/functions.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Always use
bunfor package management instead of npm or yarn
Files:
src/ai-sdk/index.tssrc/app/api/agent/token/route.tstests/mocks/ai-sdk-gateway.tssrc/ai-sdk/gateway.tstests/mocks/ai.tssrc/ai-sdk/tools.tssrc/ai-sdk/types.tssrc/inngest/types.tsjest.config.jssrc/inngest/functions.ts
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Always use
bunfor installing packages and running scripts, never use npm or pnpm
Files:
src/ai-sdk/index.tssrc/app/api/agent/token/route.tstests/mocks/ai-sdk-gateway.tssrc/ai-sdk/gateway.tstests/mocks/ai.tssrc/ai-sdk/tools.tssrc/ai-sdk/types.tssrc/inngest/types.tsjest.config.jssrc/inngest/functions.ts
src/app/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/zapdev_rules.mdc)
Default to Server Components; only add 'use client' directive when needed for event handlers, browser APIs, React hooks, or third-party client libraries
Implement Next.js App Router with server components by default, using client components only when necessary for interactivity
Files:
src/app/api/agent/token/route.ts
src/app/api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use tRPC procedures for API routes instead of raw Next.js API routes
Files:
src/app/api/agent/token/route.ts
tests/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
tests/**/*.{ts,tsx}: Include security, sanitization, and file operation tests in all Jest test files
Use Jest test patterns matching**/__tests__/**/*.tsor**/?(*.)+(spec|test).tswith coverage scopesrc/**/*.ts
Files:
tests/mocks/ai-sdk-gateway.tstests/mocks/ai.ts
src/inngest/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Implement auto-fix retry logic for code generation with max 2 retry attempts on lint/build errors, running
bun run lint && bun run buildfor validation
Files:
src/inngest/types.tssrc/inngest/functions.ts
src/inngest/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
src/inngest/**/*.{ts,tsx}: Never start dev servers in E2B sandboxes during code generation
Always runbun run lintandbun run buildfor validation after code generation in sandboxes
Files:
src/inngest/types.tssrc/inngest/functions.ts
package.json
📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)
Add
@types/nodetopackage.jsonwhen using any Node.js built-in modules
Files:
package.json
{package.json,package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb}
📄 CodeRabbit inference engine (.cursor/rules/rules.mdc)
Always use
bunfor package management - never npm, yarn, or pnpm
Files:
package.json
src/inngest/functions.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Update E2B template names in src/inngest/functions.ts around line 22 after building new sandbox templates
Files:
src/inngest/functions.ts
🧠 Learnings (21)
📓 Common learnings
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-29T05:36:45.260Z
Learning: Applies to sandbox-templates/** : Build E2B templates with Docker before running AI code generation, and update template name in src/inngest/functions.ts after building
📚 Learning: 2025-11-29T05:36:45.260Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-29T05:36:45.260Z
Learning: Set required environment variables: NEXT_PUBLIC_CONVEX_URL, AI_GATEWAY_API_KEY, AI_GATEWAY_BASE_URL, E2B_API_KEY, SCRAPYBARA_API_KEY, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, INNGEST_EVENT_KEY, INNGEST_SIGNING_KEY
Applied to files:
src/ai-sdk/gateway.tspackage.jsonsrc/inngest/functions.ts
📚 Learning: 2025-11-29T05:36:45.260Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-29T05:36:45.260Z
Learning: Applies to src/prompts/*.ts : Create framework-specific AI prompts for each supported framework (nextjs, angular, react, vue, svelte) in the prompts directory
Applied to files:
tests/mocks/ai.tssrc/ai-sdk/types.tssrc/inngest/types.tssrc/inngest/functions.ts
📚 Learning: 2025-11-29T05:36:45.260Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-29T05:36:45.260Z
Learning: Applies to sandbox-templates/** : Build E2B templates with Docker before running AI code generation, and update template name in src/inngest/functions.ts after building
Applied to files:
src/ai-sdk/tools.tsjest.config.jssrc/inngest/functions.ts
📚 Learning: 2025-11-29T05:36:45.260Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-29T05:36:45.260Z
Learning: Applies to **/*.{ts,tsx} : Use strict TypeScript without `any` types in AI agent code
Applied to files:
src/ai-sdk/types.tssrc/inngest/types.tsjest.config.js
📚 Learning: 2025-11-28T02:59:13.470Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: .cursor/rules/zapdev_rules.mdc:0-0
Timestamp: 2025-11-28T02:59:13.470Z
Learning: Applies to **/*.{ts,tsx} : Define interfaces/types for all data structures in TypeScript
Applied to files:
src/ai-sdk/types.ts
📚 Learning: 2025-11-29T05:36:32.157Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-29T05:36:32.157Z
Learning: Applies to **/*.{ts,tsx} : Prefer tRPC for type-safe API definitions between frontend and backend
Applied to files:
src/ai-sdk/types.ts
📚 Learning: 2025-11-29T05:36:32.157Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-29T05:36:32.157Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript strict mode with end-to-end type safety across frontend and backend
Applied to files:
src/ai-sdk/types.ts
📚 Learning: 2025-11-29T05:36:32.157Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-29T05:36:32.157Z
Learning: Applies to src/prompts/**/*.ts : Implement framework detection logic following priority: explicit user mention → Next.js default → enterprise indicators → Material Design preference → performance critical
Applied to files:
src/ai-sdk/types.tssrc/inngest/functions.ts
📚 Learning: 2025-11-28T02:59:13.470Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: .cursor/rules/zapdev_rules.mdc:0-0
Timestamp: 2025-11-28T02:59:13.470Z
Learning: Applies to **/?(*.)+(spec|test).{ts,tsx} : Place test files in `/tests/` directory or use patterns `**/__tests__/**/*.ts` or `**/?(*.)+(spec|test).ts`
Applied to files:
jest.config.js
📚 Learning: 2025-11-29T05:36:32.157Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-29T05:36:32.157Z
Learning: Applies to tests/**/*.{ts,tsx} : Use Jest test patterns matching `**/__tests__/**/*.ts` or `**/?(*.)+(spec|test).ts` with coverage scope `src/**/*.ts`
Applied to files:
jest.config.js
📚 Learning: 2025-11-28T02:58:53.068Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-28T02:58:53.068Z
Learning: Applies to convex/**/*.{ts,tsx} : Convex uses file-based routing; a public function `f` in `convex/example.ts` has function reference `api.example.f`
Applied to files:
jest.config.js
📚 Learning: 2025-11-28T02:58:53.068Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-28T02:58:53.068Z
Learning: Applies to convex/**/*.{ts,tsx} : Thoughtfully organize files with public query, mutation, or action functions within the `convex/` directory following file-based routing
Applied to files:
jest.config.js
📚 Learning: 2025-11-29T05:36:45.260Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-29T05:36:45.260Z
Learning: Applies to src/inngest/**/*.{ts,tsx} : Never start dev servers in E2B sandboxes during code generation
Applied to files:
jest.config.js
📚 Learning: 2025-11-29T05:36:45.260Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-29T05:36:45.260Z
Learning: Applies to src/inngest/**/*.{ts,tsx} : Always run `bun run lint` and `bun run build` for validation after code generation in sandboxes
Applied to files:
jest.config.js
📚 Learning: 2025-11-29T05:36:32.157Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-29T05:36:32.157Z
Learning: Applies to tests/**/*.{ts,tsx} : Include security, sanitization, and file operation tests in all Jest test files
Applied to files:
jest.config.js
📚 Learning: 2025-11-28T02:58:53.068Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-28T02:58:53.068Z
Learning: Applies to convex/**/*.{ts,tsx} : Private functions follow file-based routing; a private function `g` in `convex/example.ts` has function reference `internal.example.g`
Applied to files:
jest.config.js
📚 Learning: 2025-11-29T05:36:32.157Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-29T05:36:32.157Z
Learning: Applies to src/inngest/functions.ts : Update E2B template names in src/inngest/functions.ts around line 22 after building new sandbox templates
Applied to files:
src/inngest/functions.ts
📚 Learning: 2025-11-28T02:58:53.068Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-28T02:58:53.068Z
Learning: Applies to convex/**/*.{ts,tsx} : Use the `api` object from `convex/_generated/api.ts` to call public functions registered with `query`, `mutation`, or `action`
Applied to files:
src/inngest/functions.ts
📚 Learning: 2025-11-29T05:36:45.260Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-29T05:36:45.260Z
Learning: AI code generation flow: user creates project → framework selector agent chooses framework → single code generation agent runs in E2B sandbox → automatic validation → save to Convex
Applied to files:
src/inngest/functions.ts
📚 Learning: 2025-11-29T05:36:32.157Z
Learnt from: CR
Repo: otdoges/zapdev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-29T05:36:32.157Z
Learning: Implement single agent mode workflow: User Request → Framework Detection → Code Generation Agent (with createOrUpdateFiles, readFiles, terminal commands) → Save to Convex → Display Preview
Applied to files:
src/inngest/functions.ts
🧬 Code graph analysis (4)
src/ai-sdk/gateway.ts (2)
src/ai-sdk/index.ts (1)
gateway(1-1)tests/mocks/ai-sdk-gateway.ts (1)
createGateway(1-6)
src/ai-sdk/tools.ts (2)
tests/mocks/e2b-code-interpreter.ts (1)
Sandbox(1-37)src/ai-sdk/types.ts (2)
AgentState(5-10)ToolResult(24-28)
src/ai-sdk/types.ts (2)
src/inngest/types.ts (1)
Framework(3-3)tests/mocks/ai.ts (1)
CoreMessage(14-17)
src/inngest/functions.ts (6)
tests/mocks/ai.ts (1)
CoreMessage(14-17)src/ai-sdk/index.ts (3)
gateway(1-1)Framework(6-6)AgentState(4-4)src/prompt.ts (3)
FRAMEWORK_SELECTOR_PROMPT(7-7)FRAGMENT_TITLE_PROMPT(1-1)RESPONSE_PROMPT(1-1)src/ai-sdk/types.ts (2)
Framework(3-3)AgentState(5-10)src/inngest/types.ts (2)
Framework(3-3)AgentState(11-11)src/prompts/shared.ts (2)
FRAGMENT_TITLE_PROMPT(311-320)RESPONSE_PROMPT(302-309)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: claude-review
- GitHub Check: Codacy Security Scan
🔇 Additional comments (7)
package.json (1)
15-19: AI SDK dependencies wired consistentlyThe
aiand@ai-sdk/gatewaydependencies align with the newsrc/ai-sdk/*modules and Jest mocks; no issues from a dependency wiring standpoint.tests/mocks/ai-sdk-gateway.ts (1)
1-5: Gateway mock shape matches current usageThis mock mirrors how
createGatewayis used insrc/ai-sdk/gateway.ts(called once with config and returning a callablegateway(modelId)function), while still allowing assertions on the initial config viacreateGateway.mock.calls. Looks good as-is.src/ai-sdk/index.ts (1)
1-9: AI SDK index provides a clean public surfaceRe-exporting
gateway,createCodeAgentTools, and the core types from a single index keeps AI-related imports consistent and discoverable across the codebase.jest.config.js (1)
14-15: Jest moduleNameMapper updated correctly for AI SDK mocksThe new mappings for
^@ai-sdk/gateway$and^ai$correctly route AI-related imports to the mock implementations intests/mocks, matching the dependency migration away from@inngest/agent-kit.src/inngest/functions.ts (3)
1183-1263: Agent control flow, summary handling, and auto-fix orchestration look coherentThe refactored
codeAgentFunctionlogic usingstateReflooks well-structured:
- The iterative loop with
MAX_ITERATIONS,summaryRetryCount, and<task_summary>reminders provides a clear termination strategy and reduces chances of getting stuck without a summary.- Post-network fallback to explicitly request a summary when files exist but
stateRef.current.summaryis empty is a good safety net.- Validation + auto-fix flow (lint/build checks feeding into an error-focused prompt, plus capped
AUTO_FIX_MAX_ATTEMPTS) aligns with the documented “auto-fix with up to 2 retries” behavior and keeps retries bounded.Taken together, this should make downstream use of
summaryand generated files more reliable without risking unbounded loops.Also applies to: 1392-1490
1865-1883: Response/fragment persistence wiring matches new state modelThe updated “save-result” step now:
- Uses the LLM-generated
responseOutputandfragmentTitleOutput(with sanitization + sensible fallbacks),- Persists
finalFiles(merged sandbox + agent files with validation and filtering), and- Returns
summary: stateRef.current.summaryin the function result.This lines up with the new
AgentState/stateRefflow and should keep the DB fragment, user-facing message, and returned payload in sync with what the agent actually produced.Also applies to: 1924-1929
2446-2455: Spec extraction helper and spec-planning agent integration look correctThe new
extractSpecContenthelper andspecPlanningAgentFunctionwiring are coherent:
extractSpecContentprefers content inside<spec>...</spec>but gracefully falls back to the full trimmed response if tags are missing.specPlanningAgentFunction:
- Ensures framework is set (reusing the framework selector when needed),
- Builds an enhanced spec prompt including framework-specific context,
- Feeds recent conversation context as
CoreMessage[], and- Stores the extracted spec with
AWAITING_APPROVALstatus.This should give you a cleaner, more structured spec artifact without breaking existing approval flow.
Also applies to: 2458-2597
| export const generateText = jest.fn(async () => ({ | ||
| text: "Mock AI response", | ||
| toolCalls: [], | ||
| toolResults: [], | ||
| finishReason: "stop", | ||
| usage: { promptTokens: 0, completionTokens: 0 }, | ||
| })); | ||
|
|
||
| export const tool = jest.fn((config) => ({ | ||
| ...config, | ||
| execute: config.execute, | ||
| })); | ||
|
|
||
| export type CoreMessage = { | ||
| role: "user" | "assistant" | "system"; | ||
| content: string | Array<{ type: string; text?: string; image?: string }>; | ||
| }; |
There was a problem hiding this comment.
Type config in tool mock to avoid implicit any under strict TS
With strict TypeScript, the config parameter in tool will be implicitly any, which conflicts with the repo guideline to avoid any types.
You can keep the mock flexible while satisfying strict typing by introducing a lightweight config type:
-export const tool = jest.fn((config) => ({
- ...config,
- execute: config.execute,
-}));
+type ToolConfig = {
+ execute: (args: unknown) => unknown | Promise<unknown>;
+ [key: string]: unknown;
+};
+
+export const tool = jest.fn((config: ToolConfig) => ({
+ ...config,
+ execute: config.execute,
+}));This preserves the existing behavior in tests while keeping the mock compatible with strict TypeScript settings.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const generateText = jest.fn(async () => ({ | |
| text: "Mock AI response", | |
| toolCalls: [], | |
| toolResults: [], | |
| finishReason: "stop", | |
| usage: { promptTokens: 0, completionTokens: 0 }, | |
| })); | |
| export const tool = jest.fn((config) => ({ | |
| ...config, | |
| execute: config.execute, | |
| })); | |
| export type CoreMessage = { | |
| role: "user" | "assistant" | "system"; | |
| content: string | Array<{ type: string; text?: string; image?: string }>; | |
| }; | |
| export const generateText = jest.fn(async () => ({ | |
| text: "Mock AI response", | |
| toolCalls: [], | |
| toolResults: [], | |
| finishReason: "stop", | |
| usage: { promptTokens: 0, completionTokens: 0 }, | |
| })); | |
| type ToolConfig = { | |
| execute: (args: unknown) => unknown | Promise<unknown>; | |
| [key: string]: unknown; | |
| }; | |
| export const tool = jest.fn((config: ToolConfig) => ({ | |
| ...config, | |
| execute: config.execute, | |
| })); | |
| export type CoreMessage = { | |
| role: "user" | "assistant" | "system"; | |
| content: string | Array<{ type: string; text?: string; image?: string }>; | |
| }; |
🤖 Prompt for AI Agents
In tests/mocks/ai.ts around lines 1 to 17, the tool mock's config parameter is
implicitly any under strict TS; add a lightweight TypeScript type for config
(e.g., an interface/type that at minimum declares execute: (...args: any[]) =>
any and optional other props) and annotate the tool signature as (config:
YourConfigType) => { ... } so the mock remains flexible but no longer uses
implicit any.
Description
Migrates agent functionality from Inngest Gateway to Vercel's AI Gateway.
Changes
Replaced Inngest agent kit with AI SDK for agent orchestration.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.