-
Notifications
You must be signed in to change notification settings - Fork 84
feat(eval): add eval measurements and EM-001 trace_fitness scorer #6036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ascerra
wants to merge
10
commits into
main
Choose a base branch
from
feat/eval-measurements
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
06cb1ee
feat(eval): add eval measurements and EM-001 trace_fitness scorer
ascerra 16e82de
refactor(eval): keep measurements tool-agnostic and document ownership
ascerra 2add5e9
fix(eval): address review feedback on PR #6036
ascerra 71e411b
fix(eval): restore with: blocks and rename ledger extension
ascerra 930db1a
fix(eval): address review feedback and rebase onto main
ascerra 615dd69
fix(eval): move measure before upload and raise test coverage
ascerra c8438d8
fix(eval): address review findings on measurements path
ascerra 4ff9499
chore: merge origin/main into feat/eval-measurements
ascerra 3d5aefc
fix(eval): address second-round measurement review
ascerra 61b9bae
fix(eval): keep partial eval-measure rows on persist error
ascerra File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
docs/ADRs/0087-eval-measurements-online-trace-scoring.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| --- | ||
| title: "87. Eval measurements as online trace scoring with portable export" | ||
| status: Accepted | ||
| relates_to: | ||
| - operational-observability | ||
| - testing-agents | ||
| topics: | ||
| - observability | ||
| - evaluation | ||
| - opentelemetry | ||
| --- | ||
|
|
||
| # 87. Eval measurements as online trace scoring with portable export | ||
|
|
||
| Date: 2026-08-10 | ||
|
|
||
| ## Status | ||
|
|
||
| Accepted | ||
|
|
||
| ## Context | ||
|
|
||
| Agent runs already emit OpenTelemetry traces as `run-telemetry.jsonl`, with | ||
| optional live OTLP export when `OTEL_EXPORTER_OTLP_*` is set | ||
| ([ADR 0050](0050-distributed-tracing-instrumentation.md)). Separately, | ||
| [ADR 0051](0051-agent-eval-harness-for-test-infrastructure.md) owns the | ||
| **functional** eval harness: curated fixtures / scenarios in | ||
| `fullsend-ai/agents` `eval/<agent>/` that gate agent PRs. Those fixtures do | ||
| not score wild production runs. | ||
|
|
||
| Operators also need an **online / trend** layer on wild traces (completeness | ||
| first; quality signals later). Fullsend must stay **backend-agnostic**: orgs | ||
| already choose Phoenix, MLflow, Jaeger, or another OTLP collector for traces. | ||
| Baking a single product’s Assessments/Quality API into the core CLI or managed | ||
| workflows would force a tool decision on every install. | ||
|
|
||
| Adjacent telemetry proposals (not competing with this score path): | ||
|
|
||
| - **Level 3 content capture** ([#5947](https://github.com/fullsend-ai/fullsend/pull/5947) | ||
| — proposed ADRs 0084/0085): richer span content enables later scorers; | ||
| first ship reads Level 1/2 metadata in `run-telemetry.jsonl`. Measure CLI is | ||
| host-side after sandbox exit. | ||
| - **Span status from run outcome** ([#5944](https://github.com/fullsend-ai/fullsend/pull/5944)): | ||
| OTLP Status (and `fullsend.transcript_error`) become the reliable | ||
| success/failure signal. EM-001 only checks that `exit_code` is **present** | ||
| (fitness). Outcome scorers must key on Status, not `exit_code == 0`. | ||
| - **Observer / lessons → fixtures** ([#2423](https://github.com/fullsend-ai/fullsend/pull/2423)): | ||
| narrative analysis and golden-set promotion. This ADR is same-job | ||
| deterministic scoring on traces. | ||
| - **Harness snapshot / forge join keys** ([#5524](https://github.com/fullsend-ai/fullsend/pull/5524)): | ||
| sibling artifact for harness fingerprint and | ||
| forge/CI pointers beside telemetry. Complementary join/identity layer; | ||
| primary run facts belong on the OTEL trace (Level 1), while measurements | ||
| stay a derived sibling file. | ||
|
|
||
| ## Options | ||
|
|
||
| 1. **Local JSONL only** — portable offline artifact; no remote scores from | ||
| fullsend itself. | ||
| 2. **Backend-native APIs in core** (e.g. one vendor’s Assessments API) — | ||
| couples every managed workflow to that product’s auth and schema. | ||
| 3. **Local JSONL + same OTLP path as agent traces for remote** — scores travel | ||
| with the endpoint/headers orgs already configure for ADR 0050; no second | ||
| vendor stack in core. | ||
|
|
||
| ## Decision | ||
|
|
||
| Introduce **eval measurements**: deterministic scorers that read | ||
| `run-telemetry.jsonl` after `fullsend run` in the **same** managed job | ||
| (`fullsend eval-measure` in `action.yml`), **fail-open**. Functional eval | ||
| scenarios remain ADR 0051 / `eval/<agent>/`; measurements never block | ||
| delivery. | ||
|
|
||
| In plain terms: eval measurements are the concept of scoring traces. | ||
| [OTEL primary facts](../glossary.md#otel-primary-facts) are what happened | ||
| on the run (the OTEL trace / `run-telemetry.jsonl`). | ||
| [OTEL derived products](../glossary.md#otel-derived-products) are scores | ||
| computed from that trace (`eval-measurements.jsonl`). Measurements never | ||
| rewrite primary facts, and they are [fail-open](../glossary.md#fail-open). | ||
|
|
||
| Scores always land in a tool-agnostic `eval-measurements.jsonl` (plus a | ||
|
ascerra marked this conversation as resolved.
|
||
| small idempotency ledger) next to `run-telemetry.jsonl`. Remote score export | ||
| will use the same `OTEL_EXPORTER_OTLP_*` configuration as ADR 0050 — no | ||
| vendor-specific score adapters in core. `fullsend` owns the parser, scorers, | ||
| CLI, and GHA step; `fullsend-ai/agents` owns per-agent measurement manifests | ||
| (`eval/measurements/<agent>.yaml`) that declare which scorers to enable. | ||
| Stock-agent defaults resolve from `agents@v0` at runtime; local files are for | ||
| override, opt-out, or custom agents only. | ||
|
|
||
| The first scorer is `trace_fitness` (catalog id `em-001`) — span-tree and | ||
| attribute fitness so later scorers can trust the trace. EM-001 reads | ||
| experimental OpenTelemetry GenAI attribute names (`gen_ai.*` constants in | ||
| `internal/evalmeasure`); an upstream rename is an `em-001` version bump. | ||
|
|
||
| ### Versioning (per measurement, not platform “v1”) | ||
|
|
||
| There is no product-wide “eval measurements v1” switch. “First ship” just | ||
| means only one scorer is enabled yet. Each manifest entry carries: | ||
|
|
||
| | Field | Meaning | | ||
| |---|---| | ||
| | `id` | Stable catalog id (`em-001`). New measurement concept → new id. | | ||
| | `scorer` | Go dispatch name (`trace_fitness`). | | ||
| | `version` | Integer **contract** version of that measurement’s checks / pass rule. | | ||
|
|
||
| Scores and the idempotency ledger key on `id@version` (e.g. `em-001@1`). | ||
| Bump `version` when pass/fail semantics change so trends do not mix eras. | ||
| Add a check that does not change the pass definition → same version is fine. | ||
| Entirely new signal → new `em-NNN` (and usually a new `scorer` string). | ||
|
|
||
| ## Consequences | ||
|
ascerra marked this conversation as resolved.
|
||
|
|
||
| - Every measured run produces a reviewable, backend-agnostic score file beside | ||
| telemetry; missing manifests skip cleanly and measure failure never fails | ||
| the agent job. GitHub Actions is the first-ship managed path (uploads | ||
| `output/`). GitLab CI calls the same fail-open `eval-measure` CLI, writing | ||
| under `/tmp/fullsend-output` with no `artifacts:` block by default. | ||
| - Core stays tool-agnostic: no product-specific score env vars in managed | ||
| workflows; remote scores follow OTEL when that path lands. | ||
| - Functional scenarios (gate) and eval measurements (trend) stay separate; | ||
| retro can recommend either a manifest scorer or a scenario fixture. | ||
| - Richer telemetry (Level 3 / Status fixes) expands what scorers *can* assert; | ||
| it does not replace this same-job path. | ||
| - Per-measurement versioning (`id@version`) lets pass/fail semantics evolve | ||
| without mixing trend eras. | ||
| - Pre-script skipped runs (`fullsend.prescript.skipped=true` on the root span) | ||
| are excluded from EM-001: the scorer writes `label: skip` instead of failing | ||
| a run that never created a sandbox. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[HIGH] Eval-measure step/scaffold never inject GH_TOKEN, so manifest GetRef runs unauthenticated
The "Eval measurements" step's
env:block (action.yml:418-420) sets onlyAGENTandFULLSEND_DIR— unlike the preceding "Run fullsend" step (action.yml:379), which explicitly setsGH_TOKEN: ${{ inputs.github_token }}.evalMeasureFetchContext(internal/cli/evalmeasure.go:191) callstoken, _ := resolveToken()and discards the error;resolveToken()(internal/cli/admin.go:91-106) checksGH_TOKEN, thenGITHUB_TOKEN, thengh auth token, returning an error if none are set — which happens for this step. The resulting empty token is passed intogh.New(token)and used byfetchPinnedAgentsRepoFile(internal/cli/run.go:3751) to callforgeClient.GetRef(ctx, ..., "tags/"+config.DefaultUpstreamRef)against api.github.com. An unauthenticatedGetRefis subject to GitHub's 60 req/hour per-IP limit, shared across the whole GitHub-hosted runner IP pool.The identical gap exists in the new GitLab CI scaffold step (internal/scaffold/fullsend-repo-gitlab/.gitlab/ci/fullsend-agent.yml:310-320), which also invokes
fullsend eval-measure --agent ... --fullsend-dir ...with no GITHUB_TOKEN/GH_TOKEN configured anywhere in that pipeline (onlyGITLAB_TOKENis exported).Because the whole step is fail-open (
continue-on-error: true/|| true), failures are silentStepWarnskips, so stock-agent measurement scoring will intermittently (GHA) or consistently (GitLab, no GitHub identity at all) fail to fetch the SHA-pinned manifest fromfullsend-ai/agents@v0.Note: this is distinct from the earlier reviewed-and-fixed concern about sending a Bearer token to the public
raw.githubusercontent.comcurl (that curl call is gone inc8438d83) — this is about the replacementGetRefAPI call having no token available at all.Suggestion: add
GH_TOKEN: ${{ inputs.github_token }}to the action.yml "Eval measurements" step's env block (mirroring "Run fullsend"), and document/accept explicitly that the GitLab scaffold step has no GitHub token available so stock-agent measurement fetch will reliably skip there unless an operator wires one in.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in
3d5aefc6. The Eval measurements step now setsGH_TOKEN: ${{ inputs.github_token }}forGetRefofagents@v0(not a Bearer header to raw.githubusercontent.com).GitLab: documented skip — this job has no GitHub token, so stock-agent manifests skip unless an operator exports
GH_TOKEN/GITHUB_TOKEN. Local.fullsendoverride still works. Empty-token path logs a warning (TestActionYML_EvalMeasureNoFloatingV0CurlassertsGH_TOKEN:on the step; GitLab test assertsstock-agent manifests skip).