GNL
Docs menu
Studio@gnldev/studio

Eval gate (promote governance)

No suite pass, no promote (412).

What it's for / when to use it#

With Agent versioning, an agent can have several versions kept in the journal, and whichever version you choose is activated with promote. But if the decision to "ship the new system prompt/model to production" is left to human discretion, there's a regression risk. evalGate automates this: when createStudioApp is given a { datasetId, minAvg }, every promote request FIRST runs that eval dataset suite; if not all of the suite's aggregate scores clear the minAvg threshold, promote is rejected with 412 — the version doesn't go active.

Typical scenario: you define a regression dataset for your production agent (e.g. frequently asked questions + expected answers); when an engineer adds a new system prompt version and tries to promote it, Studio automatically blocks it if the suite doesn't pass — the decision itself (passed/failed + aggregate scores) is recorded in the audit log as an agent.gate event, leaving a permanent trail of who tried what and when.

Setup / import#

evalGate isn't a separate package — it's an option passed to @gnldev/studio's createStudioApp (or createStudioApi) call. For it to activate, you MUST supply the same call with a datasets (StudioDatasets contract) — without datasets, a promote request returns 501.

import
import { createStudioApp } from '@gnldev/studio';
import type { StudioDatasets, EvalDatasetResultLike } from '@gnldev/studio';
import { evalDataset } from '@gnldev/evals';

Step-by-step usage#

1) Implement the StudioDatasets contract — the Studio core doesn't depend on @gnldev/evals; inside datasets.run(id) you call evalDataset yourself and return a result that's structurally compatible with EvalDatasetResultLike:

datasets provider
const datasets: StudioDatasets = {
  list: () => [{ id: 'regression', cases: 12, description: 'Frequently asked questions' }],
  run: async (id) => {
    const report = await evalDataset({
      dataset: regressionDataset, // { id: 'regression', cases: [...] }
      run: async (input, { runId }) => gnl.run('starwars', { runId, prompt: input }),
      scorers: [exactMatch()],
      journal,
    });
    return report; // { datasetId, cases, aggregate }
  },
};

2) Give evalGate to createStudioApp — datasetId determines which suite runs, minAvg (default 0.5) sets the passing threshold:

Set up Studio with the eval gate
app.route('/studio', createStudioApp({
  reader: toJournal(storage.runs),
  gnl: createStudioRunner(gnl, config, { toJsonSchema: aiToolSchema }),
  datasets,                                     // required without it evalGate returns 501
  evalGate: { datasetId: 'regression', minAvg: 0.7 },
  auth,
}));

3) The promote request now goes through the gate — when POST /managed-agents/:name/promote is called, Studio first runs datasets.run('regression'); if any aggregate score is below 0.7 (`failing`) the request is rejected with 412, otherwise the version is activated:

promote — gate FAILED
POST /studio/api/managed-agents/starwars/promote
{ "version": 3 }

// 412 Precondition Failed
{
  "error": "the eval gate FAILED: exact-match=0.42<0.7 — promotion refused",
  "aggregate": { "exact-match": 0.42 }
}
// an 'agent.gate' event lands in the audit log: { version: 3, datasetId: 'regression', minAvg: 0.7, aggregate, passed: false }
promote — gate PASSED
POST /studio/api/managed-agents/starwars/promote
{ "version": 3 }

// 200 OK
{ "ok": true, "name": "starwars", "active": 3, "previous": 2 }
// 'agent.gate' (passed: true) lands in the audit log first, then 'agent.promote'

API reference#

typeStudioAppOptions.evalGate

{ datasetId: string; minAvg?: number } — if given, this dataset suite runs before every promote; ANY aggregate score below minAvg (default 0.5) rejects the promote with 412.

typeStudioDatasets

{ list(): DatasetMeta[]; run(id, opts?): EvalDatasetResultLike } — the dataset contract supplied by the host; evalGate and /datasets/:id/run use it.

typeEvalDatasetResultLike

{ datasetId; cases: { caseId, output, scores }[]; aggregate: Record<string, number> } — structurally compatible with @gnldev/evals' evalDataset output.

constPOST /managed-agents/:name/promote

Called with a { version } body. If evalGate is active, the suite runs: if ANY aggregate score is below minAvg, it returns 412, otherwise the version is activated. Returns 501 if datasets isn't provided.

Prerequisite
When evalGate is set but the datasets option isn't given, a promote request returns 501 ("evalGate is configured but the datasets option is missing") — the gate doesn't silently disable itself, promote is blocked entirely.
Guarantee
The gate decision (pass OR fail), regardless of the suite's outcome, is ALWAYS written to audit as an agent.gate event — even a rejected promote attempt leaves a permanent trail along with its aggregate scores.
Related
You can also run the suite by hand: POST /datasets/:id/run calls the same datasets.run — see evaluation: scorers &amp; LLM-judge.