Ver código fonte

fix: stabilize agent and skill ordering in prompt descriptions (#18261)

Co-authored-by: Aiden Cline <[email protected]>
Co-authored-by: Aiden Cline <[email protected]>
jorge g 4 semanas atrás
pai
commit
2dbcd79fd2

+ 4 - 1
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"],
+      ),
     )
   }
 

+ 1 - 1
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")
       })

+ 4 - 3
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,

+ 26 - 0
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({

+ 59 - 0
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("<name>alpha-skill</name>")
+          const middle = first!.indexOf("<name>middle-skill</name>")
+          const zeta = first!.indexOf("<name>zeta-skill</name>")
+
+          expect(alpha).toBeGreaterThan(-1)
+          expect(middle).toBeGreaterThan(alpha)
+          expect(zeta).toBeGreaterThan(middle)
+        },
+      })
+    } finally {
+      process.env.OPENCODE_TEST_HOME = home
+    }
+  })
+})

+ 50 - 0
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,

+ 45 - 0
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)
+      },
+    })
+  })
+})