mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix(sdk): emit replacement chat projection deltas
Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
@@ -219,15 +219,23 @@ function isTerminalRunEvent(event: OpenClawEvent): boolean {
|
||||
function normalizeChatProjectionEvent(
|
||||
event: OpenClawEvent,
|
||||
projection: ChatProjection,
|
||||
previousText: string | undefined,
|
||||
): OpenClawEvent {
|
||||
const text = readChatProjectionText(projection.payload);
|
||||
const isReplacement = Boolean(
|
||||
previousText && text !== undefined && !text.startsWith(previousText),
|
||||
);
|
||||
return {
|
||||
...event,
|
||||
type: projection.state === "delta" ? "assistant.delta" : "run.completed",
|
||||
data:
|
||||
projection.state === "delta"
|
||||
? text !== undefined
|
||||
? { delta: text }
|
||||
? {
|
||||
text,
|
||||
delta: isReplacement ? text : text.slice(previousText?.length ?? 0),
|
||||
...(isReplacement ? { replace: true } : {}),
|
||||
}
|
||||
: event.data
|
||||
: { phase: "end", ...(text !== undefined ? { outputText: text } : {}) },
|
||||
};
|
||||
@@ -335,20 +343,30 @@ export class OpenClaw {
|
||||
const replayEvents = this.replaySnapshot(runId);
|
||||
let hasCanonicalAssistantRunEvent = replayEvents.some(isAssistantRunEvent);
|
||||
let hasTerminalRunEvent = replayEvents.some(isTerminalRunEvent);
|
||||
let previousChatProjectionText: string | undefined;
|
||||
const toRunStreamEvent = (event: OpenClawEvent): OpenClawEvent | undefined => {
|
||||
const chatProjection = readChatProjection(event);
|
||||
if (chatProjection?.state === "delta") {
|
||||
if (hasCanonicalAssistantRunEvent) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeChatProjectionEvent(event, chatProjection);
|
||||
const runEvent = normalizeChatProjectionEvent(
|
||||
event,
|
||||
chatProjection,
|
||||
previousChatProjectionText,
|
||||
);
|
||||
const text = readChatProjectionText(chatProjection.payload);
|
||||
if (text !== undefined) {
|
||||
previousChatProjectionText = text;
|
||||
}
|
||||
return runEvent;
|
||||
}
|
||||
if (chatProjection?.state === "final") {
|
||||
if (hasTerminalRunEvent) {
|
||||
return undefined;
|
||||
}
|
||||
hasTerminalRunEvent = true;
|
||||
return normalizeChatProjectionEvent(event, chatProjection);
|
||||
return normalizeChatProjectionEvent(event, chatProjection, previousChatProjectionText);
|
||||
}
|
||||
if (isAssistantRunEvent(event)) {
|
||||
hasCanonicalAssistantRunEvent = true;
|
||||
|
||||
@@ -499,20 +499,34 @@ describe("OpenClaw SDK", () => {
|
||||
payload: {
|
||||
runId: "run_chat_only",
|
||||
sessionKey: "chat-only",
|
||||
state: "final",
|
||||
state: "delta",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "hello again" }],
|
||||
content: [{ type: "text", text: "reset" }],
|
||||
timestamp: ts + 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
fake.emit({
|
||||
event: "custom.debug",
|
||||
event: "chat",
|
||||
seq: 4,
|
||||
payload: {
|
||||
runId: "run_chat_only",
|
||||
ts: ts + 3,
|
||||
sessionKey: "chat-only",
|
||||
state: "final",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "reset" }],
|
||||
timestamp: ts + 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
fake.emit({
|
||||
event: "custom.debug",
|
||||
seq: 5,
|
||||
payload: {
|
||||
runId: "run_chat_only",
|
||||
ts: ts + 4,
|
||||
data: { ok: true },
|
||||
},
|
||||
});
|
||||
@@ -534,7 +548,7 @@ describe("OpenClaw SDK", () => {
|
||||
done: false,
|
||||
value: {
|
||||
type: "assistant.delta",
|
||||
data: { delta: "hello" },
|
||||
data: { text: "hello", delta: "hello" },
|
||||
raw: { event: "chat" },
|
||||
},
|
||||
});
|
||||
@@ -544,17 +558,27 @@ describe("OpenClaw SDK", () => {
|
||||
done: false,
|
||||
value: {
|
||||
type: "assistant.delta",
|
||||
data: { delta: "hello again" },
|
||||
data: { text: "hello again", delta: " again" },
|
||||
raw: { event: "chat" },
|
||||
},
|
||||
});
|
||||
|
||||
const third = await iterator.next();
|
||||
expect(third).toMatchObject({
|
||||
done: false,
|
||||
value: {
|
||||
type: "assistant.delta",
|
||||
data: { text: "reset", delta: "reset", replace: true },
|
||||
raw: { event: "chat" },
|
||||
},
|
||||
});
|
||||
|
||||
const fourth = await iterator.next();
|
||||
expect(fourth).toMatchObject({
|
||||
done: false,
|
||||
value: {
|
||||
type: "run.completed",
|
||||
data: { phase: "end", outputText: "hello again" },
|
||||
data: { phase: "end", outputText: "reset" },
|
||||
raw: { event: "chat" },
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user