Skip to content

InFieldAgentEvals.kt

src/jvmMain/kotlin/afield/ai/agent/InFieldAgentEvals.kt · 667 lines

package afield.ai.agent

import afield.ai.tools.aiToolMapper
import kotlinx.serialization.Serializable

A small eval suite for Field's own agent: seventeen tasks (five graded by pixels, three made of many similar calls that a tool program does in one, two that are several turns of one conversation), each a goal, the setup it starts from, checks that decide it, and a scripted solution.

The scripted solutions are real Kotlin for real Field. Replayed through ScriptedLlmBridge against a live Field (agent-eval --mode scripted) they test the tools, the execution path and the checkers end to end with no model, and a scripted pass is the floor any model run should reach. Replayed in jvmTest against a fake host, they test the harness logic. A live run hands the same goals and checks to the configured provider and ignores the solutions.

Box names are fixed, so run each task in a fresh document (AgentEvalRunner does).

object InFieldAgentEvals

The eval tasks, their scripted solutions, and the helpers that build them.

object InFieldAgentEvals {

data class EvalTask(val id: String, val request: AgentTaskRequest, val solution: String)

fun call(tool: String, vararg args: Pair<String, Any?>) = tool to args.toMap()

A step for steps: a tool call.

fun steps(vararg items: Any): String

Build a script / setup JSON from calls and (optionally) a final answer.

    fun steps(vararg items: Any): String = aiToolMapper.writeValueAsString(items.map { item ->
        when (item) {
            is Pair<*, *> -> mapOf("tool" to item.first, "arguments" to item.second)
            is String -> mapOf("final" to item)
            else -> throw IllegalArgumentException("step must be call(...) or a final string")
        }
    })

    private fun check(spec: String) = AgentCheck.parse(spec)

val PRIMES_CODE = "val primes = listOf(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\nprintln(primes)"

val BROKEN_CODE = "val count: Int = \"three\"\nprintln(\"count is \$count\")"

val FIXED_CODE = "val count: Int = 3\nprintln(\"count is \$count\")"

val SOURCE_CODE = "prop<Int>(secret) = 4217"

val READER_CODE = "println(secret)"

val TABLE_CODE

    val TABLE_CODE = "display(\"main\", \"<table><tr><td>1</td><td>1</td></tr><tr><td>2</td><td>4</td></tr><tr><td>3</td><td>9</td></tr></table>\")"

val TICKER_CODE = "var n = 0\nloop { n++ }"

val DIVIDER_CODE = "val xs = listOf<Int>()\nprintln(\"first is \" + xs.first())"

val DIVIDER_FIXED

    val DIVIDER_FIXED = "val xs = listOf<Int>()\nprintln(xs.firstOrNull()?.let { \"first is \$it\" } ?: \"empty\")"

visual tasks: the defect, or the answer, is in the pixels

val SWATCH_CODE

A swatch whose colour is decided by the cascade: a stylesheet !important red, a more specific green, an inline purple — and then a script that sets an inline !important blue, which beats them all. Reading the source invites "red"; the picture is blue.

    val SWATCH_CODE = "display(\"main\", \"\"\"<style>#sw { background: #d62728 !important; } .x .y #sw { background: #2ca02c; }</style>" +
        "<div class=\"x\"><div class=\"y\"><div id=\"sw\" style=\"background: #9467bd; width: 240px; height: 120px\"></div></div></div>" +
        "<script>document.getElementById('sw').style.setProperty('background', '#1f77b4', 'important')</script>\"\"\")"

val ANSWER_CODE = "println(\"blue\")"

val SUN_CODE

    val SUN_CODE = "display(\"main\", \"\"\"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"300\" height=\"200\">" +
        "<rect width=\"300\" height=\"200\" fill=\"#0b1a4a\"/><circle cx=\"150\" cy=\"100\" r=\"50\" fill=\"#ffd700\"/></svg>\"\"\")"

val BARS_CODE

Percent heights inside a parent of auto height compute to auto: five empty bars, zero pixels tall.

    val BARS_CODE = "val values = listOf(20, 40, 60, 80, 100)\n" +
        "val bars = values.joinToString(\"\") { v -> \"<div style='display:inline-block; vertical-align:bottom; width:20px; margin:3px; background:#1f77b4; height:\${v}%'></div>\" }\n" +
        "display(\"main\", \"<div style='background:white; padding:8px'>\$bars</div>\")"

val BARS_FIXED = BARS_CODE.replace("height:\${v}%", "height:\${v}px")

val THEME_CODE

Look → revise → look. Chart's container has class bars, and another box, Theme, displays a stylesheet that makes .bars flex-direction: row-reverse — output panels share one page, so a style in one panel reaches every other. The bars therefore appear in the reverse of the list's order, and sorting the list ascending, the obvious first move, draws them tallest-first. Chart's own code does not say so; the picture does. The stylesheet lives in Theme for that reason: in Chart's own code, reading it would stand in for looking at the output. The scripted solution makes the mistake first, sees it, and fixes it.

    val THEME_CODE = "display(\"main\", \"<style>.bars { flex-direction: row-reverse; justify-content: flex-end; }</style>" +
        "<div style='font: 12px monospace'>theme loaded</div>\")"
    private fun chartCode(values: String, tallestRed: Boolean): String =
        "val values = $values\n" +
            "val top = values.maxOrNull()\n" +
            "val bars = values.joinToString(\"\") { v -> \"<div style='width:24px; margin:3px; height:\${v}px; background:\" + " +
            (if (tallestRed) "(if (v == top) \"#d62728\" else \"#1f77b4\")" else "\"#1f77b4\"") + " + \"'></div>\" }\n" +
            "display(\"main\", \"<div class='bars' style='display:flex; align-items:flex-end; " +
            "background:white; padding:8px; height:110px'>\$bars</div>\")"

Look → revise → look, with a trap no source states: the output panel's width. Five 28 px bars fit; ten wrap onto a second row, which the picture shows and the code cannot (the panel is as wide as the box, and a new box's frame is chosen by Field). bars-sorted-red turned out not to force a revision: the live model read the stylesheet in the other box through ListContents and pre-empted it in all four runs.

    private fun rowCode(values: String, nowrap: Boolean): String =
        "val values = $values\n" +
            "val bars = values.joinToString(\"\") { v -> \"<div style='display:inline-block; vertical-align:bottom; width:28px; " +
            "margin:3px; background:#1f77b4; height:\${v}px'></div>\" }\n" +
            "display(\"main\", \"<div style='background:white; padding:8px" + (if (nowrap) "; white-space:nowrap" else "") + "'>\$bars</div>\")"

val ROW_CODE = rowCode("listOf(20, 30, 40, 50, 60)", nowrap = false)

val ROW_FIRST_TRY = rowCode("listOf(20, 30, 40, 50, 60, 70, 80, 90, 100, 110)", nowrap = false)

val ROW_FIXED = rowCode("listOf(20, 30, 40, 50, 60, 70, 80, 90, 100, 110)", nowrap = true)

many-call tasks: the work is the same call over many boxes or values

These exist to measure tool programs (RunToolProgramRequest): each is a dozen similar calls done directly, or one program. Their scripted solutions are programs; programs=off is the other arm.

val CELL_BAD = "val xs = listOf<Int>()\nprintln(\"cell \" + xs.first())"

fun cellCode(n: Int) = "println(\"cell $n ok\")"

val CELL_NAMES = (1..10).map { "Cell%02d".format(it) }

val FAILING_CELLS = listOf("Cell03", "Cell07", "Cell09")

val REPORT_CODE = "println(\"Cell03, Cell07, Cell09\")"

fun sqCode(k: Int) = "val k = $k\nprintln(\"\${k}x\${k * k}\".hashCode().mod(1000))"

A value nobody computes in their head: the hash of a string built from k, so the sweep has to run.

val SWEEP_OUTPUTS = listOf(858, 822, 788, 25, 846, 669, 494, 342)

What sqCode prints for k = 1..8, as Kotlin computes it.

val RESULTS_CODE = "println(\"${SWEEP_OUTPUTS.joinToString(" ")}\")"

val TODO_NOTES = setOf(3, 4, 8, 10, 12)

fun noteCode(n: Int)

    fun noteCode(n: Int) = "// note $n" + (if (n in TODO_NOTES) "\n// TODO: tidy this up" else "") + "\nprintln($n)"

fun noteName(n: Int) = "Note%02d".format(n)

multi-turn tasks: one conversation, several requests

These exist to measure where instructions go (the context pipeline): until 2026-10-02 every turn's full instruction block stayed in the conversation, so a four-turn task resent three stale copies.

val COUNTER_3 = "println((1..3).toList())"

val COUNTER_5 = "println((1..5).toList())"

val COUNTER_SQUARES = "println((1..5).map { it * it })"

val TOTAL_CODE = "println((1..5).sumOf { it * it })"

    private fun powersTable(n: Int, cubes: Boolean) =
        "display(\"main\", \"<table>\" + (1..$n).joinToString(\"\") { i -> \"<tr><td>\$i</td><td>\${i * i}</td>" +
            (if (cubes) "<td>\${i * i * i}</td>" else "") + "</tr>\" } + \"</table>\")"

val POWERS_3 = powersTable(3, cubes = false)

val POWERS_5 = powersTable(5, cubes = false)

val POWERS_5_CUBES = powersTable(5, cubes = true)

    private fun program(code: String, purpose: String) =
        call("RunToolProgramRequest", "code" to code.trimIndent(), "purpose" to purpose)

val CHART_CODE = chartCode("listOf(55, 25, 90, 40, 70)", tallestRed = false)

val CHART_FIRST_TRY = chartCode("listOf(55, 25, 90, 40, 70).sorted()", tallestRed = true)

The first try: sorted ascending, tallest red — drawn tallest-first because of row-reverse.

val CHART_FIXED = chartCode("listOf(55, 25, 90, 40, 70).sortedDescending()", tallestRed = true)

val tasks: List<EvalTask>

body — 288 lines, from line 150
    val tasks: List<EvalTask> = listOf(
        EvalTask(
            "primes",
            AgentTaskRequest(
                goal = "Make a box named Primes that prints the first 10 prime numbers, and run it.",
                checks = listOf(check("box-exists:Primes"), check("last-run-ok:Primes"),
                    check("output-ints-include:Primes:2,3,5,7,11,13,17,19,23,29")),
                taskId = "primes"
            ),
            steps(call("NewBoxRequest", "name" to "Primes", "code" to PRIMES_CODE),
                call("RunBoxRequest", "box" to "Primes"),
                "Made Primes and ran it; it printed the first ten primes.")
        ),
        EvalTask(
            "fix-compile-error",
            AgentTaskRequest(
                goal = "The box named Broken does not compile. Fix its code so that it runs and prints exactly: count is 3",
                setup = steps(call("NewBoxRequest", "name" to "Broken", "code" to BROKEN_CODE), call("RunBoxRequest", "box" to "Broken")),
                checks = listOf(check("last-run-ok:Broken"), check("output-contains:Broken:count is 3")),
                taskId = "fix-compile-error"
            ),
            steps(call("GetBoxPropertyRequest", "box" to "Broken"),
                call("SetBoxPropertyRequest", "box" to "Broken", "value" to FIXED_CODE),
                call("RunBoxRequest", "box" to "Broken"),
                "Changed the String to an Int; Broken now prints count is 3.")
        ),
        EvalTask(
            "parent-variable",
            AgentTaskRequest(
                goal = "Connect the box Source as the parent of the box Reader. Then make Reader print the value of " +
                    "the value `secret` that Source shares — refer to it by name, do not copy the number — and run Reader.",
                setup = steps(call("NewBoxRequest", "name" to "Source", "code" to SOURCE_CODE), call("RunBoxRequest", "box" to "Source"),
                    call("NewBoxRequest", "name" to "Reader")),
                checks = listOf(check("connected:Source:Reader"), check("last-run-ok:Reader"),
                    check("output-contains:Reader:4217"), check("property-lacks:Reader:code:4217")),
                taskId = "parent-variable"
            ),
            steps(call("ConnectBoxesRequest", "parent" to "Source", "child" to "Reader"),
                call("SetBoxPropertyRequest", "box" to "Reader", "value" to READER_CODE),
                call("RunBoxRequest", "box" to "Reader"),
                "Reader is under Source and prints secret.")
        ),
        EvalTask(
            "html-table",
            AgentTaskRequest(
                goal = "Launch a box named Table that uses display to show an HTML <table> in the output channel \"main\", " +
                    "with one row for each n in 1..3 holding n and n*n.",
                checks = listOf(check("box-exists:Table"), check("last-run-ok:Table"), check("output-contains:Table:<table"),
                    check("output-ints-include:Table:1,1,2,4,3,9")),
                taskId = "html-table"
            ),
            steps(call("NewBoxRequest", "name" to "Table", "code" to TABLE_CODE),
                call("LaunchBoxRequest", "box" to "Table"),
                "Table is launched and displays the squares.")
        ),
        EvalTask(
            "stop-ticker",
            AgentTaskRequest(
                goal = "The box Ticker is running forever. Stop it, and confirm it is no longer running.",
                setup = steps(call("NewBoxRequest", "name" to "Ticker", "code" to TICKER_CODE), call("LaunchBoxRequest", "box" to "Ticker")),
                checks = listOf(check("box-exists:Ticker"), check("not-running:Ticker")),
                taskId = "stop-ticker"
            ),
            steps(call("StopBoxRequest", "box" to "Ticker"), call("ListBoxesRequest"), "Ticker is stopped.")
        ),
        EvalTask(
            "wire-two",
            AgentTaskRequest(
                goal = "Create two boxes named Alpha and Beta, and connect them so that Beta is a child of Alpha.",
                checks = listOf(check("box-exists:Alpha"), check("box-exists:Beta"), check("connected:Alpha:Beta")),
                taskId = "wire-two"
            ),
            steps(call("NewBoxRequest", "name" to "Alpha"), call("NewBoxRequest", "name" to "Beta", "parent" to "Alpha"),
                "Alpha -> Beta.")
        ),
        EvalTask(
            "fix-runtime-error",
            AgentTaskRequest(
                goal = "Box Divider throws an exception when it runs. Keep its list empty, but fix it so that it prints " +
                    "the word empty when the list is empty. Run it to show that it works.",
                setup = steps(call("NewBoxRequest", "name" to "Divider", "code" to DIVIDER_CODE), call("RunBoxRequest", "box" to "Divider")),
                checks = listOf(check("last-run-ok:Divider"), check("output-contains:Divider:empty")),
                taskId = "fix-runtime-error"
            ),
            steps(call("RunBoxRequest", "box" to "Divider"),
                call("SetBoxPropertyRequest", "box" to "Divider", "value" to DIVIDER_FIXED),
                call("RunBoxRequest", "box" to "Divider"),
                "Divider now uses firstOrNull and prints empty.")
        ),
        EvalTask(
            "swatch-colour",
            AgentTaskRequest(
                goal = "The box Swatch displays a coloured rectangle. Make a box named Answer that prints just the name " +
                    "of the colour the rectangle is shown in, as a person looking at it would say it (one of: red, green, " +
                    "blue, purple, orange), and run Answer.",
                setup = steps(call("NewBoxRequest", "name" to "Swatch", "code" to SWATCH_CODE), call("RunBoxRequest", "box" to "Swatch")),
                checks = listOf(check("last-run-ok:Answer"), check("output-contains:Answer:blue"),
                    check("property-lacks:Answer:code:red")),
                taskId = "swatch-colour"
            ),
            steps(call("ViewBoxOutputRequest", "box" to "Swatch", "note" to "which colour the rectangle is"),
                call("NewBoxRequest", "name" to "Answer", "code" to ANSWER_CODE),
                call("RunBoxRequest", "box" to "Answer"),
                "The rectangle is blue: the script's inline !important wins. Answer prints blue.")
        ),
        EvalTask(
            "draw-sun",
            AgentTaskRequest(
                goal = "Make a box named Sun that uses display to show, in the output channel \"main\", a 300 by 200 SVG " +
                    "picture: a dark blue night sky filling the whole picture, with a filled yellow circle (the sun) in it. " +
                    "Run it and check that the picture looks right.",
                checks = listOf(check("box-exists:Sun"), check("last-run-ok:Sun"), check("output-picture:Sun"),
                    check("output-colour:Sun:#0a1850:0.15"), check("output-colour:Sun:#ffe000:0.01")),
                taskId = "draw-sun"
            ),
            steps(call("NewBoxRequest", "name" to "Sun", "code" to SUN_CODE),
                call("RunBoxRequest", "box" to "Sun"),
                call("ViewBoxOutputRequest", "box" to "Sun", "note" to "a yellow sun on a dark blue sky"),
                "Sun displays a yellow circle on a dark blue sky; the capture shows both.")
        ),
        EvalTask(
            "bars-visible",
            AgentTaskRequest(
                goal = "The box Bars is meant to show five blue bars, 20, 40, 60, 80 and 100 pixels tall, but nobody can see " +
                    "them. Fix Bars so that the five bars are visible at those heights, run it, and check what it shows.",
                setup = steps(call("NewBoxRequest", "name" to "Bars", "code" to BARS_CODE), call("RunBoxRequest", "box" to "Bars")),
                checks = listOf(check("last-run-ok:Bars"), check("output-colour:Bars:#1f77b4:0.02")),
                taskId = "bars-visible"
            ),
            steps(call("ViewBoxOutputRequest", "box" to "Bars", "note" to "are the bars visible?"),
                call("SetBoxPropertyRequest", "box" to "Bars", "value" to BARS_FIXED),
                call("RunBoxRequest", "box" to "Bars"),
                call("ViewBoxOutputRequest", "box" to "Bars", "note" to "five blue bars of increasing height"),
                "Percent heights had no definite parent height to resolve against, so every bar was 0 px tall; they are px now.")
        ),
        EvalTask(
            "bars-sorted-red",
            AgentTaskRequest(
                goal = "The box Chart shows five bars. Change it so that, as displayed, the bars are sorted by height — " +
                    "shortest on the left, tallest on the right — and the tallest bar is red (#d62728) while the others " +
                    "stay blue. Run it and check what it shows; keep going until it looks right.",
                setup = steps(call("NewBoxRequest", "name" to "Theme", "code" to THEME_CODE), call("RunBoxRequest", "box" to "Theme"),
                    call("NewBoxRequest", "name" to "Chart", "code" to CHART_CODE), call("RunBoxRequest", "box" to "Chart")),
                checks = listOf(check("last-run-ok:Chart"), check("output-colour:Chart:#d62728:0.01"),
                    check("output-rising:Chart:#1f77b4,#d62728:5")),
                taskId = "bars-sorted-red"
            ),
            steps(call("ViewBoxOutputRequest", "box" to "Chart", "note" to "the starting order"),
                call("SetBoxPropertyRequest", "box" to "Chart", "value" to CHART_FIRST_TRY),
                call("RunBoxRequest", "box" to "Chart"),
                call("ViewBoxOutputRequest", "box" to "Chart", "note" to "sorted shortest-to-tallest, tallest red?"),
                call("SetBoxPropertyRequest", "box" to "Chart", "value" to CHART_FIXED),
                call("RunBoxRequest", "box" to "Chart"),
                call("ViewBoxOutputRequest", "box" to "Chart", "note" to "rising left to right, the rightmost red"),
                "Something outside Chart reverses its bars (they drew tallest-first after sorting ascending); sorted descending they rise, the tallest red.")
        ),
        EvalTask(
            "bars-one-row",
            AgentTaskRequest(
                goal = "The box Row shows five blue bars. Add five more after them, 70, 80, 90, 100 and 110 pixels tall, " +
                    "keeping every bar 28 pixels wide, so that all ten stand side by side in a single row rising left to " +
                    "right. Run it and check what it shows; keep going until it looks right.",
                setup = steps(call("NewBoxRequest", "name" to "Row", "code" to ROW_CODE), call("RunBoxRequest", "box" to "Row")),
                checks = listOf(check("last-run-ok:Row"), check("output-rising:Row:#1f77b4:10")),
                taskId = "bars-one-row"
            ),
            steps(call("SetBoxPropertyRequest", "box" to "Row", "value" to ROW_FIRST_TRY),
                call("RunBoxRequest", "box" to "Row"),
                call("ViewBoxOutputRequest", "box" to "Row", "note" to "ten bars in one row?"),
                call("SetBoxPropertyRequest", "box" to "Row", "value" to ROW_FIXED),
                call("RunBoxRequest", "box" to "Row"),
                call("ViewBoxOutputRequest", "box" to "Row", "note" to "ten bars in one row, rising"),
                "Ten bars wrapped onto two rows in the panel's width; white-space:nowrap keeps them in one.")
        ),
        EvalTask(
            "audit-failing",
            AgentTaskRequest(
                goal = "Some of the boxes Cell01 to Cell10 fail when they run. Make a box named Report that prints the " +
                    "names of the failing ones, sorted and separated by a comma and a space (for example: Cell02, Cell05), " +
                    "and run Report.",
                setup = steps(*CELL_NAMES.mapIndexed { i, name ->
                    call("NewBoxRequest", "name" to name, "code" to if (name in FAILING_CELLS) CELL_BAD else cellCode(i + 1))
                }.toTypedArray()),
                checks = listOf(check("last-run-ok:Report"), check("output-contains:Report:Cell03, Cell07, Cell09"),
                    check("effects-absent:FILESYSTEM_WRITE")),
                taskId = "audit-failing"
            ),
            steps(program("""
                const cells = tools.ListBoxes({}).boxes.map(b => b.name).filter(n => n.startsWith("Cell")).sort();
                const failing = cells.filter(n => !tools.RunBox.raw({box: n}).success);
                tools.NewBox({name: "Report", code: 'println("' + failing.join(", ") + '")'});
                const report = tools.RunBox({box: "Report"});
                return {failing, printed: report.stdout};
            """, "run every Cell, collect the failing ones, report them"),
                "Cell03, Cell07 and Cell09 fail (an empty list's first()); Report prints them.")
        ),
        EvalTask(
            "sweep-parameter",
            AgentTaskRequest(
                goal = "The box Sq starts with the line `val k = 1`. Run Sq once for each k from 1 to 8 by changing that " +
                    "line, then make a box named Results that prints the eight numbers Sq printed, in order of k, separated " +
                    "by spaces, and run Results. Leave Sq with k = 1 when you are done.",
                setup = steps(call("NewBoxRequest", "name" to "Sq", "code" to sqCode(1))),
                checks = listOf(check("last-run-ok:Results"),
                    check("output-ints-include:Results:${SWEEP_OUTPUTS.joinToString(",")}"),
                    check("property-contains:Sq:code:val k = 1\n"), check("property-lacks:Sq:code:val k = 8")),
                taskId = "sweep-parameter"
            ),
            steps(program("""
                const printed = [];
                for (let k = 1; k <= 8; k++) {
                  tools.PatchBoxText({box: "Sq", startLine: 1, replacement: "val k = " + k});
                  printed.push(tools.RunBox({box: "Sq"}).stdout.join(" ").trim());
                }
                tools.PatchBoxText({box: "Sq", startLine: 1, replacement: "val k = 1"});
                tools.NewBox({name: "Results", code: 'println("' + printed.join(" ") + '")'});
                return {printed, results: tools.RunBox({box: "Results"}).stdout};
            """, "sweep k = 1..8 through Sq and collect what it prints"),
                "Swept k = 1..8 through Sq; Results prints the eight values in order, and Sq is back at k = 1.")
        ),
        EvalTask(
            "tag-todos",
            AgentTaskRequest(
                goal = "Some of the boxes Note01 to Note12 have a TODO comment in their code. Insert the line " +
                    "`// reviewed` as the first line of the code of each of those boxes, and change nothing else. " +
                    "Do not run any box.",
                setup = steps(*(1..12).map { n -> call("NewBoxRequest", "name" to noteName(n), "code" to noteCode(n)) }.toTypedArray()),
                checks = TODO_NOTES.map { n -> check("property-contains:${noteName(n)}:code:// reviewed\n// note $n") } +
                    listOf(1, 2, 6, 11).map { n -> check("property-lacks:${noteName(n)}:code:reviewed") } +
                    check("effects-absent:EXECUTE_CODE"),
                taskId = "tag-todos"
            ),
            steps(program("""
                const notes = tools.ListBoxes({}).boxes.map(b => b.name).filter(n => n.startsWith("Note")).sort();
                const tagged = [];
                for (const n of notes) {
                  const code = tools.GetBoxProperty({box: n}).value || "";
                  if (!code.includes("TODO")) continue;
                  tools.PatchBoxText({box: n, startLine: 1, insert: true, replacement: "// reviewed"});
                  tagged.push(n);
                }
                return tagged;
            """, "tag every Note box whose code has a TODO"),
                "Five boxes had a TODO (Note03, Note04, Note08, Note10, Note12); each now starts with // reviewed.")
        ),
        EvalTask(
            "four-turns",
            AgentTaskRequest(
                goal = "Make a box named Counter that prints the numbers 1 to 3 as one list on one line, and run it.",
                followUps = listOf(
                    "Change Counter so that it prints 1 to 5 instead, still as one list, and run it.",
                    "Now make Counter print the squares of 1 to 5 instead, as one list, and run it.",
                    "Make a second box named Total that prints the sum of those five squares, and run it.",
                ),
                checks = listOf(check("last-run-ok:Counter"), check("output-ints-include:Counter:1,4,9,16,25"),
                    check("last-run-ok:Total"), check("output-contains:Total:55")),
                taskId = "four-turns"
            ),
            steps(call("NewBoxRequest", "name" to "Counter", "code" to COUNTER_3), call("RunBoxRequest", "box" to "Counter"),
                "Counter prints 1 to 3.",
                call("SetBoxPropertyRequest", "box" to "Counter", "value" to COUNTER_5), call("RunBoxRequest", "box" to "Counter"),
                "Counter prints 1 to 5.",
                call("SetBoxPropertyRequest", "box" to "Counter", "value" to COUNTER_SQUARES), call("RunBoxRequest", "box" to "Counter"),
                "Counter prints 1, 4, 9, 16, 25.",
                call("NewBoxRequest", "name" to "Total", "code" to TOTAL_CODE), call("RunBoxRequest", "box" to "Total"),
                "Total prints 55.")
        ),
        EvalTask(
            "revise-table",
            AgentTaskRequest(
                goal = "Launch a box named Powers that uses display to show an HTML <table> in the output channel \"main\", " +
                    "with one row for each n in 1..3 holding n and n*n.",
                followUps = listOf(
                    "Extend Powers' table to n = 1..5, and launch it again.",
                    "Add a third column to every row holding n*n*n, and launch Powers again.",
                ),
                checks = listOf(check("last-run-ok:Powers"), check("output-contains:Powers:<table"),
                    check("output-ints-include:Powers:5,25,125"), check("property-contains:Powers:code:display")),
                taskId = "revise-table"
            ),
            steps(call("NewBoxRequest", "name" to "Powers", "code" to POWERS_3), call("LaunchBoxRequest", "box" to "Powers"),
                "Powers shows n and n*n for 1..3.",
                call("SetBoxPropertyRequest", "box" to "Powers", "value" to POWERS_5), call("LaunchBoxRequest", "box" to "Powers"),
                "Powers now goes to 5.",
                call("SetBoxPropertyRequest", "box" to "Powers", "value" to POWERS_5_CUBES), call("LaunchBoxRequest", "box" to "Powers"),
                "Powers shows n, n*n and n*n*n for 1..5.")
        ),
    )

