Tool Calling: Implement stealth tool defintions (#3192) * Tool Calling: Implement stealth tool defintions * Move isStealth check up * Always stop generation on stealth tool calls * Image Generation: use stealth flag for tool registration * Update stealth property description to clarify no follow-up generation will be performed * Revert "Image Generation: use stealth flag for tool registration" This reverts commit 8d13445c0b66e4c0ef1ddcfaf18ab185464de600.

7e7b3e30c481e128d78c1e6391ecebc6e230ba6e

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +68 -9Ignore whitespace
public/script.js+10 -4
@@ -4579,9 +4579,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
45794579 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor?.result);
45804580 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
45814581 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);
4582+ const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
45824583 if (hasToolCalls) {
45834584 if (!invocationResult.invocations.length && shouldDeleteMessageshouldStopGeneration) {
4584- ToolManager.showToolCallError(invocationResult.errors);
4585+ if (Array.isArray(invocationResult.errors) && invocationResult.errors.length) {
4586+ ToolManager.showToolCallError(invocationResult.errors);
4587+ }
45854588 unblockGeneration(type);
45864589 generatedPromptCache = '';
45874590 streamingProcessor = null;
@@ -4681,9 +4684,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
46814684 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage);
46824685 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
46834686 const invocationResult = await ToolManager.invokeFunctionTools(data);
4687+ const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
46844688 if (hasToolCalls) {
46854689 if (!invocationResult.invocations.length && shouldDeleteMessageshouldStopGeneration) {
4686- ToolManager.showToolCallError(invocationResult.errors);
4690+ if (Array.isArray(invocationResult.errors) && invocationResult.errors.length) {
4691+ ToolManager.showToolCallError(invocationResult.errors);
4692+ }
46874693 unblockGeneration(type);
46884694 generatedPromptCache = '';
46894695 return;
public/scripts/tool-calling.js+58 -5
@@ -25,6 +25,7 @@ import { isTrueBoolean } from './utils.js';
2525 * @typedef {object} ToolInvocationResult
2626 * @property {ToolInvocation[]} invocations Successful tool invocations
2727 * @property {Error[]} errors Errors that occurred during tool invocation
28+ * @property {string[]} stealthCalls Names of stealth tools that were invoked
2829 */
2930
3031/**
@@ -36,6 +37,7 @@ import { isTrueBoolean } from './utils.js';
3637 * @property {function} action - The action to perform when the tool is invoked.
3738 * @property {function} [formatMessage] - A function to format the tool call message.
3839 * @property {function} [shouldRegister] - A function to determine if the tool should be registered.
40+ * @property {boolean} [stealth] - A tool call result will not be shown in the chat. No follow-up generation will be performed.
3941 */
4042
4143/**
@@ -148,6 +150,12 @@ class ToolDefinition {
148150 #shouldRegister;
149151
150152 /**
153+ * A tool call result will not be shown in the chat. No follow-up generation will be performed.
154+ * @type {boolean}
155+ */
156+ #stealth;
157+
158+ /**
151159 * Creates a new ToolDefinition.
152160 * @param {string} name A unique name for the tool.
153161 * @param {string} displayName A user-friendly display name for the tool.
@@ -156,8 +164,9 @@ class ToolDefinition {
156164 * @param {function} action A function that will be called when the tool is executed.
157165 * @param {function} formatMessage A function that will be called to format the tool call toast.
158166 * @param {function} shouldRegister A function that will be called to determine if the tool should be registered.
167+ * @param {boolean} stealth A tool call result will not be shown in the chat. No follow-up generation will be performed.
159168 */
160169 constructor(name, displayName, description, parameters, action, formatMessage, shouldRegister, stealth) {
161170 this.#name = name;
162171 this.#displayName = displayName;
163172 this.#description = description;
@@ -165,6 +174,7 @@ class ToolDefinition {
165174 this.#action = action;
166175 this.#formatMessage = formatMessage;
167176 this.#shouldRegister = shouldRegister;
177+ this.#stealth = stealth;
168178 }
169179
170180 /**
@@ -214,6 +224,10 @@ class ToolDefinition {
214224 get displayName() {
215225 return this.#displayName;
216226 }
227+
228+ get stealth() {
229+ return this.#stealth;
230+ }
217231}
218232
219233/**
@@ -246,7 +260,7 @@ export class ToolManager {
246260 * Registers a new tool with the tool registry.
247261 * @param {ToolRegistration} tool The tool to register.
248262 */
249263 static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage, shouldRegister, stealth }) {
250264 // Convert WIP arguments
251265 if (typeof arguments[0] !== 'object') {
252266 [name, description, parameters, action] = arguments;
@@ -256,7 +270,16 @@ export class ToolManager {
256270 console.warn(`[ToolManager] A tool with the name "${name}" has already been registered. The definition will be overwritten.`);
257271 }
258272
259273 const definition = new ToolDefinition(name, displayName, description, parameters, action, formatMessage, shouldRegister);
274+ name,
275+ displayName,
276+ description,
277+ parameters,
278+ action,
279+ formatMessage,
280+ shouldRegister,
281+ stealth,
282+ );
260283 this.#tools.set(name, definition);
261284 console.log('[ToolManager] Registered function tool:', definition);
262285 }
@@ -303,6 +326,20 @@ export class ToolManager {
303326 }
304327
305328 /**
329+ * Checks if a tool is a stealth tool.
330+ * @param {string} name The name of the tool to check.
331+ * @returns {boolean} Whether the tool is a stealth tool.
332+ */
333+ static isStealthTool(name) {
334+ if (!this.#tools.has(name)) {
335+ return false;
336+ }
337+
338+ const tool = this.#tools.get(name);
339+ return !!tool.stealth;
340+ }
341+
342+ /**
306343 * Formats a message for a tool call by name.
307344 * @param {string} name The name of the tool to format the message for.
308345 * @param {object} parameters Function tool call parameters.
@@ -608,6 +645,7 @@ export class ToolManager {
608645 const result = {
609646 invocations: [],
610647 errors: [],
648+ stealthCalls: [],
611649 };
612650 const toolCalls = ToolManager.#getToolCallsFromData(data);
613651
@@ -625,7 +663,7 @@ export class ToolManager {
625663 const parameters = toolCall.function.arguments;
626664 const name = toolCall.function.name;
627665 const displayName = ToolManager.getDisplayName(name);
628-
666+ const isStealth = ToolManager.isStealthTool(name);
629667 const message = await ToolManager.formatToolCallMessage(name, parameters);
630668 const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
631669 const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
@@ -638,6 +676,12 @@ export class ToolManager {
638676 continue;
639677 }
640678
679+ // Don't save stealth tool invocations
680+ if (isStealth) {
681+ result.stealthCalls.push(name);
682+ continue;
683+ }
684+
641685 const invocation = {
642686 id,
643687 displayName,
@@ -860,6 +904,14 @@ export class ToolManager {
860904 isRequired: false,
861905 acceptsMultiple: false,
862906 }),
907+ SlashCommandNamedArgument.fromProps({
908+ name: 'stealth',
909+ description: 'If true, a tool call result will not be shown in the chat and no follow-up generation will be performed.',
910+ typeList: [ARGUMENT_TYPE.BOOLEAN],
911+ isRequired: false,
912+ acceptsMultiple: false,
913+ defaultValue: String(false),
914+ }),
863915 ],
864916 unnamedArgumentList: [
865917 SlashCommandArgument.fromProps({
@@ -891,7 +943,7 @@ export class ToolManager {
891943 };
892944 }
893945
894946 const { name, displayName, description, parameters, formatMessage, shouldRegister, stealth } = args;
895947
896948 if (!(action instanceof SlashCommandClosure)) {
897949 throw new Error('The unnamed argument must be a closure.');
@@ -927,6 +979,7 @@ export class ToolManager {
927979 action: actionFunc,
928980 formatMessage: formatMessageFunc,
929981 shouldRegister: shouldRegisterFunc,
982+ stealth: stealth && isTrueBoolean(String(stealth)),
930983 });
931984
932985 return '';