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

# Engine and router

> Tier 2 of Fleet Reasoner: the whole reasoning cycle composed as one dspy.Module — Selection, Routing, Invocation, Refine-validated Expansion, Critic gate, and recursion.

Tier 2 is the **whole reasoning cycle composed as one `dspy.Module`**. The current React app drives the loop from `App.tsx` click handlers; here `forward()` is the loop:

```text theme={null}
Selection → Routing → Invocation → Refine-validated Expansion → Critic gate → (Recursion) → Synthesis
```

Every LM call inside `forward()` is a sub-module, so the entire engine is compilable and evaluable as a unit. With `dspy.Flex`, even the loop structure itself can be optimized by GEPA.

## Router

`qlaw/router.py` picks the lens for a given node and decides whether to stop the recursion. Both are DSPy modules; a deterministic heuristic covers the case where no router is configured.

```python theme={null}
import dspy
from qlaw.signatures import LensRouter as LensRouterSignature
from qlaw.signatures import Termination

class LensRouter(dspy.Module):
    """Auto-select the lens from node type + context (model: deepseek-v4-flash).

    NOTE: the signature is imported ALIASED — the module class below shadows the
    bare name, so `dspy.Predict(LensRouter)` would pass the module class itself
    as a signature and break at compile time."""
    def __init__(self):
        super().__init__()
        self.predict = dspy.Predict(LensRouterSignature)

    def forward(self, node, **kwargs):
        return self.predict(node=node)  # Prediction: lens

def default_lens_for(node_type) -> str:
    """Deterministic fallback."""
    from qlaw.graph import NodeType
    return {
        NodeType.PROBLEM: "decompose", NodeType.COMPONENT: "decompose",
        NodeType.QUESTION: "ontology", NodeType.PLAN: "trajectories",
        NodeType.TRAJECTORY: "critique", NodeType.DATA: "enrich",
    }.get(node_type, "explain")

class Terminator(dspy.Module):
    """Decide whether to stop recursion at a node (model: deepseek-v4-flash)."""
    def __init__(self):
        super().__init__()
        self.predict = dspy.Predict(Termination)

    def forward(self, node, **kwargs):
        return self.predict(node=node)  # Prediction: stop (bool)
```

## Reasoning engine

`qlaw/engine.py` composes `ReasoningEngine` + `ReasoningLoop` + `SeedFlow` — the whole cycle as one program.

* `SeedFlow` — the composition `SemanticInterpreter → Decomposer` that produces the initial `ROOT` node plus its first decomposition layer.
* `ReasoningEngine` — one step of the cycle: Selection → Routing → Invocation → Expansion → Critic gate. `lens_batch()` maps every lens output to a `NodeBatch`.
* `ReasoningLoop` — recursion with `Terminator`. `max_depth` is a **constructor argument**, not a request field.

The invariant across all three: **the LM never mutates the graph**. Every lens emits a `NodeBatch`; `GraphState.apply()` does the wiring deterministically.

### One reasoning step

`ReasoningEngine.forward(graph, active_node_id, action=None)` returns a `Prediction` with:

| Field     | Purpose                                                                                                    |
| --------- | ---------------------------------------------------------------------------------------------------------- |
| `graph`   | The expanded `GraphState` (post-`apply()`).                                                                |
| `lens`    | Which lens ran.                                                                                            |
| `outputs` | The lens `Prediction`. Serialize with `Prediction.toDict()` — DSPy 3.3.0 has no `Prediction.model_dump()`. |

## Recursion depth is a constructor argument

`ReasoningLoop.max_depth` and `ReasoningEngine.max_depth` are set at construction time. They are not accepted on the `/engine` request body. This is one of the DSPy 3.3.0 gotchas — see [Gotchas](/fleet-reasoner/gotchas).

## Why the engine is optimizable

Because the whole loop is a single `dspy.Module`:

* `dspy.MIPROv2` or `dspy.BootstrapFewShot` can compile the engine from full-session trajectories in `qlaw/datasets.py::engine_trainset()`.
* `dspy.Evaluate` can score full sessions with any subset of the six metrics in `qlaw/metrics.py`.
* `dspy.Flex` (experimental) starts from a signature, not a composed module. A future signature-first rewrite of the loop can be optimizer-authored inside a `CodeInterpreter` sandbox.
