Build an AI Dev Agent with Tool Calling

Build a Code Architect AI agent that autonomously calls tools to plan projects.

Introduction

30 Second Summary

Every AI tool you've heard of lately (Cursor, Claude Code, GitHub Copilot) has one thing in common: the AI decides which actions to take on its own. You describe what you want, and the AI figures out which tools to call and in what order.

In this project, you will build an AI dev agent that plans, structures, and scaffolds coding projects for you. You'll define tools with typed schemas, wire them up to a large language model, and deploy the whole thing as a live web app.

What You'll Build

A full-stack "Code Architect" AI agent built with Next.js and the Vercel AI SDK that autonomously calls developer tools based on natural language input.

By the end of this project, you'll have:

  • A Next.js API route that configures an AI agent with three typed tool definitions using Zod schemas.
  • A streaming chat interface that visualizes tool calls in real-time as the agent works through your request.
  • A live deployment on Vercel proving you can ship the exact AI engineering pattern employers hire for.
  • Secret Mission: Add a live npm registry search tool that makes real external API calls.

Are there any prerequisites?

You should be comfortable with basic React and Next.js concepts (components, pages, API routes). You'll also need a Google account to get a free Gemini API key.

Set Up Your Project

Before you can build an AI agent, you need a working development environment with the right tools connected.

In this step, you will scaffold a Next.js project, install the Vercel AI SDK packages, and grab a free API key from Google so your app can talk to Gemini.

In this step, get ready to:
  • Scaffold a Next.js project and install the AI SDK with the Google Gemini provider.
  • Get a free Gemini API key and configure it as an environment variable.
  • Verify the dev server runs successfully.
Scaffold a Next.js project and install dependencies

You need a Next.js project as the foundation for your AI agent. Next.js gives you both a React frontend and a serverless backend in one framework.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project by running this command:
npx create-next-app@latest code-architect

What will this command do?

This launches an interactive wizard that scaffolds a complete Next.js project for you. It will ask for your project name and then ask whether you want to use the recommended defaults.

  • If npx asks to install packages, type y and press Enter.
  • The wizard will ask if you want to use the recommended Next.js defaults. Select Yes, use recommended defaults and press Enter.

What do the recommended defaults include?

The defaults set up TypeScript, ESLint, Tailwind CSS, and the App Router. These are exactly what your AI agent project needs.

Wait for the scaffolding to finish. You will see a success message once it completes.

  • Move into your new project folder by running:
cd code-architect
  • Install the AI SDK packages your agent needs by running:
npm install ai @ai-sdk/google @ai-sdk/react zod

What are these packages?

The ai package is the core Vercel AI SDK. It provides streaming, tool calling, and multi-step agent execution.

The @ai-sdk/google package connects the SDK to Google's Gemini models. The @ai-sdk/react package provides React hooks for the chat UI. The zod library defines typed schemas for your agent's tools.

  • Confirm the packages installed successfully by checking that no error messages appeared in the terminal output.

Seeing permission errors during install?

Make sure you ran cd code-architect before running the install command. You need to be inside the project folder.

If you see network errors, check your internet connection and try running the install command again.

Help me troubleshoot npm install errors

  • Open the project in Cursor by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to Desktop, select the code-architect folder, and click Open.
Get a Gemini API key and save it

Your AI agent needs credentials to call Google's Gemini model. The Gemini API has a free tier that requires no billing account.

  • Open your browser and go to the Google AI Studio API key page:

https://aistudio.google.com/app/apikey

  • Sign in with your Google account if prompted.
  • Click Create API key.
  • Select a Google Cloud project when prompted (or let it create one for you).
  • Copy the generated API key. You will only see it once.

Why is this free?

Google offers a free tier for Gemini models that requires no billing account. The gemini-2.5-flash model your agent will use has free input and output pricing for normal usage.

  • Back in Cursor, create a new file called .env.local in the root of your code-architect folder by right-clicking in the Explorer sidebar and selecting New File.
  • Name the file .env.local and press Enter.
  • Paste the following into the file, replacing your-api-key-here with the API key you just copied:
