illuma-agents 1.0.38 → 1.0.39

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/agents/AgentContext.cjs +45 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +2 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +98 -0
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/main.cjs +6 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/cache.cjs +140 -47
  10. package/dist/cjs/messages/cache.cjs.map +1 -1
  11. package/dist/cjs/schemas/validate.cjs +173 -0
  12. package/dist/cjs/schemas/validate.cjs.map +1 -0
  13. package/dist/esm/agents/AgentContext.mjs +45 -2
  14. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  15. package/dist/esm/common/enum.mjs +2 -0
  16. package/dist/esm/common/enum.mjs.map +1 -1
  17. package/dist/esm/graphs/Graph.mjs +98 -0
  18. package/dist/esm/graphs/Graph.mjs.map +1 -1
  19. package/dist/esm/main.mjs +1 -0
  20. package/dist/esm/main.mjs.map +1 -1
  21. package/dist/esm/messages/cache.mjs +140 -47
  22. package/dist/esm/messages/cache.mjs.map +1 -1
  23. package/dist/esm/schemas/validate.mjs +167 -0
  24. package/dist/esm/schemas/validate.mjs.map +1 -0
  25. package/dist/types/agents/AgentContext.d.ts +19 -1
  26. package/dist/types/common/enum.d.ts +2 -0
  27. package/dist/types/graphs/Graph.d.ts +6 -0
  28. package/dist/types/index.d.ts +1 -0
  29. package/dist/types/messages/cache.d.ts +4 -1
  30. package/dist/types/schemas/index.d.ts +1 -0
  31. package/dist/types/schemas/validate.d.ts +36 -0
  32. package/dist/types/types/graph.d.ts +69 -0
  33. package/package.json +2 -2
  34. package/src/agents/AgentContext.test.ts +312 -0
  35. package/src/agents/AgentContext.ts +56 -0
  36. package/src/common/enum.ts +2 -0
  37. package/src/graphs/Graph.ts +150 -0
  38. package/src/index.ts +3 -0
  39. package/src/messages/cache.test.ts +51 -6
  40. package/src/messages/cache.ts +149 -122
  41. package/src/schemas/index.ts +2 -0
  42. package/src/schemas/validate.test.ts +358 -0
  43. package/src/schemas/validate.ts +238 -0
  44. package/src/specs/cache.simple.test.ts +396 -0
  45. package/src/types/graph.test.ts +183 -0
  46. package/src/types/graph.ts +71 -0
@@ -666,6 +666,106 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
666
666
  }
667
667
  }
668
668
 
