Inspection And Preflight

Copy Markdown View Source

Use Jidoka.inspect/2, Jidoka.preflight/3, and Jidoka.project/1 before spending tokens. They show what the agent compiled to, what prompt would be sent, and what data shape your UI or tests can consume.

Use This When

  • an agent looks correct but behaves unexpectedly at runtime;
  • comparing a DSL-authored spec with an imported one;
  • writing golden tests against compact, deterministic projections.

Prerequisites

  • A working Jidoka agent module (see Getting Started).
  • Familiarity with the operation contract from Tools And Operations.
  • No provider keys are required; these calls do not contact an LLM.
mix deps.get
mix test

Inspect An Agent

Start with inspect/2, then use preflight/3 to assemble the exact prompt the next turn would send.

defmodule MyApp.TimeAgent do
  use Jidoka.Agent

  agent :time_agent do
    model "openai:gpt-4o-mini"
    instructions "Call local_time when asked for the time."
  end

  tools do
    action MyApp.Tools.LocalTime
  end
end

Jidoka.inspect(MyApp.TimeAgent)
#=> %{kind: :agent, module: "MyApp.TimeAgent",
#=>   spec: %{id: "time_agent", operations: [%{name: "local_time", ...}], ...},
#=>   plan: %{...}}

{:ok, preflight} = Jidoka.preflight(MyApp.TimeAgent, "What time is it?")
preflight.prompt.messages
preflight.prompt.tool_definitions
preflight.diagnostics

Nothing was sent over the network. The view shows what Jidoka would send if the turn ran for real.

Concepts

The three functions cover three layers of the data-first runtime.

  1. Jidoka.project/1 is the low-level projector. It turns Jidoka data contracts (Agent.Spec, Turn.Plan, Turn.Result, Effect.Journal, etc.) into JSON-friendly maps. Use it when you need raw compact data for tests, traces, or external rendering.
  2. Jidoka.inspect/2 is the human-facing wrapper. It dispatches on the value's struct and returns a tagged map with a :kind key plus the most useful fields for that kind. Internally it calls project/1 and often adds a timeline view, a status badge, or related context.
  3. Jidoka.preflight/3 is the only one that takes a request. It runs the workflow up to the point where the prompt is ready, but stops before the LLM intent or any operation intent is interpreted. The returned Jidoka.Inspection.Preflight struct shows the normalized agent, plan, request, prompt, events, timeline, and diagnostics.
╭───────────────╮   project    ╭───────────────────╮
│ Jidoka.value  │─────────────▶│ Stable data map   │
╰───────┬───────╯              ╰───────────────────╯
        │
        │ inspect              ╭───────────────────╮
        ╰─────────────────────▶│ %{kind: ..., ...} │
                               ╰───────────────────╯

╭────────────────╮   preflight  ╭──────────────────────╮
│ spec / module  │─────────────▶│ Inspection.Preflight │
│ + request_input│              │ - agent              │
╰────────────────╯              │ - plan               │
                                │ - request            │
                                │ - prompt             │
                                │ - events             │
                                │ - timeline           │
                                │ - diagnostics        │
                                ╰──────────────────────╯

Picking The Right Function

QuestionToolEffect-free?
"What did the DSL/import compile to?"Jidoka.inspect(agent_or_spec)yes
"How does this turn input shape the prompt?"Jidoka.preflight(agent, input)yes
"What is the deterministic projection of this value?"Jidoka.project(value)yes
"What happened during the turn that just ran?"Jidoka.inspect(turn_result)yes
"Replay this snapshot."Jidoka.resume(snapshot, opts)no, runs effects

How To

Step 1: Inspect An Agent Module Or Spec

Jidoka.inspect/2 accepts a DSL agent module, an Agent.Spec, a Turn.Plan, or any other Jidoka value. For modules and specs it returns a combined view with the compiled plan attached.

view = Jidoka.inspect(MyApp.TimeAgent)
view.kind
#=> :agent

view.module
#=> "MyApp.TimeAgent"

view.spec.id
#=> "time_agent"

view.spec.operations
#=> [%{name: "local_time", idempotency: :idempotent, metadata: %{...}}]

view.plan.prompt_strategy
#=> :default

For raw structs (Effect intents, results, sessions, eval runs) inspect dispatches on the struct and returns the matching tagged view.

Step 2: Preflight A Turn

Jidoka.preflight/3 mirrors Jidoka.turn/3's arguments minus the capabilities. It validates the context, calls Memory.Runtime.recall/3 (passing through memory_store: and session_id: like a real turn), and runs Steps.assemble_prompt/1 to build the final messages.

{:ok, preflight} =
  Jidoka.preflight(MyApp.TimeAgent, "What time is it?",
    context: %{tenant_id: "acme"},
    request_id: "req-1"
  )

Enum.map(preflight.prompt.messages, & &1.role)
#=> [:system, :user]

preflight.prompt.tool_definitions
|> Enum.map(& &1.name)
#=> ["local_time"]

