Pi Agent Harness with Local Ollama

Configure Pi Coding Agent to run entirely on your local Ollama model fleet.

Introduction

30 Second Summary

AI coding agents are powerful, but most of them require cloud API keys and send your code to someone else's servers. Running everything locally means your code stays private, your models are always available, and you pay nothing per token.

In this project, you will install and configure the Pi Agent Harness to use your local Ollama models, engineer its context system, explore its session architecture, and build a custom TypeScript extension that adds new capabilities to the agent.

What You'll Build

Picture this: you open a terminal in any project directory, type pi, and an AI coding agent powered entirely by your local hardware starts reading your files, writing code, and running tests. No API keys, no cloud bills, no data leaving your machine.

By the end of this project, you'll have:

  • Pi Agent Harness connected to your 5 local Ollama models with on-the-fly model switching mid-conversation via /model and Ctrl+P.
  • A project-level AGENTS.md file that shapes how the agent writes code, following your conventions without touching Pi's internals.
  • Session branching and context compaction working within your local models' context windows, so you can explore multiple approaches and manage token limits effectively.
  • Secret Mission: Build a permission-gate extension that intercepts dangerous tool calls and prompts for confirmation before execution.

Are there any prerequisites?

You'll need Ollama installed with these models already pulled: gemma4:12b, qwen3.6:27b, qwen3-coder:30b, gemma4:e4b, and gpt-oss:20b.

You'll also need Node.js 24 installed via nvm, and familiarity with TypeScript and terminal workflows.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

You have powerful local models sitting on your machine. This project turns them into a programmable coding agent you fully control and can extend.

Install and Configure Pi for Ollama

Pi Agent Harness is distributed as a global npm package. Without a provider configuration file, Pi has no way to reach your local Ollama models and defaults to showing only cloud providers.

In this step, you'll install Pi and create the configuration that connects it to all 5 of your local models. Once this is done, Pi treats Ollama identically to any cloud provider through its unified LLM API.

In this step, get ready to:
  • Install the Pi Coding Agent globally via npm.
  • Create the provider and model configuration files.
  • Verify Pi can see all 5 of your Ollama models.
Install Pi globally

Pi's architecture separates the harness (orchestration, tool execution, session management) from the models it connects to. The harness itself is a single npm package you install globally.

  • Install the Pi Coding Agent package globally by running this command:
npm install -g --ignore-scripts @earendil-works/pi-coding-agent

What does this command do?

The --ignore-scripts flag skips dependency lifecycle scripts that aren't needed for normal installs. This is recommended by the Pi project for a cleaner install.

  • Verify the installation succeeded by running:
pi --version

You should see 0.80.7 printed to the terminal.

Command not found?

Make sure your Node.js 24 environment is active. Run node --version to confirm. If npm's global bin directory isn't in your PATH, run npm bin -g to find it, then add it to your shell profile.

help me fix Pi not being found after install

Create the Ollama provider configuration

Pi's unified LLM API (pi-ai) treats every provider the same. Ollama exposes an OpenAI-compatible endpoint, so Pi uses the openai-completions API type to talk to it.

  • Create the Pi agent configuration directory by running:
mkdir -p ~/.pi/agent
  • Confirm the directory exists by running:
ls ~/.pi/agent

The directory should exist (empty output is fine, no error means it was created).

  • Create ~/.pi/agent/models.json and paste the following content (this is the top of the file, containing the provider settings and your first 3 models):
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "compat": {
        "supportsDeveloperRole": false,
        "supportsReasoningEffort": false
      },
      "models": [
        {
          "id": "qwen3-coder:30b",
          "name": "Qwen3 Coder 30B",
          "contextWindow": 131072,
          "maxTokens": 16384
        },
        {
          "id": "qwen3.6:27b",
          "name": "Qwen 3.6 27B",
          "contextWindow": 131072,
          "maxTokens": 16384
        },
        {
          "id": "gpt-oss:20b",
          "name": "GPT-OSS 20B",
          "reasoning": true,
          "contextWindow": 131072,
          "maxTokens": 16384
        },
  • Continue the file with the remaining 2 models and the closing brackets:
        {
          "id": "gemma4:12b",
          "name": "Gemma 4 12B",
          "input": ["text", "image"],
          "contextWindow": 131072,
          "maxTokens": 8192
        },
        {
          "id": "gemma4:e4b",
          "name": "Gemma 4 E4B",
          "contextWindow": 32768,
          "maxTokens": 8192
        }
      ]
    }
  }
}

What does this configuration do?

The baseUrl points Pi at Ollama's OpenAI-compatible endpoint on port 11434. The apiKey is a dummy value. Ollama ignores authentication, but Pi requires a value to mark the provider as connected.

The compat block is critical for local models. Ollama doesn't support the developer role or reasoning_effort parameter that cloud providers use. Without these flags, Pi sends unsupported parameters that Ollama rejects.

Each model entry declares its contextWindow and maxTokens limits. Pi uses these to manage compaction and output size for each model.

  • Verify the file was saved correctly by running:
cat ~/.pi/agent/models.json | head -10

You should see the first 10 lines of your JSON with the baseUrl and api fields visible.

Seeing a JSON parse error or empty output?

Make sure you included both code blocks together as one file. The closing brackets from the second block must follow directly after the last model entry. Check for missing commas between model objects.

help me fix my models.json formatting

Create settings and verify the connection

Pi needs one more file to know which provider and model to use by default when it starts up.

  • Create ~/.pi/agent/settings.json with the following content:
{
  "defaultProvider": "ollama",
  "defaultModel": "qwen3-coder:30b"
}

What does this file do?

This tells Pi to connect to your Ollama provider by default and start each session with qwen3-coder:30b as the active model. You can switch models on the fly during a session, but this sets your starting point.

  • Start Pi from any directory to test the connection by running:
cd /tmp
pi

Pi should start and show you an input prompt. Before you type anything, think about this: how many models do you expect to see when you open the model picker?

  • Inside Pi, type /model to open the model picker.
  • Select qwen3-coder:30b from the list.

You should see all 5 of your Ollama models listed: Qwen3 Coder 30B, Qwen 3.6 27B, GPT-OSS 20B, Gemma 4 12B, and Gemma 4 E4B. If they all appear and you can select one, your provider configuration is working.

  • Type /quit to exit Pi.

Models not showing up in the picker?

Make sure Ollama is running. Open another terminal and run ollama list to confirm your models are still pulled.

Double-check that models.json is in ~/.pi/agent/ (not ~/.pi/ directly). The file hot-reloads each time you open /model, so you can fix it and retry without restarting Pi.

help me fix Pi not finding my Ollama models

✔️ Awesome, I've got everything!

Great. Double check you've saved both files, then move on.

ⓧ I'd like to double check the full code

Scroll up and confirm you have these two files in ~/.pi/agent/:

  • models.json with the Ollama provider block (baseUrl, api, apiKey, compat) and all 5 model entries (qwen3-coder:30b, qwen3.6:27b, gpt-oss:20b, gemma4:12b, gemma4:e4b), as shown in the two JSON code blocks above.
  • settings.json with defaultProvider set to ollama and defaultModel set to qwen3-coder:30b.

You can verify by running:

cat ~/.pi/agent/settings.json

This should print your 4-line settings file.

Your harness is installed and connected to your local model fleet. Next up, you'll run the agent loop on a real task and see exactly how Pi orchestrates the LLM-tool cycle.

Run the Agent Loop on a Real Task

Pi is installed and connected to your 5 Ollama models. Now it's time to see what the harness actually does when you give it a real coding task.

The agent loop is the core of Pi's architecture. When you type a prompt, Pi sends it (plus a system prompt) to the LLM. The LLM responds with tool calls. Pi executes those calls, feeds the results back, and the loop repeats until the task is complete. In this step, you'll watch this loop unfold in real time against your local model.

In this step, get ready to:
  • Create a small project for Pi to work in.
  • Issue a coding task and observe the tool-calling loop in action.
  • Inspect the session metadata to see what was recorded.
Create a small project to work in

Pi needs a Git repository to work with. It uses the repo root to scope file operations and understand project boundaries.

  • Create a project directory, initialize a git repo, and commit starter files by running these commands:
