symfonic.agent.cutover.rounds¶
rounds ¶
One translation: the legacy loop budget expressed as the kernel's.
Split out of :mod:symfonic.agent.cutover.delegate, where it sat beside a
class it shares nothing with. The delegate serves a turn; this is arithmetic
over two constants that describe the legacy graph's shape, and the two change
for unrelated reasons — a new preset node moves the numbers here and touches
nothing there.
rounds_for_recursion_limit ¶
Translate the legacy loop budget into the kernel's round budget.
The legacy body bounds the ReAct loop with LangGraph's recursion_limit,
which counts graph super-steps; the kernel bounds it with
PlanLimits.max_model_rounds, which counts model rounds. Two steps per
round — one model node, one tool node — is the conversion, and the graph
spends _ENTRY_STEPS before the first of them, so the entry cost comes
off the budget before the division rather than after.
Both corrections point the same way: never more rounds than the plain
react_loop graph. A conversion that only divided would give
50 -> 25 where that graph reaches 24, and the extra round is a billed
model call the path being replaced would not have made. Integer division is
the floor for the remainder, for the same reason.
The invariant is stated against react_loop and not "legacy" in general
because the two constants above are read off that one wiring. presets
adds a super-step per round for elicitation, for the experimental interrupt
and for the precondition gate, so those graphs reach fewer rounds than
(recursion_limit - 1) // 2 and this conversion would over-budget them.
None of them can reach here: ask_user_enabled,
experimental_interrupt and procedural_enforce_preconditions are all
named refusals in :mod:~symfonic.agent.cutover.envelope, and a non-ReAct
graph_preset is refused there too. If one of those switches flips, this
conversion has to grow the extra step with it.
Leaving the constant in place instead would bound every migrated turn at ten rounds while the legacy body allowed twenty-four, which truncates a long tool loop and returns the empty answer as if it were the real one.