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
+95 -4
View File
@@ -3,6 +3,7 @@ package coderd
import (
"context"
"crypto/ed25519"
"crypto/tls"
"fmt"
"math"
"net/http"
@@ -15,6 +16,7 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/xerrors"
"tailscale.com/tailcfg"
@@ -100,6 +102,11 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
}
ctx, cancelFunc := context.WithCancel(ctx)
defer func() {
if err != nil {
cancelFunc()
}
}()
if options.ExternalTokenEncryption == nil {
options.ExternalTokenEncryption = make([]dbcrypt.Cipher, 0)
@@ -141,6 +148,33 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
)
}
meshTLSConfig, err := replicasync.CreateDERPMeshTLSConfig(options.AccessURL.Hostname(), options.TLSCertificates)
if err != nil {
return nil, xerrors.Errorf("create DERP mesh TLS config: %w", err)
}
var replicaManagerPtr atomic.Pointer[replicasync.Manager]
resolveReplicaAddress := func(
_ context.Context,
replicaID uuid.UUID,
) (string, bool) {
manager := replicaManagerPtr.Load()
if manager == nil {
return "", false
}
for _, replica := range manager.AllPrimary() {
if replica.ID != replicaID {
continue
}
relayAddress := strings.TrimSpace(replica.RelayAddress)
if relayAddress == "" {
return "", false
}
return relayAddress, true
}
return "", false
}
api := &API{
ctx: ctx,
cancel: cancelFunc,
@@ -156,6 +190,44 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
}
// This must happen before coderd initialization!
options.PostAuthAdditionalHeadersFunc = api.writeEntitlementWarningsHeader
// Wire up enterprise chat relay for cross-replica message_part streaming.
// Must be set before coderd.New so the chat processor gets it.
replicaHTTPClient := replicaRelayHTTPClient(options.HTTPClient, meshTLSConfig)
if replicaHTTPClient == nil {
replicaHTTPClient = options.Options.HTTPClient
}
if replicaHTTPClient == nil {
replicaHTTPClient = http.DefaultClient
}
// Use a closure that captures api by reference so it can access api.AGPL.ID
// after coderd.New is called. The provider is only invoked when Subscribe
// is called, which happens after initialization, so api.AGPL will be set.
options.Options.ChatRemotePartsProvider = func(
ctx context.Context,
chatID uuid.UUID,
workerID uuid.UUID,
requestHeader http.Header,
) (
[]codersdk.ChatStreamEvent,
<-chan codersdk.ChatStreamEvent,
func(),
error,
) {
// Get the replica ID from the API (will be set after coderd.New)
replicaID := api.AGPL.ID
if replicaID == uuid.Nil {
// Fallback if somehow called before initialization
replicaID = uuid.New()
}
provider := newRemotePartsProvider(
resolveReplicaAddress,
replicaHTTPClient,
replicaID,
)
return provider(ctx, chatID, workerID, requestHeader)
}
api.AGPL = coderd.New(options.Options)
defer func() {
if err != nil {
@@ -583,10 +655,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
})))
}
meshTLSConfig, err := replicasync.CreateDERPMeshTLSConfig(options.AccessURL.Hostname(), options.TLSCertificates)
if err != nil {
return nil, xerrors.Errorf("create DERP mesh TLS config: %w", err)
}
// We always want to run the replica manager even if we don't have DERP
// enabled, since it's used to detect other coder servers for licensing.
api.replicaManager, err = replicasync.New(ctx, options.Logger, options.Database, options.Pubsub, &replicasync.Options{
@@ -600,6 +668,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
if err != nil {
return nil, xerrors.Errorf("initialize replica: %w", err)
}
replicaManagerPtr.Store(api.replicaManager)
if api.DERPServer != nil {
api.derpMesh = derpmesh.New(options.Logger.Named("derpmesh"), api.DERPServer, meshTLSConfig)
}
@@ -651,6 +720,28 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
return api, nil
}
func replicaRelayHTTPClient(base *http.Client, tlsConfig *tls.Config) *http.Client {
if base == nil {
base = http.DefaultClient
}
clone := *base
var transport *http.Transport
switch t := base.Transport.(type) {
case *http.Transport:
transport = t.Clone()
default:
if defaultTransport, ok := http.DefaultTransport.(*http.Transport); ok {
transport = defaultTransport.Clone()
} else {
transport = &http.Transport{}
}
}
transport.TLSClientConfig = tlsConfig
clone.Transport = transport
return &clone
}
type Options struct {
*coderd.Options