Building AI Agents with Google ADK and TypeScript

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

Prerequisites

Before starting, make sure you have:

  • Node.js 20+ installed on your machine
  • TypeScript 5.x installed globally
  • Google Cloud account with Gemini API enabled
  • API key from Google AI Studio
  • Basic knowledge of TypeScript and Node.js
  • A code editor like VS Code

What You'll Build

In this guide, you'll build a complete AI agent system featuring:

  1. Search Agent — searches the web and summarizes findings
  2. Analysis Agent — processes data and generates reports
  3. Coordinator Agent — orchestrates multiple agents for complex tasks

We'll use Google Agent Development Kit (ADK) — Google's new framework for building production-ready, scalable AI agents.

What is Google ADK?

Google Agent Development Kit (ADK) is an open-source framework from Google designed to simplify building AI agents. Key features include:

  • Native Gemini integration — works seamlessly with Gemini 2.x models
  • Flexible tool system — easily add custom tools
  • Built-in memory management — persist and retrieve context across conversations
  • Multi-agent support — build collaborative agent systems
  • Scalable architecture — from prototypes to production

Step 1: Project Setup

Start by creating a new TypeScript project and installing dependencies:

mkdir google-adk-agents
cd google-adk-agents
npm init -y

Install the required packages:

npm install @google/adk @google/generative-ai zod dotenv
npm install -D typescript @types/node tsx

Create a TypeScript configuration:

npx tsc --init

Update tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"]
}

Create the project structure:

mkdir -p src/{agents,tools,config}
touch src/index.ts src/config/env.ts

Step 2: Environment Configuration

Create a .env file in the project root:

GOOGLE_API_KEY=your_gemini_api_key_here
GOOGLE_CLOUD_PROJECT=your_project_id
ADK_LOG_LEVEL=info

Then create src/config/env.ts:

import dotenv from "dotenv";
import { z } from "zod";
 
dotenv.config();
 
const envSchema = z.object({
  GOOGLE_API_KEY: z.string().min(1, "Google API key is required"),
  GOOGLE_CLOUD_PROJECT: z.string().optional(),
  ADK_LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});
 
export const env = envSchema.parse(process.env);

We use Zod to validate environment variables at startup, preventing runtime errors from missing configuration.

Step 3: Creating Custom Tools

Tools are how agents interact with the outside world. We'll build two tools:

Web Search Tool

Create src/tools/search-tool.ts:

import { Tool, ToolContext } from "@google/adk";
import { z } from "zod";
 
const searchInputSchema = z.object({
  query: z.string().describe("The search query to look up"),
  maxResults: z.number().default(5).describe("Maximum number of results"),
});
 
export const webSearchTool = new Tool({
  name: "web_search",
  description:
    "Search the web for current information on any topic. " +
    "Use this when you need up-to-date facts, news, or data.",
  inputSchema: searchInputSchema,
  async execute(input: z.infer<typeof searchInputSchema>, context: ToolContext) {
    const { query, maxResults } = input;
 
    // In production, replace with actual search API (Google Custom Search, Serper, etc.)
    const response = await fetch(
      `https://api.search-provider.com/search?q=${encodeURIComponent(query)}&num=${maxResults}`,
      {
        headers: {
          Authorization: `Bearer ${process.env.SEARCH_API_KEY}`,
        },
      }
    );
 
    if (!response.ok) {
      return {
        error: `Search failed with status ${response.status}`,
        results: [],
      };
    }
 
    const data = await response.json();
 
    return {
      query,
      results: data.results.map((r: any) => ({
        title: r.title,
        url: r.url,
        snippet: r.snippet,
      })),
      totalResults: data.totalResults,
    };
  },
});

Data Analysis Tool

Create src/tools/analysis-tool.ts:

import { Tool } from "@google/adk";
import { z } from "zod";
 
const analysisInputSchema = z.object({
  data: z.string().describe("The data to analyze (JSON string or text)"),
  analysisType: z
    .enum(["summary", "trends", "comparison", "sentiment"])
    .describe("Type of analysis to perform"),
});
 
