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	sink := NewSink(ctx, a.conn, sess.ID)
 75	sink.Start(a.app.Messages, a.app.Permissions)
 76	a.sinks.Set(sess.ID, sink)
 77
 78	return acp.NewSessionResponse{
 79		SessionId: acp.SessionId(sess.ID),
 80	}, nil
 81}
 82
 83// SetSessionMode handles mode switching (stub - Crush doesn't have modes yet).
 84func (a *Agent) SetSessionMode(ctx context.Context, params acp.SetSessionModeRequest) (acp.SetSessionModeResponse, error) {
 85	slog.Debug("ACP SetSessionMode", "mode_id", params.ModeId)
 86	return acp.SetSessionModeResponse{}, nil
 87}
 88
 89// SetSessionModel handles model switching (stub - model selection not yet wired).
 90func (a *Agent) SetSessionModel(ctx context.Context, params acp.SetSessionModelRequest) (acp.SetSessionModelResponse, error) {
 91	slog.Debug("ACP SetSessionModel", "session_id", params.SessionId, "model_id", params.ModelId)
 92	return acp.SetSessionModelResponse{}, nil
 93}
 94
 95// Prompt handles a prompt request by running the agent.
 96func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
 97	slog.Info("ACP Prompt", "session_id", params.SessionId)
 98
 99	// Extract text from content blocks.
100	var prompt string
101	for _, block := range params.Prompt {
102		if block.Text != nil {
103			prompt += block.Text.Text
104		}
105	}
106
107	if prompt == "" {
108		return acp.PromptResponse{StopReason: acp.StopReasonEndTurn}, nil
109	}
110
111	// Run the agent.
112	_, err := a.app.AgentCoordinator.Run(ctx, string(params.SessionId), prompt)
113	if err != nil {
114		if ctx.Err() != nil {
115			return acp.PromptResponse{StopReason: acp.StopReasonCancelled}, nil
116		}
117		return acp.PromptResponse{}, err
118	}
119
120	return acp.PromptResponse{StopReason: acp.StopReasonEndTurn}, nil
121}
122
123// Cancel handles cancellation of an in-flight prompt.
124func (a *Agent) Cancel(ctx context.Context, params acp.CancelNotification) error {
125	slog.Info("ACP Cancel", "session_id", params.SessionId)
126	a.app.AgentCoordinator.Cancel(string(params.SessionId))
127	return nil
128}