fix(react-google-adk): keep every text part of a multi-part event (#6221)

This commit is contained in:
KinfeMichael Tariku
2026-08-22 01:07:41 +03:00
committed by GitHub
parent 622ac53047
commit 31a049fcfa
3 changed files with 103 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@assistant-ui/react-google-adk": patch
---
fix: keep every text and reasoning part of a multi-part ADK event
@@ -1477,3 +1477,86 @@ describe("AdkEventAccumulator - user message handling", () => {
});
});
});
describe("AdkEventAccumulator - multiple parts in one event", () => {
it("keeps every text part of a single non-partial event in order", () => {
const acc = new AdkEventAccumulator();
const msgs = acc.processEvent(
makeEvent({
author: "agent",
content: { role: "model", parts: [{ text: "A" }, { text: "B" }] },
}),
);
expect(msgs[0]?.content).toEqual([
{ type: "text", text: "A" },
{ type: "text", text: "B" },
]);
});
it("preserves the Gemini code-execution shape", () => {
const acc = new AdkEventAccumulator();
const msgs = acc.processEvent(
makeEvent({
author: "agent",
content: {
role: "model",
parts: [
{ text: "Here is the code:" },
{ executableCode: { code: "print(1)", language: "python" } },
{ codeExecutionResult: { outcome: "OUTCOME_OK", output: "1" } },
{ text: "The result is 1." },
],
},
}),
);
expect(msgs[0]?.content).toEqual([
{ type: "text", text: "Here is the code:" },
{ type: "code", code: "print(1)", language: "python" },
{ type: "code_result", output: "1", outcome: "OUTCOME_OK" },
{ type: "text", text: "The result is 1." },
]);
});
it("keeps every reasoning part of a single non-partial event", () => {
const acc = new AdkEventAccumulator();
const msgs = acc.processEvent(
makeEvent({
author: "agent",
content: {
role: "model",
parts: [
{ text: "First thought", thought: true },
{ text: "Second thought", thought: true },
],
},
}),
);
expect(msgs[0]?.content).toEqual([
{ type: "reasoning", text: "First thought" },
{ type: "reasoning", text: "Second thought" },
]);
});
it("still replaces the streamed buffer with the final text", () => {
const acc = new AdkEventAccumulator();
acc.processEvent(makeTextEvent("Hel", true));
acc.processEvent(makeTextEvent("lo", true));
const msgs = acc.processEvent(
makeEvent({
author: "agent",
content: {
role: "model",
parts: [{ text: "Hello" }, { text: "Again" }],
},
}),
);
expect(msgs[0]?.content).toEqual([
{ type: "text", text: "Hello" },
{ type: "text", text: "Again" },
]);
});
});
@@ -191,6 +191,8 @@ export class AdkEventAccumulator {
private messagesMap = new Map<string, AdkMessage>();
private currentMessageId: string | null = null;
private partialTextBuffer = "";
private finalTextReplacedThisEvent = false;
private finalReasoningReplacedThisEvent = false;
private partialReasoningBuffer = "";
private accumulatedStateDelta: Record<string, unknown> = {};
private accumulatedArtifactDelta: Record<string, number> = {};
@@ -387,6 +389,11 @@ export class AdkEventAccumulator {
}
}
// Replace-semantics close out the streamed partial buffer, which only
// the first final text/reasoning part of an event may do; later parts
// of the same event are distinct content and append.
this.finalTextReplacedThisEvent = false;
this.finalReasoningReplacedThisEvent = false;
for (const [index, part] of parts.entries()) {
this.processPart(part, event, index);
}
@@ -472,9 +479,12 @@ export class AdkEventAccumulator {
if (event.partial) {
this.partialReasoningBuffer += part.text;
this.replaceLastReasoningContent(msg, this.partialReasoningBuffer);
} else {
} else if (!this.finalReasoningReplacedThisEvent) {
this.finalReasoningReplacedThisEvent = true;
this.partialReasoningBuffer = "";
this.replaceLastReasoningContent(msg, part.text);
} else {
this.appendContent(msg, { type: "reasoning", text: part.text });
}
return;
}
@@ -485,9 +495,12 @@ export class AdkEventAccumulator {
if (event.partial) {
this.partialTextBuffer += part.text;
this.replaceLastTextContent(msg, this.partialTextBuffer);
} else {
} else if (!this.finalTextReplacedThisEvent) {
this.finalTextReplacedThisEvent = true;
this.partialTextBuffer = "";
this.replaceLastTextContent(msg, part.text);
} else {
this.appendContent(msg, { type: "text", text: part.text });
}
return;
}