feat(tools): add native team orchestration tool suite

Port team lifecycle, teammate runtime, inbox messaging, and team-scoped task flows into built-in tools so multi-agent coordination works natively without external server dependencies.
This commit is contained in:
Nguyen Khac Trung Kien
2026-02-08 08:09:36 +07:00
committed by YeonGyu-Kim
parent 4ab93c0cf7
commit 0f9c93fd55
17 changed files with 1720 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs"
import { z } from "zod"
import { acquireLock, ensureDir } from "../../features/claude-tasks/storage"
import { getTeamInboxDir, getTeamInboxPath } from "./paths"
import { TeamInboxMessage, TeamInboxMessageSchema } from "./types"
const TeamInboxListSchema = z.array(TeamInboxMessageSchema)
function nowIso(): string {
return new Date().toISOString()
}
function withInboxLock<T>(teamName: string, operation: () => T): T {
const inboxDir = getTeamInboxDir(teamName)
ensureDir(inboxDir)
const lock = acquireLock(inboxDir)
if (!lock.acquired) {
throw new Error("inbox_lock_unavailable")
}
try {
return operation()
} finally {
lock.release()
}
}
function parseInboxFile(content: string): TeamInboxMessage[] {
try {
const parsed = JSON.parse(content)
const result = TeamInboxListSchema.safeParse(parsed)
return result.success ? result.data : []
} catch {
return []
}
}
function readInboxMessages(teamName: string, agentName: string): TeamInboxMessage[] {
const path = getTeamInboxPath(teamName, agentName)
if (!existsSync(path)) {
return []
}
return parseInboxFile(readFileSync(path, "utf-8"))
}
function writeInboxMessages(teamName: string, agentName: string, messages: TeamInboxMessage[]): void {
const path = getTeamInboxPath(teamName, agentName)
writeFileSync(path, JSON.stringify(messages), "utf-8")
}
export function ensureInbox(teamName: string, agentName: string): void {
withInboxLock(teamName, () => {
const path = getTeamInboxPath(teamName, agentName)
if (!existsSync(path)) {
writeFileSync(path, "[]", "utf-8")
}
})
}
export function appendInboxMessage(teamName: string, agentName: string, message: TeamInboxMessage): void {
withInboxLock(teamName, () => {
const path = getTeamInboxPath(teamName, agentName)
if (!existsSync(path)) {
writeFileSync(path, "[]", "utf-8")
}
const messages = readInboxMessages(teamName, agentName)
messages.push(TeamInboxMessageSchema.parse(message))
writeInboxMessages(teamName, agentName, messages)
})
}
export function sendPlainInboxMessage(
teamName: string,
from: string,
to: string,
text: string,
summary: string,
color?: string,
): void {
appendInboxMessage(teamName, to, {
from,
text,
timestamp: nowIso(),
read: false,
summary,
...(color ? { color } : {}),
})
}
export function sendStructuredInboxMessage(
teamName: string,
from: string,
to: string,
payload: Record<string, unknown>,
summary?: string,
): void {
appendInboxMessage(teamName, to, {
from,
text: JSON.stringify(payload),
timestamp: nowIso(),
read: false,
...(summary ? { summary } : {}),
})
}
export function readInbox(
teamName: string,
agentName: string,
unreadOnly = false,
markAsRead = true,
): TeamInboxMessage[] {
return withInboxLock(teamName, () => {
const messages = readInboxMessages(teamName, agentName)
const selected = unreadOnly ? messages.filter((message) => !message.read) : [...messages]
if (!markAsRead || selected.length === 0) {
return selected
}
const updated = messages.map((message) => {
if (!unreadOnly) {
return { ...message, read: true }
}
if (selected.some((selectedMessage) => selectedMessage.timestamp === message.timestamp && selectedMessage.from === message.from && selectedMessage.text === message.text)) {
return { ...message, read: true }
}
return message
})
writeInboxMessages(teamName, agentName, updated)
return selected
})
}
export function buildShutdownRequestId(recipient: string): string {
return `shutdown-${Date.now()}@${recipient}`
}

View File

@@ -0,0 +1 @@
export { createAgentTeamsTools } from "./tools"

View File

