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_message_threshold": 20,
|
||||||
"summarize_token_percent": 75,
|
"summarize_token_percent": 75,
|
||||||
"split_on_marker": false,
|
"split_on_marker": false,
|
||||||
|
"context_manager": "legacy",
|
||||||
|
"context_manager_config": {
|
||||||
|
"_comment": "Used by the seahorse context manager",
|
||||||
|
"fresh_tail_size": 32
|
||||||
|
},
|
||||||
"tool_feedback": {
|
"tool_feedback": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"max_args_length": 300,
|
"max_args_length": 300,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ type seahorseContextManager struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSeahorseContextManager creates a seahorse-backed ContextManager.
|
// 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 {
|
if al == nil {
|
||||||
return nil, fmt.Errorf("seahorse: AgentLoop is required")
|
return nil, fmt.Errorf("seahorse: AgentLoop is required")
|
||||||
}
|
}
|
||||||
|
|
@ -32,13 +32,19 @@ func newSeahorseContextManager(_ json.RawMessage, al *AgentLoop) (ContextManager
|
||||||
agent := al.registry.GetDefaultAgent()
|
agent := al.registry.GetDefaultAgent()
|
||||||
dbPath := agent.Workspace + "/sessions/seahorse.db"
|
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
|
// Create CompleteFn from provider
|
||||||
completeFn := providerToCompleteFn(agent.Provider, agent.Model)
|
completeFn := providerToCompleteFn(agent.Provider, agent.Model)
|
||||||
|
|
||||||
// Create engine
|
// Create engine
|
||||||
engine, err := seahorse.NewEngine(seahorse.Config{
|
engine, err := seahorse.NewEngine(seahorseConfig, completeFn)
|
||||||
DBPath: dbPath,
|
|
||||||
}, completeFn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("seahorse: create engine: %w", err)
|
return nil, fmt.Errorf("seahorse: create engine: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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) {
|
func TestProviderToSeahorseMessage(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ func (a *Assembler) Assemble(ctx context.Context, convID int64, input AssembleIn
|
||||||
resolved[i] = r
|
resolved[i] = r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split into evictable prefix and protected fresh tail
|
// Split into evictable prefix and protected fresh tail.
|
||||||
tailStart := len(resolved) - FreshTailCount
|
tailStart := len(resolved) - a.config.GetFreshTailSize()
|
||||||
if tailStart < 0 {
|
if tailStart < 0 {
|
||||||
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) {
|
func TestAssemblerSummaryXMLFormat(t *testing.T) {
|
||||||
s, convID := setupAssemblerStore(t)
|
s, convID := setupAssemblerStore(t)
|
||||||
ctx := context.Background()
|
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.
|
// 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) {
|
func (e *CompactionEngine) compactLeaf(ctx context.Context, convID int64, force ...bool) (*string, error) {
|
||||||
items, err := e.store.GetContextItems(ctx, convID)
|
items, err := e.store.GetContextItems(ctx, convID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -191,7 +191,7 @@ func (e *CompactionEngine) compactLeaf(ctx context.Context, convID int64, force
|
||||||
|
|
||||||
// Calculate fresh tail boundary (bypass when forced)
|
// Calculate fresh tail boundary (bypass when forced)
|
||||||
useForce := len(force) > 0 && force[0]
|
useForce := len(force) > 0 && force[0]
|
||||||
tailStartIdx := len(items) - FreshTailCount
|
tailStartIdx := len(items) - e.config.GetFreshTailSize()
|
||||||
if useForce {
|
if useForce {
|
||||||
tailStartIdx = len(items) // allow compacting everything
|
tailStartIdx = len(items) // allow compacting everything
|
||||||
}
|
}
|
||||||
|
|
@ -465,7 +465,7 @@ func (e *CompactionEngine) selectShallowestCondensationCandidate(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group by depth, find consecutive runs
|
// Group by depth, find consecutive runs
|
||||||
tailStartIdx := len(items) - FreshTailCount
|
tailStartIdx := len(items) - e.config.GetFreshTailSize()
|
||||||
if tailStartIdx < 0 {
|
if tailStartIdx < 0 {
|
||||||
tailStartIdx = 0
|
tailStartIdx = 0
|
||||||
}
|
}
|
||||||
|
|
@ -527,7 +527,7 @@ func (e *CompactionEngine) selectOldestChunkAtDepth(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tailStartIdx := len(items) - FreshTailCount
|
tailStartIdx := len(items) - e.config.GetFreshTailSize()
|
||||||
if tailStartIdx < 0 {
|
if tailStartIdx < 0 {
|
||||||
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) {
|
func TestCompactLeafNoCandidate(t *testing.T) {
|
||||||
ce, _, convID := newTestCompactionEngine(t)
|
ce, _, convID := newTestCompactionEngine(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@ type Config struct {
|
||||||
DBPath string `json:"dbPath"`
|
DBPath string `json:"dbPath"`
|
||||||
IgnoreSessionPatterns []string `json:"ignoreSessionPatterns,omitempty"`
|
IgnoreSessionPatterns []string `json:"ignoreSessionPatterns,omitempty"`
|
||||||
StatelessSessionPatterns []string `json:"statelessSessionPatterns,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.
|
// 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 ---
|
// --- Ingest ---
|
||||||
|
|
||||||
func TestEngineIngest(t *testing.T) {
|
func TestEngineIngest(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue