Browse Source

feat: classify agents for UI vs background use (#45)

Hide plugin subagents from the UI selector while keeping background_task delegation clean, add coverage for classification, and document primary vs subagents

docs: add pull request template

Co-authored-by: Alvin <alvin@cmngoal.com>
Ivan Marshall Widjaja 2 months ago
parent
commit
40b042ef1e
6 changed files with 116 additions and 9 deletions
  1. 32 0
      .github/PULL_REQUEST_TEMPLATE.md
  2. 3 2
      .gitignore
  3. 14 0
      README.md
  4. 30 1
      src/agents/index.test.ts
  5. 35 4
      src/agents/index.ts
  6. 2 2
      src/tools/background.ts

+ 32 - 0
.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,32 @@
+## Summary
+
+<!-- Brief description of what this PR does. 1-3 bullet points. -->
+
+-
+
+## Changes
+
+<!-- What was changed and how. List specific modifications. -->
+
+-
+
+## Screenshots
+
+<!-- If applicable, add screenshots or GIFs showing before/after. Delete this section if not needed. -->
+
+| Before | After |
+|:---:|:---:|
+|  |  |
+
+## Testing
+
+<!-- How to verify this PR works correctly. Delete if not applicable. -->
+
+```bash
+bun run typecheck
+bun test
+```
+
+## Related Issues
+
+<!-- Link related issues. Delete if not applicable. -->

+ 3 - 2
.gitignore

@@ -40,5 +40,6 @@ local
 
 
 .sisyphus/
 .sisyphus/
 .hive/
 .hive/
-
-.ignore
+.opencode/
+.claude/
+.ignore

+ 14 - 0
README.md

@@ -203,6 +203,20 @@ The plugin follows a "Hub and Spoke" model:
 
 
 ---
 ---
 
 
+### Primary vs Subagents
+
+Primary agents appear in the OpenCode UI selector, while subagents are hidden from the UI and intended for delegation through `background_task`.
+
+**Primary agents (UI):**
+- OpenCode built-ins: `Build`, `Plan` (disabled by the installer by default)
+- Plugin primary: `orchestrator`
+
+**Subagents (background_task):** `explorer`, `librarian`, `oracle`, `designer`, `fixer`
+
+The `background_task` tool lists only subagents. If the UI list looks stale after changes, restart OpenCode.
+
+---
+
 ## Meet the Pantheon
 ## Meet the Pantheon
 
 
 <br clear="both">
 <br clear="both">

+ 30 - 1
src/agents/index.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test";
 import { describe, expect, test } from "bun:test";
-import { createAgents, getAgentConfigs } from "./index";
+import { createAgents, getAgentConfigs, getSubagentNames, getPrimaryAgentNames } from "./index";
 import type { PluginConfig } from "../config";
 import type { PluginConfig } from "../config";
 
 
 describe("agent alias backward compatibility", () => {
 describe("agent alias backward compatibility", () => {
@@ -63,6 +63,35 @@ describe("agent alias backward compatibility", () => {
   });
   });
 });
 });
 
 
