The Pi SDK version

In the previous post, I built a news reader using Pi’s CLI with an extension file and a system prompt file. Two files in a .pi/ directory, run with pi -p. It already felt minimal compared to the Claude Code version, but it still relied on Pi’s runtime to discover and load the extension.
Pi also has a programmatic SDK that lets you embed the agent in your own script, the same way the Agent SDK let me collapse the Claude Code version into a single Python file.
Inline tools, inline prompt
The tool code is the same as the CLI version: web_fetch fetches a page with turndown, save_news_item writes JSON to data/. The difference is that instead of living in .pi/extensions/news-tools.ts, they’re defined inline with defineTool(). Here’s one:
import {
createAgentSession, DefaultResourceLoader, defineTool,
getAgentDir, SessionManager,
} from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import TurndownService from "turndown";
const webFetchTool = defineTool({
name: "web_fetch",
label: "Fetch URL",
description: `Fetch a web page and return content as markdown. Allowed domains: ${ALLOWED_DOMAINS.join(", ")}`,
parameters: Type.Object({
url: Type.String({ description: "URL to fetch" }),
}),
async execute(_id, params) {
const hostname = new URL(params.url).hostname;
if (!ALLOWED_DOMAINS.includes(hostname)) {
throw new Error(`Domain not allowed: ${hostname}`);
}
const resp = await fetch(params.url);
const html = await resp.text();
return { content: [{ type: "text", text: turndown.turndown(html) }], details: {} };
},
});
// save_news_item defined the same way (full source on GitHub)
The SDK’s programming model is event-based. You create a session, send a prompt, and subscribe to events as the agent works. The session setup:
// Replace Pi's default system prompt and suppress global skill discovery
const resourceLoader = new DefaultResourceLoader({
cwd: process.cwd(),
agentDir: getAgentDir(),
systemPromptOverride: () => SYSTEM_PROMPT,
appendSystemPromptOverride: () => [],
skillsOverride: () => ({ skills: [], diagnostics: [] }),
});
await resourceLoader.reload();
// Create a session with only our two custom tools
const { session } = await createAgentSession({
resourceLoader,
customTools: [webFetchTool, saveNewsItemTool],
tools: ["web_fetch", "save_news_item"],
sessionManager: SessionManager.inMemory(),
});
// Stream agent output to stdout
session.subscribe((event) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
// Run the agent
await session.prompt("Go");
session.dispose();
The result is a fully self-contained setup with no filesystem discovery and no session persistence. Authentication defaults to ~/.pi/agent/auth.json (whatever pi /login stored), but you can pass a custom authStorage for a fully independent setup.
session.subscribe() is how you observe everything the agent does. The session emits events for text output, tool calls, tool results, thinking, and compaction. Here I only care about streaming text to stdout. The SDK examples show more patterns.
Run it with npx tsx scrape.ts.
Comparing the two SDKs
The two SDKs end up similar in shape:
| Claude Agent SDK | Pi SDK | |
|---|---|---|
| Language | Python | TypeScript |
| Entry point | query() async generator | createAgentSession() + session.prompt() |
| Tool definition | @tool decorator + Pydantic | defineTool() + TypeBox |
| System prompt | prompt parameter | systemPromptOverride callback |
| Tool restriction | allowed_tools list | tools allowlist |
| Session | Stateless by default | SessionManager.inMemory() |
| Sub-agents | AgentDefinition objects | Not available |
| Output | Async iterator of messages | Event subscription |
The Claude Agent SDK has more built-in structure. Pi’s SDK gives you a session with tools and a prompt, and you build the rest yourself.
When to use the SDK over the CLI
The CLI works for one-shot automation: run a command, get results, exit. The SDK makes sense when you want to embed Pi in a larger application or chain multiple prompts in one session without re-initializing.
For this news reader, the CLI version is simpler. The SDK version is useful as a reference for when you need Pi as a library rather than a command.
The full project is on GitHub.