TypeScript SDK for working with Rool Spaces
npm install @rool-dev/sdkThe TypeScript SDK for Rool Spaces, a persistent and collaborative environment for organizing objects and their relationships.
Rool Spaces enables you to build applications where AI operates on a structured world model rather than a text conversation. The context for all AI operations is the full object graph, allowing the system to reason about, update, and expand the state of your application directly.
Use Rool to programmatically instruct agents to generate content, research topics, or reorganize data. The client manages authentication, real-time synchronization, and media storage, supporting both single-user and multi-user workflows.
Core primitives:
- Objects — Key-value records with any fields you define
- Relations — Directional links between objects (e.g., earth → orbits → sun)
- AI operations — Create, update, or query objects using natural language and {{placeholders}}
See Patterns & Examples for what you can build.
``bash`
npm install @rool-dev/sdk
`typescript
import { RoolClient } from '@rool-dev/sdk';
const client = new RoolClient();
client.initialize(); // Process auth callbacks if returning from login
if (!await client.isAuthenticated()) {
client.login('My App'); // Redirects to auth page, shows "Sign in to My App"
}
// Create a new space (or open existing with client.openSpace('id'))
const space = await client.createSpace('Solar System');
// Create objects with AI-generated content using {{placeholders}}
const { object: sun } = await space.createObject({
data: {
type: 'star',
name: 'Sun',
mass: '{{mass in solar masses}}',
temperature: '{{surface temperature in Kelvin}}'
}
});
const { object: earth } = await space.createObject({
data: {
type: 'planet',
name: 'Earth',
mass: '{{mass in Earth masses}}',
radius: '{{radius in km}}',
orbitalPeriod: '{{orbital period in days}}'
}
});
// Link objects together
await space.link(earth.id, 'orbits', sun.id);
// Use the AI agent to work with your data
const { message, objects } = await space.prompt(
'Add the other planets in our solar system and link them to the Sun'
);
console.log(message); // AI explains what it did
console.log(Created ${objects.length} objects);
// Query with natural language
const { objects: innerPlanets } = await space.findObjects({
prompt: 'planets closer to the sun than Earth'
});
// Clean up
space.close();
`
Objects are plain key-value records. The id field is reserved; everything else is application-defined.
`typescript`
{ id: 'abc123', type: 'article', title: 'Hello World', status: 'draft' }
Relations connect objects directionally through named links. Each relation name represents a multi-valued reference set on the source object.
`typescript
await space.link(earth.id, 'orbits', sun.id);
// Reads as: "earth.orbits includes sun"
const orbited = space.getChildren(earth.id, 'orbits'); // [sun] - what earth links TO
const orbiters = space.getParents(sun.id, 'orbits'); // [earth] - what links TO sun
`
Relations are indexed and idempotent — creating the same link twice has no effect.
Notes:
- Relation names are application-defined strings
- Relations are not stored in object data fields
- Only relations created via link() participate in traversal (getParents, getChildren) and indexing
- Storing object IDs inside regular object fields is possible, but those references are opaque to the system
Use {{description}} in field values to have AI generate content:
`typescript
// Create with AI-generated content
await space.createObject({
data: {
type: 'article',
headline: '{{catchy headline about coffee}}',
body: '{{informative paragraph}}'
},
prompt: 'Write about specialty coffee brewing'
});
// Update existing content with AI
await space.updateObject('abc123', {
prompt: 'Make the body shorter and more casual'
});
// Add new AI-generated field to existing object
await space.updateObject('abc123', {
data: { summary: '{{one-sentence summary}}' }
});
`
When resolving placeholders, the agent has access to the full object data and the surrounding space context (except for _-prefixed fields). Placeholders are instructions, not templates, and do not need to repeat information already present in other fields.
Placeholders are resolved by the AI during the mutation and replaced with concrete values. The {{...}} syntax is never stored — it only guides the agent while creating or updating the object.
Undo/redo works on checkpoints, not individual operations. Call checkpoint() before making changes to create a restore point.
`typescript
// Create a checkpoint before user action
await space.checkpoint('Delete object');
await space.deleteObjects([objectId]);
// User can now undo back to the checkpoint
if (await space.canUndo()) {
await space.undo(); // Restores the deleted object
}
// Redo reapplies the undone action
if (await space.canRedo()) {
await space.redo(); // Deletes the object again
}
`
Without a checkpoint, undo() has nothing to restore to. Undo always restores the space to the last checkpoint, regardless of how many changes were made since.
In collaborative scenarios, conflicting changes (modified by others since your checkpoint) are silently skipped.
Fields starting with _ (e.g., _ui, _cache) are hidden from AI but otherwise behave like normal fields — they sync in real-time, persist to the server, support undo/redo, and are visible to all users of the Space. Use them for UI state, positions, or other data the AI shouldn't see or modify:
`typescript`
await space.createObject({
data: {
title: 'My Article',
author: "John Doe",
_ui: { x: 100, y: 200, collapsed: false }
}
});
Events fire for both local and remote changes. The source field indicates origin:
- local_user — This client made the changeremote_user
- — Another user/client made the changeremote_agent
- — AI agent made the changesystem
- — Resync after error
`typescript
// All UI updates happen in one place, regardless of change source
space.on('objectUpdated', ({ objectId, object, source }) => {
renderObject(objectId, object);
if (source === 'remote_agent') {
doLayout(); // AI might have added content
}
});
// Caller just makes the change - event handler does the UI work
space.updateObject(objectId, { prompt: 'expand this' });
`
By default, createObject generates a 6-character alphanumeric ID. Provide your own via data.id:
`typescript`
await space.createObject({ data: { id: 'article-42', title: 'The Meaning of Life' } });
Why use custom IDs?
- Fire-and-forget creation — Know the ID immediately without awaiting the response. You can create an object and link to it in parallel; the sync happens in the background.
- Meaningful IDs — Use domain-specific IDs like user-123 or doc-abc for easier debugging and external references.
`typescript`
// Fire-and-forget: create and link without waiting
const id = RoolClient.generateId();
space.createObject({ data: { id, type: 'note', text: '{{expand this idea}}' } });
space.link(parentId, 'hasNote', id); // Can link immediately
Constraints:
- Must contain only alphanumeric characters, hyphens (-), and underscores (_)
- Must be unique within the space (throws if ID exists)
- Cannot be changed after creation (immutable)
Use RoolClient.generateId() when you need an ID before calling createObject but don't need it to be meaningful — it gives you a valid random ID without writing your own generator.
No configuration needed. Uses localStorage for tokens, redirects to login page.
`typescript
const client = new RoolClient();
client.initialize(); // Process auth callbacks if returning from login
if (!await client.isAuthenticated()) {
client.login('My App'); // Redirect to the auth page
}
`
For CLI tools and scripts. Stores credentials in ~/.config/rool/, opens browser for login.
`typescript
import { NodeAuthProvider } from '@rool-dev/sdk/node';
const client = new RoolClient({ authProvider: new NodeAuthProvider() });
if (!await client.isAuthenticated()) {
await client.login('My CLI Tool'); // Opens browser, waits for callback
}
`
| Method | Description |
|--------|-------------|
| initialize(): boolean | Call on app startup if running in browser. Processes auth callback from URL, sets up token refresh. |login(appName): void
| | Redirect to login page. The app name is displayed on the auth page ("Sign in to {appName}"). |logout(): void
| | Clear tokens and state |isAuthenticated(): Promise
| | Check auth status (validates token) |getToken(): Promise
| | Get current access token |getAuthUser(): AuthUser
| | Get auth identity from JWT ({ email, name }) |
The prompt() method is the primary way to invoke the AI agent. The agent has editor-level capabilities — it can create, modify, delete, link, and research — but cannot see or modify _-prefixed fields.
`typescriptAI: ${message}
const { message, objects } = await space.prompt(
"Create a topic node for the solar system, then child nodes for each planet."
);
console.log();Modified ${objects.length} objects:
console.log(, objects);`
Use checkpoint() before prompting to make operations undoable.
`typescript`
prompt(text: string, options?: PromptOptions): Promise<{ message: string; objects: RoolObject[] }>
Returns a message (the AI's response) and the list of objects that were created or modified.
| Option | Description |
|--------|-------------|
| objectIds | Limit context to specific objects |responseSchema
| | Request structured JSON instead of text summary |effort
| | Effort level: 'QUICK', 'STANDARD' (default), 'REASONING', or 'RESEARCH' |ephemeral
| | If true, don't record in conversation history (useful for tab completion) |readOnly
| | If true, disable mutation tools (create, update, link, unlink). Use for questions. |
| Level | Description |
|-------|-------------|
| QUICK | Fast, lightweight model. Best for simple questions. |STANDARD
| | Default behavior with balanced capabilities. |REASONING
| | Extended reasoning for complex tasks. |RESEARCH
| | Pre-analysis and context gathering (reserved for future use). |
`typescript
// Reorganize and link existing objects
const { objects } = await space.prompt(
"Group these notes by topic and create a parent node for each group."
);
// Work with specific objects
const result = await space.prompt(
"Summarize these articles",
{ objectIds: ['article-1', 'article-2'] }
);
// Quick question without mutations (fast model + read-only)
const { message } = await space.prompt(
"What topics are covered?",
{ effort: 'QUICK', readOnly: true }
);
// Complex analysis with extended reasoning
await space.prompt(
"Analyze relationships and reorganize",
{ effort: 'REASONING' }
);
`
Use responseSchema to get structured JSON instead of a text message:
`typescript
const { message } = await space.prompt("Categorize these items", {
objectIds: ['item-1', 'item-2', 'item-3'],
responseSchema: {
type: 'object',
properties: {
categories: {
type: 'array',
items: { type: 'string' }
},
summary: { type: 'string' }
}
}
});
const result = JSON.parse(message);
console.log(result.categories, result.summary);
`
AI operations automatically receive context:
- Interaction history — Previous interactions and their results from this conversation
- Recently modified objects — Objects created or changed recently
- Selected objects — Objects passed via objectIds are given primary focus
This context flows automatically — no configuration needed. The AI sees enough history to maintain coherent interactions while respecting the _-prefixed field hiding rules.
To add a user to a space, you need their user ID. Use searchUser() to find them by email:
`typescript
// Find the user by email
const user = await client.searchUser('colleague@example.com');
if (!user) {
throw new Error('User not found');
}
// Add them to the space
await space.addUser(user.id, 'editor');
`
| Role | Capabilities |
|------|--------------|
| owner | Full control, can delete space and manage users |editor
| | Can create, modify, delete objects and links |viewer
| | Read-only access (can query with prompt and findObjects) |
| Method | Description |
|--------|-------------|
| listUsers(): Promise | List users with access |addUser(userId, role): Promise
| | Add user to space |removeUser(userId): Promise
| | Remove user from space |
| Method | Description |
|--------|-------------|
| getCurrentUser(): Promise | Get current Rool user (id, email, name, slug, plan, creditsBalance, totalCreditsUsed, createdAt, lastActivity, processedAt, storage) |searchUser(email): Promise
| | Find user by exact email address (no partial matching) |
When multiple users have a space open, changes sync in real-time. The source field in events tells you who made the change:
`typescript`
space.on('objectUpdated', ({ objectId, object, source }) => {
if (source === 'remote_user') {
// Another user made this change
showCollaboratorActivity(object);
}
});
See Real-time Sync for more on event sources.
| Method | Description |
|--------|-------------|
| listSpaces(): Promise | List available spaces |openSpace(id, options?): Promise
| | Open a space for editing. Options: { conversationId?: string } |createSpace(name?, options?): Promise
| | Create a new space. Options: { conversationId?: string } |deleteSpace(id): Promise
| | Permanently delete a space (cannot be undone) |
Server-side key-value storage for user preferences, UI state, and other persistent data. Replaces browser localStorage with cross-device, server-synced storage.
Features:
- Sync reads from local cache (available immediately, even before auth)
- Automatic sync to server and across tabs/devices via SSE
- userStorageChanged event fires on all changes (local or remote)
- Total storage limited to 10MB per user
| Method | Description |
|--------|-------------|
| getUserStorage | Get a value (sync, from local cache) |setUserStorage(key, value): void
| | Set a value (updates cache, syncs to server) |getAllUserStorage(): Record
| | Get all stored data (sync, from local cache) |
`typescript
// Sync read at startup (before auth completes)
const theme = client.getUserStorage
applyTheme(theme ?? 'light');
// Write - updates immediately, syncs to server in background
client.setUserStorage('theme', 'dark');
client.setUserStorage('sidebar', { collapsed: true, width: 280 });
// Delete a key
client.setUserStorage('theme', null);
// The cache may be stale from a previous session — listen for updates
// to apply fresh values once sync completes (or when other tabs/devices change values)
client.on('userStorageChanged', ({ key, value, source }) => {
// source: 'local' (this client) or 'remote' (server/other client)
if (key === 'theme') applyTheme(value as string);
});
`
| Method | Description |
|--------|-------------|
| RoolClient.generateId(): string | Generate 6-char alphanumeric ID (static) |graphql
| | Execute raw GraphQL |destroy(): void
| | Clean up resources |
`typescript`
client.on('authStateChanged', (authenticated: boolean) => void)
client.on('spaceCreated', (space: RoolSpaceInfo) => void)
client.on('spaceDeleted', (spaceId: string) => void)
client.on('spaceRenamed', (spaceId: string, newName: string) => void)
client.on('userStorageChanged', ({ key, value, source }: UserStorageChangedEvent) => void)
client.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)
client.on('error', (error: Error, context?: string) => void)
Spaces are first-class objects with built-in undo/redo, event emission, and real-time sync.
| Property | Description |
|----------|-------------|
| id: string | Space ID |name: string
| | Space name |role: RoolUserRole
| | User's role ('owner' \| 'editor' \| 'viewer') |userId: string
| | Current user's ID |conversationId: string
| | ID for interaction history (tracks AI context). Writable — set to switch conversations. |isReadOnly(): boolean
| | True if viewer role |
| Method | Description |
|--------|-------------|
| close(): void | Clean up resources and stop receiving updates |rename(newName): Promise
| | Rename the space |
Objects are plain key/value records. id is the only reserved field; everything else is application-defined.
| Method | Description |
|--------|-------------|
| getObject(objectId): Promise | Get object data, or undefined if not found. |stat(objectId): Promise
| | Get object stat (audit info: modifiedAt, modifiedBy, modifiedByName), or undefined if not found. |findObjects(options): Promise<{ objects, message }>
| | Find objects using structured filters and natural language. Results sorted by modifiedAt (desc by default). |getObjectIds(options?): string[]
| | Get all object IDs. Sorted by modifiedAt (desc by default). Options: { limit?, order? }. |createObject(options): Promise<{ object, message }>
| | Create a new object. Returns the object (with AI-filled content) and message. |updateObject(objectId, options): Promise<{ object, message }>
| | Update an existing object. Returns the updated object and message. |deleteObjects(objectIds): Promise
| | Delete objects. Outbound links are removed automatically. |
#### createObject / updateObject Options
Both methods accept an options object:
| Option | Description |
|--------|-------------|
| data | Object data fields (any key-value pairs). Include id to use a custom ID (createObject only). Use {{placeholder}} for AI-generated content. Using null/undefined deletes a field. Fields prefixed with _ are hidden from AI. Optional for both methods. |prompt
| | Natural language instruction for AI to generate or modify content. |ephemeral
| | If true, the operation won't be recorded in conversation history. Useful for transient operations. |
#### findObjects Options
Find objects using structured filters, semantic matching, and natural language. All queries are executed server-side.
| Option | Description |
|--------|-------------|
| where | Structured field requirements. Static values = exact match. {{placeholder}} values = semantic match by AI. |prompt
| | Natural language query for additional filtering. |limit
| | Maximum number of results to return. |objectIds
| | Scope search to specific objects (like prompt()). |order
| | Sort order by modifiedAt: 'asc' or 'desc' (default: 'desc'). |ephemeral
| | If true, the query won't be recorded in conversation history. Useful for responsive search. |
Examples:
`typescript
// Exact field matching (no AI needed)
const { objects } = await space.findObjects({
where: { type: 'article', status: 'published' }
});
// Pure natural language query (AI interprets)
const { objects, message } = await space.findObjects({
prompt: 'articles about space exploration published this year'
});
// Semantic field matching with {{...}} placeholders
const { objects } = await space.findObjects({
where: {
type: 'product',
category: '{{something edible}}' // AI interprets this
}
});
// Combined: structured + semantic + natural language
const { objects } = await space.findObjects({
where: {
type: 'article',
topic: '{{related to climate}}'
},
prompt: 'that discuss solutions positively',
limit: 10
});
`
The AI has access to the full object graph context (except _-prefixed fields) when evaluating queries. The returned message explains why objects matched the criteria.
| Method | Description |
|--------|-------------|
| link(sourceId, relation, targetId): Promise | Add target to the named relation on source. Reads as "source.relation includes target". |unlink(sourceId, relation?, targetId?): Promise
| | Remove relations. Three forms: (source, relation, target) removes one link, (source, relation) clears all targets for that relation, (source) clears all relations on source. |getParents(objectId, relation?, options?): Promise
| | Get objects that link TO this object. Sorted by modifiedAt (desc by default). Options: { limit?, order? }. |getChildren(objectId, relation?, options?): Promise
| | Get objects this object links TO. Sorted by modifiedAt (desc by default). Options: { limit?, order? }. |
Examples:
`typescript
// Create relations
await space.link(earth.id, 'orbits', sun.id);
await space.link(sun.id, 'hasPlanet', earth.id);
// This reads as:
// "earth.orbits includes sun"
// "sun.hasPlanet includes earth"
// Remove one specific link
await space.unlink(earth.id, 'orbits', sun.id);
// Clear all targets for a relation
await space.unlink(earth.id, 'orbits');
// Clear ALL relations on an object
await space.unlink(earth.id);
// Query relations
const planets = space.getChildren(sun.id, 'hasPlanet');
const stars = space.getParents(earth.id, 'orbits');
`
| Method | Description |
|--------|-------------|
| checkpoint(label?): Promise | Call before mutations. Saves current state for undo. |canUndo(): Promise
| | Check if undo available |canRedo(): Promise
| | Check if redo available |undo(): Promise
| | Undo to previous checkpoint |redo(): Promise
| | Redo undone action |clearHistory(): Promise
| | Clear undo/redo stack |
See Checkpoints & Undo/Redo for semantics.
Store arbitrary data alongside the Space without it being part of the graph content (e.g., viewport state, user preferences).
| Method | Description |
|--------|-------------|
| setMetadata(key, value): void | Set space-level metadata |getMetadata(key): unknown
| | Get metadata value, or undefined if key not set |getAllMetadata(): Record
| | Get all metadata |
Media URLs in object fields are visible to AI. Both uploaded and AI-generated media work the same way — use fetchMedia to retrieve them for display.
| Method | Description |
|--------|-------------|
| uploadMedia(file): Promise | Upload file, returns URL |fetchMedia(url): Promise
| | Fetch any URL, returns headers and blob() method (adds auth for backend URLs, works for external URLs too) |deleteMedia(url): Promise
| | Delete media file by URL |listMedia(): Promise
| | List all media with metadata |
`typescript
// Upload an image
const url = await space.uploadMedia(file);
await space.createObject({ data: { title: 'Photo', image: url } });
// Or let AI generate one using a placeholder
await space.createObject({
data: { title: 'Mascot', image: '{{generate an image of a flying tortoise}}' }
});
// Display media (handles auth automatically)
const response = await space.fetchMedia(object.image);
if (response.contentType.startsWith('image/')) {
const blob = await response.blob();
img.src = URL.createObjectURL(blob);
}
`
Export and import space data as zip archives for backup, portability, or migration:
| Method | Description |
|--------|-------------|
| space.exportArchive(): Promise | Export objects, relations, metadata, conversations, and media as a zip archive |client.importArchive(name, archive): Promise
| | Import from a zip archive, creating a new space |
Export:
`typescript`
const archive = await space.exportArchive();
// Save as .zip file
const url = URL.createObjectURL(archive);
Import:
`typescript`
const newSpace = await client.importArchive('Imported Data', archiveBlob);
The archive format bundles data.json (with objects, relations, metadata, and conversations) and a media/ folder containing all media files. Media URLs are rewritten to relative paths within the archive and restored on import.
Semantic events describe what changed. Events fire for both local changes and remote changes.
`typescript
// source indicates origin:
// - 'local_user': This client made the change
// - 'remote_user': Another user/client made the change
// - 'remote_agent': AI agent made the change
// - 'system': Resync after error
// Object events
space.on('objectCreated', ({ objectId, object, source }) => void)
space.on('objectUpdated', ({ objectId, object, source }) => void)
space.on('objectDeleted', ({ objectId, source }) => void)
// Link events
space.on('linked', ({ sourceId, relation, targetId, source }) => void)
space.on('unlinked', ({ sourceId, relation, targetId, source }) => void)
// Space metadata
space.on('metadataUpdated', ({ metadata, source }) => void)
// Conversation updated (fetch with getInteractions())
space.on('conversationUpdated', ({ conversationId, source }) => void)
// Conversation list changed (created, deleted, renamed)
space.on('conversationsChanged', ({ action, conversationId, name, source }) => void)
// Full state replacement (undo/redo, resync after error)
space.on('reset', ({ source }) => void)
// ConversationId was changed on the space
space.on('conversationIdChanged', ({ previousConversationId, newConversationId }) => void)
// Sync error occurred, space resynced from server
space.on('syncError', (error: Error) => void)
`
AI operations may fail due to rate limiting or other transient errors. Check error.message for user-friendly error text:
`typescript`
try {
await space.updateObject(objectId, { prompt: 'expand this' });
} catch (error) {
if (error.message.includes('temporarily unavailable')) {
showToast('Service busy, please try again in a moment');
} else {
showToast(error.message);
}
}
| Method | Description |
|--------|-------------|
| getData(): RoolSpaceData | Get full space data (internal format) |
Each RoolSpace instance has a conversationId that tracks interaction history for that space. The history records all meaningful interactions (prompts, object changes, links) as self-contained entries, each capturing the request and its result. History is stored in the space data itself and syncs in real-time to all clients.
AI operations (prompt, createObject, updateObject, findObjects) automatically receive:
- Interaction history — Previous interactions and their results from this conversation
- Recently modified objects — Objects in the space recently created or changed
- Selected objects — Objects passed via objectIds are given primary focus
This context flows automatically — no configuration needed. The AI sees enough history to maintain coherent interactions while respecting the _-prefixed field hiding rules.
`typescript
// Get interactions for the current conversationId
const interactions = space.getInteractions();
// Returns: Interaction[]
// Get interactions for a specific conversation ID
const interactions = space.getInteractionsById('other-conversation-id');
// Returns: Interaction[]
// List all conversation IDs that have interactions
const conversationIds = space.getConversationIds();
// Returns: string[]
`
| Method | Description |
|--------|-------------|
| getInteractions(): Interaction[] | Get interactions for the current conversationId |getInteractionsById(id): Interaction[]
| | Get interactions for a specific conversation ID |getConversationIds(): string[]
| | List all conversation IDs that have conversations |deleteConversation(conversationId?): Promise
| | Delete a conversation and its history. Defaults to current conversation. |renameConversation(id, name): Promise
| | Rename a conversation. Creates it if it doesn't exist. |listConversations(): Promise
| | List all conversations with summary info. |getSystemInstruction(): string \| undefined
| | Get system instruction for current conversation. |setSystemInstruction(instruction): Promise
| | Set system instruction for current conversation. Pass null to clear. |
System instructions customize how the AI behaves within a conversation. The instruction persists across all prompts in that conversation.
`typescript
// Make the AI behave like an SQL interpreter
await space.setSystemInstruction(
'Behave like an intelligent SQL interpreter. Respond with simple markdown tables. ' +
'Translate the objects in the space to the implied structure in your responses.'
);
// Now prompts are interpreted as SQL-like queries
const { message } = await space.prompt('SELECT task, due_date FROM tasks ORDER BY due_date');
// Returns a markdown table of tasks, even if no "tasks" objects exist -
// the AI infers actual tasks from the space content
// Clear the instruction to return to default behavior
await space.setSystemInstruction(null);
`
System instructions are useful for:
- Defining response formats (tables, JSON, specific templates)
- Setting a persona or expertise area
- Constraining the AI to specific operations
- Creating domain-specific interfaces over your space data
`typescript`
space.on('conversationUpdated', ({ conversationId, source }) => {
// Conversation changed - refresh if needed
const interactions = space.getInteractions();
renderInteractions(interactions);
});
By default, each call to openSpace() or createSpace() generates a new conversationId. This means:
- Opening a space twice gives you two independent AI conversation histories
- Closing and reopening a space starts fresh
Set conversationId to switch conversations without reopening the space:
`typescript
const space = await client.openSpace('abc123');
// User clicks "Research" thread in sidebar
space.conversationId = 'research-thread';
await space.prompt("Analyze this data");
// User clicks "Main" thread
space.conversationId = 'main-thread';
await space.prompt("Summarize findings");
// Listen for conversation switches
space.on('conversationIdChanged', ({ previousConversationId, newConversationId }) => {
// Re-render chat UI with new conversation's history
renderChat(space.getInteractions());
});
`
Pass a conversationId at open time to start with a specific conversation:
`typescript`
// Resume a known conversation when opening
const space = await client.openSpace('abc123', { conversationId: 'research-thread' });
Use cases:
- Page refresh — Store conversationId in localStorage to maintain context across reloadsconversationId
- Multiple conversations — Switch between different conversation contexts using the setter
- Collaborative conversations — Share a between users to enable shared AI conversation history
Tip: Use the user's id as conversationId to share context across tabs/devices, or a fixed string like 'shared' to share context across all users.
Note: Interaction history is truncated to the most recent 50 entries to manage space size.
The ai field in interactions distinguishes AI-generated responses from synthetic confirmations:ai: true
- — AI processed this operation (prompt, or createObject/updateObject with placeholders)ai: false
- — System confirmation only (e.g., "Linked X to Y", "Created object abc123")
The toolCalls array captures what the AI agent did during execution. Use it to build responsive UIs that show progress while the agent works — the conversationUpdated event fires as each tool completes, letting you display status updates or hints in real-time.
`typescriptid
// RoolObject represents the object data you work with
// Always contains , plus any additional fields
// Fields prefixed with _ are hidden from AI
interface RoolObject {
id: string;
[key: string]: unknown;
}
// Object stat - audit information returned by space.stat()
interface RoolObjectStat {
modifiedAt: number;
modifiedBy: string;
modifiedByName: string | null;
}
// Conversation container with metadata
interface Conversation {
name?: string; // Conversation name (optional)
createdAt: number; // Timestamp when conversation was created
createdBy: string; // User ID who created the conversation
createdByName?: string; // Display name at time of creation
systemInstruction?: string; // Custom system instruction for AI
interactions: Interaction[]; // Interaction history
}
// Conversation summary info (returned by listConversations)
interface ConversationInfo {
id: string;
name: string | null;
createdAt: number;
createdBy: string;
createdByName: string | null;
interactionCount: number;
}
// Internal space data structure
interface RoolSpaceData {
version: number; // Monotonically increasing version for sync consistency
objects: Record
meta: Record
conversations?: Record
}
// Full stored object structure (for advanced use with getData())
interface RoolObjectEntry {
links: Record
data: RoolObject; // The actual object data
modifiedAt: number; // Timestamp of last modification
modifiedBy: string; // User ID who last modified
modifiedByName: string | null; // Display name at time of modification
}
`
`typescript
interface ToolCall {
name: string; // Tool name (e.g., "create_object", "link", "search_web")
input: unknown; // Arguments passed to the tool
result: string; // Truncated result (max 500 chars)
}
interface Interaction {
id: string; // Unique ID for this interaction
timestamp: number;
userId: string; // Who performed this interaction
userName?: string | null; // Display name at time of interaction
operation: 'prompt' | 'createObject' | 'updateObject' | 'link' | 'unlink' | 'deleteObjects';
input: string; // What the user did: prompt text or action description
output: string | null; // Result: AI response or confirmation message (null while in-progress)
ai: boolean; // Whether AI was invoked (vs synthetic confirmation)
modifiedObjectIds: string[]; // Objects affected by this interaction
toolCalls: ToolCall[]; // Tools called during this interaction (for AI prompts)
}
`
`typescript
type RoolUserRole = 'owner' | 'editor' | 'viewer';
interface RoolSpaceInfo { id: string; name: string; role: RoolUserRole; ownerId: string; size: number; createdAt: string; updatedAt: string; }
interface SpaceMember { id: string; email: string; role: RoolUserRole; }
interface UserResult { id: string; email: string; name: string | null; }
interface CurrentUser { id: string; email: string; name: string | null; slug: string; plan: string; creditsBalance: number; totalCreditsUsed: number; createdAt: string; lastActivity: string; processedAt: string; storage: Record
interface MediaInfo { uuid: string; url: string; contentType: string; size: number; createdAt: string; }
interface MediaResponse { contentType: string; size: number | null; blob(): Promise
type ChangeSource = 'local_user' | 'remote_user' | 'remote_agent' | 'system';
`
`typescript
type PromptEffort = 'QUICK' | 'STANDARD' | 'REASONING' | 'RESEARCH';
interface PromptOptions {
objectIds?: string[]; // Scope to specific objects
responseSchema?: Record
effort?: PromptEffort; // Effort level (default: 'STANDARD')
ephemeral?: boolean; // Don't record in conversation history
readOnly?: boolean; // Disable mutation tools (default: false)
}
`
A Rool Space is a persistent, shared world model. Applications project different interaction patterns onto the same core primitives:
- Objects and relations store durable state
- Interaction history tracks what happened (requests, results, modified objects)
- Events describe what changed in real-time
Below are a few representative patterns.
- Space: documents, notes, images, tasks as objects
- Interaction history: prompts and AI responses stored in space, synced across clients
- UI: renders interactions from getInteractions() as chat; derives artifact lists from object events
Pattern
- Interaction history syncs in real-time; UI renders entries as chat bubbles
- Artifacts are persistent objects
- Listen to conversationUpdated to update chat UIobjectIds
- Selecting objects defines the AI working set via
- Space: rooms, items, NPCs, players as objects
- Relations: navigation, containment, location
- Conversation: player commands and narrative continuity
Pattern
- The space is the shared world state
- Objects can be created dynamically as the world expands
- AI generates descriptions and events using {{placeholders}}
- Space: concepts, sources, hypotheses as objects
- Relations: semantic links between them
- Conversation: exploratory analysis and questioning
Pattern
- Graph structure lives in relations
- AI operates on selected subgraphs via objectIds
- Analysis results are stored; reasoning steps are transient
- Durable content lives in space objects and relations
- Interaction history lives in space conversations (persistent, synced, truncated to 50 entries)
- UI state lives in the client, space metadata, or _`-prefixed fields
- AI focus is controlled by object selection, not by replaying history
MIT - see LICENSE for details.