Skip to content

feat: AVA-73 add explicit skipped outcomes, UI styling, and example - #70

Open
eriberdy022 wants to merge 11 commits into
mainfrom
factory/AVA-73
Open

feat: AVA-73 add explicit skipped outcomes, UI styling, and example#70
eriberdy022 wants to merge 11 commits into
mainfrom
factory/AVA-73

Conversation

@eriberdy022

@eriberdy022 eriberdy022 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Rationale

Optional work needs an explicit outcome instead of using None for both intentional absence and successful empty values. This implements AVA-73, including the human-approved addition of visible skipped-node styling and a simple runnable example.

Summary

  • Add ava.skip(reason, metadata=None) and ava.Skipped. An authored skip succeeds, satisfies dependency-only successors and existing & fan-in, and remains distinct from failure, cancellation, and successful None. Python value inputs receive the explicit marker; whole-node multi-return skips retain every output position. No gather API.
  • Carry skip outcomes through Local and Ray execution status channels without eagerly fetching distributed payloads. Preserve successful execution-service finalization and receipts.
  • Distinguish whole-node multi-return absence from ordinary lists/tuples containing skipped values using explicit internal transport. Direct executor status, hooks, and service receipts preserve that distinction without driver payload materialization.
  • Normalize skipped return slots for both default RunContext and custom BaseContext workflows, including async nodes, indexed outputs, fan-in and unwrap hooks; malformed ordinary return counts still fail.
  • Preserve ordinary null-producer-slug rows during rerun exclusion, including sparse ancestry with named skipped producers; excluded named rows are not resurrected.
  • Record operator SKIPPED state with reason, JSON metadata, and start/end timestamps. Extend spawned-worker events, snapshots/live updates, regenerated Python/TypeScript protobuf bindings, result bundles, and CLI result downloads.
  • table.append(ava.skip(...)) writes a run/producer metadata receipt in Iceberg or Lance without payload rows. Streams do not consume backlog or advance cursors for skipped input. Reruns do not resurrect that producer's ancestor rows; unrelated producers remain eligible. Validate receipt data before ancestry traversal.
  • Preserve empty replay even when a producer wrote data before returning a persisted skip; physical rows remain, but excluded rows never re-enter the stream. Iceberg skip receipts use existing append serialization, refresh, and bounded conflict handling so concurrent skipped producers and mixed data writes retain every receipt.
  • Preserve skipped outcomes nested inside Pydantic result models, including aliases and lists, through the existing native-value serialization boundary. Public model JSON exposes reason/metadata rather than private storage fields.
  • Browser nodes use a purple dashed border and skip symbol, retain the symbol in compact mode, and expose accessible status names. Browser inspector and TUI show authored reasons/metadata separately from unexecuted failed/cancelled dependencies. TUI labels include skip status alongside elapsed time. Rebuild packaged browser assets.
  • Add examples/skipped_node.py, document its CLI/browser flow, include it in the existing smoke inventory, and update API, execution-service, storage, rerun documentation, and Unreleased changelog. Add focused Local/Ray/storage/operator/UI regressions and adjust affected accessible selectors.
  • Publish current custom-context outcome screenshots as native GitHub attachments. Previously committed PNGs were removed in a normal follow-up commit; no generated screenshots are in the final repository diff.

Test Plan

Current review correction

  • Current-head CI: run 34512693874 passed all four jobs at 46e68fa02c33100bf1508fe60999a60cfa92995f: Lint + tests, Browser UI, Ray tests, Tmux TUI tests (github-actions app 15368). The normal remote Ray command passed; current head is conflict-free against main 1cb60ca154abe57fbf45144d7012712f0d357af9.
  • Fresh independent adversarial review: PASS at the same head. Complete 55-path diff, all approved criteria, all three current comments and prior fixes independently verified. Focused proof: 19 Local skip cases, 12 storage/conflict cases, two real-Ray payload guards, additional custom-context async three-return/services/fan-in/unwrap checks; restored live gRPC, CLI, browser and TUI acceptance completed. Native attachments independently fetched byte-for-byte.
  • make lint && make smoke-test — passed; eight smoke cases.
  • uv run pytest test/skipped_outcome_test.py test/operator_tests/test_skipped_outcomes.py -m 'not ray' -q — 25 passed, 12 deselected.
  • RAY_ENABLE_UV_RUN_RUNTIME_ENV=0 POLARS_MAX_THREADS=2 .venv/bin/python -m pytest test/skipped_outcome_test.py test/ray_materialization_contract_test.py -m ray -q — 16 passed, 19 deselected, real Ray. Initial local uv invocation exceeded 600 seconds; workaround is local-only and does not replace normal remote CI.
  • uv run pytest test/workflow_execution_test.py::test_wrong_return_count_fails_before_downstream_can_lose_data test/execution_services_test.py::test_malformed_multi_return_aborts_before_finalize -q — 4 passed.
  • uv run pytest test/storage/test_table_contracts.py -k concurrent_skip -q — 8 passed, 24 deselected.
  • Actual direct executors, normal/custom workflows, null-slug Iceberg/Lance replay and spawned gRPC/browser/TUI status checked; authored skip retains metadata/timestamps, containers succeed, downstream completes.

