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.
tools instead.
Enable discovery
SettoolDiscovery on generate() or stream():
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.
Discovery calls
The model’s calls to the discovery tool arrive inresult.discoveryCalls, next to result.toolCalls:
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 onetool-discovery message:
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
AToolSnapshot is a tool definition whose parameters are plain JSON Schema for an object, with type: "object" at the root:
toJsonSchema(). Tools from an MCP server already carry JSON Schema:
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.
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: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
Withstream(), 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:- Your
tools. - The discovery tool, when the request sets
toolDiscovery. - The discovered tools from the
tool-discoverymessages inmessages: the latest snapshot of each name, in order of first discovery.
-
Your tools win. When a tool in
toolshas 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.
-
Discovered tools stay declared while their discovery results are in
messages, also on requests that omittoolDiscovery. WithouttoolDiscovery, 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
messageschanges the prompt prefix, so the next request rewrites the prompt cache. -
Structured output declares no discovered tools.
generateObject()andstreamObject()validate the discovery calls and results inmessageslikegenerate()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 intools. To force a discovered tool, also declare it intools.'none'keeps the model from calling any tool, including the discovery tool.- A discovery call satisfies
'required'.
Validation
core-ai throws aValidationError before any provider call when a request that uses discovery has:
- A tool in
toolsor a snapshot namedcore_ai_discover_toolsorcore_ai_deferred_tools_placeholder. core-ai reserves these names for its own tools. - A discovery call without a
tool-discoverymessage answering it before the next user or assistant message. - A
tool-discoverymessage that answers no discovery call of the preceding assistant message, or that answers a call another message already answered. - A discovery call whose
idanother call of the same assistant message also uses. - A
tool-discoverymessage with bothtoolsand anote. - A
tool-discoverymessage that lists the same tool name more than once. - A snapshot whose
namesome provider rejects. See Tool snapshots. - A snapshot whose
parametersare not plain JSON Schema, such as a Zod schema. Convert it withtoJsonSchema(). - A snapshot whose
parametersare not an object schema withtype: "object"at the root. See Tool snapshots. - A
toolChoicethat names a tool outsidetools.
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-executedtool_searchtool, 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:
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
Setstrict: 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.supportedisfalse). - Beyond the model’s
maxStrictToolsormaxUnionParameterslimit. Your strict tools count first. Then each snapshot, in order of first discovery, keepsstrictwhile it still fits both limits.
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