Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 1: Introduction to AI Agents

💻 Code: start from the lesson-01 branch of Hendrixer/agents-v2. The notes/ folder on that branch has the code you’ll write in this chapter.

What is an AI Agent?

A chatbot takes your message, sends it to an LLM, and returns the response. That’s one turn — input in, output out.

An agent is different. An agent can:

  1. Decide it needs more information
  2. Use tools to get that information
  3. Reason about the results
  4. Repeat until the task is complete

The key difference is the loop. A chatbot is a single function call. An agent is a loop that keeps running until the job is done. The LLM doesn’t just generate text — it decides what actions to take, observes the results, and plans its next move.

Here’s the mental model:

User: "What files are in my project?"

Chatbot: "I can't see your files, but typically a project has..."

Agent:
  → Thinks: "I need to list the files"
  → Calls: listFiles(".")
  → Gets: ["package.json", "src/", "README.md"]
  → Responds: "Your project has package.json, a src/ directory, and a README.md"

The agent used a tool to actually look at the filesystem, then synthesized the result into a response. That’s the fundamental pattern we’ll build in this book.

What We’re Building

By the end of this book, you’ll have a CLI AI agent that runs in your terminal. It will be able to:

  • Have multi-turn conversations
  • Read and write files
  • Run shell commands
  • Search the web
  • Execute code
  • Ask for your permission before doing anything dangerous
  • Manage long conversations without running out of context

It’s a miniature version of tools like Claude Code or GitHub Copilot in the terminal — and you’ll understand every line of code because you wrote it.

Project Setup

Let’s start from zero.

Initialize the Project

mkdir agents-v2
cd agents-v2
npm init -y

Install Dependencies

We need a few key packages:

# Core AI dependencies
npm install ai @ai-sdk/openai

# Terminal UI
npm install react ink ink-spinner

# Utilities
npm install zod shelljs

# Observability (for evals later)
npm install @lmnr-ai/lmnr

# Dev dependencies
npm install -D typescript tsx @types/node @types/react @types/shelljs @biomejs/biome

Here’s what each does:

PackagePurpose
aiVercel’s AI SDK — unified interface for LLM calls, streaming, tool calling
@ai-sdk/openaiOpenAI provider for the AI SDK
react + inkReact renderer for the terminal (like React Native, but for CLI)
zodSchema validation — used to define tool parameter shapes
shelljsCross-platform shell command execution
@lmnr-ai/lmnrLaminar — observability and structured evaluations

Configure TypeScript

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2021",
    "lib": ["ES2022"],
    "jsx": "react-jsx",
    "moduleResolution": "bundler",
    "types": ["node"],
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "moduleDetection": "force",
    "module": "Preserve",
    "resolveJsonModule": true,
    "allowJs": true
  }
}

Key choices:

  • jsx: "react-jsx" — We’ll use React for our terminal UI later
  • moduleResolution: "bundler" — Allows .ts imports
  • strict: true — Full type safety
  • module: "Preserve" — Don’t transform imports

Configure package.json

Update your package.json to add the type field and scripts:

{
  "name": "agi",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "agi": "./dist/cli.js"
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsc -p tsconfig.build.json",
    "dev": "tsx watch --env-file=.env src/index.ts",
    "start": "tsx --env-file=.env src/index.ts",
    "eval": "npx lmnr eval",
    "eval:file-tools": "npx lmnr eval evals/file-tools.eval.ts",
    "eval:shell-tools": "npx lmnr eval evals/shell-tools.eval.ts",
    "eval:agent": "npx lmnr eval evals/agent-multiturn.eval.ts"
  }
}

Here’s what each script does:

ScriptPurpose
buildCompile TypeScript to dist/ for distribution
devRun the agent in watch mode (auto-restarts on file changes)
startRun the agent once
evalRun all evaluation files
eval:file-toolsRun file tool selection evals (Chapter 3)
eval:shell-toolsRun shell tool selection evals (Chapter 8)
eval:agentRun multi-turn agent evals (Chapter 5)

The --env-file=.env flag tells Node/tsx to load environment variables from the .env file automatically.

The "type": "module" is important — it enables ES modules so we can use import/export syntax.

The "bin" field lets users install the agent globally with npm install -g and run it as agi from anywhere.

Build Configuration

