
Pi Agent Practice Guide
Implement DeepSearch capability with Pi Extension: enabling Pi to break down problems, retrieve information, organize evidence, and generate research conclusions with sources.
Quick Recap
In the conceptual article, I defined Pi Agent as a minimalist Agent Harness: it connects models, terminals, file systems, shells, sessions, and the extension system, but doesn't pre-configure a heavy workflow for you.
So, in this practice guide, I don't want to create another typical "let Pi modify files" example. While that example demonstrates the basic loop, it doesn't fully showcase Pi's extensibility.
A more suitable practical example for Pi is to add a capability it doesn't have by default, but many people genuinely need: DeepSearch.
Here, DeepSearch is not just simple online searching, but a research-oriented workflow:
| Stage | What to do |
|---|---|
| Problem Decomposition | Break down a vague question into several searchable sub-questions |
| Multi-round Retrieval | Search official documentation, code repositories, blogs, discussion forums, or papers |
| Source Filtering | Deduplicate, exclude low-quality results, prioritize primary sources |
| Evidence Organization | Extract key facts, links, dates, versions, and uncertainties |
| Synthesized Answer | Provide conclusions, along with supporting evidence and limitations |
My judgment is: DeepSearch should not be built into Pi's core, nor should it rely solely on prompt engineering. It's better suited as a Pi Extension.
The reason is simple: DeepSearch involves network requests, third-party search APIs, source filtering, result truncation, citation formatting, and security boundaries. These are all workflow capabilities, not part of the coding agent's minimal core.
Design Goals
This example aims to implement a minimal viable version, not a perfect research system.
The goals are as follows:
Add a deep_search tool to Pi.
It accepts:
- query: The user's research question
- depth: Search depth
- maxResults: Maximum number of candidate sources to return
It outputs:
- Structured search results
- Title, URL, snippet, and relevance score for each result
- Evidence prompts for the model
After Pi receives this evidence, the current model will generate the final conclusion.I will deliberately separate "retrieval" and "synthesis":
| Part | Responsible Party | Reason |
|---|---|---|
| Search API calls | DeepSearch extension | This is a deterministic external capability |
| Result deduplication and truncation | DeepSearch extension | Avoid context being filled with noise |
| Judging which evidence is important | Pi's current model | Requires reasoning and context understanding |
| Final answer writing | Pi's current model | Needs to combine user questions and project context |
This approach is more robust. The extension doesn't need to call another model itself, nor does it need to become a nested agent. It only provides high-quality evidence, allowing Pi's original model to continue reasoning.
Preparation
Pi extensions can be placed in a global directory or a project directory. Here, I recommend placing them in the project directory first:
.pi/extensions/deepsearch/
package.json
index.tsThe advantage of a project-local extension is clear boundaries. This DeepSearch capability is only enabled within the current project and will not affect all Pi sessions.
For the search service, you can choose Tavily, Exa, Brave Search, SerpAPI, or even your own search backend. For the first version, don't worry about the service provider; first abstract it into a searchWeb() function.
For example, save the API Key as an environment variable:
export TAVILY_API_KEY=tvly-...If you don't want to connect to a third-party search API, you can also use local mock data to get the extension running first. Once tool registration, parameter passing, and result formatting are stable, then connect to a real search service.
Step 1: Create the Extension Directory
First, create the directory:
mkdir -p .pi/extensions/deepsearchIf the extension needs dependencies, you can include a package.json:
{
"name": "pi-deepsearch-extension",
"private": true,
"dependencies": {
"typebox": "*",
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*"
},
"pi": {
"extensions": ["./index.ts"]
}
}Then install dependencies:
cd .pi/extensions/deepsearch
npm installPi's extensions are TypeScript modules; you don't need to compile them manually first. This experience is great for quickly experimenting with tools.
Step 2: Register the deep_search Tool
The core file is .pi/extensions/deepsearch/index.ts.
The first version can be written like this:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { StringEnum } from "@earendil-works/pi-ai";
import { Type } from "typebox";
type SearchResult = {
title: string;
url: string;
snippet: string;
score?: number;
};
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "deep_search",
label: "DeepSearch",
description: "Search the web for source-backed evidence about a question.",
promptSnippet: "Research a question with web search and return source-backed evidence.",
promptGuidelines: [
"Use deep_search when the user asks for current facts, external sources, comparison, investigation, or source-backed research.",
"After deep_search returns results, synthesize an answer with citations and clearly separate facts, inference, and uncertainty.",
"Do not treat deep_search results as final truth; inspect source quality and mention gaps."
],
parameters: Type.Object({
query: Type.String({
description: "The research question or search query."
}),
depth: Type.Optional(StringEnum(["quick", "normal", "deep"] as const)),
maxResults: Type.Optional(Type.Number({
minimum: 3,
maximum: 10,
default: 6
}))
}),
async execute(_toolCallId, params, signal) {
const depth = params.depth ?? "normal";
const maxResults = params.maxResults ?? 6;
const results = await searchWeb(params.query, depth, maxResults, signal);
return {
content: [
{
type: "text",
text: formatResultsForModel(params.query, results)
}
],
details: {
query: params.query,
depth,
results
}
};
}
});
}
async function searchWeb(
query: string,
depth: "quick" | "normal" | "deep",
maxResults: number,
signal: AbortSignal
): Promise<SearchResult[]> {
const apiKey = process.env.TAVILY_API_KEY;
if (!apiKey) {
throw new Error("Missing TAVILY_API_KEY. Set it before starting pi.");
}
const response = await fetch("https://api.tavily.com/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: apiKey,
query,
search_depth: depth === "quick" ? "basic" : "advanced",
max_results: maxResults,
include_answer: false,
include_raw_content: depth === "deep"
}),
signal
});
if (!response.ok) {
throw new Error(`Search failed: ${response.status} ${response.statusText}`);
}
const data = await response.json() as {
results?: Array<{
title?: string;
url?: string;
content?: string;
score?: number;
}>;
};
return dedupeByUrl((data.results ?? []).map((item) => ({
title: item.title ?? "Untitled",
url: item.url ?? "",
snippet: item.content ?? "",
score: item.score
}))).filter((item) => item.url);
}
function dedupeByUrl(results: SearchResult[]): SearchResult[] {
const seen = new Set<string>();
const deduped: SearchResult[] = [];
for (const result of results) {
const key = normalizeUrl(result.url);
if (seen.has(key)) continue;
seen.add(key);
deduped.push(result);
}
return deduped;
}
function normalizeUrl(url: string): string {
try {
const parsed = new URL(url);
parsed.hash = "";
parsed.searchParams.delete("utm_source");
parsed.searchParams.delete("utm_medium");
parsed.searchParams.delete("utm_campaign");
return parsed.toString();
} catch {
return url;
}
}
function formatResultsForModel(query: string, results: SearchResult[]): string {
if (results.length === 0) {
return `DeepSearch found no results for: ${query}`;
}
const lines = results.map((result, index) => {
return [
`## Source ${index + 1}`,
`Title: ${result.title}`,
`URL: ${result.url}`,
result.score === undefined ? undefined : `Score: ${result.score}`,
`Snippet: ${result.snippet}`
].filter(Boolean).join("\n");
});
return [
`DeepSearch query: ${query}`,
"",
"Use these sources as evidence. Cite URLs when making factual claims.",
"Separate confirmed facts from inference and uncertainty.",
"",
...lines
].join("\n\n");
}This code only does the most critical things:
| Code Location | Purpose |
|---|---|
pi.registerTool() | Exposes deep_search for model calls |
parameters | Tells the model what parameters the tool needs |
promptGuidelines | Tells the model when to use it and how to handle results |
searchWeb() | Calls the real search service |
dedupeByUrl() | Removes duplicate URLs |
formatResultsForModel() | Organizes search results into evidence blocks that are easy for the model to cite |
The first version should not be too complex. The real challenge of DeepSearch is controlling source quality, context length, citation format, and uncertainty.
Step 3: Add a /deepsearch Command
Tools are for models to call, but users also need a direct entry point.
You can register another command to rewrite user input into a more explicit research task:
export default function (pi: ExtensionAPI) {
pi.registerCommand("deepsearch", {
description: "Run a source-backed DeepSearch task",
handler: async (args, ctx) => {
const query = String(args ?? "").trim();
if (!query) {
ctx.ui.notify("Usage: /deepsearch <question>", "warning");
return;
}
pi.sendUserMessage(
[
"Please perform a DeepSearch on the following question.",
"",
`Question: ${query}`,
"",
"Requirements:",
"1. First, determine if deep_search needs to be called.",
"2. If the question is complex, break it down into 2-4 sub-questions and retrieve information for each.",
"3. The final answer must include source links.",
"4. Distinguish between facts, inferences, and uncertain parts.",
"5. Do not just list search results; provide a comprehensive judgment."
].join("\n"),
{ deliverAs: "followUp" }
);
}
});
pi.registerTool({
// deep_search tool definition...
});
}This way, users can directly input:
/deepsearch What capabilities does the latest version of Pi Coding Agent's extension system support?/deepsearch doesn't search directly, but sends Pi a more complete task description. The model will call deep_search based on the description, and then synthesize the results.
I prefer this design because it preserves the agent's judgment space. The search tool is just an entry point for evidence, not the final answer generator.
Step 4: Launch and Verify
Once the project-local extension is in place, you can launch Pi directly from the project root:
TAVILY_API_KEY=tvly-... piIf you are just testing temporarily, you can explicitly specify the extension:
TAVILY_API_KEY=tvly-... pi -e ./.pi/extensions/deepsearch/index.tsAfter entering Pi, ask a question that requires external facts:
/deepsearch What capabilities does the latest version of Pi Coding Agent's extension system support?An acceptable output should not just be a few search results, but should include:
| Checkpoint | Acceptable Performance |
|---|---|
| Tool invocation | deep_search is called |
| Clear sources | Each key fact is followed by a URL |
| Deduplication | No repeated citations of the same page |
| Judgment | Not just listing information, but also summarizing applicable scenarios |
| Uncertainty | Maintains boundaries for version changes, third-party APIs, and community extensions |
If the result is just a "list of search results," it means the promptGuidelines are not strong enough. You can make the guidelines more explicit:
promptGuidelines: [
"Use deep_search to gather evidence, not to produce the final answer.",
"After deep_search, write a concise research brief with citations.",
"Prefer official documentation, source code, release notes, and primary sources.",
"Mention when sources disagree or when the evidence is incomplete."
]Step 5: Make DeepSearch More Like a Research Tool
After getting the first version running, you can continue to add three types of capabilities.
Sub-problem Decomposition
The most common failure point for DeepSearch is throwing a large question directly at the search API.
For example:
Can Pi Agent replace Claude Code?This is not a good search query. It can at least be broken down into:
| Sub-question | Purpose |
|---|---|
| What is the core design of Pi Agent | Find its positioning |
| What tools and extensions does Pi Agent support | Find its capability boundaries |
| What are Claude Code's default capabilities | Find comparison objects |
| What are the differences in permissions, security, and extensibility between the two | Form a judgment |
The first version can let the model decompose itself; the second version can make the /deepsearch command force the model to first list sub-questions, then call deep_search for each.
Source Quality Layering
DeepSearch output should not just be sorted by the search API's score. When writing technical articles, I prioritize:
| Priority | Source |
|---|---|
| P0 | Official documentation, source code, release notes |
| P1 | Author's blog, maintainer's explanation, issue / PR |
| P2 | High-quality tutorials, technical analysis |
| P3 | Community discussions, Reddit, X, forums |
The extension can annotate source types in formatResultsForModel():
function classifySource(url: string): "official" | "source" | "community" | "other" {
const host = new URL(url).hostname;
if (host === "pi.dev") return "official";
if (host === "github.com") return "source";
if (host.includes("reddit.com")) return "community";
return "other";
}This way, when the model synthesizes, it won't treat community rumors and official documentation as the same level of evidence.
Context Truncation
Search results can easily pollute the context. The tool output of DeepSearch should be concise and precise.
My suggestion is:
| Content | Include in tool output |
|---|---|
| Title | Yes |
| URL | Yes |
| 200-500 word snippet | Yes |
| Full page content | No by default |
| Raw HTML | No |
| Raw JSON from search API | In details, not in the main content |
If full text reading is indeed required, a second tool can be created:
fetch_source(url)This way, DeepSearch's first step is to find candidate sources, and the second step only grabs the 2-3 most important pages. Don't immediately feed the model the full text of a dozen web pages.
Common Questions
Why not just use bash to run search scripts?
You can, but it's not as stable as an extension.
The problem with bash is that the model has to re-decide commands, parameters, output format, and error handling every time. An extension fixes these details, and the model only needs to call deep_search.
Why not write the summary in the extension as well?
Not recommended for the first version.
If the extension itself calls another model to summarize, you will encounter issues with nested model calls, cost accounting, context drift, and citation responsibility. A simpler approach is: the extension only returns evidence, and the model in the current Pi session is responsible for synthesis.
Is this DeepSearch considered MCP?
No. It's a local tool registered by a Pi extension.
If you already have a mature MCP search server, you can also integrate it via Pi's MCP-related packages or extensions. However, this example chooses to write the extension directly to understand Pi's own extension mechanism.
What security considerations should be kept in mind?
At least four things:
| Risk | Practice |
|---|---|
| API Key leakage | Read only from environment variables, do not commit to repository |
| Untrusted web content | Do not treat web content as system instructions, only as evidence to be verified |
| Search result pollution | Prioritize official and source code, reduce weight of community results |
| Context explosion | Limit result quantity and snippet length |
DeepSearch appears to be "search enhancement," but it essentially brings external web pages into the agent's context. As long as external content enters the context, prompt injection should be treated as a real risk.
Summary
I would define the first practical example of Pi Agent as the DeepSearch Extension because it simultaneously demonstrates three key characteristics of Pi:
- Pi's core is very small by default and does not include all workflows.
- Truly useful capabilities can be added via extensions.
- Extensions are not just about adding commands, but about defining the boundaries of the model's interaction with the external world.
Once this example is running, Pi will no longer be just a local code editing agent, but will have a controllable research entry point: when encountering questions requiring external information, it can first retrieve, then filter, and then answer with sources.
This is more reliable than letting the model answer from memory, and more reusable than manually writing search commands every time.
References
Pi Extensions 文档
官方扩展文档,介绍如何用 TypeScript 注册工具、命令、UI 和事件钩子。
Pi 使用文档
官方使用文档,覆盖交互模式、斜杠命令、会话、上下文文件、CLI 参数和设计原则。
Comments
Concept Introduction
This note clarifies the core positioning of Pi Agent: it's not a new model, nor a more feature-rich IDE, but a minimalist, transparent, and extensible coding agent harness.
Concept Introduction
From Vibe Coding to spec-driven development: understanding how AI programming evolves from 'intuition' to 'engineering', and mastering the new paradigm of writing specs before code