refactor(plugin): extract before_prompt_build and before_message_write hooks
This commit is contained in:
185
plugin/hooks/before-message-write.ts
Normal file
185
plugin/hooks/before-message-write.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import { resolvePolicy, type DirigentConfig } from "../rules.js";
|
||||||
|
import { getTurnDebugInfo, onSpeakerDone, setWaitingForHuman } from "../turn-manager.js";
|
||||||
|
|
||||||
|
type DebugConfig = {
|
||||||
|
enableDebugLogs?: boolean;
|
||||||
|
debugLogChannelIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type BeforeMessageWriteDeps = {
|
||||||
|
api: OpenClawPluginApi;
|
||||||
|
baseConfig: DirigentConfig;
|
||||||
|
policyState: { channelPolicies: Record<string, unknown> };
|
||||||
|
sessionAllowed: Map<string, boolean>;
|
||||||
|
sessionChannelId: Map<string, string>;
|
||||||
|
sessionAccountId: Map<string, string>;
|
||||||
|
sessionTurnHandled: Set<string>;
|
||||||
|
ensurePolicyStateLoaded: (api: OpenClawPluginApi, config: DirigentConfig) => void;
|
||||||
|
getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig;
|
||||||
|
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
|
||||||
|
ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => void;
|
||||||
|
resolveDiscordUserId: (api: OpenClawPluginApi, accountId: string) => string | undefined;
|
||||||
|
sendModeratorMessage: (
|
||||||
|
botToken: string,
|
||||||
|
channelId: string,
|
||||||
|
content: string,
|
||||||
|
logger: { info: (m: string) => void; warn: (m: string) => void },
|
||||||
|
) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): void {
|
||||||
|
const {
|
||||||
|
api,
|
||||||
|
baseConfig,
|
||||||
|
policyState,
|
||||||
|
sessionAllowed,
|
||||||
|
sessionChannelId,
|
||||||
|
sessionAccountId,
|
||||||
|
sessionTurnHandled,
|
||||||
|
ensurePolicyStateLoaded,
|
||||||
|
getLivePluginConfig,
|
||||||
|
shouldDebugLog,
|
||||||
|
ensureTurnOrder,
|
||||||
|
resolveDiscordUserId,
|
||||||
|
sendModeratorMessage,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
api.on("before_message_write", (event, ctx) => {
|
||||||
|
try {
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: DEBUG before_message_write eventKeys=${JSON.stringify(Object.keys(event ?? {}))} ctxKeys=${JSON.stringify(Object.keys(ctx ?? {}))}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const key = ctx.sessionKey;
|
||||||
|
let channelId: string | undefined;
|
||||||
|
let accountId: string | undefined;
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
channelId = sessionChannelId.get(key);
|
||||||
|
accountId = sessionAccountId.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = "";
|
||||||
|
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
||||||
|
if (msg) {
|
||||||
|
const role = msg.role as string | undefined;
|
||||||
|
if (role && role !== "assistant") return;
|
||||||
|
if (typeof msg.content === "string") {
|
||||||
|
content = msg.content;
|
||||||
|
} else if (Array.isArray(msg.content)) {
|
||||||
|
for (const part of msg.content) {
|
||||||
|
if (typeof part === "string") content += part;
|
||||||
|
else if (part && typeof part === "object" && typeof (part as Record<string, unknown>).text === "string") {
|
||||||
|
content += (part as Record<string, unknown>).text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!content) {
|
||||||
|
content = ((event as Record<string, unknown>).content as string) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: DEBUG before_message_write session=${key ?? "undefined"} channel=${channelId ?? "undefined"} accountId=${accountId ?? "undefined"} contentType=${typeof content} content=${String(content).slice(0, 200)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!key || !channelId || !accountId) return;
|
||||||
|
|
||||||
|
const currentTurn = getTurnDebugInfo(channelId);
|
||||||
|
if (currentTurn.currentSpeaker !== accountId) {
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: before_message_write skipping non-current-speaker session=${key} accountId=${accountId} currentSpeaker=${currentTurn.currentSpeaker}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig;
|
||||||
|
ensurePolicyStateLoaded(api, live);
|
||||||
|
const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record<string, any>);
|
||||||
|
|
||||||
|
const trimmed = content.trim();
|
||||||
|
const isEmpty = trimmed.length === 0;
|
||||||
|
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
||||||
|
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
||||||
|
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
||||||
|
const waitId = live.waitIdentifier || "👤";
|
||||||
|
const hasWaitIdentifier = !!lastChar && lastChar === waitId;
|
||||||
|
const wasNoReply = isEmpty || isNoReply;
|
||||||
|
|
||||||
|
const turnDebug = getTurnDebugInfo(channelId);
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: DEBUG turn state channel=${channelId} turnOrder=${JSON.stringify(turnDebug.turnOrder)} currentSpeaker=${turnDebug.currentSpeaker} noRepliedThisCycle=${JSON.stringify([...turnDebug.noRepliedThisCycle])}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasWaitIdentifier) {
|
||||||
|
setWaitingForHuman(channelId);
|
||||||
|
sessionAllowed.delete(key);
|
||||||
|
sessionTurnHandled.add(key);
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: before_message_write wait-for-human triggered session=${key} channel=${channelId} accountId=${accountId}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasAllowed = sessionAllowed.get(key);
|
||||||
|
|
||||||
|
if (wasNoReply) {
|
||||||
|
api.logger.info(`dirigent: DEBUG NO_REPLY detected session=${key} wasAllowed=${wasAllowed}`);
|
||||||
|
|
||||||
|
if (wasAllowed === undefined) return;
|
||||||
|
|
||||||
|
if (wasAllowed === false) {
|
||||||
|
sessionAllowed.delete(key);
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: before_message_write forced no-reply session=${key} channel=${channelId} - not advancing turn`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureTurnOrder(api, channelId);
|
||||||
|
const nextSpeaker = onSpeakerDone(channelId, accountId, true);
|
||||||
|
sessionAllowed.delete(key);
|
||||||
|
sessionTurnHandled.add(key);
|
||||||
|
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: before_message_write real no-reply session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!nextSpeaker) {
|
||||||
|
if (shouldDebugLog(live, channelId)) {
|
||||||
|
api.logger.info(`dirigent: before_message_write all agents no-reply, going dormant - no handoff`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (live.moderatorBotToken) {
|
||||||
|
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
|
||||||
|
if (nextUserId) {
|
||||||
|
const schedulingId = live.schedulingIdentifier || "➡️";
|
||||||
|
const handoffMsg = `<@${nextUserId}>${schedulingId}`;
|
||||||
|
void sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger).catch((err) => {
|
||||||
|
api.logger.warn(`dirigent: before_message_write handoff failed: ${String(err)}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
api.logger.warn(`dirigent: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (hasEndSymbol) {
|
||||||
|
ensureTurnOrder(api, channelId);
|
||||||
|
const nextSpeaker = onSpeakerDone(channelId, accountId, false);
|
||||||
|
sessionAllowed.delete(key);
|
||||||
|
sessionTurnHandled.add(key);
|
||||||
|
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: before_message_write end-symbol turn advance session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
api.logger.info(`dirigent: before_message_write no turn action needed session=${key} channel=${channelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
api.logger.warn(`dirigent: before_message_write hook failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
134
plugin/hooks/before-prompt-build.ts
Normal file
134
plugin/hooks/before-prompt-build.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import { evaluateDecision, resolvePolicy, type Decision, type DirigentConfig } from "../rules.js";
|
||||||
|
import { deriveDecisionInputFromPrompt } from "../decision-input.js";
|
||||||
|
|
||||||
|
type DebugConfig = {
|
||||||
|
enableDebugLogs?: boolean;
|
||||||
|
debugLogChannelIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type DecisionRecord = {
|
||||||
|
decision: Decision;
|
||||||
|
createdAt: number;
|
||||||
|
needsRestore?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BeforePromptBuildDeps = {
|
||||||
|
api: OpenClawPluginApi;
|
||||||
|
baseConfig: DirigentConfig;
|
||||||
|
sessionDecision: Map<string, DecisionRecord>;
|
||||||
|
sessionInjected: Set<string>;
|
||||||
|
policyState: { channelPolicies: Record<string, unknown> };
|
||||||
|
DECISION_TTL_MS: number;
|
||||||
|
ensurePolicyStateLoaded: (api: OpenClawPluginApi, config: DirigentConfig) => void;
|
||||||
|
getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig;
|
||||||
|
shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean;
|
||||||
|
buildEndMarkerInstruction: (endSymbols: string[], isGroupChat: boolean, schedulingIdentifier: string, waitIdentifier: string) => string;
|
||||||
|
buildSchedulingIdentifierInstruction: (schedulingIdentifier: string) => string;
|
||||||
|
buildAgentIdentity: (api: OpenClawPluginApi, agentId: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function registerBeforePromptBuildHook(deps: BeforePromptBuildDeps): void {
|
||||||
|
const {
|
||||||
|
api,
|
||||||
|
baseConfig,
|
||||||
|
sessionDecision,
|
||||||
|
sessionInjected,
|
||||||
|
policyState,
|
||||||
|
DECISION_TTL_MS,
|
||||||
|
ensurePolicyStateLoaded,
|
||||||
|
getLivePluginConfig,
|
||||||
|
shouldDebugLog,
|
||||||
|
buildEndMarkerInstruction,
|
||||||
|
buildSchedulingIdentifierInstruction,
|
||||||
|
buildAgentIdentity,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
api.on("before_prompt_build", async (event, ctx) => {
|
||||||
|
const key = ctx.sessionKey;
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig;
|
||||||
|
ensurePolicyStateLoaded(api, live);
|
||||||
|
|
||||||
|
let rec = sessionDecision.get(key);
|
||||||
|
if (!rec || Date.now() - rec.createdAt > DECISION_TTL_MS) {
|
||||||
|
if (rec) sessionDecision.delete(key);
|
||||||
|
|
||||||
|
const prompt = ((event as Record<string, unknown>).prompt as string) || "";
|
||||||
|
const derived = deriveDecisionInputFromPrompt({
|
||||||
|
prompt,
|
||||||
|
messageProvider: ctx.messageProvider,
|
||||||
|
sessionKey: key,
|
||||||
|
ctx: ctx as Record<string, unknown>,
|
||||||
|
event: event as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const decision = evaluateDecision({
|
||||||
|
config: live,
|
||||||
|
channel: derived.channel,
|
||||||
|
channelId: derived.channelId,
|
||||||
|
channelPolicies: policyState.channelPolicies as Record<string, any>,
|
||||||
|
senderId: derived.senderId,
|
||||||
|
content: derived.content,
|
||||||
|
});
|
||||||
|
rec = { decision, createdAt: Date.now() };
|
||||||
|
if (shouldDebugLog(live, derived.channelId)) {
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: debug before_prompt_build recompute session=${key} ` +
|
||||||
|
`channel=${derived.channel} channelId=${derived.channelId ?? ""} senderId=${derived.senderId ?? ""} ` +
|
||||||
|
`convSenderId=${String((derived.conv as Record<string, unknown>).sender_id ?? "")} ` +
|
||||||
|
`convSender=${String((derived.conv as Record<string, unknown>).sender ?? "")} ` +
|
||||||
|
`convChannelId=${String((derived.conv as Record<string, unknown>).channel_id ?? "")} ` +
|
||||||
|
`decision=${decision.reason} shouldNoReply=${decision.shouldUseNoReply} shouldInject=${decision.shouldInjectEndMarkerPrompt}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionDecision.delete(key);
|
||||||
|
|
||||||
|
if (sessionInjected.has(key)) {
|
||||||
|
if (shouldDebugLog(live, undefined)) {
|
||||||
|
api.logger.info(`dirigent: debug before_prompt_build session=${key} inject skipped (already injected)`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rec.decision.shouldInjectEndMarkerPrompt) {
|
||||||
|
if (shouldDebugLog(live, undefined)) {
|
||||||
|
api.logger.info(`dirigent: debug before_prompt_build session=${key} inject=false reason=${rec.decision.reason}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = ((event as Record<string, unknown>).prompt as string) || "";
|
||||||
|
const derived = deriveDecisionInputFromPrompt({
|
||||||
|
prompt,
|
||||||
|
messageProvider: ctx.messageProvider,
|
||||||
|
sessionKey: key,
|
||||||
|
ctx: ctx as Record<string, unknown>,
|
||||||
|
event: event as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
const policy = resolvePolicy(live, derived.channelId, policyState.channelPolicies as Record<string, any>);
|
||||||
|
const isGroupChat = derived.conv.is_group_chat === true || derived.conv.is_group_chat === "true";
|
||||||
|
const schedulingId = live.schedulingIdentifier || "➡️";
|
||||||
|
const waitId = live.waitIdentifier || "👤";
|
||||||
|
const instruction = buildEndMarkerInstruction(policy.endSymbols, isGroupChat, schedulingId, waitId);
|
||||||
|
|
||||||
|
let identity = "";
|
||||||
|
if (isGroupChat && ctx.agentId) {
|
||||||
|
const idStr = buildAgentIdentity(api, ctx.agentId);
|
||||||
|
if (idStr) identity = idStr + "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
let schedulingInstruction = "";
|
||||||
|
if (isGroupChat) {
|
||||||
|
schedulingInstruction = buildSchedulingIdentifierInstruction(schedulingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionInjected.add(key);
|
||||||
|
|
||||||
|
api.logger.info(`dirigent: prepend end marker instruction for session=${key}, reason=${rec.decision.reason} isGroupChat=${isGroupChat}`);
|
||||||
|
return { prependContext: identity + instruction + schedulingInstruction };
|
||||||
|
});
|
||||||
|
}
|
||||||
271
plugin/index.ts
271
plugin/index.ts
@@ -5,9 +5,10 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|||||||
import { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type DirigentConfig } from "./rules.js";
|
import { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type DirigentConfig } from "./rules.js";
|
||||||
import { checkTurn, advanceTurn, resetTurn, onNewMessage, onSpeakerDone, initTurnOrder, getTurnDebugInfo, setMentionOverride, hasMentionOverride, setWaitingForHuman, isWaitingForHuman } from "./turn-manager.js";
|
import { checkTurn, advanceTurn, resetTurn, onNewMessage, onSpeakerDone, initTurnOrder, getTurnDebugInfo, setMentionOverride, hasMentionOverride, setWaitingForHuman, isWaitingForHuman } from "./turn-manager.js";
|
||||||
import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js";
|
import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js";
|
||||||
import { extractDiscordChannelId, extractDiscordChannelIdFromSessionKey } from "./channel-resolver.js";
|
import { extractDiscordChannelId } from "./channel-resolver.js";
|
||||||
import { deriveDecisionInputFromPrompt } from "./decision-input.js";
|
|
||||||
import { registerBeforeModelResolveHook } from "./hooks/before-model-resolve.js";
|
import { registerBeforeModelResolveHook } from "./hooks/before-model-resolve.js";
|
||||||
|
import { registerBeforePromptBuildHook } from "./hooks/before-prompt-build.js";
|
||||||
|
import { registerBeforeMessageWriteHook } from "./hooks/before-message-write.js";
|
||||||
import { registerMessageSentHook } from "./hooks/message-sent.js";
|
import { registerMessageSentHook } from "./hooks/message-sent.js";
|
||||||
|
|
||||||
// ── No-Reply API child process lifecycle ──────────────────────────────
|
// ── No-Reply API child process lifecycle ──────────────────────────────
|
||||||
@@ -811,101 +812,19 @@ export default {
|
|||||||
ensureTurnOrder,
|
ensureTurnOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
api.on("before_prompt_build", async (event, ctx) => {
|
registerBeforePromptBuildHook({
|
||||||
const key = ctx.sessionKey;
|
api,
|
||||||
if (!key) return;
|
baseConfig: baseConfig as DirigentConfig,
|
||||||
|
sessionDecision,
|
||||||
const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig;
|
sessionInjected,
|
||||||
ensurePolicyStateLoaded(api, live);
|
policyState,
|
||||||
|
DECISION_TTL_MS,
|
||||||
let rec = sessionDecision.get(key);
|
ensurePolicyStateLoaded,
|
||||||
if (!rec || Date.now() - rec.createdAt > DECISION_TTL_MS) {
|
getLivePluginConfig,
|
||||||
if (rec) sessionDecision.delete(key);
|
shouldDebugLog,
|
||||||
|
buildEndMarkerInstruction,
|
||||||
const prompt = ((event as Record<string, unknown>).prompt as string) || "";
|
buildSchedulingIdentifierInstruction,
|
||||||
const derived = deriveDecisionInputFromPrompt({
|
buildAgentIdentity,
|
||||||
prompt,
|
|
||||||
messageProvider: ctx.messageProvider,
|
|
||||||
sessionKey: key,
|
|
||||||
ctx: ctx as Record<string, unknown>,
|
|
||||||
event: event as Record<string, unknown>,
|
|
||||||
});
|
|
||||||
|
|
||||||
const decision = evaluateDecision({
|
|
||||||
config: live,
|
|
||||||
channel: derived.channel,
|
|
||||||
channelId: derived.channelId,
|
|
||||||
channelPolicies: policyState.channelPolicies,
|
|
||||||
senderId: derived.senderId,
|
|
||||||
content: derived.content,
|
|
||||||
});
|
|
||||||
rec = { decision, createdAt: Date.now() };
|
|
||||||
if (shouldDebugLog(live, derived.channelId)) {
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: debug before_prompt_build recompute session=${key} ` +
|
|
||||||
`channel=${derived.channel} channelId=${derived.channelId ?? ""} senderId=${derived.senderId ?? ""} ` +
|
|
||||||
`convSenderId=${String((derived.conv as Record<string, unknown>).sender_id ?? "")} ` +
|
|
||||||
`convSender=${String((derived.conv as Record<string, unknown>).sender ?? "")} ` +
|
|
||||||
`convChannelId=${String((derived.conv as Record<string, unknown>).channel_id ?? "")} ` +
|
|
||||||
`decision=${decision.reason} shouldNoReply=${decision.shouldUseNoReply} shouldInject=${decision.shouldInjectEndMarkerPrompt}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionDecision.delete(key);
|
|
||||||
|
|
||||||
// Only inject once per session (one-time injection)
|
|
||||||
if (sessionInjected.has(key)) {
|
|
||||||
if (shouldDebugLog(live, undefined)) {
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: debug before_prompt_build session=${key} inject skipped (already injected)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rec.decision.shouldInjectEndMarkerPrompt) {
|
|
||||||
if (shouldDebugLog(live, undefined)) {
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: debug before_prompt_build session=${key} inject=false reason=${rec.decision.reason}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve end symbols from config/policy for dynamic instruction
|
|
||||||
const prompt = ((event as Record<string, unknown>).prompt as string) || "";
|
|
||||||
const derived = deriveDecisionInputFromPrompt({
|
|
||||||
prompt,
|
|
||||||
messageProvider: ctx.messageProvider,
|
|
||||||
sessionKey: key,
|
|
||||||
ctx: ctx as Record<string, unknown>,
|
|
||||||
event: event as Record<string, unknown>,
|
|
||||||
});
|
|
||||||
const policy = resolvePolicy(live, derived.channelId, policyState.channelPolicies);
|
|
||||||
const isGroupChat = derived.conv.is_group_chat === true || derived.conv.is_group_chat === "true";
|
|
||||||
const schedulingId = live.schedulingIdentifier || "➡️";
|
|
||||||
const waitId = live.waitIdentifier || "👤";
|
|
||||||
const instruction = buildEndMarkerInstruction(policy.endSymbols, isGroupChat, schedulingId, waitId);
|
|
||||||
|
|
||||||
// Inject agent identity for group chats (includes userId now)
|
|
||||||
let identity = "";
|
|
||||||
if (isGroupChat && ctx.agentId) {
|
|
||||||
const idStr = buildAgentIdentity(api, ctx.agentId);
|
|
||||||
if (idStr) identity = idStr + "\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add scheduling identifier instruction for group chats
|
|
||||||
let schedulingInstruction = "";
|
|
||||||
if (isGroupChat) {
|
|
||||||
schedulingInstruction = buildSchedulingIdentifierInstruction(schedulingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark session as injected (one-time injection)
|
|
||||||
sessionInjected.add(key);
|
|
||||||
|
|
||||||
api.logger.info(`dirigent: prepend end marker instruction for session=${key}, reason=${rec.decision.reason} isGroupChat=${isGroupChat}`);
|
|
||||||
return { prependContext: identity + instruction + schedulingInstruction };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register slash commands for Discord
|
// Register slash commands for Discord
|
||||||
@@ -955,150 +874,20 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle NO_REPLY detection before message write
|
// Handle NO_REPLY detection before message write
|
||||||
api.on("before_message_write", (event, ctx) => {
|
registerBeforeMessageWriteHook({
|
||||||
try {
|
api,
|
||||||
api.logger.info(
|
baseConfig: baseConfig as DirigentConfig,
|
||||||
`dirigent: DEBUG before_message_write eventKeys=${JSON.stringify(Object.keys(event ?? {}))} ctxKeys=${JSON.stringify(Object.keys(ctx ?? {}))}`,
|
policyState,
|
||||||
);
|
sessionAllowed,
|
||||||
|
sessionChannelId,
|
||||||
let key = ctx.sessionKey;
|
sessionAccountId,
|
||||||
let channelId: string | undefined;
|
sessionTurnHandled,
|
||||||
let accountId: string | undefined;
|
ensurePolicyStateLoaded,
|
||||||
|
getLivePluginConfig,
|
||||||
if (key) {
|
shouldDebugLog,
|
||||||
channelId = sessionChannelId.get(key);
|
ensureTurnOrder,
|
||||||
accountId = sessionAccountId.get(key);
|
resolveDiscordUserId,
|
||||||
}
|
sendModeratorMessage,
|
||||||
|
|
||||||
let content = "";
|
|
||||||
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
|
||||||
if (msg) {
|
|
||||||
const role = msg.role as string | undefined;
|
|
||||||
if (role && role !== "assistant") return;
|
|
||||||
if (typeof msg.content === "string") {
|
|
||||||
content = msg.content;
|
|
||||||
} else if (Array.isArray(msg.content)) {
|
|
||||||
for (const part of msg.content) {
|
|
||||||
if (typeof part === "string") content += part;
|
|
||||||
else if (part && typeof part === "object" && typeof (part as Record<string, unknown>).text === "string") {
|
|
||||||
content += (part as Record<string, unknown>).text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!content) {
|
|
||||||
content = ((event as Record<string, unknown>).content as string) || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: DEBUG before_message_write session=${key ?? "undefined"} channel=${channelId ?? "undefined"} accountId=${accountId ?? "undefined"} contentType=${typeof content} content=${String(content).slice(0, 200)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!key || !channelId || !accountId) return;
|
|
||||||
|
|
||||||
const currentTurn = getTurnDebugInfo(channelId);
|
|
||||||
if (currentTurn.currentSpeaker !== accountId) {
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: before_message_write skipping non-current-speaker session=${key} accountId=${accountId} currentSpeaker=${currentTurn.currentSpeaker}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const live = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig;
|
|
||||||
ensurePolicyStateLoaded(api, live);
|
|
||||||
const policy = resolvePolicy(live, channelId, policyState.channelPolicies);
|
|
||||||
|
|
||||||
const trimmed = content.trim();
|
|
||||||
const isEmpty = trimmed.length === 0;
|
|
||||||
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
|
||||||
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
|
||||||
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
|
||||||
const waitId = live.waitIdentifier || "👤";
|
|
||||||
const hasWaitIdentifier = !!lastChar && lastChar === waitId;
|
|
||||||
const wasNoReply = isEmpty || isNoReply;
|
|
||||||
|
|
||||||
const turnDebug = getTurnDebugInfo(channelId);
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: DEBUG turn state channel=${channelId} turnOrder=${JSON.stringify(turnDebug.turnOrder)} currentSpeaker=${turnDebug.currentSpeaker} noRepliedThisCycle=${JSON.stringify([...turnDebug.noRepliedThisCycle])}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait identifier: agent wants a human reply → all agents go silent
|
|
||||||
if (hasWaitIdentifier) {
|
|
||||||
setWaitingForHuman(channelId);
|
|
||||||
sessionAllowed.delete(key);
|
|
||||||
sessionTurnHandled.add(key);
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: before_message_write wait-for-human triggered session=${key} channel=${channelId} accountId=${accountId}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wasAllowed = sessionAllowed.get(key);
|
|
||||||
|
|
||||||
if (wasNoReply) {
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: DEBUG NO_REPLY detected session=${key} wasAllowed=${wasAllowed}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (wasAllowed === undefined) return;
|
|
||||||
|
|
||||||
if (wasAllowed === false) {
|
|
||||||
sessionAllowed.delete(key);
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: before_message_write forced no-reply session=${key} channel=${channelId} - not advancing turn`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureTurnOrder(api, channelId);
|
|
||||||
const nextSpeaker = onSpeakerDone(channelId, accountId, true);
|
|
||||||
sessionAllowed.delete(key);
|
|
||||||
sessionTurnHandled.add(key);
|
|
||||||
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: before_message_write real no-reply session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!nextSpeaker) {
|
|
||||||
if (shouldDebugLog(live, channelId)) {
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: before_message_write all agents no-reply, going dormant - no handoff`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger moderator handoff message using scheduling identifier format
|
|
||||||
if (live.moderatorBotToken) {
|
|
||||||
const nextUserId = resolveDiscordUserId(api, nextSpeaker);
|
|
||||||
if (nextUserId) {
|
|
||||||
const schedulingId = live.schedulingIdentifier || "➡️";
|
|
||||||
const handoffMsg = `<@${nextUserId}>${schedulingId}`;
|
|
||||||
void sendModeratorMessage(live.moderatorBotToken, channelId, handoffMsg, api.logger).catch((err) => {
|
|
||||||
api.logger.warn(`dirigent: before_message_write handoff failed: ${String(err)}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
api.logger.warn(`dirigent: cannot resolve Discord userId for next speaker accountId=${nextSpeaker}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (hasEndSymbol) {
|
|
||||||
ensureTurnOrder(api, channelId);
|
|
||||||
const nextSpeaker = onSpeakerDone(channelId, accountId, false);
|
|
||||||
sessionAllowed.delete(key);
|
|
||||||
sessionTurnHandled.add(key);
|
|
||||||
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: before_message_write end-symbol turn advance session=${key} channel=${channelId} nextSpeaker=${nextSpeaker ?? "dormant"}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
api.logger.info(
|
|
||||||
`dirigent: before_message_write no turn action needed session=${key} channel=${channelId}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
api.logger.warn(`dirigent: before_message_write hook failed: ${String(err)}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Turn advance: when an agent sends a message, check if it signals end of turn
|
// Turn advance: when an agent sends a message, check if it signals end of turn
|
||||||
|
|||||||
Reference in New Issue
Block a user