Add runtime for_each item injection - #653
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds first-class runtime for_each fan-out support to the parallel stage: a parallel node can now take a flat context key that resolves to a JSON array (inline or managed artifact), dispatch one agent/prompt execution per item with bounded concurrency and retries/timeouts, and preserve stable (target_id, index) identities plus human-friendly item_label across events, projections, API client, CLI progress, and the web timeline.
Changes:
- Implement
for_eachplanning + item→prompt injection (engine-owned fenced data) and ordered result identity (index,item_label) in the parallel handler. - Refactor node execution to share a single-attempt envelope between the core executor and parallel-branch runner; add detached stage-execution reservations for concurrent branch dispatch.
- Extend types, OpenAPI + TS client/web parsing, CLI rendering, validation rule(s), and docs to reflect the new contract and observability/security implications.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| lib/packages/fabro-api-client/src/models/parallel-branch-result.ts | Adds index and item_label fields to the TS API client model for parallel results. |
| lib/foundation/fabro-types/src/run_event/misc.rs | Extends parallel branch started/completed event props with optional item_label. |
| lib/foundation/fabro-types/src/parallel.rs | Extends ParallelBranchResult with optional index/item_label plus round-trip/legacy tests. |
| lib/foundation/fabro-types/src/graph.rs | Adds Node::for_each() accessor and tests for the new graph attribute. |
| lib/components/fabro-workflow/tests/it/integration.rs | Adds an E2E twin scenario covering structured-array → for_each fan-out → fan-in semantics and trust-boundary prompt text. |
| lib/components/fabro-workflow/src/stage_execution.rs | Introduces reserve_detached for concurrent branch executions without taking active-scope/resume provenance. |
| lib/components/fabro-workflow/src/node_handler.rs | Extracts shared execute_single_attempt + finalize_retries_exhausted for reuse by branch execution paths. |
| lib/components/fabro-workflow/src/handler/parallel.rs | Implements for_each contract: source resolution, item labeling, prompt injection, per-item retries/timeouts/cancellation, join selection, and new events/results metadata. |
| lib/components/fabro-workflow/src/git.rs | Updates tests/fixtures to include new ParallelBranchResult fields. |
| lib/components/fabro-workflow/src/event/names.rs | Updates event name tests for new optional item_label field. |
| lib/components/fabro-workflow/src/event/events.rs | Adds optional item_label to parallel branch started/completed event variants. |
| lib/components/fabro-workflow/src/event/convert.rs | Converts new parallel-branch item_label fields into stored event bodies and updates tests. |
| lib/components/fabro-workflow/src/artifact.rs | Adds resolve_json_value to hydrate structured JSON from blob/managed-file refs; includes tests. |
| lib/components/fabro-validate/src/rules/mod.rs | Registers the new for_each_contract lint rule. |
| lib/components/fabro-validate/src/rules/for_each_contract.rs | Adds validation enforcing the for_each structural contract (parallel-only, single template edge, agent/prompt target, no nesting). |
| lib/components/fabro-store/tests/serializable_projection.rs | Updates projection round-trip tests for new parallel result identity fields. |
| lib/components/fabro-store/src/run_state.rs | Updates reducer tests to account for new optional item_label on parallel branch events. |
| lib/components/fabro-dump/src/lib.rs | Updates dump tests for new parallel result fields. |
| lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs | Updates CLI progress UI tests to display labeled for_each branches distinctly. |
| lib/apps/fabro-cli/src/commands/run/run_progress/event.rs | Renders parallel branch display names using item_label + (node #index) formatting. |
| docs/public/workflows/stages-and-nodes.mdx | Documents the for_each attribute and its behavioral contract on parallel nodes. |
| docs/public/tutorials/parallel-review.mdx | Adds a runtime-candidates tutorial section showing for_each usage and security caveats. |
| docs/public/reference/dot-language.mdx | Extends DOT reference for for_each semantics, trust boundary, ordering, and identity fields. |
| docs/public/execution/outcomes.mdx | Clarifies parallel outcomes, including “empty for_each array succeeds” behavior. |
| docs/public/execution/observability.mdx | Documents index/item_label in parallel branch events and notes source-bearing prompt retention. |
| docs/public/execution/context.mdx | Updates context docs for parallel.results shape and adds for_each runtime-array section. |
| docs/public/api-reference/fabro-api.yaml | Extends OpenAPI schema for ParallelBranchResult with index and item_label. |
| docs/internal/parallel-strategy.md | Updates internal strategy docs for for_each execution model, identity, retries, and observability semantics. |
| apps/fabro-web/app/components/stage-renderers/parallel-children.tsx | Displays itemLabel for branch rows and avoids ambiguous links when multiple results share a template target id. |
| apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx | Adds UI test ensuring item labels render and that ambiguous id-only links are suppressed. |
| apps/fabro-web/app/components/stage-renderers/helpers.ts | Parses index and item_label from parallel.completed results into web view models. |
| apps/fabro-web/app/components/stage-renderers/helpers.test.ts | Adds parsing test coverage for dynamic item identity fields. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Simplification pass over the for_each branch. No behavior change. Share what was duplicated: - Node::prompt_or_label replaces the "prompt, else label" fallback that agent, prompt, and the for_each item injector each wrote out. - context::lookup_flat replaces the "exact key, then strip context." lookup that condition.rs had twice and the for_each source had again. - is_llm_handler_type replaces the inline agent/prompt match, so the runtime and the for_each_contract rule agree by construction. - find_join_node now takes branch ids, so a for_each fan-out passes its template target instead of needing find_join_for_target. - collect_events moves to test_support; parallel and integration tests shared one copy already. - One ScriptedHandler replaces four test handlers that differed only in what they returned. Straighten the branch retry loop: - Reserve the branch scope once before the loop instead of guarding it with an Option, which removes three expect() calls. - acquire_branch_permit and backoff_or_cancel replace the cancel-aware select! blocks the loop repeated verbatim. - Keep the match arms in Executor::execute_with_retry order so the two loops stay easy to compare. Drop redundant state: - BranchPlan::is_for_each derives from template_target_id. - for_each_contract checks node type before source shape, so one mistake reports one diagnostic. Prove the new wire fields survive the OpenAPI boundary: the fabro-api round-trip fixture now carries index and item_label. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Ran a simplification pass over this branch (commit ee9189d). No behavior change; Reused what already existed
Straightened the branch retry loop
Dropped redundant state
Added coverage
Left alone, for you to decide
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 38 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
lib/components/fabro-workflow/src/stage_execution.rs:173
reserve_detachedincrements the per-node high-water mark but leaves any pendingresumed_fromprovenance untouched. If a run is resumed and this node later has a normalreserve/ensure, that later (potentially unrelated) execution will incorrectly consume the stale provenance link. A detached reservation should still clear (and ideally return) the pending provenance while keeping the execution non-active.
build_branch_plan read the runtime array before run_branches looked at `simulated`, so every for_each workflow failed under --dry-run with "for_each source '...' was not found in workflow context". Nothing had populated the key yet: upstream LLM nodes take Handler::simulate, which returns no context updates. A dry run now stands in one placeholder item when the source is absent or unusable, and simulates the template target once. Graph-shape mistakes still fail, since catching those is the point of a dry run. Also from review: - ITEM_FENCE_PREFIX replaces the bare "untrusted-" literal that render_item_data and its test each spelled out. - ItemRecordingHandler no longer guesses an item label by substring search. Nothing asserted it, and the third item's label "2" matched stray hex from the random fence tag about two thirds of the time. - The twin test reads keys::PARALLEL_RESULTS instead of the raw string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Follow-up. Two more review passes landed after my first comment; both surfaced things the reuse pass missed. Fixed one real bug (commit 4efaf5c), and below is what I verified but did not change. Fixed:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 38 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
lib/components/fabro-workflow/src/handler/parallel.rs:486
- In the branch retry loop,
CoreError::Cancelledis returned asErr(Error::Cancelled), which skipsemit_branch_completed(...)inside the task. The join-loop fallback then emitsparallel.branch.completedwithduration_ms: 0(and only when a scope exists), producing misleading timing and leaving completion emission to an outer path.
Consider treating CoreError::Cancelled as a terminal per-branch failure outcome (so the branch task can emit parallel.branch.completed with the real elapsed time) while still allowing the group to end as cancelled via FailureCategory::Canceled.
break node_handler::finalize_retries_exhausted(&target, outcome);
}
Ok(outcome) => break outcome,
Err(CoreError::Cancelled) => return Err(Error::Cancelled),
Err(err) if can_retry && err.is_retryable() => {}
Err(err @ CoreError::Handler { .. }) => break err.to_fail_outcome(),
Addresses three Copilot review comments on #653. `item_label` comes from a model or a workflow author, and it reaches the terminal through the CLI progress display. A label could carry ANSI escapes, newlines, or bidi overrides and rewrite what the operator sees. It could also be whitespace-only, giving a branch a blank identity. Add `text::sanitize_display_label`: strip ANSI sequences, drop control and bidi-reordering characters, trim, and elide past 80 characters. Return an empty string when nothing printable survives so callers fall back to an identity they control. Apply it where the label is created, so events, the store, and the web UI all get a clean value instead of each consumer having to remember. `parallel_branch_display` sanitizes again, because a run recorded before this commit still has raw labels in its event log. `emit_branch_retrying` now sets `stage.retrying`'s `index` from the branch stage's execution ordinal, matching the envelope `stage_id` and the meaning every other emitter gives that field. The branch's position in the fan-out is already on `parallel.branch.started`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…jection # Conflicts: # apps/fabro-web/app/components/stage-renderers/parallel-children.tsx
Summary
for_eachcontract for runtime fan-out from inline or managed artifact-backed JSON arrays(target id, index)identities while applying bounded concurrency, retries, timeouts, cancellation, and empty/partial failure semanticsVerification
cargo +nightly-2026-04-14 fmt --check --allcargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-validate -p fabro-workflow -p fabro-api -p fabro-cli --all-targets -- -D warningsulimit -n 4096 && cargo nextest run -p fabro-types -p fabro-validate -p fabro-workflow -p fabro-store -p fabro-dump -p fabro-cli— 3,015 passed, 66 skippedcargo nextest run -p fabro-api— 202 passedfor_eachagents → fan-in scenario — passedTest-suite note
The repository-wide
bun testrun reports 731 passed and 13 failed in unrelated stale-build and Run Files tests. Those failing files pass when run in isolation, indicating existing shared-global/order sensitivity in the parallel suite; the changed feature tests pass both directly and within the full run.