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.
- package/dist/cjs/agents/AgentContext.cjs +45 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +98 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +6 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +140 -47
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/schemas/validate.cjs +173 -0
- package/dist/cjs/schemas/validate.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +45 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +98 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +140 -47
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/schemas/validate.mjs +167 -0
- package/dist/esm/schemas/validate.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +19 -1
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messages/cache.d.ts +4 -1
- package/dist/types/schemas/index.d.ts +1 -0
- package/dist/types/schemas/validate.d.ts +36 -0
- package/dist/types/types/graph.d.ts +69 -0
- package/package.json +2 -2
- package/src/agents/AgentContext.test.ts +312 -0
- package/src/agents/AgentContext.ts +56 -0
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +150 -0
- package/src/index.ts +3 -0
- package/src/messages/cache.test.ts +51 -6
- package/src/messages/cache.ts +149 -122
- package/src/schemas/index.ts +2 -0
- package/src/schemas/validate.test.ts +358 -0
- package/src/schemas/validate.ts +238 -0
- package/src/specs/cache.simple.test.ts +396 -0
- package/src/types/graph.test.ts +183 -0
- package/src/types/graph.ts +71 -0
package/src/graphs/Graph.ts
CHANGED
|
@@ -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
|
@@ -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
|
|
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.
|
|
1080
|
-
expect(resultSecond.
|
|
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[];
|
package/src/messages/cache.ts
CHANGED
|
@@ -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
|
-
*
|
|
45
|
-
*
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
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
|
-
*
|
|
66
|
-
* Required when adding cache points to ensure proper serialization.
|
|
89
|
+
* Checks if a content block is a cache point
|
|
67
90
|
*/
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
(
|
|
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
|
|
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 -
|
|
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
|
|
148
|
+
const originalMessage = updatedMessages[i];
|
|
149
|
+
const content = originalMessage.content;
|
|
144
150
|
const isUserMessage =
|
|
145
|
-
('getType' in
|
|
146
|
-
('role' in
|
|
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
|
-
|
|
149
|
-
message.content = message.content.filter(
|
|
150
|
-
(block) => !isCachePoint(block as MessageContentComplex)
|
|
151
|
-
) as typeof message.content;
|
|
167
|
+
let workingContent: MessageContentComplex[];
|
|
152
168
|
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
213
|
-
const content =
|
|
231
|
+
const originalMessage = updatedMessages[i];
|
|
232
|
+
const content = originalMessage.content;
|
|
214
233
|
|
|
215
|
-
if (Array.isArray(content)) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
243
|
-
const content =
|
|
266
|
+
const originalMessage = updatedMessages[i];
|
|
267
|
+
const content = originalMessage.content;
|
|
244
268
|
|
|
245
|
-
if (Array.isArray(content)) {
|
|
246
|
-
|
|
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;
|