# Runtime And Harness
Jidoka separates authoring, executable data, and effect execution.
```text
Jidoka.Agent DSL
-> Jidoka.Agent.Spec
-> Jidoka.Turn.Plan
-> Jidoka.Harness
-> Jidoka.Runtime.TurnRunner
-> Runic workflow steps
-> Effect interpreter
-> ReqLLM / Jido.Action
```
For process-hosted agents, `Jido.AgentServer` sits around that harness:
```text
Jido.AgentServer
-> Jido.Signal "jidoka.turn.run"
-> Jidoka.Runtime.Actions.RunTurn
-> Jidoka.Harness
-> Jido agent state update
```
## Harness
`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.
```elixir
{: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:
```elixir
{:ok, reviews} = Jidoka.Session.pending_reviews(store)
```
Replay is a projection over stored data, not a runtime call:
```elixir
{:ok, replay} = Jidoka.Session.replay(session)
replay.timeline
```
## Observability 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:
```elixir
{: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:
```elixir
{: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: ...}` in `run.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:
```elixir
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_recalled` trace event when entries are present;
- adds a compact "Relevant memory" system message;
- exposes `prompt.memory` for 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:
```elixir
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:
1. run input controls;
2. run the Runic prompt/effect planning workflow;
3. optionally hibernate at a safe checkpoint;
4. interpret pending effects through runtime capabilities;
5. apply effect results to turn state;
6. validate and optionally repair structured final results;
7. loop until final answer or max model turns;
8. 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:
```elixir
%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:
- `:pure` means the operation can be recomputed from input;
- `:idempotent` means the runtime can safely retry with the same key;
- `:dedupe` means Jidoka should prefer a recorded journal result;
- `:reconcile` means incomplete work should be surfaced for application
reconciliation;
- `:unsafe_once` means 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:
```elixir
{: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:
```elixir
def call(%Jidoka.Runtime.Controls.OperationContext{} = operation) do
if operation.operation == "refund_order" do
{:interrupt, :approval_required}
else
:cont
end
end
```
The returned snapshot has:
- `cursor.phase == :review`;
- `turn_state.status == :waiting`;
- `turn_state.pending_interrupt` as a `Jidoka.Review.Interrupt`;
- `metadata["pending_review"]` as a `Jidoka.Review.Request`.
Resume with an approval response:
```elixir
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:
```elixir
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`:
```elixir
%{
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.Agent` modules;
- tools are Jido actions;
- action schemas and execution stay on the Jido side.
- `Jidoka.Jido` is the default Jido runtime instance started by the Jidoka
application module.
- `MyAgent.start/1` and `Jidoka.start_agent/2` start DSL agents under
`Jido.AgentServer`.
- AgentServer routes `"jidoka.turn.run"` to `Jidoka.Runtime.Actions.RunTurn`,
which runs the Jidoka harness and writes `:status`, `:last_answer`, and
a typed `Jidoka.Runtime.AgentServerState` under `agent.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.