Skip to content

[kbn-evals] Move online evals onto typed workflow steps with retries - #296582

Merged
viduni94 merged 12 commits into
elastic:mainfrom
viduni94:online-evals-typed-steps
Oct 23, 2026
Merged

viduni94 merged 12 commits into
elastic:mainfrom
viduni94:online-evals-typed-steps

Conversation

@viduni94

@viduni94 viduni94 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #279267

Summary

The online eval workflow called /_evaluate and /online_scores through raw kibana.request steps with hardcoded internal headers and no failure handling. One failed call ended the whole run and dropped every remaining trace.

  • The workflow now uses typed steps: the existing ai.evals.evaluateTrace, plus a new ai.evals.persistOnlineScores. The generated YAML no longer contains kbn-xsrf, elastic-api-version, x-elastic-internal-origin, or /s/{{ workflow.spaceId }} paths, because the steps call the APIs from server code in the workflow's space.
  • Both steps retry up to 3 times with exponential backoff. HTTP 400 and 403 responses aren't retried, since they mean a misconfigured monitor or missing privileges. A trace that still fails is skipped, and the run continues with the next trace.
  • If some evaluators fail, the successful scores are still saved and the failures are counted. If every evaluator fails, evaluateTrace throws so the retry applies, unless every failure was unmet evidence, which a retry can't fix.
  • Evaluators for a trace now run up to 4 at a time in the shared executor. This also applies to offline experiments: with ai.evals.evaluateDataset running 5 examples at a time, a run can now make up to 20 concurrent judge calls instead of 5.
  • Online evals aren't exposed to users yet, so this drops support for the kibana.request workflow format, including workflows created after [kbn-evals] Scope online evaluation scores to the active space #296257. Those workflows need to be recreated.

The new step needs an approval file under workflows_extensions (ai.evals.persistOnlineScores.txt), so this needs a review from workflows-eng.

Test plan

  • Create an online evaluation and confirm the generated workflow uses ai.evals.evaluateTrace and ai.evals.persistOnlineScores, with on-failure.retry on both steps and iteration-on-failure: continue on the loop.
  • Let it run and confirm scores appear on the detail page, including in a non-default space.
  • Make one evaluator fail (for example, pick a connector that errors) and confirm the other evaluators' scores are still saved.
  • Make every evaluator fail and confirm the step retries, then the run moves on to the next trace instead of failing.
  • Use an evaluator whose evidence the sampled traces don't have (for example, one that needs tool calls) and confirm those traces are skipped without retries.

Checklist

@viduni94 viduni94 self-assigned this Oct 8, 2026
@viduni94 viduni94 added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting Team:nightshift-context-and-research Nightshift Context & Research Team labels Oct 8, 2026
@viduni94
viduni94 requested a balanced review from Copilot October 8, 2026 18:03
@viduni94

viduni94 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

/ci

Copilot AI 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.

🟢 Approval recommended

The typed steps, retry behavior, concurrency limits, space scoping, and associated tests are consistent and complete.

0 open findings

What changed in this PR

Moves online evaluations to typed, space-aware workflow steps with retries and bounded evaluator concurrency.

Changes:

  • Adds and registers ai.evals.persistOnlineScores.
  • Generates resilient workflows with exponential retries and per-trace failure isolation.
  • Runs evaluators concurrently and scopes Lens panels to the active space.
