Refactor: Tool Annotations pattern for dynamic tool filtering

Resolved 💬 3 comments Opened Dec 11, 2025 by sdolard-keyban Closed Feb 10, 2026

Context

Toize uses dynamic tool filtering to control LLM behavior based on conversation state. Currently this is done ad-hoc in route.ts, which becomes fragile as cases multiply.

Current Implementation

1. ToolSet Pattern (chat-tools.ts:1044-1079)

Static filtering by userType:

const toolsBySet = {
  detection: { getNextStep, detectUserType, setQuestionHint },
  buyer: { getNextStep, updateBuyerProfile, searchProperties, getNearbyCities, ... },
  seller: { getNextStep, estimateProperty, getCityStats, ... },
  landlord: { getNextStep, estimateRent, getCityStats, ... },
};

2. State-based filtering (route.ts:238-249)

Dynamic filtering added for "nearby cities" flow:

const hasNearbyCitiesContext = isNearbyCitiesTurn || !!profile.location.primaryArea;
const filteredTools = hasNearbyCitiesContext && !searchPerformed
  ? { ...tools, getNearbyCities: undefined, getAreasOverview: undefined }
  : tools;

3. Disambiguation mode (route.ts:242)

if (isDisambiguationTurn) return {};  // Force pure text response

Problem

The ad-hoc approach in route.ts led to two bugs during the "Budget First" flow implementation:

  1. LLM called getNearbyCities directly despite server-side orchestration
  2. Tool filtering didn't persist across messages (fixed by checking primaryArea)

As more flows are added, this pattern becomes harder to maintain.

Proposed Solution: Tool Annotations

Add declarative pre-requisites to tool definitions:

getNearbyCities: {
  description: "Find nearby cities...",
  // NEW: Declarative availability rules
  availability: {
    requires: { "profile.userType": ["acheteur", "locataire"] },
    excludesWhen: { "profile.location.primaryArea": { exists: true } },
    excludesFlags: ["searchPerformed"],
  },
  inputSchema: getNearbyCitiesSchema,
  execute: async (params) => { ... },
}

Then filter tools declaratively:

const availableTools = filterToolsByState(allTools, {
  profile: currentProfile,
  flags: { searchPerformed, isDisambiguationTurn },
});

Benefits

| Aspect | Current | With Annotations |
|--------|---------|------------------|
| Tool availability logic | Scattered in route.ts | Co-located with tool definition |
| Adding new constraints | Edit multiple files | Edit tool definition only |
| Understanding tool behavior | Read route.ts + chat-tools.ts | Read tool definition |
| Testing | Integration tests only | Unit test per tool |

Scope

  • [ ] Design ToolAvailability schema
  • [ ] Add annotations to existing tools
  • [ ] Create filterToolsByState() utility
  • [ ] Migrate ad-hoc filtering from route.ts
  • [ ] Add unit tests for tool availability

References

  • Related patterns: Tool Gating, ReWOO (Reasoning WithOut Observation)
  • Current implementation: src/lib/ai/chat-tools.ts, src/app/api/chat/route.ts
  • Orchestration: src/lib/ai/orchestration/

---
🤖 Generated with Claude Code

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