# Runtime strategies (/docs/runtime-strategies)



`SessionRuntimeConfig` carries a second set of fields the substrate
calls **strategies** — typed slots the host fills with domain code.
A strategy is not a plugin. Plugins contribute through named
slots on the `HarnessPlugin` contract; strategies are individual
typed fields on the runtime config, each with one well-defined
shape and one consumer inside the runtime or graph. When a
strategy is unset, the consumer gracefully degrades — the runtime
stays functional, the strategy-backed behavior goes inert.

Plugins and strategies meet at the same constructor. The split is
about shape, not lifecycle. A strategy is one typed field with one
caller — easy to type, easy to swap in a test. A plugin is an
extensible contributor that fills many slots on the `HarnessPlugin`
contract (tier nodes, stream observers, prompts, fabrication
detectors). Reach for a strategy when the runtime needs one answer
from a host-specific function; reach for a plugin when a package
wants to ship a bundle of contributions.

For the plugin counterpart see [Plugin contract](/docs/plugin-contract);
for how a host wires both at once see [Host adapter](/docs/host-adapter).

<SourceMeta
  source="[
  { label: &#x22;src/SessionRuntime.ts&#x22;, href: &#x22;https://github.com/pleachhq/core/blob/main/src/SessionRuntime.ts&#x22; },
  { label: &#x22;src/types/strategies.ts&#x22;, href: &#x22;https://github.com/pleachhq/core/blob/main/src/types/strategies.ts&#x22; },
]"
/>

## Representative strategy slots [#representative-strategy-slots]

| Field                                   | Type                                                | Purpose                                                                                                                                                                                                                                                                                                                                                                               |
| --------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `summaryExtractor`                      | `SummaryExtractor`                                  | Domain-aware tool-result summarizer wired through the data-silo and recall-guard nodes. Default: empty-string no-op.                                                                                                                                                                                                                                                                  |
| `manifest`                              | `ChatManifestStrategy`                              | Side-effect surface for chat-manifest persistence (system notices, provider switches). Default: every method no-op.                                                                                                                                                                                                                                                                   |
| `toolCatalog`                           | `ToolCatalogProvider`                               | Tool descriptors, couplings, and `get_data` resolution. The graph asks here instead of importing a catalog directly. Default: empty.                                                                                                                                                                                                                                                  |
| `toolExpansionTracker`                  | `ToolExpansionTracker`                              | Tracks tool-schema expansion requests across a session. Default: no-op tracker (zero pending always).                                                                                                                                                                                                                                                                                 |
| `responseSanitizer`                     | `ResponseSanitizer`                                 | Identity-by-default pass over the final synthesis text.                                                                                                                                                                                                                                                                                                                               |
| `fabricationGuardStrategy`              | `FabricationGuardStrategyBundle`                    | Typed bundle the post-graph fabrication node consumes. `quantitativePatterns` is required; every other slot is optional and degrades cleanly when omitted. See [Fabrication detection](/docs/fabrication-detection).                                                                                                                                                                  |
| `fabricationGuard&#x60; &#x2A;(legacy)* | `FabricationGuard`                                  | Older single-call shape. Retained for hosts that haven't migrated; the typed `fabricationGuardStrategy` bundle is the recommended path.                                                                                                                                                                                                                                               |
| `intentMentionDetector`                 | `IntentMentionDetector`                             | Extracts intent mentions used by the post-graph synthesis pipeline.                                                                                                                                                                                                                                                                                                                   |
| `toolQualityEvaluator`                  | `ToolQualityEvaluatorStrategy`                      | Scores completed tool calls; the post-tool quality node reads the scored results. Default: empty array (branch skipped).                                                                                                                                                                                                                                                              |
| `subagentExecutor`                      | `SubagentExecutorFn`                                | Spawns a child agent and returns its terminal result. Default: a no-op executor that emits a "not configured" failure so guard paths still emit `subagent.failed` without throwing.                                                                                                                                                                                                   |
| `capabilityRegistry`                    | `CapabilityRegistry`                                | Slim mirror of an orchestrator-side capability index. Surfaces tool clusters for diagnostics and fallback ordering. Default: empty stub.                                                                                                                                                                                                                                              |
| `planGuardProvider`                     | `PlanGuardProvider`                                 | Plan-stage guard forwarded to `DefaultAgentGraphConfig` at graph-build time. Default: no-op.                                                                                                                                                                                                                                                                                          |
| `repetitionGuard`                       | `RepetitionGuardSingleton`                          | Per-session repetition tracker forwarded to the graph. Default: no-op.                                                                                                                                                                                                                                                                                                                |
| `domainContextStrategy`                 | `DomainContextStrategy`                             | Per-runtime singleton consumed by the streamSingleTurn body to gate domain-aware detectors (token sets, regex predicates). Hosts wiring a domain (drug discovery, antibody engineering, etc.) supply a strategy whose predicate bodies enumerate that domain. Default: `createDefaultDomainContextStrategy()` — predicates return `false`, detectors fire on linguistic signal alone. |
| `byokResolver`                          | `(provider: ProviderFamily) => string \| undefined` | Bring-your-own-key resolver. The runtime calls it once per provider when assembling auth headers; returning `undefined` falls back to the substrate's default key resolution.                                                                                                                                                                                                         |
| `sandboxToolNamePrefix`                 | `string`                                            | Namespace prefix for the host's sandbox tools (default `"sandbox_"`). Override when your sandbox surface uses a different convention.                                                                                                                                                                                                                                                 |
| `delegationToolPrefix`                  | `string`                                            | Namespace prefix for subagent-dispatch tools (default `"delegate_to_"`).                                                                                                                                                                                                                                                                                                              |
| `asyncPlannerLlmCaller`                 | `AsyncPlannerLlmCaller`                             | Host-supplied LLM caller for the async planner. The runtime stays provider-agnostic; the caller routes through your model-family selection.                                                                                                                                                                                                                                           |
| `asyncPlannerStepParser`                | `AsyncPlannerStepParser`                            | Companion step parser. When both planner strategies are set, the constructor installs an `AsyncPlannerHandler` so the cold-start gate can wait for an LLM plan.                                                                                                                                                                                                                       |

The full set lives in `SessionRuntimeConfig` in
[`src/SessionRuntime.ts`](https://github.com/pleachhq/core/blob/main/src/SessionRuntime.ts)
and the shapes in
[`src/types/strategies.ts`](https://github.com/pleachhq/core/blob/main/src/types/strategies.ts).

## Wiring strategies at construction [#wiring-strategies-at-construction]

```typescript
import { SessionRuntime } from "@pleach/core";
import type {
  ToolCatalogProvider,
  SummaryExtractor,
  SubagentExecutorFn,
} from "@pleach/core/types/strategies";

const toolCatalog: ToolCatalogProvider = {
  /* host implementation */
};
const summaryExtractor: SummaryExtractor = {
  extractSummary: (_toolName, result) => ({
    description: typeof result === "string" ? result.slice(0, 280) : "",
  }),
  extractChainingFields: () => undefined,
};
const subagentExecutor: SubagentExecutorFn = async (_config, _parent, options) => {
  /* host implementation */
  return {
    id: options.id ?? crypto.randomUUID(),
    status: "completed",
    content: "",
    toolCalls: [],
    metrics: { totalSteps: 0, durationMs: 0 },
  };
};

import { SupabaseAdapter } from "@pleach/core/sessions";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!);

