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 plugEither 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");
});
});