# Host adapter (/docs/host-adapter)



`new SessionRuntime()` constructs with zero config. But
`runtime.executeMessage(...)` reaches back into the host application
for capabilities the substrate doesn't ship absorbed yet —
streaming utilities, fabrication guards, intent detectors, citation
enrichers, tool registries. The seam is `setHarnessModuleLoader`.

This page is for hosts mid-migration. If you're building a new
agent on `@pleach/core`, prefer the typed config surfaces
(`provider`, `tools`, `plugins`, `metaToolNames`) — the loader
seam exists for hosts that grew up with the substrate inside a
larger app and are unwinding the integration gradually.

```typescript
import {
  setHarnessModuleLoader,
  type HarnessModuleKey,
  type HarnessModuleLoader,
} from "@pleach/core/runtime";
```

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

## What happens without it [#what-happens-without-it]

Calling `executeMessage` without registering a loader produces:

```
HarnessModuleLoaderUnregisteredError: [harness] Module loader is not
registered. Tried to load key="orchestrator.intentDetector".
The host application must call setHarnessModuleLoader(...) at startup …
```

The error names the first key the runtime actually reaches for on a
turn — an early turn-start key like `orchestrator.intentDetector`,
not a later stream-phase key. Wire the loader at startup and the
error goes away.

## The 30-second contract [#the-30-second-contract]

```typescript
import { setHarnessModuleLoader, type HarnessModuleKey } from "@pleach/core/runtime";

setHarnessModuleLoader((key: HarnessModuleKey) => {
  switch (key) {
    case "orchestrator.streamDegenerationGuards":
      return import("@your-app/streamDegenerationGuards");
    case "orchestrator.fabricationGuard":
      return import("@your-app/fabricationGuard");
    case "orchestrator.intentDetector":
      return import("@your-app/intentDetector");
    // ... ~40 more keys ...
    default:
      throw new Error(`Unhandled harness module key: ${key}`);
  }
});
```

`HarnessModuleKey` is a string-literal union. TypeScript will tell
you when a new key gets added — the `default` arm catches anything
your switch doesn't cover.

## The retirement direction [#the-retirement-direction]

The loader surface is **shrinking**, not growing. Each `@pleach/core`
release retires keys as the underlying capability moves into the
substrate as typed config:

| Era     | Capability sat in                                              | Today                          |
| ------- | -------------------------------------------------------------- | ------------------------------ |
| `0.x`   | Loader seam                                                    | Loader seam                    |
| `1.0.0` | Loader seam                                                    | Loader seam (R-1 started)      |
| `1.1.x` | Loader seam                                                    | Some keys retired; rest stable |
| Future  | Typed `SessionRuntimeConfig` strategies, `HarnessPlugin` hooks | Loader seam minimal residual   |

Keys retired so far:

| Retired key                     | Replaced by                                                             |
| ------------------------------- | ----------------------------------------------------------------------- |
| `orchestrator.streamHelpers`    | `SessionRuntimeConfig.metaToolNames` (typed config)                     |
| `orchestrator.fabricationGuard` | `setOrchestratorHotpathModules({ fabricationGuard })` (registered shim) |

Re-adding a retired key is a regression. The runtime's substrate
test suite catches the regression at build time, not first turn.

## The R-tracks [#the-r-tracks]

The retirement work is tracked as three named tracks. Hosts can
opt into each independently.

| Track | Scope           | What lands                                                                            |
| ----- | --------------- | ------------------------------------------------------------------------------------- |
| R-1   | Absorption      | Capability moves from loader seam into the substrate, no surface change for consumers |
| R-2   | Typed config    | Loader-resolved value becomes a typed field on `SessionRuntimeConfig`                 |
| R-3   | Plugin contract | Loader-resolved value becomes a `HarnessPlugin` contribution hook                     |

The reference adapter at `examples/host-adapter/reference.ts`
in the package marks every R-1-retired arm with a
`RETIRED YYYY-MM-DD (R-1.X, commit <sha>)` comment. Hosts cribbing
from that file should delete the marked arms rather than wire
them.

## `CreatePleachRuntimeConfig.host.{strategies, modules, raw}` [#createpleachruntimeconfighoststrategies-modules-raw]

