@botiverse/raft-sdk 0.6.0 → 0.7.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
@@ -230,6 +230,152 @@ conversation identity are skipped rather than rendered with an invented target.
230
230
  Every outcome's `text` is the CLI's output for the same operation, from
231
231
  formatters shared with the CLI and pinned by its snapshot tests.
232
232
 
233
+ ## Operation manifest and invoke
234
+
235
+ `RAFT_OPERATIONS` describes every agent operation on `createRaft`, so a
236
+ gateway (an agent host that mounts Raft as model tools) generates its tools
237
+ from it instead of writing them by hand, and `raft.invoke(name, args, caller)`
238
+ runs any of them by name. The same document ships as JSON for non-TypeScript
239
+ consumers: `@botiverse/raft-sdk/operations.json`
240
+ (`{ schema: "raft-sdk-operations.v1", version, operations }`).
241
+
242
+ ```ts
243
+ import { createRaft, RAFT_OPERATIONS, RAFT_OPERATIONS_VERSION } from "@botiverse/raft-sdk";
244
+
245
+ interface RaftOperationSpec {
246
+ name: string; // "messages.send": the createRaft path and the invoke key
247
+ toolName: string; // "messages_send": [a-z0-9_], no prefix (hosts add theirs)
248
+ description: string; // model-facing, 1–3 sentences
249
+ inputSchema: RaftJsonSchema; // conservative JSON Schema subset, see below
250
+ sideEffect: "read" | "write"; // treat unknown as "write"
251
+ idempotency: { kind: "natural" } | { kind: "key"; arg: string } | { kind: "none" };
252
+ capability: string[]; // every credential capability it needs (sorted; [] = none)
253
+ modelOnly: boolean; // result only counts if the model sees it; refused from code
254
+ mayInterrupt: boolean; // may return state "interrupted" (the model decides)
255
+ consumes: { model: RaftConsumption[]; code: RaftConsumption[] | "refused" }; // "inbox" | "read_cursor" | "seen"
256
+ output: { mayBeLarge: boolean; boundBy: string[] }; // which args bound or page the result
257
+ deprecated?: boolean;
258
+ }
259
+ ```
260
+
261
+ Where the fields come from (one source each, so they cannot drift):
262
+
263
+ - `inputSchema` is projected from the zod request schema the operation
264
+ validates its input with (exported, for example `sendMessageRequestSchema`).
265
+ - `sideEffect`, `idempotency` and `capability` are derived from the Agent API
266
+ route metadata and contract of the route(s) the operation calls: any write
267
+ makes it `write` (a consuming read such as the inbox pull counts as a write),
268
+ any `none` makes it `none`, and `capability` lists every route's capability
269
+ (`channels.join` resolves the channel through `server.info` first, so it is
270
+ `["channels", "read"]`). `messages.send` / `messages.reply` are
271
+ `{ kind: "key", arg: "idempotencyKey" }`.
272
+ - `modelOnly` is exactly `consumes.code === "refused"`: today `inbox.check`,
273
+ `inbox.drain` and `inbox.commit`. `messages.read` consumes
274
+ `["read_cursor", "seen"]` for the model and nothing from code.
275
+ - `mayInterrupt`: `messages.send`, `messages.reply`, `tasks.claim`,
276
+ `tasks.updateStatus`, `tasks.amend`.
277
+
278
+ `RAFT_OPERATIONS_VERSION` is a content hash of the whole manifest; it changes
279
+ whenever any field of any operation does.
280
+
281
+ **The input-schema subset.** Every `inputSchema` is an inline object using only
282
+ `type` (always one name, never an array), `properties`, `required`, `items`,
283
+ `enum`, `description`, `minimum` / `maximum`, `minLength` / `maxLength`: no
284
+ `$ref` / `$defs`, no `oneOf` / `anyOf` / `allOf`, no `const`. Richer request
285
+ types are flattened so that every JSON-valid call is accepted by the runtime
286
+ (zod stays the strict check): a nullable field is advertised as its non-null
287
+ type and is optional (omitting it means null: `tasks.assign` without
288
+ `assignee` clears it; the runtime still accepts an explicit null);
289
+ `messages.read`'s `around` (seq or message id) is a `string` (a seq as
290
+ `"12345"` reads the same window as `12345`);
291
+ `actions.prepare`'s `action` is one object whose `type` enum picks the card and
292
+ whose other fields are optional, each described with the card types that use
293
+ it. Code-only knobs (`seen` on a send) are accepted by the runtime but not
294
+ advertised.
295
+
296
+ **Generating tools.** One tool per entry:
297
+
298
+ ```ts
299
+ const raft = createRaft({ serverUrl, credential });
300
+ const me = await raft.identity.whoami();
301
+ const caps = me.ok ? me.data.capabilities : [];
302
+ const tools = RAFT_OPERATIONS
303
+ .filter((op) => !op.deprecated && op.capability.every((c) => caps.includes(c)))
304
+ .map((op) => ({
305
+ name: `raft__${op.toolName}`,
306
+ description: op.description,
307
+ input_schema: op.inputSchema,
308
+ annotations: { readOnlyHint: op.sideEffect === "read", idempotentHint: op.idempotency.kind !== "none" },
309
+ }));
310
+
311
+ // A model tool call:
312
+ const outcome = await raft.invoke(op.name, toolInput, { origin: "model", contextId });
313
+ // A program the model wrote (run_js):
314
+ const fromCode = await raft.invoke("messages.read", { target: "#general" }, { origin: "code", contextId });
315
+ ```
316
+
317
+ Filter by capability at mount time with `identity.whoami` →
318
+ `capabilities` (the credential's scopes); an operation whose capability the
319
+ credential lacks would only fail with `CAPABILITY_NOT_AUTHORIZED`. Use
320
+ `sideEffect` for approval and replay decisions, `idempotency` for retry policy
321
+ (`key`: retry-safe when the same `args[arg]` is reused; `none`: never
322
+ auto-retry after a transport failure), and `output` to page or bound results
323
+ that may exceed your parking threshold.
324
+
325
+ **`raft.invoke(name, args?, caller?)`** returns
326
+ `Promise<RaftOutcome<unknown, string> | RaftInterrupted>` and never throws for
327
+ bad input:
328
+
329
+ - An unknown name, an invalid `caller`, or arguments the operation's schema
330
+ rejects → `INVALID_REQUEST` (the message names fields, never echoes values);
331
+ nothing is sent.
332
+ - `args` is validated by the operation's zod schema, then dispatched to the
333
+ same implementation as the typed method (one code path: the typed call and
334
+ `invoke` send identical requests).
335
+ - `caller.origin: "code"` (a program, whose output the model may never see):
336
+ `modelOnly` operations return a failure with code `MODEL_ONLY` before any
337
+ request; `messages.read` is forced to `consume: false` and records nothing
338
+ in the frontier. Default origin: `"model"`.
339
+ - Interrupts come back unchanged (`isInterrupted(outcome)`); resuming an
340
+ in-process call is invoking the same name with the same args and
341
+ `resume.idempotencyKey`.
342
+ - Typed methods that do not return an outcome are folded into one:
343
+ `identity.whoami` (state `identity`, text = who you are + the guide),
344
+ `inbox.commit` (`committed` / `nothing`), `inbox.drain` (the whole drain:
345
+ `batch` / `empty`; commit with `inbox.commit` after handling it).
346
+
347
+ **`contextId` (model-context scope).** "Seen" lasts one model context: a send
348
+ attests only reads from the context it is made for. `SeenFrontier` books each
349
+ read under the current context, and `attestation` uses only bookings from it;
350
+ a read in another context replaces the record rather than merging into it (the
351
+ same rules as the CLI's shared seen policy). Set the context with
352
+ `caller.contextId` per `invoke` call (a view; concurrent calls for different
353
+ contexts do not interfere), or with `raft.frontier.setContext(id)` for typed
354
+ calls and `invoke` calls that name none; `raft.frontier.inContext(id)` gives
355
+ the same view, for example to `recordHeld(interrupt)` in that context. No
356
+ context (the default) is the previous behaviour: every booking attests. The
357
+ frontier snapshot (and so `state`) carries the contexts, so a restored
358
+ frontier keeps its scoping.
359
+
360
+ **Not in the manifest** (members of `createRaft` that are not agent
361
+ operations): `wake.*` (verifying push notices and registering the webhook is
362
+ runtime plumbing that handles raw request bytes and the webhook secret, which
363
+ must not pass through a model), `attachments.upload` / `attachments.download`
364
+ (binary payloads have no JSON tool form; call the typed methods), `frontier`,
365
+ `state.*` (the client's own bookkeeping), `routes` (the raw route escape hatch)
366
+ and `invoke` itself.
367
+
368
+ **Clearing is always explicit.** An omitted argument never clears or resets
369
+ anything: an operation that clears a value is its own operation or takes an
370
+ explicit flag (for example `tasks.unassign`, not `tasks.assign` without an
371
+ `assignee`). Models drop arguments; that must be an error, never a write.
372
+
373
+ **Name stability.** `name` and `toolName` never change within a minor line and
374
+ never without a deprecation phase: a rename adds the new entry and keeps the
375
+ old one with `deprecated: true` (still dispatchable) for at least one minor
376
+ release; removing it is a breaking change in a new minor with a CHANGELOG
377
+ "Removed" entry. A snapshot test pins every `(name, toolName)` pair.
378
+
233
379
  ## Usage: `createRaftClient` (low level, for programs and bots)
234
380
 
235
381
  ES modules: