Mobile agents drive devices. They send messages, edit files, post events to calendars. That’s terrifying without an undo button. agentlib’s snapshot system is the undo button.

The three operations

// 1. Take a snapshot. Returns the SHA-256 id.
final id = await ctx.snapshot(label: 'before-send');

// 2. Roll back to a previous snapshot.
await ctx.revert(id);

// 3. Branch — keep the original, start a parallel timeline from this point.
final forked = await ctx.fork(id, model: OpenAIProvider(apiKey: '<KEY>'));

That’s the public API. Three operations. Content-addressed by SHA-256 of the serialised state, so identical states produce identical ids — the store dedupes automatically.

Auto-snapshots before high-impact actions

The most useful pattern: mark any destructive tool as highImpact: true, and agentlib auto-snapshots before dispatch.

final sendWhatsApp = tool(
  name: 'whatsapp.send',
  description: 'Send a WhatsApp message.',
  schema: Schema.object({
    'contact': Schema.string(),
    'message': Schema.string(),
  }),
  capabilities: const [Capability.accessibility],
  idempotent: false,
  highImpact: true,    // ← agentlib snapshots before this dispatches
  execute: (args, ctx) async { ... },
);

Now every time the model calls whatsapp.send, agentlib snapshots first. If the send goes wrong (typo’d message, wrong contact, broken tool result), revert() rolls everything back. The external side effect persists (the message was sent), but the agent’s understanding of state goes back to before the send — so the next user turn doesn’t proceed as if everything succeeded.

For truly safe undo, pair snapshots with idempotency or transactional tools. Some whatsapp.send implementations support recall-within-7-minutes; agentlib can detect a snapshot pre-recall and offer the user a one-tap “undo”.

Fork: branching to a different model

fork(id, model: ...) is the killer feature for evaluation and recovery. You don’t lose the original timeline; you start a parallel one from any past point with whatever changes you want.

Common uses:

Try a different model. “This Claude turn looked off — let me fork and see what GPT-4o would have done.”

final alt = await ctx.fork(lastTurnId, model: OpenAIProvider(apiKey: '<KEY>'));

Try a different system prompt. “The instructions might be wrong — fork with a tweaked system prompt and see if the result is better.”

final alt = await ctx.fork(lastTurnId, instructions: 'New instructions...');

Speculative execution. During a long run, fork to a smaller model for an A/B comparison. Discard whichever is worse.

The store

In production you want SqfliteSnapshotStore:

final store = SqfliteSnapshotStore(database: db);
final ctx = RunConfig(snapshotStore: store, ...);

It’s SQLite-backed. Each snapshot is a row keyed by SHA-256; large blobs (messages, scratch state) live in a content-addressed blob table with foreign keys + cascade deletes. Calling store.vacuum() walks the blob table and deletes orphans.

For tests, use InMemorySnapshotStore. Same shape, all in RAM, gone when the test finishes.

Compaction interplay

When the message history gets too long, the agent loop runs compaction — summarises old turns into a shorter representation. agentlib emits PreCompact and PostCompact hooks so you can intervene.

Compaction takes a snapshot before it runs. If the compaction summary is bad (lost important detail), the user sees it and can revert to the pre-compaction snapshot.

Hooks for snapshots

Two hooks fire around snapshots:

  • OnSnapshot — when a snapshot is taken. Log it; sync to remote; mirror to backup.
  • OnRevert — when a revert happens. Re-render UI; reset side effects; emit analytics.
hooks.register(OnSnapshot((event, _) async {
  await remoteBackup.upload(event.id, event.payload);
  return HookOutcome.carryOn();
}));

Practical patterns

Auto-snapshot every N turns. Use RunConfig(autoSnapshotEveryNTurns: 5) if you want a baseline cadence in addition to high-impact pre-snapshots.

Named user snapshots. Let users name a state (“save as ‘planning v2’”). Surface them in a UI for revert + fork.

Diff for time-machine UI. store.diff(idA, idB) returns a structured diff between two snapshots — useful for a “what changed?” view.

What snapshots don’t do

They don’t undo external side effects. They don’t time-travel device state — if the agent sent a notification, the user got the notification. They don’t replace consent: a PermissionRequestHook to confirm a destructive tool is still the right primary safety net.

They do give you a deterministic recovery story when an agent goes off the rails — and that’s the difference between shipping autonomous agents that drive a device and shipping a “tap to retry” assistant.