pleach
Eval

@pleach/recipes

One-line factories that compose @pleach/core with sibling SKUs for the four most common consumer use cases — chatbot, RAG, observability, compliance — plus an instrumented coding agent.

@pleach/recipes is the use-case-targeted composition layer over @pleach/core. Each subpath export is a one-line factory that returns a wrapper around a real SessionRuntime. Reach into bot.runtime when you need the full primitive surface; reach for the recipe when the boilerplate is what's in the way.

Each recipe lives at its own subpath so consumers only pay the import cost for what they use.

npm install @pleach/recipes @pleach/core

Optional peers — install only the ones a recipe needs:

npm install @pleach/observe       # for observability + coding-agent recipes
npm install @pleach/compliance    # for compliance recipe (v0.2)
npm install @pleach/coding-agent  # for instrumentedCodingAgent
npm install @pleach/sandbox       # for instrumentedCodingAgent

simpleChatbot

A minimal conversational agent. Composes only @pleach/core; no sibling peers required. The factory lazily creates a session on first ask() and reuses it on subsequent calls so a single simpleChatbot instance holds conversational state.

import { simpleChatbot } from "@pleach/recipes/chatbot";
import { setOrchestratorAdapterCtor } from "@pleach/core/runtime";
import { MyOrchestratorAdapter } from "./my-provider"; // your adapter

// Register the OrchestratorAdapter ctor once at app boot. The recipes
// package does NOT bundle a provider — supply your own (OpenAI,
// Anthropic, OpenRouter, BYOK, etc.).
setOrchestratorAdapterCtor(MyOrchestratorAdapter);

const bot = simpleChatbot({
  systemPrompt: "You are a helpful assistant.",
  orchestratorConfig: {
    provider: "openai",
    model: "gpt-4o-mini",
    apiKey: process.env.OPENAI_API_KEY!,
  },
});
console.log(await bot.ask("hello"));
console.log(await bot.ask("what did I just say?"));

When orchestratorConfig is omitted, ask() resolves to the substrate placeholder ("This is a placeholder response from the harness runtime."). Supplying orchestratorConfig routes the call through your registered OrchestratorAdapter.

ask() returns the final assistant text. It collects the message.delta and message.complete events from runtime.executeMessage() and concatenates the deltas; streaming consumers should reach into bot.runtime directly.

A single bot instance holds the session across ask() calls. Call await bot.reset() (or its alias await bot.newSession()) to discard the memoized session id; the next ask() lazily creates a fresh session via runtime.createSession(). If the underlying stream emits an { type: "error" } event or throws, ask() rejects with a ChatStreamError and preserves the original failure on error.cause — branch on cause to distinguish provider errors from abort signals from network timeouts. The same session API and error type are inherited by ragChatbot, observableChatbot, and compliantChatbot.

Reach for this when you want a working chatbot in under ten lines and don't yet need retrieval, observability, or compliance.

ragChatbot

A chatbot plus retrieval stage. The consumer supplies the Retriever — the recipe is storage-agnostic on purpose; it does not bind a vector store.

import { ragChatbot } from "@pleach/recipes/rag";

const bot = ragChatbot({
  retriever: async (query, opts) =>
    myVectorDb.search(query, { topK: opts?.topK ?? 4 }),
});
console.log(await bot.ask("what's our refund policy?"));

Per turn, ask() calls the retriever, prepends the top-K chunks to the user message as context with stable [N] indices for citation, and delegates to the underlying simpleChatbot. When the retriever returns no chunks or throws, the message passes through unmodified — the bot stays useful if retrieval is down.

Reach for this when answers must be grounded in your documents and you already have a vector store or full-text index. Bring your own retrieval; the recipe stays out of that decision.

observableChatbot

A chatbot wrapped with @pleach/observe sub-agent attribution. Every recordCall(...) issued during ask() inherits the outer subagent path — no recorder argument threaded through call sites.

import { init } from "@pleach/observe";
import { memory } from "@pleach/observe/destinations";
import { observableChatbot } from "@pleach/recipes/observability";

