# Tool search and dynamic catalogs

Generate loading choices and consume changing registry tools without a separate loaded-tool store.

Source: https://goa.design/docs/2-goa-ai/tool-search/

Relative links resolve against the source URL above.


Tool search loads definitions when the model needs them. A registry lets providers change the available tools without rebuilding the consumer. These are independent: static tools can use search, and dynamic tools can be advertised immediately.

## Choose which tools load through search

Suppose the compiled `Records` toolset defines `lookup`, `search`, and `analyze`. Keep the frequently used `lookup` tool immediately available by deferring only the other two:

```go
Agent("assistant", "Find and analyze records.", func() {
    Use(Records, func() {
        Deferred("search", "analyze")
    })
})
```

Only `search` and `analyze` load through search; `lookup` is advertised immediately. This changes definition loading, not permission or execution. The choice belongs inside the consuming `Use`, never on a shared `Toolset` definition or an `Export`. Shared providers, exports, and other consumers remain unchanged.

Names must exactly match authored local tool names in the compiled toolset: `"search"`, not `"records.search"` or a generated Go name. Named selection supports local tools, agents exposed as tools, external MCP tools with declared schemas, and Goa-backed MCP tools.

- `Deferred()` selects all tools in that `Use`; repeating it is valid.
- Multiple named declarations combine: `Deferred("search")` followed by `Deferred("analyze")` selects both.
- Empty or duplicate names are rejected, including duplicates across declarations. Code generation rejects unknown names after collecting the complete compiled tool list.
- Combining `Deferred()` with any named selection in the same `Use` is rejected.

## Consume a changing catalog

For a changing catalog, use `Registry`. Both `FromRegistry` toolsets and whole registries reject named `Deferred` selections because their tools are resolved at runtime:

```go
var Company = Registry("company", func() {
    URL("https://registry.example")
})
var Records = Toolset(FromRegistry(Company, "records"))

var _ = Service("assistant", func() {
    Agent("reader", "Read records.", func() {
        Use(Records, func() { Deferred() })
    })
    Agent("generalist", "Use the company catalog.", func() {
        Use(Company, func() { Deferred() })
    })
})
```

The reader resolves one required toolset; the generalist resolves every currently listed toolset. Removing `Deferred()` advertises that same catalog immediately. A named source may require `Version("1.2.3")`; this checks the current version, rather than selecting an archived one.

Duplicate or overlapping sources, inline tool declarations on registry references, and exporting registry references are rejected. Provider contracts own tool definitions; run policy filters the resolved catalog before it reaches the model.

Registry references also reject consumer `Tags(...)` overrides and `PublishTo(...)`. Providers own tool tags; consumers filter them through run policy.

## Connect and publish

Construct the clustered registry's generated service client (`registry/gen/registry.Client`) and Pulse result-stream client in application startup code. Connect them before starting runs:

```go
if err := rt.RegisterRegistry("company", registryClient, pulseClient); err != nil {
    return err
}
if err := genreader.RegisterReaderAgent(ctx, rt, genreader.ReaderAgentConfig{
    Planner: myPlanner,
}); err != nil {
    return err
}
client := genreader.NewClient(rt)
```

`Definition()` and `NewClient(rt)` require no catalog arguments and make no network calls. Register compiled tools through the usual generated helpers. The runtime executes registry tools; no discovery callback or custom dynamic executor is required. Generated HTTP catalog clients are a separate transport for matching HTTP servers.

Providers publish generated `ToolSchemas()` records with the existing schema fingerprint and provider registration lifecycle. `ConsumerContract` carries search terms, field metadata, required labels, confirmation, pagination, and server-only data. Dynamic service tools and [native Agent tools](../agent-composition/#dynamic-agent-tools) support these contracts; planner control tools remain compiled. Schema-only registrations and unsupported execution kinds fail resolution explicitly.

## Runtime-authored tools and scoped catalogs

The dynamic Agent APIs described here require Goa-AI v0.84.0 or later.

Generated toolset packages also expose `Toolset()`: the authored registration name, description, tags, and fresh tool schemas. For declarations authored dynamically in Go, `runtime/toolregistry/contract.Compile` validates a `*genregistry.ToolSchema` and returns an owned `tools.ToolSpec` with validating JSON codecs. Supply metadata explicitly; the compiler does not infer context or permissions from field names.

Use `contract.Fingerprint(toolset)` for a runtime-authored declaration or a complete `Toolset()` value, including its description and tags. The registration timestamp is excluded. The existing generated `SchemaFingerprint(name)` continues to describe provider registration without optional toolset-level annotations. Generated service tools keep their typed codecs.

For application-specific source selection, implement `runtime.RegistryTools` and attach it with `WithRegistryTools`. Inside `Resolve`, `catalog.RunLabels()` returns a copy of the current run's labels. Use them to choose sources with `IncludeToolset` or `IncludeRegistry`; `Allows` determines which sources saved calls may continue using. Run policy still filters tools. Catalogs belong to individual planning activities, so concurrent sessions do not change one another's tools. Application namespaces and authorization remain application responsibilities.

## Who performs search?

- **OpenAI Responses, direct or Bedrock:** the model emits native client search calls. The adapter ranks permitted tool names, titles, and descriptions using BM25, a word-based relevance algorithm, and returns matching definitions. The first request contains a query-only search tool, with no directory of names or descriptions. The deferred catalog stays in the application.
- **Anthropic Messages, direct or Bedrock:** send the permitted catalog with deferred-loading flags and Claude's hosted search tool. The provider searches and expands definitions. On Bedrock, use the Messages `NewAnthropic` adapter and InvokeModel transport; Converse does not implement search.
- **Other adapters:** unsupported discovery returns `model.ErrToolSearchUnsupported`. There is no eager-loading fallback.

Planners pass `input.Agent.AdvertisedToolDefinitions()` with the current messages to model requests, and explicitly set the model or model class. Search calls stay inside the adapter; planners receive ordinary tool calls. OpenAI requires a positive `MaxTokens` or adapter `MaxCompletionTokens`; native search rounds share that invocation's output budget.

## Catalog changes and history

OpenAI search results place each selected function in a native namespace with the same provider name. This lets Bedrock return a complete call identity for replay. The adapter owns this representation; no namespace DSL, application mapping, or separate loaded-tool state is needed. Eager tools keep their existing representation. Bedrock histories created with bare dynamically loaded functions may contain calls without a namespace that Bedrock rejects during replay. Start a new conversation or deliberately trim the complete affected exchange; the adapter never invents missing provider fields.

Each planning activity that can start work reads the declared sources once and keeps that catalog fixed during inference. A later activity reads again, including providers registered since the previous turn. Final-answer-only and explicit finalizer activities do not read the registry. Empty whole registries are valid; missing named sources, version mismatches, duplicate tool identities, failed reads, and removals during resolution fail explicitly.

Accepted calls save only their selected definition, any fixed pagination partner, and the existing registration token. Confirmation, result decoding, and checkpoint restoration use that saved contract without fetching today's catalog. For service tools, `CallResolvedTool` checks the original token before publication; replacement before publication records `call_not_admitted`. Overload retries retain the token and report `admission_conflict` if that admission was replaced. Published calls retain their original assignment and result.

Native Agent calls retain their selected executor, configuration, and result contract and run as child workflows. Later registry changes affect subsequent planning activities, not already accepted calls.

Native search records remain in existing message metadata. Preserve that metadata through storage and compaction. There is no separate loaded-tool database. Historical definitions explain past calls; current consumption and policy authorize new ones.

Claude add/remove history requires a model supporting tool availability changes. A changed definition under a retained name cannot be replayed by that protocol and is rejected. Start a new conversation or deliberately compact away that retained definition; the adapter never silently resets history. Native-only Claude pause continuation is not implemented.

## Examples and upgrades

Regenerate the consuming agent after changing its `Deferred` selection. Code generation prepares search word counts and emits the selected tools' existing fixed IDs through the same runtime API. Named selection adds no provider API, provider state, or namespace.

In v0.80.0, `Deferred` changed from `func()` to `func(...string)`. Calls to `Deferred()` remain valid, but passing `Deferred` itself as a `func()` callback no longer compiles. Wrap direct callback references:

```go
// Before
Use(Records, Deferred)

// After
Use(Records, func() { Deferred() })
```

Apply the same wrapper to other assignments of `Deferred` to a `func()` callback. This preserves whole-toolset deferral. Wrapping an existing callback alone does not require regeneration.

Regenerate providers and consumers with Goa v3.32.0. Replace startup `Discover` calls, `RegistryToolsets` inputs, and dynamic executor wiring with `RegisterRegistry`. Upgrade the registry to expose `ResolveToolset` and `CallResolvedTool`, and publish complete `ToolSchemas()` before enabling dynamic consumers. Old schema-only registrations remain usable by existing static integrations, but not by this dynamic path.

Confirmation templates now use JSON names, such as `{{ .key }}`, rather than Go field names such as `{{ .Key }}`. Use `{{ json .value }}` for JSON values and `index` for optional properties.

The goa-ai quickstart includes `go run ./cmd/tool-search -provider openai -model YOUR_MODEL_ID`, or `-provider anthropic`, using the corresponding API-key environment variable. This optional command makes billable model calls; the regular quickstart stays credential-free. The helper returns its fixed Tokyo example. The local SDK test covers search, execution, and replay. A one-tool example does not establish token savings; measure search quality and usage on the chosen model and real catalog.

