mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
fix(tui): emit v4 embedded chat deltas
(cherry picked from commit a6d878376b)
This commit is contained in:
@@ -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: "" }],
|
||||||
|
|||||||
@@ -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: "" }],
|
||||||
|
|||||||
Reference in New Issue
Block a user