Write a stateless plug
Derive a plug's resources from its inputs and the session id, so it needs no stored state.
Most plugs need no persisted state. Name what the plug creates after the session id, create it if it is not there, and destroy it at teardown.
1. Derive the resource
import { z } from "zod";
import { definePlug } from "@blaiseagent/runner";
export default definePlug({
name: "acme/derived-dir",
inputs: z.object({ root: z.string() }),
async onSession(ctx, next) {
// Everything comes from the inputs and the session id, so every run finds the same directory.
const dir = `${ctx.inputs.root}/${ctx.session.id}`;
await ctx.host.fs.promises.mkdir(dir, { recursive: true });
ctx.agent.cwd = dir;
ctx.setOutput("path", dir);
await next();
},
async onTeardown(ctx, next) {
await next();
await ctx.host.fs.promises.rm(`${ctx.inputs.root}/${ctx.session.id}`, {
recursive: true,
force: true,
});
},
});The directory is <root>/<session id> on every run, so a resumed run finds the one the first run
made. ctx.agent.cwd is set on every run, because the agent starts again each time.
2. Destroy it at teardown
onTeardown runs after the inner plugs have torn down. The plug removes the directory by the same
name it created it with. force: true makes a directory that is already gone a success, so a
retried teardown works.
3. Test the runs together
A harness is one run. Build one for each run, on the same host, as the environment outlives the runs:
import { describe, expect, test } from "vitest";
import { createPlugHarness, memoryHost } from "@blaiseagent/runner/testing";
import derivedDir from "./derived-dir";
describe("derived-dir", () => {
test("creates the directory on every run and removes it at teardown", async () => {
// One harness per run, on the same host, as the environment outlives the runs.
const host = memoryHost();
const run = (kind: "start" | "resume" | "teardown", number: number) =>
createPlugHarness(derivedDir, {
inputs: { root: "/work" },
session: { id: "s1" },
run: { kind, number },
host,
});
const start = run("start", 1);
await start.session({ pause: { reason: "idle", mayResume: true } });
expect(start.outputs).toStrictEqual({ path: "/work/s1" });
await expect(host.fs.promises.stat("/work/s1")).resolves.toBeDefined();
const resume = run("resume", 2);
await resume.session({ pause: { reason: "closed", mayResume: false } });
expect(resume.outputs).toStrictEqual({ path: "/work/s1" });
await run("teardown", 3).teardown();
await expect(host.fs.promises.stat("/work/s1")).rejects.toThrow(/ENOENT/);
});
});When you need persisted state instead
A resource whose name you do not choose, such as an id assigned by a provider, cannot be derived. Use persisted state for that.