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
| 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. |
fabricationGuard (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
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:
| 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), 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.
| 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.
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
SessionRuntime
The class strategies attach to — construction, sessions, executeMessage, abort.
Plugin contract
The companion extension surface — many slots per contributor.
Host adapter
The host integration that wires strategies and plugins side-by-side.
Fabrication detection
The fabrication-guard strategy bundle and its detector surface.
Post-tool tier
The contributePostToolTier hook (C10 plugin slot) — uniform enrichment across tool batches for citations, entities, quality scores, and safety flags.
Branding
White-label primitives — override the brand string, log prefixes, and error-message text the runtime emits so output reads as your product, not as Pleach. configureBranding / getBrandingConfig / resolveLogPrefix / resolveErrorMessage.