@@ -0,0 +1,199 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import type { BackgroundManager } from "../../features/background-agent"
import { buildShutdownRequestId, readInbox, sendPlainInboxMessage, sendStructuredInboxMessage } from "./inbox-store"
import { getTeamMember, listTeammates, readTeamConfigOrThrow } from "./team-config-store"
import { resumeTeammateWithMessage } from "./teammate-runtime"
import {
TeamReadInboxInputSchema,
TeamSendMessageInputSchema,
TeamToolContext,
isTeammateMember,
} from "./types"
function nowIso(): string {
return new Date().toISOString()
}
export function createSendMessageTool(manager: BackgroundManager): ToolDefinition {
return tool({
description: "Send direct or broadcast team messages and protocol responses.",
args: {
team_name: tool.schema.string().describe("Team name"),
type: tool.schema.enum(["message", "broadcast", "shutdown_request", "shutdown_response", "plan_approval_response"]),
recipient: tool.schema.string().optional().describe("Message recipient"),
content: tool.schema.string().optional().describe("Message body"),
summary: tool.schema.string().optional().describe("Short summary"),
request_id: tool.schema.string().optional().describe("Protocol request id"),
approve: tool.schema.boolean().optional().describe("Approval flag"),
sender: tool.schema.string().optional().describe("Sender name (default: team-lead)"),
},
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
try {
const input = TeamSendMessageInputSchema.parse(args)
const sender = input.sender ?? "team-lead"
const config = readTeamConfigOrThrow(input.team_name)
const memberNames = new Set(config.members.map((member) => member.name))
if (input.type === "message") {
if (!input.recipient || !input.summary || !input.content) {
return JSON.stringify({ error: "message_requires_recipient_summary_content" })
}
if (!memberNames.has(input.recipient)) {
return JSON.stringify({ error: "message_recipient_not_found" })
}
const targetMember = getTeamMember(config, input.recipient)
const color = targetMember && isTeammateMember(targetMember) ? targetMember.color : undefined
sendPlainInboxMessage(input.team_name, sender, input.recipient, input.content, input.summary, color)
if (targetMember && isTeammateMember(targetMember)) {
await resumeTeammateWithMessage(manager, context, input.team_name, targetMember, input.summary, input.content)
}
return JSON.stringify({ success: true, message: `message_sent:${input.recipient}` })
}
if (input.type === "broadcast") {
if (!input.summary) {
return JSON.stringify({ error: "broadcast_requires_summary" })
}
const teammates = listTeammates(config)
for (const teammate of teammates) {
sendPlainInboxMessage(input.team_name, sender, teammate.name, input.content ?? "", input.summary)
await resumeTeammateWithMessage(manager, context, input.team_name, teammate, input.summary, input.content ?? "")
}
return JSON.stringify({ success: true, message: `broadcast_sent:${teammates.length}` })
}
if (input.type === "shutdown_request") {
if (!input.recipient) {
return JSON.stringify({ error: "shutdown_request_requires_recipient" })
}
if (input.recipient === "team-lead") {
return JSON.stringify({ error: "cannot_shutdown_team_lead" })
}
const targetMember = getTeamMember(config, input.recipient)
if (!targetMember || !isTeammateMember(targetMember)) {
return JSON.stringify({ error: "shutdown_recipient_not_found" })
}
const requestId = buildShutdownRequestId(input.recipient)
sendStructuredInboxMessage(
input.team_name,
"team-lead",
input.recipient,
{
type: "shutdown_request",
requestId,
from: "team-lead",
reason: input.content ?? "",
timestamp: nowIso(),
},
"shutdown_request",
)
await resumeTeammateWithMessage(
manager,
context,
input.team_name,
targetMember,
"shutdown_request",
input.content ?? "Shutdown requested",
)
return JSON.stringify({ success: true, request_id: requestId, target: input.recipient })
}
if (input.type === "shutdown_response") {
if (!input.request_id) {
return JSON.stringify({ error: "shutdown_response_requires_request_id" })
}
if (input.approve) {
sendStructuredInboxMessage(
input.team_name,
sender,
"team-lead",
{
type: "shutdown_approved",
requestId: input.request_id,
from: sender,
timestamp: nowIso(),
backendType: "native",
},
"shutdown_approved",
)
return JSON.stringify({ success: true, message: `shutdown_approved:${input.request_id}` })
}
sendPlainInboxMessage(
input.team_name,
sender,
"team-lead",
input.content ?? "Shutdown rejected",
"shutdown_rejected",
)
return JSON.stringify({ success: true, message: `shutdown_rejected:${input.request_id}` })
}
if (!input.recipient) {
return JSON.stringify({ error: "plan_response_requires_recipient" })
}
if (!memberNames.has(input.recipient)) {
return JSON.stringify({ error: "plan_response_recipient_not_found" })
}
if (input.approve) {
sendStructuredInboxMessage(
input.team_name,
sender,
input.recipient,
{
type: "plan_approval",
approved: true,
requestId: input.request_id,
},
"plan_approved",
)
return JSON.stringify({ success: true, message: `plan_approved:${input.recipient}` })
}
sendPlainInboxMessage(
input.team_name,
sender,
input.recipient,
input.content ?? "Plan rejected",
"plan_rejected",
)
return JSON.stringify({ success: true, message: `plan_rejected:${input.recipient}` })
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "send_message_failed" })
}
},
})
}
export function createReadInboxTool(): ToolDefinition {
return tool({
description: "Read inbox messages for a team member.",
args: {
team_name: tool.schema.string().describe("Team name"),
agent_name: tool.schema.string().describe("Member name"),
unread_only: tool.schema.boolean().optional().describe("Return only unread messages"),
mark_as_read: tool.schema.boolean().optional().describe("Mark returned messages as read"),
},
execute: async (args: Record<string, unknown>): Promise<string> => {
try {
const input = TeamReadInboxInputSchema.parse(args)
const messages = readInbox(
input.team_name,
input.agent_name,
input.unread_only ?? false,
input.mark_as_read ?? true,
)
return JSON.stringify(messages)
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "read_inbox_failed" })
}
},
})
}

View File

@@ -0,0 +1,29 @@
const VALID_NAME_RE = /^[A-Za-z0-9_-]+$/
const MAX_NAME_LENGTH = 64
function validateName(value: string, label: "team" | "agent"): string | null {
if (!value || !value.trim()) {
return `${label}_name_required`
}
if (!VALID_NAME_RE.test(value)) {
return `${label}_name_invalid`
}
if (value.length > MAX_NAME_LENGTH) {
return `${label}_name_too_long`
}
return null
}
export function validateTeamName(teamName: string): string | null {
return validateName(teamName, "team")
}
export function validateAgentName(agentName: string): string | null {
if (agentName === "team-lead") {
return "agent_name_reserved"
}
return validateName(agentName, "agent")
}

View File

@@ -0,0 +1,40 @@
import { join } from "node:path"
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
const AGENT_TEAMS_DIR = "agent-teams"
export function getAgentTeamsRootDir(): string {
return join(getOpenCodeConfigDir({ binary: "opencode" }), AGENT_TEAMS_DIR)
}
export function getTeamsRootDir(): string {
return join(getAgentTeamsRootDir(), "teams")
}
export function getTeamTasksRootDir(): string {
return join(getAgentTeamsRootDir(), "tasks")
}
export function getTeamDir(teamName: string): string {
return join(getTeamsRootDir(), teamName)
}
export function getTeamConfigPath(teamName: string): string {
return join(getTeamDir(teamName), "config.json")
}
export function getTeamInboxDir(teamName: string): string {
return join(getTeamDir(teamName), "inboxes")
}
export function getTeamInboxPath(teamName: string, agentName: string): string {
return join(getTeamInboxDir(teamName), `${agentName}.json`)
}
export function getTeamTaskDir(teamName: string): string {
return join(getTeamTasksRootDir(), teamName)
}
export function getTeamTaskPath(teamName: string, taskId: string): string {
return join(getTeamTaskDir(teamName), `${taskId}.json`)
}

