pingerchips-js 2.1.1 → 3.0.0
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/chat.js +585 -0
- package/index.js +835 -43
- package/package.json +3 -2
- package/spaces.js +414 -0
package/chat.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { Pingerchips } from "./index.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// PingerchipsChat — entry point for the chat Session API
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Usage (client):
|
|
9
|
+
* const chat = new PingerchipsChat(appKey, { authEndpoint: "/auth/chat" });
|
|
10
|
+
* const session = await chat.connect(threadId, { clientId: "user-123" });
|
|
11
|
+
* const run = await session.view.send("Hello");
|
|
12
|
+
* run.toJSON(); // → { threadId, runId } — POST this to your agent endpoint
|
|
13
|
+
*
|
|
14
|
+
* Usage (agent — prefer AgentSession from pingerchips-js-server):
|
|
15
|
+
* const chat = new PingerchipsChat(appKey);
|
|
16
|
+
* const session = await chat.connectAgent(threadId, { secret, agentId });
|
|
17
|
+
*/
|
|
18
|
+
export class PingerchipsChat {
|
|
19
|
+
constructor(appKey, options = {}) {
|
|
20
|
+
if (!appKey) throw new Error("appKey required");
|
|
21
|
+
this.appKey = appKey;
|
|
22
|
+
this._options = options;
|
|
23
|
+
this._realtime =
|
|
24
|
+
options.realtime ||
|
|
25
|
+
new Pingerchips(appKey, {
|
|
26
|
+
endpoint: options.endpoint,
|
|
27
|
+
params: options.params,
|
|
28
|
+
serializer: options.serializer,
|
|
29
|
+
messageFormat: options.messageFormat,
|
|
30
|
+
reconnectAfterMs: options.reconnectAfterMs,
|
|
31
|
+
rejoinAfterMs: options.rejoinAfterMs,
|
|
32
|
+
});
|
|
33
|
+
if (options.autoConnect === false) this._realtime.disconnect();
|
|
34
|
+
this._sessions = {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Connect as a human client. Fetches signed auth token from authEndpoint.
|
|
39
|
+
* @param {string} threadId
|
|
40
|
+
* @param {{ clientId: string, afterLogId?: number }} options
|
|
41
|
+
* @returns {Promise<ChatSession>}
|
|
42
|
+
*/
|
|
43
|
+
async connect(threadId, { clientId, afterLogId } = {}) {
|
|
44
|
+
if (!clientId) throw new Error("clientId required");
|
|
45
|
+
|
|
46
|
+
const socketId = this._realtime.getSocketIdAsync
|
|
47
|
+
? await this._realtime.getSocketIdAsync()
|
|
48
|
+
: this._realtime.socketId;
|
|
49
|
+
if (!socketId) throw new Error("Socket ID not available");
|
|
50
|
+
|
|
51
|
+
const joinParams = { client_id: clientId };
|
|
52
|
+
if (afterLogId != null) joinParams.after_log_id = afterLogId;
|
|
53
|
+
|
|
54
|
+
if (this._options.authEndpoint || this._options.authCallback) {
|
|
55
|
+
joinParams.auth = await this._resolveAuthToken(threadId, clientId, socketId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return this._joinSession(threadId, joinParams);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Connect as an agent. Server-side only — never call from browser.
|
|
63
|
+
* @param {string} threadId
|
|
64
|
+
* @param {{ secret: string, agentId: string, afterLogId?: number }} options
|
|
65
|
+
* @returns {Promise<ChatSession>}
|
|
66
|
+
*/
|
|
67
|
+
async connectAgent(threadId, { secret, agentId, afterLogId } = {}) {
|
|
68
|
+
if (!secret) throw new Error("secret required");
|
|
69
|
+
if (!agentId) throw new Error("agentId required");
|
|
70
|
+
|
|
71
|
+
const joinParams = { secret, agent_id: agentId };
|
|
72
|
+
if (afterLogId != null) joinParams.after_log_id = afterLogId;
|
|
73
|
+
|
|
74
|
+
return this._joinSession(threadId, joinParams);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async _resolveAuthToken(threadId, clientId, socketId) {
|
|
78
|
+
const params = {
|
|
79
|
+
socket_id: socketId,
|
|
80
|
+
thread_id: threadId,
|
|
81
|
+
client_id: clientId,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (this._options.authCallback) {
|
|
85
|
+
const result = await invokeAuthCallback(this._options.authCallback, params);
|
|
86
|
+
return authToken(result);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const doFetch = this._options.fetch ?? globalThis.fetch;
|
|
90
|
+
const response = await doFetch(this._options.authEndpoint, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
...this._options.authHeaders,
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
...(this._options.authParams ?? {}),
|
|
98
|
+
...params,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const err = await response.json().catch(() => ({ error: "Auth failed" }));
|
|
104
|
+
throw new Error(err.error || "Auth failed");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return authToken(await response.json());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_joinSession(threadId, joinParams) {
|
|
111
|
+
const topic = `chat:v1:app:${this.appKey}:thread:${threadId}`;
|
|
112
|
+
const channel = this._realtime.socket.channel(topic, joinParams);
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
channel
|
|
116
|
+
.join()
|
|
117
|
+
.receive("ok", () => {
|
|
118
|
+
this._sessions[threadId]?.close();
|
|
119
|
+
const session = new ChatSession(channel, threadId, this.appKey, () => {
|
|
120
|
+
if (this._sessions[threadId] === session) delete this._sessions[threadId];
|
|
121
|
+
});
|
|
122
|
+
this._sessions[threadId] = session;
|
|
123
|
+
resolve(session);
|
|
124
|
+
})
|
|
125
|
+
.receive("error", (resp) =>
|
|
126
|
+
reject(
|
|
127
|
+
new Error(
|
|
128
|
+
`Failed to join chat thread ${threadId}: ${JSON.stringify(resp)}`,
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// ChatSession
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
export class ChatSession {
|
|
141
|
+
constructor(channel, threadId, appKey, onClose) {
|
|
142
|
+
this.threadId = threadId;
|
|
143
|
+
this.appKey = appKey;
|
|
144
|
+
this.view = new View(channel, threadId);
|
|
145
|
+
this._channel = channel;
|
|
146
|
+
this._onClose = onClose;
|
|
147
|
+
this._closed = false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
cancel(runId) {
|
|
151
|
+
return this.push("cancel", { runId });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
push(event, payload) {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
this._channel
|
|
157
|
+
.push(event, payload)
|
|
158
|
+
.receive("ok", resolve)
|
|
159
|
+
.receive("error", (e) =>
|
|
160
|
+
reject(new Error(e.reason || `${event} failed`)),
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
close() {
|
|
166
|
+
if (this._closed) return;
|
|
167
|
+
this._closed = true;
|
|
168
|
+
this.view.close();
|
|
169
|
+
this._channel.leave();
|
|
170
|
+
this._onClose?.();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
on(event, callback) {
|
|
174
|
+
this.view.on(event, callback);
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
off(event, callback) {
|
|
179
|
+
this.view.off(event, callback);
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// View
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
export class View {
|
|
189
|
+
constructor(channel, threadId) {
|
|
190
|
+
this._channel = channel;
|
|
191
|
+
this._threadId = threadId;
|
|
192
|
+
this._messages = [];
|
|
193
|
+
this._runs = {};
|
|
194
|
+
this._streams = {};
|
|
195
|
+
this._hasOlder = false;
|
|
196
|
+
this._logId = 0;
|
|
197
|
+
this._handlers = {};
|
|
198
|
+
this._siblingIndex = {};
|
|
199
|
+
|
|
200
|
+
this._channelRefs = [
|
|
201
|
+
["snapshot", channel.on("snapshot", (p) => this._onSnapshot(p))],
|
|
202
|
+
["change", channel.on("change", (p) => this._onChange(p))],
|
|
203
|
+
["batch", channel.on("batch", (p) => this._onBatch(p))],
|
|
204
|
+
["run:start", channel.on("run:start", (p) => this._onRunStart(p))],
|
|
205
|
+
["run:end", channel.on("run:end", (p) => this._onRunEnd(p))],
|
|
206
|
+
["run:suspend", channel.on("run:suspend", (p) => this._onRunSuspend(p))],
|
|
207
|
+
["run:resume", channel.on("run:resume", (p) => this._onRunResume(p))],
|
|
208
|
+
["tree:delta", channel.on("tree:delta", (p) => this._onTreeDelta(p))],
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Read ──────────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
getMessages() {
|
|
215
|
+
return this._buildBranch();
|
|
216
|
+
}
|
|
217
|
+
getChannelMessages() {
|
|
218
|
+
return [...this._messages];
|
|
219
|
+
}
|
|
220
|
+
get logId() {
|
|
221
|
+
return this._logId;
|
|
222
|
+
}
|
|
223
|
+
runs() {
|
|
224
|
+
return Object.values(this._runs);
|
|
225
|
+
}
|
|
226
|
+
hasOlder() {
|
|
227
|
+
return this._hasOlder;
|
|
228
|
+
}
|
|
229
|
+
getStream(runId) {
|
|
230
|
+
return this._streams[runId] ?? "";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Write ─────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
send(text, { parentId, messageId = crypto.randomUUID(), runId = crypto.randomUUID(), role = "user" } = {}) {
|
|
236
|
+
const payload = { messageId, runId, text, role };
|
|
237
|
+
if (parentId != null) payload.parentId = parentId;
|
|
238
|
+
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
this._channel
|
|
241
|
+
.push("send", payload)
|
|
242
|
+
.receive("ok", (ack) =>
|
|
243
|
+
resolve(
|
|
244
|
+
new ActiveRun(runId, this._threadId, this._channel, {
|
|
245
|
+
messageId,
|
|
246
|
+
serial: ack?.serial,
|
|
247
|
+
status: ack?.status,
|
|
248
|
+
}),
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
.receive("error", (e) => reject(new Error(e.reason || "send failed")));
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
regenerate(messageId, { runId = crypto.randomUUID(), parentId } = {}) {
|
|
256
|
+
const payload = { messageId, runId };
|
|
257
|
+
if (parentId != null) payload.parentId = parentId;
|
|
258
|
+
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
this._channel
|
|
261
|
+
.push("regenerate", payload)
|
|
262
|
+
.receive("ok", (ack) =>
|
|
263
|
+
resolve(
|
|
264
|
+
new ActiveRun(runId, this._threadId, this._channel, {
|
|
265
|
+
regeneratesMessageId: messageId,
|
|
266
|
+
serial: ack?.serial,
|
|
267
|
+
}),
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
.receive("error", (e) =>
|
|
271
|
+
reject(new Error(e.reason || "regenerate failed")),
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
edit(messageId, content, { newMessageId = crypto.randomUUID(), runId = crypto.randomUUID() } = {}) {
|
|
277
|
+
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
this._channel
|
|
280
|
+
.push("edit", { messageId, text: content, newMessageId, runId })
|
|
281
|
+
.receive("ok", (ack) =>
|
|
282
|
+
resolve(
|
|
283
|
+
new ActiveRun(runId, this._threadId, this._channel, {
|
|
284
|
+
messageId: newMessageId,
|
|
285
|
+
forkOf: messageId,
|
|
286
|
+
serial: ack?.serial,
|
|
287
|
+
status: ack?.status,
|
|
288
|
+
}),
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
.receive("error", (e) => reject(new Error(e.reason || "edit failed")));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
loadOlder(limit = 50) {
|
|
296
|
+
const oldest = this._messages[0];
|
|
297
|
+
if (!oldest || !this._hasOlder) return Promise.resolve([]);
|
|
298
|
+
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
this._channel
|
|
301
|
+
.push("load_older", { beforeMessageId: oldest.messageId, limit })
|
|
302
|
+
.receive("ok", ({ messages, has_older }) => {
|
|
303
|
+
this._messages = [...messages, ...this._messages];
|
|
304
|
+
this._hasOlder = has_older;
|
|
305
|
+
this._emit("update");
|
|
306
|
+
resolve(messages);
|
|
307
|
+
})
|
|
308
|
+
.receive("error", (e) =>
|
|
309
|
+
reject(new Error(e.reason || "load_older failed")),
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
selectSibling(parentId, messageId) {
|
|
315
|
+
this._siblingIndex[parentId] = messageId;
|
|
316
|
+
this._emit("update");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
approveTool(runId, toolCallId) {
|
|
320
|
+
return this._pushToolApproval(runId, toolCallId, true);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
rejectTool(runId, toolCallId) {
|
|
324
|
+
return this._pushToolApproval(runId, toolCallId, false);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
_pushToolApproval(runId, toolCallId, approved) {
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
this._channel
|
|
330
|
+
.push("tool_approval", { runId, toolCallId, approved })
|
|
331
|
+
.receive("ok", resolve)
|
|
332
|
+
.receive("error", (e) =>
|
|
333
|
+
reject(new Error(e.reason || "tool_approval failed")),
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
on(event, callback) {
|
|
339
|
+
(this._handlers[event] ??= []).push(callback);
|
|
340
|
+
return this;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
off(event, callback) {
|
|
344
|
+
if (!callback) {
|
|
345
|
+
delete this._handlers[event];
|
|
346
|
+
} else {
|
|
347
|
+
this._handlers[event] = (this._handlers[event] || []).filter(
|
|
348
|
+
(h) => h !== callback,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── Server event handlers ─────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
_onSnapshot({ state, log_id, has_older }) {
|
|
357
|
+
this._messages = state.messages ?? [];
|
|
358
|
+
this._runs = state.runs ?? {};
|
|
359
|
+
this._hasOlder = has_older ?? false;
|
|
360
|
+
this._logId = log_id ?? 0;
|
|
361
|
+
this._streams = {};
|
|
362
|
+
|
|
363
|
+
for (const [key, value] of Object.entries(state)) {
|
|
364
|
+
if (key.startsWith("stream:")) this._streams[key.slice(7)] = value;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this._emit("update");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_onChange({ key, value, log_id }) {
|
|
371
|
+
if (log_id != null) this._logId = log_id;
|
|
372
|
+
|
|
373
|
+
if (key === "messages") {
|
|
374
|
+
this._messages = value ?? [];
|
|
375
|
+
this._emit("update");
|
|
376
|
+
} else if (key.startsWith("stream:")) {
|
|
377
|
+
const runId = key.slice(7);
|
|
378
|
+
if (value === null) delete this._streams[runId];
|
|
379
|
+
else this._streams[runId] = value;
|
|
380
|
+
this._emit("update");
|
|
381
|
+
} else if (key.startsWith("run:")) {
|
|
382
|
+
const runId = key.slice(4);
|
|
383
|
+
if (value === null) delete this._runs[runId];
|
|
384
|
+
else this._runs[runId] = value;
|
|
385
|
+
this._emit("run", this._runs[runId] ?? { runId, status: null });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
_onBatch({ changes, log_id }) {
|
|
390
|
+
if (log_id != null) this._logId = log_id;
|
|
391
|
+
|
|
392
|
+
const changedRuns = [];
|
|
393
|
+
for (const { key, value } of changes) {
|
|
394
|
+
if (key === "messages") {
|
|
395
|
+
this._messages = value ?? [];
|
|
396
|
+
} else if (key.startsWith("stream:")) {
|
|
397
|
+
const runId = key.slice(7);
|
|
398
|
+
if (value === null) delete this._streams[runId];
|
|
399
|
+
else this._streams[runId] = value;
|
|
400
|
+
} else if (key.startsWith("run:")) {
|
|
401
|
+
const runId = key.slice(4);
|
|
402
|
+
if (value === null) delete this._runs[runId];
|
|
403
|
+
else this._runs[runId] = value;
|
|
404
|
+
changedRuns.push(this._runs[runId] ?? { runId, status: null });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const run of changedRuns) this._emit("run", run);
|
|
409
|
+
this._emit("update");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_onRunStart({ runId, ownerClientId, startedAt }) {
|
|
413
|
+
this._runs[runId] = {
|
|
414
|
+
runId,
|
|
415
|
+
ownerClientId,
|
|
416
|
+
status: "active",
|
|
417
|
+
startedAt,
|
|
418
|
+
endedAt: null,
|
|
419
|
+
suspendedTool: null,
|
|
420
|
+
};
|
|
421
|
+
this._emit("run", this._runs[runId]);
|
|
422
|
+
this._emit("run:start", this._runs[runId]);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
_onRunEnd({ runId, reason, endedAt }) {
|
|
426
|
+
if (this._runs[runId])
|
|
427
|
+
this._runs[runId] = { ...this._runs[runId], status: reason, endedAt };
|
|
428
|
+
delete this._streams[runId];
|
|
429
|
+
this._emit("run", this._runs[runId] ?? { runId, status: reason });
|
|
430
|
+
this._emit("run:end", { runId, reason, endedAt });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
_onRunSuspend({ runId, toolCallId, toolName, args }) {
|
|
434
|
+
if (this._runs[runId]) {
|
|
435
|
+
this._runs[runId] = {
|
|
436
|
+
...this._runs[runId],
|
|
437
|
+
status: "suspended",
|
|
438
|
+
suspendedTool: { toolCallId, toolName, args },
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
this._emit("run", this._runs[runId]);
|
|
442
|
+
this._emit("run:suspended", { runId, toolCallId, toolName, args });
|
|
443
|
+
this._emit("run:suspend", { runId, toolCallId, toolName, args });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
_onRunResume({ runId }) {
|
|
447
|
+
if (this._runs[runId]) {
|
|
448
|
+
this._runs[runId] = {
|
|
449
|
+
...this._runs[runId],
|
|
450
|
+
status: "active",
|
|
451
|
+
suspendedTool: null,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
this._emit("run", this._runs[runId]);
|
|
455
|
+
this._emit("run:resume", { runId });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
_onTreeDelta({ node }) {
|
|
459
|
+
if (!node || !node.id) return;
|
|
460
|
+
const idx = this._messages.findIndex((m) => m.messageId === node.id);
|
|
461
|
+
// Normalise the server node shape back to the wire message shape the
|
|
462
|
+
// branch resolver expects.
|
|
463
|
+
const msg = {
|
|
464
|
+
messageId: node.id,
|
|
465
|
+
parentId: node.parent_id ?? null,
|
|
466
|
+
forkOf: node.fork_of ?? null,
|
|
467
|
+
msgRegenerate: node.msg_regenerate ?? null,
|
|
468
|
+
runId: node.run_id ?? null,
|
|
469
|
+
ownerClientId: node.owner_client_id ?? null,
|
|
470
|
+
role: node.role,
|
|
471
|
+
status: node.status,
|
|
472
|
+
content: node.content,
|
|
473
|
+
createdAt: node.created_at,
|
|
474
|
+
};
|
|
475
|
+
if (idx === -1) this._messages = [...this._messages, msg];
|
|
476
|
+
else this._messages = this._messages.map((m, i) => (i === idx ? msg : m));
|
|
477
|
+
this._emit("update");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── Branch resolution ─────────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
_buildBranch() {
|
|
483
|
+
const byId = new Map(this._messages.map((m) => [m.messageId, m]));
|
|
484
|
+
const children = new Map();
|
|
485
|
+
|
|
486
|
+
for (const { messageId, parentId } of this._messages) {
|
|
487
|
+
const pid = parentId ?? "__root__";
|
|
488
|
+
(children.get(pid) ?? children.set(pid, []).get(pid)).push(messageId);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const branch = [];
|
|
492
|
+
let cursor = "__root__";
|
|
493
|
+
|
|
494
|
+
while (children.has(cursor)) {
|
|
495
|
+
const siblings = children.get(cursor);
|
|
496
|
+
const selected = this._siblingIndex[cursor];
|
|
497
|
+
const next =
|
|
498
|
+
selected && siblings.includes(selected)
|
|
499
|
+
? selected
|
|
500
|
+
: siblings[siblings.length - 1];
|
|
501
|
+
|
|
502
|
+
const msg = byId.get(next);
|
|
503
|
+
if (!msg) break;
|
|
504
|
+
branch.push(msg);
|
|
505
|
+
cursor = msg.messageId;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return branch;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
_emit(event, payload) {
|
|
512
|
+
for (const h of this._handlers[event] || []) h(payload);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
close() {
|
|
516
|
+
for (const [event, ref] of this._channelRefs) this._channel.off(event, ref);
|
|
517
|
+
this._channelRefs = [];
|
|
518
|
+
this._handlers = {};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function authToken(value) {
|
|
523
|
+
const token = typeof value === "string" ? value : value?.auth;
|
|
524
|
+
if (!token || typeof token !== "string") {
|
|
525
|
+
throw new Error("Chat auth must return a JWT string or { auth: jwt }");
|
|
526
|
+
}
|
|
527
|
+
return token;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function invokeAuthCallback(callback, params) {
|
|
531
|
+
return new Promise((resolve, reject) => {
|
|
532
|
+
let settled = false;
|
|
533
|
+
const done = (error, value) => {
|
|
534
|
+
if (settled) return;
|
|
535
|
+
settled = true;
|
|
536
|
+
error ? reject(error) : resolve(value);
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const result = callback(params, done);
|
|
541
|
+
if (result?.then) result.then((value) => done(null, value), done);
|
|
542
|
+
else if (result !== undefined) done(null, result);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
done(error);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
// ActiveRun
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
export class ActiveRun {
|
|
554
|
+
constructor(runId, threadId, channel, meta = {}) {
|
|
555
|
+
this.runId = runId;
|
|
556
|
+
this._threadId = threadId;
|
|
557
|
+
this._channel = channel;
|
|
558
|
+
// Publish-ack metadata (ADR-0011): the codec-message-id the client owns
|
|
559
|
+
// for this send/edit, the fork/regenerate anchor, and the channel serial.
|
|
560
|
+
this.messageId = meta.messageId ?? null;
|
|
561
|
+
this.forkOf = meta.forkOf ?? null;
|
|
562
|
+
this.regeneratesMessageId = meta.regeneratesMessageId ?? null;
|
|
563
|
+
this.serial = meta.serial ?? null;
|
|
564
|
+
this.status = meta.status ?? "ok";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
cancel() {
|
|
568
|
+
return new Promise((resolve, reject) => {
|
|
569
|
+
this._channel
|
|
570
|
+
.push("cancel", { runId: this.runId })
|
|
571
|
+
.receive("ok", resolve)
|
|
572
|
+
.receive("error", (e) =>
|
|
573
|
+
reject(new Error(e.reason || "cancel failed")),
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
toJSON() {
|
|
579
|
+
return { threadId: this._threadId, runId: this.runId };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
toInvocation() {
|
|
583
|
+
return { toJSON: () => this.toJSON() };
|
|
584
|
+
}
|
|
585
|
+
}
|