Skip to content

refactor(tools): adopt AsyncFn for tool handler APIs to drop Box::pin boilerplate #541

Description

@suchapalaver

Motivation

The tool-handler APIs are built on the pre-AsyncFn pattern of taking a closure that returns a manually-boxed future:

// crates/baml-rt-tools/src/tools.rs (≈2011, 2910, 2922, 2952)
F: Fn(I) -> Pin<Box<dyn Future<Output = Result<O>> + Send>> + Send + Sync + 'static

Every call site pays for this with a Box::pin(async move { … }) wrapper and a per-call heap allocation. There are ~45+ such wrappers across the codebase, e.g. the 7 near-identical handlers in crates/tools/memory/src/handlers.rs:42-132, ~9 more in tools.rs itself, and others in crates/tools/system/* and crates/tools/slack/src/producer.rs:717.

AsyncFn/AsyncFnMut/AsyncFnOnce (stable since Rust 1.85) exist precisely to replace this shape.

Proposal

  • Change the handler trait bounds from Fn(I) -> Pin<Box<dyn Future<…>>> to AsyncFn(I) -> Result<O>.
  • Drop the Box::pin(async move { … }) wrappers at call sites; closures can return async blocks (or use async ||) directly.

Caveat / scope

Threading Send through an AsyncFn bound is the known rough edge — likely needs a Send-bound helper or where F::CallRefFuture<'_>: Send. Prototype on the memory handler family first and confirm the ergonomics before fanning out to tools.rs and the other tool crates.

Acceptance

  • Handler registration APIs accept AsyncFn-style closures.
  • Box::pin(async move …) wrappers removed at the migrated call sites.
  • cargo clippy --all-targets --all-features -- -D warnings clean.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions