Skip to content

feat: implement AbortController in py, ts, go, wasm (also cancel buttons)#2357

Merged
hellovai merged 40 commits intocanaryfrom
abort-handlers
Aug 26, 2025
Merged

feat: implement AbortController in py, ts, go, wasm (also cancel buttons)#2357
hellovai merged 40 commits intocanaryfrom
abort-handlers

Conversation

@sxlijin
Copy link
Copy Markdown
Contributor

@sxlijin sxlijin commented Aug 21, 2025

Implement AbortController in python, typescript, go, and also wasm. Also implement cancel functionality for run test in wasm and add docs for py/ts/go.


First 80% of this work done by @dexhorthy and @hellovai in #2331 :

Built in close collaboration with @hellovai who sat in my apartment and shouted at claude with me all day

Note: plan/Research artifacts in thoughts/ can be removed once they get put in a good location, leaving them in for now

What problem(s) was I solving?

Previously, once a BAML function call was initiated, there was no way to cancel it. This meant:

  • Long-running LLM operations couldn't be stopped if no longer needed
  • Retry chains would continue even after the user navigated away
  • Resources were wasted on unnecessary LLM calls
  • Poor user experience when operations needed to be cancelled

What user-facing changes did I ship?

Added abort/cancellation support for all BAML operations using native language patterns:

TypeScript

const controller = new AbortController();
const promise = b.ExtractName("John Doe", { 
  abortController: controller 
});

// Cancel after 100ms
setTimeout(() => controller.abort(), 100);

try {
  await promise;
} catch (e) {
  if (e instanceof BamlAbortError) {
    console.log("Operation cancelled");
  }
}

Python

from baml_py import AbortController

controller = AbortController()
task = b.ExtractName("John Doe", 
  baml_options={"abort_controller": controller})

# Cancel after 100ms
await asyncio.sleep(0.1)
controller.abort()

try:
  await task
except Exception as e:
  if "abort" in str(e).lower():
    print("Operation cancelled")

Go

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

_, err := b.ExtractName(ctx, "John Doe")
if err != nil && strings.Contains(err.Error(), "context canceled") {
  fmt.Println("Operation cancelled")
}

How I implemented it

  • Added cancellation support at the orchestrator level in Rust runtime using stream-cancel crate's Tripwire mechanism
  • Created language-specific bindings:
    • TypeScript: Native AbortController support via NAPI bridge
    • Python: Custom AbortController class exposed via PyO3
    • Go: context.Context cancellation with early detection goroutines
  • Implemented early cancellation detection that monitors signals immediately when functions are called
  • Added new LLMResponse::Cancelled variant to handle cancelled operations
  • All changes are backward compatible - existing code works unchanged

How to verify it

Run the tests:

# Python tests
cd integ-tests/python && uv run pytest tests/test_abort_handlers.py -v

# TypeScript tests  
cd integ-tests/typescript && npm test -- tests/abort-handlers.test.ts

# Go tests (requires CFFI rebuild)
cd engine/language_client_cffi && cargo build
cd integ-tests/go && go test -v -run TestAbortHandler

Test status:

  • Python abort handler tests (7 tests) - All passing
  • TypeScript abort handler tests (11 tests) - All passing
  • Go abort handler tests (7 tests) - All passing
  • I have ensured make check test passes

Also re-enabled 6 previously disabled test files (PDF, video, OpenAI responses, expression functions) that are now compatible.

Description for the changelog

Added comprehensive abort handler support for TypeScript, Python, and Go, allowing cancellation of in-flight LLM operations through native language cancellation patterns.

dexhorthy and others added 12 commits August 16, 2025 15:53
- Added Cancelled variant to LLMResponse enum
- Integrated stream-cancel Tripwire for cancellation checks
- Updated orchestrators (sync and streaming) to accept optional Tripwire
- Handled Cancelled variant in all match statements across 8 files
- Prepared foundation for language-specific abort handler bridges

Currently passing None for cancellation tokens - ready for language bridges in Phase 2-4.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Implemented context cancellation propagation from Go to Rust:

CFFI Layer:
- Added cancel_function_call extern function for Go to call
- Integrated stream-cancel crate with Tripwire for cancellation
- Used tokio::select! to handle cancellation at CFFI layer
- Track active operations with DashMap for cancellation

Go Client:
- Added CancelFunctionCall export function
- Updated callbacks to call cancel on context.Done()
- Fixed streaming template to pass user context directly
- Added wrapper functions for dynamic library loading

Testing:
- Created comprehensive manual test suite
- All cancellation tests passing:
  ✓ Context cancellation (~100ms)
  ✓ Streaming cancellation (~50ms)
  ✓ Timeout cancellation (~200ms)
  ✓ Minimal goroutine leaks

