# Drive an agent loop

URL: https://docs.hanzo.ai/docs/guides/cookbook/loop

> An agent's control flow as Decision Programs: preflight picks the model, Zen does the work, a completion check decides whether to stop. Prompts only where text is the output.

**The loop is a graph; only one node generates.** An agent asks itself the same
bounded questions every turn — which model, which tool, is it done. Asked of a
generative model, each costs a completion and a parser. Asked of Kai, each is
one typed decision.

```text
task ─▶ preflight (Kai) ─▶ generate (Zen, model by tier) ─▶ complete? (Kai) ─┬─▶ stop
                                     ▲                                        │
                                     └──────────────── not done ──────────────┘
```

## Two programs

Trimmed from `agent.preflight@1` and `agent.complete@1`, two of the agent
programs in [Hanzo Decision](https://docs.hanzo.ai/docs/models/kai#kai-inside-enso):

```bash
cat > preflight.json <<'EOF'
{
  "model": "kai",
  "questions": {
    "model_tier": {"type": "choice", "instructions": "Which model tier should handle the task?",
                   "criteria": {"cheap": "a small fast model is enough: lookup, formatting, short answers",
                                "zen-coder": "a coding model: writing, editing or reviewing code",
                                "frontier": "the strongest model: hard reasoning, long multi-step plans, ambiguity"}},
    "needs_user": {"type": "noul", "instructions": "Does the agent need more information or a decision from the user before it can start?"}
  },
  "thresholds": {"model_tier": 0.5, "needs_user": 0.5}
}
EOF
cat > complete.json <<'EOF'
{
  "model": "kai",
  "questions": {
    "done":     {"type": "noul", "instructions": "Has the agent finished everything the user asked for?"},
    "verified": {"type": "noul", "instructions": "Is the result backed by evidence in the transcript, such as passing tests or command output?"}
  },
  "thresholds": {"done": 0.6, "verified": 0.6}
}
EOF

decide() {   # decide <program> <state json>
  jq -n --slurpfile p "$1" --argjson s "$2" '{model: $p[0].model, state: $s, questions: $p[0].questions}' \
  | curl -s "$DECISIONS/v1/decisions" -H "Authorization: Bearer $HANZO_API_KEY" \
      -H "Content-Type: application/json" -d @-
}
```

## The loop

```bash
task='Fix the failing test in config_test.go: undefined parseConfig.'

pre=$(decide preflight.json "$(jq -n --arg t "$task" '{task: $t}')")
tier=$(jq -r --slurpfile p preflight.json \
  '.answers.model_tier | if .answer_confidence >= $p[0].thresholds.model_tier then .choice else "unsure" end' <<<"$pre")
case $tier in
  cheap)     model=enso-flash ;;
  zen-coder) model=zen6 ;;
  frontier)  model=enso-ultra ;;
  *)         model=auto ;;          # Kai is unsure: let Enso route
esac

transcript=""
for turn in 1 2 3; do
  out=$(curl -s https://api.hanzo.ai/v1/chat/completions \
    -H "Authorization: Bearer $HANZO_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq -n --arg m "$model" --arg t "$task" --arg x "$transcript" \
          '{model: $m, messages: [{role: "user", content: ($t + "\n\n" + $x)}]}')" \
    | jq -r '.choices[0].message.content')
  transcript+="$out"$'\n'
  # ... run what it proposed, append the command output to $transcript ...
  fin=$(decide complete.json "$(jq -n --arg t "$task" --arg x "$transcript" '{task: $t, transcript: $x}')")
  jq -e --slurpfile p complete.json \
    '.answers.done.noul >= $p[0].thresholds.done and .answers.verified.noul >= $p[0].thresholds.verified' \
    <<<"$fin" >/dev/null && break
done
```

Stopping needs `done` **and** `verified`: finished and shown, not finished and
claimed.

## What Kai answered

The decision half, run against `kai` (kai-1, calibration `cal_e23c27a1f768bff7`,
on the v0.3.14 runtime on CPU):

| State                                                                                 | Answer                                        |
| ------------------------------------------------------------------------------------- | --------------------------------------------- |
| "Rename the variable usr to user in main.go."                                         | `zen-coder` at 0.5947 → `zen6`                |
| "Fix the failing test in config\_test.go: undefined parseConfig."                     | `frontier` at 0.3662 — under 0.5, so `auto`   |
| "Design a migration plan from our monolith to services, with rollback at every step." | `frontier` at 0.3906 — under 0.5, so `auto`   |
| "What is 2 + 2?"                                                                      | `frontier` at 0.4529 — under 0.5, so `auto`   |
| transcript "I fixed it. The test should pass now."                                    | `done` 0.3360, `verified` 0.3693 — keep going |
| transcript with `go test ./...` → `ok`                                                | `done` 0.3650, `verified` 0.4800 — keep going |

The rename routes; the other three fall under the threshold to `auto`, and
`What is 2 + 2?` among them ranks `frontier` first, the wrong tier. A claim
without evidence keeps the loop going, and so does a passing `go test`, under
the 0.6 bar. That is why these programs run in `shadow` and `advisory`,
where the deterministic policy decides and Kai's answers are recorded against
outcomes: a program moves up only when its measured accuracy clears its
threshold.

**What this buys.** The control flow is data: versioned programs with
thresholds, each answer carrying the checkpoint and weights hash that produced
it. Swap the model under a program and the loop does not change; change a
threshold and the change is a diff.
