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:
Félix Malfait
2026-03-30 18:19:25 +02:00
committed by GitHub
parent ca00e8dece
commit 8fa3962e1c
23 changed files with 983 additions and 283 deletions
@@ -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,
],
@@ -17,4 +17,5 @@ export const MESSAGE_QUEUE_PRIORITY = {
[MessageQueue.deleteCascadeQueue]: 6,
[MessageQueue.cronQueue]: 7,
[MessageQueue.aiQueue]: 5,
[MessageQueue.aiStreamQueue]: 2,
};
@@ -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 },
};
@@ -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],
);
}
}