File Description
x-pack/​platform/​plugins/​shared/​evals/​server/​workflows/​steps.ts Implements the persistence step and retry-triggering evaluation behavior.
x-pack/​platform/​plugins/​shared/​evals/​server/​workflows/​steps.test.ts Tests partial failures, retries, and score persistence.
x-pack/​platform/​plugins/​shared/​evals/​server/​workflows/​step_definitions_approval.test.ts Adds the new step to approval checks.
x-pack/​platform/​plugins/​shared/​evals/​server/​workflows/​lib.ts Adds online-score persistence support.
x-pack/​platform/​plugins/​shared/​evals/​server/​workflows/​lib.test.ts Tests persistence request mapping and failure counts.
x-pack/​platform/​plugins/​shared/​evals/​server/​routes/​evaluators/​shared/​execute_evaluators.ts Adds bounded concurrent evaluator execution.
x-pack/​platform/​plugins/​shared/​evals/​server/​routes/​evaluators/​evaluate.test.ts Tests concurrency, ordering, and failure isolation.
x-pack/​platform/​plugins/​shared/​evals/​README.md Documents the new workflow step.
x-pack/​platform/​plugins/​shared/​evals/​public/​workflows/​steps.ts Defines the public persistence step.
x-pack/​platform/​plugins/​shared/​evals/​public/​workflows/​index.ts Registers the public persistence step.
x-pack/​platform/​plugins/​shared/​evals/​public/​pages/​online_eval_detail/​index.tsx Adds active-space filtering to Lens panels.
x-pack/​platform/​plugins/​shared/​evals/​public/​pages/​online_eval_detail/​index.test.tsx Tests space-scoped panel rendering.
x-pack/​platform/​plugins/​shared/​evals/​public/​hooks/​use_active_space_id.ts Resolves the active or default space.
x-pack/​platform/​plugins/​shared/​evals/​public/​hooks/​use_active_space_id.test.tsx Tests active-space resolution behavior.
x-pack/​platform/​plugins/​shared/​evals/​common/​workflows/​steps.ts Defines typed persistence schemas and metadata.
x-pack/​platform/​plugins/​shared/​evals/​common/​online_evals/​workflow_yaml.ts Generates and parses typed retry-enabled workflows.
x-pack/​platform/​plugins/​shared/​evals/​common/​online_evals/​workflow_yaml.test.ts Tests typed workflow generation and retry configuration.
x-pack/​platform/​plugins/​shared/​evals/​common/​online_evals/​__snapshots__/​workflow_yaml.test.ts.snap Updates expected generated YAML.
src/​platform/​plugins/​shared/​workflows_extensions/​test/​scout/​api/​fixtures/​approved_step_definitions/​ai.evals.persistOnlineScores.txt Approves the new step-definition hash.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@kibanamachine

Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
evals 497.3KB 497.5KB +133.0B

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
evals 4.5KB 4.6KB +142.0B
kbnUiSharedDeps-srcJs 5.4MB 5.4MB +1.0B
shared-plugins 12.1MB 12.1MB +725.0B
total +868.0B
Unknown metric groups

total optimizer output size

id before after diff
all 65.8MB 65.8MB +1000.0B

warm start memory

id before after diff
post forced gc heap baseline - 827410738 +827410738
post forced gc heap delta - -1554750 -1554750
post forced gc heap delta standard deviation - 1558783 +1558783
post forced gc heap target - 825855988 +825855988
tail heap delta - -5210868 -5210868
total +1648059891

workflow yaml validation

id before after diff
case_response.yaml (27 steps, 66 vars)/e2e/connectorIds (collect+validate) 1 0 -1
case_response.yaml (27 steps, 66 vars)/e2e/performComputation 14 12 -2
case_response.yaml (27 steps, 66 vars)/e2e/total 19 16 -3
case_response.yaml (27 steps, 66 vars)/e2e/validateVariables 3 2 -1
infosec_demo.yaml (150 steps, 270 vars)/e2e/performComputation 57 50 -7
infosec_demo.yaml (150 steps, 270 vars)/e2e/total 155 124 -31
infosec_demo.yaml (150 steps, 270 vars)/e2e/validateIfConditions 6 5 -1
infosec_demo.yaml (150 steps, 270 vars)/e2e/validateJsonSchemaDefaults 2 1 -1
infosec_demo.yaml (150 steps, 270 vars)/e2e/validateLiquidTemplate 6 5 -1
infosec_demo.yaml (150 steps, 270 vars)/e2e/validateStepNameUniqueness 2 1 -1
infosec_demo.yaml (150 steps, 270 vars)/e2e/validateVariables 60 44 -16
infosec_demo.yaml (150 steps, 270 vars)/validateIfConditions 6 4 -2
infosec_demo.yaml (150 steps, 270 vars)/validateLiquidTemplate 6 5 -1
infosec_demo.yaml (150 steps, 270 vars)/validateVariables (270 vars) 57 42 -15
total -83

Test Failures

  • [job] [logs] Scout Lane #105 - serverless-security_complete / default / local-serverless-security_complete - Osquery live query details - accepts live query creation request (permission check)
  • [job] [logs] Scout Lane #32 - stateful-classic / default / local-stateful-classic - Document flyout v2 — alerts page entry - assignees: assigning the current user shows the avatar in the flyout header
  • [job] [logs] Scout Lane #36 - stateful-classic / default / local-stateful-classic - Overview threat intelligence panel - lists the feed and counts indicators in the time range
  • [job] [logs] Jest Tests #10 / ResponseActions labels the copy action for the user prompt when copyTarget is prompt
  • [job] [logs] Jest Tests #10 / ToolCallGroup expands to show each individual step, which opens its own flyout on click

