diff --git a/src/plugin/tool-registry.test.ts b/src/plugin/tool-registry.test.ts new file mode 100644 index 000000000..b34086213 --- /dev/null +++ b/src/plugin/tool-registry.test.ts @@ -0,0 +1,72 @@ +/// +import { describe, expect, test } from "bun:test" +import { createToolRegistry } from "./tool-registry" +import type { OhMyOpenCodeConfig } from "../config/schema" + +describe("team system tool registration", () => { + test("registers team tools when experimental.team_system is true", () => { + const pluginConfig = { + experimental: { team_system: true }, + } as unknown as OhMyOpenCodeConfig + + const result = createToolRegistry({ + ctx: {} as any, + pluginConfig, + managers: {} as any, + skillContext: {} as any, + availableCategories: [], + }) + + expect(Object.keys(result.filteredTools)).toContain("team_create") + expect(Object.keys(result.filteredTools)).toContain("team_delete") + expect(Object.keys(result.filteredTools)).toContain("send_message") + expect(Object.keys(result.filteredTools)).toContain("read_inbox") + expect(Object.keys(result.filteredTools)).toContain("read_config") + expect(Object.keys(result.filteredTools)).toContain("force_kill_teammate") + expect(Object.keys(result.filteredTools)).toContain("process_shutdown_approved") + }) + + test("does not register team tools when experimental.team_system is false", () => { + const pluginConfig = { + experimental: { team_system: false }, + } as unknown as OhMyOpenCodeConfig + + const result = createToolRegistry({ + ctx: {} as any, + pluginConfig, + managers: {} as any, + skillContext: {} as any, + availableCategories: [], + }) + + expect(Object.keys(result.filteredTools)).not.toContain("team_create") + expect(Object.keys(result.filteredTools)).not.toContain("team_delete") + expect(Object.keys(result.filteredTools)).not.toContain("send_message") + expect(Object.keys(result.filteredTools)).not.toContain("read_inbox") + expect(Object.keys(result.filteredTools)).not.toContain("read_config") + expect(Object.keys(result.filteredTools)).not.toContain("force_kill_teammate") + expect(Object.keys(result.filteredTools)).not.toContain("process_shutdown_approved") + }) + + test("does not register team tools when experimental.team_system is undefined", () => { + const pluginConfig = { + experimental: {}, + } as unknown as OhMyOpenCodeConfig + + const result = createToolRegistry({ + ctx: {} as any, + pluginConfig, + managers: {} as any, + skillContext: {} as any, + availableCategories: [], + }) + + expect(Object.keys(result.filteredTools)).not.toContain("team_create") + expect(Object.keys(result.filteredTools)).not.toContain("team_delete") + expect(Object.keys(result.filteredTools)).not.toContain("send_message") + expect(Object.keys(result.filteredTools)).not.toContain("read_inbox") + expect(Object.keys(result.filteredTools)).not.toContain("read_config") + expect(Object.keys(result.filteredTools)).not.toContain("force_kill_teammate") + expect(Object.keys(result.filteredTools)).not.toContain("process_shutdown_approved") + }) +}) diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index 94c30bc6f..0e48b8dba 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -118,8 +118,8 @@ export function createToolRegistry(args: { } : {} - const agentTeamsEnabled = pluginConfig.experimental?.agent_teams ?? false - const agentTeamsRecord: Record = agentTeamsEnabled + const teamSystemEnabled = pluginConfig.experimental?.team_system ?? false + const agentTeamsRecord: Record = teamSystemEnabled ? createAgentTeamsTools(managers.backgroundManager, { client: ctx.client, userCategories: pluginConfig.categories, diff --git a/src/tools/agent-teams/messaging-tools.test.ts b/src/tools/agent-teams/messaging-tools.test.ts index 32ee20fc7..08b116e88 100644 --- a/src/tools/agent-teams/messaging-tools.test.ts +++ b/src/tools/agent-teams/messaging-tools.test.ts @@ -7,6 +7,7 @@ import { join } from "node:path" import type { BackgroundManager } from "../../features/background-agent" import { readInbox } from "./inbox-store" import { createAgentTeamsTools } from "./tools" +import { readTeamConfig, upsertTeammate, writeTeamConfig } from "./team-config-store" interface TestToolContext { sessionID: string @@ -63,18 +64,56 @@ function createMockManager(): { manager: BackgroundManager; resumeCalls: ResumeC } async function setupTeamWithWorker( - tools: ReturnType, + _tools: ReturnType, context: TestToolContext, teamName = "core", workerName = "worker_1", ): Promise { - await executeJsonTool(tools, "team_create", { team_name: teamName }, context) - await executeJsonTool( - tools, - "spawn_teammate", - { team_name: teamName, name: workerName, prompt: "Handle tasks", category: "quick" }, - context, - ) + await executeJsonTool(_tools, "team_create", { team_name: teamName }, context) + + const config = readTeamConfig(teamName) + if (config) { + const teammate = { + agentId: `agent-${randomUUID()}`, + name: workerName, + agentType: "teammate" as const, + category: "quick", + model: "default", + prompt: "Handle tasks", + joinedAt: new Date().toISOString(), + color: "#FF5733", + cwd: process.cwd(), + planModeRequired: false, + subscriptions: [], + backendType: "native" as const, + isActive: true, + } + const updatedConfig = upsertTeammate(config, teammate) + writeTeamConfig(teamName, updatedConfig) + } +} + +async function addTeammateManually(teamName: string, workerName: string): Promise { + const config = readTeamConfig(teamName) + if (config) { + const teammate = { + agentId: `agent-${randomUUID()}`, + name: workerName, + agentType: "teammate" as const, + category: "quick", + model: "default", + prompt: "Handle tasks", + joinedAt: new Date().toISOString(), + color: "#FF5733", + cwd: process.cwd(), + planModeRequired: false, + subscriptions: [], + backendType: "native" as const, + isActive: true, + } + const updatedConfig = upsertTeammate(config, teammate) + writeTeamConfig(teamName, updatedConfig) + } } describe("agent-teams messaging tools", () => { @@ -197,12 +236,7 @@ describe("agent-teams messaging tools", () => { const leadContext = createContext() await executeJsonTool(tools, "team_create", { team_name: tn }, leadContext) for (const name of ["worker_1", "worker_2"]) { - await executeJsonTool( - tools, - "spawn_teammate", - { team_name: tn, name, prompt: "Handle tasks", category: "quick" }, - leadContext, - ) + await addTeammateManually(tn, name) } //#when diff --git a/src/tools/agent-teams/teammate-tools.ts b/src/tools/agent-teams/teammate-tools.ts index 9d3541af4..2c8f157e9 100644 --- a/src/tools/agent-teams/teammate-tools.ts +++ b/src/tools/agent-teams/teammate-tools.ts @@ -135,7 +135,7 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef description: "Force stop a teammate and clean up ownership state.", args: { team_name: tool.schema.string().describe("Team name"), - agent_name: tool.schema.string().describe("Teammate name"), + teammate_name: tool.schema.string().describe("Teammate name"), }, execute: async (args: Record, context: TeamToolContext): Promise => { try { @@ -144,17 +144,17 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef if (teamError) { return JSON.stringify({ error: teamError }) } - const agentError = validateAgentName(input.agent_name) + const agentError = validateAgentName(input.teammate_name) if (agentError) { return JSON.stringify({ error: agentError }) } - const shutdownError = await shutdownTeammateWithCleanup(manager, context, input.team_name, input.agent_name) + const shutdownError = await shutdownTeammateWithCleanup(manager, context, input.team_name, input.teammate_name) if (shutdownError) { return JSON.stringify({ error: shutdownError }) } - return JSON.stringify({ success: true, message: `${input.agent_name} stopped` }) + return JSON.stringify({ success: true, message: `${input.teammate_name} stopped` }) } catch (error) { return JSON.stringify({ error: error instanceof Error ? error.message : "force_kill_teammate_failed" }) } @@ -167,7 +167,7 @@ export function createProcessShutdownTool(manager: BackgroundManager): ToolDefin description: "Finalize an approved shutdown by removing teammate and resetting owned tasks.", args: { team_name: tool.schema.string().describe("Team name"), - agent_name: tool.schema.string().describe("Teammate name"), + teammate_name: tool.schema.string().describe("Teammate name"), }, execute: async (args: Record, context: TeamToolContext): Promise => { try { @@ -176,20 +176,20 @@ export function createProcessShutdownTool(manager: BackgroundManager): ToolDefin if (teamError) { return JSON.stringify({ error: teamError }) } - if (input.agent_name === "team-lead") { + if (input.teammate_name === "team-lead") { return JSON.stringify({ error: "cannot_shutdown_team_lead" }) } - const agentError = validateAgentName(input.agent_name) + const agentError = validateAgentName(input.teammate_name) if (agentError) { return JSON.stringify({ error: agentError }) } - const shutdownError = await shutdownTeammateWithCleanup(manager, context, input.team_name, input.agent_name) + const shutdownError = await shutdownTeammateWithCleanup(manager, context, input.team_name, input.teammate_name) if (shutdownError) { return JSON.stringify({ error: shutdownError }) } - return JSON.stringify({ success: true, message: `${input.agent_name} removed` }) + return JSON.stringify({ success: true, message: `${input.teammate_name} removed` }) } catch (error) { return JSON.stringify({ error: error instanceof Error ? error.message : "process_shutdown_failed" }) } diff --git a/src/tools/agent-teams/tools.ts b/src/tools/agent-teams/tools.ts index ff32496fa..28d9fa300 100644 --- a/src/tools/agent-teams/tools.ts +++ b/src/tools/agent-teams/tools.ts @@ -4,9 +4,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { CategoriesConfig } from "../../config/schema" import { createReadInboxTool, createSendMessageTool } from "./messaging-tools" import { createTeamCreateTool, createTeamDeleteTool, createTeamReadConfigTool } from "./team-lifecycle-tools" -import { createTeamTaskCreateTool, createTeamTaskGetTool, createTeamTaskListTool } from "./team-task-tools" -import { createTeamTaskUpdateTool } from "./team-task-update-tool" -import { createForceKillTeammateTool, createProcessShutdownTool, createSpawnTeammateTool } from "./teammate-tools" +import { createForceKillTeammateTool, createProcessShutdownApprovedTool } from "./teammate-control-tools" export interface AgentTeamsToolOptions { client?: PluginInput["client"] @@ -15,25 +13,16 @@ export interface AgentTeamsToolOptions { } export function createAgentTeamsTools( - manager: BackgroundManager, - options?: AgentTeamsToolOptions, + _manager: BackgroundManager, + _options?: AgentTeamsToolOptions, ): Record { return { team_create: createTeamCreateTool(), team_delete: createTeamDeleteTool(), - spawn_teammate: createSpawnTeammateTool(manager, { - client: options?.client, - userCategories: options?.userCategories, - sisyphusJuniorModel: options?.sisyphusJuniorModel, - }), - send_message: createSendMessageTool(manager), + send_message: createSendMessageTool(_manager), read_inbox: createReadInboxTool(), read_config: createTeamReadConfigTool(), - team_task_create: createTeamTaskCreateTool(), - team_task_update: createTeamTaskUpdateTool(), - team_task_list: createTeamTaskListTool(), - team_task_get: createTeamTaskGetTool(), - force_kill_teammate: createForceKillTeammateTool(manager), - process_shutdown_approved: createProcessShutdownTool(manager), + force_kill_teammate: createForceKillTeammateTool(), + process_shutdown_approved: createProcessShutdownApprovedTool(), } } diff --git a/src/tools/agent-teams/types.ts b/src/tools/agent-teams/types.ts index 5467cc9b0..5a1b7d60c 100644 --- a/src/tools/agent-teams/types.ts +++ b/src/tools/agent-teams/types.ts @@ -101,6 +101,8 @@ export const TeamTaskSchema = TaskObjectSchema export type TeamTask = z.infer +export type TeamTaskStatus = "pending" | "in_progress" | "completed" | "deleted" + // Input schemas for tools export const TeamCreateInputSchema = z.object({ team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64), @@ -183,6 +185,9 @@ export const TeamSpawnInputSchema = z.object({ name: z.string().min(1), category: z.string().min(1), prompt: z.string().min(1), + subagent_type: z.string().optional(), + model: z.string().optional(), + plan_mode_required: z.boolean().optional(), }) export type TeamSpawnInput = z.infer