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.
| Uses | sessions.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) |
|---|---|
| Needs | a 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 |
| Output | output/route.md (the route, hop by hop) and output/result.json (what the agent said, what was visited, the verdict) |
| Languages | Node 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();}"""Wiki race: an agent gets from one Wikipedia article to another by clicking links only. The example does not takeits word for the route: it reads the pages the browser really visited from the session's log and checks every hopwith Wikipedia's API in the session's shell, then works out whether a shorter route existed. python python/main.py (BOXLINE_API_KEY; START_URL, TARGET; a plan with shell sessions and agent runs)Writes output/result.json and output/route.md."""import jsonimport osfrom pathlib import Pathfrom boxline import Boxlinehere = Path(__file__).resolve().parentout = Path(os.environ.get("OUTPUT_DIR", "output"))start_url = os.environ.get("START_URL", "https://en.wikipedia.org/wiki/Coffee")target = os.environ.get("TARGET", "Moon")bx = Boxline()OUTPUT = { "type": "object", "properties": {"reached": {"type": "boolean"}, "path": {"type": "array", "items": {"type": "string"}}}, "required": ["reached", "path"],}with bx.sessions.create(shell=True, timeout=900, idle_timeout=300, user_metadata={"example": "wiki-race"}) as session: print(f"Session: {session.id}", flush=True) page = session.goto(start_url) print(f"Start: {page['title']} → target: {target}", flush=True) # 1. The race: links only. started = bx.agent.run( f"You are on the Wikipedia article at {start_url}. 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 " f"(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.", session_id=session.id, max_steps=30, output=OUTPUT, ) print(f"Agent run {started['id']} ({started['model']}) is racing…", flush=True) run = bx.agent.wait(started["id"]) if run["status"] != "completed" or not run["result"]: raise SystemExit(f"the run {run['status']}: {run.get('error') or 'no result'}") tool_steps = [s for s in run["steps"] if s["type"] == "tool"] clicks = sum(1 for s in tool_steps if s.get("name") == "browser_click") print(f"The agent says: {' → '.join(run['result']['path'])} ({clicks} clicks)", flush=True) # 2. What the browser really visited, from the session's log, checked hop by hop in the shell. visited = [e["url"] for e in session.events(types=["navigation"]) if e.get("url") and not e["url"].startswith("about:")] session.files.write("path.json", json.dumps({"start": start_url, "target": target, "visited": visited})) session.files.write("verify_path.py", (here.parent / "verify_path.py").read_bytes()) v = session.exec("python3 verify_path.py path.json", cwd="/workspace", timeout_ms=300_000) if v["exitCode"] != 0: raise SystemExit(f"verify_path.py failed: {(v['stderr'].strip().splitlines() or ['no output'])[-1]}") verified = json.loads(v["stdout"])marks = "".join(f"{h['from']} {'→' if h['linked'] else '⇢ (not a link!)'} " for h in verified["hops"]) + (verified["route"][-1] if verified["route"] else "")sh = verified["shortest"]short = "3 or more" if sh["hops"] == 3 else f"{sh['hops']}" + (f" (e.g. via {sh['via'][0]})" if sh["via"] else "")print(f"Route: {marks}\n{'Reached' if verified['reached'] else 'Did not reach'} \"{verified['target']}\" in {len(verified['hops'])} hops; " f"{'every hop is a link on the page' if verified['allLinked'] else 'some hops are not links'}; shortest possible: {short} ({verified['apiCalls']} API calls)")out.mkdir(parents=True, exist_ok=True)lines = [f"# {verified['start']} → {verified['target']}", ""]lines += [f"{i}. {h['from']} → {h['to']}" + ("" if h["linked"] else " (not a link on the page)") for i, h in enumerate(verified["hops"], 1)]lines += ["", f"Shortest possible: {short}.", ""](out / "route.md").write_text("\n".join(lines))result = {"startUrl": start_url, "target": target, "agent": run["result"], "visited": visited, "verified": verified, "toolSteps": [s.get("name") for s in tool_steps], "agentRuns": [{"id": run["id"]}]}(out / "result.json").write_text(json.dumps(result, indent=2, ensure_ascii=False))"""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 (defaultMoon).
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# Python 3.9 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keypip install boxline-sdkpython python/main.pyThen 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.