The eval and dev scripts don’t need a separate build step (tsx handles TypeScript directly), but for distributing the agent as an npm package, create tsconfig.build.json:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": false,
    "outDir": "dist",
    "declaration": true
  },
  "include": ["src"]
}

This extends the base tsconfig but enables emitting compiled JavaScript to dist/.

Environment Variables

Create a .env file with all the API keys you’ll need throughout the book:

OPENAI_API_KEY=your-openai-api-key-here
LMNR_API_KEY=your-laminar-api-key-here
  • OPENAI_API_KEY — Required. Get one from platform.openai.com. Used for all LLM calls.
  • LMNR_API_KEY — Optional but recommended. Get one from laminar.ai. Used for running evaluations in Chapters 3, 5, and 8. Evals will still run locally without it, but results won’t be tracked over time.

And add it to .gitignore:

node_modules
dist
.env

Create the Directory Structure

mkdir -p src/agent/tools
mkdir -p src/agent/system
mkdir -p src/agent/context
mkdir -p src/ui/components

Your First LLM Call

Let’s make sure everything works. Create src/index.ts:

import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

const result = await generateText({
  model: openai("gpt-5-mini"),
  prompt: "What is an AI agent in one sentence?",
});

console.log(result.text);

Run it:

npm run start

You should see something like:

An AI agent is an autonomous system that perceives its environment,
makes decisions, and takes actions to achieve specific goals.

That’s a single LLM call. No tools, no loop, no agent — yet.

Understanding the AI SDK

The Vercel AI SDK (ai package) is the foundation we’ll build on. It provides:

  • generateText() — Make a single LLM call and get the full response
  • streamText() — Stream tokens as they’re generated (we’ll use this for the agent)
  • tool() — Define tools the LLM can call
  • generateObject() — Get structured JSON output (we’ll use this for evals)

The SDK abstracts away the provider-specific details. We use @ai-sdk/openai as our provider, but the code would work with Anthropic, Google, or any other supported provider with minimal changes.

Adding a System Prompt

Agents need personality and guidelines. Create src/agent/system/prompt.ts:

export const SYSTEM_PROMPT = `You are a helpful AI assistant. You provide clear, accurate, and concise responses to user questions.

Guidelines:
- Be direct and helpful
- If you don't know something, say so honestly
- Provide explanations when they add value
- Stay focused on the user's actual question`;

This is intentionally simple. The system prompt tells the LLM how to behave. In production agents, this would include detailed instructions about tool usage, safety guidelines, and response formatting. Ours will grow as we add features.

Defining Types

Create src/types.ts with the core interfaces we’ll need:

export interface AgentCallbacks {
  onToken: (token: string) => void;
  onToolCallStart: (name: string, args: unknown) => void;
  onToolCallEnd: (name: string, result: string) => void;
  onComplete: (response: string) => void;
  onToolApproval: (name: string, args: unknown) => Promise<boolean>;
  onTokenUsage?: (usage: TokenUsageInfo) => void;
}

export interface ToolApprovalRequest {
  toolName: string;
  args: unknown;
  resolve: (approved: boolean) => void;
}

export interface ToolCallInfo {
  toolCallId: string;
  toolName: string;
  args: Record<string, unknown>;
}

export interface ModelLimits {
  inputLimit: number;
  outputLimit: number;
  contextWindow: number;
}

export interface TokenUsageInfo {
  inputTokens: number;
  outputTokens: number;
  totalTokens: number;
  contextWindow: number;
  threshold: number;
  percentage: number;
}

These interfaces define the contract between our agent core and the UI layer:

  • AgentCallbacks — How the agent communicates back to the UI (streaming tokens, tool calls, completions)
  • ToolCallInfo — Metadata about a tool the LLM wants to call
  • ModelLimits — Token limits for context management
  • TokenUsageInfo — Current token usage for display

We won’t use all of these immediately, but defining them now gives us a clear picture of where we’re headed.

Summary

In this chapter you:

  • Learned what makes an agent different from a chatbot (the loop)
  • Set up a TypeScript project with the AI SDK
  • Made your first LLM call
  • Created the system prompt and core type definitions

The project doesn’t do much yet — it’s just a single LLM call. In the next chapter, we’ll teach it to use tools.


Next: Chapter 2: Tool Calling →