`createPleachRuntime()` accepts a typed `host` carveout that
groups the host-private substrate dependencies under three named
fields. The carveout is the destination loader-bag keys absorb
into as they retire — once a key lands here, the loader stops
being asked for it on that runtime.

```typescript
import {
  createPleachRuntime,
  type CreatePleachRuntimeConfig,
} from "@pleach/core";
```

| Field             | Holds                                                                                                                                                                                             |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host.strategies` | Host-private strategy injection slots — the cohort marked `@strategy.host-private` on `SessionRuntimeConfig`. Typed as `Partial<Pick<SessionRuntimeConfig, ...>>` over a 15-key list (see below). |
| `host.modules`    | Operator-supplied module shims the runtime composes against — `eventLogWriter`, `store`, `interrupt`. These are substrate dependencies, not plugin contributions.                                 |
| `host.raw`        | Uncategorized escape hatch. Accepts any `Partial<SessionRuntimeConfig>`. Use only when neither the typed groups above nor the top-level options expose the field.                                 |

The 15 keys `host.strategies` accepts (from
`HostExtensionBundle`):

`orchestratorConfig`, `lineageTracker`, `agentRegistry`,
`planManager`, `learningAuditor`, `preserveDataRefFields`,
`getDataHandlerFactory`, `dataChannelRefetch`,
`continuationShadowResolver`, `entityNameCounter`,
`structurePrefetcher`, `artifactCacheReader`, `guestDeniedTools`,
`fabricationDetectorRules`, `recordGarbledOutput`.

The 3 keys `host.modules` accepts: `eventLogWriter`, `store`,
`interrupt`.

```typescript
const config: CreatePleachRuntimeConfig = {
  plugins: [compliancePlugin, gatewayPlugin],
  host: {
    strategies: {
      lineageTracker: myLineageTracker,
      planManager: myPlanManager,
      guestDeniedTools: GUEST_DENIED,
    },
    modules: {
      eventLogWriter: mySupabaseEventLogWriter,
      store: myMultiNamespaceKV,
    },
    // raw: { ... }  — only when a field is neither classified above nor a top-level option
  },
  userId: "user_123",
};

const runtime = createPleachRuntime(config);
```

The three fields are independent. A host can fill `strategies`
without touching `modules`; a host with no operator-supplied
module shims can omit `modules` entirely. `raw` is the safety
valve — prefer `strategies`/`modules` when applicable so the
autocomplete surface stays meaningful.

### Merge precedence [#merge-precedence]

Top-level fields → `host.strategies` → `host.modules` →
`host.raw` → (deprecated) `advanced`. Higher precedence wins on
key collision. `advanced?: Partial<SessionRuntimeConfig>` stays
in the shape for 1.x back-compat with the pre-1.0 surface;
it retires in 2.0. New hosts shouldn't reach for `advanced`.

### Bag-entry retirement readiness [#bag-entry-retirement-readiness]

A per-bag-entry status report tracks the migration off the
`orchestratorHotpath` bag toward the typed surface +
plugin-routed `runtime.plugins.collect*` cohort. The bag is
transitional, not terminal — every entry carries an inline
retirement-path comment on the `OrchestratorHotpathModules`
interface declaration.

| Entry              | Status                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `streamHelpers`    | **RETIRED**. Bag field deleted; the lone production reader now calls `this._getMetaToolNames()` directly. `SessionRuntimeConfig.metaToolNames` is canonical.                                                                                                                                                                                                                |
| `toolCoupling`     | **RETIRE-READY**. Typed `HarnessPlugin.contributeToolCouplingHints` is canonical; hosts adopting the plugin-routed path read through `runtime.plugins.collectToolCouplingHints()`. The bag entry is retained as CLI/raw-path fallback and retires fully once non-host consumers no longer reach it.                                                                         |
| `fabricationGuard` | **NEAR-READY**. Surface widened from 1 → 9 host-side functions (regex tables bake in domain rules); the typed `contributeFabricationGuard` hook is the canonical path, and the H-3.3 runtime probe `recordFabricationGuardResolverPath` emits per-invocation `via:"bundle"\|"bag-fallback"\|"unwired"` telemetry to drive the 3-batch soak before the bag-fallback retires. |

The canonical routing layer is `runtime.plugins.collect*` — the
bag is the legacy fallback. Hosts adopting facets should read
through the plugins facet, not the bag. See [Facets](/docs/facets).

### Audit gates enforcing the contract [#audit-gates-enforcing-the-contract]

Four gates run in CI on the upstream core repo. Their job is to
catch "we typed the hook but the bag read is still load-bearing"
regressions at build time, rather than at first turn in
production.

| Gate                                     | Enforces                                                                                                                                                                                                                                                                                                            |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `audit:bag-entries-have-retirement-path` | Every property on `OrchestratorHotpathModules` must have an inline retirement-path comment (`// TRANSITIONAL —`, `// D-PA-N<digit>`, or `// retir<inflection>`) immediately above it. New entries cannot land without a documented retirement vehicle.                                                              |
| `audit:bag-readers`                      | Inventory of `getOrchestratorHotpathModules().{fabricationGuard,toolCoupling}` read callsites. G3 routing assertion: any file reading `bag.toolCoupling` MUST also reference `collectToolCouplingHints` (plugin-routed canonical path) OR carry a `// FALLBACK-OK: <tag>` marker. Companion to the entry-side gate. |
| `audit:fabrication-guard-bag`            | Source-text regression locking R-1.C Variant A′ post-audit state. Fails on novel `dynamicImportApp("orchestrator.fabricationGuard")` call sites beyond the baselined documentation-comment hit at `appRegistries.ts`.                                                                                               |
| `audit:fabrication-guard-resolver-clean` | Runtime soak ledger — accumulates per-canvas-batch counts of the `[UXParity:fabrication-guard-resolver-path]` probe grouped by `via` discriminator. `:strict` fails until the last 3 batches show auth `via:"bundle"` > 0 AND `via:"bag-fallback"` == 0. Replaces calendar bake with runtime evidence.              |

