Skip to content

feat: move eval execution from GitHub Actions to Vercel Sandboxes - #192

Merged
mattrossman merged 42 commits into
mainfrom
mattrossman/ai-912-spike-vercel-sandbox-runner-for-evals
Aug 14, 2026
Merged

feat: move eval execution from GitHub Actions to Vercel Sandboxes#192
mattrossman merged 42 commits into
mainfrom
mattrossman/ai-912-spike-vercel-sandbox-runner-for-evals

Conversation

@mattrossman

@mattrossman mattrossman commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Moves eval execution from GitHub Actions to Vercel Sandboxes. The GitHub workflow remains for orchestrating sandboxes via a new run-vercel-evals.ts entrypoint. It uses p-limit and p-retry for concurrency limits and retry policies. Also considered effect, but our use case isn't very complex to justify the runtime.

Sandbox limits

As of Aug 5th, our Pro plan supports 10,000 concurrent sandboxes and 5,000 vCPU allocated / min with dynamic allocation quotas that ramp up during sustained usage.

I'm starting with a default of 250 concurrent sandboxes for the workflow, chosen as a round number similar to our existing 256 matrix job limit, but we can very likely increase this as our benchmark + attempt counts grow.

Note on Sandbox retries

Sandboxes creation is subject to dynamic quotas on vCPU allocation limits. The Sandbox SDK already implements a basic retry policy, but it's not the most forgiving, so we add our own exponential backoff retries on top.

Vercel project/creds

Repo is currently wired up to the evals-runner project on Vercel via VERCEL_PROJECT_ID, VERCEL_TOKEN, VERCEL_TEAM_ID, a carryover from the Vercel Workflows spike. This project is configured w/ the 3 provider API keys.

If desired, we could move sandbox usage back to the evals project since we're not deploying any new application entrypoints with this PR.

Job artifacts

Previously each matrix job in run-evals uploaded a separate results artifact which we'd then download and combine. Now, since there's only one run-evals job we upload a single combined raw-results artifact.

CleanShot 2026-08-13 at 20 44 50@2x

Job logs

I intentionally don't wait for command.logs() because it sometimes loses connection and drops output. Instead, I consume command.output('both') so you'll see the RUN and PASS/FAIL' appear only at the end of each sandbox's eval execution, which seems like a reasonable tradeoff.

Sample runs:

Results look normal after benchmark refresh (Preview):

CleanShot 2026-08-13 at 17 02 22@2x

How to test

I test by manually dispatching the refresh workflow against this branch, though it's a slow / compute heavy thing to refresh the full suite, so you can probably just review the existing runs or dispatch a smaller refresh.

Next steps

Currently, benchmark execution takes ~33 min, comparable to our latest refresh on the GitHub Actions matrix. From here, we're planning to parallelize attempts instead of giving agents 2 attempts in sequence. Depending on how many sandboxes this ends up spawning, it'll potentially decrease execution time as attempts can run in parallel.

Another optimization I haven't landed here is the warm boot trick from Pedro's #114, I'll likely revisit that as a follow up since it hasn't regressed overall benchmark duration yet.

Related:

Closes AI-912

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
evals Ignored Ignored Preview Aug 14, 2026 4:02pm

Request Review

