Jidoka has three versioned data boundaries: the import document
(Jidoka.Import.AgentDocument), the runtime snapshot
(Jidoka.Runtime.AgentSnapshot), and the harness session
(Jidoka.Harness.Session). Each one declares an explicit version/schema_version
field, fails fast on unsupported values, and stays portable across releases.
This guide documents those boundaries plus the registry shape that backs
import.
When To Use This
- Use this guide when you are persisting Jidoka data outside the BEAM (files, databases, message queues) and need to plan for forward/backward compatibility.
- Use this guide when you build importers, exporters, or serializers that cross process or release boundaries.
- Do not use this guide for in-process state handling; for that see Turn And Effect Contracts.
Prerequisites
- You have read Agent Spec Contract and Turn And Effect Contracts.
- You can build and run a Jidoka turn.
Quick Example
Each contract round-trips through its module's new/1 (or serialize/deserialize)
function. Import takes raw YAML/JSON plus a registry; snapshot takes terminal
state plus a cursor; session aggregates the rest.
# Import (version 1)
yaml = """
agent:
id: time_agent
model: openai:gpt-4o-mini
instructions: Call local_time when asked for the time.
tools:
actions:
- local_time
"""
{:ok, spec} =
Jidoka.import(yaml,
actions: %{"local_time" => MyApp.LocalTime}
)
# Snapshot (schema_version 1, "jidoka:snapshot:v1:" prefix)
{:ok, snapshot} =
Jidoka.Runtime.AgentSnapshot.from_turn_state(turn_state, Jidoka.Turn.Cursor.after_prompt())
{:ok, serialized} = Jidoka.Runtime.AgentSnapshot.serialize(snapshot)
String.starts_with?(serialized, "jidoka:snapshot:v1:")
#=> true
# Session (schema_version 1)
{:ok, session} = Jidoka.Harness.Session.start(spec)Concepts
╭───────────────────╮ ╭───────────────────╮ ╭───────────────────╮
│ Import.Agent │ │ Runtime.Agent │ │ Harness.Session │
│ Document │ │ Snapshot │ │ │
│ version: 1 │ │ schema_version:1 │ │ schema_version:1 │
╰─────────┬─────────╯ ╰─────────┬─────────╯ ╰─────────┬─────────╯
│ │ │
▼ ▼ ▼
Agent.Spec Hibernate/Resume Persisted run
(compiled) ("jidoka:snapshot:v1:") envelopeEach version field is the compatibility boundary. Constructors validate
the version on load and return {:error, {:unsupported_..._version, found, expected}}
when they do not match.
Fields
Jidoka.Import.AgentDocument
Portable JSON/YAML authoring document.
| Field | Type | Default | Purpose |
|---|---|---|---|
version | positive integer | 1 (current) | Document version. Jidoka.Import.AgentDocument.version/0 returns the supported value. |
agent | map | required | Spec attributes (id, model, instructions, etc.). |
tools | map | %{} | Tool registry references (actions, ash_resources, browsers, mcp_tools, etc.). |
controls | map | %{} | Inline controls config. |
operations | [map()] | [] | Inline operation definitions. |
runtime_defaults | map | %{} | Maps to Spec.runtime_defaults. |
metadata | map | %{} | Caller metadata. |
AgentDocument.new/1 enforces version == 1; any other value returns
{:error, {:unsupported_import_document_version, version, 1}}.
Import Registries
Imports never call String.to_atom/1 on input. Module and schema references
in YAML/JSON are resolved through caller-provided registries passed to
Jidoka.Import.import/2:
| Option | Resolves | Shape |
|---|---|---|
:actions (or :registries[:actions]) | Jido action modules referenced under tools.actions. | %{"local_time" => MyApp.LocalTime} |
:ash_resources | Ash resources for AshJido sources. | %{"posts" => MyApp.Posts.Post} |
:controls | Custom control modules. | %{"approval" => MyApp.Approvals} |
:context_schemas | Zoi schemas for spec.context_schema. | %{"chat_context" => MyApp.ChatContext.schema()} |
:result_schemas | Zoi schemas for spec.result. | %{"answer" => MyApp.Answer.schema()} |
Use the plural option (actions: %{...}) for direct overrides, or pass the
full bag via registries: [actions: ..., controls: ...].
Jidoka.Runtime.AgentSnapshot
Serializable semantic snapshot used for hibernate/resume.
| Field | Type | Default | Purpose |
|---|---|---|---|
schema_version | positive integer | 1 (current) | Compatibility boundary. AgentSnapshot.schema_version/0 returns the supported value. |
snapshot_id | non-empty string | generated "snap_…" | Stable id for storage and traces. |
agent_id | non-empty string | required | Mirrors Spec.id. |
cursor | Turn.Cursor.t() | required | Next safe resume boundary. |
turn_state | Turn.State.t() | required | Full turn state at snapshot time. |
metadata | map | %{} | Snapshot metadata (pending review data, caller tags, etc.). |
Serialization format. AgentSnapshot.serialize/1 produces an opaque
string with the prefix "jidoka:snapshot:v1:" followed by URL-safe Base64.
The prefix is the wire-level version tag. AgentSnapshot.deserialize/1
refuses any other prefix with {:error, :invalid_snapshot_serialization}.
Snapshots refuse non-portable values (functions, pids, ports, references) and
return {:error, {:non_serializable_snapshot_value, path, type}} on
serialization.
Jidoka.Harness.Session
Serializable envelope for running an agent across requests.
| Field | Type | Default | Purpose |
|---|---|---|---|
schema_version | positive integer | 1 (current) | Compatibility boundary. Harness.Session.schema_version/0 returns the supported value. |
session_id | non-empty string | generated "sess_…" | Stable session id. |
agent_id | non-empty string | required | Mirrors Spec.id. |
spec | Agent.Spec.t() | required | The compiled spec the session runs. |
status | :new | :running | :hibernated | :waiting | :finished | :error | :new | Lifecycle marker. |
requests | [Turn.Request.t()] | [] | Append-only request history. |
snapshots | [AgentSnapshot.t()] | [] | Append-only snapshot history (latest at the tail). |
result | Turn.Result.t() | nil | nil | Last finished result. |
pending_reviews | [Review.Request.t()] | [] | Outstanding review requests blocking the session. |
error | term or nil | nil | Last error when status == :error. |
metadata | map | %{} | Session metadata. |
What Changes Across Versions
The version fields exist precisely so the implementation can evolve without breaking persisted data:
| Boundary | What may change between versions | Stability promise |
|---|---|---|
AgentDocument v1 | Tool registry shape, controls vocabulary, operation kinds. | The version field is the wire signal. New versions add new constructors; old versions stay loadable until intentionally dropped. |
AgentSnapshot v1 | Internal Turn.State fields, cursor metadata, capture of new domain values. | The "jidoka:snapshot:v1:" prefix is the wire signal. New schema versions get a new prefix. |
Harness.Session v1 | Status set, snapshot list pruning, review request shape. | The schema_version field is the wire signal. |
Loaders must reject unsupported versions instead of best-effort guessing.
Common Patterns
- Persist
serialize/1output, not the struct. The serialized string is the durable wire format; the in-memory struct is for convenience. - Treat the version field as the contract. Always check it in any custom loader; never strip or rewrite it silently.
- Keep registries explicit. Build the action/control/schema registries at
boot and pass them into
Jidoka.import/2rather than relying on global state. - Pair snapshots with cursors. Snapshots without cursors cannot resume safely; the cursor is what defines the next safe phase boundary.
Testing
Round-trip tests are the cheapest correctness check for these boundaries.
test "snapshot round-trips through serialization" do
{:ok, snapshot} =
Jidoka.Runtime.AgentSnapshot.from_turn_state(turn_state, Jidoka.Turn.Cursor.after_prompt())
{:ok, serialized} = Jidoka.Runtime.AgentSnapshot.serialize(snapshot)
assert String.starts_with?(serialized, "jidoka:snapshot:v1:")
{:ok, restored} = Jidoka.Runtime.AgentSnapshot.deserialize(serialized)
assert restored.snapshot_id == snapshot.snapshot_id
assert restored.schema_version == 1
end
test "rejects an unsupported document version" do
assert {:error, {:unsupported_import_document_version, 99, 1}} =
Jidoka.Import.AgentDocument.new(%{version: 99, agent: %{"id" => "x"}})
endTroubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
{:error, {:unsupported_import_document_version, found, expected}} | YAML/JSON declares a different version. | Update the document to the expected version or migrate it before loading. |
{:error, :invalid_snapshot_serialization} | Input did not start with "jidoka:snapshot:v1:". | Confirm the durable blob was produced by AgentSnapshot.serialize/1. |
{:error, {:non_serializable_snapshot_value, path, type}} | A function, pid, port, or reference reached Turn.State. | Replace runtime values with serializable data before snapshotting. |
{:error, {:unsupported_snapshot_schema_version, found, expected}} | Snapshot map has a non-matching schema_version. | Run a migration step before calling AgentSnapshot.new/1. |
Jidoka.import/2 raises about an unknown action | Registry is missing the referenced module. | Add the action to actions: or registries[:actions]. |
Reference
Jidoka.Import.AgentDocument-version/0,new/1,new!/1.Jidoka.Import-import/2, registry options.Jidoka.Runtime.AgentSnapshot-schema_version/0,serialize/1,deserialize/1,from_turn_state/3.Jidoka.Harness.Session-schema_version/0,start/2,put_request/2,put_snapshot/2,put_result/2.Jidoka.Turn.Cursor- cursor shape stored on every snapshot.Jidoka.Turn.State- state shape stored on every snapshot.
Related Guides
- Agent Spec Contract - the spec compiled by imports and embedded in sessions.
- Turn And Effect Contracts - the state and cursor shapes inside a snapshot.
- Runtime And Harness - how sessions and snapshots are produced and resumed.