val manyCallTaskIds = setOf("audit-failing", "sweep-parameter", "tag-todos")

Tasks made of many similar calls: what tool programs are for.

val multiTurnTaskIds = setOf("four-turns", "revise-table")

Tasks that are several user turns of one conversation: what the context pipeline is for.

val visualTaskIds

Tasks whose checks read pixels; they need a Field that can capture box output.

    val visualTaskIds = setOf("swatch-colour", "draw-sun", "bars-visible", "bars-sorted-red", "bars-one-row")

fun byId(id: String): EvalTask

    fun byId(id: String): EvalTask = tasks.firstOrNull { it.id == id }
        ?: throw IllegalArgumentException("No eval task '$id'. Known: ${tasks.joinToString { it.id }}")
}

data class AgentEvalRequest

body — 43 lines, from line 452
@Serializable
data class AgentEvalRequest(
    /** Task ids; empty = all. */
    val ids: List<String> = emptyList(),
    val mode: String = "scripted",
    val model: String = "",
    val maxToolRounds: Int = 24,
    /** `on` / `off` pins the distilled lessons in every task's instructions; blank = default ([AgentLessons]). */
    val lessons: String = "",
    /** After the run, distil its tool misuse into the lessons file ([AgentLessons.distilAndWrite]). */
    val distil: Boolean = false,
    /**
     * Path to a saved [AgentEvalReport]: run nothing, distil that report instead (implies [distil]).
     * How a change to [AgentLessons.distil] is applied to a run already paid for.
     */
    val distilFrom: String = "",
    /** Withhold ViewBoxOutputRequest from every task (the counterfactual for the visual tasks). */
    val blind: Boolean = false,
    /** `off` withholds RunToolProgramRequest from every task (the tool-program A/B arm); see [AgentTaskRequest.programs]. */
    val programs: String = "",
    /** `legacy` puts every task's instructions back inside the stored message; see [AgentTaskRequest.context]. */
    val context: String = "",
    /**
     * Run the mined corpus ([AgentEvalCorpus]) instead of the hand suite: `all`, `train` or `heldout`.
     * Blank = the hand suite. [ids] then filter corpus ids (`<transcript>/<box>`).
     */
    val corpus: String = "",
    /** With [corpus]: run a seeded sample of this many tasks (0 = every task in the split). */
    val sample: Int = 0,
    val seed: Long = 0,
    /**
     * Re-mine the corpus: mine the transcripts, run every candidate's solution twice (scripted), derive checks from
     * the output that matched, and rewrite [AgentEvalCorpus.CHECKOUT_PATH]. [sample] > 0 calibrates only that many
     * candidates (and does not write).
     */
    val calibrate: Boolean = false,
    /**
     * Withhold the tools that read the repository ([AgentEvalCorpus.SEALED_TOOLS]) from every task. A mined task's solution
     * is checked in — the transcript it came from — so with search on, a model can find the answer instead of
     * writing it; the first live corpus runs did, in at least 3 and 5 of 20 tasks.
     */
    val sealed: Boolean = false,
)

