mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 10:21:47 +00:00
Port goclaw context pruning to match upstream TS design in
openclaw/src/agents/pi-hooks/context-pruning/:
- Opt-in default: prune only when mode="cache-ttl" (was opt-out)
- Remove Pass 0 per-result 30% guard (duplicated Pass 1 with different
suffix, caused wobble)
- Dedupe double prune call per iteration: PruneStage owns the single
entry point; loop_history only runs limitHistoryTurns + sanitizeHistory
- Add cache-TTL gate for Anthropic prompt cache: skip prune while cache
is live, scoped per-session via sync.Map
- Add context.pruned event emission for observability
- Configurable TTL as Go duration string ("5m", "30s")
BREAKING CHANGE: context pruning now opt-in. Add
contextPruning.mode: "cache-ttl" to config.agents.defaults to restore.
Migration 51 / SQLite v19 backfills mode="cache-ttl" for agents with
existing custom context_pruning config missing the mode field, so
previously-configured agents keep pruning after the opt-in flip.
NULL configs stay NULL (new opt-in default applies).
Web UI adds Cache TTL input + toggle wiring mode to cache-ttl/off.
63 lines
2.3 KiB
Go
63 lines
2.3 KiB
Go
//go:build !sqliteonly
|
|
|
|
package agent
|
|
|
|
import (
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/pipeline"
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
)
|
|
|
|
// TestPruneStage_SingleEntryPoint verifies that after Phase 03 refactoring:
|
|
// - loop_history.go no longer calls pruneContextMessages
|
|
// - PruneStage owns the single entry point for pruning
|
|
// - SanitizeHistory is called after PruneMessages in PruneStage
|
|
//
|
|
// Full PruneStage tests are in internal/pipeline/stages_test.go.
|
|
func TestPruneStage_SingleEntryPoint(t *testing.T) {
|
|
// Verify loop_history's sanitizeHistory works on trimmed input directly.
|
|
// This proves pruneContextMessages is no longer called in that path.
|
|
history := []providers.Message{
|
|
{Role: "user", Content: "q1"},
|
|
{Role: "assistant", Content: "", ToolCalls: []providers.ToolCall{{ID: "tc1", Name: "read"}}},
|
|
{Role: "tool", ToolCallID: "tc1", Content: "result1"},
|
|
// Orphan tool result (no matching tool_use) - sanitizeHistory should drop it
|
|
{Role: "tool", ToolCallID: "tc_orphan", Content: "orphan result"},
|
|
{Role: "assistant", Content: "a1"},
|
|
}
|
|
|
|
// limitHistoryTurns doesn't drop the orphan, but sanitizeHistory does
|
|
trimmed := limitHistoryTurns(history, 100)
|
|
if len(trimmed) != len(history) {
|
|
t.Fatalf("limitHistoryTurns altered message count: got %d, want %d", len(trimmed), len(history))
|
|
}
|
|
|
|
sanitized, droppedCount := sanitizeHistory(trimmed)
|
|
if droppedCount != 1 {
|
|
t.Errorf("sanitizeHistory should drop 1 orphan, got droppedCount=%d", droppedCount)
|
|
}
|
|
if len(sanitized) != 4 {
|
|
t.Errorf("expected 4 messages after sanitize, got %d", len(sanitized))
|
|
}
|
|
}
|
|
|
|
// TestPruneCallback_CountsOnce demonstrates the callback structure supports counting.
|
|
// Actual single-call verification is in pipeline/stages_test.go.
|
|
func TestPruneCallback_CountsOnce(t *testing.T) {
|
|
var count int32
|
|
fakePrune := func(msgs []providers.Message, budget int) ([]providers.Message, pipeline.PruneStats) {
|
|
atomic.AddInt32(&count, 1)
|
|
return msgs, pipeline.PruneStats{}
|
|
}
|
|
|
|
// Simulate what PruneStage does: call PruneMessages once
|
|
msgs := []providers.Message{{Role: "user", Content: "test"}}
|
|
_, _ = fakePrune(msgs, 10000)
|
|
|
|
if got := atomic.LoadInt32(&count); got != 1 {
|
|
t.Errorf("expected prune called exactly once, got %d", got)
|
|
}
|
|
}
|