Hooks
A hook is a function the assistant calls at a known boundary. The harness owns the loop. Your code runs at named points. Each hook lives in hooks/<name>.ts. The filename is the hook name.
The agent loop
| Node | What it means |
|---|---|
| User prompt | user-prompt-submit as the message enters the loop |
| Context check | Fits the model window, or compact first |
| Model call | pre-model-call immediately before the request |
| Model response | post-model-call; then tool, continue, or stop |
| Assistant reply | stop at the terminal end of the turn |
| Compaction | post-compact after history is reduced |
| Tool result | post-tool-use before the result rejoins history |
pre-model-call, post-model-call, and post-tool-use can fire more than once per turn.
Outside the loop
init: once when the plugin registers. Create plugin-owned storage underpluginStorageDir. Throwing aborts load.shutdown: best-effort cleanup when the assistant unloads the plugin.conversation-deleted: purge per-conversation state after the conversation rows are gone.
Anatomy
type HookFunction<TCtx> = (ctx: TCtx) => Promise<Partial<TCtx> | void>;
Mutate the context in place, or return a partial. Omitted keys keep their existing values. One hook per file, default-exported. Types come from @aevumcadence/plugin-api.
// hooks/pre-model-call.ts
import type { PreModelCallContext } from "@aevumcadence/plugin-api";
export default async function preModelCall(ctx: PreModelCallContext) {
if (ctx.callSite !== "mainAgent") {
return;
}
ctx.systemPrompt = (ctx.systemPrompt ?? "") + "\nBe concise.";
}
Resolution order
- Built-in default plugins, always first.
- User plugins, by original install date (
install-meta.json).
Each plugin contributes at most one hook per boundary. The chain is linear.
When to write a hook
When the behavior must happen every time the loop reaches that boundary: rewrite a prompt, route a call, truncate a tool result, re-inject context after compaction. If the model should choose the action by name, write a tool instead.