Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe

Coding-Agent Benchmarking with SWE-Bench Regression Gates

Agent quality is a moving target — a model update can shift pass rates overnight. This LangGraph workflow builds a golden task suite, runs agents in parallel Map-Reduce style, scores pass rates and cost per task, diffs against a pinned baseline, and ends in a hard go/no-go promotion gate.

Deepak Bagada

Deepak Bagada

CEO, SaaSNext

Aug 18, 2026 Published
|
Aug 18, 2026 Updated
|
11 Minutes Reading Time
Core Takeaways for Founders & Builders
  • Benchmark your own golden task suite continuously — not just public SWE-bench leaderboards.
  • Parallel Map-Reduce running cuts a 200-task suite from ~40 hours to ~2.
  • Regression gates stop P0-critical drops even when the aggregate score rises.
  • Model-version pinning and auditable reports are what make a promotion gate real.

By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.

Build a Coding-Agent Benchmarking Workflow with SWE-Bench Pro Regression Gates

Every AI platform team in 2026 has the same problem: your coding agent aced the demo, so you promoted it to production — and three weeks later it regressed on a class of tasks you never tested, and your merge rate silently collapsed. The reason is structural. Agent quality is a moving target: a model update, a system-prompt tweak, a new tool-version, or a changed evaluation harness can shift pass rates 10 points overnight. If you are not benchmarking continuously, you are deploying on vibes.

This guide builds a Coding-Agent Benchmarking Workflow in LangGraph that answers one question before any agent — or any new model version of an existing agent — gets promoted: does it still pass the bar on the tasks that matter? It constructs a golden task suite, runs agents across that suite in parallel (Map-Reduce style), scores each run, computes cost-per-task, diffs the results against the pinned baseline (regression gates), generates a regression report, and ends in a hard go/no-go promotion gate. No promotion, no merge, no rollout, no gate bypass.

The pattern is the agent-era equivalent of SWE-bench CI. If you are newer to these orchestration shapes, our AI workflows hub covers the full catalog, and the MCP directory lists the task-runner tool servers you'll wire into the harness. To track the model-version churn that makes this necessary, latest AI news is updated daily.

The Case for an Agent Benchmark Loop

SWE-bench gave the field a fixed set of real GitHub issues to grade agents against. "SWE-bench Pro" — the operational discipline — takes the idea and makes it continuous and yours:

  1. Your tasks, not the public set. Public benchmarks optimize for leaderboards. Your golden suite is built from your repo's resolved issues, your own regressions, and your own security patterns.
  2. Regression gates, not just scores. The question isn't "is the new version better than the old?" — it's "did it get worse on anything that shipped to customers?" You must catch a drop on critical task classes even when the aggregate score goes up.
  3. Cost per task as a first-class metric. A 3-point pass-rate gain that costs 4x per task is a business decision, not an engineering decision. The workflow reports it either way.
  4. Model-version pinning. Every run records the exact agent version, model ID, harness commit, and prompt hash. Unpinned benchmarks are un-auditable, and un-auditable gates get bypassed.

The Workflow at a Glance

flowchart TD
    A[START] --> B[task_builder]
    B --> C[parallel_runner]
    C --"fan-out"--> W1[worker_task_0]
    C --"fan-out"--> W2[worker_task_1]
    C --"fan-out"--> W3[worker_task_N]
    W1 --> R[(Results Ledger)]
    W2 --> R
    W3 --> R
    R --> D[scorer]
    D --> E{regression gate}
    E --"no critical regression + pass rate >= threshold"--> F[regression_report_generator]
    E --"critical regression"--> F
    F --> G[promotion_gate]
    G --"GO"--> H[promote / rollout]
    G --"NO-GO"--> I[hold + flag to team]
    H --> J[END]
    I --> J

The map is the parallel_runner (fan-out of worker nodes, one per task), the reduce is the scorer (aggregating into pass-rate, cost, and per-class breakdowns). Everything else — gates and reports — hangs off that aggregation.

Node by Node

task_builder

Builds the golden suite from three sources: a curated golden_tasks.jsonl (hand-marked, high value), a sample of recently fixed issues pulled from your tracker, and regression-prone classes flagged in past reports. Each task has a criticality class (P0-critical, P1, P2), an acceptance command (the test that must pass), and a max runtime. The builder pins the suite to a commit hash so comparisons across runs are apples-to-apples.

parallel_runner (Map-Reduce)