View File

@@ -0,0 +1,159 @@
import { existsSync, rmSync } from "node:fs"
import {
acquireLock,
ensureDir,
readJsonSafe,
writeJsonAtomic,
} from "../../features/claude-tasks/storage"
import {
getTeamConfigPath,
getTeamDir,
getTeamInboxDir,
getTeamTaskDir,
getTeamTasksRootDir,
getTeamsRootDir,
} from "./paths"
import {
TEAM_COLOR_PALETTE,
TeamConfig,
TeamConfigSchema,
TeamLeadMember,
TeamMember,
TeamTeammateMember,
isTeammateMember,
} from "./types"
function nowMs(): number {
return Date.now()
}
function withTeamLock<T>(teamName: string, operation: () => T): T {
const teamDir = getTeamDir(teamName)
ensureDir(teamDir)
const lock = acquireLock(teamDir)
if (!lock.acquired) {
throw new Error("team_lock_unavailable")
}
try {
return operation()
} finally {
lock.release()
}
}
function createLeadMember(teamName: string, leadSessionId: string, cwd: string, model: string): TeamLeadMember {
return {
agentId: `team-lead@${teamName}`,
name: "team-lead",
agentType: "team-lead",
model,
joinedAt: nowMs(),
cwd,
subscriptions: [],
}
}
export function ensureTeamStorageDirs(teamName: string): void {
ensureDir(getTeamsRootDir())
ensureDir(getTeamTasksRootDir())
ensureDir(getTeamDir(teamName))
ensureDir(getTeamInboxDir(teamName))
ensureDir(getTeamTaskDir(teamName))
}
export function teamExists(teamName: string): boolean {
return existsSync(getTeamConfigPath(teamName))
}
export function createTeamConfig(
teamName: string,
description: string,
leadSessionId: string,
cwd: string,
model: string,
): TeamConfig {
ensureTeamStorageDirs(teamName)
if (teamExists(teamName)) {
throw new Error("team_already_exists")
}
const leadAgentId = `team-lead@${teamName}`
const config: TeamConfig = {
name: teamName,
description,
createdAt: nowMs(),
leadAgentId,
leadSessionId,
members: [createLeadMember(teamName, leadSessionId, cwd, model)],
}
return withTeamLock(teamName, () => {
writeJsonAtomic(getTeamConfigPath(teamName), TeamConfigSchema.parse(config))
return config
})
}
export function readTeamConfig(teamName: string): TeamConfig | null {
return readJsonSafe(getTeamConfigPath(teamName), TeamConfigSchema)
}
export function readTeamConfigOrThrow(teamName: string): TeamConfig {
const config = readTeamConfig(teamName)
if (!config) {
throw new Error("team_not_found")
}
return config
}
export function writeTeamConfig(teamName: string, config: TeamConfig): TeamConfig {
return withTeamLock(teamName, () => {
const validated = TeamConfigSchema.parse(config)
writeJsonAtomic(getTeamConfigPath(teamName), validated)
return validated
})
}
export function listTeammates(config: TeamConfig): TeamTeammateMember[] {
return config.members.filter(isTeammateMember)
}
export function getTeamMember(config: TeamConfig, name: string): TeamMember | undefined {
return config.members.find((member) => member.name === name)
}
export function upsertTeammate(config: TeamConfig, teammate: TeamTeammateMember): TeamConfig {
const members = config.members.filter((member) => member.name !== teammate.name)
members.push(teammate)
return { ...config, members }
}
export function removeTeammate(config: TeamConfig, agentName: string): TeamConfig {
if (agentName === "team-lead") {
throw new Error("cannot_remove_team_lead")
}
return {
...config,
members: config.members.filter((member) => member.name !== agentName),
}
}
export function assignNextColor(config: TeamConfig): string {
const teammateCount = listTeammates(config).length
return TEAM_COLOR_PALETTE[teammateCount % TEAM_COLOR_PALETTE.length]
}
export function deleteTeamData(teamName: string): void {
const teamDir = getTeamDir(teamName)
const taskDir = getTeamTaskDir(teamName)
if (existsSync(teamDir)) {
rmSync(teamDir, { recursive: true, force: true })
}
if (existsSync(taskDir)) {
rmSync(taskDir, { recursive: true, force: true })
}
}

View File

