v0.0.5: context compaction, minimal config, retry/search/code symbols/diff/status/test commands

This commit is contained in:
loveuer
2026-06-24 02:20:19 -07:00
parent e4c75c7c0e
commit 950c63adaa
24 changed files with 1939 additions and 220 deletions
+19 -13
View File
@@ -69,6 +69,7 @@ func (m *Manager) Resume(id string) error {
}
m.currentSession = sess
m.agent.SetMessages(sess.Messages)
m.agent.SetLastUsage(sess.LastUsage)
return nil
}
@@ -78,6 +79,7 @@ func (m *Manager) Save() error {
return nil
}
m.currentSession.Messages = m.agent.Messages()
m.currentSession.LastUsage = m.agent.LastUsage()
m.currentSession.Provider = m.providerName
m.currentSession.Model = m.provider.Model
// Auto-name if empty
@@ -208,11 +210,7 @@ func (m *Manager) Info() string {
fmt.Fprintf(&b, "model: %s\n", m.CurrentModel())
fmt.Fprintf(&b, "thinking: %s\n", m.CurrentThinking())
fmt.Fprintf(&b, "providers: %s\n", strings.Join(m.providerNames(), ", "))
if len(m.provider.Models) > 0 {
fmt.Fprintf(&b, "models: %s\n", strings.Join(m.provider.Models, ", "))
} else {
b.WriteString("models: any model name is accepted for this provider\n")
}
fmt.Fprintf(&b, "models: %s\n", strings.Join(m.provider.Models, ", "))
fmt.Fprintf(&b, "thinking levels: %s\n", strings.Join(config.ThinkingLevels(), ", "))
b.WriteString("usage: /model provider <name> | /model model <name> | /model thinking <level>")
return b.String()
@@ -234,10 +232,10 @@ func (m *Manager) SetModel(model string) (string, error) {
if model == "" {
return "", fmt.Errorf("model is required")
}
if len(m.provider.Models) > 0 && !contains(m.provider.Models, model) {
if !contains(m.provider.Models, model) {
return "", fmt.Errorf("model %q is not configured for provider %q; available: %s", model, m.providerName, strings.Join(m.provider.Models, ", "))
}
m.provider.Model = model
m.provider = m.provider.WithModel(model)
m.cfg.Providers[m.providerName] = m.provider
m.syncAgent(nil)
return "Switched model.\n" + m.summary(), nil
@@ -262,14 +260,22 @@ func (m *Manager) summary() string {
return fmt.Sprintf("provider: %s\nmodel: %s\nthinking: %s", m.CurrentProvider(), m.CurrentModel(), m.CurrentThinking())
}
func (m *Manager) maxContextTokens() int {
if m.cfg != nil && m.cfg.Agent.MaxContextTokens > 0 {
return m.cfg.Agent.MaxContextTokens
}
return m.provider.ContextTokens
}
func (m *Manager) syncAgent(provider llm.Provider) {
m.agent.SetRuntime(agent.RuntimeOptions{
Provider: provider,
ProviderName: m.providerName,
Model: m.provider.Model,
Thinking: m.provider.Thinking,
ThinkingParam: m.provider.ThinkingParam,
ThinkingEnabled: m.provider.ThinkingConfigured,
Provider: provider,
ProviderName: m.providerName,
Model: m.provider.Model,
Thinking: m.provider.Thinking,
ThinkingParam: m.provider.ThinkingParam,
ThinkingEnabled: m.provider.ThinkingConfigured,
MaxContextTokens: m.maxContextTokens(),
})
}
+67
View File
@@ -116,6 +116,7 @@ func TestManagerSaveAndResume(t *testing.T) {
manager.newSession()
var out strings.Builder
assistant.RunTurn(context.Background(), "hello world", &out, nil)
assistant.SetLastUsage(&llm.Usage{PromptTokens: 10, CompletionTokens: 5, TotalTokens: 15})
// Save
if err := manager.Save(); err != nil {
@@ -153,6 +154,9 @@ func TestManagerSaveAndResume(t *testing.T) {
if !hasUser {
t.Fatal("resumed session should contain original user message")
}
if usage := assistant.LastUsage(); usage == nil || usage.TotalTokens != 15 {
t.Fatalf("resumed usage = %#v", usage)
}
}
func TestManagerRename(t *testing.T) {
@@ -264,3 +268,66 @@ func TestManagerResumeLatest(t *testing.T) {
t.Fatalf("expected %s, got %s", id1, manager.CurrentSessionID())
}
}
type compactingProvider struct {
summaryCalls int
answerCalls int
}
func (p *compactingProvider) ChatStream(ctx context.Context, req llm.ChatRequest, emit func(llm.StreamEvent) error) error {
if len(req.Messages) == 2 && req.Messages[0].Role == llm.RoleSystem {
p.summaryCalls++
return emit(llm.StreamEvent{Content: "summary"})
}
p.answerCalls++
return emit(llm.StreamEvent{Content: "answer"})
}
func TestManagerUsesModelContextForAgentCompaction(t *testing.T) {
provider := &compactingProvider{}
cfg := &config.Config{Providers: map[string]config.ProviderConfig{
"one": {
Name: "one",
BaseURL: "https://one.example.com",
APIKey: "sk-test",
Model: "model-a",
Models: []string{"model-a"},
ModelConfigs: map[string]config.ModelConfig{"model-a": {ID: "model-a", Thinking: "none", ContextTokens: 1}},
ContextTokens: 1,
},
}}
assistant := agent.New(agent.Options{Provider: provider, ProviderName: "one", Model: "model-a"})
store, err := NewStore(filepath.Join(t.TempDir(), "sessions"))
if err != nil {
t.Fatal(err)
}
manager, err := NewManager(cfg, assistant, nil, store)
if err != nil {
t.Fatal(err)
}
if _, err := manager.SetModel("model-a"); err != nil {
t.Fatal(err)
}
assistant.SetMessages([]llm.Message{
{Role: llm.RoleSystem, Content: "system"},
{Role: llm.RoleUser, Content: "old question 1"},
{Role: llm.RoleAssistant, Content: "old answer 1"},
{Role: llm.RoleUser, Content: "old question 2"},
{Role: llm.RoleAssistant, Content: "old answer 2"},
{Role: llm.RoleUser, Content: "recent question 1"},
{Role: llm.RoleAssistant, Content: "recent answer 1"},
{Role: llm.RoleUser, Content: "recent question 2"},
{Role: llm.RoleAssistant, Content: "recent answer 2"},
})
var out strings.Builder
if err := assistant.RunTurn(context.Background(), "new question", &out, nil); err != nil {
t.Fatal(err)
}
if provider.summaryCalls != 1 {
t.Fatalf("summaryCalls = %d, want 1", provider.summaryCalls)
}
if provider.answerCalls != 1 {
t.Fatalf("answerCalls = %d, want 1", provider.answerCalls)
}
}
+1
View File
@@ -23,6 +23,7 @@ type Session struct {
Provider string `json:"provider"`
Model string `json:"model"`
Messages []llm.Message `json:"messages"`
LastUsage *llm.Usage `json:"last_usage,omitempty"`
}
// SessionMeta is a lightweight summary for listing.
+4
View File
@@ -30,6 +30,7 @@ func TestSaveAndLoad(t *testing.T) {
{Role: llm.RoleUser, Content: "hello"},
{Role: llm.RoleAssistant, Content: "hi there"},
},
LastUsage: &llm.Usage{PromptTokens: 10, CompletionTokens: 5, TotalTokens: 15},
}
if err := store.Save(sess); err != nil {
t.Fatalf("save: %v", err)
@@ -51,6 +52,9 @@ func TestSaveAndLoad(t *testing.T) {
if loaded.Messages[1].Content != "hello" {
t.Errorf("Messages[1].Content = %q, want %q", loaded.Messages[1].Content, "hello")
}
if loaded.LastUsage == nil || loaded.LastUsage.TotalTokens != 15 {
t.Errorf("LastUsage = %#v", loaded.LastUsage)
}
if loaded.UpdatedAt.IsZero() {
t.Error("UpdatedAt should be set")
}