Skip to main content
Tool discovery lets an agent start with a small set of tools and ask for more while it works. Your request declares its usual tools plus a discovery tool. The model calls the discovery tool to find what it needs, your application returns the matching tool definitions, and from then on the model calls those tools by their real names with their real schemas, on every provider. core-ai does not search for tools or run them. Each generate() or stream() call is one model call. Your agent loop answers discovery calls the same way it answers tool calls.

When to use it

Use tool discovery when declaring every tool up front is too much:
  • Large or dynamic catalogs, such as the tools of several MCP servers, plugins, or per-tenant integrations.
  • Long conversations that need more tools over time, where you want to keep the prompt short and the prompt cache stable.
If your agent has a handful of tools, declare them in tools instead.

Enable discovery

Set toolDiscovery on generate() or stream():
core-ai declares a discovery tool next to your tools. The model calls it with exact tool names, when it knows them, or with a keyword query. The discovery tool’s default description explains how discovery works; description is appended to it. How you tell the model what it can discover is your choice: list the tool names in description, describe the catalog in a system message, or rely on query alone.
Keep toolDiscovery and its description the same for the whole conversation. They are part of the tool declarations, so changing them rewrites the prompt cache.

Discovery calls

The model’s calls to the discovery tool arrive in result.discoveryCalls, next to result.toolCalls:
In result.parts, each one is a ToolDiscoveryCallPart, { type: 'tool-discovery-call', discoveryCall, providerMetadata? }, just as a tool call is a ToolCallPart wrapping a ToolCall. Discovery calls never appear in result.toolCalls. Calls to tools the model discovered earlier do: they are ordinary tool calls. finishReason is 'tool-calls' when tool calls or discovery calls are pending, so continue your loop while it is 'tool-calls' and answer both toolCalls and discoveryCalls. A step can contain only discovery calls, so a loop that checks toolCalls.length alone stops too early.

Answer discovery calls

Answer each discovery call with exactly one tool-discovery message:
Place it with the step’s tool results: after the assistant message and before the next request, in any order relative to tool messages. When nothing matches, return an empty tools array, optionally with a note that tells the model why, such as “The billing tools are unavailable right now.” The model then receives your note. Without one, it receives a default text saying that no tools were found, except natively on OpenAI, where the empty result speaks for itself. A note is only allowed without tools: neither native API can carry text next to loaded tools. Put standing guidance, such as which queries work or that empty results mean a tool doesn’t exist, in toolDiscovery.description. providerOptions work as on a tool message, such as an Anthropic cache breakpoint on the discovery result.

Tool snapshots

A ToolSnapshot is a tool definition whose parameters are plain JSON Schema for an object, with type: "object" at the root:
Convert a Standard Schema, such as a Zod schema, with toJsonSchema(). Tools from an MCP server already carry JSON Schema:
A snapshot’s name may contain letters, digits, underscores, and dashes, must start with a letter or underscore, and has at most 128 characters: the names every provider accepts. MCP tool names can contain other characters, such as the dot in github.create_issue. toPortableToolName() returns a valid name unchanged and converts any other one, such as github.create_issue to github_create_issue. Different names can convert to the same one, so check that your converted names stay unique. The model then calls the tool by that name, and lists it in a discovery call’s names, so translate both back with your map before you run the MCP tool or search your catalog. Plain JSON Schema keeps the conversation plain data that you can store and reload. core-ai renders each snapshot byte for byte the same on every request, which keeps the prompt cache stable.
Snapshots are plain JSON Schema, so validateToolCall() cannot validate calls to discovered tools. Validate arguments yourself before you run the tool, or declare the tool in tools with a Standard Schema. See Validate tool calls.

Agent loop

This loop keeps a small in-memory catalog, answers discovery calls from it, and runs the discovered tools. Each catalog entry keeps its Standard Schema to validate calls, and a JSON Schema snapshot for the model:
A typical run takes three steps: the model discovers get_order_status, calls it, and answers with the shipping status. Replace findTools with your own search, such as keyword search over MCP tool lists or an embedding index.

Streaming

With stream(), a discovery call arrives as one complete tool-discovery-call event when the model finishes it. The event has the same fields as ToolDiscoveryCallPart, as tool-call-end carries a toolCall; there are no start or delta events for discovery calls.

Declared tools

Every request declares, in this order:
  1. Your tools.
  2. The discovery tool, when the request sets toolDiscovery.
  3. The discovered tools from the tool-discovery messages in messages: the latest snapshot of each name, in order of first discovery.
