feat(agent-teams): register team tools behind experimental.team_system flag

- Create barrel export in src/tools/agent-teams/index.ts
- Create factory function createAgentTeamsTools() in tools.ts
- Register 7 team tools in tool-registry.ts behind experimental flag
- Add integration tests for tool registration gating
- Fix type errors: add TeamTaskStatus, update schemas
- Task 13 complete
This commit is contained in:
YeonGyu-Kim
2026-02-12 03:33:23 +09:00
parent 16e034492c
commit 8a83020b51
6 changed files with 142 additions and 42 deletions

View File

@@ -0,0 +1,72 @@
/// <reference types="bun-types" />
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")
})
})

View File

@@ -118,8 +118,8 @@ export function createToolRegistry(args: {
}
: {}
const agentTeamsEnabled = pluginConfig.experimental?.agent_teams ?? false
const agentTeamsRecord: Record<string, ToolDefinition> = agentTeamsEnabled
const teamSystemEnabled = pluginConfig.experimental?.team_system ?? false
const agentTeamsRecord: Record<string, ToolDefinition> = teamSystemEnabled
? createAgentTeamsTools(managers.backgroundManager, {
client: ctx.client,
userCategories: pluginConfig.categories,

View File

@@ -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<typeof createAgentTeamsTools>,
_tools: ReturnType<typeof createAgentTeamsTools>,
context: TestToolContext,
teamName = "core",
workerName = "worker_1",
): Promise<void> {
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<void> {
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

View File

@@ -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<string, unknown>, context: TeamToolContext): Promise<string> => {
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<string, unknown>, context: TeamToolContext): Promise<string> => {
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" })
}

View File

@@ -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<string, ToolDefinition> {
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(),
}
}

View File

@@ -101,6 +101,8 @@ export const TeamTaskSchema = TaskObjectSchema
export type TeamTask = z.infer<typeof TeamTaskSchema>
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<typeof TeamSpawnInputSchema>