@@ -0,0 +1,95 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import { getTeamConfigPath } from "./paths"
import { validateTeamName } from "./name-validation"
import { ensureInbox } from "./inbox-store"
import {
TeamCreateInputSchema,
TeamDeleteInputSchema,
TeamReadConfigInputSchema,
TeamToolContext,
} from "./types"
import { createTeamConfig, deleteTeamData, listTeammates, readTeamConfig, readTeamConfigOrThrow } from "./team-config-store"
export function createTeamCreateTool(): ToolDefinition {
return tool({
description: "Create a team workspace with config, inboxes, and task storage.",
args: {
team_name: tool.schema.string().describe("Team name (letters, numbers, hyphens, underscores)"),
description: tool.schema.string().optional().describe("Team description"),
},
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
try {
const input = TeamCreateInputSchema.parse(args)
const nameError = validateTeamName(input.team_name)
if (nameError) {
return JSON.stringify({ error: nameError })
}
const config = createTeamConfig(
input.team_name,
input.description ?? "",
context.sessionID,
process.cwd(),
context.agent ?? "native",
)
ensureInbox(config.name, "team-lead")
return JSON.stringify({
team_name: config.name,
team_file_path: getTeamConfigPath(config.name),
lead_agent_id: config.leadAgentId,
})
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "team_create_failed" })
}
},
})
}
export function createTeamDeleteTool(): ToolDefinition {
return tool({
description: "Delete a team and its stored data. Fails if teammates still exist.",
args: {
team_name: tool.schema.string().describe("Team name"),
},
execute: async (args: Record<string, unknown>): Promise<string> => {
try {
const input = TeamDeleteInputSchema.parse(args)
const config = readTeamConfigOrThrow(input.team_name)
const teammates = listTeammates(config)
if (teammates.length > 0) {
return JSON.stringify({
error: "team_has_active_members",
members: teammates.map((member) => member.name),
})
}
deleteTeamData(input.team_name)
return JSON.stringify({ success: true, team_name: input.team_name })
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "team_delete_failed" })
}
},
})
}
export function createTeamReadConfigTool(): ToolDefinition {
return tool({
description: "Read team configuration and member list.",
args: {
team_name: tool.schema.string().describe("Team name"),
},
execute: async (args: Record<string, unknown>): Promise<string> => {
try {
const input = TeamReadConfigInputSchema.parse(args)
const config = readTeamConfig(input.team_name)
if (!config) {
return JSON.stringify({ error: "team_not_found" })
}
return JSON.stringify(config)
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "team_read_config_failed" })
}
},
})
}

View File

@@ -0,0 +1,93 @@
import type { TeamTask, TeamTaskStatus } from "./types"
type PendingEdges = Record<string, Set<string>>
export const TEAM_TASK_STATUS_ORDER: Record<TeamTaskStatus, number> = {
pending: 0,
in_progress: 1,
completed: 2,
deleted: 3,
}
export type TaskReader = (taskId: string) => TeamTask | null
export function wouldCreateCycle(
fromTaskId: string,
toTaskId: string,
pendingEdges: PendingEdges,
readTask: TaskReader,
): boolean {
const visited = new Set<string>()
const queue: string[] = [toTaskId]
while (queue.length > 0) {
const current = queue.shift()
if (!current) {
continue
}
if (current === fromTaskId) {
return true
}
if (visited.has(current)) {
continue
}
visited.add(current)
const task = readTask(current)
if (task) {
for (const dep of task.blockedBy) {
if (!visited.has(dep)) {
queue.push(dep)
}
}
}
const pending = pendingEdges[current]
if (pending) {
for (const dep of pending) {
if (!visited.has(dep)) {
queue.push(dep)
}
}
}
}
return false
}
export function ensureForwardStatusTransition(current: TeamTaskStatus, next: TeamTaskStatus): void {
const currentOrder = TEAM_TASK_STATUS_ORDER[current]
const nextOrder = TEAM_TASK_STATUS_ORDER[next]
if (nextOrder < currentOrder) {
throw new Error(`invalid_status_transition:${current}->${next}`)
}
}
export function ensureDependenciesCompleted(
status: TeamTaskStatus,
blockedBy: string[],
readTask: TaskReader,
): void {
if (status !== "in_progress" && status !== "completed") {
return
}
for (const blockerId of blockedBy) {
const blocker = readTask(blockerId)
if (blocker && blocker.status !== "completed") {
throw new Error(`blocked_by_incomplete:${blockerId}:${blocker.status}`)
}
}
}
export function createPendingEdgeMap(): PendingEdges {
return {}
}
export function addPendingEdge(pendingEdges: PendingEdges, from: string, to: string): void {
const existing = pendingEdges[from] ?? new Set<string>()
existing.add(to)
pendingEdges[from] = existing
}

View File

@@ -0,0 +1,127 @@
import { existsSync, readdirSync, unlinkSync } from "node:fs"
import { join } from "node:path"
import {
acquireLock,
ensureDir,
generateTaskId,
readJsonSafe,
writeJsonAtomic,
} from "../../features/claude-tasks/storage"
import { getTeamTaskDir, getTeamTaskPath } from "./paths"
import { TeamTask, TeamTaskSchema } from "./types"
function withTaskLock<T>(teamName: string, operation: () => T): T {
const taskDir = getTeamTaskDir(teamName)
ensureDir(taskDir)
const lock = acquireLock(taskDir)
if (!lock.acquired) {
throw new Error("team_task_lock_unavailable")
}
try {
return operation()
} finally {
lock.release()
}
}
export function readTeamTask(teamName: string, taskId: string): TeamTask | null {
return readJsonSafe(getTeamTaskPath(teamName, taskId), TeamTaskSchema)
}
export function readTeamTaskOrThrow(teamName: string, taskId: string): TeamTask {
const task = readTeamTask(teamName, taskId)
if (!task) {
throw new Error("team_task_not_found")
}
return task
}
export function listTeamTasks(teamName: string): TeamTask[] {
const taskDir = getTeamTaskDir(teamName)
if (!existsSync(taskDir)) {
return []
}
const files = readdirSync(taskDir)
.filter((file) => file.endsWith(".json") && file.startsWith("T-"))
.sort((a, b) => a.localeCompare(b))
const tasks: TeamTask[] = []
for (const file of files) {
const taskId = file.replace(/\.json$/, "")
const task = readTeamTask(teamName, taskId)
if (task) {
tasks.push(task)
}
}
return tasks
}
export function createTeamTask(
teamName: string,
subject: string,
description: string,
activeForm?: string,
metadata?: Record<string, unknown>,
): TeamTask {
if (!subject.trim()) {
throw new Error("team_task_subject_required")
}
return withTaskLock(teamName, () => {
const task: TeamTask = {
id: generateTaskId(),
subject,
description,
activeForm,
status: "pending",
blocks: [],
blockedBy: [],
...(metadata ? { metadata } : {}),
}
const validated = TeamTaskSchema.parse(task)
writeJsonAtomic(getTeamTaskPath(teamName, validated.id), validated)
return validated
})
}
export function writeTeamTask(teamName: string, task: TeamTask): TeamTask {
const validated = TeamTaskSchema.parse(task)
writeJsonAtomic(getTeamTaskPath(teamName, validated.id), validated)
return validated
}
export function deleteTeamTaskFile(teamName: string, taskId: string): void {
const taskPath = getTeamTaskPath(teamName, taskId)
if (existsSync(taskPath)) {
unlinkSync(taskPath)
}
}
export function readTaskFromDirectory(taskDir: string, taskId: string): TeamTask | null {
return readJsonSafe(join(taskDir, `${taskId}.json`), TeamTaskSchema)
}
export function resetOwnerTasks(teamName: string, ownerName: string): void {
withTaskLock(teamName, () => {
const tasks = listTeamTasks(teamName)
for (const task of tasks) {
if (task.owner !== ownerName) {
continue
}
const next: TeamTask = {
...task,
owner: undefined,
status: task.status === "completed" ? "completed" : "pending",
}
writeTeamTask(teamName, next)
}
})
}
export function withTeamTaskLock<T>(teamName: string, operation: () => T): T {
return withTaskLock(teamName, operation)
}

