feat(agent): add kill-switch for final-answer Markdown normalization

Gate MarkdownNormalizer behind mate.agent.markdown-normalize-enabled (default
true) so operators can disable the rewrite verbatim if a normalization edge
case ever mangles a legitimate answer.
This commit is contained in:
matevip 2026-06-07 19:15:14 +08:00
parent 398d7a2d80
commit 46f3d425e0
3 changed files with 29 additions and 4 deletions

View File

@ -88,6 +88,11 @@ public class AgentGraphBuilder {
@org.springframework.beans.factory.annotation.Value(
"${mateclaw.skill.disclosure.load-skill-tool.enabled:true}")
private boolean loadSkillToolEnabled;
/** Escape hatch: when false, the final answer is sent verbatim without Markdown normalization. */
@org.springframework.beans.factory.annotation.Value(
"${mate.agent.markdown-normalize-enabled:true}")
private boolean markdownNormalizeEnabled;
private final ConversationService conversationService;
private final ModelConfigService modelConfigService;
private final ModelProviderService modelProviderService;
@ -809,7 +814,7 @@ public class AgentGraphBuilder {
SummarizingNode summarizingNode = new SummarizingNode(chatModel, streamingHelper, streamTracker);
LimitExceededNode limitExceededNode = new LimitExceededNode(
chatModel, observationProcessor, streamingHelper, i18nService, progressLedgerService);
FinalAnswerNode finalAnswerNode = new FinalAnswerNode(generatedFileCache);
FinalAnswerNode finalAnswerNode = new FinalAnswerNode(generatedFileCache, markdownNormalizeEnabled);
KeyStrategyFactory keyStrategyFactory = KeyStrategy.builder()
// 输入字段

View File

@ -40,12 +40,25 @@ public class FinalAnswerNode implements NodeAction {
*/
private final GeneratedFileCache generatedFileCache;
/**
* Kill-switch for the deterministic Markdown cleanup applied to the answer
* body. {@code true} (default) runs {@link MarkdownNormalizer}; set to
* {@code false} to surface model output verbatim if a normalization edge
* case ever mangles a legitimate answer in production.
*/
private final boolean markdownNormalizeEnabled;
public FinalAnswerNode() {
this(null);
this(null, true);
}
public FinalAnswerNode(GeneratedFileCache generatedFileCache) {
this(generatedFileCache, true);
}
public FinalAnswerNode(GeneratedFileCache generatedFileCache, boolean markdownNormalizeEnabled) {
this.generatedFileCache = generatedFileCache;
this.markdownNormalizeEnabled = markdownNormalizeEnabled;
}
@Override
@ -176,8 +189,11 @@ public class FinalAnswerNode implements NodeAction {
// unaligned table pipes) that prompt rules fail to prevent; this fixes the
// mechanical defects before the answer is persisted / sent to channels.
// Verbatim tool output (RETURN_DIRECT) and approval-wait paths return early
// above and are intentionally left untouched.
finalAnswer = MarkdownNormalizer.normalize(finalAnswer);
// above and are intentionally left untouched. Gated so operators can turn
// the rewrite off (mate.agent.markdown-normalize-enabled=false).
if (markdownNormalizeEnabled) {
finalAnswer = MarkdownNormalizer.normalize(finalAnswer);
}
// Build the event list. Always carries the finish_reason event so
// downstream consumers (memory gate, channel accumulator, message

View File

@ -222,6 +222,10 @@ mateclaw:
# MateClaw Agent 配置
mate:
agent:
# Deterministic Markdown cleanup of the final answer (heading spaces, glued
# ---, table pipe alignment) before persistence / channel delivery. Set to
# false to pass model output through verbatim.
markdown-normalize-enabled: true
graph:
observation:
# 与 GraphObservationProperties.java 默认值对齐,参考 openclaw token-budget 设计