# Inspection And Preflight

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](getting-started.md)).
- Familiarity with the operation contract from
  [Tools And Operations](tools-and-operations.md).
- No provider keys are required; these calls do not contact an LLM.

```bash
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.

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

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

| Question | Tool | Effect-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.

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

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

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

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

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

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

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

| Symptom | Likely Cause | Fix |
| --- | --- | --- |
| `Jidoka.inspect(agent)` returns a plain projection without `:plan` | `Turn.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.prompt` | The `memory_store:` option was not threaded through. | Pass `memory_store: store` (and `session_id:` when needed) to `preflight/3`. |
| `preflight.diagnostics` is non-empty | The 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` entries | The turn never made a model or tool call. | Confirm the model returned an operation or final answer. |

## Reference

- [`Jidoka`](`Jidoka`) - public facade: `Jidoka.inspect/2`,
  `Jidoka.preflight/3`, `Jidoka.project/1`.
- [`Jidoka.Inspection`](`Jidoka.Inspection`) - implementation of
  `inspect/2` and the per-struct dispatchers.
- [`Jidoka.Inspection.Preflight`](`Jidoka.Inspection.Preflight`) - struct
  returned by `Jidoka.preflight/3` with fields `agent`, `plan`, `request`,
  `prompt`, `events`, `timeline`, `diagnostics`.
- [`Jidoka.Projection`](`Jidoka.Projection`) - the data-facing companion
  used by `Jidoka.project/1`.
- [`Jidoka.Agent.Spec`](`Jidoka.Agent.Spec`) - the spec inspect views
  path.

## Related Guides

- [Agent DSL](agent-dsl.md) - what the DSL compiles into, mirrored by
  inspect views.
- [Tools And Operations](tools-and-operations.md) - reading operation
  metadata from inspect views to debug control matches.
- [Memory](memory.md) - how memory contributions show up in preflight.
- [Testing And Evals](testing-and-evals.md) - using projections in
  deterministic tests and golden files.
