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
4 changes: 3 additions & 1 deletion cmd/chat_config_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"strings"
"sync"

"github.com/GrayCodeAI/rho/internal/engine/cost"

tea "charm.land/bubbletea/v2"

rhoconfig "github.com/GrayCodeAI/rho/internal/config"
Expand Down Expand Up @@ -214,7 +216,7 @@ func applyModelOptionToSession(sess *engine.Session, opt configModelOption) {
sess.EnsureAutoCompactor()
}
if opt.PriceKnown {
engine.RegisterLivePricing(opt.ID, opt.InputPricePer1M, opt.OutputPricePer1M)
cost.RegisterLivePricing(opt.ID, opt.InputPricePer1M, opt.OutputPricePer1M)
}
}

Expand Down
4 changes: 3 additions & 1 deletion cmd/compact_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"strings"
"time"

"github.com/GrayCodeAI/rho/internal/engine/token"

tea "charm.land/bubbletea/v2"
lipgloss "charm.land/lipgloss/v2"
"github.com/mattn/go-runewidth"
Expand Down Expand Up @@ -174,7 +176,7 @@ func (m *chatModel) startManualCompact() (chatModel, tea.Cmd) {
m.compactBarWindow = m.contextWindowTokens()
m.compactBarUsed = sessionContextUsedTokens(m.session)
if m.compactBarUsed <= 0 && m.session != nil {
m.compactBarUsed = engine.EstimateTokens(m.session.RawMessages())
m.compactBarUsed = token.EstimateTokens(m.session.RawMessages())
}
if m.brailleSpinner != nil {
m.brailleSpinner.SetLabel("")
Expand Down
4 changes: 3 additions & 1 deletion cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os"
"strings"

"github.com/GrayCodeAI/rho/internal/engine/scaffold"

rhoconfig "github.com/GrayCodeAI/rho/internal/config"
ctxrepomap "github.com/GrayCodeAI/rho/internal/context/repomap"
"github.com/GrayCodeAI/rho/internal/engine"
Expand Down Expand Up @@ -380,7 +382,7 @@ func configureSessionStartup(sess *engine.Session, settings rhoconfig.Settings,
sess.LifecycleSvc().SetReflector(engine.NewReflector(sess, sess.Model()))

// Few-shot learning: collect successful patterns from sessions
sess.LifecycleSvc().SetFewShotStore(engine.NewFewShotStore())
sess.LifecycleSvc().SetFewShotStore(scaffold.NewFewShotStore())

// Adaptive prompt: learn user preferences from corrections
sess.LifecycleSvc().SetAdaptivePrompt(engine.NewAdaptivePrompt())
Expand Down
4 changes: 3 additions & 1 deletion cmd/statusbar.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"sync"
"time"

"github.com/GrayCodeAI/rho/internal/engine/cost"

tea "charm.land/bubbletea/v2"
lipgloss "charm.land/lipgloss/v2"
"golang.org/x/text/language"
Expand Down Expand Up @@ -154,7 +156,7 @@ func renderStatusBarPrimaryRight(m *chatModel) string {
return strings.Join(parts, statusDimStyle.Render(" · "))
}

func formatStatusCost(c *engine.Cost) string {
func formatStatusCost(c *cost.Cost) string {
if c == nil {
return icons.Ruby() + " $0.00"
}
Expand Down
22 changes: 12 additions & 10 deletions internal/engine/agent_intelligence.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package engine
import (
"context"
"strings"

"github.com/GrayCodeAI/rho/internal/engine/agent"
)

// AgentIntelligence provides smart routing, auto-spawning, and synthesis for agents.
Expand All @@ -26,7 +28,7 @@ type SpawnDecision struct {
type SubTask struct {
ID string
Prompt string
Mode SubAgentMode
Mode agent.SubAgentMode
Priority int // higher = run first
DependsOn []string // IDs of tasks this depends on
}
Expand Down Expand Up @@ -78,19 +80,19 @@ func (ai *AgentIntelligence) AnalyzeForParallelism(prompt string) SpawnDecision
}

// SelectMode picks the optimal agent mode for a subtask.
func (ai *AgentIntelligence) SelectMode(subtask string) SubAgentMode {
func (ai *AgentIntelligence) SelectMode(subtask string) agent.SubAgentMode {
lower := strings.ToLower(subtask)

// Read-only tasks → explore mode (cheaper, faster)
readOnlyKeywords := []string{"find", "search", "list", "check", "read", "analyze", "look", "scan", "grep", "what is", "where is", "how many"}
for _, kw := range readOnlyKeywords {
if strings.Contains(lower, kw) {
return SubAgentExplore
return agent.SubAgentExplore
}
}

// Write tasks → general mode
return SubAgentGeneral
return agent.SubAgentGeneral
}

// decomposeTask splits a complex task into subtasks based on patterns.
Expand All @@ -101,17 +103,17 @@ func (ai *AgentIntelligence) decomposeTask(prompt string, scale TaskScale) []Sub
if (strings.Contains(lower, "research") || strings.Contains(lower, "analyze")) &&
(strings.Contains(lower, "implement") || strings.Contains(lower, "build") || strings.Contains(lower, "create")) {
return []SubTask{
{ID: "research", Prompt: "Research and analyze: " + prompt, Mode: SubAgentExplore},
{ID: "implement", Prompt: "Based on research, implement: " + prompt, Mode: SubAgentGeneral, DependsOn: []string{"research"}},
{ID: "research", Prompt: "Research and analyze: " + prompt, Mode: agent.SubAgentExplore},
{ID: "implement", Prompt: "Based on research, implement: " + prompt, Mode: agent.SubAgentGeneral, DependsOn: []string{"research"}},
}
}

// Pattern: multi-file refactor — pipeline
if strings.Contains(lower, "refactor") && scale >= ScaleMajor {
return []SubTask{
{ID: "scan", Prompt: "Scan and identify all files that need changes for: " + prompt, Mode: SubAgentExplore},
{ID: "plan", Prompt: "Create a refactoring plan based on scan results: " + prompt, Mode: SubAgentExplore, DependsOn: []string{"scan"}},
{ID: "execute", Prompt: "Execute the refactoring plan: " + prompt, Mode: SubAgentGeneral, DependsOn: []string{"plan"}},
{ID: "scan", Prompt: "Scan and identify all files that need changes for: " + prompt, Mode: agent.SubAgentExplore},
{ID: "plan", Prompt: "Create a refactoring plan based on scan results: " + prompt, Mode: agent.SubAgentExplore, DependsOn: []string{"scan"}},
{ID: "execute", Prompt: "Execute the refactoring plan: " + prompt, Mode: agent.SubAgentGeneral, DependsOn: []string{"plan"}},
}
}

Expand Down Expand Up @@ -185,7 +187,7 @@ func splitOnConjunctions(s string) []string {
}

// ExecuteWithIntelligence runs a task with smart agent routing.
func (ai *AgentIntelligence) ExecuteWithIntelligence(ctx context.Context, prompt string, execFn func(context.Context, string, SubAgentMode) (string, error)) (string, error) {
func (ai *AgentIntelligence) ExecuteWithIntelligence(ctx context.Context, prompt string, execFn func(context.Context, string, agent.SubAgentMode) (string, error)) (string, error) {
decision := ai.AnalyzeForParallelism(prompt)

if !decision.ShouldParallelize {
Expand Down
22 changes: 12 additions & 10 deletions internal/engine/agent_intelligence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ package engine
import (
"context"
"testing"

"github.com/GrayCodeAI/rho/internal/engine/agent"
)

func TestAgentIntelligence_SelectMode(t *testing.T) {
ai := NewAgentIntelligence()

tests := []struct {
prompt string
want SubAgentMode
want agent.SubAgentMode
}{
{"find all files using deprecated API", SubAgentExplore},
{"search for authentication code", SubAgentExplore},
{"analyze the dependency graph", SubAgentExplore},
{"implement the new endpoint", SubAgentGeneral},
{"refactor the auth module", SubAgentGeneral},
{"fix the bug in parser.go", SubAgentGeneral},
{"find all files using deprecated API", agent.SubAgentExplore},
{"search for authentication code", agent.SubAgentExplore},
{"analyze the dependency graph", agent.SubAgentExplore},
{"implement the new endpoint", agent.SubAgentGeneral},
{"refactor the auth module", agent.SubAgentGeneral},
{"fix the bug in parser.go", agent.SubAgentGeneral},
}
for _, tt := range tests {
got := ai.SelectMode(tt.prompt)
Expand Down Expand Up @@ -67,7 +69,7 @@ func TestAgentIntelligence_ExecuteWithIntelligence_Single(t *testing.T) {
ai := NewAgentIntelligence()

called := false
result, err := ai.ExecuteWithIntelligence(context.Background(), "fix typo", func(_ context.Context, prompt string, mode SubAgentMode) (string, error) {
result, err := ai.ExecuteWithIntelligence(context.Background(), "fix typo", func(_ context.Context, prompt string, mode agent.SubAgentMode) (string, error) {
called = true
return "fixed", nil
})
Expand Down Expand Up @@ -95,8 +97,8 @@ func TestSelfAwareness_ShouldDelegate(t *testing.T) {

func TestSynthesisPrompt(t *testing.T) {
subtasks := []SubTask{
{ID: "a", Prompt: "find files", Mode: SubAgentExplore},
{ID: "b", Prompt: "fix them", Mode: SubAgentGeneral},
{ID: "a", Prompt: "find files", Mode: agent.SubAgentExplore},
{ID: "b", Prompt: "fix them", Mode: agent.SubAgentGeneral},
}
results := map[string]string{"a": "found 3 files", "b": "fixed all"}
prompt := MergeSynthesisPrompt(subtasks, results)
Expand Down
42 changes: 0 additions & 42 deletions internal/engine/agent_reexports.go

This file was deleted.

42 changes: 21 additions & 21 deletions internal/engine/agent_session_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,26 +137,26 @@ func (s *Session) spawnSubAgentRequest(ctx context.Context, req agentcontracts.S
return res, nil
}

func mapContractsType(t agentcontracts.SubagentType) SubAgentMode {
func mapContractsType(t agentcontracts.SubagentType) engagent.SubAgentMode {
switch t {
case agentcontracts.TypePlan:
return SubAgentPlan
return engagent.SubAgentPlan
case agentcontracts.TypeGeneralPurpose:
return SubAgentGeneral
return engagent.SubAgentGeneral
default:
return SubAgentExplore
return engagent.SubAgentExplore
}
}

// spawnSubAgent creates a sub-agent with the given mode and depth tracking.
// Returns (output, worktreePath, error).
func (s *Session) spawnSubAgent(ctx context.Context, norm agentcontracts.Normalized, mode SubAgentMode, depth int) (string, string, error) {
if depth >= MaxAgentDepth {
return "", "", fmt.Errorf("max agent depth %d exceeded", MaxAgentDepth)
func (s *Session) spawnSubAgent(ctx context.Context, norm agentcontracts.Normalized, mode engagent.SubAgentMode, depth int) (string, string, error) {
if depth >= engagent.MaxAgentDepth {
return "", "", fmt.Errorf("max agent depth %d exceeded", engagent.MaxAgentDepth)
}

maxTurns := DefaultTurnsForMode(mode)
if mode == SubAgentExplore && norm.Thoroughness != "" {
maxTurns := engagent.DefaultTurnsForMode(mode)
if mode == engagent.SubAgentExplore && norm.Thoroughness != "" {
maxTurns = engagent.ThoroughnessTurns(engagent.ExploreThoroughness(norm.Thoroughness))
}

Expand All @@ -174,7 +174,7 @@ func (s *Session) spawnSubAgent(ctx context.Context, norm agentcontracts.Normali

// Capability mode can further restrict tools beyond profile defaults.
if norm.CapabilityMode == agentcontracts.CapReadOnly {
registry = registry.Filter(ExploreTools)
registry = registry.Filter(engagent.ExploreTools)
}

subPromptCtx := prompts.PromptContext{
Expand All @@ -185,14 +185,14 @@ func (s *Session) spawnSubAgent(ctx context.Context, norm agentcontracts.Normali
if err != nil {
subSystemPrompt = s.Persistence().System()
}
if mode == SubAgentPlan {
if mode == engagent.SubAgentPlan {
subSystemPrompt = planSystemPrefix + "\n\n" + subSystemPrompt
}

sub := s.SubSession(model, subSystemPrompt, registry)
sub.PermSvc().SetPermissionFn(s.PermSvc().PermissionFn())
// Explore/plan: hard read-only bash allowlist (in addition to tool filter).
if IsReadOnlyMode(mode) || norm.CapabilityMode == agentcontracts.CapReadOnly {
if engagent.IsReadOnlyMode(mode) || norm.CapabilityMode == agentcontracts.CapReadOnly {
sub.Tools().SetReadOnlyBash(true)
}
// A child receives an independent snapshot of the parent's policy. This
Expand Down Expand Up @@ -281,30 +281,30 @@ func (s *Session) spawnSubAgent(ctx context.Context, norm agentcontracts.Normali
const planSystemPrefix = "You are a planning sub-agent. Produce an ordered, actionable plan. " +
"Do not modify files. Prefer research tools (Read, Grep, Glob, LS) and only use Bash for read-only inspection."

func (s *Session) resolveSubAgentModel(mode SubAgentMode) string {
func (s *Session) resolveSubAgentModel(mode engagent.SubAgentMode) string {
current := s.ChatLLM().Model()
if s.LifecycleSvc().Cascade() == nil {
return current
}
switch mode {
case SubAgentExplore:
case engagent.SubAgentExplore:
return s.LifecycleSvc().Cascade().SelectModel("summarize", current, s.LifecycleSvc().Cascade().Roles.Explorer)
case SubAgentPlan:
case engagent.SubAgentPlan:
return s.LifecycleSvc().Cascade().SelectModel("planning", current, s.LifecycleSvc().Cascade().Roles.Planner)
case SubAgentGeneral:
case engagent.SubAgentGeneral:
return s.LifecycleSvc().Cascade().SelectModel("implement", current, "")
default:
return current
}
}

func (s *Session) resolveSubAgentTools(mode SubAgentMode) *tool.Registry {
func (s *Session) resolveSubAgentTools(mode engagent.SubAgentMode) *tool.Registry {
registry := s.Tools().Registry()
switch mode {
case SubAgentExplore:
return registry.Filter(ExploreTools)
case SubAgentPlan:
return registry.Filter(PlanTools)
case engagent.SubAgentExplore:
return registry.Filter(engagent.ExploreTools)
case engagent.SubAgentPlan:
return registry.Filter(engagent.PlanTools)
default:
return registry
}
Expand Down
6 changes: 4 additions & 2 deletions internal/engine/bmad_features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"os"
"path/filepath"
"testing"

"github.com/GrayCodeAI/rho/internal/engine/project"
)

func TestClassifyScale(t *testing.T) {
Expand Down Expand Up @@ -78,7 +80,7 @@ func TestProjectContext_Load(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "PROJECT_CONTEXT.md"), []byte("## Stack\n- Go 1.21\n- PostgreSQL"), 0o644)

pc := NewProjectContext(dir)
pc := project.NewProjectContext(dir)
content := pc.Load()
if !hasSubstr(content, "Go 1.21") {
t.Error("expected project context content")
Expand All @@ -89,7 +91,7 @@ func TestProjectContext_Load(t *testing.T) {
}

func TestProjectContext_NoFiles(t *testing.T) {
pc := NewProjectContext(t.TempDir())
pc := project.NewProjectContext(t.TempDir())
content := pc.Load()
if content != "" {
t.Error("expected empty content when no files exist")
Expand Down
Loading
Loading