Skip to main content

guides/jido-process-integration.md

# Jido Process Integration

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](runtime-and-harness.md).

## Prerequisites

- A working Jidoka DSL agent module. See [Getting Started](getting-started.md).
- 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`](`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:

```elixir
# config/config.exs
import Config

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

Supervision tree:

```elixir
# 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.

```elixir
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`](`Jidoka`) and sends a signal to the
`Jido.AgentServer`.

## Concepts

```diagram
╭───────────────╮      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`](`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`](`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`:

```elixir
{: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:

```elixir
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):

```elixir
{: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`](`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:

```elixir
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:

```elixir
{: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

```elixir
: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`.

```elixir
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](getting-started.md).
No provider key is required.

## Troubleshooting

| Symptom | Likely Cause | Fix |
| --- | --- | --- |
| `{:error, :not_found}` from `turn/3` or `stop_agent/2` | The 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_server` | `RunTurn` 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` restarts | Agents were supervised with `:one_for_one`. | Use `:rest_for_one` so hosted agents restart with the registry. |
| `await_agent/2` times out with `:idle` hint | No 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 id | They 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:

- [`Jidoka.Jido`](`Jidoka.Jido`) - default Jido runtime instance for Jidoka
  agents.
- [`Jidoka`](`Jidoka`) - `start_agent/2`, `stop_agent/2`, `whereis/2`,
  `await_agent/2`, `turn/3`, `chat/3`.
- [`Jidoka.Agent`](`Jidoka.Agent`) - DSL module that injects `start/1` and
  `child_spec/1` for hosted agents.
- [`Jidoka.Runtime.Signals`](`Jidoka.Runtime.Signals`) - constructor for the
  `"jidoka.turn.run"` signal.
- [`Jidoka.Runtime.Actions.RunTurn`](`Jidoka.Runtime.Actions.RunTurn`) - Jido
  action that runs the harness inside the agent server.
- [`Jidoka.Runtime.AgentServerState`](`Jidoka.Runtime.AgentServerState`) -
  typed Jidoka state stored under `agent.state[:jidoka]`.

## Related Guides

- [Getting Started](getting-started.md) - the smallest DSL agent end to end.
- [Runtime And Harness](runtime-and-harness.md) - sessions, snapshots,
  effects, and memory.
- [Live LLM Tool Loop](live-llm-tool-loop.md) - running a hosted agent
  against a real provider.
- [AshJido Resources](ash-jido.md) - exposing Ash actions as agent tools.
- [Browser Tools](browser-tools.md) - hosted agents that read the web.
- [MCP Tools](mcp-tools.md) - hosted agents that call external MCP servers.