mkdir -p ~/pi-harness-lab && cd ~/pi-harness-lab
git init
echo '# Pi Harness Lab' > README.md
echo 'console.log("hello");' > index.js
git add -A && git commit -m "init"

What does this set up?

  • ~/pi-harness-lab is a dedicated workspace for this project.
  • git init creates a repo so Pi knows the project boundaries.
  • README.md and index.js give Pi existing files to read when it assesses the project.
  • Confirm the files exist by running:
ls

You should see README.md and index.js in the output.

  • Start Pi in this project directory by running:
pi

Pi starts and loads the project context. You're now inside the Pi REPL, ready to issue tasks.

Pi not starting?

  • Make sure you ran cd ~/pi-harness-lab first. Pi needs to launch from inside a directory.
  • Confirm Ollama is running. Pi connects to the model on startup.
  • If you see a provider error, double-check that ~/.pi/agent/models.json has the correct baseUrl value.

Help me troubleshoot why Pi won't start in my project directory

Issue a task and observe the tool-calling loop

Now for the interesting part. You'll give Pi a coding task and watch the orchestration layer in action.

  • Type this prompt into Pi and press Enter:
Create a Node.js script called fib.js that exports a function to compute the nth Fibonacci number, then create a test file that verifies fib(10) === 55

Watch the output carefully. You'll see Pi's built-in tools firing in sequence as the model decides what to do.

What are you seeing?

Pi gives the model only 4 default tools. The model decides which to call and in what order. Pi executes each call and feeds the result back. This is the "harness" part: Pi harnesses the LLM's output into real file-system actions.

  • read reads existing files so the model understands the project.
  • write creates new files on disk.
  • bash runs shell commands (e.g., node test.js to verify the code works).
  • edit modifies existing file content.

Wait for Pi to finish. You'll see it create fib.js, create a test file, and then run the test using bash. The test should pass.

Model seems stuck or producing errors?

  • Local models can take longer to respond than cloud APIs. Give it 30-60 seconds before assuming something is wrong.
  • If the model produces malformed tool calls, try switching to a different model with /model and selecting qwen3.6:27b. Then re-issue the prompt.

Help me debug why my local model isn't completing tool calls in Pi

Inspect the session

Every message, tool call, and result Pi processed was recorded in a session file. The /session command lets you peek at the metadata.

Before you run this, what do you think the session recorded? How many messages do you expect to see from a single prompt that triggered multiple tool calls?

  • Type /session into Pi and press Enter.
/session

You'll see session metadata: message count, token usage, cost ($0.00 for local models), and the session file path. The message count is higher than you might expect. Your one prompt generated multiple messages because each tool call and its result counts as a separate message in the loop.

Why so many messages from one prompt?

A single prompt triggers this sequence: your message goes to the LLM, the LLM responds with a tool call, Pi executes it and sends the result back, then the LLM decides whether to call another tool or finish. Each round trip adds messages. Pi persists all of this as JSONL in ~/.pi/agent/sessions/.

  • Confirm that fib.js exists by checking your project files. Type this into Pi:
List the files in the current directory

Pi uses bash to run ls and shows you the project contents. You should see fib.js, your test file, README.md, and index.js all present.

You've just watched the full agent loop in action. Pi sent your prompt to the LLM, the model called tools, Pi executed them, and the results flowed back until the task was done. Next up, you'll learn how to shape the agent's behavior by engineering its context.

Engineer the Agent's Context

You've seen the agent loop in action. Pi dispatched tool calls, executed them, and iterated until your Fibonacci task was complete.

Right now Pi treats your project generically. It doesn't know your conventions, preferred patterns, or constraints. The harness solves this through resource discovery: it loads AGENTS.md files at startup and injects them into the system prompt. Without this, the agent makes assumptions that may not match your workflow.

In this step, get ready to:
  • Observe Pi's generic behavior when given no conventions.
  • Create a project-level AGENTS.md that shapes Pi's output to follow your TypeScript conventions.
  • Set up a global AGENTS.md and understand how Pi's resource discovery hierarchy works.
Observe the limitation

Pi produced working code in Step 2. But it had no idea about your preferences for return types, error patterns, or documentation style. It guessed. Let's make that gap visible.

