The max_iterations builtin in pkg/hooks/builtins/max_iterations.go keeps a map[SessionID]int of model-call counts and exposes a State.ClearSession that the runtime has to remember to call from session_end to avoid leaking entries.
That state is unnecessary. Two cleaner options:
- Hook owns its state: a single
int counter on the builtin (no SessionID map, no lifecycle plumbing). before_llm_call already fires once per loop iteration in the same RunStream, so a per-instance counter is enough.
- Runtime passes the iteration in the hook input: the loop already tracks
iteration in pkg/runtime/loop.go. Surface it on hooks.Input (e.g. Input.Iteration) and the builtin becomes pure: compare iteration >= limit and return a block decision. No state at all.
Either way, State.maxIterations, clearSession, and the session_end coupling can go away.
The
max_iterationsbuiltin inpkg/hooks/builtins/max_iterations.gokeeps amap[SessionID]intof model-call counts and exposes aState.ClearSessionthat the runtime has to remember to call fromsession_endto avoid leaking entries.That state is unnecessary. Two cleaner options:
intcounter on the builtin (no SessionID map, no lifecycle plumbing).before_llm_callalready fires once per loop iteration in the same RunStream, so a per-instance counter is enough.iterationinpkg/runtime/loop.go. Surface it onhooks.Input(e.g.Input.Iteration) and the builtin becomes pure: compareiteration >= limitand return a block decision. No state at all.Either way,
State.maxIterations,clearSession, and the session_end coupling can go away.