From 2dbcd79fd211513b9acfafac0e2963f26424ba78 Mon Sep 17 00:00:00 2001 From: jorge g Date: Thu, 19 Mar 2026 16:04:03 -0400 Subject: [PATCH] fix: stabilize agent and skill ordering in prompt descriptions (#18261) Co-authored-by: Aiden Cline Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/agent/agent.ts | 5 +- packages/opencode/src/skill/skill.ts | 2 +- packages/opencode/src/tool/task.ts | 7 ++- packages/opencode/test/agent/agent.test.ts | 26 ++++++++ packages/opencode/test/session/system.test.ts | 59 +++++++++++++++++++ packages/opencode/test/tool/skill.test.ts | 50 ++++++++++++++++ packages/opencode/test/tool/task.test.ts | 45 ++++++++++++++ 7 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 packages/opencode/test/session/system.test.ts create mode 100644 packages/opencode/test/tool/task.test.ts diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index b2dae0402..e30d05e93 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -260,7 +260,10 @@ export namespace Agent { return pipe( await state(), values(), - sortBy([(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"]), + sortBy( + [(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"], + [(x) => x.name, "asc"], + ), ) } diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index d7aeb911f..5339691a0 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -204,7 +204,7 @@ export namespace Skill { const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) { yield* Effect.promise(() => state.ensure()) - const list = Object.values(state.skills) + const list = Object.values(state.skills).toSorted((a, b) => a.name.localeCompare(b.name)) if (!agent) return list return list.filter((skill) => PermissionNext.evaluate("skill", skill.name, agent.permission).action !== "deny") }) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 14ecea107..ad2b41f00 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -33,12 +33,13 @@ export const TaskTool = Tool.define("task", async (ctx) => { const accessibleAgents = caller ? agents.filter((a) => PermissionNext.evaluate("task", a.name, caller.permission).action !== "deny") : agents + const list = accessibleAgents.toSorted((a, b) => a.name.localeCompare(b.name)) const description = DESCRIPTION.replace( "{agents}", - accessibleAgents - .map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`) - .join("\n"), + list.map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`).join( + "\n", + ), ) return { description, diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index d6b6ebb33..60c8e57c9 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -384,6 +384,32 @@ test("multiple custom agents can be defined", async () => { }) }) +test("Agent.list keeps the default agent first and sorts the rest by name", async () => { + await using tmp = await tmpdir({ + config: { + default_agent: "plan", + agent: { + zebra: { + description: "Zebra", + mode: "subagent", + }, + alpha: { + description: "Alpha", + mode: "subagent", + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const names = (await Agent.list()).map((a) => a.name) + expect(names[0]).toBe("plan") + expect(names.slice(1)).toEqual(names.slice(1).toSorted((a, b) => a.localeCompare(b))) + }, + }) +}) + test("Agent.get returns undefined for non-existent agent", async () => { await using tmp = await tmpdir() await Instance.provide({ diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts new file mode 100644 index 000000000..47f5f6fc2 --- /dev/null +++ b/packages/opencode/test/session/system.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import { Agent } from "../../src/agent/agent" +import { Instance } from "../../src/project/instance" +import { SystemPrompt } from "../../src/session/system" +import { tmpdir } from "../fixture/fixture" + +describe("session.system", () => { + test("skills output is sorted by name and stable across calls", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + for (const [name, description] of [ + ["zeta-skill", "Zeta skill."], + ["alpha-skill", "Alpha skill."], + ["middle-skill", "Middle skill."], + ]) { + const skillDir = path.join(dir, ".opencode", "skill", name) + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: ${name} +description: ${description} +--- + +# ${name} +`, + ) + } + }, + }) + + const home = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const build = await Agent.get("build") + const first = await SystemPrompt.skills(build!) + const second = await SystemPrompt.skills(build!) + + expect(first).toBe(second) + + const alpha = first!.indexOf("alpha-skill") + const middle = first!.indexOf("middle-skill") + const zeta = first!.indexOf("zeta-skill") + + expect(alpha).toBeGreaterThan(-1) + expect(middle).toBeGreaterThan(alpha) + expect(zeta).toBeGreaterThan(middle) + }, + }) + } finally { + process.env.OPENCODE_TEST_HOME = home + } + }) +}) diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index 7cfaee135..f622341d3 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -54,6 +54,56 @@ description: Skill for tool tests. } }) + test("description sorts skills by name and is stable across calls", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + for (const [name, description] of [ + ["zeta-skill", "Zeta skill."], + ["alpha-skill", "Alpha skill."], + ["middle-skill", "Middle skill."], + ]) { + const skillDir = path.join(dir, ".opencode", "skill", name) + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: ${name} +description: ${description} +--- + +# ${name} +`, + ) + } + }, + }) + + const home = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const first = await SkillTool.init() + const second = await SkillTool.init() + + expect(first.description).toBe(second.description) + + const alpha = first.description.indexOf("**alpha-skill**: Alpha skill.") + const middle = first.description.indexOf("**middle-skill**: Middle skill.") + const zeta = first.description.indexOf("**zeta-skill**: Zeta skill.") + + expect(alpha).toBeGreaterThan(-1) + expect(middle).toBeGreaterThan(alpha) + expect(zeta).toBeGreaterThan(middle) + }, + }) + } finally { + process.env.OPENCODE_TEST_HOME = home + } + }) + test("execute returns skill content block with files", async () => { await using tmp = await tmpdir({ git: true, diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts new file mode 100644 index 000000000..df319d8de --- /dev/null +++ b/packages/opencode/test/tool/task.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" +import { Agent } from "../../src/agent/agent" +import { Instance } from "../../src/project/instance" +import { TaskTool } from "../../src/tool/task" +import { tmpdir } from "../fixture/fixture" + +describe("tool.task", () => { + test("description sorts subagents by name and is stable across calls", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + zebra: { + description: "Zebra agent", + mode: "subagent", + }, + alpha: { + description: "Alpha agent", + mode: "subagent", + }, + }, + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const build = await Agent.get("build") + const first = await TaskTool.init({ agent: build }) + const second = await TaskTool.init({ agent: build }) + + expect(first.description).toBe(second.description) + + const alpha = first.description.indexOf("- alpha: Alpha agent") + const explore = first.description.indexOf("- explore:") + const general = first.description.indexOf("- general:") + const zebra = first.description.indexOf("- zebra: Zebra agent") + + expect(alpha).toBeGreaterThan(-1) + expect(explore).toBeGreaterThan(alpha) + expect(general).toBeGreaterThan(explore) + expect(zebra).toBeGreaterThan(general) + }, + }) + }) +})