pleach
Architecture

Plugin bundles

Thematic facet sub-paths for plugins that span multiple semantic domains — barrel imports, define-helpers, and composePlugin. The second DX layer above HarnessPlugin, peer-equivalent to definePleachPlugin.

@pleach/core ships 10 thematic plugin facet sub-paths plus a top-level composePlugin() helper. They are a structured surfacing layer above HarnessPlugin, peer-equivalent to definePleachPlugin. Both APIs produce a HarnessPlugin; both route through the same PluginManager.registerPlugin path. Choose by plugin size, not by contract surface.

You are building...Reach for
A 1–5-hook plugin in one file (custom system prompt, a single safety policy, a single observer)A raw HarnessPlugin literal
A 5–10-hook plugin with autocomplete on the most-used slotsdefinePleachPlugin({ capabilities, _raw })
A 10+-hook plugin spanning 3+ semantic domains, authored across multiple filesBundle facets + composePlugin() (this page)

The 10 facet sub-paths

Each facet exports a Pick<HarnessPlugin, ...> type-narrowed view + a defineXxxPlugin identity helper + every type that facet's hooks reference. The boundaries are drawn from the runtime's collector layout — the same way the substrate already dispatches contributions internally.

Sub-pathHooks coveredWhen you reach for it
@pleach/core/plugins/lifecyclename, version, metadata, prePlanPrimer, postSynthesisGuard, onJobDispatch, onJobComplete, extraGraphNodes, eventResolver, contributeEventTypesIdentity + plan/synthesis hooks + long-running job indexing + custom graph nodes
@pleach/core/plugins/promptscontributePromptHints, contributePrompts, contributeRuntimeAwarePrompts, contributePromptContextBridge, contributeFallbackSystemPromptBuilder, contributePromptSections, contributePromptSectionGeneratorBundle, contributeMetaLearningContext, contributeSynthesisDirectiveBlocksAnything that touches the system-prompt seam — persona, runtime-aware sections, meta-learning context, synthesis directives
@pleach/core/plugins/toolscontributeTools, contributeBatchingHints, registerAsyncExecutors, contributeIntentToolMap, contributeToolCouplingHints, contributeToolFollowUpIntents, contributeEntityExtractors, contributeIntentClassifiers, contributeIntentMentionDetectorTool definitions + how the planner reaches for them + post-tool follow-up + entity extraction
@pleach/core/plugins/streamcontributeStreamObservers, contributeObserverConsumers, contributeStreamChunkHandlers, contributeStreamEventHandlersAdapters, contributeStreamEndDiagnosticsAnalyzer, contributeDetectionRulesPer-chunk observation, mutation, provider-detection rules, stream-end diagnostics
@pleach/core/plugins/safetycontributeSafetyPolicies, contributeFabricationDetectors, contributeFabricationDetectorRules, contributeFabricationGuard, contributeRefClassValidators, contributeHallucinatedToolDetectorsThe complete safety pipeline — refusal patterns, fabrication detectors, ref-class validators
@pleach/core/plugins/synthesiscontributeFinalizationPasses, contributeCitationRuleSet, contributeCitationEntityExtractor, contributeCitationInjectorPost-synthesis sanitizers + citation eligibility + citation injection
@pleach/core/plugins/retrycontributeRetryPolicy, contributeContinuationPolicy, contributeFamilyPivot, contributeFamilyExhaustedSurfaceRetry loops, continuation resolver, model-family cascade
@pleach/core/plugins/middlewarecontributeMiddleware, contributeRuntimeAwareMiddlewareAI-SDK middlewares + orchestrator-level interceptors
@pleach/core/plugins/sandboxcontributeSandboxBridge, contributeSandboxInitialization, contributeSandboxAvailabilityEvaluatorSandbox completion plumbing — only if your plugin runs sandboxed code
@pleach/core/plugins/uicontributeApprovalFlow, contributeInterruptUIHandlers, contributeChatManifestProviderUI-coupled bits — interrupt handlers, approval policy, manifest provider

The taxonomy is finite by design. Adding an 11th facet requires a contract decision. Most plugins fill 1–3 facets.

What's not in a facet

