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 slots | definePleachPlugin({ capabilities, _raw }) |
| A 10+-hook plugin spanning 3+ semantic domains, authored across multiple files | Bundle 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-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
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";SafetyPluginFacet— aPick<HarnessPlugin, ...>view. This is the safety-only mental model. Every hook your safety facet can contribute is here; nothing else compiles.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.- The types your contributions reference —
FabricationDetector,SafetyContribution,RefClassValidator. You don't have to reach back to@pleach/corefor 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.tsEach 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:
| 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
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.
Related
- Plugin contract — the underlying
HarnessPlugininterface and thedefinePleachPluginfactory. - 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.
Plugin authoring standards
Conventions for naming, versioning, file layout, peer dependencies, license, and deprecation of plugins built against @pleach/core. Lint-enforced where mechanical, doc-recommended where judgment matters.
Extending Pleach
The gate-paired authoring map — each extension point (node, channel, event type, scrubber, plugin hook, prompt block, audit field) paired with the interface you implement, how you register it, and the CI gate that fails if you get it wrong.