Debugging

Diagnose agent issues with debug modes, event buffers, and structured diagnostics.

Setup

Mix.install([
  {:jido, "~> 2.2"}
])

Logger.configure(level: :warning)

# Livebook imports can execute generated docs as doctests.
# Disable compiler docs until the current Jido Hex release drops the invalid signal_types/0 example.
Code.put_compiler_option(:docs, false)

Jido.start()
runtime = Jido.default_instance()

Define a minimal agent for the examples in this guide.

This guide runs entirely locally. No provider keys or network calls are required.

defmodule MyApp.CounterAgent do
  use Jido.Agent,
    name: "counter_agent",
    schema: [
      count: [type: :integer, default: 0],
      status: [type: :atom, default: :idle]
    ]
end

Debug levels

Jido has three instance-wide debug levels that control logging verbosity and event capture across all agents in that instance.

LevelLoggingArgument displayDebug events
:offConfigured defaultsN/ANone
:on:debugKeys onlyMinimal
:verbose:traceFull valuesAll

Toggle the level at runtime with Jido.debug/1.

Jido.debug(:on)

Check the current level by calling Jido.debug/0.

Jido.debug()

When you need to see full argument values including sensitive fields, disable redaction.

Jido.debug(:on, redact: false)

Turn debug mode off when you are done investigating.

Jido.debug(:off)

Warning: Never leave redact: false enabled in production. It exposes sensitive values in log output.

Per-agent debug mode

Instance-level debug controls verbosity globally. Per-agent debug enables a ring buffer on a specific AgentServer process that records internal events as they happen.

Enable at start

Pass debug: true when starting the agent server.

{:ok, pid} = Jido.start_agent(
  runtime,
  MyApp.CounterAgent,
  debug: true
)

Enable at runtime

Toggle debug mode on an already-running agent server.

:ok = Jido.AgentServer.set_debug(pid, true)

The ring buffer holds up to 500 events. When the buffer fills, the oldest events are dropped.

Reading the event buffer

Retrieve events from the ring buffer with recent_events/2. Events arrive newest-first.

{:ok, events} = Jido.AgentServer.recent_events(pid, limit: 10)

Each event is a map with three keys.

%{
  at: -576460734,
  type: :signal_received,
  data: %{signal_type: "counter.increment"}
}

The :at value is a monotonic timestamp in milliseconds. Use it for relative timing between events, not absolute wall-clock time.

Event types

TypeRecorded when
:signal_receivedA signal arrives at the agent server
:directive_startedA directive begins execution

Filter by event type

Pattern match on the event list to find specific issues.

{:ok, events} = Jido.AgentServer.recent_events(pid)

signals = Enum.filter(events, &(&1.type == :signal_received))
directives = Enum.filter(events, &(&1.type == :directive_started))

If debug mode is off, recent_events/2 returns {:error, :debug_not_enabled}.

Instance-level debug status

Inspect the full debug configuration for an instance with Jido.Debug.status/1.

Jido.Debug.status(runtime)

This returns a map with the current level and all active overrides.

%{
  level: :on,
  overrides: %{
    telemetry_log_level: :debug,
    telemetry_log_args: :keys_only,
    observe_log_level: :debug,
    observe_debug_events: :minimal
  }
}

The override keys control specific subsystems.

Key:on value:verbose valueControls
telemetry_log_level:debug:traceLogger level for telemetry events
telemetry_log_args:keys_only:fullHow action arguments appear in logs
observe_log_level:debug:debugLogger level for the observer
observe_debug_events:minimal:allWhich debug events are emitted

Named instances expose this as MyApp.Jido.debug_status/0.

Timeout diagnostics

When Jido.AgentServer.await_completion/2 times out, it returns a structured diagnostic map instead of a bare :timeout error.

{:error, {:timeout, diagnostic}} =
  Jido.AgentServer.await_completion(pid, timeout: 5_000)

The diagnostic map contains five keys.

%{
  hint: "Agent is idle but await_completion is blocking",
  server_status: :idle,
  queue_length: 0,
  iteration: nil,
  waited_ms: 5000
}

Interpreting server status

StatusQueueMeaning
:idleEmptyAgent finished processing but its state does not match the await condition
:waitingAnyStrategy is waiting for an external response (LLM call, HTTP request)
:runningNon-emptyAgent is still processing directives

An :idle timeout with an empty queue usually means the agent completed its work but did not set the expected status field. Check state.agent.state.status to see where it ended up.

Querying agent state

Inspect a running agent directly without waiting for completion.

{:ok, state} = Jido.AgentServer.state(pid)

The return value is the full AgentServer.State struct. Access the agent's domain state through state.agent.state.

state.agent.state.count
state.agent.state.status

For a higher-level view, use status/1 which returns a snapshot of the agent's strategy state along with process metadata.

{:ok, agent_status} = Jido.AgentServer.status(pid)
agent_status.snapshot.status

Config-driven debug

Enable debug mode through application config so it activates automatically when the instance starts. This is useful for development environments where you always want verbose output.

Application.put_env(:my_app, MyApp.Jido, debug: true)

Set :verbose for maximum detail.

Application.put_env(:my_app, MyApp.Jido, debug: :verbose)

Jido calls Jido.Debug.maybe_enable_from_config/2 during instance initialization. The config value maps directly: true enables :on level, :verbose enables :verbose level, and any other value (or absence) keeps debug off.

Next steps

Now that you can inspect agent internals, explore related operational topics.