pleach
Get Started

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/observe SDK 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 loopYouCoreCore
Your existing infraUntouchedWrapped as adapters (provider · store · tools)Replaced by core defaults
Adoption shape~15 lines around your LLM callsImplement OrchestratorAdapter + StorageAdapter over your infra; one plugin for domain logicNew SessionRuntime on core defaults
Behavior changeNone (watch only)Full runtime governanceFull runtime governance
Audit rowObserveRow — strict subset of AuditableCall v7.Full AuditableCall v7.Full AuditableCall v7.
DestinationsPostgres / Supabase / OTel / Memory / custom.Your store, wrapped as a StorageAdapter.Postgres / Supabase / Memory / any ProviderDecisionLedger.
Family-locked routingNoYesYes
Replay determinismNoYesYes
Channels + interruptsNoYesYes
Checkpoint / restoreNoYesYes
Time-travel via @pleach/replayNoYesYes
Provider swap without breaking dialectPartial — 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/observe init config at startup. Runtime-side audit writes route through the SDK's transport rather than opening a parallel sink.
  • recordCall() is a no-op when init() hasn't run, and inside a runtime-managed turn the runtime's turn id wins — SDK call sites can drop their if (observe) guards while the runtime writes through the same destination.
  • ObserveRow is a strict subset of AuditableCall v7, so rows the SDK wrote yesterday remain valid v7 rows the runtime can read tomorrow. The tenantId, 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 / StorageAdapter yourself; 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/eval suite. 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 GatewayClient decision, 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/replay for 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

On this page