mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
73f6cd8169
Adds suffix-based agent selection for chatd. Template authors can direct chat traffic to a specific root workspace agent by naming it with the `-coderd-chat` suffix (for example, `coder_agent "dev-coderd-chat"`). When no suffix match exists, chatd falls back to the first root agent by `DisplayOrder`, then `Name`. Multiple suffix matches return an error. The selection logic lives in `coderd/x/chatd/internal/agentselect` and is shared by chatd core plus the workspace chat tools so all chat entry points pick the same agent deterministically. No database migrations, API contract changes, or provider changes. The experimental sandbox template was split out to #23777.
87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
package agentselect
|
|
|
|
import (
|
|
"cmp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
)
|
|
|
|
// Suffix marks chat-designated agents during the current PoC. This naming
|
|
// convention is an implementation detail, not a stable contract.
|
|
const Suffix = "-coderd-chat"
|
|
|
|
// IsChatAgent reports whether name uses the chat-agent suffix convention.
|
|
func IsChatAgent(name string) bool {
|
|
return strings.HasSuffix(strings.ToLower(name), Suffix)
|
|
}
|
|
|
|
// FindChatAgent picks the best workspace agent for a chat session from the
|
|
// provided candidates. It applies these rules in order:
|
|
// 1. Filter to root agents only (ParentID is null).
|
|
// 2. Sort stably and deterministically by DisplayOrder ASC, then Name ASC
|
|
// (case-insensitive), then Name ASC, then ID ASC.
|
|
// 3. If exactly one root agent name ends with Suffix (case-insensitive),
|
|
// return it.
|
|
// 4. If zero root agents match the suffix, return the first root agent after
|
|
// sorting (deterministic fallback).
|
|
// 5. If more than one root agent matches the suffix, return an error with an
|
|
// actionable message.
|
|
// 6. If no root agents exist at all, return an error.
|
|
func FindChatAgent(
|
|
agents []database.WorkspaceAgent,
|
|
) (database.WorkspaceAgent, error) {
|
|
rootAgents := make([]database.WorkspaceAgent, 0, len(agents))
|
|
matchingAgents := make([]database.WorkspaceAgent, 0, 1)
|
|
for _, agent := range agents {
|
|
if agent.ParentID.Valid {
|
|
continue
|
|
}
|
|
rootAgents = append(rootAgents, agent)
|
|
if IsChatAgent(agent.Name) {
|
|
matchingAgents = append(matchingAgents, agent)
|
|
}
|
|
}
|
|
|
|
if len(rootAgents) == 0 {
|
|
return database.WorkspaceAgent{}, xerrors.New(
|
|
"no eligible workspace agents found",
|
|
)
|
|
}
|
|
|
|
compareAgents := func(a, b database.WorkspaceAgent) int {
|
|
if order := cmp.Compare(a.DisplayOrder, b.DisplayOrder); order != 0 {
|
|
return order
|
|
}
|
|
if order := cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)); order != 0 {
|
|
return order
|
|
}
|
|
if order := cmp.Compare(a.Name, b.Name); order != 0 {
|
|
return order
|
|
}
|
|
return cmp.Compare(a.ID.String(), b.ID.String())
|
|
}
|
|
slices.SortStableFunc(rootAgents, compareAgents)
|
|
slices.SortStableFunc(matchingAgents, compareAgents)
|
|
|
|
switch len(matchingAgents) {
|
|
case 0:
|
|
return rootAgents[0], nil
|
|
case 1:
|
|
return matchingAgents[0], nil
|
|
default:
|
|
names := make([]string, 0, len(matchingAgents))
|
|
for _, agent := range matchingAgents {
|
|
names = append(names, agent.Name)
|
|
}
|
|
return database.WorkspaceAgent{}, xerrors.Errorf(
|
|
"multiple agents match the chat suffix %q: %s; only one agent should use this suffix",
|
|
Suffix,
|
|
strings.Join(names, ", "),
|
|
)
|
|
}
|
|
}
|