These rules follow from that:
  • Your tools win. When a tool in tools has the name of a discovered tool, core-ai declares only your definition. Use this to keep a tool declared regardless of history, for example after you summarize a conversation and drop its discovery results.
  • Re-discovery refreshes. Returning a snapshot for a name that was already discovered replaces its definition on later requests. What a changed definition costs depends on the rendering:
    • Natively on OpenAI, each discovery result keeps the snapshots it holds and the model uses the newest definition, so a refresh keeps the prompt cache.
    • Natively on Anthropic, the deferred tool declarations carry the latest snapshot, so a changed definition rewrites the prompt cache from the first discovery result that references the tool.
    • With emulated discovery, the tool declarations at the start of the prompt carry the latest snapshot, so a changed definition rewrites the whole prompt cache.
    Return the same snapshot unless the tool changed.
  • Discovered tools stay declared while their discovery results are in messages, also on requests that omit toolDiscovery. Without toolDiscovery, the model can call them but cannot discover more.
  • There is no way to undeclare a discovered tool while its discovery result is in the history. Removing discovery calls and results from messages changes the prompt prefix, so the next request rewrites the prompt cache.
  • Structured output declares no discovered tools. generateObject() and streamObject() validate the discovery calls and results in messages like generate() does, except for the strict-capable schema contract (they declare no snapshot, strict or not), then send them as ordinary tool calls and results, so the model returns the object instead of calling a discovered tool.

Tool choice

  • { type: 'tool', toolName } may only name a tool in tools. To force a discovered tool, also declare it in tools.
  • 'none' keeps the model from calling any tool, including the discovery tool.
  • A discovery call satisfies 'required'.

Validation

core-ai throws a ValidationError before any provider call when a request that uses discovery has:
  • A tool in tools or a snapshot named core_ai_discover_tools or core_ai_deferred_tools_placeholder. core-ai reserves these names for its own tools.
  • A discovery call without a tool-discovery message answering it before the next user or assistant message.
  • A tool-discovery message that answers no discovery call of the preceding assistant message, or that answers a call another message already answered.
  • A discovery call whose id another call of the same assistant message also uses.
  • A tool-discovery message with both tools and a note.
  • A tool-discovery message that lists the same tool name more than once.
  • A snapshot whose name some provider rejects. See Tool snapshots.
  • A snapshot whose parameters are not plain JSON Schema, such as a Zod schema. Convert it with toJsonSchema().
  • A snapshot whose parameters are not an object schema with type: "object" at the root. See Tool snapshots.
  • A toolChoice that names a tool outside tools.
A request uses discovery when it sets toolDiscovery or its messages contain discovery calls or results. Other requests are not checked against these rules.

Native and emulated discovery

model.capabilities.tools.toolDiscovery reports how a model delivers discovered tools. Your code is the same for both.
  • 'native' uses the provider’s deferred tool loading. On Anthropic, discovered tools are declared as deferred tools and loaded by references in the discovery results. On the OpenAI Responses API, the discovery tool is a client-executed tool_search tool, and discovered tools travel inside its results. The declarations at the start of the prompt stay the same, so the prompt cache survives each discovery.
  • 'emulated' declares the discovery tool and the discovered tools as ordinary tools, and sends discovery results as text tool results. Each discovery changes the declared tool list, so on providers that cache tool declarations, the next request rewrites the prompt cache once.
Set the provider option emulateToolDiscovery: true to force emulation on a native model, for example behind a proxy or gateway that does not forward the native blocks:
The option is available under the anthropic, anthropic-vertex, openai, and azure-openai keys. On OpenAI and Azure OpenAI, only the Responses API accepts it. Conversation history does not depend on the mode. core-ai renders each request from the provider-neutral messages, so you can continue a conversation on another model or provider. A request renders natively only when it sets toolDiscovery and the model supports native discovery. Switching between native and emulated rendering within a conversation, for example by omitting toolDiscovery on a native model, rewrites the prompt cache once.

Strict snapshots

Set strict: true on a snapshot to have the provider enforce its schema. Native and emulated discovery both honor it. core-ai checks every strict snapshot in the history against the strict-capable schema contract, like strict tools in tools, and throws ToolSchemaStrictnessError for a snapshot outside it. That includes snapshots a later discovery replaced, as long as the latest snapshot of that name is strict: native OpenAI rendering then still sends them as strict. A replacement without strict makes every snapshot of the name non-strict, which repairs a snapshot outside the contract for as long as the latest snapshot of that name is not strict. Only names your tools declare are exempt. The check runs on every model, so a snapshot outside the contract fails on the first generate() or stream() request after its discovery, wherever the conversation runs. Structured output skips it, because it declares no snapshot. Snapshots stay declared while their discovery results are in the history, so core-ai declares a strict snapshot without strict where the model cannot take it:
  • On a model without strict tool support (model.capabilities.tools.strictSchemas.supported is false).
  • Beyond the model’s maxStrictTools or maxUnionParameters limit. Your strict tools count first. Then each snapshot, in order of first discovery, keeps strict while it still fits both limits.
The provider then does not enforce that snapshot’s schema, so validate the arguments of the calls you run, as the catalog in the example does. Anthropic also rejects a single strict schema it finds too complex (see Provider support). core-ai cannot check that limit, so keep strict snapshots small.

Next steps

Tool Calling

Define, validate, and run tools

Streaming

Handle stream events

Messages

Message roles and content parts

Types

Type reference for discovery parts and messages