A Tool-calling Agent Core isn't particularly complex. The minimal implementation only needs to maintain messages, request models, execute Tool Calls, and put Tool Results back into the next request.
What truly causes Agent Frameworks to bloat quickly usually happens outside the Loop.
Coding Agents quickly add file system tools, Shell, MCP, Skills, permission control, model switching, Session, Compaction, commands, UI, telemetry, and SubAgents. For each capability added, there are two implementation choices: continue modifying the Agent Core, or provide a stable extension surface for external modules to plug into.
Pi chose the latter. It still maintains a relatively centralized Agent Runtime while moving a large amount of product capabilities outside the core through Extensions.
This article's focus isn't on what plugin APIs Pi provides, but a more fundamental framework design question: which capabilities should go into Agent Core, and which should be extended through Harness.
From Tool Loop to Continuously Growing Core
Unit 1 has established a relatively stable Agent Loop boundary:
Context
↓
Model
↓
Tool
↓
Input Delivery
↓
Stop Condition
This structure explains how an Agent advances through one unit of work, but it can't carry all the capabilities of a complete product.
Take permission control as an example. The most direct implementation adds a judgment before each Tool execution:
if (!permission.canExecute(toolCall)) {
return deniedResult
}
return executeTool(toolCall)
Then audit is needed:
telemetry.record(toolCall)
Then Hooks are added:
toolCall = await hooks.beforeToolCall(toolCall)
Then remote execution:
return sandbox.execute(toolCall)
If all features enter the main execution function this way, the Agent Loop gradually becomes the integration center of the entire system. Any feature can modify it, and any modification might affect other features.
This reveals the first responsibility of Harness:
Core defines stable execution semantics, Harness is responsible for organizing external capabilities within these semantic boundaries.
The value of an extension system therefore isn't about "allowing third parties to write plugins," but about controlling Core's growth rate.
Pi's Extension Chooses Stable Core + Extension Entries
Pi positions itself as a minimal terminal coding harness. The default Coding Agent only provides a few basic tools, with more behaviors added through Extensions, Skills, Prompt Templates, and Packages.[1][2]
Extensions can register Tools:
pi.registerTool(...)
They can also listen to lifecycle events:
pi.on('tool_call', ...)
pi.on('context', ...)
pi.on('agent_end', ...)
They can also register Commands, Shortcuts, Providers, or inject messages into running Sessions.[1][3]
These API categories seem very different on the surface, but they actually fall into two directions.
The first is Contribution:
Extension
↓
Register a capability with Runtime
Tool
Command
Provider
Renderer
...
The second is Hook/Interception:
Runtime lifecycle
↓
Extension observes / modifies / blocks
The former extends "what the system has," the latter extends "what happens when the system reaches certain boundaries."
Therefore, Pi Extension's core structure can be abstracted as:
Pi Runtime
│
┌──────────────┴──────────────┐
│ │
Contributions Hooks
│ │
Tool / Command / ... lifecycle events
│ │
└──────────── Extension ──────┘
This model is easier to form a unified development model than designing separate ToolPlugin, CommandPlugin, HookPlugin. Extensions themselves have closure state—Tools, Hooks, and Commands within the same extension can directly share state.
Pi once separated Hooks and custom Tools, then unified them into Extension. This evolution reflects the same direction: extension units should be organized around "a functional module," not around registration types.[4]
ExtensionAPI Is the Boundary, Not the Container Itself
Plugin architectures often fall into a trap: to let plugins "do anything," directly expose the entire Application or internal objects.
Extremely flexible in the short term, but in the long term this forms a de facto internal API. Plugins can access any implementation details, and Core loses refactoring space.
Pi uses ExtensionAPI as the plugin entry point. Extensions register capabilities through it, send messages, and read or modify controlled state.[1][3]
The relationship can be understood as:
Extension
│
▼
ExtensionAPI
│
▼
Runtime / Session / UI / Registry
The significance of the API is that it defines which system boundaries plugins can affect.
For example:
registerTool()
allows Extensions to add callable capabilities to the model.
on('tool_call')
allows Extensions to participate in control at tool call boundaries.
sendMessage()
allows Extensions to feed new input into the Agent Runtime.
These capabilities are powerful, but calls still go through the entries defined by Harness. The internal state structure of Core can continue to evolve.
Therefore, what matters more about extension APIs is whether the capability boundaries are stable—method count is just surface-level.
Why Hooks Can't Be Understood Just as Observers
pi.on(...) is easily understood as an Event Bus, but some of Pi's Hooks can modify or even block running behavior.
For example, Tool Call Hooks can inspect, modify, or reject calls; Context Hooks can adjust the messages the model is about to see.[1]
These kinds of Hooks are closer to Middleware/Interceptors:
Runtime operation
↓
Extension A
↓
Extension B
↓
Core behavior
The basic semantics of an Observer is observing facts that have already happened:
event happened
↓
listener notified
An Interceptor is inside the behavior chain:
operation requested
↓
interceptor
↓
modify / block / continue
This distinction directly affects the complexity of the plugin system.
Pure Observers only need to consider notification and exception isolation. Interceptors also need to define ordering, short-circuiting, error propagation, async behavior, and composition semantics when multiple extensions modify results simultaneously.
Pi's current tool_call explicitly specifies: modifications to input by earlier Handlers are visible to later Handlers, and Handlers can prevent calls through return values. Events aren't a simple broadcast but part of the execution protocol.[1]
This is another boundary Harness design must control: not all events should have modification rights.
If an extension point only needs observation, it should remain read-only; only execution boundaries that truly need strategy intervention should provide Interception.
Dynamic Registration Solves Runtime Extension, Lifecycle Is Still Another Matter
Pi Extensions can register Tools during runtime. After registration completes, new Tools can enter the current Session's tool set.
From a usage perspective, this already has an important capability of dynamic plugin systems:
Runtime
│
├── Tool A
├── Tool B
│
└── Extension loads
↓
Tool C
But dynamically "adding" capability is only half the problem.
The more complex problem occurs when capabilities leave the system.
Assume an Extension does these things:
register Tool A
register Command B
subscribe tool_call
start timer
provide service
When the plugin is unloaded, the system needs to fully undo these changes.
The problem further expands when dependencies increase:
Plugin A
└── provides Service X
Plugin B
├── depends on Service X
└── provides Service Y
Plugin C
└── depends on Service Y
If A is unloaded, should B continue running? Is Y provided by B still valid? How should C handle this? After A is loaded again, should B and C be restarted?
These questions go beyond Extension Registry itself, into dynamic composition and lifecycle management.
Pi's design mainly builds Extensions around a relatively stable Runtime. It's suitable for expressing:
stable runtime
+
dynamic contributions
+
lifecycle hooks
DeepSeek Harness adopts a more aggressive structure: Agent Loop, Session, Tool Registry, LLM Adapter, and other capabilities themselves are provided by plugins.[5] At this point, the plugin system must bear the dynamic changes of Runtime structure itself.
This is exactly where Cordis enters DeepSeek Harness.
A Method for Determining Core Boundaries
Whether a capability should enter Agent Core can be judged by three questions.
First, does it participate in the basic semantics of every Agent Step?
For example, Context construction, Model Request, Tool Result return flow, and stop conditions belong to the basic execution structure of the Loop. Completely removing these capabilities means the Agent Loop itself no longer exists.
Second, does it have many alternative implementations?
Persistence, Sandbox, Model Adapter, Telemetry all have many implementation options. Fixing any implementation in Core quickly increases replacement costs.
Third, does it need an independent lifecycle?
If a capability needs dynamic loading, unloading, isolation, reloading, or depends on other capabilities, it already has component attributes and is more suitable for Harness.
A practical judgment can be derived:
Agent Core
is responsible for indivisible execution semantics
Harness
is responsible for capability composition, replacement, and lifecycle
This boundary doesn't require Core to be extremely small. Over-splitting also increases understanding cost. The key is to avoid having all product capabilities connect through modifying the same Loop.
Pi demonstrates a more conservative, easier-to-understand approach: keep a stable Runtime, extend through Extension for Contributions and Hooks.
When DeepSeek Harness's goals expand further, another class of problems needs solving: when components can appear, disappear at runtime, and depend on each other, how to keep composition relationships correct.
The next discussion will focus on the paper behind Cordis, "A Programming Paradigm for Spatiotemporal Composability." The paper attempts to establish a more general model for this problem.
References
[1] Pi Extensions: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
[2] Pi Coding Agent: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/README.md
[3] Pi Extension types: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts
[4] Pi Extension unified design discussion: https://github.com/earendil-works/pi/issues/454
[5] DeepSeek Harness Architecture: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md