Custom Snapshots in MoonBit
Golem agents can implement the Snapshottable trait to support manual (snapshot-based) updates and snapshot-based recovery.
Automatic JSON Snapshotting (Default)
When the agent struct derives ToJson and @json.FromJson, the SDK's code generation automatically provides JSON-based snapshotting. Enable it via the snapshotting attribute on #derive.agent:
#derive.agent(snapshotting="every_n(1)")
struct Counter {
name : String
mut value : UInt64
} derive(ToJson, @json.FromJson)
fn Counter::new(name : String) -> Counter {
{ name, value: 0 }
}
pub fn Counter::increment(self : Self) -> Unit {
self.value += 1
}
pub fn Counter::get_value(self : Self) -> UInt64 {
self.value
}The code generation tool detects ToJson and @json.FromJson derives and generates a Snapshottable implementation that serializes the agent as JSON.
Snapshotting Modes
The snapshotting attribute accepts these values:
| Mode | Example | Description |
|---|---|---|
| (omitted) | #derive.agent | Snapshotting disabled |
| Every N | #derive.agent(snapshotting="every_n(1)") | Snapshot every N successful invocations |
Custom Snapshotting
For custom binary serialization or cross-version migration, implement the Snapshottable trait manually:
pub(open) trait Snapshottable {
save_snapshot(Self) -> Bytes
load_snapshot(Self, Bytes) -> Result[Unit, String]
}Example
#derive.agent(snapshotting="every_n(1)")
struct Counter {
name : String
mut value : UInt64
}
fn Counter::new(name : String) -> Counter {
{ name, value: 0 }
}
pub fn Counter::increment(self : Self) -> Unit {
self.value += 1
}
pub fn Counter::get_value(self : Self) -> UInt64 {
self.value
}
///|
pub impl @agents.Snapshottable for Counter with save_snapshot(self) {
// Serialize value as 8 big-endian bytes
let bytes = Bytes::new(8)
let v = self.value
for i in 0..<8 {
bytes[i] = ((v >> ((7 - i).to_uint64() * 8)).to_int() & 0xff).to_byte()
}
bytes
}
///|
pub impl @agents.Snapshottable for Counter with load_snapshot(self, bytes) {
if bytes.length() != 8 {
return Err("Expected an 8-byte long snapshot")
}
let mut v : UInt64 = 0
for i in 0..<8 {
v = v | (bytes[i].to_uint64() << ((7 - i).to_uint64() * 8))
}
self.value = v
Ok(())
}Method Signatures
// Save: serialize the agent's current state to bytes
save_snapshot(Self) -> Bytes
// Load: restore the agent's state from previously saved bytes
// Return Err to signal the update should fail and the agent should revert
load_snapshot(Self, Bytes) -> Result[Unit, String]How the SDK Wires Snapshots
The code generation tool (golem_sdk_tools agents) produces a ConstructedAgent struct for each agent. When snapshotting is enabled:
- If the agent has
ToJson+@json.FromJsonderives, the generated code automatically provides aSnapshottableimplementation using JSON serialization. - If the agent has a manual
impl Snapshottable, the custom implementation is used instead. - The
ConstructedAgentrecords thesnapshottableinterface reference andsnapshot_format(Json or Binary). - The SDK's
save-snapshotandload-snapshotWIT exports delegate to these implementations.
Best Practices
- Prefer automatic (JSON) snapshotting — derive
ToJsonand@json.FromJsonon the agent struct for zero-effort persistence. - Keep snapshots small — large snapshots impact recovery and update time.
- Version your snapshot format — include a version byte so
load_snapshotcan handle snapshots from older versions. - Test round-trips — verify that
save_snapshot→load_snapshotproduces equivalent state. - Handle migration — when the state schema changes between versions,
load_snapshotin the new version should be able to parse snapshots from the old version. - Return
Errto reject incompatible snapshots —load_snapshotreturningErrcauses the update to fail gracefully, reverting the agent to the old version.