@@ -277,6 +277,7 @@ func (c *GoogleAIClient) StartChat(systemPrompt string, model string) Chat {
277277 TopP : & topP ,
278278 MaxOutputTokens : maxOutputTokens ,
279279 ResponseMIMEType : "text/plain" ,
280+ ThinkingConfig : & genai.ThinkingConfig {IncludeThoughts : true },
280281 },
281282 history : []* genai.Content {},
282283 }
@@ -502,6 +503,183 @@ func (c *GeminiChat) Initialize(messages []*api.Message) error {
502503 return nil
503504}
504505
506+ func (c * GeminiChat ) SaveMessages (path string ) error {
507+ klog .Infof ("Saving messages to %s" , path )
508+ f , err := os .OpenFile (path , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0644 )
509+ if err != nil {
510+ return fmt .Errorf ("failed to open history file: %w" , err )
511+ }
512+ defer f .Close ()
513+
514+ // We want to save serialized version of the list of messages.
515+ // Since we are appending line by line, we just marshal the whole history as one JSON object per line.
516+ // c.history is []*genai.Content
517+
518+ bytes , err := json .Marshal (c .history )
519+ if err != nil {
520+ return fmt .Errorf ("failed to marshal history: %w" , err )
521+ }
522+
523+ // Unmarshal to generic map to perform sanitization (stripping thoughtSignature and coalescing)
524+ var rawHistory []map [string ]interface {}
525+ if err := json .Unmarshal (bytes , & rawHistory ); err != nil {
526+ return fmt .Errorf ("failed to unmarshal history for cleaning: %w" , err )
527+ }
528+
529+ // Recursively remove thoughtSignature
530+ removeKey (rawHistory , "thoughtSignature" )
531+
532+ // Coalesce history to merge consecutive messages and parts
533+ cleanHistory := coalesceHistory (rawHistory )
534+
535+ cleanBytes , err := json .Marshal (cleanHistory )
536+ if err != nil {
537+ return fmt .Errorf ("failed to marshal clean history: %w" , err )
538+ }
539+
540+ if _ , err := f .Write (cleanBytes ); err != nil {
541+ return fmt .Errorf ("failed to write history to file: %w" , err )
542+ }
543+ if _ , err := f .WriteString ("\n " ); err != nil {
544+ return fmt .Errorf ("failed to write newline to file: %w" , err )
545+ }
546+ return nil
547+ }
548+
549+ func coalesceHistory (history []map [string ]interface {}) []map [string ]interface {} {
550+ if len (history ) == 0 {
551+ return history
552+ }
553+
554+ var coalesced []map [string ]interface {}
555+ var currentMsg map [string ]interface {}
556+
557+ for _ , msg := range history {
558+ if currentMsg == nil {
559+ currentMsg = msg
560+ continue
561+ }
562+
563+ // Check if we can merge with currentMsg
564+ // We merge if the role is the same
565+ currRole , _ := currentMsg ["role" ].(string )
566+ nextRole , _ := msg ["role" ].(string )
567+
568+ if currRole == nextRole && currRole != "" {
569+ // Merge parts
570+ currParts , ok1 := currentMsg ["parts" ].([]interface {})
571+ nextParts , ok2 := msg ["parts" ].([]interface {})
572+ if ok1 && ok2 {
573+ currentMsg ["parts" ] = append (currParts , nextParts ... )
574+ }
575+ } else {
576+ coalesced = append (coalesced , currentMsg )
577+ currentMsg = msg
578+ }
579+ }
580+ if currentMsg != nil {
581+ coalesced = append (coalesced , currentMsg )
582+ }
583+
584+ // Now coalesce parts within each message
585+ for _ , msg := range coalesced {
586+ if parts , ok := msg ["parts" ].([]interface {}); ok {
587+ msg ["parts" ] = coalesceParts (parts )
588+ }
589+ }
590+
591+ return coalesced
592+ }
593+
594+ func coalesceParts (parts []interface {}) []interface {} {
595+ if len (parts ) == 0 {
596+ return parts
597+ }
598+
599+ var coalesced []interface {}
600+ var currentPart map [string ]interface {}
601+
602+ for _ , p := range parts {
603+ part , ok := p .(map [string ]interface {})
604+ if ! ok {
605+ // specific part is not a map, just append current and this one
606+ if currentPart != nil {
607+ coalesced = append (coalesced , currentPart )
608+ currentPart = nil
609+ }
610+ coalesced = append (coalesced , p )
611+ continue
612+ }
613+
614+ if currentPart == nil {
615+ currentPart = part
616+ continue
617+ }
618+
619+ // Check if we can merge consecutive parts
620+ // We can merge if:
621+ // 1. Both are text (and thought status matches)
622+ // 2. We do NOT merge function calls usually as they are distinct items, but if they are fragmented...
623+ // The user only mentioned "combining consecutive parts of same type (thought: true or functionCall or text)".
624+ // Visual observation of example shows text fragments.
625+
626+ // Check types
627+ isText1 , text1 := getText (currentPart )
628+ isText2 , text2 := getText (part )
629+
630+ isThought1 := isThought (currentPart )
631+ isThought2 := isThought (part )
632+
633+ if isText1 && isText2 && isThought1 == isThought2 {
634+ // Merge text
635+ currentPart ["text" ] = text1 + text2
636+ continue
637+ }
638+
639+ // If not mergeable, append current and start new
640+ coalesced = append (coalesced , currentPart )
641+ currentPart = part
642+ }
643+ if currentPart != nil {
644+ coalesced = append (coalesced , currentPart )
645+ }
646+
647+ return coalesced
648+ }
649+
650+ func getText (part map [string ]interface {}) (bool , string ) {
651+ if t , ok := part ["text" ].(string ); ok {
652+ return true , t
653+ }
654+ return false , ""
655+ }
656+
657+ func isThought (part map [string ]interface {}) bool {
658+ // check for "thought": true
659+ if v , ok := part ["thought" ].(bool ); ok {
660+ return v
661+ }
662+ return false
663+ }
664+
665+ func removeKey (v interface {}, key string ) {
666+ switch v := v .(type ) {
667+ case []map [string ]interface {}:
668+ for _ , m := range v {
669+ removeKey (m , key )
670+ }
671+ case map [string ]interface {}:
672+ delete (v , key )
673+ for _ , val := range v {
674+ removeKey (val , key )
675+ }
676+ case []interface {}:
677+ for _ , val := range v {
678+ removeKey (val , key )
679+ }
680+ }
681+ }
682+
505683func (c * GeminiChat ) messageToContent (msg * api.Message ) (* genai.Content , error ) {
506684 var role string
507685 switch msg .Source {
0 commit comments