Skip to content

Commit 151d0b8

Browse files
committed
refactor: streamline summarization finalizers
1 parent e4abd02 commit 151d0b8

6 files changed

Lines changed: 176 additions & 189 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ reports/
5555
CLAUDE.md
5656
*.jsonl
5757
*.txt
58+
.agents/
5859

5960
# Specs directories
6061
*/specs

adk/middlewares/summarization/consts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type summarizationContentType string
3333

3434
const (
3535
contentTypeSummary summarizationContentType = "summary"
36+
contentTypeSkills summarizationContentType = "skills"
3637
)
3738

3839
type ctxKeyModelInput struct{}

adk/middlewares/summarization/finalizer_builder.go

Lines changed: 70 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
8285
func 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.
8893
func 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]
101103
func (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}
165190
func (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-
216223
func (c *PreserveSkillsConfig) check() error {
217224
if c == nil {
218225
return fmt.Errorf("PreserveSkillsConfig is required")

0 commit comments

Comments
 (0)