Some HarnessPlugin hooks are operator-level knobs, not plugin- author surface: the host-private strategy hooks (contributeEntityNameCounter, contributeStructurePrefetcher, contributeArtifactCacheReader, contributeGetDataHandlerFactory, contributeDataChannelRefetch, contributeContinuationShadowResolver, contributeGarbledOutputRecorder, contributePreserveDataRefFields, contributeGuestDeniedTools), plus contributeFabricationGuard (which is the alternative-shape companion to contributeFabricationDetectors and stays umbrella-only on purpose). Reach for them through definePleachPlugin._raw or set them directly when constructing the runtime.

The mechanic — three pieces per facet

Each @pleach/core/plugins/<facet> sub-path exports exactly three things, illustrated for the safety facet:

// What you import:
import {
  defineSafetyPlugin,
  type SafetyPluginFacet,
  type FabricationDetector,
  type SafetyContribution,
  type RefClassValidator,
} from "@pleach/core/plugins/safety";
  1. SafetyPluginFacet — a Pick<HarnessPlugin, ...> view. This is the safety-only mental model. Every hook your safety facet can contribute is here; nothing else compiles.
  2. defineSafetyPlugin(facet) — an identity function that returns the facet. Its only job is the type inference: passing a literal to it makes TypeScript infer the tightest possible types for your contributions.
  3. The types your contributions reference — FabricationDetector, SafetyContribution, RefClassValidator. You don't have to reach back to @pleach/core for them.

composePlugin() — assembling a multi-facet plugin

// lib/plugins/myDomainPlugin.ts
import { composePlugin } from "@pleach/core/plugins";
import { definePromptsPlugin } from "@pleach/core/plugins/prompts";
import { defineSafetyPlugin } from "@pleach/core/plugins/safety";
import { defineToolsPlugin } from "@pleach/core/plugins/tools";
import { defineStreamPlugin } from "@pleach/core/plugins/stream";
import { defineLifecyclePlugin } from "@pleach/core/plugins/lifecycle";

const promptFacet = definePromptsPlugin({
  name: "my-domain",
  version: "1.0.0",
  contributeRuntimeAwarePrompts: (ctx, state) => [
    /* per-turn prompt blocks */
  ],
  contributeSynthesisDirectiveBlocks: (ctx) => [
    /* synthesis-time directives */
  ],
});

const safetyFacet = defineSafetyPlugin({
  name: "my-domain",
  version: "1.0.0",
  contributeSafetyPolicies: () => DOMAIN_SAFETY_POLICIES,
  contributeFabricationDetectors: () => DOMAIN_FABRICATION_DETECTORS,
});

const toolsFacet = defineToolsPlugin({
  name: "my-domain",
  version: "1.0.0",
  contributeTools: () => DOMAIN_TOOLS,
  contributeIntentToolMap: () => DOMAIN_INTENT_TOOL_MAP,
});

export const myDomainPlugin = composePlugin(
  { name: "my-domain", version: "1.0.0" },
  promptFacet,
  safetyFacet,
  toolsFacet,
);

composePlugin(base, ...facets) merges via Object.assign({}, ...facets, base) — base last so name and version cannot be overridden by a facet that drifted. Facets earlier in the argument list lose to facets later on per-hook collision; in practice you don't contribute the same hook from two facets in one plugin (the build catches it).

The output is a plain HarnessPlugin. The substrate has no idea it was composed.

import { SessionRuntime } from "@pleach/core";

const runtime = new SessionRuntime({
  storage: new SupabaseAdapter({ client: supabase }),
  checkpointer: new SupabaseSaver({ client: supabase }),
  plugins: [myDomainPlugin, compliancePlugin, gatewayPlugin],
  userId: "user_123",
});

Type inheritance — why Pick<> and not a new interface

Facets are type-narrowing views. If contributeFabricationDetectors's signature widens at the canonical HarnessPlugin interface — say, the context argument gains a new field — every facet picks up the change automatically with zero edits. This is what keeps audit:plugin-contract-completeness running as your authoritative contract gate. Bundles are not a parallel type to maintain; they are a structured projection.

Authoring across files

The intended usage is one facet per file in a facets/ directory:

src/myDomainPlugin/
├── index.ts               # composePlugin(base, ...facets)
├── facets/
│   ├── prompts.ts         # definePromptsPlugin({ ... })
│   ├── safety.ts          # defineSafetyPlugin({ ... })
│   ├── tools.ts           # defineToolsPlugin({ ... })
│   ├── stream.ts          # defineStreamPlugin({ ... })
│   └── lifecycle.ts       # defineLifecyclePlugin({ ... })
└── strategies/            # implementation details — pure functions
    ├── fabrication.ts
    ├── policies.ts
    └── observers.ts