const dest = memory();
init({ destination: dest });

const bot = observableChatbot({
  serviceName: "my-app-chatbot",
});
await bot.ask("hello");
console.log(dest.rows); // rows tagged subagent: "my-app-chatbot"

@pleach/observe is an optional peer. If it's not installed, ask() still works as a plain chatbot — instrumentation just doesn't fire. Set serviceName: false to disable the ALS scope when composing inside a buyer's outer scope.

Reach for this when traces, prompt/completion logs, and cost attribution are required on every turn and the boilerplate of threading the recorder through call sites isn't earning its keep.

compliantChatbot

A chatbot wrapped with PII / PHI scrubbing at the message-content boundary. As of v0.2 the profile field routes to a curated bundle from @pleach/compliance/scrubbers and applies it (plus any extraScrubbers) to the user message string before ask() forwards it to the runtime.

import { compliantChatbot } from "@pleach/recipes/compliance";

const bot = compliantChatbot({ profile: "hipaa" });
await bot.ask("patient called about prescription refill");

Profile → scrubber bundles:

  • hipaaSsnUsScrubber + UsDriverLicenseScrubber + CreditCardScrubber
  • gdprSsnUsScrubber + CreditCardScrubber
  • pci-dssCreditCardScrubber
  • soc2SsnUsScrubber + CreditCardScrubber

Scope-limit (v0.2). Scrubbing is applied at the message-content boundary, NOT at the durable EventLogWriter boundary (next iteration). Assistant responses are not currently scrubbed. EventLogWriter-boundary scrubbing is substrate-ready — @pleach/core's EventLogWriter already accepts a scrubbers: readonly Scrubber[] constructor option with a DefaultScrubberChain enforced at every write(), and the host.modules.eventLogWriter parameter on createPleachRuntime plumbs a custom writer through. Consumers needing full hash-chain attestation + attestRun today should construct ComplianceRuntime directly from @pleach/compliance.

Reach for this when HIPAA, GDPR, PCI-DSS, or SOC 2 requires the event log persisted or exported to be free of unredacted PII.

instrumentedCodingAgent

A coding agent whose executeStep(...) calls automatically inherit a @pleach/observe sub-agent attribution path. Composes @pleach/coding-agent, @pleach/sandbox, and @pleach/observe.

import { init } from "@pleach/observe";
import { memory } from "@pleach/observe/destinations";
import { createInMemorySandboxProvider } from "@pleach/sandbox/testing";
import { instrumentedCodingAgent } from "@pleach/recipes/coding-agent";

const dest = memory();
init({ destination: dest });

const agent = instrumentedCodingAgent({
  sandboxProvider: createInMemorySandboxProvider({ execHandlers: new Map() }),
  subagent: "coding-agent",
});

await agent.start();
try {
  const result = await agent.executeStep({
    userMessage: "fix the failing test in lib/parser.ts",
    stepId: "step-1",
  });
  console.log(result.ok, dest.rows); // rows tagged subagent: "coding-agent"
} finally {
  await agent.stop();
}

Compose with outer scopes for nested attribution:

import { subagent } from "@pleach/observe";

await subagent("tenant-abc").run(async () => {
  await agent.executeStep({...}); // path: ["tenant-abc", "coding-agent"]
});

init({ destination }) from @pleach/observe must be called once at process start before any executeStep; the SDK is singleton-per-process.

Reach for this when each turn decomposes into planner → executor → critic sub-roles and every LLM call needs to surface on the dashboard tagged by the sub-role that issued it.

Composing recipes by hand

Every recipe returns a Chatbot with a runtime escape hatch:

interface Chatbot {
  readonly runtime: SessionRuntime;
  ask(message: string): Promise<string>;
  newSession(): Promise<void>;
  reset(): Promise<void>;
}

When you need a shape the recipe doesn't cover — multi-session state, streaming to a UI, custom interrupt handling — drop down to bot.runtime and use executeMessage directly. Recipes are convenience over @pleach/core; nothing they wrap is hidden.

On this page