Agent Spec Contract

Copy Markdown View Source

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 and Agent DSL.

Prerequisites

  • You can compile and run the :jidoka test 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 harness options.

Fields

FieldTypeDefaultPurpose
idnon-empty stringrequiredStable identifier used by snapshots, sessions, and traces.
instructionsnon-empty stringrequiredSystem-style instructions injected into prompt assembly.
model%LLMDB.Model{}Jidoka.Config.default_model/0Normalized model spec. Strings such as "openai:gpt-4o-mini" are normalized through ReqLLM.
generationJidoka.Agent.Spec.Generation.t()Jidoka.Config.default_generation/0Provider-neutral generation defaults (params, provider_options, extra).
context_schemaZoi schema or nilnilOptional schema used by Spec.validate_context/2 against the per-turn context map.
resultJidoka.Agent.Spec.Result.t() or nilnilOptional structured result contract (Zoi schema plus max_repairs).
memoryJidoka.Agent.Spec.Memory.t() or nilnilOptional 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).
controlsJidoka.Agent.Spec.Controls.t()Controls.new!()Policy controls (input/operation/output, max_turns, timeout_ms).
runtime_defaultsmap%{}Default knobs consumed by Turn.Plan.new/1 (:workflow_profile, :max_model_turns, :timeout_ms).
metadatamap%{}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. 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!/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.

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

SymptomLikely CauseFix
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.
{: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 keysYou injected a runtime value into a spec field.Move runtime values to harness options (llm:, operations:, memory_store:).

Reference