Skip to content

bex_engine: add CancellationToken support across all layers#3136

Merged
antoniosarosi merged 10 commits intocanaryfrom
antonio/cancellation
Feb 19, 2026
Merged

bex_engine: add CancellationToken support across all layers#3136
antoniosarosi merged 10 commits intocanaryfrom
antonio/cancellation

Conversation

@antoniosarosi
Copy link
Copy Markdown
Contributor

@antoniosarosi antoniosarosi commented Feb 19, 2026

Summary

  • Phase 1 (Core): Introduce CancellationToken (wrapping tokio_util::sync::CancellationToken), add EngineError::Cancelled variant, integrate biased tokio::select! in the VM event loop so cancellation is checked before every await, and use AbortHandle to kill spawned tasks on cancel.
  • Phase 2 (Sys Ops): Wire CancellationToken through SysOpContext so all sys_ops (sleep, HTTP fetch, etc.) respect cancellation. Add baml.sys.is_cancelled() builtin for cooperative polling.
  • Phase 3 (Orchestrator): Add cooperative is_cancelled() checks in llm.baml at retry loops, primitive/fallback branches, and the top-level call_llm_function entry point.
  • Phase 4 (Bindings): Python AbortController class with abort()/aborted API, WASM callFunction(name, args, callId?) + cancelCall(callId) pattern, C FFI pass-through (already wired in Phase 1).
  • Tests: 8 integration tests in bex_engine/tests/cancellation.rs covering immediate cancel, sleep/HTTP interruption, selective cancellation, sequential sleeps, idempotent cancel, cooperative is_cancelled(), and normal completion.

Test plan

  • cargo test -p bex_engine --test cancellation — all 8 tests pass
  • cargo clippy --workspace — clean
  • All existing tests pass (snapshot updates for is_cancelled global index shift)

Summary by CodeRabbit

  • New Features

    • Execution now stops immediately when a cancellation is detected across retries, fallbacks, and orchestration.
    • Added a builtin to allow code to query whether the current call has been cancelled.
  • Breaking Changes

    • Generated sys-op method signatures were adjusted—callers may need to adapt to the new parameter ordering/format.
  • Chores

    • Added a new workspace dependency to support these changes.

Implement cooperative cancellation for BAML function execution:

- Phase 1: Core CancellationToken type, biased select in VM event loop,
  AbortHandle for killing spawned tasks, EngineError::Cancelled variant
- Phase 2: Wire cancellation through SysOpContext so sys_ops (sleep, HTTP)
  respect the token; add is_cancelled() builtin
- Phase 3: Cooperative cancellation checks in llm.baml orchestrator
  (retry loop, primitive/fallback branches, top-level call)
- Phase 4: Language binding integration - Python AbortController,
  WASM callId-based cancellation, C FFI pass-through
- Tests: 8 integration tests covering immediate cancel, sleep/HTTP
  interruption, selective cancellation, idempotency, and normal completion
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Feb 19, 2026 8:26pm
promptfiddle Ready Ready Preview, Comment Feb 19, 2026 8:26pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 19, 2026

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (4)
  • baml_language/crates/bex_engine/Cargo.toml
  • baml_language/crates/bridge_cffi/src/error.rs
  • baml_language/crates/bridge_cffi/src/ffi/functions.rs
  • baml_language/crates/bridge_wasm/src/lib.rs