cc @viduni94

# Conflicts:
#	x-pack/platform/plugins/shared/evals/common/online_evals/__snapshots__/workflow_yaml.test.ts.snap
#	x-pack/platform/plugins/shared/evals/common/online_evals/workflow_yaml.test.ts
#	x-pack/platform/plugins/shared/evals/common/online_evals/workflow_yaml.ts
@viduni94
viduni94 marked this pull request as ready for review October 22, 2026 15:00
@viduni94
viduni94 requested review from a team as code owners October 22, 2026 15:00
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown
Contributor

Pinging @elastic/nightshift-context-and-research-team (Team:nightshift-context-and-research)

@botelastic botelastic Bot added evals:smoke-tests Team:One Workflow Team label for One Workflow (Workflow automation) labels Oct 22, 2026
});

// A trace whose retries are exhausted is skipped rather than failing the whole run.
const ITERATION_ON_FAILURE = { continue: true };

@Kiryous Kiryous Oct 22, 2026 •

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.

with iteration-on-failure: continue the run ends green even if every trace was skipped (e.g. connector down). before it at least went red. is it ok, or should something flag a run where all iterations failed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review @Kiryous
Great catch! I added a fix here - 29e9a24

@Kiryous Kiryous 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 with one small question

{
name: MARK_PERSISTED_STEP_NAME,
type: 'data.set',
with: { any_trace_persisted: true },

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.

Severity: P3 (Low)

Only mark a trace as persisted when the persist step reports scores created or already present. An evaluation with only evidence_unmet failures returns empty results without throwing, and persisting those results succeeds with created: 0, skipped: 0. This unconditional flag therefore makes a run succeed when one trace lacks evidence and every other trace exhausts its retries, even though no scores were persisted—the situation the final guard is intended to detect.

Generated by Libra. React with 👍 or 👎 to give feedback on this comment.

@SrdjanLL SrdjanLL 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.

Overall LGTM, no blockers - just left some questions/suggestion for you to consider before merging.

const EVALUATE_RESULTS_TEMPLATE = '${{ steps.evaluate.output.results }}';
const EVALUATE_ERRORS_TEMPLATE = '${{ steps.evaluate.output.errors }}';
const RETRY_CONDITION_TEMPLATE =
'${{ error.details.status != 400 and error.details.status != 403 }}';

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.

[Question] So, we're retrying 5xx errors always?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review @SrdjanLL

The 5xx is retried on purpose, since that's where transient ES and inference failures end up. But your question made me look at the rest of the condition. Initially I had a deny-list (skip 400/403), so it also retried other 4xx that can't succeed, like 413. Now I switched it to an allow-list - no HTTP status (network errors, or every evaluator failing), 404 (trace still exporting), 429 and 5xx. Everything else fails straight away.

@viduni94 viduni94 Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll go ahead and merge this PR, but let me know if there are any concerns, i'll address them along with https://github.com/elastic/observability-dev/issues/6336

const MARK_PERSISTED_STEP_NAME = 'mark_persisted';
const FAIL_IF_EVERY_TRACE_FAILED_STEP_NAME = 'fail_if_every_trace_failed';
const EVERY_TRACE_FAILED_CONDITION =
'${{ steps.sample_traces.output.values.size > 0 and variables.any_trace_persisted != true }}';

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.

[thought] Instead of "any trace persisted" we could have a percentage of successfully persisted evals on traces and have better monitoring on future iterations (you could achieve the same outcome with say variables.traces_persisted_ratio == 0). No need to add now, but thinking about raising the failures based on some configuration later on.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, a ratio with a configurable failure threshold per monitor would give better monitoring than the all-or-nothing check. I'll do that in a follow-up.

{
name: EVALUATE_STEP_NAME,
type: 'kibana.request',
type: EVALUATE_TRACE_STEP_TYPE,

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.

[Question] Do you think it's worth making this a batch step that can run with some degree of concurrency that we control in the definition?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's a good callout! Will add that in a followup alonf with the ratio

import { resolveConnectorModel } from '../../../lib/resolve_connector_model';

/** Max evaluators graded in parallel for one trace; bounds concurrent judge-model calls. */
export const EVALUATOR_CONCURRENCY = 4;

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.

[Question] Any reason we can't go higher here? We could also have a single API call for all specified evaluators and evaluate in batch (API handles concurrent invocation of all evaluators sent in the request payload).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We could go higher. I'll consider this (either the make it configurable or go with your suggestion). Might do in a follow up, if that's okay.

@kibanamachine

Copy link
Copy Markdown
Contributor

PR run: bk-01a16964-d45e-42a5-9a68-0e7c06915cae::smoke-tests::anthropic-claude-4.5-haiku | Baseline (main): bk-01a13419-4e2a-4953-bac3-99e3436a364b::smoke-tests::anthropic-claude-4.5-haiku
Baseline: commit b0f2d7a, 10 days ago

Warning: Baseline is 10 days old. Results may not reflect current main.
Significance threshold: p < 0.05
Test per row is chosen from the scores: McNemar for pass/fail, otherwise Wilcoxon signed-rank (paired t-test for continuous scores when n ≥ 30 and differences look normal).

Summary
No significant regressions detected (5 evaluator comparisons).

View full comparison in UI | Refresh baseline against latest main (click Unblock in the eval build)

No significant changes (5 rows)
Dataset Evaluator N Mean (PR) Mean (main) Diff Test p-value Sig Outcome
smoke tests: es-snapshot-loader SnapshotRestored 1 1.00 1.00 0.00 (0 target only, 0 baseline only) McNemar 1.00 No -
smoke tests: llm-judge Criteria 1 1.00 1.00 0.00 (0 target only, 0 baseline only) McNemar 1.00 No -
smoke tests: score ingestion and code evaluator ContainsKibana 1 1.00 1.00 0.00 (0 target only, 0 baseline only) McNemar 1.00 No -
smoke tests: trace-retrieval Input Tokens 1 12.00 12.00 0.00 Wilcoxon 1.00 No -
smoke tests: trace-retrieval Output Tokens 1 4.00 5.00 -1.00 Wilcoxon 1.00 No -

@infra-vault-gh-plugin-prod

Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
evals 467.8KB 468.1KB +343.0B

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
evals 4.5KB 4.6KB +142.0B
kbnUiSharedDeps-srcJs 5.4MB 5.4MB +1.0B
shared-plugins 12.5MB 12.5MB +725.0B
total +868.0B
Unknown metric groups

total optimizer output size

id before after diff
all 67.2MB 67.2MB +1.2KB

warm start memory

id before after diff
post forced gc heap baseline - 716585980 +716585980
post forced gc heap delta - -836458 -836458
post forced gc heap delta standard deviation - 1430263 +1430263
post forced gc heap target - 715749522 +715749522
tail heap delta - -26219830 -26219830
total +1406709477

workflow yaml validation

id before after diff
case_response.yaml (27 steps, 66 vars)/e2e/collectAllVariables 0 1 +1
infosec_demo.yaml (150 steps, 270 vars)/e2e/performComputation 48 49 +1
infosec_demo.yaml (150 steps, 270 vars)/e2e/total 118 116 -2
infosec_demo.yaml (150 steps, 270 vars)/validateIfConditions 4 5 +1
total +1

Test Failures

  • [job] [logs] FTR Configs #83 / Endpoint plugin @ess @serverless @skipInServerlessMKI Endpoint artifacts (via lists plugin): Trusted Applications "before all" hook in "@ess @serverless @skipInServerlessMKI Endpoint artifacts (via lists plugin): Trusted Applications"
  • [job] [logs] FTR Configs #89 / Endpoint plugin spaces support "before all" hook in "Endpoint plugin spaces support"
  • [job] [logs] Scout Lane #86 - serverless-security_complete / default / local-serverless-security_complete - Osquery live query details - accepts live query creation request (permission check)
  • [job] [logs] Scout Lane #27 - stateful-classic / default / local-stateful-classic - Index actions context menu - navigates to overview, settings, and mappings tabs from the manage index menu

History

cc @viduni94

@viduni94
viduni94 added this pull request to the merge queue Oct 23, 2026
Merged via the queue into elastic:main with commit a792a61 Oct 23, 2026
25 checks passed
@viduni94
viduni94 deleted the online-evals-typed-steps branch October 23, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting evals:smoke-tests release_note:skip Skip the PR/issue when compiling release notes Team:nightshift-context-and-research Nightshift Context & Research Team Team:One Workflow Team label for One Workflow (Workflow automation) v9.7.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[kbn-evals] Custom workflow steps for online evals

5 participants