# Tool Search & Discovery Architecture

The agent has ~90 tools across 14 domains (tools + skills). To keep prompt size manageable, tools are split into **core** (always sent to the LLM) and **deferred** (discovered on demand via `search_tools`). Search uses BM25 + glossary expansion via `HybridSearch` from `@coda/search`.

---

## Core / Deferred Split

`ToolDefinition.core` is the single source of truth. `deferred.ts` reads `TOOL_DEFINITIONS` (the barrel) and partitions them:

```
TOOL_DEFINITIONS (all ~90 tools)
  ├── core: true  → CORE_TOOLS        (~13 tools, always sent to LLM)
  └── (no flag)   → DEFERRED_TOOL_MAP (~77 tools, sent only after discovery)
```

**Core tools** — `search_tools`, `search_accounts`, `search_contracts`, `get_current_statement_period`, `get_account_current_balance`, `generate_excel`, `generate_pdf`, skills (`account_overview_skill`, `revenue_overview_skill`, `snowflake_explore_skill`, `contract_overview_skill`, `graphql_explore_skill`, `resolve_artist_skill`).

**Why split?** Deferring ~77 tools reduces per-round prompt cost and lets the LLM ask for exactly what it needs.

`buildActiveToolConfig(loadedDeferredTools, isAvailable)` assembles the tool list for each LLM round: all core tools (filtered by availability) plus any deferred tools previously discovered and loaded.

---

## Search Flow

```
LLM calls search_tools({ query })
  │
  ▼
registry/handlers.ts: buildAllHandlers()
  └─ creates isAvailable predicate (all handler enabled() checks)
  └─ registers search_tools handler as closure with ToolSearchContext
       { isAvailable, toolAffinities }
  │
  ▼
search/handlers.ts: handleSearchTools(input, headers, searchContext)
  └─ calls searchCatalog(query, context)
  │
  ▼
catalog.ts: ToolCatalogSearch.search(query, context)
  ├─ hybridSearch.search(query, limit*2)   ← BM25 + GlossaryIndex + FuzzyStage via RRF
  ├─ filter results by isAvailable(name)
  ├─ if toolAffinities: multiplicative rerank  score * (1 + affinity * 0.1)
  └─ slice to limit (default 8)
  │
  ▼
results returned to LLM (name, domain, description, paramNames, hint, examples)
  │
  ▼
orchestrator.ts: parses search_tools result
  └─ for each r in results: loadedDeferredTools.add(r.name)
  │
  ▼
next round: buildActiveToolConfig(loadedDeferredTools, isAvailable)
  └─ discovered tools now present in active tool config
```

The catalog is a **module-level singleton** initialized at startup (`CATALOG_PROMISE`). Per-request concerns (availability, affinity) are applied post-fusion so the shared index is never mutated per request.

---

## Availability Gating

Availability is handler-owned and request-scoped:

- **Handler-owned predicate** — `ToolHandlerObject.enabled()` is a synchronous check owned by the handler itself. Example: Snowflake handlers check pool health; if the pool is not configured, `enabled()` returns `false`. Plain `ToolHandler` functions are always considered enabled.

- **Request-scoped predicate** — `buildAvailabilityChecker(deps)` in `registry/handlers.ts` builds all handlers once, calls `isHandlerEnabled()` on each, and returns a `(name: string) => boolean` closure. This predicate is passed into `buildActiveToolConfig` and `ToolSearchContext` so disabled tools are excluded from both the sent config and catalog results.

- **Dual guard** — handlers are gated at surfacing time (catalog search, deferred config) _and_ at execution time (`executeToolWithHandlers`). A tool invoked from stale conversation history is rejected if its handler is now disabled.

---

## Permission Model

Permissions are checked before handler execution, not during discovery:

- `ToolDefinition.permission` — canonical permission string (e.g., `"tools.snowflake.query"`).
- `checkToolPermission(toolName, ctx, accessClient)` — single RPC to access-api; used by `executeTool`.
- `checkBatchToolPermissions(toolUses, ctx, accessClient)` — collects distinct permissions, makes **one** `checkBatch()` RPC for all tools in a round; results are pre-passed to `executeToolWithHandlers` to skip per-tool RPCs.

**Fail-open**: when the access client is unavailable, or the RPC fails, or the tool definition is not found, execution is allowed (logged as a warning).

**Shadow mode** (`permissionContext.shadowMode = true`): denied outcomes are logged but execution proceeds. Used for gradual rollout of new permission rules.

---

## Tool Affinity (Memory-Based Personalization)

The memory service produces `tool_affinity` and `tool_avoidance` facts from observations across sessions. These flow into search ranking:

```
Memory service (Redis)
  └─ extractToolAffinities(facts) → Map<toolName, score>
       positive = affinity, negative = avoidance, magnitude = confidence
  │
  ▼
stream-handler.ts
  └─ passes toolAffinities into ConverseWithToolsOptions
  │
  ▼
orchestrator.ts
  └─ passes toolAffinities into executeTools deps
  │
  ▼
registry/handlers.ts: buildAllHandlers()
  └─ closes over toolAffinities in search_tools handler
  │
  ▼
catalog.ts: ToolCatalogSearch.search()
  └─ post-fusion rerank: score * max(0.01, 1 + affinity * 0.1)
     affinity > 0 → boost, affinity < 0 → demote
```

Affinity is a **tiebreaker**, not a hard filter — it shifts scores multiplicatively after BM25+glossary fusion.

---

## Extension Points

- **Adding a tool** — create `tools/<domain>/definitions.ts` with a `ToolDefinition[]` export, add the handler to `<domain>/handlers.ts`, export from the domain `index.ts`, register in `tools/definitions.ts` (barrel). The handler is composed by `registry/handlers.ts` via `buildAllHandlers()`. Add `domain` to the `ToolDomain` union in `types/ai.ts` if it's a new domain.

- **Adding glossary terms** — edit `packages/extensions/tools/tools-glossary.json`. Entries map business terms → target tool names with context, gotchas, and business rules. The glossary is loaded at startup via `toolsGlossaryEntries` from `@coda/extensions`.

- **Enabling semantic search** — pass an `embeddingProvider` to `HybridSearch.create()` in `catalog.ts`. Currently `null` (keyword-only mode).

- **Adding custom search stages** — add to the `stages[]` array in `HybridSearch.create()` config. The `GlossaryIndex` is currently both an expander and a stage.

---

## Key Files

| File                                            | Responsibility                                                                                    |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `tools/definitions.ts`                          | Barrel: merges all domain `ToolDefinition[]` arrays into `TOOL_DEFINITIONS` and `TOOL_MAP`        |
| `tools/deferred.ts`                             | Derives `CORE_TOOLS` and `DEFERRED_TOOL_MAP` from `core` flags; exports `buildActiveToolConfig()` |
| `tools/catalog.ts`                              | `ToolCatalogSearch` — `HybridSearch`-backed catalog; `searchCatalog()` module singleton           |
| `tools/registry/permissions.ts`                 | `checkToolPermission`, `checkBatchToolPermissions`, `PermissionContext`                           |
| `tools/registry/handlers.ts`                    | `buildAllHandlers`, `buildAvailabilityChecker`, `NotionDeps`                                      |
| `tools/registry/execution.ts`                   | `executeTool`, `executeTools`, `ExecuteToolDeps`, `formatToolResult`                              |
| `tools/registry/index.ts`                       | Barrel re-exports for all public APIs                                                             |
| `tools/handler-utils.ts`                        | `ToolHandler` / `ToolHandlerObject` types; `isHandlerEnabled`, `callToolHandler`, `requireId`     |
| `tools/search/definitions.ts`                   | `search_tools` definition (`core: true`)                                                          |
| `tools/search/handlers.ts`                      | `handleSearchTools` — bridges tool invocation to `searchCatalog`                                  |
| `tools/<domain>/definitions.ts`                 | Domain-scoped `ToolDefinition[]` arrays                                                           |
| `tools/<domain>/handlers.ts`                    | Domain-scoped `ToolHandler` functions                                                             |
| `ai/orchestrator.ts`                            | Conversation loop; owns `loadedDeferredTools` set; calls `buildActiveToolConfig` each round       |
| `ai/memory/affinity.ts`                         | `extractToolAffinities()` — converts facts to `Map<toolName, score>`                              |
| `packages/extensions/tools/tools-glossary.json` | Business term → tool name mappings with domain context                                            |
| `types/ai.ts`                                   | `ToolDefinition`, `ToolSearchContext`, `ToolDomain` type definitions                              |
