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 { validateAgentNameOrLead, validateTeamName } from "./name-validation" import { resumeTeammateWithMessage } from "./teammate-runtime" import { TeamConfig, TeamReadInboxInputSchema, TeamSendMessageInputSchema, TeamToolContext, isTeammateMember, } from "./types" function nowIso(): string { return new Date().toISOString() } function resolveSenderFromContext(config: TeamConfig, context: TeamToolContext): string | null { if (context.sessionID === config.leadSessionId) { return "team-lead" } const matchedMember = config.members.find((member) => isTeammateMember(member) && member.sessionID === context.sessionID) return matchedMember?.name ?? null } 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, context: TeamToolContext): Promise => { try { const input = TeamSendMessageInputSchema.parse(args) const teamError = validateTeamName(input.team_name) if (teamError) { return JSON.stringify({ error: teamError }) } const requestedSender = input.sender const senderError = requestedSender ? validateAgentNameOrLead(requestedSender) : null if (senderError) { return JSON.stringify({ error: senderError }) } const config = readTeamConfigOrThrow(input.team_name) const actor = resolveSenderFromContext(config, context) if (!actor) { return JSON.stringify({ error: "unauthorized_sender_session" }) } if (requestedSender && requestedSender !== actor) { return JSON.stringify({ error: "sender_context_mismatch" }) } const sender = requestedSender ?? actor const memberNames = new Set(config.members.map((member) => member.name)) if (sender !== "team-lead" && !memberNames.has(sender)) { return JSON.stringify({ error: "invalid_sender" }) } 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, sender, input.recipient, { type: "shutdown_request", requestId, from: sender, 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, context: TeamToolContext): Promise => { try { const input = TeamReadInboxInputSchema.parse(args) const teamError = validateTeamName(input.team_name) if (teamError) { return JSON.stringify({ error: teamError }) } const agentError = validateAgentNameOrLead(input.agent_name) if (agentError) { return JSON.stringify({ error: agentError }) } const config = readTeamConfigOrThrow(input.team_name) const actor = resolveSenderFromContext(config, context) if (!actor) { return JSON.stringify({ error: "unauthorized_reader_session" }) } if (actor !== "team-lead" && actor !== input.agent_name) { return JSON.stringify({ error: "unauthorized_reader_session" }) } 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" }) } }, }) }