data class AgentEvalReport

body — 50 lines, from line 496
@Serializable
data class AgentEvalReport(
    val mode: String,
    val provider: String,
    val model: String,
    val population: Int,
    val passed: Int,
    val results: List<AgentTaskResult>,
    val elapsedMs: Long,
    val inputTokens: Long?,
    val outputTokens: Long?,
    val error: String = "",
    /** Sums of [AgentTaskResult.cacheReadTokens] / [AgentTaskResult.cacheWriteTokens]; null when no task reported them. */
    val cacheReadTokens: Long? = null,
    val cacheWriteTokens: Long? = null,
    /** Sum of [AgentTaskResult.misuseCalls]. */
    val misuseCalls: Int = 0,
    /** Sum of [AgentTaskResult.imagesSeen]. */
    val imagesSeen: Int = 0,
    val rounds: Int = 0,
    /** Whether the lessons were on, and which version of the file was read. */
    val lessons: Boolean = false,
    val lessonsVersion: Int = 0,
    /** Set when the request asked to distil: what was written. */
    val distilled: List<String> = emptyList(),
    val distilMessage: String = "",
    /**
     * Leak evidence, filled by the server's entry points ([afield.app.server.runAgentEvalHeadless]): open documents
     * before and after the run, and AI4 capture pages still open after it. -1 = not measured.
     */
    val documentsBefore: Int = -1,
    val documentsAfter: Int = -1,
    val capturePagesOpenAfter: Int = -1,
    /** Whether tool programs were offered; sums of [AgentTaskResult.programCalls] and [AgentTaskResult.nestedCalls]. */
    val programs: Boolean = false,
    val programCalls: Int = 0,
    val nestedCalls: Int = 0,
    /** `legacy` when the run put instructions inside the stored message, else blank. */
    val context: String = "",
    /** The corpus split run (`all` / `train` / `heldout`), or blank for the hand suite; `calibrate` for a re-mine. */
    val corpus: String = "",
    /** A calibration's summary: what was mined, accepted and written. */
    val corpusMessage: String = "",
    /** A calibration's rejections, counted by reason (static and calibration stages together). */
    val rejections: Map<String, Int> = emptyMap(),
    /** Whether the repository search tools were withheld ([AgentEvalRequest.sealed]). */
    val sealed: Boolean = false,
    /** Corpus live runs: tasks whose tool results held a line of their own solution ([AgentEvalCorpus.leak]). */
    val leaked: Int = 0,
)