preflight.request.input
#=> "What time is it?"

preflight.diagnostics
#=> []

A non-empty diagnostics list flags issues the prompt assembler noticed (missing memory entries, oversized tool descriptions, etc.).

Step 3: Inspect Operation Metadata

When you need to confirm controls do operation ... when: [...] end will match, project the operations:

MyApp.TimeAgent
|> Jidoka.inspect()
|> Map.fetch!(:spec)
|> Map.fetch!(:operations)
|> Enum.map(&Map.take(&1, [:name, :idempotency, :metadata]))

The metadata map is the exact shape control when: clauses match against (:kind, :name, :source, :idempotency, and any free-form keys).

Step 4: Project Turn Results And Journals

After a turn, project the result for assertions and external rendering.

{:ok, result} = Jidoka.turn(MyApp.TimeAgent, "ping")

projected = Jidoka.project(result)
projected.content
#=> "now"

projected.journal.intent_count
#=> 1

Jidoka.inspect(result) returns a richer map with a :timeline and :status already filled in - useful for log output during development.

Step 5: Inspect A Snapshot Or Session

When a turn hibernates (typically because an operation control returned {:interrupt, _}), inspect/2 produces a snapshot view that exposes the cursor, journal, and pending review request.

case Jidoka.turn(MyApp.TimeAgent, "ping") do
  {:hibernate, snapshot} ->
    view = Jidoka.inspect(snapshot)
    view.kind
    #=> :snapshot

    view.cursor
    view.timeline

  {:ok, result} ->
    Jidoka.inspect(result)
end

For sessions, Jidoka.inspect(session) adds replay metadata, snapshot count, pending reviews, and the latest cursor. Sessions are documented in Runtime And Harness; the inspection view is the debugging entry point for them.

Step 6: Use Inspect For Logging

Because every view is a plain map, it serializes cleanly:

require Logger

Logger.info(MyApp.TimeAgent |> Jidoka.inspect() |> :json.encode())

When you only want a subset, lower with Jidoka.project/1 first and Map.take/2 the keys you care about. This is the recommended pattern for production traces; inspect/2 is the developer view.

Common Patterns

  • Preflight before live calls. The first sanity check for a new agent is Jidoka.preflight(agent, "your prompt"). If the messages and tool definitions look right, the live turn is much less likely to surprise you.
  • Snapshot views in failure logs. When a session hibernates, log the result of Jidoka.inspect(snapshot); the timeline plus pending review data is usually enough to diagnose stuck approvals.
  • Compare DSL and imported specs. Jidoka.inspect(dsl_module).spec == Jidoka.inspect(imported_spec).spec is the simplest parity assertion.
  • Strip identifiers in golden tests. Use Jidoka.project/1 and then drop generated id fields before snapshotting.
  • Never IO.inspect/1 raw structs in production. They print implementation detail; the inspection view is designed for callers.

Testing

A typical preflight test asserts both the prompt content and the absence of diagnostics.

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

  test "assembles a prompt for the time agent" do
    {:ok, preflight} = Jidoka.preflight(MyApp.TimeAgent, "What time is it?")

    system_message =
      Enum.find(preflight.prompt.messages, &(&1.role == :system))

    assert system_message.content =~ "Call local_time"
    assert Enum.find(preflight.prompt.tool_definitions, &(&1.name == "local_time"))
    assert preflight.diagnostics == []
  end

  test "inspect/1 returns a tagged map" do
    view = Jidoka.inspect(MyApp.TimeAgent)
    assert view.kind == :agent
    assert view.spec.id == "time_agent"
    assert is_list(view.spec.operations)
  end
end

Snapshot tests should generally compare against Jidoka.project/1 output rather than the full inspect/2 view; projections are smaller and rotate less between releases.

Troubleshooting

SymptomLikely CauseFix
Jidoka.inspect(agent) returns a plain projection without :planTurn.Plan.new/1 failed for the spec.Check the :error key in the view; it carries a normalized error from Jidoka.error_to_map/1.
Jidoka.preflight/3 returns {:error, %Jidoka.Error.Invalid{}}The supplied context: did not match the agent's context schema.Either update the context or relax the schema; preflight runs the same validate_context/2 as a real turn.
Memory does not appear in preflight.promptThe memory_store: option was not threaded through.Pass memory_store: store (and session_id: when needed) to preflight/3.
preflight.diagnostics is non-emptyThe prompt assembler flagged a warning (oversized description, missing schema).Read the diagnostic and adjust the source; warnings here are runtime issues at slightly higher cost.
Turn result view has no :timeline entriesThe turn never made a model or tool call.Confirm the model returned an operation or final answer.

Reference

  • Agent DSL - what the DSL compiles into, mirrored by inspect views.
  • Tools And Operations - reading operation metadata from inspect views to debug control matches.
  • Memory - how memory contributions show up in preflight.
  • Testing And Evals - using projections in deterministic tests and golden files.