import { DOMPurify } from '../lib.js'; import { addOneMessage, chat, event_types, eventSource, main_api, saveChatConditional, system_avatar, systemUserName } from '../script.js'; import { chat_completion_sources, oai_settings } from './openai.js'; import { Popup } from './popup.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; import { isTrueBoolean } from './utils.js'; /** * @typedef {object} ToolInvocation * @property {string} id - A unique identifier for the tool invocation. * @property {string} displayName - The display name of the tool. * @property {string} name - The name of the tool. * @property {string} parameters - The parameters for the tool invocation. * @property {string} result - The result of the tool invocation. */ /** * @typedef {object} ToolInvocationResult * @property {ToolInvocation[]} invocations Successful tool invocations * @property {Error[]} errors Errors that occurred during tool invocation */ /** * @typedef {object} ToolRegistration * @property {string} name - The name of the tool. * @property {string} displayName - The display name of the tool. * @property {string} description - A description of the tool. * @property {object} parameters - The parameters for the tool. * @property {function} action - The action to perform when the tool is invoked. * @property {function} [formatMessage] - A function to format the tool call message. * @property {function} [shouldRegister] - A function to determine if the tool should be registered. */ /** * @typedef {object} ToolDefinitionOpenAI * @property {string} type - The type of the tool. * @property {object} function - The function definition. * @property {string} function.name - The name of the function. * @property {string} function.description - The description of the function. * @property {object} function.parameters - The parameters of the function. * @property {function} toString - A function to convert the tool to a string. */ /** * Assigns nested variables to a scope. * @param {import('./slash-commands/SlashCommandScope.js').SlashCommandScope} scope The scope to assign variables to. * @param {object} arg Object to assign variables from. * @param {string} prefix Prefix for the variable names. */ function assignNestedVariables(scope, arg, prefix) { Object.entries(arg).forEach(([key, value]) => { const newPrefix = `${prefix}.${key}`; if (typeof value === 'object' && value !== null) { assignNestedVariables(scope, value, newPrefix); } else { scope.letVariable(newPrefix, value); } }); } /** * Checks if a string is a valid JSON string. * @param {string} str The string to check * @returns {boolean} If the string is a valid JSON string */ function isJson(str) { try { JSON.parse(str); return true; } catch { return false; } } /** * Tries to parse a string as JSON, returning the original string if parsing fails. * @param {string} str The string to try to parse * @returns {object|string} Parsed JSON or the original string */ function tryParse(str) { try { return JSON.parse(str); } catch { return str; } } /** * Stringifies an object if it is not already a string. * @param {any} obj The object to stringify * @returns {string} A JSON string representation of the object. */ function stringify(obj) { return typeof obj === 'string' ? obj : JSON.stringify(obj); } /** * A class that represents a tool definition. */ class ToolDefinition { /** * A unique name for the tool. * @type {string} */ #name; /** * A user-friendly display name for the tool. * @type {string} */ #displayName; /** * A description of what the tool does. * @type {string} */ #description; /** * A JSON schema for the parameters that the tool accepts. * @type {object} */ #parameters; /** * A function that will be called when the tool is executed. * @type {function} */ #action; /** * A function that will be called to format the tool call toast. * @type {function} */ #formatMessage; /** * A function that will be called to determine if the tool should be registered. * @type {function} */ #shouldRegister; /** * Creates a new ToolDefinition. * @param {string} name A unique name for the tool. * @param {string} displayName A user-friendly display name for the tool. * @param {string} description A description of what the tool does. * @param {object} parameters A JSON schema for the parameters that the tool accepts. * @param {function} action A function that will be called when the tool is executed. * @param {function} formatMessage A function that will be called to format the tool call toast. * @param {function} shouldRegister A function that will be called to determine if the tool should be registered. */ constructor(name, displayName, description, parameters, action, formatMessage, shouldRegister) { this.#name = name; this.#displayName = displayName; this.#description = description; this.#parameters = parameters; this.#action = action; this.#formatMessage = formatMessage; this.#shouldRegister = shouldRegister; } /** * Converts the ToolDefinition to an OpenAI API representation * @returns {ToolDefinitionOpenAI} OpenAI API representation of the tool. */ toFunctionOpenAI() { return { type: 'function', function: { name: this.#name, description: this.#description, parameters: this.#parameters, }, toString: function () { return `
${this.function.name}
${this.function.description}
${JSON.stringify(this.function.parameters, null, 2)}

