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.
Read next
- 21 hooks: guardrails for the agent loop — pair snapshots with consent + audit hooks.
- Suspend, resume, push-resume — snapshots are the same primitive that survives the OS.