pleach
Plugins

Runtime strategies

Typed strategy slots on SessionRuntimeConfig — single-shape, single-consumer fields the host fills with domain code. The companion surface to the plugin contract.

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; for how a host wires both at once see Host adapter.

Representative strategy slots

FieldTypePurpose
summaryExtractorSummaryExtractorDomain-aware tool-result summarizer wired through the data-silo and recall-guard nodes. Default: empty-string no-op.
manifestChatManifestStrategySide-effect surface for chat-manifest persistence (system notices, provider switches). Default: every method no-op.
toolCatalogToolCatalogProviderTool descriptors, couplings, and get_data resolution. The graph asks here instead of importing a catalog directly. Default: empty.
toolExpansionTrackerToolExpansionTrackerTracks tool-schema expansion requests across a session. Default: no-op tracker (zero pending always).
responseSanitizerResponseSanitizerIdentity-by-default pass over the final synthesis text.
fabricationGuardStrategyFabricationGuardStrategyBundleTyped bundle the post-graph fabrication node consumes. quantitativePatterns is required; every other slot is optional and degrades cleanly when omitted. See Fabrication detection.
fabricationGuard (legacy)FabricationGuardOlder single-call shape. Retained for hosts that haven't migrated; the typed fabricationGuardStrategy bundle is the recommended path.
intentMentionDetectorIntentMentionDetectorExtracts intent mentions used by the post-graph synthesis pipeline.
toolQualityEvaluatorToolQualityEvaluatorStrategyScores completed tool calls; the post-tool quality node reads the scored results. Default: empty array (branch skipped).
subagentExecutorSubagentExecutorFnSpawns 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.
capabilityRegistryCapabilityRegistrySlim mirror of an orchestrator-side capability index. Surfaces tool clusters for diagnostics and fallback ordering. Default: empty stub.
planGuardProviderPlanGuardProviderPlan-stage guard forwarded to DefaultAgentGraphConfig at graph-build time. Default: no-op.
repetitionGuardRepetitionGuardSingletonPer-session repetition tracker forwarded to the graph. Default: no-op.
domainContextStrategyDomainContextStrategyPer-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 | undefinedBring-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.
sandboxToolNamePrefixstringNamespace prefix for the host's sandbox tools (default "sandbox_"). Override when your sandbox surface uses a different convention.
delegationToolPrefixstringNamespace prefix for subagent-dispatch tools (default "delegate_to_").
asyncPlannerLlmCallerAsyncPlannerLlmCallerHost-supplied LLM caller for the async planner. The runtime stays provider-agnostic; the caller routes through your model-family selection.
asyncPlannerStepParserAsyncPlannerStepParserCompanion 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 and the shapes in src/types/strategies.ts.

Wiring strategies at construction

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)

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.

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

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:

AccessorReturnsRead site
getDomainContextStrategy?()DomainContextStrategystreamSingleTurn body — domain predicate gating (C1).
getHallucinationDetectorFactory?()HallucinationDetectorFactoryPer-turn detector factory the body calls once at turn entry (C2).
getRepetitionGuard?()RepetitionGuardSingleton | undefinedPer-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:

AccessorReturnsPurpose
collectFabricationGuard?()FabricationGuardImpl | nullFirst-non-null fabrication-guard contribution from plugins. Threaded from runtime.plugins.collectFabricationGuard().
collectGuestDeniedTools?()GuestDeniedToolsThunk | nullGuest-mode tool denylist thunk. Invoked lazily so mid-session guest-mode flips reflect the contributing plugin's current state.
collectContinuationPolicy?()ContinuationPolicy | nullContinuation-policy contribution consumed by the continuation resolver.
collectRuntimeAwareMiddleware?(ctx)readonly MiddlewareContribution[]Per-turn middleware contributions resolved against backend, chat, and budget context.
collectRetryPolicy?()RetryPolicyContribution | nullLast-wins retry-policy contribution consumed by the guarded stream runner.
collectCitationEntityExtractor?()CitationEntityExtractor | nullFirst-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), the SessionRuntime exposes them through facet accessors (see 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

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.

HookShapePurpose
hallucinationDetectorFactory(input: { availableToolNames, intent }) => HallucinationDetectorStrategyPer-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.
continuationMetaToolNamesReadonlySet<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 methodsThe 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.providerFallbackRegistry-seam keyModule-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_FLAGSTyped Readonly<SingleTurnFlags> exportCanonical 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.

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 for the provider-fallback registry shape, 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 for the plugin counterpart to the strategy injection pattern.

Where to go next

On this page