Building AI Agents with Google ADK and TypeScript

Noqta Team
By Noqta Team ·

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 4 Laravel 11 Basics: CSRF Protection.

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