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}