The implementation allows Go applications to cancel in-flight BAML operations
using standard context.Context patterns, properly cleaning up resources.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
…tion

Implemented early cancellation detection pattern for Go language support:
- Modified runtime.go to monitor context.Done() immediately on function call
- Removed redundant late cancellation from callbacks.go
- Updated test suite to use correct BAML client functions
- All tests passing with responsive cancellation (~100ms response time)

Key improvement: Cancellation now happens immediately when Go context is cancelled,
not waiting for data callbacks from Rust. This provides much more responsive abort handling.

Updated implementation plan with Phase 2 completion and clear instructions for
Phase 3 (TypeScript) implementation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Implement AbortController for Python and TypeScript clients
- Add cancellation support via tripwires in Rust runtime
- Update all generated client code with abort handler APIs
- Add comprehensive tests for abort functionality
- Clean up disabled test files
@vercel
Copy link
Copy Markdown

vercel bot commented Aug 21, 2025

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

1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
promptfiddle Skipped Skipped Aug 26, 2025 4:48am

@sxlijin sxlijin changed the title abort handlers Support for abort/cancel in rust w/ idiomatic support for python/go/typescript Aug 21, 2025
@sxlijin sxlijin temporarily deployed to boundary-tools-dev August 21, 2025 17:59 — with GitHub Actions Inactive
@sxlijin sxlijin temporarily deployed to boundary-tools-dev August 21, 2025 17:59 — with GitHub Actions Inactive
@sxlijin sxlijin temporarily deployed to boundary-tools-dev August 21, 2025 17:59 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

@sxlijin sxlijin temporarily deployed to boundary-tools-dev August 21, 2025 18:38 — with GitHub Actions Inactive
@sxlijin sxlijin temporarily deployed to boundary-tools-dev August 21, 2025 18:38 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

@hellovai hellovai temporarily deployed to boundary-tools-dev August 26, 2025 04:48 — with GitHub Actions Inactive
@hellovai hellovai temporarily deployed to boundary-tools-dev August 26, 2025 04:48 — with GitHub Actions Inactive
@hellovai hellovai temporarily deployed to boundary-tools-dev August 26, 2025 04:48 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

@hellovai hellovai enabled auto-merge August 26, 2025 04:51
@hellovai hellovai added this pull request to the merge queue Aug 26, 2025
@trojanowski
Copy link
Copy Markdown

trojanowski commented Aug 26, 2025

@sxlijin @hellovai I see one problem with the API added to the TypeScript SDK in this PR. The operations expect an AbortController instance as a parameter, but in my opinion, AbortSignal would be a much better choice. This would be consistent with what fetch expects, as well as multiple Node.js APIs, such as several functions in the node:fs module (just try searching for signal there) and even addEventListener from the DOM API. Additionally, this would allow us to use static methods that return AbortSignal instances, like AbortSignal.timeout or AbortSignal.any, which isn't possible with the current implementation in this PR.

After making that change, the TypeScript snippet from the PR description could be simplified as follows:

const promise = b.ExtractName("John Doe", {
  signal: AbortSignal.timeout(100)
});

try {
  await promise;
} catch (e) {
  // For consistency, I think we should also use the built-in `AbortError`
  // class here, instead of a custom one.
  if (e instanceof AbortError) {
    console.log("Operation cancelled");
  }
}

In my opinion, it would be a good idea to make these changes before a new release; otherwise, it could be a breaking change.

@hellovai
Copy link
Copy Markdown
Contributor

@trojanowski thanks for this! we actually patched it to be just that!

#2373 shows that off!

this was a really really great catch :)

@trojanowski
Copy link
Copy Markdown

@hellovai that's really great, but for consistency with other APIs, I would suggest two additional changes:

  1. Renaming the parameter from abortSignal to signal, like in all APIs mentioned in my previous comment.
  2. Using the built-in AbortError class instead of a custom one, also for consistency with the Node.js APIs and fetch.

@hellovai
Copy link
Copy Markdown
Contributor

hellovai commented Aug 27, 2025

thanks @trojanowski

  1. Renaming the parameter from abortSignal to signal, like in all APIs mentioned in my previous comment.

Oh! interesting, we explicitly left it as abortSignal as its a bit more verbose / clear, but i could be convinced. let me run this back with the team!

  1. Using the built-in AbortError class instead of a custom one, also for consistency with the Node.js APIs and fetch.

We sadly can't do this as there is no such thing as AbortError that user land has:
web world: Note: When abort() is called, the fetch() promise rejects with an Error of type DOMException, with name AbortError.
NodeJS: nodejs/node#38361

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.

4 participants