Adoption paths
Two ways to adopt Pleach — brownfield (hook @pleach/observe into your existing agent loop) or greenfield (take @pleach/core as your substrate).
There are two ways to adopt Pleach. One is to leave your agent
loop where it is and add the
@pleach/observe SDK around it. The other is
to take @pleach/core as the substrate and
migrate the loop onto it. Both land you 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 two paths
| Brownfield (SDK) | Greenfield (runtime) | |
|---|---|---|
| Package | @pleach/observe | @pleach/core |
| Adoption shape | ~15 lines around your existing LLM calls. | New SessionRuntime per process; rewrite the loop. |
| Audit row | ObserveRow — strict subset of AuditableCall v7. | Full AuditableCall v7. |
| Destinations | Postgres / Supabase / OTel / Memory / custom. | Postgres / Supabase / Memory / any ProviderDecisionLedger. |
| Family-locked routing | No. | Yes. |
| Replay determinism | No. | Yes. |
| Channels + interrupts | No. | Yes. |
| Checkpoint / restore | No. | Yes. |
Time-travel via @pleach/replay | No. | Yes. |
| Provider swap without breaking dialect | Partial — observability only. | Yes — substrate-level. |
| Migration cost | ~15 LoC per loop. | Loop rewrite; runtime config; tests. |
The rows aren't a hierarchy. The greenfield path is bigger because it gives more; brownfield is smaller because it asks less. Picking the smaller one when you don't need the bigger one 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 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 brownfield path lets you wire one loop, watch the row land, and decide whether to widen.
The brownfield 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 brownfield surface for the common case. The loop you had before stays the loop you have now.
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.
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 brownfield path is ~15 lines away.
Where to go next
@pleach/observe
The brownfield SDK — facade surface, the four destinations, what's hookable and what isn't.
@pleach/core
The greenfield substrate. SessionRuntime, channels, family-locked routing, replay determinism, checkpointing.
Migrating from the Vercel AI SDK
The greenfield walkthrough for the most common starting point. Keep the AI SDK as your provider; swap the loop for a runtime.
Migrating from LangChain
The greenfield walkthrough 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.