evalkit
AGENTS.md14 starsChanged 13 days agoApache-2.0View on GitHub
Use this vocabulary everywhere — code, specs, PRs, UI copy.
- Reads credentials
- Installs packages
- Commits and pushes
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository6 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- authoring-eval-casesSkill
- connect-tracesSkill
- eval-driven-developmentSkill
- evalkit-deploymentSkill
- evalkit-usageSkill
The file
We split this AGENTS.md into the rules it gives an agent
8 rules. About 0 are common, 2 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 5
Use this vocabulary everywhere — code, specs, PRs, UI copy.
- Very specific: line 104
Only store/blob.py imports obstore.
- Very specific: line 132
Run the server and UI locally (see "Local server".
- Very specific: line 135
Add them to the PR description under "Screenshots" with gh pr edit <n> --body-file body.md --attach ./shot.png ...: write !caption in …
- Very specific: line 140
No browser alert(), confirm() or prompt() in new UI code: use inline fields or dialogs.
- Very specific: line 175
Keep the folder path, the extensions export, UiExtensions and SIGNED_IN stable: other packages build against them.
- Very specific: line 191
When code changed (src/, ui/, docs/user/, skills/, contracts/, packaging, Dockerfile), it builds the server image and pushes it to …
- Distinctive: line 246
No new docstrings or inline comments unless they state a constraint the code cannot show.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
evalkit — agent notes
Terms
Use this vocabulary everywhere — code, specs, PRs, UI copy.
| Term | Meaning |
| Client | Any team that runs its agents through evalkit. |
| Agent harness | The client's agent system: their agent + serving infra. Not evalkit code. |
| Harness API | The HTTP contract the client's harness implements so evalkit can drive it. |
| Custom Evaluator API | The HTTP contract the client implements to expose custom (whitebox) evaluators and gauges. |
| Case | The identity of one eval question, shared by every workspace: the YAML id. |
| Case version | One immutable content of a case (input, expected behaviour, evaluator config), identified by (case, content_hash). |
| Dataset case | Which version of a case a dataset currently holds. |
| Suite | A directory of case YAML synced into one dataset with evalkit sync. The directory name is the dataset name. |
| Dataset | A named set of cases in the store, synced from YAML (kind suite). Every dataset belongs to a workspace. |
| Sync | One write of case YAML into a dataset, recorded as a dataset_syncs row. |
| Workspace | A named namespace for datasets, agents and remote evaluators. Either personal or shared. |
| Personal workspace | A workspace owned by one user, created with the user and named after them (@akshat). |
| Shared workspace | A workspace with no owner. Admins create it, e.g. @dev, @prod. There is no special canonical workspace. |
| Member | A writer or reviewer grant on a shared workspace. |
| Shared agent | An agent in a shared workspace. The server drives it. |
| Reference | How a dataset, agent or evaluator is named: name@workspace. The API and UI always use the full form; the CLI also takes a bare name, meaning your personal workspace. |
| Local run | An experiment whose agent is in a personal workspace: the user's CLI drives the harness, runs evaluators, and writes results to the server. Judge-using built-ins run on the server unless the run brings its own judge key. |
| Server run | An experiment whose agent is shared: the server's own runner drives it. |
| Developer config | evalkit.yaml in the client's repo: the credentials: map (keys for personal agents and evaluators and the judge's LLM provider), the cases / fixtures directories. Keys come from env or a .env beside it. |
| Experiment | One run of one dataset against one agent (× N repetitions). |
| Run | One case executed once against the agent, in a fresh session. |
| App run | A run of an agent outside any experiment: production, staging, or a developer using the agent by hand. Never "production run" when you mean "not an eval run". |
| Eval run | A run that answers one (experiment, case, repetition): an agent run linked from an experiment run. |
| Agent run | One execution of an agent: answer, status, trajectory, metrics. Owns the trajectory. |
| Experiment run | Links one (experiment, case, repetition) to the agent run that answers it. A resumed run points at the newest attempt. |
| Lease | A time-limited claim on a running experiment, renewed while its runner is alive. The server reaps experiments whose lease expires. |
| Trajectory | The full record of a run: messages, reasoning, tool calls/results, tokens, timing. Format: ATIF. Never "transcript". |
| Capability | A feature the harness declares in its capabilities response (provides / accepts / limits). Drives gating. |
| Evaluator | A scoring function. Compares expected vs actual, returns score + explanation. |
| Remote evaluator | A client-hosted evaluator reached through the Custom Evaluator API, stored in a workspace (evalkit evaluators create). |
| Evaluation | One evaluator's output on one run: a score or a gauge reading. |
| Server evaluator | A built-in evaluator the evalkit server runs for a local or SDK run, over POST /api/experiment-runs/{id}/server-evaluations/{name}: one call per run per evaluator; the server scores from what it stored and returns rows, the runner publishes them. |
| Local-only evaluator | A built-in that always runs on the developer's machine (builtin_evaluator(..., local_only=True)): tool_trajectory_match, required_tools_called, max_response_time. |
| Scored on | Where an evaluation was computed: local (the CLI or SDK process) or server. A column on every evaluation, separate from its source. |
| Gauge | A measurement, not a score: tokens, cost, time, tool-call counts. |
| Judge | An LLM used by an evaluator to grade an answer. Chosen per experiment; the default judge applies when the run doesn't set one. Names exactly one of a registered LLM provider (judge-using built-ins run on the server with its stored key) or a base_url (bring your own key: they run locally). A server run's judge must be a provider. |
| LLM provider | An OpenAI-compatible endpoint evalkit may call: a name (openai), a base URL and a stored key. Today only judges use providers. Admins manage them (evalkit llm-providers). |
| Default judge | The judge (provider name, model, effort, limits) an experiment uses when it does not set its own. Set by an admin in the database (evalkit judge-default set). |
| Stored key | An API key saved in the database, encrypted with the server's encryption_key, never returned (reads show has_api_key). Shared agents, shared remote evaluators, LLM providers and trace sources have one. |
| Credential registry | The CLI and SDK lookup from an agent or evaluator reference to an API key, built from the credentials: map in evalkit.yaml (agents, evaluators). Used by local and SDK runs only; the server uses stored keys. A judge the run brings carries its own key (--judge-api-key-env, Judge(api_key=...)), never stored. |
| Bucket | A scorecard grouping of evaluator scores: content, trajectory, presentation, efficiency, custom. |
| Tag | A label on a case: from its YAML tags, its folder in the suite, or an overlay. evalkit run --tag (repeatable) runs the cases carrying any of the given tags. |
| Overlay | A client extension of a case in another suite: extra evaluator config attached by case id, scored only for the agents it names. |
| Review state | Unreviewed, Review requested, Changes requested or Approved. Derived from a case's current content hash and the latest decisive event at that hash; never stored as a column. Never filters a run: evalkit run executes every selected case. Approval in a promotion source promotes the case; personal datasets have no review state. |
| Reviewer | A member with the reviewer role on a shared workspace. Only reviewers approve or request changes there. |
| Changelog | The per-case list of events: synced, review requested, approved, changes requested. |
| Promotion | Approving a case in a suite of a promotion source adds that version, approved, to the same-named suite of the promotion target, with the source workspace's overlays for it. Recorded as a dataset_syncs row with source promotion. |
| Promotion source | A shared workspace an admin linked to a target (evalkit workspaces promote-to <source> <target>). One target per source. |
| Promotion target | The shared workspace that receives promoted cases. Fed by promotion only: evalkit sync and review decisions there are refused; comments stay open. One source per target, no chains. |
| Surface | The place a fix lands during EDD: a skill, tool description, prompt, or data source. |
| EDD | Eval-driven development: author case → run (red) → diagnose → fix agent → re-run (green). |
| Experiment runner | evalkit's own two-phase engine (never "harness"). |
| Phoenix | Arize Phoenix, usable as a trace source. |
| ATIF | Agent Trajectory Interchange Format — Harbor's open trajectory schema. |
| SDK run | An experiment the client's own code runs with evalkit.sdk.run_experiment. Always a local run: the client's process drives the task and writes runs and scores through the API. |
| Harness run | An experiment driven through the Harness API, by the server (shared agent) or the CLI (personal agent). |
| Code-driven agent | An agent without a harness_url: the client's own code invokes it, through the task run_experiment calls. Only SDK runs can run it. The agent is the client's, not evalkit's. |
| Task | The client's Python function run_experiment calls once per case and repetition. Takes the case input, returns the answer. |
| Collector | The server endpoint that receives OTel spans over OTLP/HTTP (/api/otel/v1/traces). |
| Agent token | A bearer token bound to one agent and scoped to the collector only, for deployed agents that cannot carry a person's token. |
| Raw span | A span exactly as the collector received it, stored verbatim in the spans table. |
| ESF | evalkit span format: the normalized span model (EsfSpan) every raw span is mapped into before conversion to ATIF. |
| Mapping | A per-agent JSON document that says which span attribute holds each ESF field, plus answer and exclude_spans. Every OTel agent has one, versioned draft → active → retired. The connect-traces skill ships openinference and gen_ai documents to start from. |
| Session | The set of spans that belong to one run, grouped by the session id attribute. One run is one session. |
| Settled | A session whose newest span arrived longer ago than otel.settle_seconds. The signal that no more spans are coming. |
| Pushed trajectory | An ATIF document a task returns directly, bypassing the collector. |
| Trajectory source | Where an agent's trajectories come from: atif (served by a harness or returned by a task) or otel (collected spans converted by evalkit). Recorded on the agent row and the experiment. |
| Trace source | A third-party trace store (Phoenix), created by an admin and used only to import history. Its API key is stored encrypted with the server's encryption_key. |
| Python evaluator | A custom evaluator written as a Python function and passed to run_experiment. The in-process twin of the Custom Evaluator API. |
| Try-it stack | The Compose kit in skills/evalkit-deployment/try/: server and Postgres judging with the developer's LLM (LLM_BASE_URL, LLM_API_KEY, LLM_MODEL) on host port EVALKIT_PORT (default 8020), plus a sample agent run through an SDK run with OTel traces. For trying evalkit on one machine; never a deployment. |
| Sample agent | The try-it stack's agent.py: a small agent that calls the developer's LLM with one tool and emits GenAI-convention spans. Example client code, not part of the package. |
| SSO | Single sign-on: a person signs in through the IdP; the UI and CLI are public OAuth clients and the server verifies the IdP's JWT access token on every request. |
| IdP | The identity provider: the OAuth authorization server and OpenID provider an organisation runs. Configured once per server in auth.oidc. |
| Access token | The IdP-issued JWT a client presents as bearer. Its aud names evalkit. Verified against the IdP's JWKS; no auth_tokens row. |
| Subject | The pair (issuer, sub) identifying one identity at one IdP; keys the users row with the tenant. |
| Machine user | A users row bound to a machine client's subject: no password, no email, kind is machine. CI authenticates as one with the client credentials grant. |
| Tenant | One organisation's slice of the database; the public repo has exactly one, id 1. |
Layout
src/evalkit/contracts/— pydantic models for the wire contracts (ATIF, Harness API, Custom Evaluator API, case YAML, ESF and the span mapping document)src/evalkit/core/— business logic: runner (prepare_experimentserver-side;drive_experiment/evaluate_experimentanywhere; the experiment state machine and lease), evaluators, gauges, importer, lint, scorecard, pull (core/pull.py, the per-run layoutevalkit runs pullwrites), run digests (core/run_digest.py, per-run and windowed metrics from ATIF), theBlobprotocol, judge, workspace references, access rules (core/access.py), review (core/review.py, the review state model);core/traces/holds the mapping engine, the ESF → ATIF converter, the settle rule, theSessionReaderprotocol and the Phoenix import adapter;core/python_eval.pyruns Python evaluatorssrc/evalkit/store/—store/base.pyholds the store models and the runner'sStoreprotocol;store/blob.pyis the obstoreBlobsrc/evalkit/client/— the runner'sStore,BlobandSessionReaderover the evalkit HTTP API (HttpStore,HttpBlobStore,HttpSessionReader), the syncSyncApi, the stored server login (client/connection.py) and the dataset sync helpers; used by the CLI and the SDKsrc/evalkit/sdk/—run_experiment/evaluate_experiment, task binding, the@evaluatordecorator andtracing.registersrc/evalkit/db/— Postgres and the server'sStore(PgStore): users, workspaces and members, tokens, agents, remote evaluators, trace sources, cases and case versions, datasets and syncs, overlays, fixture blobs, review events and comments, experiments, agent runs, experiment runs, evaluations, SQL scorecard aggregation, spans and mappings + alembicsrc/evalkit/api/— FastAPI server (api/otel.pyis the collector,api/traces.pysessions and mappings,api/tokens.pyagent tokens,api/runs.pyandapi/pull.pyan agent's runs,api/mcp/the read-only MCP server at/mcp);src/evalkit/cli/— typer CLI (local runs, thin HTTP calls for everything else)docs/user/— the user pages (Try it locally, Quickstart, Concepts and the web UI's docs pages). The UI imports these files directly, the wheel bundles them, andevalkit skills installcopies them toevalkit-docs/beside the skills, so skills link to../evalkit-docs/<page>.md; there is no second copy. Pages link to each other aspage.mdui/— React web UI, bundled into the wheel asevalkit/bundled/ui(paths.ui_root());skills/— distributable skillsmkdocs.yml,docs/site/— the GitHub Pages site: MkDocs Material overdocs/user/, plusdocs/site/sdk-reference.md(mkdocstrings), hooks, theme and overrides;mikeversions itdeploy/— Dockerfile (server image build; clients deploy via the evalkit-deployment skill's bundled compose);contracts/— generated OpenAPI/JSON Schemas
Layering rules
api → core → store;dbmay importcore,store/base.pyandcontracts;cli → client → core;sdk → client → core. Arrows never reverse;coreimports neithersdk,clientnorapi;sdkimports nothing fromcli.core/traceshas no database or HTTP-server imports; the runner reaches sessions only through theSessionReaderonRunnerDeps.- Only
store/blob.pyimports obstore.store/never importscoreordb.clientandclimay importstore/base.py(pure models and theStoreprotocol) but neverdb,store/blob, or any server-only package (server-only commands live behind theevalkit[server]extra boundary incli/server_commands.py). - The CLI never holds database or blob credentials: local runs reach the store and blob through the evalkit API. The server's API keys are stored keys, encrypted in the database with
encryption_key; the CLI's keys live inevalkit.yamlcredentials:and env. - The base package depends on
opentelemetry-apionly; the OTel SDK and the OTLP exporter are theevalkit[otel]extra,opentelemetry-protois inserver. - evalkit code has zero knowledge of any client's agent internals: no client tool names, paths, buckets, or env vars.
Commands
uv run pytest— tests (e2e tests excluded by default)uv run ruff check/uv run ruff format— lint/formatuv run mypy src/evalkit— typescd ui && npm test— UI testsuv run python scripts/gen_schemas.py— regenerate the OpenAPI contract docs undercontracts/(a drift test fails CI when they're stale)uv run --group docs mkdocs serve— the docs site on :8000 (mkdocs build --strictis the CI check)uv run python scripts/fetch_cost_map.py— fetch the LiteLLM cost map (gitignored; the Docker build and the wheel build fetch it, and the server fetches it at runtime when missing)
Local server
uv sync --extra server, a Postgres database, andskills/evalkit-deployment/config.example.yamlcopied toconfig.yamland filled in.uv run evalkit db upgrade --config config.yaml, thenEVALKIT_ADMIN_PASSWORD=<password> uv run evalkit serve --config config.yaml(API and UI on :8020). The password createsadminon the first start. It listens on 127.0.0.1; pass--host 0.0.0.0when an agent or harness in Docker must reach it throughhost.docker.internal.cd ui && npm run devserves a hot-reloading UI on :5180 that proxies/apito :8020.- Without a checkout:
pip install "evalkit-py[server]", thenevalkit init-server(creates the database, writesconfig.yaml+.env, migrates) andevalkit servein the folder it made. serveanddb upgradeload the.envbesideconfig.yaml; the environment wins.- A source checkout reports the plain semver;
GET /api/clireturns{"version", "package"}. - The try-it stack (
skills/evalkit-deployment/try/, walkthrough indocs/user/try-locally.md) builds the image from this checkout:docker compose up -d --wait --build. uv run python -m tests.e2e.servicesserves a fake harness, custom evaluator and judge on ports 18080, 19000 and 19090, and a fake identity provider on port 19100 (issuerhttp://127.0.0.1:19100, client idevalkit-ui, machine clientci/e2e-secret; seetests/e2e/README.md), for e2e runs against a built image.
UI changes in a PR
- Run the server and UI locally (see "Local server"; a throwaway Postgres,
dummy keys and the fake services in
python -m tests.e2e.servicesare enough). - Click through every page and flow the PR changes and take screenshots.
- Add them to the PR description under "Screenshots" with
gh pr edit <n> --body-file body.md --attach ./shot.png ...: writeinbody.mdand gh uploads each file to GitHub and rewrites the link. Never commit screenshots to the repo. tsc,vitestandvite buildpassing is not a UI test.- No browser
alert(),confirm()orprompt()in new UI code: use inline fields or dialogs.
Extending evalkit
Another package can serve evalkit with API routes and UI pages of its own.
API routes. Build the app with create_app(cfg, ui_dist=..., resolve_tenant=...),
then add plain FastAPI routes with app.include_router(...).
- The UI is served by FastAPI's
app.frontend(), which is tried only after every other route, so routes added aftercreate_appanswer GET. - evalkit's routes come first and win a path clash, including ones with
parameters such as
/api/agents/{ref}. Keep yours under a prefix of your own (/api/billing/...). - A route that needs a signed-in user is added with
dependencies=SIGNED_INfromevalkit.api.auth; without it the route is public.SIGNED_INcovers path operations (@router.getand the like), notrouter.add_route()or mounts. A handler that needs the user takesCurrentUser; an admin-only one takesAdminUser.
UI pages. The UI takes extra pages and nav items from
ui/src/extensions/index.tsx, which exports extensions of type
UiExtensions (ui/src/lib/extensions.ts). In evalkit it is empty.
- Clone evalkit at the tag
vX.Y.Zof theevalkit-pythe server runs, replace theui/src/extensions/folder with yours, build the UI there (npm ci && npx tsc -b && npx vite build) and passui/distasui_dist.tsc -bchecks your pages against evalkit's types. A Vite alias to a module outsideui/does not work: imports in that module, such asreact, do not resolve. routesrender for signed-in users inside the app shell.navitems join the top bar,moreNavthe More menu, andadminNavthe admin part of that menu.adminNavonly hides the item from non-admins; the page's endpoints must check admin themselves.- Keep the folder path, the
extensionsexport,UiExtensionsandSIGNED_INstable: other packages build against them.
CI
- Open pull requests as drafts (
gh pr create --draft). CI skips drafts, so run the tests locally and mark the PR ready (gh pr ready <number>) when it should be reviewed and run CI.
.github/workflows/ci.yml runs on PRs to develop/main (non-draft; jobs are
path-filtered against the PR base) and on pushes to those branches:
python—ruff check,ruff format --check,mypy src/evalkit,pytest -n autoui—npm ci,tsc -b,vitest run,vite buildinui/
Release
.github/workflows/publish.ymlruns on pushes todevelopandmain. When code changed (src/,ui/,docs/user/,skills/,contracts/, packaging, Dockerfile), it builds the server image and pushes it toghcr.io/inferedge-ai/evalkit, tagged with the full git sha and the branch name. It always builds the wheel (web UI included) as a workflow artifact, and onmain, when code changed, it publishes it to PyPI asevalkit-py(jobpypi, trusted publishing through thepypienvironment).mainonly: the image is also tagged with the version, and the workflow creates GitHub releasevX.Y.Zwith the wheel attached and the version'sCHANGELOG.mdsection as notes.- Deploying an image is the operator's job; nothing here deploys.
- Version string:
mainbuilds areMAJOR.MINOR.PATCH; every other build is the PEP 440 pre-releaseMAJOR.MINOR.PATCH.dev<run number>. Git holds the plain semver inpyproject.toml; the workflow stamps.devNwithuv versionbefore building the image and the wheel, so both share one version. Feature PRs never bump it:developbuilds carry the last released semver plus.devN. - Releasing: a PR into
developbumpspyproject.tomland renames the## Unreleasedchangelog section to the new version; then adevelop → mainPR (merge commit, not squash) ships it. CI'sversionjob blocks a PR intomainunless the version was bumped abovemain's and the changelog has that section. Entries that land under## Unreleasedwhile the release PR is open ship in that release without notes; move them into the version's section if they matter. Bump MAJOR when the CLI and server can no longer talk, MINOR for new commands, endpoints or fields, PATCH otherwise. While evalkit is on0.x, a MINOR difference also counts as incompatible: the CLI refuses every command exceptevalkit update --server <url>. - PyPI is the only package index.
GET /api/clireturns the server's version; every response carriesX-Evalkit-Version. The CLI compares it to its own version on every call, andevalkit updatereinstallsevalkit-py==<server version>from PyPI. - The docs site:
ci.yml'spythonjob runsmkdocs build --stricton PRs;publish.yml'sdocsjob deploys it withmiketo thegh-pagesbranch,devfromdevelopandX.Y.Z(aliaslatest) frommain.
Changelog
CHANGELOG.md has one section per released version plus ## Unreleased at
the top. It is a general record of what changed in each version, for anyone
reading it: users, operators, contributors. Every PR that changes behaviour,
an interface, a dependency, the schema, packaging or the build adds its
entry under ## Unreleased, in the same PR; the release PR renames that
section. An entry says what changed and, when it is not obvious, why; name
the command, flag, endpoint, field or config key so a reader can find it.
Note anything refused or different from before in the same entry. Test-only
changes and refactors with no visible effect do not need an entry.
Specs and plans
Design specs, implementation plans and other planning documents are not checked in to this repo. Share them in the pull request instead: paste them into the description or link to them.
Code style
- No new docstrings or inline comments unless they state a constraint the code cannot show. Code is the single source of truth. The one exception: the public
evalkit.sdkAPI (evalkit.sdk.__all__) has Google-style docstrings, because they are its published reference. - f-strings everywhere except stdlib logging (
%splaceholders); structlog uses plain kwargs. X | None, neverOptional[X]. No local imports. No try/except unless the spec calls for it (fail fast).- Manage deps with
uv add/uv remove; run tooling viauv run.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

