agent.go

  1package acp
  2
  3import (
  4	"context"
  5	"log/slog"
  6
  7	"github.com/charmbracelet/crush/internal/app"
  8	"github.com/charmbracelet/crush/internal/csync"
  9	"github.com/coder/acp-go-sdk"
 10)
 11
 12// Agent implements the acp.Agent interface to handle ACP protocol methods.
 13type Agent struct {
 14	app   *app.App
 15	conn  *acp.AgentSideConnection
 16	sinks *csync.Map[string, *Sink]
 17}
 18
 19// Compile-time interface checks.
 20var (
 21	_ acp.Agent             = (*Agent)(nil)
 22	_ acp.AgentExperimental = (*Agent)(nil)
 23)
 24
 25// NewAgent creates a new ACP agent backed by a Crush app instance.
 26func NewAgent(app *app.App) *Agent {
 27	return &Agent{
 28		app:   app,
 29		sinks: csync.NewMap[string, *Sink](),
 30	}
 31}
 32
 33// SetAgentConnection stores the connection for sending notifications.
 34func (a *Agent) SetAgentConnection(conn *acp.AgentSideConnection) {
 35	a.conn = conn
 36}
 37
 38// Initialize handles the ACP initialize request.
 39func (a *Agent) Initialize(ctx context.Context, params acp.InitializeRequest) (acp.InitializeResponse, error) {
 40	slog.Debug("ACP Initialize", "protocol_version", params.ProtocolVersion)
 41	return acp.InitializeResponse{
 42		ProtocolVersion: acp.ProtocolVersionNumber,
 43		AgentCapabilities: acp.AgentCapabilities{
 44			LoadSession: false,
 45			McpCapabilities: acp.McpCapabilities{
 46				Http: false,
 47				Sse:  false,
 48			},
 49			PromptCapabilities: acp.PromptCapabilities{
 50				EmbeddedContext: true,
 51				Audio:           false,
 52				Image:           false,
 53			},
 54		},
 55	}, nil
 56}
 57
 58// Authenticate handles authentication requests (stub for local stdio).
 59func (a *Agent) Authenticate(ctx context.Context, params acp.AuthenticateRequest) (acp.AuthenticateResponse, error) {
 60	slog.Debug("ACP Authenticate")
 61	return acp.AuthenticateResponse{}, nil
 62}
 63
 64// NewSession creates a new Crush session.
 65func (a *Agent) NewSession(ctx context.Context, params acp.NewSessionRequest) (acp.NewSessionResponse, error) {
 66	slog.Info("ACP NewSession", "cwd", params.Cwd)
 67
 68	sess, err := a.app.Sessions.Create(ctx, "ACP Session")
 69	if err != nil {
 70		return acp.NewSessionResponse{}, err
 71	}
 72
 73	// Create and start the event sink to stream updates to this session.
 74	// Use a background context since the sink needs to outlive the NewSession
 75	// request.
 76	sink := NewSink(context.Background(), a.conn, sess.ID)
 77	sink.Start(a.app.Messages, a.app.Permissions, a.app.Sessions)
 78	a.sinks.Set(sess.ID, sink)
 79
 80	return acp.NewSessionResponse{
 81		SessionId: acp.SessionId(sess.ID),
 82	}, nil
 83}
 84
 85// SetSessionMode handles mode switching (stub - Crush doesn't have modes yet).
 86func (a *Agent) SetSessionMode(ctx context.Context, params acp.SetSessionModeRequest) (acp.SetSessionModeResponse, error) {
 87	slog.Debug("ACP SetSessionMode", "mode_id", params.ModeId)
 88	return acp.SetSessionModeResponse{}, nil
 89}
 90
 91// SetSessionModel handles model switching (stub - model selection not yet wired).
 92func (a *Agent) SetSessionModel(ctx context.Context, params acp.SetSessionModelRequest) (acp.SetSessionModelResponse, error) {
 93	slog.Debug("ACP SetSessionModel", "session_id", params.SessionId, "model_id", params.ModelId)
 94	return acp.SetSessionModelResponse{}, nil
 95}
 96
 97// Prompt handles a prompt request by running the agent.
 98func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
 99	slog.Info("ACP Prompt", "session_id", params.SessionId)
100
101	// Extract text from content blocks.
102	var prompt string
103	for _, block := range params.Prompt {
104		if block.Text != nil {
105			prompt += block.Text.Text
106		}
107	}
108
109	if prompt == "" {
110		return acp.PromptResponse{StopReason: acp.StopReasonEndTurn}, nil
111	}
112
113	// Run the agent.
114	_, err := a.app.AgentCoordinator.Run(ctx, string(params.SessionId), prompt)
115	if err != nil {
116		if ctx.Err() != nil {
117			return acp.PromptResponse{StopReason: acp.StopReasonCancelled}, nil
118		}
119		return acp.PromptResponse{}, err
120	}
121
122	return acp.PromptResponse{StopReason: acp.StopReasonEndTurn}, nil
123}
124
125// Cancel handles cancellation of an in-flight prompt.
126func (a *Agent) Cancel(ctx context.Context, params acp.CancelNotification) error {
127	slog.Info("ACP Cancel", "session_id", params.SessionId)
128	a.app.AgentCoordinator.Cancel(string(params.SessionId))
129	return nil
130}