Skip to main content
An agent that re-runs the same failing command forty times pays for every attempt, and nobody reads every transcript to catch it. This recipe adds a supervisor to your loop. Every few steps, it sends the transcript to ScaleDown Classify through the OpenRouter Decisions API and asks three questions. Is the run making progress, stuck, or looping? Is the agent repeating itself? How close is it to done? Fixed thresholds on the answers decide whether the loop continues, gets a nudge, or stops. Classify answers questions. It doesn’t write text. For each question you get back a typed answer instead of prose: a choice picks one of your labels and gives a probability for each, a noul gives the probability that the answer is yes, and a score places the answer on a scale you define. The supervisor compares those numbers directly, with nothing to parse. Goal: Write a supervise function that reads the transcript and returns 'continue', 'nudge', or 'halt', and call it from your loop every CHECK_EVERY steps. Outcome: A looping run stops at the first check that catches it. A blocked run gets a message telling it to change course. A healthy run carries on untouched, even when a check itself fails. Every check logs its probabilities and cost, so you can see why it acted.
Prerequisites:
  • Your existing TypeScript agent loop runs on Node.js 20+ or Bun as an ES module, with @types/node or @types/bun installed, and can turn its message history into a string.
  • You’ve configured OPENROUTER_API_KEY and installed zod 4.
  • Keep the Decisions API reference open for the request format.

1. Define the questions

Write the questions once, as a constant. For the choice, pick labels that can’t overlap and describe each one in criteria, since the model classifies against that text. Use a noul for a separate yes-or-no check, and a score when the answer is a matter of degree. A score’s criteria is an ordered list, and the answer is a position on it, from 0 to criteria.length - 1.

2. Call the Decisions API

judgeRun sends the transcript and the questions, and checks the answers against a schema. If the response is an error or doesn’t match the schema, judgeRun throws. Step 3 decides what that means for the loop.
Captured output. The response for the looping transcript from the worked example below: 14,244 characters in which the agent runs bun test packages/billing/invoice.test.ts sixty times and fails the same way each time.
Probabilities are rounded to four places. score is the weighted average of the positions, here 0.0929 on a scale of 0 to 2, so the agent is nowhere near done. Each question reads the whole transcript, so usage.input_tokens counts it once per question: asking only is_repeating used 5161 input tokens.
Each judgeRun call is one paid request billed to your OpenRouter account at the scaledown/classify-1 rate. For long runs, raise CHECK_EVERY, and drop completion if you don’t act on it.

3. Turn the answers into an action

Decide on the probabilities, not just the label: choice says looping whether the model is 41% sure or 95% sure. Stopping the run takes two signals agreeing, run_state and is_repeating, which cuts false stops. One strong signal on its own only earns a nudge. If a check itself fails, the supervisor lets the run continue: supervise logs the error and returns 'continue', so a Decisions outage or a schema change never kills a healthy run. A tool-call gate does the opposite and blocks on error, because a wrongly approved tool call can do harm. Here, a missed check costs at most a few more steps of a run that may already be wasted.
The API returns probabilities, not reasons, so the log line is your record of why the supervisor acted. Keep it with the run.

4. Hook it into the loop

Check every CHECK_EVERY steps, not every step. renderTranscript is your own function that turns the message history, including tool calls and their results, into the text Classify should judge. On halt, stop the loop and report the last log line. On nudge, add a user message telling the agent what the supervisor saw.

Worked example

The three transcripts below are made up, but they’re the ones the captured output came from, so you can run them yourself. All three start from the same task. The looping transcript runs one failing test command sixty times. The stuck transcript tries twelve different ways to read a fixture file owned by root, then gives up. The progressing transcript reads the test, finds the import-time Date.now(), injects a clock, runs the tests, and opens a PR. Save steps 1 to 3 and these two blocks as one file and run it with bun run supervise.ts.
Captured output. One supervise log line per transcript, probabilities rounded. We ran each transcript three times; the table has the ranges.
The stuck transcript scored 0.4698 for stuck, with the rest split between progressing (0.24) and looping (0.29). An earlier capture of the same transcript scored 0.617, and blocked transcripts in a second test scored around 0.53, so the score for a blocked run is rarely dominant and it moves between model versions. That’s why NUDGE_STUCK is 0.4: set it higher and a blocked agent keeps going until it gives up on its own. Treat it as a starting value with little margin, not a safe default. A blocked run that scores 0.38 gets no nudge, so log real runs, see where your blocked ones land, and set the threshold below them. Stopping needs both signals because an agent making progress through a run of similar calls can score looping well above 0.5 while is_repeating stays low. That combination gets a nudge, never a stop. If your agent normally makes long runs of similar calls, say so in the progressing description. The looping transcript’s is_repeating landed between 0.83 and 0.90, so HALT_REPEATING at 0.8 has little margin either; a loop that scores under it still gets a nudge from looping.

Check your work

  • Response shape. answers.run_state.type is 'choice' with a probability for each of the three labels, answers.is_repeating.noul is between 0 and 1, and answers.completion.score is between 0 and 2.
  • Looping run. A transcript that repeats one failing call scores looping and is_repeating at 0.8 or higher, and the loop halts. Ours did in 3 of 3 runs.
  • Healthy run. A transcript with varied actions and new results returns continue. Ours did in 3 of 3.
  • Blocked run. A transcript that tries several things, fails each time, and gives up returns nudge. Ours did in 3 of 3.
  • Fail open. Break the API key or the URL. supervise logs the error, returns continue, and the loop keeps going.
  • Stable. Run the same transcript several times and count how often the action changes. In our nine checks it never did; the stuck and progressing transcripts returned identical numbers every time, and the looping transcript’s is_repeating moved between 0.83 and 0.90.

Variations

Change the questions and the same request classifies other text: the outcome of a sales call (won, lost, escalation, or churn risk), the type and risk of a contract, which team should handle a support ticket, or a risk rating for a full privacy policy, using a score question. Classify also reads images and documents, so it can tell an invoice from a receipt on a scanned page or check a screenshot for an error. Send the file as a base64 string in state.document, with its type in state.document_mime_type.

Next steps