Skip to content

Commit e86f427

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

6 files changed

Lines changed: 141 additions & 192 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: 81 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
3738
func 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.
8293
func 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.
88101
func 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]
101111
func (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}
165200
func (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-
216233
func (c *PreserveSkillsConfig) check() error {
217234
if c == nil {
218235
return fmt.Errorf("PreserveSkillsConfig is required")

0 commit comments

Comments
 (0)