Jidoka separates authoring, executable data, and effect execution.
Jidoka.Agent DSL
-> Jidoka.Agent.Spec
-> Jidoka.Turn.Plan
-> Jidoka.Harness
-> Jidoka.Runtime.TurnRunner
-> Runic workflow steps
-> Effect interpreter
-> ReqLLM / Jido.ActionFor process-hosted agents, Jido.AgentServer sits around that harness:
Jido.AgentServer
-> Jido.Signal "jidoka.turn.run"
-> Jidoka.Runtime.Actions.RunTurn
-> Jidoka.Harness
-> Jido agent state updateHarness
Jidoka.Harness is the execution boundary. It currently owns:
run_turn/3;resume/2;- request normalization;
- context schema validation;
- runtime normalization;
- approval response normalization;
- delegation to
Jidoka.Runtime.TurnRunner.
The harness is intentionally thin. Future session queues, stores, replay,
approval flows, and eval fixtures belong here rather than in the root Jidoka
module.
Sessions And Stores
Jidoka.Session is the ergonomic API for durable sessions. It delegates to
Jidoka.Harness, and the underlying data struct is still
Jidoka.Harness.Session.
Jidoka.Harness.Session is the durable harness envelope for work that spans
requests or process restarts. It contains:
- the canonical agent spec;
- request history;
- hibernated snapshots;
- pending review requests;
- the latest result or error;
- metadata owned by the application/harness.
Sessions are still data. They do not contain runtime clients or processes.
{:ok, pid} = Jidoka.Harness.Store.InMemory.start_link()
store = {Jidoka.Harness.Store.InMemory, pid: pid}
{:ok, session} =
Jidoka.session(spec, "support-session-1", store: store)
{:hibernate, session, snapshot} =
Jidoka.Session.run(session.session_id, "Hello",
store: store,
llm: llm,
checkpoint: :after_prompt
)
{:ok, session, result} =
Jidoka.Session.resume(session.session_id,
store: store,
llm: llm
)The store behaviour is intentionally small: put/get/list sessions. Pending review listing is derived from stored session data:
{:ok, reviews} = Jidoka.Session.pending_reviews(store)Replay is a projection over stored data, not a runtime call:
{:ok, replay} = Jidoka.Session.replay(session)
replay.timelineObservability And Evals
Core runtime events are neutral Jidoka.Event data. Jidoka.Trace projects
them into a compact timeline, and callers decide whether to persist that
timeline:
{:ok, sink} = Jidoka.Trace.Sink.InMemory.start_link()
:ok =
Jidoka.Trace.record(result.events, {Jidoka.Trace.Sink.InMemory, pid: sink},
policy:
Jidoka.Trace.Policy.new!(
sample_rate: 1.0,
redact_keys: [:api_key, :authorization],
omit_keys: [:messages, :prompt]
)
)Jidoka.inspect/1 returns stable views for agents, turns, snapshots, sessions,
replay, effect journals, review objects, memory results, and eval runs. These
views are projection-oriented and avoid provider-specific client data.
Eval cases are deterministic harness fixtures:
{:ok, run} =
Jidoka.Eval.run_case(
[
id: "support_lookup",
agent: spec,
input: "Check account acct_123",
assertions: %{
contains: "acct_123",
operation_called: "lookup_account"
}
],
llm: llm,
operations: operations
)The eval runner does not add another agent runtime. It uses
Jidoka.Harness.run_turn/3, then records assertion results and observations on
Jidoka.Eval.Run.
Eval input validation and eval execution failures are intentionally different:
- invalid eval case data returns
{:error, reason}; - a harness runtime error returns
{:ok, %Jidoka.Eval.Run{status: :error}}; - a hibernated turn also returns
{:ok, %Jidoka.Eval.Run{status: :error}}with%{reason: :hibernated, snapshot: ...}inrun.error.
That keeps eval outcomes serializable as evidence while still rejecting invalid eval definitions before execution.
Memory
Memory is opt-in agent policy plus per-run store capability:
spec =
Jidoka.agent!(
id: "support_agent",
instructions: "Use recalled memory when useful.",
memory: %{scope: :session, max_entries: 5}
)
{:ok, pid} = Jidoka.Memory.Store.InMemory.start_link()
memory_store = {Jidoka.Memory.Store.InMemory, pid: pid}
{:ok, _write} =
Jidoka.Harness.write_memory(spec, "Ada prefers concise answers.",
memory_store: memory_store
)Before prompt assembly, the harness recalls memory through the supplied store
and passes a typed Jidoka.Memory.RecallResult into the Runic turn state.
Prompt assembly then:
- adds a
memory_recalledtrace event when entries are present; - adds a compact "Relevant memory" system message;
- exposes
prompt.memoryfor preflight, tests, and provider runtime code.
Jidoka.preflight/3 accepts the same memory_store: option, so memory
contributions are visible without calling an LLM.
Operation Sources
Jidoka keeps one runtime operation path. Different executable surfaces should
compile into Agent.Spec.Operation plus a capability function:
source =
Jidoka.Operation.Source.Local.new!(
operations: [
%{
name: "lookup_ticket",
description: "Looks up a ticket.",
kind: :tool,
handler: fn args -> %{ticket_id: args["ticket_id"], status: "open"} end
}
]
)
{:ok, compiled} = Jidoka.Operation.Source.compile(source)
spec =
Jidoka.agent!(
id: "support_agent",
instructions: "Use lookup_ticket when needed.",
operations: compiled.operations
)
Jidoka.turn(spec, "Check ticket T-100",
llm: llm,
operations: compiled.capability
)Controls still match by operation kind and name. The local source above
uses kind :tool; Jido action sources use kind :action. Both execute through
the same Effect.Intent / Effect.Result journal path.
Turn Runner
Jidoka.Runtime.TurnRunner owns the loop:
- run input controls;
- run the Runic prompt/effect planning workflow;
- optionally hibernate at a safe checkpoint;
- interpret pending effects through runtime capabilities;
- apply effect results to turn state;
- validate and optionally repair structured final results;
- loop until final answer or max model turns;
- run output controls before returning.
Operation controls run inside the effect interpreter immediately before an
operation capability is called. If a control returns {:interrupt, reason}, the
runner marks the turn state as :waiting and hibernates at a review cursor
instead of calling the operation.
Effects
External work is represented as data:
%Jidoka.Effect.Intent{
kind: :llm | :operation,
payload: %{},
idempotency_key: "...",
idempotency: :idempotent
}The effect interpreter records intents and results in Effect.Journal. On
resume, existing results are reused instead of re-running the same effect.
Operation Idempotency
Every operation declares one idempotency policy:
:puremeans the operation can be recomputed from input;:idempotentmeans the runtime can safely retry with the same key;:dedupemeans Jidoka should prefer a recorded journal result;:reconcilemeans incomplete work should be surfaced for application reconciliation;:unsafe_oncemeans Jidoka must not retry automatically.
:unsafe_once operations require an explicit operation control. The control
can allow, block, or interrupt for human review, but it must be present before
the spec can be compiled into a Turn.Plan. This makes risky work visible at
preflight time instead of discovering it after a model chooses the operation.
If a journal already has a result for an operation effect, resume replays that
result and does not call the operation capability again. If an :unsafe_once
intent was recorded without a result, resume returns a typed execution error
instead of retrying the operation. Later harness/session storage can use that
same shape to route the case to a reconciliation queue.
Durability
Jidoka snapshots semantic state:
{:hibernate, snapshot} =
Jidoka.turn(spec, "Hello",
llm: llm,
checkpoint: :after_prompt
)
{:ok, result} = Jidoka.resume(snapshot, llm: llm)Current checkpoint policies:
:none:after_prompt:after_each_phase:before_each_effect
This is safe-boundary durability, not arbitrary process resurrection.
Versioned durability boundaries:
Jidoka.Runtime.AgentSnapshot.schema_version() == 1;- serialized snapshots use the opaque prefix
jidoka:snapshot:v1:; Jidoka.Harness.Session.schema_version() == 1;- import documents use
Jidoka.Import.AgentDocument.version() == 1.
Unsupported versions fail during normalization instead of attempting a partial resume/import.
Human-In-The-Loop Review
An operation control can pause execution:
def call(%Jidoka.Runtime.Controls.OperationContext{} = operation) do
if operation.operation == "refund_order" do
{:interrupt, :approval_required}
else
:cont
end
endThe returned snapshot has:
cursor.phase == :review;turn_state.status == :waiting;turn_state.pending_interruptas aJidoka.Review.Interrupt;metadata["pending_review"]as aJidoka.Review.Request.
Resume with an approval response:
approval = Jidoka.Review.Response.approve(snapshot.turn_state.pending_interrupt)
{:ok, result} = Jidoka.resume(snapshot, approval: approval, llm: llm, operations: operations)Resume with a denial:
denial = Jidoka.Review.Response.deny(snapshot.turn_state.pending_interrupt, reason: :rejected)
{:error, error} = Jidoka.resume(snapshot, approval: denial, llm: llm, operations: operations)The approved operation resumes from the pending Effect.Intent; Jidoka does
not re-run operation controls for that approved interrupt. The journal still
prevents duplicate effect results on normal hibernate/resume boundaries.
Structured Results
If Agent.Spec.result is present, a final model decision must include a
structured result value in addition to user-facing content:
%{
type: :final,
content: "Ada is ready.",
result: %{name: "Ada", confidence: 10}
}The runtime validates the value with the configured Zoi schema before marking
the turn finished. Validated data is stored on Turn.State.result_value and
returned as Turn.Result.value. Output controls run after validation, so their
context receives both result text and result_value data.
If a model omits the explicit result field but returns JSON as content,
Jidoka attempts to validate that decoded JSON as the structured result. Plain
text content is still preserved for unstructured agents.
If validation fails and max_repairs has not been exhausted, Jidoka appends a
repair instruction to the durable agent state and runs another model turn. This
uses the same Runic/effect loop; it is not a provider-specific structured output
API.
Jido Relationship
Jidoka uses Jido as the foundation:
- DSL agent modules are also
Jido.Agentmodules; - tools are Jido actions;
- action schemas and execution stay on the Jido side.
Jidoka.Jidois the default Jido runtime instance started by the Jidoka application module.MyAgent.start/1andJidoka.start_agent/2start DSL agents underJido.AgentServer.- AgentServer routes
"jidoka.turn.run"toJidoka.Runtime.Actions.RunTurn, which runs the Jidoka harness and writes:status,:last_answer, and a typedJidoka.Runtime.AgentServerStateunderagent.state[:jidoka].
Jidoka does not delegate the core loop to Jido.AI.ReAct. The ReAct-style loop
is implemented through Jidoka's Runic/effect/harness spine.