merlyn/server/utils/agents/aibitat/plugins/websocket.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

163 lines
5.0 KiB
JavaScript

const chalk = require("chalk");
const { Telemetry } = require("../../../../models/telemetry");
const SOCKET_TIMEOUT_MS = 300 * 1_000; // 5 mins
/**
* Websocket Interface plugin. It prints the messages on the console and asks for feedback
* while the conversation is running in the background.
*/
// export interface AIbitatWebSocket extends ServerWebSocket<unknown> {
// askForFeedback?: any
// awaitResponse?: any
// handleFeedback?: (message: string) => void;
// }
const WEBSOCKET_BAIL_COMMANDS = [
"exit",
"/exit",
"stop",
"/stop",
"halt",
"/halt",
"/reset", // Will not reset but will bail. Powerusers always do this and the LLM responds.
];
const websocket = {
name: "websocket",
startupConfig: {
params: {
socket: {
required: true,
},
muteUserReply: {
required: false,
default: true,
},
introspection: {
required: false,
default: true,
},
},
},
plugin: function ({
socket, // @type AIbitatWebSocket
muteUserReply = true, // Do not post messages to "USER" back to frontend.
introspection = false, // when enabled will attach socket to Aibitat object with .introspect method which reports status updates to frontend.
}) {
return {
name: this.name,
setup(aibitat) {
aibitat.onError(async (error) => {
let errorMessage =
error?.message || "An error occurred while running the agent.";
console.error(chalk.red(` error: ${errorMessage}`), error);
aibitat.introspect(
`Error encountered while running: ${errorMessage}`
);
socket.send(
JSON.stringify({ type: "wssFailure", content: errorMessage })
);
aibitat.terminate();
});
aibitat.introspect = (messageText) => {
if (!introspection) return; // Dump thoughts when not wanted.
socket.send(
JSON.stringify({
type: "statusResponse",
content: messageText,
animate: true,
})
);
};
// expose function for sockets across aibitat
// type param must be set or else msg will not be shown or handled in UI.
aibitat.socket = {
send: (type = "__unhandled", content = "") => {
socket.send(JSON.stringify({ type, content }));
},
};
// aibitat.onStart(() => {
// console.log("🚀 starting chat ...");
// });
aibitat.onMessage((message) => {
if (message.from !== "USER")
Telemetry.sendTelemetry("agent_chat_sent");
if (message.from === "USER" && muteUserReply) return;
socket.send(JSON.stringify(message));
});
aibitat.onTerminate(() => {
// console.log("🚀 chat finished");
socket.close();
});
aibitat.onInterrupt(async (node) => {
const { feedback, attachments } = await socket.askForFeedback(
socket,
node
);
if (WEBSOCKET_BAIL_COMMANDS.includes(feedback)) {
socket.close();
return;
}
await aibitat.continue(feedback, attachments);
});
/**
* Socket wait for feedback on socket
*
* @param socket The content to summarize. // AIbitatWebSocket & { receive: any, echo: any }
* @param node The chat node // { from: string; to: string }
* @returns {{ feedback: string, attachments: Array }} The feedback and any attachments.
*/
socket.askForFeedback = (socket, node) => {
socket.awaitResponse = (question = "waiting...") => {
socket.send(JSON.stringify({ type: "WAITING_ON_INPUT", question }));
return new Promise(function (resolve) {
let socketTimeout = null;
socket.handleFeedback = (message) => {
const data = JSON.parse(message);
if (data.type !== "awaitingFeedback") return;
delete socket.handleFeedback;
clearTimeout(socketTimeout);
resolve({
feedback: data.feedback,
attachments: data.attachments || [],
});
return;
};
socketTimeout = setTimeout(() => {
console.log(
chalk.red(
`Client took too long to respond, chat thread is dead after ${SOCKET_TIMEOUT_MS}ms`
)
);
resolve({ feedback: "exit", attachments: [] });
return;
}, SOCKET_TIMEOUT_MS);
});
};
return socket.awaitResponse(`Provide feedback to ${chalk.yellow(
node.to
)} as ${chalk.yellow(node.from)}.
Press enter to skip and use auto-reply, or type 'exit' to end the conversation: \n`);
};
// console.log("🚀 WS plugin is complete.");
},
};
},
};
module.exports = {
websocket,
WEBSOCKET_BAIL_COMMANDS,
};