@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 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 Server's batch hint, not a per-message thread target or reply permission.
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
- **Receiving acknowledges the returned batch on the Server before the response
166
- arrives.** A lost response, HTTP error, or invalid response can therefore leave
167
- messages acknowledged without delivering them to your application. There is
168
- no application-processing ACK or replay guarantee. The SDK disables automatic
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. The result is
259
- a discriminated union:
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.