Jido Process Integration

Copy Markdown View Source

This guide explains how a Jidoka DSL agent becomes a supervised Jido.AgentServer process. It covers the default Jidoka.Jido runtime instance, the start_agent / stop_agent / whereis helpers, the generated child_spec/1, the "jidoka.turn.run" signal flow, and how the typed Jidoka state is read back out of Jido.Agent.state[:jidoka]. By the end you will be able to host an agent under a supervisor, run a turn against the registered id, and inspect its status.

When To Use This

  • Use this guide when you want a long-lived, addressable agent process: shared across requests, restartable, supervised, callable by id.
  • Use this guide when you need await_completion, hibernation, or to wire an agent into a Phoenix application.ex.
  • Do not use this guide for single-shot deterministic runs. For unit tests and one-off invocations, MyAgent.run_turn/2 and Jidoka.turn/3 against the spec are simpler and faster. See Runtime And Harness.

Prerequisites

  • A working Jidoka DSL agent module. See Getting Started.
  • Elixir ~> 1.18 and :jidoka resolved through mix deps.get.
  • The default Jidoka.Jido instance only needs to be in your supervision tree if you want supervisor-restartable agents. Direct Jidoka.start_agent/2 calls in IEx will start it on demand under the application supervisor.

Setup

Jidoka ships a default Jido runtime instance, Jidoka.Jido, which is just use Jido, otp_app: :jidoka. That single supervisor owns the registry, dynamic supervisor, task supervisor, and runtime store that hosted agents need.

Application config:

# config/config.exs
import Config

config :jidoka,
  default_model: "openai:gpt-4o-mini"

Supervision tree:

# lib/my_app/application.ex
defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      Jidoka.Jido,
      {MyApp.TimeAgent, jido: Jidoka.Jido}
    ]

    Supervisor.start_link(children, strategy: :rest_for_one, name: MyApp.Supervisor)
  end
end

Credentials for live turns (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) belong in the host process environment. Jidoka itself does not read .env files.

Security / Trust Boundaries

  • The Jidoka.Jido registry is process-local; agent ids are not a global namespace. Two applications can each host an agent with id "time-agent-1" under their own instance without collision.
  • Jidoka.Jido.start_agent/2 accepts a module, which the caller controls. Never pass untrusted module names from external input; resolve through your own allowlist first.
  • Provider credentials are taken from the process environment by ReqLLM. They are never written into Agent.Spec, snapshots, or journals.
  • Inspect normalized errors with Jidoka.error_to_map/1; credential-shaped values are sanitized before being returned.

Quick Example

Start a DSL agent under the default Jidoka.Jido supervisor and run a turn against its registered id.

defmodule MyApp.LocalTime do
  use Jidoka.Action,
    name: "local_time",
    description: "Returns the local time for a city.",
    schema: Zoi.object(%{city: Zoi.string() |> Zoi.default("Chicago")})

  @impl true
  def run(params, _context) do
    city = Map.get(params, :city) || Map.get(params, "city") || "Chicago"
    {:ok, %{city: city, time: "09:30"}}
  end
end

defmodule MyApp.TimeAgent do
  use Jidoka.Agent

  agent :time_agent do
    instructions "Use local_time when asked for the time."
  end

  tools do
    action MyApp.LocalTime
  end
end

{:ok, _pid} = MyApp.TimeAgent.start(id: "time-agent-1")
{:ok, "Chicago time is 09:30."} =
  Jidoka.chat("time-agent-1", "What time is it in Chicago?", llm: fake_llm())

The DSL module, the spec, and the plan are the same as the in-process flow. Only the execution boundary differs: Jidoka.chat/3 resolves the binary id through Jidoka.whereis/2 and sends a signal to the Jido.AgentServer.

Concepts

╭───────────────╮      start_agent       ╭────────────────────╮
│ MyApp.Agent   │───────────────────────▶│  Jidoka.Jido       │
│ (DSL module)  │                        │  (registry +       │
╰───────┬───────╯                        │   dyn supervisor)  │
        │ child_spec/1                   ╰─────────┬──────────╯
        ▼                                          │
╭───────────────────╮  "jidoka.turn.run"           ▼
│ Jido.AgentServer  │◀────────── signal ─── Jidoka.turn(id, ...)
│  state[:jidoka] = │
│  AgentServerState │──── routes to ────▶ Jidoka.Runtime.Actions.RunTurn
╰─────────┬─────────╯                            │
          │                                      ▼
          │                            ╭──────────────────────╮
          │                            │ Jidoka.Harness       │
          │                            │ (Runic + Effects)    │
          │                            ╰──────────┬───────────╯
          ▼                                       ▼
   to_jido_state/1                         Turn.Result / Snapshot

Three pieces define this boundary:

  1. Jidoka.Jido is a use Jido, otp_app: :jidoka supervisor. It owns the registry, dynamic supervisor, task supervisor, and runtime store. Applications may host their own instance instead.
  2. The DSL module's child_spec/1 wraps Jido.AgentServer.child_spec/1 with jido: Jidoka.Jido and a default id derived from the agent module. The compiled signal route {"jidoka.turn.run", Jidoka.Runtime.Actions.RunTurn} is attached at compile time.
  3. Jidoka.Runtime.AgentServerState is the typed Jidoka state stored under agent.state[:jidoka]. Conventional top-level Jido fields (:status, :last_answer, :error) are kept for Jido.AgentServer compatibility.

How To

Step 1: Start An Agent Under The Default Runtime

The DSL module exposes start/1, which calls Jidoka.start_agent/2, which delegates to Jidoka.Jido.start_agent/2:

{:ok, pid} = MyApp.TimeAgent.start(id: "time-agent-1")
^pid = Jidoka.whereis("time-agent-1")

