toolcue Join the waitlist

LangChain4j: one tool provider for per-turn tool selection

Register your tools with ToolcueToolProvider instead of AiServices.tools(...). Each user message gets only the tools it needs, chosen once by Toolcue; every step of the tool loop keeps that list, and a find_tools tool lets the model recover one that was left out.

Verified on 2026-11-04 with LangChain4j 1.13.0 and 1.23.0, with 9 end-to-end tests and a real Gemini run. Java 17+. Toolcue is in early access: join the waitlist.

Install#

xml
<dependency>
    <groupId>dev.toolcue</groupId>
    <artifactId>toolcue</artifactId>
    <version>0.1.0</version>
</dependency>

The toolcue artifact has no dependencies; it uses your application's LangChain4j (1.13 or later). Set TOOLCUE_URL, TOOLCUE_TOKEN and TOOLCUE_ADMIN_TOKEN, or pass them to ToolcueClient.builder().

Use it#

java
ToolcueToolProvider tools = ToolcueToolProvider.builder(ToolcueClient.create(), "acme-billing")
    .tools(new BillingTools())                                            // objects with @Tool methods
    .user(request -> Map.of("roles", request.invocationParameters().getOrDefault("roles", List.of())))
    .build();

// at deploy time: publish the catalog, with what the annotations can't say
tools.publish(Map.of(
    "find_customer", t -> t.withProduces("customer_id"),
    "update_tax_settings", t -> t.withRequiredRoles("admin"),
    "ask_user", t -> t.withPinned(true)));

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .chatMemoryProvider(memory)
    .toolProvider(tools)
    .build();

Pass the user per call with an InvocationParameters argument on your AI service method (or look them up from the chat memory id in user(...)).

How the provider works#

WhenWhat it does
First callBefore the turn's first model callSends the user message, the earlier messages (with tool calls and results) and the user to Toolcue
Later callsBefore every model call of the tool loopReturns the same tools, plus any find_tools found this turn
find_toolsWhen the model calls itSearches the whole catalog with the user's roles; the tools found are added for the rest of the turn
Tool callsEvery tool callOnly offered tools have executors, so a hidden tool can't run

The provider is dynamic (isDynamic()), which is why it needs LangChain4j 1.13 or later.

Pins and scopes#

java
ToolcueToolProvider.builder(client, "acme-billing")
    .tools(new BillingTools())
    .pinned(request -> PAGE_TOOLS.getOrDefault(request.invocationParameters().getOrDefault("page", ""), List.of()))
    .only(List.of("find_customer", "refund_invoice"))     // a refunds-only assistant
    .build();

pinned adds tools on every turn, with their lookups; only makes Toolcue choose only among some tools. Both take a list or a function of the request. Role rules always win.

When Toolcue is down#

The provider never fails a turn. If Toolcue is slow (over 0.5 s), unreachable or failing, the turn gets your fallbackTools(...), else only plus the pinned tools, else every tool, and a warning is logged. After three failures in a row it stops calling for 30 seconds.

Compared with LangChain4j's own tool filtering#

LangChain4j can filter tools with a model call before each request, or let the model search for deferred tools. Toolcue needs no model call (tens of milliseconds), reads the whole conversation, adds the lookups the chosen tools depend on and applies the user's roles. Keep find_tools for the rare miss.

Run the example#

LangChain4jAgent.java in the Java examples runs four turns with Claude, Gemini or any OpenAI-compatible model and prints the tools each call got.