BlaiseBlaise
Documentation
Runner and PlugsHow-to guides

Make a plug resumable

Write onSession so that a paused session can continue, and say so when it cannot.

A session is paused when it goes idle and resumes with the next message. Your plug runs again, with ctx.run.kind === "resume", in the environment the first run left. This guide makes a plug survive that.

1. Create unless it exists

Most plugs need no branch at all. Work out what you need from your inputs and the session id, and create it only if it is not there:

import { z } from "zod";
import { definePlug } from "@blaiseagent/runner";

// Stand-ins for provider calls.
async function installDependencies(_workspace: string): Promise<void> {}

export default definePlug({
  name: "acme/scratch-dir",
  inputs: z.object({ root: z.string() }),

  async onSession(ctx, next) {
    // The same path on every run of the session: no stored state needed.
    const dir = `${ctx.inputs.root}/${ctx.session.id}`;
    await ctx.host.fs.promises.mkdir(dir, { recursive: true });
    ctx.agent.env["SCRATCH_DIR"] = dir;

    if (ctx.run.kind === "start") {
      await installDependencies(dir);
    }

    await next();
  },
});

2. Restore what a run contributes

What a plug adds to a run (environment variables, the working directory, outputs) belongs to that run. The agent starts again on every resume, so the plug adds it again, as it did on start. That is the example above: SCRATCH_DIR is set on every run.

3. Branch only where the runs differ

if (ctx.run.kind === "start") {
  await installDependencies(dir);
}

4. Stop what is running, and skip what only a resume needs

After next() the plug pauses: it stops what it started. mayResume tells it whether a resume can follow, and it is Blaise's decision, not the runner's. When it cannot, a teardown follows at once, so work that only a later resume would use can be skipped:

const pause = await next();
await stopContainer(ctx.session.id);
if (pause.mayResume) {
  await snapshotContainer(ctx.session.id);
}

5. Say so when it cannot resume

If the plug cannot handle a resume run, declare it:

import { definePlug } from "@blaiseagent/runner";

export default definePlug({
  name: "acme/one-shot",
  resumable: false,

  async onSession(_ctx, next) {
    await next();
  },
});

A runner with such a plug refuses to start while the agent's idle policy is pause, and says which plug is the cause:

plug 4 (acme/one-shot) is not resumable; set the agent's idle policy to "end" in Blaise, or remove the plug

Either set the idle policy of the agent to End it (see choose what happens to idle sessions), or remove the plug.

Test it

Run the plug as a start run and as a resume run with the test harness. The harness takes the kind of run when you create it:

import { describe, expect, test } from "vitest";
import { createPlugHarness } from "@blaiseagent/runner/testing";
import scratchDir from "./scratch-dir";

async function envOf(kind: "start" | "resume", number: number): Promise<string | undefined> {
  const harness = createPlugHarness(scratchDir, {
    inputs: { root: "/tmp/scratch" },
    session: { id: "s1" },
    run: { kind, number },
  });
  const { agent } = await harness.session({
    pause: { reason: "idle", mayResume: true },
  });
  return agent.env["SCRATCH_DIR"];
}

describe("scratch-dir", () => {
  test("sets the same directory on start and resume", async () => {
    await expect(envOf("start", 1)).resolves.toBe("/tmp/scratch/s1");
    await expect(envOf("resume", 2)).resolves.toBe("/tmp/scratch/s1");
  });
});

On this page