View File

@@ -0,0 +1,106 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import { sendStructuredInboxMessage } from "./inbox-store"
import { readTeamConfigOrThrow } from "./team-config-store"
import {
TeamTaskCreateInputSchema,
TeamTaskGetInputSchema,
TeamTaskListInputSchema,
TeamTask,
} from "./types"
import { createTeamTask, listTeamTasks, readTeamTask } from "./team-task-store"
function buildTaskAssignmentPayload(task: TeamTask): Record<string, unknown> {
return {
type: "task_assignment",
taskId: task.id,
subject: task.subject,
description: task.description,
timestamp: new Date().toISOString(),
}
}
export function createTeamTaskCreateTool(): ToolDefinition {
return tool({
description: "Create a task in team-scoped storage.",
args: {
team_name: tool.schema.string().describe("Team name"),
subject: tool.schema.string().describe("Task subject"),
description: tool.schema.string().describe("Task description"),
active_form: tool.schema.string().optional().describe("Present-continuous form"),
metadata: tool.schema.record(tool.schema.string(), tool.schema.unknown()).optional().describe("Task metadata"),
},
execute: async (args: Record<string, unknown>): Promise<string> => {
try {
const input = TeamTaskCreateInputSchema.parse(args)
readTeamConfigOrThrow(input.team_name)
const task = createTeamTask(
input.team_name,
input.subject,
input.description,
input.active_form,
input.metadata,
)
return JSON.stringify(task)
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "team_task_create_failed" })
}
},
})
}
export function createTeamTaskListTool(): ToolDefinition {
return tool({
description: "List tasks for one team.",
args: {
team_name: tool.schema.string().describe("Team name"),
},
execute: async (args: Record<string, unknown>): Promise<string> => {
try {
const input = TeamTaskListInputSchema.parse(args)
readTeamConfigOrThrow(input.team_name)
return JSON.stringify(listTeamTasks(input.team_name))
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "team_task_list_failed" })
}
},
})
}
export function createTeamTaskGetTool(): ToolDefinition {
return tool({
description: "Get one task from team-scoped storage.",
args: {
team_name: tool.schema.string().describe("Team name"),
task_id: tool.schema.string().describe("Task id"),
},
execute: async (args: Record<string, unknown>): Promise<string> => {
try {
const input = TeamTaskGetInputSchema.parse(args)
readTeamConfigOrThrow(input.team_name)
const task = readTeamTask(input.team_name, input.task_id)
if (!task) {
return JSON.stringify({ error: "team_task_not_found" })
}
return JSON.stringify(task)
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "team_task_get_failed" })
}
},
})
}
export function notifyOwnerAssignment(teamName: string, task: TeamTask): void {
if (!task.owner || task.status === "deleted") {
return
}
sendStructuredInboxMessage(
teamName,
"team-lead",
task.owner,
buildTaskAssignmentPayload(task),
"task_assignment",
)
}

View File

@@ -0,0 +1,48 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import { readTeamConfigOrThrow } from "./team-config-store"
import { TeamTaskUpdateInputSchema } from "./types"
import { updateTeamTask } from "./team-task-update"
import { notifyOwnerAssignment } from "./team-task-tools"
export function createTeamTaskUpdateTool(): ToolDefinition {
return tool({
description: "Update task status, owner, dependencies, and metadata in a team task list.",
args: {
team_name: tool.schema.string().describe("Team name"),
task_id: tool.schema.string().describe("Task id"),
status: tool.schema.enum(["pending", "in_progress", "completed", "deleted"]).optional().describe("Task status"),
owner: tool.schema.string().optional().describe("Task owner"),
subject: tool.schema.string().optional().describe("Task subject"),
description: tool.schema.string().optional().describe("Task description"),
active_form: tool.schema.string().optional().describe("Present-continuous form"),
add_blocks: tool.schema.array(tool.schema.string()).optional().describe("Add task ids this task blocks"),
add_blocked_by: tool.schema.array(tool.schema.string()).optional().describe("Add blocker task ids"),
metadata: tool.schema.record(tool.schema.string(), tool.schema.unknown()).optional().describe("Metadata patch (null removes key)"),
},
execute: async (args: Record<string, unknown>): Promise<string> => {
try {
const input = TeamTaskUpdateInputSchema.parse(args)
readTeamConfigOrThrow(input.team_name)
const task = updateTeamTask(input.team_name, input.task_id, {
status: input.status,
owner: input.owner,
subject: input.subject,
description: input.description,
activeForm: input.active_form,
addBlocks: input.add_blocks,
addBlockedBy: input.add_blocked_by,
metadata: input.metadata,
})
if (input.owner !== undefined) {
notifyOwnerAssignment(input.team_name, task)
}
return JSON.stringify(task)
} catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : "team_task_update_failed" })
}
},
})
}

View File

