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
576 changes: 576 additions & 0 deletions HISTORY_IMPLEMENTATION_PLAN.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,19 @@ func (a *agent) getToolListForPrompt() string {
return toolInfo.String()
}

// SetMemory sets the conversation memory
func (a *agent) SetMemory(messages []llm.Message) {
a.mu.Lock()
defer a.mu.Unlock()

a.memory.Messages = make([]llm.Message, len(messages))
copy(a.memory.Messages, messages)

// Update token count if needed
// TODO: Implement token counting
a.memory.TokenCount = 0
}

// parseToolCallsFromContent attempts to parse tool calls from content
// This is for compatibility with providers that return tool calls in content
func (a *agent) parseToolCallsFromContent(content string) []llm.ToolCall {
Expand Down
148 changes: 148 additions & 0 deletions agent/history_agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package agent

import (
"context"
"fmt"
"os"

"github.com/nachoal/simple-agent-go/history"
)

// HistoryAgent wraps an agent with conversation history support
type HistoryAgent struct {
Agent
historyManager *history.Manager
currentSession *history.Session
}

// NewHistoryAgent creates a new agent with history support
func NewHistoryAgent(agent Agent, historyManager *history.Manager, session *history.Session) *HistoryAgent {
return &HistoryAgent{
Agent: agent,
historyManager: historyManager,
currentSession: session,
}
}

// Query sends a query and saves the conversation to history
func (ha *HistoryAgent) Query(ctx context.Context, query string) (*Response, error) {
// Remember the initial message count to rollback on failure
initialMessageCount := 0
if ha.currentSession != nil {
initialMessageCount = len(ha.currentSession.Messages)
}

// Execute query first
response, err := ha.Agent.Query(ctx, query)

// If successful, update history with the complete conversation
if err == nil && ha.currentSession != nil {
// Get the complete memory from the agent (includes all tool interactions)
agentMemory := ha.Agent.GetMemory()

// Convert and store all new messages since our last save
// We need to sync our session with the agent's memory
ha.currentSession.Messages = ha.historyManager.ConvertFromLLMMessages(agentMemory)

// Save session with complete history
if saveErr := ha.historyManager.SaveSession(ha.currentSession); saveErr != nil {
// Log error but don't fail the query
fmt.Fprintf(os.Stderr, "\n[WARNING] Failed to save conversation history: %v\n", saveErr)
fmt.Fprintf(os.Stderr, "Your conversation may not be saved. Please check disk space and permissions.\n\n")
}
Comment thread
nachoal marked this conversation as resolved.
} else if err != nil && ha.currentSession != nil {
// Query failed - rollback to initial state
ha.currentSession.Messages = ha.currentSession.Messages[:initialMessageCount]
}

return response, err
}

// QueryStream sends a query and streams the response while saving to history
func (ha *HistoryAgent) QueryStream(ctx context.Context, query string) (<-chan StreamEvent, error) {
// Remember the initial message count to rollback on failure
initialMessageCount := 0
if ha.currentSession != nil {
initialMessageCount = len(ha.currentSession.Messages)
}
Comment thread
cursor[bot] marked this conversation as resolved.

// Get the stream
events, err := ha.Agent.QueryStream(ctx, query)
if err != nil {
return nil, err
}

// Create a new channel to intercept events
intercepted := make(chan StreamEvent, 100)

go func() {
defer close(intercepted)

streamSucceeded := false

for event := range events {
// Forward the event
intercepted <- event

// Check for completion or error
switch event.Type {
case EventTypeComplete:
streamSucceeded = true
// Get the complete memory from the agent (includes all tool interactions)
if ha.currentSession != nil {
agentMemory := ha.Agent.GetMemory()
ha.currentSession.Messages = ha.historyManager.ConvertFromLLMMessages(agentMemory)

// Save session with complete history
if err := ha.historyManager.SaveSession(ha.currentSession); err != nil {
// Send error event through the stream
intercepted <- StreamEvent{
Type: EventTypeError,
Error: fmt.Errorf("failed to save conversation history: %w", err),
}
// Also log to stderr
fmt.Fprintf(os.Stderr, "\n[WARNING] Failed to save conversation history: %v\n", err)
}
}
case EventTypeError:
// Stream failed - rollback the session
if ha.currentSession != nil && !streamSucceeded {
ha.currentSession.Messages = ha.currentSession.Messages[:initialMessageCount]
}
}
}

// If stream ended without completion or error, rollback
if !streamSucceeded && ha.currentSession != nil {
ha.currentSession.Messages = ha.currentSession.Messages[:initialMessageCount]
}
}()

return intercepted, nil
}

// GetSession returns the current session
func (ha *HistoryAgent) GetSession() *history.Session {
return ha.currentSession
}

// SetSession updates the current session
func (ha *HistoryAgent) SetSession(session *history.Session) {
ha.currentSession = session
}

// RestoreMemoryFromSession restores the agent's memory from a session
func (ha *HistoryAgent) RestoreMemoryFromSession(session *history.Session) {
if session == nil || len(session.Messages) == 0 {
return
}

// Convert and restore messages
llmMessages := ha.historyManager.ConvertToLLMMessages(session.Messages)

// Set the memory directly
ha.Agent.SetMemory(llmMessages)

// Update current session
ha.currentSession = session
}
3 changes: 3 additions & 0 deletions agent/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ type Agent interface {

// SetSystemPrompt updates the system prompt
SetSystemPrompt(prompt string)

// SetMemory sets the conversation memory
SetMemory(messages []llm.Message)
}

const defaultSystemPrompt = `You are an AI assistant that can leverage external tools to answer the user.
Expand Down
166 changes: 156 additions & 10 deletions cmd/simple-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/nachoal/simple-agent-go/agent"
"github.com/nachoal/simple-agent-go/config"
"github.com/nachoal/simple-agent-go/history"
"github.com/nachoal/simple-agent-go/llm"
"github.com/nachoal/simple-agent-go/llm/anthropic"
"github.com/nachoal/simple-agent-go/llm/deepseek"
Expand All @@ -35,12 +36,17 @@ var (
verbose bool
continueConv bool
resume string
resumeSet bool

// Root command
rootCmd = &cobra.Command{
Use: "simple-agent",
Short: "AI agent with tool support",
Long: "Simple Agent Go - A powerful AI agent framework with multiple LLM providers and tool support",
PreRun: func(cmd *cobra.Command, args []string) {
// Check if resume flag was explicitly set
resumeSet = cmd.Flags().Changed("resume")
},
RunE: runTUI,
}

Expand Down Expand Up @@ -77,7 +83,10 @@ func init() {

// TUI-specific flags
rootCmd.Flags().BoolVarP(&continueConv, "continue", "c", false, "Continue last conversation")
rootCmd.Flags().StringVarP(&resume, "resume", "r", "", "Resume specific session or show picker if empty")
rootCmd.Flags().StringVarP(&resume, "resume", "r", "", "Resume specific session ID or show picker if no ID provided")

// Set NoOptDefVal for resume flag - this value is used when -r is provided without an argument
rootCmd.Flags().Lookup("resume").NoOptDefVal = "picker"

// Add subcommands
rootCmd.AddCommand(queryCmd)
Expand Down Expand Up @@ -181,13 +190,152 @@ func runTUI(cmd *cobra.Command, args []string) error {
agent.WithTemperature(0.7),
)

// Initialize history manager
historyMgr, err := history.NewManager()
if err != nil {
return fmt.Errorf("failed to initialize history: %w", err)
}

// Get current working directory
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}

var session *history.Session
var historyAgent *agent.HistoryAgent

// Handle continue/resume flags
if continueConv {
// TODO: Load last session
fmt.Println("Loading last conversation...")
} else if resume != "" {
// TODO: Load specific session or show picker
fmt.Printf("Resuming session: %s\n", resume)
// Try to load last session for this directory
session, err = historyMgr.GetLastSessionForPath(cwd)
if err != nil {
fmt.Printf("No previous conversation found for this directory.\n")
// Start new session
session, err = historyMgr.StartSession(cwd, provider, model)
if err != nil {
return fmt.Errorf("failed to start session: %w", err)
}
} else {
fmt.Printf("Continuing conversation from %s...\n", session.UpdatedAt.Format("Jan 02 15:04"))
// Update provider/model from session if different
if session.Provider != provider || session.Model != model {
provider = session.Provider
model = session.Model
// Recreate client with session's provider/model
llmClient.Close()
llmClient, err = createLLMClient(provider, model)
if err != nil {
return fmt.Errorf("failed to create %s client: %w", provider, err)
}
agentInstance = agent.New(llmClient,
agent.WithMaxIterations(10),
agent.WithTemperature(0.7),
)
}
}
} else if resumeSet {
// Show session picker if no ID provided, or load specific session
if resume == "picker" || resume == "list" || resume == "" {
sessions, err := historyMgr.ListSessionsForPath(cwd)
if err != nil {
return fmt.Errorf("failed to list sessions: %w", err)
}

if len(sessions) == 0 {
fmt.Println("No previous conversations found for this directory.")
// Start new session
session, err = historyMgr.StartSession(cwd, provider, model)
if err != nil {
return fmt.Errorf("failed to start session: %w", err)
}
} else {
// Show session picker
picker := tui.NewSessionPicker(sessions)
p := tea.NewProgram(picker)

pickerModel, err := p.Run()
if err != nil {
return fmt.Errorf("failed to run session picker: %w", err)
}

// Check if a session was selected
if verbose {
fmt.Printf("Picker model type: %T\n", pickerModel)
}
if pickerResult, ok := pickerModel.(*tui.SessionPicker); ok {
if verbose {
fmt.Printf("Picker result type assertion successful, SelectedSessionID: '%s'\n", pickerResult.SelectedSessionID)
}
if pickerResult.SelectedSessionID != "" {
// Session was selected
if verbose {
fmt.Printf("Selected session ID: %s\n", pickerResult.SelectedSessionID)
}
session, err = historyMgr.LoadSession(pickerResult.SelectedSessionID)
if err != nil {
return fmt.Errorf("failed to load session: %w", err)
}
fmt.Printf("Resuming session from %s...\n", session.UpdatedAt.Format("Jan 02 15:04"))
if verbose {
fmt.Printf("Session has %d messages\n", len(session.Messages))
}
}
} else {
if verbose {
fmt.Printf("Type assertion failed! Model type is: %T\n", pickerModel)
}
}

if session == nil {
// User cancelled - start new session instead
session, err = historyMgr.StartSession(cwd, provider, model)
if err != nil {
return fmt.Errorf("failed to start session: %w", err)
}
}
}
} else {
// Load specific session ID
session, err = historyMgr.LoadSession(resume)
if err != nil {
return fmt.Errorf("failed to load session %s: %w", resume, err)
}
}

// Update provider/model from session
if session != nil {
// Always update provider/model from the session
provider = session.Provider
model = session.Model
// Recreate client with session's provider/model
llmClient.Close()
llmClient, err = createLLMClient(provider, model)
if err != nil {
return fmt.Errorf("failed to create %s client: %w", provider, err)
}
agentInstance = agent.New(llmClient,
agent.WithMaxIterations(10),
agent.WithTemperature(0.7),
)
}
} else {
// Start new session
session, err = historyMgr.StartSession(cwd, provider, model)
if err != nil {
return fmt.Errorf("failed to start session: %w", err)
}
}

// Create history-aware agent
historyAgent = agent.NewHistoryAgent(agentInstance, historyMgr, session)

// Restore memory if continuing/resuming
if continueConv || resumeSet {
historyAgent.RestoreMemoryFromSession(session)
if verbose && session != nil {
fmt.Printf("Restored %d messages from session %s\n", len(session.Messages), session.ID)
}
}

// If verbose, show the enhanced system prompt (including tools)
Expand All @@ -210,13 +358,11 @@ func runTUI(cmd *cobra.Command, args []string) error {
}
}
fmt.Println("===================\n")
fmt.Println("Press Enter to continue...")
fmt.Scanln()
}

// Create and run TUI (bordered version with providers)
// Create and run TUI (bordered version with providers and history)
p := tea.NewProgram(
tui.NewBorderedTUIWithProviders(llmClient, agentInstance, provider, model, providers, configManager),
tui.NewBorderedTUIWithHistory(llmClient, historyAgent, provider, model, providers, configManager),
)

if _, err := p.Run(); err != nil {
Expand Down
Loading