> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reasonblocks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# StepRecord, TraceState, and StepLogEntry

> Reference for the per-step dataclasses ReasonBlocks records during a run — StepRecord and TraceState in reasonblocks.types, and StepLogEntry on the middleware.

ReasonBlocks records two parallel views of an agent run: an internal `TraceState` populated with `StepRecord` dataclasses (used by the FSM and retrieval pipeline), and a user-facing `step_log` of `StepLogEntry` objects exposed on the middleware. Most consumers read the `step_log`; `TraceState` and `StepRecord` are documented here for completeness and for callers building custom integrations.

```python theme={null}
from reasonblocks.types import StepRecord, TraceState, FSMState
from reasonblocks.middleware import StepLogEntry
```

## StepLogEntry

`StepLogEntry` is the type you actually iterate after a run. The middleware appends one entry per `wrap_model_call` (i.e. per LLM call). Access via `middleware.step_log`.

<ResponseField name="step" type="int">
  Zero-based step index. Monotonically increasing across the run.
</ResponseField>

<ResponseField name="timestamp" type="float">
  Wall-clock time (`time.time()`) when the entry was created in `before_model`.
</ResponseField>

<ResponseField name="difficulty" type="float | None">
  Difficulty score for this step, or `None` for the very first call (no thought to score yet).
</ResponseField>

<ResponseField name="fsm_state" type="str">
  String value of the FSM state (e.g. `"INIT"`, `"FAST"`, `"NORMAL"`, `"SLOW"`, `"SKIP"`).
</ResponseField>

<ResponseField name="model_id" type="str">
  Resolved model identifier used for this call after FSM-based routing. Empty string when no override applied.
</ResponseField>

<ResponseField name="injections" type="list[str]">
  Short previews (≤150 chars) of each injection appended to the system prompt for this call.
</ResponseField>

<ResponseField name="injection_sources" type="list[str]">
  Source class names of the injections (e.g. `"E1Injection"`, `"MonitorSteeringInjection"`).
</ResponseField>

<ResponseField name="intervention_texts" type="list[str]">
  Full, untruncated injection text for each injection, in the same order as `injection_sources`.
</ResponseField>

<ResponseField name="monitors_fired" type="list[str]">
  Specific monitor names that fired on this step (e.g. `"loop_detector"`, `"hedge"`).
</ResponseField>

<ResponseField name="failure_type" type="str | None">
  Categorical failure label from the server-side monitor evaluation, when present.
</ResponseField>

<ResponseField name="skipped_reason" type="str | None">
  Set when retrieval was skipped — e.g. `"FSM=FAST, e-traces skipped"`. `None` otherwise.
</ResponseField>

<ResponseField name="thought_preview" type="str">
  First 120 characters of the agent's thought for this step, or `"(first call)"` when there is no thought yet.
</ResponseField>

<ResponseField name="tool_calls" type="list[str]">
  Names of tool calls produced by the LLM for this step.
</ResponseField>

<ResponseField name="tokens" type="int">
  Total tokens for this call (extracted from the AIMessage `usage_metadata`).
</ResponseField>

<ResponseField name="latency_ms" type="float">
  Milliseconds elapsed since the previous entry's timestamp. `0.0` for the first entry.
</ResponseField>

### `as_dict()`

`StepLogEntry.as_dict()` returns a serialisable dict with every field above (rounded as appropriate). Optional fields like `difficulty`, `model_id`, `skipped_reason`, and `failure_type` are only included when they are populated.

```python theme={null}
import json

with rb.middleware(...) as mw:
    result = agent.invoke({"messages": [...]})

print(json.dumps([e.as_dict() for e in mw.step_log], indent=2))
```

***

## StepRecord

`StepRecord` is the internal per-step dataclass written into `TraceState.steps`. The FSM and retrieval pipeline read from it; callers typically use `StepLogEntry` instead.

<ResponseField name="step_index" type="int" required>
  Zero-based position of this step in the trace.
</ResponseField>

<ResponseField name="thought" type="str" required>
  The agent's reasoning text for this step (extracted from the latest `AIMessage`).
</ResponseField>

<ResponseField name="action" type="str | None">
  Tool name selected by the agent for this step, or `None` if no tool was called.
</ResponseField>

<ResponseField name="action_input" type="str | None">
  String form of the tool arguments, or `None` when `action` is `None`.
</ResponseField>

<ResponseField name="observation" type="str | None">
  Most recent tool message content (truncated to 400 chars), or `None` if no observation.
</ResponseField>

<ResponseField name="difficulty" type="float">
  Difficulty score for this step in `[0.0, 1.0]`. Defaults to `0.0` until scored.
</ResponseField>

<ResponseField name="state" type="FSMState">
  FSM state recorded *after* scoring this step. Defaults to `FSMState.INIT`.
</ResponseField>

<ResponseField name="tokens_used" type="int">
  Tokens consumed by the LLM call that produced this step. Updated post-call from `usage_metadata`.
</ResponseField>

***

## TraceState

`TraceState` is the run-level accumulator owned by `TraceStateManager`. The middleware reads from it during retrieval; consumers do not normally touch it directly.

<ResponseField name="trace_id" type="str" required>
  Unique identifier for this run.
</ResponseField>

<ResponseField name="steps" type="list[StepRecord]">
  Ordered list of every `StepRecord` recorded so far.
</ResponseField>

<ResponseField name="current_state" type="FSMState">
  The FSM state at the time of inspection. Starts as `FSMState.INIT`.
</ResponseField>

<ResponseField name="difficulty_history" type="list[float]">
  Flat list of difficulty scores in step order. Equivalent to `[s.difficulty for s in steps]`.
</ResponseField>

<ResponseField name="total_tokens" type="int">
  Sum of `StepRecord.tokens_used` across all steps.
</ResponseField>

<ResponseField name="token_budget" type="int | None">
  The `token_budget` configured on the parent `ReasonBlocks` instance, or `None` when no budget was set.
</ResponseField>

***

## Reading step\_log after a run

<CodeGroup>
  ```python Inspect every step theme={null}
  from reasonblocks import ReasonBlocks

  rb = ReasonBlocks(api_key="rb_live_...")

  with rb.middleware(run_id="run-1", agent_name="reviewer", task="review PR #42") as mw:
      result = agent.invoke({"messages": [("user", "Review the changes in PR #42")]})

  for entry in mw.step_log:
      print(
          f"  [{entry.step}] difficulty={entry.difficulty}  "
          f"state={entry.fsm_state}  tools={entry.tool_calls}"
      )
  ```

  ```python Find slow / skip steps theme={null}
  slow_steps = [
      e for e in mw.step_log
      if e.fsm_state in ("SLOW", "SKIP")
  ]
  print(f"{len(slow_steps)} slow/skip out of {len(mw.step_log)} total")
  ```

  ```python Average difficulty theme={null}
  scored = [e for e in mw.step_log if e.difficulty is not None]
  if scored:
      avg = sum(e.difficulty for e in scored) / len(scored)
      print(f"Average difficulty: {avg:.3f}")
  ```
</CodeGroup>

<Tip>
  `entry.as_dict()` returns a serialisable view useful for logging or exporting run data without writing your own field projection.
</Tip>
