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:
parent
16e034492c
commit
8a83020b51
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 teamSystemEnabled = pluginConfig.experimental?.team_system ?? false
|
||||||
const agentTeamsRecord: Record<string, ToolDefinition> = agentTeamsEnabled
|
const agentTeamsRecord: Record<string, ToolDefinition> = teamSystemEnabled
|
||||||
? createAgentTeamsTools(managers.backgroundManager, {
|
? createAgentTeamsTools(managers.backgroundManager, {
|
||||||
client: ctx.client,
|
client: ctx.client,
|
||||||
userCategories: pluginConfig.categories,
|
userCategories: pluginConfig.categories,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { join } from "node:path"
|
|||||||
import type { BackgroundManager } from "../../features/background-agent"
|
import type { BackgroundManager } from "../../features/background-agent"
|
||||||
import { readInbox } from "./inbox-store"
|
import { readInbox } from "./inbox-store"
|
||||||
import { createAgentTeamsTools } from "./tools"
|
import { createAgentTeamsTools } from "./tools"
|
||||||
|
import { readTeamConfig, upsertTeammate, writeTeamConfig } from "./team-config-store"
|
||||||
|
|
||||||
interface TestToolContext {
|
interface TestToolContext {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
@ -63,18 +64,56 @@ function createMockManager(): { manager: BackgroundManager; resumeCalls: ResumeC
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupTeamWithWorker(
|
async function setupTeamWithWorker(
|
||||||
tools: ReturnType<typeof createAgentTeamsTools>,
|
_tools: ReturnType<typeof createAgentTeamsTools>,
|
||||||
context: TestToolContext,
|
context: TestToolContext,
|
||||||
teamName = "core",
|
teamName = "core",
|
||||||
workerName = "worker_1",
|
workerName = "worker_1",
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await executeJsonTool(tools, "team_create", { team_name: teamName }, context)
|
await executeJsonTool(_tools, "team_create", { team_name: teamName }, context)
|
||||||
await executeJsonTool(
|
|
||||||
tools,
|
const config = readTeamConfig(teamName)
|
||||||
"spawn_teammate",
|
if (config) {
|
||||||
{ team_name: teamName, name: workerName, prompt: "Handle tasks", category: "quick" },
|
const teammate = {
|
||||||
context,
|
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", () => {
|
describe("agent-teams messaging tools", () => {
|
||||||
@ -197,12 +236,7 @@ describe("agent-teams messaging tools", () => {
|
|||||||
const leadContext = createContext()
|
const leadContext = createContext()
|
||||||
await executeJsonTool(tools, "team_create", { team_name: tn }, leadContext)
|
await executeJsonTool(tools, "team_create", { team_name: tn }, leadContext)
|
||||||
for (const name of ["worker_1", "worker_2"]) {
|
for (const name of ["worker_1", "worker_2"]) {
|
||||||
await executeJsonTool(
|
await addTeammateManually(tn, name)
|
||||||
tools,
|
|
||||||
"spawn_teammate",
|
|
||||||
{ team_name: tn, name, prompt: "Handle tasks", category: "quick" },
|
|
||||||
leadContext,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
|
|||||||
@ -135,7 +135,7 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef
|
|||||||
description: "Force stop a teammate and clean up ownership state.",
|
description: "Force stop a teammate and clean up ownership state.",
|
||||||
args: {
|
args: {
|
||||||
team_name: tool.schema.string().describe("Team name"),
|
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> => {
|
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
@ -144,17 +144,17 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef
|
|||||||
if (teamError) {
|
if (teamError) {
|
||||||
return JSON.stringify({ error: teamError })
|
return JSON.stringify({ error: teamError })
|
||||||
}
|
}
|
||||||
const agentError = validateAgentName(input.agent_name)
|
const agentError = validateAgentName(input.teammate_name)
|
||||||
if (agentError) {
|
if (agentError) {
|
||||||
return JSON.stringify({ error: 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) {
|
if (shutdownError) {
|
||||||
return JSON.stringify({ error: 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) {
|
} catch (error) {
|
||||||
return JSON.stringify({ error: error instanceof Error ? error.message : "force_kill_teammate_failed" })
|
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.",
|
description: "Finalize an approved shutdown by removing teammate and resetting owned tasks.",
|
||||||
args: {
|
args: {
|
||||||
team_name: tool.schema.string().describe("Team name"),
|
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> => {
|
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
@ -176,20 +176,20 @@ export function createProcessShutdownTool(manager: BackgroundManager): ToolDefin
|
|||||||
if (teamError) {
|
if (teamError) {
|
||||||
return JSON.stringify({ error: 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" })
|
return JSON.stringify({ error: "cannot_shutdown_team_lead" })
|
||||||
}
|
}
|
||||||
const agentError = validateAgentName(input.agent_name)
|
const agentError = validateAgentName(input.teammate_name)
|
||||||
if (agentError) {
|
if (agentError) {
|
||||||
return JSON.stringify({ error: 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) {
|
if (shutdownError) {
|
||||||
return JSON.stringify({ error: 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) {
|
} catch (error) {
|
||||||
return JSON.stringify({ error: error instanceof Error ? error.message : "process_shutdown_failed" })
|
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 type { CategoriesConfig } from "../../config/schema"
|
||||||
import { createReadInboxTool, createSendMessageTool } from "./messaging-tools"
|
import { createReadInboxTool, createSendMessageTool } from "./messaging-tools"
|
||||||
import { createTeamCreateTool, createTeamDeleteTool, createTeamReadConfigTool } from "./team-lifecycle-tools"
|
import { createTeamCreateTool, createTeamDeleteTool, createTeamReadConfigTool } from "./team-lifecycle-tools"
|
||||||
import { createTeamTaskCreateTool, createTeamTaskGetTool, createTeamTaskListTool } from "./team-task-tools"
|
import { createForceKillTeammateTool, createProcessShutdownApprovedTool } from "./teammate-control-tools"
|
||||||
import { createTeamTaskUpdateTool } from "./team-task-update-tool"
|
|
||||||
import { createForceKillTeammateTool, createProcessShutdownTool, createSpawnTeammateTool } from "./teammate-tools"
|
|
||||||
|
|
||||||
export interface AgentTeamsToolOptions {
|
export interface AgentTeamsToolOptions {
|
||||||
client?: PluginInput["client"]
|
client?: PluginInput["client"]
|
||||||
@ -15,25 +13,16 @@ export interface AgentTeamsToolOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createAgentTeamsTools(
|
export function createAgentTeamsTools(
|
||||||
manager: BackgroundManager,
|
_manager: BackgroundManager,
|
||||||
options?: AgentTeamsToolOptions,
|
_options?: AgentTeamsToolOptions,
|
||||||
): Record<string, ToolDefinition> {
|
): Record<string, ToolDefinition> {
|
||||||
return {
|
return {
|
||||||
team_create: createTeamCreateTool(),
|
team_create: createTeamCreateTool(),
|
||||||
team_delete: createTeamDeleteTool(),
|
team_delete: createTeamDeleteTool(),
|
||||||
spawn_teammate: createSpawnTeammateTool(manager, {
|
send_message: createSendMessageTool(_manager),
|
||||||
client: options?.client,
|
|
||||||
userCategories: options?.userCategories,
|
|
||||||
sisyphusJuniorModel: options?.sisyphusJuniorModel,
|
|
||||||
}),
|
|
||||||
send_message: createSendMessageTool(manager),
|
|
||||||
read_inbox: createReadInboxTool(),
|
read_inbox: createReadInboxTool(),
|
||||||
read_config: createTeamReadConfigTool(),
|
read_config: createTeamReadConfigTool(),
|
||||||
team_task_create: createTeamTaskCreateTool(),
|
force_kill_teammate: createForceKillTeammateTool(),
|
||||||
team_task_update: createTeamTaskUpdateTool(),
|
process_shutdown_approved: createProcessShutdownApprovedTool(),
|
||||||
team_task_list: createTeamTaskListTool(),
|
|
||||||
team_task_get: createTeamTaskGetTool(),
|
|
||||||
force_kill_teammate: createForceKillTeammateTool(manager),
|
|
||||||
process_shutdown_approved: createProcessShutdownTool(manager),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,6 +101,8 @@ export const TeamTaskSchema = TaskObjectSchema
|
|||||||
|
|
||||||
export type TeamTask = z.infer<typeof TeamTaskSchema>
|
export type TeamTask = z.infer<typeof TeamTaskSchema>
|
||||||
|
|
||||||
|
export type TeamTaskStatus = "pending" | "in_progress" | "completed" | "deleted"
|
||||||
|
|
||||||
// Input schemas for tools
|
// Input schemas for tools
|
||||||
export const TeamCreateInputSchema = z.object({
|
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),
|
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),
|
name: z.string().min(1),
|
||||||
category: z.string().min(1),
|
category: z.string().min(1),
|
||||||
prompt: 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>
|
export type TeamSpawnInput = z.infer<typeof TeamSpawnInputSchema>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user