mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-29 06:21:01 +00:00
- Add TokenCounter.CountToolSchemas() to measure JSON schema size for all tools - Include tool schemas in OverheadTokens calculation for accurate context usage - Implement dynamic max_tokens: in/25 clamp [1024, 8192] for compaction - Add characterization tests: count_tool_schemas_test.go - Add overhead verification tests: context_stage_overhead_test.go, context_stage_tool_overhead_test.go - Add integration tests: context_stage_integration_test.go - Add compact tests: loop_compact_dynamic_max_test.go, loop_compact_max_tokens_test.go - Add sanitize tests: loop_history_sanitize_max_tokens_test.go - Add integration test: loop_compact_integration_test.go
78 lines
2.6 KiB
Go
78 lines
2.6 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
)
|
|
|
|
// capturingProvider records every ChatRequest passed to Chat.
|
|
// Distinct from stubProvider in intent_classify_test.go (that one ignores the request).
|
|
type capturingProvider struct {
|
|
captured []providers.ChatRequest
|
|
response string
|
|
}
|
|
|
|
func (c *capturingProvider) Chat(_ context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) {
|
|
c.captured = append(c.captured, req)
|
|
return &providers.ChatResponse{Content: c.response}, nil
|
|
}
|
|
func (c *capturingProvider) ChatStream(_ context.Context, req providers.ChatRequest, _ func(providers.StreamChunk)) (*providers.ChatResponse, error) {
|
|
c.captured = append(c.captured, req)
|
|
return &providers.ChatResponse{Content: c.response}, nil
|
|
}
|
|
func (c *capturingProvider) DefaultModel() string { return "capturing-model" }
|
|
func (c *capturingProvider) Name() string { return "capturing" }
|
|
|
|
// TestCompactMessagesInPlace_MaxTokensDynamic verifies that compactMessagesInPlace
|
|
// passes max_tokens == dynamicSummaryMax(estimatedInputTokens) to the provider.
|
|
func TestCompactMessagesInPlace_MaxTokensDynamic(t *testing.T) {
|
|
cap := &capturingProvider{response: "Summary of conversation."}
|
|
|
|
loop := &Loop{
|
|
provider: cap,
|
|
model: "claude-3-5-sonnet",
|
|
// tokenCounter nil → estimateSummaryInputTokens uses rune/3 fallback
|
|
}
|
|
|
|
// Build 10 dummy messages (>= 6 required by compactMessagesInPlace).
|
|
msgs := make([]providers.Message, 10)
|
|
for i := range msgs {
|
|
if i%2 == 0 {
|
|
msgs[i] = providers.Message{Role: "user", Content: "user message"}
|
|
} else {
|
|
msgs[i] = providers.Message{Role: "assistant", Content: "assistant reply"}
|
|
}
|
|
}
|
|
|
|
result := loop.compactMessagesInPlace(context.Background(), msgs)
|
|
if result == nil {
|
|
t.Fatal("compactMessagesInPlace returned nil; expected compaction to succeed")
|
|
}
|
|
|
|
if len(cap.captured) != 1 {
|
|
t.Fatalf("provider.Chat called %d time(s), want 1", len(cap.captured))
|
|
}
|
|
|
|
req := cap.captured[0]
|
|
maxTokensRaw, ok := req.Options["max_tokens"]
|
|
if !ok {
|
|
t.Fatal("Options[\"max_tokens\"] not set in ChatRequest")
|
|
}
|
|
|
|
maxTokens, ok := maxTokensRaw.(int)
|
|
if !ok {
|
|
t.Fatalf("Options[\"max_tokens\"] type = %T, want int", maxTokensRaw)
|
|
}
|
|
|
|
// Compute expected using the same formula the implementation uses.
|
|
// With keepCount=4 and 10 messages, splitIdx=6 (first 6 messages summarised).
|
|
// tokenCounter nil → rune/3 fallback.
|
|
expectedIn := loop.estimateSummaryInputTokens(msgs[:6])
|
|
wantMax := dynamicSummaryMax(expectedIn)
|
|
if maxTokens != wantMax {
|
|
t.Errorf("max_tokens = %d, want %d (dynamicSummaryMax(%d))", maxTokens, wantMax, expectedIn)
|
|
}
|
|
}
|