Pi Agent Harness
Connect Agent Interface to the pi coding agent over an OpenAI-compatible stream.
Anything that can stream text can drive OpenUI's renderer, including a full coding agent. This example connects pi (@earendil-works/pi-coding-agent), running its default read / bash / edit / write tools, to <AgentInterface>. Pi calls OpenUI Gateway Completions; generateSystemPrompt({ cloud: true, library }) is appended so it emits OpenUI Lang for openuiLibrary instead of markdown.
Its mid-turn activity (reasoning and tool runs) surfaces as cards too.
How it connects
| Piece | File | Role |
|---|---|---|
| Frontend | src/app/page.tsx | A single <AgentInterface> wired with fetchLLM({ streamAdapter: openAIReadableStreamAdapter(), messageFormat: openAIMessageFormat }) and openuiLibrary. |
| Bridge route | src/app/api/chat/route.ts | Drives a pi AgentSession against OpenUI Gateway Completions and re-emits its events as NDJSON OpenAI chunks (delta.content is OpenUI Lang). |
| Session registry | src/lib/pi-session.ts | One persistent AgentSession per chat thread, keyed by threadId. Injects generateSystemPrompt({ cloud: true, library }) and the Gateway model. |
| Agent | @earendil-works/pi-coding-agent | The pi coding agent: read / bash / edit / write on the workspace you choose at launch. |
Everything runs in one Next.js process: the App-Router route is the backend. The pi SDK is embedded directly (no separate server), so there is no second service and no CORS. Each chat thread maps to one persistent pi AgentSession, so multi-turn context is preserved.
Connecting the frontend
The client is a single <AgentInterface> (the artifact chat surface with sidebar thread history). It parses the response with openAIReadableStreamAdapter() (NDJSON OpenAI chunks). The Gateway-managed system prompt is attached server-side:
import {
AgentInterface,
fetchLLM,
openAIMessageFormat,
openAIReadableStreamAdapter,
} from "@openuidev/react-ui";
import { openuiLibrary } from "@openuidev/react-ui/genui-lib";
const llm = fetchLLM({
url: "/api/chat",
streamAdapter: openAIReadableStreamAdapter(),
messageFormat: openAIMessageFormat,
});
<AgentInterface llm={llm} componentLibrary={openuiLibrary} agentName="OpenUI Agent Harness" />;generateSystemPrompt({ cloud: true, library }) from @openuidev/lang-core sends the component-library specification with the Gateway request, so the model's markup matches openuiLibrary.
The bridge route
The route keys a persistent AgentSession by threadId, injects the OpenUI Lang prompt via appendSystemPrompt, subscribes to the session's events, and re-emits them as NDJSON OpenAI chunks. Because pi keeps its own transcript, only the newest user turn is sent to session.prompt():
// lib/pi-session.ts: one AgentSession per conversation, model = OpenUI Gateway
import library from "@/generated/spec.json";
const {
AuthStorage,
createAgentSession,
DefaultResourceLoader,
getAgentDir,
ModelRegistry,
SettingsManager,
} = await import("@earendil-works/pi-coding-agent");
const authStorage = AuthStorage.inMemory();
authStorage.setRuntimeApiKey("openui-cloud", process.env.THESYS_API_KEY);
const modelRegistry = ModelRegistry.create(authStorage, modelsPath);
const model = modelRegistry.find("openui-cloud", "google/gemini-3.6-flash-free");
const loader = new DefaultResourceLoader({
cwd,
agentDir,
settingsManager,
appendSystemPrompt: [generateSystemPrompt({ cloud: true, library, instructions })],
});
await loader.reload();
const { session } = await createAgentSession({
cwd,
agentDir,
settingsManager,
resourceLoader: loader,
authStorage,
modelRegistry,
model,
});// app/api/chat/route.ts: translate pi events into OpenAI NDJSON
const unsubscribe = session.subscribe((event) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
enqueue(ndjsonChunk({ content: event.assistantMessageEvent.delta }));
}
});
await session.prompt(lastUserText);
enqueue(ndjsonChunk({}, "stop"));The pi SDK is ESM-only, so it is loaded with a native dynamic import() and marked as a webpack external in next.config.ts (the example runs with --webpack).
Thinking states
The route also forwards pi's reasoning and tool executions, mapped onto OpenAI tool_calls, which OpenUI renders as cards in a collapsible "behind the scenes" section:
} else if (event.type === "tool_execution_start") {
// e.g. read {"path":"package.json"}, bash {"command":"ls -la"}
enqueue(
toolStartChunk(indexFor(event.toolCallId), event.toolCallId, event.toolName, JSON.stringify(event.args)),
);
}
// thinking_delta events stream into a single "Thinking" card the same way.Tool results (command output) are not rendered yet: OpenUI's streaming path renders tool calls but not inline results, so surfacing those would take a custom adapter/renderer.
Choosing the workspace
Because this is a coding agent, you pick the directory it operates on at launch. pnpm dev runs a small wrapper that takes the path (or prompts for it) and starts Next with PI_AGENT_CWD set:
pnpm dev -- /path/to/your/project # explicit
pnpm dev # prompts for the workspaceThe agent's read / bash / edit / write tools act on that directory.
Security
This example executes real code on your machine. The agent has the full read / bash / edit / write toolset, tools execute without an approval prompt, and the route is unauthenticated, so treat reaching the port as remote code execution.
- Local, single-user use is equivalent to running the pi CLI yourself.
- For anything networked: set
PI_WEB_TOOLS=read-only, put it behind auth, bind to loopback (next start -H 127.0.0.1), and sandbox the agent.PI_AGENT_CWDis a discovery root, not a sandbox:bashcan escape it.
Authentication
Pi calls OpenUI Gateway Chat Completions. Set THESYS_API_KEY from console.thesys.dev/keys. Optional: OPENUI_MODEL (default google/gemini-3.6-flash-free). The pi CLI is not required.
Project layout
examples/harnesses/pi/
|- src/app/page.tsx # <AgentInterface> + chatLibrary
|- src/app/api/chat/route.ts # pi event stream into NDJSON OpenAI chunks
|- src/lib/pi-session.ts # one persistent pi AgentSession per conversation
|- src/lib/openui-cloud-models.json # Pi provider pointing at Gateway Completions
|- scripts/launch.mjs # picks the agent workspace, then starts Next
|- next.config.ts # keeps the ESM-only pi SDK externalRun the example
From the repo root, install workspace deps once, then run the example pointed at a project:
pnpm installcd examples/harnesses/pi
cp .env.example .env # set THESYS_API_KEY
pnpm dev -- /path/to/your/projectOpen http://localhost:3000 and try "Summarize the files in this project as a card" or "Read package.json and list its scripts". pi's tools run and the result renders as generative UI.