diff --git a/cmd/chat_config_models.go b/cmd/chat_config_models.go index 426e5e57..0d3f3000 100644 --- a/cmd/chat_config_models.go +++ b/cmd/chat_config_models.go @@ -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" @@ -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) } } diff --git a/cmd/compact_ui.go b/cmd/compact_ui.go index a8585227..5af4363c 100644 --- a/cmd/compact_ui.go +++ b/cmd/compact_ui.go @@ -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" @@ -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("") diff --git a/cmd/options.go b/cmd/options.go index 66dc44bb..3dac14d0 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -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" @@ -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()) diff --git a/cmd/statusbar.go b/cmd/statusbar.go index a0059765..f7cbc6d4 100644 --- a/cmd/statusbar.go +++ b/cmd/statusbar.go @@ -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" @@ -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" } diff --git a/internal/engine/agent_intelligence.go b/internal/engine/agent_intelligence.go index c783e613..bee371c0 100644 --- a/internal/engine/agent_intelligence.go +++ b/internal/engine/agent_intelligence.go @@ -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. @@ -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 } @@ -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. @@ -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"}}, } } @@ -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 { diff --git a/internal/engine/agent_intelligence_test.go b/internal/engine/agent_intelligence_test.go index 426d25b3..4db5e548 100644 --- a/internal/engine/agent_intelligence_test.go +++ b/internal/engine/agent_intelligence_test.go @@ -3,6 +3,8 @@ package engine import ( "context" "testing" + + "github.com/GrayCodeAI/rho/internal/engine/agent" ) func TestAgentIntelligence_SelectMode(t *testing.T) { @@ -10,14 +12,14 @@ func TestAgentIntelligence_SelectMode(t *testing.T) { 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) @@ -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 }) @@ -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) diff --git a/internal/engine/agent_reexports.go b/internal/engine/agent_reexports.go deleted file mode 100644 index adee48e3..00000000 --- a/internal/engine/agent_reexports.go +++ /dev/null @@ -1,42 +0,0 @@ -// This file re-exports symbols from the agent sub-package so that existing -// callers of engine.SubAgentMode, engine.NewSubAgentBudget, etc. keep compiling -// during the Stage 2 migration. See docs/plans/engine-refactor-plan.md. -package engine - -import ( - "github.com/GrayCodeAI/rho/internal/engine/agent" -) - -type ( - SubAgentMode = agent.SubAgentMode - SubAgentConfig = agent.SubAgentConfig - SubAgentBudget = agent.SubAgentBudget - BackgroundResult = agent.BackgroundResult -) - -const ( - SubAgentExplore = agent.SubAgentExplore - SubAgentGeneral = agent.SubAgentGeneral - SubAgentPlan = agent.SubAgentPlan - DefaultExploreTurns = agent.DefaultExploreTurns - DefaultGeneralTurns = agent.DefaultGeneralTurns - DefaultPlanTurns = agent.DefaultPlanTurns - MaxAgentDepth = agent.MaxAgentDepth -) - -var ( - ExploreTools = agent.ExploreTools - PlanTools = agent.PlanTools - ModeToolAllowlist = agent.ModeToolAllowlist -) - -func DefaultSubAgentConfig() SubAgentConfig { return agent.DefaultSubAgentConfig() } -func NewSubAgentBudget(mode SubAgentMode, cfg SubAgentConfig) *SubAgentBudget { - return agent.NewSubAgentBudget(mode, cfg) -} - -func FilterToolsForMode(mode SubAgentMode, available []string) []string { - return agent.FilterToolsForMode(mode, available) -} -func DefaultTurnsForMode(mode SubAgentMode) int { return agent.DefaultTurnsForMode(mode) } -func IsReadOnlyMode(mode SubAgentMode) bool { return agent.IsReadOnlyMode(mode) } diff --git a/internal/engine/agent_session_tool.go b/internal/engine/agent_session_tool.go index c1b0f13a..f74f6779 100644 --- a/internal/engine/agent_session_tool.go +++ b/internal/engine/agent_session_tool.go @@ -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)) } @@ -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{ @@ -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 @@ -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 } diff --git a/internal/engine/bmad_features_test.go b/internal/engine/bmad_features_test.go index 446052cc..1e2c385a 100644 --- a/internal/engine/bmad_features_test.go +++ b/internal/engine/bmad_features_test.go @@ -4,6 +4,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/GrayCodeAI/rho/internal/engine/project" ) func TestClassifyScale(t *testing.T) { @@ -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") @@ -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") diff --git a/internal/engine/compact.go b/internal/engine/compact.go index 52b63162..5f777cf9 100644 --- a/internal/engine/compact.go +++ b/internal/engine/compact.go @@ -77,7 +77,7 @@ func (s *Session) smartCompactBody(ctx context.Context) { // Extract file tracking from messages being compacted if s.Persistence().Files() == nil { - s.Persistence().SetFiles(NewFileTracker()) + s.Persistence().SetFiles(compact.NewFileTracker()) } files := s.Persistence().Files() compactedMsgs := raw[:len(raw)-keepEnd] @@ -138,12 +138,12 @@ func (s *Session) generateSummary(ctx context.Context, raw []types.FluxMessage) // compaction, merge the NEW messages into it rather than re-summarizing the // entire conversation from scratch. This preserves already-captured context // and avoids the cost of re-deriving it. - prior := ExtractPriorSummary(raw) + prior := compact.ExtractPriorSummary(raw) newMsgs := raw if prior != "" { // Only the messages that came after the persisted summary are "new". for i, m := range raw { - if m.Role == "user" && strings.HasPrefix(m.Content, PriorSummaryPrefix) { + if m.Role == "user" && strings.HasPrefix(m.Content, compact.PriorSummaryPrefix) { newMsgs = raw[i+1:] break } @@ -153,7 +153,7 @@ func (s *Session) generateSummary(ctx context.Context, raw []types.FluxMessage) // Build a compact version of the conversation for summarization // using the structured compaction prompt from compact_prompt.go var summaryMsgs []types.FluxMessage - compactPrompt := BuildIncrementalCompactPrompt(prior) + compactPrompt := compact.BuildIncrementalCompactPrompt(prior) summaryMsgs = append(summaryMsgs, types.FluxMessage{ Role: "user", Content: compactPrompt + "\n\nConversation:\n", @@ -209,7 +209,7 @@ func (s *Session) generateSummary(ctx context.Context, raw []types.FluxMessage) return "" } // Extract structured summary, stripping analysis block - return FormatCompactSummary(resp.Content) + return compact.FormatCompactSummary(resp.Content) } // extractSummaryFromCompressed pulls key information from token-compressed text diff --git a/internal/engine/compact_auto.go b/internal/engine/compact_auto.go index 0bc85a39..bea8551f 100644 --- a/internal/engine/compact_auto.go +++ b/internal/engine/compact_auto.go @@ -6,6 +6,10 @@ import ( "sync" "time" + "github.com/GrayCodeAI/rho/internal/engine/token" + + "github.com/GrayCodeAI/rho/internal/engine/compact" + "github.com/GrayCodeAI/rho/internal/circuitbreaker" "github.com/GrayCodeAI/rho/internal/types" ) @@ -14,13 +18,13 @@ import ( type AutoCompactor struct { mu sync.Mutex registry *StrategyRegistry - config CompactConfig + config compact.CompactConfig breaker *circuitbreaker.Breaker lastStrategy string } // NewAutoCompactor creates an auto-compactor with the given config. -func NewAutoCompactor(config CompactConfig) *AutoCompactor { +func NewAutoCompactor(config compact.CompactConfig) *AutoCompactor { return &AutoCompactor{ registry: NewStrategyRegistry(config), config: config, @@ -29,7 +33,7 @@ func NewAutoCompactor(config CompactConfig) *AutoCompactor { } // Configure updates compaction settings and rebuilds the strategy registry. -func (ac *AutoCompactor) Configure(config CompactConfig) { +func (ac *AutoCompactor) Configure(config compact.CompactConfig) { if ac == nil { return } @@ -61,7 +65,7 @@ func (ac *AutoCompactor) ShouldAutoCompact(sess *Session) bool { return false } - tokenCount := EstimateTokens(sess.Persistence().RawMessages()) + tokenCount := token.EstimateTokens(sess.Persistence().RawMessages()) threshold := ac.GetAutoCompactThreshold() return tokenCount >= threshold } @@ -73,7 +77,7 @@ func (ac *AutoCompactor) AutoCompactIfNeeded(ctx context.Context, sess *Session) return "", false } - tokensBefore := EstimateTokens(sess.Persistence().RawMessages()) + tokensBefore := token.EstimateTokens(sess.Persistence().RawMessages()) strategy, err := ac.RunCompaction(ctx, sess) if err != nil { ac.mu.Lock() @@ -84,12 +88,12 @@ func (ac *AutoCompactor) AutoCompactIfNeeded(ctx context.Context, sess *Session) "failures": ac.breaker.ConsecutiveFailures(), }) sess.compact(ctx) - tokensAfter := EstimateTokens(sess.Persistence().RawMessages()) + tokensAfter := token.EstimateTokens(sess.Persistence().RawMessages()) sess.recordCompaction("truncate_fallback", tokensBefore, tokensAfter, false) return "truncate_fallback", true } - tokensAfter := EstimateTokens(sess.Persistence().RawMessages()) + tokensAfter := token.EstimateTokens(sess.Persistence().RawMessages()) if tokensAfter >= tokensBefore { // Strategy ran but produced no reduction (e.g. LLM summary was // rejected, or messages were not reduced); fall back to truncation. @@ -101,7 +105,7 @@ func (ac *AutoCompactor) AutoCompactIfNeeded(ctx context.Context, sess *Session) "tokens_after": tokensAfter, }) sess.compact(ctx) - tokensAfter = EstimateTokens(sess.Persistence().RawMessages()) + tokensAfter = token.EstimateTokens(sess.Persistence().RawMessages()) sess.recordCompaction("truncate_fallback", tokensBefore, tokensAfter, false) return "truncate_fallback", true } @@ -122,7 +126,7 @@ func (ac *AutoCompactor) RunCompaction(ctx context.Context, sess *Session) (stri ac.mu.Unlock() messages := sess.Persistence().RawMessages() - tokenCount := EstimateTokens(messages) + tokenCount := token.EstimateTokens(messages) strategy := registry.SelectStrategy(sess, messages, tokenCount) if strategy == nil { return "", errors.New("no compaction strategy available") @@ -177,13 +181,13 @@ func (s *SmartCompactStrategy) ShouldTrigger(msgs []types.FluxMessage, tokenCoun return tokenCount >= threshold && len(msgs) > 20 } -func (s *SmartCompactStrategy) Compact(ctx context.Context, sess *Session) (*CompactResult, error) { - tokensBefore := EstimateTokens(sess.Persistence().RawMessages()) +func (s *SmartCompactStrategy) Compact(ctx context.Context, sess *Session) (*compact.CompactResult, error) { + tokensBefore := token.EstimateTokens(sess.Persistence().RawMessages()) sess.smartCompact(ctx) messages := sess.Persistence().RawMessages() - tokensAfter := EstimateTokens(messages) + tokensAfter := token.EstimateTokens(messages) - return &CompactResult{ + return &compact.CompactResult{ Messages: messages, TokensBefore: tokensBefore, TokensAfter: tokensAfter, @@ -200,13 +204,13 @@ func (s *TruncateStrategy) ShouldTrigger(_ []types.FluxMessage, tokenCount, thre return tokenCount >= threshold } -func (s *TruncateStrategy) Compact(ctx context.Context, sess *Session) (*CompactResult, error) { - tokensBefore := EstimateTokens(sess.Persistence().RawMessages()) +func (s *TruncateStrategy) Compact(ctx context.Context, sess *Session) (*compact.CompactResult, error) { + tokensBefore := token.EstimateTokens(sess.Persistence().RawMessages()) sess.compact(ctx) messages := sess.Persistence().RawMessages() - tokensAfter := EstimateTokens(messages) + tokensAfter := token.EstimateTokens(messages) - return &CompactResult{ + return &compact.CompactResult{ Messages: messages, TokensBefore: tokensBefore, TokensAfter: tokensAfter, diff --git a/internal/engine/compact_clear.go b/internal/engine/compact_clear.go index abe30faf..1fa0f366 100644 --- a/internal/engine/compact_clear.go +++ b/internal/engine/compact_clear.go @@ -4,6 +4,8 @@ import ( "context" "sort" + "github.com/GrayCodeAI/rho/internal/engine/token" + "github.com/GrayCodeAI/rho/internal/types" ) @@ -80,7 +82,7 @@ func (s *Session) ClearOldToolResults(ctx context.Context) bool { if window <= 0 { return false } - tokens := EstimateTokens(raw) + tokens := token.EstimateTokens(raw) threshold := int(float64(window) * clearThresholdFraction) if tokens < threshold { return false diff --git a/internal/engine/compact_micro_engine.go b/internal/engine/compact_micro_engine.go index 9a085183..7bf6d3f2 100644 --- a/internal/engine/compact_micro_engine.go +++ b/internal/engine/compact_micro_engine.go @@ -4,6 +4,8 @@ import ( "context" "time" + "github.com/GrayCodeAI/rho/internal/engine/token" + "github.com/GrayCodeAI/rho/internal/types" "github.com/GrayCodeAI/rho/internal/engine/compact" @@ -29,13 +31,13 @@ func (s *MicroCompactStrategy) ShouldTrigger(msgs []types.FluxMessage, tokenCoun return compact.HasTimeGap(msgs, 60*time.Minute) } -func (s *MicroCompactStrategy) Compact(ctx context.Context, sess *Session) (*CompactResult, error) { +func (s *MicroCompactStrategy) Compact(ctx context.Context, sess *Session) (*compact.CompactResult, error) { messages := sess.Persistence().RawMessages() - tokensBefore := EstimateTokens(messages) - result := compact.MicrocompactMessages(messages, DefaultMicroCompactConfig()) - tokensAfter := EstimateTokens(result) + tokensBefore := token.EstimateTokens(messages) + result := compact.MicrocompactMessages(messages, compact.DefaultMicroCompactConfig()) + tokensAfter := token.EstimateTokens(result) - return &CompactResult{ + return &compact.CompactResult{ Messages: result, TokensBefore: tokensBefore, TokensAfter: tokensAfter, diff --git a/internal/engine/compact_provider_native.go b/internal/engine/compact_provider_native.go index 066c020d..830f2352 100644 --- a/internal/engine/compact_provider_native.go +++ b/internal/engine/compact_provider_native.go @@ -4,6 +4,10 @@ import ( "context" "fmt" + "github.com/GrayCodeAI/rho/internal/engine/token" + + "github.com/GrayCodeAI/rho/internal/engine/compact" + "github.com/GrayCodeAI/flux/llm" "github.com/GrayCodeAI/rho/internal/types" ) @@ -19,7 +23,7 @@ func (s *ProviderNativeCompactStrategy) ShouldTrigger(msgs []types.FluxMessage, return tokenCount >= threshold && len(msgs) >= 8 } -func (s *ProviderNativeCompactStrategy) Compact(ctx context.Context, sess *Session) (*CompactResult, error) { +func (s *ProviderNativeCompactStrategy) Compact(ctx context.Context, sess *Session) (*compact.CompactResult, error) { if sess == nil { return nil, fmt.Errorf("no session") } @@ -32,7 +36,7 @@ func (s *ProviderNativeCompactStrategy) Compact(ctx context.Context, sess *Sessi } messagesBefore := sess.Persistence().RawMessages() - tokensBefore := EstimateTokens(messagesBefore) + tokensBefore := token.EstimateTokens(messagesBefore) summary, err := compactor.CompactNative(ctx, llm.NativeCompactionRequest{ Provider: sess.ChatLLM().Provider(), Model: sess.ChatLLM().Model(), @@ -50,11 +54,11 @@ func (s *ProviderNativeCompactStrategy) Compact(ctx context.Context, sess *Sessi keepEnd = len(messagesBefore) } tail := append([]types.FluxMessage(nil), messagesBefore[len(messagesBefore)-keepEnd:]...) - messages := append([]types.FluxMessage{{Role: "user", Content: FormatCompactSummary(summary)}}, tail...) - compact := &CompactResult{ + messages := append([]types.FluxMessage{{Role: "user", Content: compact.FormatCompactSummary(summary)}}, tail...) + compact := &compact.CompactResult{ Messages: messages, TokensBefore: tokensBefore, - TokensAfter: EstimateTokens(messages), + TokensAfter: token.EstimateTokens(messages), Strategy: "provider_native", } sess.Persistence().SetMessages(compact.Messages) diff --git a/internal/engine/compact_reexports.go b/internal/engine/compact_reexports.go deleted file mode 100644 index 0c9014e3..00000000 --- a/internal/engine/compact_reexports.go +++ /dev/null @@ -1,57 +0,0 @@ -// This file re-exports symbols from the compact sub-package so that existing -// callers of engine.* keep compiling during the Stage 2 migration. -// See docs/plans/engine-refactor-plan.md. -package engine - -import ( - "github.com/GrayCodeAI/rho/internal/types" - - "github.com/GrayCodeAI/rho/internal/engine/compact" -) - -type CompactVariant = compact.CompactVariant - -const ( - CompactBase = compact.CompactBase - CompactPartial = compact.CompactPartial - CompactUpTo = compact.CompactUpTo -) - -type ( - CompactResult = compact.CompactResult - CompactConfig = compact.CompactConfig - FileTracker = compact.FileTracker - MicroCompactConfig = compact.MicroCompactConfig - APICompactConfig = compact.APICompactConfig - SessionMemoryConfig = compact.SessionMemoryConfig - CompactionTrigger = compact.CompactionTrigger -) - -func DefaultCompactConfig() CompactConfig { return compact.DefaultCompactConfig() } -func DefaultMicroCompactConfig() MicroCompactConfig { return compact.DefaultMicroCompactConfig() } -func DefaultAPICompactConfig() APICompactConfig { return compact.DefaultAPICompactConfig() } -func DefaultSessionMemoryConfig() SessionMemoryConfig { return compact.DefaultSessionMemoryConfig() } -func NewFileTracker() *FileTracker { return compact.NewFileTracker() } -func NewCompactionTrigger(windowSize int) *CompactionTrigger { - return compact.NewCompactionTrigger(windowSize) -} - -func BuildCompactPrompt(variant CompactVariant) string { return compact.BuildCompactPrompt(variant) } -func FormatCompactSummary(raw string) string { return compact.FormatCompactSummary(raw) } -func BuildIncrementalCompactPrompt(priorSummary string) string { - return compact.BuildIncrementalCompactPrompt(priorSummary) -} - -func ExtractPriorSummary(msgs []types.FluxMessage) string { - return compact.ExtractPriorSummary(msgs) -} - -// PriorSummaryPrefix is the marker prefix rho prepends to a persisted -// conversation summary message. -const PriorSummaryPrefix = compact.PriorSummaryPrefix - -func IsCompactableTool(name string) bool { return compact.IsCompactableTool(name) } -func AdjustIndexToPreserveAPIInvariants(msgs []types.FluxMessage, startIdx int) int { - return compact.AdjustIndexToPreserveAPIInvariants(msgs, startIdx) -} -func HasTextContent(m types.FluxMessage) bool { return compact.HasTextContent(m) } diff --git a/internal/engine/compact_relevance.go b/internal/engine/compact_relevance.go index e2ca5444..3bba9155 100644 --- a/internal/engine/compact_relevance.go +++ b/internal/engine/compact_relevance.go @@ -4,6 +4,10 @@ import ( "context" "time" + "github.com/GrayCodeAI/rho/internal/engine/token" + + "github.com/GrayCodeAI/rho/internal/engine/compact" + "github.com/GrayCodeAI/rho/internal/relevanceprune" "github.com/GrayCodeAI/rho/internal/types" ) @@ -27,8 +31,8 @@ func (s *RelevancePruneStrategy) ShouldTrigger(msgs []types.FluxMessage, tokenCo return tokenCount >= threshold && len(msgs) >= 20 } -func (s *RelevancePruneStrategy) Compact(ctx context.Context, sess *Session) (*CompactResult, error) { - tokensBefore := EstimateTokens(sess.Persistence().RawMessages()) +func (s *RelevancePruneStrategy) Compact(ctx context.Context, sess *Session) (*compact.CompactResult, error) { + tokensBefore := token.EstimateTokens(sess.Persistence().RawMessages()) raw := sess.Persistence().RawMessages() taskContext := lastUserText(raw) @@ -45,8 +49,8 @@ func (s *RelevancePruneStrategy) Compact(ctx context.Context, sess *Session) (*C out := toFluxMessages(pruned) sess.Persistence().SetMessages(out) - tokensAfter := EstimateTokens(sess.Persistence().RawMessages()) - return &CompactResult{ + tokensAfter := token.EstimateTokens(sess.Persistence().RawMessages()) + return &compact.CompactResult{ Messages: out, TokensBefore: tokensBefore, TokensAfter: tokensAfter, diff --git a/internal/engine/compact_session_memory_engine.go b/internal/engine/compact_session_memory_engine.go index c4592eea..1f5b338d 100644 --- a/internal/engine/compact_session_memory_engine.go +++ b/internal/engine/compact_session_memory_engine.go @@ -6,6 +6,8 @@ import ( "os" "strings" + "github.com/GrayCodeAI/rho/internal/engine/token" + "github.com/GrayCodeAI/rho/internal/types" "github.com/GrayCodeAI/rho/internal/engine/compact" @@ -27,7 +29,7 @@ func (s *SessionMemoryStrategy) ShouldTrigger(msgs []types.FluxMessage, tokenCou return true } -func (s *SessionMemoryStrategy) Compact(ctx context.Context, sess *Session) (*CompactResult, error) { +func (s *SessionMemoryStrategy) Compact(ctx context.Context, sess *Session) (*compact.CompactResult, error) { memContent, err := compact.ReadSessionMemory("") if err != nil { return nil, fmt.Errorf("reading session memory: %w", err) @@ -37,9 +39,9 @@ func (s *SessionMemoryStrategy) Compact(ctx context.Context, sess *Session) (*Co } messages := sess.Persistence().RawMessages() - tokensBefore := EstimateTokens(messages) + tokensBefore := token.EstimateTokens(messages) - cfg := DefaultSessionMemoryConfig() + cfg := compact.DefaultSessionMemoryConfig() keepIdx := compact.CalculateMessagesToKeepIndex(messages, cfg) keepIdx = compact.AdjustIndexToPreserveAPIInvariants(messages, keepIdx) @@ -61,9 +63,9 @@ func (s *SessionMemoryStrategy) Compact(ctx context.Context, sess *Session) (*Co }) result = append(result, kept...) - tokensAfter := EstimateTokens(result) + tokensAfter := token.EstimateTokens(result) - return &CompactResult{ + return &compact.CompactResult{ Messages: result, Summary: memContent, TokensBefore: tokensBefore, diff --git a/internal/engine/compact_split.go b/internal/engine/compact_split.go index c65d3b14..0c811edb 100644 --- a/internal/engine/compact_split.go +++ b/internal/engine/compact_split.go @@ -5,6 +5,10 @@ import ( "strings" "time" + "github.com/GrayCodeAI/rho/internal/engine/token" + + "github.com/GrayCodeAI/rho/internal/engine/compact" + "github.com/GrayCodeAI/rho/internal/types" ) @@ -27,7 +31,7 @@ func turnTokenBudget(tail []types.FluxMessage) int { } totalTokens := 0 for _, msg := range tail { - totalTokens += EstimateMessageTokens(msg) + totalTokens += token.EstimateMessageTokens(msg) } budget := totalTokens / len(tail) * 3 if budget < 2000 { @@ -51,7 +55,7 @@ func (s *Session) SplitTurnNeeded(keepCount int) bool { // Check if any single message in the tail exceeds the budget for _, msg := range tail { - if EstimateMessageTokens(msg) > budget { + if token.EstimateMessageTokens(msg) > budget { return true } } @@ -80,7 +84,7 @@ func (s *Session) splitTurnCompact(ctx context.Context) { oversizedIdx := -1 for i, msg := range tail { - if EstimateMessageTokens(msg) > budget { + if token.EstimateMessageTokens(msg) > budget { oversizedIdx = i break } @@ -98,7 +102,7 @@ func (s *Session) splitTurnCompact(ctx context.Context) { // Extract file tracking before compaction if s.Persistence().Files() == nil { - s.Persistence().SetFiles(NewFileTracker()) + s.Persistence().SetFiles(compact.NewFileTracker()) } files := s.Persistence().Files() files.ExtractFromMessages(raw[:splitPoint]) @@ -176,7 +180,7 @@ func (s *Session) generatePartialSummary(ctx context.Context, messages []types.F } var summaryMsgs []types.FluxMessage - compactPrompt := BuildCompactPrompt(CompactPartial) + compactPrompt := compact.BuildCompactPrompt(compact.CompactPartial) var content strings.Builder content.WriteString(compactPrompt) content.WriteString("\n\nConversation:\n") @@ -211,7 +215,7 @@ func (s *Session) generatePartialSummary(ctx context.Context, messages []types.F if err != nil { return "" } - return FormatCompactSummary(resp.Content) + return compact.FormatCompactSummary(resp.Content) } // summarizeOversizedTurn summarizes the content of a single oversized message. @@ -237,7 +241,7 @@ func (s *Session) summarizeOversizedTurn(ctx context.Context, msg types.FluxMess var summaryMsgs []types.FluxMessage summaryMsgs = append(summaryMsgs, types.FluxMessage{ Role: "user", - Content: BuildCompactPrompt(CompactUpTo) + "\n\nSummarize the key information from this content prefix (the rest will be retained verbatim):\n\n" + + Content: compact.BuildCompactPrompt(compact.CompactUpTo) + "\n\nSummarize the key information from this content prefix (the rest will be retained verbatim):\n\n" + msg.Role + ": " + truncateRunes(content, halfLen), }) @@ -256,7 +260,7 @@ func (s *Session) summarizeOversizedTurn(ctx context.Context, msg types.FluxMess if err != nil { return "" } - return FormatCompactSummary(resp.Content) + return compact.FormatCompactSummary(resp.Content) } // smartCompactFallback is reached when split-turn compaction is detected but diff --git a/internal/engine/compact_strategy_engine.go b/internal/engine/compact_strategy_engine.go index 34897489..957bc4fa 100644 --- a/internal/engine/compact_strategy_engine.go +++ b/internal/engine/compact_strategy_engine.go @@ -3,21 +3,23 @@ package engine import ( "context" + "github.com/GrayCodeAI/rho/internal/engine/compact" + "github.com/GrayCodeAI/rho/internal/types" ) type CompactStrategy interface { Name() string ShouldTrigger(msgs []types.FluxMessage, tokenCount, threshold int) bool - Compact(ctx context.Context, s *Session) (*CompactResult, error) + Compact(ctx context.Context, s *Session) (*compact.CompactResult, error) } type StrategyRegistry struct { strategies []CompactStrategy - config CompactConfig + config compact.CompactConfig } -func NewStrategyRegistry(config CompactConfig) *StrategyRegistry { +func NewStrategyRegistry(config compact.CompactConfig) *StrategyRegistry { r := &StrategyRegistry{config: config} target := config.ContextWindowSize - config.AutoCompactBuffer - config.MaxOutputTokens r.strategies = []CompactStrategy{ diff --git a/internal/engine/compact_strategy_test.go b/internal/engine/compact_strategy_test.go index 33fdf6cb..84e3467d 100644 --- a/internal/engine/compact_strategy_test.go +++ b/internal/engine/compact_strategy_test.go @@ -6,6 +6,10 @@ import ( "testing" "time" + "github.com/GrayCodeAI/rho/internal/engine/token" + + "github.com/GrayCodeAI/rho/internal/engine/compact" + "github.com/GrayCodeAI/rho/internal/types" ) @@ -18,7 +22,7 @@ func TestSessionMemoryStrategy_ShouldTrigger(t *testing.T) { } func TestAutoCompactor_CircuitBreaker(t *testing.T) { - cfg := DefaultCompactConfig() + cfg := compact.DefaultCompactConfig() cfg.MaxFailures = 2 cfg.ContextWindowSize = 1000 cfg.AutoCompactBuffer = 100 @@ -43,7 +47,7 @@ func TestAutoCompactor_CircuitBreaker(t *testing.T) { } func TestStrategyRegistry_SelectStrategy(t *testing.T) { - cfg := DefaultCompactConfig() + cfg := compact.DefaultCompactConfig() registry := NewStrategyRegistry(cfg) msgs := makeMessages(5) @@ -109,7 +113,7 @@ func TestTurnTokenBudget(t *testing.T) { for i := range big { big[i] = types.FluxMessage{Role: "user", Content: strings.Repeat("x", 10000)} } - avg := EstimateMessageTokens(big[0]) + avg := token.EstimateMessageTokens(big[0]) if avg*3 <= 2000 { t.Fatalf("test messages too small to exceed the 2000 floor: avg=%d", avg) } diff --git a/internal/engine/context_compaction.go b/internal/engine/context_compaction.go index 5aaffbf8..a211664c 100644 --- a/internal/engine/context_compaction.go +++ b/internal/engine/context_compaction.go @@ -3,6 +3,8 @@ package engine import ( "path/filepath" + "github.com/GrayCodeAI/rho/internal/engine/token" + "github.com/GrayCodeAI/rho/internal/eventlog" "github.com/GrayCodeAI/rho/internal/session" "github.com/GrayCodeAI/rho/internal/storage" @@ -73,7 +75,7 @@ func (s *Session) ContextUsedTokens() int { return cache } - est := EstimateTokens(msgs) + est := token.EstimateTokens(msgs) persist.SetTokenEstimateCache(est, count, lastLen) return est diff --git a/internal/engine/context_governor.go b/internal/engine/context_governor.go index 9ba2e339..a49510b2 100644 --- a/internal/engine/context_governor.go +++ b/internal/engine/context_governor.go @@ -4,6 +4,10 @@ import ( "context" "fmt" + "github.com/GrayCodeAI/rho/internal/engine/token" + + "github.com/GrayCodeAI/rho/internal/engine/compact" + "github.com/GrayCodeAI/rho/internal/engine/ctxmgr" modelPkg "github.com/GrayCodeAI/rho/internal/provider/routing" ) @@ -71,11 +75,11 @@ func (s *Session) compactThresholdPct() int { return pct } -func (s *Session) compactConfig() CompactConfig { +func (s *Session) compactConfig() compact.CompactConfig { window := s.ContextWindowSize() pct := s.compactThresholdPct() target := window * pct / 100 - cfg := DefaultCompactConfig() + cfg := compact.DefaultCompactConfig() cfg.AutoEnabled = true cfg.ContextWindowSize = window cfg.MaxOutputTokens = 0 @@ -116,7 +120,7 @@ func (s *Session) WillCompactBeforeTurn() bool { if len(s.Persistence().RawMessagesView()) > maxContextMessages { return true } - convTokens := EstimateTokens(s.Persistence().RawMessagesView()) + convTokens := token.EstimateTokens(s.Persistence().RawMessagesView()) budget := ctxmgr.NewContextBudget(s.ContextWindowSize()) return budget.ShouldCompact(convTokens) } @@ -140,25 +144,25 @@ func (s *Session) ManageContextBeforeTurn(ctx context.Context) (strategy string, } if len(s.Persistence().RawMessagesView()) > maxContextMessages { - before := EstimateTokens(s.Persistence().RawMessagesView()) + before := token.EstimateTokens(s.Persistence().RawMessagesView()) beforeMsgs := len(s.Persistence().RawMessagesView()) s.smartCompact(ctx) s.lastCompactionMsgDelta = beforeMsgs - len(s.Persistence().RawMessagesView()) s.lastCompactionSummary = "" // smartCompact may set it via the summary path - s.recordCompaction("smart_message_cap", before, EstimateTokens(s.Persistence().RawMessagesView()), false) + s.recordCompaction("smart_message_cap", before, token.EstimateTokens(s.Persistence().RawMessagesView()), false) return "smart_message_cap", true } - convTokens := EstimateTokens(s.Persistence().RawMessagesView()) + convTokens := token.EstimateTokens(s.Persistence().RawMessagesView()) window := s.ContextWindowSize() budget := ctxmgr.NewContextBudget(window) if budget.ShouldCompact(convTokens) { - before := EstimateTokens(s.Persistence().RawMessagesView()) + before := token.EstimateTokens(s.Persistence().RawMessagesView()) beforeMsgs := len(s.Persistence().RawMessagesView()) s.smartCompact(ctx) s.lastCompactionMsgDelta = beforeMsgs - len(s.Persistence().RawMessagesView()) s.lastCompactionSummary = "" - s.recordCompaction("smart_budget", before, EstimateTokens(s.Persistence().RawMessagesView()), false) + s.recordCompaction("smart_budget", before, token.EstimateTokens(s.Persistence().RawMessagesView()), false) return "smart_budget", true } @@ -173,13 +177,13 @@ func (s *Session) CompactConversation(ctx context.Context) (strategy string, tok s.Persistence().SetRawMessages(ctxmgr.CollapseRepeatedMessages(s.Persistence().RawMessages())) s.EnsureAutoCompactor() beforeMsgs := len(s.Persistence().RawMessages()) - tokensBefore = EstimateTokens(s.Persistence().RawMessages()) + tokensBefore = token.EstimateTokens(s.Persistence().RawMessages()) strategy, err = s.Persistence().AutoCompactor().RunCompaction(ctx, s) if err != nil { s.smartCompact(ctx) strategy = "smart_fallback" } - tokensAfter = EstimateTokens(s.Persistence().RawMessages()) + tokensAfter = token.EstimateTokens(s.Persistence().RawMessages()) s.lastCompactionMsgDelta = beforeMsgs - len(s.Persistence().RawMessages()) s.recordCompaction(strategy, tokensBefore, tokensAfter, true) return strategy, tokensBefore, tokensAfter, nil @@ -188,6 +192,6 @@ func (s *Session) CompactConversation(ctx context.Context) (strategy string, tok // ShouldCompactByBudget reports whether conversation tokens exceed the configured % of window. func (s *Session) ShouldCompactByBudget() bool { window := s.ContextWindowSize() - conv := EstimateTokens(s.Persistence().RawMessages()) + conv := token.EstimateTokens(s.Persistence().RawMessages()) return conv >= window*s.compactThresholdPct()/100 } diff --git a/internal/engine/control_reexports.go b/internal/engine/control_reexports.go deleted file mode 100644 index 5fb30009..00000000 --- a/internal/engine/control_reexports.go +++ /dev/null @@ -1,20 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/control" - -type ( - LoopDetector = control.LoopDetector - StallEntry = control.StallEntry - StallResult = control.StallResult - StallDetector = control.StallDetector - DecisionPoint = control.DecisionPoint - BacktrackEngine = control.BacktrackEngine -) - -const DoomLoopThreshold = control.DoomLoopThreshold - -var ( - NewLoopDetector = control.NewLoopDetector - NewStallDetector = control.NewStallDetector - NewBacktrackEngine = control.NewBacktrackEngine -) diff --git a/internal/engine/cost_reexports.go b/internal/engine/cost_reexports.go deleted file mode 100644 index c42adfd9..00000000 --- a/internal/engine/cost_reexports.go +++ /dev/null @@ -1,28 +0,0 @@ -package engine - -import ( - "github.com/GrayCodeAI/rho/internal/engine/cost" - analytics "github.com/GrayCodeAI/rho/internal/observability" -) - -type ( - Cost = cost.Cost - CostSnapshot = cost.Snapshot - CostOptimizer = cost.CostOptimizer - CostTracker = cost.CostTracker - RequestCost = cost.RequestCost - ModelPrice = cost.ModelPrice - Recommendation = cost.Recommendation -) - -func NewCostOptimizer() *CostOptimizer { return cost.NewCostOptimizer() } -func NewCostTracker(sessionID string) *CostTracker { return cost.NewCostTracker(sessionID) } -func LoadCostHistory() ([]analytics.CostEntry, error) { return cost.LoadCostHistory() } -func FormatCostDisplay(totalUSD float64) string { return cost.FormatCostDisplay(totalUSD) } -func ModelPricing(modelName string) (inputPricePerM, outputPricePerM float64) { - return cost.ModelPricing(modelName) -} - -func RegisterLivePricing(model string, inputPerM, outputPerM float64) { - cost.RegisterLivePricing(model, inputPerM, outputPerM) -} diff --git a/internal/engine/developer_features_test.go b/internal/engine/developer_features_test.go index fc6bd0fd..e631e4da 100644 --- a/internal/engine/developer_features_test.go +++ b/internal/engine/developer_features_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" "time" + + "github.com/GrayCodeAI/rho/internal/engine/compact" ) func TestBackgroundRunner_Delegate(t *testing.T) { @@ -49,7 +51,7 @@ func TestBackgroundRunner_CollectWhileRunning(t *testing.T) { } func TestCompactionTrigger(t *testing.T) { - ct := NewCompactionTrigger(100000) + ct := compact.NewCompactionTrigger(100000) if ct.ShouldCompact(50000) { t.Error("50% should not trigger at 75% threshold") } diff --git a/internal/engine/diff_reexports.go b/internal/engine/diff_reexports.go deleted file mode 100644 index acc6934e..00000000 --- a/internal/engine/diff_reexports.go +++ /dev/null @@ -1,55 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/diff" - -// Types from diff sub-package. - -type ( - PendingChange = diff.PendingChange - DiffSandbox = diff.DiffSandbox - StagingArea = diff.StagingArea - StagedChange = diff.StagedChange - StagedHunk = diff.StagedHunk - DiffPreview = diff.DiffPreview - FileChange = diff.FileChange - DiffHunk = diff.DiffHunk - DiffLine = diff.DiffLine - ChangeStats = diff.ChangeStats - DiffSummary = diff.DiffSummary - FileSummary = diff.FileSummary - DiffSummarizer = diff.DiffSummarizer - TestSelector = diff.TestSelector - SelectedTests = diff.SelectedTests - Diff3Result = diff.Diff3Result - Diff3Conflict = diff.Diff3Conflict - Diff3Stats = diff.Diff3Stats - Diff3Region = diff.Diff3Region - Edit = diff.Edit -) - -// Short-name aliases. - -type ( - Preview = diff.DiffPreview - Summarizer = diff.DiffSummarizer -) - -// Functions. - -var ( - NewDiffSandbox = diff.NewDiffSandbox - NewStagingArea = diff.NewStagingArea - NewDiffPreview = diff.NewDiffPreview - NewDiffSummarizer = diff.NewDiffSummarizer - NewTestSelector = diff.NewTestSelector - ComputeDiff = diff.ComputeDiff - ComputeMyersDiff = diff.ComputeMyersDiff - RenderUnified = diff.RenderUnified - Merge3 = diff.Merge3 - MergeClean = diff.MergeClean - FormatConflictMarkers = diff.FormatConflictMarkers - LCS = diff.LCS - EditScript = diff.EditScript - BuildDependencyGraph = diff.BuildDependencyGraph - GenerateTestCommand = diff.GenerateTestCommand -) diff --git a/internal/engine/engine_integration_test.go b/internal/engine/engine_integration_test.go index 00d566fc..2372ed48 100644 --- a/internal/engine/engine_integration_test.go +++ b/internal/engine/engine_integration_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/GrayCodeAI/rho/internal/engine/cost" + contracts "github.com/GrayCodeAI/rho/internal/contracts/policy" "github.com/GrayCodeAI/rho/internal/types" @@ -365,7 +367,7 @@ func TestIntegration_MaxTurns(t *testing.T) { } // Simulate cost accumulation to test budget checking. - sess.Cost = Cost{Model: "gpt-4o"} + sess.Cost = cost.Cost{Model: "gpt-4o"} sess.Cost.Add(500_000, 200_000) // ~$3.25 which exceeds $1.0 if !sess.exceededBudget() { t.Fatal("expected budget to be exceeded") @@ -374,7 +376,7 @@ func TestIntegration_MaxTurns(t *testing.T) { // Under-budget should not trigger. sess2 := newTestSession() sess2.SetMaxBudgetUSD(100.0) - sess2.Cost = Cost{Model: "gpt-4o"} + sess2.Cost = cost.Cost{Model: "gpt-4o"} sess2.Cost.Add(100, 50) if sess2.exceededBudget() { t.Fatal("should not exceed $100 budget with minimal tokens") diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index f8ed5fc1..42e76d46 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -3,6 +3,8 @@ package engine import ( "context" "testing" + + "github.com/GrayCodeAI/rho/internal/engine/cost" ) func TestPermissionMemoryAlwaysAllow(t *testing.T) { @@ -135,7 +137,7 @@ func TestToolNeedsPermission(t *testing.T) { } func TestCostPricing(t *testing.T) { - c := Cost{Model: "claude-3-5-sonnet-20241022"} + c := cost.Cost{Model: "claude-3-5-sonnet-20241022"} c.Add(1000, 500) if c.PromptTokens != 1000 { t.Fatalf("got %d prompt tokens", c.PromptTokens) @@ -153,7 +155,7 @@ func TestCostPricing(t *testing.T) { } func TestCostSummary(t *testing.T) { - c := Cost{Model: "gpt-4o"} + c := cost.Cost{Model: "gpt-4o"} c.Add(100, 50) s := c.Summary() if s == "" { @@ -162,7 +164,7 @@ func TestCostSummary(t *testing.T) { } func TestCostTotal(t *testing.T) { - c := Cost{Model: "gpt-4o"} + c := cost.Cost{Model: "gpt-4o"} c.Add(100, 50) if c.Total() <= 0 { t.Fatal("expected positive total") diff --git a/internal/engine/errs_reexports.go b/internal/engine/errs_reexports.go deleted file mode 100644 index 6eb21669..00000000 --- a/internal/engine/errs_reexports.go +++ /dev/null @@ -1,31 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/errs" - -type ( - ErrorContext = errs.ErrorContext - ErrorHelp = errs.ErrorHelp - EnrichedError = errs.EnrichedError - ErrorGroup = errs.ErrorGroup - ErrorInstance = errs.ErrorInstance - ErrorGrouper = errs.ErrorGrouper - ErrorPattern = errs.ErrorPattern - ErrorPatternDB = errs.ErrorPatternDB - ErrorLearner = errs.ErrorLearner - ErrorLearnerStats = errs.ErrorLearnerStats - LearnedPattern = errs.LearnedPattern - ErrorRecovery = errs.ErrorRecovery - RecoveryStrategy = errs.RecoveryStrategy - RecoveryContext = errs.RecoveryContext - RecoveryResult = errs.RecoveryResult - RecoveryAttempt = errs.RecoveryAttempt -) - -func NewErrorContext() *ErrorContext { return errs.NewErrorContext() } -func FormatError(enriched *EnrichedError) string { return errs.FormatError(enriched) } -func NewErrorGrouper() *ErrorGrouper { return errs.NewErrorGrouper() } -func NormalizeError(msg string) string { return errs.NormalizeError(msg) } -func NewErrorLearner() *ErrorLearner { return errs.NewErrorLearner() } -func ExtractPattern(errorMsg string) string { return errs.ExtractPattern(errorMsg) } -func NewErrorPatternDB() *ErrorPatternDB { return errs.NewErrorPatternDB() } -func NewErrorRecovery() *ErrorRecovery { return errs.NewErrorRecovery() } diff --git a/internal/engine/history_reexports.go b/internal/engine/history_reexports.go deleted file mode 100644 index f6720c02..00000000 --- a/internal/engine/history_reexports.go +++ /dev/null @@ -1,39 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/history" - -type ( - CommandRecord = history.CommandRecord - CommandFrequency = history.CommandFrequency - AliasSuggestion = history.AliasSuggestion - CommandHistory = history.CommandHistory - SummaryLevel = history.SummaryLevel - SumMessage = history.SumMessage - Summary = history.Summary - ConversationSummarizer = history.ConversationSummarizer - DistillExample = history.DistillExample - DistillStats = history.DistillStats - DistillationPipeline = history.DistillationPipeline - HeadTailWindow = history.HeadTailWindow - WindowResult = history.WindowResult - WindowMessage = history.WindowMessage - FileMentionDetector = history.FileMentionDetector - Annotation = history.Annotation - AnnotationManager = history.AnnotationManager -) - -var ( - NewCommandHistory = history.NewCommandHistory - NewConversationSummarizer = history.NewConversationSummarizer - NewDistillationPipeline = history.NewDistillationPipeline - NewHeadTailWindow = history.NewHeadTailWindow - AdaptiveSizes = history.AdaptiveSizes - PreserveToolPairs = history.PreserveToolPairs - FormatWindow = history.FormatWindow - ShouldApply = history.ShouldApply - NewFileMentionDetector = history.NewFileMentionDetector - NewAnnotationManager = history.NewAnnotationManager - StripAnnotations = history.StripAnnotations - DetectAnnotations = history.DetectAnnotations - FormatAnnotations = history.FormatAnnotations -) diff --git a/internal/engine/integration.go b/internal/engine/integration.go index 3e45ca07..76562bbb 100644 --- a/internal/engine/integration.go +++ b/internal/engine/integration.go @@ -8,6 +8,18 @@ import ( "sync" "time" + "github.com/GrayCodeAI/rho/internal/engine/compression" + "github.com/GrayCodeAI/rho/internal/engine/control" + "github.com/GrayCodeAI/rho/internal/engine/errs" + "github.com/GrayCodeAI/rho/internal/engine/history" + "github.com/GrayCodeAI/rho/internal/engine/intelligence" + "github.com/GrayCodeAI/rho/internal/engine/memory" + "github.com/GrayCodeAI/rho/internal/engine/observability" + "github.com/GrayCodeAI/rho/internal/engine/review" + "github.com/GrayCodeAI/rho/internal/engine/streaming" + "github.com/GrayCodeAI/rho/internal/engine/validation" + "github.com/GrayCodeAI/rho/internal/engine/workflow" + "github.com/GrayCodeAI/rho/internal/engine/ctxmgr" "github.com/GrayCodeAI/rho/internal/engine/token" "github.com/GrayCodeAI/rho/internal/eventlog" @@ -27,40 +39,40 @@ import ( // through the request lifecycle. type IntegrationPipeline struct { // Pre-query pipeline - IntentClassifier *IntentClassifier - ToolSelector *ToolSelector + IntentClassifier *intelligence.IntentClassifier + ToolSelector *intelligence.ToolSelector ContextDecay *ctxmgr.ContextDecay BudgetAllocator *BudgetAllocator - TokenPredictor *TokenPredictor + TokenPredictor *token.TokenPredictor AdaptivePrompt *SystemPromptBuilder // Post-response pipeline - ResponseFormatter *ResponseFormatter - QualityScorer *QualityScorer - FileMentionDetector *FileMentionDetector + ResponseFormatter *streaming.ResponseFormatter + QualityScorer *review.QualityScorer + FileMentionDetector *history.FileMentionDetector // Post-tool pipeline - LintLoop *LintLoop - TestLoop *TestLoop - StallDetector *StallDetector - ErrorRecovery *ErrorRecovery + LintLoop *validation.LintLoop + TestLoop *validation.TestLoop + StallDetector *control.StallDetector + ErrorRecovery *errs.ErrorRecovery // Security pipeline InjectionScanner *InjectionScanner OutputRedactor *OutputRedactor // Learning pipeline - ExperienceStore *ExperienceStore - KnowledgeBase *KnowledgeBase - FeedbackCollector *FeedbackCollector - SelfAssessor *SelfAssessor + ExperienceStore *memory.ExperienceStore + KnowledgeBase *memory.KnowledgeBase + FeedbackCollector *observability.FeedbackCollector + SelfAssessor *review.SelfAssessor // Session management - Timeline *Timeline - TokenReporter *TokenReporter - WorkspaceState *WorkspaceState - CommandHistory *CommandHistory - ResponseCache *ResponseCache + Timeline *compression.Timeline + TokenReporter *token.TokenReporter + WorkspaceState *workflow.WorkspaceState + CommandHistory *history.CommandHistory + ResponseCache *streaming.ResponseCache // Internal sessionStart time.Time @@ -149,7 +161,7 @@ func (is *InjectionScanner) Scan(input string) *ScanResult { // PreQueryResult holds the aggregated results of the pre-query pipeline. type PreQueryResult struct { - Intent *Intent + Intent *intelligence.Intent SuggestedTools []string BudgetAllocation map[string]int PredictedCost float64 @@ -180,8 +192,8 @@ type PostToolResult struct { // SessionSummary captures the final assessment when a session ends. type SessionSummary struct { - Assessment *Assessment - Experience *Experience + Assessment *review.Assessment + Experience *memory.Experience Summary string TokensTotal int Duration time.Duration @@ -205,40 +217,40 @@ func NewIntegrationPipeline() *IntegrationPipeline { return &IntegrationPipeline{ // Pre-query - IntentClassifier: NewIntentClassifier(), - ToolSelector: NewToolSelector(defaultToolSet()), + IntentClassifier: intelligence.NewIntentClassifier(), + ToolSelector: intelligence.NewToolSelector(defaultToolSet()), ContextDecay: ctxmgr.NewContextDecay(30 * time.Minute), BudgetAllocator: newDefaultBudgetAllocator(), - TokenPredictor: NewTokenPredictor(), + TokenPredictor: token.NewTokenPredictor(), AdaptivePrompt: NewSystemPromptBuilder("", 4096), // Post-response - ResponseFormatter: NewResponseFormatter(), - QualityScorer: NewQualityScorer(), - FileMentionDetector: NewFileMentionDetector("."), + ResponseFormatter: streaming.NewResponseFormatter(), + QualityScorer: review.NewQualityScorer(), + FileMentionDetector: history.NewFileMentionDetector("."), // Post-tool - LintLoop: NewLintLoop(), - TestLoop: NewTestLoop(), - StallDetector: NewStallDetector(), - ErrorRecovery: NewErrorRecovery(), + LintLoop: validation.NewLintLoop(), + TestLoop: validation.NewTestLoop(), + StallDetector: control.NewStallDetector(), + ErrorRecovery: errs.NewErrorRecovery(), // Security InjectionScanner: NewInjectionScanner(), OutputRedactor: NewOutputRedactor(), // Learning - ExperienceStore: NewExperienceStore(filepath.Join(stateRoot, "experience")), - KnowledgeBase: NewKnowledgeBase(filepath.Join(stateRoot, "knowledge")), - FeedbackCollector: NewFeedbackCollector(filepath.Join(stateRoot, "feedback")), - SelfAssessor: NewSelfAssessor(), + ExperienceStore: memory.NewExperienceStore(filepath.Join(stateRoot, "experience")), + KnowledgeBase: memory.NewKnowledgeBase(filepath.Join(stateRoot, "knowledge")), + FeedbackCollector: observability.NewFeedbackCollector(filepath.Join(stateRoot, "feedback")), + SelfAssessor: review.NewSelfAssessor(), // Session management - Timeline: NewTimeline("default"), - TokenReporter: NewTokenReporter(200000), - WorkspaceState: NewWorkspaceState("."), - CommandHistory: NewCommandHistory(), - ResponseCache: NewResponseCache(1000, 24*time.Hour), + Timeline: compression.NewTimeline("default"), + TokenReporter: token.NewTokenReporter(200000), + WorkspaceState: workflow.NewWorkspaceState("."), + CommandHistory: history.NewCommandHistory(), + ResponseCache: streaming.NewResponseCache(1000, 24*time.Hour), // Internal sessionStart: time.Now(), @@ -246,8 +258,8 @@ func NewIntegrationPipeline() *IntegrationPipeline { } // defaultToolSet returns the standard set of tools available to the agent. -func defaultToolSet() []ToolInfo { - return []ToolInfo{ +func defaultToolSet() []intelligence.ToolInfo { + return []intelligence.ToolInfo{ {Name: "Read", Category: "file", Cost: "free", ReadOnly: true}, {Name: "Write", Category: "file", Cost: "cheap", ReadOnly: false}, {Name: "Edit", Category: "file", Cost: "cheap", ReadOnly: false}, @@ -328,7 +340,7 @@ func (p *IntegrationPipeline) PreQuery(messages []types.FluxMessage, userInput s result.InjectionRisk = p.InjectionScanner.Scan(userInput) // 8. Check response cache - if ShouldCache(userInput) { + if streaming.ShouldCache(userInput) { if entry, ok := p.ResponseCache.Get(userInput, ""); ok { result.CacheHit = true result.CachedResponse = entry.Response @@ -364,7 +376,7 @@ func (p *IntegrationPipeline) PostResponse(response string, messages []types.Flu // 2. Score quality userPrompt := lastUserMessage(messages) - scoreCtx := ResponseContext{ + scoreCtx := review.ResponseContext{ UserPrompt: userPrompt, AssistantResponse: result.FormattedResponse, TokensUsed: EstimateStringTokens(result.FormattedResponse), @@ -400,12 +412,12 @@ func (p *IntegrationPipeline) PostResponse(response string, messages []types.Flu p.TokenReporter.Record(0, tokensUsed, "", "", 0) // 7. Cache the response - if ShouldCache(userPrompt) && result.QualityScore >= 0.7 { + if streaming.ShouldCache(userPrompt) && result.QualityScore >= 0.7 { p.ResponseCache.Set(userPrompt, result.FormattedResponse, "", tokensUsed) } // 8. Update experience store with implicit signal - _ = p.FeedbackCollector.RecordImplicit(ImplicitSignal{ + _ = p.FeedbackCollector.RecordImplicit(observability.ImplicitSignal{ Type: "accepted", SessionID: p.Timeline.SessionID, Timestamp: time.Now(), @@ -457,7 +469,7 @@ func (p *IntegrationPipeline) PostToolExecution(toolName string, args map[string // 4. Attempt error recovery if the tool failed if err != nil { - recoveryCtx := &RecoveryContext{ + recoveryCtx := &errs.RecoveryContext{ Error: err, ErrorMsg: err.Error(), LastToolCall: toolName, @@ -521,7 +533,7 @@ func (p *IntegrationPipeline) EndSession(ctx context.Context, success bool, task } // 1. Self-assess performance - taskCtx := TaskContext{ + taskCtx := review.TaskContext{ Goal: taskGoal, FilesModified: len(summary.FilesModified), Duration: summary.Duration, @@ -547,7 +559,7 @@ func (p *IntegrationPipeline) EndSession(ctx context.Context, success bool, task // 3. Update knowledge base if success && taskGoal != "" { - _ = p.KnowledgeBase.Add(&KnowledgeEntry{ + _ = p.KnowledgeBase.Add(&memory.KnowledgeEntry{ Title: fmt.Sprintf("Completed: %s", truncateString(taskGoal, 60)), Content: fmt.Sprintf("Task completed in %s with %d files modified.", summary.Duration.Round(time.Second), len(summary.FilesModified)), Category: "session_outcome", @@ -558,7 +570,7 @@ func (p *IntegrationPipeline) EndSession(ctx context.Context, success bool, task } // 4. Collect implicit feedback - _ = p.FeedbackCollector.RecordImplicit(ImplicitSignal{ + _ = p.FeedbackCollector.RecordImplicit(observability.ImplicitSignal{ Type: "accepted", SessionID: p.Timeline.SessionID, Timestamp: time.Now(), @@ -645,7 +657,7 @@ func formatStatus(name string, active bool) string { } // intentCategory safely extracts the category from a potentially-nil intent. -func intentCategory(intent *Intent) string { +func intentCategory(intent *intelligence.Intent) string { if intent == nil { return "unknown" } diff --git a/internal/engine/integration_test.go b/internal/engine/integration_test.go index 02b651dc..93beed32 100644 --- a/internal/engine/integration_test.go +++ b/internal/engine/integration_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/GrayCodeAI/rho/internal/engine/cost" + "github.com/GrayCodeAI/rho/internal/tool" ) @@ -136,7 +138,7 @@ func TestCompact(t *testing.T) { // TestCostTracking tests cost tracking. func TestCostTracking(t *testing.T) { - c := Cost{Model: "gpt-4o"} + c := cost.Cost{Model: "gpt-4o"} c.Add(1000, 500) if c.PromptTokens != 1000 { diff --git a/internal/engine/intelligence_reexports.go b/internal/engine/intelligence_reexports.go deleted file mode 100644 index 18651b88..00000000 --- a/internal/engine/intelligence_reexports.go +++ /dev/null @@ -1,35 +0,0 @@ -// This file re-exports symbols from the intelligence sub-package so that existing -// callers of engine.Intent, engine.NewIntentClassifier, etc. keep compiling -// during the Stage 2 migration. See docs/plans/engine-refactor-plan.md. -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/intelligence" - -type ( - Intent = intelligence.Intent - IntentRule = intelligence.IntentRule - ClassifiedInput = intelligence.ClassifiedInput - IntentClassifier = intelligence.IntentClassifier - Capability = intelligence.Capability - CapabilityRegistry = intelligence.CapabilityRegistry - LanguageConfig = intelligence.LanguageConfig - LanguageRegistry = intelligence.LanguageRegistry - ToolInfo = intelligence.ToolInfo - ToolSelection = intelligence.ToolSelection - ToolSelector = intelligence.ToolSelector - CommandSuggestion = intelligence.CommandSuggestion - SuggestionRule = intelligence.SuggestionRule - SuggestionEngine = intelligence.SuggestionEngine -) - -var ( - NewIntentClassifier = intelligence.NewIntentClassifier - FormatIntent = intelligence.FormatIntent - NewCapabilityRegistry = intelligence.NewCapabilityRegistry - NewLanguageRegistry = intelligence.NewLanguageRegistry - FormatLanguages = intelligence.FormatLanguages - NewToolSelector = intelligence.NewToolSelector - FormatToolSelection = intelligence.FormatToolSelection - NewSuggestionEngine = intelligence.NewSuggestionEngine - FormatCommandSuggestions = intelligence.FormatCommandSuggestions -) diff --git a/internal/engine/io_reexports.go b/internal/engine/io_reexports.go deleted file mode 100644 index a8831883..00000000 --- a/internal/engine/io_reexports.go +++ /dev/null @@ -1,42 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/io" - -type ( - ClipboardMonitor = io.ClipboardMonitor - AIComment = io.AIComment - AIWatcher = io.AIWatcher - FileEvent = io.FileEvent - WatcherConfig = io.WatcherConfig - FileWatcher = io.FileWatcher - SingleFileWatcher = io.SingleFileWatcher - CronJob = io.CronJob - CronScheduler = io.CronScheduler - CronExpr = io.CronExpr - ClipboardBridge = io.ClipboardBridge -) - -var ( - NewClipboardMonitor = io.NewClipboardMonitor - ReadClipboard = io.ReadClipboard - WriteClipboard = io.WriteClipboard - DetectContentType = io.DetectContentType - DetectLanguage = io.DetectLanguage - NewAIWatcher = io.NewAIWatcher - ScanFile = io.ScanFile - ScanDirectory = io.ScanDirectory - BuildPrompt = io.BuildPrompt - RemoveComment = io.RemoveComment - NewFileWatcher = io.NewFileWatcher - WatchSingle = io.WatchSingle - DefaultIgnorePatterns = io.DefaultIgnorePatterns - MatchesPattern = io.MatchesPattern - DedupEvents = io.DedupEvents - FormatEvents = io.FormatEvents - NewCronScheduler = io.NewCronScheduler - ParseCron = io.ParseCron - IsDue = io.IsDue - NextRunTime = io.NextRunTime - SummarizeClipboard = io.SummarizeClipboard - FormatForContext = io.FormatForContext -) diff --git a/internal/engine/lifecycle_finalize_test.go b/internal/engine/lifecycle_finalize_test.go index 526b619c..a224d9a9 100644 --- a/internal/engine/lifecycle_finalize_test.go +++ b/internal/engine/lifecycle_finalize_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/GrayCodeAI/rho/internal/engine/lifecycle" + "github.com/GrayCodeAI/rho/internal/types" ) @@ -36,7 +38,7 @@ func (m *finalizeMockSkillStore) Retrieve(query string) []string { return nil } // distillation was dead code. func TestFinalizePopulatesToolsAndFiles_TriggersSkillDistill(t *testing.T) { sk := &finalizeMockSkillStore{} - svc := &LifecycleService{lifecycle: &SessionLifecycle{SkillStore: sk}} + svc := &LifecycleService{lifecycle: &lifecycle.SessionLifecycle{SkillStore: sk}} messages := []types.FluxMessage{ {Role: "user", Content: "Refactor the auth module"}, @@ -73,7 +75,7 @@ func TestFinalizePopulatesToolsAndFiles_TriggersSkillDistill(t *testing.T) { // tools/files does not trigger distillation (regression guard). func TestFinalizeSimpleTaskNoSkillDistill(t *testing.T) { sk := &finalizeMockSkillStore{} - svc := &LifecycleService{lifecycle: &SessionLifecycle{SkillStore: sk}} + svc := &LifecycleService{lifecycle: &lifecycle.SessionLifecycle{SkillStore: sk}} messages := []types.FluxMessage{ {Role: "user", Content: "What is 2+2?"}, diff --git a/internal/engine/lifecycle_reexports.go b/internal/engine/lifecycle_reexports.go deleted file mode 100644 index cc75d8b6..00000000 --- a/internal/engine/lifecycle_reexports.go +++ /dev/null @@ -1,31 +0,0 @@ -// This file re-exports symbols from the lifecycle sub-package so that existing -// callers of engine.SessionLifecycle, engine.NewLimitTracker, etc. keep compiling -// during the Stage 2 migration. See docs/plans/engine-refactor-plan.md. -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/lifecycle" - -type ( - SessionLifecycle = lifecycle.SessionLifecycle - EvolvingMemoryInterface = lifecycle.EvolvingMemoryInterface - SkillStoreInterface = lifecycle.SkillStoreInterface - CostTrackerInterface = lifecycle.CostTrackerInterface - CostEntry = lifecycle.CostEntry - SessionOutcome = lifecycle.SessionOutcome - EvolvingMemoryAdapter = lifecycle.EvolvingMemoryAdapter - SkillDistillerAdapter = lifecycle.SkillDistillerAdapter - SafetyLimits = lifecycle.SafetyLimits - LimitTracker = lifecycle.LimitTracker - TimeoutConfig = lifecycle.TimeoutConfig -) - -var ( - NewLimitTracker = lifecycle.NewLimitTracker - DefaultLimits = lifecycle.DefaultLimits - VibeLimits = lifecycle.VibeLimits - ResearchLimits = lifecycle.ResearchLimits - DefaultTimeoutConfig = lifecycle.DefaultTimeoutConfig - WithTimeout = lifecycle.WithTimeout - RemainingTime = lifecycle.RemainingTime - TimeoutMessage = lifecycle.TimeoutMessage -) diff --git a/internal/engine/lifecycle_service.go b/internal/engine/lifecycle_service.go index 4b5691e3..1784b702 100644 --- a/internal/engine/lifecycle_service.go +++ b/internal/engine/lifecycle_service.go @@ -5,6 +5,15 @@ import ( "sync" "time" + "github.com/GrayCodeAI/rho/internal/engine/cost" + "github.com/GrayCodeAI/rho/internal/engine/scaffold" + + "github.com/GrayCodeAI/rho/internal/engine/control" + "github.com/GrayCodeAI/rho/internal/engine/lifecycle" + "github.com/GrayCodeAI/rho/internal/engine/review" + "github.com/GrayCodeAI/rho/internal/engine/streaming" + "github.com/GrayCodeAI/rho/internal/engine/validation" + "github.com/GrayCodeAI/rho/internal/engine/branching" "github.com/GrayCodeAI/rho/internal/engine/token" "github.com/GrayCodeAI/rho/internal/intelligence/memory" @@ -34,40 +43,40 @@ type LifecycleService struct { // smart turn routing (simple/strong per turn). smartRouting *smartrouting.Config // limit tracking. - limits *LimitTracker + limits *lifecycle.LimitTracker // doom-loop / snowball / loop detection. - loopDet *LoopDetector + loopDet *control.LoopDetector snowball *branching.SnowballDetector // beliefs. beliefs *BeliefState // decision recording. - backtrack *BacktrackEngine + backtrack *control.BacktrackEngine // post-write critics. - critic *Critic + critic *review.Critic // pre-edit shadow validation. shadow *branching.ShadowWorkspace // verbal self-reflection on tool failure. reflector *Reflector // few-shot + adaptive prompt. - fewShotStore *FewShotStore + fewShotStore *scaffold.FewShotStore adaptivePrompt *AdaptivePrompt // activity tracker. activity *memory.ActivityTracker // agents accumulator. agentsAccum *prompts.AgentsAccumulator // response cache (used in agentLoop for cache hits). - responseCache *ResponseCache + responseCache *streaming.ResponseCache // integration pipeline (pre-query / post-response / end-session). pipeline *IntegrationPipeline // steering queue. - steering *SteeringQueue + steering *streaming.SteeringQueue // local quality loops run after write tools and belong to lifecycle // feedback rather than transport or persistence. - lintLoop *LintLoop - testLoop *TestLoop + lintLoop *validation.LintLoop + testLoop *validation.TestLoop // session-level lifecycle hook. - lifecycle *SessionLifecycle - costTracker *CostTracker + lifecycle *lifecycle.SessionLifecycle + costTracker *cost.CostTracker teach TeachConfig trajectory *TrajectoryDistiller // smartSkills caches loaded SmartSkills for auto-discovery per-turn. @@ -86,13 +95,13 @@ func NewLifecycleService(log *logger.Logger) *LifecycleService { log = logger.Default() } return &LifecycleService{ - limits: NewLimitTracker(DefaultLimits()), - loopDet: NewLoopDetector(10, DoomLoopThreshold), + limits: lifecycle.NewLimitTracker(lifecycle.DefaultLimits()), + loopDet: control.NewLoopDetector(10, control.DoomLoopThreshold), snowball: branching.NewSnowballDetector(500000), beliefs: NewBeliefState(), - backtrack: NewBacktrackEngine(), + backtrack: control.NewBacktrackEngine(), lifecycle: nil, // constructed in New() with cwd - responseCache: NewResponseCache(1000, 24*time.Hour), + responseCache: streaming.NewResponseCache(1000, 24*time.Hour), pipeline: NewIntegrationPipeline(), log: log, fewShotStore: nil, // lazy @@ -119,7 +128,7 @@ func (s *LifecycleService) OnSessionStart(ctx context.Context, s2 *Session, last // adaptive-prompt learning feedback. func (s *LifecycleService) OnSessionEnd(ctx context.Context, s2 *Session, success bool, duration time.Duration) { if s.lifecycle != nil { - outcome := SessionOutcome{Success: success, Duration: duration} + outcome := lifecycle.SessionOutcome{Success: success, Duration: duration} messages := s2.Persistence().RawMessages() if len(messages) > 0 { for _, m := range messages { @@ -155,7 +164,7 @@ func (s *LifecycleService) Finalize(ctx context.Context, messages []types.FluxMe if s == nil { return } - outcome := SessionOutcome{Success: success, Duration: duration, TotalCost: totalCost} + outcome := lifecycle.SessionOutcome{Success: success, Duration: duration, TotalCost: totalCost} for _, message := range messages { if message.Role == "user" && len(message.ToolResults) == 0 && outcome.TaskGoal == "" { outcome.TaskGoal = message.Content @@ -252,28 +261,28 @@ func (s *LifecycleService) SnapshotTurnProgress(tokens int, progress float64) { func (s *LifecycleService) SetCascade(c *branching.CascadeRouter) { s.cascade = c } func (s *LifecycleService) SetSmartRouting(c *smartrouting.Config) { s.smartRouting = c } -func (s *LifecycleService) SetLifecycle(l *SessionLifecycle) { s.lifecycle = l } +func (s *LifecycleService) SetLifecycle(l *lifecycle.SessionLifecycle) { s.lifecycle = l } func (s *LifecycleService) SetReflector(r *Reflector) { s.reflector = r } -func (s *LifecycleService) SetCritic(c *Critic) { s.critic = c } +func (s *LifecycleService) SetCritic(c *review.Critic) { s.critic = c } func (s *LifecycleService) SetShadow(sh *branching.ShadowWorkspace) { s.shadow = sh } -func (s *LifecycleService) SetFewShotStore(f *FewShotStore) { s.fewShotStore = f } +func (s *LifecycleService) SetFewShotStore(f *scaffold.FewShotStore) { s.fewShotStore = f } func (s *LifecycleService) SetAdaptivePrompt(a *AdaptivePrompt) { s.adaptivePrompt = a } func (s *LifecycleService) SetActivity(act *memory.ActivityTracker) { s.activity = act } func (s *LifecycleService) SetAgentsAccum(a *prompts.AgentsAccumulator) { s.agentsAccum = a } -func (s *LifecycleService) SetSteering(st *SteeringQueue) { s.steering = st } -func (s *LifecycleService) SetLintLoop(loop *LintLoop) { s.lintLoop = loop } -func (s *LifecycleService) SetTestLoop(loop *TestLoop) { s.testLoop = loop } +func (s *LifecycleService) SetSteering(st *streaming.SteeringQueue) { s.steering = st } +func (s *LifecycleService) SetLintLoop(loop *validation.LintLoop) { s.lintLoop = loop } +func (s *LifecycleService) SetTestLoop(loop *validation.TestLoop) { s.testLoop = loop } // Accessors used by stream.go and the agent loop. nil-safe. func (s *LifecycleService) Beliefs() *BeliefState { return s.beliefs } -func (s *LifecycleService) Backtrack() *BacktrackEngine { return s.backtrack } -func (s *LifecycleService) Limits() *LimitTracker { return s.limits } -func (s *LifecycleService) Critic() *Critic { return s.critic } +func (s *LifecycleService) Backtrack() *control.BacktrackEngine { return s.backtrack } +func (s *LifecycleService) Limits() *lifecycle.LimitTracker { return s.limits } +func (s *LifecycleService) Critic() *review.Critic { return s.critic } func (s *LifecycleService) Shadow() *branching.ShadowWorkspace { return s.shadow } func (s *LifecycleService) Reflector() *Reflector { return s.reflector } func (s *LifecycleService) Cascade() *branching.CascadeRouter { return s.cascade } func (s *LifecycleService) SmartRouting() *smartrouting.Config { return s.smartRouting } -func (s *LifecycleService) FewShotStore() *FewShotStore { return s.fewShotStore } +func (s *LifecycleService) FewShotStore() *scaffold.FewShotStore { return s.fewShotStore } func (s *LifecycleService) AdaptivePrompt() *AdaptivePrompt { return s.adaptivePrompt } func (s *LifecycleService) Activity() *memory.ActivityTracker { return s.activity } func (s *LifecycleService) AgentsAccum() *prompts.AgentsAccumulator { return s.agentsAccum } @@ -281,16 +290,16 @@ func (s *LifecycleService) AgentsAccum() *prompts.AgentsAccumulator { return s.a // SetAgentsAccumulator attaches the project-learning accumulator. func (s *LifecycleService) SetAgentsAccumulator(a *prompts.AgentsAccumulator) { s.agentsAccum = a } -func (s *LifecycleService) ResponseCache() *ResponseCache { return s.responseCache } -func (s *LifecycleService) Pipeline() *IntegrationPipeline { return s.pipeline } -func (s *LifecycleService) Steering() *SteeringQueue { return s.steering } -func (s *LifecycleService) Lifecycle() *SessionLifecycle { return s.lifecycle } -func (s *LifecycleService) CostTracker() *CostTracker { return s.costTracker } -func (s *LifecycleService) SetCostTracker(c *CostTracker) { s.costTracker = c } -func (s *LifecycleService) Teach() TeachConfig { return s.teach } -func (s *LifecycleService) SetTeach(t TeachConfig) { s.teach = t } -func (s *LifecycleService) Trajectory() *TrajectoryDistiller { return s.trajectory } -func (s *LifecycleService) SetTrajectory(t *TrajectoryDistiller) { s.trajectory = t } +func (s *LifecycleService) ResponseCache() *streaming.ResponseCache { return s.responseCache } +func (s *LifecycleService) Pipeline() *IntegrationPipeline { return s.pipeline } +func (s *LifecycleService) Steering() *streaming.SteeringQueue { return s.steering } +func (s *LifecycleService) Lifecycle() *lifecycle.SessionLifecycle { return s.lifecycle } +func (s *LifecycleService) CostTracker() *cost.CostTracker { return s.costTracker } +func (s *LifecycleService) SetCostTracker(c *cost.CostTracker) { s.costTracker = c } +func (s *LifecycleService) Teach() TeachConfig { return s.teach } +func (s *LifecycleService) SetTeach(t TeachConfig) { s.teach = t } +func (s *LifecycleService) Trajectory() *TrajectoryDistiller { return s.trajectory } +func (s *LifecycleService) SetTrajectory(t *TrajectoryDistiller) { s.trajectory = t } // LoadSmartSkills loads the session's auto-discovery skills once. func (s *LifecycleService) LoadSmartSkills() { @@ -359,9 +368,9 @@ func (s *LifecycleService) ToggleVerbose() bool { s.verbose = !s.verbose return s.verbose } -func (s *LifecycleService) Verbose() bool { return s != nil && s.verbose } -func (s *LifecycleService) LintLoop() *LintLoop { return s.lintLoop } -func (s *LifecycleService) TestLoop() *TestLoop { return s.testLoop } +func (s *LifecycleService) Verbose() bool { return s != nil && s.verbose } +func (s *LifecycleService) LintLoop() *validation.LintLoop { return s.lintLoop } +func (s *LifecycleService) TestLoop() *validation.TestLoop { return s.testLoop } // containsStringVec reports whether s is present in the slice. func containsStringVec(slice []string, s string) bool { diff --git a/internal/engine/memory_reexports.go b/internal/engine/memory_reexports.go deleted file mode 100644 index a5494117..00000000 --- a/internal/engine/memory_reexports.go +++ /dev/null @@ -1,22 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/memory" - -type ( - KnowledgeEntry = memory.KnowledgeEntry - KnowledgeStats = memory.KnowledgeStats - KnowledgeBase = memory.KnowledgeBase - Experience = memory.Experience - ExperienceStats = memory.ExperienceStats - ExperienceStore = memory.ExperienceStore - RawMemory = memory.RawMemory - ConsolidatedMemory = memory.ConsolidatedMemory - ConsolidatorStats = memory.ConsolidatorStats - MemoryConsolidator = memory.MemoryConsolidator -) - -var ( - NewKnowledgeBase = memory.NewKnowledgeBase - NewExperienceStore = memory.NewExperienceStore - NewMemoryConsolidator = memory.NewMemoryConsolidator -) diff --git a/internal/engine/observability_reexports.go b/internal/engine/observability_reexports.go deleted file mode 100644 index da3b6e66..00000000 --- a/internal/engine/observability_reexports.go +++ /dev/null @@ -1,34 +0,0 @@ -// This file re-exports symbols from the observability sub-package so that existing -// callers of engine.Profiler, engine.NewProfiler, etc. keep compiling -// during the Stage 2 migration. See docs/plans/engine-refactor-plan.md. -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/observability" - -type ( - Profiler = observability.Profiler - ProfileSpan = observability.ProfileSpan - Counter = observability.Counter - Timer = observability.Timer - DebugSession = observability.DebugSession - DebugStep = observability.DebugStep - Hypothesis = observability.Hypothesis - DebugRecorder = observability.DebugRecorder - LogLevel = observability.LogLevel - LogEntry = observability.LogEntry - StructuredLogger = observability.StructuredLogger - AgentLogger = observability.AgentLogger - RotatingWriter = observability.RotatingWriter - Feedback = observability.Feedback - FeedbackCollector = observability.FeedbackCollector - ImplicitSignal = observability.ImplicitSignal -) - -var ( - NewProfiler = observability.NewProfiler - NewDebugRecorder = observability.NewDebugRecorder - NewStructuredLogger = observability.NewStructuredLogger - ParseLevel = observability.ParseLevel - NewRotatingWriter = observability.NewRotatingWriter - NewFeedbackCollector = observability.NewFeedbackCollector -) diff --git a/internal/engine/persistence_service.go b/internal/engine/persistence_service.go index 1f49a339..2e6c75f8 100644 --- a/internal/engine/persistence_service.go +++ b/internal/engine/persistence_service.go @@ -4,6 +4,9 @@ import ( "strings" "sync" + "github.com/GrayCodeAI/rho/internal/engine/compact" + "github.com/GrayCodeAI/rho/internal/engine/streaming" + "github.com/GrayCodeAI/rho/internal/eventlog" "github.com/GrayCodeAI/rho/internal/observability/logger" "github.com/GrayCodeAI/rho/internal/session" @@ -46,13 +49,13 @@ type PersistenceService struct { // at explicit Save calls). writeBehind *session.WriteBehind // steering is the per-iteration user-guidance queue. - steering *SteeringQueue + steering *streaming.SteeringQueue // logger. log *logger.Logger // Compaction and checkpoint state belongs to persistence. Session keeps // deprecated aliases only for source compatibility with older callers. autoCompactor *AutoCompactor - files *FileTracker + files *compact.FileTracker persistID string lastPromptTokens int lastCompletionTokens int @@ -154,10 +157,10 @@ func (s *PersistenceService) SetGraph(graph *session.ConversationGraph) { s.grap // Steering returns the per-iteration user-guidance queue. New // code should access this through s.Persistence().Steering(). -func (s *PersistenceService) Steering() *SteeringQueue { return s.steering } +func (s *PersistenceService) Steering() *streaming.SteeringQueue { return s.steering } // SetSteering attaches the user-guidance queue. -func (s *PersistenceService) SetSteering(sq *SteeringQueue) { s.steering = sq } +func (s *PersistenceService) SetSteering(sq *streaming.SteeringQueue) { s.steering = sq } // AddAssistant appends an assistant message. func (s *PersistenceService) AddAssistant(content string) { @@ -538,13 +541,13 @@ func (s *PersistenceService) SetAutoCompactor(ac *AutoCompactor) { s.stateMu.Unlock() } -func (s *PersistenceService) Files() *FileTracker { +func (s *PersistenceService) Files() *compact.FileTracker { s.stateMu.RLock() defer s.stateMu.RUnlock() return s.files } -func (s *PersistenceService) SetFiles(files *FileTracker) { +func (s *PersistenceService) SetFiles(files *compact.FileTracker) { s.stateMu.Lock() s.files = files s.stateMu.Unlock() diff --git a/internal/engine/project_reexports.go b/internal/engine/project_reexports.go deleted file mode 100644 index c853db2f..00000000 --- a/internal/engine/project_reexports.go +++ /dev/null @@ -1,59 +0,0 @@ -// This file re-exports symbols from the project sub-package so that existing -// callers of engine.ProjectAnalysis, engine.NewProjectAnalyzer, etc. keep compiling -// during the Stage 2 migration. See docs/plans/engine-refactor-plan.md. -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/project" - -type ( - ProjectAnalysis = project.ProjectAnalysis - ModuleInfo = project.ModuleInfo - Pattern = project.Pattern - ProjectAnalyzer = project.ProjectAnalyzer - ProjectContext = project.ProjectContext - ProjectSnapshot = project.ProjectSnapshot - ProjectSnapshotCache = project.ProjectSnapshotCache - ImpactAnalysis = project.ImpactAnalysis - ImpactAnalyzer = project.ImpactAnalyzer - DependencyUpdater = project.DependencyUpdater - Dependency = project.Dependency - UpdatePlan = project.UpdatePlan - MigrationPlan = project.MigrationPlan - MigrationStep = project.MigrationStep - MigrationResult = project.MigrationResult - MigrationPlanner = project.MigrationPlanner - ReleaseManager = project.ReleaseManager - Release = project.Release - ChangeEntry = project.ChangeEntry - ReleaseStats = project.ReleaseStats - Convention = project.Convention - ConventionSet = project.ConventionSet - Violation = project.Violation -) - -var ( - NewProjectAnalyzer = project.NewProjectAnalyzer - DetectArchitecture = project.DetectArchitecture - DetectPatterns = project.DetectPatterns - GenerateOnboardingDoc = project.GenerateOnboardingDoc - FormatAnalysis = project.FormatAnalysis - NewProjectContext = project.NewProjectContext - NewProjectSnapshotCache = project.NewProjectSnapshotCache - NewImpactAnalyzer = project.NewImpactAnalyzer - BuildImportGraph = project.BuildImportGraph - FormatImpact = project.FormatImpact - NewDependencyUpdater = project.NewDependencyUpdater - ClassifyUpdate = project.ClassifyUpdate - ParseSemver = project.ParseSemver - FormatOutdated = project.FormatOutdated - FormatPlan = project.FormatPlan - NewMigrationPlanner = project.NewMigrationPlanner - NewReleaseManager = project.NewReleaseManager - ParseConventionalCommit = project.ParseConventionalCommit - BumpVersion = project.BumpVersion - GenerateChangelog = project.GenerateChangelog - FormatReleaseNotes = project.FormatReleaseNotes - UpdateVersionFile = project.UpdateVersionFile - NewConventionSet = project.NewConventionSet - FormatViolations = project.FormatViolations -) diff --git a/internal/engine/review_reexports.go b/internal/engine/review_reexports.go deleted file mode 100644 index fc7e47e8..00000000 --- a/internal/engine/review_reexports.go +++ /dev/null @@ -1,62 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/review" - -// Types from review sub-package. - -type ( - ReviewBot = review.ReviewBot - ReviewRule = review.ReviewRule - ReviewComment = review.ReviewComment - ReviewReport = review.ReviewReport - PatchVerdict = review.PatchVerdict - Critic = review.Critic - Assessment = review.Assessment - SelfAssessor = review.SelfAssessor - TaskContext = review.TaskContext - ConsensusSampler = review.ConsensusSampler - Sample = review.Sample - ConsensusResult = review.ConsensusResult - QualityScorer = review.QualityScorer - ScoreWeights = review.ScoreWeights - ScoredResponse = review.ScoredResponse - ResponseContext = review.ResponseContext - SolutionReviewer = review.SolutionReviewer - Solution = review.Solution - ReviewResult = review.ReviewResult -) - -// Short-name aliases. - -type ( - Bot = review.ReviewBot - Rule = review.ReviewRule - Comment = review.ReviewComment - Report = review.ReviewReport -) - -// Functions. - -var ( - NewReviewBot = review.NewReviewBot - NewCritic = review.NewCritic - NewSelfAssessor = review.NewSelfAssessor - NewConsensusSampler = review.NewConsensusSampler - NewQualityScorer = review.NewQualityScorer - NewSolutionReviewer = review.NewSolutionReviewer - DefaultWeights = review.DefaultWeights - FormatSelfAssessment = review.FormatSelfAssessment - FormatConsensus = review.FormatConsensus - CompareApproaches = review.CompareApproaches - FormatReview = review.FormatReview - ShouldRetry = review.ShouldRetry - FormatReport = review.FormatReport - FormatInline = review.FormatInline - FilterBySeverity = review.FilterBySeverity -) - -// NewBot delegates to review.NewBot. -func NewBot() *Bot { return review.NewBot() } - -// ReviewBeforeWrite, LLMClient, SelfReviewResult, ConfidenceThreshold -// are defined directly in package engine (self_review.go, client_interface.go). diff --git a/internal/engine/scaffold_reexports.go b/internal/engine/scaffold_reexports.go deleted file mode 100644 index b55d352d..00000000 --- a/internal/engine/scaffold_reexports.go +++ /dev/null @@ -1,28 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/scaffold" - -type ( - Template = scaffold.Template - TemplateFile = scaffold.TemplateFile - TemplateVariable = scaffold.TemplateVariable - Scaffolder = scaffold.Scaffolder - Recipe = scaffold.Recipe - RecipeRegistry = scaffold.RecipeRegistry - PromptPattern = scaffold.PromptPattern - PatternLibrary = scaffold.PatternLibrary - Skill = scaffold.Skill - SkillStep = scaffold.SkillStep - SkillResult = scaffold.SkillResult - SkillRegistry = scaffold.SkillRegistry - FewShotStore = scaffold.FewShotStore - FewShotExample = scaffold.FewShotExample -) - -func NewScaffolder() *Scaffolder { return scaffold.NewScaffolder() } -func NewRecipeRegistry(dir string) *RecipeRegistry { return scaffold.NewRecipeRegistry(dir) } -func NewPatternLibrary(dir string) *PatternLibrary { return scaffold.NewPatternLibrary(dir) } -func NewSkillRegistry(dir string) *SkillRegistry { return scaffold.NewSkillRegistry(dir) } -func NewFewShotStore() *FewShotStore { return scaffold.NewFewShotStore() } -func FormatPattern(p *PromptPattern) string { return scaffold.FormatPattern(p) } -func FormatSkill(s *Skill) string { return scaffold.FormatSkill(s) } diff --git a/internal/engine/search_reexports.go b/internal/engine/search_reexports.go deleted file mode 100644 index ed2a87fa..00000000 --- a/internal/engine/search_reexports.go +++ /dev/null @@ -1,28 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/search" - -type ( - URLScraper = search.URLScraper - ScrapeResult = search.ScrapeResult - Issue = search.Issue - SimilarIssue = search.SimilarIssue - IssueIndex = search.IssueIndex - ResearchAgent = search.ResearchAgent - ResearchQuery = search.ResearchQuery - ResearchResult = search.ResearchResult - ResearchFinding = search.ResearchFinding -) - -var ( - NewURLScraper = search.NewURLScraper - NewIssueIndex = search.NewIssueIndex - NewResearchAgent = search.NewResearchAgent - ExtractHTML = search.ExtractHTML - ExtractJSON = search.ExtractJSON - ExtractMarkdown = search.ExtractMarkdown - ExtractCode = search.ExtractCode - SuggestResolution = search.SuggestResolution - FormatIssueResults = search.FormatIssueResults - BuildSearchContext = search.BuildSearchContext -) diff --git a/internal/engine/session.go b/internal/engine/session.go index 2ad62654..6c38b242 100644 --- a/internal/engine/session.go +++ b/internal/engine/session.go @@ -10,6 +10,11 @@ import ( "sync" "time" + "github.com/GrayCodeAI/rho/internal/engine/cost" + + "github.com/GrayCodeAI/rho/internal/engine/streaming" + "github.com/GrayCodeAI/rho/internal/engine/validation" + agentcontracts "github.com/GrayCodeAI/rho/internal/contracts/agent" "github.com/GrayCodeAI/rho/internal/conversationarc" "github.com/GrayCodeAI/rho/internal/engine/planning" @@ -64,7 +69,7 @@ type SnapshotTracker interface { // and lifecycle state are owned by the corresponding services below. type Session struct { mu sync.RWMutex - Cost Cost + Cost cost.Cost // llm is the LLM transport service (Phase 1 extraction). All new // code should go through s.llm.* rather than duplicating transport state. @@ -247,8 +252,8 @@ func NewSessionWithClient(chat ChatClient, provider, model, systemPrompt string, }) s.refreshContextWindowCache() s.life.SetAgentsAccumulator(agentsAccum) - s.life.SetLintLoop(NewLintLoop()) - s.life.SetTestLoop(NewTestLoop()) + s.life.SetLintLoop(validation.NewLintLoop()) + s.life.SetTestLoop(validation.NewTestLoop()) s.promptQueue = NewPromptQueue() s.announcements = NewAnnouncementFeed() return s @@ -1037,7 +1042,7 @@ func (s *Session) EnsureSkillCatalogStatement() string { // CostValue returns the session's cost accumulator (a pointer // to a value type, so its methods can be called). New code // should call this instead of reading s.Cost directly. -func (s *Session) CostValue() *Cost { +func (s *Session) CostValue() *cost.Cost { return &s.Cost } @@ -1088,7 +1093,7 @@ func (s *Session) Schedule() *schedule.Manager { content := fmt.Sprintf("[Scheduled Reminder: %s]\n%s", item.ID, item.Prompt) if p := s.Persistence(); p != nil { if sq := p.Steering(); sq != nil { - sq.Enqueue(SteeringMessage{ + sq.Enqueue(streaming.SteeringMessage{ Content: content, Priority: 1, }) diff --git a/internal/engine/session_reexports.go b/internal/engine/session_reexports.go deleted file mode 100644 index 2558fe30..00000000 --- a/internal/engine/session_reexports.go +++ /dev/null @@ -1,27 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/compression" - -type ( - Timeline = compression.Timeline - TimelineEvent = compression.TimelineEvent - SessionCompressor = compression.SessionCompressor - CompressStrategy = compression.CompressStrategy - CompressMessage = compression.CompressMessage - CompressedBlock = compression.CompressedBlock - CompressionResult = compression.CompressionResult - CrossSessionLearner = compression.CrossSessionLearner - Insight = compression.Insight - FailurePattern = compression.FailurePattern - SessionConvention = compression.SessionConvention - LearnerStats = compression.LearnerStats -) - -func NewTimeline(sessionID string) *Timeline { return compression.NewTimeline(sessionID) } -func NewSessionCompressor(strategy CompressStrategy) *SessionCompressor { - return compression.NewSessionCompressor(strategy) -} - -func NewCrossSessionLearner(dir string) *CrossSessionLearner { - return compression.NewCrossSessionLearner(dir) -} diff --git a/internal/engine/stream.go b/internal/engine/stream.go index 0b406b6f..8a3d865f 100644 --- a/internal/engine/stream.go +++ b/internal/engine/stream.go @@ -8,6 +8,11 @@ import ( "strings" "time" + "github.com/GrayCodeAI/rho/internal/engine/cost" + "github.com/GrayCodeAI/rho/internal/engine/token" + + "github.com/GrayCodeAI/rho/internal/engine/control" + "github.com/GrayCodeAI/rho/internal/provider/gateway" "github.com/GrayCodeAI/rho/internal/smartrouting" "github.com/GrayCodeAI/rho/internal/types" @@ -228,8 +233,8 @@ func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) { toolTurns := 0 // turns that used tools (for skill distillation) var toolsUsedSet map[string]bool var filesModifiedSet map[string]bool - snowball := branching.NewSnowballDetector(500000) // 500K token ceiling - loopDet := NewLoopDetector(10, DoomLoopThreshold) // 10-step window, 3 repeats = doom loop + snowball := branching.NewSnowballDetector(500000) // 500K token ceiling + loopDet := control.NewLoopDetector(10, control.DoomLoopThreshold) // 10-step window, 3 repeats = doom loop for { // Close the boundary of the previous turn before evaluating whether to @@ -259,12 +264,12 @@ func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) { s.LifecycleSvc().Beliefs().Prune(turnCount) } // Context governor: collapse → micro/smart/truncate (settings threshold %). - tokensBefore := EstimateTokens(s.Persistence().RawMessages()) + tokensBefore := token.EstimateTokens(s.Persistence().RawMessages()) if s.WillCompactBeforeTurn() { emit(StreamEvent{Type: "compact_start"}) } if compactStrategy, didCompact := s.ManageContextBeforeTurn(ctx); didCompact { - tokensAfter := EstimateTokens(s.Persistence().RawMessages()) + tokensAfter := token.EstimateTokens(s.Persistence().RawMessages()) s.Logger().Info("context compacted", map[string]interface{}{ "strategy": compactStrategy, "messages": len(s.Persistence().RawMessages()), @@ -337,7 +342,7 @@ func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) { // Dynamic max_tokens based on task type and recent tool patterns taskType := classifyPromptForBudget(s.Persistence().RawMessages()) contextSize := s.ContextWindowSize() - maxTok := DynamicMaxTokens(s.Persistence().RawMessages(), contextSize, taskType) + maxTok := token.DynamicMaxTokens(s.Persistence().RawMessages(), contextSize, taskType) // Model cascade: select optimal model for this request activeModel := strings.TrimSpace(s.ChatLLM().Model()) @@ -426,16 +431,16 @@ func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) { // Count actual input tokens for precise budget tracking inputTokens := 0 for _, msg := range s.Persistence().RawMessages() { - inputTokens += CountTokensFast(msg.Content) + inputTokens += token.CountTokensFast(msg.Content) for _, tr := range msg.ToolResults { - inputTokens += CountTokensFast(tr.Content) + inputTokens += token.CountTokensFast(tr.Content) } } - inputTokens += CountTokensFast(s.Persistence().System()) + inputTokens += token.CountTokensFast(s.Persistence().System()) s.Logger().Info("token count", map[string]interface{}{"input_tokens": inputTokens, "model": s.ChatLLM().Model()}) // Cost warning for expensive calls - inPrice, outPrice := ModelPricing(s.ChatLLM().Model()) + inPrice, outPrice := cost.ModelPricing(s.ChatLLM().Model()) estCost := float64(inputTokens)*inPrice/1_000_000 + float64(maxTok)*outPrice/1_000_000 if estCost > 0.50 { emit(StreamEvent{Type: "blast_radius", Content: fmt.Sprintf("%s This request will use ~%d tokens (~$%.2f). Continue? The agent will proceed automatically.", icons.Alert(), inputTokens+maxTok, estCost)}) diff --git a/internal/engine/stream_guards.go b/internal/engine/stream_guards.go index c71f2df6..4ee218d9 100644 --- a/internal/engine/stream_guards.go +++ b/internal/engine/stream_guards.go @@ -6,6 +6,8 @@ import ( "fmt" "os" + "github.com/GrayCodeAI/rho/internal/engine/control" + "github.com/GrayCodeAI/rho/internal/engine/branching" ) @@ -17,7 +19,7 @@ const gracefulExhaustionEnv = "RHO_GRACEFUL_EXHAUSTION" // checkGuardConditions runs all pre-turn guard checks. // Returns false when the loop should stop (abort conditions met). // On first-stage loop detection, injects a break-loop message and continues. -func (s *Session) checkGuardConditions(ctx context.Context, ch chan<- StreamEvent, turnCount int, snowball *branching.SnowballDetector, loopDet *LoopDetector) bool { +func (s *Session) checkGuardConditions(ctx context.Context, ch chan<- StreamEvent, turnCount int, snowball *branching.SnowballDetector, loopDet *control.LoopDetector) bool { if ctx.Err() != nil { msg := "Request cancelled." if errors.Is(ctx.Err(), context.DeadlineExceeded) { diff --git a/internal/engine/stream_usage.go b/internal/engine/stream_usage.go index 19bf24bf..a66f300e 100644 --- a/internal/engine/stream_usage.go +++ b/internal/engine/stream_usage.go @@ -6,6 +6,8 @@ import ( "strings" "time" + "github.com/GrayCodeAI/rho/internal/engine/token" + "github.com/GrayCodeAI/rho/internal/types" "github.com/GrayCodeAI/rho/internal/ui/icons" "github.com/GrayCodeAI/rho/internal/usage" @@ -131,12 +133,12 @@ func (s *Session) recordStreamUsage(ch chan<- StreamEvent, prompt, completion in } func estimateStreamCompletionTokens(text string, toolCalls []types.ToolCall) int { - n := CountTokensFast(text) + n := token.CountTokensFast(text) for _, tc := range toolCalls { if b, err := json.Marshal(tc.Arguments); err == nil { - n += CountTokensFast(string(b)) + n += token.CountTokensFast(string(b)) } - n += CountTokensFast(tc.Name) + n += token.CountTokensFast(tc.Name) } return n } diff --git a/internal/engine/stream_usage_test.go b/internal/engine/stream_usage_test.go index 72602ef9..4830cda4 100644 --- a/internal/engine/stream_usage_test.go +++ b/internal/engine/stream_usage_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/GrayCodeAI/rho/internal/engine/cost" + "github.com/GrayCodeAI/rho/internal/types" ) @@ -53,7 +55,7 @@ func TestRecordStreamUsageAttributesResolvedRoute(t *testing.T) { resolvedProvider = "openai" resolvedModel = "openai/fallback-test-model" ) - RegisterLivePricing(resolvedModel, 2, 4) + cost.RegisterLivePricing(resolvedModel, 2, 4) sess := NewSessionWithClient(NewMockClientForTest(), "anthropic", "anthropic/requested-test-model", "", nil, true) ch := make(chan StreamEvent, 1) diff --git a/internal/engine/streaming_reexports.go b/internal/engine/streaming_reexports.go deleted file mode 100644 index 10b56051..00000000 --- a/internal/engine/streaming_reexports.go +++ /dev/null @@ -1,37 +0,0 @@ -// This file re-exports symbols from the streaming sub-package so that existing -// callers of engine.ResponseCache, engine.NewResponseCache, etc. keep compiling -// during the Stage 2 migration. See docs/plans/engine-refactor-plan.md. -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/streaming" - -type ( - CacheEntry = streaming.CacheEntry - CacheStats = streaming.CacheStats - ResponseCache = streaming.ResponseCache - FormatRule = streaming.FormatRule - FormattedResponse = streaming.FormattedResponse - ResponseFormatter = streaming.ResponseFormatter - StreamOptimizer = streaming.StreamOptimizer - StreamStats = streaming.StreamStats - ThinkingPhase = streaming.ThinkingPhase - ThinkingStep = streaming.ThinkingStep - ThinkingProtocol = streaming.ThinkingProtocol - SteeringQueue = streaming.SteeringQueue - SteeringMessage = streaming.SteeringMessage -) - -const DefaultMaxEntries = streaming.DefaultMaxEntries - -var ( - NewResponseCache = streaming.NewResponseCache - NewResponseFormatter = streaming.NewResponseFormatter - NewStreamOptimizer = streaming.NewStreamOptimizer - NewThinkingProtocol = streaming.NewThinkingProtocol - NewSteeringQueue = streaming.NewSteeringQueue - HashPrompt = streaming.HashPrompt - ShouldCache = streaming.ShouldCache - FixCodeFences = streaming.FixCodeFences - RemoveFluff = streaming.RemoveFluff - FixMarkdown = streaming.FixMarkdown -) diff --git a/internal/engine/token_reexports.go b/internal/engine/token_reexports.go deleted file mode 100644 index c970cde6..00000000 --- a/internal/engine/token_reexports.go +++ /dev/null @@ -1,37 +0,0 @@ -package engine - -import ( - "github.com/GrayCodeAI/rho/internal/engine/token" - "github.com/GrayCodeAI/rho/internal/types" -) - -type ( - TokenPredictor = token.TokenPredictor - PredictionRecord = token.PredictionRecord - Prediction = token.Prediction - TokenEntry = token.TokenEntry - BudgetAlert = token.BudgetAlert - TokenReporter = token.TokenReporter -) - -func NewTokenPredictor() *TokenPredictor { return token.NewTokenPredictor() } -func NewTokenReporter(sessionBudget int) *TokenReporter { return token.NewTokenReporter(sessionBudget) } - -func DynamicMaxTokens(messages []types.FluxMessage, contextSize int, taskType string) int { - return token.DynamicMaxTokens(messages, contextSize, taskType) -} -func ClassifyTaskComplexity(task string) string { return token.ClassifyTaskComplexity(task) } -func FormatPrediction(pred *Prediction, model string) string { - return token.FormatPrediction(pred, model) -} - -func WarnIfExpensive(pred *Prediction, budgetUSD float64) string { - return token.WarnIfExpensive(pred, budgetUSD) -} -func CountTokens(text string) int { return token.CountTokens(text) } -func CountTokensFast(text string) int { return token.CountTokensFast(text) } -func CompressForContext(text string, budget int) (string, int) { - return token.CompressForContext(text, budget) -} -func EstimateTokens(msgs []types.FluxMessage) int { return token.EstimateTokens(msgs) } -func EstimateMessageTokens(m types.FluxMessage) int { return token.EstimateMessageTokens(m) } diff --git a/internal/engine/tool_service.go b/internal/engine/tool_service.go index 158e048b..953282d3 100644 --- a/internal/engine/tool_service.go +++ b/internal/engine/tool_service.go @@ -10,6 +10,8 @@ import ( "strings" "sync" + "github.com/GrayCodeAI/rho/internal/engine/token" + "github.com/GrayCodeAI/rho/internal/engine/diff" "github.com/GrayCodeAI/rho/internal/engine/planning" "github.com/GrayCodeAI/rho/internal/hooks" @@ -595,8 +597,8 @@ func (s *ToolService) NormalizeOutput(output, canonicalTool, toolID string, cont } compressBudget := maxChars / 2 if len(output) > compressBudget { - compressed, tokens := CompressForContext(output, compressBudget/4) - if tokens > 0 && tokens < CountTokensFast(output) { + compressed, tokens := token.CompressForContext(output, compressBudget/4) + if tokens > 0 && tokens < token.CountTokensFast(output) { output = compressed } } diff --git a/internal/engine/validation_reexports.go b/internal/engine/validation_reexports.go deleted file mode 100644 index 2abc0069..00000000 --- a/internal/engine/validation_reexports.go +++ /dev/null @@ -1,24 +0,0 @@ -package engine - -import "github.com/GrayCodeAI/rho/internal/engine/validation" - -type ( - GenValidator = validation.GenValidator - GenCheck = validation.GenCheck - GenIssue = validation.GenIssue - GenValidation = validation.GenValidation - SchemaValidator = validation.SchemaValidator - Schema = validation.Schema - FieldSpec = validation.FieldSpec - SchemaValidationResult = validation.SchemaValidationResult - SchemaValidationError = validation.SchemaValidationError - TestLoop = validation.TestLoop - TestResult = validation.TestResult - LintLoop = validation.LintLoop - LintResult = validation.LintResult -) - -func NewGenValidator() *GenValidator { return validation.NewGenValidator() } -func NewSchemaValidator() *SchemaValidator { return validation.NewSchemaValidator() } -func NewTestLoop() *TestLoop { return validation.NewTestLoop() } -func NewLintLoop() *LintLoop { return validation.NewLintLoop() } diff --git a/internal/engine/workflow_reexports.go b/internal/engine/workflow_reexports.go deleted file mode 100644 index 8efd031b..00000000 --- a/internal/engine/workflow_reexports.go +++ /dev/null @@ -1,57 +0,0 @@ -package engine - -import ( - "context" - - "github.com/GrayCodeAI/rho/internal/engine/workflow" -) - -// Types from workflow sub-package. - -type ( - Workflow = workflow.Workflow - WorkflowStep = workflow.WorkflowStep - WorkflowResult = workflow.WorkflowResult - StepResult = workflow.StepResult - WorkflowEngine = workflow.WorkflowEngine - WorkspaceState = workflow.WorkspaceState - FileState = workflow.FileState - WorkspaceDiffReport = workflow.WorkspaceDiffReport - FileDiffReport = workflow.FileDiffReport - DiffReporter = workflow.DiffReporter - TrajectoryEvent = workflow.TrajectoryEvent - TrajectoryInspector = workflow.TrajectoryInspector -) - -// Short-name aliases provided by the workflow sub-package. - -type ( - Step = workflow.WorkflowStep - Result = workflow.WorkflowResult - Engine = workflow.WorkflowEngine -) - -// Functions. - -var ( - NewWorkflowEngine = workflow.NewWorkflowEngine - NewWorkspaceState = workflow.NewWorkspaceState - NewDiffReporter = workflow.NewDiffReporter - NewTrajectoryInspector = workflow.NewTrajectoryInspector - SubstituteVars = workflow.SubstituteVars - EvalCondition = workflow.EvalCondition - ValidateWorkflow = workflow.ValidateWorkflow - BuiltinWorkflows = workflow.BuiltinWorkflows - FormatAsMarkdown = workflow.FormatAsMarkdown - FormatAsTerminal = workflow.FormatAsTerminal - FormatForCommit = workflow.FormatForCommit - CompareReports = workflow.CompareReports -) - -// NewEngine delegates to workflow.NewEngine. -func NewEngine(executeFn func(ctx context.Context, agent, prompt string) (string, error)) *Engine { - return workflow.NewEngine(executeFn) -} - -// TrajectoryRun, TrajectoryDistiller, NewTrajectoryDistiller, SummarizeTrajectory -// are defined directly in package engine (trajectory.go).