@rool-dev/sdk 2.0.0-dev.36748aa → 2.0.0-dev.6381fab
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/LICENSE +21 -0
- package/README.md +62 -602
- package/assets/rool-logo.svg +5 -0
- package/assets/rool-wordmark-dark.svg +7 -0
- package/assets/rool-wordmark.svg +7 -0
- package/dist/agents.d.ts +5 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +21 -1
- package/dist/agents.js.map +1 -1
- package/dist/auth-base.d.ts.map +1 -1
- package/dist/auth-base.js.map +1 -1
- package/dist/auth-browser.d.ts.map +1 -1
- package/dist/auth-native.d.ts.map +1 -1
- package/dist/auth-node.d.ts.map +1 -1
- package/dist/auth-node.js.map +1 -1
- package/dist/auth-url.js.map +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +8 -0
- package/dist/client.js.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/files.d.ts +3 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +13 -2
- package/dist/files.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/machine.d.ts.map +1 -1
- package/dist/machine.js.map +1 -1
- package/dist/problem.d.ts.map +1 -1
- package/dist/structured.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +19 -12
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center" class="sdk-brand"><a href="https://rool.dev"><img class="sdk-brand-logo" src="./assets/rool-logo.svg" alt="Rool logo" width="44" height="44"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/rool-wordmark-dark.svg"><img class="sdk-brand-wordmark" src="./assets/rool-wordmark.svg" alt="Rool" width="128"></picture></a></p>
|
|
2
2
|
|
|
3
|
-
TypeScript SDK
|
|
3
|
+
<h1 align="center" class="sdk-title">TypeScript SDK</h1>
|
|
4
|
+
|
|
5
|
+
The official TypeScript SDK for building apps and automations with [Rool Machines](https://rool.dev).
|
|
6
|
+
|
|
7
|
+
A Rool Machine is a persistent cloud VM with snapshots, files, and embedded AI agents. The SDK provides one typed interface for authentication, machine management, file access, agent conversations, and live updates.
|
|
4
8
|
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
@@ -8,652 +12,108 @@ TypeScript SDK for Rool Machines.
|
|
|
8
12
|
npm install @rool-dev/sdk
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { RoolClient } from "@rool-dev/sdk";
|
|
15
|
-
|
|
16
|
-
const client = new RoolClient({
|
|
17
|
-
getTokens: () => ({
|
|
18
|
-
accessToken: currentAccessToken,
|
|
19
|
-
roolToken: currentRoolToken,
|
|
20
|
-
}),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const session = await client.getSession();
|
|
24
|
-
const account = await client.getAccount();
|
|
25
|
-
const profile = await client.getProfile();
|
|
26
|
-
const userAppData = await client.getUserAppData();
|
|
27
|
-
const greeting = await client.getGreeting("en");
|
|
28
|
-
|
|
29
|
-
await client.replaceProfile({
|
|
30
|
-
name: "Ada",
|
|
31
|
-
marketingOptIn: true,
|
|
32
|
-
});
|
|
33
|
-
await client.setUserAppData("theme", "dark");
|
|
34
|
-
await client.deleteUserAppData("theme");
|
|
35
|
-
await client.deleteAccount();
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
`UserAppData` is an opaque cross-device JSON object. App data is changed one key at a time so unrelated settings cannot overwrite each other.
|
|
39
|
-
|
|
40
|
-
## Account events
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
const unsubscribe = client.events.subscribe(async (event) => {
|
|
44
|
-
if (event.type === "session") {
|
|
45
|
-
renderSession(event.session);
|
|
46
|
-
} else if (event.type === "account_changed") {
|
|
47
|
-
renderAccount(await client.getAccount());
|
|
48
|
-
} else if (event.type === "profile_changed") {
|
|
49
|
-
renderProfile(await client.getProfile());
|
|
50
|
-
} else if (event.type === "user_app_data_changed") {
|
|
51
|
-
renderUserAppData(await client.getUserAppData());
|
|
52
|
-
} else if (event.type === "machines_changed") {
|
|
53
|
-
renderMachines(await client.listMachines());
|
|
54
|
-
} else if (event.type === "machine_members_changed") {
|
|
55
|
-
renderMembers(
|
|
56
|
-
event.machineId,
|
|
57
|
-
await client.machine(event.machineId).members.list(),
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
unsubscribe();
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Account events tell the client when something changed. Fetch the relevant route to get the latest data. The SDK starts with `/v2/session`, then long-polls for changes using its account sync token. Empty polls and network retries do not produce events or refresh the session. If a token expires, the SDK fetches a new session and sends another `session` event.
|
|
15
|
+
## Connect from Node.js
|
|
66
16
|
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
## Authentication
|
|
70
|
-
|
|
71
|
-
Auth clients own login, credential storage, and refresh. `RoolClient` only asks one for tokens and tells it when authentication is invalidated.
|
|
72
|
-
|
|
73
|
-
### Browser authentication
|
|
17
|
+
`NodeAuth` opens the user's browser for login, stores their credentials locally, and refreshes them when needed.
|
|
74
18
|
|
|
75
19
|
```typescript
|
|
76
|
-
import {
|
|
77
|
-
|
|
78
|
-
const auth = new BrowserAuth();
|
|
79
|
-
const client = new RoolClient({
|
|
80
|
-
getTokens: auth.getTokens,
|
|
81
|
-
onAuthInvalidated: auth.logout,
|
|
82
|
-
});
|
|
20
|
+
import { RoolClient } from "@rool-dev/sdk";
|
|
21
|
+
import { NodeAuth } from "@rool-dev/sdk/node";
|
|
83
22
|
|
|
84
|
-
auth
|
|
23
|
+
const auth = new NodeAuth();
|
|
85
24
|
if (!(await auth.initialize())) {
|
|
86
|
-
await auth.login("My
|
|
25
|
+
await auth.login("My app");
|
|
87
26
|
}
|
|
88
|
-
```
|
|
89
27
|
|
|
90
|
-
`initialize()` processes an auth callback in the URL. Tokens are stored in endpoint-scoped browser storage and refreshed when requested.
|
|
91
|
-
|
|
92
|
-
### Native authentication
|
|
93
|
-
|
|
94
|
-
`NativeAuth` uses system-browser PKCE for Google and Apple while also supporting password and magic-link sign-in.
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
import { App } from "@capacitor/app";
|
|
98
|
-
import { Browser } from "@capacitor/browser";
|
|
99
|
-
import { NativeAuth, RoolClient } from "@rool-dev/sdk";
|
|
100
|
-
|
|
101
|
-
const auth = new NativeAuth({
|
|
102
|
-
redirectUri: "roolandroidauth://auth/callback",
|
|
103
|
-
defaultProvider: "google",
|
|
104
|
-
openExternal: (url) => Browser.open({ url }),
|
|
105
|
-
});
|
|
106
28
|
const client = new RoolClient({
|
|
107
29
|
getTokens: auth.getTokens,
|
|
108
30
|
onAuthInvalidated: auth.logout,
|
|
109
31
|
});
|
|
110
|
-
|
|
111
|
-
App.addListener("appUrlOpen", async ({ url }) => {
|
|
112
|
-
await Browser.close();
|
|
113
|
-
await auth.handleRedirect(url);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!(await auth.initialize())) await auth.login("My App");
|
|
117
32
|
```
|
|
118
33
|
|
|
119
|
-
The
|
|
34
|
+
The package also includes:
|
|
120
35
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (result.status === "verify_required") showCheckYourEmailMessage();
|
|
124
|
-
|
|
125
|
-
await auth.requestMagicLink(email);
|
|
126
|
-
await auth.verify(verifyToken);
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
On native, an HTTPS magic link only returns to the app when Universal Links or App Links are configured for that domain.
|
|
36
|
+
- `BrowserAuth` for web apps using browser redirects and browser storage.
|
|
37
|
+
- `NativeAuth` for mobile apps that open sign-in in the system browser and return through a deep link. It also supports passwords and magic links.
|
|
130
38
|
|
|
131
|
-
|
|
39
|
+
## Put a machine to work
|
|
132
40
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
- `setPassword(password)`
|
|
136
|
-
- `requestEmailChange(newEmail)`
|
|
137
|
-
- `verify(token)`
|
|
138
|
-
- `logout()`
|
|
139
|
-
- `isAuthenticated()`
|
|
140
|
-
|
|
141
|
-
### Node.js authentication
|
|
41
|
+
This example creates a machine, gives an agent a job, uploads a receipt, and asks the agent to file it. The file API writes to the machine's WebDAV storage, so the agent and the app see the same files.
|
|
142
42
|
|
|
143
43
|
```typescript
|
|
144
|
-
import {
|
|
145
|
-
import { NodeAuth } from "@rool-dev/sdk/node";
|
|
44
|
+
import { readFile } from "node:fs/promises";
|
|
146
45
|
|
|
147
|
-
const
|
|
148
|
-
|
|
46
|
+
const created = await client.createMachine({ name: "Receipts" });
|
|
47
|
+
const machine = client.machine(created.id);
|
|
149
48
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
49
|
+
const agent = await machine.agents.create("receipt-filer", {
|
|
50
|
+
system:
|
|
51
|
+
"File receipt attachments under /rool-drive/receipts. " +
|
|
52
|
+
"Choose useful subfolders based on the receipt's contents.",
|
|
153
53
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Use `profile` or `credentialsPath` when an application needs multiple independent accounts:
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
const auth = new NodeAuth({
|
|
162
|
-
apiUrl: "https://api.example.com",
|
|
163
|
-
profile: "automation",
|
|
54
|
+
const conversation = await agent.createConversation({
|
|
55
|
+
name: "Receipt inbox",
|
|
56
|
+
visibility: "private",
|
|
164
57
|
});
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
API errors are thrown as `RoolProblem` with the server's stable `code`, HTTP `status`, `title`, and `detail`.
|
|
168
|
-
|
|
169
|
-
## Machine API
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
const created = await client.createMachine({ name: "Research" });
|
|
173
|
-
const machine = client.machine(created.id);
|
|
174
|
-
|
|
175
|
-
const details = await machine.get();
|
|
176
|
-
await machine.settings.replace({ name: "Field research" });
|
|
177
|
-
const copy = await machine.duplicate({ name: "Research copy" });
|
|
178
|
-
const response = await machine.fetchUrl("https://example.com/data.json");
|
|
179
|
-
await client.machine(copy.id).delete();
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
`client.machine(id)` returns a stable machine handle that owns machine-scoped APIs and synchronization state. Creation, listing, sessions, duplication, and `machine.get()` all return the same point-in-time `MachineSummary`; bind its ID to a handle before performing machine operations. Summaries include the machine's inbound email address, lifecycle `state`, and an opaque `meta` JSON object. `fetchUrl()` returns the upstream `Response`, including non-success statuses.
|
|
183
|
-
|
|
184
|
-
## Machine checkpoints
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
const history = await machine.checkpoints.list();
|
|
188
|
-
const checkpoint = history.checkpoints.at(-1);
|
|
189
|
-
if (checkpoint) {
|
|
190
|
-
await machine.checkpoints.restore(checkpoint.id);
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
The checkpoint collection contains the currently restorable timeline and the `baseCheckpointId` underlying the live filesystem. The live filesystem can contain newer uncheckpointed changes. Restoring a checkpoint preserves those changes as a new checkpoint and causes a watched machine file tree to reconcile completely. Moving backward does not discard later checkpoints, but modifying the filesystem from that earlier position replaces the later timeline.
|
|
195
|
-
|
|
196
|
-
## Machine files
|
|
197
58
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const path = "/rool-drive/documents/report.pdf";
|
|
201
|
-
|
|
202
|
-
const storage = await files.getStorageUsage();
|
|
203
|
-
console.log(storage.usedBytes, storage.availableBytes);
|
|
204
|
-
await files.createDirectory("/rool-drive/documents");
|
|
205
|
-
const written = await files.write(path, reportBlob, {
|
|
59
|
+
const receiptPath = "/rool-drive/receipts/unsorted/receipt.pdf";
|
|
60
|
+
await machine.files.write(receiptPath, await readFile("./receipt.pdf"), {
|
|
206
61
|
contentType: "application/pdf",
|
|
207
|
-
|
|
208
|
-
onUploadProgress: ({ transferredBytes, totalBytes }) => {
|
|
209
|
-
if (totalBytes) renderUploadProgress(transferredBytes / totalBytes);
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
await files.write(
|
|
213
|
-
"/rool-drive/documents/archive.bin",
|
|
214
|
-
() => createArchiveStream(),
|
|
215
|
-
{ contentType: "application/octet-stream" },
|
|
216
|
-
);
|
|
217
|
-
const info = await files.stat(path);
|
|
218
|
-
const documents = await files.list("/rool-drive/documents");
|
|
219
|
-
const response = await files.read(path, {
|
|
220
|
-
range: { start: 0, end: 1023 },
|
|
221
|
-
ifMatch: info.etag,
|
|
62
|
+
createParents: true,
|
|
222
63
|
});
|
|
223
|
-
const hydrated = await files.readMultiple([
|
|
224
|
-
"/space/.meta.json",
|
|
225
|
-
"/rool-drive/documents/notes.json",
|
|
226
|
-
]);
|
|
227
|
-
await files.copy(path, "/rool-drive/documents/report-backup.pdf", {
|
|
228
|
-
overwrite: false,
|
|
229
|
-
ifMatch: written.etag,
|
|
230
|
-
});
|
|
231
|
-
await files.move(path, "/rool-drive/documents/final-report.pdf");
|
|
232
|
-
const deleted = await files.deleteMultiple([
|
|
233
|
-
"/rool-drive/documents/final-report.pdf",
|
|
234
|
-
{ path: "/rool-drive/documents/notes.json", ifMatch: '"notes-etag"' },
|
|
235
|
-
]);
|
|
236
|
-
for (const result of deleted) {
|
|
237
|
-
if (!result.ok)
|
|
238
|
-
console.error(`Failed to delete ${result.path}`, result.error);
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
Paths are absolute machine paths under `/space` or `/rool-drive`. `list()` without a path enumerates those storage roots; pass `{ recursive: true }` to enumerate a complete subtree. File and directory metadata has a discriminating `kind` field. Reads return the native `Response` so callers can stream the body. `readMultiple()` hydrates ordered small files in one request and returns an `ok` result with binary-safe bytes and validators, or a per-file HTTP failure. A batch accepts at most 128 paths, 2 MiB per successful file, and 16 MiB across successful files. Writes accept any `BodyInit`, including `Blob` and `ReadableStream`, and return the same complete file metadata as `stat()` and `list()`. Pass a function that creates a fresh `ReadableStream` when an upload must be replayable after a machine route change; a directly passed stream remains one-shot. `onUploadProgress` reports transferred bytes and includes the total size when it is known; successful completion is confirmed by the `write()` promise. Copy and move operations overwrite by default; pass `{ overwrite: false }` for create-only behavior.
|
|
243
|
-
|
|
244
|
-
`deleteMultiple()` sends independent DAV requests with at most eight in flight. It accepts plain paths and targets carrying their own HTTP preconditions, and returns one ordered success or failure result per target. The requests are not atomic. A directory target recursively deletes its contents, so callers should omit redundant descendants; duplicate and overlapping targets otherwise remain independent and can race.
|
|
245
|
-
|
|
246
|
-
Every file and directory has protected `access` metadata. `currentUser` says whether the requesting user can read or write it. `readableBy` and `writableBy` describe its filesystem audiences as `resource-owner`, `machine-admins`, `machine-editors`, and `machine-members`. For a file, write means changing its contents. For a directory, write means adding, removing, or renaming entries. Members without read access to a directory do not receive that directory through listing, direct lookup, or synchronization.
|
|
247
|
-
|
|
248
|
-
`getStorageUsage()` reports the used and writable bytes on the machine's complete live persistent filesystem, including public files and private runtime or system state. It excludes checkpoint history and the ephemeral operating-system overlay.
|
|
249
|
-
|
|
250
|
-
Watch the files when an application needs a live machine file tree:
|
|
251
64
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const unsubscribe = files.tree.subscribe(({ reset, changed, deleted }) => {
|
|
256
|
-
renderFileChanges({ reset, changed, deleted });
|
|
65
|
+
await conversation.prompt("File this receipt.", {
|
|
66
|
+
attachments: [receiptPath],
|
|
257
67
|
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
68
|
+
await conversation.follow({
|
|
69
|
+
onEvent(event) {
|
|
70
|
+
if (event.type === "output.delta" && event.content.type === "text") {
|
|
71
|
+
process.stdout.write(event.content.text);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
263
74
|
});
|
|
264
|
-
|
|
265
|
-
unsubscribe();
|
|
266
|
-
files.unwatch();
|
|
267
75
|
```
|
|
268
76
|
|
|
269
|
-
`
|
|
77
|
+
Prompting is asynchronous: `prompt()` returns when Rool accepts the work, while the agent keeps running on the machine. An app can follow the current run, leave, and reconnect later. UI clients can instead watch a conversation and receive a current view as its saved turns and live output change.
|
|
270
78
|
|
|
271
|
-
##
|
|
79
|
+
## Give agents data your app understands
|
|
272
80
|
|
|
273
|
-
|
|
81
|
+
A machine can hold records as well as documents. A collection defines the fields in each record, and Rool rejects writes that do not match. For example, an app can keep an index alongside the receipt files:
|
|
274
82
|
|
|
275
83
|
```typescript
|
|
276
|
-
await machine.
|
|
277
|
-
|
|
278
|
-
const task = await machine.collections.create("task", {
|
|
84
|
+
await machine.collections.create("receipts", {
|
|
279
85
|
fields: [
|
|
280
|
-
{ name: "
|
|
281
|
-
{ name: "
|
|
86
|
+
{ name: "vendor", type: { kind: "string" } },
|
|
87
|
+
{ name: "amount", type: { kind: "number" } },
|
|
88
|
+
{ name: "currency", type: { kind: "string" } },
|
|
89
|
+
{ name: "document", type: { kind: "string" } },
|
|
282
90
|
],
|
|
283
91
|
});
|
|
284
92
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
93
|
+
await machine.objects.create("/space/receipts/cafe.json", {
|
|
94
|
+
vendor: "Cafe",
|
|
95
|
+
amount: 12,
|
|
96
|
+
currency: "EUR",
|
|
97
|
+
document: receiptPath,
|
|
288
98
|
});
|
|
289
|
-
const objectPaths = machine.objects.list({ collection: "task" });
|
|
290
|
-
const object = await machine.objects.get(first.path);
|
|
291
|
-
const [sameObject, missing] = await machine.objects.getMultiple([
|
|
292
|
-
first.path,
|
|
293
|
-
"/space/task/missing.json",
|
|
294
|
-
]);
|
|
295
|
-
|
|
296
|
-
await machine.objects.patch(first.path, { done: true });
|
|
297
|
-
await machine.objects.move(first.path, "/space/task/renamed.json");
|
|
298
|
-
const [removal] = await machine.objects.removeMultiple([
|
|
299
|
-
"/space/task/renamed.json",
|
|
300
|
-
]);
|
|
301
|
-
if (!removal) throw new Error("Object removal returned no result");
|
|
302
|
-
if (!removal.ok) throw removal.error;
|
|
303
|
-
await machine.collections.remove(task.name);
|
|
304
99
|
```
|
|
305
100
|
|
|
306
|
-
|
|
101
|
+
The record is also a normal JSON file at `/space/receipts/cafe.json`. The app can work with it through `machine.objects`, while agents and programs inside the machine can use normal file tools. Changes from either side enter the same watched file tree.
|
|
307
102
|
|
|
308
|
-
|
|
103
|
+
Collections can power task lists, catalogues, contact records, or a memory view with links between people, projects, and notes. The app gets predictable fields for its UI without hiding the data from the agent.
|
|
309
104
|
|
|
310
|
-
|
|
105
|
+
## What the SDK handles
|
|
311
106
|
|
|
312
|
-
|
|
107
|
+
- **Machines:** create, configure, duplicate, checkpoint, share, and delete persistent VMs.
|
|
108
|
+
- **Files:** upload, stream, move, and watch files under `/space` and `/rool-drive`.
|
|
109
|
+
- **Agents:** define agents, keep conversation history, attach machine files, stream runs, and request structured output.
|
|
110
|
+
- **Live state:** learn when account data changes, keep a file tree current, and stream conversation updates.
|
|
111
|
+
- **Shared app data:** define records that apps, agents, and programs inside the machine can all read and edit.
|
|
313
112
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
const defaultAgent = await machine.agents.get("rool");
|
|
318
|
-
if (!defaultAgent) throw new Error("Rool agent is unavailable");
|
|
319
|
-
|
|
320
|
-
const conversation = defaultAgent.conversation("research-chat");
|
|
321
|
-
const stopWatching = conversation.watch((view) => {
|
|
322
|
-
renderConversation({
|
|
323
|
-
turns: view.turns,
|
|
324
|
-
output: view.output,
|
|
325
|
-
isRunning: view.isRunning,
|
|
326
|
-
loading: view.loading,
|
|
327
|
-
error: view.error,
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
await conversation.prompt("Explain the result.", { effort: "reasoning" });
|
|
332
|
-
|
|
333
|
-
// When this conversation leaves the UI:
|
|
334
|
-
stopWatching();
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
`prompt()` starts the conversation's current run and resolves once the server accepts it. The agent runs as a detached job. A conversation can only have one current run; call `cancel()` and wait for `follow()` to finish before prompting again. The `readOnly` option is accepted for compatibility with legacy prompting but currently has no effect.
|
|
338
|
-
|
|
339
|
-
`watch()` is the normal UI API. It fetches only turns after the last turn it has seen, follows the current run, and refreshes the durable turns when that stream ends. `turns` contains saved history through the current user message while a run is active; `output` contains that run's replayed and live output. The first listener starts the work and removing the last listener stops it. Saved turns remain cached on the conversation handle for the next listener.
|
|
340
|
-
|
|
341
|
-
`follow()` is the lower-level streaming API. It performs one `GET` of the conversation's current run. It receives the complete current-run output and then continues with new events until that response ends. Tool calls and their results arrive as `output.delta` events with matching IDs. A tool result contains nested content parts and an optional `error` flag. `follow()` returns `false` when there is no current run. Aborting `follow()` only stops that request; call `cancel()` to stop the detached job.
|
|
342
|
-
|
|
343
|
-
The SDK uses `conversation_changed` account events to wake active watchers. A watcher also refreshes after prompting, cancellation, stream completion, connection failure, and account event-token replacement.
|
|
344
|
-
|
|
345
|
-
Prompt attachments are existing `/space` or `/rool-drive` paths. Pass a durable user turn's `id` as `replaceTurnId` to replace that message and everything after it. This supports edits and rerolls, including the first message. A replacement gets a new user turn ID; use that ID to edit it again.
|
|
346
|
-
|
|
347
|
-
Pass a JSON Schema as `responseSchema` to request structured output. Tools are skipped for that run. The successful assistant turn contains one JSON content part; the value is JSON directly, not a JSON string.
|
|
348
|
-
|
|
349
|
-
```typescript
|
|
350
|
-
await conversation.prompt("Return the number of records.", {
|
|
351
|
-
responseSchema: {
|
|
352
|
-
type: "object",
|
|
353
|
-
properties: { count: { type: "integer" } },
|
|
354
|
-
required: ["count"],
|
|
355
|
-
additionalProperties: false,
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
await conversation.follow();
|
|
359
|
-
|
|
360
|
-
const turns = await conversation.listTurns();
|
|
361
|
-
const part = turns.at(-1)?.content[0];
|
|
362
|
-
if (part?.type !== "json") throw new Error("No structured result");
|
|
363
|
-
console.log(part.value); // { count: ... }
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
Custom agent definitions currently contain one plain system prompt. The server owns the executable agent implementation.
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
const researcher = await machine.agents.create("researcher", {
|
|
370
|
-
system: "Investigate carefully and distinguish facts from uncertainty.",
|
|
371
|
-
});
|
|
372
|
-
const customConversation = await researcher.createConversation({
|
|
373
|
-
name: "Climate report",
|
|
374
|
-
visibility: "private",
|
|
375
|
-
});
|
|
376
|
-
await customConversation.prompt("Investigate this claim.");
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
Agents expose `replace()` and `delete()`. Conversations expose metadata replacement, listing, durable turn reads, rename, and deletion. Listed and fetched conversation metadata includes server-managed ISO 8601 `createdAt` and `updatedAt` timestamps plus `isRunning`, which can drive a running indicator without opening every run stream. Visibility defaults to private. The built-in `rool` agent cannot be replaced or deleted.
|
|
380
|
-
|
|
381
|
-
## Members and invites
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
const invite = await machine.invites.create({
|
|
385
|
-
role: "editor",
|
|
386
|
-
maxUses: 1,
|
|
387
|
-
});
|
|
388
|
-
const token = invite.url.split("/").at(-1)!;
|
|
389
|
-
|
|
390
|
-
await client.getInvitePreview(token);
|
|
391
|
-
await client.redeemInvite(token);
|
|
392
|
-
|
|
393
|
-
const members = await machine.members.list();
|
|
394
|
-
console.log(members[1].name ?? members[1].email);
|
|
395
|
-
await machine.members.replaceRole(members[1].userId, { role: "viewer" });
|
|
396
|
-
await machine.members.remove(members[1].userId);
|
|
397
|
-
await machine.invites.revoke(invite.id);
|
|
398
|
-
```
|
|
113
|
+
Long-running work belongs to the machine rather than the client connection. File changes made by the app, a user, or an agent enter the same synchronized file tree, while account events tell the app when to fetch fresh account or machine data.
|
|
399
114
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
## Gifts
|
|
403
|
-
|
|
404
|
-
A gift carries something of value in a short code. Users receive gifts from Rool and give the codes away themselves. Claiming a gift is single-use, and claiming your own gift is allowed.
|
|
405
|
-
|
|
406
|
-
```typescript
|
|
407
|
-
const { gifts } = await client.listGifts();
|
|
408
|
-
for (const gift of gifts) {
|
|
409
|
-
console.log(gift.code, gift.url, gift.description, gift.claimedAt);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const preview = await client.previewGift(code); // no auth required
|
|
413
|
-
console.log(
|
|
414
|
-
`${preview.holderName ?? "Someone"} gave you ${preview.description}`,
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
const { gift } = await client.claimGift(code);
|
|
418
|
-
if (gift.kind === "credits") console.log(`+${gift.credits} credits`);
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
Codes are case-insensitive and the dash is optional. Prefer the server-rendered `description` for display. Narrow `gift.kind` when using the structured payload because new gift kinds may be added.
|
|
422
|
-
|
|
423
|
-
A holder can add a note, archive a gift, or replace an unclaimed gift's code. These actions do not change what the gift grants. Archiving only hides it from the holder's normal view.
|
|
424
|
-
|
|
425
|
-
```typescript
|
|
426
|
-
await client.updateGift(giftId, { note: "sent to Peter" });
|
|
427
|
-
await client.updateGift(giftId, { archived: true });
|
|
428
|
-
await client.updateGift(giftId, { note: null });
|
|
429
|
-
const updated = await client.rotateGiftCode(giftId);
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
Gift failures are `RoolProblem` errors. `gift_invalid` means the code or gift is unavailable to the caller. `gift_claimed` means it was already claimed.
|
|
433
|
-
|
|
434
|
-
## API problems
|
|
435
|
-
|
|
436
|
-
Each problem `type` links to its entry below.
|
|
437
|
-
|
|
438
|
-
<a id="problem-authentication_required"></a>
|
|
439
|
-
|
|
440
|
-
### `authentication_required`
|
|
441
|
-
|
|
442
|
-
**Documentation placeholder.**
|
|
443
|
-
|
|
444
|
-
<a id="problem-invalid_authentication"></a>
|
|
445
|
-
|
|
446
|
-
### `invalid_authentication`
|
|
447
|
-
|
|
448
|
-
**Documentation placeholder.**
|
|
449
|
-
|
|
450
|
-
<a id="problem-email_unverified"></a>
|
|
451
|
-
|
|
452
|
-
### `email_unverified`
|
|
453
|
-
|
|
454
|
-
**Documentation placeholder.**
|
|
455
|
-
|
|
456
|
-
<a id="problem-account_suspended"></a>
|
|
457
|
-
|
|
458
|
-
### `account_suspended`
|
|
459
|
-
|
|
460
|
-
**Documentation placeholder.**
|
|
461
|
-
|
|
462
|
-
<a id="problem-invalid_profile"></a>
|
|
463
|
-
|
|
464
|
-
### `invalid_profile`
|
|
465
|
-
|
|
466
|
-
**Documentation placeholder.**
|
|
467
|
-
|
|
468
|
-
<a id="problem-invalid_user_app_data"></a>
|
|
469
|
-
|
|
470
|
-
### `invalid_user_app_data`
|
|
471
|
-
|
|
472
|
-
**Documentation placeholder.**
|
|
473
|
-
|
|
474
|
-
<a id="problem-invalid_json"></a>
|
|
475
|
-
|
|
476
|
-
### `invalid_json`
|
|
477
|
-
|
|
478
|
-
**Documentation placeholder.**
|
|
479
|
-
|
|
480
|
-
<a id="problem-payload_too_large"></a>
|
|
481
|
-
|
|
482
|
-
### `payload_too_large`
|
|
483
|
-
|
|
484
|
-
**Documentation placeholder.**
|
|
485
|
-
|
|
486
|
-
<a id="problem-user_app_data_too_large"></a>
|
|
487
|
-
|
|
488
|
-
### `user_app_data_too_large`
|
|
489
|
-
|
|
490
|
-
**Documentation placeholder.**
|
|
491
|
-
|
|
492
|
-
<a id="problem-not_found"></a>
|
|
493
|
-
|
|
494
|
-
### `not_found`
|
|
495
|
-
|
|
496
|
-
**Documentation placeholder.**
|
|
497
|
-
|
|
498
|
-
<a id="problem-checkpoint_not_found"></a>
|
|
499
|
-
|
|
500
|
-
### `checkpoint_not_found`
|
|
501
|
-
|
|
502
|
-
**Documentation placeholder.**
|
|
503
|
-
|
|
504
|
-
<a id="problem-sync_token_required"></a>
|
|
505
|
-
|
|
506
|
-
### `sync_token_required`
|
|
507
|
-
|
|
508
|
-
The account event route requires the sync token returned by `/v2/session`.
|
|
509
|
-
|
|
510
|
-
<a id="problem-invalid_sync_token"></a>
|
|
511
|
-
|
|
512
|
-
### `invalid_sync_token`
|
|
513
|
-
|
|
514
|
-
The account event history no longer contains everything after this token. Fetch `/v2/session` and continue with its new token.
|
|
515
|
-
|
|
516
|
-
<a id="problem-invalid_wait_preference"></a>
|
|
517
|
-
|
|
518
|
-
### `invalid_wait_preference`
|
|
519
|
-
|
|
520
|
-
The account event wait must be an integer from 0 through 50 seconds.
|
|
521
|
-
|
|
522
|
-
<a id="problem-internal_error"></a>
|
|
523
|
-
|
|
524
|
-
### `internal_error`
|
|
525
|
-
|
|
526
|
-
**Documentation placeholder.**
|
|
527
|
-
|
|
528
|
-
<a id="problem-server_misconfigured"></a>
|
|
529
|
-
|
|
530
|
-
### `server_misconfigured`
|
|
531
|
-
|
|
532
|
-
**Documentation placeholder.**
|
|
533
|
-
|
|
534
|
-
<a id="problem-current_run_exists"></a>
|
|
535
|
-
|
|
536
|
-
### `current_run_exists`
|
|
537
|
-
|
|
538
|
-
The conversation is already running. Cancel or follow that run before prompting again. This also applies to prompts with `replaceTurnId`.
|
|
539
|
-
|
|
540
|
-
<a id="problem-replace_turn_not_found"></a>
|
|
541
|
-
|
|
542
|
-
### `replace_turn_not_found`
|
|
543
|
-
|
|
544
|
-
The user turn supplied as `replaceTurnId` is no longer in the conversation. Fetch the turns again before retrying the edit.
|
|
545
|
-
|
|
546
|
-
<a id="problem-invalid_member_role"></a>
|
|
547
|
-
|
|
548
|
-
### `invalid_member_role`
|
|
549
|
-
|
|
550
|
-
**Documentation placeholder.**
|
|
551
|
-
|
|
552
|
-
<a id="problem-role_not_replaceable"></a>
|
|
553
|
-
|
|
554
|
-
### `role_not_replaceable`
|
|
555
|
-
|
|
556
|
-
**Documentation placeholder.**
|
|
557
|
-
|
|
558
|
-
<a id="problem-membership_not_removable"></a>
|
|
559
|
-
|
|
560
|
-
### `membership_not_removable`
|
|
561
|
-
|
|
562
|
-
**Documentation placeholder.**
|
|
563
|
-
|
|
564
|
-
<a id="problem-invalid_invite"></a>
|
|
565
|
-
|
|
566
|
-
### `invalid_invite`
|
|
567
|
-
|
|
568
|
-
**Documentation placeholder.**
|
|
569
|
-
|
|
570
|
-
<a id="problem-invite_invalid"></a>
|
|
571
|
-
|
|
572
|
-
### `invite_invalid`
|
|
573
|
-
|
|
574
|
-
**Documentation placeholder.**
|
|
575
|
-
|
|
576
|
-
<a id="problem-invite_expired"></a>
|
|
577
|
-
|
|
578
|
-
### `invite_expired`
|
|
579
|
-
|
|
580
|
-
**Documentation placeholder.**
|
|
581
|
-
|
|
582
|
-
<a id="problem-invite_revoked"></a>
|
|
583
|
-
|
|
584
|
-
### `invite_revoked`
|
|
585
|
-
|
|
586
|
-
**Documentation placeholder.**
|
|
587
|
-
|
|
588
|
-
<a id="problem-invite_exhausted"></a>
|
|
589
|
-
|
|
590
|
-
### `invite_exhausted`
|
|
591
|
-
|
|
592
|
-
**Documentation placeholder.**
|
|
593
|
-
|
|
594
|
-
<a id="problem-invite_email_mismatch"></a>
|
|
595
|
-
|
|
596
|
-
### `invite_email_mismatch`
|
|
597
|
-
|
|
598
|
-
**Documentation placeholder.**
|
|
599
|
-
|
|
600
|
-
<a id="problem-gift_invalid"></a>
|
|
601
|
-
|
|
602
|
-
### `gift_invalid`
|
|
603
|
-
|
|
604
|
-
The code is not valid, or the requested gift does not belong to the current user.
|
|
605
|
-
|
|
606
|
-
<a id="problem-gift_claimed"></a>
|
|
607
|
-
|
|
608
|
-
### `gift_claimed`
|
|
609
|
-
|
|
610
|
-
The gift has already been claimed. A claimed gift cannot be claimed again or given a new code.
|
|
611
|
-
|
|
612
|
-
<a id="problem-invalid_input"></a>
|
|
613
|
-
|
|
614
|
-
### `invalid_input`
|
|
615
|
-
|
|
616
|
-
The gift update is empty or contains an invalid note or archived value.
|
|
617
|
-
|
|
618
|
-
## Development
|
|
619
|
-
|
|
620
|
-
```bash
|
|
621
|
-
pnpm build
|
|
622
|
-
pnpm typecheck
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
The user-route smoke test must target a dedicated non-production account. It replaces profile and app data, then schedules and cancels account deletion. The member-route smoke test requires a second non-production account to exercise invite redemption and membership changes. The local gift fixture accounts are deleted after the gift smoke test.
|
|
626
|
-
|
|
627
|
-
Copy `.env.example` to the ignored `.env` file and configure the local endpoints and expected primary account ID.
|
|
628
|
-
|
|
629
|
-
| Variable | Purpose |
|
|
630
|
-
| ------------------------------- | ---------------------------------------------------------------------- |
|
|
631
|
-
| `ROOL_TEST_API_URL` | API origin. HTTPS is required except for loopback development servers. |
|
|
632
|
-
| `ROOL_TEST_AUTH_URL` | Auth endpoint override required for a loopback API. |
|
|
633
|
-
| `ROOL_TEST_ROUTER_URL` | Local machine-router origin. |
|
|
634
|
-
| `ROOL_TEST_USER_ID` | Expected primary account ID, preventing mutation of the wrong account. |
|
|
635
|
-
| `ROOL_TEST_GIFT_HOLDER_EMAIL` | Local fixture account that holds a gift. |
|
|
636
|
-
| `ROOL_TEST_GIFT_CLAIMANT_EMAIL` | Local fixture account that claims the gift. |
|
|
637
|
-
| `ROOL_TEST_INTERNAL_SECRET` | Local rool-server secret used to create and remove fixtures. |
|
|
638
|
-
|
|
639
|
-
Log the `sdk-v2-primary` and `sdk-v2-member` Node profiles into two dedicated accounts:
|
|
640
|
-
|
|
641
|
-
```bash
|
|
642
|
-
node --env-file=.env --import tsx test/integration/v2/login.ts
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
Then run the complete v2 smoke-test suite:
|
|
646
|
-
|
|
647
|
-
```bash
|
|
648
|
-
pnpm test:v2
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
To run one smoke test manually, invoke its script directly, for example:
|
|
652
|
-
|
|
653
|
-
```bash
|
|
654
|
-
node --env-file=.env --import tsx test/integration/v2/machine-routes.test.ts
|
|
655
|
-
```
|
|
115
|
+
The package exports its public TypeScript types, so editor autocomplete shows the detailed options and results. See [docs.rool.dev](https://docs.rool.dev/) for the published documentation.
|
|
656
116
|
|
|
657
117
|
## License
|
|
658
118
|
|
|
659
|
-
MIT — see [LICENSE](
|
|
119
|
+
MIT — see [LICENSE](./LICENSE).
|