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 isolation —
tenant_idscoping, 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
| Want | Use |
|---|---|
| HTTP boundary, streaming over fetch | createPleachRoute() |
| Node/CLI/script, in-process | simpleChatbot() or createPleachRuntime() |
| React UI, no custom transport | <ChatBox /> + createPleachRoute() |
| Custom transport (WebSocket, SSE-with-custom-framing) | createPleachRuntime() directly |
| Custom plugins, tools, intent resolvers | createPleachRuntime({ 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
Runtime construction
`SessionRuntime` constructor + `createPleachRuntime` factory in full detail.
Session runtime
The 17-method `SessionRuntime` surface — sessions, messages, plugins, observers.
Plugin contract
`HarnessPlugin` — the 50+ contribution hooks for customizing runtime behavior.
Subpath exports
`@pleach/core` ships 40+ subpaths. Which one to import from.
Providers
Provider switching, family-strict cascade, model resolution.