const runtime = new SessionRuntime({
  storage: new SupabaseAdapter({ client: supabase }),
  userId: "user_123",
  toolCatalog,
  summaryExtractor,
  subagentExecutor,
});
```

## `fabricationGuardStrategy` (typed bundle) [#fabricationguardstrategy-typed-bundle]

The `fabricationGuardStrategy` slot takes the typed bundle. The empty
default is the floor — `quantitativePatterns` is required, every other
slot is optional and degrades cleanly when omitted.

```typescript
import { SessionRuntime } from "@pleach/core";
import {
  DEFAULT_FABRICATION_GUARD_STRATEGY_BUNDLE,
  type FabricationGuardStrategyBundle,
} from "@pleach/core/types/strategies";

const fabricationGuardStrategy: FabricationGuardStrategyBundle = {
  ...DEFAULT_FABRICATION_GUARD_STRATEGY_BUNDLE,
  quantitativePatterns: [/\b\d+(?:\.\d+)?%/g, /\$\d[\d,]*/g],
};

const runtime = new SessionRuntime({
  storage,
  userId: "user_123",
  fabricationGuardStrategy,
});
```

## Strategy dep-injection via `harnessRuntime` accessors [#strategy-dep-injection-via-harnessruntime-accessors]

A second category of strategy plumbing rides on `OrchestratorConfig.harnessRuntime`
— a structural shape the runtime threads into the orchestrator config so the
streamSingleTurn body and graph nodes can read per-runtime strategies without
importing `SessionRuntime` directly. Each accessor is optional; CLI and raw
orchestrator entry points leave the shape minimal (often only
`getIntentPriorityTools`) and the consumer falls back to a default.

The body lift (PO-A1) cohort threads four strategy slots this way:

| Accessor                             | Returns                                 | Read site                                                         |
| ------------------------------------ | --------------------------------------- | ----------------------------------------------------------------- |
| `getDomainContextStrategy?()`        | `DomainContextStrategy`                 | streamSingleTurn body — domain predicate gating (C1).             |
| `getHallucinationDetectorFactory?()` | `HallucinationDetectorFactory`          | Per-turn detector factory the body calls once at turn entry (C2). |
| `getRepetitionGuard?()`              | `RepetitionGuardSingleton \| undefined` | Per-session repetition tracker (C3).                              |
| `getContinuationMetaToolNames?()`    | `ReadonlySet<string>`                   | Depth-zero continuation gate exemption set (C4).                  |

Sibling accessors carry plugin-contributed strategies and registries that
share the same dep-inversion shape:

| Accessor                              | Returns                                                           | Purpose                                                                                                                         |
| ------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `collectFabricationGuard?()`          | `FabricationGuardImpl \| null`                                    | First-non-null fabrication-guard contribution from plugins. Threaded from `runtime.plugins.collectFabricationGuard()`.          |
| `collectGuestDeniedTools?()`          | `GuestDeniedToolsThunk \| null`                                   | Guest-mode tool denylist thunk. Invoked lazily so mid-session guest-mode flips reflect the contributing plugin's current state. |
| `collectContinuationPolicy?()`        | `ContinuationPolicy \| null`                                      | Continuation-policy contribution consumed by the continuation resolver.                                                         |
| `collectRuntimeAwareMiddleware?(ctx)` | `readonly MiddlewareContribution[]`                               | Per-turn middleware contributions resolved against backend, chat, and budget context.                                           |
| `collectRetryPolicy?()`               | `RetryPolicyContribution \| null`                                 | Last-wins retry-policy contribution consumed by the guarded stream runner.                                                      |
| `collectCitationEntityExtractor?()`   | `CitationEntityExtractor \| null`                                 | First-non-null citation-entity extractor for the post-response citation injection step.                                         |
| `getSafetyRegistry?()`                | `{ collectActiveContributions(): readonly SafetyContribution[] }` | Per-runtime `SafetyPolicyRegistry` accessor consumed by the resolver and BYOK / modal-llm fallback path.                        |

The pattern is consistent: hosts wire strategies through plugin contributions
(see [Plugin contract](/docs/plugin-contract)), the `SessionRuntime` exposes
them through facet accessors (see [Facets](/docs/facets)), and the body lift
reads them through `harnessRuntime?.getX?.()` or `harnessRuntime?.collectX?.()`
optional chains. CLI and raw orchestrator paths leave the accessors undefined
and the consumer falls back to an inline default — typically a no-op or a
direct module import retained for back-compat.

## Newer injection hooks [#newer-injection-hooks]

The substrate keeps growing strategy slots as more body-level
behaviors get lifted into the typed contract. The hooks below ship
today and follow the same shape rules as the slots above — typed
field on `SessionRuntimeConfig`, one consumer, graceful no-op when
omitted.

| Hook                                      | Shape                                                                      | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ----------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hallucinationDetectorFactory`            | `(input: { availableToolNames, intent }) => HallucinationDetectorStrategy` | Per-turn factory the stream body calls once with the per-turn input bundle. Returns a detector that consumes streamed content via `feed` / `onToolUse` / `onMetaToolUse` and surfaces `hallucinationDetected` / `narratedWithoutExecution` getters. Default: `createNoOpHallucinationDetector` (every getter reports `false`, every mutator is inert) — the substrate's hallucination arm stays inert until a host wires a real detector.                                                                                                                                                      |
| `continuationMetaToolNames`               | `ReadonlySet<string>`                                                      | Names of "continuation meta-tools" the depth-zero continuation gate exempts from tool-call counting. Distinct from `metaToolNames` so a host can keep two exemption sets — the recovery-primer exemption set (`metaToolNames`) and the continuation exemption set — without conflating one surface. Default: undefined; consumer falls back to an empty set and counts every tool as continuation-relevant.                                                                                                                                                                                    |
| `repetitionGuard` (extended)              | `RepetitionGuardSingleton` with new optional methods                       | The existing `repetitionGuard` slot grew three optional methods consumed by the lifted body: `setPlanActive(active)` (tightens budget behavior while a deterministic plan is active), `getPriorTurnRunawayDirective()` (reads the runaway directive carried forward from the previous turn boundary, or `null`), and `clearPriorTurnRunawayDirective()` (paired consumer-side clear after injection). All three are optional — hosts that don't surface runaway directives leave them undefined and the body silently no-ops.                                                                  |
| `orchestrator.providers.providerFallback` | Registry-seam key                                                          | Module-loader registry-seam (not a config field) consumed by the body's provider-fallback path. Host wires a real implementation through the orchestrator's dynamic-import registry. This key has no substrate default: absent a registration, `dynamicImportApp` rejects, the body's catch attempts a legacy relative import that no longer resolves post-lift, and the call throws `MODULE_NOT_FOUND`. The consumer must wire the loader for this path to succeed. Lives next to the existing `orchestrator.providers.{circuitBreaker, fallbackExecutor, modelAvailabilityChecker}` cluster. |
| `EMPTY_SINGLE_TURN_FLAGS`                 | Typed `Readonly<SingleTurnFlags>` export                                   | Canonical per-turn reset baseline — a frozen object with the four boolean defaults (`truncated`, `modelFallback`, `contextOverflow`, `hallucinationDetected`). Not a config slot; a typed export hosts spread (`{ ...EMPTY_SINGLE_TURN_FLAGS }`) at turn boundaries instead of re-deriving the defaults inline. Reachable from `@pleach/core/types/singleTurn`.                                                                                                                                                                                                                                |

