illuma-agents 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cjs/graphs/Graph.cjs +80 -1
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  4. package/dist/cjs/main.cjs +4 -0
  5. package/dist/cjs/main.cjs.map +1 -1
  6. package/dist/cjs/messages/cache.cjs +87 -14
  7. package/dist/cjs/messages/cache.cjs.map +1 -1
  8. package/dist/cjs/messages/format.cjs +242 -6
  9. package/dist/cjs/messages/format.cjs.map +1 -1
  10. package/dist/cjs/stream.cjs +3 -2
  11. package/dist/cjs/stream.cjs.map +1 -1
  12. package/dist/cjs/tools/handlers.cjs +5 -5
  13. package/dist/cjs/tools/handlers.cjs.map +1 -1
  14. package/dist/esm/graphs/Graph.mjs +80 -1
  15. package/dist/esm/graphs/Graph.mjs.map +1 -1
  16. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  17. package/dist/esm/main.mjs +2 -2
  18. package/dist/esm/messages/cache.mjs +86 -15
  19. package/dist/esm/messages/cache.mjs.map +1 -1
  20. package/dist/esm/messages/format.mjs +242 -8
  21. package/dist/esm/messages/format.mjs.map +1 -1
  22. package/dist/esm/stream.mjs +3 -2
  23. package/dist/esm/stream.mjs.map +1 -1
  24. package/dist/esm/tools/handlers.mjs +5 -5
  25. package/dist/esm/tools/handlers.mjs.map +1 -1
  26. package/dist/types/graphs/Graph.d.ts +19 -2
  27. package/dist/types/messages/cache.d.ts +16 -0
  28. package/dist/types/messages/format.d.ts +25 -1
  29. package/dist/types/tools/handlers.d.ts +2 -1
  30. package/dist/types/types/stream.d.ts +1 -0
  31. package/package.json +4 -1
  32. package/src/graphs/Graph.ts +99 -2
  33. package/src/llm/anthropic/utils/message_outputs.ts +289 -289
  34. package/src/messages/cache.test.ts +499 -3
  35. package/src/messages/cache.ts +115 -25
  36. package/src/messages/ensureThinkingBlock.test.ts +393 -0
  37. package/src/messages/format.ts +312 -6
  38. package/src/messages/labelContentByAgent.test.ts +887 -0
  39. package/src/scripts/test-multi-agent-list-handoff.ts +169 -13
  40. package/src/scripts/test-parallel-agent-labeling.ts +325 -0
  41. package/src/scripts/test-thinking-handoff-bedrock.ts +153 -0
  42. package/src/scripts/test-thinking-handoff.ts +147 -0
  43. package/src/specs/thinking-handoff.test.ts +620 -0
  44. package/src/stream.ts +19 -10
  45. package/src/tools/handlers.ts +36 -18
  46. package/src/types/stream.ts +1 -0
