feat: add experimental agents support (#22290)

feat: add AI chat system with agent tools and chat UI

Introduce the chatd subsystem and Agents UI for AI-powered chat
within Coder workspaces.

- Add chatd package with chat loop, message compaction, prompt
  management, and LLM provider integration (OpenAI, Anthropic)
- Add agent tools: create workspace, list/read templates, read/write/
  edit files, execute commands
- Add chat API endpoints with streaming, message editing, and
  durable reconnection
- Add database schema and migrations for chats, chat messages, chat
  providers, and chat model configs
- Add RBAC policies and dbauthz enforcement for chat resources
- Add Agents UI pages with conversation timeline, queued messages
  list, diff viewer, and model configuration panel
- Add comprehensive test coverage including coderd integration tests,
  chatd unit tests, and Storybook stories
- Gate feature behind experiments flag

---------

Co-authored-by: Cian Johnston <cian@coder.com>
Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
Co-authored-by: Jeremy Ruppel <jeremy@coder.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle Carberry
2026-02-27 11:50:56 -05:00
committed by GitHub
parent 67da4e8b56
commit edee917d88
201 changed files with 44828 additions and 1859 deletions
+62 -2
View File
@@ -49,6 +49,7 @@ import (
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/awsidentity"
"github.com/coder/coder/v2/coderd/boundaryusage"
"github.com/coder/coder/v2/coderd/chatd"
"github.com/coder/coder/v2/coderd/connectionlog"
"github.com/coder/coder/v2/coderd/cryptokeys"
"github.com/coder/coder/v2/coderd/database"
@@ -238,6 +239,9 @@ type Options struct {
SSHConfig codersdk.SSHConfigResponse
HTTPClient *http.Client
// ChatRemotePartsProvider provides cross-replica message_part streaming.
// Set by enterprise for HA deployments. Nil in AGPL single-replica.
ChatRemotePartsProvider chatd.RemotePartsProvider
UpdateAgentMetrics func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric)
StatsBatcher workspacestats.Batcher
@@ -588,7 +592,6 @@ func New(options *Options) *API {
var buildUsageChecker atomic.Pointer[wsbuilder.UsageChecker]
var noopUsageChecker wsbuilder.UsageChecker = wsbuilder.NoopUsageChecker{}
buildUsageChecker.Store(&noopUsageChecker)
api := &API{
ctx: ctx,
cancel: cancel,
@@ -754,6 +757,17 @@ func New(options *Options) *API {
panic("failed to setup server tailnet: " + err.Error())
}
api.agentProvider = stn
api.chatDaemon = chatd.New(chatd.Config{
Logger: options.Logger.Named("chats"),
Database: options.Database,
ReplicaID: api.ID,
RemotePartsProvider: options.ChatRemotePartsProvider,
ProviderAPIKeys: chatProviderAPIKeysFromDeploymentValues(options.DeploymentValues),
AgentConn: api.agentProvider.AgentConn,
CreateWorkspace: api.chatCreateWorkspace,
Pubsub: options.Pubsub,
})
if options.DeploymentValues.Prometheus.Enable {
options.PrometheusRegistry.MustRegister(stn)
api.lifecycleMetrics = agentapi.NewLifecycleMetrics(options.PrometheusRegistry)
@@ -1085,6 +1099,48 @@ func New(options *Options) *API {
})
})
})
r.Route("/chats", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentAgents),
)
r.Get("/", api.listChats)
r.Post("/", api.postChats)
r.Get("/models", api.listChatModels)
r.Get("/watch", api.watchChats)
r.Route("/providers", func(r chi.Router) {
r.Get("/", api.listChatProviders)
r.Post("/", api.createChatProvider)
r.Route("/{providerConfig}", func(r chi.Router) {
r.Patch("/", api.updateChatProvider)
r.Delete("/", api.deleteChatProvider)
})
})
r.Route("/model-configs", func(r chi.Router) {
r.Get("/", api.listChatModelConfigs)
r.Post("/", api.createChatModelConfig)
r.Route("/{modelConfig}", func(r chi.Router) {
r.Patch("/", api.updateChatModelConfig)
r.Delete("/", api.deleteChatModelConfig)
})
})
r.Route("/{chat}", func(r chi.Router) {
r.Use(httpmw.ExtractChatParam(options.Database))
r.Get("/", api.getChat)
r.Delete("/", api.deleteChat)
r.Post("/messages", api.postChatMessages)
r.Patch("/messages/{message}", api.patchChatMessage)
r.Get("/stream", api.streamChat)
r.Post("/interrupt", api.interruptChat)
r.Get("/diff-status", api.getChatDiffStatus)
r.Get("/diff", api.getChatDiffContents)
r.Route("/queue/{queuedMessage}", func(r chi.Router) {
r.Delete("/", api.deleteChatQueuedMessage)
r.Post("/promote", api.promoteChatQueuedMessage)
})
})
})
r.Route("/mcp", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
@@ -1902,6 +1958,8 @@ type API struct {
// dbRolluper rolls up template usage stats from raw agent and app
// stats. This is used to provide insights in the WebUI.
dbRolluper *dbrollup.Rolluper
// chatDaemon handles background processing of pending chats.
chatDaemon *chatd.Server
}
// Close waits for all WebSocket connections to drain before returning.
@@ -1930,8 +1988,10 @@ func (api *API) Close() error {
case <-timer.C:
api.Logger.Warn(api.ctx, "websocket shutdown timed out after 10 seconds")
}
api.dbRolluper.Close()
if err := api.chatDaemon.Close(); err != nil {
api.Logger.Warn(api.ctx, "close chat processor", slog.Error(err))
}
api.metricsCache.Close()
if api.updateChecker != nil {
api.updateChecker.Close()