fix/no-reply-empty-content-detection #18
@@ -1,7 +1,7 @@
|
|||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
|
|
||||||
const port = Number(process.env.PORT || 8787);
|
const port = Number(process.env.PORT || 8787);
|
||||||
const modelName = process.env.NO_REPLY_MODEL || "dirigent-no-reply-v1";
|
const modelName = process.env.NO_REPLY_MODEL || "no-reply";
|
||||||
const authToken = process.env.AUTH_TOKEN || "";
|
const authToken = process.env.AUTH_TOKEN || "";
|
||||||
|
|
||||||
function sendJson(res, status, payload) {
|
function sendJson(res, status, payload) {
|
||||||
|
|||||||
@@ -62,9 +62,25 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
|
|||||||
|
|
||||||
let content = "";
|
let content = "";
|
||||||
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
const msg = (event as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
||||||
|
const msgContent = msg?.content;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
const role = msg.role as string | undefined;
|
const role = msg.role as string | undefined;
|
||||||
if (role && role !== "assistant") return;
|
if (role && role !== "assistant") return;
|
||||||
|
|
||||||
|
// Detect tool calls — intermediate model step, not a final response.
|
||||||
|
// Skip turn processing entirely to avoid false NO_REPLY detection.
|
||||||
|
if (Array.isArray(msgContent)) {
|
||||||
|
const hasToolCalls = (msgContent as Record<string, unknown>[]).some(
|
||||||
|
(part) => part?.type === "toolCall" || part?.type === "tool_call" || part?.type === "tool_use",
|
||||||
|
);
|
||||||
|
if (hasToolCalls) {
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: before_message_write skipping tool-call message session=${key ?? "undefined"} channel=${channelId ?? "undefined"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof msg.content === "string") {
|
if (typeof msg.content === "string") {
|
||||||
content = msg.content;
|
content = msg.content;
|
||||||
} else if (Array.isArray(msg.content)) {
|
} else if (Array.isArray(msg.content)) {
|
||||||
@@ -99,13 +115,15 @@ export function registerBeforeMessageWriteHook(deps: BeforeMessageWriteDeps): vo
|
|||||||
const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record<string, any>);
|
const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record<string, any>);
|
||||||
|
|
||||||
const trimmed = content.trim();
|
const trimmed = content.trim();
|
||||||
const isEmpty = trimmed.length === 0;
|
|
||||||
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
||||||
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
||||||
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
||||||
const waitId = live.waitIdentifier || "👤";
|
const waitId = live.waitIdentifier || "👤";
|
||||||
const hasWaitIdentifier = !!lastChar && lastChar === waitId;
|
const hasWaitIdentifier = !!lastChar && lastChar === waitId;
|
||||||
const wasNoReply = isEmpty || isNoReply;
|
// Only treat explicit NO_REPLY/HEARTBEAT_OK keywords as no-reply.
|
||||||
|
// Empty content is NOT treated as no-reply — it may come from intermediate
|
||||||
|
// model responses (e.g. thinking-only blocks) that are not final answers.
|
||||||
|
const wasNoReply = isNoReply;
|
||||||
|
|
||||||
const turnDebug = getTurnDebugInfo(channelId);
|
const turnDebug = getTurnDebugInfo(channelId);
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ export function registerBeforeModelResolveHook(deps: BeforeModelResolveDeps): vo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
sessionAllowed.set(key, true);
|
sessionAllowed.set(key, true);
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: turn allowed, skipping rules override session=${key} accountId=${accountId}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,13 +74,13 @@ export function registerMessageSentHook(deps: MessageSentDeps): void {
|
|||||||
const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record<string, any>);
|
const policy = resolvePolicy(live, channelId, policyState.channelPolicies as Record<string, any>);
|
||||||
|
|
||||||
const trimmed = content.trim();
|
const trimmed = content.trim();
|
||||||
const isEmpty = trimmed.length === 0;
|
|
||||||
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
||||||
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
||||||
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
||||||
const waitId = live.waitIdentifier || "👤";
|
const waitId = live.waitIdentifier || "👤";
|
||||||
const hasWaitIdentifier = !!lastChar && lastChar === waitId;
|
const hasWaitIdentifier = !!lastChar && lastChar === waitId;
|
||||||
const wasNoReply = isEmpty || isNoReply;
|
// Only treat explicit NO_REPLY/HEARTBEAT_OK keywords as no-reply.
|
||||||
|
const wasNoReply = isNoReply;
|
||||||
|
|
||||||
if (key && sessionTurnHandled.has(key)) {
|
if (key && sessionTurnHandled.has(key)) {
|
||||||
sessionTurnHandled.delete(key);
|
sessionTurnHandled.delete(key);
|
||||||
@@ -100,7 +100,7 @@ export function registerMessageSentHook(deps: MessageSentDeps): void {
|
|||||||
|
|
||||||
if (wasNoReply || hasEndSymbol) {
|
if (wasNoReply || hasEndSymbol) {
|
||||||
const nextSpeaker = onSpeakerDone(channelId, accountId, wasNoReply);
|
const nextSpeaker = onSpeakerDone(channelId, accountId, wasNoReply);
|
||||||
const trigger = wasNoReply ? (isEmpty ? "empty" : "no_reply_keyword") : "end_symbol";
|
const trigger = wasNoReply ? "no_reply_keyword" : "end_symbol";
|
||||||
api.logger.info(
|
api.logger.info(
|
||||||
`dirigent: turn onSpeakerDone channel=${channelId} from=${accountId} next=${nextSpeaker ?? "dormant"} trigger=${trigger}`,
|
`dirigent: turn onSpeakerDone channel=${channelId} from=${accountId} next=${nextSpeaker ?? "dormant"} trigger=${trigger}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const PLUGINS_DIR = path.join(OPENCLAW_DIR, "plugins");
|
|||||||
const PLUGIN_INSTALL_DIR = path.join(PLUGINS_DIR, "dirigent");
|
const PLUGIN_INSTALL_DIR = path.join(PLUGINS_DIR, "dirigent");
|
||||||
const NO_REPLY_INSTALL_DIR = path.join(PLUGIN_INSTALL_DIR, "no-reply-api");
|
const NO_REPLY_INSTALL_DIR = path.join(PLUGIN_INSTALL_DIR, "no-reply-api");
|
||||||
|
|
||||||
const NO_REPLY_PROVIDER_ID = process.env.NO_REPLY_PROVIDER_ID || "dirigentway";
|
const NO_REPLY_PROVIDER_ID = process.env.NO_REPLY_PROVIDER_ID || "dirigent";
|
||||||
const NO_REPLY_MODEL_ID = process.env.NO_REPLY_MODEL_ID || "no-reply";
|
const NO_REPLY_MODEL_ID = process.env.NO_REPLY_MODEL_ID || "no-reply";
|
||||||
const NO_REPLY_PORT = Number(process.env.NO_REPLY_PORT || argNoReplyPort);
|
const NO_REPLY_PORT = Number(process.env.NO_REPLY_PORT || argNoReplyPort);
|
||||||
const NO_REPLY_BASE_URL = process.env.NO_REPLY_BASE_URL || `http://127.0.0.1:${NO_REPLY_PORT}/v1`;
|
const NO_REPLY_BASE_URL = process.env.NO_REPLY_BASE_URL || `http://127.0.0.1:${NO_REPLY_PORT}/v1`;
|
||||||
@@ -223,6 +223,11 @@ if (mode === "install") {
|
|||||||
};
|
};
|
||||||
setJson("models.providers", providers);
|
setJson("models.providers", providers);
|
||||||
|
|
||||||
|
// Add no-reply model to agents.defaults.models allowlist
|
||||||
|
const agentsDefaultsModels = getJson("agents.defaults.models") || {};
|
||||||
|
agentsDefaultsModels[`${NO_REPLY_PROVIDER_ID}/${NO_REPLY_MODEL_ID}`] = {};
|
||||||
|
setJson("agents.defaults.models", agentsDefaultsModels);
|
||||||
|
|
||||||
step(6, 6, "enable plugin in allowlist");
|
step(6, 6, "enable plugin in allowlist");
|
||||||
const allow = getJson("plugins.allow") || [];
|
const allow = getJson("plugins.allow") || [];
|
||||||
if (!allow.includes("dirigent")) {
|
if (!allow.includes("dirigent")) {
|
||||||
|
|||||||
Reference in New Issue
Block a user