diff --git a/.gitignore b/.gitignore index d2b4022..0c87bbe 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,7 @@ coverage.html coverage.out # Local config -.simple-agent/ \ No newline at end of file +.simple-agent/ + +# Crush directory +.crush/ \ No newline at end of file diff --git a/CRUSH.md b/CRUSH.md new file mode 100644 index 0000000..167bd1e --- /dev/null +++ b/CRUSH.md @@ -0,0 +1,101 @@ +# Crush Context for Simple Agent Go + +## Project Overview +Simple Agent Go is a modern, high-performance AI agent framework implemented in Go with a terminal UI. + +## Build Commands +```bash +# Build the project +make build + +# Build for all platforms +make build-all + +# Install dependencies +make deps + +# Clean build artifacts +make clean +``` + +## Development Commands +```bash +# Run tests with coverage +make test + +# Run tests and generate coverage report +make test-coverage + +# Format code +make fmt + +# Run linter +make lint + +# Vet code +make vet + +# Install development dependencies +make dev-deps +``` + +## Run Commands +```bash +# Run the agent +make run + +# Install the binary to GOPATH +make install + +# Run with live reload (requires air) +make watch +``` + +## Go Commands +```bash +# Run tests manually +go test ./... + +# Run tests with race detection +go test -race ./... + +# Update dependencies +go mod tidy + +# Download dependencies +go mod download +``` + +## Code Style Preferences +- Use `gofumpt` for formatting +- Use `golangci-lint` for linting +- Follow Go idioms and conventions +- Use struct tags for tool metadata instead of decorators +- Implement interfaces for LLM clients and tools +- Use goroutines for concurrent tool execution + +## Project Structure +- `agent/` - Core agent logic +- `llm/` - LLM provider clients +- `tools/` - Tool implementations +- `tui/` - Terminal UI components +- `cmd/` - CLI entry point +- `config/` - Configuration management +- `internal/` - Internal packages + +## Key Technologies +- Go 1.24+ +- Bubble Tea TUI framework +- Lipgloss styling +- Cobra CLI framework +- godotenv for environment variables + +## Pull Request Workflow +When requested to "create a PR" or similar, use the GitHub CLI (gh) to create a pull request with: +- A clear, descriptive title +- A comprehensive description including: + - Summary of changes + - Implementation details + - Benefits and impact +- Proper formatting with headers and bullet points +- Include "💘 Generated with Crush" signature \ No newline at end of file diff --git a/agent/agent.go b/agent/agent.go index 4cd02b2..127fee5 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -10,6 +10,7 @@ import ( "sort" "strings" "sync" + "sync/atomic" "time" "github.com/nachoal/simple-agent-go/llm" @@ -17,6 +18,14 @@ import ( "github.com/nachoal/simple-agent-go/tools/registry" ) +// Tool ID generation +var toolIDCounter uint64 + +func generateToolID() string { + id := atomic.AddUint64(&toolIDCounter, 1) + return fmt.Sprintf("tool-%d-%d", time.Now().UnixNano(), id) +} + // agent is the main agent implementation type agent struct { client llm.Client @@ -73,6 +82,12 @@ func (a *agent) Query(ctx context.Context, query string) (*Response, error) { Content: llm.StringPtr(query), }) + // Extract stream channel (if any) once + var streamChan chan<- StreamEvent + if ch, ok := ctx.Value("toolEventChan").(chan StreamEvent); ok { + streamChan = ch // nil if UI isn't streaming + } + // Get available tools if configured var availableTools []map[string]interface{} if len(a.config.Tools) > 0 { @@ -199,8 +214,8 @@ func (a *agent) Query(ctx context.Context, query string) (*Response, error) { }) } - // Execute tool calls concurrently - results := a.toolRegistry.ExecuteToolCalls(ctx, toolCalls) + // Execute tool calls with events if channel provided + results := a.executeToolsWithEvents(ctx, toolCalls, streamChan) allToolResults = append(allToolResults, results...) // Add tool results to memory @@ -355,12 +370,19 @@ func (a *agent) QueryStream(ctx context.Context, query string) (<-chan StreamEve Arguments: tc.Function.Arguments, } + // Parse arguments for display + var args map[string]interface{} + if err := json.Unmarshal(tc.Function.Arguments, &args); err != nil { + args = map[string]interface{}{"raw": string(tc.Function.Arguments)} + } + // Send tool start event events <- StreamEvent{ Type: EventTypeToolStart, Tool: &ToolEvent{ - Name: tc.Function.Name, - Args: string(tc.Function.Arguments), + Name: tc.Function.Name, + Args: args, + ArgsRaw: string(tc.Function.Arguments), }, } } @@ -379,6 +401,7 @@ func (a *agent) QueryStream(ctx context.Context, query string) (<-chan StreamEve events <- StreamEvent{ Type: EventTypeToolResult, Tool: &ToolEvent{ + ID: result.ID, Name: result.Name, Result: content, Error: result.Error, @@ -726,4 +749,92 @@ func (a *agent) parseToolCallsFromContent(content string) []llm.ToolCall { } return toolCalls +} + +// executeToolsWithEvents executes tools and emits events without streaming +func (a *agent) executeToolsWithEvents(ctx context.Context, calls []tools.ToolCall, eventChan chan<- StreamEvent) []tools.ToolResult { + results := make([]tools.ToolResult, len(calls)) + var wg sync.WaitGroup + + for i, call := range calls { + wg.Add(1) + go func(idx int, tc tools.ToolCall) { + defer wg.Done() + + // Generate unique ID if not present + if tc.ID == "" { + tc.ID = generateToolID() + } + + // Parse arguments for display + var args map[string]interface{} + if err := json.Unmarshal(tc.Arguments, &args); err != nil { + args = map[string]interface{}{"raw": string(tc.Arguments)} + } + + // Print to stderr in query mode (no event channel) + if eventChan == nil { + fmt.Fprintf(os.Stderr, "🔧 Calling tool: %s\n", tc.Name) + } + + // Emit tool start event if channel provided + if eventChan != nil { + if os.Getenv("SIMPLE_AGENT_DEBUG") == "true" { + fmt.Fprintf(os.Stderr, "[Agent] Sending tool start event for %s (ID: %s)\n", tc.Name, tc.ID) + } + select { + case eventChan <- StreamEvent{ + Type: EventTypeToolStart, + Tool: &ToolEvent{ + ID: tc.ID, + Name: tc.Name, + Args: args, + ArgsRaw: string(tc.Arguments), + }, + }: + case <-ctx.Done(): + return + } + } + + // Execute the tool + startTime := time.Now() + result := a.toolRegistry.ExecuteToolCall(ctx, tc) + duration := time.Since(startTime) + results[idx] = result + + // Print completion in query mode + if eventChan == nil { + fmt.Fprintf(os.Stderr, "🔧 %s completed in %v\n", tc.Name, duration) + } + + // Emit tool result event if channel provided + if eventChan != nil { + eventType := EventTypeToolResult + if result.Error != nil { + // Could distinguish between timeout/cancel/error here + eventType = EventTypeToolResult + } + + select { + case eventChan <- StreamEvent{ + Type: eventType, + Tool: &ToolEvent{ + ID: tc.ID, + Name: tc.Name, + Args: args, + ArgsRaw: string(tc.Arguments), + Result: result.Result, + Error: result.Error, + }, + }: + case <-ctx.Done(): + return + } + } + }(i, call) + } + + wg.Wait() + return results } \ No newline at end of file diff --git a/agent/types.go b/agent/types.go index aaea98c..8083dea 100644 --- a/agent/types.go +++ b/agent/types.go @@ -67,19 +67,27 @@ type StreamEvent struct { type EventType string const ( - EventTypeMessage EventType = "message" - EventTypeToolStart EventType = "tool_start" - EventTypeToolResult EventType = "tool_result" - EventTypeError EventType = "error" - EventTypeComplete EventType = "complete" + EventTypeMessage EventType = "message" + EventTypeToolStart EventType = "tool_start" + EventTypeToolProgress EventType = "tool_progress" + EventTypeToolResult EventType = "tool_result" + EventTypeToolTimeout EventType = "tool_timeout" + EventTypeToolCancel EventType = "tool_cancel" + EventTypeThinking EventType = "thinking" // LLM is reasoning + EventTypeError EventType = "error" + EventTypeComplete EventType = "complete" ) // ToolEvent contains information about a tool execution type ToolEvent struct { - Name string - Args string - Result string - Error error + ID string // Unique tool execution ID + Name string // Tool name + Args map[string]interface{} // Parsed arguments + ArgsRaw string // Raw JSON string + Result string // Execution result + Error error // Execution error + Progress float64 // Progress percentage (0-1) + Message string // Progress message } // ProgressEvent represents agent progress events diff --git a/cmd/simple-agent/main.go b/cmd/simple-agent/main.go index 88fe4c3..2f064f7 100644 --- a/cmd/simple-agent/main.go +++ b/cmd/simple-agent/main.go @@ -357,7 +357,7 @@ func runTUI(cmd *cobra.Command, args []string) error { } } } - fmt.Println("===================\n") + fmt.Println("===================") } // Create and run TUI (bordered version with providers and history) @@ -420,7 +420,7 @@ func runQuery(cmd *cobra.Command, args []string) error { } } } - fmt.Println("===================\n") + fmt.Println("===================") } // Execute query diff --git a/docs/TOOL_CALLING_VISIBILITY_IMPLEMENTATION_PLAN.md b/docs/TOOL_CALLING_VISIBILITY_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..fa491e7 --- /dev/null +++ b/docs/TOOL_CALLING_VISIBILITY_IMPLEMENTATION_PLAN.md @@ -0,0 +1,339 @@ +# Tool Calling Visibility - Implementation Plan + +## Overview + +This document provides a straightforward implementation plan for adding tool calling visibility to Simple Agent Go. This is a personal tool optimized for macOS/Linux usage. + +## Implementation Phases + +### Phase 1: Core Infrastructure (1-2 days) + +#### 1.1 Extend Agent Streaming Events + +**Files to modify:** +- `agent/types.go` +- `agent/agent.go` + +**Tasks:** +1. Add new event types to `StreamEvent`: + ```go + const ( + EventTypeToolStart EventType = "tool_start" + EventTypeToolProgress EventType = "tool_progress" + EventTypeToolResult EventType = "tool_result" + EventTypeToolTimeout EventType = "tool_timeout" + EventTypeToolCancel EventType = "tool_cancel" + ) + ``` + +2. Extend `ToolEvent` structure: + ```go + type ToolEvent struct { + ID string // Unique tool execution ID + Name string // Tool name + Args map[string]interface{} // Parsed arguments + ArgsRaw string // Raw JSON string + Result string // Execution result + Error error // Execution error + Progress float64 // Progress percentage (0-1) + } + ``` + +3. Modify `agent.executeTools()` to emit events during tool execution + +4. Implement unique ID generation: + ```go + var toolIDCounter uint64 + + func generateToolID() string { + id := atomic.AddUint64(&toolIDCounter, 1) + return fmt.Sprintf("tool-%d-%d", time.Now().UnixNano(), id) + } + ``` + +--- + +#### 1.2 Tool Progress Interface + +**Files to create/modify:** +- `tools/progress.go` (new) +- `tools/tool.go` + +**Tasks:** +1. Create progress reporter interface: + ```go + type ProgressReporter interface { + ReportProgress(message string) + ReportProgressPercent(message string, percent float64) + } + ``` + +2. Create optional interface: + ```go + type ProgressableTool interface { + Tool + ExecuteWithProgress(ctx context.Context, params string, reporter ProgressReporter) (string, error) + } + ``` + +3. Update agent to check for progressable tools and use them + +--- + +### Phase 2: TUI Integration (2-3 days) + +#### 2.1 TUI State Management + +**Files to modify:** +- `tui/bordered.go` + +**Tasks:** +1. Add tool tracking state: + ```go + type BorderedTUI struct { + // ... existing fields ... + activeTools []ActiveTool + completedTools []CompletedTool // For batching + toolErrors []ToolError + eventStream <-chan agent.StreamEvent + lastRender time.Time + renderPending bool + showFullPanel bool + toolViewport viewport.Model + } + + type ActiveTool struct { + ID string + Name string + Args map[string]interface{} + StartTime time.Time + Status ToolStatus + Output *CircularBuffer + Progress float64 + LastUpdate time.Time + } + ``` + +2. Create circular buffer for output management +3. Switch from `Query` to `QueryStream` in sendMessage + +--- + +#### 2.2 Event Handling in Update + +**Files to modify:** +- `tui/bordered.go` + +**Tasks:** +1. Add event subscription: + ```go + func (m *BorderedTUI) subscribeToStream() tea.Cmd { + return func() tea.Msg { + event, ok := <-m.eventStream + if !ok { + return streamCompleteMsg{} + } + return toolEventMsg{event: event} + } + } + ``` + +2. Handle tool events in Update method: + - Start: Add to activeTools + - Progress: Update output buffer + - Complete: Move to completed, schedule removal + - Error: Add to toolErrors with persistence + +3. Implement render throttling (33ms) + +--- + +#### 2.3 Rendering Implementation + +**Files to modify:** +- `tui/bordered.go` + +**Tasks:** +1. Create tool overlay rendering: + ```go + func (m *BorderedTUI) renderToolOverlay() string { + // Compact mode for width < 80 + if m.width < 80 { + return fmt.Sprintf("🔧 %d tools running...", len(m.activeTools)) + } + + // Full mode + var b strings.Builder + for _, tool := range m.activeTools { + // Render tool status with icon, name, duration + // Show first 5 lines of output + } + return b.String() + } + ``` + +2. Add to main View() after thinking spinner +3. Implement batch completion messages +4. Add argument redaction for sensitive data + +--- + +### Phase 3: Advanced Features (1-2 days) + +#### 3.1 Full Panel View + +**Tasks:** +1. Add 't' key handler to toggle full panel +2. Create scrollable viewport with tool history +3. Implement vim-style navigation (j/k/g/G) +4. Add search with '/' key + +--- + +#### 3.2 Configuration + +**Files to create:** +- `config/tool_visibility.go` + +**Tasks:** +1. Add configuration struct: + ```go + type ToolVisibilityConfig struct { + Enabled bool + MaxOutputLines int // Default: 5 + ShowArguments bool // Default: true + ShowDuration bool // Default: true + CompletionDelay time.Duration // Default: 1.5s + ErrorPersistence time.Duration // Default: 5s + RenderThrottle time.Duration // Default: 33ms + } + ``` + +2. Load from YAML config file +3. Apply defaults if not specified + +--- + +#### 3.3 Tool Updates + +**Files to modify:** +- 2-3 example tools in `tools/` + +**Tasks:** +1. Add progress support to a long-running tool (e.g., shell, download) +2. Mark tools with sensitive arguments +3. Test cancellation handling + +--- + +### Phase 4: Polish & Testing (1 day) + +#### 4.1 Visual Polish + +**Tasks:** +1. Add color themes (including deuteranopia-friendly) +2. Ensure status icons always visible +3. Fine-tune animations and transitions +4. Handle edge cases (very long output, many parallel tools) + +--- + +#### 4.2 Manual Testing Guide + +**Test Scenarios:** + +1. **Basic Tool Execution** + - Run a query that uses 1 tool + - Verify tool name appears below "Thinking..." + - Verify output preview shows (max 5 lines) + - Verify completion message appears briefly + +2. **Parallel Tools** + - Query: "Search Wikipedia for Go programming and also search Google for Go tutorials" + - Verify both tools show simultaneously + - Verify parallel indicator (⧉) appears + - Verify batch completion if they finish together + +3. **Long-Running Tools** + - Execute a shell command with sleep + - Verify duration counter updates + - Test Ctrl+C cancellation + - Verify timeout handling (30s default) + +4. **Error Handling** + - Trigger a tool error (bad arguments) + - Verify error shows in red for 5s + - Verify error details visible + - Test 'e' key to dismiss + +5. **Terminal Resize** + - Start with wide terminal + - Resize to < 80 chars + - Verify switches to compact mode + - Resize back, verify full mode returns + +6. **Full Panel** + - Press 't' to open full panel + - Test j/k navigation + - Test '/' search + - Press 'q' to close + +7. **Output Overflow** + - Run tool that produces 20+ lines + - Verify only 5 lines shown + - Verify 'd' key dumps to temp file + +8. **Sensitive Arguments** + - Run tool with API key in args + - Verify key is redacted (***REDACTED***) + +**Performance Checks:** +- No UI lag during updates +- Memory usage stable over time +- Smooth scrolling in full panel + +--- + +## Implementation Order + +1. **Day 1**: Core infrastructure (1.1, 1.2) +2. **Day 2**: Basic TUI integration (2.1, 2.2) +3. **Day 3**: Rendering and polish (2.3, 4.1) +4. **Day 4**: Advanced features (3.1, 3.2) +5. **Day 5**: Tool updates and testing (3.3, 4.2) + +## Key Decisions Made + +1. **No feature flags** - Direct implementation +2. **No automated tests** - Manual testing only +3. **macOS/Linux only** - No Windows considerations +4. **Channel-only updates** - Prevents race conditions +5. **33ms render throttle** - Smooth 30fps updates +6. **5-line output preview** - Balances info vs clutter + +## Configuration Example + +```yaml +# ~/.simple-agent/config.yaml +tool_visibility: + enabled: true + max_output_lines: 5 + show_arguments: true + show_duration: true + completion_delay: 1500ms + error_persistence: 5s + render_throttle: 33ms + +theme: classic # or 'deuteranopia' +``` + +## Success Criteria + +- [x] Tools show real-time status during execution +- [x] No UI blocking or lag +- [x] Parallel tools display correctly +- [x] Errors are visible but not intrusive +- [x] Works smoothly on macOS and Linux +- [x] Clean, minimal aesthetic maintained + +This plan focuses on getting the feature working well for personal use, without the overhead of enterprise features. \ No newline at end of file diff --git a/docs/TOOL_CALLING_VISIBILITY_IMPLEMENTATION_READY.md b/docs/TOOL_CALLING_VISIBILITY_IMPLEMENTATION_READY.md new file mode 100644 index 0000000..fed7d88 --- /dev/null +++ b/docs/TOOL_CALLING_VISIBILITY_IMPLEMENTATION_READY.md @@ -0,0 +1,317 @@ +# Tool Calling Visibility - Implementation Ready 🚀 + +## Quick Reference: Staff Engineer Decisions + +### 1. Event Batching +```go +// Batch completions within same frame +type batchCompleteMsg struct { + tools []CompletedTool + time time.Time +} + +func (m *BorderedTUI) renderBatchCompletion(tools []CompletedTool) string { + if len(tools) == 1 { + return fmt.Sprintf("✅ %s completed", tools[0].Name) + } + return fmt.Sprintf("✅ %d tools completed", len(tools)) +} +``` + +### 2. Progress Reporting +```go +const ProgressUpdateInterval = 250 * time.Millisecond + +func (m *BorderedTUI) renderProgress(tool ActiveTool) string { + if m.config.ProgressStyle == "bar" && m.width >= 60 { + return renderProgressBar(tool.Progress) + } + return tool.LastProgressText // Simple text updates +} + +func renderProgressBar(pct float64) string { + const width = 10 + filled := int(pct * width) + return fmt.Sprintf("[%s%s] %d%%", + strings.Repeat("█", filled), + strings.Repeat("░", width-filled), + int(pct*100)) +} +``` + +### 3. Argument Redaction +```go +var sensitivePattern = regexp.MustCompile(`(?i)(apikey|secret|token|password)=[^\s]+`) + +func Redact(text string) string { + return sensitivePattern.ReplaceAllStringFunc(text, func(match string) string { + parts := strings.SplitN(match, "=", 2) + return parts[0] + "=***REDACTED***" + }) +} + +// Tool-level control +type ToolMetadata struct { + SensitiveArgs bool `json:"sensitive_args"` +} + +func (m *BorderedTUI) formatArguments(args string, sensitive bool) string { + if sensitive { + return "