GOOGLE_GENERATIVE_AI_API_KEY=your-api-key-here

Why .env.local?

Next.js automatically reads environment variables from .env.local and makes them available on the server. The @ai-sdk/google package looks for the GOOGLE_GENERATIVE_AI_API_KEY variable by default, so no extra configuration is needed.

  • Save .env.local.

Having trouble creating your API key?

Make sure you are signed in with a personal Google account. Work or school (Google Workspace) accounts may have restrictions that block key creation.

If you see a permission denied error, try opening the page in an incognito window or a different browser.

Help me troubleshoot API key creation

✔️ Awesome, I've got everything!

Great. Double check you have saved your .env.local file with your real API key pasted in.

ⓧ I'd like to double check the full code

Your .env.local file should look like this (with your actual key replacing the placeholder):

GOOGLE_GENERATIVE_AI_API_KEY=your-api-key-here
Verify the dev server runs

Before writing any agent code, confirm the base project starts correctly. This catches any setup issues early.

  • Open the integrated terminal in Cursor by pressing Ctrl+`.
  • Start the development server by running:
npm run dev
  • Open your browser and go to http://localhost:3000.
  • You should see the default Next.js welcome page with the Next.js logo and links to documentation.

Not seeing the welcome page?

Check the terminal for error messages. If you see a port conflict, another process is using port 3000. Stop it or let Next.js use a different port (it will suggest one automatically).

If you see module not found errors, run npm install again to make sure all dependencies are present.

Help me debug my dev server

Your development environment is ready. Next up, you will build the AI agent API route that defines your tools and gives the model its instructions.

Build the AI Agent API Route

In Step 1, you set up your environment and installed all the dependencies your agent needs. Now it is time to build the brain of your AI agent.

The core of any AI agent is a loop: the user sends a message, the model reads it, decides which tools to call, executes them, reads the results, and decides what to do next. You will build this entire loop in a single API route using the Vercel AI SDK.

In this step, get ready to:
  • Create an API route with three typed tool definitions using Zod schemas.
  • Configure multi-step execution so the agent can chain tool calls autonomously.
  • Add a system prompt that tells the agent how to behave and when to use tools.
Create the API route with tool definitions

The Vercel AI SDK uses a pattern called tool calling. You define tools with typed schemas (using Zod), and the LLM autonomously decides which tools to invoke based on user input.

  • In Cursor, create the folder path app/api/chat/ inside your project.
  • Create a new file called route.ts inside app/api/chat/.
  • Add the imports and the first half of the route (through the planProject tool) by pasting this code:
import {
  streamText,
  UIMessage,
  convertToModelMessages,
  tool,
  stepCountIs,
} from 'ai';
import { google } from '@ai-sdk/google';
import { z } from 'zod';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: google('gemini-2.5-flash'),
    system: `You are Code Architect, an AI dev agent that helps developers plan and scaffold projects.
