Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions packages/claw-client/src/lib/chat/openclaw-agui-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export function createOpenClawAGUIMapper(onEvent: (event: Record<string, unknown
} {
let messageId: string | null = null;
let emittedTextContent = false;
// Whether we've streamed visible assistant text into the current run's
// trailing text run. Used to decide if a v4 `replace` event needs a
// boundary marker to relegate the superseded draft.
let assistantTextStreamed = false;
const activeToolCallIds = new Set<string>();

const extractTextFromMessageContent = (message: unknown): string => {
Expand Down Expand Up @@ -98,6 +102,7 @@ export function createOpenClawAGUIMapper(onEvent: (event: Record<string, unknown
const ensureMessageStarted = (runId: string) => {
if (!messageId) {
messageId = runId;
assistantTextStreamed = false;
emitEvent({ type: EventType.TEXT_MESSAGE_START, messageId, role: "assistant" });
}
};
Expand Down Expand Up @@ -215,13 +220,49 @@ export function createOpenClawAGUIMapper(onEvent: (event: Record<string, unknown
}

if (evt.stream === "assistant") {
// `data.delta` from the openclaw 2026.5.x gateway is already
// incremental — the resolver subtracts cumulative snapshots upstream
// and emits only the new tokens. Forward straight to the AG-UI
// consumer; no client-side dedupe needed.
// v4 `replace`: the new cumulative text does NOT extend what we've
// already streamed (a rewrite — e.g. a heartbeat placeholder swapped
// for the real answer). AG-UI text content is append-only, so we
// can't mutate the prior text in place. Emit an `assistant_update`
// boundary marker: `extractAssistantTimeline` keeps only the LAST
// text run as the visible answer and relegates the superseded text
// before the marker into a collapsed draft row — the same primitive
// history-merger uses. Without the marker the old and new text would
// concatenate ("GotGot it…"-style duplication).
if (evt.data.replace === true) {
const replacement =
typeof evt.data.text === "string" && evt.data.text
? evt.data.text
: typeof evt.data.delta === "string"
? evt.data.delta
: "";
ensureMessageStarted(evt.runId);
if (assistantTextStreamed) {
emitEvent({
type: EventType.TEXT_MESSAGE_CONTENT,
messageId,
delta: encodeAssistantTimelineSegment({ type: "assistant_update", text: "" }),
});
assistantTextStreamed = false;
}
if (replacement) {
emittedTextContent = true;
assistantTextStreamed = true;
emitEvent({
type: EventType.TEXT_MESSAGE_CONTENT,
messageId,
delta: replacement,
});
}
return;
}
// Normal stream: `data.delta` is already incremental — the gateway
// subtracts cumulative snapshots upstream and emits only the new
// tokens. Forward straight to the AG-UI consumer; no dedupe needed.
if (typeof evt.data.delta === "string" && evt.data.delta) {
ensureMessageStarted(evt.runId);
emittedTextContent = true;
assistantTextStreamed = true;
emitEvent({
type: EventType.TEXT_MESSAGE_CONTENT,
messageId,
Expand Down
2 changes: 1 addition & 1 deletion packages/claw-client/src/lib/gateway/handshake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { signMessage, toBase64Url } from "./device-identity";
import type { ConnectParams } from "./types";
import { GATEWAY_CLIENT_CAPS, GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./types";

const PROTOCOL_VERSION = 3;
const PROTOCOL_VERSION = 4;
const CLIENT_ID = GATEWAY_CLIENT_IDS.CONTROL_UI;
const CLIENT_MODE = GATEWAY_CLIENT_MODES.UI;
const CLIENT_VERSION = "0.1.0";
Expand Down
10 changes: 10 additions & 0 deletions packages/claw-client/src/lib/gateway/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,17 @@ export interface ChatEvent {

// Typed data shapes for each event:agent stream variant
export interface AssistantStreamData {
/** Incremental new tokens. On a `replace` event this is the full new text. */
delta: string;
/** Cumulative assistant text so far (authoritative on `replace`). */
text?: string;
/**
* Set by the v4 gateway when the new cumulative text does NOT extend the
* previously streamed text (a rewrite, e.g. a heartbeat placeholder being
* swapped for the real answer). Consumers must discard prior streamed text
* rather than append.
*/
replace?: boolean;
}

export interface ThinkingStreamData {
Expand Down
2 changes: 1 addition & 1 deletion packages/claw-plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openuidev/openclaw-os-plugin",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"description": "OpenClaw plugin that turns agent responses into Generative UI and serves the claw-client at /plugins/openclawos.",
"keywords": [
Expand Down
Loading