Model hooks
Two interception points around every provider call — pre-model hooks reshape the request, post-model hooks validate or retry the response. Four hooks ship in the box.
@pleach/core/hooks gives you two places to intervene around each
provider call. A pre-model hook runs before the request leaves —
it can trim history, swap the model, or short-circuit the call
entirely. A post-model hook runs after the response lands — it can
rewrite the answer or send it back for a retry. Both are plain async
functions; you register them on the runtime, and they run in
registration order.
import type { PreModelHook, PostModelHook } from "@pleach/core/hooks";Registering a hook
Hooks attach to the runtime through the model facet. Registration is
append-only, and hooks fire sequentially in the order you add them.
import { createPleachRuntime } from "@pleach/core";
import { historyTrimmerHook, toolCallValidatorHook } from "@pleach/core/hooks";
const runtime = createPleachRuntime();
runtime.model.registerPreHook(historyTrimmerHook); // before each model call
runtime.model.registerPostHook(toolCallValidatorHook); // after each model callruntime.model.registerPreHook / registerPostHook are the facet
aliases; runtime.registerPreModelHook / registerPostModelHook are
the equivalent methods on the runtime itself. Both append to the same
lists — there is no de-registration handle.
The pre-model hook
A pre-model hook receives the assembled request and returns the parts it wants to change. Anything it omits is left as-is.
type PreModelHook = (ctx: PreModelHookContext) => Promise<PreModelHookResult>;
interface PreModelHookContext {
messages: Message[];
model: string;
modelProfile: ModelProfile | null; // token limits, cost, capability flags
tools: ToolDefinition[];
systemPrompt: string;
state: Record<string, unknown>;
}
interface PreModelHookResult {
messages?: Message[]; // replace the message list
model?: string; // swap the model for this call
systemPromptSuffix?: string; // append to the system prompt
tools?: ToolDefinition[]; // replace the tool set
shortCircuit?: Message; // skip the model, return this instead
interrupt?: { reason: string; value: unknown }; // pause the turn
}modelProfile carries the resolved model's limits and capabilities —
maxInputTokens, maxOutputTokens, per-million-token cost, and the
supportsParallelToolCalls / supportsVision / supportsCaching
flags — so a hook can size its work to the exact model.
The post-model hook
A post-model hook sees the response plus the request that produced it. It can hand back a rewritten response, or ask the loop to retry with feedback.
type PostModelHook = (ctx: PostModelHookContext) => Promise<PostModelHookResult>;
interface PostModelHookContext {
response: Message;
request: {
messages: Message[];
model: string;
tools: ToolDefinition[];
state: Record<string, unknown>;
};
state: Record<string, unknown>;
}
interface PostModelHookResult {
response?: Message; // replace the response
retry?: { feedback: string; maxRetries?: number }; // re-run with feedback
interrupt?: { reason: string; value: unknown };
}The four shipped hooks
All four are zero-config direct exports — register the ones you want.
| Hook | Kind | What it does |
|---|---|---|
historyTrimmerHook | pre-model | Prunes the oldest messages when the estimated token count runs over budget. Trims toward ~85% of modelProfile.maxInputTokens, keeps the last six messages, and preserves every system message. |
lazyModelDefaultsHook | pre-model | Caps the tool set to what the model supports — 128 tools when supportsParallelToolCalls, 32 otherwise — slicing the array before the call. |
toolCallValidatorHook | post-model | Returns retry feedback when the response calls a tool name that isn't in the available set, so a hallucinated tool never dispatches. |
repetitionDetectorHook | post-model | Requests one retry (maxRetries: 1) when the same tool is called three or more times across the last five assistant messages. |
The two post-model validators (toolCallValidatorHook,
repetitionDetectorHook) are registered for you when the runtime's
default orchestrator middleware is initialized. The
pre-model hooks you register yourself.
Ordering and short-circuit rules
- Sequential, in registration order. Each hook's output becomes the next hook's input. Pre-model hooks compose the request; post-model hooks compose the response.
- First escape wins. A pre-model hook that returns
shortCircuitorinterruptstops the chain — no later pre-hook runs, and the model call is skipped. A post-model hook that returnsretryorinterruptstops the chain immediately. - Async throughout. Every hook returns a
Promise, so a hook can await a lookup before deciding.
The retry path pairs with the dead-air hard-cut:
a repetitionDetectorHook retry curbs a tool-looping model, and the
hard-cut backstops a model that stalls without producing content.
Where to go next
- Orchestrator middleware — the before/after wrap around model and tool calls, and where the shipped validators auto-register.
- Tools — the prompt-side budget for weak-tool-schema model families.
- Plugin contract — the higher-level plugin
hooks (
postSynthesisGuard,finalizeAnswer) that operate on the synthesized answer rather than a single model call.
Prompt builder
`composeBudgetedPrompt` — the deterministic composer that turns resolved contributions into the final system prompt with quantized budget math.
Orchestrator middleware
Wrap the model- and tool-call lifecycle with before/after interceptors. Two context-management middlewares ship — result eviction and history summarization.