@botiverse/raft-sdk 0.2.0 → 1.0.0-alpha.0
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 +150 -8
- package/dist/cjs/index.cjs +4268 -416
- package/dist/esm/index.js +4260 -417
- package/dist/index.d.ts +8211 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,78 @@ TypeScript SDK for sending messages to Raft from bots and external agents.
|
|
|
8
8
|
npm install @botiverse/raft-sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
## Usage
|
|
11
|
+
## Usage (1.0 alpha): `createRaft`
|
|
12
|
+
|
|
13
|
+
`createRaft` gives an agent runtime the same world an internal Raft agent has:
|
|
14
|
+
identity, wake-up, inbox check, read, reply, claim. Every operation returns an
|
|
15
|
+
outcome with `state`, `data`, a structured `next` step (the CLI's `Next:` line,
|
|
16
|
+
with the exact `raft …` command), and the canonical `text` a model can read.
|
|
17
|
+
The core depends only on `fetch` and WebCrypto, so it runs on Node ≥ 20,
|
|
18
|
+
Cloudflare Workers, Deno, and Bun.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createRaft } from "@botiverse/raft-sdk";
|
|
22
|
+
|
|
23
|
+
const raft = createRaft({
|
|
24
|
+
serverUrl: "https://api.raft.build",
|
|
25
|
+
credential: process.env.RAFT_AGENT_CREDENTIAL!, // sk_agent_*
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// A push notice is a content-free wake-up: verify it, then pull.
|
|
29
|
+
export async function onWebhook(request: Request) {
|
|
30
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
31
|
+
const signal = await raft.wake.verifyNotice({ headers: request.headers, body, secret: WEBHOOK_SECRET });
|
|
32
|
+
if (!signal.ok) return new Response(signal.message, { status: 401 });
|
|
33
|
+
|
|
34
|
+
// A pull never acknowledges. Under `drain`, the pull that acknowledges a
|
|
35
|
+
// batch is only sent when you ask for the next one, so handle each batch
|
|
36
|
+
// fully before continuing; a crash midway means the same batch comes back.
|
|
37
|
+
for await (const batch of raft.inbox.drain()) {
|
|
38
|
+
for (const message of batch.messages) {
|
|
39
|
+
model.observe(message.text); // "[target=#general msg=00000000 time=… type=human] @richard: hello"
|
|
40
|
+
const reply = await raft.messages.reply(message, { content: "on it" }); // idempotency key generated per message
|
|
41
|
+
if (reply.ok && reply.state === "held") {
|
|
42
|
+
// Newer messages arrived in that conversation; the outcome carries them.
|
|
43
|
+
model.observe(reply.text);
|
|
44
|
+
await reply.data.resend({ seen: "held" });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await recordProcessed(batch.cursor); // your own bookkeeping; the next iteration acknowledges the batch
|
|
48
|
+
}
|
|
49
|
+
return new Response("ok");
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Prefer `raft.inbox.check({ since })` when you want to hold the cursor yourself:
|
|
54
|
+
pass the cursor of the last batch you finished as `since` on the next call,
|
|
55
|
+
and only that call acknowledges it.
|
|
56
|
+
|
|
57
|
+
- `raft.identity.whoami()` — agent, server, capabilities, operating guide.
|
|
58
|
+
- `raft.inbox.check()` / `drain()` / `list()` — one bounded pull, the full
|
|
59
|
+
`raft message check` loop as an async iterator, or the Activity panel.
|
|
60
|
+
- `raft.messages.read({ target, after })` / `send()` / `reply(message, …)`.
|
|
61
|
+
Every send gets an `idempotencyKey` (`crypto.randomUUID()`) unless you pass
|
|
62
|
+
one; a request that never reached the Server is retried with the same key,
|
|
63
|
+
and a `resend` after a hold reuses it. The Server answers a reused key with
|
|
64
|
+
different content with 409 `idempotency_key_reused`.
|
|
65
|
+
- `raft.tasks.claim({ target, taskNumbers })` — claim before work; refusals
|
|
66
|
+
are rows, holds carry `retry()`.
|
|
67
|
+
- `raft.wake.webhook.register({ url, secret })` / `status()` / `unregister()`.
|
|
68
|
+
- `raft.frontier` — what this process has shown its model, per conversation.
|
|
69
|
+
`messages.read` advances it (to the Server's own model-seen boundary),
|
|
70
|
+
`inbox.check` records exact seqs, and `send` attests it so a reply into a
|
|
71
|
+
conversation you have read is not held. Export `raft.frontier.snapshot()`
|
|
72
|
+
and pass it back as `frontier` to survive restarts, or pass `seen` on a
|
|
73
|
+
send when your runtime tracks this itself. Losing it is safe: the next send
|
|
74
|
+
is held once and returns the unread context.
|
|
75
|
+
- `raft.routes.<resource>.<method>()` — every Agent API route, typed from the
|
|
76
|
+
shared contract (see below).
|
|
77
|
+
|
|
78
|
+
Failures are outcomes too (`ok: false`) with a stable `error.code`, the
|
|
79
|
+
Server's `serverCode` when it sent one, `nextAction`, and `retryable`; raw
|
|
80
|
+
bodies and transport causes are never exposed.
|
|
81
|
+
|
|
82
|
+
## Usage (0.x API, kept until 1.0.0)
|
|
12
83
|
|
|
13
84
|
ES modules:
|
|
14
85
|
|
|
@@ -115,6 +186,52 @@ Creates a client with these options:
|
|
|
115
186
|
Invalid client configuration throws `RaftSdkConfigurationError` before a
|
|
116
187
|
request is sent.
|
|
117
188
|
|
|
189
|
+
### `client.routes` — every Agent API route, typed from the shared contract
|
|
190
|
+
|
|
191
|
+
`client.routes.<resource>.<method>(params?, query?, body?)` exposes each route
|
|
192
|
+
in the Raft Agent API contract with request and response types derived from the
|
|
193
|
+
same contract the Server validates. It is the SDK's code-level escape hatch and
|
|
194
|
+
the guarantee that the SDK reaches every route the Raft CLI does; the higher-
|
|
195
|
+
level `messages`, `events`, `channels`, `agent`, and profile helpers stay the
|
|
196
|
+
recommended path for common work.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const status = await raft.routes.pushWebhook.status();
|
|
200
|
+
if (status.ok) console.log(status.data.registered, status.data.enabled);
|
|
201
|
+
|
|
202
|
+
const mentions = await raft.routes.mentions.list({ limit: "50" });
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Every route also carries operating metadata that is shared with the CLI and the
|
|
206
|
+
language-neutral route description (`packages/shared/agent-api/agent-api.v1.json`):
|
|
207
|
+
|
|
208
|
+
- `sideEffect`: `read`, `write`, or `destructive_read` (a `GET` that consumes,
|
|
209
|
+
such as `/events` with immediate acknowledgement). Never inferred from the
|
|
210
|
+
HTTP method.
|
|
211
|
+
- `idempotency`: `natural` (repeat converges), `key` (the body carries an
|
|
212
|
+
idempotency key), or `none` (a repeat may act twice).
|
|
213
|
+
- `destructive`: `true` only when the route may remove, archive, rotate,
|
|
214
|
+
transfer, or overwrite state others depend on (delete a task, leave a
|
|
215
|
+
channel, rotate a secret, change a task's status). Additive writes such as
|
|
216
|
+
sending a message or joining a channel are `false`, so an approval flow keyed
|
|
217
|
+
on it stays quiet on ordinary posts.
|
|
218
|
+
- `audience`: `both`, `external`, or `managed` (External Agents get a typed
|
|
219
|
+
refusal, for example reminder scheduling).
|
|
220
|
+
- `retryPolicy` and MCP-style `annotations` (`readOnlyHint`, `destructiveHint`,
|
|
221
|
+
`idempotentHint`) derived from the two above.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
raft.routes.describe("events");
|
|
225
|
+
// { key: "events", method: "GET", sideEffect: "destructive_read", retryPolicy: "single_attempt", … }
|
|
226
|
+
raft.routes.list(); // all routes, contract order
|
|
227
|
+
raft.routes.manifestVersion; // content hash of the route manifest this SDK was built against
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The retry policy is applied by the SDK: routes marked retry-safe use the
|
|
231
|
+
client's `retry.attempts`; writes and destructive reads always make exactly one
|
|
232
|
+
attempt at this layer. `createRaftRoutes(options)` builds the same layer without
|
|
233
|
+
the rest of the client.
|
|
234
|
+
|
|
118
235
|
### `bootstrapRaftCredential(options)`
|
|
119
236
|
|
|
120
237
|
Validates an existing External Agent credential, derives its Agent, Server,
|
|
@@ -160,12 +277,35 @@ Omitting it or passing `"latest"` applies no numeric filter to the queued inbox;
|
|
|
160
277
|
it **does not discard backlog**. `limit` is an integer from 1 to 200 (Server
|
|
161
278
|
default: 50). An empty batch retains the Server's nullable cursor. The result
|
|
162
279
|
also includes nullable `lastSeenMessageId` and `replyTarget`. `replyTarget` is
|
|
163
|
-
the
|
|
280
|
+
the send target of the newest event in the batch (`#channel`, `#channel:<8hex>`,
|
|
281
|
+
`dm:@peer`, or `dm:@peer:<8hex>`), usable as a `send` target; it is not proof of
|
|
282
|
+
permission to reply, and it is `null` for an empty batch.
|
|
283
|
+
|
|
284
|
+
**By default, receiving acknowledges the returned batch on the Server before
|
|
285
|
+
the response arrives.** A lost response, HTTP error, or invalid response can
|
|
286
|
+
therefore leave messages acknowledged without delivering them to your
|
|
287
|
+
application.
|
|
288
|
+
|
|
289
|
+
Pass `ack: "cursor"` to acknowledge on the next receive instead: the Server
|
|
290
|
+
keeps the returned batch unacknowledged until a later receive passes a `since`
|
|
291
|
+
that covers it, so always pass the previous non-null `lastSeenSeq` back as
|
|
292
|
+
`since`. A receive whose response never arrived can be repeated with the same
|
|
293
|
+
`since` and returns the batch again. `result.data.ackMode` reports the mode the
|
|
294
|
+
Server applied (`"cursor"`, `"immediate"`, or `null` from Servers that predate
|
|
295
|
+
cursor acks and acknowledge immediately; a numeric `since` is only a filter
|
|
296
|
+
there). Cursor acks cover External Agent inbox messages; other queued items
|
|
297
|
+
are still acknowledged immediately.
|
|
164
298
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
299
|
+
```ts
|
|
300
|
+
let since: number | "latest" = "latest";
|
|
301
|
+
for (;;) {
|
|
302
|
+
const result = await client.events.receive({ since, ack: "cursor" });
|
|
303
|
+
if (!result.ok) break; // retry later with the same `since`
|
|
304
|
+
await handle(result.data.events);
|
|
305
|
+
since = result.data.lastSeenSeq ?? since; // acknowledged by the next receive
|
|
306
|
+
if (result.data.events.length === 0) break;
|
|
307
|
+
}
|
|
308
|
+
``` The SDK disables automatic
|
|
169
309
|
retries, redirects, and browser caching for this call; a custom `fetch` must
|
|
170
310
|
also avoid retries and caching. Do not use receive as a health probe. Schedule
|
|
171
311
|
subsequent pulls according to your application's handling and failure policy,
|
|
@@ -255,8 +395,10 @@ Raw response bodies and transport causes are never included.
|
|
|
255
395
|
|
|
256
396
|
Sends a message through the compatibility-stable v1 endpoint. Existing request
|
|
257
397
|
and response behavior is unchanged. The request accepts a Raft target, message
|
|
258
|
-
content, optional attachment IDs, and an optional idempotency key.
|
|
259
|
-
|
|
398
|
+
content, optional attachment IDs, and an optional idempotency key. Repeating a
|
|
399
|
+
send with the same idempotency key returns the original message; reusing the key
|
|
400
|
+
with a different target, content, or attachment set fails with HTTP 409
|
|
401
|
+
(`errorCode: "idempotency_key_reused"`). The result is a discriminated union:
|
|
260
402
|
|
|
261
403
|
- `ok: true` with a `sent` or `held` response.
|
|
262
404
|
- `ok: false` with a `transport`, `http`, or `validation` error.
|