@mattrossman mattrossman changed the title feat: run evals in Vercel Sandboxes feat: run evals in Vercel Sandboxes with p-limit / p-retry Aug 13, 2026
@mattrossman mattrossman changed the title feat: run evals in Vercel Sandboxes with p-limit / p-retry feat: Vercel Sandboxes runner p-limit / p-retry Aug 13, 2026
@mattrossman mattrossman changed the title feat: Vercel Sandboxes runner p-limit / p-retry feat: Vercel Sandboxes runner w/ p-limit / p-retry Aug 13, 2026
requireVercelCredentials and vercelCredentialsFromEnv checked the same
3 env vars separately, and the latter was re-derived from process.env
on every concurrent pair. Compute it once in runPairs and thread it
through instead.
4 was overly conservative for the actual benchmark suite (238 pairs
today, growing). 250 is validated against a real full-benchmark run
(#192) with clean completion and no permanent failures,
with the 429 rate-limit retries handling the initial creation burst.
@mattrossman
mattrossman marked this pull request as ready for review August 14, 2026 01:39
@mattrossman
mattrossman requested a review from a team August 14, 2026 01:39
Comment thread apps/framework/scripts/run-vercel-evals.ts Outdated
Comment thread apps/framework/scripts/run-vercel-evals.ts Outdated
Comment thread .github/workflows/eval-refresh.yml
Comment thread apps/framework/scripts/run-vercel-evals.ts
@Rodriguespn

Copy link
Copy Markdown
Contributor

nit: We should clean the run-evals-sandboxes and the other related Github labels that I added while working on #192

isRetryableSandboxCreateError treated every non-APIError as retryable,
so deterministic failures like a bad OIDC token got the full 12-attempt
backoff budget on top of the pair-level retry. Now it bails on known
credential errors, retries known network faults, and caps anything
unrecognized to a couple of attempts with a warning so gaps in the
categorization show up in logs.
… errors

createSandbox now rethrows a definitive 4xx or credential error as
p-retry's AbortError, which propagates through any pRetry call it
bubbles through. That keeps runPairs' pair-level retry from wasting
another attempt on a sandbox create that already failed for good.
vercelCredentialsFromEnv() now always requires explicit VERCEL_TOKEN,
VERCEL_TEAM_ID, and VERCEL_PROJECT_ID instead of deferring to the SDK's
OIDC resolution when VERCEL_OIDC_TOKEN happened to be set. That token
only appears locally as a stale leftover from vercel env pull on an
unrelated project (never used in CI, which has no id-token permission),
and pnpm eval:vercel doesn't even load .env.local where it would live.

Since Sandbox.create() now always gets full explicit credentials, the
SDK's local credential-resolution errors (bad/missing OIDC token) can
no longer be thrown, so the retry classifier's special-casing for them
is removed too.
Adds timeout-minutes: 60 to run-evals (was defaulting to GitHub's
360-min ceiling), based on a real 237-pair run finishing in ~28 min
under heavy retry contention. publish-results now runs even if
run-evals times out or partially fails (needs.run-evals.result !=
'skipped', with an explicit always() to bypass the implicit success()
gate), so completed pairs still get published instead of stranded in
a 3-day-retention artifact.

A new "Audit pair results" step diffs expected pairs against what
actually downloaded, posts a warning annotation and job summary on
any gap, skips the unattended scheduled auto-merge when partial, and
a final "Require complete results" step fails the job after
publishing so an incomplete refresh can't go unnoticed.

Also drops submodules: recursive from the run-evals/publish-results
checkouts: carryover from the old per-pair GH-hosted matrix jobs,
unneeded now that sandboxes clone and init their own submodules.
@mattrossman
mattrossman force-pushed the mattrossman/ai-912-spike-vercel-sandbox-runner-for-evals branch from 4bf8a84 to 117ec3a Compare August 14, 2026 15:16
Forces investigate-security-001-public-table to sleep 10 min so a
manual dispatch can produce a real mixed success/failure outcome to
verify the new eval-refresh.yml partial-results handling. Reverting
before this PR is done.
@mattrossman
mattrossman force-pushed the mattrossman/ai-912-spike-vercel-sandbox-runner-for-evals branch from 117ec3a to 489b380 Compare August 14, 2026 15:18
If every pair fails, run-evals never uploads a raw-results artifact at
all (nothing to upload), so Download raw results hard-errors and skips
every step after it, including the new Audit/Require steps meant to
report exactly this. continue-on-error lets those steps still run and
report clearly on a total failure instead of masking it as an
unrelated artifact-download error.
Live-tested the total-failure path: with 0/N pairs succeeding,
export-results legitimately errors on "no result files matched" and
nothing gets committed, but the warning/summary/error text all
unconditionally said "results were published" or "partial results
published" regardless. Adds a genuinely-all-missing branch to Audit
pair results and drops the false claim from Require complete results.
The eval-refresh.yml partial-results handling has been verified live
via manual dispatches. Removes the throwaway test-only delay.
@mattrossman

Copy link
Copy Markdown
Collaborator Author

We should clean the run-evals-sandboxes and the other related Github labels

@Rodriguespn I've removed the 2 sandbox related labels, along with the stale AI gateway opt-in label

@Rodriguespn Rodriguespn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for addressing the feedback 🙌 Great work adding a partial results recovery mechanism!

@mattrossman
mattrossman merged commit d5db72e into main Aug 14, 2026
7 checks passed
claude Bot pushed a commit that referenced this pull request Aug 14, 2026
Brings in a week of upstream work, most notably #192 (eval execution moved
from GitHub Actions to Vercel Sandboxes), #178 (claude-code MCP tool-name
normalization), #168 (RLS docs guide eval) and several results refreshes.

One conflict, in apps/framework/package.json: both sides extended the
`check` script. Upstream added `test:vercel-runner` for the new Vercel
runner tests; this branch added `test` to give the prompt-assembly tests a
runner. Both are kept:

  check: pnpm typecheck && pnpm test && pnpm test:framework && pnpm test:vercel-runner

The dependency block in that file merged cleanly (upstream re-sorted it and
added @vercel/sandbox, p-limit, p-retry and zod).

run-eval.ts, packages/core/src/agents/engine.ts and packages/core/src/index.ts
merged without conflict and both intents are intact:

- run-eval.ts now takes readFlag/positiveInteger from lib/cli-args.js
  (upstream hoisted the local readFlag out) while still assembling its prompt
  through buildSystemPrompt() and recording systemPrompt in the result JSON.
- engine.ts still stages no system-prompt file when the assembled prompt is
  empty, and additionally passes upstream's mcpServerNames to parseTranscript.
- index.ts keeps upstream's parser/transcript type changes alongside this
  branch's AgentRunArgs.systemPrompt and LocalStackSessionArgs.agent docs.

No new CLI harness landed upstream (still claude-code, codex, opencode), so
the `skills add --agent` list is unchanged. The new Vercel runner shells out
to `pnpm eval`, so it inherits this branch's prompt behaviour with no change.

apps/web/src/data/*eval-results.json and results/ are untouched by this
branch and the merge takes main's versions wholesale.

Refs AI-1034, #164

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants