Skip to content

Commit e4942bc

Browse files
authored
fix(core): fail fast when token limiter removes all input (#16063)
1 parent 1b55954 commit e4942bc

3 files changed

Lines changed: 72 additions & 1 deletion

File tree

.changeset/two-nights-decide.md

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 TokenLimiterProcessor failing silently when no input messages fit the token budget.

packages/core/src/processors/processors/token-limiter.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
33

44
import type { MastraDBMessage } from '../../agent/message-list';
55
import { MessageList } from '../../agent/message-list';
6+
import { TripWire } from '../../agent/trip-wire';
67
import type { IMastraLogger } from '../../logger';
78
import { ProcessorRunner } from '../../processors/runner';
89
import type { ChunkType } from '../../stream';
@@ -870,6 +871,54 @@ describe('TokenLimiterProcessor', () => {
870871
).rejects.toThrow('System messages alone exceed token limit');
871872
});
872873

874+
it('should throw TripWire when no messages fit within the remaining token budget', async () => {
875+
const processor = new TokenLimiterProcessor({ limit: 25 });
876+
const messageList = new MessageList();
877+
878+
messageList.add(
879+
{
880+
id: 'user-1',
881+
role: 'user',
882+
content: { format: 2, content: 'Hello', parts: [{ type: 'text', text: 'Hello' }] },
883+
createdAt: new Date('2023-01-01T00:00:00Z'),
884+
},
885+
'input',
886+
);
887+
888+
try {
889+
await processor.processInputStep({
890+
messageList,
891+
stepNumber: 1,
892+
model: createMockModel(),
893+
steps: [],
894+
systemMessages: [],
895+
state: {},
896+
retryCount: 0,
897+
abort: (() => {
898+
throw new Error('aborted');
899+
}) as any,
900+
});
901+
expect.fail('Expected TokenLimiterProcessor to throw a TripWire');
902+
} catch (error) {
903+
expect(error).toBeInstanceOf(TripWire);
904+
expect(error).toHaveProperty(
905+
'message',
906+
'TokenLimiterProcessor: No messages fit within the remaining token budget. Cannot send LLM a request with no messages.',
907+
);
908+
expect((error as TripWire).options).toEqual({
909+
retry: false,
910+
metadata: {
911+
systemTokens: 0,
912+
limit: 25,
913+
remainingBudget: 1,
914+
messageCount: 1,
915+
},
916+
});
917+
}
918+
919+
expect(messageList.get.all.db()).toHaveLength(1);
920+
});
921+
873922
it('should handle tool call messages in token counting', async () => {
874923
const processor = new TokenLimiterProcessor({ limit: 100 });
875924

packages/core/src/processors/processors/token-limiter.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,14 @@ export interface TokenLimiterOptions {
3636
* - Input processor: Filters historical messages to fit within context window, prioritizing recent messages
3737
* - Output processor: Limits generated response tokens via streaming (processOutputStream) or non-streaming (processOutputResult)
3838
*/
39-
export class TokenLimiterProcessor implements Processor<'token-limiter', { systemTokens: number; limit: number }> {
39+
type TokenLimiterTripWireMetadata = {
40+
systemTokens: number;
41+
limit: number;
42+
remainingBudget?: number;
43+
messageCount?: number;
44+
};
45+
46+
export class TokenLimiterProcessor implements Processor<'token-limiter', TokenLimiterTripWireMetadata> {
4047
public readonly id = 'token-limiter';
4148
public readonly name = 'Token Limiter';
4249
private encoderPromise: Promise<Tiktoken> | undefined;
@@ -152,6 +159,16 @@ export class TokenLimiterProcessor implements Processor<'token-limiter', { syste
152159
}
153160
}
154161

162+
if (messagesToKeep.length === 0) {
163+
throw new TripWire(
164+
'TokenLimiterProcessor: No messages fit within the remaining token budget. Cannot send LLM a request with no messages.',
165+
{
166+
retry: false,
167+
metadata: { systemTokens, limit, remainingBudget, messageCount: messages.length },
168+
},
169+
);
170+
}
171+
155172
// Remove messages that don't fit within the token budget
156173
const keepIds = new Set(messagesToKeep.map(m => m.id));
157174
const idsToRemove = messages.filter(m => !keepIds.has(m.id)).map(m => m.id);

0 commit comments

Comments
 (0)