@obayd/agentic
Version:
A powerful agent framework for LLMs.
731 lines (651 loc) • 38.7 kB
JavaScript
// src/Conversation.js
import { FUNCTION_PROMPT, TOOLPACKS_PROMPT } from './prompts.js';
import { makeid, parseAttributes, normalizeToolResult } from './utils.js';
import { Tool } from './Tool.js';
import { Toolpack } from './Toolpack.js';
export class Conversation {
#llmCallback;
#contentDefinition = [];
#options = {};
#invokeArgs = [];
messages = [];
enabledToolpacks = new Set();
#activeToolsMap = new Map();
#allDefinedToolpacksMap = new Map();
#functionTag = "fn";
#resultTag = "rs";
#functionCompleteRegex = null;
#functionStartRegex = null;
#functionEndRegexStr = ""; // Store string for dynamic regex creation
constructor(llmCallback, options = {}) {
if (
typeof llmCallback !== "function" ||
llmCallback.constructor.name !== "AsyncGeneratorFunction"
) {
throw new Error("llmCallback must be an async generator function.");
}
this.#llmCallback = llmCallback;
this.#options = { ...options };
this.enabledToolpacks = new Set(
this.#options.initialEnabledToolpacks || []
);
const tagSuffixLength = 6;
this.#functionTag = `fn_${makeid(tagSuffixLength)}`;
this.#resultTag = `rs_${makeid(tagSuffixLength)}`;
this.#functionEndRegexStr = `<\\/${this.#functionTag}>`;
this.#functionCompleteRegex = new RegExp(
`<${
this.#functionTag
}\\s+callId="([^"]+)"\\s+name="([a-zA-Z0-9_]+)"\\s*([^>]*?)>([\\s\\S]*?)${
this.#functionEndRegexStr
}>`,
"g"
);
this.#functionStartRegex = new RegExp(
`<${
this.#functionTag
}\\s+callId="([^"]+)"\\s+name="([a-zA-Z0-9_]+)"\\s*([^>]*?)>`,
"" // No 'g' flag
);
}
content(definition) {
if (!Array.isArray(definition))
throw new Error("Content definition must be an array.");
this.#contentDefinition = definition;
if (definition.some((item) => item instanceof Toolpack)) {
this.#addEnableToolpackTool();
}
return this;
}
#addEnableToolpackTool() {
const alreadyHasEnableTool = this.#contentDefinition.some(
(item) => item instanceof Tool && item.name === "enable_toolpack"
);
if (!alreadyHasEnableTool) {
const enableTool = Tool.make("enable_toolpack")
.description(
"Enables a toolpack, making its functions available for use in subsequent turns."
)
.param("pack_name", "The exact name of the toolpack to enable.", {
type: "string",
required: true,
})
.action(async (params, conversationInstance) => {
const packName = params?.pack_name;
if (!packName)
return { error: "Missing required parameter 'pack_name'." };
const pack =
conversationInstance.#allDefinedToolpacksMap.get(packName);
if (!pack) return { error: `Toolpack '${packName}' not found.` };
if (conversationInstance.enabledToolpacks.has(packName)) {
return { content: `Toolpack '${packName}' is already enabled.` };
}
conversationInstance.enabledToolpacks.add(packName);
console.log(`[Tool] Toolpack '${packName}' enabled.`);
return {
content: `Toolpack '${packName}' enabled successfully. You can now use its tools.`,
};
});
this.#contentDefinition.push(enableTool);
}
}
async #buildConversationState() {
const activeTools = [];
const allDefinedToolpacks = new Map();
const baseSystemPromptParts = [];
const processItem = async (item) => {
if (typeof item === "string") {
baseSystemPromptParts.push({ type: "text", text: item });
} else if (item instanceof Tool) {
activeTools.push(item);
} else if (item instanceof Toolpack) {
if (!allDefinedToolpacks.has(item.name))
allDefinedToolpacks.set(item.name, item);
else console.warn(`Duplicate toolpack definition: ${item.name}.`);
} else if (typeof item === "function") {
try {
const result = await item(this, ...this.#invokeArgs);
if (Array.isArray(result)) {
for (const resItem of result) {
if (resItem) await processItem(resItem); // Recurse for array results
}
} else if (result) {
await processItem(result); // Recurse for single result
}
} catch (e) {
console.error("Error executing content definition function:", e);
baseSystemPromptParts.push({
type: "text",
text: "[Error Generating Dynamic Content]",
});
}
} else if (typeof item === "object" && item !== null && item.type) {
// Assume it's a valid content part object (e.g., { type: 'image', ... })
baseSystemPromptParts.push(item);
} else if (item !== null && item !== undefined){ // Avoid warning for null/undefined results from functions
console.warn("Unsupported content definition element:", item);
}
};
// Process all items defined in content()
for (const item of this.#contentDefinition) {
await processItem(item);
}
const toolpackDescriptions = [];
allDefinedToolpacks.forEach((tp) => {
const isEnabled = this.enabledToolpacks.has(tp.name);
toolpackDescriptions.push(tp.buildPromptString(isEnabled));
if (isEnabled) activeTools.push(...tp.getTools());
});
const finalActiveToolsMap = new Map();
activeTools.forEach((tool) => {
if (!finalActiveToolsMap.has(tool.name)) {
finalActiveToolsMap.set(tool.name, tool);
} else {
// Handle potential duplicates (e.g., tool defined directly and via enabled pack)
// Prioritize the directly defined one or the first encountered one?
// Current logic: First one encountered wins (direct definition processed first usually).
// console.warn(`Duplicate active tool detected: ${tool.name}. Using first encountered definition.`);
}
});
// --- Assemble the final system prompt content ---
const systemPromptContent = [...baseSystemPromptParts]; // Start with base text/objects
if (finalActiveToolsMap.size > 0) {
const functionsString = Array.from(finalActiveToolsMap.values())
.map((tool) => tool.buildPromptString())
.join("\n\n");
const funcPrompt = FUNCTION_PROMPT.replaceAll(
"%FUNCTAG%", this.#functionTag
).replaceAll(
"%RESULTTAG%", this.#resultTag
).replace(
"%FUNCTIONS%", functionsString.trim() || "No functions available."
);
systemPromptContent.push({ type: "text", text: funcPrompt });
}
if (toolpackDescriptions.length > 0) {
const tpPrompt = TOOLPACKS_PROMPT.replaceAll(
"%FUNCTAG%", this.#functionTag
).replaceAll(
"%RESULTTAG%", this.#resultTag
).replace(
"%TOOLPACKS%", toolpackDescriptions.join("\n").trim() || "No toolpacks defined."
);
systemPromptContent.push({ type: "text", text: tpPrompt });
}
// --- Update internal state ---
this.#activeToolsMap = finalActiveToolsMap;
this.#allDefinedToolpacksMap = allDefinedToolpacks; // Store all *defined* packs
return systemPromptContent; // Return the structured content array
}
#prepareMessagesForLLM(systemContent) {
const history = this.messages;
const llmMessages = [];
// Add the system message first, with its structured content
llmMessages.push({ role: "system", content: systemContent });
for (const msg of history) {
switch (msg.role) {
case "user":
case "assistant":
// Pass user/assistant messages directly
llmMessages.push({ role: msg.role, content: msg.content });
break;
case "tool":
// Format tool result message for the LLM
if (msg.result === null || msg.result === undefined) {
// This might happen if processing failed before result was attached
console.warn(`Tool message for callId ${msg.callId} has null/undefined result.`);
continue; // Skip sending this malformed message back
}
const status = msg.error ? "error" : "success";
let resultString;
try {
// Send the raw result back, not the normalized 'content' field used for history
resultString = JSON.stringify(msg.result);
} catch (e) {
console.error(`Failed to stringify tool result for callId ${msg.callId}:`, msg.result);
resultString = JSON.stringify({
error: "Failed to serialize tool result for LLM.",
});
}
const resultTag = `<${this.#resultTag} callId="${msg.callId}" status="${status}">${resultString}</${this.#resultTag}>`;
// Send tool results back as 'user' role message containing the result tag
llmMessages.push({ role: "user", content: resultTag });
break;
case "error":
// Send internal error messages as user messages for context
llmMessages.push({
role: "user",
content: `[System Error: ${msg.content}]`,
});
break;
// 'system' role is only added at the beginning
}
}
return llmMessages;
}
async *#invokeAndProcess() {
const systemContent = await this.#buildConversationState();
const messagesForLLM = this.#prepareMessagesForLLM(systemContent);
let currentBuffer = "";
let ongoingToolCalls = []; // Stores promises for pending tool executions
let generatingToolCall = null; // State: { callId, name, params, raw, startTagContent }
let accumulatedAssistantContent = "";
// Dynamic end tag regex (case-insensitive)
const functionEndRegex = new RegExp(this.#functionEndRegexStr, "i");
try {
// Assuming llmCallback returns an async generator yielding string chunks
const llmStream = this.#llmCallback(messagesForLLM, { stream: true });
for await (const chunk of llmStream) {
if (typeof chunk !== "string") {
console.warn("LLM stream yielded non-string chunk:", chunk);
continue;
};
currentBuffer += chunk;
// --- State: Currently Parsing/Generating a Tool Call ---
if (generatingToolCall) {
const endMatch = currentBuffer.match(functionEndRegex);
if (endMatch) {
// Found the end tag for the current tool call
const endIndex = endMatch.index; // Index where '</fn_...>' starts
const startTagEndIndex = currentBuffer.indexOf(generatingToolCall.startTagContent) + generatingToolCall.startTagContent.length;
let rawContent = "";
if (endIndex >= 0 && endIndex > startTagEndIndex) {
rawContent = currentBuffer.substring(startTagEndIndex, endIndex);
}
// Yield final raw chunk if needed
const newRawChunk = rawContent.substring(generatingToolCall.raw.length);
if (newRawChunk) {
generatingToolCall.raw = rawContent; // Update total raw *before* yielding final generating event
yield {
type: "tool.generating",
role: "tool.generating", // Maintain consistent role property
callId: generatingToolCall.callId,
name: generatingToolCall.name,
params: generatingToolCall.params,
rawChunk: newRawChunk,
raw: generatingToolCall.raw, // The complete raw content now
};
}
// Yield 'tool.calling' event and add to history/promises
const finalCallId = generatingToolCall.callId;
const finalParams = generatingToolCall.params;
const finalRaw = generatingToolCall.raw || null; // Use null if empty string
const finalName = generatingToolCall.name;
yield {
type: "tool.calling",
role: "tool.calling",
callId: finalCallId,
name: finalName,
params: finalParams,
raw: finalRaw,
};
const toolMessage = {
role: "tool",
callId: finalCallId,
name: finalName,
params: finalParams,
raw: finalRaw,
result: null, // Placeholder for result
content: null, // Placeholder for normalized content
};
this.messages.push(toolMessage);
const tool = this.#activeToolsMap.get(finalName);
const toolPromise = tool
? tool.call(finalParams, finalRaw, [this, ...this.#invokeArgs]) // Pass conversation instance + invokeArgs
: Promise.resolve({ error: `Function '${finalName}' not found or not enabled.` }); // Handle missing tool immediately
ongoingToolCalls.push(
toolPromise
.then((result) => ({ callId: finalCallId, result }))
.catch((error) => {
// Catch errors *during* the tool.call promise execution
console.error(`Critical error during tool.call for ${finalName} (${finalCallId}):`, error);
return {
callId: finalCallId,
result: { error: `Tool execution critically failed: ${error.message}` }
};
})
);
// Reset state and buffer
currentBuffer = currentBuffer.substring(endIndex + endMatch[0].length);
generatingToolCall = null;
// IMPORTANT: After finding a tool call, restart processing the remaining buffer
// This handles cases where assistant text follows a tool call immediately.
continue; // Go to the next iteration of the main loop
} else {
// End tag not found yet. Check if new raw content has arrived.
// Find the start tag within the current buffer (it should be at index 0 if state is correct)
const startTagIndexInBuffer = currentBuffer.indexOf(generatingToolCall.startTagContent);
if (startTagIndexInBuffer === 0) { // Verify it's still at the start
const potentialRaw = currentBuffer.substring(generatingToolCall.startTagContent.length);
if (potentialRaw.length > generatingToolCall.raw.length) {
const newRawChunk = potentialRaw.substring(generatingToolCall.raw.length);
// Yield the generating event, but DON'T update generatingToolCall.raw yet.
// We only update the final raw content once the end tag is confirmed.
yield {
type: "tool.generating",
role: "tool.generating",
callId: generatingToolCall.callId,
name: generatingToolCall.name,
params: generatingToolCall.params,
rawChunk: newRawChunk,
raw: generatingToolCall.raw + newRawChunk, // Show projected total raw
};
}
} else {
// This case should ideally not happen if logic is sound, but indicates a parsing issue.
console.warn("[WARN] Start tag no longer at buffer start while generating tool call. Buffer:", currentBuffer);
// Potentially treat buffer as text and reset generatingToolCall? Or just wait?
// For now, we wait, assuming more chunks might resolve it.
}
}
}
// --- State: Not Currently Parsing a Tool Call ---
else if (currentBuffer) {
// Use the non-global regex here
const startMatch = currentBuffer.match(this.#functionStartRegex);
if (startMatch) {
// Found a potential start tag
const startIndex = startMatch.index;
const fullStartTag = startMatch[0];
const callId = startMatch[1];
const toolName = startMatch[2];
const attrs = startMatch[3];
// --- Yield preceding text ---
if (startIndex > 0) {
const precedingText = currentBuffer.substring(0, startIndex);
yield { type: "assistant", role: "assistant", content: precedingText };
accumulatedAssistantContent += precedingText;
}
// --- Start processing the tool call ---
let params = {};
try {
params = parseAttributes(attrs);
} catch (e) {
console.error(`[ERROR] Failed to parse attributes for ${toolName}(${callId}): ${attrs}`, e);
// Treat the invalid tag as text? Or yield an error? Yielding as text for now.
const invalidTagAsText = currentBuffer.substring(startIndex, startIndex + fullStartTag.length);
yield { type: "assistant", role: "assistant", content: invalidTagAsText };
accumulatedAssistantContent += invalidTagAsText;
currentBuffer = currentBuffer.substring(startIndex + fullStartTag.length);
continue; // Process rest of buffer
}
// --- Initialize generating state ---
generatingToolCall = {
callId,
name: toolName,
params,
raw: "", // Raw content starts empty
startTagContent: fullStartTag,
};
// Adjust buffer: Remove preceding text, keep the tag and the rest
currentBuffer = currentBuffer.substring(startIndex);
// --- Check if the end tag is *already* in the buffer ---
// Create a temporary buffer *after* the start tag for end tag checking
const contentAfterStartTag = currentBuffer.substring(fullStartTag.length);
const endMatchImmediate = contentAfterStartTag.match(functionEndRegex);
if (endMatchImmediate) {
// Complete tool call found within the current chunk!
const rawContent = contentAfterStartTag.substring(0, endMatchImmediate.index);
generatingToolCall.raw = rawContent; // Set final raw content
// Yield 'generating' if there was raw content
if (rawContent) {
yield {
type: "tool.generating",
role: "tool.generating",
callId: generatingToolCall.callId,
name: generatingToolCall.name,
params: generatingToolCall.params,
rawChunk: rawContent, // The entire raw content is the chunk here
raw: rawContent,
};
}
// Yield 'calling' event
yield {
type: "tool.calling",
role: "tool.calling",
callId: generatingToolCall.callId,
name: generatingToolCall.name,
params: generatingToolCall.params,
raw: generatingToolCall.raw || null, // Use null if empty
};
// Add to history and promises
const toolMessage = {
role: "tool",
callId: generatingToolCall.callId,
name: generatingToolCall.name,
params: generatingToolCall.params,
raw: generatingToolCall.raw || null,
result: null,
content: null,
};
this.messages.push(toolMessage);
const tool = this.#activeToolsMap.get(generatingToolCall.name);
const toolPromise = tool
? tool.call(generatingToolCall.params, generatingToolCall.raw || null, [this, ...this.#invokeArgs])
: Promise.resolve({ error: `Function '${generatingToolCall.name}' not found or not enabled.` });
ongoingToolCalls.push(
toolPromise
.then((result) => ({ callId: generatingToolCall.callId, result }))
.catch((error) => {
console.error(`Critical error during tool.call for ${generatingToolCall.name} (${generatingToolCall.callId}):`, error);
return {
callId: generatingToolCall.callId,
result: { error: `Tool execution critically failed: ${error.message}` }
};
})
);
// Update buffer: remove the entire tool call block
currentBuffer = contentAfterStartTag.substring(endMatchImmediate.index + endMatchImmediate[0].length);
generatingToolCall = null; // Reset state
// Restart processing the remaining buffer
continue;
} else {
// Start tag found, but no end tag *yet*.
// The buffer currently holds: <fn...> potentially_partial_raw_content
const potentialRawChunk = contentAfterStartTag;
// Yield 'generating' event with the initial potential raw chunk
yield {
type: "tool.generating",
role: "tool.generating",
callId: generatingToolCall.callId,
name: generatingToolCall.name,
params: generatingToolCall.params,
rawChunk: potentialRawChunk,
raw: potentialRawChunk, // Raw *so far* is this chunk
};
// Don't update generatingToolCall.raw here, wait for end tag confirmation.
// The buffer remains as is (start tag + potential raw) to check for end tag in next chunks.
}
} else {
// No start tag found in the current buffer. Treat the buffer cautiously.
// To avoid prematurely yielding partial tags, yield content only up to the last '<'.
const lastLtIndex = currentBuffer.lastIndexOf("<");
if (lastLtIndex !== -1) {
// Yield text before the potential partial tag start
const textToYield = currentBuffer.substring(0, lastLtIndex);
if (textToYield) {
yield { type: "assistant", role: "assistant", content: textToYield };
accumulatedAssistantContent += textToYield;
}
// Keep the potential partial tag and anything after it in the buffer
currentBuffer = currentBuffer.substring(lastLtIndex);
} else {
// No '<' found, the entire buffer is likely assistant text.
yield { type: "assistant", role: "assistant", content: currentBuffer };
accumulatedAssistantContent += currentBuffer;
currentBuffer = ""; // Clear buffer
}
}
}
} // End for await (chunk of llmStream)
// --- Stream Ended ---
// Handle incomplete tool call at end of stream
if (generatingToolCall) {
console.warn(
`[WARN] LLM stream ended mid-tool-call for ${generatingToolCall.name}(${generatingToolCall.callId}). Treating as text.`
);
// Yield the incomplete tag and raw content as assistant text
const incompleteText = generatingToolCall.startTagContent + generatingToolCall.raw;
if (incompleteText) {
yield { type: "assistant", role: "assistant", content: incompleteText };
accumulatedAssistantContent += incompleteText;
}
generatingToolCall = null; // Clear the state
}
// Yield any remaining content in the buffer (should be assistant text)
if (currentBuffer) {
yield { type: "assistant", role: "assistant", content: currentBuffer };
accumulatedAssistantContent += currentBuffer;
}
// --- Add final assistant message to history ---
// Only add if there was actual content OR if no tool calls were made (to represent an empty response)
const assistantHasContent = accumulatedAssistantContent && accumulatedAssistantContent.trim().length > 0;
// Check if the *last* turn involved initiating tool calls (check promises generated in *this* invocation)
const hadToolCallsInThisTurn = ongoingToolCalls.length > 0;
if (assistantHasContent) {
// Avoid adding duplicate empty messages if the stream only yielded tool calls
const lastMessage = this.messages[this.messages.length - 1];
if (!(lastMessage?.role === "assistant" && lastMessage?.content === accumulatedAssistantContent)) {
this.messages.push({
role: "assistant",
content: accumulatedAssistantContent,
});
}
} else if (!assistantHasContent && !hadToolCallsInThisTurn) {
// If the LLM responded with nothing (no text, no tool calls), record an empty assistant message.
const lastMessage = this.messages[this.messages.length - 1];
// Add only if the previous message wasn't already an empty assistant message.
if (!(lastMessage?.role === 'assistant' && !lastMessage?.content)) {
this.messages.push({ role: "assistant", content: "" });
}
}
} catch (error) {
console.error("[ERROR] LLM stream or processing failed:", error);
const errorContent = `LLM stream/processing failed: ${error.message}`;
yield {
type: "error",
role: "error", // Use 'error' role for yielded error object
content: errorContent,
};
// Add error to history if not already present
if (!this.messages.some(m => m.role === 'error' && m.content === errorContent)) {
this.messages.push({ role: "error", content: errorContent });
}
generatingToolCall = null; // Ensure state is reset on error
// Do not proceed to tool result processing if the stream failed
return; // Exit the generator
} finally {
// Ensure state is always reset, regardless of errors or normal completion
generatingToolCall = null;
}
// --- Process Tool Results (only if stream completed successfully) ---
if (ongoingToolCalls.length > 0) {
let toolResults = [];
try {
// Wait for all initiated tool calls in this turn to complete
toolResults = await Promise.all(ongoingToolCalls);
} catch (error) {
// This catch is for Promise.all itself rejecting, though individual errors are caught above.
console.error("[ERROR] Unexpected error during Promise.all for tool calls:", error);
// Yield a general error? Maybe rely on individual error reporting.
}
ongoingToolCalls = []; // Clear the promises for this turn
let requiresRecursion = false; // Flag if any tool result needs LLM follow-up
for (const resultWrapper of toolResults) {
// Basic validation of the wrapper structure
if (!resultWrapper || typeof resultWrapper.callId === 'undefined' || typeof resultWrapper.result === 'undefined') {
console.error("[ERROR] Invalid tool result wrapper received:", resultWrapper);
continue;
}
const { callId, result } = resultWrapper;
// Find the corresponding message in history to update
const toolMessageIndex = this.messages.findIndex(
(m) => m.role === "tool" && m.callId === callId && m.result === null // Ensure we only update once
);
if (toolMessageIndex !== -1) {
requiresRecursion = true; // Found a result, LLM needs to see it
const normalized = normalizeToolResult(result);
// Update the history message
this.messages[toolMessageIndex].result = result; // Store raw result
this.messages[toolMessageIndex].content = normalized.content; // Store normalized content for display/history
this.messages[toolMessageIndex].error = normalized.error; // Store potential error from normalization/tool
// Add any other keys from normalized result (that aren't content/error)
Object.keys(normalized).forEach(key => {
if (key !== 'content' && key !== 'error') {
this.messages[toolMessageIndex][key] = normalized[key];
}
});
// Yield the processed tool result message
yield {
type: "tool",
role: "tool", // Use 'tool' role for yielded result object
callId: callId,
name: this.messages[toolMessageIndex].name,
params: this.messages[toolMessageIndex].params,
raw: this.messages[toolMessageIndex].raw,
result: this.messages[toolMessageIndex].result, // Yield raw result
content: this.messages[toolMessageIndex].content, // Yield normalized content
error: this.messages[toolMessageIndex].error, // Yield error status
};
} else {
// This might happen if the stream ended prematurely after the tool call was logged but before results processed.
// Or if the same callId somehow got processed twice.
console.warn(`[WARN] Could not find pending tool message or already processed for callId: ${callId}`);
}
}
// If any tool results were processed, recursively call invokeAndProcess
// to send the results back to the LLM and get the next response.
if (requiresRecursion) {
yield* this.#invokeAndProcess(); // Tail recursion simulation
}
}
} // End #invokeAndProcess
async *send(messageContent, ...args) {
this.#invokeArgs = args; // Store args for potential tool use
let userMessage;
// Normalize incoming message to the Message structure
if (typeof messageContent === "string") {
userMessage = { role: "user", content: messageContent };
} else if (Array.isArray(messageContent)) {
// Assume it's an array of content parts
userMessage = { role: "user", content: messageContent };
} else if (typeof messageContent === "object" && messageContent !== null) {
// Allow passing a pre-formatted message object
if (messageContent.role === "user" && typeof messageContent.content !== 'undefined') {
userMessage = messageContent;
}
// Allow passing a single content part object
else if (messageContent.type && typeof messageContent.content === 'undefined') {
userMessage = { role: "user", content: [messageContent] };
}
else {
console.error("Invalid object passed to send():", messageContent);
throw new Error("Invalid message format. Object must have role='user' and 'content', or be a single content part object.");
}
} else {
throw new Error("Invalid message format. Must be string, array of content parts, or user message object.");
}
this.messages.push(userMessage);
try {
// Start the generation and processing loop
yield* this.#invokeAndProcess();
} catch (error) {
// Catch critical errors from the invoke/processing loop itself
console.error("[ERROR] Critical conversation processing error in send():", error);
const errorContent = `Conversation failed critically: ${error.message}`;
const errorMsg = {
type: "error",
role: "error",
content: errorContent,
};
yield errorMsg; // Yield the error to the consumer
// Add to history if not already there
if (!this.messages.some(m => m.role === 'error' && m.content === errorContent)) {
this.messages.push({ role: "error", content: errorContent });
}
} finally {
// Clean up invoke args after the send operation (including all recursive calls) completes or errors out
this.#invokeArgs = [];
}
}
}