You have access to tools that generate project plans, file structures, and code scaffolds.
When a user describes what they want to build, use your tools to provide structured, actionable output.
Always start with planProject to understand the scope, then use generateFileTree to show the structure, and scaffoldFile for key files.
You can call multiple tools in sequence to build a complete picture.`,
    messages: await convertToModelMessages(messages),
    tools: {
      planProject: tool({
        description:
          'Create a high-level project plan with architecture decisions and recommended steps',
        inputSchema: z.object({
          description: z
            .string()
            .describe('What the user wants to build'),
          language: z
            .string()
            .describe('Primary programming language'),
          constraints: z
            .array(z.string())
            .describe('Any constraints like budget, timeline, or requirements'),
        }),
        execute: async ({ description, language, constraints }) => {
          return {
            projectName: description
              .split(' ')
              .slice(0, 3)
              .join('-')
              .toLowerCase(),
            language,
            constraints,
            architecture: [
              'API Layer - handles HTTP requests and routing',
              'Service Layer - contains business logic',
              'Data Layer - manages database operations',
              'Config - environment and app configuration',
            ],
            steps: [
              'Initialize project and install dependencies',
              'Set up project structure and configuration',
              'Implement core features',
              'Add error handling and validation',
              'Write tests',
              'Configure deployment',
            ],
          };
        },
      }),

What does this code do?

  • streamText streams the AI's response back to the client in real-time, so the user sees output as it is generated.
  • convertToModelMessages transforms the UI chat messages into the format the model expects. It must be awaited because it is async.
  • tool() defines a tool the model can call. Each tool has a description (so the model knows when to use it), an inputSchema (a Zod schema that types the inputs), and an execute function (what runs when the model calls it).
  • The planProject tool takes a project description, language, and constraints, then returns structured architecture recommendations and next steps.
  • Continue building the route by pasting the remaining two tools and the closing of the function directly below the planProject tool:
      generateFileTree: tool({
        description:
          'Generate a recommended file and folder structure for a project',
        inputSchema: z.object({
          projectName: z.string().describe('Name of the project'),
          framework: z
            .string()
            .describe('Framework to use (e.g., Next.js, Express, FastAPI)'),
          features: z
            .array(z.string())
            .describe('Features the project needs'),
        }),
        execute: async ({ projectName, framework, features }) => {
          const featureFolders = features.map(
            (f) => `  src/${f.toLowerCase().replace(/\s+/g, '-')}/`
          );
          return {
            projectName,
            framework,
            tree: [
              `${projectName}/`,
              '  src/',
              ...featureFolders,
              '  src/config/',
              '  src/utils/',
              '  tests/',
              '  package.json',
              '  tsconfig.json',
              '  .env.example',
              '  README.md',
            ],
          };
        },
      }),
      scaffoldFile: tool({
        description: 'Generate starter code content for a specific file',
        inputSchema: z.object({
          filename: z.string().describe('Name/path of the file to generate'),
          purpose: z.string().describe('What this file should do'),
          framework: z.string().describe('Framework context'),
        }),
        execute: async ({ filename, purpose, framework }) => {
          return {
            filename,
            purpose,
            framework,
            code: `// ${filename}\n// Purpose: ${purpose}\n// Framework: ${framework}\n\n// TODO: Implement ${purpose}`,
          };
        },
      }),
    },
    stopWhen: stepCountIs(5),
  });

  return result.toUIMessageStreamResponse();
}

What does this code do?

  • generateFileTree takes a project name, framework, and features list, then returns a recommended folder structure. It dynamically creates folders from the features array.
  • scaffoldFile generates starter code for any file. The model provides the filename and purpose, and the tool returns a template with comments describing what to implement.
  • stopWhen: stepCountIs(5) allows the model to call up to 5 tools in sequence before stopping. Without this, the model would stop after a single tool call.
  • result.toUIMessageStreamResponse() converts the streamed result into a format the useChat hook on the frontend can consume.
  • Save app/api/chat/route.ts.
  • Check the terminal where npm run dev is running. You should see the file compile without any errors.

Seeing a compile error?

  • Make sure you pasted both code blocks into the same file, one after the other. The second block continues immediately after the planProject closing }), line.
  • Check that every opening brace { has a matching closing brace }. A missing brace at the end of the file is the most common issue.
  • Confirm your .env.local file exists in the project root with the GOOGLE_GENERATIVE_AI_API_KEY variable set.

Why isn't my route.ts compiling?

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

Here is the complete app/api/chat/route.ts file. Make sure yours matches exactly:

