pleach
Get Started

Upgrading to @pleach/core

When `simpleChatbot` and `createPleachRoute` aren't enough — direct `SessionRuntime` use, custom plugins, custom tools.

The quickstart surface — createPleachRoute() and simpleChatbot() — covers most "I want a streaming chat" cases. When you need more control, you drop one layer down to @pleach/core directly. This page walks the upgrade.

When to upgrade

Reach for @pleach/core directly when you need:

  • A HarnessPlugin — custom prompt contributions, intent resolvers, tool-call validators, stream observers, post-tool enrichment. See Plugin contract.
  • Custom tools — anything you'd write as a tool definition the agent can invoke. See Tools and Base tools.
  • Custom orchestratorConfig — pinned model, family-strict cascade rules, provider hedge policies, custom transport (Bedrock, Vertex, Azure OpenAI). See Providers.
  • Custom storage — Postgres / Supabase / your own SessionStorageAdapter. See Storage.
  • Custom rendering — your own React UI, your own streaming protocol, your own non-HTTP transport.
  • Multi-tenant isolationtenant_id scoping, per-tenant rate limiting, per-tenant cost rollup. See Multi-tenant.
  • Interrupts — pause-and-edit flows, human-in-the-loop. See Interrupts.
  • Checkpointing + time travel — rollback, replay, divergent history. See Checkpointing and Time travel.

If none of the above fit, you don't need to upgrade. Stay on the quickstart surface.

Escape hatch: bot.runtime

If you're already using simpleChatbot from @pleach/recipes and just need to reach under the hood, the Chatbot returned by simpleChatbot() exposes its underlying SessionRuntime directly:

import { simpleChatbot } from "@pleach/recipes/chatbot";

const bot = simpleChatbot({ /* ... */ });

// `bot.runtime` IS the SessionRuntime — same type, same identity.
// Reach for it directly:
const session = await bot.runtime.createSession();
const stream = bot.runtime.executeMessage(session.id, "hello");

for await (const event of stream) {
  // handle the StreamEvent discriminated union yourself
}

The escape hatch is the documented progressive-disclosure path. The factory contract at packages/recipes/src/chatbot.ts line 80 declares readonly runtime: SessionRuntime and the README marks it as the supported escape route. You can mix-and-match: use bot.ask() for the easy path, reach into bot.runtime for the hard parts.

Direct createPleachRuntime use

Skip the recipes wrapper entirely:

import {
  createPleachRuntime,
  setOrchestratorAdapterCtor,
} from "@pleach/core/runtime";

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

const runtime = createPleachRuntime({
  host: {
    strategies: {
      orchestratorConfig: {
        provider: "anthropic",
        model: "claude-sonnet-4",
        apiKey: process.env.ANTHROPIC_API_KEY!,
      },
    },
  },
  plugins: [myPlugin],
});

const session = await runtime.createSession();

for await (const event of runtime.executeMessage(session.id, "hello")) {
  switch (event.type) {
    case "text":
      process.stdout.write(event.delta);
      break;
    case "tool-call":
      console.log("tool:", event.toolName, event.args);
      break;
    case "error":
      console.error("stream error:", event.message);
      break;
  }
}

The SessionRuntime returned is the same type simpleChatbot() wraps. Session semantics are preserved: createSession() mints a session id; executeMessage(sessionId, message) appends a user turn and streams the assistant response; multi-turn memory is the default (use a fresh createSession() to start over).

Why a subpath for createPleachRuntime?

The factory imports from @pleach/core/runtime, not the top-level @pleach/core barrel. This is intentional and documented at packages/recipes/src/chatbot.ts lines 18-26.

The bundle-local _registeredOrchestratorAdapterCtor state that the factory reads is the SAME state that consumers register via setOrchestratorAdapterCtor, which is also exposed on the @pleach/core/runtime subpath. The top-level @pleach/core ESM bundle does NOT re-export the setter today; the subpath does. Using the subpath in both places makes the orchestrator-config plumbing actually wire end-to-end. See Subpath exports for the full subpath surface.

Trade-off: route handler vs direct runtime

WantUse
HTTP boundary, streaming over fetchcreatePleachRoute()
Node/CLI/script, in-processsimpleChatbot() or createPleachRuntime()
React UI, no custom transport<ChatBox /> + createPleachRoute()
Custom transport (WebSocket, SSE-with-custom-framing)createPleachRuntime() directly
Custom plugins, tools, intent resolverscreatePleachRuntime({ plugins })

Mix freely. The route handler is a thin shell around createPleachRuntime; the recipes simpleChatbot is a thin shell around createPleachRuntime; both pass options through.

Where to go next

On this page