+describe("agent classification", () => {
+  test("getPrimaryAgentNames returns only orchestrator", () => {
+    const names = getPrimaryAgentNames();
+    expect(names).toEqual(["orchestrator"]);
+  });
+
+  test("getSubagentNames excludes orchestrator", () => {
+    const names = getSubagentNames();
+    expect(names).not.toContain("orchestrator");
+    expect(names).toContain("explorer");
+    expect(names).toContain("fixer");
+  });
+
+  test("getAgentConfigs applies correct classification visibility and mode", () => {
+    const configs = getAgentConfigs();
+
+    // Primary agent
+    expect(configs["orchestrator"].mode).toBe("primary");
+    expect(configs["orchestrator"].hidden).toBeFalsy();
+
+    // Subagents
+    const subagents = getSubagentNames();
+    for (const name of subagents) {
+      expect(configs[name].mode).toBe("subagent");
+      expect(configs[name].hidden).toBe(true);
+    }
+  });
+});
+
 describe("createAgents", () => {
 describe("createAgents", () => {
   test("creates all agents without config", () => {
   test("creates all agents without config", () => {
     const agents = createAgents();
     const agents = createAgents();

+ 35 - 4
src/agents/index.ts

@@ -1,5 +1,5 @@
 import type { AgentConfig as SDKAgentConfig } from "@opencode-ai/sdk";
 import type { AgentConfig as SDKAgentConfig } from "@opencode-ai/sdk";
-import { DEFAULT_MODELS, type AgentName, type PluginConfig, type AgentOverrideConfig } from "../config";
+import { DEFAULT_MODELS, type PluginConfig, type AgentOverrideConfig } from "../config";
 import { createOrchestratorAgent, type AgentDefinition } from "./orchestrator";
 import { createOrchestratorAgent, type AgentDefinition } from "./orchestrator";
 import { createOracleAgent } from "./oracle";
 import { createOracleAgent } from "./oracle";
 import { createLibrarianAgent } from "./librarian";
 import { createLibrarianAgent } from "./librarian";
@@ -37,7 +37,24 @@ function applyDefaultPermissions(agent: AgentDefinition): void {
   agent.config.permission = { ...existing, question: "allow" } as SDKAgentConfig["permission"];
   agent.config.permission = { ...existing, question: "allow" } as SDKAgentConfig["permission"];
 }
 }
 
 
-type SubagentName = Exclude<AgentName, "orchestrator">;
+/** Constants for agent classification */
+export const PRIMARY_AGENT_NAMES = ["orchestrator"] as const;
+export type PrimaryAgentName = (typeof PRIMARY_AGENT_NAMES)[number];
+
+export const SUBAGENT_NAMES = ["explorer", "librarian", "oracle", "designer", "fixer"] as const;
+export type SubagentName = (typeof SUBAGENT_NAMES)[number];
+
+export function getPrimaryAgentNames(): PrimaryAgentName[] {
+  return [...PRIMARY_AGENT_NAMES];
+}
+
+export function getSubagentNames(): SubagentName[] {
+  return [...SUBAGENT_NAMES];
+}
+
+export function isSubagent(name: string): name is SubagentName {
+  return (SUBAGENT_NAMES as readonly string[]).includes(name);
+}
 
 
 /** Agent factories indexed by name */
 /** Agent factories indexed by name */
 const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
 const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
@@ -50,7 +67,7 @@ const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
 
 
 /** Get list of agent names */
 /** Get list of agent names */
 export function getAgentNames(): SubagentName[] {
 export function getAgentNames(): SubagentName[] {
-  return Object.keys(SUBAGENT_FACTORIES) as SubagentName[];
+  return getSubagentNames();
 }
 }
 
 
 export function createAgents(config?: PluginConfig): AgentDefinition[] {
 export function createAgents(config?: PluginConfig): AgentDefinition[] {
@@ -97,5 +114,19 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
 
 
 export function getAgentConfigs(config?: PluginConfig): Record<string, SDKAgentConfig> {
 export function getAgentConfigs(config?: PluginConfig): Record<string, SDKAgentConfig> {
   const agents = createAgents(config);
   const agents = createAgents(config);
-  return Object.fromEntries(agents.map((a) => [a.name, { ...a.config, description: a.description }]));
+  return Object.fromEntries(
+    agents.map((a) => {
+      const sdkConfig: SDKAgentConfig = { ...a.config, description: a.description };
+
+      // Apply classification-based visibility and mode
+      if (isSubagent(a.name)) {
+        sdkConfig.mode = "subagent";
+        sdkConfig.hidden = true;
+      } else if (a.name === "orchestrator") {
+        sdkConfig.mode = "primary";
+      }
+
+      return [a.name, sdkConfig];
+    })
+  );
 }
 }

+ 2 - 2
src/tools/background.ts

@@ -1,6 +1,6 @@
 import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin";
 import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin";
 import type { BackgroundTaskManager } from "../features";
 import type { BackgroundTaskManager } from "../features";
-import { getAgentNames } from "../agents";
+import { getSubagentNames } from "../agents";
 import {
 import {
   POLL_INTERVAL_MS,
   POLL_INTERVAL_MS,
   MAX_POLL_TIME_MS,
   MAX_POLL_TIME_MS,
@@ -27,7 +27,7 @@ export function createBackgroundTools(
   tmuxConfig?: TmuxConfig,
   tmuxConfig?: TmuxConfig,
   pluginConfig?: PluginConfig
   pluginConfig?: PluginConfig
 ): Record<string, ToolDefinition> {
 ): Record<string, ToolDefinition> {
-  const agentNames = getAgentNames().join(", ");
+  const agentNames = getSubagentNames().join(", ");
 
 
   const background_task = tool({
   const background_task = tool({
     description: `Run agent task. Use sync=true to wait for result, sync=false (default) to run in background.
     description: `Run agent task. Use sync=true to wait for result, sync=false (default) to run in background.