import {
  streamText,
  UIMessage,
  convertToModelMessages,
  tool,
  stepCountIs,
} from 'ai';
import { google } from '@ai-sdk/google';
import { z } from 'zod';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: google('gemini-2.5-flash'),
    system: `You are Code Architect, an AI dev agent that helps developers plan and scaffold projects.
You have access to tools that generate project plans, file structures, and code scaffolds.
When a user describes what they want to build, use your tools to provide structured, actionable output.
Always start with planProject to understand the scope, then use generateFileTree to show the structure, and scaffoldFile for key files.
You can call multiple tools in sequence to build a complete picture.`,
    messages: await convertToModelMessages(messages),
    tools: {
      planProject: tool({
        description:
          'Create a high-level project plan with architecture decisions and recommended steps',
        inputSchema: z.object({
          description: z
            .string()
            .describe('What the user wants to build'),
          language: z
            .string()
            .describe('Primary programming language'),
          constraints: z
            .array(z.string())
            .describe('Any constraints like budget, timeline, or requirements'),
        }),
        execute: async ({ description, language, constraints }) => {
          return {
            projectName: description
              .split(' ')
              .slice(0, 3)
              .join('-')
              .toLowerCase(),
            language,
            constraints,
            architecture: [
              'API Layer - handles HTTP requests and routing',
              'Service Layer - contains business logic',
              'Data Layer - manages database operations',
              'Config - environment and app configuration',
            ],
            steps: [
              'Initialize project and install dependencies',
              'Set up project structure and configuration',
              'Implement core features',
              'Add error handling and validation',
              'Write tests',
              'Configure deployment',
            ],
          };
        },
      }),
      generateFileTree: tool({
        description:
          'Generate a recommended file and folder structure for a project',
        inputSchema: z.object({
          projectName: z.string().describe('Name of the project'),
          framework: z
            .string()
            .describe('Framework to use (e.g., Next.js, Express, FastAPI)'),
          features: z
            .array(z.string())
            .describe('Features the project needs'),
        }),
        execute: async ({ projectName, framework, features }) => {
          const featureFolders = features.map(
            (f) => `  src/${f.toLowerCase().replace(/\s+/g, '-')}/`
          );
          return {
            projectName,
            framework,
            tree: [
              `${projectName}/`,
              '  src/',
              ...featureFolders,
              '  src/config/',
              '  src/utils/',
              '  tests/',
              '  package.json',
              '  tsconfig.json',
              '  .env.example',
              '  README.md',
            ],
          };
        },
      }),
      scaffoldFile: tool({
        description: 'Generate starter code content for a specific file',
        inputSchema: z.object({
          filename: z.string().describe('Name/path of the file to generate'),
          purpose: z.string().describe('What this file should do'),
          framework: z.string().describe('Framework context'),
        }),
        execute: async ({ filename, purpose, framework }) => {
          return {
            filename,
            purpose,
            framework,
            code: `// ${filename}\n// Purpose: ${purpose}\n// Framework: ${framework}\n\n// TODO: Implement ${purpose}`,
          };
        },
      }),
    },
    stopWhen: stepCountIs(5),
  });

  return result.toUIMessageStreamResponse();
}
Understand multi-step execution

The line stopWhen: stepCountIs(5) is what transforms a simple chatbot into an agent. Without it, the model calls one tool and stops.

How does multi-step execution work?

When a user asks "Plan me a REST API with auth", the model does not just call one tool. It enters a loop:

  • Step 1: The model calls planProject with the description, language, and constraints.
  • Step 2: The SDK executes the tool and feeds the result back to the model.
  • Step 3: The model reads the result and decides to call generateFileTree next.
  • Step 4: That result feeds back in, and the model calls scaffoldFile for key files.
  • Step 5: The model responds with a final text summary tying everything together.

The stepCountIs(5) limit prevents infinite loops. The model can take up to 5 steps before the SDK forces it to finish.

Review the system prompt strategy

The system prompt you added tells the model exactly how to behave and in what order to call tools. This is critical for consistent agent behavior.

Why does the system prompt matter for tool calling?

Without explicit instructions, the model might only call one tool or skip tools entirely. The system prompt establishes a predictable workflow: always start with planProject, then generateFileTree, then scaffoldFile.

This is the same pattern used in production AI agents. The tools define what the agent CAN do. The system prompt defines what the agent SHOULD do and in what order.

Your agent's brain is built. It knows what tools it has, when to use them, and how to chain them together. Next up, you will build the streaming chat UI that lets you talk to this agent and watch its tool calls happen in real-time.

