Plugins
Extend Better Agent with cross-agent behavior.
Plugins add behavior that applies across your entire app: logging, auth, rate limiting, lifecycle hooks, and custom endpoints. Register them once at the app level and they apply to every agent.
import { betterAgent } from "@better-agent/core";
import { rateLimitPlugin, loggingPlugin } from "@better-agent/plugins";
export const app = betterAgent({
agents: [myAgent],
plugins: [
rateLimitPlugin({ windowMs: 60_000, max: 20 }),
loggingPlugin({ level: "info" }),
],
});Creating a Plugin
Use definePlugin() to create a custom plugin. Every plugin needs a unique id.
import { definePlugin } from "@better-agent/core";
export const myPlugin = definePlugin({
id: "my-plugin",
});A plugin can use any combination of: guards, lifecycle hooks, event middleware, event observation, tools, and endpoints.
Guards
Guards run before a request is processed. Return null to allow, or a Response to reject.
const workspaceAuth = definePlugin({
id: "workspace-auth",
guards: [
async ({ mode, agentName, request, input }) => {
const token = request.headers.get("authorization");
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
const workspace = await resolveWorkspace(token);
if (!workspace) {
return new Response("Invalid workspace", { status: 403 });
}
return null;
},
],
});The guard context includes:
mode: the request type:"run","stream","load_conversation","abort_run","resume_stream", or"resume_conversation"agentName: resolved agent nameinput: parsed request bodyrequest: the rawRequestobject
Guard failures throw and stop the request. This is different from lifecycle hooks, which are swallowed on failure.
Lifecycle Hooks
Plugins can hook into every phase of the agent loop. All hooks run in plugin registration order, before the agent's own hooks.
onStep
Runs before each step. Can mutate messages, set tool choice, filter active tools, or override the system instruction.
const contextInjector = definePlugin({
id: "context-injector",
onStep: async (ctx) => {
if (ctx.stepIndex === 0) {
ctx.updateMessages((messages) => [
...messages,
{
role: "system",
content: `Current time: ${new Date().toISOString()}`,
},
]);
}
},
});Available methods on the onStep context:
updateMessages(updater): rewrite the message listsetToolChoice(choice): override tool choice for this stepsetActiveTools(names): restrict which tools are availablesetSystemInstruction(instruction): override the system instruction
onBeforeModelCall
Runs just before the provider call. Can mutate the model input, tools, and tool choice.
const toolFilter = definePlugin({
id: "tool-filter",
onBeforeModelCall: async (ctx) => {
const filtered = ctx.tools.filter((t) => t.name !== "dangerous_tool");
ctx.setTools(filtered);
},
});onAfterModelCall
Observes the provider response after the model call completes.
const usageTracker = definePlugin({
id: "usage-tracker",
onAfterModelCall: async (ctx) => {
const usage = ctx.response.usage;
if (usage) {
await recordUsage(ctx.agentName, ctx.runId, usage);
}
},
});onBeforeToolCall
Runs before a tool executes. Can modify args or skip the tool entirely.
const toolGuard = definePlugin({
id: "tool-guard",
onBeforeToolCall: async (ctx) => {
if (ctx.toolName === "delete_user" && !isAdmin(ctx)) {
return { skip: true, result: { error: "Not authorized" } };
}
return undefined;
},
});Return { skip: true, result? } to skip execution and optionally provide a fallback result. Return undefined to proceed normally.
onAfterToolCall
Runs after a tool executes. Can observe or replace the result.
const resultRedactor = definePlugin({
id: "result-redactor",
onAfterToolCall: async (ctx) => {
if (ctx.toolName === "get_user" && ctx.result) {
ctx.setResult(redactPII(ctx.result));
}
},
});onBeforeSave
Runs before conversation items are persisted. Can rewrite or filter items.
const saveFilter = definePlugin({
id: "save-filter",
onBeforeSave: async (ctx) => {
ctx.setItems(
ctx.items.filter((item) => item.type !== "tool_result" || !item.meta?.ephemeral),
);
},
});All lifecycle hook contexts include runId, agentName, and conversationId. Plugin hooks receive agent context as unknown, so plugins cannot assume its shape.
Plugin hook contexts are broad by design. Some controls only appear when the active model supports them, like tool-mutation helpers on tool-capable runs.
Event Middleware
Use event middleware to transform or drop events before they reach the stream.
const sensitiveFilter = definePlugin({
id: "sensitive-filter",
events: {
subscribe: ["TOOL_CALL_RESULT"],
middleware: [
async (event, ctx, next) => {
if (event.type === "TOOL_CALL_RESULT" && event.toolCallName === "get_secrets") {
return next({ ...event, result: "[redacted]" });
}
return next(event);
},
],
},
});subscribefilters which event types the middleware receives. Omit it to receive all events.- Return
next(event)to pass the event through, optionally modified. - Return
nullto drop the event from the stream.
Event Observation
Use onEvent to observe committed events without mutating them. This runs after middleware.
const analytics = definePlugin({
id: "analytics",
onEvent: async (event, ctx) => {
await trackEvent({
type: event.type,
runId: ctx.runId,
agent: ctx.agentName,
timestamp: event.timestamp,
});
},
});Plugin Tools
Plugins can provide tools to all agents. These are merged with each agent's own tools at runtime.
const currentTime = defineTool({
name: "current_time",
description: "Returns the current UTC time",
schema: z.object({}),
}).server(async () => {
return { time: new Date().toISOString() };
});
const timeTool = definePlugin({
id: "time-tool",
tools: currentTime,
});Endpoints
Plugins can add custom HTTP endpoints to the Better Agent server.
const healthCheck = definePlugin({
id: "health",
endpoints: [
{
method: "GET",
path: "/health",
public: true,
handler: async ({ request, params, query }) => {
return Response.json({ status: "ok" });
},
},
],
});pathmust start with/methodacceptsGET,POST,PUT,PATCH,DELETE,OPTIONSor an arraypublic: truebypasses the app's built-in bearer auth for this endpointhandlerreceives{ request, params, query }
Error Behavior
Lifecycle hooks and event middleware are fault-tolerant. If a hook throws, Better Agent logs the error and continues the run. This ensures plugins never break agent execution.
Guards are the exception. A guard failure throws an error and stops the request. Design guards to handle their own errors if you want fail-open behavior.