Adoption paths
Three ways to adopt Pleach — observe brownfield (@pleach/observe watches your existing loop), core brownfield (core governs the loop through adapters over your own infra), or greenfield (a fresh @pleach/core runtime on core defaults).
There are three ways to adopt Pleach. They differ on one axis: who owns the turn loop, and what happens to the infrastructure you already run.
- Observe brownfield. You keep the loop. Add the
@pleach/observeSDK around it and it watches — one audit row per call, no behavior change. - Core brownfield (adapter graft). Core owns the loop, but your provider client, your database, and your tools stay yours — wrapped as adapters. No loop rewrite; you reuse the infra core doesn't ship a built-in adapter for.
- Greenfield. Core owns the loop and you build on core's
defaults — the Memory/Supabase adapters,
AiSdkProvider, base-tools. Nothing existing to wrap.
The one-liner that separates them: observe watches; core governs; and core-brownfield lets core govern while your infra stays yours. All three land at the same audit row; the cost shape and the feature ceiling are different.
This page is the decision. The per-stack walkthroughs
(Vercel AI SDK,
LangChain,
Anthropic Enterprise,
OpenAI Enterprise)
sit downstream of it. The migration guides are the
recognized-provider case of core-brownfield: core ships
AiSdkProvider or a LangChain adapter, so your provider drops
in. When your infra is bespoke, the same graft works — you
implement the adapter interface yourself.
The three paths
| Observe brownfield (SDK) | Core brownfield (adapter graft) | Greenfield (runtime) | |
|---|---|---|---|
| Package | @pleach/observe | @pleach/core | @pleach/core |
| Who owns the turn loop | You | Core | Core |
| Your existing infra | Untouched | Wrapped as adapters (provider · store · tools) | Replaced by core defaults |
| Adoption shape | ~15 lines around your LLM calls | Implement OrchestratorAdapter + StorageAdapter over your infra; one plugin for domain logic | New SessionRuntime on core defaults |
| Behavior change | None (watch only) | Full runtime governance | Full runtime governance |
| Audit row | ObserveRow — strict subset of AuditableCall v7. | Full AuditableCall v7. | Full AuditableCall v7. |
| Destinations | Postgres / Supabase / OTel / Memory / custom. | Your store, wrapped as a StorageAdapter. | Postgres / Supabase / Memory / any ProviderDecisionLedger. |
| Family-locked routing | No | Yes | Yes |
| Replay determinism | No | Yes | Yes |
| Channels + interrupts | No | Yes | Yes |
| Checkpoint / restore | No | Yes | Yes |
Time-travel via @pleach/replay | No | Yes | Yes |
| Provider swap without breaking dialect | Partial — observability only. | Yes — substrate-level. | Yes — substrate-level. |
| Migration cost | ~15 LoC per loop. | Adapter shims + one plugin; no loop rewrite. | Fresh build. |
The rows aren't a hierarchy. Observe asks the least and gives the least; greenfield gives the most and asks you to build fresh. Core-brownfield sits between them: full runtime governance over infrastructure you already run. Picking the one that matches what you already have is the right call.
The brownfield path is a runway, not a dead end
Every choice you make on the SDK destination travels with you when you adopt the full runtime. The two surfaces are designed to compose:
- The runtime detects an
@pleach/observeinitconfig at startup. Runtime-side audit writes route through the SDK's transport rather than opening a parallel sink. recordCall()is a no-op wheninit()hasn't run, and inside a runtime-managed turn the runtime's turn id wins — SDK call sites can drop theirif (observe)guards while the runtime writes through the same destination.ObserveRowis a strict subset ofAuditableCallv7, so rows the SDK wrote yesterday remain valid v7 rows the runtime can read tomorrow. ThetenantId,turnId, and(model, family, callClass)join keys don't change.- Fingerprint compute is shared. Both surfaces import from
@pleach/core/fingerprint; no two implementations to drift.
A buyer who adopts the SDK first and the runtime second keeps
the audit history. The migration is monotonic by construction,
not by a legacy_* column.
Pick observe-brownfield when
- You already run an agent loop on the Vercel AI SDK, LangChain, or a thin wrapper over a provider SDK, and you don't want to rewrite it this quarter.
- A regulator, a finance team, or a customer is asking for
per-tenant cost attribution —
(tenantId, turnId, toolName, model, tokens, costUSD)joinable to your billing schema — and the row is what's missing, not the loop. - You already have an OTel collector (Honeycomb, Datadog, Grafana Tempo, an OTLP gateway). The OTel destination emits each row as a span on a backend you already operate.
- You want to start small. The observe path lets you wire one loop, watch the row land, and decide whether to widen.
The observe shape:
import { init, recordCall } from "@pleach/observe";
import { postgres } from "@pleach/observe/destinations";
init({ destination: postgres({ connectionString: process.env.PG_URL! }) });
const startedAt = Date.now();
const reply = await yourExistingLLMCall(messages);
recordCall({
turnId: sessionId,
providerId: "anthropic",
family: "anthropic",
callClass: "synthesize",
model: "claude-sonnet-4-6",
inputTokens: reply.usage.inputTokens,
outputTokens: reply.usage.outputTokens,
costUSD: reply.usage.costUSD,
startedAt,
completedAt: Date.now(),
tags: { tenantId },
});That's the whole observe surface for the common case. The loop you had before stays the loop you have now.
Pick core-brownfield when
- You already run an agent loop and you need runtime behavior — routing, replay, checkpoints, interrupts, safety enforcement — not just the audit row, but you do not want to throw away your provider client, your database, or your tools.
- Your provider or store is bespoke — an in-house gateway, a
wrapped Bedrock or Vertex client, a Postgres schema that isn't
Supabase — so there's no built-in adapter. You implement
OrchestratorAdapter/StorageAdapteryourself; each is a small interface. - You want core to govern the turn — enforce safety policies, correct fabrications, halt a bad chunk mid-stream, walk a family-locked cascade — while every I/O boundary stays your code.
The adapter-graft shape wires your infra underneath the runtime:
import { createPleachRuntime, setOrchestratorAdapterCtor } from "@pleach/core/runtime";
import { definePleachPlugin } from "@pleach/core";
// 1. Wrap your existing LLM call as an OrchestratorAdapter (core drives it).
setOrchestratorAdapterCtor(MyExistingProviderAdapter);
// 2. Wrap your existing DB as a StorageAdapter + Checkpointer, and fold
// your prompts / tools / safety rules into ONE plugin.
const runtime = createPleachRuntime({
storage: new MyPostgresStorageAdapter(pool),
checkpointer: new MyPostgresCheckpointer(pool),
plugins: [myDomainPlugin],
host: { strategies: { orchestratorConfig: { /* your provider config */ } } },
});
// Core now owns the lattice, family-lock, replay, checkpoints, interrupts —
// over YOUR provider, YOUR store, YOUR tools. No loop rewrite.Adopt storage and checkpointer as a pair
A SessionRuntime splits durable state across two adapters: the
StorageAdapter holds the session row (messages, metadata) and the
Checkpointer holds graph/turn state. If you supply a custom durable
StorageAdapter but leave the checkpointer defaulted (the in-memory
MemorySaver), resumeSession reconciles graph state from the empty
checkpointer and writes it back — silently overwriting session data.
Always pass a matching durable Checkpointer alongside a custom
StorageAdapter (SupabaseAdapter + SupabaseSaver, or your own pair).
Conversation messages are written by executeMessage through both adapters —
never by a hand-spread saveSession. The createBrownfieldRuntime
menu factory (exported from @pleach/core/runtime, taking one flat
BrownfieldRuntimeMenu options object) enforces this pairing for you;
or wire both fields explicitly as shown above.
The bundled examples/brownfield-adapter/ in the
@pleach/core tarball is a runnable end-to-end version — it
implements both adapter interfaces over a fake "existing app".
The migrating-from-* guides are the recognized-provider
instances of this same graft, where core already ships the
wrapper.
Pick greenfield when
- You need replay determinism — recording a turn today and
replaying it against a fresh runtime tomorrow, byte-for-byte,
for regression tests or an
@pleach/evalsuite. The SDK writes a row; replay needs the event log. - You need family-locked routing — the cascade-on-503 walk
that stays inside one provider family and refuses to silently
widen across families. That's a
GatewayClientdecision, made before the wire call; the SDK observes calls, it doesn't make them. - You need reactive channels — fan-out, back-pressure, and
interrupt handling tied to the same session. Those live in
@pleach/core/channels. - You need checkpoint / restore for long-running sessions
that survive process restarts, or time-travel forking via
@pleach/replayfor what-if analysis. Both consume the canonical event log. - You're starting fresh, the loop doesn't exist yet, and the cost of building on the runtime substrate is the same as the cost of building any other way.
- You're happy on core's built-in defaults — the Memory or
Supabase adapters,
AiSdkProvider, base-tools. The difference from core-brownfield is you're not wrapping existing infra; there's nothing to graft, so you take the defaults.
The greenfield shape leads with the runtime:
import { SessionRuntime, AiSdkProvider } from "@pleach/core";
import { anthropic } from "@ai-sdk/anthropic";
const runtime = new SessionRuntime({
provider: new AiSdkProvider({
model: anthropic("claude-sonnet-4-6"),
maxSteps: 5,
}),
userId: req.user.id,
});
const session = await runtime.createSession({
tools: { enabled: Object.keys(tools) },
});
for await (const event of runtime.executeMessage(session.id, prompt)) {
// stream events, write audit rows, walk the family cascade —
// all of it is the runtime's job, not yours.
}The migration walkthroughs for the four common starting points — AI SDK, LangChain, Anthropic Enterprise, OpenAI Enterprise — each show the loop rewrite step by step.
Pick neither when
The two paths cover most starting points, but not all of them. Stay where you are when:
- You're shipping a single-shot RAG bot with no tools, no sub-agents, and no per-tenant attribution requirement. The row is overhead you won't recoup.
- Your audit need is satisfied by the provider's own dashboard (an Anthropic Workspace, an OpenAI Project, a Bedrock invocation log). Adding a second ledger to maintain isn't free.
- You're prototyping an agent shape and the loop is going to change three more times this week. Wire the SDK once the shape stabilizes.
The voice on those constraints is information, not gatekeeping — if you do need the row later, the observe path is ~15 lines away.
Where to go next
@pleach/observe
The observe-brownfield SDK — facade surface, the four destinations, what's hookable and what isn't.
@pleach/core
The runtime substrate. SessionRuntime, channels, family-locked routing, replay determinism, checkpointing.
Host adapters
The setHarnessModuleLoader seam — for hosts mid-migration wiring their own implementations of capabilities the substrate hasn't fully absorbed yet.
Migrating from the Vercel AI SDK
The recognized-provider graft for the most common starting point. Keep the AI SDK as your provider; core drives the loop.
Migrating from LangChain
The recognized-provider graft from LangChain. Adapter-bridged tools and agents stay; the orchestrator becomes a SessionRuntime.
Runtime construction
When you need to drop below `createPleachRoute` — the `createPleachRuntime` factory, the `SessionRuntime` constructor, and the React `HarnessProvider`.
Project layout
Pleach doesn't prescribe a layout. The runtime is constructed in user code; adapters are constructor args. A working default plus per-use-case shapes.