export const dataAnalysisTool = new Tool({
  name: "analyze_data",
  description:
    "Analyze data to extract insights, trends, and patterns. " +
    "Supports summary, trend analysis, comparison, and sentiment analysis.",
  inputSchema: analysisInputSchema,
  async execute(input: z.infer<typeof analysisInputSchema>) {
    const { data, analysisType } = input;
 
    let parsedData: any;
    try {
      parsedData = JSON.parse(data);
    } catch {
      parsedData = data;
    }
 
    switch (analysisType) {
      case "summary":
        return {
          type: "summary",
          dataPoints: Array.isArray(parsedData) ? parsedData.length : 1,
          summary: `Analyzed ${typeof parsedData === "object" ? Object.keys(parsedData).length : 1} data dimensions`,
          timestamp: new Date().toISOString(),
        };
 
      case "trends":
        return {
          type: "trends",
          direction: "upward",
          confidence: 0.85,
          periods: Array.isArray(parsedData) ? parsedData.length : 0,
          timestamp: new Date().toISOString(),
        };
 
      case "sentiment":
        return {
          type: "sentiment",
          overall: "positive",
          score: 0.72,
          breakdown: {
            positive: 0.72,
            neutral: 0.2,
            negative: 0.08,
          },
        };
 
      default:
        return {
          type: analysisType,
          result: "Analysis complete",
          data: parsedData,
        };
    }
  },
});

Step 4: Building the Search Agent

Now let's build the first agent — a search agent that uses the web search tool.

Create src/agents/search-agent.ts:

import { Agent, AgentConfig } from "@google/adk";
import { webSearchTool } from "../tools/search-tool";
 
const searchAgentConfig: AgentConfig = {
  name: "search_agent",
  model: "gemini-2.5-flash",
  description:
    "A specialized agent for searching the web and summarizing findings. " +
    "Excels at finding current information and presenting it clearly.",
  instruction: `You are a research assistant specialized in web search.
 
Your responsibilities:
1. Search for information using the web_search tool
2. Synthesize results from multiple searches
3. Present findings in a clear, structured format
4. Always cite your sources with URLs
 
Guidelines:
- Perform multiple searches to verify information
- Prioritize recent and authoritative sources
- If you cannot find reliable information, say so clearly
- Respond in the same language as the user's query`,
 
  tools: [webSearchTool],
 
  generationConfig: {
    temperature: 0.3,
    maxOutputTokens: 2048,
  },
};
 
export const searchAgent = new Agent(searchAgentConfig);

Step 5: Building the Analysis Agent

Create src/agents/analysis-agent.ts:

import { Agent, AgentConfig } from "@google/adk";
import { dataAnalysisTool } from "../tools/analysis-tool";
 
const analysisAgentConfig: AgentConfig = {
  name: "analysis_agent",
  model: "gemini-2.5-pro",
  description:
    "An expert data analyst that processes information and generates insights.",
  instruction: `You are a data analysis expert.
 
Your responsibilities:
1. Analyze data provided to you using the analyze_data tool
2. Identify patterns, trends, and anomalies
3. Generate clear reports with actionable insights
4. Create visualization descriptions when helpful
 
Guidelines:
- Always explain your methodology
- Quantify findings when possible
- Highlight key takeaways prominently
- Flag any data quality issues you notice
- Respond in the same language as the user's query`,
 
  tools: [dataAnalysisTool],
 
  generationConfig: {
    temperature: 0.2,
    maxOutputTokens: 4096,
  },
};
 
export const analysisAgent = new Agent(analysisAgentConfig);

Step 6: Building the Coordinator (Multi-Agent)

This is the most important part — the coordinator agent that manages other agents.

Create src/agents/coordinator-agent.ts:

import { Agent, AgentConfig, AgentTool } from "@google/adk";
import { searchAgent } from "./search-agent";
import { analysisAgent } from "./analysis-agent";
 
const searchAgentTool = new AgentTool({
  agent: searchAgent,
  name: "research",
  description:
    "Delegate research tasks to the search agent. " +
    "Use this for finding current information from the web.",
});
 
