Yesterday I finished reading Pi's multi-Provider API, context handoff, and structured tool results. Today I continued reading the second half of What I learned building an opinionated and minimal coding agent and verified the implementation against the current source code of earendil-works/pi.
After finishing, my primary concern is no longer "why doesn't Pi have this popular feature," but a different question: how much state should a coding agent harness really save for users?
Minimal agent scaffold: Does no step limit mean no boundaries?
Pi's basic agent loop is easy to describe: process user message, call the model, execute tool calls returned by the model, return results to the model, and repeat until the model stops calling tools.
User message
↓
LLM response
↓
Tool calls? ── no ──→ End
│
yes
↓
Execute tools
↓
Tool results
└──────────────→LLM responseThe author deliberately did not provide max_steps. His reasoning is not that models never loop, but that he doesn't use this parameter in his personal interactive workflow: the user watches the Agent work and can abort anytime, so a fixed upper limit might unnecessarily interrupt complex tasks midway.
When learning smolagents in the Hugging Face Agent Course, I used max_steps, so my first reaction was: is it just a teaching parameter? The answer is no. In unattended, pay-per-use, multi-tenant, or side-effecting Agent systems, step limits are an important resource fuse that restricts infinite loops, costs, and runtime.
This made me realize that Pi's choice must be understood within its use case context. Personal interactive coding agents can rely on manual abort; production Agents usually also need step, time, token, and cost budgets. There is no context-independent single correct answer.
The author considers more valuable capabilities of the Agent class to be state management, event subscription, message queues, attachments, and transport abstraction. They don't solve "how many loops at most," but rather how Agents truly integrate into applications: how the UI observes runs, how users add instructions mid-work, how images enter messages, and how browsers call models through proxies.
CLI, TUI, and text_delta
At first, I didn't quite understand: Codex CLI, Claude Code CLI, and Pi all seem to have terminal interfaces—what's the relationship between CLI and TUI?
Now my understanding is that they describe two different levels:
- CLI (Command-Line Interface) describes how users invoke a program through "commands + arguments."
- TUI (Terminal User Interface) describes how, after the program runs, it provides a persistent interface in the terminal that can respond to keyboard input and update locally.
The typical lifecycle of a CLI is:
Input a command
→ Program parses parameters
→ Execute task
→ Output result
→ Process exitThe typical lifecycle of a TUI is:
Program startup
→ Continuously display interface
→ Wait for keyboard input
→ Update messages, input box, or status
→ User actively exitsThey are not mutually exclusive. A program can start via CLI and then enter TUI. The following scenarios make the distinction clearer.
Scenario one: View help, CLI only
pi --helpHere --help is a CLI argument. Pi prints the help text and exits immediately, with no continuous interaction, input boxes, or interface redraws, so no TUI is needed.
Shell
→ pi parse --help
→ stdout output help
→ ExitScenario two: Execute a single task, still primarily CLI
pi --print "Explain this project's directory structure"The user provides a prompt through CLI arguments. Pi executes the Agent, prints the answer, and exits. Streaming output might be used in between, but "gradually outputting text" itself does not constitute TUI; if the program just keeps appending to stdout with no input components and no local redraws, it remains closer to traditional CLI.
Scenario three: Start Pi directly, CLI enters TUI
piThe shell first starts pi through CLI. Since no one-time prompt was specified, the program enters interactive mode:
CLI: Responsible for starting processes and parsing startup parameters
↓
TUI: Display chat history, input box, tool calls, and execution statusAt this point, users can ask continuous questions, use keyboard shortcuts, edit multi-line input, and abort anytime. Although the chat history still displays in the terminal, it is no longer simply "output some text after command execution," but rather a continuously running terminal application.
Scenario four: JSON mode has CLI but no human-facing TUI
pi --mode json "Check project"This also starts Pi through CLI, but the output target is another program, not a human user. Pi writes Agent events as JSONL and needs no chat bubbles, input boxes, colors, or keyboard shortcuts:
CLI startup
→ Headless Agent
→ JSON events written to stdout
→ jq, CI, or other programs readTherefore, "whether running in a terminal" cannot be used to distinguish CLI from TUI. A more accurate judgment is:
| Question | CLI | TUI |
|---|---|---|
| How does the user start and configure the program? | Commands and arguments | Usually not responsible |
| Does the program continuously wait for interaction? | Not necessarily | Usually yes |
| Does it handle keyboard shortcuts and input components? | Usually not | Yes |
| Does it move the cursor and redraw locally? | Usually not needed | Usually needed |
| Can it output and exit immediately? | Common | Uncommon |
This is also why product names like "Codex CLI" or "Claude Code CLI" don't indicate that there's no TUI internally: CLI is their launch and distribution form, while the interaction mode is implemented by TUI capabilities.
How text_delta connects Agent and TUI
When the model outputs, the Provider doesn't wait for the complete answer to be generated before returning it all at once. Instead, it continuously sends increments. Pi represents each new text snippet uniformly as text_delta:
text_delta: "I will"
text_delta: "first check"
text_delta: "project structure"
text_delta: "。"The Agent core accumulates deltas in sequence into a complete assistant message. Different run modes can consume the same event but adopt different presentation approaches:
Provider SSE
→ pi-ai generates text_delta
→ Agent core accumulates messages
├─ TUI: Partial redraw of messages being generated
├─ JSON mode: Output one line JSON event
└─ Print mode: Append text directly to stdoutSo text_delta is not a TUI-exclusive concept, but an Agent streaming event. TUI is just one consumer of it. The "model is typing" that users see in the interactive interface is essentially the result of three layers collaborating: Provider stream, Agent event, and TUI rendering.
Session branching: Conversations are a tree
Pi's session branching is not Git branches, but conversation branches. Each session entry has an id and parentId, with the end of the current path represented by the active leaf.
A: Initial question
├─ B: Solution one
│ └─ C: Continue solution one
└─ D: Return to A and try solution two
└─ E: Continue solution twoWhen creating a new branch, Pi doesn't delete old messages but rather moves the leaf to a historical entry. The next message uses that entry as parentId, naturally forming another child path. When constructing model context, Pi only traces back from the current leaf along parentId to root, not passing the entire tree to the model.
When leaving a path that has already accumulated much work, Pi can also generate a branch summary, attaching key discoveries from the old path to the new location. This again demonstrates context engineering: preserving "what the old branch discovered" is not the same as replaying all tool outputs from the old branch.
But branching only changes the conversation path. File modifications already executed in old branches are not automatically undone; restoring code state still requires Git checkpoints or other external mechanisms.
Headless operation: Handing Agent core to other applications
Pi provides two ways to run without starting TUI:
| Mode | Primary use |
|---|---|
| JSON streaming | One-time tasks, Shell, CI, event logging |
| RPC mode | IDE, desktop UI, cross-language apps, long-running subprocesses |
JSON mode writes standardized Agent events line-by-line as JSONL:
{"type":"agent_start"}
{"type":"message_update","assistantMessageEvent":{"type":"text_delta","delta":"Hello"}}
{"type":"agent_end"}It outputs not the raw Provider SSE, but Agent events already unified by Pi.
RPC mode receives JSONL commands through stdin and returns responses and asynchronous events through stdout:
{"id":"req-1","type":"prompt","message":"Check project structure"}
{"id":"req-1","type":"response","command":"prompt","success":true}External programs can also send commands such as steer, follow_up, abort, get_state, and set_model. In other words, Pi is not just a terminal interface, but an Agent runtime that can be driven by other UIs and processes.
Minimal system prompt: Let the host read AGENTS.md
Pi does not merely tell the model "there might be an AGENTS.md in the project; remember to read it when needed." Instead, ResourceLoader searches for AGENTS.md or CLAUDE.md in the global, parent, and current directories at startup, then appends the file content directly to the system prompt:
<project_context>
<project_instructions path=".../AGENTS.md">
Project rules content
</project_instructions>
</project_context>This eliminates an unstable link: the model does not need to first realize the file exists and then decide to call read. It can see the rules from the first turn.
Of course, "already injected" does not mean "the model will necessarily comply." Rule conflicts, excessive context length, and model attention can still lead to oversights. What Pi improves is the reliability of discovery and loading, not turning a probabilistic model into a deterministic rule engine.
Minimal toolset: What kind of Agent is Pi?
According to the Hugging Face Agent Course classification, Pi belongs by default to a Tool Calling Agent, not a Code Agent like smolagents. The model's action is a structured tool call:
{
"name": "read",
"arguments": { "path": "src/index.ts" }
}A Code Agent's main action is a piece of Python code to be executed. Although Pi adopts the Tool Calling protocol, bash can run arbitrary scripts, so in practical capability it approximates a Code Agent.
Pi provides only read, write, edit, and bash by default. The current source code places them in packages/coding-agent/src/core/tools/, and each tool consists of a name, description, TypeBox schema, and execute() function.
Custom tools are registered via extension. The following structure comes from the project's current minimal example:
// packages/coding-agent/examples/extensions/hello.ts
import { Type } from "@earendil-works/pi-ai";
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
const helloTool = defineTool({
name: "hello",
label: "Hello",
description: "A simple greeting tool",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: { greeted: params.name },
};
},
});
export default function (pi: ExtensionAPI) {
pi.registerTool(helloTool);
}It serves the same purpose as smolagents' @tool/Tool: defining name, parameters, and execution function. The difference is that Pi uses plain TypeScript objects and extension APIs instead of decorators or base classes.
YOLO and extension: Guardrails are not security boundaries
YOLO is short for "You Only Live Once." In the coding agent context, it does not encourage the model to take risks; rather, it means the harness does not pop up a permission confirmation before each operation: the Agent can directly call file and command tools, and the tool process inherits the current user permissions of the process that started Pi.
Normal permission mode: Agent action → User confirmation → Execute or reject
YOLO mode: Agent action ─────────→ Execute directlyTherefore, Pi's YOLO mode means it can directly read and modify files that the current user has access to, and can also execute commands that the current user can execute via bash. It is not synonymous with administrator privileges; what Pi can do still depends on what operating system permissions the launched process itself possesses.
Guardrail refers to a rule for behavior protection. It sits between the Agent producing an action and the tool actually executing it, inspecting operations:
Agent generates tool call
→ Guardrail check
→ Allow / Reject / Modify / Request human confirmation
→ Tool executionA whitelist is one form of guardrail, for example only allowing npm run check, git diff, and file operations within the project directory; other forms include dangerous command blacklists, sensitive path protection, parameter validation, execution budget, human approval, and audit logs.
Pi reads and writes files and executes commands with current user permissions by default. The author believes that as long as an Agent can read data, run code, and access the network, application-layer permission prompts cannot fundamentally prevent data leaks.
I agree with simple, transparent defaults, but I am also curious: can one build a safer Agent through extensions? The answer is that risk can be reduced, but true isolation cannot be achieved.
Extensions can implement guardrails:
- Only allow specified commands or paths;
- Intercept dangerous
bash; - Request confirmation before modifying files, installing dependencies, or pushing to Git;
- Replace built-in tools, exposing only fixed
run_tests; - Clean environment variables passed to subprocesses;
- Record audit logs.
However, extensions and Pi typically still run under the same process permissions. As long as arbitrary bash is retained, incomplete string checks can be bypassed by scripts or indirect commands. True security boundaries still come from containers, VMs, or OS sandboxes.
AGENTS.md Behavior hints
Extension guardrail Application layer policy
Container / VM System permission boundaryA more reliable combination is to let extensions handle "policies" and let a properly configured sandbox handle the fallback—ensuring that even if guardrails are bypassed, actual impact remains within a controllable range.
A series of Nos: What does the author really refuse?
The article then continuously discusses No built-in to-dos, No plan mode, No MCP support, No background bash, and No sub-agents. On the surface it looks like removing features, but in essence it is reducing internal state within the harness, context burden, and unobservable mechanisms.
Todo: Plan state and actual state may become out of sync
An Agent already has to track code, tests, tool results, and new user instructions. Built-in todos add another declared state:
Real working state ↔ todo stateExploratory tasks constantly overturn initial assumptions, and todos easily become outdated. The model must frequently update pending, in_progress, and completed, and may even mistake "checking off all items" as the final goal.
The author does not object to task recording; rather, he recommends writing to a regular TODO.md. Files are visible to users, editable, version-controllable, and usable across sessions. For fixed production workflows, structured todos still have value; the author's judgment primarily targets personal coding workflows.
Plan: Planning should be observable ordinary work
The author believes that most planning only requires telling the Agent: investigate and formulate a plan first, do not modify files. All reading, searching, and inference stay in the currently observable session, and users can steer at any time.
When persistence is needed, write PLAN.md; when enforcing read-only mode is needed, only expose read, grep, find, and ls.
His main criticism of Claude Code Plan Mode is not the Markdown file, but that investigation may be handed to an opaque sub-agent. Users find it difficult to confirm what it has read and what it has missed, and also difficult to intervene in time before an erroneous plan direction is formed.
MCP: Not against the protocol, but against unconditional injection
The author uses CLI tool + README to replace most MCP Servers:
Agent knows a certain CLI exists
→ Current task actually needs it
→ Read README
→ Invoke CLI via bash
→ Read stdoutIf the current task does not need that tool, do not load the full specification. This is progressive disclosure.
The prevalence of 1M context windows has eased the question of "can it fit," but has not eliminated input cost, latency, prompt cache, and tool selection noise. Tool definitions of approximately 20K tokens occupy roughly 8% of a 256K context and about 2% of a 1M context, but facing the model with many irrelevant schemas in every turn is still suboptimal.
One must also fairly distinguish between the MCP protocol and naive client implementations. Current MCP official best practices likewise recommend progressive discovery: search for tools first, load the complete schema only when needed. CLI and MCP are not an absolute either-or: simple, local, composable capabilities suit CLI; cross-application, remote, strongly structured integration suits MCP.
Background bash: Entrust long-lived terminal state to tmux
Normal bash is synchronous: if a command does not end, the Agent cannot proceed. Background bash requires managing PIDs, output, input, cleanup, and recovery.
The author chooses tmux instead of reimplementing a process manager within Pi:
tmux new-session -d -s dev "npm run dev"
tmux capture-pane -t dev -p
tmux list-sessions
tmux attach-session -t devtmux is a terminal multiplexer. Programs run in independent persistent terminal sessions, and the Agent can query and send input, while users can also attach and take over manually. Even if context compacts or the Pi session ends, tmux list-sessions can still rediscover the state.
The built-in background process API is more model-friendly and easier to automatically clean up; the advantage of tmux is that state exists outside the harness, offering transparency and mature query methods. This is still a tradeoff between automation and observability.
Sub-agent: Independent Process Instead of Built-in Abstraction
The sub-agent in the article and smolagents multi-agent belong to the same conceptual family: the main Agent delegates subtasks to another Agent. Multi-agent is a broader system, and sub-agent is a common hierarchical structure.
Pi doesn't have a dedicated sub-agent tool, but the main Pi can launch another Pi through bash:
Main Pi
→ pi --print "Review this change"
→ Child Pi executes independently
→ stdout / artifact returns to main PiBehaviorally it's still a sub-agent, except it doesn't become a first-class status in the agent core. The independent Pi has its own context, model, process, and session; the failure boundary is also clearer. If the user saves the session or puts it into tmux, they can open it later and continue asking questions.
This approach of launching another Pi through bash has obvious costs: task distribution, context handoff, retry logic, and result merging all need to be handled manually. The author also doesn't advocate multiple Agents modifying the same codebase in parallel; different Agents' assumptions about interfaces and latest state easily become inconsistent. He prefers independent code review, or using a research session to produce reusable artifacts first, then implementing in a clean session.
Benchmark: Minimal Doesn't Mean Just Going by Feeling
The author finally evaluates Pi using Terminal-Bench 2.0. The Runner places the Agent in an isolated terminal task environment where it actually modifies files, configures systems, or generates artifacts, and finally an independent verifier determines whether the task is completed.
Task instruction
→ Isolated container
→ Pi + Claude Opus 4.5
→ Terminal interaction
→ Hidden verifier/tests
→ Pass / failThe author runs 5 trials for each task, because Agents are non-deterministic—a single success or failure is insufficient to represent true capability. He also notices that Provider errors at different times affect results, further demonstrating that eval must record error rates, time, retries, and model versions.
If I'm developing my own Agent, I shouldn't just run public leaderboards. Instead, I should establish a four-layer eval framework. This is the preliminary framework I've outlined so far, which still needs to be validated against mainstream Agent Eval methods:
- Test tool validation, abort, event ordering, and session recovery using a fake provider.
- Test complete agent loop scenarios with fixed model responses.
- Establish private task sets aligned with real business and hidden verifiers.
- Finally compare external performance using public benchmarks like Terminal-Bench and SWE-bench.
Core metrics can't be just success rate:
| Metric | Question Answered |
|---|---|
| Pass@1 / Pass@k | What's the probability the Agent actually completes the task? |
| Token & Cost | What's the cost of success? |
| Latency & Turns | Does it complete fast enough? |
| Tool errors | Where does the harness and model fail on calls? |
| Security violations | Does it exceed permissions, access the network, or modify tests? |
| Failure classification | Is it missing context, implementation error, or Provider error? |
The most important principle of evaluation is verifying external results, not trusting the Agent's completion claims. The verifier should be independent of the Agent workspace, preventing the Agent from creating "success" by deleting tests, modifying expectations, or hardcoding answers.
Conclusion: Minimalism is State Boundary Design
After reading the full article, I no longer simply interpret Pi's Minimalism as "fewer features." The author didn't eliminate planning, task tracking, external tools, background tasks, and sub-Agents; instead, he placed them in different mediums:
| Requirement | Common Harness Built-in | Pi Author's Preferred External |
|---|---|---|
| Todo | Todo tool/state | TODO.md |
| Plan | Plan Mode | Regular session + PLAN.md |
| External Tools | MCP tools | CLI + README |
| Background Process | Background process manager | tmux |
| Sub-agent | Built-in Task tool | Independent Pi process/session |
| Behavior Constraints | Large system prompt | Minimal prompt + AGENTS.md |
These alternatives share a common characteristic: users can directly view, edit, query, version-control, or retake control. Pi's core point isn't "all harnesses shouldn't have these features," but rather: each time you add internal state, you must answer:
Is it visible to users?
Can it be modified?
Can it be recovered after process restart?
Can it still be found after context compaction?
Is it really better than existing files and CLI?I don't fully accept all conclusions. Production Agents may still need max_steps, structured workflows, MCP, background tasks, and multi-agent orchestration. But Pi provides a valuable review standard: don't default a capability into the harness just because it's popular; first confirm it solves real problems and doesn't trade convenience for more hidden state.
This is also the biggest takeaway from this article: the quality of an Agent harness depends not only on how much the model can do, but also on whether it lets users understand what the model is doing and where state actually exists.