Build the Streaming Chat UI

Your API route is ready to receive messages and stream responses back. Now you need a frontend that lets you talk to the agent and see its tool calls in real-time.

In this step, you will build a chat interface that streams text from the agent and renders each tool call as a visual card. This is what makes the agent feel alive. You will see it "think" by calling tools one after another.

In this step, get ready to:
  • Build a chat component using the useChat hook from the AI SDK.
  • Render streaming text and tool call results as styled cards.
  • Test the agent by asking it to plan a project and observe multi-tool chaining.
Replace the default page with a chat interface

The AI SDK provides a React hook called useChat that handles the entire client-side chat lifecycle. It manages the message list, sends requests to your API route, and streams responses back in real-time.

  • In the Cursor sidebar, open app/page.tsx.
  • Select all the existing content and replace it with the following code:
'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

export default function Chat() {
  // Local state for the text input field
  const [input, setInput] = useState('');
  // useChat connects to your /api/chat route automatically
  const { messages, sendMessage } = useChat();

  return (
    <div className="flex flex-col h-screen bg-zinc-950 text-zinc-100">
      {/* Header section */}
      <header className="p-4 border-b border-zinc-800">
        <h1 className="text-xl font-bold">Code Architect Agent</h1>
        <p className="text-sm text-zinc-400">
          Describe what you want to build. The agent will plan, structure, and
          scaffold it for you.
        </p>
      </header>

      {/* Scrollable message list */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${
              message.role === 'user' ? 'justify-end' : 'justify-start'
            }`}
          >
            <div
              className={`max-w-[85%] rounded-lg p-3 ${
                message.role === 'user'
                  ? 'bg-blue-600'
                  : 'bg-zinc-800 border border-zinc-700'
              }`}
            >
              {/* Iterate over message parts to render text and tool results */}
              {message.parts.map((part, i) => {
                if (part.type === 'text') {
                  return (
                    <p
                      key={`${message.id}-${i}`}
                      className="whitespace-pre-wrap"
                    >
                      {part.text}
                    </p>
                  );
                }
                if (part.type.startsWith('tool-')) {
                  const toolName = part.type.replace('tool-', '');
                  return (
                    <div
                      key={`${message.id}-${i}`}
                      className="mt-2 p-3 bg-zinc-900 rounded border border-zinc-600"
                    >
                      <div className="flex items-center gap-2 mb-2">
                        <span className="text-xs font-mono bg-green-900 text-green-300 px-2 py-0.5 rounded">
                          {toolName}
                        </span>
                      </div>
                      <pre className="text-xs text-zinc-300 overflow-x-auto whitespace-pre-wrap">
                        {JSON.stringify((part as any).result, null, 2)}
                      </pre>
                    </div>
                  );
                }
                return null;
              })}
            </div>
          </div>
        ))}
      </div>

      {/* Input form */}
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!input.trim()) return;
          sendMessage({ text: input });
          setInput('');
        }}
        className="p-4 border-t border-zinc-800"
      >
        <div className="flex gap-2">
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="I want to build a REST API with authentication..."
            className="flex-1 p-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:border-blue-500"
          />
          <button
            type="submit"
            disabled={!input.trim()}
            className="px-6 py-3 bg-blue-600 rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  );
}

What does this code do?

  • useChat() automatically connects to your /api/chat route. It returns a messages array (the conversation history) and a sendMessage function to send new messages.
  • Each message has a .parts array. Text parts render as paragraphs. Tool result parts (whose type starts with tool-) render as styled cards.
  • The form calls sendMessage({ text: input }) on submit and clears the input field, keeping the UI responsive.
  • Save app/page.tsx.
  • Switch back to your browser and refresh the page. You should see a dark-themed chat interface with a header that says "Code Architect Agent", an empty message area, and a text input at the bottom.

Don't see the chat interface?

  • Make sure your dev server is still running in the terminal. If it stopped, run npm run dev again.
  • Check that you saved the file. Unsaved changes won't trigger a hot reload.
  • If you see a red error overlay, check for missing closing brackets or mismatched quotes in the JSX.

Help me fix my chat UI rendering error.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

export default function Chat() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat();

  return (
    <div className="flex flex-col h-screen bg-zinc-950 text-zinc-100">
      <header className="p-4 border-b border-zinc-800">
        <h1 className="text-xl font-bold">Code Architect Agent</h1>
        <p className="text-sm text-zinc-400">
          Describe what you want to build. The agent will plan, structure, and
          scaffold it for you.
        </p>
      </header>

      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${
              message.role === 'user' ? 'justify-end' : 'justify-start'
            }`}
          >
            <div
              className={`max-w-[85%] rounded-lg p-3 ${
                message.role === 'user'
                  ? 'bg-blue-600'
                  : 'bg-zinc-800 border border-zinc-700'
              }`}
            >
              {message.parts.map((part, i) => {
                if (part.type === 'text') {
                  return (
                    <p
                      key={`${message.id}-${i}`}
                      className="whitespace-pre-wrap"
                    >
                      {part.text}
                    </p>
                  );
                }
                if (part.type.startsWith('tool-')) {
                  const toolName = part.type.replace('tool-', '');
                  return (
                    <div
                      key={`${message.id}-${i}`}
                      className="mt-2 p-3 bg-zinc-900 rounded border border-zinc-600"
                    >
                      <div className="flex items-center gap-2 mb-2">
                        <span className="text-xs font-mono bg-green-900 text-green-300 px-2 py-0.5 rounded">
                          {toolName}
                        </span>
                      </div>
                      <pre className="text-xs text-zinc-300 overflow-x-auto whitespace-pre-wrap">
                        {JSON.stringify((part as any).result, null, 2)}
                      </pre>
                    </div>
                  );
                }
                return null;
              })}
            </div>
          </div>
        ))}
      </div>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!input.trim()) return;
          sendMessage({ text: input });
          setInput('');
        }}
        className="p-4 border-t border-zinc-800"
      >
        <div className="flex gap-2">
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="I want to build a REST API with authentication..."
            className="flex-1 p-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:border-blue-500"
          />
          <button
            type="submit"
            disabled={!input.trim()}
            className="px-6 py-3 bg-blue-600 rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  );
}
Understand how message parts render

