1package backend
2
3import (
4 "context"
5 "errors"
6
7 "github.com/charmbracelet/crush/internal/agent"
8 "github.com/charmbracelet/crush/internal/agent/notify"
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/proto"
11 "github.com/charmbracelet/crush/internal/pubsub"
12)
13
14// SendMessage validates and accepts a prompt for the workspace's agent,
15// then dispatches the run on a goroutine bound to the workspace context
16// and returns immediately. It does not wait for the LLM turn to
17// complete: the run's lifetime is owned by the workspace, not by the
18// caller. Errors from the dispatched run reach observers through the
19// agent event channels (a notify.TypeAgentError notification), not
20// through this return value.
21//
22// SendMessage returns synchronously when the request cannot be accepted:
23// ErrWorkspaceNotFound if the workspace is missing, ErrAgentNotInitialized
24// if its coordinator is nil, the structural validation errors from
25// agent.ValidateCall (ErrEmptyPrompt, ErrSessionMissing) when the prompt
26// or session is missing, and ErrWorkspaceClosing if the workspace is
27// being torn down.
28func (b *Backend) SendMessage(workspaceID string, msg proto.AgentMessage) error {
29 ws, err := b.GetWorkspace(workspaceID)
30 if err != nil {
31 return err
32 }
33
34 if ws.AgentCoordinator == nil {
35 return ErrAgentNotInitialized
36 }
37
38 if err := agent.ValidateCall(agent.SessionAgentCall{
39 SessionID: msg.SessionID,
40 Prompt: msg.Prompt,
41 Attachments: proto.AttachmentsToMessage(msg.Attachments),
42 }); err != nil {
43 return err
44 }
45
46 accept := ws.AgentCoordinator.BeginAccepted(msg.SessionID)
47
48 ws.runMu.Lock()
49 if ws.closing {
50 ws.runMu.Unlock()
51 accept.Close()
52 return ErrWorkspaceClosing
53 }
54 ws.runWG.Add(1)
55 ws.runMu.Unlock()
56
57 go b.runAgent(ws, msg, accept)
58 return nil
59}
60
61// runAgent executes an accepted agent run for the workspace. It owns the
62// accept reservation (releasing it on return) and the runWG ticket added
63// by SendMessage. The run is bound to the workspace context so its
64// lifetime is independent of any client's HTTP request. On a non-cancel
65// error it surfaces the failure to observers via a notify.TypeAgentError
66// notification; context.Canceled is expected (the FinishReasonCanceled
67// marker is already published by sessionAgent.Run) and swallowed.
68//
69// When msg.RunID is non-empty it is attached to the context via
70// agent.WithRunID so the coordinator can stamp the terminal
71// notify.RunComplete event with that correlator.
72func (b *Backend) runAgent(ws *Workspace, msg proto.AgentMessage, accept *agent.AcceptedRun) {
73 defer ws.runWG.Done()
74 defer accept.Close()
75
76 ctx := ws.ctx
77 if msg.RunID != "" {
78 ctx = agent.WithRunID(ctx, msg.RunID)
79 }
80
81 _, err := ws.AgentCoordinator.RunAccepted(ctx, accept, msg.SessionID, msg.Prompt, proto.AttachmentsToMessage(msg.Attachments)...)
82 if err == nil || errors.Is(err, context.Canceled) {
83 return
84 }
85
86 ws.AgentNotifications().Publish(pubsub.CreatedEvent, notify.Notification{
87 SessionID: msg.SessionID,
88 Type: notify.TypeAgentError,
89 Message: err.Error(),
90 })
91}
92
93// GetAgentInfo returns the agent's model and busy status.
94func (b *Backend) GetAgentInfo(workspaceID string) (proto.AgentInfo, error) {
95 ws, err := b.GetWorkspace(workspaceID)
96 if err != nil {
97 return proto.AgentInfo{}, err
98 }
99
100 var agentInfo proto.AgentInfo
101 if ws.AgentCoordinator != nil {
102 m := ws.AgentCoordinator.Model()
103 agentInfo = proto.AgentInfo{
104 Model: m.CatwalkCfg,
105 ModelCfg: m.ModelCfg,
106 IsBusy: ws.AgentCoordinator.IsBusy(),
107 IsReady: true,
108 }
109 }
110 return agentInfo, nil
111}
112
113// InitAgent initializes the coder agent for the workspace.
114func (b *Backend) InitAgent(ctx context.Context, workspaceID string) error {
115 ws, err := b.GetWorkspace(workspaceID)
116 if err != nil {
117 return err
118 }
119
120 return ws.InitCoderAgent(ctx)
121}
122
123// UpdateAgent reloads the agent model configuration.
124func (b *Backend) UpdateAgent(ctx context.Context, workspaceID string) error {
125 ws, err := b.GetWorkspace(workspaceID)
126 if err != nil {
127 return err
128 }
129
130 return ws.UpdateAgentModel(ctx)
131}
132
133// CancelSession cancels an ongoing agent operation for the given
134// session.
135func (b *Backend) CancelSession(workspaceID, sessionID string) error {
136 ws, err := b.GetWorkspace(workspaceID)
137 if err != nil {
138 return err
139 }
140
141 if ws.AgentCoordinator != nil {
142 ws.AgentCoordinator.Cancel(sessionID)
143 }
144 return nil
145}
146
147// SummarizeSession triggers a session summarization.
148func (b *Backend) SummarizeSession(ctx context.Context, workspaceID, sessionID string) error {
149 ws, err := b.GetWorkspace(workspaceID)
150 if err != nil {
151 return err
152 }
153
154 if ws.AgentCoordinator == nil {
155 return ErrAgentNotInitialized
156 }
157
158 return ws.AgentCoordinator.Summarize(ctx, sessionID)
159}
160
161// QueuedPrompts returns the number of queued prompts for the session.
162func (b *Backend) QueuedPrompts(workspaceID, sessionID string) (int, error) {
163 ws, err := b.GetWorkspace(workspaceID)
164 if err != nil {
165 return 0, err
166 }
167
168 if ws.AgentCoordinator == nil {
169 return 0, nil
170 }
171
172 return ws.AgentCoordinator.QueuedPrompts(sessionID), nil
173}
174
175// ClearQueue clears the prompt queue for the session.
176func (b *Backend) ClearQueue(workspaceID, sessionID string) error {
177 ws, err := b.GetWorkspace(workspaceID)
178 if err != nil {
179 return err
180 }
181
182 if ws.AgentCoordinator != nil {
183 ws.AgentCoordinator.ClearQueue(sessionID)
184 }
185 return nil
186}
187
188// QueuedPromptsList returns the list of queued prompt strings for a
189// session.
190func (b *Backend) QueuedPromptsList(workspaceID, sessionID string) ([]string, error) {
191 ws, err := b.GetWorkspace(workspaceID)
192 if err != nil {
193 return nil, err
194 }
195
196 if ws.AgentCoordinator == nil {
197 return nil, nil
198 }
199
200 return ws.AgentCoordinator.QueuedPromptsList(sessionID), nil
201}
202
203// GetDefaultSmallModel returns the default small model for a provider.
204func (b *Backend) GetDefaultSmallModel(workspaceID, providerID string) (config.SelectedModel, error) {
205 ws, err := b.GetWorkspace(workspaceID)
206 if err != nil {
207 return config.SelectedModel{}, err
208 }
209
210 return ws.GetDefaultSmallModel(providerID), nil
211}