Skip to content

Commit 2b0f355

Browse files
Aayush-engineerabhiaiyer91Mastra Code (anthropic/claude-opus-4-7)
authored
fix(core): preserve chunk id and runId in BatchPartsProcessor when batching text-deltas (#14974)
Co-authored-by: Abhi Aiyer <abhiaiyer91@gmail.com> Co-authored-by: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
1 parent 549d2cc commit 2b0f355

3 files changed

Lines changed: 63 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@mastra/core": patch
3+
---
4+
5+
Fixed BatchPartsProcessor using a hardcoded id in batched text-delta chunks. The real message id and runId are now preserved from the original chunks, preventing AI SDK UIMessage stream from dropping batched deltas.

packages/core/src/processors/processors/batch-parts.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,60 @@ describe('BatchPartsProcessor', () => {
120120
payload: { text: 'ABCDE', id: 'text-1' },
121121
});
122122
});
123+
124+
it('should preserve id and runId from the first chunk when batching (regression #14890)', async () => {
125+
processor = new BatchPartsProcessor({ batchSize: 3 });
126+
127+
const chunks: ChunkType[] = [
128+
{
129+
type: 'text-delta',
130+
payload: { text: 'Hello', id: 'msg_abc123' },
131+
runId: 'run_xyz789',
132+
from: ChunkFrom.AGENT,
133+
},
134+
{
135+
type: 'text-delta',
136+
payload: { text: ' ', id: 'msg_abc123' },
137+
runId: 'run_xyz789',
138+
from: ChunkFrom.AGENT,
139+
},
140+
{
141+
type: 'text-delta',
142+
payload: { text: 'world', id: 'msg_abc123' },
143+
runId: 'run_xyz789',
144+
from: ChunkFrom.AGENT,
145+
},
146+
];
147+
148+
const state: BatchPartsState = { batch: [], timeoutId: undefined, timeoutTriggered: false };
149+
150+
for (let i = 0; i < 2; i++) {
151+
await processor.processOutputStream({
152+
part: chunks[i]!,
153+
streamParts: chunks.slice(0, i),
154+
state,
155+
abort: () => {
156+
throw new Error('abort');
157+
},
158+
});
159+
}
160+
161+
const result = await processor.processOutputStream({
162+
part: chunks[2]!,
163+
streamParts: chunks.slice(0, 2),
164+
state,
165+
abort: () => {
166+
throw new Error('abort');
167+
},
168+
});
169+
170+
expect(result).toEqual({
171+
type: 'text-delta',
172+
runId: 'run_xyz789',
173+
from: ChunkFrom.AGENT,
174+
payload: { text: 'Hello world', id: 'msg_abc123' },
175+
});
176+
});
123177
});
124178

125179
describe('non-text chunks', () => {

packages/core/src/processors/processors/batch-parts.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,12 @@ export class BatchPartsProcessor implements Processor<'batch-parts'> {
136136
// Combine all text deltas
137137
const combinedText = textChunks.map(part => (part.type === 'text-delta' ? part.payload.text : '')).join('');
138138

139-
// Create a new combined text part
139+
// Create a new combined text part, preserving id and runId from the first chunk
140+
const firstChunk = textChunks[0] as ChunkType & { type: 'text-delta' };
140141
const combinedChunk: ChunkType = {
141142
type: 'text-delta',
142-
payload: { text: combinedText, id: 'text-1' },
143-
runId: '1',
143+
payload: { text: combinedText, id: firstChunk.payload.id },
144+
runId: firstChunk.runId,
144145
from: ChunkFrom.AGENT,
145146
};
146147

0 commit comments

Comments
 (0)