Earlier implementation checks (retained history)

  • make lint — passed.
  • make smoke-test — 8 passed, including the new example and actual CLI/gRPC result download.
  • make web-lint — passed.
  • make web-format-check — passed for both browser packages.
  • uv run pytest test/skipped_outcome_test.py test/operator_tests/test_skipped_outcomes.py test/rerun_test.py test/tui_test.py -m 'not ray' -q — 33 passed, 8 deselected.
  • uv run pytest test/skipped_outcome_test.py test/ray_materialization_contract_test.py -m ray -q — 9 passed, 12 deselected with real Ray, including driver-materialization checks.
  • uv run pytest test/storage/test_table_contracts.py test/iceberg/namespace_test.py -q — 42 passed; concurrent skips/data, separate handles, and persistent conflict failure included.
  • uv run pytest test/operator_tests/test_workflow_results.py -k spawned_results_materialize -q — 1 passed, 23 deselected.
  • Exact independent-review reproductions after correction: both stores report LIVE [] / REPLAY []; concurrent Iceberg reports SUCCESS 8 / RECEIPTS 8; Pydantic nesting preserves native skipped values. Actual spawned operator and ava result download preserve nested public reason/metadata.
  • pnpm exec vitest run src/Inspector.test.tsx src/state.test.ts src/api.test.ts from web/operator — 21 passed; pnpm exec vitest run src/OperatorUi.test.tsx — 3 passed; final pnpm test src/Inspector.test.tsx — 5 passed.
  • make web-build — packaged assets rebuilt and committed; protobuf bindings regenerated with make proto / make web-proto.
  • uv run python examples/skipped_node.py — prints successful downstream completion.
  • Actual operator/browser Run and TUI r start: overall success, skipped source, successful downstream, reason/metadata/timestamps; full and compact views and keyboard inspection exercised. Focused skip-detail axe scan: 17 passes, zero violations/incomplete.
  • Qualification: the reviewer's combined local Ray run had one existing indexed-reference test time out at 30 seconds (8 passed). The identical isolated test passed in 23.08 seconds; all seven skip-specific Ray cases and current-head remote Ray CI passed. Root cause of that local timeout is not established.

Runtime evidence

Current captures are from source 389c13860e0d197a933d2d440ad2fea72a7312da; final head 46e68fa02c33100bf1508fe60999a60cfa92995f differs only by import ordering. The custom-context run shows a whole-node skip, successful containers containing skipped values, and completed downstream work. Authenticated GitHub-rendered markup and fetched image bytes match each original PNG; only permanent native attachment URLs are published.

Custom context skipped producer and successful containers

Whole node skip retains reason and metadata

Container containing skipped values remains successful

Terminal custom context skips and completed downstream

Boundaries and risks

Durability is limited to existing table metadata and result-bundle storage. Operator run state and embedded handles remain process-local; no coordinator recovery, rollback, exactly-once execution, or production guarantees are added. Direct return ava.skip(...) does not infer a table or undo prior side effects. Consumers of Python value edges must handle ava.Skipped; successful None is unchanged. Selecting a skipped producer in a rerun executes it again; pruned Python inputs retain existing rejection semantics. Generated Python protobuf files include generator formatting changes.

@eriberdy022
eriberdy022 marked this pull request as ready for review September 10, 2026 06:44

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

do not commit this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in cc4c8dc. The unchanged capture is now a native GitHub attachment: https://github.com/user-attachments/assets/8d7ec381-b4f4-4599-815c-e7d110397f14 (also embedded in the PR body; original PNG bytes verified). Only the three requested PNGs were deleted; application source and history are unchanged. All four new-head CI jobs and fresh independent review passed. Leaving this thread open for your acceptance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

do not commit this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in cc4c8dc. The unchanged capture is now a native GitHub attachment: https://github.com/user-attachments/assets/fe216904-0150-4bd4-82d8-63b6789d06e8 (also embedded in the PR body; original PNG bytes verified). Only the three requested PNGs were deleted; application source and history are unchanged. All four new-head CI jobs and fresh independent review passed. Leaving this thread open for your acceptance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

do not commit this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in cc4c8dc. The unchanged capture is now a native GitHub attachment: https://github.com/user-attachments/assets/3f1c3522-53af-463c-8c2d-9e6100976532 (also embedded in the PR body; original PNG bytes verified). Only the three requested PNGs were deleted; application source and history are unchanged. All four new-head CI jobs and fresh independent review passed. Leaving this thread open for your acceptance.

