πͺ Make tools lazy
The bot already has six tools and is about to get five more β return policy, size guide, care instructions, restock alerts, and a shipping estimator. That is a lot of tool descriptions to send to the model on every single message, even when the conversation is just "show me red shoes."
In this exercise you'll make those five new tools lazy. A lazy tool is advertised to the model by name only, on a synthetic discovery tool that TanStack AI adds for you. When the model decides it might need a lazy tool, it calls discovery first to load the full description and schema, then calls the real tool. The result: the prompt stays slim, and niche tools only enter the conversation when they are actually relevant.
π¨ What you will do
Same three-piece shape as the last few exercises β define a tool, register it on the server, watch it work β but this time with a
lazy: true flag on each definition.1. app/tools/shipping.ts β define the lazy tool
The schemas (
ShippingEstimateInput, ShippingEstimateOutput) and the estimate(zipCode) helper are pre-built.- π¨ At the bottom of the file, build and export
getShippingEstimate. The π¨ block walks you through it β sametoolDefinition({...})shape you've used before, plus one new field:lazy: true. The.server(execute)callback should be a one-liner that calls the pre-builtestimate(zipCode)helper.
2. app/routes/api.chat.tsx β register all five lazy tools
The four other lazy tools (return policy, size guide, care instructions, restock) are already built β read through them in the
app/tools/ folder if you want to see what a "this is the entire tool" file looks like. They are imported for you at the top of the chat route.- π¨ Add the five lazy tools (
getReturnPolicy,getSizeGuide,getCareInstructions,subscribeToRestock,getShippingEstimate) to thetools: [...]array on thechat({...})call. The order does not matter β eager and lazy tools sit in the same array. Thelazy: trueflag on each definition is what splits them apart at runtime. - π£ Delete the five
voidlines above the action once the array has them.
π¦ What happens behind the scenes
When
chat() sees a tool with lazy: true, it does not pass that tool's description and schema to the model. Instead, TanStack AI builds a single discovery tool whose input is "the name of a lazy tool you want to use." The model only sees that one extra tool β its enum lists every lazy tool by name.When the model wants to call a lazy tool (say
getReturnPolicy):- It calls the discovery tool with
{ name: 'getReturnPolicy' }. - TanStack AI loads the full description and schema for
getReturnPolicyand sends them back to the model on the next turn. - The model now has everything it needs and calls
getReturnPolicyfor real. - The tool runs and the result flows back like any other tool result.
Once a lazy tool is discovered in a conversation, it stays available for the rest of that conversation β the model does not have to discover it again.
π¦ When to make a tool lazy
Lazy is for niche tools β things the customer brings up rarely, where the cost of describing the tool on every message is not worth it. Compare:
- Eager (always described):
searchProducts,navigateTo,addToCartβ the customer touches these constantly. - Lazy (described on demand):
getReturnPolicy,getCareInstructions,getShippingEstimateβ the customer brings these up occasionally.
If you find yourself adding a 10th, 15th, 20th tool and the model is getting distracted, lazy is the answer. If the tool is core to every conversation, eager is fine.
π Docs
β How to know it is working
Open the chat panel and try these:
- "What's your return policy?" β the bot calls the discovery tool to load
getReturnPolicy, then calls it, then summarizes the policy. The chat trace shows both calls. - On a product page, "How do I wash these?" β discovery β
getCareInstructions({ productId })β answer. - "How long does shipping take to 94103?" β discovery β
getShippingEstimate({ zipCode: '94103' })β "$5, about 2 days, West Coast." - "This is sold out in size 10 β let me know when it's back, my email is me@example.com" β discovery β
subscribeToRestock({...})β confirmation. - "How does Nike sizing run?" β discovery β
getSizeGuide({ productId })β "Nike runs about half a size smallβ¦"
For "show me red shoes" or "take me to the cart", the bot should not call discovery β those eager tools are still right there.
π¨ If you get stuck
- TypeScript says
getShippingEstimateis not exported from#app/tools/shipping.tsβ you have not built the tool yet. The π¨ block inshipping.tsis the spec. - The bot calls a lazy tool with no prompt to discover it first β the
lazy: trueflag is missing on that tool's definition. Without it, the tool is eager and gets described upfront. - The bot says "I don't have access to that information" instead of calling a lazy tool β the lazy tool's name in the discovery enum is the only thing the model sees by default. The name has to be specific enough that the model recognizes it as relevant.
getReturnPolicyis clearer thanpolicy. - A tool that should be lazy is described on every message β check that
lazy: trueis insidetoolDefinition({ ... }), not as a sibling at the file level.