@@ -33,9 +33,16 @@ import (
3333// 1. Replaces the <all_user_messages>...</all_user_messages> section in the model-generated
3434// summary with recent original user messages from the conversation (up to ~30k tokens).
3535// 2. Adds a preamble and a postamble around the summary content.
36- // 3. Converts the summary into a user message, prepended with the original system messages.
36+ // 3. Converts the summary into a user message, prepended with the original system
37+ // messages and messages preserved by TypedFinalizerBuilder.
3738func DefaultFinalize [M adk.MessageType ](ctx context.Context , originalMessages []M , summary M ) ([]M , error ) {
3839 systemMsgs , contextMsgs := splitSystemAndContextMsgs (originalMessages )
40+ var preservedMsgs []M
41+ for _ , msg := range contextMsgs {
42+ if isPreservedMessage (msg ) {
43+ preservedMsgs = append (preservedMsgs , msg )
44+ }
45+ }
3946
4047 processed , err := postProcessSummary (ctx , & postProcessSummaryParams [M ]{
4148 contextMsgs : contextMsgs ,
@@ -45,59 +52,62 @@ func DefaultFinalize[M adk.MessageType](ctx context.Context, originalMessages []
4552 return nil , err
4653 }
4754
48- return append (systemMsgs , processed ), nil
55+ result := make ([]M , 0 , len (systemMsgs )+ len (preservedMsgs )+ 1 )
56+ result = append (result , systemMsgs ... )
57+ result = append (result , preservedMsgs ... )
58+ result = append (result , processed )
59+ return result , nil
4960}
5061
51- // TypedFinalizerBuilder builds a TypedFinalizeFunc by chaining handlers
52- // and an optional custom finalizer, generic over message type M.
62+ // TypedFinalizerBuilder builds a TypedFinalizeFunc by chaining handlers,
63+ // generic over message type M.
64+ type TypedFinalizerBuilder [M adk.MessageType ] struct {
65+ handlers []TypedFinalizeFunc [M ]
66+ errs []error
67+ }
68+
69+ // FinalizerBuilder is a backward-compatible alias for TypedFinalizerBuilder
70+ // specialized with *schema.Message.
5371//
54- // Handlers (e.g. PreserveSkills) transform the summary message sequentially,
55- // and the custom finalizer (set via Custom) determines the final output messages.
72+ // Deprecated: use TypedFinalizerBuilder[*schema.Message] instead.
73+ type FinalizerBuilder = TypedFinalizerBuilder [* schema.Message ]
74+
75+ // NewTypedFinalizer creates a new TypedFinalizerBuilder.
76+ //
77+ // Handlers run in registration order, and DefaultFinalize is applied after all
78+ // handlers have run. For example, with PreserveSkills and a system message in
79+ // originalMessages, the final output is:
80+ //
81+ // [system message, preserved skill message, processed summary]
5682//
5783// Example:
5884//
59- // finalizer, err := NewFinalizer ().
85+ // finalizer, err := NewTypedFinalizer[*schema.Message] ().
6086// PreserveSkills(&PreserveSkillsConfig{}).
61- // Custom(func(ctx context.Context, originalMessages []adk.Message, summary adk.Message) ([]adk.Message, error) {
62- // return []adk.Message{schema.SystemMessage("system prompt"), summary}, nil
63- // }).
6487// Build()
6588//
6689// cfg := &Config{
6790// Finalize: finalizer,
6891// // ...
6992// }
70- type TypedFinalizerBuilder [M adk.MessageType ] struct {
71- handlers []TypedFinalizeFunc [M ]
72- custom TypedFinalizeFunc [M ]
73- errs []error
74- }
75-
76- // FinalizerBuilder is a backward-compatible alias for TypedFinalizerBuilder
77- // specialized with *schema.Message.
78- type FinalizerBuilder = TypedFinalizerBuilder [* schema.Message ]
79-
80- // NewTypedFinalizer creates a new TypedFinalizerBuilder that builds a TypedFinalizeFunc
81- // by chaining handlers and an optional custom finalizer.
8293func NewTypedFinalizer [M adk.MessageType ]() * TypedFinalizerBuilder [M ] {
8394 return & TypedFinalizerBuilder [M ]{}
8495}
8596
8697// NewFinalizer creates a new FinalizerBuilder that builds a FinalizeFunc
87- // by chaining handlers and an optional custom finalizer.
98+ // by chaining handlers.
99+ //
100+ // Deprecated: use NewTypedFinalizer[*schema.Message] instead.
88101func NewFinalizer () * FinalizerBuilder {
89102 return & FinalizerBuilder {}
90103}
91104
92- // Custom sets a custom finalizer that determines the final output messages.
93- // If called multiple times, the last custom finalizer takes effect.
94- func (b * TypedFinalizerBuilder [M ]) Custom (fn TypedFinalizeFunc [M ]) * TypedFinalizerBuilder [M ] {
95- b .custom = fn
96- return b
97- }
98-
99105// Build constructs the final TypedFinalizeFunc by chaining all registered handlers
100- // and the optional custom finalizer.
106+ // and applying DefaultFinalize semantics on the result.
107+ // For example, with PreserveSkills and a system message in
108+ // originalMessages, the final output is:
109+ //
110+ // [system message, preserved skill message, processed summary]
101111func (b * TypedFinalizerBuilder [M ]) Build () (TypedFinalizeFunc [M ], error ) {
102112 if len (b .errs ) > 0 {
103113 msgs := make ([]string , len (b .errs ))
@@ -107,28 +117,44 @@ func (b *TypedFinalizerBuilder[M]) Build() (TypedFinalizeFunc[M], error) {
107117 return nil , fmt .Errorf ("failed to build finalizer:\n %s" , strings .Join (msgs , "\n " ))
108118 }
109119
110- if len (b .handlers ) == 0 && b . custom == nil {
111- return nil , fmt .Errorf ("at least one handler or custom finalizer is required" )
120+ if len (b .handlers ) == 0 {
121+ return nil , fmt .Errorf ("at least one handler is required" )
112122 }
113123
114124 handlers := make ([]TypedFinalizeFunc [M ], len (b .handlers ))
115125 copy (handlers , b .handlers )
116- custom := b .custom
117126
118127 return func (ctx context.Context , originalMessages []M , summary M ) ([]M , error ) {
128+ var extraMessages []M
119129 for _ , fn := range handlers {
120130 result , err := fn (ctx , originalMessages , summary )
121131 if err != nil {
122132 return nil , err
123133 }
124- summary = result [0 ]
134+ if len (result ) == 0 {
135+ return nil , fmt .Errorf ("finalizer handler returned no messages" )
136+ }
137+ extraMessages = append (extraMessages , result [:len (result )- 1 ]... )
138+ summary = result [len (result )- 1 ]
139+ }
140+
141+ baseResult , err := DefaultFinalize (ctx , originalMessages , summary )
142+ if err != nil {
143+ return nil , err
125144 }
126145
127- if custom != nil {
128- return custom ( ctx , originalMessages , summary )
146+ if len ( extraMessages ) == 0 {
147+ return baseResult , nil
129148 }
130149
131- return []M {summary }, nil
150+ systemMsgs , _ := splitSystemAndContextMsgs (baseResult )
151+ processedSummary := baseResult [len (baseResult )- 1 ]
152+
153+ result := make ([]M , 0 , len (systemMsgs )+ len (extraMessages )+ 1 )
154+ result = append (result , systemMsgs ... )
155+ result = append (result , extraMessages ... )
156+ result = append (result , processedSummary )
157+ return result , nil
132158 }, nil
133159}
134160
@@ -159,9 +185,18 @@ type PreserveSkillsConfig struct {
159185 SkillsTokenBudget * int
160186}
161187
162- // PreserveSkills extracts skill contents loaded by the ADK skill middleware
163- // from the conversation history and prepends them to the summary message,
164- // ensuring the agent retains skill knowledge after the context window is compacted.
188+ // PreserveSkills preserves skill contents loaded by the ADK skill middleware.
189+ // It scans the conversation for matching skill tool calls and returns the preserved
190+ // skill content as a user message before the summary.
191+ //
192+ // Example:
193+ //
194+ // messages: [assistant(tool_call: skill "foo"), tool(content: "bar")]
195+ // summary: S
196+ //
197+ // When skill content is found, PreserveSkills returns:
198+ //
199+ // []M{user("<preserved foo: bar>"), S}
165200func (b * TypedFinalizerBuilder [M ]) PreserveSkills (config * PreserveSkillsConfig ) * TypedFinalizerBuilder [M ] {
166201 if err := config .check (); err != nil {
167202 b .errs = append (b .errs , fmt .Errorf ("PreserveSkills: %w" , err ))
@@ -184,35 +219,17 @@ func (b *TypedFinalizerBuilder[M]) PreserveSkills(config *PreserveSkillsConfig)
184219 return nil , err
185220 }
186221
187- if skillText ! = "" {
188- summary = prependMsgTextContent ( summary , skillText )
222+ if skillText = = "" {
223+ return [] M { summary }, nil
189224 }
190225
191- return []M {summary }, nil
226+ preserved := makeUserMsg [M ](skillText )
227+ setMsgExtra (preserved , extraKeyContentType , string (contentTypeSkills ))
228+ return []M {preserved , summary }, nil
192229 })
193230 return b
194231}
195232
196- func prependMsgTextContent [M adk.MessageType ](msg M , text string ) M {
197- switch m := any (msg ).(type ) {
198- case * schema.Message :
199- m .UserInputMultiContent = append ([]schema.MessageInputPart {
200- {
201- Type : schema .ChatMessagePartTypeText ,
202- Text : text ,
203- },
204- }, m .UserInputMultiContent ... )
205- return any (m ).(M )
206- case * schema.AgenticMessage :
207- m .ContentBlocks = append ([]* schema.ContentBlock {
208- schema .NewContentBlock (& schema.UserInputText {Text : text }),
209- }, m .ContentBlocks ... )
210- return any (m ).(M )
211- default :
212- panic ("unreachable" )
213- }
214- }
215-
216233func (c * PreserveSkillsConfig ) check () error {
217234 if c == nil {
218235 return fmt .Errorf ("PreserveSkillsConfig is required" )
0 commit comments