⛔ Files ignored due to path filters (32)
  • baml_language/crates/baml_tests/snapshots/attribute_validation/baml_tests__attribute_validation__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/basic_types/baml_tests__basic_types__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/big_t_type_annotation/baml_tests__big_t_type_annotation__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/builtin_io/baml_tests__builtin_io__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/comment_after_string_in_config/baml_tests__comment_after_string_in_config__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/comment_in_type/baml_tests__comment_in_type__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/config_dictionary/baml_tests__config_dictionary__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/duplicate_class_span/baml_tests__duplicate_class_span__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/function_call/baml_tests__function_call__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/function_types/baml_tests__function_types__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/generator/baml_tests__generator__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/header_in_llm_function/baml_tests__header_in_llm_function__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/headers_edge_cases/baml_tests__headers_edge_cases__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/match_exhaustiveness/baml_tests__match_exhaustiveness__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/paren_union_test/baml_tests__paren_union_test__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_constructors/baml_tests__parser_constructors__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_error_recovery/baml_tests__parser_error_recovery__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_expressions/baml_tests__parser_expressions__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_speculative/baml_tests__parser_speculative__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_statements/baml_tests__parser_statements__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_stress/baml_tests__parser_stress__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_strings/baml_tests__parser_strings__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/pending_greaters_fix/baml_tests__pending_greaters_fix__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/retry_policy/baml_tests__retry_policy__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/simple_function/baml_tests__simple_function__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/simple_type_error/baml_tests__simple_type_error__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/top_level_header_comment/baml_tests__top_level_header_comment__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/top_level_let/baml_tests__top_level_let__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/type_aliases/baml_tests__type_aliases__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/type_builder_errors/baml_tests__type_builder_errors__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/type_builder_test/baml_tests__type_builder_test__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/unknown_type_error/baml_tests__unknown_type_error__06_codegen.snap is excluded by !**/*.snap

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds workspace dependency tokio-util; introduces a public sys-op cancellation_requested(); inserts early cancellation short-circuits across LLM execution, retries, and fallback paths; and updates sys-op codegen to conditionally place the SysOpContext parameter with or without a leading comma.

Changes

Cohort / File(s) Summary
Dependencies
baml_language/Cargo.toml
Added workspace dependency tokio-util = { version = "0.7" }.
LLM execution flow
baml_language/crates/baml_builtins/baml/llm.baml
Added early cancellation checks and short-circuit behavior across retry loops, execute_client_once (Primitive, Fallback, RoundRobin), and call_llm_function; removed previous cancellation TODO.
Builtins sys-op
baml_language/crates/baml_builtins/src/lib.rs
Added public sys-op fn cancellation_requested() -> bool annotated with #[sys_op] and #[uses(engine_ctx)] (sync/Ready path behavior documented).
Codegen for sys-ops
baml_language/crates/baml_builtins_macros/src/codegen_sys_ops.rs
Adjusted generation to detect regular parameters and conditionally emit the SysOpContext parameter and its leading-comma placement in trait signatures and glue calls (", ctx: &SysOpContext"/", ctx" vs "ctx: &SysOpContext"/"ctx").

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Orchestrator as LLM_Orchestrator
    participant Executor as execute_client
    participant SysOp as cancellation_requested

    Client->>Orchestrator: call_llm_function(...)
    Orchestrator->>Executor: execute_client(...)
    Executor->>SysOp: cancellation_requested()
    SysOp-->>Executor: true/false
    alt cancellation == true
        Executor-->>Orchestrator: short-circuit / cancel
        Orchestrator-->>Client: propagate cancellation/error
    else cancellation == false
        Executor-->>Orchestrator: continue (attempt / primitive / fallback)
        Orchestrator-->>Client: result or final failure
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'bex_engine: add CancellationToken support across all layers' directly and accurately summarizes the main objective of the PR, which implements cooperative cancellation across the BAML execution stack.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch antonio/cancellation

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
Contributor

@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: 1

Comment thread baml_language/crates/baml_builtins/src/lib.rs Outdated
Clearer name — makes it obvious this checks whether cancellation
has been requested for the current call, not some ambient state.
Copy link
Copy Markdown
Contributor

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
baml_language/crates/baml_builtins/baml/llm.baml (1)

263-270: ⚠️ Potential issue | 🟡 Minor

RoundRobin branch advances the atomic counter before checking cancellation.

Fallback checks cancellation_requested() before each sub-client iteration; RoundRobin calls round_robin_next (advancing the counter atomically) before any cancellation check. If the token is already set, the counter slot is wasted and the client selection is skewed. The actual call is still caught by the checks inside execute_client / execute_client_once, but the counter side-effect has already occurred.

🛡️ Proposed fix
         baml.llm.ClientType.RoundRobin => {
+            if (baml.sys.cancellation_requested()) {
+                return baml.llm.ExecutionResult { ok: false, value: null };
+            }
             let idx = baml.llm.round_robin_next(llm_client.name) % llm_client.sub_clients.length();
             baml.llm.execute_client(
                 llm_client.sub_clients.at(idx),

Comment thread baml_language/crates/baml_builtins/baml/llm.baml
- Add early cancellation check at top of call_function so pre-cancelled
  tokens always produce Err(Cancelled) regardless of function contents
- Upgrade any error to Cancelled when the token is cancelled, so
  cooperative BAML-level checks (via baml.sys.panic) are reported as
  Cancelled to callers
- Fix poisoned mutex handling in C FFI (unwrap_or_else instead of silent skip)
- Error on WASM call_id collision instead of silently replacing
- Document WASM orphan future limitation and cooperative check semantics
- Split cancellation_requested test into false/true cases, remove stale comment
true is only observable if all preceding sys_ops were sync;
any async op would trigger VM-level cancellation at its Await first.
Replace platform-specific abort logic with futures::future::AbortHandle
which works on both native (tokio::spawn) and WASM (spawn_local). WASM
orphan futures are now properly aborted on cancellation instead of
running to completion.
Copy link
Copy Markdown
Contributor

@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.

Caution

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

⚠️ Outside diff range comments (1)
baml_language/crates/baml_builtins/baml/llm.baml (1)

268-276: 🧹 Nitpick | 🔵 Trivial

No cancellation check in the RoundRobin branch of execute_client_once.

The Primitive (Line 222) and Fallback (Line 253) branches both have early cancellation_requested() guards, but the RoundRobin branch at Line 268 does not. This is likely acceptable because:

  1. round_robin_next is an async sys_op, so the VM's biased select! would catch cancellation at that await point.
  2. The recursive execute_client call will hit the cancellation check in the resolved child's branch.

However, the round_robin_next call still executes and mutates the atomic counter even when cancellation is already requested. If counter accuracy across cancelled calls matters, consider adding a guard here too.

Optional: add cancellation guard before round-robin counter mutation
         baml.llm.ClientType.RoundRobin => {
+            if (baml.sys.cancellation_requested()) {
+                return baml.llm.ExecutionResult { ok: false, value: null };
+            }
             let idx = baml.llm.round_robin_next(llm_client.name) % llm_client.sub_clients.length();

Prevents round_robin_next from mutating the atomic counter when
cancellation is already requested, matching the Primitive and Fallback
branches.
Copy link
Copy Markdown
Contributor

@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: 1

Comment thread baml_language/crates/baml_builtins/baml/llm.baml
C FFI: reject duplicate call_ids with DuplicateCallId error instead of
silently overwriting the previous entry (matching WASM behavior).

WASM: surface EngineError::Cancelled as a JS Error with
name="BamlCancelledError" so callers can programmatically distinguish
cancellation from other failures (matching Python's BamlCancelledError).
# Conflicts:
#	baml_language/crates/baml_tests/snapshots/attribute_validation/baml_tests__attribute_validation__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/basic_types/baml_tests__basic_types__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/builtin_io/baml_tests__builtin_io__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/comment_after_string_in_config/baml_tests__comment_after_string_in_config__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/comment_in_type/baml_tests__comment_in_type__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/config_dictionary/baml_tests__config_dictionary__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/duplicate_class_span/baml_tests__duplicate_class_span__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/function_call/baml_tests__function_call__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/function_types/baml_tests__function_types__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/generator/baml_tests__generator__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/header_in_llm_function/baml_tests__header_in_llm_function__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/headers_edge_cases/baml_tests__headers_edge_cases__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/match_exhaustiveness/baml_tests__match_exhaustiveness__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/paren_union_test/baml_tests__paren_union_test__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/parser_constructors/baml_tests__parser_constructors__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/parser_error_recovery/baml_tests__parser_error_recovery__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/parser_expressions/baml_tests__parser_expressions__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/parser_speculative/baml_tests__parser_speculative__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/parser_statements/baml_tests__parser_statements__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/parser_stress/baml_tests__parser_stress__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/parser_strings/baml_tests__parser_strings__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/pending_greaters_fix/baml_tests__pending_greaters_fix__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/retry_policy/baml_tests__retry_policy__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/simple_function/baml_tests__simple_function__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/simple_type_error/baml_tests__simple_type_error__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/top_level_header_comment/baml_tests__top_level_header_comment__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/top_level_let/baml_tests__top_level_let__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/type_aliases/baml_tests__type_aliases__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/type_builder_errors/baml_tests__type_builder_errors__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/type_builder_test/baml_tests__type_builder_test__06_codegen.snap
#	baml_language/crates/baml_tests/snapshots/unknown_type_error/baml_tests__unknown_type_error__06_codegen.snap
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Feb 19, 2026

Merging this PR will degrade performance by 11.63%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

❌ 5 regressed benchmarks
✅ 10 untouched benchmarks
⏩ 84 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime bench_single_simple_file 926.7 µs 1,046.2 µs -11.43%
WallTime bench_incremental_add_string_char 918.2 µs 1,039.1 µs -11.63%
WallTime bench_incremental_close_string 921.2 µs 1,038.6 µs -11.3%
WallTime bench_incremental_add_attribute 924.1 µs 1,037.1 µs -10.9%
WallTime bench_empty_project 855.5 µs 963.3 µs -11.19%

Comparing antonio/cancellation (64208f9) with canary (8838648)

Open in CodSpeed

Footnotes

  1. 84 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 19, 2026

Binary size checks passed

7 passed

Artifact Platform Gzip Baseline Delta Status
bridge_cffi Linux 4.1 MB 4.0 MB +69.7 KB (+1.7%) OK
bridge_cffi-stripped Linux 2.2 MB 2.1 MB +71.1 KB (+3.3%) OK
bridge_cffi macOS 3.3 MB 3.3 MB +49.4 KB (+1.5%) OK
bridge_cffi-stripped macOS 1.8 MB 1.7 MB +56.1 KB (+3.3%) OK
bridge_cffi Windows 3.3 MB 3.3 MB +57.7 KB (+1.8%) OK
bridge_cffi-stripped Windows 1.8 MB 1.8 MB +61.5 KB (+3.5%) OK
bridge_wasm WASM 1.4 MB 1.3 MB +42.9 KB (+3.3%) OK

Generated by cargo size-gate · workflow run

tokio::select! requires the macros feature, which was only enabled for
native targets. The select! macro is a pure proc macro with no runtime
dependency, so it works on WASM.
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