@@ -9,6 +9,9 @@ type MessageWithContent = {
9
9
 
10
10
  /**
11
11
  * Anthropic API: Adds cache control to the appropriate user messages in the payload.
12
+ * Strips ALL existing cache control (both Anthropic and Bedrock formats) from all messages,
13
+ * then adds fresh cache control to the last 2 user messages in a single backward pass.
14
+ * This ensures we don't accumulate stale cache points across multiple turns.
12
15
  * @param messages - The array of message objects.
13
16
  * @returns - The updated array of message objects with cache control added.
14
17
  */
@@ -22,15 +25,26 @@ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
22
25
  const updatedMessages = [...messages];
23
26
  let userMessagesModified = 0;
24
27
 
25
- for (
26
- let i = updatedMessages.length - 1;
27
- i >= 0 && userMessagesModified < 2;
28
- i--
29
- ) {
28
+ for (let i = updatedMessages.length - 1; i >= 0; i--) {
30
29
  const message = updatedMessages[i];
31
- if ('getType' in message && message.getType() !== 'human') {
32
- continue;
33
- } else if ('role' in message && message.role !== 'user') {
30
+ const isUserMessage =
31
+ ('getType' in message && message.getType() === 'human') ||
32
+ ('role' in message && message.role === 'user');
33
+
34
+ if (Array.isArray(message.content)) {
35
+ message.content = message.content.filter(
36
+ (block) => !isCachePoint(block as MessageContentComplex)
37
+ ) as typeof message.content;
38
+
39
+ for (let j = 0; j < message.content.length; j++) {
40
+ const block = message.content[j] as Record<string, unknown>;
41
+ if ('cache_control' in block) {
42
+ delete block.cache_control;
43
+ }
44
+ }
45
+ }
46
+
47
+ if (userMessagesModified >= 2 || !isUserMessage) {
34
48
  continue;
35
49
  }
36
50
 
@@ -60,10 +74,77 @@ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
60
74
  return updatedMessages;
61
75
  }
62
76
 
77
+ /**
78
+ * Checks if a content block is a cache point
79
+ */
80
+ function isCachePoint(block: MessageContentComplex): boolean {
81
+ return 'cachePoint' in block && !('type' in block);
82
+ }
83
+
84
+ /**
85
+ * Removes all Anthropic cache_control fields from messages
86
+ * Used when switching from Anthropic to Bedrock provider
87
+ */
88
+ export function stripAnthropicCacheControl<T extends MessageWithContent>(
89
+ messages: T[]
90
+ ): T[] {
91
+ if (!Array.isArray(messages)) {
92
+ return messages;
93
+ }
94
+
95
+ const updatedMessages = [...messages];
96
+
97
+ for (let i = 0; i < updatedMessages.length; i++) {
98
+ const message = updatedMessages[i];
99
+ const content = message.content;
100
+
101
+ if (Array.isArray(content)) {
102
+ for (let j = 0; j < content.length; j++) {
103
+ const block = content[j] as Record<string, unknown>;
104
+ if ('cache_control' in block) {
105
+ delete block.cache_control;
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ return updatedMessages;
112
+ }
113
+
114
+ /**
115
+ * Removes all Bedrock cachePoint blocks from messages
116
+ * Used when switching from Bedrock to Anthropic provider
117
+ */
118
+ export function stripBedrockCacheControl<T extends MessageWithContent>(
119
+ messages: T[]
120
+ ): T[] {
121
+ if (!Array.isArray(messages)) {
122
+ return messages;
123
+ }
124
+
125
+ const updatedMessages = [...messages];
126
+
127
+ for (let i = 0; i < updatedMessages.length; i++) {
128
+ const message = updatedMessages[i];
129
+ const content = message.content;
130
+
131
+ if (Array.isArray(content)) {
132
+ message.content = content.filter(
133
+ (block) => !isCachePoint(block as MessageContentComplex)
134
+ ) as typeof content;
135
+ }
136
+ }
137
+
138
+ return updatedMessages;
139
+ }
140
+
63
141
  /**
64
142
  * Adds Bedrock Converse API cache points to the last two messages.
65
143
  * Inserts `{ cachePoint: { type: 'default' } }` as a separate content block
66
144
  * immediately after the last text block in each targeted message.
145
+ * Strips ALL existing cache control (both Bedrock and Anthropic formats) from all messages,
146
+ * then adds fresh cache points to the last 2 messages in a single backward pass.
147
+ * This ensures we don't accumulate stale cache points across multiple turns.
67
148
  * @param messages - The array of message objects.
68
149
  * @returns - The updated array of message objects with cache points added.
69
150
  */
@@ -77,23 +158,32 @@ export function addBedrockCacheControl<
77
158
  const updatedMessages: T[] = messages.slice();
78
159
  let messagesModified = 0;
79
160
 
80
- for (
81
- let i = updatedMessages.length - 1;
82
- i >= 0 && messagesModified < 2;
83
- i--
84
- ) {
161
+ for (let i = updatedMessages.length - 1; i >= 0; i--) {
85
162
  const message = updatedMessages[i];
86
-
87
- if (
163
+ const isToolMessage =
88
164
  'getType' in message &&
89
165
  typeof message.getType === 'function' &&
90
- message.getType() === 'tool'
91
- ) {
92
- continue;
93
- }
166
+ message.getType() === 'tool';
94
167
 
95
168
  const content = message.content;
96
169
 
170
+ if (Array.isArray(content)) {
171
+ message.content = content.filter(
172
+ (block) => !isCachePoint(block)
173
+ ) as typeof content;
174
+
175
+ for (let j = 0; j < message.content.length; j++) {
176
+ const block = message.content[j] as Record<string, unknown>;
177
+ if ('cache_control' in block) {
178
+ delete block.cache_control;
179
+ }
180
+ }
181
+ }
182
+
183
+ if (messagesModified >= 2 || isToolMessage) {
184
+ continue;
185
+ }
186
+
97
187
  if (typeof content === 'string' && content === '') {
98
188
  continue;
99
189
  }
@@ -107,9 +197,9 @@ export function addBedrockCacheControl<
107
197
  continue;
108
198
  }
109
199
 
110
- if (Array.isArray(content)) {
200
+ if (Array.isArray(message.content)) {
111
201
  let hasCacheableContent = false;
112
- for (const block of content) {
202
+ for (const block of message.content) {
113
203
  if (block.type === ContentTypes.TEXT) {
114
204
  if (typeof block.text === 'string' && block.text !== '') {
115
205
  hasCacheableContent = true;
@@ -123,15 +213,15 @@ export function addBedrockCacheControl<
123
213
  }
124
214
 
125
215
  let inserted = false;
126
- for (let j = content.length - 1; j >= 0; j--) {
127
- const block = content[j] as MessageContentComplex;
216
+ for (let j = message.content.length - 1; j >= 0; j--) {
217
+ const block = message.content[j] as MessageContentComplex;
128
218
  const type = (block as { type?: string }).type;
129
219
  if (type === ContentTypes.TEXT || type === 'text') {
130
220
  const text = (block as { text?: string }).text;
131
221
  if (text === '' || text === undefined) {
132
222
  continue;
133
223
  }
134
- content.splice(j + 1, 0, {
224
+ message.content.splice(j + 1, 0, {
135
225
  cachePoint: { type: 'default' },
136
226
  } as MessageContentComplex);
137
227
  inserted = true;
@@ -139,7 +229,7 @@ export function addBedrockCacheControl<
139
229
  }
140
230
  }
141
231
  if (!inserted) {
142
- content.push({
232
+ message.content.push({
143
233
  cachePoint: { type: 'default' },
144
234
  } as MessageContentComplex);
145
235
  }
@@ -0,0 +1,393 @@
1
+ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
2
+ import type { ExtendedMessageContent } from '@/types';
3
+ import { ensureThinkingBlockInMessages } from './format';
4
+ import { Providers, ContentTypes } from '@/common';
5
+
6
+ describe('ensureThinkingBlockInMessages', () => {
7
+ describe('messages with thinking blocks (should not be modified)', () => {
8
+ test('should not modify AI message that already has thinking block', () => {
9
+ const messages = [
10
+ new HumanMessage({ content: 'Hello' }),
11
+ new AIMessage({
12
+ content: [
13
+ { type: ContentTypes.THINKING, thinking: 'Let me think...' },
14
+ { type: 'text', text: 'Hi there!' },
15
+ ],
16
+ }),
17
+ ];
18
+
19
+ const result = ensureThinkingBlockInMessages(
20
+ messages,
21
+ Providers.ANTHROPIC
22
+ );
23
+
24
+ expect(result).toHaveLength(2);
25
+ expect(result[0]).toBeInstanceOf(HumanMessage);
26
+ expect(result[1]).toBeInstanceOf(AIMessage);
27
+ expect((result[1].content as ExtendedMessageContent[])[0].type).toBe(
28
+ ContentTypes.THINKING
29
+ );
30
+ });
31
+
32
+ test('should not modify AI message that has redacted_thinking block', () => {
33
+ const messages = [
34
+ new HumanMessage({ content: 'Hello' }),
35
+ new AIMessage({
36
+ content: [
37
+ { type: 'redacted_thinking', data: 'redacted' },
38
+ { type: 'text', text: 'Hi there!' },
39
+ ],
40
+ }),
41
+ ];
42
+
43
+ const result = ensureThinkingBlockInMessages(
44
+ messages,
45
+ Providers.ANTHROPIC
46
+ );
47
+
48
+ expect(result).toHaveLength(2);
49
+ expect(result[0]).toBeInstanceOf(HumanMessage);
50
+ expect(result[1]).toBeInstanceOf(AIMessage);
51
+ expect((result[1].content as ExtendedMessageContent[])[0].type).toBe(
52
+ 'redacted_thinking'
53
+ );
54
+ });
55
+ });
56
+
57
+ describe('messages with tool_calls (should be converted)', () => {
58
+ test('should convert AI message with tool_calls to HumanMessage', () => {
59
+ const messages = [
60
+ new HumanMessage({ content: 'What is the weather?' }),
61
+ new AIMessage({
62
+ content: 'Let me check the weather.',
63
+ tool_calls: [
64
+ {
65
+ id: 'call_123',
66
+ name: 'get_weather',
67
+ args: { location: 'NYC' },
68
+ type: 'tool_call',
69
+ },
70
+ ],
71
+ }),
72
+ new ToolMessage({
73
+ content: 'Sunny, 75°F',
74
+ tool_call_id: 'call_123',
75
+ }),
76
+ ];
77
+
78
+ const result = ensureThinkingBlockInMessages(
79
+ messages,
80
+ Providers.ANTHROPIC
81
+ );
82
+
83
+ // Should have 2 messages: HumanMessage + converted HumanMessage
84
+ expect(result).toHaveLength(2);
85
+ expect(result[0]).toBeInstanceOf(HumanMessage);
86
+ expect(result[0].content).toBe('What is the weather?');
87
+ expect(result[1]).toBeInstanceOf(HumanMessage);
88
+
89
+ // Check that the converted message includes the context prefix
90
+ expect(result[1].content).toContain('[Previous agent context]');
91
+ expect(result[1].content).toContain('Let me check the weather');
92
+ expect(result[1].content).toContain('Sunny, 75°F');
93
+ });
94
+
95
+ test('should convert AI message with tool_use in content to HumanMessage', () => {
96
+ const messages = [
97
+ new HumanMessage({ content: 'Search for something' }),
98
+ new AIMessage({
99
+ content: [
100
+ { type: 'text', text: 'Searching...' },
101
+ {
102
+ type: 'tool_use',
103
+ id: 'call_456',
104
+ name: 'search',
105
+ input: { query: 'test' },
106
+ },
107
+ ],
108
+ }),
109
+ new ToolMessage({
110
+ content: 'Found results',
111
+ tool_call_id: 'call_456',
112
+ }),
113
+ ];
114
+
115
+ const result = ensureThinkingBlockInMessages(
116
+ messages,
117
+ Providers.ANTHROPIC
118
+ );
119
+
120
+ expect(result).toHaveLength(2);
121
+ expect(result[0]).toBeInstanceOf(HumanMessage);
122
+ expect(result[1]).toBeInstanceOf(HumanMessage);
123
+ expect(result[1].content).toContain('[Previous agent context]');
124
+ expect(result[1].content).toContain('Searching...');
125
+ expect(result[1].content).toContain('Found results');
126
+ });
127
+
128
+ test('should handle multiple tool messages in sequence', () => {
129
+ const messages = [
130
+ new HumanMessage({ content: 'Do multiple things' }),
131
+ new AIMessage({
132
+ content: 'I will perform multiple actions.',
133
+ tool_calls: [
134
+ {
135
+ id: 'call_1',
136
+ name: 'action1',
137
+ args: { param: 'a' },
138
+ type: 'tool_call',
139
+ },
140
+ {
141
+ id: 'call_2',
142
+ name: 'action2',
143
+ args: { param: 'b' },
144
+ type: 'tool_call',
145
+ },
146
+ ],
147
+ }),
148
+ new ToolMessage({
149
+ content: 'Result 1',
150
+ tool_call_id: 'call_1',
151
+ }),
152
+ new ToolMessage({
153
+ content: 'Result 2',
154
+ tool_call_id: 'call_2',
155
+ }),
156
+ ];
157
+
158
+ const result = ensureThinkingBlockInMessages(
159
+ messages,
160
+ Providers.ANTHROPIC
161
+ );
162
+
163
+ // Should combine all tool messages into one HumanMessage
164
+ expect(result).toHaveLength(2);
165
+ expect(result[1]).toBeInstanceOf(HumanMessage);
166
+ expect(result[1].content).toContain('Result 1');
167
+ expect(result[1].content).toContain('Result 2');
168
+ });
169
+ });
170
+
171
+ describe('messages without tool calls (should pass through)', () => {
172
+ test('should not modify AI message without tool calls', () => {
173
+ const messages = [
174
+ new HumanMessage({ content: 'Hello' }),
175
+ new AIMessage({ content: 'Hi there, how can I help?' }),
176
+ ];
177
+
178
+ const result = ensureThinkingBlockInMessages(
179
+ messages,
180
+ Providers.ANTHROPIC
181
+ );
182
+
183
+ expect(result).toHaveLength(2);
184
+ expect(result[0]).toBeInstanceOf(HumanMessage);
185
+ expect(result[0].content).toBe('Hello');
186
+ expect(result[1]).toBeInstanceOf(AIMessage);
187
+ expect(result[1].content).toBe('Hi there, how can I help?');
188
+ });
189
+
190
+ test('should preserve HumanMessages and other message types', () => {
191
+ const messages = [
192
+ new HumanMessage({ content: 'Question 1' }),
193
+ new AIMessage({ content: 'Answer 1' }),
194
+ new HumanMessage({ content: 'Question 2' }),
195
+ new AIMessage({ content: 'Answer 2' }),
196
+ ];
197
+
198
+ const result = ensureThinkingBlockInMessages(
199
+ messages,
200
+ Providers.ANTHROPIC
201
+ );
202
+
203
+ expect(result).toHaveLength(4);
204
+ expect(result[0]).toBeInstanceOf(HumanMessage);
205
+ expect(result[1]).toBeInstanceOf(AIMessage);
206
+ expect(result[2]).toBeInstanceOf(HumanMessage);
207
+ expect(result[3]).toBeInstanceOf(AIMessage);
208
+ });
209
+ });
210
+
211
+ describe('mixed scenarios', () => {
212
+ test('should handle mix of normal and tool-using messages', () => {
213
+ const messages = [
214
+ new HumanMessage({ content: 'First question' }),
215
+ new AIMessage({ content: 'First answer without tools' }),
216
+ new HumanMessage({ content: 'Second question' }),
217
+ new AIMessage({
218
+ content: 'Using a tool',
219
+ tool_calls: [
220
+ {
221
+ id: 'call_abc',
222
+ name: 'some_tool',
223
+ args: {},
224
+ type: 'tool_call',
225
+ },
226
+ ],
227
+ }),
228
+ new ToolMessage({
229
+ content: 'Tool result',
230
+ tool_call_id: 'call_abc',
231
+ }),
232
+ new HumanMessage({ content: 'Third question' }),
233
+ new AIMessage({ content: 'Third answer without tools' }),
234
+ ];
235
+
236
+ const result = ensureThinkingBlockInMessages(
237
+ messages,
238
+ Providers.ANTHROPIC
239
+ );
240
+
241
+ // Original message 1: HumanMessage (preserved)
242
+ // Original message 2: AIMessage without tools (preserved)
243
+ // Original message 3: HumanMessage (preserved)
244
+ // Original messages 4-5: AIMessage with tool + ToolMessage (converted to 1 HumanMessage)
245
+ // Original message 6: HumanMessage (preserved)
246
+ // Original message 7: AIMessage without tools (preserved)
247
+ expect(result).toHaveLength(6);
248
+ expect(result[0]).toBeInstanceOf(HumanMessage);
249
+ expect(result[1]).toBeInstanceOf(AIMessage);
250
+ expect(result[2]).toBeInstanceOf(HumanMessage);
251
+ expect(result[3]).toBeInstanceOf(HumanMessage); // Converted
252
+ expect(result[4]).toBeInstanceOf(HumanMessage);
253
+ expect(result[5]).toBeInstanceOf(AIMessage);
254
+ });
255
+
256
+ test('should handle multiple tool-using sequences', () => {
257
+ const messages = [
258
+ new HumanMessage({ content: 'Do task 1' }),
259
+ new AIMessage({
260
+ content: 'Doing task 1',
261
+ tool_calls: [
262
+ {
263
+ id: 'call_1',
264
+ name: 'tool1',
265
+ args: {},
266
+ type: 'tool_call',
267
+ },
268
+ ],
269
+ }),
270
+ new ToolMessage({
271
+ content: 'Result 1',
272
+ tool_call_id: 'call_1',
273
+ }),
274
+ new HumanMessage({ content: 'Do task 2' }),
275
+ new AIMessage({
276
+ content: 'Doing task 2',
277
+ tool_calls: [
278
+ {
279
+ id: 'call_2',
280
+ name: 'tool2',
281
+ args: {},
282
+ type: 'tool_call',
283
+ },
284
+ ],
285
+ }),
286
+ new ToolMessage({
287
+ content: 'Result 2',
288
+ tool_call_id: 'call_2',
289
+ }),
290
+ ];
291
+
292
+ const result = ensureThinkingBlockInMessages(
293
+ messages,
294
+ Providers.ANTHROPIC
295
+ );
296
+
297
+ // Each tool sequence should be converted to a HumanMessage
298
+ expect(result).toHaveLength(4);
299
+ expect(result[0]).toBeInstanceOf(HumanMessage);
300
+ expect(result[0].content).toBe('Do task 1');
301
+ expect(result[1]).toBeInstanceOf(HumanMessage);
302
+ expect(result[1].content).toContain('Doing task 1');
303
+ expect(result[2]).toBeInstanceOf(HumanMessage);
304
+ expect(result[2].content).toBe('Do task 2');
305
+ expect(result[3]).toBeInstanceOf(HumanMessage);
306
+ expect(result[3].content).toContain('Doing task 2');
307
+ });
308
+ });
309
+
310
+ describe('edge cases', () => {
311
+ test('should handle empty messages array', () => {
312
+ const messages: never[] = [];
313
+
314
+ const result = ensureThinkingBlockInMessages(
315
+ messages,
316
+ Providers.ANTHROPIC
317
+ );
318
+
319
+ expect(result).toHaveLength(0);
320
+ });
321
+
322
+ test('should handle AI message with empty content array', () => {
323
+ const messages = [
324
+ new HumanMessage({ content: 'Hello' }),
325
+ new AIMessage({ content: [] }),
326
+ ];
327
+
328
+ const result = ensureThinkingBlockInMessages(
329
+ messages,
330
+ Providers.ANTHROPIC
331
+ );
332
+
333
+ expect(result).toHaveLength(2);
334
+ expect(result[1]).toBeInstanceOf(AIMessage);
335
+ });
336
+
337
+ test('should work with different providers', () => {
338
+ const messages = [
339
+ new AIMessage({
340
+ content: 'Using tool',
341
+ tool_calls: [
342
+ {
343
+ id: 'call_x',
344
+ name: 'test',
345
+ args: {},
346
+ type: 'tool_call',
347
+ },
348
+ ],
349
+ }),
350
+ new ToolMessage({
351
+ content: 'Result',
352
+ tool_call_id: 'call_x',
353
+ }),
354
+ ];
355
+
356
+ // Test with Anthropic
357
+ const resultAnthropic = ensureThinkingBlockInMessages(
358
+ messages,
359
+ Providers.ANTHROPIC
360
+ );
361
+ expect(resultAnthropic).toHaveLength(1);
362
+ expect(resultAnthropic[0]).toBeInstanceOf(HumanMessage);
363
+
364
+ // Test with Bedrock
365
+ const resultBedrock = ensureThinkingBlockInMessages(
366
+ messages,
367
+ Providers.BEDROCK
368
+ );
369
+ expect(resultBedrock).toHaveLength(1);
370
+ expect(resultBedrock[0]).toBeInstanceOf(HumanMessage);
371
+ });
372
+
373
+ test('should handle tool message without preceding AI message', () => {
374
+ const messages = [
375
+ new HumanMessage({ content: 'Hello' }),
376
+ new ToolMessage({
377
+ content: 'Unexpected tool result',
378
+ tool_call_id: 'call_orphan',
379
+ }),
380
+ ];
381
+
382
+ const result = ensureThinkingBlockInMessages(
383
+ messages,
384
+ Providers.ANTHROPIC
385
+ );
386
+
387
+ // Should preserve both messages as-is since tool message has no preceding AI message with tools
388
+ expect(result).toHaveLength(2);
389
+ expect(result[0]).toBeInstanceOf(HumanMessage);
390
+ expect(result[1]).toBeInstanceOf(ToolMessage);
391
+ });
392
+ });
393
+ });