Merge feb4feac89 into f334ac6d01
This commit is contained in:
commit
362bb55016
9 changed files with 148 additions and 10 deletions
|
|
@ -11,6 +11,11 @@
|
|||
"summarize_message_threshold": 20,
|
||||
"summarize_token_percent": 75,
|
||||
"split_on_marker": false,
|
||||
"context_manager": "legacy",
|
||||
"context_manager_config": {
|
||||
"_comment": "Used by the seahorse context manager",
|
||||
"fresh_tail_size": 32
|
||||
},
|
||||
"tool_feedback": {
|
||||
"enabled": false,
|
||||
"max_args_length": 300,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ type seahorseContextManager struct {
|
|||
}
|
||||
|
||||
// newSeahorseContextManager creates a seahorse-backed ContextManager.
|
||||
func newSeahorseContextManager(_ json.RawMessage, al *AgentLoop) (ContextManager, error) {
|
||||
func newSeahorseContextManager(cfg json.RawMessage, al *AgentLoop) (ContextManager, error) {
|
||||
if al == nil {
|
||||
return nil, fmt.Errorf("seahorse: AgentLoop is required")
|
||||
}
|
||||
|
|
@ -32,13 +32,19 @@ func newSeahorseContextManager(_ json.RawMessage, al *AgentLoop) (ContextManager
|
|||
agent := al.registry.GetDefaultAgent()
|
||||
dbPath := agent.Workspace + "/sessions/seahorse.db"
|
||||
|
||||
seahorseConfig := seahorse.Config{DBPath: dbPath}
|
||||
if len(cfg) > 0 {
|
||||
if err := json.Unmarshal(cfg, &seahorseConfig); err != nil {
|
||||
return nil, fmt.Errorf("seahorse: decode config: %w", err)
|
||||
}
|
||||
seahorseConfig.DBPath = dbPath
|
||||
}
|
||||
|
||||
// Create CompleteFn from provider
|
||||
completeFn := providerToCompleteFn(agent.Provider, agent.Model)
|
||||
|
||||
// Create engine
|
||||
engine, err := seahorse.NewEngine(seahorse.Config{
|
||||
DBPath: dbPath,
|
||||
}, completeFn)
|
||||
engine, err := seahorse.NewEngine(seahorseConfig, completeFn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seahorse: create engine: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package agent
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -46,6 +47,62 @@ func TestSeahorseCMRegistration(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSeahorseContextManagerConfigFreshTailSize(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: t.TempDir(),
|
||||
ModelName: "test-model",
|
||||
MaxTokens: 4096,
|
||||
MaxToolIterations: 10,
|
||||
ContextManager: "seahorse",
|
||||
ContextManagerConfig: json.RawMessage(`{"fresh_tail_size":2}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
al := NewAgentLoop(cfg, msgBus, &simpleMockProvider{response: "ok"})
|
||||
|
||||
seahorseCM, ok := al.contextManager.(*seahorseContextManager)
|
||||
if !ok {
|
||||
t.Fatal("expected seahorseContextManager")
|
||||
}
|
||||
|
||||
store := seahorseCM.engine.GetRetrieval().Store()
|
||||
ctx := context.Background()
|
||||
conv, err := store.GetOrCreateConversation(ctx, "fresh-tail-config")
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreateConversation: %v", err)
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
msg, addErr := store.AddMessage(ctx, conv.ConversationID, "user", fmt.Sprintf("msg %d", i), 10)
|
||||
if addErr != nil {
|
||||
t.Fatalf("AddMessage %d: %v", i, addErr)
|
||||
}
|
||||
if appendErr := store.AppendContextMessage(ctx, conv.ConversationID, msg.ID); appendErr != nil {
|
||||
t.Fatalf("AppendContextMessage %d: %v", i, appendErr)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := seahorseCM.Assemble(ctx, &AssembleRequest{
|
||||
SessionKey: "fresh-tail-config",
|
||||
Budget: 20,
|
||||
MaxTokens: 0,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.History) != 2 {
|
||||
t.Fatalf("History = %d, want 2", len(resp.History))
|
||||
}
|
||||
if resp.History[0].Content != "msg 3" {
|
||||
t.Errorf("first history message = %q, want %q", resp.History[0].Content, "msg 3")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderToSeahorseMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ func (a *Assembler) Assemble(ctx context.Context, convID int64, input AssembleIn
|
|||
resolved[i] = r
|
||||
}
|
||||
|
||||
// Split into evictable prefix and protected fresh tail
|
||||
tailStart := len(resolved) - FreshTailCount
|
||||
// Split into evictable prefix and protected fresh tail.
|
||||
tailStart := len(resolved) - a.config.GetFreshTailSize()
|
||||
if tailStart < 0 {
|
||||
tailStart = 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,6 +197,38 @@ func TestAssemblerBudgetFitsAll(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAssemblerUsesConfiguredFreshTailSize(t *testing.T) {
|
||||
s, convID := setupAssemblerStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
msgs := make([]*Message, 6)
|
||||
items := make([]ContextItem, 6)
|
||||
for i := 0; i < 6; i++ {
|
||||
m, _ := s.AddMessage(ctx, convID, "user", "msg", 10)
|
||||
msgs[i] = m
|
||||
items[i] = ContextItem{
|
||||
Ordinal: (i + 1) * 100,
|
||||
ItemType: "message",
|
||||
MessageID: m.ID,
|
||||
TokenCount: 10,
|
||||
}
|
||||
}
|
||||
s.UpsertContextItems(ctx, convID, items)
|
||||
|
||||
a := &Assembler{store: s, config: Config{FreshTailSize: 3}}
|
||||
result, err := a.Assemble(ctx, convID, AssembleInput{Budget: 30})
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Messages) != 3 {
|
||||
t.Fatalf("Messages = %d, want 3", len(result.Messages))
|
||||
}
|
||||
if result.Messages[0].ID != msgs[3].ID {
|
||||
t.Errorf("first message ID = %d, want %d", result.Messages[0].ID, msgs[3].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssemblerSummaryXMLFormat(t *testing.T) {
|
||||
s, convID := setupAssemblerStore(t)
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ func (e *CompactionEngine) CompactUntilUnder(ctx context.Context, convID int64,
|
|||
}
|
||||
|
||||
// compactLeaf compresses the oldest contiguous message chunk into a leaf summary.
|
||||
// When force is true, FreshTailCount protection is bypassed (used by CompactUntilUnder).
|
||||
// When force is true, fresh tail protection is bypassed (used by CompactUntilUnder).
|
||||
func (e *CompactionEngine) compactLeaf(ctx context.Context, convID int64, force ...bool) (*string, error) {
|
||||
items, err := e.store.GetContextItems(ctx, convID)
|
||||
if err != nil {
|
||||
|
|
@ -191,7 +191,7 @@ func (e *CompactionEngine) compactLeaf(ctx context.Context, convID int64, force
|
|||
|
||||
// Calculate fresh tail boundary (bypass when forced)
|
||||
useForce := len(force) > 0 && force[0]
|
||||
tailStartIdx := len(items) - FreshTailCount
|
||||
tailStartIdx := len(items) - e.config.GetFreshTailSize()
|
||||
if useForce {
|
||||
tailStartIdx = len(items) // allow compacting everything
|
||||
}
|
||||
|
|
@ -465,7 +465,7 @@ func (e *CompactionEngine) selectShallowestCondensationCandidate(
|
|||
}
|
||||
|
||||
// Group by depth, find consecutive runs
|
||||
tailStartIdx := len(items) - FreshTailCount
|
||||
tailStartIdx := len(items) - e.config.GetFreshTailSize()
|
||||
if tailStartIdx < 0 {
|
||||
tailStartIdx = 0
|
||||
}
|
||||
|
|
@ -527,7 +527,7 @@ func (e *CompactionEngine) selectOldestChunkAtDepth(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
tailStartIdx := len(items) - FreshTailCount
|
||||
tailStartIdx := len(items) - e.config.GetFreshTailSize()
|
||||
if tailStartIdx < 0 {
|
||||
tailStartIdx = 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,25 @@ func TestCompactLeaf(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCompactLeafUsesConfiguredFreshTailSize(t *testing.T) {
|
||||
ce, s, convID := newTestCompactionEngine(t)
|
||||
ce.config = Config{FreshTailSize: 2}
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
m, _ := s.AddMessage(ctx, convID, "user", "message content for compaction test", 100)
|
||||
s.AppendContextMessage(ctx, convID, m.ID)
|
||||
}
|
||||
|
||||
summaryID, err := ce.compactLeaf(ctx, convID)
|
||||
if err != nil {
|
||||
t.Fatalf("compactLeaf: %v", err)
|
||||
}
|
||||
if summaryID == nil {
|
||||
t.Fatal("expected compactLeaf to compact with custom fresh tail size")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompactLeafNoCandidate(t *testing.T) {
|
||||
ce, _, convID := newTestCompactionEngine(t)
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ type Config struct {
|
|||
DBPath string `json:"dbPath"`
|
||||
IgnoreSessionPatterns []string `json:"ignoreSessionPatterns,omitempty"`
|
||||
StatelessSessionPatterns []string `json:"statelessSessionPatterns,omitempty"`
|
||||
FreshTailSize int `json:"fresh_tail_size,omitempty"`
|
||||
}
|
||||
|
||||
// GetFreshTailSize returns the configured fresh tail size or the default.
|
||||
func (c Config) GetFreshTailSize() int {
|
||||
if c.FreshTailSize > 0 {
|
||||
return c.FreshTailSize
|
||||
}
|
||||
return FreshTailCount
|
||||
}
|
||||
|
||||
// CompleteFn is the LLM completion function type.
|
||||
|
|
|
|||
|
|
@ -172,6 +172,16 @@ func TestNewEngineWithPatterns(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConfigGetFreshTailSize(t *testing.T) {
|
||||
if got := (Config{}).GetFreshTailSize(); got != FreshTailCount {
|
||||
t.Errorf("default fresh tail size = %d, want %d", got, FreshTailCount)
|
||||
}
|
||||
|
||||
if got := (Config{FreshTailSize: 7}).GetFreshTailSize(); got != 7 {
|
||||
t.Errorf("configured fresh tail size = %d, want 7", got)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Ingest ---
|
||||
|
||||
func TestEngineIngest(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue