@@ -941,3 +941,168 @@ func TestEngine_Run_CacheAccumulation_NoCache(t *testing.T) {
941941 t .Errorf ("TotalCachedTokens = %d, want 0" , engine .TotalCachedTokens )
942942 }
943943}
944+
945+ // ── Prompt Tiering Tests ───────────────────────────────────────────
946+
947+ // TestPromptTiering_SeparateMemoryMessage verifies that memory is injected
948+ // as a separate system message rather than concatenated into messages[0].
949+ // This ensures messages[0] (baseSystem) remains stable across turns for
950+ // DeepSeek/Anthropic prompt caching.
951+ func TestPromptTiering_SeparateMemoryMessage (t * testing.T ) {
952+ callCount := 0
953+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
954+ callCount ++
955+ var body struct {
956+ Messages []struct {
957+ Role string `json:"role"`
958+ Content string `json:"content"`
959+ } `json:"messages"`
960+ }
961+ if err := json .NewDecoder (r .Body ).Decode (& body ); err != nil {
962+ return
963+ }
964+
965+ if callCount == 1 {
966+ // Verify: messages[0] = baseSystem (stable), messages[1] = memory (volatile)
967+ if len (body .Messages ) < 2 {
968+ t .Errorf ("expected at least 2 messages (system + memory + user), got %d" , len (body .Messages ))
969+ } else {
970+ if body .Messages [0 ].Role != "system" {
971+ t .Errorf ("messages[0].Role = %q, want system" , body .Messages [0 ].Role )
972+ }
973+ if body .Messages [0 ].Content != "You are a stable base." {
974+ t .Errorf ("messages[0].Content = %q, want %q" , body .Messages [0 ].Content , "You are a stable base." )
975+ }
976+ if body .Messages [1 ].Role != "system" {
977+ t .Errorf ("messages[1].Role = %q, want system (memory)" , body .Messages [1 ].Role )
978+ }
979+ if body .Messages [1 ].Content != "memory-block-v1" {
980+ t .Errorf ("messages[1].Content = %q, want memory-block-v1" , body .Messages [1 ].Content )
981+ }
982+ }
983+ // Return a tool call to force another iteration.
984+ fmt .Fprint (w , `{"choices":[{"message":{"content":"","tool_calls":[{"id":"call_1","function":{"name":"echo","arguments":"{}"}}]}}]}` )
985+ } else {
986+ // Second call: memory should be updated.
987+ if len (body .Messages ) >= 2 && body .Messages [1 ].Role == "system" {
988+ if body .Messages [1 ].Content != "memory-block-v2" {
989+ t .Errorf ("messages[1].Content = %q, want memory-block-v2" , body .Messages [1 ].Content )
990+ }
991+ // messages[0] must still be the stable base.
992+ if body .Messages [0 ].Content != "You are a stable base." {
993+ t .Errorf ("messages[0].Content changed: %q, want %q" , body .Messages [0 ].Content , "You are a stable base." )
994+ }
995+ }
996+ fmt .Fprint (w , `{"choices":[{"message":{"content":"done"}}]}` )
997+ }
998+ }))
999+ defer server .Close ()
1000+
1001+ client := llm .New (server .URL , "sk-test" , "test-model" , "" , 0 )
1002+ echoTool := & fakeTool {name : "echo" , description : "echo" , output : "ok" }
1003+ registry := tool .NewRegistry ([]tool.Tool {echoTool })
1004+ engine := New (client , registry , 10 , "You are a stable base." , nil , 0 )
1005+
1006+ // Set up memory callback that returns different values per call.
1007+ memVersion := 0
1008+ engine .SetMemoryPromptFunc (func () string {
1009+ memVersion ++
1010+ return fmt .Sprintf ("memory-block-v%d" , memVersion )
1011+ })
1012+
1013+ _ , err := engine .Run (context .Background (), "test" )
1014+ if err != nil {
1015+ t .Fatalf ("Run() error: %v" , err )
1016+ }
1017+ }
1018+
1019+ // TestPromptTiering_NoMemoryDropsMessage verifies that when the memory
1020+ // callback returns empty, the memory system message is removed.
1021+ func TestPromptTiering_NoMemoryDropsMessage (t * testing.T ) {
1022+ callCount := 0
1023+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
1024+ callCount ++
1025+ var body struct {
1026+ Messages []struct {
1027+ Role string `json:"role"`
1028+ Content string `json:"content"`
1029+ } `json:"messages"`
1030+ }
1031+ if err := json .NewDecoder (r .Body ).Decode (& body ); err != nil {
1032+ return
1033+ }
1034+
1035+ if callCount == 1 {
1036+ // First call: memory is non-empty, should be at index 1.
1037+ if len (body .Messages ) >= 2 && body .Messages [1 ].Role == "system" {
1038+ if body .Messages [1 ].Content != "initial-memory" {
1039+ t .Errorf ("unexpected memory: %q" , body .Messages [1 ].Content )
1040+ }
1041+ }
1042+ fmt .Fprint (w , `{"choices":[{"message":{"content":"","tool_calls":[{"id":"call_1","function":{"name":"echo","arguments":"{}"}}]}}]}` )
1043+ } else {
1044+ // Second call: memory is empty, should NOT have a second system message.
1045+ systemCount := 0
1046+ for _ , m := range body .Messages {
1047+ if m .Role == "system" {
1048+ systemCount ++
1049+ }
1050+ }
1051+ if systemCount != 1 {
1052+ t .Errorf ("expected 1 system message (base only), got %d" , systemCount )
1053+ }
1054+ fmt .Fprint (w , `{"choices":[{"message":{"content":"done"}}]}` )
1055+ }
1056+ }))
1057+ defer server .Close ()
1058+
1059+ client := llm .New (server .URL , "sk-test" , "test-model" , "" , 0 )
1060+ echoTool := & fakeTool {name : "echo" , description : "echo" , output : "ok" }
1061+ registry := tool .NewRegistry ([]tool.Tool {echoTool })
1062+ engine := New (client , registry , 10 , "You are a stable base." , nil , 0 )
1063+
1064+ memVersion := 0
1065+ engine .SetMemoryPromptFunc (func () string {
1066+ memVersion ++
1067+ if memVersion == 1 {
1068+ return "initial-memory"
1069+ }
1070+ return "" // Empty after first call
1071+ })
1072+
1073+ _ , err := engine .Run (context .Background (), "test" )
1074+ if err != nil {
1075+ t .Fatalf ("Run() error: %v" , err )
1076+ }
1077+ }
1078+
1079+ // TestPromptTiering_MemMsgIdxResets verifies that memMsgIdx resets
1080+ // between Run calls, preventing stale index carry-over.
1081+ func TestPromptTiering_MemMsgIdxResets (t * testing.T ) {
1082+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
1083+ fmt .Fprint (w , `{"choices":[{"message":{"content":"ok"}}]}` )
1084+ }))
1085+ defer server .Close ()
1086+
1087+ client := llm .New (server .URL , "sk-test" , "test-model" , "" , 0 )
1088+ engine := New (client , registryOrNil (), 10 , "base system" , nil , 0 )
1089+
1090+ // Run 1 with memory
1091+ engine .SetMemoryPromptFunc (func () string { return "mem-run1" })
1092+ engine .Run (context .Background (), "run1" )
1093+
1094+ // memMsgIdx should be set
1095+ if engine .memMsgIdx != 1 {
1096+ t .Errorf ("after run1: memMsgIdx = %d, want 1" , engine .memMsgIdx )
1097+ }
1098+
1099+ // Run 2 without memory callback — should reset
1100+ engine .memoryPromptFunc = nil
1101+ engine .Run (context .Background (), "run2" )
1102+
1103+ if engine .memMsgIdx != - 1 {
1104+ t .Errorf ("after run2 (no callback): memMsgIdx = %d, want -1" , engine .memMsgIdx )
1105+ }
1106+ }
1107+
1108+ func registryOrNil () * tool.Registry { return tool .NewRegistry (nil ) }
0 commit comments