# Agent Spec Contract
`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`, harness, 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](getting-started.md) and [Agent DSL](agent-dsl.md).
## Prerequisites
- You can compile and run the `:jidoka` test suite.
- You have read the spec section of [Getting Started](getting-started.md).
```bash
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`).
```elixir
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.
```diagram
╭──────────────╮ ╭──────────────╮ ╭──────────────╮
│ 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 harness options.
## 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.
### `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`](`Jidoka.Config`). Use
[`Jidoka.Config.model_ref/1`](`Jidoka.Config`) to read it back as a
`"provider:id"` string.
### `generation`
A [`Jidoka.Agent.Spec.Generation`](`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`](`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`](`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`](`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`](`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](controls.md).
### `runtime_defaults` And `metadata`
Plain maps. `runtime_defaults` feeds defaults into
[`Jidoka.Turn.Plan.new/1`](`Jidoka.Turn.Plan`). `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!/1` with 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/1`** when a caller may already hold a `%Spec{}`. It
delegates to `Spec.new/1` and accepts both.
- **Keep adapter metadata in `Operation.metadata`.** Source kinds (`:action`,
`:ash_resource`, `:browser`, `:mcp`, etc.) are discovered through
`Jidoka.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.
```elixir
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()
end
```
For 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 matching operation control. | Add a control entry under `controls.operations` for that operation. See [Controls](controls.md). |
| `{: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 harness options (`llm:`, `operations:`, `memory_store:`). |
## Reference
- [`Jidoka.Agent.Spec`](`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`](`Jidoka.Agent.Spec.Controls`) - control
policy struct.
- [`Jidoka.Agent.Spec.Generation`](`Jidoka.Agent.Spec.Generation`) - generation
defaults.
- [`Jidoka.Agent.Spec.Memory`](`Jidoka.Agent.Spec.Memory`) - memory policy.
- [`Jidoka.Agent.Spec.Operation`](`Jidoka.Agent.Spec.Operation`) - operation
definition + `idempotency`, `kind/1`, `requires_control?/1`.
- [`Jidoka.Agent.Spec.Result`](`Jidoka.Agent.Spec.Result`) - structured result
contract.
- [`Jidoka.Config`](`Jidoka.Config`) - default model, generation, max turns,
turn timeout.
## Related Guides
- [Agent DSL](agent-dsl.md) - DSL surface that compiles into this spec.
- [Controls](controls.md) - `Spec.Controls` policy semantics.
- [Structured Results](structured-results.md) - `Spec.Result` and the repair
loop.
- [Turn And Effect Contracts](turn-and-effect-contracts.md) - the next layer
down.
- [Errors And Config Reference](errors-and-config-reference.md) - defaults
used by `Spec.new/1`.