Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ coverage.html
coverage.out

# Local config
.simple-agent/
.simple-agent/

# Crush directory
.crush/
101 changes: 101 additions & 0 deletions CRUSH.md
Original file line number Diff line number Diff line change
@@ -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
119 changes: 115 additions & 4 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ import (
"sort"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/nachoal/simple-agent-go/llm"
"github.com/nachoal/simple-agent-go/tools"
"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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
},
}
}
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
26 changes: 17 additions & 9 deletions agent/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions cmd/simple-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -420,7 +420,7 @@ func runQuery(cmd *cobra.Command, args []string) error {
}
}
}
fmt.Println("===================\n")
fmt.Println("===================")
}

// Execute query
Expand Down
Loading