object AgentEvalRunner

object AgentEvalRunner {

const val MAX_LIVE_TASKS = 17

Hard caps for a live eval: one run of at most this many tasks, at most this many rounds each.

const val MAX_LIVE_ROUNDS = 32

fun run(request: AgentEvalRequest, runTask: (AgentTaskRequest) -> AgentTaskResult): AgentEvalReport {

Run the suite, one task at a time. runTask runs one request in a fresh document and returns its result; the server passes one that opens a headless scratch document per task.

body — 72 lines, from line 556
    fun run(request: AgentEvalRequest, runTask: (AgentTaskRequest) -> AgentTaskResult): AgentEvalReport {
        if (request.distilFrom.isNotBlank()) {
            val saved = AgentLessons.readReport(request.distilFrom)
            val outcome = AgentLessons.distilAndWrite(saved)
            return saved.copy(distilled = outcome.lessons.map { it.bullet() }, distilMessage = outcome.message)
        }
        if (request.calibrate) return calibrate(request, runTask)
        val started = System.nanoTime()
        val chosen = if (request.corpus.isNotBlank()) {
            val corpus = AgentEvalCorpus.load()
            require(corpus.tasks.isNotEmpty()) { "No mined corpus at ${AgentEvalCorpus.CHECKOUT_PATH}; run agent-eval --calibrate first." }
            AgentEvalCorpus.select(corpus, request.corpus, request.sample, request.seed, request.ids)
                .map { InFieldAgentEvals.EvalTask(it.id, it.request, it.solution) }
        } else if (request.ids.isEmpty()) InFieldAgentEvals.tasks else request.ids.map(InFieldAgentEvals::byId)
        val live = request.mode == "live"
        val cap = if (request.corpus.isNotBlank()) AgentEvalCorpus.MAX_LIVE_SAMPLE else MAX_LIVE_TASKS
        require(!live || chosen.size <= cap) { "A live eval is capped at $cap tasks (use --sample)." }
        val rounds = if (live) request.maxToolRounds.coerceAtMost(MAX_LIVE_ROUNDS) else request.maxToolRounds
        val lessonsOn = AgentLessons.resolve(request.lessons)
        val lessonsPin = if (lessonsOn) "on" else "off"
        val lessonsVersion = AgentLessons.rawText()?.let { AgentLessons.parse(it).version } ?: 0
        val results = chosen.map { task ->
            runTask(task.request.copy(
                mode = request.mode,
                model = request.model,
                maxToolRounds = rounds,
                script = if (live) "" else task.solution,
                lessons = lessonsPin,
                blind = request.blind,
                programs = request.programs,
                context = request.context,
                withhold = if (request.sealed) AgentEvalCorpus.SEALED_TOOLS else emptyList(),
            )).let { result ->
                // A corpus solution is checked in; a turn that read it has not shown it can write it.
                val leakedVia = if (request.corpus.isNotBlank() && live) AgentEvalCorpus.leak(task.solution, result) else null
                when {
                    leakedVia == null -> result
                    request.sealed && result.verdict == "pass" -> result.copy(verdict = "leaked", leakedVia = leakedVia)
                    else -> result.copy(leakedVia = leakedVia)
                }
            }
        }
        fun sum(f: (AgentTaskResult) -> Long?): Long? = results.mapNotNull(f).takeIf { it.isNotEmpty() }?.sum()
        val report = AgentEvalReport(
            mode = request.mode,
            provider = results.firstOrNull { it.provider.isNotBlank() }?.provider.orEmpty(),
            model = results.firstOrNull { it.model.isNotBlank() }?.model.orEmpty(),
            population = results.size,
            passed = results.count { it.verdict == "pass" },
            results = results,
            elapsedMs = (System.nanoTime() - started) / 1_000_000,
            inputTokens = sum { it.inputTokens },
            outputTokens = sum { it.outputTokens },
            cacheReadTokens = sum { it.cacheReadTokens },
            cacheWriteTokens = sum { it.cacheWriteTokens },
            misuseCalls = results.sumOf { it.misuseCalls },
            imagesSeen = results.sumOf { it.imagesSeen },
            rounds = results.sumOf { it.rounds.coerceAtLeast(0) },
            lessons = lessonsOn,
            lessonsVersion = lessonsVersion,
            programs = results.any { it.programs },
            context = request.context,
            programCalls = results.sumOf { it.programCalls },
            nestedCalls = results.sumOf { it.nestedCalls },
            corpus = request.corpus,
            sealed = request.sealed,
            leaked = results.count { it.leakedVia.isNotBlank() },
        )
        if (!request.distil) return report
        val outcome = AgentLessons.distilAndWrite(report)
        return report.copy(distilled = outcome.lessons.map { it.bullet() }, distilMessage = outcome.message)
    }

fun calibrate(request: AgentEvalRequest, runTask: (AgentTaskRequest) -> AgentTaskResult): AgentEvalReport {

Re-mine the corpus (AgentEvalCorpus): every statically mined candidate's solution runs twice, scripted, with no checks; the runs' output decides its checks. The report's results are the second runs, passed the tasks accepted.

    fun calibrate(request: AgentEvalRequest, runTask: (AgentTaskRequest) -> AgentTaskResult): AgentEvalReport {
        val started = System.nanoTime()
        val mined = AgentEvalCorpus.mine()
        val candidates = mined.candidates.let { all ->
            if (request.ids.isNotEmpty()) all.filter { it.id in request.ids }
            else if (request.sample > 0) all.shuffled(java.util.Random(request.seed)).take(request.sample) else all
        }
        val tasks = ArrayList<AgentEvalCorpus.CorpusTask>()
        val rejections = ArrayList(mined.rejections)
        val results = ArrayList<AgentTaskResult>()
        for (c in candidates) {
            val first = runTask(AgentEvalCorpus.calibrationRequest(c))
            val second = runTask(AgentEvalCorpus.calibrationRequest(c))
            results += second
            when (val outcome = AgentEvalCorpus.calibrate(c, first, second)) {
                is AgentEvalCorpus.Accepted -> tasks += outcome.task
                is AgentEvalCorpus.Refused -> rejections += outcome.rejection
            }
        }
        val corpus = AgentEvalCorpus.Corpus(transcripts = mined.transcripts, codeBoxes = mined.codeBoxes, tasks = tasks, rejections = rejections)
        val whole = request.ids.isEmpty() && request.sample <= 0
        if (whole) AgentEvalCorpus.write(corpus)
        val message = "${mined.transcripts} transcripts, ${mined.codeBoxes} code boxes, ${mined.candidates.size} candidates; " +
            "calibrated ${candidates.size}, accepted ${tasks.size} (" +
            tasks.groupingBy { it.kind }.eachCount().entries.joinToString { "${it.key} ${it.value}" } + "; " +
            tasks.count { it.split == "heldout" } + " heldout)" +
            (if (whole) ", wrote ${AgentEvalCorpus.CHECKOUT_PATH}" else ", partial: not written")
        return AgentEvalReport(
            mode = "scripted", provider = "", model = "", population = candidates.size, passed = tasks.size,
            results = results, elapsedMs = (System.nanoTime() - started) / 1_000_000, inputTokens = null, outputTokens = null,
            corpus = "calibrate", corpusMessage = message, rejections = corpus.rejectionCounts(),
        )
    }
}