@strands-agents/sdk 1.1.0 → 1.3.0

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 (199) hide show
  1. package/README.md +6 -0
  2. package/dist/src/__tests__/interrupt.test.js +13 -8
  3. package/dist/src/__tests__/interrupt.test.js.map +1 -1
  4. package/dist/src/__tests__/mcp.test.js +221 -7
  5. package/dist/src/__tests__/mcp.test.js.map +1 -1
  6. package/dist/src/agent/__tests__/agent.interrupt.test.js +54 -5
  7. package/dist/src/agent/__tests__/agent.interrupt.test.js.map +1 -1
  8. package/dist/src/agent/__tests__/agent.test.js +56 -0
  9. package/dist/src/agent/__tests__/agent.test.js.map +1 -1
  10. package/dist/src/agent/__tests__/printer.test.js +58 -18
  11. package/dist/src/agent/__tests__/printer.test.js.map +1 -1
  12. package/dist/src/agent/__tests__/snapshot.test.js +98 -0
  13. package/dist/src/agent/__tests__/snapshot.test.js.map +1 -1
  14. package/dist/src/agent/agent-as-tool.d.ts.map +1 -1
  15. package/dist/src/agent/agent-as-tool.js +2 -3
  16. package/dist/src/agent/agent-as-tool.js.map +1 -1
  17. package/dist/src/agent/agent.d.ts +65 -0
  18. package/dist/src/agent/agent.d.ts.map +1 -1
  19. package/dist/src/agent/agent.js +89 -10
  20. package/dist/src/agent/agent.js.map +1 -1
  21. package/dist/src/agent/printer.d.ts +14 -1
  22. package/dist/src/agent/printer.d.ts.map +1 -1
  23. package/dist/src/agent/printer.js +33 -5
  24. package/dist/src/agent/printer.js.map +1 -1
  25. package/dist/src/agent/snapshot.d.ts +9 -17
  26. package/dist/src/agent/snapshot.d.ts.map +1 -1
  27. package/dist/src/agent/snapshot.js +9 -17
  28. package/dist/src/agent/snapshot.js.map +1 -1
  29. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +371 -39
  30. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js.map +1 -1
  31. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +26 -7
  32. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts.map +1 -1
  33. package/dist/src/conversation-manager/sliding-window-conversation-manager.js +192 -41
  34. package/dist/src/conversation-manager/sliding-window-conversation-manager.js.map +1 -1
  35. package/dist/src/hooks/events.d.ts +24 -3
  36. package/dist/src/hooks/events.d.ts.map +1 -1
  37. package/dist/src/hooks/events.js +26 -4
  38. package/dist/src/hooks/events.js.map +1 -1
  39. package/dist/src/hooks/index.d.ts +1 -1
  40. package/dist/src/hooks/index.d.ts.map +1 -1
  41. package/dist/src/hooks/index.js +1 -1
  42. package/dist/src/hooks/index.js.map +1 -1
  43. package/dist/src/hooks/types.d.ts +2 -0
  44. package/dist/src/hooks/types.d.ts.map +1 -1
  45. package/dist/src/hooks/types.js +2 -0
  46. package/dist/src/hooks/types.js.map +1 -1
  47. package/dist/src/index.d.ts +8 -4
  48. package/dist/src/index.d.ts.map +1 -1
  49. package/dist/src/index.js +6 -2
  50. package/dist/src/index.js.map +1 -1
  51. package/dist/src/interrupt.d.ts +29 -2
  52. package/dist/src/interrupt.d.ts.map +1 -1
  53. package/dist/src/interrupt.js +45 -3
  54. package/dist/src/interrupt.js.map +1 -1
  55. package/dist/src/interventions/__tests__/handler.test.d.ts +2 -0
  56. package/dist/src/interventions/__tests__/handler.test.d.ts.map +1 -0
  57. package/dist/src/interventions/__tests__/handler.test.js +35 -0
  58. package/dist/src/interventions/__tests__/handler.test.js.map +1 -0
  59. package/dist/src/interventions/__tests__/registry.test.d.ts +2 -0
  60. package/dist/src/interventions/__tests__/registry.test.d.ts.map +1 -0
  61. package/dist/src/interventions/__tests__/registry.test.js +692 -0
  62. package/dist/src/interventions/__tests__/registry.test.js.map +1 -0
  63. package/dist/src/interventions/actions.d.ts +186 -0
  64. package/dist/src/interventions/actions.d.ts.map +1 -0
  65. package/dist/src/interventions/actions.js +56 -0
  66. package/dist/src/interventions/actions.js.map +1 -0
  67. package/dist/src/interventions/handler.d.ts +44 -0
  68. package/dist/src/interventions/handler.d.ts.map +1 -0
  69. package/dist/src/interventions/handler.js +41 -0
  70. package/dist/src/interventions/handler.js.map +1 -0
  71. package/dist/src/interventions/index.d.ts +12 -0
  72. package/dist/src/interventions/index.d.ts.map +1 -0
  73. package/dist/src/interventions/index.js +4 -0
  74. package/dist/src/interventions/index.js.map +1 -0
  75. package/dist/src/interventions/registry.d.ts +34 -0
  76. package/dist/src/interventions/registry.d.ts.map +1 -0
  77. package/dist/src/interventions/registry.js +248 -0
  78. package/dist/src/interventions/registry.js.map +1 -0
  79. package/dist/src/mcp.d.ts +38 -2
  80. package/dist/src/mcp.d.ts.map +1 -1
  81. package/dist/src/mcp.js +84 -7
  82. package/dist/src/mcp.js.map +1 -1
  83. package/dist/src/models/__tests__/anthropic.test.js +74 -10
  84. package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
  85. package/dist/src/models/__tests__/bedrock.test.js +43 -20
  86. package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
  87. package/dist/src/models/__tests__/google.test.js +14 -6
  88. package/dist/src/models/__tests__/google.test.js.map +1 -1
  89. package/dist/src/models/anthropic.d.ts +15 -3
  90. package/dist/src/models/anthropic.d.ts.map +1 -1
  91. package/dist/src/models/anthropic.js +30 -9
  92. package/dist/src/models/anthropic.js.map +1 -1
  93. package/dist/src/models/bedrock.d.ts +6 -7
  94. package/dist/src/models/bedrock.d.ts.map +1 -1
  95. package/dist/src/models/bedrock.js +31 -18
  96. package/dist/src/models/bedrock.js.map +1 -1
  97. package/dist/src/models/google/model.js +1 -1
  98. package/dist/src/models/google/model.js.map +1 -1
  99. package/dist/src/models/google/types.d.ts +5 -3
  100. package/dist/src/models/google/types.d.ts.map +1 -1
  101. package/dist/src/models/openai/__tests__/chat.test.js +10 -2
  102. package/dist/src/models/openai/__tests__/chat.test.js.map +1 -1
  103. package/dist/src/models/openai/__tests__/mantle.test.d.ts +2 -0
  104. package/dist/src/models/openai/__tests__/mantle.test.d.ts.map +1 -0
  105. package/dist/src/models/openai/__tests__/mantle.test.js +189 -0
  106. package/dist/src/models/openai/__tests__/mantle.test.js.map +1 -0
  107. package/dist/src/models/openai/__tests__/responses.test.js +19 -0
  108. package/dist/src/models/openai/__tests__/responses.test.js.map +1 -1
  109. package/dist/src/models/openai/errors.d.ts.map +1 -1
  110. package/dist/src/models/openai/errors.js +7 -4
  111. package/dist/src/models/openai/errors.js.map +1 -1
  112. package/dist/src/models/openai/index.d.ts +1 -0
  113. package/dist/src/models/openai/index.d.ts.map +1 -1
  114. package/dist/src/models/openai/mantle.d.ts +77 -0
  115. package/dist/src/models/openai/mantle.d.ts.map +1 -0
  116. package/dist/src/models/openai/mantle.js +83 -0
  117. package/dist/src/models/openai/mantle.js.map +1 -0
  118. package/dist/src/models/openai/model.d.ts.map +1 -1
  119. package/dist/src/models/openai/model.js +29 -1
  120. package/dist/src/models/openai/model.js.map +1 -1
  121. package/dist/src/models/openai/types.d.ts +11 -0
  122. package/dist/src/models/openai/types.d.ts.map +1 -1
  123. package/dist/src/multiagent/__tests__/graph.tracer.test.js +14 -0
  124. package/dist/src/multiagent/__tests__/graph.tracer.test.js.map +1 -1
  125. package/dist/src/multiagent/__tests__/interrupts.test.d.ts +2 -0
  126. package/dist/src/multiagent/__tests__/interrupts.test.d.ts.map +1 -0
  127. package/dist/src/multiagent/__tests__/interrupts.test.js +390 -0
  128. package/dist/src/multiagent/__tests__/interrupts.test.js.map +1 -0
  129. package/dist/src/multiagent/__tests__/state.test.js +139 -1
  130. package/dist/src/multiagent/__tests__/state.test.js.map +1 -1
  131. package/dist/src/multiagent/events.d.ts +15 -1
  132. package/dist/src/multiagent/events.d.ts.map +1 -1
  133. package/dist/src/multiagent/events.js +18 -0
  134. package/dist/src/multiagent/events.js.map +1 -1
  135. package/dist/src/multiagent/graph.d.ts +37 -1
  136. package/dist/src/multiagent/graph.d.ts.map +1 -1
  137. package/dist/src/multiagent/graph.js +171 -43
  138. package/dist/src/multiagent/graph.js.map +1 -1
  139. package/dist/src/multiagent/multiagent.d.ts +78 -6
  140. package/dist/src/multiagent/multiagent.d.ts.map +1 -1
  141. package/dist/src/multiagent/multiagent.js +115 -1
  142. package/dist/src/multiagent/multiagent.js.map +1 -1
  143. package/dist/src/multiagent/nodes.d.ts.map +1 -1
  144. package/dist/src/multiagent/nodes.js +55 -21
  145. package/dist/src/multiagent/nodes.js.map +1 -1
  146. package/dist/src/multiagent/state.d.ts +39 -3
  147. package/dist/src/multiagent/state.d.ts.map +1 -1
  148. package/dist/src/multiagent/state.js +80 -1
  149. package/dist/src/multiagent/state.js.map +1 -1
  150. package/dist/src/multiagent/swarm.d.ts +15 -0
  151. package/dist/src/multiagent/swarm.d.ts.map +1 -1
  152. package/dist/src/multiagent/swarm.js +127 -37
  153. package/dist/src/multiagent/swarm.js.map +1 -1
  154. package/dist/src/registry/__tests__/tool-registry.test.js +26 -0
  155. package/dist/src/registry/__tests__/tool-registry.test.js.map +1 -1
  156. package/dist/src/registry/tool-registry.d.ts +9 -7
  157. package/dist/src/registry/tool-registry.d.ts.map +1 -1
  158. package/dist/src/registry/tool-registry.js +29 -10
  159. package/dist/src/registry/tool-registry.js.map +1 -1
  160. package/dist/src/session/__tests__/session-manager.test.js +45 -3
  161. package/dist/src/session/__tests__/session-manager.test.js.map +1 -1
  162. package/dist/src/session/session-manager.d.ts +5 -2
  163. package/dist/src/session/session-manager.d.ts.map +1 -1
  164. package/dist/src/session/session-manager.js +9 -6
  165. package/dist/src/session/session-manager.js.map +1 -1
  166. package/dist/src/telemetry/__tests__/meter.test.js +5 -27
  167. package/dist/src/telemetry/__tests__/meter.test.js.map +1 -1
  168. package/dist/src/telemetry/meter.d.ts +12 -4
  169. package/dist/src/telemetry/meter.d.ts.map +1 -1
  170. package/dist/src/telemetry/meter.js +13 -8
  171. package/dist/src/telemetry/meter.js.map +1 -1
  172. package/dist/src/tools/mcp-tool.d.ts.map +1 -1
  173. package/dist/src/tools/mcp-tool.js +3 -2
  174. package/dist/src/tools/mcp-tool.js.map +1 -1
  175. package/dist/src/tsconfig.tsbuildinfo +1 -1
  176. package/dist/src/types/__tests__/agent.test.js +97 -0
  177. package/dist/src/types/__tests__/agent.test.js.map +1 -1
  178. package/dist/src/types/agent.d.ts +26 -5
  179. package/dist/src/types/agent.d.ts.map +1 -1
  180. package/dist/src/types/agent.js +20 -3
  181. package/dist/src/types/agent.js.map +1 -1
  182. package/dist/src/types/messages.d.ts +3 -1
  183. package/dist/src/types/messages.d.ts.map +1 -1
  184. package/dist/src/types/messages.js.map +1 -1
  185. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js +208 -0
  186. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js.map +1 -1
  187. package/dist/src/vended-plugins/context-offloader/__tests__/search.test.d.ts +2 -0
  188. package/dist/src/vended-plugins/context-offloader/__tests__/search.test.d.ts.map +1 -0
  189. package/dist/src/vended-plugins/context-offloader/__tests__/search.test.js +149 -0
  190. package/dist/src/vended-plugins/context-offloader/__tests__/search.test.js.map +1 -0
  191. package/dist/src/vended-plugins/context-offloader/plugin.d.ts.map +1 -1
  192. package/dist/src/vended-plugins/context-offloader/plugin.js +57 -10
  193. package/dist/src/vended-plugins/context-offloader/plugin.js.map +1 -1
  194. package/dist/src/vended-plugins/context-offloader/search.d.ts +25 -0
  195. package/dist/src/vended-plugins/context-offloader/search.d.ts.map +1 -0
  196. package/dist/src/vended-plugins/context-offloader/search.js +120 -0
  197. package/dist/src/vended-plugins/context-offloader/search.js.map +1 -0
  198. package/dist/src/vended-tools/notebook/notebook.d.ts +1 -1
  199. package/package.json +11 -1
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
2
  import { SlidingWindowConversationManager } from '../sliding-window-conversation-manager.js';
