Spring AI: only the tool callbacks each turn needs
Pass ChatClient the tool callbacks Toolcue picks for this user message instead of all of them. The model sees fewer, better-matched tools, and the tool loop inside the call keeps that list.
Verified on 2026-11-04 with Spring AI 1.1.8 and 2.0.2, with 5 end-to-end tests and a real Gemini run. Java 17+. Toolcue is in early access: join the waitlist.
Install#
<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 Spring AI. Set TOOLCUE_URL, TOOLCUE_TOKEN and TOOLCUE_ADMIN_TOKEN, or pass them to ToolcueClient.builder().
Use it#
List<ToolCallback> all = List.of(ToolCallbacks.from(new BillingTools())); // your @Tool methods
ToolcueToolCallbacks toolcue = ToolcueToolCallbacks.builder(ToolcueClient.create(), "acme-billing", all).build();
// at deploy time: publish the catalog, with what the annotations can't say
toolcue.publish(Map.of(
"find_customer", t -> t.withProduces("customer_id"),
"update_tax_settings", t -> t.withRequiredRoles("admin"),
"ask_user", t -> t.withPinned(true)));
// every user turn
String answer = chatClient.prompt()
.messages(history)
.user(message)
.toolCallbacks(toolcue.forTurn(message, history, Map.of("roles", user.roles())))
.call().content();forTurn sends the message, the last ten messages of history (with tool calls and their results) and the user's roles. It returns the callbacks for this turn: the matches, the lookups they need (find_customer before refund_invoice) and your pinned tools. A tool that wasn't offered can't run, because Spring AI only executes the callbacks you pass.
In a Spring Boot app, make ToolcueClient and ToolcueToolCallbacks beans and inject them where you build prompts. Don't also give the ChatClient every tool with defaultToolCallbacks: pass the turn's tools on each prompt.
Pins and scopes#
ToolcueToolCallbacks.builder(client, "acme-billing", all)
.pinned(List.of("ask_user")) // on every turn, with their lookups
.only(List.of("find_customer", "refund_invoice")) // a refunds-only assistant
.build();
toolcue.forTurn(Turn.of(message).user(user).pinned(PAGE_TOOLS.get(page))); // per turnA turn's pins are added to the builder's; its only, when set, replaces the builder's. Role rules always win.
When Toolcue is down#
forTurn never throws. If Toolcue is slow (over 0.5 s), unreachable or failing, it returns your fallbackTools(...), else only plus the pinned tools, else every callback, and logs a warning. After three failures in a row it stops calling for 30 seconds, so an outage costs nothing per turn.
Run the example#
SpringAiAgent.java in the Java examples runs four turns with Gemini or any OpenAI-compatible model and prints the tools each call got.