v0.0.5: context compaction, minimal config, retry/search/code symbols/diff/status/test commands
This commit is contained in:
+97
-35
@@ -21,37 +21,50 @@ type Config struct {
|
||||
|
||||
type ProviderConfig struct {
|
||||
Name string
|
||||
BaseURL string `yaml:"base_url"`
|
||||
Model string `yaml:"model"`
|
||||
Models []string `yaml:"models"`
|
||||
APIKey string `yaml:"api_key"`
|
||||
Thinking string `yaml:"thinking"`
|
||||
ThinkingParam string `yaml:"thinking_param"`
|
||||
BaseURL string
|
||||
Model string
|
||||
Models []string
|
||||
ModelConfigs map[string]ModelConfig
|
||||
ContextTokens int
|
||||
APIKey string
|
||||
Thinking string
|
||||
ThinkingParam string
|
||||
ThinkingConfigured bool
|
||||
}
|
||||
|
||||
type ModelConfig struct {
|
||||
ID string `yaml:"id"`
|
||||
Thinking string `yaml:"thinking"`
|
||||
ContextTokens int `yaml:"context"`
|
||||
}
|
||||
|
||||
type AgentConfig struct {
|
||||
SystemPrompt string `yaml:"system_prompt"`
|
||||
WorkingDir string `yaml:"working_dir"`
|
||||
ToolTimeout time.Duration `yaml:"tool_timeout"`
|
||||
SystemPrompt string `yaml:"system_prompt"`
|
||||
WorkingDir string `yaml:"working_dir"`
|
||||
ToolTimeout time.Duration `yaml:"tool_timeout"`
|
||||
MaxContextTokens int `yaml:"max_context_tokens"`
|
||||
}
|
||||
|
||||
type rawConfig struct {
|
||||
Providers map[string]rawProviderConfig `yaml:"providers"`
|
||||
Agent struct {
|
||||
SystemPrompt string `yaml:"system_prompt"`
|
||||
WorkingDir string `yaml:"working_dir"`
|
||||
ToolTimeout string `yaml:"tool_timeout"`
|
||||
SystemPrompt string `yaml:"system_prompt"`
|
||||
WorkingDir string `yaml:"working_dir"`
|
||||
ToolTimeout string `yaml:"tool_timeout"`
|
||||
MaxContextTokens int `yaml:"max_context_tokens"`
|
||||
} `yaml:"agent"`
|
||||
}
|
||||
|
||||
type rawProviderConfig struct {
|
||||
BaseURL string `yaml:"base_url"`
|
||||
Model string `yaml:"model"`
|
||||
Models []string `yaml:"models"`
|
||||
APIKey string `yaml:"api_key"`
|
||||
Thinking *string `yaml:"thinking"`
|
||||
ThinkingParam string `yaml:"thinking_param"`
|
||||
BaseURL string `yaml:"base_url"`
|
||||
Models []rawModelConfig `yaml:"models"`
|
||||
APIKey string `yaml:"api_key"`
|
||||
}
|
||||
|
||||
type rawModelConfig struct {
|
||||
ID string `yaml:"id"`
|
||||
Thinking string `yaml:"thinking"`
|
||||
ContextTokens int `yaml:"context"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
@@ -76,8 +89,9 @@ func Load(path string) (*Config, error) {
|
||||
cfg := &Config{
|
||||
Providers: normalizeProviders(raw),
|
||||
Agent: AgentConfig{
|
||||
SystemPrompt: raw.Agent.SystemPrompt,
|
||||
WorkingDir: raw.Agent.WorkingDir,
|
||||
SystemPrompt: raw.Agent.SystemPrompt,
|
||||
WorkingDir: raw.Agent.WorkingDir,
|
||||
MaxContextTokens: raw.Agent.MaxContextTokens,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -142,8 +156,8 @@ func (c *Config) Validate() error {
|
||||
if strings.TrimSpace(provider.BaseURL) == "" {
|
||||
missing = append(missing, prefix+".base_url")
|
||||
}
|
||||
if strings.TrimSpace(provider.Model) == "" {
|
||||
missing = append(missing, prefix+".model")
|
||||
if len(provider.Models) == 0 {
|
||||
missing = append(missing, prefix+".models")
|
||||
}
|
||||
if strings.TrimSpace(provider.APIKey) == "" {
|
||||
missing = append(missing, prefix+".api_key")
|
||||
@@ -151,6 +165,19 @@ func (c *Config) Validate() error {
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("missing required config: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
for _, model := range provider.Models {
|
||||
if strings.TrimSpace(model) == "" {
|
||||
return fmt.Errorf("%s.models.id is required", prefix)
|
||||
}
|
||||
}
|
||||
for _, model := range provider.ModelConfigs {
|
||||
if model.ContextTokens < 0 {
|
||||
return fmt.Errorf("%s.models.context must be greater than or equal to zero", prefix)
|
||||
}
|
||||
if model.Thinking != "" && !IsThinkingLevel(model.Thinking) {
|
||||
return fmt.Errorf("%s.models.thinking must be one of: %s", prefix, strings.Join(ThinkingLevels(), ", "))
|
||||
}
|
||||
}
|
||||
if provider.ThinkingConfigured && !IsThinkingLevel(provider.Thinking) {
|
||||
return fmt.Errorf("%s.thinking must be one of: %s", prefix, strings.Join(ThinkingLevels(), ", "))
|
||||
}
|
||||
@@ -158,6 +185,9 @@ func (c *Config) Validate() error {
|
||||
if c.Agent.ToolTimeout <= 0 {
|
||||
return errors.New("agent.tool_timeout must be greater than zero")
|
||||
}
|
||||
if c.Agent.MaxContextTokens < 0 {
|
||||
return errors.New("agent.max_context_tokens must be greater than or equal to zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -187,27 +217,59 @@ func normalizeProviders(raw rawConfig) map[string]ProviderConfig {
|
||||
}
|
||||
|
||||
func normalizeProvider(name string, raw rawProviderConfig) ProviderConfig {
|
||||
models, modelConfigs := normalizeModelConfigs(raw.Models)
|
||||
provider := ProviderConfig{
|
||||
Name: name,
|
||||
BaseURL: raw.BaseURL,
|
||||
Model: raw.Model,
|
||||
Models: append([]string(nil), raw.Models...),
|
||||
APIKey: raw.APIKey,
|
||||
ThinkingParam: raw.ThinkingParam,
|
||||
Name: name,
|
||||
BaseURL: raw.BaseURL,
|
||||
Models: models,
|
||||
ModelConfigs: modelConfigs,
|
||||
APIKey: raw.APIKey,
|
||||
}
|
||||
if provider.Model == "" && len(provider.Models) > 0 {
|
||||
if len(provider.Models) > 0 {
|
||||
provider.Model = provider.Models[0]
|
||||
}
|
||||
if raw.Thinking != nil {
|
||||
provider.Thinking = strings.ToLower(strings.TrimSpace(*raw.Thinking))
|
||||
provider.ThinkingConfigured = true
|
||||
if provider.ThinkingParam == "" {
|
||||
provider.ThinkingParam = "thinking"
|
||||
}
|
||||
applyModelDefaults(&provider)
|
||||
if provider.ThinkingConfigured && provider.ThinkingParam == "" {
|
||||
provider.ThinkingParam = "thinking"
|
||||
}
|
||||
return provider
|
||||
}
|
||||
|
||||
func normalizeModelConfigs(rawModels []rawModelConfig) ([]string, map[string]ModelConfig) {
|
||||
models := make([]string, 0, len(rawModels))
|
||||
configs := make(map[string]ModelConfig, len(rawModels))
|
||||
for _, raw := range rawModels {
|
||||
model := ModelConfig{
|
||||
ID: strings.TrimSpace(raw.ID),
|
||||
Thinking: strings.ToLower(strings.TrimSpace(raw.Thinking)),
|
||||
ContextTokens: raw.ContextTokens,
|
||||
}
|
||||
models = append(models, model.ID)
|
||||
if model.ID != "" {
|
||||
configs[model.ID] = model
|
||||
}
|
||||
}
|
||||
return models, configs
|
||||
}
|
||||
|
||||
func applyModelDefaults(provider *ProviderConfig) {
|
||||
model, ok := provider.ModelConfigs[provider.Model]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
provider.ContextTokens = model.ContextTokens
|
||||
if model.Thinking != "" {
|
||||
provider.Thinking = model.Thinking
|
||||
provider.ThinkingConfigured = true
|
||||
}
|
||||
}
|
||||
|
||||
func (p ProviderConfig) WithModel(model string) ProviderConfig {
|
||||
p.Model = strings.TrimSpace(model)
|
||||
applyModelDefaults(&p)
|
||||
return p
|
||||
}
|
||||
|
||||
func ThinkingLevels() []string {
|
||||
return []string{"none", "middle", "high", "xhigh", "max"}
|
||||
}
|
||||
|
||||
+130
-15
@@ -16,8 +16,9 @@ func TestLoadExpandsEnvAndDefaults(t *testing.T) {
|
||||
providers:
|
||||
test:
|
||||
base_url: https://example.com
|
||||
model: test-model
|
||||
api_key: ${AGENTU_TEST_KEY}
|
||||
models:
|
||||
- id: test-model
|
||||
agent:
|
||||
working_dir: .
|
||||
`), 0o644); err != nil {
|
||||
@@ -43,6 +44,115 @@ agent:
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMinimalModelObjectConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("AGENTU_TEST_KEY", "sk-test")
|
||||
path := filepath.Join(dir, "agentu.yaml")
|
||||
if err := os.WriteFile(path, []byte(`
|
||||
providers:
|
||||
test:
|
||||
base_url: https://example.com
|
||||
api_key: ${AGENTU_TEST_KEY}
|
||||
models:
|
||||
- id: model-a
|
||||
thinking: none
|
||||
context: 256000
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
provider := cfg.Providers["test"]
|
||||
if provider.Model != "model-a" {
|
||||
t.Fatalf("model = %q", provider.Model)
|
||||
}
|
||||
if len(provider.Models) != 1 || provider.Models[0] != "model-a" {
|
||||
t.Fatalf("models = %#v", provider.Models)
|
||||
}
|
||||
if provider.ContextTokens != 256000 {
|
||||
t.Fatalf("context tokens = %d", provider.ContextTokens)
|
||||
}
|
||||
if !provider.ThinkingConfigured || provider.Thinking != "none" || provider.ThinkingParam != "thinking" {
|
||||
t.Fatalf("thinking config = %#v", provider)
|
||||
}
|
||||
if cfg.Agent.ToolTimeout != 30*time.Second {
|
||||
t.Fatalf("tool timeout = %s", cfg.Agent.ToolTimeout)
|
||||
}
|
||||
if cfg.Agent.SystemPrompt == "" {
|
||||
t.Fatal("system prompt default was not applied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsStringModelEntries(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "agentu.yaml")
|
||||
if err := os.WriteFile(path, []byte(`
|
||||
providers:
|
||||
test:
|
||||
base_url: https://example.com
|
||||
api_key: sk-test
|
||||
models:
|
||||
- test-model
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := Load(path)
|
||||
if err == nil || !strings.Contains(err.Error(), "cannot unmarshal") {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMaxContextTokens(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "agentu.yaml")
|
||||
if err := os.WriteFile(path, []byte(`
|
||||
providers:
|
||||
test:
|
||||
base_url: https://example.com
|
||||
api_key: sk-test
|
||||
models:
|
||||
- id: test-model
|
||||
agent:
|
||||
max_context_tokens: 120000
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Agent.MaxContextTokens != 120000 {
|
||||
t.Fatalf("max context tokens = %d", cfg.Agent.MaxContextTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsNegativeMaxContextTokens(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "agentu.yaml")
|
||||
if err := os.WriteFile(path, []byte(`
|
||||
providers:
|
||||
test:
|
||||
base_url: https://example.com
|
||||
api_key: sk-test
|
||||
models:
|
||||
- id: test-model
|
||||
agent:
|
||||
max_context_tokens: -1
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := Load(path)
|
||||
if err == nil || !strings.Contains(err.Error(), "agent.max_context_tokens") {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUsesDefaultConfigPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("HOME", dir)
|
||||
@@ -56,8 +166,9 @@ func TestLoadUsesDefaultConfigPath(t *testing.T) {
|
||||
providers:
|
||||
test:
|
||||
base_url: https://example.com
|
||||
model: test-model
|
||||
api_key: ${AGENTU_TEST_KEY}
|
||||
models:
|
||||
- id: test-model
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -93,8 +204,9 @@ func TestLoadDefaultSystemPromptDiscouragesEagerTools(t *testing.T) {
|
||||
providers:
|
||||
test:
|
||||
base_url: https://example.com
|
||||
model: test-model
|
||||
api_key: ${AGENTU_TEST_KEY}
|
||||
models:
|
||||
- id: test-model
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -119,17 +231,15 @@ providers:
|
||||
loveuer:
|
||||
base_url: https://example.com
|
||||
api_key: ${AGENTU_TEST_KEY}
|
||||
model: model-a
|
||||
models:
|
||||
- model-a
|
||||
- model-b
|
||||
thinking: xhigh
|
||||
thinking_param: thinking
|
||||
- id: model-a
|
||||
thinking: xhigh
|
||||
- id: model-b
|
||||
backup:
|
||||
base_url: https://backup.example.com
|
||||
api_key: ${AGENTU_TEST_KEY}
|
||||
models:
|
||||
- model-c
|
||||
- id: model-c
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -162,11 +272,13 @@ providers:
|
||||
two:
|
||||
base_url: https://two.example.com
|
||||
api_key: ${AGENTU_TEST_KEY}
|
||||
model: model-b
|
||||
models:
|
||||
- id: model-b
|
||||
one:
|
||||
base_url: https://one.example.com
|
||||
api_key: ${AGENTU_TEST_KEY}
|
||||
model: model-a
|
||||
models:
|
||||
- id: model-a
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -188,9 +300,10 @@ func TestLoadRejectsInvalidThinking(t *testing.T) {
|
||||
providers:
|
||||
test:
|
||||
base_url: https://example.com
|
||||
model: test-model
|
||||
api_key: ${AGENTU_TEST_KEY}
|
||||
thinking: huge
|
||||
models:
|
||||
- id: test-model
|
||||
thinking: huge
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -224,8 +337,9 @@ func TestLoadRejectsLegacyProviderConfig(t *testing.T) {
|
||||
if err := os.WriteFile(path, []byte(`
|
||||
provider:
|
||||
base_url: https://example.com
|
||||
model: test-model
|
||||
api_key: sk-test
|
||||
models:
|
||||
- id: test-model
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -244,8 +358,9 @@ active_provider: loveuer
|
||||
providers:
|
||||
loveuer:
|
||||
base_url: https://example.com
|
||||
model: test-model
|
||||
api_key: sk-test
|
||||
models:
|
||||
- id: test-model
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user