@rool-dev/sdk 0.3.1 → 0.3.2-dev.1f10ef1
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 +119 -163
- package/dist/apps.d.ts +1 -1
- package/dist/apps.d.ts.map +1 -1
- package/dist/apps.js +1 -5
- package/dist/apps.js.map +1 -1
- package/dist/channel.d.ts +136 -4
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +373 -32
- package/dist/channel.js.map +1 -1
- package/dist/client.d.ts +18 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +26 -7
- package/dist/client.js.map +1 -1
- package/dist/graphql.d.ts +18 -11
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +91 -31
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/space.d.ts +0 -1
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js.map +1 -1
- package/dist/subscription.d.ts +6 -0
- package/dist/subscription.d.ts.map +1 -1
- package/dist/subscription.js +66 -6
- package/dist/subscription.js.map +1 -1
- package/dist/types.d.ts +144 -25
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/channel.js
CHANGED
|
@@ -36,6 +36,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
36
36
|
_linkAccess;
|
|
37
37
|
_userId;
|
|
38
38
|
_channelId;
|
|
39
|
+
_conversationId;
|
|
39
40
|
_closed = false;
|
|
40
41
|
graphqlClient;
|
|
41
42
|
mediaClient;
|
|
@@ -49,6 +50,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
49
50
|
_channel;
|
|
50
51
|
_objectIds;
|
|
51
52
|
_objectStats;
|
|
53
|
+
_hasConnected = false;
|
|
52
54
|
// Object collection: tracks pending local mutations for dedup
|
|
53
55
|
// Maps objectId → optimistic object data (for create/update) or null (for delete)
|
|
54
56
|
_pendingMutations = new Map();
|
|
@@ -65,6 +67,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
65
67
|
this._userId = config.userId;
|
|
66
68
|
this._emitterLogger = config.logger;
|
|
67
69
|
this._channelId = config.channelId;
|
|
70
|
+
this._conversationId = 'default';
|
|
68
71
|
this.graphqlClient = config.graphqlClient;
|
|
69
72
|
this.mediaClient = config.mediaClient;
|
|
70
73
|
this.logger = config.logger;
|
|
@@ -133,17 +136,83 @@ export class RoolChannel extends EventEmitter {
|
|
|
133
136
|
get channelId() {
|
|
134
137
|
return this._channelId;
|
|
135
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Get the conversation ID for this channel.
|
|
141
|
+
* Defaults to 'default' for most apps.
|
|
142
|
+
*/
|
|
143
|
+
get conversationId() {
|
|
144
|
+
return this._conversationId;
|
|
145
|
+
}
|
|
136
146
|
get isReadOnly() {
|
|
137
147
|
return this._role === 'viewer';
|
|
138
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Get the app URL if this channel was created via installApp, or null.
|
|
151
|
+
*/
|
|
152
|
+
get appUrl() {
|
|
153
|
+
return this._channel?.appUrl ?? null;
|
|
154
|
+
}
|
|
139
155
|
// ===========================================================================
|
|
140
156
|
// Channel History Access
|
|
141
157
|
// ===========================================================================
|
|
142
158
|
/**
|
|
143
|
-
* Get interactions for
|
|
159
|
+
* Get interactions for the current conversation.
|
|
144
160
|
*/
|
|
145
161
|
getInteractions() {
|
|
146
|
-
return this.
|
|
162
|
+
return this._getInteractionsImpl(this._conversationId);
|
|
163
|
+
}
|
|
164
|
+
/** @internal */
|
|
165
|
+
_getInteractionsImpl(conversationId) {
|
|
166
|
+
return this._channel?.conversations[conversationId]?.interactions ?? [];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get all conversations in this channel.
|
|
170
|
+
* Returns summary info (no full interaction data) for each conversation.
|
|
171
|
+
*/
|
|
172
|
+
getConversations() {
|
|
173
|
+
if (!this._channel)
|
|
174
|
+
return [];
|
|
175
|
+
return Object.entries(this._channel.conversations).map(([id, conv]) => ({
|
|
176
|
+
id,
|
|
177
|
+
name: conv.name ?? null,
|
|
178
|
+
systemInstruction: conv.systemInstruction ?? null,
|
|
179
|
+
createdAt: conv.createdAt,
|
|
180
|
+
createdBy: conv.createdBy,
|
|
181
|
+
interactionCount: conv.interactions.length,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Delete a conversation from this channel.
|
|
186
|
+
* Cannot delete the conversation you are currently using.
|
|
187
|
+
*/
|
|
188
|
+
async deleteConversation(conversationId) {
|
|
189
|
+
if (conversationId === this._conversationId) {
|
|
190
|
+
throw new Error('Cannot delete the active conversation');
|
|
191
|
+
}
|
|
192
|
+
await this.graphqlClient.deleteConversation(this._id, this._channelId, conversationId);
|
|
193
|
+
// Optimistic local update — remove from cache and emit event
|
|
194
|
+
// in case the server doesn't send a conversation_updated event for deletes
|
|
195
|
+
if (this._channel?.conversations[conversationId]) {
|
|
196
|
+
delete this._channel.conversations[conversationId];
|
|
197
|
+
this.emit('conversationUpdated', {
|
|
198
|
+
conversationId,
|
|
199
|
+
channelId: this._channelId,
|
|
200
|
+
source: 'local_user',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// ===========================================================================
|
|
205
|
+
// Conversations
|
|
206
|
+
// ===========================================================================
|
|
207
|
+
/**
|
|
208
|
+
* Get a handle for a specific conversation within this channel.
|
|
209
|
+
* The handle scopes AI and mutation operations to that conversation's
|
|
210
|
+
* interaction history, while sharing the channel's single SSE connection.
|
|
211
|
+
*
|
|
212
|
+
* Conversations are auto-created on first interaction — no explicit create needed.
|
|
213
|
+
*/
|
|
214
|
+
conversation(conversationId) {
|
|
215
|
+
return new ConversationHandle(this, conversationId);
|
|
147
216
|
}
|
|
148
217
|
// ===========================================================================
|
|
149
218
|
// Channel Lifecycle
|
|
@@ -246,7 +315,11 @@ export class RoolChannel extends EventEmitter {
|
|
|
246
315
|
* @returns The matching objects and a descriptive message.
|
|
247
316
|
*/
|
|
248
317
|
async findObjects(options) {
|
|
249
|
-
return this.
|
|
318
|
+
return this._findObjectsImpl(options, this._conversationId);
|
|
319
|
+
}
|
|
320
|
+
/** @internal */
|
|
321
|
+
_findObjectsImpl(options, conversationId) {
|
|
322
|
+
return this.graphqlClient.findObjects(this._id, options, this._channelId, conversationId);
|
|
250
323
|
}
|
|
251
324
|
/**
|
|
252
325
|
* Get all object IDs (sync, from local cache).
|
|
@@ -271,6 +344,10 @@ export class RoolChannel extends EventEmitter {
|
|
|
271
344
|
* @returns The created object (with AI-filled content) and message
|
|
272
345
|
*/
|
|
273
346
|
async createObject(options) {
|
|
347
|
+
return this._createObjectImpl(options, this._conversationId);
|
|
348
|
+
}
|
|
349
|
+
/** @internal */
|
|
350
|
+
async _createObjectImpl(options, conversationId) {
|
|
274
351
|
const { data, ephemeral } = options;
|
|
275
352
|
// Use data.id if provided (string), otherwise generate
|
|
276
353
|
const objectId = typeof data.id === 'string' ? data.id : generateEntityId();
|
|
@@ -285,7 +362,8 @@ export class RoolChannel extends EventEmitter {
|
|
|
285
362
|
try {
|
|
286
363
|
// Await mutation — server processes AI placeholders before responding.
|
|
287
364
|
// SSE events arrive during the await and are buffered via _deliverObject.
|
|
288
|
-
const
|
|
365
|
+
const interactionId = generateEntityId();
|
|
366
|
+
const { message } = await this.graphqlClient.createObject(this.id, dataWithId, this._channelId, conversationId, interactionId, ephemeral);
|
|
289
367
|
// Collect resolved object from buffer (or wait if not yet arrived)
|
|
290
368
|
const object = await this._collectObject(objectId);
|
|
291
369
|
return { object, message };
|
|
@@ -309,6 +387,10 @@ export class RoolChannel extends EventEmitter {
|
|
|
309
387
|
* @returns The updated object (with AI-filled content) and message
|
|
310
388
|
*/
|
|
311
389
|
async updateObject(objectId, options) {
|
|
390
|
+
return this._updateObjectImpl(objectId, options, this._conversationId);
|
|
391
|
+
}
|
|
392
|
+
/** @internal */
|
|
393
|
+
async _updateObjectImpl(objectId, options, conversationId) {
|
|
312
394
|
const { data, ephemeral } = options;
|
|
313
395
|
// id is immutable after creation (but null/undefined means delete attempt, which we also reject)
|
|
314
396
|
if (data?.id !== undefined && data.id !== null) {
|
|
@@ -334,7 +416,8 @@ export class RoolChannel extends EventEmitter {
|
|
|
334
416
|
this.emit('objectUpdated', { objectId, object: optimistic, source: 'local_user' });
|
|
335
417
|
}
|
|
336
418
|
try {
|
|
337
|
-
const
|
|
419
|
+
const interactionId = generateEntityId();
|
|
420
|
+
const { message } = await this.graphqlClient.updateObject(this.id, objectId, this._channelId, conversationId, interactionId, serverData, options.prompt, ephemeral);
|
|
338
421
|
const object = await this._collectObject(objectId);
|
|
339
422
|
return { object, message };
|
|
340
423
|
}
|
|
@@ -352,6 +435,10 @@ export class RoolChannel extends EventEmitter {
|
|
|
352
435
|
* Other objects that reference deleted objects via data fields will retain stale ref values.
|
|
353
436
|
*/
|
|
354
437
|
async deleteObjects(objectIds) {
|
|
438
|
+
return this._deleteObjectsImpl(objectIds, this._conversationId);
|
|
439
|
+
}
|
|
440
|
+
/** @internal */
|
|
441
|
+
async _deleteObjectsImpl(objectIds, conversationId) {
|
|
355
442
|
if (objectIds.length === 0)
|
|
356
443
|
return;
|
|
357
444
|
// Track for dedup and emit optimistic events
|
|
@@ -360,7 +447,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
360
447
|
this.emit('objectDeleted', { objectId, source: 'local_user' });
|
|
361
448
|
}
|
|
362
449
|
try {
|
|
363
|
-
await this.graphqlClient.deleteObjects(this.id, objectIds, this._channelId);
|
|
450
|
+
await this.graphqlClient.deleteObjects(this.id, objectIds, this._channelId, conversationId);
|
|
364
451
|
}
|
|
365
452
|
catch (error) {
|
|
366
453
|
this.logger.error('[RoolChannel] Failed to delete objects:', error);
|
|
@@ -389,6 +476,10 @@ export class RoolChannel extends EventEmitter {
|
|
|
389
476
|
* @returns The created CollectionDef
|
|
390
477
|
*/
|
|
391
478
|
async createCollection(name, fields) {
|
|
479
|
+
return this._createCollectionImpl(name, fields, this._conversationId);
|
|
480
|
+
}
|
|
481
|
+
/** @internal */
|
|
482
|
+
async _createCollectionImpl(name, fields, conversationId) {
|
|
392
483
|
if (this._schema[name]) {
|
|
393
484
|
throw new Error(`Collection "${name}" already exists`);
|
|
394
485
|
}
|
|
@@ -396,7 +487,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
396
487
|
const optimisticDef = { fields: fields.map(f => ({ name: f.name, type: f.type })) };
|
|
397
488
|
this._schema[name] = optimisticDef;
|
|
398
489
|
try {
|
|
399
|
-
return await this.graphqlClient.createCollection(this._id, name, fields, this._channelId);
|
|
490
|
+
return await this.graphqlClient.createCollection(this._id, name, fields, this._channelId, conversationId);
|
|
400
491
|
}
|
|
401
492
|
catch (error) {
|
|
402
493
|
this.logger.error('[RoolChannel] Failed to create collection:', error);
|
|
@@ -411,6 +502,10 @@ export class RoolChannel extends EventEmitter {
|
|
|
411
502
|
* @returns The updated CollectionDef
|
|
412
503
|
*/
|
|
413
504
|
async alterCollection(name, fields) {
|
|
505
|
+
return this._alterCollectionImpl(name, fields, this._conversationId);
|
|
506
|
+
}
|
|
507
|
+
/** @internal */
|
|
508
|
+
async _alterCollectionImpl(name, fields, conversationId) {
|
|
414
509
|
if (!this._schema[name]) {
|
|
415
510
|
throw new Error(`Collection "${name}" not found`);
|
|
416
511
|
}
|
|
@@ -418,7 +513,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
418
513
|
// Optimistic local update
|
|
419
514
|
this._schema[name] = { fields: fields.map(f => ({ name: f.name, type: f.type })) };
|
|
420
515
|
try {
|
|
421
|
-
return await this.graphqlClient.alterCollection(this._id, name, fields, this._channelId);
|
|
516
|
+
return await this.graphqlClient.alterCollection(this._id, name, fields, this._channelId, conversationId);
|
|
422
517
|
}
|
|
423
518
|
catch (error) {
|
|
424
519
|
this.logger.error('[RoolChannel] Failed to alter collection:', error);
|
|
@@ -431,6 +526,10 @@ export class RoolChannel extends EventEmitter {
|
|
|
431
526
|
* @param name - Name of the collection to drop
|
|
432
527
|
*/
|
|
433
528
|
async dropCollection(name) {
|
|
529
|
+
return this._dropCollectionImpl(name, this._conversationId);
|
|
530
|
+
}
|
|
531
|
+
/** @internal */
|
|
532
|
+
async _dropCollectionImpl(name, conversationId) {
|
|
434
533
|
if (!this._schema[name]) {
|
|
435
534
|
throw new Error(`Collection "${name}" not found`);
|
|
436
535
|
}
|
|
@@ -438,7 +537,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
438
537
|
// Optimistic local update
|
|
439
538
|
delete this._schema[name];
|
|
440
539
|
try {
|
|
441
|
-
await this.graphqlClient.dropCollection(this._id, name, this._channelId);
|
|
540
|
+
await this.graphqlClient.dropCollection(this._id, name, this._channelId, conversationId);
|
|
442
541
|
}
|
|
443
542
|
catch (error) {
|
|
444
543
|
this.logger.error('[RoolChannel] Failed to drop collection:', error);
|
|
@@ -450,48 +549,118 @@ export class RoolChannel extends EventEmitter {
|
|
|
450
549
|
// System Instructions
|
|
451
550
|
// ===========================================================================
|
|
452
551
|
/**
|
|
453
|
-
* Get the system instruction for
|
|
552
|
+
* Get the system instruction for the current conversation.
|
|
454
553
|
* Returns undefined if no system instruction is set.
|
|
455
554
|
*/
|
|
456
555
|
getSystemInstruction() {
|
|
457
|
-
return this.
|
|
556
|
+
return this._getSystemInstructionImpl(this._conversationId);
|
|
557
|
+
}
|
|
558
|
+
/** @internal */
|
|
559
|
+
_getSystemInstructionImpl(conversationId) {
|
|
560
|
+
return this._channel?.conversations[conversationId]?.systemInstruction;
|
|
458
561
|
}
|
|
459
562
|
/**
|
|
460
|
-
* Set the system instruction for
|
|
563
|
+
* Set the system instruction for the current conversation.
|
|
461
564
|
* Pass null to clear the instruction.
|
|
462
565
|
*/
|
|
463
566
|
async setSystemInstruction(instruction) {
|
|
567
|
+
return this._setSystemInstructionImpl(instruction, this._conversationId);
|
|
568
|
+
}
|
|
569
|
+
/** @internal */
|
|
570
|
+
async _setSystemInstructionImpl(instruction, conversationId) {
|
|
464
571
|
// Optimistic local update
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
createdBy: this._userId,
|
|
469
|
-
interactions: [],
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
const previous = this._channel;
|
|
572
|
+
this._ensureConversationImpl(conversationId);
|
|
573
|
+
const conv = this._channel.conversations[conversationId];
|
|
574
|
+
const previousInstruction = conv.systemInstruction;
|
|
473
575
|
if (instruction === null) {
|
|
474
|
-
|
|
475
|
-
this._channel = rest;
|
|
576
|
+
delete conv.systemInstruction;
|
|
476
577
|
}
|
|
477
578
|
else {
|
|
478
|
-
|
|
579
|
+
conv.systemInstruction = instruction;
|
|
479
580
|
}
|
|
480
|
-
// Emit
|
|
481
|
-
this.emit('
|
|
581
|
+
// Emit events for backward compat and new API
|
|
582
|
+
this.emit('conversationUpdated', {
|
|
583
|
+
conversationId,
|
|
482
584
|
channelId: this._channelId,
|
|
483
585
|
source: 'local_user',
|
|
484
586
|
});
|
|
587
|
+
if (conversationId === this._conversationId) {
|
|
588
|
+
this.emit('channelUpdated', {
|
|
589
|
+
channelId: this._channelId,
|
|
590
|
+
source: 'local_user',
|
|
591
|
+
});
|
|
592
|
+
}
|
|
485
593
|
// Call server
|
|
486
594
|
try {
|
|
487
|
-
await this.graphqlClient.
|
|
595
|
+
await this.graphqlClient.updateConversation(this._id, this._channelId, conversationId, { systemInstruction: instruction });
|
|
488
596
|
}
|
|
489
597
|
catch (error) {
|
|
490
598
|
this.logger.error('[RoolChannel] Failed to set system instruction:', error);
|
|
491
|
-
|
|
599
|
+
// Rollback
|
|
600
|
+
if (previousInstruction === undefined) {
|
|
601
|
+
delete conv.systemInstruction;
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
conv.systemInstruction = previousInstruction;
|
|
605
|
+
}
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Rename the current conversation.
|
|
611
|
+
*/
|
|
612
|
+
async renameConversation(name) {
|
|
613
|
+
return this._renameConversationImpl(name, this._conversationId);
|
|
614
|
+
}
|
|
615
|
+
/** @internal */
|
|
616
|
+
async _renameConversationImpl(name, conversationId) {
|
|
617
|
+
// Optimistic local update
|
|
618
|
+
this._ensureConversationImpl(conversationId);
|
|
619
|
+
const conv = this._channel.conversations[conversationId];
|
|
620
|
+
const previousName = conv.name;
|
|
621
|
+
conv.name = name;
|
|
622
|
+
this.emit('conversationUpdated', {
|
|
623
|
+
conversationId,
|
|
624
|
+
channelId: this._channelId,
|
|
625
|
+
source: 'local_user',
|
|
626
|
+
});
|
|
627
|
+
if (conversationId === this._conversationId) {
|
|
628
|
+
this.emit('channelUpdated', {
|
|
629
|
+
channelId: this._channelId,
|
|
630
|
+
source: 'local_user',
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
// Call server
|
|
634
|
+
try {
|
|
635
|
+
await this.graphqlClient.updateConversation(this._id, this._channelId, conversationId, { name });
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
this.logger.error('[RoolChannel] Failed to rename conversation:', error);
|
|
639
|
+
// Rollback
|
|
640
|
+
conv.name = previousName;
|
|
492
641
|
throw error;
|
|
493
642
|
}
|
|
494
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Ensure a conversation exists in the local channel cache.
|
|
646
|
+
* @internal
|
|
647
|
+
*/
|
|
648
|
+
_ensureConversationImpl(conversationId) {
|
|
649
|
+
if (!this._channel) {
|
|
650
|
+
this._channel = {
|
|
651
|
+
createdAt: Date.now(),
|
|
652
|
+
createdBy: this._userId,
|
|
653
|
+
conversations: {},
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
if (!this._channel.conversations[conversationId]) {
|
|
657
|
+
this._channel.conversations[conversationId] = {
|
|
658
|
+
createdAt: Date.now(),
|
|
659
|
+
createdBy: this._userId,
|
|
660
|
+
interactions: [],
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|
|
495
664
|
// ===========================================================================
|
|
496
665
|
// Metadata Operations
|
|
497
666
|
// ===========================================================================
|
|
@@ -500,10 +669,14 @@ export class RoolChannel extends EventEmitter {
|
|
|
500
669
|
* Metadata is stored in meta and hidden from AI operations.
|
|
501
670
|
*/
|
|
502
671
|
setMetadata(key, value) {
|
|
672
|
+
this._setMetadataImpl(key, value, this._conversationId);
|
|
673
|
+
}
|
|
674
|
+
/** @internal */
|
|
675
|
+
_setMetadataImpl(key, value, conversationId) {
|
|
503
676
|
this._meta[key] = value;
|
|
504
677
|
this.emit('metadataUpdated', { metadata: this._meta, source: 'local_user' });
|
|
505
678
|
// Fire-and-forget server call
|
|
506
|
-
this.graphqlClient.setSpaceMeta(this.id, this._meta, this._channelId)
|
|
679
|
+
this.graphqlClient.setSpaceMeta(this.id, this._meta, this._channelId, conversationId)
|
|
507
680
|
.catch((error) => {
|
|
508
681
|
this.logger.error('[RoolChannel] Failed to set meta:', error);
|
|
509
682
|
});
|
|
@@ -528,13 +701,18 @@ export class RoolChannel extends EventEmitter {
|
|
|
528
701
|
* @returns The message from the AI and the list of objects that were created or modified
|
|
529
702
|
*/
|
|
530
703
|
async prompt(prompt, options) {
|
|
704
|
+
return this._promptImpl(prompt, options, this._conversationId);
|
|
705
|
+
}
|
|
706
|
+
/** @internal */
|
|
707
|
+
async _promptImpl(prompt, options, conversationId) {
|
|
531
708
|
// Upload attachments via media endpoint, then send URLs to the server
|
|
532
709
|
const { attachments, ...rest } = options ?? {};
|
|
533
710
|
let attachmentUrls;
|
|
534
711
|
if (attachments?.length) {
|
|
535
712
|
attachmentUrls = await Promise.all(attachments.map(file => this.mediaClient.upload(this._id, file)));
|
|
536
713
|
}
|
|
537
|
-
const
|
|
714
|
+
const interactionId = generateEntityId();
|
|
715
|
+
const result = await this.graphqlClient.prompt(this._id, prompt, this._channelId, conversationId, { ...rest, attachmentUrls, interactionId });
|
|
538
716
|
// Collect modified objects — they arrive via SSE events during/after the mutation.
|
|
539
717
|
// Try collecting from buffer first, then fetch any missing from server.
|
|
540
718
|
const objects = [];
|
|
@@ -569,7 +747,24 @@ export class RoolChannel extends EventEmitter {
|
|
|
569
747
|
* Rename this channel.
|
|
570
748
|
*/
|
|
571
749
|
async rename(newName) {
|
|
572
|
-
|
|
750
|
+
// Optimistic local update
|
|
751
|
+
const previousName = this._channel?.name;
|
|
752
|
+
if (this._channel) {
|
|
753
|
+
this._channel.name = newName;
|
|
754
|
+
}
|
|
755
|
+
this.emit('channelUpdated', { channelId: this._channelId, source: 'local_user' });
|
|
756
|
+
// Call server
|
|
757
|
+
try {
|
|
758
|
+
await this.graphqlClient.renameChannel(this._id, this._channelId, newName);
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
this.logger.error('[RoolChannel] Failed to rename channel:', error);
|
|
762
|
+
// Rollback
|
|
763
|
+
if (this._channel) {
|
|
764
|
+
this._channel.name = previousName;
|
|
765
|
+
}
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
573
768
|
}
|
|
574
769
|
// ===========================================================================
|
|
575
770
|
// Media Operations
|
|
@@ -671,6 +866,23 @@ export class RoolChannel extends EventEmitter {
|
|
|
671
866
|
return;
|
|
672
867
|
const changeSource = event.source === 'agent' ? 'remote_agent' : 'remote_user';
|
|
673
868
|
switch (event.type) {
|
|
869
|
+
case 'connected':
|
|
870
|
+
// On reconnection, do a full reload to catch up on missed events.
|
|
871
|
+
// Skip on initial connection — data was already fetched by openChannel.
|
|
872
|
+
if (this._hasConnected) {
|
|
873
|
+
void this.graphqlClient.openChannel(this._id, this._channelId).then((result) => {
|
|
874
|
+
if (this._closed)
|
|
875
|
+
return;
|
|
876
|
+
this._meta = result.meta;
|
|
877
|
+
this._schema = result.schema;
|
|
878
|
+
this._channel = result.channel;
|
|
879
|
+
this._objectIds = result.objectIds;
|
|
880
|
+
this._objectStats = new Map(Object.entries(result.objectStats));
|
|
881
|
+
this.emit('reset', { source: 'system' });
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
this._hasConnected = true;
|
|
885
|
+
break;
|
|
674
886
|
case 'object_created':
|
|
675
887
|
if (event.objectId && event.object) {
|
|
676
888
|
if (event.objectStat)
|
|
@@ -694,6 +906,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
694
906
|
case 'schema_updated':
|
|
695
907
|
if (event.schema) {
|
|
696
908
|
this._schema = event.schema;
|
|
909
|
+
this.emit('schemaUpdated', { schema: this._schema, source: changeSource });
|
|
697
910
|
}
|
|
698
911
|
break;
|
|
699
912
|
case 'metadata_updated':
|
|
@@ -703,10 +916,47 @@ export class RoolChannel extends EventEmitter {
|
|
|
703
916
|
}
|
|
704
917
|
break;
|
|
705
918
|
case 'channel_updated':
|
|
706
|
-
// Only update if it's our channel
|
|
919
|
+
// Only update if it's our channel — channel_updated is now metadata-only (name, appUrl)
|
|
707
920
|
if (event.channelId === this._channelId && event.channel) {
|
|
921
|
+
const changed = JSON.stringify(this._channel) !== JSON.stringify(event.channel);
|
|
708
922
|
this._channel = event.channel;
|
|
709
|
-
|
|
923
|
+
if (changed) {
|
|
924
|
+
this.emit('channelUpdated', { channelId: event.channelId, source: changeSource });
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
break;
|
|
928
|
+
case 'conversation_updated':
|
|
929
|
+
// Only update if it's our channel
|
|
930
|
+
if (event.channelId === this._channelId && event.conversationId) {
|
|
931
|
+
if (!this._channel) {
|
|
932
|
+
this._channel = {
|
|
933
|
+
createdAt: Date.now(),
|
|
934
|
+
createdBy: this._userId,
|
|
935
|
+
conversations: {},
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const prev = this._channel.conversations[event.conversationId];
|
|
939
|
+
if (event.conversation) {
|
|
940
|
+
// Update or create conversation in local cache
|
|
941
|
+
this._channel.conversations[event.conversationId] = event.conversation;
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
// Conversation was deleted
|
|
945
|
+
delete this._channel.conversations[event.conversationId];
|
|
946
|
+
}
|
|
947
|
+
// Skip emit if data is unchanged (e.g. echo of our own optimistic update)
|
|
948
|
+
if (JSON.stringify(prev) === JSON.stringify(event.conversation))
|
|
949
|
+
break;
|
|
950
|
+
// Emit the new conversationUpdated event
|
|
951
|
+
this.emit('conversationUpdated', {
|
|
952
|
+
conversationId: event.conversationId,
|
|
953
|
+
channelId: event.channelId,
|
|
954
|
+
source: changeSource,
|
|
955
|
+
});
|
|
956
|
+
// Backward compat: also emit channelUpdated when the active conversation updates
|
|
957
|
+
if (event.conversationId === this._conversationId) {
|
|
958
|
+
this.emit('channelUpdated', { channelId: event.channelId, source: changeSource });
|
|
959
|
+
}
|
|
710
960
|
}
|
|
711
961
|
break;
|
|
712
962
|
case 'space_changed':
|
|
@@ -797,4 +1047,95 @@ export class RoolChannel extends EventEmitter {
|
|
|
797
1047
|
}
|
|
798
1048
|
}
|
|
799
1049
|
}
|
|
1050
|
+
// =============================================================================
|
|
1051
|
+
// ConversationHandle — Scoped proxy for a specific conversation
|
|
1052
|
+
// =============================================================================
|
|
1053
|
+
/**
|
|
1054
|
+
* A lightweight handle for a specific conversation within a channel.
|
|
1055
|
+
*
|
|
1056
|
+
* Scopes AI and mutation operations to a particular conversation's interaction
|
|
1057
|
+
* history, while sharing the channel's single SSE connection and object state.
|
|
1058
|
+
*
|
|
1059
|
+
* Obtain via `channel.conversation('thread-id')`.
|
|
1060
|
+
* Conversations are auto-created on first interaction.
|
|
1061
|
+
*/
|
|
1062
|
+
export class ConversationHandle {
|
|
1063
|
+
/** @internal */
|
|
1064
|
+
_channel;
|
|
1065
|
+
_conversationId;
|
|
1066
|
+
/** @internal */
|
|
1067
|
+
constructor(channel, conversationId) {
|
|
1068
|
+
this._channel = channel;
|
|
1069
|
+
this._conversationId = conversationId;
|
|
1070
|
+
}
|
|
1071
|
+
/** The conversation ID this handle is scoped to. */
|
|
1072
|
+
get conversationId() { return this._conversationId; }
|
|
1073
|
+
// ---------------------------------------------------------------------------
|
|
1074
|
+
// Conversation History
|
|
1075
|
+
// ---------------------------------------------------------------------------
|
|
1076
|
+
/** Get interactions for this conversation. */
|
|
1077
|
+
getInteractions() {
|
|
1078
|
+
return this._channel._getInteractionsImpl(this._conversationId);
|
|
1079
|
+
}
|
|
1080
|
+
/** Get the system instruction for this conversation. */
|
|
1081
|
+
getSystemInstruction() {
|
|
1082
|
+
return this._channel._getSystemInstructionImpl(this._conversationId);
|
|
1083
|
+
}
|
|
1084
|
+
/** Set the system instruction for this conversation. Pass null to clear. */
|
|
1085
|
+
async setSystemInstruction(instruction) {
|
|
1086
|
+
return this._channel._setSystemInstructionImpl(instruction, this._conversationId);
|
|
1087
|
+
}
|
|
1088
|
+
/** Rename this conversation. */
|
|
1089
|
+
async rename(name) {
|
|
1090
|
+
return this._channel._renameConversationImpl(name, this._conversationId);
|
|
1091
|
+
}
|
|
1092
|
+
// ---------------------------------------------------------------------------
|
|
1093
|
+
// Object Operations (scoped to this conversation's interaction history)
|
|
1094
|
+
// ---------------------------------------------------------------------------
|
|
1095
|
+
/** Find objects using structured filters and/or natural language. */
|
|
1096
|
+
async findObjects(options) {
|
|
1097
|
+
return this._channel._findObjectsImpl(options, this._conversationId);
|
|
1098
|
+
}
|
|
1099
|
+
/** Create a new object. */
|
|
1100
|
+
async createObject(options) {
|
|
1101
|
+
return this._channel._createObjectImpl(options, this._conversationId);
|
|
1102
|
+
}
|
|
1103
|
+
/** Update an existing object. */
|
|
1104
|
+
async updateObject(objectId, options) {
|
|
1105
|
+
return this._channel._updateObjectImpl(objectId, options, this._conversationId);
|
|
1106
|
+
}
|
|
1107
|
+
/** Delete objects by IDs. */
|
|
1108
|
+
async deleteObjects(objectIds) {
|
|
1109
|
+
return this._channel._deleteObjectsImpl(objectIds, this._conversationId);
|
|
1110
|
+
}
|
|
1111
|
+
// ---------------------------------------------------------------------------
|
|
1112
|
+
// AI
|
|
1113
|
+
// ---------------------------------------------------------------------------
|
|
1114
|
+
/** Send a prompt to the AI agent, scoped to this conversation's history. */
|
|
1115
|
+
async prompt(text, options) {
|
|
1116
|
+
return this._channel._promptImpl(text, options, this._conversationId);
|
|
1117
|
+
}
|
|
1118
|
+
// ---------------------------------------------------------------------------
|
|
1119
|
+
// Schema (scoped to this conversation's interaction history)
|
|
1120
|
+
// ---------------------------------------------------------------------------
|
|
1121
|
+
/** Create a new collection schema. */
|
|
1122
|
+
async createCollection(name, fields) {
|
|
1123
|
+
return this._channel._createCollectionImpl(name, fields, this._conversationId);
|
|
1124
|
+
}
|
|
1125
|
+
/** Alter an existing collection schema. */
|
|
1126
|
+
async alterCollection(name, fields) {
|
|
1127
|
+
return this._channel._alterCollectionImpl(name, fields, this._conversationId);
|
|
1128
|
+
}
|
|
1129
|
+
/** Drop a collection schema. */
|
|
1130
|
+
async dropCollection(name) {
|
|
1131
|
+
return this._channel._dropCollectionImpl(name, this._conversationId);
|
|
1132
|
+
}
|
|
1133
|
+
// ---------------------------------------------------------------------------
|
|
1134
|
+
// Metadata (scoped to this conversation's interaction history)
|
|
1135
|
+
// ---------------------------------------------------------------------------
|
|
1136
|
+
/** Set a space-level metadata value. */
|
|
1137
|
+
setMetadata(key, value) {
|
|
1138
|
+
return this._channel._setMetadataImpl(key, value, this._conversationId);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
800
1141
|
//# sourceMappingURL=channel.js.map
|