fix(tui): emit v4 embedded chat deltas

(cherry picked from commit a6d878376b)
This commit is contained in:
Peter Steinberger
2026-05-13 16:27:38 +01:00
parent 64ba5e2ae3
commit 58591c37a4
2 changed files with 83 additions and 0 deletions

View File

@@ -207,6 +207,7 @@ describe("EmbeddedTuiBackend", () => {
runId: "run-local-1", runId: "run-local-1",
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
state: "delta", state: "delta",
deltaText: "hello",
message: { message: {
role: "assistant", role: "assistant",
content: [{ type: "text", text: "hello" }], content: [{ type: "text", text: "hello" }],
@@ -303,6 +304,65 @@ describe("EmbeddedTuiBackend", () => {
}); });
}); });
it("marks local embedded replacement deltas", async () => {
const { EmbeddedTuiBackend } = await import("./embedded-backend.js");
const pending = deferred<{
payloads: Array<{ text: string }>;
meta: Record<string, unknown>;
}>();
agentCommandFromIngressMock.mockReturnValueOnce(pending.promise);
const backend = new EmbeddedTuiBackend();
const events: Array<{ event: string; payload: unknown }> = [];
backend.onEvent = (evt) => {
events.push({ event: evt.event, payload: evt.payload });
};
backend.start();
await backend.sendChat({
sessionKey: "agent:main:main",
message: "replace",
runId: "run-local-replace",
});
registeredListener?.({
runId: "run-local-replace",
stream: "assistant",
data: { text: "Hello world" },
});
registeredListener?.({
runId: "run-local-replace",
stream: "assistant",
data: { text: "Goodbye world" },
});
pending.resolve({ payloads: [{ text: "Goodbye world" }], meta: {} });
await flushMicrotasks();
const chatPayloads = events
.filter((entry) => entry.event === "chat")
.map(
(entry) =>
entry.payload as {
state?: string;
deltaText?: string;
replace?: boolean;
},
);
expect(
chatPayloads
.filter((payload) => payload.state === "delta")
.map((payload) => ({
state: payload.state,
deltaText: payload.deltaText,
replace: payload.replace,
})),
).toEqual([
{ state: "delta", deltaText: "Hello world", replace: undefined },
{ state: "delta", deltaText: "Goodbye world", replace: true },
]);
});
it("keeps a fallback response deliverable after a retryable lifecycle error", async () => { it("keeps a fallback response deliverable after a retryable lifecycle error", async () => {
const { EmbeddedTuiBackend } = await import("./embedded-backend.js"); const { EmbeddedTuiBackend } = await import("./embedded-backend.js");
const pending = deferred<{ const pending = deferred<{
@@ -496,6 +556,7 @@ describe("EmbeddedTuiBackend", () => {
runId: "run-tool-first", runId: "run-tool-first",
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
state: "delta", state: "delta",
deltaText: "",
message: { message: {
role: "assistant", role: "assistant",
content: [{ type: "text", text: "" }], content: [{ type: "text", text: "" }],

View File

@@ -60,6 +60,7 @@ type LocalRunState = {
sessionKey: string; sessionKey: string;
controller: AbortController; controller: AbortController;
buffer: string; buffer: string;
lastBroadcastText?: string;
isBtw: boolean; isBtw: boolean;
question?: string; question?: string;
finalSent: boolean; finalSent: boolean;
@@ -106,6 +107,16 @@ function timeoutSecondsFromMs(timeoutMs?: number): string | undefined {
return String(Math.max(0, Math.ceil(timeoutMs / 1000))); return String(Math.max(0, Math.ceil(timeoutMs / 1000)));
} }
function resolveDeltaPayload(text: string, previousText: string | undefined) {
if (previousText === undefined) {
return { deltaText: text };
}
if (!text.startsWith(previousText)) {
return { deltaText: text, replace: true as const };
}
return { deltaText: text.slice(previousText.length) };
}
export class EmbeddedTuiBackend implements TuiBackend { export class EmbeddedTuiBackend implements TuiBackend {
readonly connection = { url: "local embedded" }; readonly connection = { url: "local embedded" };
@@ -396,11 +407,17 @@ export class EmbeddedTuiBackend implements TuiBackend {
if (!text || projected.suppress) { if (!text || projected.suppress) {
return; return;
} }
const deltaPayload = resolveDeltaPayload(text, run.lastBroadcastText);
if (!deltaPayload.deltaText && !deltaPayload.replace) {
return;
}
run.registered = true; run.registered = true;
run.lastBroadcastText = text;
this.emit("chat", { this.emit("chat", {
runId, runId,
sessionKey: run.sessionKey, sessionKey: run.sessionKey,
state: "delta", state: "delta",
...deltaPayload,
message: { message: {
role: "assistant", role: "assistant",
content: [{ type: "text", text }], content: [{ type: "text", text }],
@@ -416,6 +433,7 @@ export class EmbeddedTuiBackend implements TuiBackend {
} }
run.finalSent = true; run.finalSent = true;
run.registered = true; run.registered = true;
run.lastBroadcastText = undefined;
const projected = projectLiveAssistantBufferedText(run.buffer.trim(), { const projected = projectLiveAssistantBufferedText(run.buffer.trim(), {
suppressLeadFragments: false, suppressLeadFragments: false,
}); });
@@ -445,6 +463,7 @@ export class EmbeddedTuiBackend implements TuiBackend {
} }
run.finalSent = true; run.finalSent = true;
run.registered = true; run.registered = true;
run.lastBroadcastText = undefined;
this.emit("chat", { this.emit("chat", {
runId, runId,
sessionKey: run.sessionKey, sessionKey: run.sessionKey,
@@ -459,6 +478,7 @@ export class EmbeddedTuiBackend implements TuiBackend {
} }
run.finalSent = true; run.finalSent = true;
run.registered = true; run.registered = true;
run.lastBroadcastText = undefined;
this.emit("chat", { this.emit("chat", {
runId, runId,
sessionKey: run.sessionKey, sessionKey: run.sessionKey,
@@ -472,10 +492,12 @@ export class EmbeddedTuiBackend implements TuiBackend {
return; return;
} }
run.registered = true; run.registered = true;
run.lastBroadcastText = "";
this.emit("chat", { this.emit("chat", {
runId, runId,
sessionKey: run.sessionKey, sessionKey: run.sessionKey,
state: "delta", state: "delta",
deltaText: "",
message: { message: {
role: "assistant", role: "assistant",
content: [{ type: "text", text: "" }], content: [{ type: "text", text: "" }],