BlaiseBlaise
Documentation
Runner and PlugsHow-to guides

Write a local plug

Add a plug to your repository as a TypeScript file and use it from blaise.yml.

A plug is a TypeScript module that default-exports the result of definePlug. Any path in use: that starts with ./ or ../ is loaded as a plug.

1. Create the module

plugs/preamble.ts
import { z } from "zod";
import { definePlug } from "@blaiseagent/runner";

export default definePlug({
  name: "acme/preamble",
  inputs: z.object({ text: z.string() }),

  // Add a line before every prompt the agent receives.
  onMessage(ctx, message, next) {
    if (message.method === "session/prompt") {
      return next({
        ...message,
        params: {
          ...message.params,
          prompt: [{ type: "text", text: `${ctx.inputs.text}\n\n` }, ...message.params.prompt],
        },
      });
    }
    return next(message);
  },
});

The project needs @blaiseagent/runner and zod in its dependencies.

2. Use it

blaise.yml
version: 1
setup:
  - use: ./plugs/preamble.ts
    with:
      text: "Follow the house rules in docs/rules.md."

The path is relative to the file that contains the entry. with: is checked against the plug's inputs schema when the runner starts, so a mistake is reported before any session runs.

3. Test it

Run the plug in isolation with the testing harness:

import { describe, expect, test } from "vitest";
import type { Message } from "@blaiseagent/runner";
import { createPlugHarness } from "@blaiseagent/runner/testing";
import preamble from "./preamble";

const prompt: Message = {
  direction: "to_agent",
  method: "session/prompt",
  params: { sessionId: "s", prompt: [{ type: "text", text: "Fix the bug." }] },
};

describe("preamble", () => {
  test("puts the text before the prompt", async () => {
    const harness = createPlugHarness(preamble, {
      inputs: { text: "Follow the house rules." },
    });
    const { forwarded } = await harness.message(prompt);
    expect(forwarded).toStrictEqual([
      {
        ...prompt,
        params: {
          sessionId: "s",
          prompt: [
            { type: "text", text: "Follow the house rules.\n\n" },
            { type: "text", text: "Fix the bug." },
          ],
        },
      },
    ]);
  });

  test("passes other messages on unchanged", async () => {
    const harness = createPlugHarness(preamble, { inputs: { text: "x" } });
    const cancel: Message = {
      direction: "to_agent",
      method: "session/cancel",
      params: { sessionId: "s" },
    };
    const { forwarded } = await harness.message(cancel);
    expect(forwarded).toStrictEqual([cancel]);
  });
});

Then start the runner to try it for real.

Things to know

  • In onMessage, pass what you do not care about on with return next(message). Change a message by passing a copy with different params to next; the method cannot change.
  • The message the agent receives, and the one recorded in the transcript, include your change. A message that a plug answers itself is still recorded, tagged with the plug's name.
  • Throw a RequestError to answer an agent request with an error. Anything else you throw fails the run, which is what you want from a redactor that broke.
  • The full list of handlers and context is in the plug API reference.

On this page