assertWaypoint tool replaces the per-step postcondition field¶
Summary¶
Retired the bespoke step-level postcondition: { waypoint: ... } YAML field and replaced it with
assertWaypoint, a first-class framework tool. Waypoint end-state assertions are now something any
step can call, a branchy tool can compose, and — most importantly — the agent can select on its
own while authoring. Behavior is preserved; the capability just moved from a field to a tool.
Key decision (and why)¶
A postcondition: field can only ever be authored by a human (or special-cased codegen). The agent
drives an app by selecting tools — tap, inputText, assertVisible — and a step field isn’t
in that vocabulary, so the agent could never decide “I’ve arrived; lock in that I’m on the Checkout
waypoint.” That cuts against Trailblaze’s core idea of agent-driven authoring via tool selection.
Making it the assertWaypoint tool subsumes the field:
- The agent can pick it during authoring/exploration like any other tool.
- A branchy tool (handleOptionalScreens, dismissSetupBanner) can call it before returning to
guarantee its own “ends in a known state” contract.
- It stays hand-authorable in YAML/scripted trails (three-way tool parity).
The field was the strictly-less-capable form. The tool is the primitive.
What changed¶
AssertWaypointTrailblazeTool(trailblaze-common) —@TrailblazeToolClass(name = "assertWaypoint", isVerification = true, requiresHost = true). Reads the live screen + the session’s resolved target from theTrailblazeToolExecutionContext, resolves the registry host-side, polls, returnsSuccesson match /Erroron timeout/unknown-waypoint. Listed in the genericverificationtoolset (android/ios) and inweb_verification+compose_verificationso it surfaces on every host-orchestrated driver that supplies a selector node tree. Revyl is intentionally excluded — it’s vision-grounded and has no selector tree for the matcher to evaluate.WaypointAssertion— the pure poll-until-match engine, relocated fromStepPostconditionAsserterand decoupled from the deletedStepPostconditionmodel (takes plainwaypointId/timeoutMs/pollIntervalMs+ injectedscreenStateProvider/waypointResolver/now). Unit-tested directly.WaypointRegistryResolver— consolidates the two near-identical host helpers (resolveWaypointsForRun/resolveWaypointsForTrail) into one process-cached resolver. The analyzer-backed scripted-tool enrichment (host-only) is installed once viaTrailblazeHostYamlRunner’sinit.- Removed the
postconditionfield fromPromptStep/DirectionStep/VerificationStep+ its serializer element + theStepPostconditionmodel, and the dormant per-step assertion blocks inDeterministicTrailExecutor,MultiAgentV3Runner, andTrailblazeRunnerUtil(plus theirscreenStateProvider/waypointResolver/targetwiring throughTrailApi,TrailblazeHostYamlRunner,BaseHostTrailblazeTest,AndroidTrailblazeRule). trailblaze waypoint shortcut verifycodegen now emitsassertWaypointrecorded-tool steps.- Regenerated the
:trailblaze-modelsapiCheckbaselines (thepostconditionaccessor is gone).
Why requiresHost¶
The waypoint registry + matcher live host-side. With requiresHost = true the tool executes on the
host JVM for host-orchestrated runs of any driver (where the registry resolves), while pure
on-device agents — which can’t reach the registry — drop it at registration. Keeping the class in
trailblaze-common (not trailblaze-host) means the toolset name still resolves on every classpath;
the flag, not the class location, is what scopes execution.
One wiring gap fixed along the way: the host V3-accessibility tool-execution context didn’t populate
screenStateProvider, so a host-side verification tool had nothing to poll on that path. It now does.
What this rode on top of¶
Landed right after the Waypoints v2 hard cut (#4229, classifier-keyed route-bound definitions). The
matcher/asserter public surface was unchanged by v2 — the only v2-specific addition the relocated
engine carries is the NO_CLASSIFIER_BLOCK skip reason in the mismatch diagnostic.
Migrated callers¶
- The one in-tree trail under
trails/that declared a postcondition (a Square iOS create-item case) had its postcondition removed, not converted. That postcondition was a dormant no-op on the real run paths, so dropping it is behavior-preserving. It is NOT re-expressed as anassertWaypointstep yet because the registry resolver only loads classpath-bundled framework waypoints, and that trail’s waypoint (square/ios/more-tab-no-sheet) is an app/workspace waypoint the resolver can’t see on the on-demand IOS_HOST path — making the live assertion fail where the no-op silently passed. Wiring app/workspace-waypoint resolution is the immediate follow-up; that trail (and others) can adoptassertWaypointonce it lands. acceptance/postcondition-wiring-smoke— deleted. Its only purpose was proving the now-removed wiring fired through the daemon, asserting an intentionally-unknown waypoint (could never pass as a real assertion). The tool gets focused unit + contract tests instead.
Known limitation / immediate follow-up¶
WaypointRegistryResolver resolves classpath-bundled waypoints only (the framework trailblaze
trailmap). Waypoints defined by a target app’s own workspace trailmap (rather than the framework
stdlib) aren’t loaded, so assertWaypoint against them reports “unknown waypoint” on
host-orchestrated runs. The dormant postcondition hid this; the live tool exposes it. Follow-up:
load the workspace trailmaps (via the configured trailblaze.yaml anchor) in the resolver so
app-defined waypoints resolve, then migrate the real callers.
Not touched (different systems)¶
EnhancedRecording.postconditions (inferred recording conditions) and the TypeScript SDK’s
conditional-action.ts postcondition predicate are unrelated and left alone.
Tests¶
WaypointAssertionTest— the four poll outcomes (Matched / NotMatched / WaypointNotFound / NoScreenState), late-match settling, and templated-target forwarding, all with an injected clock.AssertWaypointTrailblazeToolTest— construction validation and the verdict polarity of the Result →TrailblazeToolResultmapping (match is the only success).