Before you type this prompt, think about what you expect. Will Pi use explicit return types? Will it add JSDoc? Will it use a specific error handling pattern?

  • In your Pi session in ~/pi-harness-lab (run pi in that directory if you've exited), type this prompt:
Convert fib.js to TypeScript with proper types and error handling

Pi produces valid TypeScript. But look closely at the output. It has no idea about your preferences: maybe you want explicit return types, maybe you require a Result<T, E> error pattern instead of throwing, maybe you need JSDoc comments. Pi guesses, and it guesses generically.

Why does Pi guess?

Pi's system prompt tells the model what tools are available and how to behave, but it says nothing about YOUR coding conventions. The model falls back on its training data defaults.

This is the gap that context engineering fills. You'll shape Pi's behavior without modifying any of its internals.

Create project-level AGENTS.md

Pi loads AGENTS.md files from your project directory and injects their content into the system prompt at startup. This is how you shape the agent's behavior for a specific project.

  • Open a separate terminal tab (keep Pi running in the other). Create the AGENTS.md file in your project root by running this command:
cat > ~/pi-harness-lab/AGENTS.md << 'EOF'
# Project Conventions

## TypeScript
- Always use explicit return types on exported functions
- Use `Result<T, E>` pattern for error handling (return objects, never throw)
- Prefer `const` assertions and `satisfies` over type annotations where possible
- All functions must have JSDoc with @param and @returns

## Testing
- Use Node.js built-in test runner (`node:test`)
- Test files go in `__tests__/` directory
- Name pattern: `<module>.test.ts`

## General
- No default exports, always named exports
- Prefer functional patterns over classes
EOF

What do these conventions do?

  • The TypeScript section tells Pi to always annotate return types, use a Result<T, E> pattern (returning success/error objects instead of throwing), and require JSDoc on every function.
  • The Testing section standardizes test location (__tests__/) and the built-in node:test runner.
  • The General section enforces named exports and functional patterns over classes.
  • Confirm the file was created by running:
cat ~/pi-harness-lab/AGENTS.md

You should see the full conventions file printed to your terminal.

  • Switch back to your Pi session. Reload Pi's context without restarting by typing:
/reload

This time, what do you think Pi's TypeScript output will look like? Will it be different from before?

  • Ask the same question again:
Convert fib.js to TypeScript with proper types and error handling

The output now follows your conventions: explicit return types, the Result pattern for error handling, and JSDoc comments with @param and @returns. The harness loaded AGENTS.md into the system prompt, shaping behavior without you modifying Pi's internals.

Output still looks generic?

  • Make sure AGENTS.md is in the ~/pi-harness-lab/ directory (not a subdirectory). Pi discovers it at the project root.
  • Confirm you typed /reload after creating the file. Pi loads context files at startup, and /reload triggers a fresh load without restarting.
  • Local models may follow instructions less strictly than cloud models. If you see partial compliance, the file is working. Try a more explicit prompt like "follow AGENTS.md strictly".

Pi still ignoring my AGENTS.md conventions?

Understand the resource discovery hierarchy

Pi loads context files in a specific order. Later files can override earlier ones, letting you set baseline behavior globally and specialize it per project.

How does the AGENTS.md hierarchy work?

Pi loads AGENTS.md in this order:

  • ~/.pi/agent/AGENTS.md (global, applies to all projects).
  • Parent directory AGENTS.md files (walking up to the git root).
  • Current directory AGENTS.md (most specific, highest priority).
  • In your separate terminal tab, create a global AGENTS.md with baseline preferences by running this command:
cat > ~/.pi/agent/AGENTS.md << 'EOF'
# Global Agent Instructions

- Be concise in explanations
- Prefer the simplest solution that works
- Always verify changes with a test or command before reporting done
EOF

What does the global file do?

These three rules apply to every project you open with Pi. The agent will keep explanations short, avoid over-engineering, and always run a verification step before marking a task complete.

  • Switch back to your Pi session and reload context:
/reload

Now Pi has both files loaded. What do you expect to be different about its behavior this time?

  • Ask Pi to do the TypeScript conversion one more time:
Convert fib.js to TypeScript with proper types and error handling

The output now reflects both layers. Your global preferences (concise, simplest solution, verify before done) combine with your project conventions (explicit types, Result pattern, JSDoc). Pi may also run a test or verification command before reporting the task complete.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files. Your context engineering is in place.

ⓧ I'd like to double check the full code

Here's the project-level AGENTS.md at ~/pi-harness-lab/AGENTS.md:

# Project Conventions

## TypeScript
- Always use explicit return types on exported functions
- Use `Result<T, E>` pattern for error handling (return objects, never throw)
- Prefer `const` assertions and `satisfies` over type annotations where possible
- All functions must have JSDoc with @param and @returns

## Testing
- Use Node.js built-in test runner (`node:test`)
- Test files go in `__tests__/` directory
- Name pattern: `<module>.test.ts`

## General
- No default exports, always named exports
- Prefer functional patterns over classes

And the global ~/.pi/agent/AGENTS.md:

# Global Agent Instructions

- Be concise in explanations
- Prefer the simplest solution that works
- Always verify changes with a test or command before reporting done

Not seeing both layers reflected?

  • Check that ~/.pi/agent/AGENTS.md exists. Run cat ~/.pi/agent/AGENTS.md in your other terminal to confirm.
  • Make sure you ran /reload after creating the global file. Pi only reads context files at startup or on reload.

Help me debug my AGENTS.md resource discovery setup.

Your context engineering is live. Pi now follows your global preferences and project-specific conventions automatically. Next up, you'll explore Pi's session architecture to manage context windows, switch models mid-conversation, and branch session history.

Explore Session Architecture and Model Switching

Your Pi harness now understands your project conventions and generates code that follows them. But with local models, there's a critical constraint you need to manage: context windows.

Your models range from 32K tokens (gemma4:e4b) to 131K tokens. Pi's session architecture handles this through compaction, branching, and model switching. Understanding this layer lets you work effectively within local model constraints rather than hitting silent context overflow.

In this step, get ready to:
  • Switch models mid-session without losing conversation history.
  • Branch your session tree and explore parallel conversation paths.
  • Trigger context compaction to fit within smaller model context windows.
Switch models mid-session

Pi maintains conversation state independently of which model is responding. This means you can swap models without restarting your session or losing any history.

  • In your active Pi session, press Ctrl+L to open the model picker.
  • Select gemma4:12b from the list.

The session continues with your full conversation history intact. Pi didn't restart. The harness maintains the conversation state independent of which model is responding.

  • Now switch to your smallest context model by typing /model and selecting gemma4:e4b (32K context window).
  • Give it a task by typing this prompt:
Add input validation to the fib function - reject negative numbers and non-integers

The model responds within its 32K context window. Pi tracks the contextWindow value you set in models.json and will auto-compact older messages if the session grows too large for the active model.

Why does model-independent state matter?

With local models, you often want to try different models on the same task. A coding task might work better on qwen3-coder:30b, while a quick question might be fine on the faster gemma4:e4b.

Because Pi stores the conversation independently, you can switch freely without losing context. The harness handles the translation between models transparently.

Model picker showing empty or failing?

Make sure Ollama is still running in the background. Pi re-reads models.json each time you open /model, so a stopped Ollama server means no models appear.

Help me debug why my model picker is empty in Pi

Session branching with /tree

Pi stores sessions as JSONL files with tree structure. Each message stores a parent reference, enabling non-destructive branching. You can jump back to any point and explore a different path without losing the original.

  • Inspect your session's message tree by typing:
/tree

You'll see a tree view of your conversation. Each node represents a message exchange. The current position is highlighted.

  • Use /tree to jump back to a point before the input validation task you just ran.
  • From that earlier point, ask for a different approach:
Instead of modifying fib.js directly, create a wrapper function that validates inputs before calling fib

This creates a branch. You now have two parallel paths in the same session file: the direct modification path and the wrapper approach.

  • Type /tree again to see the branching structure.

Why is tree-based storage useful for local models?

When you want to try the same task with different models or approaches, branching lets you compare results without losing any work. The JSONL file uses parent references per message, so branches are lightweight. No data is duplicated or destroyed.

Trigger and observe compaction

As conversations grow, they can exceed a model's context window. Pi handles this by compacting older messages into summaries while preserving recent context. This is especially important for gemma4:e4b with its 32K window.

  • Make sure you're still on the gemma4:e4b model (use /model to check or switch if needed).
  • Check your current session stats by typing:
/session

Note the current token count. This is the total tokens stored across all messages in your session.

  • Trigger manual compaction by typing:
/compact

What does compaction do?

Pi summarizes older messages while keeping recent exchanges intact. The LLM still has the gist of earlier conversation, but the token count drops to fit within the model's contextWindow value. Pi also auto-triggers compaction when a session approaches the active model's limit.

Before you check the result, what do you think happened to the token count after compaction?

  • Check your session stats again by typing:
/session

The token count is lower than before. Pi summarized older messages while preserving your recent context. The cost field shows $0 since you're running on local Ollama models.

Token count didn't change?

If your session is very short, compaction may not have much to summarize. Have a few more back-and-forth exchanges with the model (ask it to explain your code, suggest improvements, etc.) and then try /compact again.

Help me understand why /compact didn't reduce my token count

You now understand the three pillars of Pi's session architecture: model-independent state (switch freely), tree branching (explore without destroying), and compaction (fit within any context window). These work together to make local models with limited context practical for real work.

Next up, you'll extend Pi itself. You'll write a custom TypeScript extension that hooks into the agent loop, registers a tool the LLM can call, and adds a slash command you can invoke directly.

Secret mission

Build a Permission-Gate Extension

You've seen Pi's built-in tools in action. Now it's time to extend the harness itself. In this mission, you'll write a TypeScript extension that intercepts dangerous tool calls, registers a custom tool the LLM can invoke, and adds a slash command you can run directly.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • Pi Coding Agent global npm package (@earendil-works/pi-coding-agent).
  • Pi agent configuration directory (~/.pi/agent/) containing models.json, settings.json, and AGENTS.md.
  • Pi session files stored in ~/.pi/agent/sessions/.
  • Project directory ~/pi-harness-lab/ containing project files and the custom extension.

Keep everything running

No action needed. Choose this if you plan to keep using Pi as your daily coding agent.

  • Your ~/.pi/agent/models.json, settings.json, and AGENTS.md persist across sessions and apply to all projects.
  • The extension in ~/pi-harness-lab/.pi/extensions/ loads automatically whenever you run pi in that project directory.
  • Session files in ~/.pi/agent/sessions/ accumulate over time but are small JSONL files. Clean up old ones with standard file operations if needed.

Pause - I'll come back to this later

Since everything is local, there's nothing to shut down. Your files will be waiting for you when you come back.

  • All configuration in ~/.pi/agent/ stays in place. Run pi again anytime to resume.
  • Your session history is preserved. Use /tree to jump back to any previous conversation point.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to fully clean up.

  • Remove the project directory by running:
rm -rf ~/pi-harness-lab
  • Remove the Pi agent configuration directory (this deletes all models config, settings, global AGENTS.md, and session history):
rm -rf ~/.pi
  • Uninstall the Pi Coding Agent global package:
npm uninstall -g @earendil-works/pi-coding-agent
  • Confirm the uninstall by running:
pi --version
  • You should see command not found, confirming Pi has been removed.

Nice Work!

Nice Work!

You've built a fully local AI coding agent powered entirely by your own hardware. Pi Agent Harness is now connected to your Ollama model fleet, shaped by your project conventions, and extended with custom tooling you wrote yourself.

You've learned how to:

  • Install Pi Agent Harness and configure it to use 5 local Ollama models via the unified provider abstraction in models.json.
  • Observe the agent loop in action: your prompt goes to the LLM, the LLM responds with tool calls, Pi executes them, feeds results back, and the loop continues until the task is done.
  • Engineer agent context through the AGENTS.md resource discovery system at both global and project levels.
  • Explore session architecture: model switching, tree branching, and context compaction for local model context windows.
  • Build a custom TypeScript extension with event interception, a registered tool, and a slash command (Secret Mission).

Ready to quiz yourself?