@@ -0,0 +1,210 @@
import { existsSync, readdirSync, unlinkSync } from "node:fs"
import { join } from "node:path"
import { readJsonSafe, writeJsonAtomic } from "../../features/claude-tasks/storage"
import { getTeamTaskDir, getTeamTaskPath } from "./paths"
import {
addPendingEdge,
createPendingEdgeMap,
ensureDependenciesCompleted,
ensureForwardStatusTransition,
wouldCreateCycle,
} from "./team-task-dependency"
import { TeamTask, TeamTaskSchema, TeamTaskStatus } from "./types"
import { withTeamTaskLock } from "./team-task-store"
export interface TeamTaskUpdatePatch {
status?: TeamTaskStatus
owner?: string
subject?: string
description?: string
activeForm?: string
addBlocks?: string[]
addBlockedBy?: string[]
metadata?: Record<string, unknown>
}
function writeTaskToPath(path: string, task: TeamTask): void {
writeJsonAtomic(path, TeamTaskSchema.parse(task))
}
export function updateTeamTask(teamName: string, taskId: string, patch: TeamTaskUpdatePatch): TeamTask {
return withTeamTaskLock(teamName, () => {
const taskDir = getTeamTaskDir(teamName)
const taskPath = getTeamTaskPath(teamName, taskId)
const currentTask = readJsonSafe(taskPath, TeamTaskSchema)
if (!currentTask) {
throw new Error("team_task_not_found")
}
const cache = new Map<string, TeamTask | null>()
cache.set(taskId, currentTask)
const readTask = (id: string): TeamTask | null => {
if (cache.has(id)) {
return cache.get(id) ?? null
}
const loaded = readJsonSafe(join(taskDir, `${id}.json`), TeamTaskSchema)
cache.set(id, loaded)
return loaded
}
const pendingEdges = createPendingEdgeMap()
if (patch.addBlocks) {
for (const blockedTaskId of patch.addBlocks) {
if (blockedTaskId === taskId) {
throw new Error("team_task_self_block")
}
if (!readTask(blockedTaskId)) {
throw new Error(`team_task_reference_not_found:${blockedTaskId}`)
}
addPendingEdge(pendingEdges, blockedTaskId, taskId)
}
for (const blockedTaskId of patch.addBlocks) {
if (wouldCreateCycle(blockedTaskId, taskId, pendingEdges, readTask)) {
throw new Error(`team_task_cycle_detected:${taskId}->${blockedTaskId}`)
}
}
}
if (patch.addBlockedBy) {
for (const blockerId of patch.addBlockedBy) {
if (blockerId === taskId) {
throw new Error("team_task_self_dependency")
}
if (!readTask(blockerId)) {
throw new Error(`team_task_reference_not_found:${blockerId}`)
}
addPendingEdge(pendingEdges, taskId, blockerId)
}
for (const blockerId of patch.addBlockedBy) {
if (wouldCreateCycle(taskId, blockerId, pendingEdges, readTask)) {
throw new Error(`team_task_cycle_detected:${taskId}<-${blockerId}`)
}
}
}
if (patch.status && patch.status !== "deleted") {
ensureForwardStatusTransition(currentTask.status, patch.status)
const effectiveBlockedBy = Array.from(new Set([...(currentTask.blockedBy ?? []), ...(patch.addBlockedBy ?? [])]))
ensureDependenciesCompleted(patch.status, effectiveBlockedBy, readTask)
}
let nextTask: TeamTask = { ...currentTask }
if (patch.subject !== undefined) {
nextTask.subject = patch.subject
}
if (patch.description !== undefined) {
nextTask.description = patch.description
}
if (patch.activeForm !== undefined) {
nextTask.activeForm = patch.activeForm
}
if (patch.owner !== undefined) {
nextTask.owner = patch.owner
}
const pendingWrites = new Map<string, TeamTask>()
if (patch.addBlocks) {
const existingBlocks = new Set(nextTask.blocks)
for (const blockedTaskId of patch.addBlocks) {
if (!existingBlocks.has(blockedTaskId)) {
nextTask.blocks.push(blockedTaskId)
existingBlocks.add(blockedTaskId)
}
const otherPath = getTeamTaskPath(teamName, blockedTaskId)
const other = pendingWrites.get(otherPath) ?? readTask(blockedTaskId)
if (other && !other.blockedBy.includes(taskId)) {
pendingWrites.set(otherPath, { ...other, blockedBy: [...other.blockedBy, taskId] })
}
}
}
if (patch.addBlockedBy) {
const existingBlockedBy = new Set(nextTask.blockedBy)
for (const blockerId of patch.addBlockedBy) {
if (!existingBlockedBy.has(blockerId)) {
nextTask.blockedBy.push(blockerId)
existingBlockedBy.add(blockerId)
}
const otherPath = getTeamTaskPath(teamName, blockerId)
const other = pendingWrites.get(otherPath) ?? readTask(blockerId)
if (other && !other.blocks.includes(taskId)) {
pendingWrites.set(otherPath, { ...other, blocks: [...other.blocks, taskId] })
}
}
}
if (patch.metadata !== undefined) {
const merged: Record<string, unknown> = { ...(nextTask.metadata ?? {}) }
for (const [key, value] of Object.entries(patch.metadata)) {
if (value === null) {
delete merged[key]
} else {
merged[key] = value
}
}
nextTask.metadata = Object.keys(merged).length > 0 ? merged : undefined
}
if (patch.status !== undefined) {
nextTask.status = patch.status
}
const allTaskFiles = readdirSync(taskDir).filter((file) => file.endsWith(".json") && file.startsWith("T-"))
if (nextTask.status === "completed") {
for (const file of allTaskFiles) {
const otherId = file.replace(/\.json$/, "")
if (otherId === taskId) continue
const otherPath = getTeamTaskPath(teamName, otherId)
const other = pendingWrites.get(otherPath) ?? readTask(otherId)
if (other?.blockedBy.includes(taskId)) {
pendingWrites.set(otherPath, {
...other,
blockedBy: other.blockedBy.filter((id) => id !== taskId),
})
}
}
}
if (patch.status === "deleted") {
for (const file of allTaskFiles) {
const otherId = file.replace(/\.json$/, "")
if (otherId === taskId) continue
const otherPath = getTeamTaskPath(teamName, otherId)
const other = pendingWrites.get(otherPath) ?? readTask(otherId)
if (!other) continue
const nextOther = {
...other,
blockedBy: other.blockedBy.filter((id) => id !== taskId),
blocks: other.blocks.filter((id) => id !== taskId),
}
pendingWrites.set(otherPath, nextOther)
}
}
for (const [path, task] of pendingWrites.entries()) {
writeTaskToPath(path, task)
}
if (patch.status === "deleted") {
if (existsSync(taskPath)) {
unlinkSync(taskPath)
}
return TeamTaskSchema.parse({ ...nextTask, status: "deleted" })
}
writeTaskToPath(taskPath, nextTask)
return TeamTaskSchema.parse(nextTask)
})
}

