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( @org.springframework.beans.factory.annotation.Value(
"${mateclaw.skill.disclosure.load-skill-tool.enabled:true}") "${mateclaw.skill.disclosure.load-skill-tool.enabled:true}")
private boolean loadSkillToolEnabled; 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 ConversationService conversationService;
private final ModelConfigService modelConfigService; private final ModelConfigService modelConfigService;
private final ModelProviderService modelProviderService; private final ModelProviderService modelProviderService;
@ -809,7 +814,7 @@ public class AgentGraphBuilder {
SummarizingNode summarizingNode = new SummarizingNode(chatModel, streamingHelper, streamTracker); SummarizingNode summarizingNode = new SummarizingNode(chatModel, streamingHelper, streamTracker);
LimitExceededNode limitExceededNode = new LimitExceededNode( LimitExceededNode limitExceededNode = new LimitExceededNode(
chatModel, observationProcessor, streamingHelper, i18nService, progressLedgerService); chatModel, observationProcessor, streamingHelper, i18nService, progressLedgerService);
FinalAnswerNode finalAnswerNode = new FinalAnswerNode(generatedFileCache); FinalAnswerNode finalAnswerNode = new FinalAnswerNode(generatedFileCache, markdownNormalizeEnabled);
KeyStrategyFactory keyStrategyFactory = KeyStrategy.builder() KeyStrategyFactory keyStrategyFactory = KeyStrategy.builder()
// 输入字段 // 输入字段

View File

@ -40,12 +40,25 @@ public class FinalAnswerNode implements NodeAction {
*/ */
private final GeneratedFileCache generatedFileCache; 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() { public FinalAnswerNode() {
this(null); this(null, true);
} }
public FinalAnswerNode(GeneratedFileCache generatedFileCache) { public FinalAnswerNode(GeneratedFileCache generatedFileCache) {
this(generatedFileCache, true);
}
public FinalAnswerNode(GeneratedFileCache generatedFileCache, boolean markdownNormalizeEnabled) {
this.generatedFileCache = generatedFileCache; this.generatedFileCache = generatedFileCache;
this.markdownNormalizeEnabled = markdownNormalizeEnabled;
} }
@Override @Override
@ -176,8 +189,11 @@ public class FinalAnswerNode implements NodeAction {
// unaligned table pipes) that prompt rules fail to prevent; this fixes the // unaligned table pipes) that prompt rules fail to prevent; this fixes the
// mechanical defects before the answer is persisted / sent to channels. // mechanical defects before the answer is persisted / sent to channels.
// Verbatim tool output (RETURN_DIRECT) and approval-wait paths return early // Verbatim tool output (RETURN_DIRECT) and approval-wait paths return early
// above and are intentionally left untouched. // above and are intentionally left untouched. Gated so operators can turn
finalAnswer = MarkdownNormalizer.normalize(finalAnswer); // 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 // Build the event list. Always carries the finish_reason event so
// downstream consumers (memory gate, channel accumulator, message // downstream consumers (memory gate, channel accumulator, message

View File

@ -222,6 +222,10 @@ mateclaw:
# MateClaw Agent 配置 # MateClaw Agent 配置
mate: mate:
agent: 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: graph:
observation: observation:
# 与 GraphObservationProperties.java 默认值对齐,参考 openclaw token-budget 设计 # 与 GraphObservationProperties.java 默认值对齐,参考 openclaw token-budget 设计