Skip to content

iterate-policy: consult selfLoopAutonomy in decideNextAction's pass-to-handoff step #6560

Description

@JSONbored

Context

Follow-up to #6060's decided ADR (see that issue's closing comment for the full decision). Blocked by the config-schema issue that adds AmsPolicySpec.selfLoopAutonomy -- this issue reads that field. This issue wires it into decideNextAction's actual decision logic and threads it end-to-end from the operator's parsed AmsPolicySpec through to IterationState.

decideNextActionWithReason (packages/loopover-engine/src/miner/iterate-policy.ts:143-179) currently has NO notion of autonomy -- its header explicitly calls this out as a deferred gap (iterate-policy.ts:18-21: "this module's decideNextAction is autonomy-level-agnostic today"). The precedence order (iterate-policy.ts:132-141) is: (1) rejectionSignaled always wins (iterate-policy.ts:10-13, "disengage SILENTLY on rejection" -- must stay absolute), (2) selfReview.kind === "ambiguous" always abandons, (3) selfReview.kind === "pass" is currently the ONLY path to "handoff" (iterate-policy.ts:154-156), (4)-(6) further abandon/continue checks. The new autonomy check narrows step 3 ONLY -- steps 1 and 2 are untouched.

The end-to-end plumbing precedent for a new field reaching IterationState already exists for rejectionSignaled: it's declared on IterateLoopInput (packages/loopover-engine/src/miner/iterate-loop.ts:85-88), resolved by the caller, and copied verbatim into the per-iteration IterationState at the loop's state-construction site (iterate-loop.ts:449-455). The actual "caller" that assembles IterateLoopInput from a real AmsPolicySpec is buildAttemptLoopInput in packages/loopover-miner/lib/attempt-input-builder.js:73-103 -- it already maps input.amsPolicySpec.maxIterations/maxTurnsPerIteration (attempt-input-builder.js:80-81) and passes input.rejectionSignaled straight through (attempt-input-builder.js:101); the new field follows the exact same two mappings.

Requirements

  • Add autonomyLevel?: AutonomyLevel | undefined to IterationState (iterate-policy.ts:65-88), importing AutonomyLevel the same way settings/autonomy.ts:1 does (from ../types/manifest-deps-types.js, since iterate-policy.ts lives one level under src/ same as settings/autonomy.ts). Optional and defaults to treated-as-"auto" when undefined -- same precedent as costCeilingReached (iterate-policy.ts:71-77, with its own dedicated omitted/false tests at packages/loopover-engine/test/iterate-policy.test.ts:114-131) -- so every pre-existing hand-built IterationState fixture across the test suite remains valid with unchanged output.
  • Add requiresApproval?: true | undefined to IterateLoopDecision (iterate-policy.ts:111-118), mirroring autonomyRequiresApproval (settings/autonomy.ts:46-48). Additive-only field; do not touch any existing .action-only assertion.
  • Add "autonomy_observe_only" to the AbandonReason union (iterate-policy.ts:30-38).
  • In decideNextActionWithReason, narrow the step-3 selfReview.kind === "pass" branch ONLY (iterate-policy.ts:154-156) into exactly this behavior, resolving the effective level as state.autonomyLevel ?? "auto":
    • "auto" (or unset): unchanged -- { action: "handoff", reason: "..." }, no requiresApproval.
    • "auto_with_approval": still { action: "handoff", ... }, PLUS requiresApproval: true.
    • "observe": { action: "abandon", abandonReason: "autonomy_observe_only", reason: "..." } -- the reason string must note a clean predicted-gate pass WAS reached but the configured level keeps the loop observe-only (do not reuse self_review_ambiguous's or any other existing reason's wording).
    • Do not alter steps 1, 2, 4, 5, or 6 of the precedence ladder in any way.
  • Add autonomyLevel?: AutonomyLevel | undefined to IterateLoopInput (iterate-loop.ts:43-89), same optional/doc-comment shape as rejectionSignaled (iterate-loop.ts:85-88), and copy it into the IterationState construction at iterate-loop.ts:449-455.
  • In buildAttemptLoopInput (packages/loopover-miner/lib/attempt-input-builder.js:73-103), map autonomyLevel: input.amsPolicySpec.selfLoopAutonomy into the returned IterateLoopInput, following the exact same pass-through pattern as rejectionSignaled at attempt-input-builder.js:101.
  • maxIterations/capLimits remain the only iteration/budget controls -- this field must gate ONLY the pass->handoff transition, nothing else (no change to iteration-count or write-action-scope logic anywhere in this diff).

Deliverables

  • IterationState.autonomyLevel, IterateLoopDecision.requiresApproval, AbandonReason's new "autonomy_observe_only" variant.
  • The step-3 narrowing in decideNextActionWithReason for all three levels.
  • IterateLoopInput.autonomyLevel plus its wiring into IterationState inside runIterateLoopCore.
  • buildAttemptLoopInput's amsPolicySpec.selfLoopAutonomy -> autonomyLevel mapping.

Test Coverage Requirements

Read this before starting. Same Codecov caveat as the config-schema issue: packages/loopover-engine/src/** and packages/loopover-miner/lib/** are BOTH in vitest.config.ts's coverage.include, and BOTH are measured for Codecov's 99% patch gate ONLY through root test/**/*.test.ts (vitest) -- never through packages/loopover-engine's own node --test suite. Update BOTH the root vitest tests (required for Codecov) and packages/loopover-engine/test/iterate-policy.test.ts (required to keep npm run test --workspace @loopover/engine green in test:ci).

  • All three autonomyLevel values tested against a selfReview.kind === "pass" state: "auto" -> handoff/no requiresApproval; "auto_with_approval" -> handoff/requiresApproval: true; "observe" -> abandon/abandonReason: "autonomy_observe_only".
  • An explicit byte-identical regression test: construct the exact same passing IterationState twice, once with autonomyLevel: undefined (or the field omitted entirely) and once with autonomyLevel: "auto" explicitly, and assert decideNextActionWithReason returns deep-equal decisions for both -- proving the new field is a true no-op when unset, matching costCeilingReached's own omitted/explicit-default test pair (packages/loopover-engine/test/iterate-policy.test.ts:114-131).
  • A test proving rejectionSignaled and selfReview.kind === "ambiguous" still win over ANY autonomyLevel value, including "observe" (i.e. these steps are unreachable-by-autonomy, not merely untested by omission).
  • buildAttemptLoopInput's new mapping needs its own assertion in test/unit/miner-attempt-input-builder.test.ts (existing describe("buildAttemptLoopInput (#5132)", ...) block at line 112, following the same pattern as its existing "threads a real rejectionSignaled:true through unchanged" test at line 166) proving amsPolicySpec.selfLoopAutonomy flows unchanged into the built IterateLoopInput.autonomyLevel.

Expected Outcome

An operator's .loopover-ams.yml selfLoopAutonomy setting now actually gates whether a clean self-review pass hands off unconditionally, hands off pending approval, or stays observe-only-and-abandons -- with a repo that leaves the field unset behaving byte-identically to pre-this-issue code.

Links & Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:featureGittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions