Files
buzz/crates
Michael Neale 979eff99fd refactor(buzz-agent): express the round budget as a goose Operation
First step of moving buzz's loop decisions into goose's StateMachine, one
at a time (PLANS/BUZZ_OPERATIONS_MIGRATION.md). The remaining decisions
still live in loop_drive.rs; this adds the gate they will move into.

Two things the state-machine shape buys immediately:

- The budget now counts assistant turns since the kickoff message rather
  than loop iterations, so it survives a process restart. The loop-local
  counter did not.
- Precedence between the guards becomes list order rather than control
  flow, which is what makes the remaining conversions safe to do
  incrementally.

Deliberately not goose's MaxTurnsOperation: that appends an assistant
message ('Would you like me to continue?') before yielding. buzz-acp
publishes assistant messages into a channel, so a synthesised one would
be indistinguishable from the agent saying it. buzz signals the exhausted
budget through the max_turn_requests stop reason instead.

Two things found while building it, both now pinned by tests:

- goose's operation helpers (messages_since_kickoff, assistant_turn_count)
  are pub in a *private* module, so they are not reachable from outside
  the crate -- only the names re-exported from state_machine are.
  Reimplemented here against goose's semantics.
- A mid-turn steer is user-visible, so it becomes the new kickoff and
  restarts the budget. That is the behaviour we want, but it is emergent
  rather than chosen, so a test pins it.

StepResult cannot carry a stop reason, so operations record one in a
shared Outcome cell; first writer wins, since the machine stops at the
first operation that applies.

Verified: max_rounds=2 against a real provider returns max_turn_requests
with the gate firing from buzz_agent::ops; handtest.py --all 18/18;
79 unit + 21 integration green.

Co-authored-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
2026-08-11 19:07:07 +10:00
..