fix(streaming): preserve incomplete bracket tails in final replies (#116983)

Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
Peter Steinberger
2026-07-31 13:45:07 -07:00
committed by GitHub
parent d66b514a7e
commit bc1e95fb62
2 changed files with 43 additions and 9 deletions
@@ -470,6 +470,41 @@ describe("handleMessageUpdate text signatures", () => {
expect(context.state.lastStreamedAssistantCleaned).toBe("Hello");
});
it.each([
{
name: "the directive accumulator has no parsed result",
text: "answer part A msg [[E1008]timeout] answer part B",
hasParsedDirectives: false,
},
{
name: "the directive accumulator flushes a buffered tail",
text: "answer part A msg [[E1008]timeout] answer part B",
hasParsedDirectives: true,
},
{
name: "the final text ends with one bracket",
text: "answer part A [",
hasParsedDirectives: true,
},
])("keeps literal final text when $name", ({ text, hasParsedDirectives }) => {
const onAgentEvent = vi.fn();
const context = createMessageUpdateContext({
onAgentEvent,
...(hasParsedDirectives ? {} : { consumePartialReplyDirectives: vi.fn(() => null) }),
});
updateMessage(context, {
message: { role: "assistant", content: [] },
assistantMessageEvent: { type: "text_end", content: text },
});
expect(context.state.lastStreamedAssistantCleaned).toBe(text);
expect(firstMockArg(onAgentEvent, "final assistant event")).toMatchObject({
stream: "assistant",
data: { text },
});
});
it("keeps stripped reply directives out of later plain deltas", () => {
const onAgentEvent = vi.fn();
const context = createMessageUpdateContext({ onAgentEvent });
@@ -616,10 +616,6 @@ function mergeReplyDirectiveResults(
};
}
function parseFullStreamingReplyText(text: string): string {
return parseReplyDirectives(splitTrailingDirective(text).text).text;
}
function containsCompleteMediaDirectiveLine(text: string): boolean {
return /(?:^|\n)\s*MEDIA:\s*\S[^\n]*(?:\n|$)/i.test(text);
}
@@ -664,13 +660,16 @@ function resolveStreamingReplyText(params: {
parsedStreamDirectives: ReplyDirectiveParseResult | null;
shouldUsePhaseAwareBlockReply: boolean;
}): string {
if (!params.parsedStreamDirectives) {
return params.evtType === "text_delta"
? params.previousCleaned
: parseFullStreamingReplyText(params.next);
if (!params.parsedStreamDirectives && params.evtType === "text_delta") {
return params.previousCleaned;
}
return resolveIncrementalStreamingReplyText(params) ?? parseFullStreamingReplyText(params.next);
return (
resolveIncrementalStreamingReplyText(params) ??
parseReplyDirectives(
params.evtType === "text_end" ? params.next : splitTrailingDirective(params.next).text,
).text
);
}
/** Records parsed reply directives until a sendable reply payload is built. */