import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { evaluateDecision, type Decision, type DirigentConfig } from "../rules.js"; import { checkTurn } from "../turn-manager.js"; import { deriveDecisionInputFromPrompt } from "../decision-input.js"; type DebugConfig = { enableDebugLogs?: boolean; debugLogChannelIds?: string[]; }; type DecisionRecord = { decision: Decision; createdAt: number; needsRestore?: boolean; }; type BeforeModelResolveDeps = { api: OpenClawPluginApi; baseConfig: DirigentConfig; sessionDecision: Map; sessionAllowed: Map; sessionChannelId: Map; sessionAccountId: Map; recordDiscussionSession?: (channelId: string, sessionKey: string) => void; forceNoReplySessions: Set; policyState: { channelPolicies: Record }; DECISION_TTL_MS: number; ensurePolicyStateLoaded: (api: OpenClawPluginApi, config: DirigentConfig) => void; resolveAccountId: (api: OpenClawPluginApi, agentId: string) => string | undefined; pruneDecisionMap: () => void; shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise | void; isMultiMessageMode: (channelId: string) => boolean; discussionService?: { isClosedDiscussion: (channelId: string) => boolean; }; }; export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): void { const { api, baseConfig, sessionDecision, sessionAllowed, sessionChannelId, sessionAccountId, recordDiscussionSession, forceNoReplySessions, policyState, DECISION_TTL_MS, ensurePolicyStateLoaded, resolveAccountId, pruneDecisionMap, shouldDebugLog, ensureTurnOrder, isMultiMessageMode, discussionService, } = deps; api.on("before_model_resolve", async (event, ctx) => { const key = ctx.sessionKey; if (!key) return; const live = baseConfig as DirigentConfig & DebugConfig; ensurePolicyStateLoaded(api, live); if (forceNoReplySessions.has(key)) { return { model: ctx.model, provider: ctx.provider, noReply: true, }; } const prompt = ((event as Record).prompt as string) || ""; if (live.enableDebugLogs) { api.logger.info( `dirigent: DEBUG_BEFORE_MODEL_RESOLVE ctx=${JSON.stringify({ sessionKey: ctx.sessionKey, messageProvider: ctx.messageProvider, agentId: ctx.agentId })} ` + `promptPreview=${prompt.slice(0, 300)}`, ); } const derived = deriveDecisionInputFromPrompt({ prompt, messageProvider: ctx.messageProvider, sessionKey: key, ctx: ctx as Record, event: event as Record, }); const hasConvMarker = prompt.includes("Conversation info (untrusted metadata):"); if (live.discordOnly !== false && (!hasConvMarker || derived.channel !== "discord")) return; if (derived.channelId) { sessionChannelId.set(key, derived.channelId); recordDiscussionSession?.(derived.channelId, key); if (discussionService?.isClosedDiscussion(derived.channelId)) { sessionAllowed.set(key, false); api.logger.info(`dirigent: before_model_resolve forcing no-reply for closed discussion channel=${derived.channelId} session=${key}`); return { model: ctx.model, provider: ctx.provider, noReply: true, }; } if (isMultiMessageMode(derived.channelId)) { sessionAllowed.set(key, false); api.logger.info(`dirigent: before_model_resolve forcing no-reply for multi-message mode channel=${derived.channelId} session=${key}`); return { model: ctx.model, provider: ctx.provider, noReply: true, }; } } const resolvedAccountId = resolveAccountId(api, ctx.agentId || ""); if (resolvedAccountId) { sessionAccountId.set(key, resolvedAccountId); } let rec = sessionDecision.get(key); if (!rec || Date.now() - rec.createdAt > DECISION_TTL_MS) { if (rec) sessionDecision.delete(key); const decision = evaluateDecision({ config: live, channel: derived.channel, channelId: derived.channelId, channelPolicies: policyState.channelPolicies as Record, senderId: derived.senderId, content: derived.content, }); rec = { decision, createdAt: Date.now() }; sessionDecision.set(key, rec); pruneDecisionMap(); if (shouldDebugLog(live, derived.channelId)) { api.logger.info( `dirigent: debug before_model_resolve recompute session=${key} ` + `channel=${derived.channel} channelId=${derived.channelId ?? ""} senderId=${derived.senderId ?? ""} ` + `convSenderId=${String((derived.conv as Record).sender_id ?? "")} ` + `convSender=${String((derived.conv as Record).sender ?? "")} ` + `convChannelId=${String((derived.conv as Record).channel_id ?? "")} ` + `decision=${decision.reason} shouldNoReply=${decision.shouldUseNoReply} shouldInject=${decision.shouldInjectEndMarkerPrompt}`, ); } } if (derived.channelId) { await ensureTurnOrder(api, derived.channelId); const accountId = resolveAccountId(api, ctx.agentId || ""); if (accountId) { const turnCheck = checkTurn(derived.channelId, accountId); if (!turnCheck.allowed) { sessionAllowed.set(key, false); api.logger.info( `dirigent: before_model_resolve blocking out-of-turn speaker session=${key} channel=${derived.channelId} accountId=${accountId} currentSpeaker=${turnCheck.currentSpeaker}`, ); return { model: ctx.model, provider: ctx.provider, noReply: true, }; } sessionAllowed.set(key, true); } } if (!rec.decision.shouldUseNoReply) return; const out: Record = { noReply: true }; if (rec.decision.provider) out.provider = rec.decision.provider; if (rec.decision.model) out.model = rec.decision.model; return out; }); }