feat(config): add configurable agent ordering

This commit is contained in:
YeonGyu-Kim
2026-05-08 16:08:18 +09:00
parent 8667d7e5b8
commit 9522dd4ca4
16 changed files with 400 additions and 57 deletions
+61
View File
@@ -0,0 +1,61 @@
import { AGENT_DISPLAY_NAMES, getAgentConfigKey, getAgentListDisplayName } from "./agent-display-names"
export const DEFAULT_AGENT_ORDER = [
"sisyphus",
"hephaestus",
"prometheus",
"atlas",
] as const
export type AgentOrderValidation = {
order: string[]
invalid: string[]
duplicates: string[]
}
const KNOWN_AGENT_KEYS = new Set(Object.keys(AGENT_DISPLAY_NAMES))
function appendUnique(target: string[], value: string): void {
if (!target.includes(value)) {
target.push(value)
}
}
export function validateAgentOrder(agentOrder: readonly string[] | undefined): AgentOrderValidation {
const order: string[] = []
const invalid: string[] = []
const duplicates: string[] = []
const seen = new Set<string>()
for (const rawName of agentOrder ?? []) {
const trimmed = rawName.trim()
if (trimmed.length === 0) {
invalid.push(rawName)
continue
}
const configKey = getAgentConfigKey(trimmed)
if (!KNOWN_AGENT_KEYS.has(configKey)) {
invalid.push(rawName)
continue
}
if (seen.has(configKey)) {
duplicates.push(rawName)
continue
}
seen.add(configKey)
order.push(configKey)
}
for (const configKey of DEFAULT_AGENT_ORDER) {
appendUnique(order, configKey)
}
return { order, invalid, duplicates }
}
export function resolveAgentOrderDisplayNames(agentOrder: readonly string[] | undefined): string[] {
return validateAgentOrder(agentOrder).order.map((configKey) => getAgentListDisplayName(configKey))
}
+29 -2
View File
@@ -1,8 +1,8 @@
/// <reference types="bun-types" />
import { beforeAll, describe, expect, test } from "bun:test"
import { afterEach, beforeAll, describe, expect, test } from "bun:test"
import { installAgentSortShim } from "./agent-sort-shim"
import { installAgentSortShim, setAgentSortOrder } from "./agent-sort-shim"
import { AGENT_DISPLAY_NAMES } from "./agent-display-names"
type AgentListItem = {
@@ -10,15 +10,26 @@ type AgentListItem = {
default_agent?: boolean
}
declare global {
interface Array<T> {
toSorted(compareFn?: (a: T, b: T) => number): T[]
}
}
describe("agent-sort-shim", () => {
beforeAll(() => {
installAgentSortShim()
})
afterEach(() => {
setAgentSortOrder(undefined)
})
describe("#given an array of all 4 core agent objects in random order", () => {
describe("#when toSorted with alphabetical compareFn", () => {
test("#then returns canonical sisyphus->hephaestus->prometheus->atlas order", () => {
// given
setAgentSortOrder(undefined)
const sisyphus = { name: "Sisyphus - Ultraworker" }
const hephaestus = { name: "Hephaestus - Deep Agent" }
const prometheus = { name: "Prometheus - Plan Builder" }
@@ -31,6 +42,22 @@ describe("agent-sort-shim", () => {
// then
expect(result).toEqual([sisyphus, hephaestus, prometheus, atlas])
})
test("#then follows configured core agent order", () => {
// given
setAgentSortOrder(["hephaestus", "sisyphus", "prometheus", "atlas"])
const sisyphus = { name: "Sisyphus - Ultraworker" }
const hephaestus = { name: "Hephaestus - Deep Agent" }
const prometheus = { name: "Prometheus - Plan Builder" }
const atlas = { name: "Atlas - Plan Executor" }
const input = [atlas, prometheus, hephaestus, sisyphus]
// when
const result = input.toSorted((a, b) => a.name.localeCompare(b.name))
// then
expect(result).toEqual([hephaestus, sisyphus, prometheus, atlas])
})
})
})
+27 -17
View File
@@ -3,10 +3,9 @@
*
* OpenCode 1.4.x ignores the agent `order` field (sst/opencode#19127) and
* sorts the agent list by `agent.name` via Remeda `sortBy(x => x.name, "asc")`
* at packages/opencode/src/agent/agent.ts. Without intervention, the four
* core agents collapse into Atlas -> Hephaestus -> Prometheus -> Sisyphus,
* which inverts the canonical sisyphus -> hephaestus -> prometheus -> atlas
* order this project ships.
* at packages/opencode/src/agent/agent.ts. Without intervention, core agents
* collapse into name order, which can invert the default sisyphus -> hephaestus
* -> prometheus -> atlas order or a user's configured `agent_order`.
*
* Earlier attempts to bias the sort key with invisible characters (ZWSP,
* U+2060 WORD JOINER, U+00AD SOFT HYPHEN, ANSI escape) caused visible-gap
@@ -17,22 +16,21 @@
* 1. `isAgentArray` rejects any array element that is null, non-object, or
* lacks a string `name`, eliminating the throw-on-mixed-array failure
* mode that closed the original PR.
* 2. The activation predicate requires >= 2 elements whose `.name` is one
* of the four canonical core display names, so unrelated `.sort()` and
* `.toSorted()` calls (string arrays, number arrays, generic objects)
* execute native behavior unchanged.
* 2. The activation predicate requires >= 2 elements whose `.name` is ranked
* by the active agent order, so unrelated `.sort()` and `.toSorted()` calls
* (string arrays, number arrays, generic objects) execute native behavior
* unchanged.
*
* Remove this shim once OpenCode honors the agent `order` field
* (sst/opencode#19127).
*/
import { CANONICAL_CORE_AGENT_ORDER } from "../plugin-handlers/agent-priority-order"
import { AGENT_DISPLAY_NAMES } from "./agent-display-names"
import { DEFAULT_AGENT_ORDER, resolveAgentOrderDisplayNames } from "./agent-ordering"
import { getAgentListDisplayName } from "./agent-display-names"
const AGENT_RANK: ReadonlyMap<string, number> = new Map(
CANONICAL_CORE_AGENT_ORDER.map(
(configKey, index): [string, number] => [AGENT_DISPLAY_NAMES[configKey], index + 1],
),
let agentRank: ReadonlyMap<string, number> = createAgentRank(undefined)
const AGENT_ARRAY_SENTINELS = new Set(
DEFAULT_AGENT_ORDER.map((configKey) => getAgentListDisplayName(configKey)),
)
const UNRANKED = Number.MAX_SAFE_INTEGER
@@ -51,7 +49,7 @@ function isAgentArray(arr: ReadonlyArray<unknown>): boolean {
if (element === null || typeof element !== "object") return false
const name = (element as { name?: unknown }).name
if (typeof name !== "string") return false
if (AGENT_RANK.has(name)) rankedCount++
if (AGENT_ARRAY_SENTINELS.has(name)) rankedCount++
}
return rankedCount >= 2
@@ -62,8 +60,8 @@ function agentComparator(
b: unknown,
fallback: ((a: unknown, b: unknown) => number) | undefined,
): number {
const aRank = AGENT_RANK.get(extractAgentName(a)) ?? UNRANKED
const bRank = AGENT_RANK.get(extractAgentName(b)) ?? UNRANKED
const aRank = agentRank.get(extractAgentName(a)) ?? UNRANKED
const bRank = agentRank.get(extractAgentName(b)) ?? UNRANKED
if (aRank !== bRank) return aRank - bRank
if (fallback) return fallback(a, b)
@@ -72,6 +70,18 @@ function agentComparator(
let installed = false
function createAgentRank(agentOrder: readonly string[] | undefined): ReadonlyMap<string, number> {
return new Map(
resolveAgentOrderDisplayNames(agentOrder).map(
(displayName, index): [string, number] => [displayName, index + 1],
),
)
}
export function setAgentSortOrder(agentOrder: readonly string[] | undefined): void {
agentRank = createAgentRank(agentOrder)
}
export function installAgentSortShim(): void {
if (installed) return