### `pleach-plugin-modernize` codemod [#pleach-plugin-modernize-codemod]

The codemod rewrites the four soft-deprecated bare
properties into their typed `contribute*` method form. It does
not migrate to `definePleachPlugin` — it's a narrower rewrite
that produces method-form plugins which `definePleachPlugin` can
then accept.

```text
tools: [a, b]            →  contributeTools: () => [a, b]
events: { foo: ... }     →  contributeEventTypes: () => ({ foo: ... })
customEventTypes: [...]  →  contributeEventTypes: () => [...]
batchingHints: [a, b]    →  contributeBatchingHints: () => [a, b]
```

The codemod is an internal repo script in the upstream core repo,
not a published npm binary:

```bash
node scripts/codemods/pleach-plugin-modernize.mjs <glob> [--dry-run] [--json]
```

It only rewrites bare properties inside object literals containing
a `name: "<string>"` field, so arbitrary objects aren't touched.
Collision-safe — if a `contribute*` method already exists alongside
the bare property in the same plugin object, the codemod warns to
stderr and skips. `customEventTypes` folds into `contributeEventTypes`
only when no `events` / `contributeEventTypes` already exists in
the same literal; otherwise it's left for manual merge.

It's the prep step for the upcoming bare-property cut, after which those
forms become type errors. The output composes with
[`definePleachPlugin`](/docs/plugin-contract#definepleachplugin--the-typed-factory)
without further edits.

## `setOrchestratorHotpathModules` [#setorchestratorhotpathmodules]

A second registration helper for hot-path modules — capabilities
called synchronously inside the per-call seam loop, where dynamic
import overhead would matter.

```typescript
import { setOrchestratorHotpathModules } from "@pleach/core/runtime";

setOrchestratorHotpathModules({
  fabricationGuard: fabricationGuardModule as never,
  // ... other hotpath modules
});
```

Used for capabilities that are domain-coupled in a way the
substrate doesn't want to absorb. The fabrication guard, for
example, carries host-specific regex tables (tool aliases, phantom
tool replacements, identifier patterns) — registering it
synchronously keeps host-domain rules in host code without
introducing dynamic-import latency on the per-call path.

## `metaToolNames` (the canonical R-1.A migration) [#metatoolnames-the-canonical-r-1a-migration]

Hosts that previously resolved `CONTINUATION_META_TOOL_NAMES`
through the loader should now pass them as typed config:

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

const runtime = new SessionRuntime({
  metaToolNames: new Set([
    "set_step_complete",
    "wait_for_jobs",
    // ... your meta-tool names
  ]),
  // ...other config
});
```

Without the field, the runtime fires a one-shot probe **lazily on
the first read** of the meta-tool set (not at construction) — the
`_getMetaToolNames()` accessor emits it the first time a guard
reaches for the set and finds it undefined:

```
[UXParity:metaToolNames-config-missing] { reason: "metaToolNames-undefined-at-read", ... }
```

The runtime then falls back to a shared empty set, which silently
disables continuation and fabrication guards that key off the set.
(`setOrchestratorAdapter` is unrelated — it's a deprecated
delegating stub that forwards to the `adapter` facet and emits no
metaToolNames warning.) Fix by passing the field; never ship past
the probe in production.

## The reference adapter [#the-reference-adapter]

The package ships two reference adapter files at
`examples/host-adapter/`:

| File           | Purpose                                                                       |
| -------------- | ----------------------------------------------------------------------------- |
| `index.mjs`    | Minimal no-op adapter that throws a doc-pointer error for every key           |
| `reference.ts` | Full production-shaped adapter with vendor paths neutralized to `@your-app/*` |

Start from `index.mjs` for a brand-new host — implement one key at
a time as you need it. Crib from `reference.ts` to see the
full set of keys a mature integration wires.

## What gets called when [#what-gets-called-when]

Different keys resolve at different turn phases. A rough map:

| Phase                | Keys resolved                                                                                                                         |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Runtime construction | `supabase`, `stores.*`, registry constants                                                                                            |
| Turn start           | `orchestrator.intentDetector`, `orchestrator.fileIntentDetector`                                                                      |
| Stream phase         | `orchestrator.streamSingleTurn`, `orchestrator.streamDegenerationGuards`                                                              |
| Tool dispatch        | `orchestrator.tools.*` (registry, dataflow, guards, execution)                                                                        |
| Synthesis            | `orchestrator.synthesis.*`, `orchestrator.finalization.finalizeContent`                                                               |
| Quality / post-turn  | `orchestrator.quality.evaluator`, `orchestrator.workflow.workflowHintResolver`                                                        |
| Middleware           | `orchestrator.middleware.creditBudget`, `orchestrator.middleware.safetyReview`, `orchestrator.middleware.enrichmentCitations`         |
| Provider fallback    | `orchestrator.providers.circuitBreaker`, `orchestrator.providers.fallbackExecutor`, `orchestrator.providers.modelAvailabilityChecker` |

The runtime does not cache resolved modules itself — `dynamicImportApp`
re-invokes the registered loader on every call. Deduplication comes
from the process-wide `import()` module cache the loader delegates to,
so a key resolves cheaply on repeat reaches but the loader still runs
each time (scope is process-wide, not per-runtime-lifetime).

## When you don't need the loader [#when-you-dont-need-the-loader]

For a brand-new host that's not bringing a legacy orchestrator
into the substrate:

* Use the typed `provider` config field for LLM execution.
* Use the `plugins` config field for domain extensions.
* Use `metaToolNames` for continuation flow control.
* Register tools through plugin `contributeTools` or the legacy
  `setOrchestratorRegistry` shim.

You'll still hit some loader-required keys (the substrate is mid-
migration), but the minimum set is small. The no-op adapter at
`examples/host-adapter/index.mjs` implements just the keys
required for a first-turn execution to land.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="SessionRuntime" href="/docs/session-runtime" description="The typed config fields that supersede loader keys." />

  <Card title="Plugin contract" href="/docs/plugin-contract" description="The contribution hooks R-3 routes loader keys into." />

  <Card title="Tools" href="/docs/tools" description="setOrchestratorRegistry and the modern defineTool path." />

  <Card title="Language-agnostic contract" href="/docs/language-agnostic-contract" description="Why the loader seam is TypeScript-specific and the wire contract isn't." />
</Cards>
