Agent Runtime Examples

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.

View source on GitHub →

How it connects

PieceFileRole
Frontendsrc/app/page.tsxA single <AgentInterface> wired with fetchLLM({ streamAdapter: openAIReadableStreamAdapter(), messageFormat: openAIMessageFormat }) and openuiLibrary.
Bridge routesrc/app/api/chat/route.tsDrives a pi AgentSession against OpenUI Gateway Completions and re-emits its events as NDJSON OpenAI chunks (delta.content is OpenUI Lang).
Session registrysrc/lib/pi-session.tsOne persistent AgentSession per chat thread, keyed by threadId. Injects generateSystemPrompt({ cloud: true, library }) and the Gateway model.
Agent@earendil-works/pi-coding-agentThe 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 workspace

The 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_CWD is a discovery root, not a sandbox: bash can 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 external

Run the example

From the repo root, install workspace deps once, then run the example pointed at a project:

pnpm install
cd examples/harnesses/pi
cp .env.example .env          # set THESYS_API_KEY
pnpm dev -- /path/to/your/project

Open 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.

On this page