@rool-dev/sdk 0.11.8 → 0.11.9-dev.38b242c
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 +25 -25
- package/dist/graphql.d.ts +10 -8
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +26 -31
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/space-session.d.ts +88 -105
- package/dist/space-session.d.ts.map +1 -1
- package/dist/space-session.js +236 -343
- package/dist/space-session.js.map +1 -1
- package/dist/space.d.ts +11 -6
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js +16 -30
- package/dist/space.js.map +1 -1
- package/dist/types.d.ts +13 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/space-session.js
CHANGED
|
@@ -26,7 +26,7 @@ function walkBranch(interactions, leafId) {
|
|
|
26
26
|
}
|
|
27
27
|
/** Find the default leaf: the most recent interaction by timestamp that has no children. */
|
|
28
28
|
function findDefaultLeaf(interactions) {
|
|
29
|
-
if (!interactions
|
|
29
|
+
if (!interactions)
|
|
30
30
|
return undefined;
|
|
31
31
|
const childSet = new Set();
|
|
32
32
|
for (const ix of Object.values(interactions)) {
|
|
@@ -156,13 +156,20 @@ function base64Body(data) {
|
|
|
156
156
|
return bytes.buffer;
|
|
157
157
|
}
|
|
158
158
|
/**
|
|
159
|
-
*
|
|
159
|
+
* A thin handle over a space's raw APIs (GraphQL, WebDAV, REST) plus the
|
|
160
|
+
* conversation roster the server pushes over SSE. It holds no schema, metadata,
|
|
161
|
+
* or conversation *content* state — schema and meta live in the filesystem
|
|
162
|
+
* (`/space/<collection>/.schema.json`, `/space/.meta.json`) and are read on
|
|
163
|
+
* demand via {@link readSchema} / {@link readMeta}; conversation contents live
|
|
164
|
+
* on {@link ConversationHandle} objects acquired on demand via
|
|
165
|
+
* {@link conversation}. The lightweight conversation meta list (ids + names +
|
|
166
|
+
* counts) is maintained here from openSpace + SSE. Reactive consumers (e.g. the
|
|
167
|
+
* Svelte wrapper) own any cached presentation of that state; this class just
|
|
168
|
+
* wraps the wire.
|
|
160
169
|
*
|
|
161
|
-
* Objects are addressed by machine path (`/space/.../*.json`). Conversation
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* fetched on demand. Object/file reactivity is exposed at the space level via
|
|
165
|
-
* WebDAV sync notifications.
|
|
170
|
+
* Objects are addressed by machine path (`/space/.../*.json`). Conversation
|
|
171
|
+
* handles carry attribution headers for WebDAV writes. Object/file reactivity
|
|
172
|
+
* is exposed via `filesChanged` / `filesReset` plus WebDAV `syncCollection()`.
|
|
166
173
|
*/
|
|
167
174
|
export class SpaceOperations extends EventEmitter {
|
|
168
175
|
_id;
|
|
@@ -175,13 +182,9 @@ export class SpaceOperations extends EventEmitter {
|
|
|
175
182
|
_webdav;
|
|
176
183
|
_onCloseCallback;
|
|
177
184
|
_logger;
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
_conversations;
|
|
182
|
-
_objectStats;
|
|
183
|
-
// Active leaf per conversation (client-side tree cursor)
|
|
184
|
-
_activeLeaves = new Map();
|
|
185
|
+
// Conversation roster — lightweight meta, no interaction bodies. Maintained
|
|
186
|
+
// from openSpace + SSE conversation_updated events.
|
|
187
|
+
_conversationMeta;
|
|
185
188
|
constructor(config) {
|
|
186
189
|
super();
|
|
187
190
|
this._id = config.id;
|
|
@@ -194,11 +197,7 @@ export class SpaceOperations extends EventEmitter {
|
|
|
194
197
|
this._webdav = config.webdav;
|
|
195
198
|
this._logger = config.logger;
|
|
196
199
|
this._onCloseCallback = config.onClose;
|
|
197
|
-
|
|
198
|
-
this._meta = config.meta;
|
|
199
|
-
this._schema = config.schema;
|
|
200
|
-
this._conversations = config.conversations;
|
|
201
|
-
this._objectStats = new Map(Object.entries(config.objectStats));
|
|
200
|
+
this._conversationMeta = config.conversationMeta;
|
|
202
201
|
}
|
|
203
202
|
/**
|
|
204
203
|
* Handle an event from the shared space subscription.
|
|
@@ -208,21 +207,6 @@ export class SpaceOperations extends EventEmitter {
|
|
|
208
207
|
_handleEvent(event) {
|
|
209
208
|
this.applySpaceContentEvent(event);
|
|
210
209
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Apply resync data after reconnection. Called by the client, which
|
|
213
|
-
* fetches space data once and applies it.
|
|
214
|
-
* @internal
|
|
215
|
-
*/
|
|
216
|
-
_applyResyncData(data) {
|
|
217
|
-
if (this._closed)
|
|
218
|
-
return;
|
|
219
|
-
this._meta = data.meta;
|
|
220
|
-
this._schema = data.schema;
|
|
221
|
-
this._objectStats = new Map(Object.entries(data.objectStats));
|
|
222
|
-
this._conversations = data.conversations;
|
|
223
|
-
this._activeLeaves.clear();
|
|
224
|
-
this.emit('reset', { source: 'system' });
|
|
225
|
-
}
|
|
226
210
|
get id() {
|
|
227
211
|
return this._id;
|
|
228
212
|
}
|
|
@@ -239,77 +223,34 @@ export class SpaceOperations extends EventEmitter {
|
|
|
239
223
|
get isReadOnly() {
|
|
240
224
|
return this._role === 'viewer';
|
|
241
225
|
}
|
|
242
|
-
/** @internal */
|
|
243
|
-
_getInteractionsImpl(conversationId) {
|
|
244
|
-
const interactions = this._conversations[conversationId]?.interactions;
|
|
245
|
-
if (!interactions)
|
|
246
|
-
return [];
|
|
247
|
-
// Handle legacy array format
|
|
248
|
-
if (Array.isArray(interactions))
|
|
249
|
-
return interactions;
|
|
250
|
-
const leafId = this._getActiveLeafImpl(conversationId);
|
|
251
|
-
if (!leafId)
|
|
252
|
-
return [];
|
|
253
|
-
return walkBranch(interactions, leafId);
|
|
254
|
-
}
|
|
255
|
-
/** @internal */
|
|
256
|
-
_getTreeImpl(conversationId) {
|
|
257
|
-
const interactions = this._conversations[conversationId]?.interactions;
|
|
258
|
-
if (!interactions || Array.isArray(interactions))
|
|
259
|
-
return {};
|
|
260
|
-
return interactions;
|
|
261
|
-
}
|
|
262
|
-
/** @internal */
|
|
263
|
-
_getActiveLeafImpl(conversationId) {
|
|
264
|
-
return this._activeLeaves.get(conversationId) ?? findDefaultLeaf(this._conversations[conversationId]?.interactions);
|
|
265
|
-
}
|
|
266
|
-
/** @internal */
|
|
267
|
-
_setActiveLeafImpl(interactionId, conversationId) {
|
|
268
|
-
const interactions = this._conversations[conversationId]?.interactions;
|
|
269
|
-
if (!interactions || Array.isArray(interactions) || !interactions[interactionId]) {
|
|
270
|
-
throw new Error(`Interaction "${interactionId}" not found in conversation "${conversationId}"`);
|
|
271
|
-
}
|
|
272
|
-
this._activeLeaves.set(conversationId, interactionId);
|
|
273
|
-
this.emit('conversationUpdated', {
|
|
274
|
-
conversationId,
|
|
275
|
-
source: 'local_user',
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
226
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
227
|
+
* Lightweight conversation roster (no interaction bodies). Maintained from
|
|
228
|
+
* openSpace + SSE; reactive consumers mirror this list.
|
|
281
229
|
*/
|
|
282
230
|
getConversations() {
|
|
283
|
-
return
|
|
284
|
-
id,
|
|
285
|
-
name: conv.name ?? null,
|
|
286
|
-
systemInstruction: conv.systemInstruction ?? null,
|
|
287
|
-
createdAt: conv.createdAt,
|
|
288
|
-
createdBy: conv.createdBy,
|
|
289
|
-
interactionCount: conv.interactions ? Object.keys(conv.interactions).length : 0,
|
|
290
|
-
}));
|
|
231
|
+
return this._conversationMeta;
|
|
291
232
|
}
|
|
292
233
|
/**
|
|
293
|
-
* Delete a conversation
|
|
234
|
+
* Delete a conversation. The server confirms via SSE (`conversation_updated`
|
|
235
|
+
* with a null conversation), which updates the roster and any live handle.
|
|
294
236
|
*/
|
|
295
237
|
async deleteConversation(conversationId) {
|
|
296
238
|
await this._graphqlClient.deleteConversation(this._id, conversationId);
|
|
297
|
-
// Optimistic local update — remove from cache and emit event
|
|
298
|
-
// in case the server doesn't send a conversation_updated event for deletes
|
|
299
|
-
if (this._conversations[conversationId]) {
|
|
300
|
-
delete this._conversations[conversationId];
|
|
301
|
-
this.emit('conversationUpdated', {
|
|
302
|
-
conversationId,
|
|
303
|
-
source: 'local_user',
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
239
|
}
|
|
307
240
|
/**
|
|
308
|
-
* Get a handle for a
|
|
241
|
+
* Get a handle for a conversation. The handle holds its own interaction tree
|
|
242
|
+
* + cursor; call {@link ConversationHandle.load} to fetch an existing
|
|
243
|
+
* conversation's contents, or just {@link ConversationHandle.prompt} to start
|
|
244
|
+
* a new one (SSE fills the tree). Consumers feed SSE updates into the handle
|
|
245
|
+
* via {@link ConversationHandle.applyUpdate}.
|
|
309
246
|
*/
|
|
310
247
|
conversation(conversationId) {
|
|
311
248
|
return new ConversationHandle(this, conversationId);
|
|
312
249
|
}
|
|
250
|
+
/** @internal — fetch a conversation's full contents from the server. */
|
|
251
|
+
async _fetchConversationImpl(conversationId) {
|
|
252
|
+
return this._graphqlClient.getConversation(this._id, conversationId);
|
|
253
|
+
}
|
|
313
254
|
/**
|
|
314
255
|
* Close this space session and clean up resources.
|
|
315
256
|
*/
|
|
@@ -351,10 +292,8 @@ export class SpaceOperations extends EventEmitter {
|
|
|
351
292
|
async clearHistory() {
|
|
352
293
|
this._logger.warn('[RoolSpace] clearHistory() is a no-op: checkpoint history is now managed by the server.');
|
|
353
294
|
}
|
|
354
|
-
davHeaders(
|
|
355
|
-
const headers = new Headers(
|
|
356
|
-
'X-Rool-Conversation-Id': conversationId,
|
|
357
|
-
});
|
|
295
|
+
davHeaders(interactionId) {
|
|
296
|
+
const headers = new Headers();
|
|
358
297
|
if (interactionId)
|
|
359
298
|
headers.set('X-Rool-Interaction-Id', interactionId);
|
|
360
299
|
return headers;
|
|
@@ -398,19 +337,15 @@ export class SpaceOperations extends EventEmitter {
|
|
|
398
337
|
}
|
|
399
338
|
return result;
|
|
400
339
|
}
|
|
401
|
-
/**
|
|
402
|
-
|
|
403
|
-
return this._objectStats.get(objectPath(path));
|
|
404
|
-
}
|
|
405
|
-
/** @internal */
|
|
406
|
-
async _putObjectImpl(path, body, conversationId) {
|
|
340
|
+
/** Create or replace an object JSON file. */
|
|
341
|
+
async putObject(path, body) {
|
|
407
342
|
const canonical = objectPath(path);
|
|
408
343
|
const optimistic = objectFromBody(canonical, body);
|
|
409
344
|
try {
|
|
410
345
|
const interactionId = generateEntityId();
|
|
411
346
|
await this._webdav.put(canonical, JSON.stringify(body), {
|
|
412
347
|
contentType: 'application/json',
|
|
413
|
-
headers: this.davHeaders(
|
|
348
|
+
headers: this.davHeaders(interactionId),
|
|
414
349
|
});
|
|
415
350
|
const fresh = await this.getObject(canonical) ?? optimistic;
|
|
416
351
|
return { object: fresh, message: `Put ${canonical}` };
|
|
@@ -421,8 +356,8 @@ export class SpaceOperations extends EventEmitter {
|
|
|
421
356
|
throw error;
|
|
422
357
|
}
|
|
423
358
|
}
|
|
424
|
-
/**
|
|
425
|
-
async
|
|
359
|
+
/** Patch an existing object JSON file. */
|
|
360
|
+
async patchObject(path, options) {
|
|
426
361
|
const canonical = objectPath(path);
|
|
427
362
|
const data = options.data ?? {};
|
|
428
363
|
const current = await this.readObject(canonical);
|
|
@@ -435,7 +370,7 @@ export class SpaceOperations extends EventEmitter {
|
|
|
435
370
|
await this._webdav.put(canonical, JSON.stringify(body), {
|
|
436
371
|
contentType: 'application/json',
|
|
437
372
|
ifMatch: current.etag ?? undefined,
|
|
438
|
-
headers: this.davHeaders(
|
|
373
|
+
headers: this.davHeaders(interactionId),
|
|
439
374
|
});
|
|
440
375
|
const fresh = await this.getObject(canonical) ?? optimistic;
|
|
441
376
|
return { object: fresh, message: `Patched ${canonical}` };
|
|
@@ -446,23 +381,22 @@ export class SpaceOperations extends EventEmitter {
|
|
|
446
381
|
throw error;
|
|
447
382
|
}
|
|
448
383
|
}
|
|
449
|
-
/**
|
|
450
|
-
async
|
|
384
|
+
/** Move (rename/relocate) an object. */
|
|
385
|
+
async moveObject(from, to, options) {
|
|
451
386
|
const fromPath = objectPath(from);
|
|
452
387
|
const toPath = objectPath(to);
|
|
453
388
|
const optimistic = objectFromBody(toPath, options?.body ?? {});
|
|
454
389
|
try {
|
|
455
390
|
const interactionId = generateEntityId();
|
|
456
391
|
await this._webdav.move(fromPath, toPath, {
|
|
457
|
-
headers: this.davHeaders(
|
|
392
|
+
headers: this.davHeaders(interactionId),
|
|
458
393
|
});
|
|
459
394
|
if (options?.body) {
|
|
460
395
|
await this._webdav.put(toPath, JSON.stringify(options.body), {
|
|
461
396
|
contentType: 'application/json',
|
|
462
|
-
headers: this.davHeaders(
|
|
397
|
+
headers: this.davHeaders(interactionId),
|
|
463
398
|
});
|
|
464
399
|
}
|
|
465
|
-
this._objectStats.delete(fromPath);
|
|
466
400
|
const fresh = await this.getObject(toPath) ?? optimistic;
|
|
467
401
|
return { object: fresh, message: `Moved ${fromPath} to ${toPath}` };
|
|
468
402
|
}
|
|
@@ -472,8 +406,8 @@ export class SpaceOperations extends EventEmitter {
|
|
|
472
406
|
throw error;
|
|
473
407
|
}
|
|
474
408
|
}
|
|
475
|
-
/**
|
|
476
|
-
async
|
|
409
|
+
/** Delete object JSON files by path. */
|
|
410
|
+
async deleteObjects(paths) {
|
|
477
411
|
if (paths.length === 0)
|
|
478
412
|
return;
|
|
479
413
|
const canonical = paths.map(objectPath);
|
|
@@ -481,9 +415,8 @@ export class SpaceOperations extends EventEmitter {
|
|
|
481
415
|
const interactionId = generateEntityId();
|
|
482
416
|
for (const path of canonical) {
|
|
483
417
|
await this._webdav.delete(path, {
|
|
484
|
-
headers: this.davHeaders(
|
|
418
|
+
headers: this.davHeaders(interactionId),
|
|
485
419
|
});
|
|
486
|
-
this._objectStats.delete(path);
|
|
487
420
|
}
|
|
488
421
|
}
|
|
489
422
|
catch (error) {
|
|
@@ -492,158 +425,95 @@ export class SpaceOperations extends EventEmitter {
|
|
|
492
425
|
throw error;
|
|
493
426
|
}
|
|
494
427
|
}
|
|
495
|
-
/**
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
throw new Error(`Collection "${name}" already exists`);
|
|
503
|
-
}
|
|
504
|
-
// Optimistic local update
|
|
505
|
-
const optimisticDef = collectionDef(fields, options);
|
|
506
|
-
this._schema[name] = optimisticDef;
|
|
507
|
-
try {
|
|
508
|
-
await this._webdav.mkcol(collectionPath(name), { headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
509
|
-
await this._webdav.put(schemaPath(name), JSON.stringify(optimisticDef), {
|
|
510
|
-
contentType: 'application/json',
|
|
511
|
-
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
512
|
-
});
|
|
513
|
-
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
514
|
-
return optimisticDef;
|
|
515
|
-
}
|
|
516
|
-
catch (error) {
|
|
517
|
-
this._logger.error('[RoolSpace] Failed to create collection:', error);
|
|
518
|
-
delete this._schema[name];
|
|
519
|
-
throw error;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
/** @internal */
|
|
523
|
-
async _alterCollectionImpl(name, fields, options, conversationId) {
|
|
524
|
-
if (!this._schema[name]) {
|
|
525
|
-
throw new Error(`Collection "${name}" not found`);
|
|
526
|
-
}
|
|
527
|
-
const previous = this._schema[name];
|
|
528
|
-
this._schema[name] = collectionDef(fields, options);
|
|
529
|
-
try {
|
|
530
|
-
const updated = this._schema[name];
|
|
531
|
-
await this._webdav.put(schemaPath(name), JSON.stringify(updated), {
|
|
532
|
-
contentType: 'application/json',
|
|
533
|
-
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
534
|
-
});
|
|
535
|
-
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
536
|
-
return updated;
|
|
537
|
-
}
|
|
538
|
-
catch (error) {
|
|
539
|
-
this._logger.error('[RoolSpace] Failed to alter collection:', error);
|
|
540
|
-
this._schema[name] = previous;
|
|
541
|
-
throw error;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
/** @internal */
|
|
545
|
-
async _dropCollectionImpl(name, conversationId) {
|
|
546
|
-
if (!this._schema[name]) {
|
|
547
|
-
throw new Error(`Collection "${name}" not found`);
|
|
548
|
-
}
|
|
549
|
-
const previous = this._schema[name];
|
|
550
|
-
delete this._schema[name];
|
|
428
|
+
/**
|
|
429
|
+
* Read space metadata from `/space/.meta.json`. Returns `{}` when the space has
|
|
430
|
+
* no metadata file yet. Stateless — callers (e.g. a reactive wrapper) cache and
|
|
431
|
+
* re-fetch this on their own schedule, typically when a file-tree sync reports
|
|
432
|
+
* the node changed.
|
|
433
|
+
*/
|
|
434
|
+
async readMeta() {
|
|
551
435
|
try {
|
|
552
|
-
await this._webdav.
|
|
553
|
-
|
|
436
|
+
const response = await this._webdav.get('/space/.meta.json');
|
|
437
|
+
return jsonObject(await response.json(), 'space meta');
|
|
554
438
|
}
|
|
555
439
|
catch (error) {
|
|
556
|
-
|
|
557
|
-
|
|
440
|
+
if (error instanceof WebDAVError && error.status === 404)
|
|
441
|
+
return {};
|
|
558
442
|
throw error;
|
|
559
443
|
}
|
|
560
444
|
}
|
|
561
|
-
/**
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
async
|
|
567
|
-
this.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (instruction === null) {
|
|
571
|
-
delete conv.systemInstruction;
|
|
572
|
-
}
|
|
573
|
-
else {
|
|
574
|
-
conv.systemInstruction = instruction;
|
|
575
|
-
}
|
|
576
|
-
this.emit('conversationUpdated', {
|
|
577
|
-
conversationId,
|
|
578
|
-
source: 'local_user',
|
|
445
|
+
/**
|
|
446
|
+
* Write the full metadata blob to `/space/.meta.json`, attributed to a
|
|
447
|
+
* conversation. Callers compose the blob (e.g. read-merge-write) — this does no
|
|
448
|
+
* merging.
|
|
449
|
+
*/
|
|
450
|
+
async writeMeta(meta) {
|
|
451
|
+
await this._webdav.put('/space/.meta.json', JSON.stringify(meta), {
|
|
452
|
+
contentType: 'application/json',
|
|
453
|
+
headers: this.davHeaders(generateEntityId()),
|
|
579
454
|
});
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Read the collection schema: one `/space/<name>/.schema.json` per collection
|
|
458
|
+
* directory under `/space`. Returns `{}` for a space with no collections.
|
|
459
|
+
* Stateless — reactive callers re-fetch when a `.schema.json` node changes.
|
|
460
|
+
*/
|
|
461
|
+
async readSchema() {
|
|
462
|
+
const listing = await this._webdav.propfind('/space', { depth: '1', props: ['resourcetype'] });
|
|
463
|
+
const collections = listing.responses
|
|
464
|
+
.filter((r) => r.isCollection && r.path !== '/space')
|
|
465
|
+
.map((r) => r.path.split('/').pop());
|
|
466
|
+
const entries = await Promise.all(collections.map(async (name) => {
|
|
467
|
+
try {
|
|
468
|
+
const response = await this._webdav.get(`/space/${name}/.schema.json`);
|
|
469
|
+
return [name, jsonObject(await response.json(), `schema ${name}`)];
|
|
587
470
|
}
|
|
588
|
-
|
|
589
|
-
|
|
471
|
+
catch (error) {
|
|
472
|
+
if (error instanceof WebDAVError && error.status === 404)
|
|
473
|
+
return null;
|
|
474
|
+
throw error;
|
|
590
475
|
}
|
|
591
|
-
|
|
592
|
-
}
|
|
476
|
+
}));
|
|
477
|
+
const schema = {};
|
|
478
|
+
for (const entry of entries)
|
|
479
|
+
if (entry)
|
|
480
|
+
schema[entry[0]] = entry[1];
|
|
481
|
+
return schema;
|
|
593
482
|
}
|
|
594
|
-
/**
|
|
595
|
-
async
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
conversationId,
|
|
602
|
-
source: 'local_user',
|
|
483
|
+
/** Create a new collection (MKCOL + schema JSON). */
|
|
484
|
+
async createCollection(name, fields, options) {
|
|
485
|
+
const def = collectionDef(fields, options);
|
|
486
|
+
await this._webdav.mkcol(collectionPath(name), { headers: this.davHeaders(generateEntityId()) });
|
|
487
|
+
await this._webdav.put(schemaPath(name), JSON.stringify(def), {
|
|
488
|
+
contentType: 'application/json',
|
|
489
|
+
headers: this.davHeaders(generateEntityId()),
|
|
603
490
|
});
|
|
604
|
-
|
|
605
|
-
await this._graphqlClient.updateConversation(this._id, conversationId, { name });
|
|
606
|
-
}
|
|
607
|
-
catch (error) {
|
|
608
|
-
this._logger.error('[RoolSpace] Failed to rename conversation:', error);
|
|
609
|
-
conv.name = previousName;
|
|
610
|
-
throw error;
|
|
611
|
-
}
|
|
491
|
+
return def;
|
|
612
492
|
}
|
|
613
|
-
/**
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
interactions: {},
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/** @internal */
|
|
624
|
-
_setMetadataImpl(key, value, conversationId) {
|
|
625
|
-
this._meta[key] = value;
|
|
626
|
-
this.emit('metadataUpdated', { metadata: this._meta, source: 'local_user' });
|
|
627
|
-
// Fire-and-forget server call
|
|
628
|
-
this._graphqlClient.setSpaceMeta(this._id, this._meta, conversationId)
|
|
629
|
-
.catch((error) => {
|
|
630
|
-
this._logger.error('[RoolSpace] Failed to set meta:', error);
|
|
493
|
+
/** Alter an existing collection's schema JSON. */
|
|
494
|
+
async alterCollection(name, fields, options) {
|
|
495
|
+
const def = collectionDef(fields, options);
|
|
496
|
+
await this._webdav.put(schemaPath(name), JSON.stringify(def), {
|
|
497
|
+
contentType: 'application/json',
|
|
498
|
+
headers: this.davHeaders(generateEntityId()),
|
|
631
499
|
});
|
|
500
|
+
return def;
|
|
632
501
|
}
|
|
633
|
-
/**
|
|
634
|
-
|
|
635
|
-
|
|
502
|
+
/** Drop a collection (DELETE). */
|
|
503
|
+
async dropCollection(name) {
|
|
504
|
+
await this._webdav.delete(collectionPath(name), { collection: true, headers: this.davHeaders(generateEntityId()) });
|
|
636
505
|
}
|
|
637
|
-
/**
|
|
638
|
-
|
|
639
|
-
|
|
506
|
+
/** @internal */
|
|
507
|
+
async _setSystemInstructionImpl(instruction, conversationId) {
|
|
508
|
+
await this._graphqlClient.updateConversation(this._id, conversationId, { systemInstruction: instruction });
|
|
509
|
+
}
|
|
510
|
+
/** @internal */
|
|
511
|
+
async _renameConversationImpl(name, conversationId) {
|
|
512
|
+
await this._graphqlClient.updateConversation(this._id, conversationId, { name });
|
|
640
513
|
}
|
|
641
514
|
/** @internal */
|
|
642
515
|
async _promptImpl(prompt, options, conversationId) {
|
|
643
|
-
const { attachments,
|
|
644
|
-
// Callers may supply the id so they can render an optimistic message keyed by
|
|
645
|
-
// the id the server will echo back (see PromptOptions.interactionId).
|
|
646
|
-
const interactionId = providedId ?? generateEntityId();
|
|
516
|
+
const { attachments, signal, interactionId = generateEntityId(), parentInteractionId = null, ...rest } = options ?? {};
|
|
647
517
|
let attachmentRefs;
|
|
648
518
|
if (attachments?.length) {
|
|
649
519
|
attachmentRefs = await Promise.all(attachments.map(async (attachment) => {
|
|
@@ -651,12 +521,6 @@ export class SpaceOperations extends EventEmitter {
|
|
|
651
521
|
return machineUri(path);
|
|
652
522
|
}));
|
|
653
523
|
}
|
|
654
|
-
// Auto-continue from active leaf if no explicit parent provided
|
|
655
|
-
const parentInteractionId = explicitParent !== undefined
|
|
656
|
-
? explicitParent
|
|
657
|
-
: (this._getActiveLeafImpl(conversationId) ?? null);
|
|
658
|
-
// Optimistically set active leaf before the server call.
|
|
659
|
-
this._activeLeaves.set(conversationId, interactionId);
|
|
660
524
|
let onAbort;
|
|
661
525
|
if (signal) {
|
|
662
526
|
if (signal.aborted) {
|
|
@@ -691,6 +555,7 @@ export class SpaceOperations extends EventEmitter {
|
|
|
691
555
|
return {
|
|
692
556
|
message: result.message,
|
|
693
557
|
objects,
|
|
558
|
+
creditsUsed: result.creditsUsed,
|
|
694
559
|
};
|
|
695
560
|
}
|
|
696
561
|
/**
|
|
@@ -702,19 +567,6 @@ export class SpaceOperations extends EventEmitter {
|
|
|
702
567
|
async stopInteraction(interactionId) {
|
|
703
568
|
return this._graphqlClient.stopInteraction(this._id, interactionId);
|
|
704
569
|
}
|
|
705
|
-
/** @internal */
|
|
706
|
-
async _stopImpl(conversationId) {
|
|
707
|
-
const leafId = this._getActiveLeafImpl(conversationId);
|
|
708
|
-
if (!leafId)
|
|
709
|
-
return false;
|
|
710
|
-
const interactions = this._conversations[conversationId]?.interactions;
|
|
711
|
-
const interaction = interactions && !Array.isArray(interactions) ? interactions[leafId] : undefined;
|
|
712
|
-
// Skip the round trip when we already know the interaction has settled.
|
|
713
|
-
if (interaction && (interaction.status === 'done' || interaction.status === 'error')) {
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
|
-
return this.stopInteraction(leafId);
|
|
717
|
-
}
|
|
718
570
|
/**
|
|
719
571
|
* Fetch an external URL via the server proxy, bypassing CORS restrictions.
|
|
720
572
|
*/
|
|
@@ -738,46 +590,59 @@ export class SpaceOperations extends EventEmitter {
|
|
|
738
590
|
applySpaceContentEvent(event) {
|
|
739
591
|
if (this._closed)
|
|
740
592
|
return;
|
|
593
|
+
if (event.type !== 'conversation_updated' || !event.conversationId)
|
|
594
|
+
return;
|
|
741
595
|
const changeSource = event.source === 'agent' ? 'remote_agent' : 'remote_user';
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
596
|
+
const conversation = event.conversation ?? null;
|
|
597
|
+
this._updateConversationMeta(event.conversationId, conversation);
|
|
598
|
+
this.emit('conversationUpdated', {
|
|
599
|
+
conversationId: event.conversationId,
|
|
600
|
+
conversation,
|
|
601
|
+
source: changeSource,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
/** Maintain the conversation meta list from an SSE update. */
|
|
605
|
+
_updateConversationMeta(id, conversation) {
|
|
606
|
+
if (conversation === null) {
|
|
607
|
+
this._conversationMeta = this._conversationMeta.filter((m) => m.id !== id);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const interactionCount = conversation.interactions ? Object.keys(conversation.interactions).length : 0;
|
|
611
|
+
const entry = {
|
|
612
|
+
id,
|
|
613
|
+
name: conversation.name ?? null,
|
|
614
|
+
systemInstruction: conversation.systemInstruction ?? null,
|
|
615
|
+
createdAt: conversation.createdAt,
|
|
616
|
+
createdBy: conversation.createdBy,
|
|
617
|
+
interactionCount,
|
|
618
|
+
updatedAt: Date.now(),
|
|
619
|
+
};
|
|
620
|
+
const idx = this._conversationMeta.findIndex((m) => m.id === id);
|
|
621
|
+
if (idx >= 0) {
|
|
622
|
+
this._conversationMeta = [
|
|
623
|
+
...this._conversationMeta.slice(0, idx),
|
|
624
|
+
entry,
|
|
625
|
+
...this._conversationMeta.slice(idx + 1),
|
|
626
|
+
];
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
this._conversationMeta = [entry, ...this._conversationMeta];
|
|
771
630
|
}
|
|
772
631
|
}
|
|
773
632
|
}
|
|
774
633
|
/**
|
|
775
|
-
* A
|
|
634
|
+
* A stateful handle for a single conversation. Holds the interaction tree and a
|
|
635
|
+
* client-side branch cursor (active leaf). Acquired on demand via
|
|
636
|
+
* {@link SpaceOperations.conversation}; a fresh handle each call — the SDK
|
|
637
|
+
* holds no handle map. The handle starts unloaded; call {@link load} to fetch
|
|
638
|
+
* an existing conversation's contents, or just {@link prompt} to start a new
|
|
639
|
+
* one. Feed SSE updates via {@link applyUpdate}.
|
|
776
640
|
*/
|
|
777
641
|
export class ConversationHandle {
|
|
778
|
-
/** @internal */
|
|
779
642
|
_space;
|
|
780
643
|
_conversationId;
|
|
644
|
+
_data = null;
|
|
645
|
+
_activeLeaf;
|
|
781
646
|
/** @internal */
|
|
782
647
|
constructor(space, conversationId) {
|
|
783
648
|
this._space = space;
|
|
@@ -785,25 +650,75 @@ export class ConversationHandle {
|
|
|
785
650
|
}
|
|
786
651
|
/** The conversation ID this handle is scoped to. */
|
|
787
652
|
get conversationId() { return this._conversationId; }
|
|
788
|
-
/**
|
|
653
|
+
/** Whether the conversation contents have been loaded. */
|
|
654
|
+
get loaded() { return this._data !== null; }
|
|
655
|
+
/** Fetch the full conversation from the server. Skips the overwrite if SSE
|
|
656
|
+
* already filled the tree during the fetch (SSE is the real-time source of
|
|
657
|
+
* truth). Pass `true` to force a reload — used after a reconnect `reset`,
|
|
658
|
+
* where we may have missed SSE updates while disconnected. */
|
|
659
|
+
async load(force = false) {
|
|
660
|
+
const data = await this._space._fetchConversationImpl(this._conversationId);
|
|
661
|
+
if (!force && this._data)
|
|
662
|
+
return;
|
|
663
|
+
this._data = data;
|
|
664
|
+
this._activeLeaf = this._data ? findDefaultLeaf(this._data.interactions) : undefined;
|
|
665
|
+
}
|
|
666
|
+
/** Apply an SSE conversation update — updates the tree + cursor. Pass `null`
|
|
667
|
+
* for a deletion. */
|
|
668
|
+
applyUpdate(conversation) {
|
|
669
|
+
if (conversation === null) {
|
|
670
|
+
this._data = null;
|
|
671
|
+
this._activeLeaf = undefined;
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
// Advance the cursor to a new child of the current leaf, if one appeared.
|
|
675
|
+
const currentLeaf = this._activeLeaf;
|
|
676
|
+
if (currentLeaf) {
|
|
677
|
+
for (const ix of Object.values(conversation.interactions)) {
|
|
678
|
+
if (ix.parentId === currentLeaf && ix.id !== currentLeaf) {
|
|
679
|
+
this._activeLeaf = ix.id;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
this._data = conversation;
|
|
685
|
+
if (this._activeLeaf === undefined) {
|
|
686
|
+
this._activeLeaf = findDefaultLeaf(conversation.interactions);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/** Get the active branch as a flat array (root → leaf). Empty until loaded. */
|
|
789
690
|
getInteractions() {
|
|
790
|
-
|
|
691
|
+
if (!this._data)
|
|
692
|
+
return [];
|
|
693
|
+
const leaf = this._activeLeaf ?? findDefaultLeaf(this._data.interactions);
|
|
694
|
+
if (!leaf)
|
|
695
|
+
return [];
|
|
696
|
+
return walkBranch(this._data.interactions, leaf);
|
|
791
697
|
}
|
|
792
|
-
/** Get the full interaction tree as a record. */
|
|
698
|
+
/** Get the full interaction tree as a record. Empty until loaded. */
|
|
793
699
|
getTree() {
|
|
794
|
-
|
|
700
|
+
if (!this._data)
|
|
701
|
+
return {};
|
|
702
|
+
return this._data.interactions;
|
|
795
703
|
}
|
|
796
704
|
/** Get the active leaf interaction ID, or undefined if empty. */
|
|
797
705
|
get activeLeafId() {
|
|
798
|
-
|
|
706
|
+
if (this._activeLeaf)
|
|
707
|
+
return this._activeLeaf;
|
|
708
|
+
if (!this._data)
|
|
709
|
+
return undefined;
|
|
710
|
+
return findDefaultLeaf(this._data.interactions);
|
|
799
711
|
}
|
|
800
712
|
/** Switch to a different branch by setting the active leaf. */
|
|
801
713
|
setActiveLeaf(interactionId) {
|
|
802
|
-
this.
|
|
714
|
+
if (!this._data || !this._data.interactions[interactionId]) {
|
|
715
|
+
throw new Error(`Interaction "${interactionId}" not found in conversation "${this._conversationId}"`);
|
|
716
|
+
}
|
|
717
|
+
this._activeLeaf = interactionId;
|
|
803
718
|
}
|
|
804
719
|
/** Get the system instruction for this conversation. */
|
|
805
720
|
getSystemInstruction() {
|
|
806
|
-
return this.
|
|
721
|
+
return this._data?.systemInstruction;
|
|
807
722
|
}
|
|
808
723
|
/** Set the system instruction for this conversation. Pass null to clear. */
|
|
809
724
|
async setSystemInstruction(instruction) {
|
|
@@ -817,29 +732,16 @@ export class ConversationHandle {
|
|
|
817
732
|
async delete() {
|
|
818
733
|
return this._space.deleteConversation(this._conversationId);
|
|
819
734
|
}
|
|
820
|
-
/**
|
|
821
|
-
|
|
822
|
-
return this._space._putObjectImpl(path, body, this._conversationId);
|
|
823
|
-
}
|
|
824
|
-
/** Patch an existing object JSON file. */
|
|
825
|
-
async patchObject(path, options) {
|
|
826
|
-
return this._space._patchObjectImpl(path, options, this._conversationId);
|
|
827
|
-
}
|
|
828
|
-
/** Move (rename/relocate) an object. */
|
|
829
|
-
async moveObject(from, to, options) {
|
|
830
|
-
return this._space._moveObjectImpl(from, to, options, this._conversationId);
|
|
831
|
-
}
|
|
832
|
-
/** Delete object JSON files by path. */
|
|
833
|
-
async deleteObjects(paths) {
|
|
834
|
-
return this._space._deleteObjectsImpl(paths, this._conversationId);
|
|
835
|
-
}
|
|
836
|
-
/** @deprecated Use deleteObjects instead. */
|
|
837
|
-
async deletePaths(paths) {
|
|
838
|
-
return this.deleteObjects(paths);
|
|
839
|
-
}
|
|
840
|
-
/** Send a prompt to the AI agent, scoped to this conversation's history. */
|
|
735
|
+
/** Send a prompt to the AI agent, scoped to this conversation's history.
|
|
736
|
+
* Auto-continues from the active leaf unless `parentInteractionId` is set. */
|
|
841
737
|
async prompt(text, options) {
|
|
842
|
-
|
|
738
|
+
const interactionId = options?.interactionId ?? generateEntityId();
|
|
739
|
+
const parentInteractionId = options?.parentInteractionId !== undefined
|
|
740
|
+
? options.parentInteractionId
|
|
741
|
+
: (this.activeLeafId ?? null);
|
|
742
|
+
// Optimistic: advance the cursor to the new interaction.
|
|
743
|
+
this._activeLeaf = interactionId;
|
|
744
|
+
return this._space._promptImpl(text, { ...options, interactionId, parentInteractionId }, this._conversationId);
|
|
843
745
|
}
|
|
844
746
|
/**
|
|
845
747
|
* Stop this conversation's in-flight interaction, if any. No-op returning
|
|
@@ -847,22 +749,13 @@ export class ConversationHandle {
|
|
|
847
749
|
* {@link RoolSpace.stopInteraction}.
|
|
848
750
|
*/
|
|
849
751
|
async stop() {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
async alterCollection(name, fields, options) {
|
|
858
|
-
return this._space._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
859
|
-
}
|
|
860
|
-
/** Drop a collection schema. */
|
|
861
|
-
async dropCollection(name) {
|
|
862
|
-
return this._space._dropCollectionImpl(name, this._conversationId);
|
|
863
|
-
}
|
|
864
|
-
setMetadata(key, value) {
|
|
865
|
-
return this._space._setMetadataImpl(key, value, this._conversationId);
|
|
752
|
+
const leafId = this.activeLeafId;
|
|
753
|
+
if (!leafId)
|
|
754
|
+
return false;
|
|
755
|
+
const interaction = this._data?.interactions[leafId];
|
|
756
|
+
if (interaction && (interaction.status === 'done' || interaction.status === 'error'))
|
|
757
|
+
return false;
|
|
758
|
+
return this._space.stopInteraction(leafId);
|
|
866
759
|
}
|
|
867
760
|
}
|
|
868
761
|
//# sourceMappingURL=space-session.js.map
|