"""Agentic multi-task instrument — the v2-hard gauntlet. Runs multiple build-a-real-app tasks (each in a different language/framework) against one model, sequentially. Each task is a self-contained module under `bench/tasks/multitask/` exposing TASK_NAME, PLAN, STARTER, REFERENCE, validate(), gates(), TOOLS, TOOL_IMPLS. The harness materializes the starter, runs a multi-turn tool loop, then scores via the task's gates(). Shipped tasks (the gauntlet): go_htmx — Go + HTMX partials + embedded SQLite + recursive CTE rust_cli — Rust + reqwest + quick-xml + async + Digest auth ts_next — Next.js + Prisma + Zod validation + API route These are realistic small-app shapes that test the real agent workload: multi-file creation, dependency management, iterative build/test cycles, and cross-file reasoning. Add your own by dropping a task module into `bench/tasks/multitask/` — it auto-discovers. Usage (via the bench.py CLI): python bench.py agentic-multitask validate go_htmx python bench.py agentic-multitask run go_htmx python bench.py agentic-multitask cohort go_htmx rust_cli ts_next Env (via .env): ENDPOINT, MODEL, API_KEY, MAX_TOKENS, MAX_TURNS, TEMPERATURE. """ import importlib.util import json import os import re import sys import time from bench.core import client from bench.core import tools from bench.core.reporting import write_json, safe_label HERE = os.path.dirname(os.path.abspath(__file__)) MULTITASK_DIR = os.path.join(HERE, "multitask") WORK_BASE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "work") MAX_TOKENS = int(os.environ.get("MAX_TOKENS", "16000")) MAX_TURNS = int(os.environ.get("MAX_TURNS", "40")) TEMPERATURE = float(os.environ.get("TEMPERATURE", "0")) def load_task(name): path = os.path.join(MULTITASK_DIR, f"{name}.py") if not os.path.isfile(path): print(f"ERROR: no such task module: {path}", file=sys.stderr) sys.exit(2) spec = importlib.util.spec_from_file_location(f"task_{name}", path) m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) return { "TASK_NAME": m.TASK_NAME, "PLAN": m.PLAN, "STARTER": m.STARTER, "REFERENCE": m.REFERENCE, "validate": m.validate, "gates": m.gates, "TOOLS": getattr(m, "TOOLS", tools.TOOLS), "TOOL_IMPLS": getattr(m, "TOOL_IMPLS", tools.TOOL_IMPLS), } def available_tasks(): if not os.path.isdir(MULTITASK_DIR): return [] return sorted(f[:-3] for f in os.listdir(MULTITASK_DIR) if f.endswith(".py") and not f.startswith("_")) def _materialize(files, dest): import shutil shutil.rmtree(dest, ignore_errors=True) os.makedirs(dest, exist_ok=True) for path, content in files.items(): fp = os.path.join(dest, path) os.makedirs(os.path.dirname(fp), exist_ok=True) with open(fp, "w") as f: f.write(content) return dest def run_solo(task, work_dir): tools.set_work_dir(work_dir) task_tools = task.get("TOOLS", tools.TOOLS) task_tool_impls = task.get("TOOL_IMPLS", tools.TOOL_IMPLS) _materialize(task["STARTER"], dest=work_dir) messages = [ {"role": "system", "content": "You are a coding agent working in a project work dir. You are given " "a task plan below. Execute it step by step using the tools. You can " "write files, edit files, run builds and tests, and run shell commands " "in the work dir. Keep going until the build is green, the tests pass, " "and the app/binary works. Use the tools to verify your own progress — " "don't guess; compile and test.\n\n" "When you are confident the task is complete and tests pass, say " "`DONE` in your final message (no tool call) and stop.\n\n" "TASK PLAN:\n" + task["PLAN"]}, {"role": "user", "content": "The work dir has the starter scaffold. Execute the task plan. Begin " "by adding any dependencies and writing the files. Use the tools to " "build and test as you go."}, ] turns_log = [] peak_prompt = total_completion = total_reasoning = 0 finish_reasons = [] t0 = time.time() turns = 0 done_said = False no_tool_streak = 0 for turn in range(1, MAX_TURNS + 1): turns = turn body = {"model": client.MODEL, "messages": messages, "max_tokens": MAX_TOKENS, "temperature": TEMPERATURE, "tools": task_tools} try: resp, dt = client.call_model(messages, tools=task_tools, max_tokens=MAX_TOKENS, temperature=TEMPERATURE) except Exception as e: print(f" turn {turn}: REQUEST ERROR {e!r}") turns_log.append({"turn": turn, "error": repr(e)}) break u = client.usage(resp) peak_prompt = max(peak_prompt, u["prompt_tokens"]) total_completion += u["completion_tokens"] total_reasoning += u["reasoning_tokens"] choice = resp["choices"][0] fr = choice.get("finish_reason") finish_reasons.append(fr) msg = choice["message"] tcs = msg.get("tool_calls") or [] content = msg.get("content") or "" tlog = {"turn": turn, "dt": round(dt, 1), "prompt_tok": u["prompt_tokens"], "completion_tok": u["completion_tokens"], "reasoning_tok": u["reasoning_tokens"], "finish_reason": fr, "n_tool_calls": len(tcs)} turns_log.append(tlog) tc_summary = ",".join((tc["function"]["name"] for tc in tcs)) or "(no tool call)" print(f" turn {turn:2d}: {dt:5.1f}s ptok={u['prompt_tokens']:6d} ctok={u['completion_tokens']:5d} " f"rtok={u['reasoning_tokens']:5d} fr={fr:8s} tools=[{tc_summary}]") if not tcs and "DONE" in content.upper(): done_said = True tlog["done"] = True break if not tcs: no_tool_streak += 1 if no_tool_streak >= 2: print(" -> giving up after 2 no-tool turns") break messages.append({"role": "assistant", "content": content}) messages.append({"role": "user", "content": "You must either call a tool to keep working, or say DONE if " "you believe the task is complete and tests pass."}) continue no_tool_streak = 0 a_msg = {"role": "assistant", "content": content} if tcs: a_msg["tool_calls"] = tcs messages.append(a_msg) for tc in tcs: fn = tc["function"] name = fn["name"] try: args = json.loads(fn["arguments"] or "{}") except json.JSONDecodeError as e: args = {} result = f"ERROR: invalid JSON args ({e}); re-emit the {name} call." else: impl = task_tool_impls.get(name) result = impl(args) if impl else f"ERROR: unknown tool {name!r}" if len(result) > 6000: result = result[:6000] + f"\n...[truncated, {len(result)} total]" messages.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": result}) tlog.setdefault("tool_results", []).append({ "tool": name, "result_head": result[:120].replace("\n", " ")}) wall = time.time() - t0 gate_results = task["gates"](work_dir) fr_counter = {} for f in finish_reasons: if f: fr_counter[f] = fr_counter.get(f, 0) + 1 rec = { "architecture": "solo", "task": task["TASK_NAME"], "model": client.MODEL, "max_tokens_per_turn": MAX_TOKENS, "max_turns": MAX_TURNS, "turns_used": turns, "done_said": done_said, "wall_s": round(wall, 1), "peak_prompt_tokens": peak_prompt, "total_completion_tokens": total_completion, "total_reasoning_tokens": total_reasoning, "finish_reasons": fr_counter, "checkpoints": gate_results, "all_pass": all(gate_results.values()), "turns_log": turns_log, } return rec def _report(rec): tag = safe_label(f"{rec['task']}__{rec['model']}") print("\n===================== V2-HARD RESULT =====================") print(f"task : {rec['task']}") print(f"model : {rec['model']}") print(f"turns used : {rec['turns_used']}/{rec['max_turns']}") print(f"done_said : {rec['done_said']}") print(f"wall clock : {rec['wall_s']}s ({rec['wall_s']/60:.1f} min)") print(f"peak prompt tok : {rec['peak_prompt_tokens']}") print(f"total completion : {rec['total_completion_tokens']}") print(f"total reasoning : {rec['total_reasoning_tokens']}") print(f"finish reasons : {rec['finish_reasons']}") print() print("--- gates ---") for k, v in rec["checkpoints"].items(): print(f" {k:16s}: {'PASS' if v else 'fail'}") print(f" => {'ALL PASS' if rec['all_pass'] else 'INCOMPLETE'}") write_json(rec, tag, prefix="v2hard") print() def validate(task_name): task = load_task(task_name) work_dir = os.path.join(WORK_BASE, f"multitask_validate_{task_name}") print(f"=== VALIDATION: {task_name} reference app ===") task["validate"](work_dir) def run(task_name): task = load_task(task_name) work_dir = os.path.join(WORK_BASE, f"multitask_solo_{task_name}") rec = run_solo(task, work_dir) _report(rec) def cohort(*task_names): results = [] for name in task_names: print(f"\n{'='*60}\n=== COHORT: {name} ===\n{'='*60}") task = load_task(name) work_dir = os.path.join(WORK_BASE, f"multitask_cohort_{name}") rec = run_solo(task, work_dir) _report(rec) results.append(rec) print(f"\n{'='*60}\n=== COHORT SUMMARY ===\n{'='*60}") print(f"{'task':16s} {'gates':>6s} {'wall':>8s} {'turns':>6s} {'comp tok':>9s} {'rsn tok':>9s}") for r in results: g = 'PASS' if r['all_pass'] else 'fail' print(f"{r['task']:16s} {g:>6s} {r['wall_s']/60:>7.1f}m {r['turns_used']:>6} {r['total_completion_tokens']:>9,} {r['total_reasoning_tokens']:>9,}") def main(): if len(sys.argv) < 2: print(f"usage: python bench.py agentic-multitask [validate|run|cohort] ...") print(f"available tasks: {', '.join(available_tasks())}") sys.exit(1) cmd = sys.argv[1] if cmd == "validate": validate(sys.argv[2]) elif cmd == "run": run(sys.argv[2]) elif cmd == "cohort": cohort(*sys.argv[2:]) else: print(f"unknown command: {cmd}") sys.exit(1) if __name__ == "__main__": main()