# Model hooks (/docs/hooks)



`@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.

```typescript
import type { PreModelHook, PostModelHook } from "@pleach/core/hooks";
```

<SourceMeta source="{ label: &#x22;src/hooks/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/hooks&#x22; }" />

## Registering a hook [#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.

```typescript
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 [#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.

```typescript
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 [#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.

```typescript
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 [#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](/docs/middleware) is initialized. The
pre-model hooks you register yourself.

## Ordering and short-circuit rules [#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](/docs/turn-lifecycle#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 [#where-to-go-next]

* [Orchestrator middleware](/docs/middleware) — the before/after wrap
  around model and tool calls, and where the shipped validators auto-register.
* [Tools](/docs/tools#curbing-tool-call-runaway-on-weak-schema-models) —
  the prompt-side budget for weak-tool-schema model families.
* [Plugin contract](/docs/plugin-contract) — the higher-level plugin
  hooks (`postSynthesisGuard`, `finalizeAnswer`) that operate on the
  synthesized answer rather than a single model call.
