Hooks
Intercept and customize the AI pipeline with chat and reply hooks.
Hooks let you intercept the AI pipeline at two points: before messages reach the engine (chat hook) and after the response is generated (reply hook). Use them for content filtering, response augmentation, analytics, or custom post-processing.
Registration
Two ways to register hooks.
Via plugin config
export default {
hooks: {
chat: async (ctx) => {
// Preprocess messages
},
reply: (ctx) => {
// Postprocess response
}
}
}export default {
hooks: {
chat: async (ctx) => {
// Preprocess messages
},
reply: (ctx) => {
// Postprocess response
}
}
}Via engine instance
import { OpenAiEngine } from '@robojs/ai/engines/openai'
const engine = new OpenAiEngine({ /* ... */ })
engine.on('chat', async (ctx) => {
// Preprocess messages
})
engine.on('reply', (ctx) => {
// Postprocess response
})
export default { engine }import { OpenAiEngine } from '@robojs/ai/engines/openai'
const engine = new OpenAiEngine({ /* ... */ })
engine.on('chat', async (ctx) => {
// Preprocess messages
})
engine.on('reply', (ctx) => {
// Postprocess response
})
export default { engine }Unregister a hook with engine.off('chat', handler) or engine.off('reply', handler).
Chat hook
Runs before messages reach the engine. Use it to filter, modify, or inject messages.
Signature: (context: ChatHookContext) => Promise<void | ChatMessage[]> | void | ChatMessage[]
ChatHookContext
Prop
Type
You can either mutate ctx.messages in place or return a new ChatMessage[] array to replace it entirely.
Example -- content filtering
export default {
hooks: {
chat: async (ctx) => {
ctx.messages = ctx.messages.filter((m) => {
if (typeof m.content === 'string') {
return !m.content.includes('BLOCKED_WORD')
}
return true
})
}
}
}export default {
hooks: {
chat: async (ctx) => {
ctx.messages = ctx.messages.filter((m) => {
if (typeof m.content === 'string') {
return !m.content.includes('BLOCKED_WORD')
}
return true
})
}
}
}Reply hook
Runs after the engine generates a response. Use it to modify, replace, or augment the reply before it's sent to Discord.
Signature: (context: ReplyHookContext) => Promise<ChatReply | void> | ChatReply | void
ReplyHookContext
Prop
Type
Return a ChatReply to override the response. Return nothing to keep the default.
ChatReply
Prop
Type
Example -- append footer
import type { ReplyHookContext, ChatReply } from '@robojs/ai'
export default {
hooks: {
reply: (ctx: ReplyHookContext): ChatReply | void => {
const content = ctx.response.message?.content
if (typeof content === 'string') {
return { text: content + '\n\n*Powered by Sage*' }
}
}
}
}export default {
hooks: {
reply: (ctx) => {
const content = ctx.response.message?.content
if (typeof content === 'string') {
return { text: content + '\n\n*Powered by Sage*' }
}
}
}
}Example -- MCP degradation handling
export default {
hooks: {
reply: (ctx) => {
if (ctx.degradedMcpServers?.length) {
console.warn('Degraded MCP servers:', ctx.degradedMcpServers)
const text = ctx.response.message?.content ?? ''
return { text: text + '\n\nSome external tools were temporarily unavailable.' }
}
}
}
}export default {
hooks: {
reply: (ctx) => {
if (ctx.degradedMcpServers?.length) {
console.warn('Degraded MCP servers:', ctx.degradedMcpServers)
const text = ctx.response.message?.content ?? ''
return { text: text + '\n\nSome external tools were temporarily unavailable.' }
}
}
}
}Execution order
Hooks run sequentially in registration order. All chat hooks run before the engine call. All reply hooks run after. If multiple hooks are registered for the same event, they execute one at a time in the order they were added.
For reply hooks, the first hook that returns a ChatReply wins. Subsequent reply hooks are skipped once a reply is returned.
