Plugins

Plugins add behavior at the app level. Register them once and they apply to every agent in the app.

import { betterAgent, definePlugin } from "@better-agent/core";

const analytics = definePlugin({
  id: "analytics",
  onEvent: async (event, ctx) => {
    await trackEvent({
      type: event.type,
      runId: ctx.runId,
      agent: ctx.agentName,
    });
  },
});

export const app = betterAgent({
  agents: [supportAgent],
  plugins: [analytics],
});

Use plugins for cross-agent concerns: request guards, logging, analytics, shared tools, custom endpoints, and model/tool/event hooks.

Create a plugin

Use definePlugin when behavior should apply across agents.

import { definePlugin } from "@better-agent/core";

export const analytics = definePlugin({
  id: "analytics",
  onEvent: async (event, ctx) => {
    await trackEvent({
      type: event.type,
      runId: ctx.runId,
      agent: ctx.agentName,
    });
  },
});

Each plugin needs a unique id.

Guards

Guards run before app requests. Return null to allow the request, or return a Response to stop it.

const workspaceGuard = definePlugin({
  id: "workspace-guard",
  guards: [
    async ({ request, auth }) => {
      const workspaceId = request.headers.get("x-workspace-id");

      if (!auth || !workspaceId) {
        return new Response("Unauthorized", { status: 401 });
      }

      const allowed = await canAccessWorkspace(auth, workspaceId);
      return allowed ? null : new Response("Forbidden", { status: 403 });
    },
  ],
});

The guard context includes agentName, parsed input, the raw request, and resolved auth. See Auth for request identity.

Lifecycle hooks

Plugins can observe or adjust the agent loop.

const tenantContext = definePlugin({
  id: "tenant-context",
  onStep: async (ctx) => {
    ctx.updateMessages((messages) => [
      {
        role: "system",
        content: `Run id: ${ctx.runId}`,
      },
      ...messages,
    ]);
  },
});

Use onStep to adjust messages or active tools before a step. Use onStepFinish to observe the step result after it finishes.

Model hooks

Model hooks run around provider calls.

const modelAudit = definePlugin({
  id: "model-audit",
  onBeforeModelCall: async (ctx) => {
    await auditModelInput({
      runId: ctx.runId,
      agent: ctx.agentName,
      messageCount: ctx.messages.length,
    });
  },
  onAfterModelCall: async (ctx) => {
    await auditModelOutput({
      runId: ctx.runId,
      usage: ctx.response.usage,
    });
  },
});

onBeforeModelCall can update messages, tools, tool choice, provider tools, and provider options. onAfterModelCall observes the provider response.

Tool hooks

Tool hooks run around tool calls.

const blockDangerousTools = definePlugin({
  id: "block-dangerous-tools",
  onBeforeToolCall: async (ctx) => {
    if (ctx.toolName === "delete_user") {
      return {
        skip: true,
        status: "error",
        error: "This tool is disabled.",
      };
    }
  },
});

onBeforeToolCall can update tool input or skip execution. onAfterToolCall can update the tool result, status, or error.

Event middleware

Event middleware can transform or drop streamed events before they reach the client.

import { EventType } from "@better-agent/core";

const redactToolResults = definePlugin({
  id: "redact-tool-results",
  events: {
    subscribe: [EventType.TOOL_CALL_RESULT],
    middleware: [
      async (event, _ctx, next) => {
        if (event.type !== EventType.TOOL_CALL_RESULT) {
          return next(event);
        }

        return next({
          ...event,
          result: "[redacted]",
        });
      },
    ],
  },
});

Use subscribe to limit which event types middleware receives. Return next(event) to continue, or return null to drop the event.

Event observation

Use onEvent when you only need to observe events.

const analytics = definePlugin({
  id: "analytics",
  onEvent: async (event, ctx) => {
    await trackEvent({
      type: event.type,
      runId: ctx.runId,
      agent: ctx.agentName,
    });
  },
});

See Events for the app-level event stream.

Plugin tools

Plugins can add tools that are available to agents.

import { definePlugin, defineTool } from "@better-agent/core";
import { z } from "zod";

const currentTime = defineTool({
  name: "current_time",
  target: "server",
  description: "Return the current UTC time.",
  inputSchema: z.object({}),
  execute: async () => ({ time: new Date().toISOString() }),
});

export const time = definePlugin({
  id: "time",
  tools: [currentTime],
});

See Tools for tool targets, approvals, MCP tools, and provider tools.

Endpoints

Plugins can add HTTP routes to the Better Agent handler.

const health = definePlugin({
  id: "health",
  endpoints: [
    {
      method: "GET",
      path: "/health",
      handler: async () => Response.json({ ok: true }),
    },
  ],
});

Endpoint paths must start with /. Methods can be GET, POST, PUT, PATCH, DELETE, OPTIONS, or an array of those methods.

Error behavior

Guards can stop a request by returning a Response. If a guard throws, the request fails. Other plugin hooks are logged on failure and the run continues.