The key to visualizing tool calls is the message.parts array. Each message from the agent can contain multiple parts. Some parts are plain text, and others are tool results.

How do message parts work?

When the agent streams a response, it sends a mix of text and tool results. The AI SDK packages these into a .parts array on each message object.

  • Text parts have type: 'text' and contain a .text string. These render as normal paragraphs.
  • Tool result parts have a type like tool-planProject or tool-generateFileTree. The code strips the tool- prefix to display the tool name in a green badge.
  • The structured result from each tool is displayed as formatted JSON using JSON.stringify. This lets you see exactly what data each tool returned.

Why is this the same pattern as Claude Code and Cursor?

Every agentic AI tool follows this exact flow: the model receives a prompt, decides which tools to call, executes them, reads the results, and decides whether to call more tools or respond with text. The multi-step execution you configured with stopWhen: stepCountIs(5) is what allows the agent to chain calls automatically.

Your UI makes this visible. Instead of hiding tool calls behind a loading spinner, you show each one as a card. This is how modern AI dev tools build trust with users.

Test the agent by asking it to plan a project

Time to see your agent in action. You will send it a project request and watch it autonomously decide which tools to call and in what order.

  • Switch back to your browser at localhost:3000.
  • In the input field at the bottom, type something like: I want to build a REST API with authentication using Express.
  • Click Send or press Enter.

Watch the message area. You should see the agent start streaming text, then tool call cards appearing one after another. The agent should call planProject first, then generateFileTree, and then scaffoldFile for key files. Each card shows the tool name in a green badge and the structured JSON output below it.

What should I see?

