import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { ChannelStore } from "../core/channel-store.js"; import type { IdentityRegistry } from "../core/identity-registry.js"; import { createDiscordChannel, getBotUserIdFromToken } from "../core/moderator-discord.js"; import { setSpeakerList } from "../turn-manager.js"; type ToolDeps = { api: OpenClawPluginApi; channelStore: ChannelStore; identityRegistry: IdentityRegistry; moderatorBotToken: string | undefined; scheduleIdentifier: string; /** Called by create-discussion-channel to initialize the discussion. */ onDiscussionCreate?: (params: { channelId: string; guildId: string; initiatorAgentId: string; callbackGuildId: string; callbackChannelId: string; discussionGuide: string; participants: string[]; }) => Promise; }; function getGuildIdFromSessionKey(sessionKey: string): string | undefined { // sessionKey doesn't encode guild — it's not available directly. // Guild is passed explicitly by the agent. return undefined; } function parseDiscordChannelIdFromSession(sessionKey: string): string | undefined { const m = sessionKey.match(/:discord:channel:(\d+)$/); return m?.[1]; } function textResult(text: string) { return { content: [{ type: "text" as const, text }], details: undefined }; } function errorResult(text: string) { return { content: [{ type: "text" as const, text }], details: { error: true } }; } export function registerDirigentTools(deps: ToolDeps): void { const { api, channelStore, identityRegistry, moderatorBotToken, scheduleIdentifier, onDiscussionCreate } = deps; // ─────────────────────────────────────────────── // dirigent-register // ─────────────────────────────────────────────── api.registerTool((ctx) => ({ name: "dirigent-register", label: "Dirigent Register", description: "Register or update this agent's Discord user ID in Dirigent's identity registry.", parameters: { type: "object", additionalProperties: false, properties: { discordUserId: { type: "string", description: "The agent's Discord user ID" }, agentName: { type: "string", description: "Display name (optional, defaults to agentId)" }, }, required: ["discordUserId"], }, execute: async (_toolCallId: string, params: unknown) => { const agentId = ctx?.agentId; if (!agentId) return errorResult("Cannot resolve agentId from session context"); const p = params as { discordUserId: string; agentName?: string }; identityRegistry.upsert({ agentId, discordUserId: p.discordUserId, agentName: p.agentName ?? agentId, }); return textResult(`Registered: agentId=${agentId} discordUserId=${p.discordUserId}`); }, })); // ─────────────────────────────────────────────── // Helper: create channel + set mode // ─────────────────────────────────────────────── async function createManagedChannel(opts: { guildId: string; name: string; memberDiscordIds: string[]; mode: "chat" | "report" | "work"; }): Promise<{ ok: boolean; channelId?: string; error?: string }> { if (!moderatorBotToken) return { ok: false, error: "moderatorBotToken not configured" }; const botId = getBotUserIdFromToken(moderatorBotToken); const overwrites: Array<{ id: string; type: number; allow?: string; deny?: string }> = [ { id: opts.guildId, type: 0, deny: "1024" }, // deny everyone ]; if (botId) overwrites.push({ id: botId, type: 1, allow: "1024" }); for (const uid of opts.memberDiscordIds) { if (uid) overwrites.push({ id: uid, type: 1, allow: "1024" }); } let channelId: string; try { channelId = await createDiscordChannel({ token: moderatorBotToken, guildId: opts.guildId, name: opts.name, permissionOverwrites: overwrites, logger: api.logger, }); } catch (err) { return { ok: false, error: String(err) }; } try { channelStore.setLockedMode(channelId, opts.mode); } catch { channelStore.setMode(channelId, opts.mode); } return { ok: true, channelId }; } // ─────────────────────────────────────────────── // create-chat-channel // ─────────────────────────────────────────────── api.registerTool({ name: "create-chat-channel", label: "Create Chat Channel", description: "Create a new private Discord channel in the specified guild with mode=chat.", parameters: { type: "object", additionalProperties: false, properties: { guildId: { type: "string", description: "Guild ID to create the channel in" }, name: { type: "string", description: "Channel name" }, participants: { type: "array", items: { type: "string" }, description: "Discord user IDs to add (moderator bot always added)", }, }, required: ["guildId", "name"], }, execute: async (_toolCallId: string, params: unknown) => { const p = params as { guildId: string; name: string; participants?: string[] }; const result = await createManagedChannel({ guildId: p.guildId, name: p.name, memberDiscordIds: p.participants ?? [], mode: "chat", }); if (!result.ok) return errorResult(`Failed: ${result.error}`); return textResult(`Created chat channel: ${result.channelId}`); }, }); // ─────────────────────────────────────────────── // create-report-channel // ─────────────────────────────────────────────── api.registerTool({ name: "create-report-channel", label: "Create Report Channel", description: "Create a new private Discord channel with mode=report. Agents can post to it but are not woken by messages.", parameters: { type: "object", additionalProperties: false, properties: { guildId: { type: "string", description: "Guild ID" }, name: { type: "string", description: "Channel name" }, members: { type: "array", items: { type: "string" }, description: "Discord user IDs to add" }, }, required: ["guildId", "name"], }, execute: async (_toolCallId: string, params: unknown) => { const p = params as { guildId: string; name: string; members?: string[] }; const result = await createManagedChannel({ guildId: p.guildId, name: p.name, memberDiscordIds: p.members ?? [], mode: "report", }); if (!result.ok) return errorResult(`Failed: ${result.error}`); return textResult(`Created report channel: ${result.channelId}`); }, }); // ─────────────────────────────────────────────── // create-work-channel // ─────────────────────────────────────────────── api.registerTool((ctx) => ({ name: "create-work-channel", label: "Create Work Channel", description: "Create a new private Discord workspace channel with mode=work (turn-manager disabled, mode locked).", parameters: { type: "object", additionalProperties: false, properties: { guildId: { type: "string", description: "Guild ID" }, name: { type: "string", description: "Channel name" }, members: { type: "array", items: { type: "string" }, description: "Additional Discord user IDs to add" }, }, required: ["guildId", "name"], }, execute: async (_toolCallId: string, params: unknown) => { const p = params as { guildId: string; name: string; members?: string[] }; // Include calling agent's Discord ID if known const callerDiscordId = ctx?.agentId ? identityRegistry.findByAgentId(ctx.agentId)?.discordUserId : undefined; const members = [...(p.members ?? [])]; if (callerDiscordId && !members.includes(callerDiscordId)) members.push(callerDiscordId); const result = await createManagedChannel({ guildId: p.guildId, name: p.name, memberDiscordIds: members, mode: "work", }); if (!result.ok) return errorResult(`Failed: ${result.error}`); return textResult(`Created work channel: ${result.channelId}`); }, })); // ─────────────────────────────────────────────── // create-discussion-channel // ─────────────────────────────────────────────── api.registerTool((ctx) => ({ name: "create-discussion-channel", label: "Create Discussion Channel", description: "Create a structured discussion channel between agents. The calling agent becomes the initiator.", parameters: { type: "object", additionalProperties: false, properties: { callbackGuildId: { type: "string", description: "Guild ID of your current channel (for callback after discussion)" }, callbackChannelId: { type: "string", description: "Channel ID to post the summary to after discussion completes" }, name: { type: "string", description: "Discussion channel name" }, discussionGuide: { type: "string", description: "Topic, goals, and completion criteria for the discussion" }, participants: { type: "array", items: { type: "string" }, description: "Discord user IDs of participating agents" }, }, required: ["callbackGuildId", "callbackChannelId", "name", "discussionGuide", "participants"], }, execute: async (_toolCallId: string, params: unknown) => { const p = params as { callbackGuildId: string; callbackChannelId: string; name: string; discussionGuide: string; participants: string[]; }; const initiatorAgentId = ctx?.agentId; if (!initiatorAgentId) { return errorResult("Cannot resolve initiator agentId from session"); } if (!moderatorBotToken) { return errorResult("moderatorBotToken not configured"); } if (!onDiscussionCreate) { return errorResult("Discussion service not available"); } const botId = getBotUserIdFromToken(moderatorBotToken); const initiatorDiscordId = identityRegistry.findByAgentId(initiatorAgentId)?.discordUserId; const memberIds = [...new Set([ ...(initiatorDiscordId ? [initiatorDiscordId] : []), ...p.participants, ...(botId ? [botId] : []), ])]; const overwrites: Array<{ id: string; type: number; allow?: string; deny?: string }> = [ { id: p.callbackGuildId, type: 0, deny: "1024" }, ...memberIds.map((id) => ({ id, type: 1, allow: "1024" })), ]; let channelId: string; try { channelId = await createDiscordChannel({ token: moderatorBotToken, guildId: p.callbackGuildId, name: p.name, permissionOverwrites: overwrites, logger: api.logger, }); } catch (err) { return errorResult(`Failed to create channel: ${String(err)}`); } try { channelStore.setLockedMode(channelId, "discussion", { initiatorAgentId, callbackGuildId: p.callbackGuildId, callbackChannelId: p.callbackChannelId, concluded: false, }); } catch (err) { return errorResult(`Failed to register channel: ${String(err)}`); } await onDiscussionCreate({ channelId, guildId: p.callbackGuildId, initiatorAgentId, callbackGuildId: p.callbackGuildId, callbackChannelId: p.callbackChannelId, discussionGuide: p.discussionGuide, participants: p.participants, }); return textResult(`Discussion channel created: ${channelId}`); }, })); // ─────────────────────────────────────────────── // discussion-complete // ─────────────────────────────────────────────── api.registerTool((ctx) => ({ name: "discussion-complete", label: "Discussion Complete", description: "Mark a discussion as complete, archive the channel, and post the summary path to the callback channel.", parameters: { type: "object", additionalProperties: false, properties: { discussionChannelId: { type: "string", description: "The discussion channel ID" }, summary: { type: "string", description: "File path to the summary (must be under {workspace}/discussion-summary/)" }, }, required: ["discussionChannelId", "summary"], }, execute: async (_toolCallId: string, params: unknown) => { const p = params as { discussionChannelId: string; summary: string }; const callerAgentId = ctx?.agentId; if (!callerAgentId) { return errorResult("Cannot resolve agentId from session"); } const rec = channelStore.getRecord(p.discussionChannelId); if (rec.mode !== "discussion") { return errorResult(`Channel ${p.discussionChannelId} is not a discussion channel`); } if (!rec.discussion) { return errorResult("Discussion metadata not found"); } if (rec.discussion.initiatorAgentId !== callerAgentId) { return errorResult(`Only the initiator (${rec.discussion.initiatorAgentId}) may call discussion-complete`); } if (!p.summary.includes("discussion-summary")) { return errorResult("Summary path must be under {workspace}/discussion-summary/"); } channelStore.concludeDiscussion(p.discussionChannelId); if (moderatorBotToken) { const { sendModeratorMessage } = await import("../core/moderator-discord.js"); await sendModeratorMessage( moderatorBotToken, rec.discussion.callbackChannelId, `Discussion complete. Summary: ${p.summary}`, api.logger, ).catch(() => undefined); } return textResult(`Discussion ${p.discussionChannelId} concluded. Summary posted to ${rec.discussion.callbackChannelId}.`); }, })); }