@@ -45,59 +45,61 @@ func DefaultFinalize[M adk.MessageType](ctx context.Context, originalMessages []
4545 return nil , err
4646 }
4747
48- return append (systemMsgs , processed ), nil
48+ result := make ([]M , 0 , len (systemMsgs )+ 1 )
49+ result = append (result , systemMsgs ... )
50+ result = append (result , processed )
51+ return result , nil
4952}
5053
51- // TypedFinalizerBuilder builds a TypedFinalizeFunc by chaining handlers
52- // and an optional custom finalizer, generic over message type M.
54+ // TypedFinalizerBuilder builds a TypedFinalizeFunc by chaining handlers,
55+ // generic over message type M.
56+ type TypedFinalizerBuilder [M adk.MessageType ] struct {
57+ handlers []TypedFinalizeFunc [M ]
58+ errs []error
59+ }
60+
61+ // FinalizerBuilder is a backward-compatible alias for TypedFinalizerBuilder
62+ // specialized with *schema.Message.
63+ //
64+ // Deprecated: use TypedFinalizerBuilder[*schema.Message] instead.
65+ type FinalizerBuilder = TypedFinalizerBuilder [* schema.Message ]
66+
67+ // NewTypedFinalizer creates a new TypedFinalizerBuilder.
5368//
54- // Handlers (e.g. PreserveSkills) transform the summary message sequentially,
55- // and the custom finalizer (set via Custom) determines the final output messages.
69+ // Handlers run in registration order, and the summary message is post-processed
70+ // as DefaultFinalize does after all handlers have run. For example, with
71+ // PreserveSkills and a system message in originalMessages, the final output is:
72+ //
73+ // [system message, preserved skill message, processed summary]
5674//
5775// Example:
5876//
59- // finalizer, err := NewFinalizer ().
77+ // finalizer, err := NewTypedFinalizer[*schema.Message] ().
6078// 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- // }).
6479// Build()
6580//
6681// cfg := &Config{
6782// Finalize: finalizer,
6883// // ...
6984// }
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.
8285func NewTypedFinalizer [M adk.MessageType ]() * TypedFinalizerBuilder [M ] {
8386 return & TypedFinalizerBuilder [M ]{}
8487}
8588
8689// NewFinalizer creates a new FinalizerBuilder that builds a FinalizeFunc
87- // by chaining handlers and an optional custom finalizer.
90+ // by chaining handlers.
91+ //
92+ // Deprecated: use NewTypedFinalizer[*schema.Message] instead.
8893func NewFinalizer () * FinalizerBuilder {
8994 return & FinalizerBuilder {}
9095}
9196
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-
9997// Build constructs the final TypedFinalizeFunc by chaining all registered handlers
100- // and the optional custom finalizer.
98+ // and post-processing the summary message as DefaultFinalize does.
99+ // For example, with PreserveSkills and a system message in
100+ // originalMessages, the final output is:
101+ //
102+ // [system message, preserved skill message, processed summary]
101103func (b * TypedFinalizerBuilder [M ]) Build () (TypedFinalizeFunc [M ], error ) {
102104 if len (b .errs ) > 0 {
103105 msgs := make ([]string , len (b .errs ))
@@ -107,28 +109,42 @@ func (b *TypedFinalizerBuilder[M]) Build() (TypedFinalizeFunc[M], error) {
107109 return nil , fmt .Errorf ("failed to build finalizer:\n %s" , strings .Join (msgs , "\n " ))
108110 }
109111
110- if len (b .handlers ) == 0 && b . custom == nil {
111- return nil , fmt .Errorf ("at least one handler or custom finalizer is required" )
112+ if len (b .handlers ) == 0 {
113+ return nil , fmt .Errorf ("at least one handler is required" )
112114 }
113115
114116 handlers := make ([]TypedFinalizeFunc [M ], len (b .handlers ))
115117 copy (handlers , b .handlers )
116- custom := b .custom
117118
118119 return func (ctx context.Context , originalMessages []M , summary M ) ([]M , error ) {
120+ var extraMessages []M
119121 for _ , fn := range handlers {
120122 result , err := fn (ctx , originalMessages , summary )
121123 if err != nil {
122124 return nil , err
123125 }
124- summary = result [0 ]
126+ if len (result ) == 0 {
127+ return nil , fmt .Errorf ("finalizer handler returned no messages" )
128+ }
129+ extraMessages = append (extraMessages , result [:len (result )- 1 ]... )
130+ summary = result [len (result )- 1 ]
125131 }
126132
127- if custom != nil {
128- return custom (ctx , originalMessages , summary )
133+ systemMsgs , contextMsgs := splitSystemAndContextMsgs (originalMessages )
134+
135+ processed , err := postProcessSummary (ctx , & postProcessSummaryParams [M ]{
136+ contextMsgs : contextMsgs ,
137+ summaryContent : getAssistantTextContent (summary ),
138+ })
139+ if err != nil {
140+ return nil , err
129141 }
130142
131- return []M {summary }, nil
143+ result := make ([]M , 0 , len (systemMsgs )+ len (extraMessages )+ 1 )
144+ result = append (result , systemMsgs ... )
145+ result = append (result , extraMessages ... )
146+ result = append (result , processed )
147+ return result , nil
132148 }, nil
133149}
134150
@@ -159,9 +175,18 @@ type PreserveSkillsConfig struct {
159175 SkillsTokenBudget * int
160176}
161177
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.
178+ // PreserveSkills preserves skill contents loaded by the ADK skill middleware.
179+ // It scans the conversation for matching skill tool calls and returns the preserved
180+ // skill content as a user message before the summary.
181+ //
182+ // Example:
183+ //
184+ // messages: [assistant(tool_call: skill "foo"), tool(content: "bar")]
185+ // summary: S
186+ //
187+ // When skill content is found, PreserveSkills returns:
188+ //
189+ // []M{user("<preserved foo: bar>"), S}
165190func (b * TypedFinalizerBuilder [M ]) PreserveSkills (config * PreserveSkillsConfig ) * TypedFinalizerBuilder [M ] {
166191 if err := config .check (); err != nil {
167192 b .errs = append (b .errs , fmt .Errorf ("PreserveSkills: %w" , err ))
@@ -184,35 +209,17 @@ func (b *TypedFinalizerBuilder[M]) PreserveSkills(config *PreserveSkillsConfig)
184209 return nil , err
185210 }
186211
187- if skillText ! = "" {
188- summary = prependMsgTextContent ( summary , skillText )
212+ if skillText = = "" {
213+ return [] M { summary }, nil
189214 }
190215
191- return []M {summary }, nil
216+ preserved := makeUserMsg [M ](skillText )
217+ setMsgExtra (preserved , extraKeyContentType , string (contentTypeSkills ))
218+ return []M {preserved , summary }, nil
192219 })
193220 return b
194221}
195222
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-
216223func (c * PreserveSkillsConfig ) check () error {
217224 if c == nil {
218225 return fmt .Errorf ("PreserveSkillsConfig is required" )
0 commit comments