Fans out one worker node per task — each worker boots a clean fixture environment, invokes the agent under test, applies the produced patch, and runs the acceptance command. Because workers are independent, this is trivially parallel; a 200-task suite that took 40 hours serial drops to ~2 hours at 20-way concurrency. This is the single biggest operational win in the workflow, and LangGraph's fan-out/join (Send/Reduce) makes it a few lines instead of a job-scheduler project.

scorer

The reduce step. Aggregates raw worker results into: overall pass rate, pass rate by criticality class, cost per task and per class, mean iterations-to-solve, and the 90th-percentile latency. It also computes the delta vs the pinned baseline (the last approved agent/model version) — the input to the regression gate.

regression_report_generator

Produces the artifact humans actually read: a table of per-class pass rates (new vs baseline), a ranked list of regressed tasks with the failing test output, cost deltas, and the exact versions under test. This report is what gets attached to a rollout ticket — it makes the promotion decision auditable, which is what separates a gate from a checkbox.

promotion_gate

The go/no-go decision. Default logic: NO-GO if any P0-critical task regressed, if overall pass rate is below the floor threshold, or if cost-per-task exploded beyond the ceiling. Otherwise GO. The gate is configurable per team, but the shape — hard stop on critical regression, automatic hold, artifact attached — never varies.

The Multi-File Implementation

# .env
BENCHMARK_REPO=/opt/agent-bench
FIXTURE_REPO_URL=git@github.com:org/core-service.git
GOLDEN_TASKS=config/golden_tasks.jsonl
MAX_PARALLEL_WORKERS=20
WORKER_TIMEOUT_S=1800
PASS_RATE_FLOOR=0.70
COST_PER_TASK_CEILING_USD=1.50
PINNED_MODEL_ID=frontier-code-2026.08.04
PINNED_HARNESS_COMMIT=$(git -C . rev-parse HEAD)
REPORT_OUTPUT=reports/
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00/B00/xxxx
# ============================================================
# schemas.py
# ============================================================
from __future__ import annotations
import datetime as dt
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field


class Criticality(str, Enum):
    P0_CRITICAL = "P0-critical"
    P1 = "P1"
    P2 = "P2"


class GoldenTask(BaseModel):
    id: str
    title: str
    criticality: Criticality
    setup_cmd: str = ""
    patch: str = ""                    # agent's output for this task
    acceptance_cmd: str = Field(..., description="test that must pass")
    max_runtime_s: int = 1800
    fixture_ref: str = ""


class WorkerResult(BaseModel):
    task_id: str
    passed: bool
    iterations: int
    latency_s: float
    cost_usd: float
    log_tail: str = ""
    error: Optional[str] = None


class SuiteResult(BaseModel):
    run_id: str
    started_at: dt.datetime
    agent_version: str
    model_id: str
    harness_commit: str
    total_tasks: int
    passed: int
    pass_rate: float
    cost_per_task_usd: float
    by_criticality: dict[str, dict] = Field(default_factory=dict)
    deltas_vs_baseline: dict[str, float] = Field(default_factory=dict)
    regressed_tasks: list[str] = Field(default_factory=list)


class PromotionDecision(BaseModel):
    decision: str = "NO-GO"            # GO | NO-GO
    reasons: list[str] = Field(default_factory=list)
    report_path: str = ""


class BenchState(BaseModel):
    tasks: list[GoldenTask] = Field(default_factory=list)
    results: dict[str, WorkerResult] = Field(default_factory=dict)   # reduce field
    suite: Optional[SuiteResult] = None
    decision: Optional[PromotionDecision] = None
    baseline: Optional[SuiteResult] = None
    attempts: int = 0
    last_error: Optional[str] = None
# ============================================================
# tools.py — environment, agent invocation, cost tracking
# ============================================================
from __future__ import annotations
import asyncio, hashlib, json, os, subprocess, time
from schemas import GoldenTask, WorkerResult

WORKER_TIMEOUT_S = int(os.getenv("WORKER_TIMEOUT_S", "1800"))


def backoff(attempt: int) -> float:
    import random
    return min(2 ** attempt, 30.0) * (0.5 + random.random() / 2)


def provision_fixture(task: GoldenTask) -> str:
    """Clean checkout of the pinned repo at the fixture ref. Idempotent."""
    ref = task.fixture_ref or os.getenv("PINNED_MODEL_ID", "HEAD")
    workdir = f"/tmp/bench-{hashlib.md5(task.id.encode()).hexdigest()[:8]}"
    if not os.path.isdir(workdir):
        subprocess.run(["git", "clone", "-q", os.environ["FIXTURE_REPO_URL"], workdir],
                       check=True, timeout=300)
    subprocess.run(["git", "-C", workdir, "checkout", "-q", ref], check=True, timeout=60)
    if task.setup_cmd:
        subprocess.run(["sh", "-c", task.setup_cmd], cwd=workdir, check=True, timeout=300)
    return workdir


async def run_worker(task: GoldenTask, agent_cmd: str,
                     budget_usd: float) -> WorkerResult:
    """One golden task, one environment, one acceptance run. Retried 2x on infra errors."""
    for attempt in range(2):
        try:
            wd = provision_fixture(task)
            started = time.monotonic()
            proc = await asyncio.create_subprocess_shell(
                f"{agent_cmd} --task {task.id} --patch-out /tmp/{task.id}.patch",
                cwd=wd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
            )
            try:
                out, _ = await asyncio.wait_for(proc.communicate(), timeout=WORKER_TIMEOUT_S)
            except asyncio.TimeoutError:
                proc.kill()
                return WorkerResult(task_id=task.id, passed=False, iterations=0,
                                    latency_s=WORKER_TIMEOUT_S, cost_usd=budget_usd,
                                    log_tail="worker timeout", error="timeout")

            patch = f"/tmp/{task.id}.patch"
            accepted = subprocess.run(["sh", "-c", task.acceptance_cmd],
                                      cwd=wd, capture_output=True, text=True,
                                      timeout=int(task.max_runtime_s))
            return WorkerResult(
                task_id=task.id,
                passed=accepted.returncode == 0,
                iterations=int(proc.returncode) if proc.returncode else 1,
                latency_s=time.monotonic() - started,
                cost_usd=budget_usd * (0.5 if accepted.returncode == 0 else 1.0),
                log_tail=out.decode()[-2000:],
            )
        except (subprocess.CalledProcessError, FileNotFoundError):
            if attempt == 1:
                return WorkerResult(task_id=task.id, passed=False, iterations=0,
                                    latency_s=0.0, cost_usd=budget_usd,
                                    log_tail="fixture provisioning failed", error="infra")
            await asyncio.sleep(backoff(attempt))


def load_golden_tasks() -> list[GoldenTask]:
    path = os.environ["GOLDEN_TASKS"]
    tasks = []
    with open(path) as fh:
        for line in fh:
            if line.strip():
                tasks.append(GoldenTask(**json.loads(line)))
    return tasks
# ============================================================
# graph.py
# ============================================================
from __future__ import annotations
import json, os
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from schemas import (BenchState, GoldenTask, WorkerResult, SuiteResult,
                     PromotionDecision, Criticality)
import tools

FLOOR = float(os.getenv("PASS_RATE_FLOOR", "0.70"))
COST_CEILING = float(os.getenv("COST_PER_TASK_CEILING_USD", "1.50"))


async def task_builder(state: BenchState) -> dict:
    tasks = tools.load_golden_tasks()
    return {"tasks": tasks}


def fan_out(state: BenchState) -> list[Send]:
    """Map step: one worker per golden task, bounded by the parallel cap."""
    cap = int(os.getenv("MAX_PARALLEL_WORKERS", "20"))
    agent_cmd = os.getenv("AGENT_CMD", "agent-run")
    budget = COST_CEILING
    return [Send("worker_task", {"task": t, "agent_cmd": agent_cmd,
                                 "budget_usd": budget})
            for t in state.tasks[:cap]]


def reduce_results(left: dict, right: dict) -> dict:
    """Reduce step for the shared results ledger."""
    left.update(right)
    return left


async def worker_task(payload: dict) -> dict:
    task: GoldenTask = payload["task"]
    result = await tools.run_worker(task, payload["agent_cmd"], payload["budget_usd"])
    return {"results": {task.id: result}}


async def scorer(state: BenchState) -> dict:
    results = state.results
    passed = sum(1 for r in results.values() if r.passed)
    total = max(len(results), 1)
    by_crit: dict[str, dict] = {}
    for c in Criticality:
        subset = [r for t, r in results.items()
                  if next(x for x in state.tasks if x.id == t).criticality == c]
        if subset:
            by_crit[c.value] = {
                "passed": sum(1 for r in subset if r.passed),
                "total": len(subset),
                "rate": sum(1 for r in subset if r.passed) / len(subset),
            }
    suite = SuiteResult(
        run_id=f"run-{state.attempts}",
        started_at=__import__("datetime").datetime.utcnow(),
        agent_version=os.getenv("AGENT_VERSION", "dev"),
        model_id=os.getenv("PINNED_MODEL_ID", "unknown"),
        harness_commit=os.getenv("PINNED_HARNESS_COMMIT", "unknown"),
        total_tasks=total,
        passed=passed,
        pass_rate=passed / total,
        cost_per_task_usd=sum(r.cost_usd for r in results.values()) / total,
        by_criticality=by_crit,
        regressed_tasks=[t for t in results if not results[t].passed],
    )
    return {"suite": suite}


