pleach

Quickstart

For a SaaS engineer adding streaming chat. Three files, one env var, one minute.

If you're adding a streaming chat surface to an existing app — Next.js, Astro, SvelteKit, Remix, Vite, Workers, Hono, plain Node — this is the shortest path. It's the same wire-level surface Getting started covers; this page goes deeper on the env-var detection, the error contract, and what useChat actually gives you.

Already have an agent loop?

This page adds the full runtime to build a chat surface. To add just the audit row to a loop you already run — no rewrite — see @pleach/observe (days away on npm), or graft the runtime over your existing infra with createBrownfieldRuntime on Adoption paths.

One install, one env var, three files

npm install @pleach/core

Set one provider env var:

# .env (or your platform's env config)
ANTHROPIC_API_KEY=sk-ant-...
// app/api/chat/route.ts
import { createPleachRoute } from "@pleach/core/quickstart";

export const POST = createPleachRoute();
// app/page.tsx
"use client";
import { ChatBox } from "@pleach/core/quickstart";

export default function Home() {
  return <ChatBox apiUrl="/api/chat" />;
}

Run npm run dev. You have a streaming chat against Anthropic.

.env setup

The runtime checks env vars in priority order. The first one with a non-empty value wins. Default order is alphabetical — not preference-biased — so ANTHROPIC_API_KEY wins over OPENROUTER_API_KEY when both are set. This behavior is regression-tested at packages/core/test/quickstart/providerDetection.test.mjs.

ProviderEnv var
AnthropicANTHROPIC_API_KEY
DeepSeekDEEPSEEK_API_KEY
GoogleGOOGLE_GENERATIVE_AI_API_KEY (or legacy GOOGLE_API_KEY)
MistralMISTRAL_API_KEY
MoonshotMOONSHOT_API_KEY
OpenAIOPENAI_API_KEY
OpenRouterOPENROUTER_API_KEY

The seven-provider surface mirrors the ProviderFamily set exported from @pleach/core/modelfamily plus OpenRouter as a gateway transport. The (family × callClass) resolution matrix itself is host-supplied — reached through the AgentAdapter.resolveModel<C>() seam, not shipped in the package. See Provider detection for the full resolution matrix and how to override priority.

What you get

  • Streaming response<ChatBox /> renders tokens as they arrive via the useChat hook, which speaks Pleach's StreamEvent discriminated union directly.
  • Multi-turn memoryuseChat owns a sessionId and reuses it across calls. Each user message appends to the same session history.
  • bot.reset() equivalent — in the React surface, this is clearMessages() on the useChat return; on the recipes path (below), it's bot.reset() or bot.newSession() on the Chatbot.
  • ChatStreamError rejection — when the underlying stream emits { type: "error" } or the generator throws, the failure surfaces to the caller. The original cause is preserved on error.cause; any stream-emitted code is preserved on error.code. The factory contract is at packages/recipes/src/chatbot.ts.

Beyond one route: the whole family

createPleachRoute mounts the single execute leg. When you want the rest of what the runtime speaks — sessions CRUD, interrupts, checkpoints, rollback, events-recall, sync, fork — reach for the standalone-agent façade from the same subpath:

// app/api/harness/[...path]/route.ts
import { createPleachAgent } from "@pleach/core/quickstart";

const agent = createPleachAgent({
  context: { provider: "anthropic", systemPrompt: "You are a helpful assistant." },
  // tools defaults to the base-tools bundle
  // db omitted → in-memory durable store (recall works, no Supabase)
});

export const { GET, POST, PUT, DELETE } = agent.nextRoutes();

createPleachAgent({ context, tools, db }) takes three arms and hands back the whole route family plus agent.store for direct reads. Omit db and it uses an in-memory DurableStore — recall and the audit ledger work with no external database, state resets on restart. See Standalone agent for the lower-level createPleachRoutes factory and the durable interrupt-resolver note.

Troubleshooting

"No provider API key found in environment." (HTTP 503)

The route handler fails loudly when no provider env var is set rather than silently falling back to a stub. Surface the 503 in your client; it's the signal that ANTHROPIC_API_KEY (or your equivalent) didn't reach the runtime.

The error payload is:

{
  "error": "no_provider_detected",
  "detail": "No provider API key found in environment. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, or pass `provider` to createPleachRoute()."
}

Fix: set one of the env vars from the table above, or pass provider: "anthropic" explicitly to createPleachRoute({ provider }).

For keyless local dev — no env var, no 503 — pass createPleachRoute({ demo: process.env.NODE_ENV !== "production" }). With no key it drives a real graph turn over canned model text and writes real audit rows (response header x-pleach-mode: demo); a resolved key always wins, and it stays off in a production build. It's the mode npx pleach init scaffolds, behind an /audit view.

localStorage is not defined

If you see this from the runtime in an SSR / Node context (server route, edge function, test harness), it was resolved 2026-06-15 in fix(core): generateClientId guards window.localStorage consistently (jsdom + SSR safe). The fix lives at packages/core/src/utils/uuid.tsgenerateClientId() now checks typeof window === "undefined" || !window.localStorage before attempting any read.

Upgrade @pleach/core to a version published after 2026-06-15.

ChatStreamError rejection

Chatbot.ask() (on the recipes path) rejects with ChatStreamError when the underlying stream fails. The original failure is preserved:

try {
  await bot.ask("hello");
} catch (err) {
  if (err instanceof ChatStreamError) {
    console.error("stream failed", err.message);
    console.error("root cause:", err.cause);
    console.error("stream code:", err.code);
  }
}

Common root causes: provider API key invalid (check .env), provider rate limit hit, request aborted upstream. Inspect err.cause for the provider-level error.

Alternative: @pleach/recipes/chatbot (no route handler)

If you don't need an HTTP boundary — for example you're building a CLI, a Node script, or a server-internal helper — reach for the recipes path:

npm install @pleach/recipes @pleach/core
import { simpleChatbot } from "@pleach/recipes/chatbot";
import { setOrchestratorAdapterCtor } from "@pleach/core/runtime";

// At app boot: register your provider adapter ctor once.
setOrchestratorAdapterCtor(MyOrchestratorAdapter);

const bot = simpleChatbot({
  systemPrompt: "You are a helpful assistant.",
  orchestratorConfig: {
    provider: "anthropic",
    model: "claude-sonnet-4",
    apiKey: process.env.ANTHROPIC_API_KEY!,
  },
});

console.log(await bot.ask("hello"));
console.log(await bot.ask("what did I just say?"));  // same session
await bot.reset();
console.log(await bot.ask("fresh conversation"));    // new session

Honest scope-limit: unlike the route handler, simpleChatbot does NOT auto-detect provider env vars. You supply the provider explicitly via orchestratorConfig and register the adapter ctor at app boot. When orchestratorConfig is omitted, ask() resolves to the substrate placeholder string ("This is a placeholder response from the harness runtime.") — by design, so consumers without a wired provider see something deterministic rather than a crash.

The bot.runtime field is the underlying SessionRuntime. Use it directly when you need plugins, checkpoint inspection, custom observers, or anything below the ask() surface. See Upgrading to @pleach/core.

Where to go next

On this page