Controls are Jidoka's policy layer. They are declared on Agent.Spec and run
while a turn is executing.
Boundaries
Jidoka currently supports these control points:
inputruns before prompt assembly and the first model call.operationruns before a model-requested operation capability executes.outputruns after structured result validation and before the turn returns.max_turnsbounds model/operation loops.timeoutbounds wall-clock turn runtime in milliseconds.
Controls may return:
:cont,:allow, or:okto continue;{:block, reason}to fail deterministically;{:interrupt, reason}to pause when supported by that boundary;{:error, reason}to fail as a control error.
Operation interrupts are durable today. Input/output interrupts are currently reported as errors until those boundaries get resumable wait semantics.
Input Controls
Input controls receive a map with the request, context, metadata, and input text:
defmodule MyApp.NoSecrets do
use Jidoka.Control, name: "no_secrets"
@impl true
def call(%{input: input}) do
if String.contains?(input, "secret") do
{:block, :secret_input}
else
:cont
end
end
endDeclare the control in the agent:
defmodule MyApp.SupportAgent do
use Jidoka.Agent
agent :support_agent do
instructions "Answer support questions tersely."
end
controls do
input MyApp.NoSecrets
end
endOperation Controls And Approvals
Operation controls receive Jidoka.Runtime.Controls.OperationContext. This is
the safety boundary for tool/action execution.
defmodule MyApp.RequireRefundApproval do
use Jidoka.Control, name: "require_refund_approval"
@impl true
def call(%Jidoka.Runtime.Controls.OperationContext{} = operation) do
if operation.operation == "refund_order" do
{:interrupt, :approval_required}
else
:cont
end
end
endAttach it to a specific operation:
controls do
operation MyApp.RequireRefundApproval,
when: [kind: :action, name: :refund_order]
endOperation matches can be broad or narrow. Supported match keys are kind,
name, source, idempotency, and top-level metadata values:
controls do
operation MyApp.RequireRefundApproval,
when: [
kind: :tool,
source: :payments,
idempotency: :unsafe_once,
metadata: %{risk: "high"}
]
endIf an operation control interrupts, the turn hibernates:
{:hibernate, snapshot} =
Jidoka.turn(MyApp.RefundAgent, "Refund order_123")
review = snapshot.metadata["pending_review"]
approval = Jidoka.Review.Response.approve(review.interrupt_id)
{:ok, result} =
Jidoka.resume(snapshot, approval: approval)Operations marked :unsafe_once must have a matching operation control before
the agent can compile into a plan. This makes risky work visible during
preflight instead of after a model chooses the operation.
Output Controls
Output controls run after any configured structured result schema validates.
They receive both the assistant text and result_value:
defmodule MyApp.SafeReply do
use Jidoka.Control, name: "safe_reply"
@impl true
def call(%{result: text, result_value: value}) do
cond do
String.contains?(text, "forbidden") -> {:block, :unsafe_reply}
match?(%{approved: false}, value) -> {:block, :unapproved_result}
true -> :cont
end
end
endImport Shape
JSON/YAML controls use string refs resolved through registries:
controls:
max_turns: 8
timeout: 30000
inputs:
- control: no_secrets
operations:
- control: require_refund_approval
when:
kind: action
name: refund_order
outputs:
- control: safe_reply{:ok, spec} =
Jidoka.import(yaml,
registries: %{
controls: %{
"no_secrets" => MyApp.NoSecrets,
"require_refund_approval" => MyApp.RequireRefundApproval,
"safe_reply" => MyApp.SafeReply
}
}
)Testing
Use a fake LLM and local operation capability for deterministic control tests. Existing examples live under:
test/integration/controls_integration_test.exstest/integration/human_in_the_loop_integration_test.exstest/support/integration/controls/