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 tool

The 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 stateCommandNext state
frame/exploreexplore
explore/shortlistshortlist
shortlist/commitcommitted
any/resetframe
any/status or ordinary textunchanged

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-parity

This 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.