Each facet file imports its define* helper and the types it needs from one sub-path. A reviewer reading safety.ts doesn't need to context-switch to understand what's in scope — the import line shows the surface area, and TypeScript's autocomplete only suggests members of the facet view.

For a worked example of this shape at scale (~42 hooks across 5 facets, ~400 LoC of business logic distributed across the files), see the open-source plugin authoring layout linked under Reference apps.

Empty implementations are now explicit

The 10-facet split makes "this plugin chose not to contribute this" structurally legible. Compare:

// Without facets — the absence of `contributeChatManifestProvider`
// is invisible unless you know to look for it:
export const myPlugin: HarnessPlugin = {
  name: "my-plugin",
  contributePrompts: () => [...],
  contributeSafetyPolicies: () => [...],
  contributeTools: () => [...],
  // ... 30 other contribute* hooks
};

// With facets — each file's surface area is exactly what's
// in scope, and absent hooks are absent from the file entirely:
export const safetyFacet = defineSafetyPlugin({
  name: "my-plugin",
  version: "1.0.0",
  contributeSafetyPolicies: () => [...],
  contributeFabricationDetectors: () => [...],
  // contributeRefClassValidators not contributed — intentional
});

Diagnostic breadcrumbs

Two runtime diagnostics help authors notice when the construction shape diverges from what the runtime expected. Both are non-blocking — they emit once per session at startup.

[Pleach:facet-not-contributed] — missing domain plugin

When no plugin in the runtime contributes any hook for a high-leverage facet, the runtime emits one breadcrumb naming the facet. This is diagnostic of a misconfigured runtime — a domain plugin was probably forgotten at registration time — not a per-plugin design choice.

Instrumented for 5 facets where the aggregate-empty state is suspicious:

FacetWhy aggregate-empty matters
safetyNo safety policies, fabrication detectors, or ref-class validators anywhere in the runtime
toolsZero tool definitions across all plugins — synthesize-only mode
promptsNaked baseline prompt — almost certainly missing a domain plugin
streamNo observers + no detection rules — silent PII / metrics gap
lifecycleNo extraGraphNodes, no plan primer, no job dispatch hooks — no domain integration

Not instrumented: sandbox, middleware, ui, retry, synthesis — aggregate-empty here is a legitimate operational choice (many runtimes don't sandbox, don't use React, inherit default retry behavior, and so on).

A correctly-configured runtime with at least one domain plugin typically emits zero of these breadcrumbs.

[Pleach:single-facet-composePlugin] — over-ceremony

When composePlugin(base, oneFacet) is called with exactly one facet in a non-production build, the runtime suggests the flat factory — definePleachPlugin({capabilities}) — as a shorter path. Bundles pay off at 2+ facets across 2+ files; one facet is just ceremony. Suppressed in production.

Suppress either breadcrumb with the standard breadcrumb opt-out.

Choosing the right construction path

You don't have to commit to one. All three forms are interoperable; they all produce a HarnessPlugin that registers the same way.

// Mix and match — a small plugin embedded inside a larger app:
const runtime = new SessionRuntime({
  /* ... */
  plugins: [
    myDomainPlugin,                    // composePlugin(...) — multi-facet
    rawPlugin satisfies HarnessPlugin, // raw literal — one observer
    definePleachPlugin({                // flat factory — 3 capabilities
      name: "telemetry",
      version: "0.1.0",
      capabilities: {
        streamObservers: [recordChunks],
      },
    }),
  ],
});

A useful rule of thumb: switch from definePleachPlugin to bundle facets when your plugin's single-file shape stops fitting on one screen, or when you find yourself wanting to test one domain's contributions in isolation. Both are signals the multi-file shape is paying off.

  • Plugin contract — the underlying HarnessPlugin interface and the definePleachPlugin factory.
  • Concept clusters — why plugins are a thematic island, and where bundles sit relative to the six cluster triplets.
  • Reference apps — open-source plugin authoring at multi-facet scale.
  • Fabrication detection — the safety facet's largest hook (contributeFabricationDetectors).
  • Prompts — the prompt facet's composition order.

On this page