n8n
Version:
n8n Workflow Automation Tool
282 lines • 11.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentChatStreamConsumer = void 0;
const is_record_1 = require("@n8n/utils/is-record");
const agent_chat_suspension_cards_1 = require("./agent-chat-suspension-cards");
const createResponseState = () => ({
hasVisibleResponse: false,
suppressText: false,
fallbackSource: null,
fallbackError: null,
});
class AgentChatStreamConsumer {
constructor(options) {
this.options = options;
}
async consume(stream, thread, options = {}) {
if (this.options.disableStreaming || options.forceBuffered) {
await this.consumeBuffered(stream, thread, {
statusHandle: options.statusHandle,
});
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(async (postError) => {
await this.options.postErrorToThread(thread, postError);
this.options.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();
};
const responseLifecycle = this.createResponseLifecycle({
statusHandle: options.statusHandle,
ensureStreamingPost,
endStreamingPost,
});
const responseState = createResponseState();
try {
for await (const chunk of stream) {
switch (chunk.type) {
case 'text-delta': {
if (responseState.suppressText)
break;
const { delta } = chunk;
await responseLifecycle.startStreamingResponse();
textStream.yield?.(delta);
if (delta.trim())
responseState.hasVisibleResponse = true;
break;
}
case 'tool-call-suspended': {
await responseLifecycle.startDiscreteResponse();
const result = await this.options.handleSuspension(chunk, thread);
responseState.hasVisibleResponse ||= result === 'posted';
if (result === 'failed') {
responseState.fallbackSource = 'suspension';
responseState.fallbackError = new Error('Failed to post tool approval request');
}
break;
}
case 'message':
await responseLifecycle.startDiscreteResponse();
responseState.hasVisibleResponse ||= await this.options.handleMessage(chunk, thread);
break;
case 'error':
await responseLifecycle.startDiscreteResponse();
await this.options.postErrorToThread(thread, chunk.error);
responseState.hasVisibleResponse = true;
break;
case 'tool-result':
if (chunk.isError) {
responseState.fallbackSource = 'tool-error';
responseState.fallbackError = chunk.output;
}
else if (responseState.fallbackSource === 'tool-error') {
responseState.fallbackSource = null;
}
if (this.isSilentOutcome(chunk))
responseState.suppressText = true;
break;
default:
break;
}
}
await this.postFallbackIfNeeded(responseState, responseLifecycle, thread);
}
finally {
await responseLifecycle.finish();
}
}
isSilentOutcome(chunk) {
if (chunk.isError || !(this.options.isIntegrationActionTool?.(chunk.toolName) ?? false)) {
return false;
}
if (!(0, is_record_1.isRecord)(chunk.output))
return false;
if (chunk.output.silent === true)
return true;
return (Array.isArray(chunk.output.results) &&
chunk.output.results.some((entry) => (0, is_record_1.isRecord)(entry) &&
entry.action === 'do_not_respond' &&
(0, is_record_1.isRecord)(entry.result) &&
entry.result.ok === true &&
entry.result.silent === true));
}
createResponseLifecycle(options) {
let responseStarted = false;
const clearStatusBeforeFirstResponse = async () => {
if (responseStarted)
return;
responseStarted = true;
await options.statusHandle?.clearBeforeResponse();
};
return {
startStreamingResponse: async () => {
await clearStatusBeforeFirstResponse();
options.ensureStreamingPost?.();
},
startDiscreteResponse: async () => {
await options.endStreamingPost?.();
await clearStatusBeforeFirstResponse();
},
finish: async () => {
await options.endStreamingPost?.();
await clearStatusBeforeFirstResponse();
},
};
}
async postFallbackIfNeeded(state, lifecycle, thread) {
if (!state.fallbackSource)
return;
if (state.fallbackSource === 'tool-error' && state.hasVisibleResponse)
return;
await lifecycle.startDiscreteResponse();
await this.options.postErrorToThread(thread, state.fallbackError);
state.hasVisibleResponse = true;
}
async consumeBuffered(stream, thread, options = {}) {
let buffer = '';
const responseState = createResponseState();
const responseLifecycle = this.createResponseLifecycle({
statusHandle: options.statusHandle,
});
const flushBuffer = async () => {
const text = buffer;
buffer = '';
if (!text.trim())
return;
try {
await responseLifecycle.startDiscreteResponse();
await thread.post({ markdown: text });
}
catch (postError) {
await this.options.postErrorToThread(thread, postError);
this.options.logger.error('[AgentChatBridge] Buffered post failed', {
error: postError instanceof Error ? postError.message : String(postError),
});
}
responseState.hasVisibleResponse = true;
};
try {
for await (const chunk of stream) {
switch (chunk.type) {
case 'text-delta':
if (!responseState.suppressText)
buffer += chunk.delta;
break;
case 'tool-call-suspended': {
if ((0, agent_chat_suspension_cards_1.isIntegrationActionSuspendPayload)(chunk.suspendPayload)) {
buffer = '';
}
else {
await flushBuffer();
}
await responseLifecycle.startDiscreteResponse();
const result = await this.options.handleSuspension(chunk, thread);
responseState.hasVisibleResponse ||= result === 'posted';
if (result === 'failed') {
responseState.fallbackSource = 'suspension';
responseState.fallbackError = new Error('Failed to post tool approval request');
}
break;
}
case 'message':
await flushBuffer();
await responseLifecycle.startDiscreteResponse();
responseState.hasVisibleResponse ||= await this.options.handleMessage(chunk, thread);
break;
case 'error':
await flushBuffer();
await responseLifecycle.startDiscreteResponse();
await this.options.postErrorToThread(thread, chunk.error);
responseState.hasVisibleResponse = true;
break;
case 'tool-result':
if (chunk.isError) {
responseState.fallbackSource = 'tool-error';
responseState.fallbackError = chunk.output;
}
else if (responseState.fallbackSource === 'tool-error') {
responseState.fallbackSource = null;
}
if (this.isSilentOutcome(chunk)) {
responseState.suppressText = true;
buffer = '';
}
break;
default:
break;
}
}
await flushBuffer();
await this.postFallbackIfNeeded(responseState, responseLifecycle, thread);
}
finally {
await flushBuffer();
await responseLifecycle.finish();
}
}
}
exports.AgentChatStreamConsumer = AgentChatStreamConsumer;
//# sourceMappingURL=agent-chat-stream-consumer.js.map