View File

@@ -0,0 +1,148 @@
import type { BackgroundManager } from "../../features/background-agent"
import { ensureInbox, sendPlainInboxMessage } from "./inbox-store"
import { assignNextColor, getTeamMember, readTeamConfigOrThrow, removeTeammate, upsertTeammate, writeTeamConfig } from "./team-config-store"
import type { TeamTeammateMember, TeamToolContext } from "./types"
function parseModel(model: string | undefined): { providerID: string; modelID: string } | undefined {
if (!model) {
return undefined
}
const [providerID, modelID] = model.split("/", 2)
if (!providerID || !modelID) {
return undefined
}
return { providerID, modelID }
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function buildLaunchPrompt(teamName: string, teammateName: string, userPrompt: string): string {
return [
`You are teammate "${teammateName}" in team "${teamName}".`,
`When you need updates, call read_inbox with team_name="${teamName}" and agent_name="${teammateName}".`,
"Initial assignment:",
userPrompt,
].join("\n\n")
}
function buildDeliveryPrompt(teamName: string, teammateName: string, summary: string, content: string): string {
return [
`New team message for "${teammateName}" in team "${teamName}".`,
`Summary: ${summary}`,
"Content:",
content,
].join("\n\n")
}
export interface SpawnTeammateParams {
teamName: string
name: string
prompt: string
subagentType: string
model?: string
planModeRequired: boolean
context: TeamToolContext
manager: BackgroundManager
}
export async function spawnTeammate(params: SpawnTeammateParams): Promise<TeamTeammateMember> {
const config = readTeamConfigOrThrow(params.teamName)
if (getTeamMember(config, params.name)) {
throw new Error("teammate_already_exists")
}
const teammate: TeamTeammateMember = {
agentId: `${params.name}@${params.teamName}`,
name: params.name,
agentType: params.subagentType,
model: params.model ?? "native",
prompt: params.prompt,
color: assignNextColor(config),
planModeRequired: params.planModeRequired,
joinedAt: Date.now(),
cwd: process.cwd(),
subscriptions: [],
backendType: "native",
isActive: false,
}
writeTeamConfig(params.teamName, upsertTeammate(config, teammate))
ensureInbox(params.teamName, params.name)
sendPlainInboxMessage(params.teamName, "team-lead", params.name, params.prompt, "initial_prompt", teammate.color)
try {
const resolvedModel = parseModel(params.model)
const launched = await params.manager.launch({
description: `[team:${params.teamName}] ${params.name}`,
prompt: buildLaunchPrompt(params.teamName, params.name, params.prompt),
agent: params.subagentType,
parentSessionID: params.context.sessionID,
parentMessageID: params.context.messageID,
...(resolvedModel ? { model: resolvedModel } : {}),
parentAgent: params.context.agent,
})
const start = Date.now()
let sessionID = launched.sessionID
while (!sessionID && Date.now() - start < 30_000) {
await delay(50)
const task = params.manager.getTask(launched.id)
sessionID = task?.sessionID
}
const nextMember: TeamTeammateMember = {
...teammate,
isActive: true,
backgroundTaskID: launched.id,
...(sessionID ? { sessionID } : {}),
}
const current = readTeamConfigOrThrow(params.teamName)
writeTeamConfig(params.teamName, upsertTeammate(current, nextMember))
return nextMember
} catch (error) {
const rollback = readTeamConfigOrThrow(params.teamName)
writeTeamConfig(params.teamName, removeTeammate(rollback, params.name))
throw error
}
}
export async function resumeTeammateWithMessage(
manager: BackgroundManager,
context: TeamToolContext,
teamName: string,
teammate: TeamTeammateMember,
summary: string,
content: string,
): Promise<void> {
if (!teammate.sessionID) {
return
}
try {
await manager.resume({
sessionId: teammate.sessionID,
prompt: buildDeliveryPrompt(teamName, teammate.name, summary, content),
parentSessionID: context.sessionID,
parentMessageID: context.messageID,
parentAgent: context.agent,
})
} catch {
return
}
}
export async function cancelTeammateRun(manager: BackgroundManager, teammate: TeamTeammateMember): Promise<void> {
if (!teammate.backgroundTaskID) {
return
}
await manager.cancelTask(teammate.backgroundTaskID, {
source: "team_force_kill",
abortSession: true,
skipNotification: true,
})
}

View File

@@ -0,0 +1,121 @@
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, 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 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)
writeTeamConfig(input.team_name, removeTeammate(config, 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(): 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)
if (input.agent_name === "team-lead") {
return JSON.stringify({ error: "cannot_shutdown_team_lead" })
}
const config = readTeamConfigOrThrow(input.team_name)
const member = getTeamMember(config, input.agent_name)
if (!member || !isTeammateMember(member)) {
return JSON.stringify({ error: "teammate_not_found" })
}
writeTeamConfig(input.team_name, removeTeammate(config, 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" })
}
},
})
}

View File

@@ -0,0 +1,24 @@
import type { ToolDefinition } from "@opencode-ai/plugin"
import type { BackgroundManager } from "../../features/background-agent"
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"
export function createAgentTeamsTools(manager: BackgroundManager): Record<string, ToolDefinition> {
return {
team_create: createTeamCreateTool(),
team_delete: createTeamDeleteTool(),
spawn_teammate: createSpawnTeammateTool(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(),
}
}

View File

@@ -0,0 +1,180 @@
import { z } from "zod"
export const TEAM_COLOR_PALETTE = [
"blue",
"green",
"yellow",
"purple",
"orange",
"pink",
"cyan",
"red",
] as const
export const TeamTaskStatusSchema = z.enum(["pending", "in_progress", "completed", "deleted"])
export type TeamTaskStatus = z.infer<typeof TeamTaskStatusSchema>
export const TeamLeadMemberSchema = z.object({
agentId: z.string(),
name: z.literal("team-lead"),
agentType: z.literal("team-lead"),
model: z.string(),
joinedAt: z.number(),
cwd: z.string(),
subscriptions: z.array(z.unknown()).default([]),
}).strict()
export const TeamTeammateMemberSchema = z.object({
agentId: z.string(),
name: z.string(),
agentType: z.string(),
model: z.string(),
prompt: z.string(),
color: z.string(),
planModeRequired: z.boolean().default(false),
joinedAt: z.number(),
cwd: z.string(),
subscriptions: z.array(z.unknown()).default([]),
backendType: z.literal("native").default("native"),
isActive: z.boolean().default(false),
sessionID: z.string().optional(),
backgroundTaskID: z.string().optional(),
}).strict()
export const TeamMemberSchema = z.union([TeamLeadMemberSchema, TeamTeammateMemberSchema])
export const TeamConfigSchema = z.object({
name: z.string(),
description: z.string().default(""),
createdAt: z.number(),
leadAgentId: z.string(),
leadSessionId: z.string(),
members: z.array(TeamMemberSchema),
}).strict()
export const TeamInboxMessageSchema = z.object({
from: z.string(),
text: z.string(),
timestamp: z.string(),
read: z.boolean().default(false),
summary: z.string().optional(),
color: z.string().optional(),
}).strict()
export const TeamTaskSchema = z.object({
id: z.string(),
subject: z.string(),
description: z.string(),
activeForm: z.string().optional(),
status: TeamTaskStatusSchema,
blocks: z.array(z.string()).default([]),
blockedBy: z.array(z.string()).default([]),
owner: z.string().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
}).strict()
export const TeamSendMessageTypeSchema = z.enum([
"message",
"broadcast",
"shutdown_request",
"shutdown_response",
"plan_approval_response",
])
export type TeamLeadMember = z.infer<typeof TeamLeadMemberSchema>
export type TeamTeammateMember = z.infer<typeof TeamTeammateMemberSchema>
export type TeamMember = z.infer<typeof TeamMemberSchema>
export type TeamConfig = z.infer<typeof TeamConfigSchema>
export type TeamInboxMessage = z.infer<typeof TeamInboxMessageSchema>
export type TeamTask = z.infer<typeof TeamTaskSchema>
export type TeamSendMessageType = z.infer<typeof TeamSendMessageTypeSchema>
export const TeamCreateInputSchema = z.object({
team_name: z.string(),
description: z.string().optional(),
})
export const TeamDeleteInputSchema = z.object({
team_name: z.string(),
})
export const TeamReadConfigInputSchema = z.object({
team_name: z.string(),
})
export const TeamSpawnInputSchema = z.object({
team_name: z.string(),
name: z.string(),
prompt: z.string(),
subagent_type: z.string().optional(),
model: z.string().optional(),
plan_mode_required: z.boolean().optional(),
})
export const TeamSendMessageInputSchema = z.object({
team_name: z.string(),
type: TeamSendMessageTypeSchema,
recipient: z.string().optional(),
content: z.string().optional(),
summary: z.string().optional(),
request_id: z.string().optional(),
approve: z.boolean().optional(),
sender: z.string().optional(),
})
export const TeamReadInboxInputSchema = z.object({
team_name: z.string(),
agent_name: z.string(),
unread_only: z.boolean().optional(),
mark_as_read: z.boolean().optional(),
})
export const TeamTaskCreateInputSchema = z.object({
team_name: z.string(),
subject: z.string(),
description: z.string(),
active_form: z.string().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
})
export const TeamTaskUpdateInputSchema = z.object({
team_name: z.string(),
task_id: z.string(),
status: TeamTaskStatusSchema.optional(),
owner: z.string().optional(),
subject: z.string().optional(),
description: z.string().optional(),
active_form: z.string().optional(),
add_blocks: z.array(z.string()).optional(),
add_blocked_by: z.array(z.string()).optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
})
export const TeamTaskListInputSchema = z.object({
team_name: z.string(),
})
export const TeamTaskGetInputSchema = z.object({
team_name: z.string(),
task_id: z.string(),
})
export const TeamForceKillInputSchema = z.object({
team_name: z.string(),
agent_name: z.string(),
})
export const TeamProcessShutdownInputSchema = z.object({
team_name: z.string(),
agent_name: z.string(),
})
export interface TeamToolContext {
sessionID: string
messageID: string
agent?: string
}
export function isTeammateMember(member: TeamMember): member is TeamTeammateMember {
return member.agentType !== "team-lead"
}

View File

@@ -37,6 +37,7 @@ type OpencodeClient = PluginInput["client"]
export { createCallOmoAgent } from "./call-omo-agent"
export { createLookAt } from "./look-at"
export { createDelegateTask } from "./delegate-task"
export { createAgentTeamsTools } from "./agent-teams"
export {
createTaskCreateTool,
createTaskGetTool,