669
+ /**
670
+ * Execute model invocation with structured output.
671
+ * Uses withStructuredOutput to force the model to return JSON conforming to the schema.
672
+ * Disables streaming and returns a validated JSON response.
673
+ */
674
+ private async attemptStructuredInvoke(
675
+ {
676
+ currentModel,
677
+ finalMessages,
678
+ schema,
679
+ structuredOutputConfig,
680
+ }: {
681
+ currentModel: t.ChatModelInstance;
682
+ finalMessages: BaseMessage[];
683
+ schema: Record<string, unknown>;
684
+ structuredOutputConfig: t.StructuredOutputConfig;
685
+ },
686
+ config?: RunnableConfig
687
+ ): Promise<{
688
+ structuredResponse: Record<string, unknown>;
689
+ rawMessage?: AIMessageChunk;
690
+ }> {
691
+ const model = this.overrideModel ?? currentModel;
692
+ if (!model) {
693
+ throw new Error('No model found');
694
+ }
695
+
696
+ const {
697
+ name = 'StructuredResponse',
698
+ includeRaw = false,
699
+ handleErrors = true,
700
+ maxRetries = 2,
701
+ } = structuredOutputConfig;
702
+
703
+ // Use withStructuredOutput to bind the schema
704
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
705
+ const structuredModel = (model as any).withStructuredOutput(schema, {
706
+ name,
707
+ includeRaw,
708
+ strict: structuredOutputConfig.strict !== false,
709
+ });
710
+
711
+ let lastError: Error | undefined;
712
+ let attempts = 0;
713
+
714
+ while (attempts <= maxRetries) {
715
+ try {
716
+ const result = await structuredModel.invoke(finalMessages, config);
717
+
718
+ // Handle includeRaw response format
719
+ if (includeRaw && result.raw && result.parsed) {
720
+ return {
721
+ structuredResponse: result.parsed as Record<string, unknown>,
722
+ rawMessage: result.raw as AIMessageChunk,
723
+ };
724
+ }
725
+
726
+ // Direct response
727
+ return {
728
+ structuredResponse: result as Record<string, unknown>,
729
+ };
730
+ } catch (error) {
731
+ lastError = error as Error;
732
+ attempts++;
733
+
734
+ // If error handling is disabled, throw immediately
735
+ if (handleErrors === false) {
736
+ throw error;
737
+ }
738
+
739
+ // If we've exhausted retries, throw
740
+ if (attempts > maxRetries) {
741
+ throw new Error(
742
+ `Structured output failed after ${maxRetries + 1} attempts: ${lastError.message}`
743
+ );
744
+ }
745
+
746
+ // Add error message to conversation for retry
747
+ const errorMessage =
748
+ typeof handleErrors === 'string'
749
+ ? handleErrors
750
+ : `The response did not match the expected schema. Error: ${lastError.message}. Please try again with a valid response.`;
751
+
752
+ console.warn(
753
+ `[Graph] Structured output attempt ${attempts} failed: ${lastError.message}. Retrying...`
754
+ );
755
+
756
+ // Add the error as a human message for context
757
+ finalMessages = [
758
+ ...finalMessages,
759
+ new HumanMessage({
760
+ content: `[VALIDATION ERROR]\n${errorMessage}`,
761
+ }),
762
+ ];
763
+ }
764
+ }
765
+
766
+ throw lastError ?? new Error('Structured output failed');
767
+ }
768
+
669
769
  cleanupSignalListener(currentModel?: t.ChatModel): void {
670
770
  if (!this.signal) {
671
771
  return;
@@ -948,6 +1048,56 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
948
1048
  config
949
1049
  );
950
1050
 
1051
+ // Check if structured output mode is enabled
1052
+ if (
1053
+ agentContext.isStructuredOutputMode &&
1054
+ agentContext.structuredOutput
1055
+ ) {
1056
+ const schema = agentContext.getStructuredOutputSchema();
1057
+ if (!schema) {
1058
+ throw new Error('Structured output schema is not configured');
1059
+ }
1060
+
1061
+ try {
1062
+ // Use structured output invocation (non-streaming)
1063
+ const { structuredResponse, rawMessage } =
1064
+ await this.attemptStructuredInvoke(
1065
+ {
1066
+ currentModel: model as t.ChatModelInstance,
1067
+ finalMessages,
1068
+ schema,
1069
+ structuredOutputConfig: agentContext.structuredOutput,
1070
+ },
1071
+ config
1072
+ );
1073
+
1074
+ // Emit structured output event
1075
+ await safeDispatchCustomEvent(
1076
+ GraphEvents.ON_STRUCTURED_OUTPUT,
1077
+ {
1078
+ structuredResponse,
1079
+ schema,
1080
+ raw: rawMessage,
1081
+ },
1082
+ config
1083
+ );
1084
+
1085
+ agentContext.currentUsage = rawMessage
1086
+ ? this.getUsageMetadata(rawMessage)
1087
+ : undefined;
1088
+ this.cleanupSignalListener();
1089
+
1090
+ // Return both the structured response and the raw message
1091
+ return {
1092
+ messages: rawMessage ? [rawMessage] : [],
1093
+ structuredResponse,
1094
+ };
1095
+ } catch (structuredError) {
1096
+ console.error('[Graph] Structured output failed:', structuredError);
1097
+ throw structuredError;
1098
+ }
1099
+ }
1100
+
951
1101
  try {
952
1102
  result = await this.attemptInvoke(
953
1103
  {
package/src/index.ts CHANGED
@@ -17,6 +17,9 @@ export * from './tools/ToolSearch';
17
17
  export * from './tools/handlers';
18
18
  export * from './tools/search';
19
19
 
20
+ /* Schemas */
21
+ export * from './schemas';
22
+
20
23
  /* Misc. */
21
24
  export * from './common';
22
25
  export * from './utils';
@@ -920,6 +920,54 @@ describe('Immutability - addCacheControl does not mutate original messages', ()
920
920
 
921
921
  expect('cache_control' in originalFirstBlock).toBe(true);
922
922
  });
923
+
924
+ it('should remove lc_kwargs to prevent serialization mismatch for LangChain messages', () => {
925
+ type LangChainLikeMsg = TestMsg & {
926
+ lc_kwargs?: { content?: MessageContentComplex[] };
927
+ };
928
+
929
+ const messagesWithLcKwargs: LangChainLikeMsg[] = [
930
+ {
931
+ role: 'user',
932
+ content: [{ type: ContentTypes.TEXT, text: 'First user message' }],
933
+ lc_kwargs: {
934
+ content: [{ type: ContentTypes.TEXT, text: 'First user message' }],
935
+ },
936
+ },
937
+ {
938
+ role: 'assistant',
939
+ content: [{ type: ContentTypes.TEXT, text: 'Assistant response' }],
940
+ lc_kwargs: {
941
+ content: [{ type: ContentTypes.TEXT, text: 'Assistant response' }],
942
+ },
943
+ },
944
+ {
945
+ role: 'user',
946
+ content: [{ type: ContentTypes.TEXT, text: 'Second user message' }],
947
+ lc_kwargs: {
948
+ content: [{ type: ContentTypes.TEXT, text: 'Second user message' }],
949
+ },
950
+ },
951
+ ];
952
+
953
+ const result = addCacheControl(messagesWithLcKwargs as never);
954
+
955
+ const resultFirst = result[0] as LangChainLikeMsg;
956
+ const resultThird = result[2] as LangChainLikeMsg;
957
+
958
+ expect(resultFirst.lc_kwargs).toBeUndefined();
959
+ expect(resultThird.lc_kwargs).toBeUndefined();
960
+
961
+ const firstContent = resultFirst.content as MessageContentComplex[];
962
+ expect('cache_control' in firstContent[0]).toBe(true);
963
+
964
+ const originalFirst = messagesWithLcKwargs[0];
965
+ const originalContent = originalFirst.content as MessageContentComplex[];
966
+ const originalLcContent = originalFirst.lc_kwargs
967
+ ?.content as MessageContentComplex[];
968
+ expect('cache_control' in originalContent[0]).toBe(false);
969
+ expect('cache_control' in originalLcContent[0]).toBe(false);
970
+ });
923
971
  });
924
972
 
925
973
  describe('Immutability - addBedrockCacheControl does not mutate original messages', () => {
@@ -1049,7 +1097,7 @@ describe('Immutability - addBedrockCacheControl does not mutate original message
1049
1097
  expect('cache_control' in anthropicFirstContent[0]).toBe(true);
1050
1098
  });
1051
1099
 
1052
- it('should keep lc_kwargs.content in sync with content for LangChain messages', () => {
1100
+ it('should remove lc_kwargs to prevent serialization mismatch for LangChain messages', () => {
1053
1101
  type LangChainLikeMsg = TestMsg & {
1054
1102
  lc_kwargs?: { content?: MessageContentComplex[] };
1055
1103
  };
@@ -1076,14 +1124,11 @@ describe('Immutability - addBedrockCacheControl does not mutate original message
1076
1124
  const resultFirst = bedrockResult[0] as LangChainLikeMsg;
1077
1125
  const resultSecond = bedrockResult[1] as LangChainLikeMsg;
1078
1126
 
1079
- expect(resultFirst.content).toEqual(resultFirst.lc_kwargs?.content);
1080
- expect(resultSecond.content).toEqual(resultSecond.lc_kwargs?.content);
1127
+ expect(resultFirst.lc_kwargs).toBeUndefined();
1128
+ expect(resultSecond.lc_kwargs).toBeUndefined();
1081
1129
 
1082
1130
  const firstContent = resultFirst.content as MessageContentComplex[];
1083
- const firstLcContent = resultFirst.lc_kwargs
1084
- ?.content as MessageContentComplex[];
1085
1131
  expect(firstContent.some((b) => 'cachePoint' in b)).toBe(true);
1086
- expect(firstLcContent.some((b) => 'cachePoint' in b)).toBe(true);
1087
1132
 
1088
1133
  const originalFirst = messagesWithLcKwargs[0];
1089
1134
  const originalContent = originalFirst.content as MessageContentComplex[];
@@ -1,11 +1,4 @@
1
- import {
2
- BaseMessage,
3
- MessageContentComplex,
4
- AIMessage,
5
- HumanMessage,
6
- SystemMessage,
7
- ToolMessage,
8
- } from '@langchain/core/messages';
1
+ import { BaseMessage, MessageContentComplex } from '@langchain/core/messages';
9
2
  import type { AnthropicMessage } from '@/types/messages';
10
3
  import type Anthropic from '@anthropic-ai/sdk';
11
4
  import { ContentTypes } from '@/common/enum';
@@ -41,84 +34,95 @@ function deepCloneContent<T extends string | MessageContentComplex[]>(
41
34
  }
42
35
 
43
36
  /**
44
- * Simple shallow clone with deep-cloned content.
45
- * Used for stripping cache control where we don't need proper LangChain instances.
37
+ * Clones a message with deep-cloned content, explicitly excluding LangChain
38
+ * serialization metadata to prevent coercion issues.
39
+ * Keeps lc_kwargs in sync with content to prevent LangChain serialization issues.
46
40
  */
47
- function _shallowCloneMessage<T extends MessageWithContent>(message: T): T {
48
- const cloned = {
49
- ...message,
50
- content: deepCloneContent(message.content ?? ''),
51
- } as T;
52
- const lcKwargs = (cloned as Record<string, unknown>).lc_kwargs as
41
+ function cloneMessage<T extends MessageWithContent>(
42
+ message: T,
43
+ content: string | MessageContentComplex[]
44
+ ): T {
45
+ const {
46
+ lc_kwargs: _lc_kwargs,
47
+ lc_serializable: _lc_serializable,
48
+ lc_namespace: _lc_namespace,
49
+ ...rest
50
+ } = message as T & {
51
+ lc_kwargs?: unknown;
52
+ lc_serializable?: unknown;
53
+ lc_namespace?: unknown;
54
+ };
55
+
56
+ const cloned = { ...rest, content } as T;
57
+
58
+ // Sync lc_kwargs.content with the new content to prevent LangChain coercion issues
59
+ const lcKwargs = (message as Record<string, unknown>).lc_kwargs as
53
60
  | Record<string, unknown>
54
61
  | undefined;
55
62
  if (lcKwargs != null) {
56
63
  (cloned as Record<string, unknown>).lc_kwargs = {
57
64
  ...lcKwargs,
58
- content: cloned.content,
65
+ content: content,
66
+ };
67
+ }
68
+
69
+ // LangChain messages don't have a direct 'role' property - derive it from getType()
70
+ if (
71
+ 'getType' in message &&
72
+ typeof message.getType === 'function' &&
73
+ !('role' in cloned)
74
+ ) {
75
+ const msgType = (message as unknown as BaseMessage).getType();
76
+ const roleMap: Record<string, string> = {
77
+ human: 'user',
78
+ ai: 'assistant',
79
+ system: 'system',
80
+ tool: 'tool',
59
81
  };
82
+ (cloned as Record<string, unknown>).role = roleMap[msgType] || msgType;
60
83
  }
84
+
61
85
  return cloned;
62
86
  }
63
87
 
64
88
  /**
65
- * Creates a new LangChain message instance with the given content.
66
- * Required when adding cache points to ensure proper serialization.
89
+ * Checks if a content block is a cache point
67
90
  */
68
- function _createNewMessage<T extends MessageWithContent>(
69
- message: T,
70
- content: MessageContentComplex[]
71
- ): T {
72
- if ('getType' in message && typeof message.getType === 'function') {
73
- const baseMsg = message as unknown as BaseMessage;
74
- const msgType = baseMsg.getType();
75
-
76
- const baseFields = {
77
- content,
78
- name: baseMsg.name,
79
- additional_kwargs: { ...baseMsg.additional_kwargs },
80
- response_metadata: { ...baseMsg.response_metadata },
81
- id: baseMsg.id,
82
- };
91
+ function isCachePoint(block: MessageContentComplex): boolean {
92
+ return 'cachePoint' in block && !('type' in block);
93
+ }
83
94
 
84
- switch (msgType) {
85
- case 'human':
86
- return new HumanMessage(baseFields) as unknown as T;
87
- case 'ai': {
88
- const aiMsg = baseMsg as AIMessage;
89
- return new AIMessage({
90
- ...baseFields,
91
- tool_calls: aiMsg.tool_calls ? [...aiMsg.tool_calls] : [],
92
- invalid_tool_calls: aiMsg.invalid_tool_calls
93
- ? [...aiMsg.invalid_tool_calls]
94
- : [],
95
- usage_metadata: aiMsg.usage_metadata,
96
- }) as unknown as T;
97
- }
98
- case 'system':
99
- return new SystemMessage(baseFields) as unknown as T;
100
- case 'tool': {
101
- const toolMsg = baseMsg as ToolMessage;
102
- return new ToolMessage({
103
- ...baseFields,
104
- tool_call_id: toolMsg.tool_call_id,
105
- status: toolMsg.status,
106
- artifact: toolMsg.artifact,
107
- }) as unknown as T;
108
- }
109
- default:
110
- break;
111
- }
95
+ /**
96
+ * Checks if a message's content needs cache control stripping.
97
+ * Returns true if content has cachePoint blocks or cache_control fields.
98
+ */
99
+ function needsCacheStripping(content: MessageContentComplex[]): boolean {
100
+ for (let i = 0; i < content.length; i++) {
101
+ const block = content[i];
102
+ if (isCachePoint(block)) return true;
103
+ if ('cache_control' in block) return true;
112
104
  }
105
+ return false;
106
+ }
113
107
 
114
- const cloned = { ...message, content } as T;
115
- const lcKwargs = (cloned as Record<string, unknown>).lc_kwargs as
116
- | Record<string, unknown>
117
- | undefined;
118
- if (lcKwargs != null) {
119
- (cloned as Record<string, unknown>).lc_kwargs = { ...lcKwargs, content };
108
+ /**
109
+ * Checks if a message's content has Anthropic cache_control fields.
110
+ */
111
+ function hasAnthropicCacheControl(content: MessageContentComplex[]): boolean {
112
+ for (let i = 0; i < content.length; i++) {
113
+ if ('cache_control' in content[i]) return true;
120
114
  }
121
- return cloned;
115
+ return false;
116
+ }
117
+
118
+ /**
119
+ * Checks if a message's content has Bedrock cachePoint blocks.
120
+ */
121
+ function hasBedrockCachePoint(content: MessageContentComplex[]): boolean {
122
+ for (let i = 0; i < content.length; i++) {
123
+ if (isCachePoint(content[i])) return true;
124
+ }
125
+ return false;
122
126
  }
123
127
 
124
128
  /**
@@ -126,8 +130,9 @@ function _createNewMessage<T extends MessageWithContent>(
126
130
  * Strips ALL existing cache control (both Anthropic and Bedrock formats) from all messages,
127
131
  * then adds fresh cache control to the last 2 user messages in a single backward pass.
128
132
  * This ensures we don't accumulate stale cache points across multiple turns.
133
+ * Returns a new array - only clones messages that require modification.
129
134
  * @param messages - The array of message objects.
130
- * @returns - The updated array of message objects with cache control added.
135
+ * @returns - A new array of message objects with cache control added.
131
136
  */
132
137
  export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
133
138
  messages: T[]
@@ -136,68 +141,82 @@ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
136
141
  return messages;
137
142
  }
138
143
 
139
- const updatedMessages = [...messages];
144
+ const updatedMessages: T[] = [...messages];
140
145
  let userMessagesModified = 0;
141
146
 
142
147
  for (let i = updatedMessages.length - 1; i >= 0; i--) {
143
- const message = updatedMessages[i];
148
+ const originalMessage = updatedMessages[i];
149
+ const content = originalMessage.content;
144
150
  const isUserMessage =
145
- ('getType' in message && message.getType() === 'human') ||
146
- ('role' in message && message.role === 'user');
151
+ ('getType' in originalMessage && originalMessage.getType() === 'human') ||
152
+ ('role' in originalMessage && originalMessage.role === 'user');
153
+
154
+ const hasArrayContent = Array.isArray(content);
155
+ const needsStripping =
156
+ hasArrayContent &&
157
+ needsCacheStripping(content as MessageContentComplex[]);
158
+ const needsCacheAdd =
159
+ userMessagesModified < 2 &&
160
+ isUserMessage &&
161
+ (typeof content === 'string' || hasArrayContent);
162
+
163
+ if (!needsStripping && !needsCacheAdd) {
164
+ continue;
165
+ }
147
166
 
148
- if (Array.isArray(message.content)) {
149
- message.content = message.content.filter(
150
- (block) => !isCachePoint(block as MessageContentComplex)
151
- ) as typeof message.content;
167
+ let workingContent: MessageContentComplex[];
152
168
 
153
- for (let j = 0; j < message.content.length; j++) {
154
- const block = message.content[j] as Record<string, unknown>;
169
+ if (hasArrayContent) {
170
+ workingContent = deepCloneContent(
171
+ content as MessageContentComplex[]
172
+ ).filter((block) => !isCachePoint(block as MessageContentComplex));
173
+
174
+ for (let j = 0; j < workingContent.length; j++) {
175
+ const block = workingContent[j] as Record<string, unknown>;
155
176
  if ('cache_control' in block) {
156
177
  delete block.cache_control;
157
178
  }
158
179
  }
180
+ } else if (typeof content === 'string') {
181
+ workingContent = [
182
+ { type: 'text', text: content },
183
+ ] as MessageContentComplex[];
184
+ } else {
185
+ workingContent = [];
159
186
  }
160
187
 
161
188
  if (userMessagesModified >= 2 || !isUserMessage) {
189
+ updatedMessages[i] = cloneMessage(
190
+ originalMessage as MessageWithContent,
191
+ workingContent
192
+ ) as T;
162
193
  continue;
163
194
  }
164
195
 
165
- if (typeof message.content === 'string') {
166
- message.content = [
167
- {
168
- type: 'text',
169
- text: message.content,
170
- cache_control: { type: 'ephemeral' },
171
- },
172
- ];
173
- userMessagesModified++;
174
- } else if (Array.isArray(message.content)) {
175
- for (let j = message.content.length - 1; j >= 0; j--) {
176
- const contentPart = message.content[j];
177
- if ('type' in contentPart && contentPart.type === 'text') {
178
- (contentPart as Anthropic.TextBlockParam).cache_control = {
179
- type: 'ephemeral',
180
- };
181
- userMessagesModified++;
182
- break;
183
- }
196
+ for (let j = workingContent.length - 1; j >= 0; j--) {
197
+ const contentPart = workingContent[j];
198
+ if ('type' in contentPart && contentPart.type === 'text') {
199
+ (contentPart as Anthropic.TextBlockParam).cache_control = {
200
+ type: 'ephemeral',
201
+ };
202
+ userMessagesModified++;
203
+ break;
184
204
  }
185
205
  }
206
+
207
+ updatedMessages[i] = cloneMessage(
208
+ originalMessage as MessageWithContent,
209
+ workingContent
210
+ ) as T;
186
211
  }
187
212
 
188
213
  return updatedMessages;
189
214
  }
190
215
 
191
- /**
192
- * Checks if a content block is a cache point
193
- */
194
- function isCachePoint(block: MessageContentComplex): boolean {
195
- return 'cachePoint' in block && !('type' in block);
196
- }
197
-
198
216
  /**
199
217
  * Removes all Anthropic cache_control fields from messages
200
218
  * Used when switching from Anthropic to Bedrock provider
219
+ * Returns a new array - only clones messages that require modification.
201
220
  */
202
221
  export function stripAnthropicCacheControl<T extends MessageWithContent>(
203
222
  messages: T[]
@@ -206,20 +225,24 @@ export function stripAnthropicCacheControl<T extends MessageWithContent>(
206
225
  return messages;
207
226
  }
208
227
 
209
- const updatedMessages = [...messages];
228
+ const updatedMessages: T[] = [...messages];
210
229
 
211
230
  for (let i = 0; i < updatedMessages.length; i++) {
212
- const message = updatedMessages[i];
213
- const content = message.content;
231
+ const originalMessage = updatedMessages[i];
232
+ const content = originalMessage.content;
214
233
 
215
- if (Array.isArray(content)) {
216
- for (let j = 0; j < content.length; j++) {
217
- const block = content[j] as Record<string, unknown>;
218
- if ('cache_control' in block) {
219
- delete block.cache_control;
220
- }
234
+ if (!Array.isArray(content) || !hasAnthropicCacheControl(content)) {
235
+ continue;
236
+ }
237
+
238
+ const clonedContent = deepCloneContent(content);
239
+ for (let j = 0; j < clonedContent.length; j++) {
240
+ const block = clonedContent[j] as Record<string, unknown>;
241
+ if ('cache_control' in block) {
242
+ delete block.cache_control;
221
243
  }
222
244
  }
245
+ updatedMessages[i] = cloneMessage(originalMessage, clonedContent) as T;
223
246
  }
224
247
 
225
248
  return updatedMessages;
@@ -228,6 +251,7 @@ export function stripAnthropicCacheControl<T extends MessageWithContent>(
228
251
  /**
229
252
  * Removes all Bedrock cachePoint blocks from messages
230
253
  * Used when switching from Bedrock to Anthropic provider
254
+ * Returns a new array - only clones messages that require modification.
231
255
  */
232
256
  export function stripBedrockCacheControl<T extends MessageWithContent>(
233
257
  messages: T[]
@@ -236,17 +260,20 @@ export function stripBedrockCacheControl<T extends MessageWithContent>(
236
260
  return messages;
237
261
  }
238
262
 
239
- const updatedMessages = [...messages];
263
+ const updatedMessages: T[] = [...messages];
240
264
 
241
265
  for (let i = 0; i < updatedMessages.length; i++) {
242
- const message = updatedMessages[i];
243
- const content = message.content;
266
+ const originalMessage = updatedMessages[i];
267
+ const content = originalMessage.content;
244
268
 
245
- if (Array.isArray(content)) {
246
- message.content = content.filter(
247
- (block) => !isCachePoint(block as MessageContentComplex)
248
- ) as typeof content;
269
+ if (!Array.isArray(content) || !hasBedrockCachePoint(content)) {
270
+ continue;
249
271
  }
272
+
273
+ const clonedContent = deepCloneContent(content).filter(
274
+ (block) => !isCachePoint(block as MessageContentComplex)
275
+ );
276
+ updatedMessages[i] = cloneMessage(originalMessage, clonedContent) as T;
250
277
  }
251
278
 
252
279
  return updatedMessages;
@@ -0,0 +1,2 @@
1
+ // src/schemas/index.ts
2
+ export * from './validate';