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}