v0.0.5: context compaction, minimal config, retry/search/code symbols/diff/status/test commands
This commit is contained in:
+19
-13
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user