All Courses

How to Build a Harness

How to Build a Harness

A hands-on course that walks you through building a minimal coding agent (like pi) that talks to Anthropic Claude and edits your code — in ~400 lines of TypeScript.

Advanced English 20 chapters

claude   agent-harness   typescript   anthropic   tool-use

A hands-on course that walks you through building a minimal coding agent (like pi) that talks to Anthropic Claude and edits your code.

Inspired by:

Reference implementation: pi-mono  ·  Provider: Anthropic Claude (anthropic-messages API)  ·  Language: TypeScript / Node.js

Part 1 — The Mental Model

1. What Is a Harness?

Understand the loop: prompt → LLM → tool calls → results → repeat.

A harness is a program that:

  1. Sends a user message to an LLM
  2. The LLM replies — sometimes with text, sometimes with tool calls
  3. Your program executes those tool calls (read a file, edit code, run a command)
  4. The results are sent back to the LLM
  5. Repeat until the LLM says “I’m done” (no more tool calls)

That’s the whole architecture. The LLM never touches your filesystem. It only asks for things to happen, and your code makes them happen.

Both reference blogs emphasize this: the core loop is ~100 lines. Everything else is tooling around it.

File: packages/agent/src/agent-loop.ts L155-232

async function runLoop(
  currentContext: AgentContext,
  newMessages: AgentMessage[],
  config: AgentLoopConfig,
  signal: AbortSignal | undefined,
  emit: AgentEventSink,
  streamFn?: StreamFn
): Promise<void> {
  let hasMoreToolCalls = true;

  while (hasMoreToolCalls || pendingMessages.length > 0) {
    // Stream assistant response
    const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
    newMessages.push(message);

    // Check for tool calls
    const toolCalls = message.content.filter((c) => c.type === "toolCall");
    hasMoreToolCalls = toolCalls.length > 0;

    if (hasMoreToolCalls) {
      const toolResults = await executeToolCalls(currentContext, message, config, signal, emit);
      for (const result of toolResults) {
        currentContext.messages.push(result);
        newMessages.push(result);
      }
    }
  }
}

The key insight: the inner loop continues while hasMoreToolCalls is true. Each iteration streams a response, checks if the LLM wants to use tools, executes them, and loops again.