Docs
Dashboard
Examples / AI agentsBeta

Wiki race

The old game: get from one Wikipedia article to another by clicking links only. An agent plays it in a real browser. The example then judges the game without trusting the agent: it reads the pages the browser really visited from the session's log, checks every hop against Wikipedia's API in the session's shell, and works out whether a shorter route existed.

When to use it: You want to see an agent play a goal with many steps, judged from what the browser really visited and not from the agent's word.

Usessessions.create with shell, session.goto, agent.run in that session, session.events (the pages visited), session.files, session.exec (verify_path.py: the MediaWiki API)
Needsa plan with shell sessions and agent runs
Site[Wikipedia](https://en.wikipedia.org/) (any language), read at a person's pace; the API calls carry a user agent naming this example, as [Wikimedia's API etiquette](https://www.mediawiki.org/wiki/API:Etiquette) asks
Outputoutput/route.md (the route, hop by hop) and output/result.json (what the agent said, what was visited, the verdict)
LanguagesNode and Python

How the route is judged (verify_path.py):

  • the route is the visited articles in order; going back to an earlier article cuts off the dead end after it;
  • a hop A → B counts only when A's page links to B (redirects followed, as the browser follows them); a search page or a typed address breaks the chain, and so does any page of another site;
  • the shortest possible route is 1 hop when the start links to the target, and 2 when one of the start's links links to it (the start's links intersected with the target's backlinks); otherwise 3 or more.

Cost: the agent reads whole articles, so a race costs about $1 of model use with the default model (two runs on 2026-10-03: 3 and 4 hops). A target close to the start, or maxSteps lower, costs less.

The code

The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.

/** * Wiki race: an agent gets from one Wikipedia article to another by clicking links only. The example does not take * its word for the route: it reads the pages the browser really visited from the session's log and checks every hop * with Wikipedia's API in the session's shell, then works out whether a shorter route existed. * *   npx tsx node/index.ts            (BOXLINE_API_KEY; START_URL, TARGET; a plan with shell sessions and agent runs) * * Writes output/result.json and output/route.md. */import { mkdirSync, readFileSync, writeFileSync } from "node:fs";import { dirname, join } from "node:path";import { fileURLToPath } from "node:url";import { Boxline } from "@boxline/sdk";const here = dirname(fileURLToPath(import.meta.url));const out = process.env.OUTPUT_DIR ?? "output";const startUrl = process.env.START_URL ?? "https://en.wikipedia.org/wiki/Coffee";const target = process.env.TARGET ?? "Moon";const bx = new Boxline();const session = await bx.sessions.create({ shell: true, timeout: 900, idleTimeout: 300, userMetadata: { example: "wiki-race" } });console.log(`Session: ${session.id}`);try {  const page = await session.goto(startUrl);  console.log(`Start: ${page.title} → target: ${target}`);  // 1. The race: links only.  const started = await bx.agent.run({    sessionId: session.id,    task:      `You are on the Wikipedia article at ${startUrl}. Get to the article "${target}" by clicking links in the ` +      `articles' text, in as few clicks as you can. Rules: do not use the search box, do not type or change addresses ` +      `(no navigating to a URL), stay on this wiki. Going back from a dead end is allowed. When the "${target}" article ` +      `is open, finish with the titles of the articles you went through, in order.`,    maxSteps: 30,    output: {      type: "object",      properties: { reached: { type: "boolean" }, path: { type: "array", items: { type: "string" } } },      required: ["reached", "path"],    },  });  console.log(`Agent run ${started.id} (${started.model}) is racing…`);  const run = await bx.agent.wait<{ reached: boolean; path: string[] }>(started.id);  if (run.status !== "completed" || !run.result) throw new Error(`the run ${run.status}: ${run.error ?? "no result"}`);  const toolSteps = run.steps.filter((s) => s.type === "tool");  console.log(`The agent says: ${run.result.path.join(" → ")} (${toolSteps.filter((s) => s.name === "browser_click").length} clicks)`);  // 2. What the browser really visited, from the session's log, checked hop by hop in the shell.  const visited: string[] = [];  for await (const e of session.events({ types: ["navigation"] })) if (e.url && !e.url.startsWith("about:")) visited.push(e.url);  await session.files.write("path.json", JSON.stringify({ start: startUrl, target, visited }));  await session.files.write("verify_path.py", readFileSync(join(here, "../verify_path.py")));  const v = await session.exec("python3 verify_path.py path.json", { cwd: "/workspace", timeoutMs: 300_000 });  if (v.exitCode !== 0) throw new Error(`verify_path.py failed: ${v.stderr.trim().split("\n").at(-1)}`);  const verified = JSON.parse(v.stdout);  const marks = verified.hops.map((h: any) => `${h.from} ${h.linked ? "→" : "⇢ (not a link!)"} `).join("") + (verified.route.at(-1) ?? "");  const short = verified.shortest.hops === 3 ? "3 or more" : `${verified.shortest.hops}${verified.shortest.via.length ? ` (e.g. via ${verified.shortest.via[0]})` : ""}`;  console.log(`Route: ${marks}\n${verified.reached ? "Reached" : "Did not reach"} "${verified.target}" in ${verified.hops.length} hops; ${verified.allLinked ? "every hop is a link on the page" : "some hops are not links"}; shortest possible: ${short} (${verified.apiCalls} API calls)`);  mkdirSync(out, { recursive: true });  writeFileSync(    join(out, "route.md"),    [`# ${verified.start} → ${verified.target}`, "", ...verified.hops.map((h: any, i: number) => `${i + 1}. ${h.from} → ${h.to}${h.linked ? "" : " (not a link on the page)"}`), "", `Shortest possible: ${short}.`, ""].join("\n"),  );  writeFileSync(    join(out, "result.json"),    JSON.stringify({ startUrl, target, agent: run.result, visited, verified, toolSteps: toolSteps.map((s) => s.name), agentRuns: [{ id: run.id }] }, null, 2),  );} finally {  await session.stop();}
verify_path.py
"""Runs in the session's shell: checks a Wikipedia race from the pages the browser really visited, with the MediaWikiAPI, and works out whether a shorter route existed.    python3 verify_path.py path.json      path.json: {"start": url, "target": title, "visited": [url, ...]}- The route: the visited articles in order, with a return to an earlier article (Back) cutting off the dead end.- Each hop A → B counts only when A's page links to B (redirects followed, as the browser does).- Shortest: 1 when the start links to the target; 2 when one of the start's links links to it (found by intersecting  the start's links with the target's backlinks); otherwise 3 or more.Prints JSON. Requests carry a User-Agent naming this example, as Wikimedia's API etiquette asks."""import jsonimport sysfrom urllib.parse import unquote, urlparseimport requestsspec = json.load(open(sys.argv[1]))host = urlparse(spec["start"]).netlocAPI = f"https://{host}/w/api.php"http = requests.Session()http.headers["User-Agent"] = "BoxlineExamples/1.0 (https://github.com/Boxline-dev/examples; wiki-race)"calls = 0def query(**params):    """Every page of a query's results (continuation followed)."""    global calls    params = {"action": "query", "format": "json", "formatversion": "2", **params}    while True:        calls += 1        r = http.get(API, params=params, timeout=60).json()        yield r.get("query", {})        if "continue" not in r:            return        params.update(r["continue"])def title_of(url):    p = urlparse(url)    if p.netloc != host or not p.path.startswith("/wiki/"):        return None    return unquote(p.path[len("/wiki/"):]).replace("_", " ")def canonical(title):    for q in query(titles=title, redirects=1):        pages = q.get("pages", [])        return pages[0]["title"] if pages and not pages[0].get("missing") else title    return titledef links_of(title, namespace=None):    out = set()    extra = {"gplnamespace": namespace} if namespace is not None else {}    for q in query(titles=title, generator="links", gpllimit="max", redirects=1, **extra):        out.update(p["title"] for p in q.get("pages", []))    return outtarget = canonical(spec["target"])route = []for url in spec["visited"]:    t = title_of(url)    if not t:        route.append({"title": None, "url": url})  # not an article of this wiki (a search page, another site)        continue    t = canonical(t)    if route and route[-1]["title"] == t:        continue    seen = [i for i, r in enumerate(route) if r["title"] == t]    if seen:        route = route[: seen[-1] + 1]  # back to an earlier article: the dead end after it is not part of the route        continue    route.append({"title": t, "url": url})hops = []for a, b in zip(route, route[1:]):    linked = bool(a["title"] and b["title"] and b["title"] in links_of(a["title"]))    hops.append({"from": a["title"] or a["url"], "to": b["title"] or b["url"], "linked": linked})start = route[0]["title"] if route else canonical(title_of(spec["start"]))first = links_of(start, namespace=0)if target in first:    shortest = {"hops": 1, "via": []}else:    back = set()    for q in query(list="backlinks", bltitle=target, blnamespace=0, bllimit="max", blredirect=1):        for b in q.get("backlinks", []):            back.add(b["title"])            back.update(r["title"] for r in b.get("redirlinks", []))        if len(back) > 50_000:            break    via = sorted(first & back)    shortest = {"hops": 2, "via": via[:5]} if via else {"hops": 3, "via": [], "note": "3 or more"}print(json.dumps({    "start": start, "target": target, "route": [r["title"] or r["url"] for r in route], "hops": hops,    "reached": bool(route) and route[-1]["title"] == target, "allLinked": all(h["linked"] for h in hops),    "shortest": shortest, "apiCalls": calls,}, ensure_ascii=False))

Inputs

Environment variables the example reads:

  • START_URL: the article to start from (default [Coffee](https://en.wikipedia.org/wiki/Coffee)).
  • TARGET: the title of the article to reach (default Moon).

Run it

Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/wiki-race. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.

# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=…   # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts

Then check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.

What its check proves

check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.

npx tsx check.ts output exits with 0 only when the browser reached the target, every hop of the route is a link on its page, the agent never typed an address, and the route is no shorter than the shortest possible (a sanity check of the verdict itself).

Source

github.com/Boxline-dev/examples/wiki-race on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.