n8n
Version:
n8n Workflow Automation Tool
540 lines • 20.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentChatBridge = void 0;
const di_1 = require("@n8n/di");
const agent_chat_integration_1 = require("./agent-chat-integration");
const callback_store_1 = require("./callback-store");
const types_1 = require("./types");
class AgentChatBridge {
constructor(chat, agentId, agentService, componentMapper, logger, n8nProjectId, integrationType) {
this.chat = chat;
this.agentId = agentId;
this.agentService = agentService;
this.componentMapper = componentMapper;
this.logger = logger;
this.n8nProjectId = n8nProjectId;
this.integrationType = integrationType;
this.activeResumedRuns = new Set();
this.richInteractionInputs = new Map();
this.integration = di_1.Container.get(agent_chat_integration_1.ChatIntegrationRegistry).get(integrationType);
if (this.integration?.needsShortCallbackData) {
this.callbackStore = new callback_store_1.CallbackStore();
}
this.disableStreaming = this.integration?.disableStreaming ?? false;
this.registerHandlers();
}
static create(chat, agentId, agentService, componentMapper, logger, n8nProjectId, integrationType) {
const agentExecutor = {
async *executeForChatPublished({ memory, agentId: aid, message, integrationType }) {
yield* agentService.executeForChatPublished({
agentId: aid,
projectId: n8nProjectId,
message,
memory: { threadId: memory.threadId.id, resourceId: memory.resourceId },
integrationType,
});
},
async *resumeForChat(config) {
yield* agentService.resumeForChat(config);
},
};
return new AgentChatBridge(chat, agentId, agentExecutor, componentMapper, logger, n8nProjectId, integrationType);
}
registerHandlers() {
this.chat.onNewMention(async (thread, message) => {
try {
await thread.subscribe();
await this.executeAndStream(thread, message);
}
catch (error) {
await this.postErrorToThread(thread, error);
}
});
this.chat.onSubscribedMessage(async (thread, message) => {
try {
await this.executeAndStream(thread, message);
}
catch (error) {
await this.postErrorToThread(thread, error);
}
});
this.chat.onAction(async (event) => {
try {
await this.handleAction(event);
}
catch (error) {
await this.postErrorToThread(event.thread, error);
}
});
}
dispose() {
this.callbackStore?.dispose();
}
resolveThreadId(thread) {
return (0, types_1.toInternalThreadId)(this.integration?.formatThreadId?.fromSdk(thread) ?? thread.id);
}
getShortenCallback() {
if (!this.callbackStore)
return undefined;
const store = this.callbackStore;
return async (actionId, value) => {
const key = await store.store(actionId, value);
return { id: key, value: '' };
};
}
async executeAndStream(thread, message) {
const text = message.text?.trim();
if (!text)
return;
const threadId = this.resolveThreadId(thread);
const stream = this.agentService.executeForChatPublished({
agentId: this.agentId,
projectId: this.n8nProjectId,
message: text,
memory: { threadId, resourceId: message.author.userId },
integrationType: this.integrationType,
});
await this.consumeStream(stream, thread);
}
async consumeStream(stream, thread) {
if (this.disableStreaming) {
await this.consumeStreamBuffered(stream, thread);
return;
}
const textStream = {
yield: null,
end: null,
};
let streamingPost = null;
const createTextIterable = () => {
const queue = [];
let done = false;
let waiting = null;
textStream.yield = (text) => {
if (waiting) {
const resolve = waiting;
waiting = null;
resolve({ value: text, done: false });
}
else {
queue.push(text);
}
};
textStream.end = () => {
done = true;
if (waiting) {
const resolve = waiting;
waiting = null;
resolve({ value: '', done: true });
}
};
return {
[Symbol.asyncIterator]() {
return {
async next() {
if (queue.length > 0) {
return { value: queue.shift(), done: false };
}
if (done) {
return { value: '', done: true };
}
return await new Promise((resolve) => {
waiting = resolve;
});
},
};
},
};
};
const startStreamingPost = () => {
const iterable = createTextIterable();
streamingPost = thread.post(iterable).catch((postError) => {
this.logger.error('[AgentChatBridge] Streaming post failed', {
error: postError instanceof Error ? postError.message : String(postError),
});
});
};
const endStreamingPost = async () => {
if (textStream.end) {
textStream.end();
textStream.end = null;
textStream.yield = null;
}
if (streamingPost) {
await streamingPost;
streamingPost = null;
}
};
const ensureStreamingPost = () => {
if (!streamingPost)
startStreamingPost();
};
for await (const chunk of stream) {
switch (chunk.type) {
case 'text-delta': {
const { delta } = chunk;
ensureStreamingPost();
textStream.yield?.(delta);
break;
}
case 'reasoning-delta': {
const { delta } = chunk;
ensureStreamingPost();
textStream.yield?.(`_${delta}_`);
break;
}
case 'tool-call':
this.stashRichInteractionInput(chunk);
break;
case 'tool-call-suspended':
this.richInteractionInputs.delete(chunk.toolCallId);
await endStreamingPost();
await this.handleSuspension(chunk, thread);
break;
case 'tool-result':
if (this.isRichInteractionDisplayOnly(chunk)) {
await endStreamingPost();
await this.handleDisplayOnly(chunk, thread);
}
else {
this.richInteractionInputs.delete(chunk.toolCallId);
}
break;
case 'message':
await endStreamingPost();
await this.handleMessage(chunk, thread);
break;
case 'error':
await endStreamingPost();
await this.postErrorToThread(thread, chunk.error);
break;
default:
break;
}
}
await endStreamingPost();
}
async consumeStreamBuffered(stream, thread) {
let buffer = '';
const flushBuffer = async () => {
const text = buffer;
buffer = '';
if (!text.trim())
return;
try {
await thread.post({ markdown: text });
}
catch (postError) {
await this.postErrorToThread(thread, postError);
this.logger.error('[AgentChatBridge] Buffered post failed', {
error: postError instanceof Error ? postError.message : String(postError),
});
}
};
for await (const chunk of stream) {
switch (chunk.type) {
case 'text-delta':
buffer += chunk.delta;
break;
case 'reasoning-delta':
buffer += `_${chunk.delta}_`;
break;
case 'tool-call':
this.stashRichInteractionInput(chunk);
break;
case 'tool-call-suspended':
this.richInteractionInputs.delete(chunk.toolCallId);
await flushBuffer();
await this.handleSuspension(chunk, thread);
break;
case 'tool-result':
if (this.isRichInteractionDisplayOnly(chunk)) {
await flushBuffer();
await this.handleDisplayOnly(chunk, thread);
}
else {
this.richInteractionInputs.delete(chunk.toolCallId);
}
break;
case 'message':
await flushBuffer();
await this.handleMessage(chunk, thread);
break;
case 'error':
await flushBuffer();
await this.postErrorToThread(thread, chunk.error);
break;
default:
break;
}
}
await flushBuffer();
}
async handleSuspension(chunk, thread) {
const { runId, toolCallId, suspendPayload } = chunk;
if (!runId || !toolCallId) {
this.logger.warn('[AgentChatBridge] Suspended chunk missing runId or toolCallId');
return;
}
if (chunk.toolName === 'rich_interaction') {
await this.handleRichInteraction(chunk, thread);
return;
}
const payload = suspendPayload;
const hasComponents = payload &&
'components' in payload &&
Array.isArray(payload.components) &&
payload.components.length > 0;
let cardPayload;
if (hasComponents) {
cardPayload = payload;
}
else {
const message = payload && typeof payload === 'object' && 'message' in payload
? String(payload.message)
: 'Action required — approve or deny?';
cardPayload = {
title: message,
components: [
{ type: 'button', label: 'Approve', value: 'true', style: 'primary' },
{ type: 'button', label: 'Deny', value: 'false', style: 'danger' },
],
};
}
try {
const card = await this.componentMapper.toCard(cardPayload, runId, toolCallId, chunk.resumeSchema, this.getShortenCallback(), this.integrationType);
await thread.post({ card });
}
catch (error) {
this.logger.error('[AgentChatBridge] Failed to post suspension card', {
agentId: this.agentId,
runId,
toolCallId,
error: error instanceof Error ? error.message : String(error),
});
}
}
async handleRichInteraction(chunk, thread) {
const { runId, toolCallId, suspendPayload } = chunk;
const payload = suspendPayload;
if (!payload?.components?.length) {
this.logger.warn('[AgentChatBridge] rich_interaction has no components');
return;
}
const riResumeSchema = {
type: 'object',
properties: {
type: { type: 'string' },
value: { type: 'string' },
},
};
try {
const card = await this.componentMapper.toCard(payload, runId, toolCallId, riResumeSchema, this.getShortenCallback(), this.integrationType);
await thread.post(card);
}
catch (error) {
this.logger.error('[AgentChatBridge] Failed to post rich interaction card', {
error: error instanceof Error ? error.message : String(error),
});
}
}
stashRichInteractionInput(chunk) {
if (chunk.toolName !== 'rich_interaction')
return;
this.richInteractionInputs.set(chunk.toolCallId, chunk.input);
}
isRichInteractionDisplayOnly(chunk) {
if (chunk.toolName !== 'rich_interaction')
return false;
const out = chunk.output;
return (typeof out === 'object' &&
out !== null &&
'displayOnly' in out &&
out.displayOnly === true);
}
async handleDisplayOnly(chunk, thread) {
const { toolCallId } = chunk;
const input = this.richInteractionInputs.get(toolCallId);
this.richInteractionInputs.delete(toolCallId);
const cardPayload = input;
if (!cardPayload?.components?.length) {
this.logger.warn('[AgentChatBridge] display-only rich_interaction has no components', {
toolCallId,
});
return;
}
const displayResumeSchema = {
type: 'object',
properties: { type: { type: 'string' }, value: { type: 'string' } },
};
try {
const card = await this.componentMapper.toCard(cardPayload, '', toolCallId, displayResumeSchema, this.getShortenCallback(), this.integrationType);
await thread.post({ card });
}
catch (error) {
this.logger.error('[AgentChatBridge] Failed to post display card', {
error: error instanceof Error ? error.message : String(error),
});
}
}
async handleMessage(chunk, thread) {
const agentMessage = chunk.message;
if (!('content' in agentMessage) || !Array.isArray(agentMessage.content))
return;
const textParts = agentMessage.content
.filter((part) => part.type === 'text' && 'text' in part)
.map((part) => part.text);
const textToPost = textParts.join('');
if (!textToPost.trim())
return;
try {
await thread.post(textToPost);
}
catch (error) {
this.logger.error('[AgentChatBridge] Failed to post message chunk', {
agentId: this.agentId,
threadId: thread.id,
error: error instanceof Error ? error.message : String(error),
});
}
}
parseActionId(actionId, value) {
if (actionId.startsWith('ri-btn:')) {
const parts = actionId.split(':');
if (parts.length < 4) {
this.logger.warn('[AgentChatBridge] Malformed ri-btn action ID', { actionId });
return null;
}
let resumeData;
try {
resumeData = JSON.parse(value ?? '');
}
catch {
resumeData = { type: 'button', value };
}
return { runId: parts[1], toolCallId: parts.slice(2, -1).join(':'), resumeData };
}
if (actionId.startsWith('ri-sel:')) {
const parts = actionId.split(':');
if (parts.length < 4) {
this.logger.warn('[AgentChatBridge] Malformed ri-sel action ID', { actionId });
return null;
}
return {
runId: parts[2],
toolCallId: parts.slice(3).join(':'),
resumeData: { type: 'select', id: parts[1], value },
};
}
if (actionId.startsWith('resume:')) {
const parts = actionId.split(':');
if (parts.length < 4) {
this.logger.warn('[AgentChatBridge] Malformed action ID', { actionId });
return null;
}
let resumeData;
try {
resumeData = JSON.parse(value ?? '');
}
catch {
resumeData = { value };
}
return { runId: parts[1], toolCallId: parts.slice(2, -1).join(':'), resumeData };
}
return null;
}
async resolveCallbackData(actionId, value, thread) {
if (!this.callbackStore)
return { actionId, value };
const resolved = await this.callbackStore.resolve(actionId);
if (!resolved) {
this.logger.warn('[AgentChatBridge] Callback key not found or expired', { actionId });
await thread.post('This action is no longer available. The link may have expired or already been used.');
return null;
}
return { actionId: resolved.actionId, value: resolved.value };
}
async cleanUpBeforeResume(event) {
try {
await event.adapter.deleteMessage(event.threadId, event.messageId);
}
catch (deleteError) {
this.logger.warn('[AgentChatBridge] Failed to delete card message', {
error: deleteError instanceof Error ? deleteError.message : String(deleteError),
});
}
const threadInternal = event.thread;
if (threadInternal?._currentMessage?.raw) {
const raw = threadInternal._currentMessage.raw;
if (raw.team && typeof raw.team === 'object' && !raw.team_id) {
raw.team_id = raw.team.id;
}
}
}
async executeResume(thread, runId, toolCallId, resumeData) {
if (this.activeResumedRuns.has(runId)) {
this.logger.warn('[AgentChatBridge] Run is already active', { runId, toolCallId });
await thread.post('This action has already been handled');
return;
}
this.activeResumedRuns.add(runId);
try {
const stream = this.agentService.resumeForChat({
agentId: this.agentId,
projectId: this.n8nProjectId,
runId,
toolCallId,
resumeData,
integrationType: this.integrationType,
});
await this.consumeStream(stream, thread);
}
finally {
this.activeResumedRuns.delete(runId);
}
}
async handleAction(event) {
const { thread } = event;
if (!thread) {
this.logger.warn('[AgentChatBridge] Thread is not set for event', {
threadId: event.threadId,
actionId: event.actionId,
});
return;
}
const callbackData = await this.resolveCallbackData(event.actionId, event.value, thread);
if (!callbackData)
return;
const parsed = this.parseActionId(callbackData.actionId, callbackData.value);
if (!parsed)
return;
await this.cleanUpBeforeResume(event);
await this.executeResume(thread, parsed.runId, parsed.toolCallId, parsed.resumeData);
}
async postErrorToThread(thread, error) {
const message = error instanceof Error ? error.message : 'An unexpected error occurred';
this.logger.error('[AgentChatBridge] Error in handler', {
agentId: this.agentId,
threadId: thread?.id,
error: message,
});
try {
if (!thread) {
this.logger.warn("[AgentChatBridge] Couldn't post error message because thread is not set", {
agentId: this.agentId,
error: message,
});
return;
}
await thread.post('⚠️ Something went wrong while processing your request. Please try again.');
}
catch (postError) {
this.logger.error('[AgentChatBridge] Failed to post error message', {
agentId: this.agentId,
error: postError instanceof Error ? postError.message : String(postError),
});
}
}
}
exports.AgentChatBridge = AgentChatBridge;
//# sourceMappingURL=agent-chat-bridge.js.map