package service import ( "log/slog" "codespace/internal/acp" "codespace/internal/model" "codespace/internal/process" "codespace/internal/workspace" ) // AcpService wraps the low-level acp.Service with logging and model mapping. type AcpService struct { svc acp.Service lg *slog.Logger } // NewAcpService creates an AcpService. // If lg is nil, slog.Default() is used. func NewAcpService(processes process.Manager, workspaces workspace.Manager, lg *slog.Logger) *AcpService { if lg == nil { lg = slog.Default() } return &AcpService{svc: acp.NewService(processes, workspaces, lg), lg: lg} } // Status returns the ACP status for the workspace. func (s *AcpService) Status(workspaceID string) (model.AcpStatusResponse, error) { status, err := s.svc.Status(workspaceID) if err != nil { s.lg.Error("acp status failed", "workspace_id", workspaceID, "error", err) return model.AcpStatusResponse{}, err } return model.AcpStatusResponse{ WorkspaceID: status.WorkspaceID, Ready: status.Ready, SessionID: status.SessionID, Running: status.Running, PID: status.PID, Error: status.Error, }, nil } // History returns the ACP conversation history for the workspace. func (s *AcpService) History(workspaceID string) (model.AcpHistoryResponse, error) { hist, err := s.svc.History(workspaceID) if err != nil { s.lg.Error("acp history failed", "workspace_id", workspaceID, "error", err) return model.AcpHistoryResponse{}, err } messages := make([]model.AcpMessage, len(hist.Messages)) for i, m := range hist.Messages { messages[i] = model.AcpMessage{Role: m.Role, Text: m.Text, Time: m.Time, MessageID: m.MessageID} } return model.AcpHistoryResponse{SessionID: hist.SessionID, Messages: messages}, nil } // Prompt sends a prompt to the agent and returns its response. func (s *AcpService) Prompt(workspaceID string, content string) (model.AcpPromptResponse, error) { res, err := s.svc.Prompt(workspaceID, content) if err != nil { s.lg.Error("acp prompt failed", "workspace_id", workspaceID, "error", err) return model.AcpPromptResponse{}, err } return model.AcpPromptResponse{ SessionID: res.SessionID, StopReason: res.StopReason, Text: res.Text, }, nil } // Cancel interrupts an in-flight prompt. func (s *AcpService) Cancel(workspaceID string) error { if err := s.svc.Cancel(workspaceID); err != nil { s.lg.Error("acp cancel failed", "workspace_id", workspaceID, "error", err) return err } return nil } // Stream sends a prompt to the agent and forwards streamed events to out. func (s *AcpService) Stream(workspaceID string, content string, out chan<- model.AcpStreamEvent) error { acpOut := make(chan acp.StreamEvent, 32) var streamErr error done := make(chan struct{}) go func() { defer close(done) streamErr = s.svc.Stream(workspaceID, content, acpOut) close(acpOut) }() for ev := range acpOut { out <- model.AcpStreamEvent{ Type: ev.Type, MessageID: ev.MessageID, Text: ev.Text, StopReason: ev.StopReason, Error: ev.Error, } } <-done if streamErr != nil { s.lg.Error("acp stream failed", "workspace_id", workspaceID, "error", streamErr) } return streamErr } // EnsureReady ensures the ACP process is started and initialized. func (s *AcpService) EnsureReady(workspaceID string) error { if err := s.svc.EnsureReady(workspaceID); err != nil { s.lg.Error("acp ensure ready failed", "workspace_id", workspaceID, "error", err) return err } return nil }