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]; } export function registerDirigentTools(deps: ToolDeps): void { const { api, channelStore, identityRegistry, moderatorBotToken, scheduleIdentifier, onDiscussionCreate } = deps; // ─────────────────────────────────────────────── // dirigent-register // ─────────────────────────────────────────────── api.registerTool({ name: "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"], }, handler: async (params, ctx) => { const agentId = ctx?.agentId; if (!agentId) return { content: [{ type: "text", text: "Cannot resolve agentId from session context" }], isError: true }; const p = params as { discordUserId: string; agentName?: string }; identityRegistry.upsert({ agentId, discordUserId: p.discordUserId, agentName: p.agentName ?? agentId, }); return { content: [{ type: "text", text: `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"; callerCtx: { agentId?: string }; }): 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", 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"], }, handler: async (params, ctx) => { 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", callerCtx: { agentId: ctx?.agentId }, }); if (!result.ok) return { content: [{ type: "text", text: `Failed: ${result.error}` }], isError: true }; return { content: [{ type: "text", text: `Created chat channel: ${result.channelId}` }] }; }, }); // ─────────────────────────────────────────────── // create-report-channel // ─────────────────────────────────────────────── api.registerTool({ name: "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"], }, handler: async (params, ctx) => { 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", callerCtx: { agentId: ctx?.agentId }, }); if (!result.ok) return { content: [{ type: "text", text: `Failed: ${result.error}` }], isError: true }; return { content: [{ type: "text", text: `Created report channel: ${result.channelId}` }] }; }, }); // ─────────────────────────────────────────────── // create-work-channel // ─────────────────────────────────────────────── api.registerTool({ name: "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"], }, handler: async (params, ctx) => { 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", callerCtx: { agentId: ctx?.agentId }, }); if (!result.ok) return { content: [{ type: "text", text: `Failed: ${result.error}` }], isError: true }; return { content: [{ type: "text", text: `Created work channel: ${result.channelId}` }] }; }, }); // ─────────────────────────────────────────────── // create-discussion-channel // ─────────────────────────────────────────────── api.registerTool({ name: "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"], }, handler: async (params, ctx) => { const p = params as { callbackGuildId: string; callbackChannelId: string; name: string; discussionGuide: string; participants: string[]; }; const initiatorAgentId = ctx?.agentId; if (!initiatorAgentId) { return { content: [{ type: "text", text: "Cannot resolve initiator agentId from session" }], isError: true }; } if (!moderatorBotToken) { return { content: [{ type: "text", text: "moderatorBotToken not configured" }], isError: true }; } if (!onDiscussionCreate) { return { content: [{ type: "text", text: "Discussion service not available" }], isError: true }; } 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 { content: [{ type: "text", text: `Failed to create channel: ${String(err)}` }], isError: true }; } try { channelStore.setLockedMode(channelId, "discussion", { initiatorAgentId, callbackGuildId: p.callbackGuildId, callbackChannelId: p.callbackChannelId, concluded: false, }); } catch (err) { return { content: [{ type: "text", text: `Failed to register channel: ${String(err)}` }], isError: true }; } await onDiscussionCreate({ channelId, guildId: p.callbackGuildId, initiatorAgentId, callbackGuildId: p.callbackGuildId, callbackChannelId: p.callbackChannelId, discussionGuide: p.discussionGuide, participants: p.participants, }); return { content: [{ type: "text", text: `Discussion channel created: ${channelId}` }] }; }, }); // ─────────────────────────────────────────────── // discussion-complete // ─────────────────────────────────────────────── api.registerTool({ name: "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"], }, handler: async (params, ctx) => { const p = params as { discussionChannelId: string; summary: string }; const callerAgentId = ctx?.agentId; if (!callerAgentId) { return { content: [{ type: "text", text: "Cannot resolve agentId from session" }], isError: true }; } const rec = channelStore.getRecord(p.discussionChannelId); if (rec.mode !== "discussion") { return { content: [{ type: "text", text: `Channel ${p.discussionChannelId} is not a discussion channel` }], isError: true }; } if (!rec.discussion) { return { content: [{ type: "text", text: "Discussion metadata not found" }], isError: true }; } if (rec.discussion.initiatorAgentId !== callerAgentId) { return { content: [{ type: "text", text: `Only the initiator (${rec.discussion.initiatorAgentId}) may call discussion-complete` }], isError: true, }; } if (!p.summary.includes("discussion-summary")) { return { content: [{ type: "text", text: "Summary path must be under {workspace}/discussion-summary/" }], isError: true, }; } 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 { content: [{ type: "text", text: `Discussion ${p.discussionChannelId} concluded. Summary posted to ${rec.discussion.callbackChannelId}.` }] }; }, }); }