Your message appears on the right in blue. The agent's response appears on the left with a dark border. Inside the agent's response, you will see green-badged tool cards with names like planProject and generateFileTree, each containing JSON output.

The agent may also include text between tool calls explaining its reasoning. The number of tools called depends on how the model interprets your request.

Not seeing any tool cards?

  • Check the terminal running your dev server for error messages. A 403 or 429 error means your Gemini API key may not be active yet. Wait a few minutes and try again.
  • Verify that your .env.local file has the correct API key and that the variable is named GOOGLE_GENERATIVE_AI_API_KEY exactly.
  • If you see text responses but no tool cards, your prompt may not be triggering tool calls. Try a more specific request like "Build me a Node.js REST API with JWT authentication and PostgreSQL".

Help me debug why my agent is not calling tools.

Your AI agent is fully working locally. It receives prompts, autonomously decides which tools to call, chains them together, and streams everything back to the UI. Next up, you will deploy this to the internet so anyone can use it.

Deploy to Vercel

Your AI agent is working locally. Now it's time to put it on the internet where anyone can use it.

Vercel is built by the Next.js team and optimized for Next.js apps. You'll push your code to GitHub, import the repository in Vercel, and have a live production URL in minutes.

In this step, get ready to:
  • Initialize a Git repository and push your code to GitHub.
  • Import the project in Vercel and add your API key as an environment variable.
  • Verify the live agent works at your production URL.
Initialize a Git repository and push to GitHub

Before Vercel can deploy your app, your code needs to live in a GitHub repository. You'll create a local Git repository, commit your code, then push it to a new repo on GitHub.

What about my API key?

Your .env.local file is already excluded from Git by the default Next.js .gitignore. Your secret stays on your machine and never gets pushed to GitHub.

  • Stop the dev server in your terminal by pressing Ctrl+C.
  • Initialize a Git repository and commit all your project files by running these commands:
git init
git add .
git commit -m "Initial commit"

What do these commands do?

  • git init creates a new Git repository in your project folder.
  • git add . stages all your files (except those in .gitignore) for the commit.
  • git commit -m "Initial commit" saves a snapshot of your code with a descriptive message.
  • Open github.com in your browser and sign in.
  • Click the + icon in the top right corner, then select New repository.
  • Name the repository code-architect-agent (or any name you like).
  • Leave all other settings as default. Do not initialize with a README, .gitignore, or license.
  • Click Create repository.

GitHub will show you setup commands. Copy the two lines under the heading that says "push an existing repository from the command line."

  • Back in your terminal, push your code to GitHub by running these commands:
git remote add origin [[REPO_URL="https://github.com/YOUR_USERNAME/code-architect-agent.git"]]
git branch -M main
git push -u origin main
  • Refresh the GitHub repository page in your browser. You should see all your project files listed.

Push failed or permission denied?

  • Make sure you're signed in to the correct GitHub account in your terminal. Run git config user.name to check.
  • If you see a credentials error, GitHub may need you to authenticate with a personal access token or SSH key instead of a password.
  • Double-check the remote URL matches what GitHub showed you on the new repository page.

Why can't I push my code to GitHub?

Import the project in Vercel and configure the environment variable

Vercel connects directly to your GitHub repository. Every time you push new code, Vercel automatically builds and deploys a fresh version.

  • Open vercel.com in your browser and sign in with your Vercel account.
  • Click Add New... in the top right, then select Project.
  • Under Import Git Repository, find the code-architect-agent repository you just pushed. Click Import.

Don't see your repository?

If this is your first time using Vercel, you'll need to connect your GitHub account first. Vercel will prompt you to install its GitHub app and authorize access to your repositories.

Before clicking Deploy, you need to add your Gemini API key as an environment variable. Vercel can't read your local .env.local file, so you provide the key through its dashboard instead.

  • On the project configuration page, scroll down to the Environment Variables section.
  • In the Name field, enter GOOGLE_GENERATIVE_AI_API_KEY.
  • In the Value field, paste your Gemini API key (the same value from your local .env.local file).
  • Click Deploy.

Why add the environment variable here?

