@hypercli.com/sdk 2026.3.16 → 2026.3.18-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/gateway.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * Protocol: OpenClaw Gateway v3
8
8
  */
9
9
  import { getPublicKeyAsync, signAsync, utils as edUtils } from "@noble/ed25519";
10
+ import NodeWebSocket from "ws";
10
11
  class GatewayRequestError extends Error {
11
12
  gatewayCode;
12
13
  details;
@@ -21,8 +22,8 @@ const PROTOCOL_VERSION = 3;
21
22
  const DEFAULT_TIMEOUT = 15_000;
22
23
  const CHAT_TIMEOUT = 120_000;
23
24
  const RECONNECT_CLOSE_CODE = 4008;
24
- const DEFAULT_CLIENT_ID = "openclaw-control-ui";
25
- const DEFAULT_CLIENT_MODE = "webchat";
25
+ const DEFAULT_CLIENT_ID = "cli";
26
+ const DEFAULT_CLIENT_MODE = "cli";
26
27
  const DEFAULT_CLIENT_VERSION = "@hypercli/sdk";
27
28
  const DEFAULT_CAPS = ["tool-events"];
28
29
  const CONNECT_TIMER_MS = 750;
@@ -72,6 +73,331 @@ function asRecord(value) {
72
73
  ? value
73
74
  : null;
74
75
  }
76
+ function asContentItems(value) {
77
+ if (!Array.isArray(value)) {
78
+ return [];
79
+ }
80
+ return value
81
+ .map((item) => asRecord(item))
82
+ .filter((item) => item !== null);
83
+ }
84
+ function normalizeToolArgs(value) {
85
+ if (typeof value !== "string") {
86
+ return value;
87
+ }
88
+ const trimmed = value.trim();
89
+ if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) {
90
+ return value;
91
+ }
92
+ try {
93
+ return JSON.parse(trimmed);
94
+ }
95
+ catch {
96
+ return value;
97
+ }
98
+ }
99
+ function stringifyToolResult(value) {
100
+ if (value == null) {
101
+ return undefined;
102
+ }
103
+ if (typeof value === "string") {
104
+ return value;
105
+ }
106
+ try {
107
+ return JSON.stringify(value, null, 2);
108
+ }
109
+ catch {
110
+ return String(value);
111
+ }
112
+ }
113
+ function gatewayToolCallId(record) {
114
+ const direct = (typeof record.id === "string" && record.id.trim()) ||
115
+ (typeof record.toolCallId === "string" && record.toolCallId.trim()) ||
116
+ (typeof record.tool_call_id === "string" && record.tool_call_id.trim());
117
+ return direct || undefined;
118
+ }
119
+ function gatewayToolName(record) {
120
+ const direct = (typeof record.name === "string" && record.name.trim()) ||
121
+ (typeof record.toolName === "string" && record.toolName.trim()) ||
122
+ (typeof record.tool_name === "string" && record.tool_name.trim());
123
+ return direct || undefined;
124
+ }
125
+ function mergeGatewayToolResult(toolCalls, result) {
126
+ const next = [...toolCalls];
127
+ let index = -1;
128
+ for (let cursor = next.length - 1; cursor >= 0; cursor -= 1) {
129
+ const entry = next[cursor];
130
+ if (result.id && entry.id && entry.id === result.id) {
131
+ index = cursor;
132
+ break;
133
+ }
134
+ if (result.name && entry.name === result.name && entry.result == null) {
135
+ index = cursor;
136
+ break;
137
+ }
138
+ }
139
+ if (index >= 0) {
140
+ const current = next[index];
141
+ next[index] = {
142
+ ...current,
143
+ ...(result.id ? { id: result.id } : {}),
144
+ ...(result.result !== undefined ? { result: result.result } : {}),
145
+ };
146
+ return next;
147
+ }
148
+ next.push(result);
149
+ return next;
150
+ }
151
+ export function extractGatewayChatThinking(message) {
152
+ const record = asRecord(message);
153
+ if (!record) {
154
+ return "";
155
+ }
156
+ const parts = asContentItems(record.content)
157
+ .map((item) => {
158
+ if (item.type !== "thinking" || typeof item.thinking !== "string") {
159
+ return null;
160
+ }
161
+ return item.thinking.trim();
162
+ })
163
+ .filter((value) => Boolean(value));
164
+ return parts.join("\n");
165
+ }
166
+ export function extractGatewayChatMediaUrls(message) {
167
+ const record = asRecord(message);
168
+ if (!record) {
169
+ return [];
170
+ }
171
+ const mediaUrls = [];
172
+ for (const item of asContentItems(record.content)) {
173
+ if (item.type !== "image") {
174
+ continue;
175
+ }
176
+ const source = asRecord(item.source);
177
+ if (!source) {
178
+ continue;
179
+ }
180
+ if (source.type === "url" && typeof source.url === "string" && source.url.trim()) {
181
+ mediaUrls.push(source.url);
182
+ continue;
183
+ }
184
+ if (source.type === "base64" && typeof source.data === "string" && source.data.trim()) {
185
+ const mimeType = typeof source.media_type === "string" && source.media_type.trim()
186
+ ? source.media_type.trim()
187
+ : "image/png";
188
+ mediaUrls.push(`data:${mimeType};base64,${source.data}`);
189
+ }
190
+ }
191
+ if (typeof record.mediaUrl === "string" && record.mediaUrl.trim()) {
192
+ mediaUrls.push(record.mediaUrl);
193
+ }
194
+ if (Array.isArray(record.mediaUrls)) {
195
+ for (const entry of record.mediaUrls) {
196
+ if (typeof entry === "string" && entry.trim()) {
197
+ mediaUrls.push(entry);
198
+ }
199
+ }
200
+ }
201
+ return mediaUrls;
202
+ }
203
+ export function extractGatewayChatToolCalls(message) {
204
+ const record = asRecord(message);
205
+ if (!record) {
206
+ return [];
207
+ }
208
+ let toolCalls = [];
209
+ for (const item of asContentItems(record.content)) {
210
+ const kind = typeof item.type === "string" ? item.type.trim().toLowerCase() : "";
211
+ const name = gatewayToolName(item);
212
+ const id = gatewayToolCallId(item);
213
+ if (kind === "toolcall" ||
214
+ kind === "tool_call" ||
215
+ kind === "tooluse" ||
216
+ kind === "tool_use" ||
217
+ (name && (item.arguments !== undefined || item.args !== undefined))) {
218
+ toolCalls.push({
219
+ ...(id ? { id } : {}),
220
+ name: name ?? "tool",
221
+ args: normalizeToolArgs(item.arguments ?? item.args),
222
+ });
223
+ continue;
224
+ }
225
+ if (kind === "toolresult" || kind === "tool_result") {
226
+ toolCalls = mergeGatewayToolResult(toolCalls, {
227
+ ...(id ? { id } : {}),
228
+ name: name ?? "tool",
229
+ result: stringifyToolResult(item.text ?? item.content ?? item.result),
230
+ });
231
+ }
232
+ }
233
+ if (Array.isArray(record.tool_calls)) {
234
+ for (const item of record.tool_calls) {
235
+ const tool = asRecord(item);
236
+ if (!tool)
237
+ continue;
238
+ const name = gatewayToolName(tool);
239
+ if (!name)
240
+ continue;
241
+ toolCalls.push({
242
+ ...(gatewayToolCallId(tool) ? { id: gatewayToolCallId(tool) } : {}),
243
+ name,
244
+ args: normalizeToolArgs(tool.arguments ?? tool.args),
245
+ });
246
+ }
247
+ }
248
+ const topLevelToolName = gatewayToolName(record);
249
+ const topLevelResult = stringifyToolResult(record.result ?? record.content ?? record.text ?? record.partialResult);
250
+ const role = typeof record.role === "string" ? record.role.trim().toLowerCase() : "";
251
+ if (topLevelToolName &&
252
+ topLevelResult &&
253
+ (role === "toolresult" || role === "tool_result" || record.toolCallId || record.tool_call_id)) {
254
+ toolCalls = mergeGatewayToolResult(toolCalls, {
255
+ ...(gatewayToolCallId(record) ? { id: gatewayToolCallId(record) } : {}),
256
+ name: topLevelToolName,
257
+ result: topLevelResult,
258
+ });
259
+ }
260
+ return toolCalls;
261
+ }
262
+ export function normalizeGatewayChatMessage(message) {
263
+ const record = asRecord(message);
264
+ if (!record) {
265
+ return null;
266
+ }
267
+ const text = extractMessageText(record) ?? "";
268
+ const thinking = extractGatewayChatThinking(record);
269
+ const toolCalls = extractGatewayChatToolCalls(record);
270
+ const mediaUrls = extractGatewayChatMediaUrls(record);
271
+ const timestamp = typeof record.timestamp === "number" ? record.timestamp : undefined;
272
+ const role = typeof record.role === "string" && record.role.trim() ? record.role : "assistant";
273
+ if (!text && !thinking && toolCalls.length === 0 && mediaUrls.length === 0) {
274
+ return null;
275
+ }
276
+ return {
277
+ role,
278
+ text,
279
+ thinking,
280
+ toolCalls,
281
+ mediaUrls,
282
+ ...(timestamp !== undefined ? { timestamp } : {}),
283
+ };
284
+ }
285
+ function extractMessageText(message) {
286
+ if (typeof message === "string") {
287
+ return message;
288
+ }
289
+ const record = asRecord(message);
290
+ if (!record) {
291
+ return null;
292
+ }
293
+ const content = record.content;
294
+ if (typeof content === "string") {
295
+ return content;
296
+ }
297
+ if (Array.isArray(content)) {
298
+ const parts = content
299
+ .map((entry) => {
300
+ const item = asRecord(entry);
301
+ if (!item || item.type !== "text" || typeof item.text !== "string" || !item.text) {
302
+ return null;
303
+ }
304
+ return item.text;
305
+ })
306
+ .filter((value) => typeof value === "string");
307
+ if (parts.length > 0) {
308
+ return parts.join("\n");
309
+ }
310
+ }
311
+ return typeof record.text === "string" ? record.text : null;
312
+ }
313
+ function extractMessageRunId(message) {
314
+ const record = asRecord(message);
315
+ if (!record) {
316
+ return null;
317
+ }
318
+ const directRunId = typeof record.runId === "string" ? record.runId.trim() : "";
319
+ if (directRunId) {
320
+ return directRunId;
321
+ }
322
+ const agentRunId = typeof record.agentRunId === "string" ? record.agentRunId.trim() : "";
323
+ if (agentRunId) {
324
+ return agentRunId;
325
+ }
326
+ const meta = asRecord(record.meta);
327
+ const metaRunId = typeof meta?.runId === "string" ? meta.runId.trim() : "";
328
+ return metaRunId || null;
329
+ }
330
+ function latestHistoryAssistantText(messages, acceptedRunIds) {
331
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
332
+ const message = asRecord(messages[index]);
333
+ if (!message) {
334
+ continue;
335
+ }
336
+ const role = typeof message.role === "string" ? message.role.trim().toLowerCase() : "";
337
+ if (role !== "assistant") {
338
+ continue;
339
+ }
340
+ const messageRunId = extractMessageRunId(message);
341
+ if (messageRunId && acceptedRunIds.size > 0 && !acceptedRunIds.has(messageRunId)) {
342
+ continue;
343
+ }
344
+ const text = extractMessageText(message)?.trim();
345
+ if (text) {
346
+ return text;
347
+ }
348
+ }
349
+ return null;
350
+ }
351
+ function streamDelta(previousText, nextText) {
352
+ if (!nextText) {
353
+ return { delta: "", nextText: previousText };
354
+ }
355
+ if (previousText && nextText.startsWith(previousText)) {
356
+ return { delta: nextText.slice(previousText.length), nextText };
357
+ }
358
+ if (nextText === previousText) {
359
+ return { delta: "", nextText: previousText };
360
+ }
361
+ return { delta: nextText, nextText };
362
+ }
363
+ function parseAgentSessionKey(sessionKey) {
364
+ const normalized = (sessionKey ?? "").trim().toLowerCase();
365
+ if (!normalized) {
366
+ return null;
367
+ }
368
+ const parts = normalized.split(":").filter(Boolean);
369
+ if (parts.length < 3 || parts[0] !== "agent") {
370
+ return null;
371
+ }
372
+ const agentId = parts[1]?.trim();
373
+ const rest = parts.slice(2).join(":").trim();
374
+ if (!agentId || !rest) {
375
+ return null;
376
+ }
377
+ return { agentId, rest };
378
+ }
379
+ function sameSessionKey(left, right) {
380
+ const normalizedLeft = (left ?? "").trim().toLowerCase();
381
+ const normalizedRight = (right ?? "").trim().toLowerCase();
382
+ if (!normalizedLeft || !normalizedRight) {
383
+ return false;
384
+ }
385
+ if (normalizedLeft === normalizedRight) {
386
+ return true;
387
+ }
388
+ const parsedLeft = parseAgentSessionKey(normalizedLeft);
389
+ const parsedRight = parseAgentSessionKey(normalizedRight);
390
+ if (parsedLeft && parsedRight) {
391
+ return parsedLeft.agentId === parsedRight.agentId && parsedLeft.rest === parsedRight.rest;
392
+ }
393
+ if (parsedLeft) {
394
+ return parsedLeft.rest === normalizedRight;
395
+ }
396
+ if (parsedRight) {
397
+ return normalizedLeft === parsedRight.rest;
398
+ }
399
+ return false;
400
+ }
75
401
  function splitConfigPath(path) {
76
402
  return path
77
403
  .split(".")
@@ -641,7 +967,12 @@ export class GatewayClient {
641
967
  this.caps = Array.isArray(options.caps)
642
968
  ? options.caps.map((cap) => cap.trim()).filter(Boolean)
643
969
  : [...DEFAULT_CAPS];
644
- this.origin = options.origin ?? "https://hypercli.com";
970
+ // Non-browser SDK clients should not send Origin by default. OpenClaw
971
+ // treats any Origin header as browser-originated and applies browser
972
+ // origin checks to the connection.
973
+ this.origin = typeof options.origin === "string" && options.origin.trim()
974
+ ? options.origin.trim()
975
+ : undefined;
645
976
  this.defaultTimeout = options.timeout ?? DEFAULT_TIMEOUT;
646
977
  this.onHello = options.onHello;
647
978
  this.onClose = options.onClose;
@@ -766,7 +1097,8 @@ export class GatewayClient {
766
1097
  openSocket() {
767
1098
  if (this.closed || this.ws)
768
1099
  return;
769
- if (typeof WebSocket === "undefined") {
1100
+ const useBrowserSocket = "localStorage" in globalThis && typeof WebSocket !== "undefined";
1101
+ if (!useBrowserSocket && typeof NodeWebSocket === "undefined") {
770
1102
  throw new Error("WebSocket is not available in this environment");
771
1103
  }
772
1104
  if (this.reconnectTimer) {
@@ -776,20 +1108,38 @@ export class GatewayClient {
776
1108
  const wsUrl = this.token
777
1109
  ? `${this.url}${this.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.token)}`
778
1110
  : this.url;
779
- const ws = new WebSocket(wsUrl);
1111
+ const ws = useBrowserSocket
1112
+ ? new WebSocket(wsUrl)
1113
+ : new NodeWebSocket(wsUrl, this.origin ? { headers: { Origin: this.origin } } : undefined);
780
1114
  this.ws = ws;
781
- ws.onopen = () => {
1115
+ if (useBrowserSocket) {
1116
+ ws.onopen = () => {
1117
+ this.queueConnect();
1118
+ };
1119
+ ws.onmessage = (event) => {
1120
+ this.handleMessage(String(event.data ?? ""));
1121
+ };
1122
+ ws.onerror = () => {
1123
+ // Close handling covers retries and surfaced errors.
1124
+ };
1125
+ ws.onclose = (event) => {
1126
+ this.handleClose(ws, event.code ?? 1006, String(event.reason ?? ""));
1127
+ };
1128
+ return;
1129
+ }
1130
+ const nodeWs = ws;
1131
+ nodeWs.on("open", () => {
782
1132
  this.queueConnect();
783
- };
784
- ws.onmessage = (event) => {
785
- this.handleMessage(String(event.data ?? ""));
786
- };
787
- ws.onerror = () => {
1133
+ });
1134
+ nodeWs.on("message", (data) => {
1135
+ this.handleMessage(typeof data === "string" ? data : data.toString());
1136
+ });
1137
+ nodeWs.on("error", () => {
788
1138
  // Close handling covers retries and surfaced errors.
789
- };
790
- ws.onclose = (event) => {
791
- this.handleClose(ws, event.code ?? 1006, String(event.reason ?? ""));
792
- };
1139
+ });
1140
+ nodeWs.on("close", (code, reason) => {
1141
+ this.handleClose(ws, code ?? 1006, reason?.toString() ?? "");
1142
+ });
793
1143
  }
794
1144
  queueConnect() {
795
1145
  this.connectNonce = null;
@@ -1112,15 +1462,89 @@ export class GatewayClient {
1112
1462
  return normalizeOpenClawConfigSchema(res) ?? { schema: {}, uiHints: {} };
1113
1463
  }
1114
1464
  async configPatch(patch) {
1115
- await this.rpc("config.patch", { patch });
1465
+ const { hash, baseHash } = await this.rpc("config.get");
1466
+ await this.rpc("config.patch", {
1467
+ raw: JSON.stringify(patch),
1468
+ baseHash: hash ?? baseHash ?? "",
1469
+ });
1470
+ }
1471
+ async configApply(config) {
1472
+ const { hash, baseHash } = await this.rpc("config.get");
1473
+ await this.rpc("config.apply", {
1474
+ raw: JSON.stringify(config),
1475
+ baseHash: hash ?? baseHash ?? "",
1476
+ });
1116
1477
  }
1117
1478
  async configSet(config) {
1118
- await this.rpc("config.set", { config });
1479
+ const { hash, baseHash } = await this.rpc("config.get");
1480
+ await this.rpc("config.set", {
1481
+ raw: JSON.stringify(config),
1482
+ baseHash: hash ?? baseHash ?? "",
1483
+ });
1119
1484
  }
1120
1485
  async modelsList() {
1121
1486
  const res = await this.rpc("models.list");
1122
1487
  return res?.models ?? res ?? [];
1123
1488
  }
1489
+ async waitReady(timeoutMs = 300_000, options = {}) {
1490
+ const retryIntervalMs = options.retryIntervalMs ?? 5_000;
1491
+ const probe = options.probe ?? "config";
1492
+ const deadline = Date.now() + timeoutMs;
1493
+ let lastError = null;
1494
+ while (!this.closed) {
1495
+ try {
1496
+ if (!this.connected) {
1497
+ await this.connect();
1498
+ }
1499
+ if (probe === "status") {
1500
+ return await this.status();
1501
+ }
1502
+ return await this.configGet();
1503
+ }
1504
+ catch (error) {
1505
+ lastError = error;
1506
+ this.close();
1507
+ if (Date.now() >= deadline) {
1508
+ break;
1509
+ }
1510
+ await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
1511
+ }
1512
+ }
1513
+ const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
1514
+ throw new Error(`Gateway readiness probe timed out after ${timeoutMs}ms${detail}`);
1515
+ }
1516
+ async channelsStatus(probe = false, timeoutMs) {
1517
+ const params = { probe };
1518
+ if (timeoutMs !== undefined)
1519
+ params.timeoutMs = timeoutMs;
1520
+ return await this.rpc("channels.status", params);
1521
+ }
1522
+ async channelsLogout(channel, accountId) {
1523
+ const params = { channel };
1524
+ if (accountId)
1525
+ params.accountId = accountId;
1526
+ return await this.rpc("channels.logout", params);
1527
+ }
1528
+ async webLoginStart(options = {}) {
1529
+ const params = {};
1530
+ if (options.force)
1531
+ params.force = true;
1532
+ if (options.timeoutMs !== undefined)
1533
+ params.timeoutMs = options.timeoutMs;
1534
+ if (options.verbose)
1535
+ params.verbose = true;
1536
+ if (options.accountId)
1537
+ params.accountId = options.accountId;
1538
+ return await this.rpc("web.login.start", params, 30_000);
1539
+ }
1540
+ async webLoginWait(options = {}) {
1541
+ const params = {};
1542
+ if (options.timeoutMs !== undefined)
1543
+ params.timeoutMs = options.timeoutMs;
1544
+ if (options.accountId)
1545
+ params.accountId = options.accountId;
1546
+ return await this.rpc("web.login.wait", params, CHAT_TIMEOUT);
1547
+ }
1124
1548
  // ---------------------------------------------------------------------------
1125
1549
  // Sessions
1126
1550
  // ---------------------------------------------------------------------------
@@ -1148,6 +1572,7 @@ export class GatewayClient {
1148
1572
  async sendChat(message, sessionKey = "main", agentId, attachments) {
1149
1573
  const params = {
1150
1574
  message,
1575
+ deliver: false,
1151
1576
  sessionKey,
1152
1577
  idempotencyKey: makeId(),
1153
1578
  };
@@ -1167,90 +1592,204 @@ export class GatewayClient {
1167
1592
  if (!this.connected || !this.ws) {
1168
1593
  throw new Error("Not connected");
1169
1594
  }
1170
- const id = makeId();
1171
- const req = {
1172
- type: "req",
1173
- id,
1174
- method: "chat.send",
1175
- params: {
1176
- message,
1177
- sessionKey,
1178
- idempotencyKey: makeId(),
1179
- },
1180
- };
1181
- const events = [];
1595
+ const idempotencyKey = makeId();
1596
+ const acceptedRunIds = new Set([idempotencyKey]);
1597
+ const queuedEvents = [];
1182
1598
  let resolveWait = null;
1183
- let done = false;
1184
- let error = null;
1185
- // Temporary event handler for chat events
1599
+ let streamedDisplayText = false;
1600
+ let lastLegacyText = "";
1601
+ let lastThinkingText = "";
1602
+ const seenToolCallIds = new Set();
1603
+ const seenToolResultIds = new Set();
1186
1604
  const handler = (evt) => {
1187
- if (evt.event?.startsWith("chat.")) {
1188
- const payload = evt.payload ?? {};
1605
+ if (evt.event === "chat" || evt.event?.startsWith("chat.")) {
1606
+ queuedEvents.push(evt);
1607
+ const waiter = resolveWait;
1608
+ resolveWait = null;
1609
+ waiter?.();
1610
+ }
1611
+ };
1612
+ this.eventHandlers.add(handler);
1613
+ try {
1614
+ const ack = await this.rpc("chat.send", {
1615
+ message,
1616
+ deliver: false,
1617
+ sessionKey,
1618
+ idempotencyKey,
1619
+ }, CHAT_TIMEOUT);
1620
+ const serverRunId = typeof ack?.runId === "string" ? ack.runId.trim() : "";
1621
+ if (serverRunId) {
1622
+ acceptedRunIds.add(serverRunId);
1623
+ }
1624
+ let deadline = Date.now() + CHAT_TIMEOUT;
1625
+ while (Date.now() < deadline) {
1626
+ if (queuedEvents.length === 0) {
1627
+ const remainingMs = Math.max(100, Math.min(1000, deadline - Date.now()));
1628
+ await new Promise((resolve) => {
1629
+ const timer = setTimeout(() => {
1630
+ if (resolveWait === release) {
1631
+ resolveWait = null;
1632
+ }
1633
+ resolve();
1634
+ }, remainingMs);
1635
+ const release = () => {
1636
+ clearTimeout(timer);
1637
+ resolve();
1638
+ };
1639
+ resolveWait = release;
1640
+ });
1641
+ continue;
1642
+ }
1643
+ const evt = queuedEvents.shift();
1644
+ const payload = asRecord(evt.payload) ?? {};
1645
+ const payloadRunId = typeof payload.runId === "string" ? payload.runId.trim() : "";
1646
+ const payloadSessionKey = typeof payload.sessionKey === "string" ? payload.sessionKey.trim() : "";
1647
+ if (payloadRunId && !acceptedRunIds.has(payloadRunId)) {
1648
+ continue;
1649
+ }
1650
+ if (payloadSessionKey && !sameSessionKey(payloadSessionKey, sessionKey)) {
1651
+ continue;
1652
+ }
1653
+ deadline = Date.now() + CHAT_TIMEOUT;
1189
1654
  if (evt.event === "chat.content") {
1190
- events.push({ type: "content", text: payload.text ?? "" });
1655
+ const text = typeof payload.text === "string" ? payload.text : "";
1656
+ if (text) {
1657
+ streamedDisplayText = true;
1658
+ yield { type: "content", text };
1659
+ }
1660
+ continue;
1191
1661
  }
1192
- else if (evt.event === "chat.thinking") {
1193
- events.push({ type: "thinking", text: payload.text ?? "" });
1662
+ if (evt.event === "chat.thinking") {
1663
+ const text = typeof payload.text === "string" ? payload.text : "";
1664
+ if (text) {
1665
+ lastThinkingText += text;
1666
+ }
1667
+ yield { type: "thinking", text };
1668
+ continue;
1194
1669
  }
1195
- else if (evt.event === "chat.tool_call") {
1196
- events.push({ type: "tool_call", data: payload });
1670
+ if (evt.event === "chat.tool_call") {
1671
+ const toolCallId = typeof payload.toolCallId === "string" && payload.toolCallId.trim()
1672
+ ? payload.toolCallId.trim()
1673
+ : `${typeof payload.name === "string" ? payload.name : "tool"}:${JSON.stringify(payload.args ?? payload.arguments ?? null)}`;
1674
+ seenToolCallIds.add(toolCallId);
1675
+ yield { type: "tool_call", data: payload };
1676
+ continue;
1197
1677
  }
1198
- else if (evt.event === "chat.tool_result") {
1199
- events.push({ type: "tool_result", data: payload });
1678
+ if (evt.event === "chat.tool_result") {
1679
+ const toolCallId = typeof payload.toolCallId === "string" && payload.toolCallId.trim()
1680
+ ? payload.toolCallId.trim()
1681
+ : `${typeof payload.name === "string" ? payload.name : "tool"}:${JSON.stringify(payload.args ?? payload.arguments ?? null)}`;
1682
+ seenToolResultIds.add(toolCallId);
1683
+ yield { type: "tool_result", data: payload };
1684
+ continue;
1200
1685
  }
1201
- else if (evt.event === "chat.done") {
1202
- events.push({ type: "done" });
1203
- done = true;
1686
+ if (evt.event === "chat.done") {
1687
+ yield { type: "done", data: payload };
1688
+ return;
1204
1689
  }
1205
- else if (evt.event === "chat.error") {
1206
- events.push({ type: "error", text: payload.message ?? "Unknown error" });
1207
- done = true;
1690
+ if (evt.event === "chat.error") {
1691
+ yield {
1692
+ type: "error",
1693
+ text: typeof payload.message === "string" ? payload.message : "Unknown error",
1694
+ data: payload,
1695
+ };
1696
+ return;
1208
1697
  }
1209
- resolveWait?.();
1210
- }
1211
- };
1212
- this.eventHandlers.add(handler);
1213
- // Also listen for the RPC response
1214
- const timer = setTimeout(() => {
1215
- this.pending.delete(id);
1216
- error = new Error("Chat timeout");
1217
- done = true;
1218
- resolveWait?.();
1219
- }, CHAT_TIMEOUT);
1220
- this.pending.set(id, {
1221
- resolve: () => {
1222
- clearTimeout(timer);
1223
- done = true;
1224
- resolveWait?.();
1225
- },
1226
- reject: (rejection) => {
1227
- clearTimeout(timer);
1228
- error = rejection instanceof Error ? rejection : new Error(String(rejection));
1229
- done = true;
1230
- resolveWait?.();
1231
- },
1232
- timer,
1233
- });
1234
- this.ws.send(JSON.stringify(req));
1235
- try {
1236
- while (!done || events.length > 0) {
1237
- if (events.length > 0) {
1238
- yield events.shift();
1698
+ if (evt.event !== "chat") {
1699
+ continue;
1239
1700
  }
1240
- else if (!done) {
1241
- await new Promise((resolve) => {
1242
- resolveWait = resolve;
1243
- });
1244
- resolveWait = null;
1701
+ const state = typeof payload.state === "string" ? payload.state.trim().toLowerCase() : "";
1702
+ const currentText = extractMessageText(payload.message) ?? "";
1703
+ const normalizedMessage = normalizeGatewayChatMessage(payload.message);
1704
+ if (state === "delta") {
1705
+ const streamed = streamDelta(lastLegacyText, currentText);
1706
+ lastLegacyText = streamed.nextText;
1707
+ if (streamed.delta) {
1708
+ streamedDisplayText = true;
1709
+ yield { type: "content", text: streamed.delta, data: payload };
1710
+ }
1711
+ continue;
1712
+ }
1713
+ if (state === "final") {
1714
+ const thinkingDelta = normalizedMessage?.thinking
1715
+ ? streamDelta(lastThinkingText, normalizedMessage.thinking)
1716
+ : { delta: "", nextText: lastThinkingText };
1717
+ lastThinkingText = thinkingDelta.nextText;
1718
+ if (thinkingDelta.delta) {
1719
+ yield { type: "thinking", text: thinkingDelta.delta, data: payload };
1720
+ }
1721
+ for (const toolCall of normalizedMessage?.toolCalls ?? []) {
1722
+ const toolCallKey = toolCall.id?.trim() ||
1723
+ `${toolCall.name}:${JSON.stringify(toolCall.args ?? null)}`;
1724
+ if (toolCall.args !== undefined && !seenToolCallIds.has(toolCallKey)) {
1725
+ seenToolCallIds.add(toolCallKey);
1726
+ yield {
1727
+ type: "tool_call",
1728
+ data: {
1729
+ ...(toolCall.id ? { toolCallId: toolCall.id } : {}),
1730
+ name: toolCall.name,
1731
+ args: toolCall.args,
1732
+ },
1733
+ };
1734
+ }
1735
+ if (toolCall.result !== undefined && !seenToolResultIds.has(toolCallKey)) {
1736
+ seenToolResultIds.add(toolCallKey);
1737
+ yield {
1738
+ type: "tool_result",
1739
+ data: {
1740
+ ...(toolCall.id ? { toolCallId: toolCall.id } : {}),
1741
+ name: toolCall.name,
1742
+ result: toolCall.result,
1743
+ },
1744
+ };
1745
+ }
1746
+ }
1747
+ if (currentText) {
1748
+ const streamed = streamDelta(lastLegacyText, currentText);
1749
+ lastLegacyText = streamed.nextText;
1750
+ if (streamed.delta) {
1751
+ streamedDisplayText = true;
1752
+ yield { type: "content", text: streamed.delta, data: payload };
1753
+ }
1754
+ yield { type: "done", data: payload };
1755
+ return;
1756
+ }
1757
+ if (streamedDisplayText || lastLegacyText) {
1758
+ yield { type: "done", data: payload };
1759
+ return;
1760
+ }
1761
+ if (normalizedMessage?.thinking || (normalizedMessage?.toolCalls.length ?? 0) > 0) {
1762
+ yield { type: "done", data: payload };
1763
+ return;
1764
+ }
1765
+ const historyText = latestHistoryAssistantText(await this.chatHistory(sessionKey, 20), acceptedRunIds);
1766
+ if (historyText) {
1767
+ yield { type: "content", text: historyText, data: payload };
1768
+ }
1769
+ yield { type: "done", data: payload };
1770
+ return;
1771
+ }
1772
+ if (state === "error" || state === "aborted") {
1773
+ if (currentText) {
1774
+ const streamed = streamDelta(lastLegacyText, currentText);
1775
+ lastLegacyText = streamed.nextText;
1776
+ if (streamed.delta) {
1777
+ streamedDisplayText = true;
1778
+ yield { type: "content", text: streamed.delta, data: payload };
1779
+ }
1780
+ }
1781
+ yield {
1782
+ type: "error",
1783
+ text: typeof payload.errorMessage === "string" ? payload.errorMessage : state,
1784
+ data: payload,
1785
+ };
1786
+ return;
1245
1787
  }
1246
1788
  }
1247
- if (error)
1248
- throw error;
1789
+ throw new Error("Streaming chat.send timed out");
1249
1790
  }
1250
1791
  finally {
1251
- clearTimeout(timer);
1252
1792
  this.eventHandlers.delete(handler);
1253
- this.pending.delete(id);
1254
1793
  }
1255
1794
  }
1256
1795
  // ---------------------------------------------------------------------------