feat(#130): on_result expression: matcher + matches regex operator#157
Merged
AlexChesser merged 2 commits intomainfrom Apr 15, 2026
Merged
feat(#130): on_result expression: matcher + matches regex operator#157AlexChesser merged 2 commits intomainfrom
expression: matcher + matches regex operator#157AlexChesser merged 2 commits intomainfrom
Conversation
…ex operator
Implements the spec committed earlier on this branch:
- §5.4 `expression:` matcher — arbitrary §12.2 condition against any
template variable accessible in the turn log.
- §5.4 `matches: /PAT/FLAGS` named matcher — shorthand for
`expression: '{{ step.<id>.response }} matches /.../flags'`.
- §12.2 `matches` operator — regex comparison, shared with `condition:`.
- §12.3 regex syntax — single source of truth for `/PAT/FLAGS` form;
flags i/m/s accepted, g rejected at parse time, other Perl flags
rejected with a specific error that points to inline `(?x)` for
verbose mode.
Design notes:
- Regex is compiled at parse time (in `parse_regex_literal`) so
malformed patterns fail pipeline load, not match time. Source literal
preserved alongside the compiled `regex::Regex` for diagnostics and
materialize output.
- `Condition` gains a `Regex(RegexCondition)` variant. `PartialEq` is
dropped from `Condition` (regex::Regex has no PartialEq); the one
existing `assert_eq!` on `Option<Condition>` was rewritten as a
`matches!` pattern match. No production code compared Condition
values for equality.
- `ResultMatcher::Expression { source, condition }` reuses the
condition evaluator for both comparison and regex forms, so the two
grammars cannot drift apart.
- `evaluate_on_result()` now takes `&Session` and returns
`Result<Option<ResultAction>, AilError>`. Unresolvable template
variables in an `expression:` LHS abort the pipeline via
CONDITION_INVALID — same contract as `condition:` (SPEC §11).
- Named `matches:` is desugared at parse time into the expression form,
so the runtime has exactly one regex evaluation path.
- Materialize round-trips `expression:` using the preserved source.
- do_while's `exit_when` deliberately NOT extended — it stays
ConditionExpr-only for now. Regex in loop exits is out of scope for
this change; can be added later by widening exit_when to `Condition`.
Testing: 911 tests pass (419 lib + 492 integration). New coverage:
- regex_literal: 20 unit tests (parsing, flags, error cases)
- condition.rs: 4 new tests for Condition::Regex evaluation
- s12_step_conditions: 6 new integration tests (matches operator,
case sensitivity, parser path, invalid regex, g-flag rejection)
- s05_3_on_result: 5 new integration tests (expression: matcher on
stderr, named matches: shorthand, expression with matches op,
unresolvable template, parse-time matcher count enforcement)
e1e1540 to
19c0a8c
Compare
…l site The cherry-pick missed the second `evaluate_on_result` call site, which was added by PR #154 (parallel execution) after this branch originally forked. The §29 join-step code path needs the same signature update the sequential dispatch path already got: pass `&Session` + `&step_id`, route `Err` through the parallel outcome cell instead of `?`. The borrow shape differs slightly — the parallel path can't just re-borrow the turn_log entry while also passing `&session`, because the enclosing closure captures session by mutable reference. Clone the last entry up front to release the immutable borrow before calling the evaluator.
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.
Closes part of #130 — the consumption half (how users gate on a signal). The production half (capturing logprobs/confidence from native runners) is deliberately deferred until the native LLM runner (#128) lands with tool support.
Summary
Decouples two concerns that #130 conflated:
This PR ships (2). When (1) lands,
expression: "{{ step.x.confidence }} >= 0.75"drops in without further spec or grammar work onon_result.What's new
expression:matcher — arbitrary §12.2 condition against any template variable in the turn log. Unlocks branching onstdout/stderr(whichcontains:can't reach on context steps), specific exit codes beyond0/any, and cross-step references.matches: /PAT/FLAGSnamed matcher — regex shorthand for the response.matchesoperator — regex comparison, shared withcondition:so the two grammars cannot drift./PAT/FLAGS. Flagsi/m/saccepted.grejected at parse time with a specific diagnostic (boolean matching — "global" is meaningless). Other Perl flags rejected; guidance points at inline(?x)for verbose.Design choices worth flagging
on_resultfor months can't surprise you at 3am when a specific response finally triggers it.ConditiondropsPartialEq.regex::Regexhas noPartialEq; the one existingassert_eq!onOption<Condition>was rewritten as amatches!pattern. No production code compared Condition values for equality.ResultMatcher::Expression { source, condition }— thesourcefield preserves the original literal for materialize round-trips and diagnostics.conditionreuses the fullConditionunion, so comparison and regex forms go through one evaluator.matches:desugars at parse time into the expression form. Runtime has exactly one regex evaluation path.evaluate_on_resultsignature change — now takes&Sessionand returnsResult<Option<ResultAction>, AilError>. Unresolvable template in anexpression:LHS aborts viaCONDITION_INVALID, matching the §11 template-resolution contract. No silent-non-match fallback.gflag rejected with a specific error, not silently ignored. Half the web has muscle memory for/foo/gi; silently dropping it would let patterns look like they mean something./PASS/matches"tests PASSED". Matches JavaScript/Perl/Ruby convention rather than inventing a different default.(?i)or/foo/ifor case-insensitive.Deliberately out of scope
DoWhile::exit_whenstaysConditionExpr— no regex support in loop exits yet. Widening it to the fullConditionunion needs its own validation rules (exit_when rejectsAlways/Never) and is a separate follow-up.<,<=,>,>=) and boolean combinators (&&,||). The spec's planned-extensions block flags these as the joint §12.2 + §5.4 extension that unlocks confidence-score gating once native runners surface the signals.Test plan
\/escaping,grejection, invalid patterns, empty-pattern guard)Condition::Regexevaluation: 4 new unit testsmatchesintegration tests: basic, case-sensitivity defaults,iflag, full parser path, invalid regex → parse failure,g-flag rejectionexpression:on stderr, namedmatches:shorthand,expression:withmatchesoperator, unresolvable template aborts, parse-time multi-matcher rejectioncargo clippy— zero new warnings introduced (29 pre-existing errors in unrelated files unchanged)cargo fmt --checkcleanCommit history on the branch
a434577expression:matcher0c4e660matchesoperator17450dd/PATTERN/FLAGSregex-literal syntax0086ac5e1e1540https://claude.ai/code/session_01GX2TW85n2yzAyTZ8TyM1Wj