Vercel runs your app on its own servers, not your laptop. It has no access to your local files. Adding the key through the dashboard ensures Vercel injects it into your app at build and runtime, exactly like .env.local does locally.

Vercel will now build your Next.js app. You can watch the build logs stream in real-time. This typically takes 30-60 seconds.

Build failed?

  • Check the build logs for a specific error message. TypeScript errors are the most common cause.
  • Make sure the environment variable name is exactly GOOGLE_GENERATIVE_AI_API_KEY with no extra spaces.
  • If you see a dependency error, make sure your package.json was committed and pushed to GitHub.

Why is my Vercel build failing?

Verify the live agent works at your production URL

Once the build succeeds, Vercel assigns your app a production URL. This is your live, public AI agent.

  • On the deployment success screen, click the production URL that Vercel shows you. It will look something like https://code-architect-agent.vercel.app.
  • In the chat input, type a test message like I want to build a REST API with authentication and click Send.
  • Watch the agent call your tools in sequence. You should see green tool-call cards appear as the agent plans, generates a file tree, and scaffolds files.

Agent not responding on the live URL?

  • Open your browser's developer console (F12) and check for network errors on the /api/chat request.
  • A 500 error usually means the GOOGLE_GENERATIVE_AI_API_KEY environment variable is missing or incorrect. Go to your Vercel project settings to double-check it.
  • If you just created your Gemini API key, wait a few minutes for it to fully activate before retrying.

Why isn't my deployed agent responding to messages?

Your AI agent is live on the internet. You've just deployed the exact same tool-calling pattern that powers Claude Code, Cursor, and GitHub Copilot. Next up, you'll extend your agent with a secret mission.

Secret mission

Add a Live npm Search Tool

Your agent currently returns static, pre-built data from its tools. Real AI agents interact with the outside world. In this mission, you'll add a fourth tool that makes live HTTP requests to the npm registry, fetching real package data in real-time.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs on free tiers for both Vercel hosting and the Gemini API, so there are no ongoing costs.

Resources you used:

  • Vercel deployment (free tier, no cost).
  • Google AI Studio API key (free tier, no charges).
  • GitHub repository.
  • Local project folder on your Desktop.

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your AI agent.

  • Vercel's free tier has no idle costs. Your deployment stays live at no charge.
  • The Gemini API free tier has no charges for normal usage within rate limits.
  • Your GitHub repository and local files remain untouched.

Pause - I'll come back to this later

Since both Vercel and the Gemini API free tier have no idle costs, there is nothing to pause. Your resources stay available without incurring charges.

  • Your Vercel deployment will continue serving traffic at no cost.
  • Your API key remains active but costs nothing when unused.
  • Come back any time and pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to fully clean up.

  • Delete the Vercel project: open your Vercel dashboard, select your project, navigate to Settings, scroll to the bottom of the page, and click Delete Project. Confirm the deletion when prompted.
  • Delete the API key: go to Google AI Studio, click Get API key in the left sidebar, find the key you created for this project, click the trash icon next to it, and confirm deletion.
  • Delete the GitHub repository: open your repository on GitHub, go to Settings, scroll to the Danger Zone at the bottom, and click Delete this repository. Type the repository name to confirm.
  • Delete the local project folder by running this command in your terminal:
rm -rf ~/Desktop/code-architect

On Windows, run this instead:

Remove-Item -Recurse -Force ~\Desktop\code-architect

Nice Work!

Nice Work!

Nice work! You just built a full-stack AI agent from scratch and deployed it to the web.

You've learned how to:

  • Build an AI agent API route with typed tool definitions using Zod schemas, the same pattern behind tools like Claude Code and Cursor.
  • Implement multi-step agent execution where the model autonomously chains tool calls to build a complete answer from a single prompt.
  • Build a streaming chat UI that visualizes tool calls in real-time and deploy it live on Vercel.
  • Secret Mission: Added a live npm search tool that makes real external API calls, demonstrating how agents interact with the real world.

Ready to quiz yourself?