polpo-ai
v0.15.32
Published
The open backend for AI agents
Maintainers
Readme
What is Polpo?
Polpo is an open-source runtime for building, running, and managing AI agents. It provides the infrastructure layer so you can focus on what your agents do, not how they run.
- Tasks -- assign work to agents, track status, retry on failure
- Missions -- multi-step workflows with checkpoints and delays
- Tools -- filesystem, browser, HTTP, email, PDF, Excel, audio, images, vault
- Completions -- OpenAI-compatible
/v1/chat/completionsendpoint - Loops -- beta project-level deterministic graphs assigned to agents
- Real-time -- SSE event streaming for live agent activity
- Storage -- file (default), SQLite, or PostgreSQL via Drizzle
- Assessment -- built-in quality scoring with LLM review
- Skills -- reusable agent capabilities loaded from YAML playbooks
- Dashboard -- reusable v2 React views and a single-tenant self-host app
- CLI --
polpo create,polpo dev,polpo deploy
Quick start
npx polpo createScaffolds a new Polpo project (cloud + local) with an interactive wizard: pick an org, a project name, and a template. Link an existing project instead:
npx polpo link --project-id <id>Install globally so polpo is on your PATH:
npm i -g @polpo-ai/cliThe local server starts on http://localhost:3890. Open the API at /api/v1/health.
Programmatic usage
import { Orchestrator } from "polpo-ai";
const orchestrator = new Orchestrator("./my-project");
await orchestrator.init();
await orchestrator.run();Packages
| Package | Description | npm |
|---------|-------------|-----|
| polpo-ai | Main package -- CLI, server, orchestrator | |
|
@polpo-ai/core | Pure business logic, zero Node.js deps | |
|
@polpo-ai/drizzle | Drizzle ORM stores (SQLite + PostgreSQL) | |
|
@polpo-ai/server | Hono route factories (shared between OSS and cloud) | |
|
@polpo-ai/sdk | TypeScript client SDK | |
|
@polpo-ai/react | React hooks (TanStack Query + SSE) | |
|
@polpo-ai/dashboard | Runtime dashboard views for OSS and managed hosts | |
|
@polpo-ai/tools | Extended tool definitions | |
|
@polpo-ai/vault-crypto | Encryption for vault secrets | |
Architecture
@polpo-ai/core Pure logic, types, state machine, store interfaces
|
@polpo-ai/drizzle SQLite + PostgreSQL store implementations
|
polpo-ai Node.js shell: orchestrator, CLI, Hono server, tools
|
@polpo-ai/server Shared Hono route factories
@polpo-ai/sdk Client SDK (fetch + SSE)
@polpo-ai/react React hooks wrapping the SDK
@polpo-ai/dashboard Reusable v2 runtime viewsCore contains zero Node.js dependencies. The shell (polpo-ai) wires concrete adapters: file stores, Drizzle stores, the LLM engine, and the HTTP server.
Self-host with the dashboard
The repository includes a single-tenant dashboard host that keeps the runtime API key on the server. Start the production-oriented example with PostgreSQL:
cp docker/self-host/.env.example docker/self-host/.env
# Replace both secrets and provide AI_GATEWAY_API_KEY in docker/self-host/.env.
docker compose \
--env-file docker/self-host/.env \
-f docker/self-host/compose.example.yml \
up --build --detach --waitOpen http://localhost:3000. The runtime is only exposed to the private Compose network; the dashboard proxies API and completion requests with POLPO_API_KEY server-side.
Use the deterministic, isolated verification stack before deploying changes:
pnpm test:self-hostThat test creates disposable PostgreSQL storage, exercises authenticated REST and chat completion paths, renders the dashboard, and removes its containers and volumes when complete.
For Railway, deploy the runtime and dashboard as separate services and mount a runtime volume at /app/workspace; see docker/railway/README.md.
Storage
Polpo supports three storage backends:
// .polpo/polpo.json
{
"settings": {
"storage": "file" // default -- JSON/MD files in .polpo/
// "storage": "sqlite" // better-sqlite3 via Drizzle
// "storage": "postgres" // PostgreSQL via Drizzle
}
}Tools
Agents get access to tools based on their configuration. Built-in tool groups:
- System -- bash, read, write, edit, glob, grep, memory
- Browser -- Playwright-based web automation
- HTTP -- fetch, download
- Email -- SMTP send, IMAP read/search
- PDF -- read, create, merge
- Excel -- read/write spreadsheets
- Docx -- read Word documents
- Audio -- STT/TTS (Deepgram, OpenAI Whisper, ElevenLabs)
- Image -- generation and analysis
- Vault -- encrypted secret management
Loops Beta
Loops are project-level deterministic graphs stored in .polpo/loops/*.json or authored as static .polpo/loops/*.ts DSL files, then assigned to agents from .polpo/agents.json. This avoids duplicating loop definitions across agents: a loop has name, context, start, and steps; an agent has assignedLoops and defaultLoop.
Use type: "tool" for deterministic sandbox/tool actions without an LLM turn, and toolChoice on type: "agent" when the model should still reason but must use a tool. Secrets stay in Connections; loop JSON should only contain non-secret input, while custom tools resolve credentials with ctx.connections.
.polpo/loops/router-flow.json:
{
"name": "router-flow",
"context": "shared",
"start": "clone_repo",
"steps": {
"clone_repo": {
"type": "tool",
"tool": "clone_repository",
"input": {
"repoUrl": "https://github.com/acme/app.git",
"targetDir": "workspace/app"
},
"saveAs": "repo.clone",
"next": "classify"
},
"classify": {
"type": "agent",
"systemPrompt": "Classify the incoming request.",
"tools": ["read"],
"skills": ["classification"],
"output": {
"schema": {
"type": "object",
"properties": {
"route": { "type": "string" }
}
}
},
"stopWhen": { "expression": "classify.route != null" },
"next": [
{ "when": "classify.route == 'answer'", "to": "answer" },
{ "to": "human_review" }
]
},
"answer": {
"type": "agent",
"systemPrompt": "Answer using the selected route.",
"tools": ["write"],
"toolChoice": { "mode": "required", "tool": "write" },
"next": "end"
},
"human_review": {
"type": "human",
"output": {
"schema": {
"type": "object",
"properties": {
"decision": { "type": "string" }
}
}
},
"next": "end"
}
}
}.polpo/agents.json:
[
{
"agent": {
"name": "router",
"role": "Deterministic request router",
"runtime": "polpo-runner",
"assignedLoops": ["router-flow"],
"defaultLoop": "router-flow"
},
"teamName": "default"
}
]Loop guards use Polpo's safe expression evaluator instead of JavaScript eval or new Function. Step outputs are available in the shared context bag by step id or saveAs path, e.g. classify.route, review.approved, or timing.start. saveAs writes context data; it does not create shell variables inside later bash commands. The OSS surface validates and round-trips the contract through core types, API schemas, SDK types, polpo deploy, and polpo pull.
Loops also have first-class governance fields:
permissions: readable allow/deny/approval rules for resources such astool,step,model,human, andloop. Use this for least-privilege runtime constraints beyond an agent's broad tool assignment.policies: expression-based gates for advanced compliance rules.hooks: deterministic tool actions at lifecycle points such asloop:start,tool:before,tool:after, andloop:end.loop_trace: durable runtime events includingpermission.result,policy.result,approval.required, tool calls, transitions, and step outcomes.
When a permission or policy requires approval, the runtime stores a checkpoint on the loop run. Approving the gate moves the run to approval_approved; POST /loop-runs/:id/resume continues from the saved context and remaining steps without replaying completed steps.
You can also keep loops as static TypeScript source. The CLI validates the file locally, deploys the source to /v1/loops, and the server compiles it to the same canonical JSON contract without executing arbitrary code:
// .polpo/loops/router-flow.ts
import { agentStep, defineProjectLoop, requireTool, toolStep, when, otherwise } from "@polpo-ai/core/loop-code";
export default defineProjectLoop({
name: "router-flow",
permissions: [
{
id: "router-tool-allowlist",
resource: "tool",
action: "call",
effect: "allow",
match: { tool: ["read", "write"] }
}
],
start: "classify",
steps: {
classify: agentStep({
label: "Classify",
systemPrompt: "Classify the incoming request.",
tools: ["read"],
next: [when("classify.route == 'answer'", "answer"), otherwise("end")],
}),
answer: agentStep({
label: "Answer",
systemPrompt: "Answer using the selected route.",
tools: ["write"],
toolChoice: requireTool("write"),
next: "audit",
}),
audit: toolStep({
label: "Audit",
tool: "audit_log",
input: { flow: "router" },
next: "end",
}),
},
});CLI support:
polpo loops validate
polpo loops compile .polpo/loops/router-flow.ts
polpo deploy # deploys .json as JSON and .ts/.js/.mjs as sourceAgent-direct chat can target a loop explicitly:
{
"agent": "router",
"loop": "router-flow",
"messages": [{ "role": "user", "content": "Route this request" }]
}At runtime, the selected project loop can narrow the effective prompt, tools, skills, model, reasoning, tool choice, and max turns per agent step. If a step omits skills, it inherits the agent-level skills. Project loop execution in chat completions uses the shared context graph: deterministic tool steps run first, store outputs in the context bag, and later agent steps receive that context as runtime data in their system prompt. Core keeps a compatibility normalizer for legacy inline loops + pipeline configs and ships a pure PipelineExecutor for sequential, tool, switch, parallel, and human nodes; hosts wire runLoop, runTool, and handleHuman callbacks to their concrete runtime.
Project loops also support governance fields:
{
"version": "1",
"kind": "graph",
"name": "governed-build",
"context": "shared",
"hooks": {
"loop:start": [
{ "tool": "unix_time", "saveAs": "timing.start" }
],
"tool:after": [
{ "tool": "audit_step", "input": { "level": "info" }, "saveAs": "audit.last", "onError": "continue" }
],
"loop:end": [
{ "tool": "unix_time", "saveAs": "timing.end" }
]
},
"policies": [
{
"id": "only-safe-tools",
"hook": "tool:before",
"effect": "allow",
"when": "tool.name == 'read' || tool.name == 'grep' || tool.name == 'unix_time'"
},
{
"id": "deny-shell",
"hook": "tool:before",
"effect": "deny",
"when": "tool.name == 'bash'",
"message": "bash requires an explicit build loop"
}
],
"start": "capture_start",
"steps": {
"capture_start": {
"type": "tool",
"tool": "unix_time",
"saveAs": "timing.start",
"next": "plan"
},
"plan": {
"type": "agent",
"systemPrompt": "Plan the work. Do not edit files.",
"tools": ["read", "grep"],
"next": "end"
}
}
}Lifecycle hooks are deterministic tool actions run by the host runtime at loop:start, step:before, model:before, tool:before, tool:after, step:after, loop:transition, and loop:end. Hook when guards are evaluated against the shared context plus lifecycle payload such as step.name, step.type, tool.name, tool.input, and transition.from/to. Hook outputs are saved into the context bag with saveAs; onError: "continue" turns a failed hook into trace-only telemetry, while the default is fail-closed.
Policies are evaluated before hook actions at the same lifecycle point. deny fails the loop with LoopPolicyDeniedError, approval raises LoopApprovalRequiredError, and allow policies form an allow-list for that lifecycle point when at least one exists. If an allow-list is present and no allow rule matches, the loop is blocked.
When the host wires LoopRunStore, chat completions create durable loop runs, append every loop_trace event, and return loop_run_id. When the host also wires ApprovalStore, approval policies create a pending approval request and mark the loop run as awaiting_approval with approvalRequestId. The SDK exposes getLoopRuns() and getLoopRun(id) for audit/history surfaces. Streaming completions still emit each trace incrementally.
SDK
Client SDK
import { PolpoClient } from "@polpo-ai/sdk";
const client = new PolpoClient({
baseUrl: "http://localhost:3890",
});
const tasks = await client.getTasks();
const agents = await client.getAgents();React SDK
import { PolpoProvider, useTasks, useAgents } from "@polpo-ai/react";
function App() {
return (
<PolpoProvider baseUrl="http://localhost:3890">
<TaskList />
</PolpoProvider>
);
}
function TaskList() {
const { tasks, createTask } = useTasks();
// Real-time updates via SSE
return <ul>{tasks.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}Development
git clone https://github.com/lumea-labs/polpo.git
cd polpo
pnpm install
pnpm build
pnpm testProject structure
src/ Main package source
adapters/ Node.js runtime adapters (engine, filesystem, shell)
assessment/ Quality scoring and LLM review
cli/ Commander CLI commands
core/ Orchestrator wiring + re-exports from @polpo-ai/core
server/ Hono HTTP server + routes
stores/ File-based store implementations
tools/ Tool implementations (browser, email, PDF, etc.)
packages/
core/ @polpo-ai/core -- pure business logic
drizzle/ @polpo-ai/drizzle -- SQL store implementations
server/ @polpo-ai/server -- shared Hono route factories
client-sdk/ @polpo-ai/sdk -- TypeScript client
react-sdk/ @polpo-ai/react -- React hooks
dashboard/ @polpo-ai/dashboard -- reusable v2 views
tools/ @polpo-ai/tools -- tool definitions
vault-crypto/ @polpo-ai/vault-crypto -- encryption
examples/
chat-app/ React chat app example
apps/
dashboard/ Single-tenant Next.js dashboard host
docker/self-host/ Compose example and isolated E2E fixtureCloud
Polpo Cloud is the managed version at polpo.sh. It uses the same open-source core with managed infrastructure: Neon PostgreSQL, sandboxed execution, and a dashboard.
License
Apache 2.0 -- Lumea Labs
