@recursiv/sdk 0.7.82 → 0.7.83
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 +44 -1
- package/dist/assistant/fixedCardData.d.ts +91 -0
- package/dist/assistant/fixedCardData.js +372 -0
- package/dist/assistant/fixedCards.d.ts +35 -0
- package/dist/assistant/fixedCards.js +904 -0
- package/dist/assistant/hiddenText.d.ts +24 -0
- package/dist/assistant/hiddenText.js +263 -0
- package/dist/assistant/index.d.ts +22 -0
- package/dist/assistant/index.js +22 -0
- package/dist/assistant/render.d.ts +8 -0
- package/dist/assistant/render.js +253 -0
- package/dist/assistant/subjectBundle.d.ts +30 -0
- package/dist/assistant/subjectBundle.js +518 -0
- package/dist/assistant/types.d.ts +353 -0
- package/dist/assistant/types.js +11 -0
- package/dist/assistant-surfaces.d.ts +42 -0
- package/dist/assistant-surfaces.js +113 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +1 -0
- package/dist/realtime.d.ts +10 -3
- package/dist/realtime.js +11 -2
- package/dist/resources/agents.d.ts +6 -1
- package/dist/resources/agents.js +12 -2
- package/dist/resources/e2ee.d.ts +56 -0
- package/dist/resources/e2ee.js +21 -0
- package/dist/resources/link-preview.d.ts +42 -1
- package/dist/resources/link-preview.js +36 -2
- package/dist/resources/network.d.ts +6 -0
- package/dist/resources/project-brain.d.ts +8 -0
- package/dist/resources/users.d.ts +11 -2
- package/dist/resources/users.js +1 -1
- package/dist/types.d.ts +34 -0
- package/llm.md +32 -2
- package/package.json +6 -1
package/dist/resources/agents.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RecursivError } from '../errors.js';
|
|
1
|
+
import { RateLimitError, rateLimitDetailsFromResponse, RecursivError } from '../errors.js';
|
|
2
2
|
import { parseSSE } from '../lib/sse.js';
|
|
3
3
|
// ── Agent directory (GET /agents/discover) ──
|
|
4
4
|
/** The orders the agent directory serves (server: features/agents/agentDirectory.ts). */
|
|
@@ -83,7 +83,10 @@ export class AgentsResource {
|
|
|
83
83
|
delete(id) {
|
|
84
84
|
return this.client.delete(`/agents/${id}`);
|
|
85
85
|
}
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Send a message to an agent (async — agent responds asynchronously).
|
|
88
|
+
* `turn: { safe: true }` makes it a safe turn in the caller's Asks thread (see `AgentChatTurn`).
|
|
89
|
+
*/
|
|
87
90
|
chat(id, input) {
|
|
88
91
|
return this.client.post(`/agents/${id}/chat`, input);
|
|
89
92
|
}
|
|
@@ -224,6 +227,8 @@ export class AgentsResource {
|
|
|
224
227
|
*
|
|
225
228
|
* Requires `ReadableStream` support (Node.js, modern browsers).
|
|
226
229
|
* For React Native, use `chatStreamText()` instead.
|
|
230
|
+
* `turn: { safe: true }` streams a safe turn in the caller's Asks thread (see `AgentChatTurn`); a refusal throws a
|
|
231
|
+
* `RecursivError` whose code is a `SafeTurnRefusalCode`, before anything streams.
|
|
227
232
|
*/
|
|
228
233
|
async *chatStream(id, input) {
|
|
229
234
|
yield* this.streamChunks(id, input);
|
|
@@ -286,6 +291,11 @@ export class AgentsResource {
|
|
|
286
291
|
code: 'stream_error',
|
|
287
292
|
});
|
|
288
293
|
}
|
|
294
|
+
// A 429 is a RateLimitError, as on every other request: with its scope and reset (a safe turn's daily cap is
|
|
295
|
+
// `scope: 'action'`, see SafeTurnRefusalCode).
|
|
296
|
+
if (response.status === 429 && errorBody?.error && typeof errorBody.error === 'object') {
|
|
297
|
+
throw new RateLimitError(errorBody.error, rateLimitDetailsFromResponse(response.headers, errorBody.error));
|
|
298
|
+
}
|
|
289
299
|
throw new RecursivError(response.status, errorBody.error);
|
|
290
300
|
}
|
|
291
301
|
return response;
|
package/dist/resources/e2ee.d.ts
CHANGED
|
@@ -68,6 +68,38 @@ export interface E2eeAccountReset {
|
|
|
68
68
|
* - `conflict` (409): the device name or key was used before; start the device again with fresh ones.
|
|
69
69
|
*/
|
|
70
70
|
export type E2eeResetErrorCode = 'confirmation_failed' | 'code_limit' | 'no_email_on_file' | 'email_delivery_unavailable' | 'reset_required' | 'conflict';
|
|
71
|
+
/**
|
|
72
|
+
* One account whose main device a reset of encrypted chat replaced (`peerChanges`): `at` is when the new main
|
|
73
|
+
* device was registered (the relay's clock). Look at its devices again (`devices({ user_id })`).
|
|
74
|
+
*/
|
|
75
|
+
export interface E2eePeerChange {
|
|
76
|
+
userId: string;
|
|
77
|
+
at: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The realtime `e2ee:peer` event (`r.realtime.onE2eePeer`), sent to each member of an encrypted conversation the
|
|
81
|
+
* account `userId` shares with them, once per such conversation, when that account reset encrypted chat
|
|
82
|
+
* (`reason: 'root_changed'`): its security code changed there. Only to sockets whose credential can read that
|
|
83
|
+
* conversation over the relay. A hint: look at the account's devices again. An app in the background catches up
|
|
84
|
+
* with `peerChanges` when it comes to the foreground.
|
|
85
|
+
*/
|
|
86
|
+
export interface E2eePeerRealtimeEvent {
|
|
87
|
+
userId: string;
|
|
88
|
+
reason: 'root_changed';
|
|
89
|
+
conversationId: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Codes a message that declares `trust_change` can be refused with in a direct chat whose other person reset
|
|
93
|
+
* encrypted chat, where `status().accountReset` is on (409 `ConflictError` for both):
|
|
94
|
+
* - `peer_not_joined`: no device of this account has added the other person's new device to the chat yet, so the
|
|
95
|
+
* message would be unreadable on it. `details: { userId }` names the person. Add the device, then send again.
|
|
96
|
+
* - `stale_epoch`: encrypted under a key generation before the chat's last key update. Sync, then encrypt it again.
|
|
97
|
+
*/
|
|
98
|
+
export type E2eePeerErrorCode = 'peer_not_joined' | 'stale_epoch';
|
|
99
|
+
/** The `details` of a `peer_not_joined` refusal. */
|
|
100
|
+
export interface E2eePeerNotJoinedDetails {
|
|
101
|
+
userId: string;
|
|
102
|
+
}
|
|
71
103
|
/** Opaque `recursiv-mls-v2-draft1` envelope with base64 MLS bytes. */
|
|
72
104
|
export interface E2eeEnvelope {
|
|
73
105
|
profile: 'recursiv-mls-v2-draft1';
|
|
@@ -367,6 +399,15 @@ export declare class E2eeResource {
|
|
|
367
399
|
* delete. A commit ignores the flag. The relay learns only that the event
|
|
368
400
|
* is quiet; the flag is not stored. A relay that predates it drops the
|
|
369
401
|
* key, so the event is simply not quiet there.
|
|
402
|
+
*
|
|
403
|
+
* `trust_change: true` says this device's engine can accept a contact's new
|
|
404
|
+
* main device after they reset encrypted chat. Where `status().accountReset`
|
|
405
|
+
* is on, such a message in a direct chat whose other person reset is refused
|
|
406
|
+
* 409 `peer_not_joined` (`details: { userId }`) until a device of this
|
|
407
|
+
* account added their new device, and 409 `stale_epoch` when encrypted under
|
|
408
|
+
* a key generation before the chat's last key update (E2eePeerErrorCode). A
|
|
409
|
+
* message without it is accepted as before. Never stored; relays without it
|
|
410
|
+
* ignore it.
|
|
370
411
|
*/
|
|
371
412
|
appendEvent(conversationId: string, input: {
|
|
372
413
|
device_id: string;
|
|
@@ -375,11 +416,26 @@ export declare class E2eeResource {
|
|
|
375
416
|
welcome_for_device_id?: string;
|
|
376
417
|
expected_commit_seq?: number;
|
|
377
418
|
silent?: boolean;
|
|
419
|
+
trust_change?: boolean;
|
|
378
420
|
}): Promise<SingleResponse<{
|
|
379
421
|
seq: number;
|
|
380
422
|
duplicate: boolean;
|
|
381
423
|
commitSeq: number;
|
|
382
424
|
}>>;
|
|
425
|
+
/**
|
|
426
|
+
* The accounts sharing an encrypted conversation with this account (a direct
|
|
427
|
+
* chat, or a group both accepted) whose main device a reset of encrypted chat
|
|
428
|
+
* replaced after `since` (ISO 8601; default and floor: 7 days ago), oldest
|
|
429
|
+
* first, at most 200 (ask again from the last `at` for more). Read it when the
|
|
430
|
+
* app comes to the foreground, then look at each account's devices again; the
|
|
431
|
+
* live form is `r.realtime.onE2eePeer`. Empty where `status().accountReset` is
|
|
432
|
+
* off. Relays without it answer 404 (`NotFoundError`): treat that as no changes.
|
|
433
|
+
*/
|
|
434
|
+
peerChanges(params?: {
|
|
435
|
+
since?: string;
|
|
436
|
+
}): Promise<{
|
|
437
|
+
data: E2eePeerChange[];
|
|
438
|
+
}>;
|
|
383
439
|
events(conversationId: string, params: {
|
|
384
440
|
device_id: string;
|
|
385
441
|
after_seq?: number;
|
package/dist/resources/e2ee.js
CHANGED
|
@@ -96,11 +96,32 @@ export class E2eeResource {
|
|
|
96
96
|
* delete. A commit ignores the flag. The relay learns only that the event
|
|
97
97
|
* is quiet; the flag is not stored. A relay that predates it drops the
|
|
98
98
|
* key, so the event is simply not quiet there.
|
|
99
|
+
*
|
|
100
|
+
* `trust_change: true` says this device's engine can accept a contact's new
|
|
101
|
+
* main device after they reset encrypted chat. Where `status().accountReset`
|
|
102
|
+
* is on, such a message in a direct chat whose other person reset is refused
|
|
103
|
+
* 409 `peer_not_joined` (`details: { userId }`) until a device of this
|
|
104
|
+
* account added their new device, and 409 `stale_epoch` when encrypted under
|
|
105
|
+
* a key generation before the chat's last key update (E2eePeerErrorCode). A
|
|
106
|
+
* message without it is accepted as before. Never stored; relays without it
|
|
107
|
+
* ignore it.
|
|
99
108
|
*/
|
|
100
109
|
appendEvent(conversationId, input) {
|
|
101
110
|
// Retries are safe: the server is idempotent on (conversation, device, event id).
|
|
102
111
|
return this.client.post(`/e2ee/conversations/${encodeURIComponent(conversationId)}/events`, input, { retry: true });
|
|
103
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* The accounts sharing an encrypted conversation with this account (a direct
|
|
115
|
+
* chat, or a group both accepted) whose main device a reset of encrypted chat
|
|
116
|
+
* replaced after `since` (ISO 8601; default and floor: 7 days ago), oldest
|
|
117
|
+
* first, at most 200 (ask again from the last `at` for more). Read it when the
|
|
118
|
+
* app comes to the foreground, then look at each account's devices again; the
|
|
119
|
+
* live form is `r.realtime.onE2eePeer`. Empty where `status().accountReset` is
|
|
120
|
+
* off. Relays without it answer 404 (`NotFoundError`): treat that as no changes.
|
|
121
|
+
*/
|
|
122
|
+
peerChanges(params) {
|
|
123
|
+
return this.client.get('/e2ee/peer-changes', params);
|
|
124
|
+
}
|
|
104
125
|
events(conversationId, params) {
|
|
105
126
|
return this.client.get(`/e2ee/conversations/${encodeURIComponent(conversationId)}/events`, params);
|
|
106
127
|
}
|
|
@@ -17,6 +17,30 @@ export interface LinkPreviewResponse {
|
|
|
17
17
|
*/
|
|
18
18
|
data: LinkPreview | null;
|
|
19
19
|
cached?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* `'ai_link'`: the link is not in the named message in a person's own words,
|
|
22
|
+
* and the caller did not write the same address in that conversation, so the
|
|
23
|
+
* server did not fetch it (it may be an AI's). Show the plain link, and do not
|
|
24
|
+
* ask again for this message. Only on a request that named its message or
|
|
25
|
+
* conversation (`source`).
|
|
26
|
+
*/
|
|
27
|
+
refused?: 'ai_link';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Where a chat link came from. Name the message when you have its id; the
|
|
31
|
+
* conversation alone when you do not have it yet (an AI reply still being
|
|
32
|
+
* saved). With the conversation alone, a link is previewed only when the caller
|
|
33
|
+
* wrote it there, so another member's message not yet saved waits for its id.
|
|
34
|
+
* At least one of the two must be a non-empty id: `get` rejects otherwise and
|
|
35
|
+
* never falls back to a request without a source.
|
|
36
|
+
*
|
|
37
|
+
* Not for an encrypted conversation: the server has no readable messages there,
|
|
38
|
+
* so every link would be refused, the member's own included. There the app
|
|
39
|
+
* decides by its own rule (no preview of a link the member did not write).
|
|
40
|
+
*/
|
|
41
|
+
export interface LinkPreviewSource {
|
|
42
|
+
messageId?: string;
|
|
43
|
+
conversationId?: string;
|
|
20
44
|
}
|
|
21
45
|
export declare class LinkPreviewResource {
|
|
22
46
|
private client;
|
|
@@ -27,6 +51,23 @@ export declare class LinkPreviewResource {
|
|
|
27
51
|
* preview (`data: null`). No auth required — the endpoint is intentionally
|
|
28
52
|
* public so unauthenticated UIs can render link previews. A URL that is not
|
|
29
53
|
* http(s), or points at a private or internal address, still rejects (422).
|
|
54
|
+
*
|
|
55
|
+
* For a link in a chat message, pass `source`: the server then previews the
|
|
56
|
+
* link only when it is in the named message and a person wrote that message,
|
|
57
|
+
* or when the caller wrote the same address in that conversation (in a
|
|
58
|
+
* message, or as the card link of a Minds post they shared there). Otherwise
|
|
59
|
+
* it answers `{ data: null, refused: 'ai_link' }` without fetching it. A
|
|
60
|
+
* request with `source` is the caller's own: make it on a signed-in client
|
|
61
|
+
* whose key has `chat:read` (a keyless one gets 401). A message or
|
|
62
|
+
* conversation the caller cannot read rejects with 404. A `source` with no
|
|
63
|
+
* id in it (an AI reply whose id is not known yet, say) rejects with a
|
|
64
|
+
* ValidationError and sends nothing. Do not pass `source` in an encrypted
|
|
65
|
+
* conversation (see LinkPreviewSource). Servers before this change ignore
|
|
66
|
+
* `source` and preview the link, so an app keeps its own rule too: no
|
|
67
|
+
* preview of an AI-written link the member did not write.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const { data, refused } = await r.linkPreview.get(url, { messageId: message.id });
|
|
30
71
|
*/
|
|
31
|
-
get(url: string): Promise<LinkPreviewResponse>;
|
|
72
|
+
get(url: string, source?: LinkPreviewSource | null): Promise<LinkPreviewResponse>;
|
|
32
73
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValidationError } from '../errors.js';
|
|
1
2
|
export class LinkPreviewResource {
|
|
2
3
|
client;
|
|
3
4
|
constructor(client) {
|
|
@@ -9,8 +10,41 @@ export class LinkPreviewResource {
|
|
|
9
10
|
* preview (`data: null`). No auth required — the endpoint is intentionally
|
|
10
11
|
* public so unauthenticated UIs can render link previews. A URL that is not
|
|
11
12
|
* http(s), or points at a private or internal address, still rejects (422).
|
|
13
|
+
*
|
|
14
|
+
* For a link in a chat message, pass `source`: the server then previews the
|
|
15
|
+
* link only when it is in the named message and a person wrote that message,
|
|
16
|
+
* or when the caller wrote the same address in that conversation (in a
|
|
17
|
+
* message, or as the card link of a Minds post they shared there). Otherwise
|
|
18
|
+
* it answers `{ data: null, refused: 'ai_link' }` without fetching it. A
|
|
19
|
+
* request with `source` is the caller's own: make it on a signed-in client
|
|
20
|
+
* whose key has `chat:read` (a keyless one gets 401). A message or
|
|
21
|
+
* conversation the caller cannot read rejects with 404. A `source` with no
|
|
22
|
+
* id in it (an AI reply whose id is not known yet, say) rejects with a
|
|
23
|
+
* ValidationError and sends nothing. Do not pass `source` in an encrypted
|
|
24
|
+
* conversation (see LinkPreviewSource). Servers before this change ignore
|
|
25
|
+
* `source` and preview the link, so an app keeps its own rule too: no
|
|
26
|
+
* preview of an AI-written link the member did not write.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const { data, refused } = await r.linkPreview.get(url, { messageId: message.id });
|
|
12
30
|
*/
|
|
13
|
-
get(url) {
|
|
14
|
-
|
|
31
|
+
async get(url, source) {
|
|
32
|
+
const params = { url };
|
|
33
|
+
if (source != null) {
|
|
34
|
+
// A source with no id must not turn into a bare request: the server would
|
|
35
|
+
// then fetch the link with no check at all.
|
|
36
|
+
if (!source.messageId && !source.conversationId) {
|
|
37
|
+
throw new ValidationError({
|
|
38
|
+
type: 'validation_error',
|
|
39
|
+
code: 'link_preview_source_missing',
|
|
40
|
+
message: 'linkPreview.get: source needs a messageId or a conversationId. Wait for the id, or show the plain link.',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (source.messageId)
|
|
44
|
+
params.message_id = source.messageId;
|
|
45
|
+
if (source.conversationId)
|
|
46
|
+
params.conversation_id = source.conversationId;
|
|
47
|
+
}
|
|
48
|
+
return this.client.get('/link-preview', params);
|
|
15
49
|
}
|
|
16
50
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
2
|
import type { SingleResponse } from '../types.js';
|
|
3
|
+
import type { AssistantSettings } from '../assistant-surfaces.js';
|
|
3
4
|
interface NetworkConfig {
|
|
4
5
|
name: string;
|
|
5
6
|
slug: string | null;
|
|
@@ -25,6 +26,11 @@ interface NetworkConfig {
|
|
|
25
26
|
auth_methods: string;
|
|
26
27
|
default_template_name: string | null;
|
|
27
28
|
locale: string;
|
|
29
|
+
/**
|
|
30
|
+
* The assistant surface switches for the app this key is bound to (absent on servers from before George plan G7a).
|
|
31
|
+
* Read them with `assistantSurfaceOn(config, id)`, which is off for anything but an explicit true.
|
|
32
|
+
*/
|
|
33
|
+
assistant?: AssistantSettings;
|
|
28
34
|
}
|
|
29
35
|
export declare class NetworkResource {
|
|
30
36
|
private client;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
2
|
import type { SingleResponse } from '../types.js';
|
|
3
|
+
import type { AssistantSettings, AssistantSettingsUpdate } from '../assistant-surfaces.js';
|
|
3
4
|
export interface BrainTask {
|
|
4
5
|
id: string;
|
|
5
6
|
title: string;
|
|
@@ -153,6 +154,8 @@ export interface ProjectBrainSettings {
|
|
|
153
154
|
maxStepsPerRun?: number;
|
|
154
155
|
heartbeatTimeoutMinutes?: number;
|
|
155
156
|
dailySpendingLimitUsd?: number;
|
|
157
|
+
/** The assistant surface switches: every surface id, off unless stored as true (absent on older servers). */
|
|
158
|
+
assistant?: AssistantSettings;
|
|
156
159
|
[key: string]: unknown;
|
|
157
160
|
}
|
|
158
161
|
export interface UpdateProjectSettingsInput {
|
|
@@ -161,6 +164,11 @@ export interface UpdateProjectSettingsInput {
|
|
|
161
164
|
max_steps_per_run?: number;
|
|
162
165
|
heartbeat_timeout_minutes?: number;
|
|
163
166
|
daily_spending_limit_usd?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Turn assistant surfaces on or off for this app (George plan G7a): the ids given are set, the rest kept. The
|
|
169
|
+
* project's owner or an admin only (403 `project_admin_required` otherwise); an unknown id is a 400.
|
|
170
|
+
*/
|
|
171
|
+
assistant?: AssistantSettingsUpdate;
|
|
164
172
|
}
|
|
165
173
|
export interface BrowserUseRunTaskInput {
|
|
166
174
|
task: string;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
2
|
import type { PaginatedResponse, SingleResponse, User, FollowUser, PaginationParams } from '../types.js';
|
|
3
|
+
import type { AssistantSettings } from '../assistant-surfaces.js';
|
|
4
|
+
/** The signed-in user as `users.me()` returns it. */
|
|
5
|
+
export interface CurrentUser extends User {
|
|
6
|
+
/**
|
|
7
|
+
* The assistant surface switches for the app this key is bound to (absent on servers from before George plan G7a).
|
|
8
|
+
* Read them with `assistantSurfaceOn(me, id)`, which is off for anything but an explicit true.
|
|
9
|
+
*/
|
|
10
|
+
assistant?: AssistantSettings;
|
|
11
|
+
}
|
|
3
12
|
export declare class UsersResource {
|
|
4
13
|
private client;
|
|
5
14
|
constructor(client: HttpClient);
|
|
6
|
-
/** Get the authenticated user's profile */
|
|
7
|
-
me(): Promise<SingleResponse<
|
|
15
|
+
/** Get the authenticated user's profile, with the app's assistant surface switches */
|
|
16
|
+
me(): Promise<SingleResponse<CurrentUser>>;
|
|
8
17
|
/** Get a user by ID */
|
|
9
18
|
get(id: string): Promise<SingleResponse<User>>;
|
|
10
19
|
/** List a user's followers */
|
package/dist/resources/users.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1316,7 +1316,35 @@ export interface AgentChatInput {
|
|
|
1316
1316
|
message: string;
|
|
1317
1317
|
conversation_id?: string;
|
|
1318
1318
|
new_conversation?: boolean;
|
|
1319
|
+
/** Per-turn options. `{ safe: true }` makes this a safe turn (see {@link AgentChatTurn}). */
|
|
1320
|
+
turn?: AgentChatTurn;
|
|
1319
1321
|
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Per-turn options for an agent chat message.
|
|
1324
|
+
*
|
|
1325
|
+
* `safe: true` (George plan G6a) asks about something on the network, typically someone else's post, on the caller's
|
|
1326
|
+
* own personal agent, in their one Asks thread with it (found or made by the server; leave out `new_conversation`, and
|
|
1327
|
+
* send `conversation_id` only for that thread). The turn has no tools whatever the agent's tool mode, reads and saves
|
|
1328
|
+
* no memory, sends OpenRouter `data_collection: 'deny'` whatever the member chose, writes no audit copy of the prompt
|
|
1329
|
+
* or reply (`ai_usage_detail`), sends pictures as alt text and stored descriptions only, and counts against a small
|
|
1330
|
+
* daily allowance. Every later turn in the Asks thread is a safe turn too, however it is sent. The reply
|
|
1331
|
+
* carries `metadata.safe_turn: true`. Refusals answer with a fixed {@link SafeTurnRefusalCode}; none names a plan or
|
|
1332
|
+
* a price, and the server never creates an agent for an ask.
|
|
1333
|
+
*/
|
|
1334
|
+
export interface AgentChatTurn {
|
|
1335
|
+
safe?: boolean;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Why the server refused a safe turn (the `code` of the error):
|
|
1339
|
+
* - `safe_turn_limit` (429): today's asks are used up (an app shows its own "limit" words). Marked
|
|
1340
|
+
* `X-RateLimit-Scope: action`, so the SDK throws it at once as a `RateLimitError` with `scope: 'action'` and
|
|
1341
|
+
* `resetAt` (the next midnight UTC), and does not resend it.
|
|
1342
|
+
* - `safe_turn_needs_personal_agent` (409): the agent is not the caller's own personal agent.
|
|
1343
|
+
* - `safe_turn_needs_app` (403): the key is bound to no app (a personal key, say); an ask goes with the app's own key.
|
|
1344
|
+
* - `safe_turn_ai_off` / `safe_turn_standard_off` (403): the member's AI choices refuse it.
|
|
1345
|
+
* - `safe_turn_conversation` (400): `new_conversation` was sent, or `conversation_id` is not the Asks thread.
|
|
1346
|
+
*/
|
|
1347
|
+
export type SafeTurnRefusalCode = 'safe_turn_limit' | 'safe_turn_needs_personal_agent' | 'safe_turn_needs_app' | 'safe_turn_ai_off' | 'safe_turn_standard_off' | 'safe_turn_conversation';
|
|
1320
1348
|
/** Answer an existing message of yours again, without sending it a second time. */
|
|
1321
1349
|
export interface AgentChatRetryInput {
|
|
1322
1350
|
conversation_id: string;
|
|
@@ -1597,6 +1625,12 @@ export interface ContentPreferences {
|
|
|
1597
1625
|
autoplay_video: boolean;
|
|
1598
1626
|
ai_enabled: boolean;
|
|
1599
1627
|
default_feed: 'foryou' | 'following';
|
|
1628
|
+
/**
|
|
1629
|
+
* True when the member picked `default_feed` themselves (any `updateContent({ default_feed })` sets it).
|
|
1630
|
+
* When false, `default_feed` is only the default and the app may choose for them. Absent from APIs older than
|
|
1631
|
+
* this field: treat absent as unknown, not as false.
|
|
1632
|
+
*/
|
|
1633
|
+
default_feed_chosen?: boolean;
|
|
1600
1634
|
}
|
|
1601
1635
|
export interface UpdateNotificationChannelsInput {
|
|
1602
1636
|
/** Master switch: sets in-app on every kind. */
|
package/llm.md
CHANGED
|
@@ -214,6 +214,27 @@ Methods: `tasks`, `stats`, `decisions`, `milestones`, `agents`, `teamActivity`,
|
|
|
214
214
|
|
|
215
215
|
---
|
|
216
216
|
|
|
217
|
+
## Assistant subjects — `@recursiv/sdk/assistant` (unreleased)
|
|
218
|
+
|
|
219
|
+
Pure, no network: the same input on a server, a phone or the web. Build the bundle from data you already hold, then render it; never paste post text into a prompt by hand.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { buildSubjectBundle, renderBundleForModel } from '@recursiv/sdk/assistant'
|
|
223
|
+
const { bundle, refused, excluded } = buildSubjectBundle({ mode: 'standard', asker: { id, blocked_ids, muted_ids }, post, parent, quoted, replies }) // server: also blocked_by_ids
|
|
224
|
+
if (bundle) {
|
|
225
|
+
const { system, user, references } = renderBundleForModel(bundle, 'summarise', { boundary }) // boundary: fresh random, 16+ chars, per call
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- `bundle` is null when the post itself may not be read: author asks off (`ai_ask_allowed` `'off'`, or `'sealed_only'` in Standard), the asker blocked or muted the author or the author blocked the asker, known-minor author (unless asker is author), child-safety hold, moderator suppression, `assist_readable: 'none'`. Make no model call.
|
|
230
|
+
- `refused` and `excluded` (`{ role, reason, count }[]`, never ids) are safe to show the asker: blocks, minors, holds and suppression all read `'unavailable'`. `audit` has the detail: server log only, never to a client.
|
|
231
|
+
- `known_minor`, `child_safety_hold`, `assistant_suppressed`, media `held`, `blocked_by_ids`: server-side inputs, never sent to another member's client; a client passes the server's per-viewer `assist_readable` instead. Flags fail closed (anything but false/null/absent is set); an unreadable id list throws.
|
|
232
|
+
- Replies, parent and quote are left out by the same rules. Media go as alt text and stored caption only; picture and media addresses never reach `references`. Hidden text is stripped; top 20 replies; per-field and total caps (lower them with `limits`, never raise).
|
|
233
|
+
- Intents: `explain`, `fact_context` (post, parent, quote), `summarise` (plus replies), `translate` (needs `language`, a BCP 47 tag), `describe` (text only). Output filters allow only `references` ids, handles and URLs, as plain links (never render an image the model wrote). Render only bundles you built from your own reads.
|
|
234
|
+
- Fixed cards, before any model call: `matchFixedCard(memberWords, { country, locale })` → `{ kind: 'crisis' | 'election', card } | null`. Crisis (self-harm, suicide risk, immediate danger) always shows the card first, then the turn may answer; election logistics only (never candidates, results, fraud claims, votes on a post or poll), and `fixedCardAnswer('election', Date.now())` is `'card_only'` until 2026-11-04T12:00Z. English and Spanish; every number and URL is sourced data (`CRISIS_LINES`, `ELECTION_SITES`), never generated. Pass the member's words, not the post they ask about.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
217
238
|
## Evidence — Measure agents (evals, accuracy, cost)
|
|
218
239
|
|
|
219
240
|
### r.evidence — Author evals, run simulations, read graded results
|
|
@@ -564,6 +585,14 @@ const { data: reset } = await r.e2ee.resetAccount({ email_code, device_name, rea
|
|
|
564
585
|
// → { reset, resetAt, retiredRoot, revokedDeviceIds, device (the new main device), signedOut: { sessions, keys, connectedApps } }; a retry after a lost answer gets the same result
|
|
565
586
|
```
|
|
566
587
|
|
|
588
|
+
A contact's reset (D2 R2, 0.7.83; where `accountReset` is on). Each member of an encrypted conversation the account shares (a direct chat, or a group both accepted) gets `e2ee:peer` `{ userId, reason: 'root_changed', conversationId }`, once per shared conversation, and each direct-chat contact one push (at most one a day per person who reset; it does not name them): "A contact reset encrypted chat. Open Minds to keep chatting." `peerChanges({ since })` lists the accounts whose main device a reset replaced since then (default and floor 7 days, oldest first, at most 200; a relay without it answers 404: treat it as none). An engine that can accept a contact's new main device sends `trust_change: true` with its messages; in a direct chat with someone who reset, such a message gets 409 `peer_not_joined` (`details.userId`) until a device of this account added their new device, and 409 `stale_epoch` under a key generation before the chat's last key update. Messages without it are accepted as before.
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
r.realtime.onE2eePeer(({ userId, conversationId }) => {}); // look at userId's devices again
|
|
592
|
+
const { data: changes } = await r.e2ee.peerChanges({ since }); // [{ userId, at }] after time in the background
|
|
593
|
+
await r.e2ee.appendEvent(conversationId, { device_id, envelope, trust_change: true }); // ConflictError 'peer_not_joined' | 'stale_epoch'
|
|
594
|
+
```
|
|
595
|
+
|
|
567
596
|
### r.realtime — WebSocket Chat Client
|
|
568
597
|
|
|
569
598
|
Provides persistent real-time streaming and chat presence, useful for building frontends (e.g. React Native, Web).
|
|
@@ -597,7 +626,7 @@ unsub()
|
|
|
597
626
|
const off = r.realtime.on('notification', (n) => refreshBadge(n))
|
|
598
627
|
```
|
|
599
628
|
|
|
600
|
-
Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`, `onE2eeEvent`, `onE2eeLink`, `onE2eeDevices`, `on(event, fn)`
|
|
629
|
+
Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`, `onE2eeEvent`, `onE2eeLink`, `onE2eeDevices`, `onE2eePeer`, `on(event, fn)`
|
|
601
630
|
|
|
602
631
|
Reconnects on its own (transport drops, token expiry, refused handshakes) with jittered backoff and a fresh single-use token per attempt; listeners and joined rooms carry over, so no app-side reconnect shim is needed. It stops only on `disconnect()` or a 401/403 from the token source. `connect()` is safe to call repeatedly.
|
|
603
632
|
|
|
@@ -794,7 +823,7 @@ Methods: `list`, `get`, `create`, `delete`
|
|
|
794
823
|
|
|
795
824
|
## Platform Resources
|
|
796
825
|
|
|
797
|
-
- `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed? })
|
|
826
|
+
- `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed? })`; `content.default_feed_chosen` is true once the member set `default_feed` themselves (absent on older APIs). Notifications (0.7.72): `getPreferences()` returns `notifications.channels`, each kind (`post_mention`, `post_reply`, `post_repost`, `post_reaction`, `follow`, `message`, and the operator kinds) with `{ inApp, push, email }`; `updateNotifications({ channels: { post_reaction: { push: false } } })` changes that kind only, while `in_app`/`push`/`email` stay master switches over every kind. `disableAccount({ password })` (0.7.71) deactivates the account (hidden, nothing deleted; signing back in restores it), asked once; an API that does not serve deactivation answers 404 `NotFoundError`, so keep the control hidden until yours does.
|
|
798
827
|
- `r.signals` (0.7.71) — The member's engagement signals for their own ranking (For You), under their app-bound key (`agents:write`): `batch(events, { keepalive?, retry? })` sends up to `SIGNAL_BATCH_MAX` (100) `{ event_type, post_id?, dwell_ms?, metadata? }` to POST /signals/batch; `record(event)` sends one to POST /signals/post (for an API before the batch route, which answers that one 404). `event_type`: `view`, `click`, `dwell`, `react_up`, `react_down`, `save`, `hide`, `share`, `mute_source`, `open_chat`, `ask_agent`, `more_like`, `unhide`. Best-effort: a signal the server cannot store answers 200 with `recorded` 0 (or false) and a `reason` (`no_project_scope`, `project_db_unavailable`). `keepalive: true` is for a flush as the page is hidden or unloads: the request is on the wire before the call returns and is asked once.
|
|
799
828
|
|
|
800
829
|
```typescript
|
|
@@ -855,6 +884,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
855
884
|
- `r.dispatcher` — Task queue: `tasks()`, `create()`, `claimNext()`, `complete()`, `release()`, `stats()`, `signals()`, `outcomes()`, `staleItems()`, `stuckItems()`, `discoveries()`
|
|
856
885
|
- `r.dispatcher` (webhooks): `createWebhook()`, `listWebhooks()`, `updateWebhook()`, `deleteWebhook()`, `testWebhook()`, `webhookDeliveries()`, `cleanupWebhookLogs()`
|
|
857
886
|
- `r.goals` — Org goals that dispatcher tasks roll up into: `list()`, `get()`, `create()`, `update()`, `archive()`. Tasks link via `goal_id` on `r.dispatcher.create()/update()`; `tasks({ goal_id })` filters (use `'none'` for goal-less tasks).
|
|
887
|
+
- `r.linkPreview` — Open Graph card for a public link: `get(url)` answers `{ data: LinkPreview | null }` (null: no preview, show the plain link); keyless clients may call it. For a link in a chat message pass where it came from, `get(url, { messageId })` (or `{ conversationId }` before the message has an id), on a signed-in key with `chat:read`: the link is fetched only when it is in the named message and a person wrote that message, or when the caller wrote the same address in that conversation (a message, or the card link of a Minds post they shared there), else `{ data: null, refused: 'ai_link' }` with nothing fetched; 401 without a key, 404 for a message or conversation the caller cannot read. A `source` with no id rejects with `ValidationError` and sends nothing. Never pass `source` in an encrypted conversation: the server cannot read it, so every link would be refused; the app's own rule decides there.
|
|
858
888
|
|
|
859
889
|
```typescript
|
|
860
890
|
// Create a goal and group tasks under it (personal keys: pass organization_id)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@recursiv/sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.83",
|
|
4
4
|
"description": "TypeScript SDK for the Recursiv API",
|
|
5
5
|
"license": "FSL-1.1-ALv2",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
"import": "./dist/index.js",
|
|
14
14
|
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
|
+
"./assistant": {
|
|
17
|
+
"types": "./dist/assistant/index.d.ts",
|
|
18
|
+
"import": "./dist/assistant/index.js",
|
|
19
|
+
"default": "./dist/assistant/index.js"
|
|
20
|
+
},
|
|
16
21
|
"./react": {
|
|
17
22
|
"types": "./dist/react/index.d.ts",
|
|
18
23
|
"import": "./dist/react/index.js",
|