Skip to content

feat(workflows): expose $LOOP_PREV_OUTPUT in loop node prompts (#1286)#1367

Merged
Wirasm merged 4 commits intocoleam00:devfrom
voidborne-d:feat/loop-prev-output
Apr 27, 2026
Merged

feat(workflows): expose $LOOP_PREV_OUTPUT in loop node prompts (#1286)#1367
Wirasm merged 4 commits intocoleam00:devfrom
voidborne-d:feat/loop-prev-output

Conversation

@voidborne-d
Copy link
Copy Markdown
Contributor

@voidborne-d voidborne-d commented Apr 22, 2026

Summary

  • Problem: loop: nodes compute lastIterationOutput at the end of each iteration but immediately discard it — the prompt-substitution layer never sees it. With fresh_context: true the next iteration starts blind, so retry-on-failure / generate-then-review patterns can't see what the previous pass produced.
  • Why it matters: enables the implement→validate, generate→review, and retry-on-failure patterns described in feat: $LOOP_PREV_OUTPUT variable for loop nodes #1286 without dragging full session history forward (fresh_context: false defeats the point) or abusing until_bash / $ARTIFACTS_DIR shuffling as a data channel.
  • What changed: implements $LOOP_PREV_OUTPUT — empty string on iteration 1, previous iteration's cleaned output (after stripCompletionTags) on iteration 2+. substituteWorkflowVariables accepts a 10th positional loopPrevOutput?: string; executeLoopNode passes lastIterationOutput. Closes feat: $LOOP_PREV_OUTPUT variable for loop nodes #1286.
  • What did not change (scope boundary): no schema change, no breaking change to LoopNode YAML. Prompts that don't reference $LOOP_PREV_OUTPUT are bit-for-bit unaffected — the substitution regex is a literal no-op when the token is absent. The DoD item archon-compose-plan-implement-qa.yaml was skipped (file does not exist on dev); flagged below for maintainer direction.

UX Journey

Before

  Workflow author       Loop iter N           Substitution       Loop iter N+1
  ──────────────        ───────────           ─────────────      ─────────────
  defines prompt
  with no way to
  reference previous
  output                runs prompt ───────▶  $LOOP_USER_INPUT   (fresh_context=true)
                        produces output ──▶   $REJECTION_REASON  starts blind, no
                        lastIterationOutput   $nodeId.output     visibility into
                        DISCARDED            (no PREV_OUTPUT)    iter N's result

After

  Workflow author       Loop iter N           Substitution                 Loop iter N+1
  ──────────────        ───────────           ─────────────                ─────────────
  references
  $LOOP_PREV_OUTPUT
  in prompt             runs prompt ───────▶  $LOOP_USER_INPUT             receives iter N's
                        produces output ──▶   $REJECTION_REASON            cleaned output
                        cleanOutput stored    $nodeId.output               via $LOOP_PREV_OUTPUT
                        as lastIterationOutput *$LOOP_PREV_OUTPUT*         (empty on iter 1
                                                                             and on resume)

Architecture Diagram

Before

  packages/workflows/src/
    ├── executor-shared.ts
    │     └── substituteWorkflowVariables(prompt, ctx, ..., loopUserInput?, rejectionReason?)
    │            (9 positional params)
    └── dag-executor.ts
          └── executeLoopNode
                ├── lastIterationOutput: string | undefined
                ├── per iteration: passes loopUserInput / rejectionReason to substitution
                └── computes lastIterationOutput = cleanOutput(...)
                    └── DISCARDED — substitution never sees it

After

  packages/workflows/src/
    ├── [~] executor-shared.ts
    │     └── substituteWorkflowVariables(prompt, ctx, ..., loopUserInput?, rejectionReason?, [+] loopPrevOutput?)
    │            (10 positional params; default '')
    │            substitutes $LOOP_PREV_OUTPUT (literal regex pass)
    └── [~] dag-executor.ts
          └── executeLoopNode
                ├── lastIterationOutput: string | undefined
                ├── per iteration: now also passes lastIterationOutput as loopPrevOutput
                │     === iter 1 (or first-after-resume): '' (matches $LOOP_USER_INPUT guard)
                │     === iter 2+: previous iteration's cleanOutput
                └── computes lastIterationOutput = cleanOutput(...) — used next iter

Connection inventory:

From To Status Notes
executeLoopNode substituteWorkflowVariables modified passes lastIterationOutput as 10th arg
substituteWorkflowVariables prompt regex pass modified adds $LOOP_PREV_OUTPUT token
docs (variables.md, loop-nodes.md, CLAUDE.md) reference list modified new variable documented alongside $LOOP_USER_INPUT / $REJECTION_REASON
YAML LoopNode schema substitution unchanged no schema change — variable is purely runtime

Label Snapshot

  • Risk: risk: low
  • Size: size: M
  • Scope: workflows
  • Module: workflows:executor

Change Metadata

  • Change type: feature
  • Primary scope: workflows

Linked Issue

Validation Evidence (required)

bun test (workflows)   # 843 pass / 0 fail (5 new added: 3 unit + 2 integration)
bun run type-check     # workflows: clean
bun run lint           # root: clean
bun run format:check   # root: clean
bun run validate       # all workspaces, exit 0
  • Evidence provided: command names + pass counts above; new tests cover (a) substitution with value, (b) empty when omitted, (c) prompts that don't reference the variable are unaffected, (d) iter 1 sees '' and iter 2 sees iter 1's verbatim output, (e) <promise> tags are stripped (uses the same cleanOutput lastIterationOutput already stores).
  • Skipped commands: none.

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No
  • Note: $LOOP_PREV_OUTPUT carries AI-generated text from the same workflow run — same trust boundary as $nodeId.output. No new external surface.

Compatibility / Migration

  • Backward compatible? Yes — new parameter is optional with '' default; existing prompts without the token are no-ops.
  • Config/env changes? No
  • Database migration needed? No

Human Verification (required)

  • Verified scenarios: ran the new integration tests locally; manually traced executeLoopNode to confirm iter-1 vs iter-2+ values; verified the cleanOutput invariant (the same value lastIterationOutput stores is the value substituted).
  • Edge cases checked: resume-after-interactive-approval — the resumed first iteration starts with a fresh lastIterationOutput, so $LOOP_PREV_OUTPUT is '' there as well (consistent with iter 1 behaviour, documented in loop-nodes.md); prompts without the token (regex no-op); <promise> / completion tag stripping.
  • What was not verified: full end-to-end run of an archon-compose-* workflow — the DoD-referenced YAML doesn't exist on dev. See "Risks" below.

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: workflow variable substitution and loop: node execution only.
  • Potential unintended effects: extremely large lastIterationOutput blobs would now flow into the next iteration's prompt for users who reference $LOOP_PREV_OUTPUT. Same concern as $nodeId.output; mitigated by the existing cleanOutput step.
  • Guardrails / monitoring for early detection: existing workflow-execution telemetry; substitution failures surface through the standard executor error path.

Rollback Plan (required)

  • Fast rollback: git revert <merge-commit> — change is additive (new optional parameter + new token); revert is clean.
  • Feature flags or config toggles: none — variable is opt-in by virtue of needing to appear in the prompt; absent prompts are unaffected.
  • Observable failure symptoms: if a regression broke substitution, every loop using $LOOP_PREV_OUTPUT would see literal $LOOP_PREV_OUTPUT in the rendered prompt. Detect by greping prompt logs.

Risks and Mitigations

  • Risk: positional-arg drift — substituteWorkflowVariables now takes 10 positional params; future additions risk argument-order bugs.
    • Mitigation: matched the existing $LOOP_USER_INPUT / $REJECTION_REASON extension pattern intentionally for consistency. Happy to refactor to an options object in a follow-up if maintainers prefer; flagged but not in scope here.
  • Risk: DoD checkbox archon-compose-plan-implement-qa.yaml updated is unchecked — the referenced file does not exist on dev (find … archon-compose* returns nothing).
    • Mitigation: skipped pending direction. Happy to add a bundled example workflow demonstrating the variable, or wire it into one of the existing archon-*.yaml defaults — please advise which.
  • Risk: very large prior outputs inflate the next prompt and prompt-cache footprint.
    • Mitigation: substituted value is the cleaned output (post-stripCompletionTags), same shape as lastIterationOutput. Users opt in by referencing the token; non-referencing loops pay nothing.

Summary by CodeRabbit

  • New Features

    • Added $LOOP_PREV_OUTPUT: supplies the cleaned output from the previous loop iteration (empty on first iteration and after resuming) for fresh-context loops.
  • Bug Fixes

    • Prior-iteration output is cleaned (completion/tag segments removed) before being substituted into $LOOP_PREV_OUTPUT.
  • Documentation

    • Docs updated with behavior, substitution order, examples, retry patterns, and resume semantics.

…m00#1286)

Adds a new substitution variable that carries the previous loop iteration's
cleaned output into the next iteration's prompt. Empty on iteration 1; the
prior iteration's output (after stripCompletionTags) on iteration 2+.

Why: fresh_context: true loops have no way to reference what the previous
pass produced or why it failed without dragging the full session forward.
$LOOP_PREV_OUTPUT closes that gap with zero session-cost — same trust
boundary as $nodeId.output, no new external surface.

Changes:
- packages/workflows/src/executor-shared.ts: substituteWorkflowVariables
  accepts a 10th positional loopPrevOutput arg and substitutes
  $LOOP_PREV_OUTPUT (defaults to '').
- packages/workflows/src/dag-executor.ts: executeLoopNode passes
  lastIterationOutput on iteration 2+ (and explicit '' on iteration 1 /
  the first iteration of an interactive resume, since lastIterationOutput
  is a per-call variable that does not survive resume metadata).
- Unit tests: 3 new cases in executor-shared.test.ts.
- Integration tests: 2 new cases in dag-executor.test.ts verifying the
  prompt sent to the AI on iter 1 vs iter 2, and that the value reflects
  cleaned output (no <promise> tags).
- Docs: variables.md, loop-nodes.md (new "Retry-on-failure" pattern),
  CLAUDE.md variable reference.

Backward compatibility: prompts that don't reference $LOOP_PREV_OUTPUT are
unaffected. All 843 workflow tests + type-check + lint + format:check +
bun run validate pass locally.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

Added a loop-only workflow variable $LOOP_PREV_OUTPUT that substitutes the cleaned output from the previous loop iteration (empty on the first iteration and after resuming an interactive gate). Loop execution now passes prior-iteration output into prompt substitution; docs, tests, and changelog updated accordingly.

Changes

Cohort / File(s) Summary
Documentation
CLAUDE.md, packages/docs-web/src/content/docs/guides/loop-nodes.md, packages/docs-web/src/content/docs/reference/variables.md, CHANGELOG.md
Documented new $LOOP_PREV_OUTPUT variable, empty-on-first-iteration semantics, availability limited to loop prompts, behavior on resume after approval gates, guidance for fresh_context: true, retry-on-failure example, and that <promise> completions are stripped before substitution.
Core Implementation
packages/workflows/src/executor-shared.ts, packages/workflows/src/dag-executor.ts
Extended substituteWorkflowVariables(...) signature to accept loopPrevOutput?: string and updated executeLoopNode to pass '' on iteration 1 or lastIterationOutput on subsequent iterations into substitution; no other control-flow changes.
Tests
packages/workflows/src/executor-shared.test.ts, packages/workflows/src/dag-executor.test.ts
Added unit and integration tests verifying $LOOP_PREV_OUTPUT is empty on first iteration, populated with prior cleaned output thereafter, <promise> is stripped before substitution, and resume-after-gate yields empty $LOOP_PREV_OUTPUT for the resumed iteration. Existing tests for other variables remain unchanged.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(200,200,255,0.5)
    participant LoopExecutor
    participant Substituter
    participant Model
  end

  LoopExecutor->>Substituter: substituteWorkflowVariables(promptTemplate, ..., rejectionReason?, loopPrevOutput)
  Substituter-->>LoopExecutor: promptWithLoopPrevOutput
  LoopExecutor->>Model: sendQuery(promptWithLoopPrevOutput)
  Model-->>LoopExecutor: assistantOutput
  LoopExecutor->>Substituter: clean assistantOutput (strip <promise>) -> lastIterationOutput
  Note right of LoopExecutor: Next iteration uses lastIterationOutput as loopPrevOutput ('' on first/resumed iteration)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 I hop from pass to pass with nimble feet,
I carry last output so the loop can meet,
First hop is empty, then lessons show,
<promise> trimmed so the next run can know,
Retry, refine, repeat — and off I leap!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the main change—introducing $LOOP_PREV_OUTPUT variable in loop node prompts—and is specific and actionable.
Description check ✅ Passed PR description fills all template sections: summary (problem/why/what changed/scope), UX journey (before/after flows), architecture diagram (module changes), labels, metadata, linked issue, validation evidence, security, compatibility, human verification, side effects, and rollback plan.
Linked Issues check ✅ Passed All core objectives from #1286 are met: $LOOP_PREV_OUTPUT substitution added to executor-shared.ts, passed from executeLoopNode, documented in variables.md/loop-nodes.md/CLAUDE.md, tested with unit and integration tests covering iteration-1/2+ behavior and resume semantics.
Out of Scope Changes check ✅ Passed All changes align with #1286 scope: executor substitution, loop node execution, workflow docs, and tests. No schema changes, no DB/network/permission changes. One DoD item (archon-compose-plan-implement-qa.yaml) was skipped intentionally and documented as file-absent on dev; this is acknowledged and flagged for maintainer direction, not an unplanned change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/workflows/src/dag-executor.ts (1)

1769-1784: ⚠️ Potential issue | 🟠 Major

Compute $LOOP_PREV_OUTPUT from the fully stripped iteration output.

Line 1985 can fall back to raw fullOutput when cleanOutput is empty, which reintroduces <promise>...</promise> content into the next prompt even though $LOOP_PREV_OUTPUT is documented as cleaned. Strip the accumulated output once after the iteration and use that value.

🐛 Proposed fix
-    lastIterationOutput = cleanOutput || fullOutput;
+    lastIterationOutput = stripCompletionTags(fullOutput, loop.until);

Based on learnings, Variable substitution supports $LOOP_PREV_OUTPUT as the cleaned output of the previous loop iteration, empty on the first iteration.

Also applies to: 1985-1985

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/dag-executor.ts` around lines 1769 - 1784, The code is
passing raw fullOutput into substituteWorkflowVariables as lastIterationOutput
when cleanOutput is empty, which reintroduces <promise>…</promise> fragments;
compute a single stripped/cleaned iteration output (e.g., derive loopPrevOutput
by preferring cleanOutput but always applying the final strip/cleanup to the
accumulated output once after each iteration) and store it in
lastIterationOutput (or a new variable) and pass that sanitized value into
substituteWorkflowVariables (the call in dag-executor.ts using loop.prompt and
lastIterationOutput) so $LOOP_PREV_OUTPUT always receives the fully stripped
cleaned output.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/docs-web/src/content/docs/guides/loop-nodes.md`:
- Around line 207-209: The paragraph incorrectly states that all iterations 2+
receive the previous iteration's cleaned output; update the docs to document the
interactive-resume exception: explain that when the executor resumes
interactively it passes an empty string for $LOOP_PREV_OUTPUT if i ===
startIteration, so the very first resumed iteration (even if its numeric index
is >=2) receives '' rather than the previous cleaned output; reference the
executor behavior and the startIteration check ($LOOP_PREV_OUTPUT, executor, i
=== startIteration, startIteration) so readers understand this exception.

In `@packages/docs-web/src/content/docs/reference/variables.md`:
- Line 30: You added the new variable $LOOP_PREV_OUTPUT but didn’t update the
substitution-order list and the availability table; add $LOOP_PREV_OUTPUT to the
substitution-order list (in the Variable substitution section) and to the
availability table alongside other loop variables, documenting it as "Cleaned
output of the previous loop iteration (loop nodes only) — empty string on the
first iteration; useful for fresh_context: true loops that need the prior pass
without full session history." Ensure the exact variable name $LOOP_PREV_OUTPUT
is used in both places so the reference sections stay in sync.

---

Outside diff comments:
In `@packages/workflows/src/dag-executor.ts`:
- Around line 1769-1784: The code is passing raw fullOutput into
substituteWorkflowVariables as lastIterationOutput when cleanOutput is empty,
which reintroduces <promise>…</promise> fragments; compute a single
stripped/cleaned iteration output (e.g., derive loopPrevOutput by preferring
cleanOutput but always applying the final strip/cleanup to the accumulated
output once after each iteration) and store it in lastIterationOutput (or a new
variable) and pass that sanitized value into substituteWorkflowVariables (the
call in dag-executor.ts using loop.prompt and lastIterationOutput) so
$LOOP_PREV_OUTPUT always receives the fully stripped cleaned output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 791124e5-4828-4efa-b3ac-782dd479b704

📥 Commits

Reviewing files that changed from the base of the PR and between b99cee4 and f2cec71.

📒 Files selected for processing (7)
  • CLAUDE.md
  • packages/docs-web/src/content/docs/guides/loop-nodes.md
  • packages/docs-web/src/content/docs/reference/variables.md
  • packages/workflows/src/dag-executor.test.ts
  • packages/workflows/src/dag-executor.ts
  • packages/workflows/src/executor-shared.test.ts
  • packages/workflows/src/executor-shared.ts

Comment thread packages/docs-web/src/content/docs/guides/loop-nodes.md Outdated
Comment thread packages/docs-web/src/content/docs/reference/variables.md
- variables.md: include $LOOP_PREV_OUTPUT in substitution-order list and
  availability table to match the new variable row at line 30
- loop-nodes.md: document the interactive-resume exception where the first
  iteration after an approval-gate resume still receives an empty
  $LOOP_PREV_OUTPUT regardless of iteration number (per dag-executor.ts
  L1781-1783 where i === startIteration always clears prev output)
@voidborne-d
Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed both doc nits in 4d0ae8b:

  • loop-nodes.md: split the "empty on iter 1" note into continuous-run vs interactive-resume paths, with the reason the resume also gets an empty prev output even at iter 2+ (per dag-executor.ts L1781-1783 where i === startIteration always clears).
  • variables.md: added $LOOP_PREV_OUTPUT to both the substitution-order list at L92 and the availability table — now in sync with the row at L30.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

Hi @voidborne-d — thanks for opening this PR.

This repository uses a PR template at .github/pull_request_template.md with several required sections. A few of them appear to be empty or placeholder here:

  • UX Journey
  • Architecture Diagram
  • Label Snapshot
  • Change Metadata
  • Security Impact
  • Rollback Plan
  • Risks and Mitigations

Could you fill those out (even briefly)? The template helps reviewers understand scope, risk, and rollback — it speeds up review significantly.

If a section genuinely doesn't apply, just write "N/A" in it rather than leaving it blank.

@voidborne-d
Copy link
Copy Markdown
Contributor Author

Thanks for the prompt @Wirasm — filled out all seven sections per the template (UX Journey, Architecture Diagram, Label Snapshot, Change Metadata, Security Impact, Rollback Plan, Risks and Mitigations). One DoD item is still unchecked and called out under Risks: archon-compose-plan-implement-qa.yaml does not exist on dev, so I skipped it — happy to add a bundled example workflow demonstrating $LOOP_PREV_OUTPUT, or to wire the variable into one of the existing archon-*.yaml defaults if you point me at the right one.

@voidborne-d
Copy link
Copy Markdown
Contributor Author

Hi @Wirasm — thanks for the pass. I think the comment may have landed against a stale PR-body snapshot: all seven flagged sections are filled in the description right now. Quick anchors so you can scroll-verify without re-reading the whole body:

  • UX Journey — Before/After ASCII diagrams of "Workflow author / Loop iter N / Substitution / Loop iter N+1" showing where $LOOP_PREV_OUTPUT plugs in vs. the previous discard.
  • Architecture Diagrampackages/workflows/src/ tree (Before vs After with [~] / [+] markers) plus the connection inventory table covering executeLoopNode → substituteWorkflowVariables, prompt regex pass, docs, and YAML schema (unchanged).
  • Label Snapshotrisk: low, size: M, scope: workflows, module: workflows:executor.
  • Change Metadata — type feature, primary scope workflows.
  • Security ImpactNo / No / No / No, with a note that $LOOP_PREV_OUTPUT carries AI-generated text from the same workflow run (same trust boundary as $nodeId.output, no new external surface).
  • Rollback Plangit revert <merge-commit> (additive: optional 10th positional + new token); observable failure symptom is literal $LOOP_PREV_OUTPUT showing up in rendered prompt logs, greppable.
  • Risks and Mitigations — three risks listed: positional-arg drift on substituteWorkflowVariables (offered to refactor to options object as a follow-up), the unchecked DoD item for archon-compose-plan-implement-qa.yaml which doesn't exist on dev (asking for direction), and large-prior-output prompt-bloat (mitigated by cleanOutput/stripCompletionTags).

If any of those still reads as too thin or non-load-bearing, happy to expand whichever specifically — just call out which.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

Review Summary

Verdict: minor-fixes-needed

This PR adds the $LOOP_PREV_OUTPUT workflow variable for loop nodes — a well-implemented, backwards-compatible addition that lets iterations reference the previous pass's cleaned output. The implementation is solid, the tests are thorough (5 new tests covering unit and integration paths), and the documentation across all three Starlight pages is comprehensive. Two items need attention before merge.

Blocking issues

None.

Suggested fixes

  • CHANGELOG.md: No [Unreleased] entry for $LOOP_PREV_OUTPUT. Add a new ### Added subsection under ## [Unreleased]:

    **`$LOOP_PREV_OUTPUT` workflow variable (loop nodes only)** — exposes the previous iteration's cleaned output (after `<promise>` tag stripping) to the current iteration's prompt. Empty on the first iteration and on the first iteration after resuming from an interactive approval gate. Enables `fresh_context: true` loops to reference what the prior pass said or did without carrying full session history.
    
  • packages/workflows/src/dag-executor.test.ts: The continuous 2-iteration integration test covers normal runs but doesn't exercise the resume-from-approval scenario where $LOOP_PREV_OUTPUT must be empty (the prior output lived in a different run). Consider adding an integration test that calls executeDagWorkflow twice — first call triggers an approval gate, second call resumes and verifies $LOOP_PREV_OUTPUT is <<>> on iteration 1. You could extend the existing resume with priorCompletedNodes block to add this assertion and reuse the existing mock setup.

Minor / nice-to-have

  • loop-nodes.md frontmatter: The page's description field could mention $LOOP_PREV_OUTPUT to surface it in search and overview contexts. The in-body coverage is already thorough, so this is optional.

Compliments

  • The implementation precisely mirrors the existing $LOOP_USER_INPUT / $REJECTION_REASON extension pattern — no new surprises for someone reading the codebase.
  • The WHY-only comment at dag-executor.ts:1769-1773 documenting the resume-gate behavior is exactly the kind of institutional knowledge this codebase needs.
  • The "Retry-on-failure with $LOOP_PREV_OUTPUT" section in loop-nodes.md with its illustrative YAML is a model of clear feature documentation.

Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: code-review, test-coverage, comment-quality, docs-impact.

@voidborne-d
Copy link
Copy Markdown
Contributor Author

Thanks for the review @Wirasm 🔹

  • CHANGELOG: added the [Unreleased]### Added entry verbatim from your suggestion in 3fc5883.
  • Resume-from-approval integration test: agreed the existing 2-iteration test doesn't exercise the empty-on-resume branch. I'll add an integration test that drives executeDagWorkflow through an approval gate + resume and asserts $LOOP_PREV_OUTPUT is empty on the first post-resume iteration in a follow-up commit on this branch — keeping it scoped so the diff stays reviewable.
  • loop-nodes.md frontmatter description: I'll skip the frontmatter mention for now since you flagged it as optional and the in-body coverage is already deep; happy to add it if you'd rather have it surfaced in search.

Will ping again once the test commit lands.

…OUTPUT (coleam00#1367 review)

Per maintainer-review-pr suggestion (Wirasm): two-call integration test
covering the resume-from-approval scenario.

  - Call 1: fresh interactive loop pauses at the gate after iteration 1 and
    asserts $LOOP_PREV_OUTPUT substitutes to empty on iter 1 (no prior
    output) plus the gate pause is recorded.
  - Call 2: resumed run with metadata.approval populated. The first
    resumed iteration must substitute $LOOP_PREV_OUTPUT to '', NOT to the
    paused run's iter-1 output (which lived in a different process and is
    not persisted). $LOOP_USER_INPUT still flows through as normal.

Locks the documented invariant at dag-executor.ts:1769-1772.
@voidborne-d
Copy link
Copy Markdown
Contributor Author

@Wirasm thanks for the structured review — both suggested fixes addressed. PR head now 88a84c7.

1. CHANGELOG [Unreleased] entry — added in 3fc5883 with the verbatim copy you supplied (with (#1367) suffix on the heading).

2. Resume-from-approval integration test — added in 88a84c7 (packages/workflows/src/dag-executor.test.ts).

Wrote the two-call shape exactly as you outlined:

  • Call 1: fresh interactive loop, iter 1 emits no completion → pauses at the gate. Asserts $LOOP_PREV_OUTPUT substitutes to empty on iter 1 (PREV=<<>>.) plus the pauseWorkflowRun payload (type: 'interactive_loop', iteration: 1).
  • Call 2: resumed run with metadata.approval = { type: 'interactive_loop', nodeId: 'refine', iteration: 1, sessionId: 'loop-session-1', ... } and loop_user_input: 'looks good, ship it'. The resumed iteration emits <promise>COMPLETE</promise> so the loop exits via the completion path, not a second pause. Asserts $LOOP_PREV_OUTPUT is empty on the first resumed iteration (PREV=<<>>.) AND that the prior run's distinctive output string (Iter1 output: 2 type errors) is not present in the resumed prompt — that's the actual "different process, not persisted" invariant. $LOOP_USER_INPUT correctly flows through (User: looks good, ship it.).

Locks the documented invariant at dag-executor.ts:1769-1772.

Local gates green:

  • bun test src/dag-executor.test.ts → 192 pass / 0 fail (191 existing + 1 new)
  • bun run type-check → clean
  • prettier --check → clean
  • husky/lint-staged ran clean on commit

(3) — frontmatter description: skipping, since you flagged it as optional and the in-body coverage already names $LOOP_PREV_OUTPUT in two H2 sections.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/workflows/src/dag-executor.test.ts (1)

3203-3260: Tag‑stripping assertion is solid; consider also asserting iter‑2 doesn't contain the inner signal.

expect(promptIter2).not.toContain('<promise>') proves the angle‑bracketed tags are removed, but a future regression could strip only the wrapper tags while leaving the inner NOT_DONE_YET text in lastIterationOutput. An extra expect(promptIter2).not.toContain('NOT_DONE_YET') would fully lock in “cleaned via stripCompletionTags”.

♻️ Optional tightening
     expect(promptIter2).toContain('PREV=[Real work output.');
     expect(promptIter2).not.toContain('<promise>');
+    // Tag *contents* must also be gone — guards against a regression that
+    // strips only the wrapper tags.
+    expect(promptIter2).not.toContain('NOT_DONE_YET');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/dag-executor.test.ts` around lines 3203 - 3260, Add a
second negative assertion to ensure the inner signal text is removed as well:
after the existing checks on mockSendQueryDag and promptIter2 in the test that
calls executeDagWorkflow, assert that promptIter2 does not contain the inner
token (e.g., expect(promptIter2).not.toContain('NOT_DONE_YET')) so the test
verifies stripCompletionTags removes both wrapper tags and their inner content.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/workflows/src/dag-executor.test.ts`:
- Around line 3203-3260: Add a second negative assertion to ensure the inner
signal text is removed as well: after the existing checks on mockSendQueryDag
and promptIter2 in the test that calls executeDagWorkflow, assert that
promptIter2 does not contain the inner token (e.g.,
expect(promptIter2).not.toContain('NOT_DONE_YET')) so the test verifies
stripCompletionTags removes both wrapper tags and their inner content.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 10f93f8a-c9e6-4b6e-b87e-156f72e471b4

📥 Commits

Reviewing files that changed from the base of the PR and between 3fc5883 and 88a84c7.

📒 Files selected for processing (1)
  • packages/workflows/src/dag-executor.test.ts

@Wirasm Wirasm merged commit 287bb35 into coleam00:dev Apr 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: $LOOP_PREV_OUTPUT variable for loop nodes

2 participants