const analysisAgentTool = new AgentTool({
  agent: analysisAgent,
  name: "analyze",
  description:
    "Delegate analysis tasks to the analysis agent. " +
    "Use this for processing data and generating insights.",
});
 
const coordinatorConfig: AgentConfig = {
  name: "coordinator",
  model: "gemini-2.5-pro",
  description:
    "The main coordinator that orchestrates research and analysis tasks.",
  instruction: `You are an AI coordinator managing a team of specialized agents.
 
Your team:
1. **Research Agent** - Use the "research" tool for web searches
2. **Analysis Agent** - Use the "analyze" tool for data processing
 
Workflow:
1. Understand the user's request
2. Break it into sub-tasks
3. Delegate to the appropriate agent(s)
4. Synthesize the results into a coherent response
 
Guidelines:
- Clearly explain your plan before executing
- Use both agents when the task requires research AND analysis
- Provide a unified, well-structured final response
- Handle errors gracefully and try alternative approaches
- Respond in the same language as the user's query`,
 
  tools: [searchAgentTool, analysisAgentTool],
 
  generationConfig: {
    temperature: 0.4,
    maxOutputTokens: 8192,
  },
};
 
export const coordinatorAgent = new Agent(coordinatorConfig);

Step 7: Adding Memory Management

Google ADK provides a built-in memory system for maintaining context across conversations.

Create src/config/memory.ts:

import { MemoryService, InMemoryStore, Session } from "@google/adk";
 
const memoryStore = new InMemoryStore();
 
export const memoryService = new MemoryService({
  store: memoryStore,
  searchConfig: {
    maxResults: 10,
    similarityThreshold: 0.7,
  },
});
 
export async function createSession(userId: string): Promise<Session> {
  return memoryService.createSession({
    userId,
    metadata: {
      createdAt: new Date().toISOString(),
      source: "tutorial-app",
    },
  });
}
 
export async function getOrCreateSession(userId: string): Promise<Session> {
  const existingSessions = await memoryService.listSessions({ userId });
 
  if (existingSessions.length > 0) {
    return existingSessions[0];
  }
 
  return createSession(userId);
}

Step 8: Building the Main Application

Now let's bring everything together.

Create src/index.ts:

import { Runner } from "@google/adk";
import { coordinatorAgent } from "./agents/coordinator-agent";
import { getOrCreateSession, memoryService } from "./config/memory";
import { env } from "./config/env";
import * as readline from "readline";
 
async function main() {
  console.log("🤖 Starting AI Agent System...");
  console.log(`📋 Log level: ${env.ADK_LOG_LEVEL}`);
 
  const runner = new Runner({
    agent: coordinatorAgent,
    appName: "ai-research-assistant",
    memoryService,
  });
 
  const userId = "demo-user";
  const session = await getOrCreateSession(userId);
  console.log(`📍 Session: ${session.id}`);
  console.log("✅ System ready! Type your questions below.\n");
 
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
 
  const askQuestion = () => {
    rl.question("You: ", async (input) => {
      const trimmed = input.trim();
 
      if (trimmed.toLowerCase() === "exit") {
        console.log("👋 Goodbye!");
        rl.close();
        process.exit(0);
      }
 
      if (!trimmed) {
        askQuestion();
        return;
      }
 
      try {
        console.log("\n🔄 Processing...\n");
 
        const response = await runner.run({
          userId,
          sessionId: session.id,
          newMessage: {
            role: "user",
            parts: [{ text: trimmed }],
          },
        });
 
        for (const event of response) {
          if (event.content?.parts) {
            for (const part of event.content.parts) {
              if (part.text) {
                console.log(`Agent: ${part.text}\n`);
              }
            }
          }
        }
      } catch (error) {
        console.error("❌ Error:", error);
      }
 
      askQuestion();
    });
  };
 
  askQuestion();
}
 
main().catch(console.error);

Step 9: Adding Error Handling and Monitoring

Create src/config/logging.ts to track agent performance:

import { CallbackHandler, AgentEvent } from "@google/adk";
 
export class AgentLogger implements CallbackHandler {
  private startTimes = new Map<string, number>();
 
  onAgentStart(event: AgentEvent): void {
    const agentName = event.agentName;
    this.startTimes.set(agentName, Date.now());
    console.log(`[${this.timestamp()}] ▶️  Agent "${agentName}" started`);
  }
 
  onAgentEnd(event: AgentEvent): void {
    const agentName = event.agentName;
    const startTime = this.startTimes.get(agentName);
    const duration = startTime ? Date.now() - startTime : 0;
    console.log(
      `[${this.timestamp()}] ✅ Agent "${agentName}" completed in ${duration}ms`
    );
    this.startTimes.delete(agentName);
  }
 
  onToolCall(event: AgentEvent): void {
    console.log(
      `[${this.timestamp()}] 🔧 Tool "${event.toolName}" called by "${event.agentName}"`
    );
  }
 
  onError(event: AgentEvent): void {
    console.error(
      `[${this.timestamp()}] ❌ Error in "${event.agentName}":`,
      event.error
    );
  }
 
  private timestamp(): string {
    return new Date().toISOString().split("T")[1].split(".")[0];
  }
}

Update src/index.ts to use the logger:

import { AgentLogger } from "./config/logging";
 
const runner = new Runner({
  agent: coordinatorAgent,
  appName: "ai-research-assistant",
  memoryService,
  callbacks: [new AgentLogger()],
});

Step 10: Adding an HTTP API

Let's make the agent system accessible via HTTP.

Create src/server.ts:

import { createServer, IncomingMessage, ServerResponse } from "http";
import { Runner } from "@google/adk";
import { coordinatorAgent } from "./agents/coordinator-agent";
import { getOrCreateSession, memoryService } from "./config/memory";
import { AgentLogger } from "./config/logging";
 
const runner = new Runner({
  agent: coordinatorAgent,
  appName: "ai-research-assistant",
  memoryService,
  callbacks: [new AgentLogger()],
});
 
async function handleChat(req: IncomingMessage, res: ServerResponse) {
  const chunks: Buffer[] = [];
  for await (const chunk of req) {
    chunks.push(chunk as Buffer);
  }
  const body = JSON.parse(Buffer.concat(chunks).toString());
 
  const { userId, message } = body;
 
  if (!userId || !message) {
    res.writeHead(400, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: "userId and message are required" }));
    return;
  }
 
  const session = await getOrCreateSession(userId);
 
  const response = await runner.run({
    userId,
    sessionId: session.id,
    newMessage: {
      role: "user",
      parts: [{ text: message }],
    },
  });
 
  const parts: string[] = [];
  for (const event of response) {
    if (event.content?.parts) {
      for (const part of event.content.parts) {
        if (part.text) {
          parts.push(part.text);
        }
      }
    }
  }
 
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(
    JSON.stringify({
      sessionId: session.id,
      response: parts.join("\n"),
    })
  );
}
 
const server = createServer(async (req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
 
  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }
 
  if (req.method === "POST" && req.url === "/chat") {
    await handleChat(req, res);
    return;
  }
 
  res.writeHead(404, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ error: "Not found" }));
});
 
const PORT = process.env.PORT || 3001;
server.listen(PORT, () => {
  console.log(`🚀 Agent API server running on http://localhost:${PORT}`);
  console.log(`📡 POST /chat - Send messages to the agent`);
});

Step 11: Running and Testing

Add run scripts to package.json:

{
  "scripts": {
    "dev": "tsx src/index.ts",
    "server": "tsx src/server.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Interactive Mode

npm run dev

You'll see the welcome message and can start chatting:

🤖 Starting AI Agent System...
📋 Log level: info
📍 Session: session_abc123
✅ System ready! Type your questions below.

You: What are the latest developments in AI?

API Server Mode

npm run server

Test with curl:

curl -X POST http://localhost:3001/chat \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user-1",
    "message": "Research the latest Google AI news and analyze the trends"
  }'

Step 12: Adding a Storage Tool

Let's add a tool for persisting research results:

// src/tools/storage-tool.ts
import { Tool } from "@google/adk";
import { z } from "zod";
 
const storage = new Map<string, any>();
 
const saveInputSchema = z.object({
  key: z.string().describe("Unique key for the data"),
  data: z.any().describe("Data to store"),
  tags: z.array(z.string()).optional().describe("Tags for categorization"),
});
 
export const storageSaveTool = new Tool({
  name: "save_data",
  description: "Save research results or analysis data for later retrieval.",
  inputSchema: saveInputSchema,
  async execute(input: z.infer<typeof saveInputSchema>) {
    const entry = {
      ...input,
      savedAt: new Date().toISOString(),
    };
    storage.set(input.key, entry);
    return { success: true, key: input.key, message: "Data saved successfully" };
  },
});
 
const retrieveInputSchema = z.object({
  key: z.string().describe("Key of the data to retrieve"),
});
 
export const storageRetrieveTool = new Tool({
  name: "retrieve_data",
  description: "Retrieve previously saved research or analysis data.",
  inputSchema: retrieveInputSchema,
  async execute(input: z.infer<typeof retrieveInputSchema>) {
    const data = storage.get(input.key);
    if (!data) {
      return { found: false, message: `No data found for key: ${input.key}` };
    }
    return { found: true, data };
  },
});

Troubleshooting

Invalid API Key

Error: API key not valid. Please pass a valid API key.

Solution: Verify that GOOGLE_API_KEY is set correctly in your .env file and the key is active in Google AI Studio.

Rate Limit Exceeded

Error: 429 Resource has been exhausted

Solution: Add delays between requests or increase your usage quota in Google Cloud Console.

Model Not Available

Error: Model gemini-2.5-pro is not available

Solution: Check that the requested model is available in your region. You can substitute gemini-2.5-flash as an alternative.

Slow Performance

If responses are slow:

  • Use gemini-2.5-flash instead of pro for simple tasks
  • Reduce maxOutputTokens as needed
  • Enable streaming with runner.runStreaming() for long responses

Best Practices

1. Tool Design

// ✅ Good: specific, focused tool
const weatherTool = new Tool({
  name: "get_weather",
  description: "Get current weather for a specific city",
});
 
// ❌ Bad: overly generic tool
const doAnythingTool = new Tool({
  name: "do_anything",
  description: "Does everything",
});

2. Clear Instructions

// ✅ Good: specific instructions with examples
instruction: `You are a financial analyst.
When asked about stocks, always include:
- Current price
- 30-day trend
- Key metrics (P/E, Market Cap)
Format as a structured table.`
 
// ❌ Bad: vague instructions
instruction: `Help with financial stuff.`

3. Error Handling

// ✅ Handle errors explicitly
async execute(input) {
  try {
    const result = await externalAPI.call(input);
    return { success: true, data: result };
  } catch (error) {
    return {
      success: false,
      error: error.message,
      suggestion: "Try with different parameters"
    };
  }
}

Next Steps

After completing this guide, you can:

  • Add more tools — connect your agents to real databases, external APIs, or cloud services
  • Use Vertex AI — deploy agents to Google Cloud with Vertex AI Agent Builder
  • Build a UI — create a React/Next.js interface for interacting with your agents
  • Enable streaming — use runner.runStreaming() for real-time responses
  • Add tests — write unit tests for your tools and agents

Useful Resources

Conclusion

In this guide, we learned how to build a complete AI agent system using Google ADK with TypeScript. We covered creating custom tools, building specialized agents, orchestrating them through a coordinator agent, and adding memory management and performance monitoring.

Google ADK significantly simplifies building AI agents compared to other frameworks, especially with its native Gemini integration. Whether you're building a research assistant, automation system, or advanced chatbot — ADK provides a solid foundation to start and scale.


Want to read more tutorials? Check out our latest tutorial on Detection and identification of plant leaf diseases using YOLOv4.

Discuss Your Project with Us

We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.

Let's find the best solutions for your needs.

Related Articles