{"slug": "pi-agent-harness-what-a-unified-llm-api-and-agent-loop-reveal-about-tool-calling", "title": "Pi Agent Harness: What a Unified LLM API and Agent Loop Reveal About Tool-Calling Boundaries", "summary": "Pi, a self-extensible coding agent developed by the Gatsby team, reached 1.0 after nearly a year of development and is trending at #8 on GitHub with over 100,000 stars. The project's runtime layer, including its unified multi-provider LLM API and agent loop, reveals how tool-calling is normalized across OpenAI, Anthropic, and Google, while explicitly deferring permission boundaries to external isolation patterns like Docker or Kubernetes.", "body_md": "Pi hit 1.0 after nearly a year of development by the Gatsby team. It's trending at #8 on GitHub with 100K+ stars, positioned as a self-extensible coding agent with a unified multi-provider LLM API. The interesting part is not the coding agent itself. It's the runtime layer underneath: how Pi normalizes tool-calling across OpenAI, Anthropic, and Google, manages state across multi-step workflows, and explicitly punts on permission boundaries.\n\nThis is a case study in agent runtime design. Pi exposes the plumbing between reasoning (LLM calls) and execution (tool invocation), and it forces you to make a choice: convenience or isolation.\n\nEvery major LLM provider has a different tool-calling schema. OpenAI uses `tools`\n\nwith `function`\n\nobjects. Anthropic uses `tools`\n\nwith `input_schema`\n\n. Google uses `function_declarations`\n\n. Pi's `@earendil-works/pi-ai`\n\npackage abstracts this into a single interface.\n\nHere's what that looks like:\n\n``` js\nimport { createLLM } from '@earendil-works/pi-ai';\n\nconst llm = createLLM({\n  provider: 'openai', // or 'anthropic', 'google', custom endpoint\n  model: 'gpt-4',\n  apiKey: process.env.OPENAI_API_KEY,\n});\n\nconst response = await llm.chat({\n  messages: [{ role: 'user', content: 'What is the weather in SF?' }],\n  tools: [\n    {\n      name: 'get_weather',\n      description: 'Get current weather for a location',\n      parameters: {\n        type: 'object',\n        properties: {\n          location: { type: 'string' },\n        },\n        required: ['location'],\n      },\n    },\n  ],\n});\n```\n\nThe abstraction hides three things:\n\nThe impedance mismatch is real. OpenAI returns `tool_calls`\n\nas an array. Anthropic returns `content`\n\nblocks with `tool_use`\n\ntypes. Google returns `functionCall`\n\nobjects. Pi's abstraction layer maps all of these to a single `ToolCall`\n\ntype with `name`\n\n, `arguments`\n\n, and `id`\n\n.\n\nThe `@earendil-works/pi-agent-core`\n\npackage sits on top of the LLM API. It manages the agent loop: prompt, tool call, tool execution, result injection, repeat.\n\nThe runtime tracks:\n\nWhen a tool call happens, the runtime:\n\n`tool`\n\nrole message.This is a synchronous blocking loop. If a tool takes 30 seconds to run, the agent waits 30 seconds. If a tool fails, the error message goes back to the LLM, and the LLM decides what to do next (retry, skip, abort).\n\nPi does not have built-in retry logic or circuit breakers. If a tool throws an exception, the runtime catches it, serializes the error message, and appends it to the conversation history as a tool result with an error flag.\n\nThe LLM sees:\n\n```\n{\n  \"role\": \"tool\",\n  \"tool_call_id\": \"call_abc123\",\n  \"content\": \"Error: ENOENT: no such file or directory, open '/tmp/missing.txt'\"\n}\n```\n\nThe LLM can:\n\nThis is a design choice. Pi treats the LLM as the orchestrator. The runtime is just plumbing. If you want retries, timeouts, or fallback logic, you implement them in your tool handlers or wrap the agent loop.\n\nPi's documentation is explicit: \"Pi does not include a built-in permission system for restricting filesystem, process, network, or credential access. By default, it runs with the permissions of the user and process that launched it.\"\n\nThis is not an oversight. It's a trade-off. Adding a permission system means:\n\nPi punts on this. If you need isolation, you containerize. The docs outline three patterns:\n\n| Pattern | Boundary | Overhead | Use Case |\n|---|---|---|---|\nGondolin extension |\nBrowser extension sandbox | Low | Keep Pi and provider auth on host, isolate tool execution in browser |\nDocker Compose |\nContainer network + volume mounts | Medium | Run Pi in a container, mount specific directories, restrict network access |\nKubernetes |\nPod security policies + network policies | High | Multi-tenant deployments, strict resource limits, audit logs |\n\nThe Gondolin pattern is interesting. It runs Pi on the host but executes tools inside a browser extension sandbox. The extension has limited filesystem access and no direct network access. Tool results flow back to Pi over a message-passing bridge.\n\nDocker Compose is the middle ground. You define a `docker-compose.yml`\n\nwith volume mounts for the directories Pi needs to read/write, and you use Docker's network isolation to block outbound connections except to specific hosts (LLM APIs, internal services).\n\nKubernetes is the heavy option. You use pod security policies to drop capabilities, network policies to enforce egress rules, and resource quotas to prevent runaway tool execution.\n\nPi is four packages:\n\n`@earendil-works/pi-ai`\n\n`@earendil-works/pi-agent-core`\n\n`@earendil-works/pi-coding-agent`\n\n`@earendil-works/pi-tui`\n\nThe flow:\n\n```\nUser input → TUI → Agent Core → LLM API → Provider (OpenAI/Anthropic/Google)\n                        ↓\n                   Tool Registry\n                        ↓\n                   Tool Handlers (filesystem, shell, etc.)\n                        ↓\n                   Tool Results → Agent Core → LLM API → Provider\n```\n\nThe agent core is stateful. It holds the conversation history in memory. If the process crashes, you lose the session. There is no built-in persistence layer. If you need durable state, you wrap the agent core and snapshot the conversation history to disk or a database after each turn.\n\nThe coding agent can modify its own tools. It has a `create_tool`\n\ntool that generates a new tool definition and registers it at runtime. The tool definition is TypeScript code. The agent writes it, saves it to disk, and dynamically imports it.\n\nThis is powerful and dangerous. The agent can:\n\nThere is no approval gate. If the LLM decides to create a tool, the tool gets created. If you're running Pi with your AWS credentials in the environment, the agent can create a tool that reads them and sends them to an external server.\n\nThe mitigation is containerization. If Pi runs in a container with no AWS credentials, no network access, and a read-only filesystem except for a scratch directory, the blast radius is limited.\n\nPi includes a `@earendil-works/pi-telemetry`\n\npackage. It defines vendor-neutral telemetry contracts: structured logs, traces, and metrics. The reference adapter writes to stdout in JSON format.\n\nYou can plug in your own adapter to send telemetry to Datadog, Honeycomb, or an OpenTelemetry collector. The telemetry schema includes:\n\nThis is useful for debugging multi-step workflows. You can trace a failed tool call back to the LLM response that triggered it, see the arguments that were passed, and inspect the error message.\n\nPi's failure modes are predictable:\n\nThe common thread: Pi does not hide errors. It surfaces them to the LLM and lets the LLM decide what to do.\n\n| Aspect | Pi's Choice | Alternative | Implication |\n|---|---|---|---|\nPermission system |\nNone (user's permissions) | Built-in sandboxing | You must containerize for isolation |\nState persistence |\nIn-memory only | Automatic snapshots | Session lost on crash |\nTool retries |\nNone (LLM decides) | Automatic retry with backoff | More code in tool handlers |\nContext window |\nNo truncation | Sliding window or summarization | Agent stops when context overflows |\nProvider lock-in |\nUnified API | Provider-specific code | Easier to switch providers, harder to use provider-specific features |\n\n**Use Pi when:**\n\n**Avoid Pi when:**\n\nPi is plumbing. It solves the provider abstraction problem and gives you a basic agent loop. Everything else (security, persistence, observability, error handling) is your responsibility. That's a feature, not a bug. It keeps the runtime small and forces you to think about the boundaries that matter for your deployment.", "url": "https://wpnews.pro/news/pi-agent-harness-what-a-unified-llm-api-and-agent-loop-reveal-about-tool-calling", "canonical_source": "https://dev.to/mech_app_ai/pi-agent-harness-what-a-unified-llm-api-and-agent-loop-reveal-about-tool-calling-boundaries-51a5", "published_at": "2026-09-02 20:07:19+00:00", "updated_at": "2026-09-02 20:24:13.576468+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "developer-tools", "large-language-models"], "entities": ["Pi", "Gatsby", "OpenAI", "Anthropic", "Google", "Docker", "Kubernetes"], "alternates": {"html": "https://wpnews.pro/news/pi-agent-harness-what-a-unified-llm-api-and-agent-loop-reveal-about-tool-calling", "markdown": "https://wpnews.pro/news/pi-agent-harness-what-a-unified-llm-api-and-agent-loop-reveal-about-tool-calling.md", "text": "https://wpnews.pro/news/pi-agent-harness-what-a-unified-llm-api-and-agent-loop-reveal-about-tool-calling.txt", "jsonld": "https://wpnews.pro/news/pi-agent-harness-what-a-unified-llm-api-and-agent-loop-reveal-about-tool-calling.jsonld"}}