Shell-only agent
An agent run with no browser at all: session: { browser: false, shell: true } gives it a machine with only a shell and a /workspace disk. The agent generates a CSV by a rule, computes statistics from it with Python and writes a report. Then the example reads the files back through the API (files.readText) and recomputes every number itself, so the result does not rest on what the agent says.
| Uses | agent.run with session: { browser: false, shell: true } and keepSession, agent.wait, sessions.files.readText, sessions.get, sessions.stop |
|---|---|
| Needs | a plan with shell sessions and agent runs |
| Site | none: the data is made up by a rule (240 rows, four regions) |
| Output | output/sales.csv, output/stats.json, output/report.md and output/result.json (what the agent wrote, what the rule gives, the agent's shell commands) |
| Languages | Node and Python |
The agent's task fixes the rule (region = [north, south, east, west][i % 4], units = (i * 37) % 23 + 1, unit_price = 4.50 + (i % 5) * 1.25) and the statistics to compute (totals, revenue by region, the top region, the mean, median and population standard deviation of the units). Everything it needs is in Python's standard library.
Why the example computes them again: the agent's report is a claim. The example regenerates the 240 rows from the rule, compares them with the agent's sales.csv, and computes the same statistics itself (in the language of the example). The session is shell-only, so it has no liveUrl; the example prints and records that too.
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.
/** * Shell-only agent: an agent run in its own session with no browser, only a shell. It generates a CSV by a rule, computes * statistics from it with Python and writes a report; then the example reads the files back (`files.readText`) and * recomputes every number itself, so the result does not rest on the agent's word. * * npx tsx node/index.ts (BOXLINE_API_KEY; a plan with shell sessions and agent runs) * * The data is made up by a rule (no site is visited). Writes output/sales.csv, output/stats.json, output/report.md and * output/result.json. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { Boxline } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const bx = new Boxline();// The rule the agent follows, and the example checks against.const ROWS = 240;const REGIONS = ["north", "south", "east", "west"];const rule = (i: number) => ({ id: i, region: REGIONS[i % 4]!, units: ((i * 37) % 23) + 1, price: 4.5 + (i % 5) * 1.25 });const round2 = (x: number) => Math.round(x * 100) / 100;/** The numbers the agent should find, from the rule alone. */function expectedStats() { const rows = Array.from({ length: ROWS }, (_, k) => rule(k + 1)); const byRegion = Object.fromEntries(REGIONS.map((r) => [r, round2(rows.filter((x) => x.region === r).reduce((s, x) => s + x.units * x.price, 0))])); const units = rows.map((x) => x.units).sort((a, b) => a - b); const mean = units.reduce((s, u) => s + u, 0) / units.length; const mid = units.length / 2; return { total_units: units.reduce((s, u) => s + u, 0), total_revenue: round2(rows.reduce((s, x) => s + x.units * x.price, 0)), revenue_by_region: byRegion, top_region: Object.entries(byRegion).sort((a, b) => b[1] - a[1])[0]![0], units_mean: round2(mean), units_median: units.length % 2 ? units[Math.floor(mid)]! : (units[mid - 1]! + units[mid]!) / 2, units_stdev: round2(Math.sqrt(units.reduce((s, u) => s + (u - mean) ** 2, 0) / units.length)), };}const task = `Work in /workspace with Python 3 (standard library only). Do these three things, in this order:\n` + `1. Write sales.csv with the header "id,region,units,unit_price" and one row for each i from 1 to ${ROWS}: ` + `id = i, region = ["north", "south", "east", "west"][i % 4], units = (i * 37) % 23 + 1, unit_price = 4.50 + (i % 5) * 1.25 (two decimals).\n` + `2. Read sales.csv back with a Python script and write stats.json with these keys: total_units (integer), total_revenue ` + `(the sum of units * unit_price), revenue_by_region (an object with the four regions), top_region (the region with the highest revenue), ` + `units_mean, units_median and units_stdev (the population standard deviation, statistics.pstdev). Round every decimal to 2 places.\n` + `3. Write report.md: a heading, a table of revenue by region, and one sentence that names the top region and the total revenue ` + `with two decimals. Take every number from stats.json.\n` + `When all three files exist, reply with one sentence.`;const started = await bx.agent.run({ task, // The run's own session: no browser, only a shell. keepSession leaves it running so the files can be read. session: { browser: false, shell: true, timeout: 900, idleTimeout: 300, userMetadata: { example: "shell-only-agent" } }, keepSession: true, maxSteps: 20,});console.log(`Session: ${started.sessionId}`);console.log(`Agent run ${started.id} (${started.model}) is working in a shell-only session…`);try { const run = await bx.agent.wait(started.id); if (run.status !== "completed") throw new Error(`the run ${run.status}: ${run.error ?? "no result"}`); const toolSteps = run.steps.filter((s) => s.type === "tool"); console.log(`The agent finished after ${toolSteps.length} tool steps: ${String(run.result).slice(0, 200)}`); // The files the agent wrote, read through the API. const files = bx.sessions.files; const csv = await files.readText(started.sessionId, "sales.csv"); const reported = JSON.parse(await files.readText(started.sessionId, "stats.json")); const report = await files.readText(started.sessionId, "report.md"); const lines = csv.trim().split(/\r?\n/); // Python's csv module ends lines with \r\n const matchesRule = lines[0] === "id,region,units,unit_price" && lines.length === ROWS + 1 && lines.slice(1).every((line, k) => { const [id, region, units, price] = line.split(","); const want = rule(k + 1); return Number(id) === want.id && region === want.region && Number(units) === want.units && Math.abs(Number(price) - want.price) < 1e-9; }); const expected = expectedStats(); console.log(`sales.csv: ${lines.length - 1} rows${matchesRule ? ", as the rule says" : ", NOT as the rule says"}`); console.log(`The agent's total revenue ${reported.total_revenue}, expected ${expected.total_revenue}; top region ${reported.top_region}, expected ${expected.top_region}`); // What kind of session the run made. const session = (await bx.sessions.get(started.sessionId)).data; console.log(`The run's session: browser ${session.browser}, shell ${session.shell}, liveUrl ${session.liveUrl}`); mkdirSync(out, { recursive: true }); writeFileSync(join(out, "sales.csv"), csv); writeFileSync(join(out, "stats.json"), JSON.stringify(reported, null, 2)); writeFileSync(join(out, "report.md"), report); writeFileSync( join(out, "result.json"), JSON.stringify( { csv: { rows: lines.length - 1, matchesRule }, expected, reported, report, session: { id: session.id, browser: session.browser, shell: session.shell, liveUrl: session.liveUrl }, toolSteps: toolSteps.map((s) => ({ name: s.name, input: s.input })), agentRuns: [{ id: run.id }], }, null, 2, ), );} finally { await bx.sessions.stop(started.sessionId).catch(() => undefined);}"""Shell-only agent: an agent run in its own session with no browser, only a shell. It generates a CSV by a rule, computesstatistics from it with Python and writes a report; then the example reads the files back (``files.read_text``) andrecomputes every number itself, so the result does not rest on the agent's word. python python/main.py (BOXLINE_API_KEY; a plan with shell sessions and agent runs)The data is made up by a rule (no site is visited). Writes output/sales.csv, output/stats.json, output/report.md andoutput/result.json."""import jsonimport osimport statisticsfrom pathlib import Pathfrom boxline import Boxlineout = Path(os.environ.get("OUTPUT_DIR", "output"))bx = Boxline()# The rule the agent follows, and the example checks against.ROWS = 240REGIONS = ["north", "south", "east", "west"]def rule(i: int) -> dict: return {"id": i, "region": REGIONS[i % 4], "units": (i * 37) % 23 + 1, "price": 4.5 + (i % 5) * 1.25}def expected_stats() -> dict: """The numbers the agent should find, from the rule alone.""" rows = [rule(i) for i in range(1, ROWS + 1)] by_region = {r: round(sum(x["units"] * x["price"] for x in rows if x["region"] == r), 2) for r in REGIONS} units = [x["units"] for x in rows] return { "total_units": sum(units), "total_revenue": round(sum(x["units"] * x["price"] for x in rows), 2), "revenue_by_region": by_region, "top_region": max(by_region, key=by_region.get), "units_mean": round(statistics.mean(units), 2), "units_median": statistics.median(units), "units_stdev": round(statistics.pstdev(units), 2), }TASK = ( "Work in /workspace with Python 3 (standard library only). Do these three things, in this order:\n" f'1. Write sales.csv with the header "id,region,units,unit_price" and one row for each i from 1 to {ROWS}: ' 'id = i, region = ["north", "south", "east", "west"][i % 4], units = (i * 37) % 23 + 1, unit_price = 4.50 + (i % 5) * 1.25 (two decimals).\n' "2. Read sales.csv back with a Python script and write stats.json with these keys: total_units (integer), total_revenue " "(the sum of units * unit_price), revenue_by_region (an object with the four regions), top_region (the region with the highest revenue), " "units_mean, units_median and units_stdev (the population standard deviation, statistics.pstdev). Round every decimal to 2 places.\n" "3. Write report.md: a heading, a table of revenue by region, and one sentence that names the top region and the total revenue " "with two decimals. Take every number from stats.json.\n" "When all three files exist, reply with one sentence.")started = bx.agent.run( TASK, # The run's own session: no browser, only a shell. keep_session leaves it running so the files can be read. session={"browser": False, "shell": True, "timeout": 900, "idleTimeout": 300, "userMetadata": {"example": "shell-only-agent"}}, keep_session=True, max_steps=20,)session_id = started["sessionId"]print(f"Session: {session_id}", flush=True)print(f"Agent run {started['id']} ({started['model']}) is working in a shell-only session…", flush=True)try: run = bx.agent.wait(started["id"]) if run["status"] != "completed": 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"] print(f"The agent finished after {len(tool_steps)} tool steps: {str(run['result'])[:200]}", flush=True) # The files the agent wrote, read through the API. csv = bx.sessions.files.read_text(session_id, "sales.csv") reported = json.loads(bx.sessions.files.read_text(session_id, "stats.json")) report = bx.sessions.files.read_text(session_id, "report.md") lines = csv.strip().splitlines() # also \r\n, which Python's csv module writes matches_rule = lines[0] == "id,region,units,unit_price" and len(lines) == ROWS + 1 if matches_rule: for k, line in enumerate(lines[1:]): id_, region, units, price = line.split(",") want = rule(k + 1) if not (int(id_) == want["id"] and region == want["region"] and int(units) == want["units"] and abs(float(price) - want["price"]) < 1e-9): matches_rule = False break expected = expected_stats() print(f"sales.csv: {len(lines) - 1} rows" + (", as the rule says" if matches_rule else ", NOT as the rule says")) print(f"The agent's total revenue {reported.get('total_revenue')}, expected {expected['total_revenue']}; top region {reported.get('top_region')}, expected {expected['top_region']}") # What kind of session the run made. session = bx.sessions.get(session_id).data print(f"The run's session: browser {session['browser']}, shell {session['shell']}, liveUrl {session['liveUrl']}") out.mkdir(parents=True, exist_ok=True) (out / "sales.csv").write_text(csv) (out / "stats.json").write_text(json.dumps(reported, indent=2)) (out / "report.md").write_text(report) result = { "csv": {"rows": len(lines) - 1, "matchesRule": matches_rule}, "expected": expected, "reported": reported, "report": report, "session": {"id": session["id"], "browser": session["browser"], "shell": session["shell"], "liveUrl": session["liveUrl"]}, "toolSteps": [{"name": s.get("name"), "input": s.get("input")} for s in tool_steps], "agentRuns": [{"id": run["id"]}], } (out / "result.json").write_text(json.dumps(result, indent=2, ensure_ascii=False))finally: try: bx.sessions.stop(session_id) except Exception: passRun it
Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/shell-only-agent. 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 run's session was shell-only (no browser, no liveUrl), sales.csv has the 240 rows the rule gives, every statistic in the agent's stats.json equals the one the example computed (to a cent), report.md shows the total revenue and names the top region, and the agent ran Python in the shell.
Source
github.com/Boxline-dev/examples/shell-only-agent 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.