@eriberdy022
eriberdy022 marked this pull request as draft September 10, 2026 14:12
@eriberdy022

eriberdy022 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Resolved by operator-authorized commit cc4c8dc: only the three requested repository PNGs were removed, without application changes or history rewrite. The unchanged captures are native GitHub attachments in the PR body; original bytes and rendered markup are verified. All four new-head CI jobs and fresh independent review passed. Replies link the correction and individual attachment in each review thread; none was auto-resolved. This PR is ready for human acceptance again.

@eriberdy022
eriberdy022 marked this pull request as ready for review September 10, 2026 15:06

@eriberdy022 eriberdy022 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed the complete 52-file diff at cc4c8dc against 1cb60ca with eight read-only reviewer assignments. Found three actionable P2 defects, detailed inline. Static remote review only; no repository code or tests were executed.

Comment thread src/avalanche/outcomes.py Outdated
return value.value if isinstance(value.value, Skipped) else None
if isinstance(value, Skipped):
return value
if isinstance(value, (tuple, list)) and value:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Distinguish container values from whole-node skips

_skip_outcome treats every nonempty tuple/list of equal skips as a whole-node absence, although the container can be the actual single-return value. For example, LocalExecutor().submit_with_status(lambda: [ava.skip("nested")]) reports a skipped status rather than successful None; the same classification reaches on_node_skipped for a workflow declared with a plain BaseContext subclass, whose wrapper does not add a LineagedResult. This contradicts the documented distinction between a container containing skips and a skipped node. Recognize expanded multi-return transport explicitly instead of inferring it from arbitrary container contents.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 389c138 (current head 46e68fa adds only import ordering). Whole-node expansion now carries an explicit internal marker; ordinary lists/tuples and equal returned slots remain successful values. Direct executor status and normal/custom-context hooks were independently exercised on Local and real Ray, including services and unwrap hooks, without driver payload materialization. Successful-container UI evidence: https://github.com/user-attachments/assets/f9cd08fd-97e9-492b-b179-21c0b86f6fb1 . All four current-head CI jobs pass. Leaving the thread open for your acceptance.

Comment thread src/avalanche/dag.py Outdated
if context.node_slug is not None:
lineage[context.node_slug] = context.run_id

if num_returns > 1 and isinstance(value, Skipped):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Normalize multi-return skips for custom BaseContext workflows

The expansion is confined to _wrap_lineaged_result, but supported @ava.workflow(context=MyContext) declarations with MyContext(ava.BaseContext) use _with_current_run_context's plain branch and never call this helper. A @ava.source(num_returns=2) returning ava.skip(...) therefore returns a bare Skipped: LocalExecutor rejects its return shape and Ray's status wrapper tries to unpack a non-iterable value. Intentional absence becomes a failed run merely because an application context is supplied. Normalize whole-node skip return shapes on both context paths, before executor return-count validation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 389c138 (current head 46e68fa). Both RunContext and plain BaseContext paths normalize resolved sync/async whole-node skips before return-count validation. Indexed outputs, flattened fan-in, service receipts and unwrap hooks were independently exercised on Local and real Ray; custom contexts retain only their declared fields and malformed ordinary returns still fail. Custom-context authored skip evidence: https://github.com/user-attachments/assets/7e5b6ed4-b970-4b52-a184-479414a802e5 . All four current-head CI jobs pass. Leaving the thread open for your acceptance.

empty_result = df.head(0)
skipped_slugs = skips_by_run.get(current_run_id, set())
if "_ava_node_slug" in df.columns:
df = df.filter(~pl.col("_ava_node_slug").is_in(seen_slugs | skipped_slugs))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Preserve null producer slugs when excluding skipped versions

The public run_with_context(RunContext(run_id="original", workflow_name="flow"), table.append, rows) can write rows with a valid run ID and a null _ava_node_slug, since node_slug defaults to None. Replaying that run through consume_stream(table, rerun=Rerun(run_id="original", start=["consumer"])) now silently discards those rows: is_in propagates null and filter rejects the null predicate, even without skip receipts. The pre-existing _overlay_rerun_frames explicitly preserves the null-slug producer group. Keep null slugs in this exclusion predicate so the new receipt handling does not remove ordinary replay data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 389c138 (current head 46e68fa). The exclusion predicate explicitly retains null producer slugs. Independent Iceberg and Lance regressions retain ordinary null-slug rows both without receipts and across sparse rerun ancestry, while named skipped producer rows remain excluded. Physical original rows are unchanged; no fallback resurrection. Covered by test_null_slug_replay_survives_named_skips_and_sparse_ancestry. All four current-head CI jobs pass. Leaving the thread open for your acceptance.

@eriberdy022
eriberdy022 marked this pull request as draft September 10, 2026 15:25
@eriberdy022
eriberdy022 marked this pull request as ready for review September 10, 2026 18:36
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.

1 participant