def regression_check(state: BenchState) -> str:
    suite = state.suite
    reasons = []
    for c in (Criticality.P0_CRITICAL,):
        rate = suite.by_criticality.get(c.value, {}).get("rate", 1.0)
        if rate < 1.0:
            reasons.append(f"{c.value} pass rate {rate:.0%} < 100%")
    if suite.pass_rate < FLOOR:
        reasons.append(f"overall pass rate {suite.pass_rate:.0%} < floor {FLOOR:.0%}")
    if suite.cost_per_task_usd > COST_CEILING:
        reasons.append(f"cost/task ${suite.cost_per_task_usd:.2f} > ceiling ${COST_CEILING:.2f}")
    # -> 'report' either way; the gate uses the reasons list.
    state.suite.regressed_tasks = suite.regressed_tasks
    state.suite.deltas_vs_baseline = _deltas(suite, state.baseline)
    return "report"


async def regression_report_generator(state: BenchState) -> dict:
    suite = state.suite
    lines = [
        f"# Bench Report {suite.run_id}",
        f"model: {suite.model_id} | harness: {suite.harness_commit[:8]}",
        f"pass rate: {suite.pass_rate:.0%} ({suite.passed}/{suite.total_tasks})",
        f"cost/task: ${suite.cost_per_task_usd:.2f}",
        "## by criticality",
    ]
    for c, v in suite.by_criticality.items():
        lines.append(f"- {c}: {v['rate']:.0%} ({v['passed']}/{v['total']})")
    lines.append("## regressed tasks")
    lines += [f"- {t}" for t in suite.regressed_tasks]
    os.makedirs(os.environ["REPORT_OUTPUT"], exist_ok=True)
    path = f"{os.environ['REPORT_OUTPUT']}/{suite.run_id}.md"
    with open(path, "w") as fh:
        fh.write("
".join(lines))
    return {"decision": PromotionDecision(decision="NO-GO", reasons=["pending"],
                                         report_path=path)}


async def promotion_gate(state: BenchState) -> dict:
    suite, decision = state.suite, state.decision
    reasons = []
    p0 = suite.by_criticality.get(Criticality.P0_CRITICAL.value, {}).get("rate", 1.0)
    if p0 < 1.0:
        reasons.append("P0-critical regression detected — promotion blocked")
    if suite.pass_rate < FLOOR:
        reasons.append(f"pass rate {suite.pass_rate:.0%} below floor {FLOOR:.0%}")
    if suite.cost_per_task_usd > COST_CEILING:
        reasons.append(f"cost/task ${suite.cost_per_task_usd:.2f} above ceiling")
    go = not reasons
    decision = PromotionDecision(
        decision="GO" if go else "NO-GO",
        reasons=reasons or ["all thresholds satisfied"],
        report_path=decision.report_path,
    )
    print(f"[gate] {decision.decision}: {decision.reasons}")
    return {"decision": decision}


builder = StateGraph(BenchState)
builder.add_node("task_builder", task_builder)
builder.add_node("worker_task", worker_task)
builder.add_node("scorer", scorer)
builder.add_node("regression_report_generator", regression_report_generator)
builder.add_node("promotion_gate", promotion_gate)

builder.add_edge(START, "task_builder")
builder.add_conditional_edges("task_builder", fan_out, ["worker_task"])
builder.add_edges(["worker_task"], "scorer", reducer=reduce_results)
builder.add_edge("scorer", "regression_report_generator")
builder.add_edge("regression_report_generator", "promotion_gate")
builder.add_edge("promotion_gate", END)

benchmark = builder.compile()
# ============================================================
# main.py
# ============================================================
from __future__ import annotations
import asyncio, os
from schemas import BenchState
from graph import benchmark


async def main() -> None:
    # Baseline pin: the last GO-approved suite is loaded from disk.
    baseline = None
    if os.path.exists("reports/last_approved.json"):
        baseline = __import__("json").load(open("reports/last_approved.json"))

    result = await benchmark.ainvoke(BenchState(baseline=baseline))
    decision = result["decision"]
    suite = result["suite"]
    print(f"run_id={suite.run_id} pass_rate={suite.pass_rate:.0%} "
          f"cost/task=${suite.cost_per_task_usd:.2f}")
    print(f"GATE: {decision.decision}")
    for reason in decision.reasons:
        print(" -", reason)
    if decision.decision == "GO":
        print("Promote agent to production shadow rollout.")
    else:
        print("Hold promotion. Attach report to follow-up ticket.")


if __name__ == "__main__":
    asyncio.run(main())

Retry Rules & Error Handling

Benchmarking must never let infrastructure noise masquerade as a score. Every infra-level failure is retried; every agent-level failure is recorded, not retried silently:

Failure mode Backoff / retry Handling Gate impact
Fixture clone/checkout fails exp. backoff 1s→8s, 2 attempts mark task infra-failed excluded from score, flagged
Worker timeout (task > max_runtime) 0 retries record as FAIL with error=timeout counts as regression
Acceptance command crashes 1 rerun of acceptance only rerun, keep first result if pass counts against pass rate
Harness/worker node dies supervisor re-fans-out that task 1 retry with clean env excluded if infra-only
Scorer/report generator error exp. backoff, 3 attempts regenerate from results ledger gate defaults to NO-GO

The golden rule: a NO-GO default on any orchestration failure. If the harness cannot prove the agent passed, the agent does not promote. Timeouts count as failures (never auto-excluded), and only infra-tagged results are dropped from scoring — and always shown in the report so nobody hides a broken harness behind "pass rates."

Thresholds & Go/No-Go Matrix

Condition Gate outcome Reasoning
P0-critical pass rate < 100% NO-GO one critical regression blocks promotion, period
Overall pass rate ≥ floor, no P0 drop GO baseline behavior preserved within budget
Pass rate up, cost/task > ceiling NO-GO business gate: unaffordable quality is still unaffordable
Pass rate below floor, cost fine NO-GO quality floor is non-negotiable
Harness failed >5% of tasks NO-GO un-auditable run cannot authorize a promotion
Metric Baseline (approved) Candidate (model v2) Delta
Overall pass rate 78% 81% +3 pts ✅
P0-critical pass rate 100% 96% −4 pts ❌
Cost / task $0.40 $1.20 +$0.80 ⚠️
Median solve latency 210s 150s −60s ✅

Run this on every candidate — new agent, new model version, new system prompt, new tool release — and keep the last-approved suite as your pinned baseline. That is the whole loop: build tasks → parallel-run → score → diff against baseline → gate → report. Do it every release and promotion becomes a number on a report instead of a gut call, which is exactly how you ship agents you can actually sleep through.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

Frequently Asked Questions
The operational discipline of continuous agent benchmarking: build a golden suite from your own resolved issues and regressions, grade candidates against it, and block promotion on P0-critical drops or pass rates below a configured floor — even when the aggregate score improves.
Each golden task runs in an independent fixture environment, so fan-out is trivially parallel. A 200-task suite that takes roughly 40 hours serial drops to about 2 hours at 20-way concurrency — which is what makes continuous benchmarking affordable at all.
Recording the exact agent version, model ID, harness commit, and prompt hash for every run. Unpinned benchmarks are un-auditable, and un-auditable gates get bypassed. Pinning is what makes a go/no-go decision defensible to a review board.
Each worker returns the tokens spent on its golden task; the scorer aggregates into cost-per-task and cost-by-criticality-class. A pass-rate gain that costs 4x per task becomes an explicit business decision at the promotion gate, not an engineering accident.
Any P0-critical regression, overall pass rate below the configured floor, cost per task above the ceiling, or a harness that failed more than ~5% of tasks. On orchestration failure the default is NO-GO — if the harness can't prove the agent passed, the agent doesn't promote.
Deepak Bagada
Author Profile

Deepak Bagada

CEO, SaaSNext

Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.

Related Intelligence Analysis

Research Breakdown AI Workflows

The Step-by-Step Guide to Automating Meeting Tasks with Whisper

You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...

Deepak Bagada Deepak Bagada
9m read
Research Breakdown AI Workflows

Lovable AI UI-to-Code Pipeline: 2026 Tutorial

Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...

Deepak Bagada Deepak Bagada
8m read
Breaking AI Workflows

Claude Code's New Browser: 5 Workflows That Save Hours Daily

Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...

Deepak Bagada Deepak Bagada
12m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc