oh-my-opencode/src/tools/agent-teams/teammate-tools.ts

152 lines
6.0 KiB
TypeScript

import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import type { BackgroundManager } from "../../features/background-agent"
import { validateAgentName, validateTeamName } from "./name-validation"
import {
TeamForceKillInputSchema,
TeamProcessShutdownInputSchema,
TeamSpawnInputSchema,
TeamToolContext,
isTeammateMember,
} from "./types"
import { getTeamMember, readTeamConfigOrThrow, removeTeammate, updateTeamConfig, writeTeamConfig } from "./team-config-store"
import { cancelTeammateRun, spawnTeammate } from "./teammate-runtime"
import { resetOwnerTasks } from "./team-task-store"
export function createSpawnTeammateTool(manager: BackgroundManager): ToolDefinition {
return tool({
description: "Spawn a teammate using native internal agent execution.",
args: {
team_name: tool.schema.string().describe("Team name"),
name: tool.schema.string().describe("Teammate name"),
prompt: tool.schema.string().describe("Initial teammate prompt"),
subagent_type: tool.schema.string().optional().describe("Agent name to run (default: sisyphus-junior)"),
model: tool.schema.string().optional().describe("Optional model override in provider/model format"),
plan_mode_required: tool.schema.boolean().optional().describe("Enable plan mode flag in teammate metadata"),
},
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
try {
const input = TeamSpawnInputSchema.parse(args)
const teamError = validateTeamName(input.team_name)
if (teamError) {
return JSON.stringify({ error: teamError })
}
const agentError = validateAgentName(input.name)
if (agentError) {
return JSON.stringify({ error: agentError })
}
const teammate = await spawnTeammate({
teamName: input.team_name,
name: input.name,
prompt: input.prompt,
subagentType: input.subagent_type ?? "sisyphus-junior",
model: input.model,
planModeRequired: input.plan_mode_required ?? false,
context,
manager,
})
return JSON.stringify({
agent_id: teammate.agentId,
name: teammate.name,
team_name: input.team_name,
session_id: teammate.sessionID,
task_id: teammate.backgroundTaskID,
})
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "spawn_teammate_failed" })
}
},
})
}
export function createForceKillTeammateTool(manager: BackgroundManager): ToolDefinition {
return tool({
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"),
},
execute: async (args: Record<string, unknown>): Promise<string> => {
try {
const input = TeamForceKillInputSchema.parse(args)
const teamError = validateTeamName(input.team_name)
if (teamError) {
return JSON.stringify({ error: teamError })
}
const agentError = validateAgentName(input.agent_name)
if (agentError) {
return JSON.stringify({ error: agentError })
}
const config = readTeamConfigOrThrow(input.team_name)
const member = getTeamMember(config, input.agent_name)
if (!member || !isTeammateMember(member)) {
return JSON.stringify({ error: "teammate_not_found" })
}
await cancelTeammateRun(manager, member)
const refreshedConfig = readTeamConfigOrThrow(input.team_name)
const refreshedMember = getTeamMember(refreshedConfig, input.agent_name)
if (refreshedMember && isTeammateMember(refreshedMember)) {
writeTeamConfig(input.team_name, removeTeammate(refreshedConfig, input.agent_name))
}
resetOwnerTasks(input.team_name, input.agent_name)
return JSON.stringify({ success: true, message: `${input.agent_name} stopped` })
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "force_kill_teammate_failed" })
}
},
})
}
export function createProcessShutdownTool(manager: BackgroundManager): ToolDefinition {
return tool({
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"),
},
execute: async (args: Record<string, unknown>): Promise<string> => {
try {
const input = TeamProcessShutdownInputSchema.parse(args)
const teamError = validateTeamName(input.team_name)
if (teamError) {
return JSON.stringify({ error: teamError })
}
if (input.agent_name === "team-lead") {
return JSON.stringify({ error: "cannot_shutdown_team_lead" })
}
const agentError = validateAgentName(input.agent_name)
if (agentError) {
return JSON.stringify({ error: agentError })
}
const config = readTeamConfigOrThrow(input.team_name)
const member = getTeamMember(config, input.agent_name)
if (!member || !isTeammateMember(member)) {
return JSON.stringify({ error: "teammate_not_found" })
}
await cancelTeammateRun(manager, member)
updateTeamConfig(input.team_name, (current) => {
const refreshedMember = getTeamMember(current, input.agent_name)
if (!refreshedMember || !isTeammateMember(refreshedMember)) {
return current
}
return removeTeammate(current, input.agent_name)
})
resetOwnerTasks(input.team_name, input.agent_name)
return JSON.stringify({ success: true, message: `${input.agent_name} removed` })
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "process_shutdown_failed" })
}
},
})
}