feat: deterministic trajectory assertions for agent evals with --fail-on-assertions CI gate - #58
Open
brainsparker wants to merge 1 commit into
Open
feat: deterministic trajectory assertions for agent evals with --fail-on-assertions CI gate#58brainsparker wants to merge 1 commit into
brainsparker wants to merge 1 commit into
Conversation
…ns CI gate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What it does
Adds deterministic trajectory assertions: behavior checks over the tool calls in a response, evaluated locally with zero LLM calls. Judge scores tell you whether an answer was good. Trajectory assertions tell you whether the agent did the right thing: called the required tools, avoided forbidden ones, followed the expected workflow order, and stayed within a call budget. Because they are pure Python over the captured tool calls, they cost nothing to run, never flake on judge variance, and are safe to gate CI on.
New
trajectoryblock on any test case:Wired end to end:
<failure type="TrajectoryAssertionFailure">; a case with passing assertions and no judge score now counts as a pass instead of skipped, since it was evaluated deterministically--fail-on-assertionsCI gate (exit code 2, matching the--fail-underand--fail-on-regressionsemantics)examples/golden_sets/trajectory_assertions.yaml(required calls, safety, workflow order, whitelist)Why now
Deterministic trajectory checking became table stakes across the eval ecosystem in Q2-Q3 2026, and PromptLens had no deterministic layer at all: every tool evaluation went through the LLM judge, which costs money and can flake in CI.
PromptLens's positioning is lightweight and local-first. A deterministic assertion layer is the one trajectory-eval capability that fits that positioning perfectly: no server, no new dependencies, no API spend. This was also flagged as a runner-up candidate in frugal PR #87 three weeks ago; the market signal has only strengthened since.
Runner-up opportunities considered today and passed on: (1) frugal: per-tool cost attribution via OpenTelemetry span export, timely because MCP's 2026-07-28 revision deprecated protocol-level logging in favor of OTel, but frugal received the previous feature PR and rotation favored spreading coverage; (2) frugal: tools/list TTL caching per SEP-2549 to cut tool-catalog token bloat.
Implementation notes
promptlens/models/trajectory.py: pydantic models (TrajectoryAssertions,ToolCallMatcher,TrajectoryCheck,TrajectoryResult) with validation that rejects empty assertion blocks, invalid arg-match modes, contradictory count bounds, and whitelist mode without an allowed setpromptlens/assertions/trajectory.py: pure evaluator, no LLM, no network; call_order is a greedy subsequence scan; must_call failure messages distinguish "tool never called" from "called with non-matching arguments"trajectoryis optional, existing golden sets and stored results parse unchanged, and JUnit mapping for cases without assertions is untouchedTesting
tests/test_trajectory_assertions.py: model validation, all matcher modes (partial/exact/ignore, min/max times), must_not_call, call_order subsequence semantics, max_calls, whitelist mode, TestCase parsing, JUnit mapping (failure type, error precedence, pass-without-judge, skip semantics, system-out stats), and the CLI gate helperpromptlens validateaccepts the new example golden setBuilt by Daily Feature Builder.