@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
586 lines • 25.3 kB
JavaScript
import { ModelBehaviorError, UserError } from "../errors.mjs";
import logger from "../logger.mjs";
import { getClientToolSearchExecutor, getToolSearchRuntimeToolKey, getToolSearchRuntimeRoutingKey, } from "../tool.mjs";
import { buildFunctionToolLookupMap, getBareTopLevelFunctionToolName, getExplicitFunctionToolNamespace, getFunctionToolNamespaceDescription, getFunctionToolQualifiedName, isDeferredTopLevelFunctionTool, toolQualifiedName, } from "../toolIdentity.mjs";
import { serializeTool } from "../utils/serialize.mjs";
import { normalizeHostedMcpRequireApproval } from "../utils/mcpApproval.mjs";
import { resolveToolSearchCallId, getToolSearchExecution, } from "../utils/toolSearch.mjs";
function isDeferredFunctionTool(tool) {
return tool.type === 'function' && tool.deferLoading === true;
}
function isTopLevelDeferredFunctionTool(tool) {
return (isDeferredFunctionTool(tool) && !getExplicitFunctionToolNamespace(tool));
}
export function isDeferredHostedMcpTool(tool) {
return (tool.type === 'hosted_tool' &&
tool.providerData?.type === 'mcp' &&
tool.providerData.defer_loading === true);
}
function getBuiltInClientToolSearchArguments(value) {
if (!value || typeof value !== 'object') {
return undefined;
}
const paths = value.paths;
if (!Array.isArray(paths)) {
return undefined;
}
return {
paths: paths.filter((path) => typeof path === 'string' && path.length > 0),
};
}
function getDeferredFunctionToolsByName(tools) {
return new Map(tools
.filter(isTopLevelDeferredFunctionTool)
.map((tool) => [tool.name, tool]));
}
function getDeferredNamespaceToolsByName(tools) {
const namespaces = new Map();
for (const tool of tools) {
if (!isDeferredFunctionTool(tool)) {
continue;
}
const namespace = getExplicitFunctionToolNamespace(tool);
if (!namespace) {
continue;
}
const description = getFunctionToolNamespaceDescription(tool);
if (!description) {
throw new UserError(`Deferred namespace "${namespace}" must provide a description for built-in client tool_search.`);
}
const existing = namespaces.get(namespace);
if (!existing) {
namespaces.set(namespace, { description, tools: [tool] });
continue;
}
existing.tools.push(tool);
}
return namespaces;
}
function getDeferredHostedMcpToolsByServerLabel(tools) {
return new Map(tools
.filter(isDeferredHostedMcpTool)
.map((tool) => [tool.providerData.server_label, tool]));
}
function getDeferredNamespacedFunctionToolsByQualifiedName(tools) {
const qualifiedTools = new Map();
for (const tool of tools) {
if (!isDeferredFunctionTool(tool)) {
continue;
}
const namespace = getExplicitFunctionToolNamespace(tool);
if (!namespace) {
continue;
}
const description = getFunctionToolNamespaceDescription(tool);
const qualifiedName = getFunctionToolQualifiedName(tool);
if (!description || !qualifiedName) {
continue;
}
qualifiedTools.set(qualifiedName, { namespace, description, tool });
}
return qualifiedTools;
}
function serializeFunctionToolForToolSearchOutput(tool) {
const serialized = serializeTool(tool);
const namespace = getExplicitFunctionToolNamespace(tool);
const { providerData: _providerData, ...toolSearchPayload } = serialized;
void _providerData;
if (!namespace) {
return toolSearchPayload;
}
const { namespace: _namespace, namespaceDescription, ...toolPayload } = toolSearchPayload;
void namespaceDescription;
return toolPayload;
}
function serializeHostedMcpToolForToolSearchOutput(tool) {
const { on_approval: _onApproval, ...providerData } = tool.providerData;
void _onApproval;
return providerData;
}
function appendNamespaceMemberToolSearchOutput(args) {
const { namespace, description, tool, namespaceOutputIndexByName, resolvedTools, } = args;
const existingIndex = namespaceOutputIndexByName.get(namespace);
const serializedTool = serializeFunctionToolForToolSearchOutput(tool);
if (typeof existingIndex === 'number') {
const existingNamespace = resolvedTools[existingIndex];
if (existingNamespace?.type === 'namespace' &&
Array.isArray(existingNamespace.tools)) {
existingNamespace.tools.push(serializedTool);
return;
}
}
namespaceOutputIndexByName.set(namespace, resolvedTools.length);
resolvedTools.push({
type: 'namespace',
name: namespace,
description,
tools: [serializedTool],
});
}
function isClientToolSearchToolWithCustomParameters(tool) {
return (tool.type === 'hosted_tool' &&
tool.providerData?.type === 'tool_search' &&
tool.providerData.execution === 'client' &&
tool.providerData.parameters != null);
}
function isClientToolSearchTool(tool) {
return (tool.type === 'hosted_tool' &&
tool.providerData?.type === 'tool_search' &&
tool.providerData.execution === 'client');
}
function normalizeClientToolSearchExecutorResult(result) {
if (typeof result === 'undefined' || result === null) {
return [];
}
return Array.isArray(result) ? result : [result];
}
function addLoadedToolName(loadedToolNames, name, namespace) {
if (typeof name !== 'string' || name.length === 0) {
return;
}
const explicitNamespace = typeof namespace === 'string' && namespace.length > 0
? namespace
: undefined;
const qualifiedName = toolQualifiedName(name, explicitNamespace);
if (qualifiedName) {
loadedToolNames.add(qualifiedName);
}
if (!explicitNamespace || explicitNamespace === name) {
loadedToolNames.add(name);
}
}
function collectLoadedToolNamesFromSearchResult(searchResult, loadedToolNames, namespace, includeMcpDescriptors = true) {
if (!searchResult || typeof searchResult !== 'object') {
return;
}
const candidate = searchResult;
if (candidate.type === 'tool_reference') {
addLoadedToolName(loadedToolNames, candidate.functionName, candidate.namespace ?? namespace);
return;
}
if (candidate.type === 'function') {
addLoadedToolName(loadedToolNames, candidate.name, candidate.namespace ?? namespace);
return;
}
const mcpProviderData = getHostedMcpProviderDataFromSearchResult(candidate);
if (mcpProviderData) {
if (includeMcpDescriptors) {
addLoadedToolName(loadedToolNames, mcpProviderData.server_label);
}
return;
}
if (candidate.type === 'namespace' && Array.isArray(candidate.tools)) {
const nestedNamespace = typeof candidate.name === 'string' && candidate.name.length > 0
? candidate.name
: namespace;
for (const nestedTool of candidate.tools) {
collectLoadedToolNamesFromSearchResult(nestedTool, loadedToolNames, nestedNamespace, includeMcpDescriptors);
}
}
}
function normalizeStringRecord(value) {
if (!value || typeof value !== 'object') {
return undefined;
}
const entries = Object.entries(value).filter(([key, entryValue]) => typeof key === 'string' && typeof entryValue === 'string');
if (entries.length === 0) {
return undefined;
}
return Object.fromEntries(entries);
}
function normalizeHostedMcpProviderData(value) {
if (!value || typeof value !== 'object') {
return undefined;
}
const candidate = value;
if (candidate.type !== 'mcp' ||
typeof candidate.server_label !== 'string' ||
candidate.server_label.length === 0) {
return undefined;
}
const normalized = {
type: 'mcp',
server_label: candidate.server_label,
require_approval: normalizeHostedMcpRequireApproval(candidate.require_approval),
};
if (typeof candidate.server_url === 'string') {
normalized.server_url = candidate.server_url;
}
if (typeof candidate.connector_id === 'string') {
normalized.connector_id = candidate.connector_id;
}
if (typeof candidate.authorization === 'string') {
normalized.authorization = candidate.authorization;
}
if (candidate.allowed_tools !== undefined) {
normalized.allowed_tools =
candidate.allowed_tools;
}
if (Array.isArray(candidate.allowed_callers) &&
candidate.allowed_callers.every((caller) => caller === 'direct' || caller === 'programmatic')) {
normalized.allowed_callers = candidate.allowed_callers;
}
if (candidate.defer_loading === true) {
normalized.defer_loading = true;
}
const headers = normalizeStringRecord(candidate.headers);
if (headers) {
normalized.headers = headers;
}
if (typeof candidate.server_description === 'string') {
normalized.server_description = candidate.server_description;
}
return normalized;
}
function getHostedMcpProviderDataFromSearchResult(searchResult) {
const directProviderData = normalizeHostedMcpProviderData(searchResult);
if (directProviderData) {
return directProviderData;
}
if (!searchResult || typeof searchResult !== 'object') {
return undefined;
}
const hostedCandidate = searchResult;
if (hostedCandidate.type !== 'hosted_tool') {
return undefined;
}
return normalizeHostedMcpProviderData(hostedCandidate.providerData);
}
export function addLoadedToolNamesFromToolSearchOutput(toolSearchOutput, loadedToolNames) {
for (const tool of toolSearchOutput.tools) {
collectLoadedToolNamesFromSearchResult(tool, loadedToolNames);
}
}
/** Snapshots namespace discovery before configuration changes can alter its meaning. */
export function recordHostedMcpDiscovery(output, tools) {
const names = new Set();
if (getToolSearchExecution(output) !== 'client') {
for (const result of output.tools) {
collectLoadedToolNamesFromSearchResult(result, names, undefined, false);
}
}
const functionNames = new Set(tools
.filter((tool) => tool.type === 'function')
.flatMap((tool) => [
getFunctionToolQualifiedName(tool),
...(isDeferredTopLevelFunctionTool(tool)
? [toolQualifiedName(tool.name, tool.name)]
: []),
]));
const toolSearchMcpToolNames = [...names].filter((name) => name.startsWith('mcp_') && !functionNames.has(name));
const { toolSearchMcpToolNames: _suppliedProvenance, ...rawOutput } = output;
return toolSearchMcpToolNames.length > 0
? { ...rawOutput, toolSearchMcpToolNames }
: rawOutput;
}
/** Matches explicit MCP discovery or an unambiguous hosted namespace result. */
export function toolSearchOutputLoadsHostedMcpTool(output, serverLabel, toolName) {
if (output.tools.some((result) => getHostedMcpProviderDataFromSearchResult(result)?.server_label ===
serverLabel)) {
return true;
}
// Client search namespaces describe application functions, not hosted MCP tools.
if (getToolSearchExecution(output) === 'client' ||
typeof toolName !== 'string') {
return false;
}
const qualifiedName = toolQualifiedName(toolName, `mcp_${serverLabel}`);
if (!qualifiedName ||
!output.toolSearchMcpToolNames?.includes(qualifiedName)) {
// Missing provenance cannot be reconstructed from today's tool configuration.
// Require fresh discovery rather than reinterpreting historical function results.
return false;
}
const loadedToolNames = new Set();
addLoadedToolNamesFromToolSearchOutput(output, loadedToolNames);
return loadedToolNames.has(qualifiedName);
}
export function addHostedMcpToolsFromToolSearchOutput(toolSearchOutput, hostedMcpToolsByServerLabel, options) {
const preserveExistingServerLabels = options?.preserveExistingServerLabels;
for (const tool of toolSearchOutput.tools) {
const providerData = getHostedMcpProviderDataFromSearchResult(tool);
if (!providerData) {
continue;
}
if (preserveExistingServerLabels?.has(providerData.server_label) &&
hostedMcpToolsByServerLabel.has(providerData.server_label)) {
continue;
}
hostedMcpToolsByServerLabel.set(providerData.server_label, {
type: 'hosted_tool',
name: 'hosted_mcp',
providerData,
});
}
}
export function validateClientToolSearchSupport(tools) {
if (!tools.some((tool) => isClientToolSearchToolWithCustomParameters(tool) &&
!getClientToolSearchExecutor(tool))) {
return;
}
throw new UserError('Runner.run() and Runner.runStreamed() require toolSearchTool({ execution: "client", execute }) when custom client tool_search parameters are provided. Leave parameters unset to use the default built-in { paths: string[] } loader, or use server execution / a custom model loop instead.');
}
export function resolveBuiltInClientToolSearchTools(paths, tools) {
const deferredToolsByName = getDeferredFunctionToolsByName(tools);
const deferredNamespaceToolsByName = getDeferredNamespaceToolsByName(tools);
const deferredNamespacedToolsByQualifiedName = getDeferredNamespacedFunctionToolsByQualifiedName(tools);
const deferredHostedMcpToolsByServerLabel = getDeferredHostedMcpToolsByServerLabel(tools);
const ambiguousPath = paths.find((path) => Number(deferredToolsByName.has(path)) +
Number(deferredNamespaceToolsByName.has(path)) +
Number(deferredNamespacedToolsByQualifiedName.has(path)) +
Number(deferredHostedMcpToolsByServerLabel.has(path)) >
1);
if (ambiguousPath) {
throw new UserError(`Runner.run() and Runner.runStreamed() cannot disambiguate built-in client tool_search path "${ambiguousPath}" because it matches multiple deferred tool_search surfaces. Rename one of them or use server tool_search execution.`);
}
const requestedNamespacePaths = new Set(paths.filter((path) => deferredNamespaceToolsByName.has(path)));
const resolvedTools = [];
const seenPaths = new Set();
const resolvedQualifiedNames = new Set();
for (const path of paths) {
if (seenPaths.has(path)) {
continue;
}
seenPaths.add(path);
const tool = deferredToolsByName.get(path);
if (!tool) {
const namespaceTools = deferredNamespaceToolsByName.get(path);
if (namespaceTools) {
const unresolvedTools = namespaceTools.tools.filter((tool) => {
const qualifiedName = getFunctionToolQualifiedName(tool);
return !qualifiedName || !resolvedQualifiedNames.has(qualifiedName);
});
if (unresolvedTools.length === 0) {
continue;
}
for (const namespaceTool of unresolvedTools) {
const qualifiedName = getFunctionToolQualifiedName(namespaceTool);
if (qualifiedName) {
resolvedQualifiedNames.add(qualifiedName);
}
}
resolvedTools.push(...unresolvedTools);
continue;
}
const namespacedTool = deferredNamespacedToolsByQualifiedName.get(path);
if (namespacedTool) {
if (requestedNamespacePaths.has(namespacedTool.namespace)) {
continue;
}
if (resolvedQualifiedNames.has(path)) {
continue;
}
resolvedQualifiedNames.add(path);
resolvedTools.push(namespacedTool.tool);
continue;
}
}
if (tool) {
resolvedTools.push(tool);
continue;
}
const hostedMcpTool = deferredHostedMcpToolsByServerLabel.get(path);
if (hostedMcpTool) {
resolvedTools.push(hostedMcpTool);
}
}
return resolvedTools;
}
export function createClientToolSearchOutputFromTools(toolSearchCall, tools) {
const callId = resolveToolSearchCallId(toolSearchCall);
const resolvedTools = [];
const namespaceOutputIndexByName = new Map();
const seenToolKeys = new Set();
for (const tool of tools) {
const runtimeToolKey = getToolSearchRuntimeRoutingKey(tool);
if (runtimeToolKey && seenToolKeys.has(runtimeToolKey)) {
continue;
}
if (runtimeToolKey) {
seenToolKeys.add(runtimeToolKey);
}
if (tool.type === 'function') {
const namespace = getExplicitFunctionToolNamespace(tool);
if (namespace) {
const description = getFunctionToolNamespaceDescription(tool);
if (!description) {
throw new UserError(`Client tool_search execute() returned namespace member "${namespace}.${tool.name}" without a namespace description.`);
}
appendNamespaceMemberToolSearchOutput({
namespace,
description,
tool,
namespaceOutputIndexByName,
resolvedTools,
});
continue;
}
resolvedTools.push(serializeFunctionToolForToolSearchOutput(tool));
continue;
}
if (isDeferredHostedMcpTool(tool) || tool.type === 'hosted_tool') {
if (tool.type === 'hosted_tool' && tool.providerData?.type === 'mcp') {
resolvedTools.push(serializeHostedMcpToolForToolSearchOutput(tool));
continue;
}
}
throw new UserError('Client tool_search execute() may only return function tools or hosted MCP tools.');
}
return {
type: 'tool_search_output',
status: 'completed',
tools: resolvedTools,
providerData: {
call_id: callId,
execution: 'client',
},
};
}
export function createBuiltInClientToolSearchOutput(toolSearchCall, tools) {
const callId = resolveToolSearchCallId(toolSearchCall);
const builtInArgs = getBuiltInClientToolSearchArguments(toolSearchCall.arguments);
if (!builtInArgs) {
throw new UserError(`Runner.run() and Runner.runStreamed() only auto-execute built-in client tool_search calls with { paths: string[] }. Custom client tool_search schemas require toolSearchTool({ execution: "client", execute }) or a custom model loop before re-processing call ${callId}.`);
}
return createClientToolSearchOutputFromTools(toolSearchCall, resolveBuiltInClientToolSearchTools(builtInArgs.paths, tools));
}
function indexCustomClientToolSearchRuntimeTools(runtimeTools) {
const runtimeToolsByKey = new Map();
for (const runtimeTool of runtimeTools) {
if (runtimeTool.type === 'function' &&
getExplicitFunctionToolNamespace(runtimeTool) === runtimeTool.name) {
throw new ModelBehaviorError('Responses tool search reserves same-name namespaces for deferred top-level function tools. Rename the namespace or tool name to avoid ambiguous dispatch.');
}
const runtimeToolKey = getToolSearchRuntimeRoutingKey(runtimeTool);
if (!runtimeToolKey) {
throw new ModelBehaviorError('Client tool_search execute() returned an unsupported tool type.');
}
if (runtimeToolsByKey.has(runtimeToolKey)) {
throw new ModelBehaviorError('Client tool_search execute() returned multiple tools with the same routed identity. Return only one tool for each routed identity.');
}
runtimeToolsByKey.set(runtimeToolKey, runtimeTool);
}
return runtimeToolsByKey;
}
export async function executeCustomClientToolSearch(args) {
const { agent, runContext, toolSearchCall, toolSearchTool, tools } = args;
const executor = getClientToolSearchExecutor(toolSearchTool);
if (!executor) {
throw new UserError('Client tool_search execution requires a registered execute callback.');
}
const callbackRuntimeTools = normalizeClientToolSearchExecutorResult(await executor({
agent,
availableTools: [...tools],
loadDefault: (paths) => resolveBuiltInClientToolSearchTools(paths, tools),
runContext,
toolCall: toolSearchCall,
}));
const runtimeTools = await filterEnabledToolSearchRuntimeTools({
agent,
runContext,
runtimeTools: callbackRuntimeTools,
});
indexCustomClientToolSearchRuntimeTools(runtimeTools);
return {
output: createClientToolSearchOutputFromTools(toolSearchCall, runtimeTools),
runtimeTools,
callbackRuntimeTools,
};
}
export async function filterEnabledToolSearchRuntimeTools(args) {
const { agent, runContext, runtimeTools } = args;
const enabledTools = [];
for (const runtimeTool of runtimeTools) {
if (runtimeTool.type === 'function' &&
!(await runtimeTool.isEnabled(runContext, agent))) {
continue;
}
enabledTools.push(runtimeTool);
}
return enabledTools;
}
export function registerRuntimeToolSearchTools(args) {
const { availableTools, functionMap, handoffMap, mcpToolMap, replaceableRuntimeToolKeys, replacedRuntimeTools = [], runtimeTools, } = args;
const availableToolsByKey = new Map();
for (const tool of availableTools) {
const key = getToolSearchRuntimeRoutingKey(tool);
if (key) {
availableToolsByKey.set(key, tool);
}
}
const runtimeToolsByKey = indexCustomClientToolSearchRuntimeTools(runtimeTools);
for (const [runtimeToolKey, runtimeTool] of runtimeToolsByKey) {
const existingTool = availableToolsByKey.get(runtimeToolKey);
if (existingTool &&
existingTool !== runtimeTool &&
!replaceableRuntimeToolKeys?.has(runtimeToolKey)) {
throw new ModelBehaviorError(logger.dontLogToolData
? 'Client tool_search execute() returned a tool that conflicts with an existing available tool.'
: `Client tool_search execute() returned tool "${getToolSearchRuntimeToolKey(runtimeTool) ?? runtimeToolKey}" that conflicts with an existing available tool.`);
}
if (runtimeTool.type === 'function') {
const bareToolName = getBareTopLevelFunctionToolName(runtimeTool);
if (bareToolName && handoffMap.has(bareToolName)) {
throw new ModelBehaviorError('Client tool_search execute() returned a bare function tool that conflicts with an available handoff. Assign a unique tool name or use a namespace.');
}
continue;
}
if (runtimeTool.type === 'hosted_tool' &&
runtimeTool.providerData?.type === 'mcp') {
continue;
}
throw new ModelBehaviorError('Client tool_search execute() returned an unsupported tool type.');
}
const replacedRuntimeToolSet = new Set(replacedRuntimeTools);
for (let index = availableTools.length - 1; index >= 0; index--) {
if (replacedRuntimeToolSet.has(availableTools[index])) {
availableTools.splice(index, 1);
}
}
for (const [key, tool] of functionMap) {
if (replacedRuntimeToolSet.has(tool)) {
functionMap.delete(key);
}
}
for (const [serverLabel, tool] of mcpToolMap) {
if (replacedRuntimeToolSet.has(tool)) {
mcpToolMap.delete(serverLabel);
}
}
const registeredTools = [];
for (const [runtimeToolKey, runtimeTool] of runtimeToolsByKey) {
const existingIndex = availableTools.findIndex((availableTool) => getToolSearchRuntimeRoutingKey(availableTool) === runtimeToolKey);
const existingTool = existingIndex >= 0 ? availableTools[existingIndex] : undefined;
if (existingTool === runtimeTool) {
if (replaceableRuntimeToolKeys?.has(runtimeToolKey)) {
registeredTools.push(runtimeTool);
}
continue;
}
if (existingIndex >= 0) {
availableTools[existingIndex] = runtimeTool;
}
else {
availableTools.push(runtimeTool);
}
registeredTools.push(runtimeTool);
replaceableRuntimeToolKeys?.add(runtimeToolKey);
if (runtimeTool.type === 'function') {
const runtimeFunctionMap = buildFunctionToolLookupMap([runtimeTool]);
for (const [key, tool] of runtimeFunctionMap) {
functionMap.set(key, tool);
}
continue;
}
if (runtimeTool.type === 'hosted_tool' &&
runtimeTool.providerData?.type === 'mcp') {
mcpToolMap.set(runtimeTool.providerData.server_label, runtimeTool);
continue;
}
}
return registeredTools;
}
export function getClientToolSearchHelper(tools) {
return tools.find(isClientToolSearchTool);
}
//# sourceMappingURL=toolSearch.mjs.map