Build a Buzz state machine in Kaya
Keep deterministic agent workflow state in Kaya while a model handles each conversational step
An interactive agent does not have to give a language model control over its workflow. In this example Kaya accepts the Buzz turn, computes a transition, and only then lets the model respond inside the resulting state.
The repository example and source-validation instructions below require Kaya repository access; the example project is not included in the standalone archive.
Buzz prompt → input() → nextWorkshopState() → state prompt → chat() → Buzz toolThe deterministic part
machine.kaya contains a pure transition function. A shortened version is:
function nextWorkshopState(state: string, message: string): string {
if message.startsWith(value: "/reset") {
return "frame"
}
if state == "frame" {
if message.startsWith(value: "/explore") {
return "explore"
}
}
return state
}Buzz sends a bare slash command first and may append channel context after it.
startsWith() recognizes that wire shape without accidentally treating a
command quoted later in the conversation as a new transition.
The full machine accepts only these transitions:
| Current state | Command | Next state |
|---|---|---|
frame | /explore | explore |
explore | /shortlist | shortlist |
shortlist | /commit | committed |
| any | /reset | frame |
| any | /status or ordinary text | unchanged |
The generative part
The main loop stores state once per ACP session:
import nextWorkshopState from "./machine"
let state = "frame"
while true {
let message = input()
let previousState = state
state = nextWorkshopState(state: state, message: message)
[system] `Previous state: {{previousState}}
Current state: {{state}}`
// Add state-specific instructions here.
chat(maxToolSteps: 20)
}That state variable belongs to the kayad session. A different Buzz
conversation creates a different ACP session and therefore a separate machine.
The model can explain or work within a state, but it cannot make an invalid
transition true.
Why this split is useful
- transitions are testable without a model or network
- prompts can change without changing the workflow contract
- a model failure cannot silently advance the process
- Buzz remains the conversation and identity layer
- kayad remains the execution and persistence layer
The repository fixture imports the real example's machine.kaya and supplies
valid, invalid, status and reset commands in Buzz's wrapped shape. Its recorded
native outcome is checked with the admitted corpus. From a repository checkout:
bun run ci:kaya -- rust-corpus-parityThis command runs the complete native corpus comparison. Bun is a repository validation dependency; the deterministic transition function itself needs no model or network. The interactive loop additionally needs the adapter, Buzz services and model configuration from Setup.
This pattern scales beyond workshops. Replace the states with intake, research, review, approval, and publish; replace slash commands with whatever explicit events your program should accept. Keep irreversible actions behind a state transition that Kaya—not prose generation—validates.