3
- import { ContextWindowOverflowError, Message, TextBlock, ToolUseBlock, ToolResultBlock, } from '../../index.js';
3
+ import { ContextWindowOverflowError, DocumentBlock, ImageBlock, JsonBlock, Message, TextBlock, ToolUseBlock, ToolResultBlock, VideoBlock, } from '../../index.js';
4
4
  import { AfterInvocationEvent, AfterModelCallEvent, BeforeModelCallEvent } from '../../hooks/events.js';
5
5
  import { createMockAgent, invokeTrackedHook } from '../../__fixtures__/agent-helpers.js';
6
6
  async function triggerSlidingWindow(manager, agent) {
@@ -46,7 +46,7 @@ describe('SlidingWindowConversationManager', () => {
46
46
  new ToolResultBlock({
47
47
  toolUseId: 'tool-1',
48
48
  status: 'success',
49
- content: [new TextBlock('Large tool result content')],
49
+ content: [new TextBlock('x'.repeat(500))],
50
50
  }),
51
51
  ],
52
52
  }),
@@ -123,8 +123,10 @@ describe('SlidingWindowConversationManager', () => {
123
123
  });
124
124
  });
125
125
  describe('reduceContext - tool result truncation', () => {
126
- it('truncates tool results when shouldTruncateResults is true', async () => {
126
+ it('partially truncates large tool results preserving first and last 200 chars', async () => {
127
127
  const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
128
+ const middle = 'MIDDLE_CONTENT_TO_REMOVE'.repeat(10); // 240 chars, safely above MIN_TRUNCATION_GAIN
129
+ const original = 'A'.repeat(200) + middle + 'B'.repeat(200);
128
130
  const messages = [
129
131
  new Message({
130
132
  role: 'user',
@@ -132,19 +134,41 @@ describe('SlidingWindowConversationManager', () => {
132
134
  new ToolResultBlock({
133
135
  toolUseId: 'tool-1',
134
136
  status: 'success',
135
- content: [new TextBlock('Large tool result content')],
137
+ content: [new TextBlock(original)],
136
138
  }),
137
139
  ],
138
140
  }),
139
141
  ];
140
142
  const mockAgent = createMockAgent({ messages });
141
143
  await triggerContextOverflow(manager, mockAgent, new ContextWindowOverflowError('Context overflow'));
142
- const toolResult = messages[0].content[0];
143
- expect(toolResult.status).toBe('error');
144
- expect(toolResult.content[0]).toEqual({ type: 'textBlock', text: 'The tool result was too large!' });
144
+ const expectedText = `${'A'.repeat(200)}\n<truncated chars="${middle.length}"/>\n${'B'.repeat(200)}`;
145
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
146
+ toolUseId: 'tool-1',
147
+ status: 'success',
148
+ content: [new TextBlock(expectedText)],
149
+ }));
145
150
  });
146
- it('finds last message with tool results', async () => {
151
+ it('leaves small tool results unchanged', () => {
147
152
  const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
153
+ const messages = [
154
+ new Message({
155
+ role: 'user',
156
+ content: [
157
+ new ToolResultBlock({
158
+ toolUseId: 'tool-1',
159
+ status: 'success',
160
+ content: [new TextBlock('Small result')],
161
+ }),
162
+ ],
163
+ }),
164
+ ];
165
+ const result = manager._truncateToolResults(messages, 0);
166
+ expect(result).toBe(false);
167
+ });
168
+ it('finds oldest message with tool results', async () => {
169
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
170
+ const firstOriginal = 'F'.repeat(500);
171
+ const secondOriginal = 'S'.repeat(500);
148
172
  const messages = [
149
173
  new Message({ role: 'user', content: [new TextBlock('Message 1')] }),
150
174
  new Message({
@@ -153,7 +177,7 @@ describe('SlidingWindowConversationManager', () => {
153
177
  new ToolResultBlock({
154
178
  toolUseId: 'tool-1',
155
179
  status: 'success',
156
- content: [new TextBlock('First result')],
180
+ content: [new TextBlock(firstOriginal)],
157
181
  }),
158
182
  ],
159
183
  }),
@@ -164,21 +188,25 @@ describe('SlidingWindowConversationManager', () => {
164
188
  new ToolResultBlock({
165
189
  toolUseId: 'tool-2',
166
190
  status: 'success',
167
- content: [new TextBlock('Second result')],
191
+ content: [new TextBlock(secondOriginal)],
168
192
  }),
169
193
  ],
170
194
  }),
171
195
  ];
172
196
  const mockAgent = createMockAgent({ messages });
173
197
  await triggerContextOverflow(manager, mockAgent, new ContextWindowOverflowError('Context overflow'));
174
- // Should truncate the last message with tool results (index 3)
175
- const lastToolResult = messages[3].content[0];
176
- expect(lastToolResult.status).toBe('error');
177
- expect(lastToolResult.content[0]).toEqual({ type: 'textBlock', text: 'The tool result was too large!' });
178
- // Earlier tool result should remain unchanged
179
- const firstToolResult = messages[1].content[0];
180
- expect(firstToolResult.status).toBe('success');
181
- expect(firstToolResult.content[0]).toEqual({ type: 'textBlock', text: 'First result' });
198
+ // Oldest tool-result message is truncated; newer one is untouched.
199
+ const expectedTruncated = `${'F'.repeat(200)}\n<truncated chars="100"/>\n${'F'.repeat(200)}`;
200
+ expect(messages[1].content[0]).toEqual(new ToolResultBlock({
201
+ toolUseId: 'tool-1',
202
+ status: 'success',
203
+ content: [new TextBlock(expectedTruncated)],
204
+ }));
205
+ expect(messages[3].content[0]).toEqual(new ToolResultBlock({
206
+ toolUseId: 'tool-2',
207
+ status: 'success',
208
+ content: [new TextBlock(secondOriginal)],
209
+ }));
182
210
  });
183
211
  it('returns after successful truncation without trimming messages', async () => {
184
212
  const manager = new SlidingWindowConversationManager({ windowSize: 2, shouldTruncateResults: true });
@@ -191,7 +219,7 @@ describe('SlidingWindowConversationManager', () => {
191
219
  new ToolResultBlock({
192
220
  toolUseId: 'tool-1',
193
221
  status: 'success',
194
- content: [new TextBlock('Large result')],
222
+ content: [new TextBlock('L'.repeat(500))],
195
223
  }),
196
224
  ],
197
225
  }),
@@ -217,7 +245,7 @@ describe('SlidingWindowConversationManager', () => {
217
245
  new ToolResultBlock({
218
246
  toolUseId: 'tool-1',
219
247
  status: 'success',
220
- content: [new TextBlock('Large result')],
248
+ content: [new TextBlock('L'.repeat(500))],
221
249
  }),
222
250
  ],
223
251
  }),
@@ -228,24 +256,30 @@ describe('SlidingWindowConversationManager', () => {
228
256
  expect(mockAgent.messages).toHaveLength(3);
229
257
  expect(mockAgent.messages[0].role).toBe('user');
230
258
  // Tool result should not be truncated
231
- const toolResult = mockAgent.messages[2].content[0];
232
- expect(toolResult.status).toBe('success');
259
+ expect(mockAgent.messages[2].content[0]).toEqual(new ToolResultBlock({
260
+ toolUseId: 'tool-1',
261
+ status: 'success',
262
+ content: [new TextBlock('L'.repeat(500))],
263
+ }));
233
264
  });
234
- it('does not truncate already-truncated results', async () => {
265
+ it('does not re-truncate already-truncated results', async () => {
235
266
  const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
267
+ // Produced by an earlier run: 200 chars + marker + 200 chars = well under the 450-char
268
+ // threshold below which truncation is not worth running.
269
+ const alreadyTruncated = 'A'.repeat(200) + '\n<truncated chars="1000"/>\n' + 'B'.repeat(200);
236
270
  const messages = [
237
271
  new Message({
238
272
  role: 'user',
239
273
  content: [
240
274
  new ToolResultBlock({
241
275
  toolUseId: 'tool-1',
242
- status: 'error',
243
- content: [new TextBlock('The tool result was too large!')],
276
+ status: 'success',
277
+ content: [new TextBlock(alreadyTruncated)],
244
278
  }),
245
279
  ],
246
280
  }),
247
281
  ];
248
- // First call should return false (already truncated)
282
+ // First call should return false (too short to gain anything from re-truncating)
249
283
  const result = manager._truncateToolResults(messages, 0);
250
284
  expect(result).toBe(false);
251
285
  // reduceContext should fall through to message trimming
@@ -255,19 +289,316 @@ describe('SlidingWindowConversationManager', () => {
255
289
  content: [
256
290
  new ToolResultBlock({
257
291
  toolUseId: 'tool-1',
258
- status: 'error',
259
- content: [new TextBlock('The tool result was too large!')],
292
+ status: 'success',
293
+ content: [new TextBlock(alreadyTruncated)],
260
294
  }),
261
295
  ],
262
296
  }),
263
297
  new Message({ role: 'assistant', content: [new TextBlock('Response')] }),
264
298
  new Message({ role: 'user', content: [new TextBlock('Message')] }),
265
299
  ];
266
- const mockAgent = { messages: messages2 };
300
+ const mockAgent = createMockAgent({ messages: messages2 });
267
301
  await triggerContextOverflow(manager, mockAgent, new ContextWindowOverflowError('Context overflow'));
268
302
  // Should have trimmed messages since truncation was skipped
269
303
  expect(mockAgent.messages.length).toBeLessThan(3);
270
304
  });
305
+ it('replaces image blocks nested in tool results with descriptive placeholders', () => {
306
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
307
+ const bytes = new Uint8Array(1234);
308
+ const messages = [
309
+ new Message({
310
+ role: 'user',
311
+ content: [
312
+ new ToolResultBlock({
313
+ toolUseId: 'tool-1',
314
+ status: 'success',
315
+ content: [new ImageBlock({ format: 'png', source: { bytes } }), new TextBlock('tail')],
316
+ }),
317
+ ],
318
+ }),
319
+ ];
320
+ const changed = manager._truncateToolResults(messages, 0);
321
+ expect(changed).toBe(true);
322
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
323
+ toolUseId: 'tool-1',
324
+ status: 'success',
325
+ content: [new TextBlock('[image: png, source: bytes, 1234 bytes]'), new TextBlock('tail')],
326
+ }));
327
+ });
328
+ it('preserves the error field on truncated tool results', () => {
329
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
330
+ const originalError = new Error('tool blew up');
331
+ const messages = [
332
+ new Message({
333
+ role: 'user',
334
+ content: [
335
+ new ToolResultBlock({
336
+ toolUseId: 'tool-1',
337
+ status: 'error',
338
+ content: [new TextBlock('x'.repeat(500))],
339
+ error: originalError,
340
+ }),
341
+ ],
342
+ }),
343
+ ];
344
+ const changed = manager._truncateToolResults(messages, 0);
345
+ expect(changed).toBe(true);
346
+ const expectedText = `${'x'.repeat(200)}\n<truncated chars="100"/>\n${'x'.repeat(200)}`;
347
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
348
+ toolUseId: 'tool-1',
349
+ status: 'error',
350
+ content: [new TextBlock(expectedText)],
351
+ error: originalError,
352
+ }));
353
+ });
354
+ it('image placeholder reflects non-bytes source kinds honestly', () => {
355
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
356
+ const messages = [
357
+ new Message({
358
+ role: 'user',
359
+ content: [
360
+ new ToolResultBlock({
361
+ toolUseId: 'tool-1',
362
+ status: 'success',
363
+ content: [
364
+ new ImageBlock({ format: 'jpeg', source: { url: 'https://example.com/x.jpg' } }),
365
+ new ImageBlock({ format: 'png', source: { location: { type: 's3', uri: 's3://bucket/key' } } }),
366
+ ],
367
+ }),
368
+ ],
369
+ }),
370
+ ];
371
+ manager._truncateToolResults(messages, 0);
372
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
373
+ toolUseId: 'tool-1',
374
+ status: 'success',
375
+ content: [new TextBlock('[image: jpeg, source: url]'), new TextBlock('[image: png, source: s3]')],
376
+ }));
377
+ });
378
+ it('replaces video bytes blocks with a descriptive placeholder', () => {
379
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
380
+ const messages = [
381
+ new Message({
382
+ role: 'user',
383
+ content: [
384
+ new ToolResultBlock({
385
+ toolUseId: 'tool-1',
386
+ status: 'success',
387
+ content: [new VideoBlock({ format: 'mp4', source: { bytes: new Uint8Array(4096) } })],
388
+ }),
389
+ ],
390
+ }),
391
+ ];
392
+ const changed = manager._truncateToolResults(messages, 0);
393
+ expect(changed).toBe(true);
394
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
395
+ toolUseId: 'tool-1',
396
+ status: 'success',
397
+ content: [new TextBlock('[video: mp4, source: bytes, 4096 bytes]')],
398
+ }));
399
+ });
400
+ it('replaces video s3 blocks with a descriptive placeholder', () => {
401
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
402
+ const messages = [
403
+ new Message({
404
+ role: 'user',
405
+ content: [
406
+ new ToolResultBlock({
407
+ toolUseId: 'tool-1',
408
+ status: 'success',
409
+ content: [
410
+ new VideoBlock({
411
+ format: 'mp4',
412
+ source: { location: { type: 's3', uri: 's3://bucket/key' } },
413
+ }),
414
+ ],
415
+ }),
416
+ ],
417
+ }),
418
+ ];
419
+ const changed = manager._truncateToolResults(messages, 0);
420
+ expect(changed).toBe(true);
421
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
422
+ toolUseId: 'tool-1',
423
+ status: 'success',
424
+ content: [new TextBlock('[video: mp4, source: s3]')],
425
+ }));
426
+ });
427
+ it('replaces document bytes blocks with a descriptive placeholder', () => {
428
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
429
+ const messages = [
430
+ new Message({
431
+ role: 'user',
432
+ content: [
433
+ new ToolResultBlock({
434
+ toolUseId: 'tool-1',
435
+ status: 'success',
436
+ content: [
437
+ new DocumentBlock({
438
+ name: 'report',
439
+ format: 'pdf',
440
+ source: { bytes: new Uint8Array(8192) },
441
+ }),
442
+ ],
443
+ }),
444
+ ],
445
+ }),
446
+ ];
447
+ const changed = manager._truncateToolResults(messages, 0);
448
+ expect(changed).toBe(true);
449
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
450
+ toolUseId: 'tool-1',
451
+ status: 'success',
452
+ content: [new TextBlock('[document: report, pdf, source: bytes, 8192 bytes]')],
453
+ }));
454
+ });
455
+ it('replaces document s3 blocks with a descriptive placeholder', () => {
456
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
457
+ const messages = [
458
+ new Message({
459
+ role: 'user',
460
+ content: [
461
+ new ToolResultBlock({
462
+ toolUseId: 'tool-1',
463
+ status: 'success',
464
+ content: [
465
+ new DocumentBlock({
466
+ name: 'spec',
467
+ format: 'pdf',
468
+ source: { location: { type: 's3', uri: 's3://b/k' } },
469
+ }),
470
+ ],
471
+ }),
472
+ ],
473
+ }),
474
+ ];
475
+ const changed = manager._truncateToolResults(messages, 0);
476
+ expect(changed).toBe(true);
477
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
478
+ toolUseId: 'tool-1',
479
+ status: 'success',
480
+ content: [new TextBlock('[document: spec, pdf, source: s3]')],
481
+ }));
482
+ });
483
+ it('partially truncates large text inside a document text source', () => {
484
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
485
+ const middle = 'M'.repeat(240);
486
+ const originalText = 'A'.repeat(200) + middle + 'B'.repeat(200);
487
+ const messages = [
488
+ new Message({
489
+ role: 'user',
490
+ content: [
491
+ new ToolResultBlock({
492
+ toolUseId: 'tool-1',
493
+ status: 'success',
494
+ content: [new DocumentBlock({ name: 'report', format: 'txt', source: { text: originalText } })],
495
+ }),
496
+ ],
497
+ }),
498
+ ];
499
+ const changed = manager._truncateToolResults(messages, 0);
500
+ expect(changed).toBe(true);
501
+ const expectedText = `${'A'.repeat(200)}\n<truncated chars="${middle.length}"/>\n${'B'.repeat(200)}`;
502
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
503
+ toolUseId: 'tool-1',
504
+ status: 'success',
505
+ content: [new DocumentBlock({ name: 'report', format: 'txt', source: { text: expectedText } })],
506
+ }));
507
+ });
508
+ it('leaves small text inside a document text source unchanged', () => {
509
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
510
+ const messages = [
511
+ new Message({
512
+ role: 'user',
513
+ content: [
514
+ new ToolResultBlock({
515
+ toolUseId: 'tool-1',
516
+ status: 'success',
517
+ content: [new DocumentBlock({ name: 'short', format: 'txt', source: { text: 'hello' } })],
518
+ }),
519
+ ],
520
+ }),
521
+ ];
522
+ const changed = manager._truncateToolResults(messages, 0);
523
+ expect(changed).toBe(false);
524
+ });
525
+ it('truncates long nested text blocks inside a document content source', () => {
526
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
527
+ const longText = 'A'.repeat(200) + 'M'.repeat(240) + 'B'.repeat(200);
528
+ const messages = [
529
+ new Message({
530
+ role: 'user',
531
+ content: [
532
+ new ToolResultBlock({
533
+ toolUseId: 'tool-1',
534
+ status: 'success',
535
+ content: [
536
+ new DocumentBlock({
537
+ name: 'pages',
538
+ format: 'txt',
539
+ source: { content: [new TextBlock(longText), new TextBlock('short')] },
540
+ }),
541
+ ],
542
+ }),
543
+ ],
544
+ }),
545
+ ];
546
+ const changed = manager._truncateToolResults(messages, 0);
547
+ expect(changed).toBe(true);
548
+ const expectedText = `${'A'.repeat(200)}\n<truncated chars="240"/>\n${'B'.repeat(200)}`;
549
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
550
+ toolUseId: 'tool-1',
551
+ status: 'success',
552
+ content: [
553
+ new DocumentBlock({
554
+ name: 'pages',
555
+ format: 'txt',
556
+ source: { content: [new TextBlock(expectedText), new TextBlock('short')] },
557
+ }),
558
+ ],
559
+ }));
560
+ });
561
+ it('replaces large json blocks with a size placeholder', () => {
562
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
563
+ const big = { payload: 'x'.repeat(1000) };
564
+ const size = JSON.stringify(big).length;
565
+ const messages = [
566
+ new Message({
567
+ role: 'user',
568
+ content: [
569
+ new ToolResultBlock({
570
+ toolUseId: 'tool-1',
571
+ status: 'success',
572
+ content: [new JsonBlock({ json: big })],
573
+ }),
574
+ ],
575
+ }),
576
+ ];
577
+ const changed = manager._truncateToolResults(messages, 0);
578
+ expect(changed).toBe(true);
579
+ expect(messages[0].content[0]).toEqual(new ToolResultBlock({
580
+ toolUseId: 'tool-1',
581
+ status: 'success',
582
+ content: [new TextBlock(`[json: ${size} chars]`)],
583
+ }));
584
+ });
585
+ it('leaves small json blocks unchanged', () => {
586
+ const manager = new SlidingWindowConversationManager({ shouldTruncateResults: true });
587
+ const messages = [
588
+ new Message({
589
+ role: 'user',
590
+ content: [
591
+ new ToolResultBlock({
592
+ toolUseId: 'tool-1',
593
+ status: 'success',
594
+ content: [new JsonBlock({ json: { ok: true } })],
595
+ }),
596
+ ],
597
+ }),
598
+ ];
599
+ const changed = manager._truncateToolResults(messages, 0);
600
+ expect(changed).toBe(false);
601
+ });
271
602
  it('does not call truncateToolResults unless an error is passed in', async () => {
272
603
  const manager = new SlidingWindowConversationManager({ windowSize: 2, shouldTruncateResults: true });
273
604
  const messages = [
@@ -545,7 +876,7 @@ describe('SlidingWindowConversationManager', () => {
545
876
  });
546
877
  });
547
878
  describe('helper methods', () => {
548
- describe('findLastMessageWithToolResults', () => {
879
+ describe('findOldestMessageWithToolResults', () => {
549
880
  it('returns correct index when tool results exist', () => {
550
881
  const manager = new SlidingWindowConversationManager();
551
882
  const messages = [
@@ -562,7 +893,7 @@ describe('SlidingWindowConversationManager', () => {
562
893
  }),
563
894
  new Message({ role: 'assistant', content: [new TextBlock('Response')] }),
564
895
  ];
565
- const index = manager._findLastMessageWithToolResults(messages);
896
+ const index = manager._findOldestMessageWithToolResults(messages);
566
897
  expect(index).toBe(1);
567
898
  });
568
899
  it('returns undefined when no tool results exist', () => {
@@ -571,10 +902,10 @@ describe('SlidingWindowConversationManager', () => {
571
902
  new Message({ role: 'user', content: [new TextBlock('Message 1')] }),
572
903
  new Message({ role: 'assistant', content: [new TextBlock('Response 1')] }),
573
904
  ];
574
- const index = manager._findLastMessageWithToolResults(messages);
905
+ const index = manager._findOldestMessageWithToolResults(messages);
575
906
  expect(index).toBeUndefined();
576
907
  });
577
- it('iterates backwards from end', () => {
908
+ it('iterates forward from start', () => {
578
909
  const manager = new SlidingWindowConversationManager();
579
910
  const messages = [
580
911
  new Message({
@@ -599,9 +930,9 @@ describe('SlidingWindowConversationManager', () => {
599
930
  ],
600
931
  }),
601
932
  ];
602
- const index = manager._findLastMessageWithToolResults(messages);
603
- // Should find the last one (index 2), not the first one (index 0)
604
- expect(index).toBe(2);
933
+ const index = manager._findOldestMessageWithToolResults(messages);
934
+ // Should find the first one (index 0), not the last (index 2)
935
+ expect(index).toBe(0);
605
936
  });
606
937
  });
607
938
  describe('truncateToolResults', () => {
@@ -614,7 +945,7 @@ describe('SlidingWindowConversationManager', () => {
614
945
  new ToolResultBlock({
615
946
  toolUseId: 'id-1',
616
947
  status: 'success',
617
- content: [new TextBlock('Large result')],
948
+ content: [new TextBlock('x'.repeat(500))],
618
949
  }),
619
950
  ],
620
951
  }),
@@ -624,14 +955,15 @@ describe('SlidingWindowConversationManager', () => {
624
955
  });
625
956
  it('returns false when already truncated', () => {
626
957
  const manager = new SlidingWindowConversationManager();
958
+ const alreadyTruncated = 'A'.repeat(200) + '\n<truncated chars="1000"/>\n' + 'B'.repeat(200);
627
959
  const messages = [
628
960
  new Message({
629
961
  role: 'user',
630
962
  content: [
631
963
  new ToolResultBlock({
632
964
  toolUseId: 'id-1',
633
- status: 'error',
634
- content: [new TextBlock('The tool result was too large!')],
965
+ status: 'success',
966
+ content: [new TextBlock(alreadyTruncated)],
635
967
  }),
636
968
  ],
637
969
  }),