mirror of
https://github.com/block/buzz.git
synced 2026-11-01 06:51:17 +00:00
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>