Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions internal/ai/prompts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ai

// SimpleMutationPrompt returns a minimal system prompt that enforces
// SEARCH/REPLACE-only output with zero conversational filler. Designed
// for SIMPLE_MUTATION tier requests where max_tokens ≤ 150.
func SimpleMutationPrompt() string {
return `STRICT RULE: Output ONLY a valid SEARCH/REPLACE block for the change.
NO conversational filler. NO markdown prose outside code blocks. NO full file rewrites.

FORMAT EXAMPLE:
<<<<<<< SEARCH
old_line
=======
new_line
>>>>>>>`
}

// IntentClassifyPrompt returns a minimal system prompt for cloud-based
// intent classification with extremely tight token budget (max_tokens: 30).
func IntentClassifyPrompt() string {
return `Classify intent: MUTATE, READ, or DIAGNOSE. Output one word.`
}
1 change: 1 addition & 0 deletions internal/ai/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Request struct {
System string `json:"-"` // Explicit system prompt (top-level for Anthropic, prepended for OpenAI-compatible)
MaxTokens int `json:"-"` // 0 = use provider default
Stop []string `json:"-"` // Optional stop sequences (e.g. [">>>>>>>"])
Temperature float64 `json:"-"` // 0 = use provider default
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
}

Expand Down
81 changes: 74 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,66 @@ type Config struct {
}

type ModelConfig struct {
Default string `yaml:"default"`
Fast string `yaml:"fast"`
Provider string `yaml:"provider"`
MaxTokens int `yaml:"max_tokens"`
SessionModel string `yaml:"-"` // runtime session override, never persisted
ModeDefaults map[string]string `yaml:"mode_defaults,omitempty"`
Modes map[string]ModeSpec `yaml:"modes,omitempty"`
Default string `yaml:"default"`
Fast string `yaml:"fast"`
Provider string `yaml:"provider"`
MaxTokens int `yaml:"max_tokens"`
SessionModel string `yaml:"-"` // runtime session override, never persisted
ModeDefaults map[string]string `yaml:"mode_defaults,omitempty"`
Modes map[string]ModeSpec `yaml:"modes,omitempty"`
Tiers map[string]IntentTierConfig `yaml:"tiers,omitempty"`
}

type IntentTierConfig struct {
Provider string `yaml:"provider,omitempty"`
Model string `yaml:"model,omitempty"`
ActiveOverride string `yaml:"active_override,omitempty"`
}

// ResolveTierModel returns the effective model for the given intent tier.
// It first checks for an active_override (set via /model), then falls back
// to the tier's model, then to the global ModelConfig.Default.
func (c *Config) ResolveTierModel(tier string) string {
if c.Models.Tiers != nil {
if tc, ok := c.Models.Tiers[tier]; ok {
if tc.ActiveOverride != "" {
return tc.ActiveOverride
}
if tc.Model != "" {
return tc.Model
}
}
}
return c.ActiveModelName()
}

// SetTierOverride sets the active_override for the given intent tier,
// persisting the model selection as the session-level override.
func (c *Config) SetTierOverride(tier, modelName string) {
if c.Models.Tiers == nil {
c.Models.Tiers = make(map[string]IntentTierConfig)
}
tc := c.Models.Tiers[tier]
tc.ActiveOverride = modelName
c.Models.Tiers[tier] = tc
c.Models.SessionModel = modelName
}

// ActiveTierForFile returns the intent tier for a given file path based
// on its extension and purpose. License files, Dockerfiles, and dotfiles
// are classified as high_intent since they carry legal/operational weight.
func (c *Config) ActiveTierForFile(file string) string {
base := strings.ToLower(strings.TrimSpace(file))
switch {
case base == "license" || base == "license.md" || base == "license.txt" ||
strings.HasPrefix(base, ".env") || base == "dockerfile" ||
strings.HasPrefix(base, "dockerfile."):
return "high_intent"
case strings.HasSuffix(base, ".md") || strings.HasSuffix(base, ".txt"):
return "medium_intent"
default:
return "low_intent"
}
}

type ModeSpec struct {
Expand Down Expand Up @@ -286,6 +339,20 @@ func Default() *Config {
"review": {Provider: "", Model: ""},
"investigate": {Provider: "", Model: ""},
},
Tiers: map[string]IntentTierConfig{
"low_intent": {
Provider: "ollama",
Model: "qwen2.5-coder:7b",
},
"medium_intent": {
Provider: "ollama",
Model: "qwen2.5-coder:7b",
},
"high_intent": {
Provider: "ollama",
Model: "qwen2.5-coder:7b",
},
},
},
Execution: ExecutionConfig{
Sandbox: true,
Expand Down
42 changes: 42 additions & 0 deletions internal/execution/execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,3 +480,45 @@ func TestRunnerDir(t *testing.T) {
t.Fatalf("expected '/tmp', got %q", result.Stdout)
}
}

func TestSanitizeLLMResponseBoldFileCreate(t *testing.T) {
cases := []struct {
name string
input string
want string
}{
{
name: "bold FILE_CREATE header stripped",
input: "**FILE_CREATE: LICENSE**\nMIT License\n\nCopyright (c) 2026\n```",
want: "MIT License\n\nCopyright (c) 2026",
},
{
name: "bold FILE_CREATE with fence",
input: "**FILE_CREATE: LICENSE**\n```mit\nMIT License\n\nCopyright (c) 2026\n```",
want: "MIT License\n\nCopyright (c) 2026",
},
{
name: "bold FILE_CREATE no colon",
input: "**FILE_CREATE** .gitignore\n```\n*.log\n```",
want: "*.log",
},
{
name: "double bold FILE_CREATE",
input: "**FILE_CREATE: .env**\n```env\nPORT=8080\n```",
want: "PORT=8080",
},
{
name: "no hallucination passthrough",
input: "MIT License\n\nCopyright (c) 2026",
want: "MIT License\n\nCopyright (c) 2026",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := SanitizeLLMResponse(c.input)
if got != c.want {
t.Errorf("SanitizeLLMResponse =\n %q\nwant\n %q", got, c.want)
}
})
}
}
Loading
Loading