Jidoka.Agent.Spec is the immutable definition of a Jidoka agent. Every
authoring path (Spark DSL, JSON/YAML import, Jidoka.agent/1) compiles into
the same struct, and every downstream layer (Turn.Plan, runtime, snapshots)
consumes that struct as its single source of truth. This guide enumerates each
field and the constructors that produce a valid spec.
When To Use This
- Use this guide when you need to know the exact shape of an agent definition, for example when building a custom authoring path, an importer, or a snapshot inspector.
- Use this guide when you want to confirm what is and is not stored on a spec (clients, processes, and credentials are not).
- Do not use this guide as an introduction to authoring agents. Start with Getting Started and Agent DSL.
Prerequisites
- You can compile and run the
:jidokatest suite. - You have read the spec section of Getting Started.
mix deps.get
mix test
Quick Example
Jidoka.Agent.Spec.new!/1 is the canonical constructor. The DSL and the
importer both end up calling it (directly or through from_input/1).
alias Jidoka.Agent.Spec
spec =
Spec.new!(
id: "time_agent",
instructions: "Call local_time when asked for the time.",
model: "openai:gpt-4o-mini",
generation: %{temperature: 0.0, max_tokens: 500},
operations: [
%{name: "local_time", description: "Returns local time for a city."}
]
)
spec.id #=> "time_agent"
spec.controls #=> %Jidoka.Agent.Spec.Controls{max_turns: nil, ...}
spec.result #=> nil (no structured result declared)A spec is plain data. It contains no processes, no provider clients, and no credentials. It can be inspected, diffed, serialized, and shipped across versions without leaking anything runtime-specific.
Concepts
A spec is a closed contract between authoring and runtime. The runtime never reads anything the spec does not expose.
╭──────────────╮ ╭──────────────╮ ╭──────────────╮
│ DSL / │────▶│ Spec.new! │────▶│ Agent.Spec │
│ Import │ │ Spec.new │ │ (immutable) │
╰──────────────╯ ╰──────────────╯ ╰──────┬───────╯
│
▼
╭───────────────╮
│ Turn.Plan │
╰───────────────╯The fields below are the entire surface. Anything else (capabilities, stores, keys) is supplied at run time through facade options.
Normalization Boundary
Spec.new/1 and Spec.from_input/1 accept untrusted keyword lists and maps.
They normalize aliases, nested input, defaults, and the model value before
they build the struct. The module @schema describes this normalized final
struct. It does not describe every input form that the constructors accept.
All authoring paths must produce the same final schema. A spec built by the DSL, an import, a map, or an existing spec must have the same field types and defaults. New input aliases belong only at the constructor boundary. Normalize them first, then validate the normalized value against the final schema.
Fields
| Field | Type | Default | Purpose |
|---|---|---|---|
id | non-empty string | required | Stable identifier used by snapshots, sessions, and traces. |
instructions | non-empty string | required | System-style instructions injected into prompt assembly. |
model | %LLMDB.Model{} | Jidoka.Config.default_model/0 | Normalized model spec. Strings such as "openai:gpt-4o-mini" are normalized through ReqLLM. |
generation | Jidoka.Agent.Spec.Generation.t() | Jidoka.Config.default_generation/0 | Provider-neutral generation defaults (params, provider_options, extra). |
context_schema | Zoi schema or nil | nil | Optional schema used by Spec.validate_context/2 against the per-turn context map. |
result | Jidoka.Agent.Spec.Result.t() or nil | nil | Optional structured result contract (Zoi schema plus max_repairs). |
memory | Jidoka.Agent.Spec.Memory.t() or nil | nil | Optional conversation memory policy. Runtime stores are injected separately. |
operations | [Jidoka.Agent.Spec.Operation.t()] | [] | Model-callable operation definitions (data only; the operation source supplies the capability). |
controls | Jidoka.Agent.Spec.Controls.t() | Controls.new!() | Policy controls (input/operation/output, max_turns, timeout_ms). |
runtime_defaults | map | %{} | Default knobs consumed by Turn.Plan.new/1 (:workflow_profile, :max_model_turns, :timeout_ms). |
metadata | map | %{} | Caller-defined metadata; opaque to Jidoka. |
id And instructions
Both are required non-empty strings. id is the spec identity used everywhere
durable (sessions, snapshots, traces). instructions is the system-style
prompt body assembled into each turn.
The spec value stays static. To resolve instructions for one request, pass
instructions: to Jidoka.turn/3, Jidoka.Session.run/3, or
Jidoka.preflight/3. The option accepts a non-empty string, a two-argument
function, or a module that implements Jidoka.Instructions:
instructions = fn base, context ->
"#{base}\nServe tenant #{context.data.tenant_id}."
end
{:ok, result} =
Jidoka.turn(MyApp.SupportAgent, "Help me",
context: %{tenant_id: "tenant_1"},
instructions: instructions
)The provider receives the base instructions and a new public
Jidoka.Context. It can read the request input, public data, and request
metadata. It cannot read trusted runtime data or internal request objects. The
resolved text is stored in the turn plan and prompt. A hibernated turn resumes
with the same resolved text and does not call the provider again.
model
Stored as a normalized %LLMDB.Model{} struct. Spec.new/1 accepts any
ReqLLM-supported model input and runs it through
Jidoka.Config.normalize_model_spec/2. Use
Jidoka.Config.model_ref/1 to read it back as a
"provider:id" string.
generation
A Jidoka.Agent.Spec.Generation struct with
three maps:
params- known, provider-neutral keys (:temperature,:max_tokens,:top_p,:tool_choice, etc.).provider_options- opaque provider-specific knobs forwarded to ReqLLM.extra- escape hatch for caller metadata.
context_schema And Per-Turn Context
context_schema is a Zoi schema (or nil). The runtime validates the per-turn
context map through Spec.validate_context/2. A missing schema accepts any
map.
result
A Jidoka.Agent.Spec.Result struct describing
the structured app-facing return value. The Zoi schema and a bounded
max_repairs count drive the structured-result repair loop in
Turn.State. When result is nil, the turn returns plain assistant text.
memory
A Jidoka.Agent.Spec.Memory policy describing
scope (:agent or :session), capture (:manual, :conversation,
:off), inject (:instructions or :context), max_entries, and an
optional namespace. The policy is definition data; the actual
Jidoka.Memory.Store is supplied per run.
operations
A list of Jidoka.Agent.Spec.Operation
structs. Each operation carries a name, optional description, an
idempotency value (:pure, :idempotent, :dedupe, :reconcile,
:unsafe_once), and metadata. Operations are data; the executable capability
comes from a Jidoka.Operation.Source.
controls
A Jidoka.Agent.Spec.Controls struct with
max_turns, timeout_ms, and three control lists (inputs, operations,
outputs). Used by the runtime for policy enforcement; see
Controls.
runtime_defaults And metadata
Plain maps. runtime_defaults feeds defaults into
Jidoka.Turn.Plan.new/1. metadata is opaque caller
data.
Common Patterns
- Build specs from maps, not strings. Strings cross the import boundary;
in-process code should call
Spec.new!/1with a map or keyword list. - Treat the spec as a value. Pass it by reference, snapshot it, diff it. Never mutate it.
- Reuse
Spec.from_input/1when a caller may already hold a%Spec{}. It delegates toSpec.new/1and accepts both. - Keep adapter metadata in
Operation.metadata. Source kinds (:action,:ash_resource,:browser,:mcp, etc.) are discovered throughJidoka.Agent.Spec.Operation.kind/1.
Testing
A spec test is the cheapest unit test in the system: build it, assert on its fields, optionally compile a plan.
test "compiles a tool_loop plan from a minimal spec" do
spec =
Spec.new!(
id: "echo",
instructions: "Echo the user input.",
model: "openai:gpt-4o-mini"
)
assert {:ok, plan} = Jidoka.Turn.Plan.new(spec)
assert plan.workflow_profile == :tool_loop
assert plan.max_model_turns == Jidoka.Config.default_max_model_turns()
endFor coverage of the DSL/import to spec contract, see
test/jidoka/golden/dsl_to_spec_test.exs.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
ArgumentError: invalid agent spec: ... | A required field is missing or a value failed Zoi parsing. | Inspect the inner reason; common fixes are non-empty id/instructions and a valid model string. |
{:error, {:invalid_context_schema, _}} | context_schema is not a Zoi schema. | Pass a Zoi.* value or nil. |
{:error, {:unsafe_once_requires_control, name, kind}} | An :unsafe_once operation has no approval policy or matching operation control. | Add approval: true or a control entry under controls.operations. See Controls. |
{:error, {:invalid_result_schema, _}} | result was given a non-Zoi value. | Wrap the schema with Zoi.* constructors before passing it. |
| Spec inspection shows live processes or keys | You injected a runtime value into a spec field. | Move runtime values to facade options (llm:, operations:, memory_store:). |
Reference
Jidoka.Agent.Spec- canonical struct,new/1,new!/1,from_input/1,validate_context/2,validate_result/2,validate_operation_policies/1.Jidoka.Agent.Spec.Controls- control policy struct.Jidoka.Agent.Spec.Generation- generation defaults.Jidoka.Agent.Spec.Memory- memory policy.Jidoka.Agent.Spec.Operation- operation definition +idempotency,kind/1,requires_control?/1.Jidoka.Agent.Spec.Result- structured result contract.Jidoka.Config- default model, generation, max turns, turn timeout.
Related Guides
- Agent DSL - DSL surface that compiles into this spec.
- Controls -
Spec.Controlspolicy semantics. - Structured Results -
Spec.Resultand the repair loop. - Turn And Effect Contracts - the next layer down.
- Errors And Config Reference - defaults
used by
Spec.new/1.