If id: is omitted, the agent module supplies one derived from its DSL agent id (:time_agent becomes "time_agent").

Step 2: Supervise An Agent In Your Application

For production callers, prefer child_spec/1 over start_agent/2 so the agent restarts with the rest of your tree:

children = [
  Jidoka.Jido,
  {MyApp.TimeAgent, jido: Jidoka.Jido, id: "time-agent-1"}
]

Supervisor.start_link(children, strategy: :rest_for_one, name: MyApp.Supervisor)

MyApp.TimeAgent.child_spec/1 calls Jido.AgentServer.child_spec/1 with the right defaults. The :rest_for_one strategy ensures that a restart of Jidoka.Jido also restarts the agents that depend on its registry.

Step 3: Run A Turn Against A Registered Id

The facade accepts a process ref (pid, registered binary id, or :via tuple):

{:ok, %Jidoka.Turn.Result{} = result} =
  Jidoka.turn("time-agent-1", "What time is it in Chicago?",
    timeout: 30_000,
    llm: fake_llm()
  )

result.content
#=> "Chicago time is 09:30."

Under the hood Jidoka:

  1. Builds a signal with Jidoka.Runtime.Signals.turn_run/2 (type "jidoka.turn.run").
  2. Resolves the binary id through Jidoka.whereis/2.
  3. Calls Jido.AgentServer.call(pid, signal, timeout) which routes to Jidoka.Runtime.Actions.RunTurn.
  4. Reads the typed result back out of agent.state[:jidoka] and returns {:ok, Turn.Result.t()}, {:hibernate, snapshot}, or {:error, reason}.

Step 4: Read State Out Of A Hosted Agent

The current Jidoka state can be inspected directly:

agent = :sys.get_state(Jidoka.whereis("time-agent-1")).agent

{:ok, jidoka_state} =
  Jidoka.Runtime.AgentServerState.from_jido_state(agent.state)

jidoka_state.status        #=> :completed
jidoka_state.result.content

from_jido_state/1 reads state[:jidoka] and returns the typed AgentServerState. Use to_run_result/1 to convert it back into the {:ok, ...} | {:hibernate, ...} | {:error, ...} envelope.

Step 5: Await Terminal Status

Most callers will just block on Jidoka.turn/3, but for fire-and-forget signal dispatch you can wait for a terminal Jido status:

{:ok, status_map} =
  Jidoka.await_agent("time-agent-1", timeout: 30_000)

status_map.status
#=> :completed

await_agent/2 is only meaningful for process-hosted agents. It is a thin wrapper around Jido.AgentServer.await_completion/2 with Jidoka error normalization.

Step 6: Stop An Agent

:ok = Jidoka.stop_agent("time-agent-1")

stop_agent/2 accepts a pid or the registered binary id. It returns {:error, :not_found} if the id has no running process.

Common Patterns

  • Treat the registered id as your routing key. Phoenix controllers and LiveViews should call Jidoka.turn(id, ...) instead of looking up a pid and threading it through assigns.
  • Use a custom Jido instance per app boundary. If a host app already defines MyApp.Jido, pass jido: MyApp.Jido to the child spec so the agent lives under that supervisor instead of Jidoka.Jido.
  • Prefer :rest_for_one when supervising agents alongside Jidoka.Jido so the registry and the agents that depend on it restart together.
  • Inspect with Jidoka.inspect/1. Run it on the pid or registered id when you want a stable, human-readable view of agent status without poking into the raw state[:jidoka] struct.

Testing

Process-hosted tests use the same deterministic capabilities as direct turns. The test owns the supervised process; the runtime opts are forwarded as the signal's runtime_opts and threaded into RunTurn.

defmodule MyApp.TimeAgentTest do
  use ExUnit.Case, async: true

  setup do
    start_supervised!(Jidoka.Jido)
    id = "time-agent-#{System.unique_integer([:positive])}"
    start_supervised!({MyApp.TimeAgent, jido: Jidoka.Jido, id: id})
    %{id: id}
  end

  test "answers the time against a hosted agent", %{id: id} do
    llm = fn _intent, journal ->
      llm_calls =
        Enum.count(journal.results, fn {_id, r} -> r.kind == :llm end)

      case llm_calls do
        0 ->
          {:ok,
           %{type: :operation, name: "local_time", arguments: %{"city" => "Chicago"}}}

        1 ->
          {:ok, %{type: :final, content: "Chicago time is 09:30."}}
      end
    end

    assert {:ok, "Chicago time is 09:30."} =
             Jidoka.chat(id, "What time is it in Chicago?", llm: llm)
  end
end

The fake llm is the same shape used in Getting Started. No provider key is required.

Troubleshooting

SymptomLikely CauseFix
{:error, :not_found} from turn/3 or stop_agent/2The binary id is not registered in Jidoka.Jido.Confirm with Jidoka.whereis(id). Start with MyAgent.start(id: ...) or supervise via child_spec/1.
{:error, %Jidoka.Error{} = e} with phase: :agent_serverRunTurn failed to build a valid request (missing :input or agent module context).Send a non-empty string and verify the agent module compiled cleanly with Jidoka.inspect(MyAgent).
Process exits when Jidoka.Jido restartsAgents were supervised with :one_for_one.Use :rest_for_one so hosted agents restart with the registry.
await_agent/2 times out with :idle hintNo turn was ever sent; the agent has no work to wait on.Send a turn/3 or chat/3 before awaiting, or skip await_agent and use the synchronous facade.
Different apps clash on the same idThey share the same Jidoka.Jido instance.Each app should use Jido, otp_app: :my_app and host its own runtime instance.

Reference

Key modules touched in this guide: