refactor(chat): move tool handling to cmd/chat.go to resolve import cycle

Relocate ToolCall struct and HandleToolCall function from internal/agent/tools.go
to cmd/chat.go, renaming to handleToolCall for package-internal use. This eliminates
the import cycle between cmd and agent packages while preserving agent mode functionality.
This commit is contained in:
Gregory Gauthier 2026-03-04 11:59:23 +00:00
parent 69c5d776e2
commit a5fda5bbfd
2 changed files with 61 additions and 105 deletions

View File

@ -12,7 +12,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"gmgauthier.com/grokkit/config" "gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/agent" "gmgauthier.com/grokkit/internal/git"
"gmgauthier.com/grokkit/internal/grok" "gmgauthier.com/grokkit/internal/grok"
) )
@ -20,6 +20,15 @@ type ChatHistory struct {
Messages []map[string]string `json:"messages"` Messages []map[string]string `json:"messages"`
} }
type ToolCall struct {
Tool string `json:"tool"`
File string `json:"file,omitempty"`
Path string `json:"path,omitempty"`
Instruction string `json:"instruction,omitempty"`
Description string `json:"description,omitempty"`
Message string `json:"message,omitempty"`
}
func loadChatHistory() []map[string]string { func loadChatHistory() []map[string]string {
histFile := getChatHistoryFile() histFile := getChatHistoryFile()
data, err := os.ReadFile(histFile) data, err := os.ReadFile(histFile)
@ -31,7 +40,6 @@ func loadChatHistory() []map[string]string {
if err := json.Unmarshal(data, &hist); err != nil { if err := json.Unmarshal(data, &hist); err != nil {
return nil return nil
} }
return hist.Messages return hist.Messages
} }
@ -50,7 +58,6 @@ func getChatHistoryFile() string {
if configFile != "" { if configFile != "" {
return configFile return configFile
} }
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
if home == "" { if home == "" {
home = "." home = "."
@ -80,7 +87,6 @@ func runChat(cmd *cobra.Command, args []string) {
agentMode, _ := cmd.Flags().GetBool("agent") agentMode, _ := cmd.Flags().GetBool("agent")
modelFlag, _ := cmd.Flags().GetString("model") modelFlag, _ := cmd.Flags().GetString("model")
// Model switching logic
var model string var model string
if modelFlag != "" { if modelFlag != "" {
model = modelFlag model = modelFlag
@ -115,7 +121,7 @@ func runChat(cmd *cobra.Command, args []string) {
} else if history[0]["role"] != "system" { } else if history[0]["role"] != "system" {
history = append([]map[string]string{systemPrompt}, history...) history = append([]map[string]string{systemPrompt}, history...)
} else { } else {
history[0] = systemPrompt // refresh system prompt history[0] = systemPrompt
} }
color.Cyan("┌──────────────────────────────────────────────────────────────┐") color.Cyan("┌──────────────────────────────────────────────────────────────┐")
@ -144,18 +150,63 @@ func runChat(cmd *cobra.Command, args []string) {
color.Green("Grok > ") color.Green("Grok > ")
reply := client.Stream(history, model) reply := client.Stream(history, model)
// Agent mode: check for tool calls // === AGENT TOOL CALL HANDLING ===
if agentMode && strings.Contains(reply, "```tool") { if agentMode && strings.Contains(reply, "```tool") {
agent.HandleToolCall(reply, &history, model) handleToolCall(reply, &history, model)
continue continue
} }
history = append(history, map[string]string{"role": "assistant", "content": reply}) history = append(history, map[string]string{"role": "assistant", "content": reply})
_ = saveChatHistory(history) _ = saveChatHistory(history)
history = append(history, map[string]string{"role": "assistant", "content": reply})
_ = saveChatHistory(history)
fmt.Println() fmt.Println()
} }
} }
// handleToolCall is now inside cmd package — no import cycle
func handleToolCall(reply string, history *[]map[string]string, model string) {
start := strings.Index(reply, "```tool")
if start == -1 {
return
}
end := strings.Index(reply[start+7:], "```")
if end == -1 {
return
}
block := strings.TrimSpace(reply[start+7 : start+7+end])
var tc ToolCall
if err := json.Unmarshal([]byte(block), &tc); err != nil {
color.Red("Failed to parse tool call: %v", err)
return
}
color.Yellow("\n[Agent] Grok wants to call tool: %s", tc.Tool)
switch tc.Tool {
case "edit":
if tc.File != "" && tc.Instruction != "" {
RunEditWithInstruction(tc.File, tc.Instruction)
}
case "scaffold":
if tc.Path != "" && tc.Description != "" {
RunScaffoldWithDescription(tc.Path, tc.Description)
}
case "testgen":
if tc.File != "" {
RunTestgenWithFile(tc.File)
}
case "lint":
if tc.File != "" {
RunLintWithFile(tc.File)
}
case "commit":
if tc.Message != "" {
git.Run([]string{"commit", "-m", tc.Message})
}
default:
color.Red("Unknown tool: %s", tc.Tool)
}
*history = append(*history, map[string]string{"role": "assistant", "content": reply})
}

View File

@ -1,95 +0,0 @@
package agent
import (
"encoding/json"
_ "fmt"
"strings"
"github.com/fatih/color"
"gmgauthier.com/grokkit/cmd"
"gmgauthier.com/grokkit/internal/git"
)
// ToolCall represents a tool request from Grok
type ToolCall struct {
Tool string `json:"tool"`
File string `json:"file,omitempty"`
Path string `json:"path,omitempty"`
Instruction string `json:"instruction,omitempty"`
Description string `json:"description,omitempty"`
Message string `json:"message,omitempty"`
}
// handleToolCall parses and executes tool calls from Grok's response
func HandleToolCall(reply string, history *[]map[string]string, model string) {
// Look for ```tool ... ``` blocks
start := strings.Index(reply, "```tool")
if start == -1 {
return
}
end := strings.Index(reply[start:], "```")
if end == -1 {
return
}
block := strings.TrimSpace(reply[start+7 : start+end])
var tc ToolCall
if err := json.Unmarshal([]byte(block), &tc); err != nil {
color.Red("Failed to parse tool call: %v", err)
return
}
color.Yellow("\n[Agent] Grok wants to call tool: %s", tc.Tool)
switch tc.Tool {
case "edit":
if tc.File == "" || tc.Instruction == "" {
color.Red("Invalid edit call — missing file or instruction")
break
}
color.Cyan("Editing %s with instruction: %s", tc.File, tc.Instruction)
// Reuse the existing edit flow (it already does preview + confirm)
cmd.RunEditWithInstruction(tc.File, tc.Instruction)
case "scaffold":
if tc.Path == "" || tc.Description == "" {
color.Red("Invalid scaffold call")
break
}
color.Cyan("Scaffolding %s: %s", tc.Path, tc.Description)
cmd.RunScaffoldWithDescription(tc.Path, tc.Description)
case "testgen":
if tc.File == "" {
color.Red("Invalid testgen call")
break
}
color.Cyan("Generating tests for %s", tc.File)
cmd.RunTestgenWithFile(tc.File)
case "lint":
if tc.File == "" {
color.Red("Invalid lint call")
break
}
color.Cyan("Linting %s", tc.File)
cmd.RunLintWithFile(tc.File)
case "commit":
if tc.Message == "" {
color.Red("Invalid commit call")
break
}
color.Cyan("Committing with message: %s", tc.Message)
git.Run([]string{"commit", "-m", tc.Message})
default:
color.Red("Unknown tool: %s", tc.Tool)
}
// Feed result back to Grok
*history = append(*history, map[string]string{
"role": "assistant",
"content": reply,
})
}