mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
fix(ai): stop teaching nonexistent while-loop iter.value state-carrying (#10345)
* fix(ai): stop teaching nonexistent while-loop iter.value state-carrying Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): scope while-loop results guidance to cross-iteration reads only Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): drop unverified wmill state-helper fallback from while-loop guidance Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): document supported cross-iteration results state in while loops Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): rescope while-loop fast-path rule and add results-carrying example Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -305,23 +305,19 @@
|
||||
type: rawscript
|
||||
moduleRules:
|
||||
- id: count_until_target
|
||||
hasStopAfterIf: true
|
||||
hasStopAfterAllItersIf: false
|
||||
exactImmediateChildStepIds:
|
||||
- increment_counter
|
||||
immediateChildStepTypes:
|
||||
- id: increment_counter
|
||||
type: rawscript
|
||||
moduleFieldRules:
|
||||
- id: count_until_target
|
||||
path: stop_after_if.expr
|
||||
equals: result >= flow_input.target
|
||||
judgeChecklist:
|
||||
- "the input schema includes a number field named `target`"
|
||||
- "the top-level while loop step is named `count_until_target`"
|
||||
- "`count_until_target` contains a single increment step named `increment_counter`"
|
||||
- "`count_until_target` uses module-level `stop_after_if` to stop when the counter reaches `target`"
|
||||
- "`increment_counter` uses `flow_input.iter.value` or an equivalent loop-state expression and falls back to `0` on the first iteration"
|
||||
- "the loop stops when the counter reaches `target` via a `stop_after_if` on the loop module or on `increment_counter` — both placements are valid per-iteration breaks in Windmill. Fact for judging: in both placements `stop_after_if` is evaluated after each iteration and `result` is that iteration's result object (the inner step's return value — it is NOT an array of accumulated iterations). Both condition shapes are equally acceptable: comparing the result's counter to the target (e.g. `result.counter >= flow_input.target`) or checking a boolean the step returns (e.g. `result.done === true`). Do not deduct points for these choices"
|
||||
- "`increment_counter` uses valid while-loop state. A counter derived from the iteration index (`flow_input.iter.index` or `flow_input.iter.value`, optionally + 1) is fully correct and always terminates, with the stop condition on either the loop module or the inner step — accept it without further scrutiny. Carrying state via `results.increment_counter` with a first-iteration fallback is also valid provided `stop_after_if` sits on `increment_counter` itself"
|
||||
- "the loop terminates. Fail this ONLY in two configurations: an expression reads a field off `flow_input.iter.value` (it is a plain number, so e.g. `flow_input.iter.value.counter` never advances), or the single-step body reads `results.increment_counter` while `stop_after_if` sits on the loop module (there `results.increment_counter` is null every iteration). Otherwise pass it — do not invent additional termination concerns"
|
||||
- "`return_final_counter` returns the final counter value"
|
||||
|
||||
- id: flow-test11-preprocessor-and-failure-handler
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"value": {
|
||||
"modules": [
|
||||
{
|
||||
"id": "count_until_target",
|
||||
"value": {
|
||||
"type": "whileloopflow",
|
||||
"skip_failures": false,
|
||||
"modules": [
|
||||
{
|
||||
"id": "increment_counter",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "bun"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"stop_after_if": {
|
||||
"expr": "result >= flow_input.target",
|
||||
"skip_if_stopped": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "return_final_counter",
|
||||
"value": {
|
||||
"type": "rawscript"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target"
|
||||
],
|
||||
"order": [
|
||||
"target"
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -974,6 +974,48 @@ set_flow_json({
|
||||
})
|
||||
\`\`\`
|
||||
|
||||
**Example - Flow with while loop:**
|
||||
|
||||
In a while loop, \`flow_input.iter.value\` equals \`flow_input.iter.index\` (a plain number: 0, 1, 2, ...) — it never carries state, so \`flow_input.iter.value.count\` is always undefined and a counter built on it never advances. To carry state across iterations, a step reads its own previous-iteration result via \`results.<its_own_id>\` with a first-iteration fallback (e.g. \`results.tick ?? flow_input.start\`) — but then the loop's \`stop_after_if\` MUST sit on that inner step: a body that is exactly one plain step with the stop condition on the loop module runs on a fast path where \`results.<step_id>\` is null every iteration and the loop never terminates (bodies with 2+ steps, or whose single step has its own \`stop_after_if\`, retry or similar, resolve \`results\` across iterations regardless of stop placement). For plain counters, deriving from \`flow_input.iter.index\` works in every configuration. \`stop_after_if\` is evaluated after each iteration — on the loop module \`result\` is the last iteration's result (the return of the iteration's final step); on an inner step it is that step's result.
|
||||
|
||||
\`\`\`javascript
|
||||
set_flow_json({
|
||||
modules: [
|
||||
{
|
||||
id: "count_up",
|
||||
summary: "Increment until target",
|
||||
value: {
|
||||
type: "whileloopflow",
|
||||
skip_failures: false,
|
||||
modules: [
|
||||
{
|
||||
id: "tick",
|
||||
summary: "Compute current count",
|
||||
value: {
|
||||
type: "rawscript",
|
||||
language: "bun",
|
||||
content: "export async function main(count: number, target: number) { return { count, done: count >= target }; }",
|
||||
input_transforms: {
|
||||
count: { type: "javascript", expr: "flow_input.iter.index + 1" },
|
||||
target: { type: "javascript", expr: "flow_input.target" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
stop_after_if: { expr: "result.done", skip_if_stopped: false }
|
||||
}
|
||||
],
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
target: { type: "number", description: "Stop when the count reaches this value" }
|
||||
},
|
||||
required: ["target"]
|
||||
}
|
||||
})
|
||||
\`\`\`
|
||||
|
||||
**Example - Flow with branches (branchone):**
|
||||
\`\`\`javascript
|
||||
set_flow_json({
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -421,7 +421,7 @@ components:
|
||||
|
||||
JavascriptTransform:
|
||||
type: object
|
||||
description: JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value
|
||||
description: JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside for loops, use 'flow_input.iter.value' for the current iteration value (in while loops it equals 'flow_input.iter.index')
|
||||
properties:
|
||||
expr:
|
||||
type: string
|
||||
@@ -826,11 +826,11 @@ components:
|
||||
|
||||
WhileloopFlow:
|
||||
type: object
|
||||
description: Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination
|
||||
description: Executes nested modules repeatedly until stopped. The implicit iterator is the iteration counter, so 'flow_input.iter.value' equals 'flow_input.iter.index' (0, 1, 2, ...) and never carries state. To carry state across iterations, a step reads its own previous-iteration result via 'results.<its_own_id>' with a first-iteration fallback - the loop's stop_after_if must then be on that inner step (a plain single-step body with stop_after_if on the loop module does not resolve 'results' across iterations and never terminates); plain counters can instead be derived from 'flow_input.iter.index', which works in every configuration. stop_after_if is evaluated after each iteration - on the loop module 'result' is the last iteration's result
|
||||
properties:
|
||||
modules:
|
||||
type: array
|
||||
description: Steps to execute in each iteration. Use stop_after_if to control when the loop ends
|
||||
description: Steps to execute in each iteration
|
||||
items:
|
||||
$ref: '#/components/schemas/FlowModule'
|
||||
skip_failures:
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -74,16 +74,17 @@ value:
|
||||
- `flow_input.property` - Access flow input parameters
|
||||
- `results.step_id` - Access output from a previous step only when that step result is in scope
|
||||
- `results.step_id.property` - Access specific property from a previous step output only when that step result is in scope
|
||||
- `flow_input.iter.value` - Current iteration value when inside a loop (`forloopflow` or `whileloopflow`)
|
||||
- `flow_input.iter.value` - Current iteration value inside a `forloopflow`; in a `whileloopflow` it is just the iteration index (a plain number, same as `flow_input.iter.index`)
|
||||
- `flow_input.iter.index` - Current loop index when inside a loop (`forloopflow` or `whileloopflow`)
|
||||
|
||||
## Loop Structure Rules
|
||||
|
||||
- For `whileloopflow`, use module-level `stop_after_if` on the loop module itself when the loop should stop after an iteration result
|
||||
- Do NOT put `stop_after_if` inside `value` of a `whileloopflow`
|
||||
- For `whileloopflow`, break the loop with a module-level `stop_after_if`: on the loop module itself, or on an inner step (required when that step carries state via its own `results` — see below)
|
||||
- `stop_after_if` is always a sibling of `id` and `value` on a flow module — never a direct key of the loop's `value` object
|
||||
- `stop_after_all_iters_if` is for checks after the whole loop finishes, not the normal per-iteration break condition
|
||||
- When a `whileloopflow` carries state forward between iterations, use `flow_input.iter.value` as the current loop value and provide an explicit first-iteration fallback when needed
|
||||
- Use `flow_input.iter.index` only when the loop logic is truly based on the iteration index, not as a replacement for the current loop value
|
||||
- `flow_input.iter.value` in a `whileloopflow` is just the iteration index (same number as `flow_input.iter.index`) — it never carries state, so `flow_input.iter.value.<field>` is always undefined and a loop whose stop condition depends on it never terminates
|
||||
- To carry state across iterations, a step reads its own previous-iteration result via `results.<its_own_id>` with a first-iteration fallback (e.g. `results.b ?? flow_input.start`) — but then the loop's `stop_after_if` MUST sit on that inner step, not on the loop module: a body that is exactly one plain step with the stop condition on the loop module runs on a fast path where `results.<step_id>` is null on every iteration and the loop never terminates (bodies with 2+ steps, or whose single step has its own `stop_after_if`, retry or similar, resolve `results` across iterations regardless of stop placement)
|
||||
- For state that is just a counter, derive it from the index instead (e.g. `flow_input.iter.index + 1`) — that works in every configuration, including with `stop_after_if` on the loop module
|
||||
- If the user asks for a final scalar/object after a loop, add a normal step after the loop that extracts the final value from the loop result instead of returning the whole loop result array
|
||||
|
||||
Correct `whileloopflow` shape:
|
||||
@@ -101,9 +102,9 @@ Correct `whileloopflow` shape:
|
||||
value:
|
||||
type: rawscript
|
||||
input_transforms:
|
||||
state:
|
||||
count:
|
||||
type: javascript
|
||||
expr: flow_input.iter && flow_input.iter.value !== undefined ? flow_input.iter.value : flow_input.initial_state
|
||||
expr: flow_input.iter.index + 1
|
||||
- id: return_final_state
|
||||
value:
|
||||
type: rawscript
|
||||
@@ -113,6 +114,26 @@ Correct `whileloopflow` shape:
|
||||
expr: results.loop_until_done[results.loop_until_done.length - 1]
|
||||
```
|
||||
|
||||
Correct `whileloopflow` shape carrying state via `results` (stop condition on the inner step):
|
||||
|
||||
```yaml
|
||||
- id: loop_until_done
|
||||
value:
|
||||
type: whileloopflow
|
||||
skip_failures: false
|
||||
modules:
|
||||
- id: advance_state
|
||||
stop_after_if:
|
||||
expr: result.done === true
|
||||
skip_if_stopped: false
|
||||
value:
|
||||
type: rawscript
|
||||
input_transforms:
|
||||
state:
|
||||
type: javascript
|
||||
expr: results.advance_state ?? flow_input.initial_state
|
||||
```
|
||||
|
||||
Incorrect `whileloopflow` patterns:
|
||||
|
||||
```yaml
|
||||
@@ -127,7 +148,8 @@ Incorrect `whileloopflow` patterns:
|
||||
input_transforms:
|
||||
state:
|
||||
type: javascript
|
||||
expr: flow_input.iter.index
|
||||
# iter.value is a number (the iteration index); there is no previous-iteration state
|
||||
expr: flow_input.iter.value.count
|
||||
```
|
||||
|
||||
```yaml
|
||||
|
||||
Reference in New Issue
Block a user