Skip to content

std:retry has no validation combinator: retry-until-predicate exists only at the workflow altitude #466

Closed

Description

@Masterplanner25

Summary

std:retry retries on failure. There is no combinator that retries on a
validation predicate — "call this, check the result, call it again with the
failure in hand if the check does not hold, up to N times."

goal ... over ... { until reached("label") budget { max_iterations: N } } (#409,
5.0.0) supplies exactly this at the workflow altitude. Nothing supplies it at the
function altitude, so a caller who wants a bounded validated retry around a single
call has to stand up a workflow to get one.

Current state

src/nodus/stdlib/retry.nd is two functions:

fn available() { return retry_available() }
fn call(func, policy) { return retry_call(func, policy) }

retry_call re-attempts on error. A result that returns successfully but is wrong
— a malformed edit, a schema-invalid payload, a plan that fails its own check — is
not a retry trigger.

The workflow-altitude form does work, and the state carrier is real: builtin_run_goal_pursuit
resumes the same graph from the last checkpoint rather than restarting it
(src/nodus/vm/vm.py:1433-1448), so workflow state survives across passes. A step
can write the failure into state and the next pass reads it. So the semantics are
already settled — this issue is about exposing them one altitude down.

What to add

import "std:retry"

let result = retry.until(
    fn() { return edit(source) },
    fn(r) { return r.ok },            // predicate; retry while false
    {max_attempts: 3}
)

The predicate receives the previous result so the caller can decide; the failing
result should also be reachable by the next attempt (a last field on the policy
map, or pass it as a second argument to func) — otherwise "re-send the error"
has no carrier and the retry is a blind re-roll, which is the thing the pattern
exists to avoid.

Design notes

  • Do not duplicate the goal budget vocabulary. If retry.until takes
    {max_attempts, deadline_ms} it should mean the same thing budget { max_iterations, deadline_ms } means, so the two altitudes read alike.
  • @retry requires the nodus-lang[retry] extra; the runtime falls back to the
    built-in InMemoryEffectStore when absent. Decide whether retry.until needs
    the effect store at all — it probably does not, since a bounded in-process loop
    has nothing to make idempotent.

Provenance

C:\codev\Aider research\AIDER_AINDY_LENS_AUDIT.md §5 "Absorb into Nodus": "the
max_reflections=3, re-send-the-error loop is a reusable in-VM control structure;
lift it into the language as a guarded retry-on-validation-failure step."
Written
2026-06-24 against 4.0.5, before #409 existed. Half of it shipped in 5.0.0; this is
the other half.

Affected versions

v5.0.4 (current when filed).

Activity

  1. added this to the v5.0 milestone on Aug 18, 2026
  2. Masterplanner25 commented on Aug 18, 2026

    @Masterplanner25
    OwnerAuthor

    Filed together from one cross-check of the four C:\codev\Aider research documents against 5.0.4 (AIDER_AINDY_LENS_AUDIT.md, AIDER_ON_AINDY_RUNTIME_PORTABILITY_ANALYSIS.md, ACCURACY_CHECK_vs_aindy-runtime_2.4.0.md; AIDER_ARCHITECTURAL_AUDIT.md contains no Nodus claims). The set:

    Ranked: #467 first, then #466, then #465, then #468.

    Nothing else the documents raised is Nodus work — the remaining corrections are aindy-runtime pinning nodus-lang==5.0.1 and needing 5.0.4 (#411 annotation forgery, #185 shared memory store, #390 workflow ownership all land in 5.0.2/5.0.3, and 5.0.3 alone breaks nodus-sdk).

  3. removed this from the v5.0 milestone on Aug 25, 2026
  4. added 2 commits that reference this issue on Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions