feat(config): add configurable agent ordering
This commit is contained in:
@@ -4,11 +4,12 @@
|
||||
|
||||
## CRITICAL: AGENT ORDERING
|
||||
|
||||
The canonical agent order is **sisyphus → hephaestus → prometheus → atlas**.
|
||||
The default agent order is **sisyphus → hephaestus → prometheus → atlas**. User config may override it with `agent_order`; omitted core agents fall back to this default order.
|
||||
|
||||
This order is enforced via two cooperating mechanisms:
|
||||
1. `CANONICAL_CORE_AGENT_ORDER` in `agent-priority-order.ts` controls object key insertion order in the agent map produced by `applyAgentConfig`.
|
||||
2. `installAgentSortShim()` in `src/shared/agent-sort-shim.ts` narrows `Array.prototype.toSorted` and `Array.prototype.sort` so that whenever the sorted array contains two or more agent objects whose `.name` matches a canonical core display name, OpenCode's `Agent.list()` (and any other sort site) returns the canonical order. The shim is installed once at plugin entry, before any agent registration.
|
||||
1. `DEFAULT_AGENT_ORDER` in `src/shared/agent-ordering.ts` supplies the fallback order used when `agent_order` is absent or incomplete.
|
||||
2. `reorderAgentsByPriority()` in `agent-priority-order.ts` controls object key insertion order in the agent map produced by `applyAgentConfig`.
|
||||
3. `installAgentSortShim()` in `src/shared/agent-sort-shim.ts` narrows `Array.prototype.toSorted` and `Array.prototype.sort` so that whenever the sorted array contains two or more ranked agent objects, OpenCode's `Agent.list()` (and any other sort site) returns the active configured/default order. The shim is installed once at plugin entry, before any agent registration, and its rank map is updated after plugin config loads.
|
||||
|
||||
### Why a Sort Shim
|
||||
|
||||
@@ -18,7 +19,7 @@ OpenCode 1.4.x sorts agents purely by `agent.name` via Remeda `sortBy`, which us
|
||||
- Removing the prefix and relying on insertion order alone falls back to alphabetical Atlas → Hephaestus → Prometheus → Sisyphus.
|
||||
|
||||
The sort shim resolves this by intercepting only the narrow case it cares about, with strict activation guards to prevent collateral damage from a global prototype patch:
|
||||
- The activation predicate (`isAgentArray`) requires `arr.length >= 2`, every element is a non-null object with a string `.name`, and at least 2 elements have a `.name` matching one of the four canonical core display names. This rejects mixed-type arrays (numbers, strings, plain objects without `.name`) so unrelated `.sort()` / `.toSorted()` calls execute native semantics.
|
||||
- The activation predicate (`isAgentArray`) requires `arr.length >= 2`, every element is a non-null object with a string `.name`, and at least 2 elements have a `.name` ranked by the active order. This rejects mixed-type arrays (numbers, strings, plain objects without `.name`) so unrelated `.sort()` / `.toSorted()` calls execute native semantics.
|
||||
- The comparator never throws on mixed input — it defensively extracts `.name` and falls back to the user-supplied `compareFn`.
|
||||
- `installAgentSortShim()` is idempotent.
|
||||
|
||||
@@ -34,7 +35,7 @@ Agent ordering has caused 15+ commits, 8+ PRs, and multiple reverts. Notable mil
|
||||
DO NOT introduce:
|
||||
- ZWSP, U+2060, U+00AD, ANSI escape, or any other invisible / control character in agent names, display names, or object keys.
|
||||
- ASCII spaces or other visible sort prefixes on agent names.
|
||||
- Alternative ordering constants outside `CANONICAL_CORE_AGENT_ORDER`.
|
||||
- Alternative ordering constants outside `DEFAULT_AGENT_ORDER` / `CANONICAL_CORE_AGENT_ORDER`, or ordering code that bypasses `validateAgentOrder`.
|
||||
- Object.entries() iteration-order dependencies.
|
||||
- Agent name string comparisons that skip `getAgentConfigKey` / `stripInvisibleAgentCharacters` (legacy ZWSP-baked data must keep resolving).
|
||||
|
||||
|
||||
@@ -395,6 +395,7 @@ export async function applyAgentConfig(params: {
|
||||
);
|
||||
params.config.agent = reorderAgentsByPriority(
|
||||
params.config.agent as Record<string, unknown>,
|
||||
params.pluginConfig.agent_order,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,48 @@ describe("agent-priority-order", () => {
|
||||
expect(keys[3]).toBe(atlas)
|
||||
})
|
||||
|
||||
test("#when custom agent order is provided #then follows configured core ordering", () => {
|
||||
// given
|
||||
const agents: Record<string, unknown> = {
|
||||
[atlas]: { name: "atlas" },
|
||||
[prometheus]: { name: "prometheus" },
|
||||
[hephaestus]: { name: "hephaestus" },
|
||||
[sisyphus]: { name: "sisyphus" },
|
||||
}
|
||||
|
||||
// when
|
||||
const result = reorderAgentsByPriority(agents, [
|
||||
"hephaestus",
|
||||
"sisyphus",
|
||||
"prometheus",
|
||||
"atlas",
|
||||
])
|
||||
|
||||
// then
|
||||
expect(Object.keys(result)).toEqual([hephaestus, sisyphus, prometheus, atlas])
|
||||
})
|
||||
|
||||
test("#when custom agent order contains invalid entries #then ignores them and keeps valid/default ordering", () => {
|
||||
// given
|
||||
const agents: Record<string, unknown> = {
|
||||
[atlas]: { name: "atlas" },
|
||||
[prometheus]: { name: "prometheus" },
|
||||
[hephaestus]: { name: "hephaestus" },
|
||||
[sisyphus]: { name: "sisyphus" },
|
||||
}
|
||||
|
||||
// when
|
||||
const result = reorderAgentsByPriority(agents, [
|
||||
"not-real",
|
||||
"atlas",
|
||||
"hephaestus",
|
||||
"atlas",
|
||||
])
|
||||
|
||||
// then
|
||||
expect(Object.keys(result)).toEqual([atlas, hephaestus, sisyphus, prometheus])
|
||||
})
|
||||
|
||||
test("#when core agents mixed with non-core #then core agents come first in canonical order", () => {
|
||||
// given: mixed order with non-core agents interleaved
|
||||
const agents: Record<string, unknown> = {
|
||||
@@ -199,6 +241,21 @@ describe("agent-priority-order", () => {
|
||||
expect(result[atlas]).toEqual({ name: "atlas", mode: "primary", order: 4 })
|
||||
})
|
||||
|
||||
test("#when custom agent order is provided #then injects matching order fields", () => {
|
||||
// given
|
||||
const agents: Record<string, unknown> = {
|
||||
[sisyphus]: { name: "sisyphus", mode: "primary" },
|
||||
[hephaestus]: { name: "hephaestus", mode: "primary" },
|
||||
}
|
||||
|
||||
// when
|
||||
const result = reorderAgentsByPriority(agents, ["hephaestus", "sisyphus"])
|
||||
|
||||
// then
|
||||
expect(result[hephaestus]).toEqual({ name: "hephaestus", mode: "primary", order: 1 })
|
||||
expect(result[sisyphus]).toEqual({ name: "sisyphus", mode: "primary", order: 2 })
|
||||
})
|
||||
|
||||
test("#when core agent is non-object #then leaves value unchanged", () => {
|
||||
// given
|
||||
const agents: Record<string, unknown> = {
|
||||
|
||||
@@ -1,35 +1,16 @@
|
||||
import { getAgentListDisplayName } from "../shared/agent-display-names"
|
||||
import { DEFAULT_AGENT_ORDER, resolveAgentOrderDisplayNames } from "../shared/agent-ordering"
|
||||
|
||||
/**
|
||||
* CRITICAL: This is the ONLY source of truth for core agent ordering.
|
||||
* The order is: sisyphus → hephaestus → prometheus → atlas
|
||||
* Default source of truth for core agent ordering.
|
||||
* The default order is: sisyphus → hephaestus → prometheus → atlas.
|
||||
*
|
||||
* DO NOT CHANGE THIS ORDER. Any PR attempting to modify this order
|
||||
* or introduce alternative ordering mechanisms (ZWSP prefixes, sort
|
||||
* shims, etc.) will be rejected.
|
||||
* User config may override the runtime order through `agent_order`; missing
|
||||
* core agents still fall back to this default order. Do not reintroduce sort
|
||||
* key prefixes or a second ordering constant.
|
||||
*
|
||||
* See: src/plugin-handlers/AGENTS.md for architectural context.
|
||||
*/
|
||||
export const CANONICAL_CORE_AGENT_ORDER = [
|
||||
"sisyphus",
|
||||
"hephaestus",
|
||||
"prometheus",
|
||||
"atlas",
|
||||
] as const
|
||||
|
||||
type CoreAgentName = (typeof CANONICAL_CORE_AGENT_ORDER)[number]
|
||||
|
||||
const CORE_AGENT_ORDER: ReadonlyArray<{
|
||||
configKey: CoreAgentName
|
||||
displayName: string
|
||||
order: number
|
||||
}> = CANONICAL_CORE_AGENT_ORDER.map((configKey, index) => ({
|
||||
configKey,
|
||||
displayName: getAgentListDisplayName(configKey),
|
||||
order: index + 1,
|
||||
}))
|
||||
|
||||
const CORE_DISPLAY_NAMES = new Set(CORE_AGENT_ORDER.map((a) => a.displayName))
|
||||
export const CANONICAL_CORE_AGENT_ORDER = DEFAULT_AGENT_ORDER
|
||||
|
||||
function injectOrderField(agentConfig: unknown, order: number): unknown {
|
||||
if (typeof agentConfig === "object" && agentConfig !== null) {
|
||||
@@ -40,13 +21,15 @@ function injectOrderField(agentConfig: unknown, order: number): unknown {
|
||||
|
||||
export function reorderAgentsByPriority(
|
||||
agents: Record<string, unknown>,
|
||||
agentOrder?: readonly string[],
|
||||
): Record<string, unknown> {
|
||||
const ordered: Record<string, unknown> = {}
|
||||
const seen = new Set<string>()
|
||||
const orderedDisplayNames = resolveAgentOrderDisplayNames(agentOrder)
|
||||
|
||||
for (const { displayName, order } of CORE_AGENT_ORDER) {
|
||||
for (const [index, displayName] of orderedDisplayNames.entries()) {
|
||||
if (Object.prototype.hasOwnProperty.call(agents, displayName)) {
|
||||
ordered[displayName] = injectOrderField(agents[displayName], order)
|
||||
ordered[displayName] = injectOrderField(agents[displayName], index + 1)
|
||||
seen.add(displayName)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user