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:
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.
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:
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.
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.
Last modified on September 7, 2026