Commit ae939c6
cmd/del/main.go
@@ -23,8 +23,7 @@ Help developers write better, safer, and more efficient code through intelligent
# CORE BEHAVIOR
-1. **Smart Tool Usage**: When users need file operations, code analysis, or system commands, use tools automatically:
- TOOL_USE: tool_name {JSON args}
+1. **Smart Tool Usage**: When users need file operations, code analysis, or system commands, use your available function calling capabilities to invoke the appropriate tools automatically. **IMPORTANT: STOP immediately after making tool calls - do NOT generate fake results or continue the response.**
2. **Expert Coding Advice**: Provide insightful, actionable coding guidance when tools aren't needed
@@ -59,45 +58,38 @@ Help developers write better, safer, and more efficient code through intelligent
- web_search: Search for information {"query": string}
- test_runner: Run project tests {"type": string, "path": string}
+📝 Multi-File Operations:
+- multi_edit: Edit multiple files with find-replace {"operations": [{"file": string, "old": string, "new": string}]}
+- batch_operations: Execute multiple file operations {"operations": [{"type": "read|write|list", "path": string, "content": string}]}
+
# INTERACTION STYLE
✅ DO:
- Use tools when users need file ops, analysis, or commands
-- Give concise, expert coding advice
+- STOP immediately after making tool calls (let Del execute them)
+- Give concise, expert coding advice when tools aren't needed
- Suggest best practices and optimizations
- Be proactive about security and code quality
❌ DON'T:
+- Generate fake tool outputs or results
+- Continue responding after making tool calls
+- Add <|tool▁outputs▁begin|> or similar fake output blocks
- Over-explain tool usage
-- Apologize unnecessarily
-- Add markdown formatting to tool responses
-- Repeat the user's request
+- Apologize unnecessarily
# EXAMPLES
-User: "show me the main function"
-TOOL_USE: read_file {"path": "main.go"}
-
-User: "what's the git status?"
-TOOL_USE: run_command {"command": "git status"}
-
-User: "how do I optimize this algorithm?"
-[Provide expert algorithmic advice without tools]
-
-User: "scan this project for security issues"
-TOOL_USE: security_scan {"path": "."}
+When users make requests that require tools, automatically invoke the appropriate functions:
-User: "check git status"
-TOOL_USE: git_operation {"operation": "status"}
+User: "show me the main function" → read_file with path "main.go"
+User: "what's the git status?" → git_operation with operation "status"
+User: "scan this project for security issues" → security_scan with path "."
+User: "run the tests" → test_runner to auto-detect and run tests
+User: "search for golang best practices" → web_search with relevant query
+User: "how do I optimize this algorithm?" → provide expert advice (no tools needed)
-User: "commit these changes with message fix bug"
-TOOL_USE: git_operation {"operation": "commit", "message": "fix bug"}
-
-User: "search for golang best practices"
-TOOL_USE: web_search {"query": "golang best practices"}
-
-User: "run the tests"
-TOOL_USE: test_runner {"type": "", "path": "."}
+Use your natural function calling format - Del supports all major tool calling standards.
Stay funky, keep coding! 🎵`
@@ -107,10 +99,17 @@ type Tool struct {
Handler func(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error)
}
+type ToolCall struct {
+ Name string
+ Args map[string]interface{}
+}
+
type Del struct {
- aiProvider AIProvider
- tools map[string]*Tool
- chatHistory []api.Message
+ aiProvider AIProvider
+ tools map[string]*Tool
+ chatHistory []api.Message
+ safeMode bool
+ confirmedActions map[string]bool
}
type AIProvider interface {
@@ -791,6 +790,445 @@ func (d *Del) testRunner(ctx context.Context, args map[string]interface{}, ch ch
return result, nil
}
+// multi_edit: Edit multiple files with find-replace operations
+func (d *Del) multiEdit(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+ operations, ok := args["operations"].([]interface{})
+ if !ok {
+ return nil, fmt.Errorf("missing 'operations' array argument")
+ }
+
+ var results []string
+ var output strings.Builder
+
+ output.WriteString("🔧 Multi-file editing:\n\n")
+
+ for i, op := range operations {
+ opMap, ok := op.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("operation %d is not a valid object", i)
+ }
+
+ filePath, _ := opMap["file"].(string)
+ oldText, _ := opMap["old"].(string)
+ newText, _ := opMap["new"].(string)
+
+ if filePath == "" || oldText == "" {
+ return nil, fmt.Errorf("operation %d missing required 'file' or 'old' fields", i)
+ }
+
+ // Read the file
+ data, err := os.ReadFile(filePath)
+ if err != nil {
+ msg := fmt.Sprintf("❌ %s: failed to read (%v)\n", filePath, err)
+ output.WriteString(msg)
+ results = append(results, msg)
+ continue
+ }
+
+ content := string(data)
+
+ // Check if old text exists
+ if !strings.Contains(content, oldText) {
+ msg := fmt.Sprintf("⚠️ %s: text not found\n", filePath)
+ output.WriteString(msg)
+ results = append(results, msg)
+ continue
+ }
+
+ // Perform replacement
+ newContent := strings.ReplaceAll(content, oldText, newText)
+
+ // Write back to file
+ if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
+ msg := fmt.Sprintf("❌ %s: failed to write (%v)\n", filePath, err)
+ output.WriteString(msg)
+ results = append(results, msg)
+ continue
+ }
+
+ msg := fmt.Sprintf("✅ %s: updated successfully\n", filePath)
+ output.WriteString(msg)
+ results = append(results, msg)
+ }
+
+ result := output.String()
+ ch <- result
+ return result, nil
+}
+
+// batch_operations: Execute multiple file operations (read, write, list) in sequence
+func (d *Del) batchOperations(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+ operations, ok := args["operations"].([]interface{})
+ if !ok {
+ return nil, fmt.Errorf("missing 'operations' array argument")
+ }
+
+ var output strings.Builder
+ output.WriteString("📋 Batch operations:\n\n")
+
+ for i, op := range operations {
+ opMap, ok := op.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("operation %d is not a valid object", i)
+ }
+
+ opType, _ := opMap["type"].(string)
+ path, _ := opMap["path"].(string)
+
+ switch opType {
+ case "read":
+ if path == "" {
+ return nil, fmt.Errorf("operation %d: read requires 'path'", i)
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ output.WriteString(fmt.Sprintf("❌ Read %s: %v\n", path, err))
+ } else {
+ output.WriteString(fmt.Sprintf("📖 Read %s (%d bytes):\n%s\n\n", path, len(data), string(data)))
+ }
+
+ case "list":
+ if path == "" {
+ path = "."
+ }
+ entries, err := os.ReadDir(path)
+ if err != nil {
+ output.WriteString(fmt.Sprintf("❌ List %s: %v\n", path, err))
+ } else {
+ output.WriteString(fmt.Sprintf("📁 List %s:\n", path))
+ for _, entry := range entries {
+ prefix := "📄"
+ if entry.IsDir() {
+ prefix = "📂"
+ }
+ output.WriteString(fmt.Sprintf(" %s %s\n", prefix, entry.Name()))
+ }
+ output.WriteString("\n")
+ }
+
+ case "write":
+ content, _ := opMap["content"].(string)
+ if path == "" || content == "" {
+ return nil, fmt.Errorf("operation %d: write requires 'path' and 'content'", i)
+ }
+ if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+ output.WriteString(fmt.Sprintf("❌ Write %s: %v\n", path, err))
+ } else {
+ output.WriteString(fmt.Sprintf("✅ Write %s (%d bytes)\n", path, len(content)))
+ }
+
+ default:
+ return nil, fmt.Errorf("operation %d: unsupported type '%s'", i, opType)
+ }
+ }
+
+ result := output.String()
+ ch <- result
+ return result, nil
+}
+
+// requiresConfirmation checks if an operation requires user confirmation
+func (d *Del) requiresConfirmation(operation string, args map[string]interface{}) bool {
+ if !d.safeMode {
+ return false
+ }
+
+ // Define sensitive operations
+ sensitiveOps := map[string]bool{
+ "write_file": true,
+ "multi_edit": true,
+ "git_push": true,
+ "git_commit": true,
+ "run_command": true,
+ "format_code": true,
+ }
+
+ // Check for sensitive git operations
+ if operation == "git_operation" {
+ if gitOp, ok := args["operation"].(string); ok {
+ return gitOp == "push" || gitOp == "commit" || gitOp == "reset" || gitOp == "clean"
+ }
+ }
+
+ // Check for dangerous shell commands
+ if operation == "run_command" {
+ if cmd, ok := args["command"].(string); ok {
+ dangerousCommands := []string{"rm", "sudo", "mv", "cp", "chmod", "chown", "dd", "mkfs"}
+ cmdLower := strings.ToLower(cmd)
+ for _, dangerous := range dangerousCommands {
+ if strings.Contains(cmdLower, dangerous) {
+ return true
+ }
+ }
+ }
+ }
+
+ return sensitiveOps[operation]
+}
+
+// askConfirmation prompts user for confirmation
+func (d *Del) askConfirmation(operation string, description string) bool {
+ actionKey := fmt.Sprintf("%s:%s", operation, description)
+
+ // Check if already confirmed for this session
+ if confirmed, exists := d.confirmedActions[actionKey]; exists && confirmed {
+ return true
+ }
+
+ fmt.Printf("\n⚠️ CONFIRMATION REQUIRED ⚠️\n")
+ fmt.Printf("Operation: %s\n", operation)
+ fmt.Printf("Description: %s\n", description)
+ fmt.Printf("Continue? (y/N): ")
+
+ var response string
+ fmt.Scanln(&response)
+
+ confirmed := strings.ToLower(strings.TrimSpace(response)) == "y"
+ d.confirmedActions[actionKey] = confirmed
+
+ return confirmed
+}
+
+// parseChineseFormat handles Chinese model format: <|tool▁calls▁begin|>...
+func (d *Del) parseChineseFormat(response string) []ToolCall {
+ var calls []ToolCall
+
+ // Simple pattern to match Chinese model tool calls
+ // This handles: function<|tool▁sep|>name {...}
+ re := regexp.MustCompile(`function<|tool▁sep|>(\w+)`)
+ matches := re.FindAllStringSubmatch(response, -1)
+
+ for _, match := range matches {
+ if len(match) >= 2 {
+ toolName := match[1]
+
+ // Look for JSON args after the function name
+ // Find the position after this match
+ startPos := strings.Index(response, match[0]) + len(match[0])
+ remaining := response[startPos:]
+
+ // Look for JSON object - simple approach
+ var args map[string]interface{}
+ jsonStart := strings.Index(remaining, "{")
+ if jsonStart != -1 {
+ jsonEnd := strings.Index(remaining[jsonStart:], "}")
+ if jsonEnd != -1 {
+ jsonStr := remaining[jsonStart : jsonStart+jsonEnd+1]
+ json.Unmarshal([]byte(jsonStr), &args)
+ }
+ }
+
+ if args == nil {
+ args = make(map[string]interface{})
+ }
+
+ calls = append(calls, ToolCall{Name: toolName, Args: args})
+ }
+ }
+
+ // Fallback: try simpler regex pattern
+ if len(calls) == 0 {
+ re = regexp.MustCompile(`(?s)<|tool▁call▁begin|>.*?function<|tool▁sep|>(\w+)\s*(\{[^}]*\}?)`)
+ matches = re.FindAllStringSubmatch(response, -1)
+
+ for _, match := range matches {
+ if len(match) >= 3 {
+ toolName := match[1]
+ argsJSON := strings.TrimSpace(match[2])
+
+ var args map[string]interface{}
+ if argsJSON == "" || argsJSON == "{}" {
+ args = make(map[string]interface{})
+ } else {
+ json.Unmarshal([]byte(argsJSON), &args)
+ if args == nil {
+ args = make(map[string]interface{})
+ }
+ }
+
+ calls = append(calls, ToolCall{Name: toolName, Args: args})
+ }
+ }
+ }
+
+ return calls
+}
+
+// parseOpenAIXMLFormat handles OpenAI XML format: <tool_calls><invoke>...
+func (d *Del) parseOpenAIXMLFormat(response string) []ToolCall {
+ var calls []ToolCall
+
+ // Pattern to match OpenAI XML tool calls
+ re := regexp.MustCompile(`(?s)<tool_calls>.*?<invoke name="(\w+)">(.*?)</invoke>.*?</tool_calls>`)
+ matches := re.FindAllStringSubmatch(response, -1)
+
+ for _, match := range matches {
+ if len(match) >= 3 {
+ toolName := match[1]
+ paramsXML := match[2]
+
+ // Parse parameters from XML
+ args := make(map[string]interface{})
+ paramRe := regexp.MustCompile(`<parameter name="([^"]+)">([^<]*)</parameter>`)
+ paramMatches := paramRe.FindAllStringSubmatch(paramsXML, -1)
+
+ for _, paramMatch := range paramMatches {
+ if len(paramMatch) >= 3 {
+ args[paramMatch[1]] = paramMatch[2]
+ }
+ }
+
+ calls = append(calls, ToolCall{Name: toolName, Args: args})
+ }
+ }
+
+ return calls
+}
+
+// parseOpenAIJSONFormat handles OpenAI JSON format: [{"name": "...", "parameters": {...}}]
+func (d *Del) parseOpenAIJSONFormat(response string) []ToolCall {
+ var calls []ToolCall
+
+ // Look for JSON array of tool calls
+ re := regexp.MustCompile(`(?s)\[(\{[^}]*"name"[^}]*\}(?:,\s*\{[^}]*"name"[^}]*\})*)\]`)
+ matches := re.FindAllStringSubmatch(response, -1)
+
+ for _, match := range matches {
+ if len(match) >= 2 {
+ jsonStr := "[" + match[1] + "]"
+
+ var toolCalls []map[string]interface{}
+ if err := json.Unmarshal([]byte(jsonStr), &toolCalls); err != nil {
+ continue
+ }
+
+ for _, tc := range toolCalls {
+ if name, ok := tc["name"].(string); ok {
+ args := make(map[string]interface{})
+ if params, ok := tc["parameters"].(map[string]interface{}); ok {
+ args = params
+ }
+ calls = append(calls, ToolCall{Name: name, Args: args})
+ }
+ }
+ }
+ }
+
+ return calls
+}
+
+// parseLlamaFormat handles Llama format: <function=name>args</function>
+func (d *Del) parseLlamaFormat(response string) []ToolCall {
+ var calls []ToolCall
+
+ // Pattern to match Llama function calls
+ re := regexp.MustCompile(`(?s)<function=(\w+)>(\{[^}]*\}?)</function>`)
+ matches := re.FindAllStringSubmatch(response, -1)
+
+ for _, match := range matches {
+ if len(match) >= 3 {
+ toolName := match[1]
+ argsJSON := match[2]
+
+ var args map[string]interface{}
+ if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
+ continue // Skip invalid JSON
+ }
+
+ calls = append(calls, ToolCall{Name: toolName, Args: args})
+ }
+ }
+
+ return calls
+}
+
+// parseCustomFormat handles Del's legacy format: TOOL_USE: name args
+func (d *Del) parseCustomFormat(response string) []ToolCall {
+ var calls []ToolCall
+
+ // Pattern to match custom TOOL_USE format
+ re := regexp.MustCompile(`(?s)TOOL_USE:\s*(\w+)\s*(\{.*?\})`)
+ matches := re.FindAllStringSubmatch(response, -1)
+
+ for _, match := range matches {
+ if len(match) >= 3 {
+ toolName := match[1]
+ argsJSON := match[2]
+
+ var args map[string]interface{}
+ if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
+ continue // Skip invalid JSON
+ }
+
+ calls = append(calls, ToolCall{Name: toolName, Args: args})
+ }
+ }
+
+ return calls
+}
+
+// parseMarkdownFormat handles markdown function calls: ## Function Call\nname(args)
+func (d *Del) parseMarkdownFormat(response string) []ToolCall {
+ var calls []ToolCall
+
+ // Pattern to match markdown function calls
+ re := regexp.MustCompile(`(?s)##\s*(?:Function Call|Tool Call)[\s\S]*?(\w+)\s*\((\{[^}]*\}?)\)`)
+ matches := re.FindAllStringSubmatch(response, -1)
+
+ for _, match := range matches {
+ if len(match) >= 3 {
+ toolName := match[1]
+ argsJSON := match[2]
+
+ var args map[string]interface{}
+ if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
+ continue // Skip invalid JSON
+ }
+
+ calls = append(calls, ToolCall{Name: toolName, Args: args})
+ }
+ }
+
+ return calls
+}
+
+// parseToolCalls attempts to parse tool calls from response using all known formats
+func (d *Del) parseToolCalls(response string) []ToolCall {
+ // Try all known formats in order of likelihood
+
+ // 1. Chinese models (DeepSeek, Qwen, etc.) - most common currently
+ if calls := d.parseChineseFormat(response); len(calls) > 0 {
+ return calls
+ }
+
+ // 2. OpenAI XML format
+ if calls := d.parseOpenAIXMLFormat(response); len(calls) > 0 {
+ return calls
+ }
+
+ // 3. OpenAI JSON format
+ if calls := d.parseOpenAIJSONFormat(response); len(calls) > 0 {
+ return calls
+ }
+
+ // 4. Llama format
+ if calls := d.parseLlamaFormat(response); len(calls) > 0 {
+ return calls
+ }
+
+ // 5. Markdown format
+ if calls := d.parseMarkdownFormat(response); len(calls) > 0 {
+ return calls
+ }
+
+ // 6. Legacy custom format (for backward compatibility)
+ if calls := d.parseCustomFormat(response); len(calls) > 0 {
+ return calls
+ }
+
+ return []ToolCall{}
+}
+
// mcp_call (stub for now)
func (d *Del) stubMCP(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
endpoint, _ := args["endpoint"].(string)
@@ -824,8 +1262,10 @@ When users say:
Always assume "." (current directory) when no path is specified for file operations.`, cwd)
d := &Del{
- aiProvider: provider,
- tools: make(map[string]*Tool),
+ aiProvider: provider,
+ tools: make(map[string]*Tool),
+ safeMode: true, // Enable safe mode by default
+ confirmedActions: make(map[string]bool),
chatHistory: []api.Message{
{
Role: "system",
@@ -908,6 +1348,16 @@ Always assume "." (current directory) when no path is specified for file operati
Description: "Detect and run tests for various languages (Go, Node.js, Python, Rust)",
Handler: d.testRunner,
}
+ d.tools["multi_edit"] = &Tool{
+ Name: "multi_edit",
+ Description: "Edit multiple files with find-replace operations",
+ Handler: d.multiEdit,
+ }
+ d.tools["batch_operations"] = &Tool{
+ Name: "batch_operations",
+ Description: "Execute multiple file operations (read, write, list) in sequence",
+ Handler: d.batchOperations,
+ }
d.tools["mcp_call"] = &Tool{
Name: "mcp_call",
Description: "Stub for MCP integration",
@@ -920,50 +1370,70 @@ func (d *Del) streamChat(ctx context.Context, history []api.Message) (string, er
return d.aiProvider.StreamChat(ctx, history)
}
-func (d *Del) handleToolCalls(ctx context.Context, response string) string {
- re := regexp.MustCompile(`(?s)TOOL_USE:\s*(\w+)\s*(\{.*?\})`)
- matches := re.FindAllStringSubmatch(response, -1)
- if len(matches) == 0 {
- return response
- }
- var finalOutput strings.Builder
- for _, match := range matches {
- toolName := match[1]
- argsJSON := match[2]
- tool, ok := d.tools[toolName]
+func (d *Del) executeToolCalls(ctx context.Context, toolCalls []ToolCall) map[string]interface{} {
+ results := make(map[string]interface{})
+
+ fmt.Printf("\n🔧 Executing %d tool call(s)...\n", len(toolCalls))
+
+ for i, toolCall := range toolCalls {
+ tool, ok := d.tools[toolCall.Name]
if !ok {
- finalOutput.WriteString(fmt.Sprintf("[error] unknown tool: %s\n", toolName))
+ results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
+ "tool": toolCall.Name,
+ "error": fmt.Sprintf("unknown tool: %s", toolCall.Name),
+ }
continue
}
- var args map[string]interface{}
- if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
- finalOutput.WriteString(fmt.Sprintf("[error] bad args: %v\n", err))
- continue
+
+ // Check if confirmation is required
+ if d.requiresConfirmation(toolCall.Name, toolCall.Args) {
+ argsJSON, _ := json.Marshal(toolCall.Args)
+ description := fmt.Sprintf("%s with args: %s", toolCall.Name, string(argsJSON))
+ if !d.askConfirmation(toolCall.Name, description) {
+ results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
+ "tool": toolCall.Name,
+ "error": "operation cancelled by user",
+ }
+ continue
+ }
}
+
ch := make(chan string, 10)
go func() {
for line := range ch {
fmt.Print(line)
}
}()
- result, err := tool.Handler(ctx, args, ch)
+
+ result, err := tool.Handler(ctx, toolCall.Args, ch)
close(ch)
+
if err != nil {
- finalOutput.WriteString(fmt.Sprintf("[tool error] %v\n", err))
- continue
- }
- switch val := result.(type) {
- case string:
- finalOutput.WriteString(val)
- if !strings.HasSuffix(val, "\n") {
- finalOutput.WriteString("\n")
+ results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
+ "tool": toolCall.Name,
+ "error": err.Error(),
+ }
+ } else {
+ results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
+ "tool": toolCall.Name,
+ "result": result,
}
- default:
- outBytes, _ := json.MarshalIndent(val, "", " ")
- finalOutput.WriteString(string(outBytes) + "\n")
}
}
- return finalOutput.String()
+
+ return results
+}
+
+func (d *Del) handleToolCalls(ctx context.Context, response string) (bool, map[string]interface{}) {
+ // Use universal parser to detect tool calls in any format
+ toolCalls := d.parseToolCalls(response)
+ if len(toolCalls) == 0 {
+ return false, nil // No tool calls found
+ }
+
+ // Execute all tool calls and return results
+ results := d.executeToolCalls(ctx, toolCalls)
+ return true, results
}
func (d *Del) showHelp() {
@@ -976,6 +1446,8 @@ func (d *Del) showHelp() {
clear, cls Clear screen
context, ctx Show project context
tools List all available tools
+ safe Toggle safe mode (confirmation prompts)
+ unsafe Disable safe mode (DANGEROUS)
🔥 Power User Tips:
Just talk naturally! Del understands:
@@ -989,6 +1461,7 @@ func (d *Del) showHelp() {
🔧 Available Tools:
✓ File Operations • read, write, list files
+ ✓ Multi-File Editing • batch edits across files
✓ Code Analysis • analyze, format, lint code
✓ Security Scanning • find vulnerabilities
✓ Git Operations • status, commit, push, diff, log
@@ -996,6 +1469,7 @@ func (d *Del) showHelp() {
✓ Test Running • auto-detect and run project tests
✓ Command Execution • run any shell command
✓ Project Understanding • context-aware assistance
+ 🛡️ Safe Mode • confirmation prompts for dangerous ops
🎯 Pro Tip: Del learns your project as you work!
`)
@@ -1007,12 +1481,17 @@ func (d *Del) clearScreen() {
func (d *Del) showContext() {
cwd, _ := os.Getwd()
+ safeStatus := "🛡️ ENABLED"
+ if !d.safeMode {
+ safeStatus = "⚠️ DISABLED"
+ }
fmt.Printf(`
💼 Current Project Context:
📁 Directory: %s
🤖 Model: %s
💬 Chat History: %d messages
-`, cwd, d.aiProvider.Name(), len(d.chatHistory))
+ 🛡️ Safe Mode: %s
+`, cwd, d.aiProvider.Name(), len(d.chatHistory), safeStatus)
// Show project type detection
projectTypes := map[string]string{
@@ -1100,24 +1579,58 @@ func (d *Del) StartREPL(ctx context.Context) {
case "tools":
d.showTools()
continue
+ case "safe":
+ d.safeMode = !d.safeMode
+ if d.safeMode {
+ fmt.Println("🛡️ Safe mode ENABLED - dangerous operations will require confirmation")
+ } else {
+ fmt.Println("⚠️ Safe mode DISABLED - dangerous operations will execute without confirmation")
+ }
+ continue
+ case "unsafe":
+ d.safeMode = false
+ fmt.Println("⚠️ Safe mode DISABLED - BE CAREFUL!")
+ continue
}
fmt.Print("🤖 Del: ")
d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: input})
- // Use non-streaming for now to debug
+ // Stage 1: Get initial response from model (should contain tool calls)
response, err := d.aiProvider.Chat(ctx, d.chatHistory)
if err != nil {
fmt.Printf("\n⚠️ Error: %s\n\n", err)
continue
}
- fmt.Print(response)
- d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: response})
- output := d.handleToolCalls(ctx, response)
- if output != "" {
- fmt.Print(output)
+ // Check if there are tool calls to execute
+ hasTools, toolResults := d.handleToolCalls(ctx, response)
+
+ if hasTools {
+ // Stage 2: Tool calls were found and executed, ask model for final response
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: response})
+
+ fmt.Printf("\n📋 Providing tool results to model for final response...\n")
+
+ // Create tool results message
+ toolResultsJSON, _ := json.MarshalIndent(toolResults, "", " ")
+ toolMessage := fmt.Sprintf("Tool execution results:\n```json\n%s\n```\n\nPlease provide a natural response based on these results.", string(toolResultsJSON))
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: toolMessage})
+
+ // Get final response with tool results
+ finalResponse, err := d.aiProvider.Chat(ctx, d.chatHistory)
+ if err != nil {
+ fmt.Printf("\n⚠️ Error getting final response: %s\n\n", err)
+ continue
+ }
+ fmt.Printf("🤖 Del: %s", finalResponse)
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: finalResponse})
+ } else {
+ // No tool calls, just display the response normally
+ fmt.Print(response)
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: response})
}
+
fmt.Println()
}
}
go.sum
@@ -2,10 +2,10 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
-github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/ollama/ollama v0.9.2 h1:NEzeLb0gwz1XRyQUCPb30zqDyO/bze+Hiq9NUuYEUy4=
-github.com/ollama/ollama v0.9.2/go.mod h1:+5wt6UPgPmzYhnpLJ/rObxJJyEXURZ/SKKCMQsff8bA=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/ollama/ollama v0.9.2 h1:mN651uuzTx3Ct3QKUPNHZspqnrG/XlzxukNLCnMJMsk=
+github.com/ollama/ollama v0.9.2/go.mod h1:aio9yQ7nc4uwIbn6S0LkGEPgn8/9bNQLL1nHuH+OcD0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=