`; }, }; } /** * Invokes the tool with the given parameters. * @param {object} parameters The parameters to pass to the tool. * @returns {Promise} The result of the tool's action function. */ async invoke(parameters) { return await this.#action(parameters); } /** * Formats a message with the tool invocation. * @param {object} parameters The parameters to pass to the tool. * @returns {Promise} The formatted message. */ async formatMessage(parameters) { return typeof this.#formatMessage === 'function' ? await this.#formatMessage(parameters) : `Invoking tool: ${this.#displayName || this.#name}`; } async shouldRegister() { return typeof this.#shouldRegister === 'function' ? await this.#shouldRegister() : true; } get displayName() { return this.#displayName; } } /** * A class that manages the registration and invocation of tools. */ export class ToolManager { /** * A map of tool names to tool definitions. * @type {Map} */ static #tools = new Map(); static #INPUT_DELTA_KEY = '__input_json_delta'; /** * The maximum number of times to recurse when parsing tool calls. * @type {number} */ static RECURSE_LIMIT = 5; /** * Returns an Array of all tools that have been registered. * @type {ToolDefinition[]} */ static get tools() { return Array.from(this.#tools.values()); } /** * Registers a new tool with the tool registry. * @param {ToolRegistration} tool The tool to register. */ static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage, shouldRegister }) { // Convert WIP arguments if (typeof arguments[0] !== 'object') { [name, description, parameters, action] = arguments; } if (this.#tools.has(name)) { console.warn(`[ToolManager] A tool with the name "${name}" has already been registered. The definition will be overwritten.`); } const definition = new ToolDefinition(name, displayName, description, parameters, action, formatMessage, shouldRegister); this.#tools.set(name, definition); console.log('[ToolManager] Registered function tool:', definition); } /** * Removes a tool from the tool registry. * @param {string} name The name of the tool to unregister. */ static unregisterFunctionTool(name) { if (!this.#tools.has(name)) { return; } this.#tools.delete(name); console.log(`[ToolManager] Unregistered function tool: ${name}`); } /** * Invokes a tool by name. Returns the result of the tool's action function. * @param {string} name The name of the tool to invoke. * @param {object} parameters Function parameters. For example, if the tool requires a "name" parameter, you would pass {name: "value"}. * @returns {Promise} The result of the tool's action function. If an error occurs, null is returned. Non-string results are JSON-stringified. */ static async invokeFunctionTool(name, parameters) { try { if (!this.#tools.has(name)) { throw new Error(`No tool with the name "${name}" has been registered.`); } const invokeParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters; const tool = this.#tools.get(name); const result = await tool.invoke(invokeParameters); return typeof result === 'string' ? result : JSON.stringify(result); } catch (error) { console.error(`[ToolManager] An error occurred while invoking the tool "${name}":`, error); if (error instanceof Error) { error.cause = name; return error.toString(); } return new Error('Unknown error occurred while invoking the tool.', { cause: name }).toString(); } } /** * Formats a message for a tool call by name. * @param {string} name The name of the tool to format the message for. * @param {object} parameters Function tool call parameters. * @returns {Promise} The formatted message for the tool call. */ static async formatToolCallMessage(name, parameters) { if (!this.#tools.has(name)) { return `Invoked unknown tool: ${name}`; } try { const tool = this.#tools.get(name); const formatParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters; return await tool.formatMessage(formatParameters); } catch (error) { console.error(`[ToolManager] An error occurred while formatting the tool call message for "${name}":`, error); return `Invoking tool: ${name}`; } } /** * Gets the display name of a tool by name. * @param {string} name * @returns {string} The display name of the tool. */ static getDisplayName(name) { if (!this.#tools.has(name)) { return name; } const tool = this.#tools.get(name); return tool.displayName || name; } /** * Register function tools for the next chat completion request. * @param {object} data Generation data */ static async registerFunctionToolsOpenAI(data) { const tools = []; for (const tool of ToolManager.tools) { const register = await tool.shouldRegister(); if (!register) { console.log('[ToolManager] Skipping tool registration:', tool); continue; } tools.push(tool.toFunctionOpenAI()); } if (tools.length) { console.log('[ToolManager] Registered function tools:', tools); data['tools'] = tools; data['tool_choice'] = 'auto'; } } /** * Utility function to parse tool calls from a parsed response. * @param {any[]} toolCalls The tool calls to update. * @param {any} parsed The parsed response from the OpenAI API. * @returns {void} */ static parseToolCalls(toolCalls, parsed) { if (!this.isToolCallingSupported()) { return; } if (Array.isArray(parsed?.choices)) { for (const choice of parsed.choices) { const choiceIndex = (typeof choice.index === 'number') ? choice.index : null; const choiceDelta = choice.delta; if (choiceIndex === null || !choiceDelta) { continue; } const toolCallDeltas = choiceDelta?.tool_calls; if (!Array.isArray(toolCallDeltas)) { continue; } if (!Array.isArray(toolCalls[choiceIndex])) { toolCalls[choiceIndex] = []; } for (const toolCallDelta of toolCallDeltas) { const toolCallIndex = (typeof toolCallDelta?.index === 'number') ? toolCallDelta.index : toolCallDeltas.indexOf(toolCallDelta); if (isNaN(toolCallIndex) || toolCallIndex < 0) { continue; } if (toolCalls[choiceIndex][toolCallIndex] === undefined) { toolCalls[choiceIndex][toolCallIndex] = {}; } const targetToolCall = toolCalls[choiceIndex][toolCallIndex]; ToolManager.#applyToolCallDelta(targetToolCall, toolCallDelta); } } } const cohereToolEvents = ['message-start', 'tool-call-start', 'tool-call-delta', 'tool-call-end']; if (cohereToolEvents.includes(parsed?.type) && typeof parsed?.delta?.message === 'object') { const choiceIndex = 0; const toolCallIndex = parsed?.index ?? 0; if (!Array.isArray(toolCalls[choiceIndex])) { toolCalls[choiceIndex] = []; } if (toolCalls[choiceIndex][toolCallIndex] === undefined) { toolCalls[choiceIndex][toolCallIndex] = {}; } const targetToolCall = toolCalls[choiceIndex][toolCallIndex]; ToolManager.#applyToolCallDelta(targetToolCall, parsed.delta.message); } if (typeof parsed?.content_block === 'object') { const choiceIndex = 0; const toolCallIndex = parsed?.index ?? 0; if (parsed?.content_block?.type === 'tool_use') { if (!Array.isArray(toolCalls[choiceIndex])) { toolCalls[choiceIndex] = []; } if (toolCalls[choiceIndex][toolCallIndex] === undefined) { toolCalls[choiceIndex][toolCallIndex] = {}; } const targetToolCall = toolCalls[choiceIndex][toolCallIndex]; ToolManager.#applyToolCallDelta(targetToolCall, parsed.content_block); } } if (typeof parsed?.delta === 'object') { const choiceIndex = 0; const toolCallIndex = parsed?.index ?? 0; const targetToolCall = toolCalls[choiceIndex]?.[toolCallIndex]; if (targetToolCall) { if (parsed?.delta?.type === 'input_json_delta') { const jsonDelta = parsed?.delta?.partial_json; if (!targetToolCall[this.#INPUT_DELTA_KEY]) { targetToolCall[this.#INPUT_DELTA_KEY] = ''; } targetToolCall[this.#INPUT_DELTA_KEY] += jsonDelta; } } } if (parsed?.type === 'content_block_stop') { const choiceIndex = 0; const toolCallIndex = parsed?.index ?? 0; const targetToolCall = toolCalls[choiceIndex]?.[toolCallIndex]; if (targetToolCall) { const jsonDeltaString = targetToolCall[this.#INPUT_DELTA_KEY]; if (jsonDeltaString) { try { const jsonDelta = { input: JSON.parse(jsonDeltaString) }; delete targetToolCall[this.#INPUT_DELTA_KEY]; ToolManager.#applyToolCallDelta(targetToolCall, jsonDelta); } catch (error) { console.warn('[ToolManager] Failed to apply input JSON delta:', error); } } } } } /** * Apply a tool call delta to a target object. * @param {object} target The target object to apply the delta to * @param {object} delta The delta object to apply */ static #applyToolCallDelta(target, delta) { for (const key in delta) { if (!Object.prototype.hasOwnProperty.call(delta, key)) continue; if (key === '__proto__' || key === 'constructor') continue; const deltaValue = delta[key]; const targetValue = target[key]; if (deltaValue === null || deltaValue === undefined) { target[key] = deltaValue; continue; } if (typeof deltaValue === 'string') { if (typeof targetValue === 'string') { // Concatenate strings target[key] = targetValue + deltaValue; } else { target[key] = deltaValue; } } else if (typeof deltaValue === 'object' && !Array.isArray(deltaValue)) { if (typeof targetValue !== 'object' || targetValue === null || Array.isArray(targetValue)) { target[key] = {}; } // Recursively apply deltas to nested objects ToolManager.#applyToolCallDelta(target[key], deltaValue); } else { // Assign other types directly target[key] = deltaValue; } } } /** * Checks if tool calling is supported for the current settings and generation type. * @returns {boolean} Whether tool calling is supported for the given type */ static isToolCallingSupported() { if (main_api !== 'openai' || !oai_settings.function_calling) { return false; } const supportedSources = [ chat_completion_sources.OPENAI, chat_completion_sources.CUSTOM, chat_completion_sources.MISTRALAI, chat_completion_sources.CLAUDE, chat_completion_sources.OPENROUTER, chat_completion_sources.GROQ, chat_completion_sources.COHERE, ]; return supportedSources.includes(oai_settings.chat_completion_source); } /** * Checks if tool calls can be performed for the current settings and generation type. * @param {string} type Generation type * @returns {boolean} Whether tool calls can be performed for the given type */ static canPerformToolCalls(type) { const noToolCallTypes = ['impersonate', 'quiet', 'continue']; const isSupported = ToolManager.isToolCallingSupported(); return isSupported && !noToolCallTypes.includes(type); } /** * Utility function to get tool calls from the response data. * @param {any} data Response data * @returns {any[]} Tool calls from the response data */ static #getToolCallsFromData(data) { const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id; const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } }); // Parsed tool calls from streaming data if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) { if (isClaudeToolCall(data[0])) { return data[0].filter(x => x).map(convertClaudeToolCall); } if (typeof data[0]?.[0]?.tool_calls === 'object') { return Array.isArray(data[0]?.[0]?.tool_calls) ? data[0][0].tool_calls : [data[0][0].tool_calls]; } return data[0]; } // Parsed tool calls from non-streaming data if (Array.isArray(data?.choices)) { // Find a choice with 0-index const choice = data.choices.find(choice => choice.index === 0); if (choice) { return choice.message.tool_calls; } } // Claude tool calls to OpenAI tool calls if (Array.isArray(data?.content)) { const content = data.content.filter(c => c.type === 'tool_use').map(convertClaudeToolCall); if (content) { return content; } } // Cohere tool calls if (typeof data?.message?.tool_calls === 'object') { return Array.isArray(data?.message?.tool_calls) ? data.message.tool_calls : [data.message.tool_calls]; } } /** * Checks if the response data contains tool calls. * @param {object} data Response data * @returns {boolean} Whether the response data contains tool calls */ static hasToolCalls(data) { const toolCalls = ToolManager.#getToolCallsFromData(data); return Array.isArray(toolCalls) && toolCalls.length > 0; } /** * Check for function tool calls in the response data and invoke them. * @param {any} data Reply data * @returns {Promise} Successful tool invocations */ static async invokeFunctionTools(data) { /** @type {ToolInvocationResult} */ const result = { invocations: [], errors: [], }; const toolCalls = ToolManager.#getToolCallsFromData(data); if (!Array.isArray(toolCalls)) { return result; } for (const toolCall of toolCalls) { if (typeof toolCall.function !== 'object') { continue; } console.log('[ToolManager] Function tool call:', toolCall); const id = toolCall.id; const parameters = toolCall.function.arguments; const name = toolCall.function.name; const displayName = ToolManager.getDisplayName(name); const message = await ToolManager.formatToolCallMessage(name, parameters); const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 }); const toolResult = await ToolManager.invokeFunctionTool(name, parameters); toastr.clear(toast); console.log('[ToolManager] Function tool result:', result); // Save a successful invocation if (toolResult instanceof Error) { result.errors.push(toolResult); continue; } const invocation = { id, displayName, name, parameters: stringify(parameters), result: toolResult, }; result.invocations.push(invocation); } return result; } /** * Groups tool names by count. * @param {string[]} toolNames Tool names * @returns {string} Grouped tool names */ static #groupToolNames(toolNames) { const toolCounts = toolNames.reduce((acc, name) => { acc[name] = (acc[name] || 0) + 1; return acc; }, {}); return Object.entries(toolCounts).map(([name, count]) => count > 1 ? `${name} (${count})` : name).join(', '); } /** * Formats a message with tool invocations. * @param {ToolInvocation[]} invocations Tool invocations. * @returns {string} Formatted message with tool invocations. */ static #formatToolInvocationMessage(invocations) { const data = structuredClone(invocations); const detailsElement = document.createElement('details'); const summaryElement = document.createElement('summary'); const preElement = document.createElement('pre'); const codeElement = document.createElement('code'); codeElement.classList.add('language-json'); data.forEach(i => { i.parameters = tryParse(i.parameters); i.result = tryParse(i.result); }); codeElement.textContent = JSON.stringify(data, null, 2); const toolNames = data.map(i => i.displayName || i.name); summaryElement.textContent = `Tool calls: ${this.#groupToolNames(toolNames)}`; preElement.append(codeElement); detailsElement.append(summaryElement, preElement); return detailsElement.outerHTML; } /** * Saves function tool invocations to the last user chat message extra metadata. * @param {ToolInvocation[]} invocations Successful tool invocations */ static async saveFunctionToolInvocations(invocations) { if (!Array.isArray(invocations) || invocations.length === 0) { return; } const message = { name: systemUserName, force_avatar: system_avatar, is_system: true, is_user: false, mes: ToolManager.#formatToolInvocationMessage(invocations), extra: { isSmallSys: true, tool_invocations: invocations, }, }; chat.push(message); await eventSource.emit(event_types.TOOL_CALLS_PERFORMED, invocations); addOneMessage(message); await eventSource.emit(event_types.TOOL_CALLS_RENDERED, invocations); await saveChatConditional(); } /** * Shows an error message for tool calls. * @param {Error[]} errors Errors that occurred during tool invocation * @returns {void} */ static showToolCallError(errors) { toastr.error('An error occurred while invoking function tools. Click here for more details.', 'Tool Calling', { onclick: () => Popup.show.text('Tool Calling Errors', DOMPurify.sanitize(errors.map(e => `${e.cause}: ${e.message}`).join('
'))), timeOut: 5000, }); } static initToolSlashCommands() { const toolsEnumProvider = () => ToolManager.tools.map(tool => { const toolOpenAI = tool.toFunctionOpenAI(); return new SlashCommandEnumValue(toolOpenAI.function.name, toolOpenAI.function.description, enumTypes.enum, enumIcons.closure); }); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'tools-list', aliases: ['tool-list'], helpString: 'Gets a list of all registered tools in the OpenAI function JSON format. Use the return argument to specify the return value type.', returns: 'A list of all registered tools.', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'return', description: 'The way how you want the return value to be provided', typeList: [ARGUMENT_TYPE.STRING], defaultValue: 'none', enumList: slashCommandReturnHelper.enumList({ allowObject: true }), forceEnum: true, }), ], callback: async (args) => { /** @type {any} */ const returnType = String(args?.return ?? 'popup-html').trim().toLowerCase(); const objectToStringFunc = (tools) => Array.isArray(tools) ? tools.map(x => x.toString()).join('\n\n') : tools.toString(); const tools = ToolManager.tools.map(tool => tool.toFunctionOpenAI()); return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', tools ?? [], { objectToStringFunc }); }, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'tools-invoke', aliases: ['tool-invoke'], helpString: 'Invokes a registered tool by name. The parameters argument MUST be a JSON-serialized object.', namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'parameters', description: 'The parameters to pass to the tool.', typeList: [ARGUMENT_TYPE.DICTIONARY], isRequired: true, acceptsMultiple: false, }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'The name of the tool to invoke.', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, acceptsMultiple: false, forceEnum: true, enumProvider: toolsEnumProvider, }), ], callback: async (args, name) => { const { parameters } = args; const result = await ToolManager.invokeFunctionTool(String(name), parameters); if (result instanceof Error) { throw result; } return result; }, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'tools-register', aliases: ['tool-register'], helpString: `
Registers a new tool with the tool registry.
  • The parameters argument MUST be a JSON-serialized object with a valid JSON schema.
  • The unnamed argument MUST be a closure that accepts the function parameters as local script variables.
See json-schema.org and OpenAI Function Calling for more information.
Example:
/let key=echoSchema
{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "properties": {
        "message": {
            "type": "string",
            "description": "The message to echo."
        }
    },
    "required": [
        "message"
    ]
}
||
/tools-register name=Echo description="Echoes a message. Call when the user is asking to repeat something" parameters={{var::echoSchema}} {: /echo {{var::arg.message}} :}
`, namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'name', description: 'The name of the tool.', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, acceptsMultiple: false, }), SlashCommandNamedArgument.fromProps({ name: 'description', description: 'A description of what the tool does.', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, acceptsMultiple: false, }), SlashCommandNamedArgument.fromProps({ name: 'parameters', description: 'The parameters for the tool.', typeList: [ARGUMENT_TYPE.DICTIONARY], isRequired: true, acceptsMultiple: false, }), SlashCommandNamedArgument.fromProps({ name: 'displayName', description: 'The display name of the tool.', typeList: [ARGUMENT_TYPE.STRING], isRequired: false, acceptsMultiple: false, }), SlashCommandNamedArgument.fromProps({ name: 'formatMessage', description: 'The closure to be executed to format the tool call message. Must return a string.', typeList: [ARGUMENT_TYPE.CLOSURE], isRequired: true, acceptsMultiple: false, }), SlashCommandNamedArgument.fromProps({ name: 'shouldRegister', description: 'The closure to be executed to determine if the tool should be registered. Must return a boolean.', typeList: [ARGUMENT_TYPE.CLOSURE], isRequired: false, acceptsMultiple: false, }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'The closure to be executed when the tool is invoked.', typeList: [ARGUMENT_TYPE.CLOSURE], isRequired: true, acceptsMultiple: false, }), ], callback: async (args, action) => { /** * Converts a slash command closure to a function. * @param {SlashCommandClosure} action Closure to convert to a function * @param {function(any): any} convertResult Function to convert the result * @returns {function} Function that executes the closure */ function closureToFunction(action, convertResult) { return async (args) => { const localClosure = action.getCopy(); localClosure.onProgress = () => { }; const scope = localClosure.scope; if (typeof args === 'object' && args !== null) { assignNestedVariables(scope, args, 'arg'); } else if (typeof args !== 'undefined') { scope.letVariable('arg', args); } const result = await localClosure.execute(); return convertResult(result.pipe); }; } const { name, displayName, description, parameters, formatMessage, shouldRegister } = args; if (!(action instanceof SlashCommandClosure)) { throw new Error('The unnamed argument must be a closure.'); } if (typeof name !== 'string' || !name) { throw new Error('The "name" argument must be a non-empty string.'); } if (typeof description !== 'string' || !description) { throw new Error('The "description" argument must be a non-empty string.'); } if (typeof parameters !== 'string' || !isJson(parameters)) { throw new Error('The "parameters" argument must be a JSON-serialized object.'); } if (displayName && typeof displayName !== 'string') { throw new Error('The "displayName" argument must be a string.'); } if (formatMessage && !(formatMessage instanceof SlashCommandClosure)) { throw new Error('The "formatMessage" argument must be a closure.'); } if (shouldRegister && !(shouldRegister instanceof SlashCommandClosure)) { throw new Error('The "shouldRegister" argument must be a closure.'); } const actionFunc = closureToFunction(action, x => x); const formatMessageFunc = formatMessage instanceof SlashCommandClosure ? closureToFunction(formatMessage, x => String(x)) : null; const shouldRegisterFunc = shouldRegister instanceof SlashCommandClosure ? closureToFunction(shouldRegister, x => isTrueBoolean(x)) : null; ToolManager.registerFunctionTool({ name: String(name ?? ''), displayName: String(displayName ?? ''), description: String(description ?? ''), parameters: JSON.parse(parameters ?? '{}'), action: actionFunc, formatMessage: formatMessageFunc, shouldRegister: shouldRegisterFunc, }); return ''; }, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'tools-unregister', aliases: ['tool-unregister'], helpString: 'Unregisters a tool from the tool registry.', unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'The name of the tool to unregister.', typeList: [ARGUMENT_TYPE.STRING], isRequired: true, acceptsMultiple: false, forceEnum: true, enumProvider: toolsEnumProvider, }), ], callback: async (name) => { if (typeof name !== 'string' || !name) { throw new Error('The unnamed argument must be a non-empty string.'); } ToolManager.unregisterFunctionTool(name); return ''; }, })); } }