feat: add resumable stream support for agent chat (#19107)
## Overview Add resumable stream support for agent chat to allow clients to reconnect and resume streaming responses if the connection is interrupted (e.g., during page refresh). ## Changes ### Backend (Twenty Server) - Add `activeStreamId` column to `AgentChatThreadEntity` to track ongoing streams - Create `AgentChatResumableStreamService` to manage Redis-backed resumable streams using the `resumable-stream` library with ioredis - Extend `AgentChatController` with: - `GET /:threadId/stream` endpoint to resume an existing stream - `DELETE /:threadId/stream` endpoint to stop an active stream - Update `AgentChatStreamingService` to store streams in Redis and track active stream IDs - Add `resumable-stream@^2.2.12` dependency to package.json ### Frontend (Twenty Front) - Update `useAgentChat` hook to: - Use a persistent transport with `prepareReconnectToStreamRequest` for resumable streams - Export `resumeStream` function from useChat - Add `handleStop` callback to clear active stream on DELETE endpoint - Use thread ID as stable message ID instead of including message count - Add stream resumption logic in `AgentChatAiSdkStreamEffect` component to automatically call `resumeStream()` when switching threads ## Database Migration New migration `1774003611071-add-active-stream-id-to-agent-chat-thread` adds the `activeStreamId` column to store the current resumable stream identifier. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspa
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { AiAgentMonitorModule } from 'src/engine/metadata-modules/ai/ai-agent-monitor/ai-agent-monitor.module';
|
||||
import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
|
||||
@@ -67,6 +68,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
SubscriptionsModule,
|
||||
AuditJobModule,
|
||||
AiAgentMonitorModule,
|
||||
AiChatModule,
|
||||
LogicFunctionModule,
|
||||
EnterpriseModule,
|
||||
],
|
||||
|
||||
+1
@@ -17,4 +17,5 @@ export const MESSAGE_QUEUE_PRIORITY = {
|
||||
[MessageQueue.deleteCascadeQueue]: 6,
|
||||
[MessageQueue.cronQueue]: 7,
|
||||
[MessageQueue.aiQueue]: 5,
|
||||
[MessageQueue.aiStreamQueue]: 2,
|
||||
};
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
|
||||
export const QUEUE_WORKER_OPTIONS: Partial<
|
||||
Record<MessageQueue, { concurrency: number }>
|
||||
> = {
|
||||
[MessageQueue.aiStreamQueue]: { concurrency: 20 },
|
||||
};
|
||||
+1
@@ -19,4 +19,5 @@ export enum MessageQueue {
|
||||
logicFunctionQueue = 'logic-function-queue',
|
||||
triggerQueue = 'trigger-queue',
|
||||
aiQueue = 'ai-queue',
|
||||
aiStreamQueue = 'ai-stream-queue',
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { type MessageQueueWorkerOptions } from 'src/engine/core-modules/message-
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { MessageQueueMetadataAccessor } from 'src/engine/core-modules/message-queue/message-queue-metadata.accessor';
|
||||
import { type MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { type MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { QUEUE_WORKER_OPTIONS } from 'src/engine/core-modules/message-queue/message-queue-worker-options.constant';
|
||||
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
|
||||
import { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util';
|
||||
|
||||
@@ -67,6 +69,7 @@ export class MessageQueueExplorer implements OnModuleInit {
|
||||
this.handleProcessorGroupCollection(
|
||||
processorGroupCollection,
|
||||
messageQueueService,
|
||||
QUEUE_WORKER_OPTIONS[queueName as MessageQueue],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user