Macro Interceptor¶
Permission required: macro_interceptor
Macro interceptors run at the top of MacroEvaluator.evaluate(), once per fixed-point iteration, before Lumi parses the template. They receive the raw template plus a read-only env snapshot, and return a transformed template or void to pass through.
spindle.registerMacroInterceptor(async (ctx) => {
if (!ctx.template.includes('{{my_macro')) return
return myInWorkerEvaluator(ctx.template, ctx.env)
}, 100)
Use this when per-macro RPC cost dominates iteration-heavy templates ({{#each LARGE_LIST}}…{{my_macro}}…{{/each}}). For single macros, prefer registerMacro.
Parameters¶
| Param | Type | Description |
|---|---|---|
handler |
(ctx: MacroInterceptorCtx) => Promise<string \| void> |
Returns the transformed template, or void to pass through |
priority |
number |
Optional. Lower values run first. Default: 100 |
One interceptor per extension; a second registration replaces the first.
Context Object¶
interface MacroInterceptorCtx {
template: string
env: { commit, names, character, chat, system, variables: { local, global, chat }, dynamicMacros, extra }
commit: boolean
phase: "prompt" | "display" | "response" | "other"
sourceHint?: string
userId?: string
}
env is a structured-clone snapshot. Persist state via spindle.variables.*, not the snapshot.
env.dynamicMacros carries per-call macro overrides supplied by the caller (Record<string, string>). The display-regex pipeline (phase: "display") sets chat_index to the rendered message's index in the chat, which lets handlers compute per-message context that registered macros cannot reach on their own. Other callers may set additional fields.
Composition Order¶
Multiple interceptors run in priority order (lower first), with registration order as the tie-breaker. Each interceptor receives the previous one's returned template.
If a returned template no longer contains {{, the iteration's parse and dispatch are skipped.
Timeout¶
Each interceptor runs inside a 10-second wall-clock budget. On timeout or thrown error: the chain logs the failure and forwards the previous template to the next handler. Macro evaluation itself never aborts.
Users notice the wait
The interceptor fires inside every macro evaluation, including prompt assembly. Slow handlers add visible latency before the first streamed token.
Macro Interceptor vs Interceptor vs Context Handler¶
| Hook | When it fires | What it changes |
|---|---|---|
| Macro Interceptor | Per template, before parse | The raw template |
| Context Handler | Before prompt assembly | The generation context |
| Interceptor | After assembly, before LLM call | The outgoing message array |