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; policyState: { channelPolicies: Record }; DECISION_TTL_MS: number; ensurePolicyStateLoaded: (api: OpenClawPluginApi, config: DirigentConfig) => void; getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig; resolveAccountId: (api: OpenClawPluginApi, agentId: string) => string | undefined; pruneDecisionMap: () => void; shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => Promise | void; }; export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): void { const { api, baseConfig, sessionDecision, sessionAllowed, sessionChannelId, sessionAccountId, policyState, DECISION_TTL_MS, ensurePolicyStateLoaded, getLivePluginConfig, resolveAccountId, pruneDecisionMap, shouldDebugLog, ensureTurnOrder, } = deps; api.on("before_model_resolve", async (event, ctx) => { const key = ctx.sessionKey; if (!key) return; const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig; ensurePolicyStateLoaded(api, live); 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); } 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: turn gate blocked session=${key} accountId=${accountId} currentSpeaker=${turnCheck.currentSpeaker} reason=${turnCheck.reason}`, ); return { providerOverride: live.noReplyProvider, modelOverride: live.noReplyModel, }; } sessionAllowed.set(key, true); api.logger.info( `dirigent: turn allowed, skipping rules override session=${key} accountId=${accountId}`, ); return; } } if (!rec.decision.shouldUseNoReply) { if (rec.needsRestore) { sessionDecision.delete(key); return { providerOverride: undefined, modelOverride: undefined, }; } return; } rec.needsRestore = true; sessionDecision.set(key, rec); if (live.enableDebugLogs) { const hasConvMarker2 = prompt.includes("Conversation info (untrusted metadata):"); api.logger.info( `dirigent: DEBUG_NO_REPLY_TRIGGER 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 ?? "")} ` + `decision=${rec.decision.reason} ` + `shouldNoReply=${rec.decision.shouldUseNoReply} shouldInject=${rec.decision.shouldInjectEndMarkerPrompt} ` + `hasConvMarker=${hasConvMarker2} promptLen=${prompt.length}`, ); } api.logger.info( `dirigent: override model for session=${key}, provider=${live.noReplyProvider}, model=${live.noReplyModel}, reason=${rec.decision.reason}`, ); return { providerOverride: live.noReplyProvider, modelOverride: live.noReplyModel, }; }); }