# Plugin bundles (/docs/plugin-bundles)



`@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`](/docs/plugin-contract#definepleachplugin--the-typed-factory).
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 slots                                     | [`definePleachPlugin({ capabilities, _raw })`](/docs/plugin-contract#definepleachplugin--the-typed-factory) |
| A 10+-hook plugin spanning 3+ semantic domains, authored across multiple files                  | Bundle facets + `composePlugin()` (this page)                                                               |

<SourceMeta source="{ label: &#x22;src/plugins/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/plugins&#x22; }" />

## The 10 facet sub-paths [#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-path                          | Hooks covered                                                                                                                                                                                                                                                                                        | When you reach for it                                                                                                       |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `@pleach/core/plugins/lifecycle`  | `name`, `version`, `metadata`, `prePlanPrimer`, `postSynthesisGuard`, `onJobDispatch`, `onJobComplete`, `extraGraphNodes`, `eventResolver`, `contributeEventTypes`                                                                                                                                   | Identity + plan/synthesis hooks + long-running job indexing + custom graph nodes                                            |
| `@pleach/core/plugins/prompts`    | `contributePromptHints`, `contributePrompts`, `contributeRuntimeAwarePrompts`, `contributePromptContextBridge`, `contributeFallbackSystemPromptBuilder`, `contributePromptSections`, `contributePromptSectionGeneratorBundle`, `contributeMetaLearningContext`, `contributeSynthesisDirectiveBlocks` | Anything that touches the system-prompt seam — persona, runtime-aware sections, meta-learning context, synthesis directives |
| `@pleach/core/plugins/tools`      | `contributeTools`, `contributeBatchingHints`, `registerAsyncExecutors`, `contributeIntentToolMap`, `contributeToolCouplingHints`, `contributeToolFollowUpIntents`, `contributeEntityExtractors`, `contributeIntentClassifiers`, `contributeIntentMentionDetector`                                    | Tool definitions + how the planner reaches for them + post-tool follow-up + entity extraction                               |
| `@pleach/core/plugins/stream`     | `contributeStreamObservers`, `contributeObserverConsumers`, `contributeStreamChunkHandlers`, `contributeStreamEventHandlersAdapters`, `contributeStreamEndDiagnosticsAnalyzer`, `contributeDetectionRules`                                                                                           | Per-chunk observation, mutation, provider-detection rules, stream-end diagnostics                                           |
| `@pleach/core/plugins/safety`     | `contributeSafetyPolicies`, `contributeFabricationDetectors`, `contributeFabricationDetectorRules`, `contributeFabricationGuard`, `contributeRefClassValidators`, `contributeHallucinatedToolDetectors`                                                                                              | The complete safety pipeline — refusal patterns, fabrication detectors, ref-class validators                                |
| `@pleach/core/plugins/synthesis`  | `contributeFinalizationPasses`, `contributeCitationRuleSet`, `contributeCitationEntityExtractor`, `contributeCitationInjector`                                                                                                                                                                       | Post-synthesis sanitizers + citation eligibility + citation injection                                                       |
| `@pleach/core/plugins/retry`      | `contributeRetryPolicy`, `contributeContinuationPolicy`, `contributeFamilyPivot`, `contributeFamilyExhaustedSurface`                                                                                                                                                                                 | Retry loops, continuation resolver, model-family cascade                                                                    |
| `@pleach/core/plugins/middleware` | `contributeMiddleware`, `contributeRuntimeAwareMiddleware`                                                                                                                                                                                                                                           | AI-SDK middlewares + orchestrator-level interceptors                                                                        |
| `@pleach/core/plugins/sandbox`    | `contributeSandboxBridge`, `contributeSandboxInitialization`, `contributeSandboxAvailabilityEvaluator`                                                                                                                                                                                               | Sandbox completion plumbing — only if your plugin runs sandboxed code                                                       |
| `@pleach/core/plugins/ui`         | `contributeApprovalFlow`, `contributeInterruptUIHandlers`, `contributeChatManifestProvider`                                                                                                                                                                                                          | UI-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 [#whats-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`](/docs/plugin-contract#_raw--the-forward-compat-escape-hatch)
or set them directly when constructing the runtime.

## The mechanic — three pieces per facet [#the-mechanic--three-pieces-per-facet]

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

```typescript
// 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 [#composeplugin--assembling-a-multi-facet-plugin]

```typescript
// 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.

```typescript
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 [#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 [#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](/docs/reference-apps).

## Empty implementations are now explicit [#empty-implementations-are-now-explicit]

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

```typescript
// 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 [#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 [#pleachfacet-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:

| Facet       | Why aggregate-empty matters                                                                |
| ----------- | ------------------------------------------------------------------------------------------ |
| `safety`    | No safety policies, fabrication detectors, or ref-class validators anywhere in the runtime |
| `tools`     | Zero tool definitions across all plugins — synthesize-only mode                            |
| `prompts`   | Naked baseline prompt — almost certainly missing a domain plugin                           |
| `stream`    | No observers + no detection rules — silent PII / metrics gap                               |
| `lifecycle` | No `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 [#pleachsingle-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](/docs/runtime-inspector#breadcrumbs).

## Choosing the right construction path [#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.

```typescript
// 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.

## Related [#related]

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