Skip to content

Commit f2f2b66

Browse files
committed
fix(cli): suppress claude commentary answer partials
1 parent c75dc62 commit f2f2b66

13 files changed

Lines changed: 347 additions & 149 deletions

extensions/discord/src/monitor/message-handler.process.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,9 @@ async function processDiscordMessageInner(
10001000
suppressDefaultToolProgressMessages: draftPreview.suppressDefaultToolProgressMessages
10011001
? true
10021002
: undefined,
1003+
commentaryProgressEnabled: draftPreview.isProgressMode
1004+
? draftPreview.commentaryProgressEnabled
1005+
: undefined,
10031006
onReasoningStream: async (payload) => {
10041007
await statusReactions.setThinking();
10051008
await draftPreview.pushReasoningProgress(payload?.text, {

extensions/telegram/src/bot-message-dispatch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,6 +2317,8 @@ export const dispatchTelegramMessage = async ({
23172317
!streamDeliveryEnabled || Boolean(answerLane.stream),
23182318
allowProgressCallbacksWhenSourceDeliverySuppressed:
23192319
!isRoomEvent && Boolean(answerLane.stream),
2320+
commentaryProgressEnabled:
2321+
streamMode === "progress" ? progressDraft.commentaryProgressEnabled : undefined,
23202322
onToolStart: async (payload) => {
23212323
const toolName = payload.name?.trim();
23222324
const progressPromise = pushStreamToolProgress(

src/agents/cli-output.test.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,7 @@ describe("createCliJsonlStreamingParser", () => {
905905
sessionIdFields: ["session_id"],
906906
},
907907
providerId: "claude-cli",
908+
classifyCommentaryText: true,
908909
onAssistantDelta: (delta) => deltas.push({ text: delta.text, delta: delta.delta }),
909910
onCommentaryText: (text) => commentaryTexts.push(text),
910911
});
@@ -939,8 +940,93 @@ describe("createCliJsonlStreamingParser", () => {
939940
parser.finish();
940941

941942
expect(commentaryTexts).toEqual(["Let me check that for you."]);
942-
expect(deltas).toHaveLength(2);
943-
expect(deltas[1]?.text).toBe("Let me check that for you.");
943+
expect(deltas).toEqual([]);
944+
});
945+
946+
it("flushes Claude text as an assistant delta when no tool follows", () => {
947+
const commentaryTexts: string[] = [];
948+
const deltas: Array<{ text: string; delta: string }> = [];
949+
const parser = createCliJsonlStreamingParser({
950+
backend: {
951+
command: "claude",
952+
output: "jsonl",
953+
jsonlDialect: "claude-stream-json",
954+
sessionIdFields: ["session_id"],
955+
},
956+
providerId: "claude-cli",
957+
classifyCommentaryText: true,
958+
onAssistantDelta: (delta) => deltas.push({ text: delta.text, delta: delta.delta }),
959+
onCommentaryText: (text) => commentaryTexts.push(text),
960+
});
961+
962+
parser.push(
963+
[
964+
JSON.stringify({ type: "init", session_id: "session-answer" }),
965+
JSON.stringify({
966+
type: "stream_event",
967+
event: {
968+
type: "content_block_delta",
969+
delta: { type: "text_delta", text: "Final " },
970+
},
971+
}),
972+
JSON.stringify({
973+
type: "stream_event",
974+
event: {
975+
type: "content_block_delta",
976+
delta: { type: "text_delta", text: "answer." },
977+
},
978+
}),
979+
JSON.stringify({
980+
type: "stream_event",
981+
event: {
982+
type: "message_stop",
983+
},
984+
}),
985+
].join("\n") + "\n",
986+
);
987+
parser.finish();
988+
989+
expect(commentaryTexts).toEqual([]);
990+
expect(deltas).toEqual([{ text: "Final answer.", delta: "Final answer." }]);
991+
});
992+
993+
it("drops Claude commentary text when classification is enabled without delivery", () => {
994+
const deltas: Array<{ text: string; delta: string }> = [];
995+
const parser = createCliJsonlStreamingParser({
996+
backend: {
997+
command: "claude",
998+
output: "jsonl",
999+
jsonlDialect: "claude-stream-json",
1000+
sessionIdFields: ["session_id"],
1001+
},
1002+
providerId: "claude-cli",
1003+
classifyCommentaryText: true,
1004+
onAssistantDelta: (delta) => deltas.push({ text: delta.text, delta: delta.delta }),
1005+
});
1006+
1007+
parser.push(
1008+
[
1009+
JSON.stringify({ type: "init", session_id: "session-drop-commentary" }),
1010+
JSON.stringify({
1011+
type: "stream_event",
1012+
event: {
1013+
type: "content_block_delta",
1014+
delta: { type: "text_delta", text: "Let me inspect the repo." },
1015+
},
1016+
}),
1017+
JSON.stringify({
1018+
type: "stream_event",
1019+
event: {
1020+
type: "content_block_start",
1021+
index: 1,
1022+
content_block: { type: "tool_use", id: "toolu_1", name: "Read", input: {} },
1023+
},
1024+
}),
1025+
].join("\n") + "\n",
1026+
);
1027+
parser.finish();
1028+
1029+
expect(deltas).toEqual([]);
9441030
});
9451031

9461032
it("does not fire onCommentaryText when no text precedes tool_use", () => {
@@ -953,6 +1039,7 @@ describe("createCliJsonlStreamingParser", () => {
9531039
sessionIdFields: ["session_id"],
9541040
},
9551041
providerId: "claude-cli",
1042+
classifyCommentaryText: true,
9561043
onAssistantDelta: () => undefined,
9571044
onCommentaryText: (text) => commentaryTexts.push(text),
9581045
});
@@ -985,6 +1072,7 @@ describe("createCliJsonlStreamingParser", () => {
9851072
sessionIdFields: ["session_id"],
9861073
},
9871074
providerId: "claude-cli",
1075+
classifyCommentaryText: true,
9881076
onAssistantDelta: () => undefined,
9891077
onCommentaryText: (text) => commentaryTexts.push(text),
9901078
});
@@ -1032,6 +1120,7 @@ describe("createCliJsonlStreamingParser", () => {
10321120
sessionIdFields: ["session_id"],
10331121
},
10341122
providerId: "claude-cli",
1123+
classifyCommentaryText: true,
10351124
onAssistantDelta: () => undefined,
10361125
onCommentaryText: (text) => commentaryTexts.push(text),
10371126
});

src/agents/cli-output.ts

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -632,16 +632,45 @@ export function createCliJsonlStreamingParser(params: {
632632
onAssistantDelta: (delta: CliStreamingDelta) => void;
633633
onToolUseStart?: (delta: CliToolUseStartDelta) => void;
634634
onToolResult?: (delta: CliToolResultDelta) => void;
635+
classifyCommentaryText?: boolean;
635636
onCommentaryText?: (text: string) => void;
636637
}) {
637638
let lineBuffer = "";
638639
let assistantText = "";
640+
let pendingClaudeText = "";
639641
let sessionId: string | undefined;
640642
let usage: CliUsage | undefined;
641643
let output: CliOutput | null = null;
642-
let lastFlushedCommentaryLength = 0;
643644
const texts: string[] = [];
644645
const toolTracker = createToolUseTracker();
646+
const classifyClaudeCommentary =
647+
params.classifyCommentaryText === true && usesClaudeStreamJsonDialect(params);
648+
649+
const flushPendingClaudeAssistantText = () => {
650+
if (!pendingClaudeText) {
651+
return;
652+
}
653+
const delta = pendingClaudeText;
654+
pendingClaudeText = "";
655+
assistantText = `${assistantText}${delta}`;
656+
params.onAssistantDelta({
657+
text: assistantText,
658+
delta,
659+
sessionId,
660+
usage,
661+
});
662+
};
663+
664+
const flushPendingClaudeCommentaryText = () => {
665+
if (!pendingClaudeText) {
666+
return;
667+
}
668+
const text = pendingClaudeText.trim();
669+
pendingClaudeText = "";
670+
if (text) {
671+
params.onCommentaryText?.(text);
672+
}
673+
};
645674

646675
const handleParsedRecord = (parsed: Record<string, unknown>) => {
647676
sessionId = pickCliSessionId(parsed, params.backend) ?? sessionId;
@@ -659,6 +688,10 @@ export function createCliJsonlStreamingParser(params: {
659688
usage = nextUsage ?? usage;
660689
}
661690

691+
if (classifyClaudeCommentary && parsed.type === "result") {
692+
flushPendingClaudeAssistantText();
693+
}
694+
662695
const result = parseClaudeCliJsonlResult({
663696
backend: params.backend,
664697
providerId: params.providerId,
@@ -679,29 +712,16 @@ export function createCliJsonlStreamingParser(params: {
679712
}
680713
}
681714

682-
// Flush accumulated assistant text as commentary when a tool_use block starts.
683-
// Text preceding a tool call is inter-tool commentary, not final answer text.
684-
// Only flush if new text has arrived since the last flush to avoid duplicates
685-
// when multiple tool_use blocks appear consecutively without intervening text.
686-
if (
687-
params.onCommentaryText &&
688-
usesClaudeStreamJsonDialect(params) &&
689-
parsed.type === "stream_event" &&
690-
isRecord(parsed.event)
691-
) {
715+
if (classifyClaudeCommentary && parsed.type === "stream_event" && isRecord(parsed.event)) {
692716
const evt = parsed.event;
693717
if (
694718
evt.type === "content_block_start" &&
695719
isRecord(evt.content_block) &&
696720
isClaudeToolUseBlockType(evt.content_block.type)
697721
) {
698-
if (assistantText.length > lastFlushedCommentaryLength) {
699-
const newSegment = assistantText.slice(lastFlushedCommentaryLength).trim();
700-
if (newSegment) {
701-
params.onCommentaryText(newSegment);
702-
}
703-
lastFlushedCommentaryLength = assistantText.length;
704-
}
722+
flushPendingClaudeCommentaryText();
723+
} else if (evt.type === "content_block_start" || evt.type === "message_stop") {
724+
flushPendingClaudeAssistantText();
705725
}
706726
}
707727

@@ -727,6 +747,10 @@ export function createCliJsonlStreamingParser(params: {
727747
if (!delta) {
728748
return;
729749
}
750+
if (classifyClaudeCommentary) {
751+
pendingClaudeText = `${pendingClaudeText}${delta.delta}`;
752+
return;
753+
}
730754
assistantText = delta.text;
731755
params.onAssistantDelta(delta);
732756
};
@@ -769,6 +793,9 @@ export function createCliJsonlStreamingParser(params: {
769793
},
770794
finish() {
771795
flushLines(true);
796+
if (classifyClaudeCommentary) {
797+
flushPendingClaudeAssistantText();
798+
}
772799
},
773800
getOutput() {
774801
if (output) {

src/agents/cli-runner/claude-live-session.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,7 @@ function createTurn(params: {
11221122
onAssistantDelta: (delta: CliStreamingDelta) => void;
11231123
onToolUseStart?: (delta: CliToolUseStartDelta) => void;
11241124
onToolResult?: (delta: CliToolResultDelta) => void;
1125+
classifyCommentaryText?: boolean;
11251126
onCommentaryText?: (text: string) => void;
11261127
session: ClaudeLiveSession;
11271128
execPermission: ClaudeLiveExecPermission;
@@ -1149,6 +1150,7 @@ function createTurn(params: {
11491150
onAssistantDelta: params.onAssistantDelta,
11501151
onToolUseStart: params.onToolUseStart,
11511152
onToolResult: params.onToolResult,
1153+
classifyCommentaryText: params.classifyCommentaryText,
11521154
onCommentaryText: params.onCommentaryText,
11531155
}),
11541156
execPermission: params.execPermission,
@@ -1338,6 +1340,7 @@ export async function runClaudeLiveSessionTurn(params: {
13381340
onAssistantDelta: params.onAssistantDelta,
13391341
onToolUseStart: params.onToolUseStart,
13401342
onToolResult: params.onToolResult,
1343+
classifyCommentaryText: params.context.params.classifyCommentaryText,
13411344
onCommentaryText: params.onCommentaryText,
13421345
session: liveSession,
13431346
execPermission,

src/agents/cli-runner/execute.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,8 @@ export async function executePreparedCliRun(
556556
},
557557
onToolUseStart: emitCliToolUseStart,
558558
onToolResult: emitCliToolResult,
559-
onCommentaryText: emitCliCommentaryText,
559+
classifyCommentaryText: context.params.classifyCommentaryText,
560+
onCommentaryText: context.params.emitCommentaryText ? emitCliCommentaryText : undefined,
560561
cleanup: async () => {
561562
try {
562563
await fallbackClaudeSkillsPlugin?.cleanup();
@@ -601,7 +602,10 @@ export async function executePreparedCliRun(
601602
},
602603
onToolUseStart: emitCliToolUseStart,
603604
onToolResult: emitCliToolResult,
604-
onCommentaryText: emitCliCommentaryText,
605+
classifyCommentaryText: context.params.classifyCommentaryText,
606+
onCommentaryText: context.params.emitCommentaryText
607+
? emitCliCommentaryText
608+
: undefined,
605609
})
606610
: null;
607611
const supervisor = executeDeps.getProcessSupervisor();

src/agents/cli-runner/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export type RunCliAgentParams = {
102102
firstModelCallStarted?: boolean;
103103
}) => void;
104104
replyOperation?: ReplyOperation;
105+
classifyCommentaryText?: boolean;
106+
emitCommentaryText?: boolean;
105107
/**
106108
* Close any long-lived CLI live session created for this run after the run
107109
* finishes. Intended for temporary helper calls that should not keep process

src/auto-reply/get-reply-options.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ export type GetReplyOptions = {
137137
approvalId?: string;
138138
approvalSlug?: string;
139139
}) => Promise<void> | void;
140+
/** In progress mode, classify Claude pre-tool text; true also renders it as commentary. */
141+
commentaryProgressEnabled?: boolean;
140142
/** Called when the agent emits a structured plan update. */
141143
onPlanUpdate?: (payload: {
142144
phase?: string;

0 commit comments

Comments
 (0)