Integrate an agent harness
This guide is for the team that builds a platform where people create and deploy agents (an agent harness) and wants every deployed agent scored, and a bad one kept from shipping. It covers four steps: install Evals.si beside the agent harness, connect the agent harness’s traces, gate deploys, and read the scores, then pipeline writes and retention. Each step has a runnable stand-in in examples/demo, where Deep Agents and DeepSeek Harness play the agent harness’s agents, an MLflow server plays its trace store, and a mock model keeps the run free and repeatable.
Evals.si has three doors, and an agent harness can use any mix:
| Door | The agent harness does | Section |
|---|---|---|
| Push | exports OpenTelemetry traces, or calls the API from its deploy pipeline | 2, 3 |
| Pull | already stores traces in MLflow, Phoenix or Langfuse: a trace source reads them and writes scores back | 2 |
| Embed | calls the SDK, including from a training loop | 3 |
1. Install beside the agent harness
Section titled “1. Install beside the agent harness”Evals.si installs from its own charts, in the agent harness’s namespace or its own. Every name starts with the release name, so releases can share a namespace (Names).
helm install evalsi-crds deploy/helm/evalsi-crds --set operator.namespace=studiohelm install evalsi deploy/helm/evalsi -n studio \ --set storage.postgres.dsnSecret.name=evalsi-postgres \ --set storage.s3.bucket=evalsi \ --set sandbox.pool.enabled=true --set sandbox.pool.ladder='{pod}'-
No cluster-admin? Install namespace-only: see Installing without cluster-admin. The sandbox pool on the pod rung needs nothing outside the namespace.
-
Any cluster shape. Every pod and Service takes tolerations, a node selector, labels and an image registry as values (Taints and service meshes). The demo overlays show kind, tainted nodes and Istio ambient.
-
A project per tenant. The bootstrap Job creates projects, API keys and credential grants from values, so an agent harness installs with no manual step (
policiesandwebhookstoo):bootstrap:enabled: trueprojects: [{name: studio}]apiKeys:- {name: studio-ingest, roles: {studio: [ingest]}, secret: {name: studio-ingest-key}}- {name: studio-ci, roles: {studio: [editor]}, secret: {name: studio-ci-key}}credentialGrants:- {env: STUDIO_MODEL_KEY, projects: [studio]}webhooks:- {name: studio, project: studio, url: "http://studio.studio.svc/hooks/evalsi", events: [run.finished, run.gate_failed], secret: {name: studio-evalsi-webhook}}# A default policy: every production trace that names its workflow is# scored, so a new workflow is scored from its first execution.policies:- name: studio-defaultproject: studioselector: '"workflow" in labels && labels["stage"] == "production"'stages:- evaluators: [{ref: tool-errors}, {ref: loop-detection}, {ref: builtin/latency}]window: 3600sThe keys are written to the Secrets named, once, and kept across upgrades. Policies here are written in their JSON field names (
window: 3600s, not1h). A credential grant lets a project’s runs name a worker’s environment variable (for a model key) without the key being in a spec.
2. Connect traces
Section titled “2. Connect traces”Push. Point the agent harness’s OpenTelemetry exporter at Evals.si with an ingest key bound to the project (OTel GenAI, OpenInference, OpenLLMetry and MLflow conventions are understood):
OTEL_EXPORTER_OTLP_ENDPOINT=https://evalsi.studio.svc:4318OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer $(kubectl get secret studio-ingest-key -o jsonpath='{.data.api_key}' | base64 -d)"The chart’s certificate comes from its internal CA (evalsi-server-tls, ca.crt); give the exporter that CA, or terminate TLS in your mesh. Traces land in the key’s project with its labels.
What a trace needs: an MLflow LangChain/LangGraph trace (what Deep Agents emits with mlflow.langchain.autolog() and MLflow’s OTLP exporter) or any GenAI-convention trace is read into a trajectory: model calls with their messages and token counts, tool calls with arguments and results, and errors. The demo’s recorded export (internal/ingest/testdata) is a test of exactly that. A CrewAI 1.x studio traced by MLflow needs mlflow.openai.autolog() beside mlflow.crewai.autolog() when its agents use CrewAI’s own OpenAI provider (the default), or its model calls are missing (seen with CrewAI 1.15.27 and MLflow 3.17.0). The agent frameworks guide covers CrewAI and the other first-cut frameworks. Agents that are CLIs, like dsh, don’t need tracing: their --json run events are the trajectory (output_format: dsh-json).
Pull. If the agent harness already keeps traces in MLflow, Phoenix or Langfuse, a trace source reads them without a second exporter, history included, and writes the scores back to the same traces (MLflow assessments, Phoenix annotations, Langfuse scores), so they show in the agent harness’s own UI:
apiVersion: evals.si/v1alpha1kind: TraceSourcemetadata: {name: studio-mlflow, labels: {evals.si/project: studio}}spec: connector: mlflow endpoint: http://mlflow.studio.svc:5000 locations: [studio-workflows] # the MLflow experiment, by name or ID backfill: {since: 720h} writeBack: {enabled: true}An in-cluster, plain-HTTP MLflow must be allowed in the chart (sources.allowHosts: [mlflow.studio.svc]), and a token, if it needs one, is a granted Secret (sources.secrets, credentials: {file: <secret>/<key>}). The demo does exactly this for the Deep Agents service (R6). Pulled traces carry the label source: studio-mlflow. MLflow 3.17.0, Phoenix 20.20.0 and self-hosted Langfuse 4.56.0 have run against real servers; Databricks and SageMaker are tested against their client code only. The trace sources guide says what is verified for each.
Then say what to score and how often, with a policy per environment:
apiVersion: evals.si/v1alpha1kind: OnlineEvalPolicymetadata: name: studio-agents labels: {evals.si/project: studio}spec: selector: 'labels["deployed"] == "true"' sampling: {rate: 0.1, always: ["error"]} stages: - evaluators: [{ref: builtin/latency}, {ref: tool-errors}, {ref: loop-detection}] window: 1hkubectl apply it, or evalsi policy apply -f. A policy whose evaluators need a judge is refused until the server has one (default_judge, or judge: in the policy).
Four templates for a harness’s deployed workflows are in examples/watch: harness-default (tool errors, loops, latency, tokens, on every production execution), harness-safety (PII and secrets, with alerts), harness-quality (a judged sample, after cheap checks) and harness-cost (cost and steps per execution). Each selects production traces that name their workflow.
SDK logging without OTel
Section titled “SDK logging without OTel”import evalsi
logger = evalsi.configure_logging("https://evalsi.studio.svc:4318", api_key=ingest_key)future = evalsi.log("question", "answer", metadata={"epoch": 2})logger.log_batch([{"input": "next question", "output": "next answer"}])evalsi.flush(timeout=10)trace_id = future.result()logger.close()Use the server CA through SSL_CERT_FILE. Configuration is explicit, or set
EVALSI_OTLP_ENDPOINT; no judge is selected or billed. Calls serialize locally
and return futures without waiting for network I/O. PyTorch tensors are detached
to CPU; batches accept dict records, pandas frames and Hugging Face datasets.
Queued and in-flight records share limits (1024 records/8 MiB by default).
Overflow, HTTP failures and partial rejection fail futures. flush and close
raise a failure once, to the call that sees it; records that succeed afterwards
do not make later calls fail, and configure_logging works after a failed
batch. Normal process exit drains the queue for at most exit_timeout seconds
(30 by default), so a dead endpoint cannot hold a finished program; records not
sent by then fail their futures. close(timeout=...) does the same on demand.
SIGKILL cannot flush. A forked child (a data-loader worker, say) does not inherit
the parent’s logger: its pending records fail, and evalsi.log starts a fresh
logger there from EVALSI_OTLP_ENDPOINT.
Non-finite metadata values (nan, inf) are sent as the text "NaN",
"Infinity" and "-Infinity", since JSON has no such numbers.
The byte cap applies after serialization, not to caller-owned tensors or
temporary serialization memory. log_batch returns one future per input row.
Metadata arrives in the trace’s Record after server credential/PII redaction.
Server-derived service/error fields remain authoritative. The server keeps at
most 256 keys of at most 128 characters from at most 64 KiB of metadata, and
ignores keys starting with evalsi., which are reserved. trace_id, when
supplied, is a nonzero 32-character lowercase hexadecimal ID.
3. Gate deploys
Section titled “3. Gate deploys”Before a workflow goes live, run it against a suite. The run spec is the same file as in the demos; the agent harness’s deploy step applies it and waits.
evalsi run -f small-repo-fixes.yaml --server $URL # exits 3 when a gate fails
# or on Kubernetes, with the same file:kubectl apply -n studio -f small-repo-fixes.yamluntil phase=$(kubectl get evalrun small-repo-fixes -n studio -o jsonpath='{.status.phase}'); \ [[ $phase =~ ^(Succeeded|Failed|Error|Cancelled)$ ]]; do sleep 10; done[ "$phase" = Succeeded ] # Failed: a gate did not passA pipeline that has the API but not kubectl uses evalsi gate, which runs a spec for one release and exits as evalsi run does (0 pass, 3 gate failed, 1 error):
evalsi gate -f support-bot-gate.yaml --server $URL --release v7 \ --target-url http://support-bot-v7.studio.svc:8080/invoke # the candidateA release is gated once: rerunning the same release (a pipeline retry) returns the same run, and the same release with a changed spec (--set trials=5, say) is refused, for the server’s 24-hour idempotency window. The run carries the label release.
A spec’s gates turn metrics into a verdict (task-success.pass^3 >= 0.8 means three trials in a row must pass in at least 80% of tasks). A failed gate makes evalsi run exit 3 and the EvalRun phase Failed, which blocks a pipeline step with no extra code. From a service, use the API or a webhook instead of a pipeline (webhooks): run.gate_failed is sent when a run ends with a failed gate, signed with HMAC, so the agent harness can mark the deployment blocked.
The demo plays this whole step for one workflow: deploy/deploy.sh stages a release as a candidate Service, runs its gate against it, and moves production to the release only when the run Succeeds; a regressed release exits 3 and production stays where it was (walkthrough). The kind e2e job runs it.
Embedded in the agent harness’s own Python, the same check is a call:
from evalsi.client import Client
with Client(url, api_key=key) as client: result = client.evaluate(records, ["task-success"], project="studio") # scored on the clusterRuns execute the agent in a sandbox per trial (the pod rung on Kubernetes), with the network denied except the hosts the spec allows, so a candidate agent cannot reach anything it was not given.
4. Read the scores
Section titled “4. Read the scores”-
The web UI at
/ui/on theevalsiService: runs, scores per metric, and each trial’s trajectory. -
MLflow. Add the sink and run scores appear as MLflow runs with metrics, beside the agent harness’s traces:
server:config:sinks:- mlflow: {tracking_uri: "http://mlflow.studio.svc:5000", experiment: evalsi} -
Prometheus and Grafana. The chart ships a dashboard and metrics; see Dashboards.
-
Per execution and per workflow, in the agent harness’s own UI.
GET /v1alpha1/scoresreturns the online scores of live traffic, pushed or pulled alike, one entry per trace (newest first) with its summary and its results grouped by policy, without the trace’s full record. Filter it withproject,service,policy,evaluator,since(RFC 3339) and any number oflabels=key=value.GET /v1alpha1/traces/{trace_id}/scoresreturns one execution’s. Label each workflow’s traces with the resource attributesevalsi.label.workflowandevalsi.label.version(or a trace source’s labels; with OpenTelemetry,OTEL_RESOURCE_ATTRIBUTES=evalsi.label.workflow=support-bot,evalsi.label.version=v7and no code change), and a workflow’s page is one query:Terminal window curl -H "Authorization: Bearer $KEY" \"$URL/v1alpha1/scores?project=studio&labels=workflow=support-bot&since=2026-10-01T00:00:00Z&page_size=50"Pass
next_page_tokenback aspage_tokenfor the next page; a page can be shorter thanpage_sizewhile a token is set. A caller sees only the traces itstraces.readgrant covers, including service- and label-scoped grants, so an agent harness key can be limited to its own workflows. Link each entry to/ui/for the trajectory.To be told instead of asking, subscribe a webhook to
trace.scored: one signed delivery per scored execution and policy, with the same summary and results (webhooks).alert.firedtells the agent harness when a workflow’s quality drops below a policy’s alert threshold. -
Raw traces, for debugging and regression sets.
evalsi traces listshows what was ingested,evalsi traces get <trace id>one trace’s record (the trajectory the evaluators saw) and its scores, andevalsi traces export --labels workflow=support-bot -o traces.jsonlwrites them as dataset records: a regression set for the next gate, a fixture for a mapping profile, or what to look at when an execution was skipped instead of scored. -
The API and CLI.
evalsi report <run id> --server $URL -o report.html, or theRunServiceAPI (ListRunResults) for a gate run’s results. Live-traffic scores for a policy as a window:evalsi policy stats studio-agents.
5. Pipeline writes, datasets and retention
Section titled “5. Pipeline writes, datasets and retention”Send Idempotency-Key on retryable API writes. The same authenticated principal,
operation and payload replay the stored outcome for 24 hours; a changed payload
under the same key conflicts. Permissions are checked again on every retry.
An unavailable response saying the outcome is uncertain requires reconciliation
of the original operation, not a new key. Generated API-key secrets are returned
only on the first successful call, never persisted for replay.
Python callers can pass idempotency_key to Client.call, create_run,
delete_run and import_dataset. Keyed gRPC write streams are bounded to 64 MiB
and 100001 messages and require the sender to close its input before reading
responses; omit the header for incremental streaming. Responses are bounded to
64 MiB, and each principal/operation scope holds at most 1024 keys and 64 MiB of
response data. An unfinished claim remains reserved until reconciled.
evalsi dataset upload qa.jsonl --name qa --project studio --server "$URL"evalsi dataset import s3://evalsi/datasets/incoming/studio/qa.parquet \ --name qa --project studio --format parquet --server "$URL"evalsi delete-run "$RUN_ID" --yes --server "$URL"Upload JSONL or Parquet with
POST /v1alpha1/datasets/<name>/upload?project=studio&format=jsonl; its body is the
file, not multipart. The Connect/gRPC DatasetService.UploadDataset begins with
a config message followed by data messages. ImportDataset takes config and
an S3 uri under the configured dataset root’s incoming/<project>/ prefix.
These operations need datasets.write; no arbitrary URL or other bucket is read.
The response’s path is usable as RunSpec.dataset.path. Limits are 64 MiB
uploaded and normalized, 100000 records and 4 MiB per JSONL row/Parquet page.
Rows must be Record objects; text input/output/reference strings are normalized
to Content and null stays absent. Missing IDs become record-<row> with a
deterministic suffix when an explicit ID reserves that name; duplicate explicit IDs are rejected.
Errors name the offending row without echoing raw fields. Publication occurs
only after validation, and its path contains the normalized content hash.
There are two conversion slots per replica; capacity refusals are retryable.
DeleteRun, also DELETE /v1alpha1/runs/<id>, requires runs.delete (admin by
default). Pending/running or actively leased runs are refused. Terminal deletion
removes records, outputs, results and queued webhook deliveries and recovers the
stored-run quota. Only authorization metadata remains for 24-hour retries.
Data already delivered to external sinks, source datasets and promoted datasets
remain; deleting an EvalRun CR does not invoke this API.
To delete finished runs automatically, set runs.retention (for example
2160h, 90 days) and, per project, runs.project_retention ("0" keeps a
project’s runs forever). Every hour, each replica deletes the runs that
finished before the cutoff (or, with no finish time, were created before it),
in the same way as DeleteRun, so the stored-run quota is recovered. Runs that
are pending, running or leased are never touched, and neither is a run
labelled evalsi.keep: "true". Data already delivered to external sinks, and
source and promoted datasets, remain. Each automatic delete writes an audit
event (runs.delete, principal system:retention), and each pass logs
run retention with the counts.
Archive instead of delete
Section titled “Archive instead of delete”ArchiveRun (POST /v1alpha1/runs/{id}:archive, evalsi archive-run ID --server URL --yes)
drops a finished run’s records, outputs, results and queued webhook deliveries
and keeps the run: its spec, status, metric summaries, gates, usage and labels,
now with archived_at set. It needs the permission DeleteRun needs
(runs.delete) and has the same refusals for runs that are not finished. An
archived run can still be read (GetRun, the web UI’s run page, evalsi report --server shows its summary only) and deleted, but results, ResumeRun, CompareRuns,
PromoteResults, shadow replays and annotation queues sourced from it answer
FailedPrecondition saying it was archived, and SliceRuns skips it. It still
counts toward the stored-run quota until it is deleted.
To archive automatically, set runs.archive_after (for example 168h) and, per
project, runs.project_archive_after ("0" never archives). It must be shorter
than the retention it sits with, so a run goes whole, then summary-only, then
away. Each pass checks every older run once an hour; evalsi.keep: "true"
exempts a run, and each archive writes a runs.archive audit event.
A checklist
Section titled “A checklist”- Evals.si installed; the sandbox pool answers (
kubectl get sandboxclassor the pool pod is Ready). -
studio-ingestkey set on the agent harness’s exporter, or aTraceSourceon its MLflow; a trace appears in the UI. - An
OnlineEvalPolicyapplied;evalsi policy statsshows scored traces. - A suite for the platform’s agents, with gates; the deploy step runs it and fails on exit 3 or
phase: Failed. - A webhook (or the API) tells the agent harness when a gate fails.
- Scores visible where people look:
/ui/, MLflow, Grafana.
To see all of it run first, follow the demo walkthroughs: the same install, the same specs, a mock model, on your laptop or on kind.