Store state and migrate it
Keep an id that a provider assigned, read it back on every run, and change its shape later.
Use persisted state only for what you cannot derive from your inputs and the session id. This guide stores the id of a database branch that a provider chose, and later changes its shape. Read Persisted state for the rules and limits.
1. Declare the state, and give the entry an id
import { z } from "zod";
import { definePlug } from "@blaiseagent/runner";
import { createBranch, deleteBranch } from "./branch-provider";
export default definePlug({
name: "acme/db-branch",
inputs: z.object({ project: z.string() }),
persisted: z.object({ branchId: z.string() }),
async onSession(ctx, next) {
let branchId = ctx.persisted.value?.branchId;
if (branchId === undefined) {
branchId = await createBranch(ctx.inputs.project, ctx.session.id);
// Record it right after creating it, so a crash leaves something to tear down.
await ctx.persisted.set({ branchId });
}
ctx.agent.env["DATABASE_BRANCH"] = branchId;
await next();
},
async onTeardown(ctx, next) {
await next();
if (ctx.persisted.value) {
await deleteBranch(ctx.persisted.value.branchId);
await ctx.persisted.clear();
}
},
});./branch-provider stands for the client of your database provider. The entry in blaise.yml needs
an id; without one the runner refuses to start:
setup:
- id: db
use: ./plugs/db-branch.ts
with:
project: main2. Change the shape
Version 1 stored { branch }; version 2 stores { branchId }. Add an onMigrate that converts the
old value to the new shape:
onMigrate(_ctx, stored) {
const v1 = z.object({ branch: z.string() }).safeParse(stored);
// Returning undefined drops the value, and the plug starts without state.
return v1.success ? { branchId: v1.data.branch } : undefined;
},onMigrate runs only when the stored value no longer validates. A value it returns is validated and
written back before onLoad and onSession. If it throws, returns something invalid, or does not
exist, the run fails with the plug named.
3. Test the migration
test("migrates the state stored by version 1", async () => {
const store = memoryStateStore([{ id: "db", plug: "acme/db-branch", value: { branch: "br_1" } }]);
const harness = createPlugHarness(dbBranch, {
id: "db",
store,
inputs: { project: "main" },
});
await harness.session();
expect(store.records.get("db")?.value).toStrictEqual({ branchId: "br_1" });
});Things to know
- Renaming the
idor switching to another plug leaves the old record without an owner. The runner reports it, and the dashboard lists it. See handle an orphaned environment. - The record is deleted when the session's teardown succeeds. Clear it yourself only if the resource it points at is gone earlier.