🔄 refactor: Sequential Event Ordering in Redis Streaming Mode (#11650)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

* chore: linting image context file

* refactor: Event Emission with Async Handling for Redis Ordering

- Updated emitEvent and related functions to be async, ensuring proper event ordering in Redis mode.
- Refactored multiple handlers to await emitEvent calls, improving reliability for streaming deltas.
- Enhanced GenerationJobManager to await chunk emissions, critical for maintaining sequential event delivery.
- Added tests to verify that events are delivered in strict order when using Redis, addressing previous issues with out-of-order messages.

* refactor: Clear Pending Buffers and Timeouts in RedisEventTransport

- Enhanced the cleanup process in RedisEventTransport by ensuring that pending messages and flush timeouts are cleared when the last subscriber unsubscribes.
- Updated the destroy method to also clear pending messages and flush timeouts for all streams, improving resource management and preventing memory leaks.

* refactor: Update Event Emission to Async for Improved Ordering

- Refactored GenerationJobManager and RedisEventTransport to make emitDone and emitError methods async, ensuring proper event ordering in Redis mode.
- Updated all relevant calls to await these methods, enhancing reliability in event delivery.
- Adjusted tests to verify that events are processed in the correct sequence, addressing previous issues with out-of-order messages.

* refactor: Adjust RedisEventTransport for 0-Indexed Sequence Handling

- Updated sequence handling in RedisEventTransport to be 0-indexed, ensuring consistency across event emissions and buffer management.
- Modified integration tests to reflect the new sequence logic, improving the accuracy of event processing and delivery order.
- Enhanced comments for clarity on sequence management and terminal event handling.

* chore: Add Redis dump file to .gitignore

- Included dump.rdb in .gitignore to prevent accidental commits of Redis database dumps, enhancing repository cleanliness and security.

* test: Increase wait times in RedisEventTransport integration tests for CI stability

- Adjusted wait times for subscription establishment and event propagation from 100ms and 200ms to 500ms to improve reliability in CI environments.
- Enhanced code readability by formatting promise resolution lines for better clarity.
This commit is contained in:
Danny Avila 2026-02-05 17:57:33 +01:00 committed by GitHub
parent 46624798b6
commit feb72ad2dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1032 additions and 116 deletions

View file

@ -152,13 +152,15 @@ function checkIfLastAgent(last_agent_id, langgraph_node) {
/**
* Helper to emit events either to res (standard mode) or to job emitter (resumable mode).
* In Redis mode, awaits the emit to guarantee event ordering (critical for streaming deltas).
* @param {ServerResponse} res - The server response object
* @param {string | null} streamId - The stream ID for resumable mode, or null for standard mode
* @param {Object} eventData - The event data to send
* @returns {Promise<void>}
*/
function emitEvent(res, streamId, eventData) {
async function emitEvent(res, streamId, eventData) {
if (streamId) {
GenerationJobManager.emitChunk(streamId, eventData);
await GenerationJobManager.emitChunk(streamId, eventData);
} else {
sendEvent(res, eventData);
}
@ -206,18 +208,18 @@ function getDefaultHandlers({
* @param {StreamEventData} data - The event data.
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
*/
handle: (event, data, metadata) => {
handle: async (event, data, metadata) => {
if (data?.stepDetails.type === StepTypes.TOOL_CALLS) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
} else if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
} else if (!metadata?.hide_sequential_outputs) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
} else {
const agentName = metadata?.name ?? 'Agent';
const isToolCall = data?.stepDetails.type === StepTypes.TOOL_CALLS;
const action = isToolCall ? 'performing a task...' : 'thinking...';
emitEvent(res, streamId, {
await emitEvent(res, streamId, {
event: 'on_agent_update',
data: {
runId: metadata?.run_id,
@ -235,13 +237,13 @@ function getDefaultHandlers({
* @param {StreamEventData} data - The event data.
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
*/
handle: (event, data, metadata) => {
handle: async (event, data, metadata) => {
if (data?.delta.type === StepTypes.TOOL_CALLS) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
} else if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
} else if (!metadata?.hide_sequential_outputs) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
}
aggregateContent({ event, data });
},
@ -253,13 +255,13 @@ function getDefaultHandlers({
* @param {StreamEventData & { result: ToolEndData }} data - The event data.
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
*/
handle: (event, data, metadata) => {
handle: async (event, data, metadata) => {
if (data?.result != null) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
} else if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
} else if (!metadata?.hide_sequential_outputs) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
}
aggregateContent({ event, data });
},
@ -271,11 +273,11 @@ function getDefaultHandlers({
* @param {StreamEventData} data - The event data.
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
*/
handle: (event, data, metadata) => {
handle: async (event, data, metadata) => {
if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
} else if (!metadata?.hide_sequential_outputs) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
}
aggregateContent({ event, data });
},
@ -287,11 +289,11 @@ function getDefaultHandlers({
* @param {StreamEventData} data - The event data.
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
*/
handle: (event, data, metadata) => {
handle: async (event, data, metadata) => {
if (checkIfLastAgent(metadata?.last_agent_id, metadata?.langgraph_node)) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
} else if (!metadata?.hide_sequential_outputs) {
emitEvent(res, streamId, { event, data });
await emitEvent(res, streamId, { event, data });
}
aggregateContent({ event, data });
},
@ -307,6 +309,7 @@ function getDefaultHandlers({
/**
* Helper to write attachment events either to res or to job emitter.
* Note: Attachments are not order-sensitive like deltas, so fire-and-forget is acceptable.
* @param {ServerResponse} res - The server response object
* @param {string | null} streamId - The stream ID for resumable mode, or null for standard mode
* @param {Object} attachment - The attachment data