The factory-shaped hook (`hallucinationDetectorFactory`) is the
shape to reach for when a strategy needs **per-turn allocation** —
the consumer calls the factory once at turn entry with a per-turn
input bundle, and the returned detector instance is discarded at
turn end. The other strategy slots above are **per-runtime
singletons** the consumer calls many times across many turns.

```typescript
import { SessionRuntime } from "@pleach/core";
import type { HallucinationDetectorFactory } from "@pleach/core/strategies/hallucinationDetector";

const hallucinationDetectorFactory: HallucinationDetectorFactory = (input) => {
  // Host builds a detector parameterized by this turn's
  // `input.availableToolNames` + `input.intent`. The body calls
  // `feed(chunk)` on every content delta and reads the two getters
  // after the turn settles.
  return /* host implementation */ {} as ReturnType<HallucinationDetectorFactory>;
};

const runtime = new SessionRuntime({
  storage,
  userId: "user_123",
  hallucinationDetectorFactory,
  continuationMetaToolNames: new Set(["set_step_complete", "wait_for_jobs"]),
});
```

See [Providers](/docs/providers) for the provider-fallback registry
shape, [Fabrication detection](/docs/fabrication-detection) for the
broader fabrication-guard surface (separate contract from
hallucination detection — fabrication is per-call quantitative
pattern matching; hallucination is per-turn narration vs. execution
analysis), and the [Plugin contract](/docs/plugin-contract) for the
plugin counterpart to the strategy injection pattern.

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

<Cards>
  <Card title="SessionRuntime" href="/docs/session-runtime" description="The class strategies attach to — construction, sessions, executeMessage, abort." />

  <Card title="Plugin contract" href="/docs/plugin-contract" description="The companion extension surface — many slots per contributor." />

  <Card title="Host adapter" href="/docs/host-adapter" description="The host integration that wires strategies and plugins side-by-side." />

  <Card title="Fabrication detection" href="/docs/fabrication-detection" description="The fabrication-guard strategy bundle and its detector surface." />
</Cards>
