A Jidoka session keeps an agent conversation alive across turns. It stores request history, snapshots, pending reviews, and the latest result. It does not own provider clients or long-running processes.
Use This When
- Use a session when the same agent answers more than one user message.
- Use a session when a turn must resume after a process restart.
- Use a session when a human-in-the-loop interrupt must be picked up later.
- Use a single
Jidoka.turn/3orJidoka.chat/3call when the work is one-shot and the caller does not need to remember anything between turns. - Use a store when sessions must survive a node restart or be shared across workers; keep the in-memory store for tests and local exploration.
Prerequisites
- A working Jidoka agent. The smallest one is enough; see Getting Started.
- A provider key in scope for live examples.
- For persistence: a started
Jidoka.Harness.Store.InMemoryprocess, or a module that implementsJidoka.Harness.Store.
mix deps.get
mix test
Start A Session
The smallest durable session is a store plus a session id.
{:ok, pid} = Jidoka.Harness.Store.InMemory.start_link()
store = {Jidoka.Harness.Store.InMemory, pid: pid}
{:ok, session} =
Jidoka.Session.start(MyApp.SupportAgent, "support-123", store: store)
{:ok, session, text} =
Jidoka.Session.chat(session.session_id, "Say hi to Ada.", store: store)That call ran through the same runtime as Jidoka.turn/3, then persisted the
updated session under id "support-123". A second Jidoka.Session.chat/3
against the same id continues the conversation.
Concepts
A session is data. Apps usually call Jidoka.Session; stores persist the
session data between turns.
╭──────────────────────╮
│ Jidoka.Session │
│ start / run / chat │
│ resume / replay │
╰──────────┬───────────╯
│ reads and writes
▼
╭──────────────────────────╮
│ Durable session data │
│ spec / requests │
│ snapshots / result │
│ pending_reviews │
╰──────────┬───────────────╯
│ persists through
▼
╭──────────────────────────╮
│ Store │
│ put / get / list / claim │
╰──────────────────────────╯Jidoka.Sessionis the developer-facing facade. It wrapsstart/run/chat/resumeand derives sensible defaults.Jidoka.Harness.Sessionis the durable data struct. Itsschema_version/0is1; older or newer payloads fail at normalization rather than silently loading a half-valid session.Jidoka.Harness.Storeis the persistence behaviour:put_session/2,get_session/2,list_sessions/1, and optionalclaim_session/3for atomic single-runner semantics.
A session status is one of :new, :running, :hibernated, :waiting,
:finished, or :error. Jidoka computes it from snapshots, pending reviews,
and the latest result.
How To
Step 1: Start A Session
Jidoka.Session.start/2 accepts a DSL module, a Jidoka.Agent.Spec, or a
keyword list of spec attributes. Pass store: to persist immediately.
{:ok, session} =
Jidoka.Session.start(MyApp.SupportAgent,
session_id: "support-123",
store: store,
metadata: %{tenant: "acme"}
)
session.session_id
#=> "support-123"
session.status
#=> :new
session.metadata
#=> %{tenant: "acme"}If no session id is supplied, Jidoka generates one through
Jidoka.Id.generate/2. Passing session_id: is preferred for any flow that
needs a persistent external handle (a chat thread id, a ticket id, a workflow id).
Step 2: Run Turns
Jidoka.Session.run/3 is the full-result API. It returns the underlying
Jidoka.Turn.Result, a hibernation snapshot, or an error, along with the
updated session struct so callers without a store still have durable state.
{:ok, session, %Jidoka.Turn.Result{} = result} =
Jidoka.Session.run(session.session_id, "Look up order A1001",
store: store
)
result.content
result.events
result.valueJidoka.Session.chat/3 is the text-only API. It is the right default for
product code.
{:ok, session, text} =
Jidoka.Session.chat(session.session_id, "And what is its status?",
store: store
)Both functions accept either a session struct or a session id. With a store the id is enough; without a store, hold onto the returned struct.
Step 3: Hibernate And Resume
Pass a checkpoint policy when you want the turn to pause at a safe boundary:
{:hibernate, session, snapshot} =
Jidoka.Session.chat(session.session_id, "Refund order A1001",
store: store,
checkpoint: :after_prompt
)
session.status
#=> :hibernatedResume picks up the latest snapshot recorded on the session:
{:ok, session, %Jidoka.Turn.Result{}} =
Jidoka.Session.resume(session.session_id,
store: store
)See Snapshots And Resume for the full snapshot lifecycle and serialization format.
Step 4: List Pending Reviews
Pending review requests are derived from snapshot metadata when an operation
control returns {:interrupt, reason}. They can be listed per session or
across an entire store:
{:ok, [%Jidoka.Review.Request{} = request]} =
Jidoka.Session.pending_reviews(session)
{:ok, all_pending} = Jidoka.Session.pending_reviews(store)The store-level helper iterates list_sessions/1 and flattens
session.pending_reviews, so it works the same for any compliant backend.
For the durable approval flow itself, see
Human In The Loop.
Step 5: Implement A Custom Store
A store is a module implementing Jidoka.Harness.Store. The required
callbacks are small.
defmodule MyApp.PostgresSessionStore do
@behaviour Jidoka.Harness.Store
alias Jidoka.Harness.Session
@impl true
def put_session(%Session{} = session, _opts) do
MyApp.Repo.upsert_session(session)
{:ok, session}
end
@impl true
def get_session(session_id, _opts) when is_binary(session_id) do
case MyApp.Repo.fetch_session(session_id) do
nil -> {:error, {:session_not_found, session_id}}
session -> {:ok, session}
end
end
@impl true
def list_sessions(_opts) do
{:ok, MyApp.Repo.all_sessions()}
end
endclaim_session/3 is optional. Implement it when the backend has a native
atomic compare-and-set; otherwise the default fallback uses get_session/2
followed by put_session/2 after rejecting any session already in
:running state.
Callers reference a store as either Module or {Module, opts}. The
in-memory store is {Jidoka.Harness.Store.InMemory, pid: pid} so the same
shape works for stores that need configuration (database, namespace, region).
Step 6: Inspect Sessions
Replay is a data-only projection over what a session already knows. It does not call any capability and is safe to run anywhere.
{:ok, replay} = Jidoka.Session.replay(session)
replay.timeline
replay.journal
replay.pending_reviewsFor human-readable inspection of a session, snapshot, or request, use
Jidoka.inspect/1. For trace projection see
Tracing And Events.
Common Patterns
- Session per external identifier. Use the same id the surrounding product uses (chat thread, ticket, workflow) instead of generating a fresh one. This keeps lookups idempotent.
- Pass the store on every call. The store reference is just data, and passing it makes the call self-contained. Avoid hiding it behind global state.
- Prefer
chat/3for product code. Reach forrun/3when you need the full result, the journal, or to observe a hibernation snapshot. - Keep capabilities out of session metadata. Provider clients, pids, and credentials belong in the runtime options for each call, not on the serializable session.
- Use
claim_session/3in multi-worker deployments. It is the difference between two workers racing on the same turn and one worker observing{:error, {:session_already_running, _}}and backing off.
Testing
Sessions are easy to test because every capability is injectable. A deterministic LLM and the in-memory store are usually enough.
test "session keeps history across turns" do
{:ok, pid} = Jidoka.Harness.Store.InMemory.start_link()
store = {Jidoka.Harness.Store.InMemory, pid: pid}
llm = fn _intent, journal ->
case map_size(journal.results) do
0 -> {:ok, %{type: :final, content: "first"}}
_ -> {:ok, %{type: :final, content: "second"}}
end
end
{:ok, session} = Jidoka.Session.start(MyApp.SupportAgent, "s1", store: store)
{:ok, _session, "first"} = Jidoka.Session.chat("s1", "hi", store: store, llm: llm)
{:ok, session, "second"} = Jidoka.Session.chat("s1", "again", store: store, llm: llm)
assert length(session.requests) == 2
assert session.status == :finished
endFor multi-worker safety, write a test that calls Jidoka.Session.run/3
twice concurrently against the same id and assert one call returns
{:error, {:session_already_running, _}}.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
{:error, :missing_harness_store} | A session id was passed without a store: option. | Pass the store on every call, or hold the session struct and pass it directly. |
{:error, {:session_not_found, id}} | The id was never started against this store. | Call Jidoka.Session.start/3 with session_id: id, store: store. |
{:error, {:session_already_running, id}} | Two callers tried to run the same session at the same time. | Serialize callers; if this is expected, retry after the prior call returns. |
{:error, {:missing_session_snapshot, id}} | Resume was called on a session that never hibernated. | Run a new turn instead, or hibernate explicitly with a checkpoint policy. |
{:error, {:conflicting_session_ids, _, _}} | Both :id and :session_id were passed with different values. | Pass only :session_id, or make them equal. |
{:error, {:unsupported_session_schema_version, _, 1}} | A persisted payload predates the current schema. | Migrate the row to schema version 1 or discard it. |
Reference
Key modules touched in this guide:
Jidoka.Session- public facade forstart/2,run/3,chat/3,resume/2,pending_reviews/1,replay/1.Jidoka.Harness.Session- durable session struct withschema_version/0 == 1.Jidoka.Harness.Store- persistence behaviour.Jidoka.Harness.Store.InMemory- reference store for tests and examples.Jidoka.Review.Request- shape returned bypending_reviews/1.
Related Guides
- Snapshots And Resume - the durable artifact a session hibernates to.
- Human In The Loop - pending reviews and the approve/deny resume path.
- Tracing And Events - what
Jidoka.Session.replay/1projects under the hood. - Runtime And Harness - internals for sessions, stores, and replay.