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:
72
src/plugin/tool-registry.test.ts
Normal file
72
src/plugin/tool-registry.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" })
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user