@recursiv/sdk 0.7.50 → 0.7.51
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/README.md +46 -10
- package/dist/index.d.ts +11 -5
- package/dist/index.js +4 -0
- package/dist/realtime.d.ts +79 -9
- package/dist/realtime.js +339 -76
- package/dist/resources/agents.d.ts +28 -2
- package/dist/resources/agents.js +38 -23
- package/dist/resources/app-subscriptions.d.ts +54 -2
- package/dist/resources/app-subscriptions.js +25 -1
- package/dist/resources/auth.d.ts +198 -2
- package/dist/resources/auth.js +397 -3
- package/dist/resources/boosts.d.ts +148 -0
- package/dist/resources/boosts.js +70 -0
- package/dist/resources/chat.d.ts +33 -3
- package/dist/resources/chat.js +38 -2
- package/dist/resources/communities.d.ts +22 -10
- package/dist/resources/communities.js +19 -5
- package/dist/resources/deployments.d.ts +8 -3
- package/dist/resources/notifications.d.ts +74 -5
- package/dist/resources/notifications.js +38 -8
- package/dist/resources/posts.d.ts +4 -2
- package/dist/resources/posts.js +5 -1
- package/dist/resources/profiles.d.ts +45 -9
- package/dist/resources/profiles.js +54 -3
- package/dist/resources/protocols.d.ts +185 -10
- package/dist/resources/protocols.js +67 -8
- package/dist/resources/settings.d.ts +25 -3
- package/dist/resources/settings.js +28 -2
- package/dist/resources/video.d.ts +19 -0
- package/dist/resources/video.js +8 -0
- package/dist/resources/wallet.d.ts +41 -1
- package/dist/resources/wallet.js +8 -0
- package/dist/types.d.ts +201 -3
- package/llm.md +60 -16
- package/package.json +1 -1
package/dist/realtime.js
CHANGED
|
@@ -16,11 +16,62 @@ async function loadIo() {
|
|
|
16
16
|
}
|
|
17
17
|
return cachedIo;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Wider and more jittered than socket.io's own 1s/5s: when an API restart
|
|
21
|
+
* drops every client at once they must not all re-fetch `/realtime/ws-token`
|
|
22
|
+
* in the same second, which rate-limits it and keeps them all offline.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_BACKOFF = {
|
|
25
|
+
reconnectionDelay: 2_000,
|
|
26
|
+
reconnectionDelayMax: 30_000,
|
|
27
|
+
randomizationFactor: 0.5,
|
|
28
|
+
};
|
|
29
|
+
/** The token source refused the credential itself; retrying cannot succeed. */
|
|
30
|
+
function isCredentialRefusal(err) {
|
|
31
|
+
const status = err?.status;
|
|
32
|
+
return status === 401 || status === 403;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* WebSocket client for chat and presence.
|
|
36
|
+
*
|
|
37
|
+
* One socket at a time, and it survives the ways a socket dies:
|
|
38
|
+
* - a transport drop (network blip, sleep, backgrounding, API restart) is
|
|
39
|
+
* retried by socket.io with jittered backoff;
|
|
40
|
+
* - the server ending the connection because its token expired, or refusing
|
|
41
|
+
* a reconnect handshake (spent or expired token, rate limit), is retried
|
|
42
|
+
* here with the same backoff, since socket.io does not retry either;
|
|
43
|
+
* - every attempt authenticates with its own token (the server accepts each
|
|
44
|
+
* token once), and a failed token fetch sends none rather than a spent one;
|
|
45
|
+
* - listeners registered with `on()`/`onMessage()`… and rooms joined with
|
|
46
|
+
* `joinConversation()` are re-applied to every connection, including a
|
|
47
|
+
* socket rebuilt by a later `connect()`.
|
|
48
|
+
*
|
|
49
|
+
* Reconnecting stops only on `disconnect()` or when the token source refuses
|
|
50
|
+
* the credential (401/403).
|
|
51
|
+
*/
|
|
19
52
|
export class RealtimeClient {
|
|
20
53
|
socket = null;
|
|
21
54
|
client;
|
|
22
55
|
connectionPromise = null;
|
|
23
56
|
tokenProvider = null;
|
|
57
|
+
backoff = { ...DEFAULT_BACKOFF };
|
|
58
|
+
/** Every subscription, attached to each socket this client builds. */
|
|
59
|
+
listeners = new Map();
|
|
60
|
+
/** Conversation rooms to join on every connect. */
|
|
61
|
+
rooms = new Set();
|
|
62
|
+
/** connect() callers waiting on a socket that is reconnecting. */
|
|
63
|
+
waiters = [];
|
|
64
|
+
recoveryTimer = null;
|
|
65
|
+
recoveryAttempts = 0;
|
|
66
|
+
credentialRefused = false;
|
|
67
|
+
/** Bumped by disconnect(), so a connect() still in flight does not revive a socket. */
|
|
68
|
+
generation = 0;
|
|
69
|
+
/**
|
|
70
|
+
* The socket whose first handshake a connect() is awaiting. Closing a socket
|
|
71
|
+
* that never connected makes socket.io emit nothing, so discardSocket()
|
|
72
|
+
* settles that connect() through this.
|
|
73
|
+
*/
|
|
74
|
+
pendingOpen = null;
|
|
24
75
|
constructor(client) {
|
|
25
76
|
this.client = client;
|
|
26
77
|
}
|
|
@@ -32,107 +83,324 @@ export class RealtimeClient {
|
|
|
32
83
|
const response = await this.client.get('/realtime/ws-token');
|
|
33
84
|
return response.data?.token || undefined;
|
|
34
85
|
}
|
|
35
|
-
catch {
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (isCredentialRefusal(err))
|
|
88
|
+
this.credentialRefused = true;
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** A token for one reconnect attempt. Never throws: no token means the attempt is refused and retried. */
|
|
93
|
+
async mintToken() {
|
|
94
|
+
try {
|
|
95
|
+
const token = this.tokenProvider ? await this.tokenProvider() : await this.fetchTokenWithClientKey();
|
|
96
|
+
if (token)
|
|
97
|
+
this.credentialRefused = false;
|
|
98
|
+
return token || undefined;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (isCredentialRefusal(err))
|
|
102
|
+
this.credentialRefused = true;
|
|
36
103
|
return undefined;
|
|
37
104
|
}
|
|
38
105
|
}
|
|
39
106
|
/**
|
|
40
107
|
* Connects to the WebSocket server using a temporary WebSocket token.
|
|
108
|
+
* Resolves with the connected socket.
|
|
41
109
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* successful connect, socket.io's auto-reconnection owns recovery and
|
|
47
|
-
* transient errors never disable it;
|
|
48
|
-
* - auth uses socket.io's function form, so every (re)connection attempt
|
|
49
|
-
* fetches a fresh token via `options.getToken` or the client's key.
|
|
110
|
+
* Safe to call as often as you like: a connected socket is returned as is,
|
|
111
|
+
* a socket that is reconnecting is waited on (the promise rejects if that
|
|
112
|
+
* attempt fails, while the client keeps retrying), and only a socket that
|
|
113
|
+
* has stopped for good is replaced.
|
|
50
114
|
*/
|
|
51
115
|
async connect(options) {
|
|
52
116
|
if (options?.getToken) {
|
|
53
117
|
this.tokenProvider = options.getToken;
|
|
54
118
|
}
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (this.socket && !this.socket.connected && !this.socket.active) {
|
|
61
|
-
this.socket.disconnect();
|
|
62
|
-
this.socket = null;
|
|
63
|
-
this.connectionPromise = null;
|
|
119
|
+
if (options)
|
|
120
|
+
this.applyBackoff(options);
|
|
121
|
+
const current = this.socket;
|
|
122
|
+
if (current?.connected) {
|
|
123
|
+
return current;
|
|
64
124
|
}
|
|
65
125
|
if (this.connectionPromise) {
|
|
66
126
|
return this.connectionPromise;
|
|
67
127
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
provider()
|
|
82
|
-
.then((fresh) => cb({ token: fresh ?? token }))
|
|
83
|
-
.catch(() => cb({ token }));
|
|
84
|
-
},
|
|
85
|
-
autoConnect: true,
|
|
86
|
-
reconnection: true,
|
|
87
|
-
});
|
|
88
|
-
this.socket = socket;
|
|
89
|
-
return await new Promise((resolve, reject) => {
|
|
90
|
-
const onConnect = () => {
|
|
91
|
-
socket.off('connect', onConnect);
|
|
92
|
-
socket.off('connect_error', onInitialError);
|
|
93
|
-
resolve(socket);
|
|
94
|
-
};
|
|
95
|
-
const onInitialError = (err) => {
|
|
96
|
-
// Initial handshake failed — tear down and surface the error.
|
|
97
|
-
// This handler is removed on first successful connect, so later
|
|
98
|
-
// reconnect errors never disable the socket.
|
|
99
|
-
socket.off('connect', onConnect);
|
|
100
|
-
socket.off('connect_error', onInitialError);
|
|
101
|
-
socket.disconnect();
|
|
102
|
-
if (this.socket === socket) {
|
|
103
|
-
this.socket = null;
|
|
104
|
-
}
|
|
105
|
-
reject(err);
|
|
106
|
-
};
|
|
107
|
-
socket.on('connect', onConnect);
|
|
108
|
-
socket.on('connect_error', onInitialError);
|
|
109
|
-
});
|
|
110
|
-
})();
|
|
128
|
+
// socket.io is retrying the transport, or a retry of a refused handshake
|
|
129
|
+
// is scheduled. Building a second socket here would leave the first one
|
|
130
|
+
// reconnecting in the background with its own listeners: duplicate events
|
|
131
|
+
// and an extra server connection per call.
|
|
132
|
+
if (current && (current.active || this.recoveryTimer)) {
|
|
133
|
+
return this.waitForConnect();
|
|
134
|
+
}
|
|
135
|
+
// Neither connected nor reconnecting: socket.io has given up on it.
|
|
136
|
+
if (current) {
|
|
137
|
+
this.discardSocket(current, new Error('Realtime connection replaced'));
|
|
138
|
+
}
|
|
139
|
+
const opening = this.openSocket();
|
|
140
|
+
this.connectionPromise = opening;
|
|
111
141
|
try {
|
|
112
|
-
return await
|
|
142
|
+
return await opening;
|
|
113
143
|
}
|
|
114
144
|
finally {
|
|
115
145
|
// Clear after settling (success OR failure): the live socket is held in
|
|
116
|
-
// this.socket; a dead one must not be served from a stale promise.
|
|
117
|
-
|
|
146
|
+
// this.socket; a dead one must not be served from a stale promise. Only
|
|
147
|
+
// clear our own: after disconnect() a later connect() may already own
|
|
148
|
+
// the slot, and forgetting it would let the next call open a second
|
|
149
|
+
// socket beside it.
|
|
150
|
+
if (this.connectionPromise === opening)
|
|
151
|
+
this.connectionPromise = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
applyBackoff(options) {
|
|
155
|
+
const next = { ...this.backoff };
|
|
156
|
+
if (typeof options.reconnectionDelay === 'number' && options.reconnectionDelay >= 0) {
|
|
157
|
+
next.reconnectionDelay = options.reconnectionDelay;
|
|
158
|
+
}
|
|
159
|
+
if (typeof options.reconnectionDelayMax === 'number' && options.reconnectionDelayMax >= 0) {
|
|
160
|
+
next.reconnectionDelayMax = options.reconnectionDelayMax;
|
|
118
161
|
}
|
|
162
|
+
if (typeof options.randomizationFactor === 'number') {
|
|
163
|
+
next.randomizationFactor = Math.min(1, Math.max(0, options.randomizationFactor));
|
|
164
|
+
}
|
|
165
|
+
this.backoff = next;
|
|
119
166
|
}
|
|
167
|
+
async openSocket() {
|
|
168
|
+
const generation = this.generation;
|
|
169
|
+
const io = await loadIo();
|
|
170
|
+
const token = this.tokenProvider ? await this.tokenProvider() : await this.fetchTokenWithClientKey();
|
|
171
|
+
if (!token) {
|
|
172
|
+
throw new Error('Failed to fetch WebSocket token');
|
|
173
|
+
}
|
|
174
|
+
if (generation !== this.generation) {
|
|
175
|
+
throw new Error('Realtime disconnected before the connection opened');
|
|
176
|
+
}
|
|
177
|
+
this.credentialRefused = false;
|
|
178
|
+
// The server accepts each token once. The first handshake spends the one
|
|
179
|
+
// fetched above; every later attempt (socket.io's transport reconnects and
|
|
180
|
+
// the recoveries below) mints its own, and sends none when that fails
|
|
181
|
+
// rather than replaying a spent token.
|
|
182
|
+
let unspent = token;
|
|
183
|
+
const socket = io(this.client.baseOrigin, {
|
|
184
|
+
path: '/socket.io',
|
|
185
|
+
transports: ['websocket', 'polling'],
|
|
186
|
+
// Function-form auth: socket.io invokes this on EVERY connection attempt.
|
|
187
|
+
auth: (cb) => {
|
|
188
|
+
const first = unspent;
|
|
189
|
+
unspent = undefined;
|
|
190
|
+
if (first) {
|
|
191
|
+
cb({ token: first });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
void this.mintToken().then((fresh) => cb(fresh ? { token: fresh } : {}));
|
|
195
|
+
},
|
|
196
|
+
autoConnect: true,
|
|
197
|
+
reconnection: true,
|
|
198
|
+
reconnectionDelay: this.backoff.reconnectionDelay,
|
|
199
|
+
reconnectionDelayMax: this.backoff.reconnectionDelayMax,
|
|
200
|
+
randomizationFactor: this.backoff.randomizationFactor,
|
|
201
|
+
});
|
|
202
|
+
this.socket = socket;
|
|
203
|
+
this.watchLifecycle(socket);
|
|
204
|
+
for (const [event, listeners] of this.listeners) {
|
|
205
|
+
for (const listener of listeners)
|
|
206
|
+
socket.on(event, listener);
|
|
207
|
+
}
|
|
208
|
+
return await new Promise((resolve, reject) => {
|
|
209
|
+
const settle = () => {
|
|
210
|
+
socket.off('connect', onConnect);
|
|
211
|
+
socket.off('connect_error', onInitialError);
|
|
212
|
+
if (this.pendingOpen?.socket === socket)
|
|
213
|
+
this.pendingOpen = null;
|
|
214
|
+
};
|
|
215
|
+
const onConnect = () => {
|
|
216
|
+
settle();
|
|
217
|
+
resolve(socket);
|
|
218
|
+
};
|
|
219
|
+
const onInitialError = (err) => {
|
|
220
|
+
// Initial handshake failed — tear down and surface the error. This
|
|
221
|
+
// handler is removed on first successful connect, so later reconnect
|
|
222
|
+
// errors never disable the socket.
|
|
223
|
+
settle();
|
|
224
|
+
this.discardSocket(socket, err);
|
|
225
|
+
reject(err);
|
|
226
|
+
};
|
|
227
|
+
// disconnect() before the first handshake completes.
|
|
228
|
+
this.pendingOpen = {
|
|
229
|
+
socket,
|
|
230
|
+
fail: (err) => {
|
|
231
|
+
settle();
|
|
232
|
+
reject(err);
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
socket.on('connect', onConnect);
|
|
236
|
+
socket.on('connect_error', onInitialError);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/** Room rejoin, waiter settlement and recovery from the disconnects socket.io leaves alone. */
|
|
240
|
+
watchLifecycle(socket) {
|
|
241
|
+
let connectedOnce = false;
|
|
242
|
+
let tokenExpired = false;
|
|
243
|
+
socket.on('connect', () => {
|
|
244
|
+
if (this.socket !== socket)
|
|
245
|
+
return;
|
|
246
|
+
connectedOnce = true;
|
|
247
|
+
tokenExpired = false;
|
|
248
|
+
this.recoveryAttempts = 0;
|
|
249
|
+
// Room membership lives on the server-side connection, so a reconnect
|
|
250
|
+
// starts in no rooms. Rejoin every room the app is in.
|
|
251
|
+
for (const conversationId of this.rooms) {
|
|
252
|
+
socket.emit('join_conversation', conversationId);
|
|
253
|
+
}
|
|
254
|
+
const waiters = this.waiters;
|
|
255
|
+
this.waiters = [];
|
|
256
|
+
for (const waiter of waiters)
|
|
257
|
+
waiter.resolve(socket);
|
|
258
|
+
});
|
|
259
|
+
// The server announces an expired token just before it ends the connection.
|
|
260
|
+
socket.on('token_expired', () => {
|
|
261
|
+
tokenExpired = true;
|
|
262
|
+
});
|
|
263
|
+
socket.on('disconnect', (reason) => {
|
|
264
|
+
if (this.socket !== socket)
|
|
265
|
+
return;
|
|
266
|
+
// socket.io never reconnects a server-side disconnect. One that follows
|
|
267
|
+
// token_expired is routine (every token TTL, 10 minutes by default), so
|
|
268
|
+
// reconnect with a fresh token. Any other server disconnect, such as an
|
|
269
|
+
// eviction for too many open sockets, is left alone; the next connect()
|
|
270
|
+
// replaces the socket.
|
|
271
|
+
if (reason === 'io server disconnect' && tokenExpired) {
|
|
272
|
+
tokenExpired = false;
|
|
273
|
+
this.scheduleRecovery(socket);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
socket.on('connect_error', (err) => {
|
|
277
|
+
if (this.socket !== socket || !connectedOnce)
|
|
278
|
+
return;
|
|
279
|
+
const waiters = this.waiters;
|
|
280
|
+
this.waiters = [];
|
|
281
|
+
for (const waiter of waiters)
|
|
282
|
+
waiter.reject(err);
|
|
283
|
+
// `active` stays true while socket.io retries a transport failure. It
|
|
284
|
+
// turns false when the server refused the handshake (spent or expired
|
|
285
|
+
// token, rate limit), which socket.io never retries.
|
|
286
|
+
if (!socket.active) {
|
|
287
|
+
this.scheduleRecovery(socket);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
scheduleRecovery(socket) {
|
|
292
|
+
if (this.socket !== socket || this.recoveryTimer)
|
|
293
|
+
return;
|
|
294
|
+
if (this.credentialRefused) {
|
|
295
|
+
// The token source refused the credential. Stop here; an explicit
|
|
296
|
+
// connect() tries again from scratch.
|
|
297
|
+
this.discardSocket(socket, new Error('Realtime stopped: the WebSocket token request was refused (401/403)'));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const delay = this.recoveryDelay(this.recoveryAttempts++);
|
|
301
|
+
this.recoveryTimer = setTimeout(() => {
|
|
302
|
+
this.recoveryTimer = null;
|
|
303
|
+
if (this.socket !== socket || socket.connected || socket.active)
|
|
304
|
+
return;
|
|
305
|
+
socket.connect();
|
|
306
|
+
}, delay);
|
|
307
|
+
}
|
|
308
|
+
/** Exponential backoff with jitter, bounded by reconnectionDelayMax. */
|
|
309
|
+
recoveryDelay(attempt) {
|
|
310
|
+
const { reconnectionDelay, reconnectionDelayMax, randomizationFactor } = this.backoff;
|
|
311
|
+
const base = Math.min(reconnectionDelayMax, reconnectionDelay * 2 ** Math.min(attempt, 20));
|
|
312
|
+
const jitter = base * randomizationFactor * (Math.random() * 2 - 1);
|
|
313
|
+
return Math.round(Math.min(reconnectionDelayMax, Math.max(0, base + jitter)));
|
|
314
|
+
}
|
|
315
|
+
waitForConnect() {
|
|
316
|
+
return new Promise((resolve, reject) => {
|
|
317
|
+
this.waiters.push({ resolve, reject });
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
discardSocket(socket, reason) {
|
|
321
|
+
if (this.recoveryTimer) {
|
|
322
|
+
clearTimeout(this.recoveryTimer);
|
|
323
|
+
this.recoveryTimer = null;
|
|
324
|
+
}
|
|
325
|
+
if (this.pendingOpen?.socket === socket) {
|
|
326
|
+
this.pendingOpen.fail(reason);
|
|
327
|
+
}
|
|
328
|
+
socket.disconnect();
|
|
329
|
+
for (const [event, listeners] of this.listeners) {
|
|
330
|
+
for (const listener of listeners)
|
|
331
|
+
socket.off(event, listener);
|
|
332
|
+
}
|
|
333
|
+
if (this.socket === socket) {
|
|
334
|
+
this.socket = null;
|
|
335
|
+
}
|
|
336
|
+
const waiters = this.waiters;
|
|
337
|
+
this.waiters = [];
|
|
338
|
+
for (const waiter of waiters)
|
|
339
|
+
waiter.reject(reason);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Close the connection and stop reconnecting. Joined rooms are forgotten;
|
|
343
|
+
* listeners stay registered and attach to the next connection.
|
|
344
|
+
*/
|
|
120
345
|
disconnect() {
|
|
346
|
+
this.generation += 1;
|
|
347
|
+
this.rooms.clear();
|
|
348
|
+
this.recoveryAttempts = 0;
|
|
349
|
+
this.credentialRefused = false;
|
|
121
350
|
if (this.socket) {
|
|
122
|
-
this.socket
|
|
123
|
-
|
|
351
|
+
this.discardSocket(this.socket, new Error('Realtime disconnected'));
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const waiters = this.waiters;
|
|
355
|
+
this.waiters = [];
|
|
356
|
+
for (const waiter of waiters)
|
|
357
|
+
waiter.reject(new Error('Realtime disconnected'));
|
|
124
358
|
}
|
|
125
359
|
this.connectionPromise = null;
|
|
126
360
|
}
|
|
127
361
|
getSocket() {
|
|
128
362
|
return this.socket;
|
|
129
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Listen for any server event, including the lifecycle events `connect`
|
|
366
|
+
* (initial and every reconnect: refetch what you may have missed) and
|
|
367
|
+
* `disconnect`. The listener stays attached across reconnects and socket
|
|
368
|
+
* replacement. Returns an unsubscribe function.
|
|
369
|
+
*
|
|
370
|
+
* Server events include `notification`, `feed_update`,
|
|
371
|
+
* `conversation_created`, `chat_message`, `chat_typing` and `agent_thinking`.
|
|
372
|
+
*/
|
|
373
|
+
on(event, listener) {
|
|
374
|
+
// Every call is its own subscription, even for a function that is already
|
|
375
|
+
// subscribed: it fires once per subscription, as socket.io's own `on`
|
|
376
|
+
// does, and its unsubscribe removes only itself, so one consumer can never
|
|
377
|
+
// cancel another's.
|
|
378
|
+
const handler = (...args) => listener(...args);
|
|
379
|
+
let set = this.listeners.get(event);
|
|
380
|
+
if (!set) {
|
|
381
|
+
set = new Set();
|
|
382
|
+
this.listeners.set(event, set);
|
|
383
|
+
}
|
|
384
|
+
set.add(handler);
|
|
385
|
+
this.socket?.on(event, handler);
|
|
386
|
+
return () => {
|
|
387
|
+
const current = this.listeners.get(event);
|
|
388
|
+
if (!current?.delete(handler))
|
|
389
|
+
return;
|
|
390
|
+
if (current.size === 0)
|
|
391
|
+
this.listeners.delete(event);
|
|
392
|
+
this.socket?.off(event, handler);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/** Join a conversation room now, and again after every reconnect, until you leave it. */
|
|
130
396
|
joinConversation(conversationId) {
|
|
397
|
+
this.rooms.add(conversationId);
|
|
131
398
|
if (this.socket?.connected) {
|
|
132
399
|
this.socket.emit('join_conversation', conversationId);
|
|
133
400
|
}
|
|
134
401
|
}
|
|
135
402
|
leaveConversation(conversationId) {
|
|
403
|
+
this.rooms.delete(conversationId);
|
|
136
404
|
if (this.socket?.connected) {
|
|
137
405
|
this.socket.emit('leave_conversation', conversationId);
|
|
138
406
|
}
|
|
@@ -143,23 +411,18 @@ export class RealtimeClient {
|
|
|
143
411
|
}
|
|
144
412
|
}
|
|
145
413
|
onMessage(callback) {
|
|
146
|
-
this.
|
|
147
|
-
return () => this.socket?.off('chat_message', callback);
|
|
414
|
+
return this.on('chat_message', callback);
|
|
148
415
|
}
|
|
149
416
|
onMessageEdit(callback) {
|
|
150
|
-
this.
|
|
151
|
-
return () => this.socket?.off('chat_message_edit', callback);
|
|
417
|
+
return this.on('chat_message_edit', callback);
|
|
152
418
|
}
|
|
153
419
|
onMessageDelete(callback) {
|
|
154
|
-
this.
|
|
155
|
-
return () => this.socket?.off('chat_message_delete', callback);
|
|
420
|
+
return this.on('chat_message_delete', callback);
|
|
156
421
|
}
|
|
157
422
|
onTyping(callback) {
|
|
158
|
-
this.
|
|
159
|
-
return () => this.socket?.off('chat_typing', callback);
|
|
423
|
+
return this.on('chat_typing', callback);
|
|
160
424
|
}
|
|
161
425
|
onAgentThinking(callback) {
|
|
162
|
-
this.
|
|
163
|
-
return () => this.socket?.off('agent_thinking', callback);
|
|
426
|
+
return this.on('agent_thinking', callback);
|
|
164
427
|
}
|
|
165
428
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
-
import type { PaginatedResponse, SingleResponse, DeleteResponse, Agent, AgentDetail, CreateAgentInput, CreatedAgent, UpdateAgentInput, UpdatedAgent, AgentChatInput, AgentChatResult, AgentConversation, AgentConversationMessage, PaginationParams, StreamChunk } from '../types.js';
|
|
2
|
+
import type { PaginatedResponse, SingleResponse, DeleteResponse, Agent, AgentDetail, CreateAgentInput, CreatedAgent, UpdateAgentInput, UpdatedAgent, AgentChatInput, AgentChatRetryInput, AgentChatResult, AgentConversation, AgentConversationMessage, PaginationParams, StreamChunk } from '../types.js';
|
|
3
3
|
export interface AgentMessage {
|
|
4
4
|
id: string;
|
|
5
5
|
networkId: string;
|
|
@@ -137,7 +137,14 @@ export declare class AgentsResource {
|
|
|
137
137
|
* nothing landed, but never why.
|
|
138
138
|
*/
|
|
139
139
|
conversationMessages(id: string, conversationId: string, params?: PaginationParams): Promise<PaginatedResponse<AgentConversationMessage>>;
|
|
140
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* List the agents a member may discover: an app key gets the agents of its
|
|
142
|
+
* project and owning org, a personal key gets global and same-org agents.
|
|
143
|
+
* Only member-facing agents are returned. Agents that run a tenant (a
|
|
144
|
+
* project's Lead, swarm workers, dispatcher role agents, MCP client agents,
|
|
145
|
+
* the workspace AI, anyone's personal AI) never appear here; list your own
|
|
146
|
+
* with `list()`.
|
|
147
|
+
*/
|
|
141
148
|
listDiscoverable(params?: PaginationParams & {
|
|
142
149
|
organization_id?: string;
|
|
143
150
|
global?: boolean;
|
|
@@ -264,6 +271,16 @@ export declare class AgentsResource {
|
|
|
264
271
|
* For React Native, use `chatStreamText()` instead.
|
|
265
272
|
*/
|
|
266
273
|
chatStream(id: string, input: AgentChatInput): AsyncGenerator<StreamChunk>;
|
|
274
|
+
/**
|
|
275
|
+
* Retry a turn: stream a fresh reply to one of your existing messages
|
|
276
|
+
* without sending it again. Use it for "Try again" after a reply failed.
|
|
277
|
+
* The agent's earlier reply to that message, if any, is removed first.
|
|
278
|
+
*
|
|
279
|
+
* The message must be yours and the latest one in the conversation
|
|
280
|
+
* (409 `retry_not_latest` otherwise), and a reply still being written must
|
|
281
|
+
* finish or be stopped first (409 `run_in_progress`).
|
|
282
|
+
*/
|
|
283
|
+
retryChatStream(id: string, input: AgentChatRetryInput): AsyncGenerator<StreamChunk>;
|
|
267
284
|
/**
|
|
268
285
|
* Chat with an agent and get the full response text.
|
|
269
286
|
*
|
|
@@ -287,4 +304,13 @@ export declare class AgentsResource {
|
|
|
287
304
|
conversationId: string;
|
|
288
305
|
chunks: StreamChunk[];
|
|
289
306
|
}>;
|
|
307
|
+
/** `retryChatStream()` for platforms without `ReadableStream` (React Native). */
|
|
308
|
+
retryChatStreamText(id: string, input: AgentChatRetryInput): Promise<{
|
|
309
|
+
content: string;
|
|
310
|
+
conversationId: string;
|
|
311
|
+
chunks: StreamChunk[];
|
|
312
|
+
}>;
|
|
313
|
+
private openStream;
|
|
314
|
+
private streamChunks;
|
|
315
|
+
private streamText;
|
|
290
316
|
}
|
package/dist/resources/agents.js
CHANGED
|
@@ -92,7 +92,14 @@ export class AgentsResource {
|
|
|
92
92
|
conversationMessages(id, conversationId, params) {
|
|
93
93
|
return this.client.get(`/agents/${id}/conversations/${conversationId}/messages`, params);
|
|
94
94
|
}
|
|
95
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* List the agents a member may discover: an app key gets the agents of its
|
|
97
|
+
* project and owning org, a personal key gets global and same-org agents.
|
|
98
|
+
* Only member-facing agents are returned. Agents that run a tenant (a
|
|
99
|
+
* project's Lead, swarm workers, dispatcher role agents, MCP client agents,
|
|
100
|
+
* the workspace AI, anyone's personal AI) never appear here; list your own
|
|
101
|
+
* with `list()`.
|
|
102
|
+
*/
|
|
96
103
|
listDiscoverable(params) {
|
|
97
104
|
return this.client.get('/agents/discoverable', params);
|
|
98
105
|
}
|
|
@@ -181,26 +188,19 @@ export class AgentsResource {
|
|
|
181
188
|
* For React Native, use `chatStreamText()` instead.
|
|
182
189
|
*/
|
|
183
190
|
async *chatStream(id, input) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
198
|
-
code: 'stream_error',
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
throw new RecursivError(response.status, errorBody.error);
|
|
202
|
-
}
|
|
203
|
-
yield* parseSSE(response.body);
|
|
191
|
+
yield* this.streamChunks(id, input);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Retry a turn: stream a fresh reply to one of your existing messages
|
|
195
|
+
* without sending it again. Use it for "Try again" after a reply failed.
|
|
196
|
+
* The agent's earlier reply to that message, if any, is removed first.
|
|
197
|
+
*
|
|
198
|
+
* The message must be yours and the latest one in the conversation
|
|
199
|
+
* (409 `retry_not_latest` otherwise), and a reply still being written must
|
|
200
|
+
* finish or be stopped first (409 `run_in_progress`).
|
|
201
|
+
*/
|
|
202
|
+
async *retryChatStream(id, input) {
|
|
203
|
+
yield* this.streamChunks(id, input);
|
|
204
204
|
}
|
|
205
205
|
/**
|
|
206
206
|
* Chat with an agent and get the full response text.
|
|
@@ -220,12 +220,19 @@ export class AgentsResource {
|
|
|
220
220
|
* });
|
|
221
221
|
* ```
|
|
222
222
|
*/
|
|
223
|
-
|
|
223
|
+
chatStreamText(id, input) {
|
|
224
|
+
return this.streamText(id, input);
|
|
225
|
+
}
|
|
226
|
+
/** `retryChatStream()` for platforms without `ReadableStream` (React Native). */
|
|
227
|
+
retryChatStreamText(id, input) {
|
|
228
|
+
return this.streamText(id, input);
|
|
229
|
+
}
|
|
230
|
+
async openStream(id, input, accept) {
|
|
224
231
|
const response = await this.client.fetchRaw(`/agents/${id}/chat/stream`, {
|
|
225
232
|
method: 'POST',
|
|
226
233
|
headers: {
|
|
227
234
|
'Content-Type': 'application/json',
|
|
228
|
-
Accept:
|
|
235
|
+
...(accept ? { Accept: accept } : {}),
|
|
229
236
|
},
|
|
230
237
|
body: JSON.stringify(input),
|
|
231
238
|
});
|
|
@@ -243,6 +250,14 @@ export class AgentsResource {
|
|
|
243
250
|
}
|
|
244
251
|
throw new RecursivError(response.status, errorBody.error);
|
|
245
252
|
}
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
async *streamChunks(id, input) {
|
|
256
|
+
const response = await this.openStream(id, input);
|
|
257
|
+
yield* parseSSE(response.body);
|
|
258
|
+
}
|
|
259
|
+
async streamText(id, input) {
|
|
260
|
+
const response = await this.openStream(id, input, 'text/event-stream');
|
|
246
261
|
const text = await response.text();
|
|
247
262
|
const lines = text.split('\n');
|
|
248
263
|
let fullText = '';
|