pleach
Build

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 call

runtime.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.

HookKindWhat it does
historyTrimmerHookpre-modelPrunes 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.
lazyModelDefaultsHookpre-modelCaps the tool set to what the model supports — 128 tools when supportsParallelToolCalls, 32 otherwise — slicing the array before the call.
toolCallValidatorHookpost-modelReturns retry feedback when the response calls a tool name that isn't in the available set, so a hallucinated tool never dispatches.
repetitionDetectorHookpost-modelRequests 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 shortCircuit or interrupt stops the chain — no later pre-hook runs, and the model call is skipped. A post-model hook that returns retry or interrupt stops 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.

On this page