illuma-agents 1.0.38 → 1.0.40

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 +110 -1
  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 +110 -1
  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 +166 -2
  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,116 @@ 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
+ // Check if model supports withStructuredOutput
697
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
698
+ if (typeof (model as any).withStructuredOutput !== 'function') {
699
+ throw new Error(
700
+ `The selected model does not support structured output. ` +
701
+ `Please use a model that supports JSON schema output (e.g., OpenAI GPT-4, Anthropic Claude, Google Gemini) ` +
702
+ `or disable structured output for this agent.`
703
+ );
704
+ }
705
+
706
+ const {
707
+ name = 'StructuredResponse',
708
+ includeRaw = false,
709
+ handleErrors = true,
710
+ maxRetries = 2,
711
+ } = structuredOutputConfig;
712
+
713
+ // Use withStructuredOutput to bind the schema
714
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
715
+ const structuredModel = (model as any).withStructuredOutput(schema, {
716
+ name,
717
+ includeRaw,
718
+ strict: structuredOutputConfig.strict !== false,
719
+ });
720
+
721
+ let lastError: Error | undefined;
722
+ let attempts = 0;
723
+
724
+ while (attempts <= maxRetries) {
725
+ try {
726
+ const result = await structuredModel.invoke(finalMessages, config);
727
+
728
+ // Handle includeRaw response format
729
+ if (includeRaw && result.raw && result.parsed) {
730
+ return {
731
+ structuredResponse: result.parsed as Record<string, unknown>,
732
+ rawMessage: result.raw as AIMessageChunk,
733
+ };
734
+ }
735
+
736
+ // Direct response
737
+ return {
738
+ structuredResponse: result as Record<string, unknown>,
739
+ };
740
+ } catch (error) {
741
+ lastError = error as Error;
742
+ attempts++;
743
+
744
+ // If error handling is disabled, throw immediately
745
+ if (handleErrors === false) {
746
+ throw error;
747
+ }
748
+
749
+ // If we've exhausted retries, throw
750
+ if (attempts > maxRetries) {
751
+ throw new Error(
752
+ `Structured output failed after ${maxRetries + 1} attempts: ${lastError.message}`
753
+ );
754
+ }
755
+
756
+ // Add error message to conversation for retry
757
+ const errorMessage =
758
+ typeof handleErrors === 'string'
759
+ ? handleErrors
760
+ : `The response did not match the expected schema. Error: ${lastError.message}. Please try again with a valid response.`;
761
+
762
+ console.warn(
763
+ `[Graph] Structured output attempt ${attempts} failed: ${lastError.message}. Retrying...`
764
+ );
765
+
766
+ // Add the error as a human message for context
767
+ finalMessages = [
768
+ ...finalMessages,
769
+ new HumanMessage({
770
+ content: `[VALIDATION ERROR]\n${errorMessage}`,
771
+ }),
772
+ ];
773
+ }
774
+ }
775
+
776
+ throw lastError ?? new Error('Structured output failed');
777
+ }
778
+
669
779
  cleanupSignalListener(currentModel?: t.ChatModel): void {
670
780
  if (!this.signal) {
671
781
  return;
@@ -732,14 +842,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
732
842
  }
733
843
 
734
844
  const toolsForBinding = agentContext.getToolsForBinding();
735
- let model =
845
+ // Store reference to the raw model before piping for structured output support
846
+ const rawModel =
736
847
  this.overrideModel ??
737
848
  this.initializeModel({
738
849
  tools: toolsForBinding,
739
850
  provider: agentContext.provider,
740
851
  clientOptions: agentContext.clientOptions,
741
852
  });
742
-
853
+
854
+ // Use piped model for regular invocations (includes system message handling)
855
+ let model = rawModel;
743
856
  if (agentContext.systemRunnable) {
744
857
  model = agentContext.systemRunnable.pipe(model as Runnable);
745
858
  }
@@ -948,6 +1061,57 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
948
1061
  config
949
1062
  );
950
1063
 
1064
+ // Check if structured output mode is enabled
1065
+ if (
1066
+ agentContext.isStructuredOutputMode &&
1067
+ agentContext.structuredOutput
1068
+ ) {
1069
+ const schema = agentContext.getStructuredOutputSchema();
1070
+ if (!schema) {
1071
+ throw new Error('Structured output schema is not configured');
1072
+ }
1073
+
1074
+ try {
1075
+ // Use structured output invocation (non-streaming)
1076
+ // Use rawModel (not piped model) because withStructuredOutput is only on the chat model class
1077
+ const { structuredResponse, rawMessage } =
1078
+ await this.attemptStructuredInvoke(
1079
+ {
1080
+ currentModel: rawModel as t.ChatModelInstance,
1081
+ finalMessages,
1082
+ schema,
1083
+ structuredOutputConfig: agentContext.structuredOutput,
1084
+ },
1085
+ config
1086
+ );
1087
+
1088
+ // Emit structured output event
1089
+ await safeDispatchCustomEvent(
1090
+ GraphEvents.ON_STRUCTURED_OUTPUT,
1091
+ {
1092
+ structuredResponse,
1093
+ schema,
1094
+ raw: rawMessage,
1095
+ },
1096
+ config
1097
+ );
1098
+
1099
+ agentContext.currentUsage = rawMessage
1100
+ ? this.getUsageMetadata(rawMessage)
1101
+ : undefined;
1102
+ this.cleanupSignalListener();
1103
+
1104
+ // Return both the structured response and the raw message
1105
+ return {
1106
+ messages: rawMessage ? [rawMessage] : [],
1107
+ structuredResponse,
1108
+ };
1109
+ } catch (structuredError) {
1110
+ console.error('[Graph] Structured output failed:', structuredError);
1111
+ throw structuredError;
1112
+ }
1113
+ }
1114
+
951
1115
  try {
952
1116
  result = await this.attemptInvoke(
953
1117
  {
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';