merlyn/server/utils/chats/agents.js
Timothy Carambat f395083978
Automatic mode for workspace (Agent mode default) (#5143)
* Add automatic chat mode with native tool calling support

    Introduces a new automatic chat mode (now the default) that automatically invokes tools when the provider supports native tool calling. Conditionally shows/hides the @agent command based on whether native tooling is available.

    - Add supportsNativeToolCalling() to AI providers (OpenAI, Anthropic, Azure always support; others opt-in via ENV)
    - Update all locale translations with new mode descriptions
    - Enhance translator to preserve Trans component tags
    - Remove deprecated ability tags UI

* rebase translations

* WIP on image attachments. Supports initial image attachment + subsequent attachments

* persist images

* Image attachments and updates for providers

* desktop pre-change

* always show command on failure

* add back gemini streaming detection

* move provider native tooling flag to Provider func

* whoops - forgot to delete

* strip "@agent" from prompts to prevent weird replies

* translations for automatic-mode (#5145)

* translations for automatic-mode

* rebase

* translations

* lint

* fix dead translations

* change default for now to chat mode just for rollout

* remove pfp for workspace

* passthrough workspace for showAgentCommand detection and rendering

* Agent API automatic mode support

* ephemeral attachments passthrough

* support reading of pinned documents in agent context
2026-03-18 12:35:43 -07:00

116 lines
3.3 KiB
JavaScript

const pluralize = require("pluralize");
const {
WorkspaceAgentInvocation,
} = require("../../models/workspaceAgentInvocation");
const { writeResponseChunk } = require("../helpers/chat/responses");
const { Workspace } = require("../../models/workspace");
/**
* In-memory cache for attachments associated with agent invocations.
* Attachments are stored here when grepAgents creates an invocation,
* then retrieved by AgentHandler when the websocket connects.
* @type {Map<string, Array>}
*/
const invocationAttachmentsCache = new Map();
/**
* Store attachments for an invocation UUID
* @param {string} uuid - The invocation UUID
* @param {Array} attachments - The attachments array
*/
function cacheInvocationAttachments(uuid, attachments = []) {
if (attachments.length > 0) {
invocationAttachmentsCache.set(uuid, attachments);
}
}
/**
* Retrieve and remove attachments for an invocation UUID
* @param {string} uuid - The invocation UUID
* @returns {Array} The attachments array (empty if none cached)
*/
function getAndClearInvocationAttachments(uuid) {
const attachments = invocationAttachmentsCache.get(uuid) || [];
invocationAttachmentsCache.delete(uuid);
return attachments;
}
async function grepAgents({
uuid,
response,
message,
workspace,
user = null,
thread = null,
attachments = [],
}) {
let nativeToolingEnabled = false;
// If the workspace is in automatic mode, check if the workspace supports native tooling
// to determine if the agent flow should be used or not.
if (workspace?.chatMode === "automatic")
nativeToolingEnabled = await Workspace.supportsNativeToolCalling(workspace);
const agentHandles = WorkspaceAgentInvocation.parseAgents(message);
if (agentHandles.length > 0 || nativeToolingEnabled) {
const { invocation: newInvocation } = await WorkspaceAgentInvocation.new({
prompt: message,
workspace: workspace,
user: user,
thread: thread,
});
if (!newInvocation) {
writeResponseChunk(response, {
id: uuid,
type: "statusResponse",
textResponse: `${pluralize(
"Agent",
agentHandles.length
)} ${agentHandles.join(
", "
)} could not be called. Chat will be handled as default chat.`,
sources: [],
close: true,
animate: false,
error: null,
});
return;
}
// Cache attachments for the websocket handler to retrieve later
cacheInvocationAttachments(newInvocation.uuid, attachments);
writeResponseChunk(response, {
id: uuid,
type: "agentInitWebsocketConnection",
textResponse: null,
sources: [],
close: false,
error: null,
websocketUUID: newInvocation.uuid,
});
// Close HTTP stream-able chunk response method because we will swap to agents now.
writeResponseChunk(response, {
id: uuid,
type: "statusResponse",
textResponse: `${pluralize(
"Agent",
agentHandles.length
)} ${agentHandles.join(
", "
)} invoked.\nSwapping over to agent chat. Type /exit to exit agent execution loop early.`,
sources: [],
close: true,
error: null,
animate: true,
});
return true;
}
return false;
}
module.exports = { grepAgents, getAndClearInvocationAttachments };