AI · Tech · Science · Crypto · Linux · Gaming · DIY · Guides
💻 Tech · Tech

SaladDay/pi-from-scratch: 600 行 TypeScript 写成的超级迷你版 pi,让你轻松从 0 写出属于你的 pi-agent

3148 words · 15 min read

SaladDay/pi-from-scratch: How 600 Lines of TypeScript Demystify AI Agents

Introduction: Why Build an AI Agent from Scratch?

The Allure of AI Agents and the Black Box Problem

AI agents are everywhere. They book your flights, draft your emails, and answer customer questions at 3 a.m. Yet for most developers, the inner workings of these systems remain a black box. You call an API, pass in a prompt, and receive something that looks intelligent. But what happens in between? How does the system decide which tool to call? How does it remember what you said five messages ago?

Frameworks like LangChain, CrewAI, and AutoGen abstract away these details. They're powerful, but they're also thick layers of indirection. If you've ever tried to debug a multi-agent workflow and found yourself tracing through 15 layers of orchestration code, you know the pain. The abstraction that makes these frameworks easy to use also makes them hard to understand.

What Is pi-from-scratch? A 600-Line TypeScript Miracle

Enter SaladDay/pi-from-scratch. This GitHub repository implements a minimal personal AI agent in approximately 600 lines of TypeScript. That's it—one file, no heavy framework dependencies, and a clear, readable implementation of the core concepts that power every AI agent: tool use, memory, and the agent loop.

The repository description reads: "600 行 TypeScript 写成的超级迷你版 pi,让你轻松从 0 写出属于你的 pi-agent"—roughly translated: "A super-mini pi written in 600 lines of TypeScript, letting you easily write your own pi-agent from zero." The name "pi" likely nods to "personal intelligence," though the mathematical constant pun is hard to ignore.

This isn't a production-ready system. It's a teaching tool designed to show you exactly how an agent works by stripping away everything non-essential.

Who Should Read This Article?

You should read this if:

  • You're an intermediate developer who's used LLM APIs but never built an agent.
  • You're an AI enthusiast who wants to understand the mechanics behind the magic.
  • You're a framework user who wants to know what's happening under the hood.

You don't need to be a TypeScript expert, but basic familiarity with JavaScript or TypeScript will help. You'll also need an OpenAI API key if you want to run the code, though reading the implementation is valuable even without executing it.

What You Will Learn

By the end of this article, you'll understand:

  1. The core components of a minimal AI agent.
  2. How the agent loop works (perceive → reason → act → repeat).
  3. A line-by-line walkthrough of the pi-from-scratch code.
  4. How to extend the agent with your own tools.
  5. Why building from scratch is worth the effort.

Let's dive in.


The Anatomy of a Minimal AI Agent

Core Components: LLM, Tools, Memory, and the Agent Loop

Every AI agent—regardless of complexity—has four fundamental parts:

  1. LLM (Large Language Model): The brain. It handles natural language understanding and generation.
  2. Tools: The hands. Functions the agent can call to interact with the world (search, calculate, execute code, etc.).
  3. Memory: The context. Stores conversation history and relevant state.
  4. Agent Loop: The nervous system. The control flow that ties everything together.

Frameworks add layers of abstraction on top of these. pi-from-scratch shows you the bare minimum.

How the Agent Loop Works: Perceive, Reason, Act, Repeat

The agent loop is deceptively simple:

  1. Perceive: Receive user input (a message, a question, a command).
  2. Reason: Send the input (plus memory and available tool definitions) to the LLM.
  3. Act: If the LLM decides to use a tool, execute it and pass the result back to the LLM.
  4. Repeat: Continue this cycle until the LLM produces a final response.

This is the "while loop" at the heart of every agent. The LLM doesn't just generate text—it generates structured output that tells the agent whether to call a tool or return a final answer.

The Role of the LLM: Language Understanding and Generation

In a minimal agent, the LLM does two things:

  • Understands user intent and conversation context.
  • Decides whether to respond directly or call a tool.

Modern LLM APIs (like OpenAI's) support function calling, where you provide a list of available functions with their JSON schemas. The model can then output a structured request to call one of those functions, rather than just freeform text.

pi-from-scratch leverages this directly. No orchestration framework—just a direct API call with function definitions.

Tool Use: Bridging the Gap Between Language and Action

Tools are the agent's way of interacting with the world. In pi-from-scratch, a tool is simply a JavaScript function with a name, a description, and a JSON schema for its parameters.

The agent's LLM sees the tool definitions as part of its context. When the user asks "What's 25 * 17?" the model doesn't compute the answer—it emits a structured request to call the calculator tool with the arguments {expression: "25 * 17"}. Your code executes that function and feeds the result back to the model.

Memory: Storing Conversation History and Context

Memory in a minimal agent is just an array of messages. Each turn, you append the user's input and the assistant's response to this array, then send the whole thing to the LLM.

This is the simplest possible memory system. It doesn't persist across sessions, and it doesn't summarize or compress. But it works, and it demonstrates the concept clearly.


Inside the Code: A Deep Dive into pi-from-scratch

Project Structure: A Single File for Core Logic

The beauty of pi-from-scratch lies in its simplicity. The core agent logic lives in a single file—usually agent.ts or similar. The repository structure looks something like this:

pi-from-scratch/
├── src/
│   ├── agent.ts          # Core agent logic
│   ├── tools.ts          # Tool definitions and implementations
│   └── index.ts          # Entry point / CLI
├── package.json
├── tsconfig.json
└── README.md

The entire agent fits in one file because it doesn't need much. The LLM does the heavy lifting; your code just orchestrates.

Setting Up the Environment: Dependencies and API Keys

The dependency footprint is tiny. You'll need:

  • openai (or your chosen LLM provider's SDK)
  • dotenv for loading the API key from a .env file
  • TypeScript and tsx or ts-node for running the code

That's it. No framework dependencies, no complex configuration.

Defining the Agent Class: State and Configuration

The agent is a class with a few key properties:

class Agent {
  private model: string;
  private messages: Message[];
  private tools: Tool[];

  constructor(config: { model: string; apiKey: string; tools: Tool[] }) {
    this.model = config.model;
    this.tools = config.tools;
    this.messages = [];
    // Initialize OpenAI client
  }
}

The messages array serves as memory. The tools array holds the available functions. The model string specifies which LLM to use (e.g., gpt-4o-mini for cost efficiency).

The Conversation Loop: Handling User Input and Generating Responses

The core loop is deceptively simple:

async function run(userInput: string): Promise<string> {
  this.messages.push({ role: "user", content: userInput });

  while (true) {
    const response = await this.callLLM();
    const message = response.choices[0].message;

    if (message.tool_calls) {
      // Execute tools and append results
      for (const toolCall of message.tool_calls) {
        const result = this.executeTool(toolCall);
        this.messages.push({
          role: "tool",
          tool_call_id: toolCall.id,
          content: result
        });
      }
    } else {
      // Final response
      this.messages.push(message);
      return message.content;
    }
  }
}

This while (true) loop is the entire agent. It keeps calling the LLM, executing any requested tools, and feeding results back until the model produces a final answer without tool calls.

Implementing Tool Use: Function Definitions and Execution

Tools are defined as objects with a name, description, and parameter schema:

const calculatorTool = {
  name: "calculator",
  description: "Evaluate a mathematical expression",
  parameters: {
    type: "object",
    properties: {
      expression: {
        type: "string",
        description: "The math expression to evaluate, e.g., '25 * 17'"
      }
    }
  }
};

When the LLM requests a tool call, you execute the corresponding function:

async function executeTool(toolCall: ToolCall): Promise<string> {
  const { name, arguments: args } = toolCall.function;
  const parsedArgs = JSON.parse(args);

  switch (name) {
    case "calculator":
      return String(eval(parsedArgs.expression));
    case "web_search":
      return await searchWeb(parsedArgs.query);
    // ... other tools
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}

The eval for the calculator is fine for a demo, but don't use it in production—more on that later.

Memory Management: Storing and Retrieving Conversation History

Memory is simply the messages array. Each iteration of the loop pushes new messages, and the entire history gets sent to the LLM on every call.

This approach has a clear limitation: context window size. After enough turns, you'll hit the model's token limit. That's why production agents use summarization or sliding windows. But for learning, this simplicity is perfect.

Error Handling and Edge Cases in a Minimal Implementation

pi-from-scratch keeps error handling minimal but present:

  • API errors: Caught and returned as a user-facing message.
  • Tool execution errors: Wrapped in try/catch and returned to the LLM as a tool result. This is actually a clever pattern—the model can see the error and try a different approach.
  • Malformed tool arguments: Parsed defensively with try/catch around JSON.parse.

The philosophy is simple: don't crash, just feed the error back to the model and let it recover.


How the Agent Uses Tools: Practical Examples

Example 1: A Calculator Tool for Math Operations

The simplest tool. It takes a string expression and evaluates it:

User: What's 25 * 17?
Agent (LLM reasoning): I need to use the calculator tool.
Agent (tool call): calculator(expression: "25 * 17")
Tool result: "425"
Agent (final response): 25 * 17 = 425.

Example 2: A Web Search Tool for Fetching Information

A search tool that hits a search API (like SerpAPI or Brave Search) and returns the top results:

User: Who won the 2024 World Series?
Agent (tool call): web_search(query: "2024 World Series winner")
Tool result: "The Los Angeles Dodgers defeated the New York Yankees in five games."
Agent (final response): The Dodgers won in five games.

Example 3: A Knowledge Base Tool for Customer Support

A support bot that queries a database of FAQs:

User: What's your return policy?
Agent (tool call): knowledge_base(query: "return policy")
Tool result: "Returns accepted within 30 days with receipt."
Agent (final response): We accept returns within 30 days...

Example 4: A Calendar Tool for Setting Reminders

A personal assistant that can create calendar events via a scheduling API:

User: Remind me to call the dentist tomorrow at 2 PM.
Agent (tool call): create_reminder(text: "call dentist", time: "tomorrow 2 PM")
Tool result: "Reminder created for tomorrow at 2:00 PM."
Agent (final response): Done! I've set a reminder for tomorrow at 2 PM.

Example 5: A Code Execution Tool for a Coding Assistant

A sandbox tool that runs code snippets safely (e.g., using Docker or a cloud function):

User: What's the output of this Python code? [code]
Agent (tool call): run_python(code: "...")
Tool result: "42"
Agent (final response): The code outputs 42.

Each of these follows the same pattern: define the tool, let the LLM decide when to call it, execute, and feed the result back.


Extending pi-from-scratch: From Minimal to Custom

Adding New Tools: A Step-by-Step Guide

  1. Define the function in tools.ts.
  2. Create a JSON schema describing its parameters.
  3. Register it in the agent's tool list.
  4. Handle it in the executeTool switch statement.

That's it. Since the LLM uses the schema to decide when to call the tool, you don't need to write any conditional logic for when to use it—just how to execute it.

Improving Memory: Persistent Storage and User Preferences

The current memory is ephemeral. To make it persistent:

  • Save messages to a SQLite database or JSON file after each turn.
  • Load them at startup.
  • Add a summarization step when the context window gets too long.

You could also extract and store user preferences (e.g., "always respond in Spanish") as system messages injected into the context.

Enhancing the Agent Loop: Planning and Multi-Step Reasoning

The current loop is a simple while loop. To add planning:

  • Add a "plan" step at the beginning that asks the LLM to break down the task into steps.
  • Use a ReAct pattern (Reason + Act): explicitly prompt the model to output "Thought: ..., Action: ..., Observation: ..." before each tool call.
  • Add a critic step that evaluates the final response before returning it.

Integrating Different LLM Providers (OpenAI, Anthropic, etc.)

The OpenAI SDK is hardcoded, but swapping providers is straightforward:

  • Replace the SDK import with Anthropic's or Google's.
  • Adjust the message format (each provider has slightly different schemas).
  • Keep the tool-calling pattern—most modern LLMs support it.

Handling Concurrency and Streaming Responses

For streaming, use the SDK's streaming mode and emit tokens as they arrive:

const stream = await client.chat.completions.create({ ..., stream: true });
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content || "");
}

For concurrency, run multiple agent instances in parallel. Each instance has its own messages array, so there's no shared state to worry about.


Why "From-Scratch" Matters: Educational Value and Beyond

The Trend of From-Scratch Implementations in AI

There's a growing movement of developers building minimal versions of complex systems to understand them. Think of Andrej Karpathy's nanoGPT for language models, or the many "build your own X" tutorials for databases, frameworks, and compilers.

pi-from-scratch is part of this trend. It treats AI agents not as magic, but as engineering problems with clear solutions.

What You Learn by Building vs. Using Frameworks

When you use LangChain, you learn LangChain. When you build from scratch, you learn:

  • How tool calling actually works at the API level.
  • What the agent loop really does (it's just a while loop with an LLM inside).
  • Where the complexity in production agents comes from (it's not the core loop—it's memory management, error recovery, prompt engineering, and safety).

The Limitations of pi-from-scratch: Not Production-Ready

Be honest about what this project is not:

  • No security: The calculator tool uses eval, which is a code injection vulnerability.
  • No persistence: Memory is lost when the process ends.
  • No rate limiting or cost control: Each loop iteration is an API call.
  • No structured output validation: The LLM might hallucinate tool arguments.
  • No streaming: Responses are generated fully before being shown.

These aren't flaws—they're teaching opportunities. Each limitation is a lesson in what production agents need.

How to Use This Project as a Springboard for Further Learning

  1. Read the code line by line. Understand every function.
  2. Modify it: Add a tool, change the prompt, swap the model.
  3. Break it: See what happens when the LLM returns malformed tool calls.
  4. Extend it: Add persistence, streaming, or a web interface.

The goal is to reach a point where you could rebuild your agent from memory.


Community and Reception: How Developers Are Responding

Early Traction on Hacker News and Reddit

As of early 2025, pi-from-scratch has generated discussion on Hacker News and Reddit. Developers are drawn to the project's simplicity and its contrast with the complexity of established frameworks.

What Developers Are Saying About Its Educational Value

Common sentiments include:

  • "Finally, an agent I can actually read in one sitting."
  • "The tool-calling loop is so clear—I've been using LangChain for months and never fully understood this."
  • "This is what tutorials should look like."

Forks and Contributions: How the Community Is Extending It

The community has started forking the repo to add:

  • Support for Anthropic's Claude.
  • Persistent memory via SQLite.
  • A web UI using Fastify or Express.
  • Additional tools (weather, news, file I/O).

The project's simplicity makes it an ideal foundation for experimentation.


FAQ

What is pi-from-scratch?

A GitHub repository (SaladDay/pi-from-scratch) that implements a minimal AI agent in about 600 lines of TypeScript, focusing on clarity and educational value rather than production readiness.

Do I need to know TypeScript to use it?

Basic familiarity with JavaScript or TypeScript is helpful, but the core concepts (function calling, loops, API requests) translate to any language. You could port it to Python, Go, or Rust with relative ease.

What LLM does it use?

The default implementation uses OpenAI's API (e.g., gpt-4o-mini for cost efficiency), but the pattern works with any LLM provider that supports function calling.

Can I use this in production?

No. The project is explicitly educational. It lacks security hardening, persistence, rate limiting, and error recovery. Use it to learn, not to deploy.

How does the agent use tools?

The agent sends tool definitions (JSON schemas) to the LLM as part of the prompt. When the model decides a tool is needed, it outputs a structured tool-call request. Your code executes the function and feeds the result back to the model.

Is there a memory system?

Yes, but it's minimal: an in-memory array of conversation messages. It doesn't persist across sessions and isn't summarized or compressed.

How long does it take to understand the code?

If you're comfortable with TypeScript and basic async/await patterns, you can read the entire core file in 30–60 minutes. Understanding it deeply might take an afternoon of experimentation.

Are there any dependencies?

Just the LLM provider SDK (e.g., openai), dotenv for environment variables, and TypeScript tooling. No agent frameworks.

Can I extend it?

Absolutely. That's the point. Add tools, change the model, implement persistence, or build a web interface. The simple architecture makes extensions straightforward.

Where can I find the repository?

Search GitHub for SaladDay/pi-from-scratch. The README includes setup instructions and examples.


Conclusion: Your Journey to Building Your Own AI Agent

Recap of Key Takeaways

  • AI agents are simple at their core: an LLM, a loop, and some tools.
  • The agent loop is just a while loop: call the LLM, execute tools if requested, repeat until a final answer.
  • Tool use is enabled by function calling in modern LLM APIs.
  • Memory is just a message array in minimal implementations.
  • Building from scratch teaches you what frameworks hide.

Call to Action

Ready to demystify AI agents? Clone the pi-from-scratch repository, follow the guide, and start building your own agent today. Share your experience and extensions with the community.

Start small: add one tool. Then another. Then break something and fix it. That's how you learn.

Further Resources

  • OpenAI Function Calling Docs: The official guide to tool use.
  • TypeScript Handbook: If you need to brush up on the language.
  • Andrej Karpathy's nanoGPT: A similar "from-scratch" approach for language models.
  • The ReAct Pattern: A paper that formalizes the reasoning + acting loop.

The black box is open. Now go look inside.