Skip to main content
Glama
manager.go18.1 kB
package aicoder import ( "context" "fmt" "os" "os/exec" "strings" "sync" "time" "github.com/google/uuid" ) // AICoderManager manages AI coder instances type AICoderManager struct { coders map[string]*AICoderProcess mu sync.RWMutex eventBus EventBus config Config workspaceMgr *WorkspaceManager providerReg *ProviderRegistry processMgr *ProcessManager // PTY support for interactive sessions ptyManager *PTYManager } // NewAICoderManager creates a new AI coder manager func NewAICoderManager(config Config, eventBus EventBus) (*AICoderManager, error) { aiConfig := config.GetAICoderConfig() // Create workspace manager workspaceMgr, err := NewWorkspaceManager(aiConfig.WorkspaceBaseDir) if err != nil { return nil, fmt.Errorf("failed to create workspace manager: %w", err) } // Create provider registry providerReg := NewProviderRegistry() // Get provider configurations to check which ones have CLI tools providerConfigs := config.GetProviderConfigs() // Register built-in providers only if they don't have CLI tool configurations if _, hasCLI := providerConfigs["claude"]; !hasCLI { claudeProvider := NewClaudeProvider("", "") if err := providerReg.Register("claude", claudeProvider); err != nil { return nil, fmt.Errorf("failed to register claude provider: %w", err) } } // Register OpenAI provider (always register as no CLI config exists) openaiProvider := NewOpenAIProvider("", "") if err := providerReg.Register("openai", openaiProvider); err != nil { return nil, fmt.Errorf("failed to register openai provider: %w", err) } if _, hasCLI := providerConfigs["gemini"]; !hasCLI { geminiProvider := NewGeminiProvider("", "") if err := providerReg.Register("gemini", geminiProvider); err != nil { return nil, fmt.Errorf("failed to register gemini provider: %w", err) } } if _, hasCLI := providerConfigs["terminal"]; !hasCLI { terminalProvider := NewTerminalProvider("") if err := providerReg.Register("terminal", terminalProvider); err != nil { return nil, fmt.Errorf("failed to register terminal provider: %w", err) } } // Register CLI tool providers based on configuration if err := registerCLIToolProviders(providerReg, config); err != nil { return nil, fmt.Errorf("failed to register CLI tool providers: %w", err) } manager := &AICoderManager{ coders: make(map[string]*AICoderProcess), eventBus: eventBus, config: config, workspaceMgr: workspaceMgr, providerReg: providerReg, } // Create process manager manager.processMgr = NewProcessManager(manager, workspaceMgr, providerReg) return manager, nil } // NewAICoderManagerWithPTY creates a new AI coder manager with PTY support func NewAICoderManagerWithPTY(config Config, eventBus EventBus, dataProvider BrummerDataProvider) (*AICoderManager, error) { // Create the standard manager first manager, err := NewAICoderManager(config, eventBus) if err != nil { return nil, err } // Add PTY support manager.ptyManager = NewPTYManager(dataProvider, eventBus) return manager, nil } // NewAICoderManagerWithoutMockProvider creates a new AI coder manager without registering the mock provider // This is useful for testing when you want to control provider registration func NewAICoderManagerWithoutMockProvider(config Config, eventBus EventBus) (*AICoderManager, error) { aiConfig := config.GetAICoderConfig() // Create workspace manager workspaceMgr, err := NewWorkspaceManager(aiConfig.WorkspaceBaseDir) if err != nil { return nil, fmt.Errorf("failed to create workspace manager: %w", err) } // Create provider registry (no providers registered) providerReg := NewProviderRegistry() manager := &AICoderManager{ coders: make(map[string]*AICoderProcess), eventBus: eventBus, config: config, workspaceMgr: workspaceMgr, providerReg: providerReg, } // Create process manager manager.processMgr = NewProcessManager(manager, workspaceMgr, providerReg) return manager, nil } // CreateCoder creates a new AI coder instance func (m *AICoderManager) CreateCoder(ctx context.Context, req CreateCoderRequest) (*AICoderProcess, error) { m.mu.Lock() defer m.mu.Unlock() // Check max concurrent limit aiConfig := m.config.GetAICoderConfig() if m.processMgr.GetActiveCount() >= aiConfig.MaxConcurrent { return nil, fmt.Errorf("maximum concurrent AI coders limit reached (%d)", aiConfig.MaxConcurrent) } // Generate unique ID coderID := uuid.New().String() // Use default provider if not specified provider := req.Provider if provider == "" { provider = aiConfig.DefaultProvider if provider == "" { provider = "claude" // Default to Claude provider } } // Validate provider exists if _, err := m.providerReg.Get(provider); err != nil { return nil, fmt.Errorf("invalid provider %s: %w", provider, err) } // Create workspace workspaceDir, err := m.workspaceMgr.CreateWorkspace(coderID) if err != nil { return nil, fmt.Errorf("failed to create workspace: %w", err) } // Create AI coder process now := time.Now() coder := &AICoderProcess{ ID: coderID, Name: req.Name, Provider: provider, WorkspaceDir: workspaceDir, Status: StatusCreating, CreatedAt: now, UpdatedAt: now, Task: req.Task, Progress: 0.0, SessionID: fmt.Sprintf("ai-coder-%s", coderID[:8]), // Short session ID for tmux-style workspaceMgr: m.workspaceMgr, // Inject workspace manager } // Store the coder m.coders[coderID] = coder // Emit creation event if m.eventBus != nil { m.eventBus.Emit(string(EventAICoderCreated), AICoderEvent{ Type: string(EventAICoderCreated), CoderID: coderID, CoderName: coder.Name, Status: string(coder.Status), Message: fmt.Sprintf("AI coder created: %s", coder.Name), Time: now, Data: map[string]interface{}{ "provider": provider, "task": req.Task, }, }) } // Copy initial workspace files if provided if len(req.WorkspaceFiles) > 0 { for _, file := range req.WorkspaceFiles { // In a real implementation, this would copy files from the project // to the AI coder's workspace content := fmt.Sprintf("// Initial file: %s\n// Task: %s\n", file, req.Task) if err := m.workspaceMgr.WriteFile(workspaceDir, file, []byte(content)); err != nil { // Log error but don't fail creation fmt.Printf("Warning: failed to create initial file %s: %v\n", file, err) } } } return coder, nil } // GetCoder retrieves an AI coder by ID func (m *AICoderManager) GetCoder(id string) (*AICoderProcess, bool) { m.mu.RLock() defer m.mu.RUnlock() coder, exists := m.coders[id] return coder, exists } // ListCoders returns all AI coder instances func (m *AICoderManager) ListCoders() []*AICoderProcess { m.mu.RLock() defer m.mu.RUnlock() coders := make([]*AICoderProcess, 0, len(m.coders)) for _, coder := range m.coders { coders = append(coders, coder) } return coders } // StartCoder starts an AI coder process func (m *AICoderManager) StartCoder(coderID string) error { coder, exists := m.GetCoder(coderID) if !exists { return fmt.Errorf("AI coder %s not found", coderID) } // Check if coder can be started status := coder.GetStatus() if status != StatusCreating && status != StatusStopped && status != StatusPaused { return fmt.Errorf("AI coder %s cannot be started from status %s", coderID, status) } // Start the coder process if err := m.processMgr.StartCoder(coder); err != nil { return fmt.Errorf("failed to start AI coder: %w", err) } // Wait a brief moment to check for immediate failures time.Sleep(50 * time.Millisecond) if coder.GetStatus() == StatusFailed { return fmt.Errorf("AI coder failed to start: provider error") } // Emit start event if m.eventBus != nil { m.eventBus.Emit(string(EventAICoderStarted), AICoderEvent{ Type: string(EventAICoderStarted), CoderID: coderID, CoderName: coder.Name, Status: string(StatusRunning), Message: "AI coder started", Time: time.Now(), }) } return nil } // registerCLIToolProviders registers CLI tool providers based on configuration and installation check func registerCLIToolProviders(providerReg *ProviderRegistry, config Config) error { // Get provider configurations providerConfigs := config.GetProviderConfigs() // Go through all configured providers for name, providerConfig := range providerConfigs { // Skip if no CLI tool configuration if providerConfig == nil || providerConfig.CLITool == nil { continue } cliToolConfig := providerConfig.CLITool // Get the command name command := cliToolConfig.Command if command == "" { command = name // default to provider name } // Check if the command is installed if _, err := exec.LookPath(command); err != nil { // Command not found, skip this provider continue } // Create and register the provider cliProvider := NewCLIToolProvider(name, *cliToolConfig) if err := providerReg.Register(name, cliProvider); err != nil { // Log error but continue with other providers fmt.Printf("Warning: failed to register %s provider: %v\n", name, err) } } return nil } // StopCoder stops an AI coder process func (m *AICoderManager) StopCoder(coderID string) error { coder, exists := m.GetCoder(coderID) if !exists { return fmt.Errorf("AI coder %s not found", coderID) } // Stop the coder process if err := m.processMgr.StopCoder(coderID); err != nil { return fmt.Errorf("failed to stop AI coder: %w", err) } // Emit stop event if m.eventBus != nil { m.eventBus.Emit(string(EventAICoderStopped), AICoderEvent{ Type: string(EventAICoderStopped), CoderID: coderID, CoderName: coder.Name, Status: string(StatusStopped), Message: "AI coder stopped", Time: time.Now(), }) } return nil } // PauseCoder pauses an AI coder process func (m *AICoderManager) PauseCoder(coderID string) error { coder, exists := m.GetCoder(coderID) if !exists { return fmt.Errorf("AI coder %s not found", coderID) } if err := m.processMgr.PauseCoder(coderID); err != nil { return fmt.Errorf("failed to pause AI coder: %w", err) } // Emit pause event if m.eventBus != nil { m.eventBus.Emit(string(EventAICoderPaused), AICoderEvent{ Type: string(EventAICoderPaused), CoderID: coderID, CoderName: coder.Name, Status: string(StatusPaused), Message: "AI coder paused", Time: time.Now(), }) } return nil } // ResumeCoder resumes a paused AI coder process func (m *AICoderManager) ResumeCoder(coderID string) error { coder, exists := m.GetCoder(coderID) if !exists { return fmt.Errorf("AI coder %s not found", coderID) } if err := m.processMgr.ResumeCoder(coderID); err != nil { return fmt.Errorf("failed to resume AI coder: %w", err) } // Emit resume event if m.eventBus != nil { m.eventBus.Emit(string(EventAICoderResumed), AICoderEvent{ Type: string(EventAICoderResumed), CoderID: coderID, CoderName: coder.Name, Status: string(StatusRunning), Message: "AI coder resumed", Time: time.Now(), }) } return nil } // DeleteCoder deletes an AI coder instance func (m *AICoderManager) DeleteCoder(coderID string) error { m.mu.Lock() defer m.mu.Unlock() coder, exists := m.coders[coderID] if !exists { return fmt.Errorf("AI coder %s not found", coderID) } // Stop if running if m.processMgr.IsActive(coderID) { if err := m.processMgr.StopCoder(coderID); err != nil { return fmt.Errorf("failed to stop AI coder before deletion: %w", err) } } // Clean up workspace if err := m.workspaceMgr.CleanupWorkspace(coder.WorkspaceDir); err != nil { // Log error but continue with deletion fmt.Printf("Warning: failed to cleanup workspace: %v\n", err) } // Remove from map delete(m.coders, coderID) // Emit deletion event if m.eventBus != nil { m.eventBus.Emit(string(EventAICoderDeleted), AICoderEvent{ Type: string(EventAICoderDeleted), CoderID: coderID, CoderName: coder.Name, Status: string(StatusStopped), Message: "AI coder deleted", Time: time.Now(), }) } return nil } // UpdateCoderTask updates the task description for an AI coder func (m *AICoderManager) UpdateCoderTask(coderID string, task string) error { m.mu.Lock() defer m.mu.Unlock() coder, exists := m.coders[coderID] if !exists { return fmt.Errorf("AI coder %s not found", coderID) } coder.Task = task coder.UpdatedAt = time.Now() return nil } // RegisterProvider registers a new AI provider func (m *AICoderManager) RegisterProvider(name string, provider AIProvider) error { return m.providerReg.Register(name, provider) } // GetProviders returns a list of available providers func (m *AICoderManager) GetProviders() []string { return m.providerReg.List() } // GetCoderWorkspace returns the workspace directory for an AI coder func (m *AICoderManager) GetCoderWorkspace(coderID string) (string, error) { coder, exists := m.GetCoder(coderID) if !exists { return "", fmt.Errorf("AI coder %s not found", coderID) } return coder.WorkspaceDir, nil } // PTY Support Methods // GetPTYManager returns the PTY manager func (m *AICoderManager) GetPTYManager() *PTYManager { return m.ptyManager } // CreatePTYSession creates a new PTY session for an AI coder func (m *AICoderManager) CreatePTYSession(name, command string, args []string) (*PTYSession, error) { if m.ptyManager == nil { return nil, fmt.Errorf("PTY support not initialized") } return m.ptyManager.CreateSession(name, command, args) } // CreateInteractiveCLISession creates a PTY session for interactive CLI usage func (m *AICoderManager) CreateInteractiveCLISession(configKey string) (*PTYSession, error) { return m.CreateInteractiveCLISessionWithEnv(configKey, nil) } // CreateInteractiveCLISessionWithEnv creates a PTY session for interactive CLI usage with environment variables func (m *AICoderManager) CreateInteractiveCLISessionWithEnv(configKey string, extraEnv map[string]string) (*PTYSession, error) { if m.ptyManager == nil { return nil, fmt.Errorf("PTY support not initialized") } // Get CLI command configuration command, args, err := m.getCLICommandFromConfig(configKey, "", extraEnv) if err != nil { return nil, fmt.Errorf("failed to get CLI configuration: %w", err) } sessionName := fmt.Sprintf("%s-interactive", configKey) return m.ptyManager.CreateSessionWithEnv(sessionName, command, args, extraEnv) } // CreateTaskCLISession creates a PTY session for CLI with a specific task func (m *AICoderManager) CreateTaskCLISession(configKey, task string) (*PTYSession, error) { return m.CreateTaskCLISessionWithEnv(configKey, task, nil) } // CreateTaskCLISessionWithEnv creates a PTY session for CLI with a specific task and environment variables func (m *AICoderManager) CreateTaskCLISessionWithEnv(configKey, task string, extraEnv map[string]string) (*PTYSession, error) { if m.ptyManager == nil { return nil, fmt.Errorf("PTY support not initialized") } // Get CLI command configuration with task command, args, err := m.getCLICommandFromConfig(configKey, task, extraEnv) if err != nil { return nil, fmt.Errorf("failed to get CLI configuration: %w", err) } sessionName := fmt.Sprintf("%s-task", configKey) return m.ptyManager.CreateSessionWithEnv(sessionName, command, args, extraEnv) } // getCLICommandFromConfig retrieves CLI command and args from configuration func (m *AICoderManager) getCLICommandFromConfig(configKey string, task string, extraEnv map[string]string) (string, []string, error) { // Get provider configurations providerConfigs := m.config.GetProviderConfigs() // Look for provider configuration providerConfig, exists := providerConfigs[configKey] if !exists || providerConfig == nil || providerConfig.CLITool == nil { return "", nil, fmt.Errorf("CLI tool configuration not found for '%s'", configKey) } cliTool := providerConfig.CLITool // Get command command := cliTool.Command if command == "" { command = configKey // default to provider name } // Start with base args args := make([]string, len(cliTool.BaseArgs)) copy(args, cliTool.BaseArgs) // Perform variable replacement in args args = m.replaceVariables(args, extraEnv) // If task is provided, add it based on the tool's flag mapping if task != "" { // Check if there's a specific task/message/prompt flag in the mapping taskFlag := "" for _, key := range []string{"task", "message", "prompt"} { if flag, exists := cliTool.FlagMapping[key]; exists { taskFlag = flag break } } if taskFlag != "" { // Tools that use flags for tasks args = append(args, taskFlag, task) } else { // Tools that accept task as direct input (like claude) // For non-interactive mode with claude, use structured output if command == "claude" { args = append(args, "--print", "--verbose", "--output-format", "stream-json") } args = append(args, task) } } // Note: For interactive mode (no task), no special flags are added // This gives us the full SSH-like experience return command, args, nil } // replaceVariables replaces environment variable placeholders in arguments func (m *AICoderManager) replaceVariables(args []string, extraEnv map[string]string) []string { result := make([]string, len(args)) for i, arg := range args { // Replace ${VAR_NAME} patterns with environment variable values replaced := arg // First check extraEnv (has priority) if extraEnv != nil { for key, value := range extraEnv { placeholder := "${" + key + "}" replaced = strings.ReplaceAll(replaced, placeholder, value) } } // Then check system environment variables for _, env := range os.Environ() { pair := strings.SplitN(env, "=", 2) if len(pair) == 2 { placeholder := "${" + pair[0] + "}" replaced = strings.ReplaceAll(replaced, placeholder, pair[1]) } } result[i] = replaced } return result }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/standardbeagle/brummer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server