@botiverse/raft-sdk 1.0.0-alpha.3 → 1.0.0-alpha.5
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 +53 -4
- package/dist/cjs/index.cjs +363 -31
- package/dist/esm/index.js +359 -32
- package/dist/index.d.ts +101 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,52 @@ export async function onStep(state: Stored) {
|
|
|
58
58
|
await raft.messages.send(pending); // { target, content, idempotencyKey, seen: { upToSeq } }
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Persisting state between tool calls (`state`)
|
|
62
|
+
|
|
63
|
+
If your runtime keeps nothing in memory between model steps, give the client a
|
|
64
|
+
store. The SDK loads it before the first operation and saves after each
|
|
65
|
+
successful operation that changed it; you implement two async methods.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const raft = createRaft({ serverUrl, credential, state: store });
|
|
69
|
+
|
|
70
|
+
await raft.inbox.commit(); // the batch the previous call pulled is now processed
|
|
71
|
+
const batch = await raft.inbox.check(); // acknowledges it on the Server, returns the next batch
|
|
72
|
+
// … hand batch.data.messages to the model …
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `inbox.check()` records the returned batch's cursor as **pending**; pulling
|
|
76
|
+
acknowledges nothing.
|
|
77
|
+
- `inbox.commit()` promotes the pending cursor to **committed** (also accepts
|
|
78
|
+
`{ cursor }`). The next `check()` sends it as `since`, which is what
|
|
79
|
+
acknowledges that batch. The SDK never commits on its own, so a call that
|
|
80
|
+
dies before `commit()` gets the same batch again.
|
|
81
|
+
- The seen frontier and held-send keys are saved too: resending the same
|
|
82
|
+
content to the same target after a hold reuses its idempotency key. After a
|
|
83
|
+
hold, `raft.frontier.recordHeld(held.data)` then `await raft.state.save()`.
|
|
84
|
+
- Saving is one attempt and never fails the operation; failures and stale
|
|
85
|
+
writes go to `onStateSaveError`. Losing the state is safe: at worst a batch
|
|
86
|
+
is delivered once more or a send is held once.
|
|
87
|
+
|
|
88
|
+
The state is one small versioned JSON value:
|
|
89
|
+
`{ schema: "raft-sdk-state.v1", version, cursor, pendingCursor, frontier, continuations }`.
|
|
90
|
+
`save(state, { expectedVersion })` receives the `version` this client loaded;
|
|
91
|
+
throw to reject a stale write, or ignore it if your store cannot compare.
|
|
92
|
+
An IndexedDB-style store with a synchronous transaction:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const store: RaftStateStore = {
|
|
96
|
+
load: async () => (await db.get("inbox", "state")) ?? null,
|
|
97
|
+
save: async (state, { expectedVersion }) => {
|
|
98
|
+
await db.transaction("inbox", "readwrite", (tx) => {
|
|
99
|
+
const cur = tx.get("inbox", "state") as { version?: number } | undefined;
|
|
100
|
+
if (cur?.version !== expectedVersion) throw new Error("stale");
|
|
101
|
+
tx.put("inbox", state, "state");
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
61
107
|
A push notice is a content-free wake-up: verify it, then pull.
|
|
62
108
|
|
|
63
109
|
```ts
|
|
@@ -101,10 +147,13 @@ if (!signal.ok) return new Response(signal.message, { status: 401 });
|
|
|
101
147
|
- `raft.messages.search / resolve / react / unreact` — find a specific
|
|
102
148
|
message (previews neutralise `@handles` and `#channels`), resolve one id to
|
|
103
149
|
its canonical form and reply target, add or remove a reaction.
|
|
104
|
-
- `raft.attachments.upload({ target, filename, bytes })` —
|
|
105
|
-
upload
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
- `raft.attachments.upload({ target, filename, bytes })` — multipart below the
|
|
151
|
+
Server's direct-upload threshold, an upload session (presigned PUT with
|
|
152
|
+
`fetch`, then complete) at or above it, exactly as the CLI chooses. The
|
|
153
|
+
target is resolved to a channel id first. `download`, `comments`.
|
|
154
|
+
- `raft.actions.prepare({ target, action })` — post an action card
|
|
155
|
+
(`channel:create`, `channel:add_member`, `agent:create`, integration cards)
|
|
156
|
+
for a human to confirm; the human who clicks it executes it.
|
|
108
157
|
- `raft.mentions.pending / execute / deliveries` — @mentions you sent that
|
|
109
158
|
reached nobody, the notify/add recovery, and per-target delivery outcomes.
|
|
110
159
|
- `raft.manual.get / search` — the Raft Manual for Agents; both need a short
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -11044,13 +11044,17 @@ var SeenFrontier = class SeenFrontier {
|
|
|
11044
11044
|
aliases = /* @__PURE__ */ new Map();
|
|
11045
11045
|
static fromSnapshot(snapshot) {
|
|
11046
11046
|
const frontier = new SeenFrontier();
|
|
11047
|
-
|
|
11047
|
+
frontier.absorb(snapshot);
|
|
11048
|
+
return frontier;
|
|
11049
|
+
}
|
|
11050
|
+
/** Merge a snapshot into this frontier by the same monotonic rules (never lowers a mark). */
|
|
11051
|
+
absorb(snapshot) {
|
|
11052
|
+
if (!snapshot || snapshot.version !== 1) return;
|
|
11053
|
+
for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) this.recordAlias(alias, canonical);
|
|
11048
11054
|
for (const [target, state] of Object.entries(snapshot.targets ?? {})) {
|
|
11049
|
-
if (positiveInt(state.upTo))
|
|
11050
|
-
if (Array.isArray(state.exact))
|
|
11055
|
+
if (positiveInt(state.upTo)) this.recordUpTo(target, state.upTo);
|
|
11056
|
+
if (Array.isArray(state.exact)) this.recordExact(target, state.exact);
|
|
11051
11057
|
}
|
|
11052
|
-
for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) frontier.recordAlias(alias, canonical);
|
|
11053
|
-
return frontier;
|
|
11054
11058
|
}
|
|
11055
11059
|
/** Remember that the Server resolved `requested` to `canonical` (a thread spelling, a `~agent` suffix, …). */
|
|
11056
11060
|
recordAlias(requested, canonical) {
|
|
@@ -11197,7 +11201,7 @@ async function checkInbox(client, request = {}, frontier) {
|
|
|
11197
11201
|
* return value summarises the drain; the batch that ends the iteration is
|
|
11198
11202
|
* acknowledged only by the caller's next pull (`summary.cursor`).
|
|
11199
11203
|
*/
|
|
11200
|
-
async function* drainInbox(client, request = {}, frontier) {
|
|
11204
|
+
async function* drainInbox(client, request = {}, frontier, onPull) {
|
|
11201
11205
|
const ack = request.ack ?? "cursor";
|
|
11202
11206
|
let cursor = request.since ?? null;
|
|
11203
11207
|
let ackMode = ack;
|
|
@@ -11220,6 +11224,7 @@ async function* drainInbox(client, request = {}, frontier) {
|
|
|
11220
11224
|
error: round
|
|
11221
11225
|
};
|
|
11222
11226
|
const batch = round.data;
|
|
11227
|
+
await onPull?.(sent, batch);
|
|
11223
11228
|
ackMode = batch.ackMode;
|
|
11224
11229
|
stillUnread = batch.stillUnreadConversations;
|
|
11225
11230
|
if (batch.messages.length > 0) {
|
|
@@ -11528,13 +11533,6 @@ async function sendMessage(client, request, frontier, internal = {}) {
|
|
|
11528
11533
|
if (data.state === "sent") return sentOutcome(request.target, data);
|
|
11529
11534
|
return failureOutcome(opError("INVALID_RESPONSE", { message: `Unexpected send state "${data.state}".` }));
|
|
11530
11535
|
}
|
|
11531
|
-
/** Reply where a received message came from. */
|
|
11532
|
-
function replyTo(client, message, request, frontier) {
|
|
11533
|
-
return sendMessage(client, {
|
|
11534
|
-
...request,
|
|
11535
|
-
target: message.target
|
|
11536
|
-
}, frontier);
|
|
11537
|
-
}
|
|
11538
11536
|
function taskTimestamps(t) {
|
|
11539
11537
|
return `${t.createdAt ? ` created=${formatUtcTimestamp(t.createdAt)}` : ""}${t.updatedAt ? ` updated=${formatUtcTimestamp(t.updatedAt)}` : ""}`;
|
|
11540
11538
|
}
|
|
@@ -12931,10 +12929,9 @@ function inferAttachmentMimeType(filename, bytes, explicit) {
|
|
|
12931
12929
|
return sniffMimeType(bytes) ?? FILENAME_MIME_MAP[ext] ?? "application/octet-stream";
|
|
12932
12930
|
}
|
|
12933
12931
|
/**
|
|
12934
|
-
*
|
|
12935
|
-
* direct-upload threshold
|
|
12936
|
-
*
|
|
12937
|
-
* next action rather than attempting silently.
|
|
12932
|
+
* Upload bytes into a conversation, choosing the path the CLI would: multipart
|
|
12933
|
+
* `POST /upload` below the Server's direct-upload threshold, an upload session
|
|
12934
|
+
* (create → PUT to a presigned URL → complete) at or above it.
|
|
12938
12935
|
*/
|
|
12939
12936
|
async function uploadAttachment(client, transport, request) {
|
|
12940
12937
|
if (!request.target?.trim() || !request.filename?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and a filename are required to upload." }));
|
|
@@ -12944,15 +12941,11 @@ async function uploadAttachment(client, transport, request) {
|
|
|
12944
12941
|
const maxBytes = capability.ok ? capability.data.maxBytes : AGENT_ATTACHMENT_UPLOAD_FALLBACK_MAX_BYTES;
|
|
12945
12942
|
const threshold = capability.ok && capability.data.directUploadEnabled ? capability.data.directUploadThresholdBytes : null;
|
|
12946
12943
|
if (request.bytes.byteLength > maxBytes) return failureOutcome(opError("INVALID_REQUEST", { message: `Attachment is ${request.bytes.byteLength} bytes; the Server's maximum is ${maxBytes}.` }));
|
|
12947
|
-
if (threshold !== null && request.bytes.byteLength >= threshold) return failureOutcome(opError("UNAVAILABLE", {
|
|
12948
|
-
message: `Files of ${threshold} bytes or more use the direct-upload session routes, which this operation does not drive yet.`,
|
|
12949
|
-
nextAction: "Use routes.attachments.createUploadSession / completeUploadSession, or upload a smaller file.",
|
|
12950
|
-
retryable: false
|
|
12951
|
-
}));
|
|
12952
12944
|
const resolved = await client.channels.resolve({ target: request.target });
|
|
12953
12945
|
if (!resolved.ok) return failureFromClientResult(resolved);
|
|
12954
12946
|
const channelId = resolved.data.channelId;
|
|
12955
12947
|
const mimeType = inferAttachmentMimeType(request.filename, request.bytes, request.mimeType);
|
|
12948
|
+
if (threshold !== null && request.bytes.byteLength >= threshold) return uploadThroughSession(client, transport, request, channelId, mimeType);
|
|
12956
12949
|
const copy = new Uint8Array(request.bytes.byteLength);
|
|
12957
12950
|
copy.set(request.bytes);
|
|
12958
12951
|
const form = new FormData();
|
|
@@ -12992,20 +12985,22 @@ async function uploadAttachment(client, transport, request) {
|
|
|
12992
12985
|
}
|
|
12993
12986
|
const parsed = agentApiContract.attachmentUpload.response.body.safeParse(body);
|
|
12994
12987
|
if (!parsed.success) return failureOutcome(opError("INVALID_RESPONSE"));
|
|
12995
|
-
|
|
12988
|
+
return uploadedOutcome(request.target, parsed.data, mimeType);
|
|
12989
|
+
}
|
|
12990
|
+
function uploadedOutcome(target, data, mimeType) {
|
|
12996
12991
|
return {
|
|
12997
12992
|
ok: true,
|
|
12998
12993
|
state: "uploaded",
|
|
12999
12994
|
data: {
|
|
13000
12995
|
...data,
|
|
13001
|
-
target
|
|
12996
|
+
target,
|
|
13002
12997
|
mimeType: data.mimeType ?? mimeType
|
|
13003
12998
|
},
|
|
13004
12999
|
next: {
|
|
13005
13000
|
kind: "send_with_attachment",
|
|
13006
|
-
command: `raft message send --target "${
|
|
13001
|
+
command: `raft message send --target "${target}" --attachment-id ${data.id}`,
|
|
13007
13002
|
args: {
|
|
13008
|
-
target
|
|
13003
|
+
target,
|
|
13009
13004
|
attachmentIds: [data.id]
|
|
13010
13005
|
},
|
|
13011
13006
|
why: "The upload alone posts nothing; send a message that links the attachment id."
|
|
@@ -13013,6 +13008,69 @@ async function uploadAttachment(client, transport, request) {
|
|
|
13013
13008
|
text: formatAgentAttachmentUploaded(data)
|
|
13014
13009
|
};
|
|
13015
13010
|
}
|
|
13011
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13012
|
+
/**
|
|
13013
|
+
* Direct upload for files at or above the Server's threshold, mirroring the
|
|
13014
|
+
* CLI: create a session, PUT the bytes to the presigned URL (fetch; one retry
|
|
13015
|
+
* when the object store may have committed), then complete, which the Server
|
|
13016
|
+
* verifies against the object store. A PUT that definitely failed cancels the
|
|
13017
|
+
* session; a PUT whose outcome is unknown leaves it for completion to verify.
|
|
13018
|
+
*/
|
|
13019
|
+
async function uploadThroughSession(client, transport, request, channelId, mimeType) {
|
|
13020
|
+
const created = await client.attachments.createUploadSession({
|
|
13021
|
+
channelId,
|
|
13022
|
+
filename: request.filename,
|
|
13023
|
+
mimeType,
|
|
13024
|
+
sizeBytes: request.bytes.byteLength,
|
|
13025
|
+
clientRequestId: crypto.randomUUID()
|
|
13026
|
+
});
|
|
13027
|
+
if (!created.ok) return failureFromClientResult(created);
|
|
13028
|
+
const { uploadId, upload } = created.data;
|
|
13029
|
+
const body = new Uint8Array(request.bytes.byteLength);
|
|
13030
|
+
body.set(request.bytes);
|
|
13031
|
+
let definitelyFailed = null;
|
|
13032
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
13033
|
+
let response;
|
|
13034
|
+
try {
|
|
13035
|
+
response = await transport.fetch(upload.url, {
|
|
13036
|
+
method: upload.method ?? "PUT",
|
|
13037
|
+
headers: { ...upload.headers },
|
|
13038
|
+
body: body.buffer,
|
|
13039
|
+
redirect: "error"
|
|
13040
|
+
});
|
|
13041
|
+
} catch {
|
|
13042
|
+
if (attempt === 0) continue;
|
|
13043
|
+
break;
|
|
13044
|
+
}
|
|
13045
|
+
if (response.ok || response.status === 412) {
|
|
13046
|
+
definitelyFailed = null;
|
|
13047
|
+
break;
|
|
13048
|
+
}
|
|
13049
|
+
const mayExist = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
13050
|
+
if (mayExist && attempt === 0) continue;
|
|
13051
|
+
if (!mayExist) definitelyFailed = response.status;
|
|
13052
|
+
break;
|
|
13053
|
+
}
|
|
13054
|
+
if (definitelyFailed !== null) {
|
|
13055
|
+
await client.attachments.cancelUploadSession({ uploadId }).catch(() => void 0);
|
|
13056
|
+
return failureOutcome(opError("HTTP_ERROR", {
|
|
13057
|
+
message: `Direct object upload failed with HTTP ${definitelyFailed}; the upload session was cancelled.`,
|
|
13058
|
+
status: definitelyFailed,
|
|
13059
|
+
nextAction: "Retry the upload; nothing was attached."
|
|
13060
|
+
}));
|
|
13061
|
+
}
|
|
13062
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
13063
|
+
const completed = await client.attachments.completeUploadSession({ uploadId });
|
|
13064
|
+
if (completed.ok) {
|
|
13065
|
+
const attachment = completed.data.attachment;
|
|
13066
|
+
return uploadedOutcome(request.target, attachment, mimeType);
|
|
13067
|
+
}
|
|
13068
|
+
const code = completed.error.kind === "http" ? completed.error.errorCode : null;
|
|
13069
|
+
if (!(code === "UPLOAD_OBJECT_NOT_FOUND" || code === "UPLOAD_VERIFICATION_IN_PROGRESS") || attempt === 2) return failureFromClientResult(completed);
|
|
13070
|
+
await wait(250 * (attempt + 1));
|
|
13071
|
+
}
|
|
13072
|
+
return failureOutcome(opError("UNAVAILABLE", { message: "Direct upload completion ended without a terminal response." }));
|
|
13073
|
+
}
|
|
13016
13074
|
async function downloadAttachment(client, request) {
|
|
13017
13075
|
if (!request.attachmentId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "An attachment id is required." }));
|
|
13018
13076
|
const result = await client.attachments.download({ attachmentId: request.attachmentId });
|
|
@@ -13614,11 +13672,210 @@ async function searchManual(client, request) {
|
|
|
13614
13672
|
};
|
|
13615
13673
|
}
|
|
13616
13674
|
//#endregion
|
|
13675
|
+
//#region ../shared/src/agentOps/state.ts
|
|
13676
|
+
const RAFT_STATE_SCHEMA = "raft-sdk-state.v1";
|
|
13677
|
+
const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
|
|
13678
|
+
const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
|
|
13679
|
+
function isNullableCursor(value) {
|
|
13680
|
+
return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
13681
|
+
}
|
|
13682
|
+
/** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
|
|
13683
|
+
function parseRaftState(value) {
|
|
13684
|
+
if (!value || typeof value !== "object") return null;
|
|
13685
|
+
const v = value;
|
|
13686
|
+
if (v.schema !== "raft-sdk-state.v1") return null;
|
|
13687
|
+
if (typeof v.version !== "number" || !Number.isInteger(v.version) || v.version < 0) return null;
|
|
13688
|
+
if (!isNullableCursor(v.cursor ?? null) || !isNullableCursor(v.pendingCursor ?? null)) return null;
|
|
13689
|
+
const frontier = v.frontier;
|
|
13690
|
+
const continuations = Array.isArray(v.continuations) ? v.continuations.filter((c) => {
|
|
13691
|
+
const r = c;
|
|
13692
|
+
return Boolean(r) && typeof r.target === "string" && typeof r.idempotencyKey === "string" && typeof r.contentHash === "string" && typeof r.heldAt === "string";
|
|
13693
|
+
}) : [];
|
|
13694
|
+
return {
|
|
13695
|
+
schema: RAFT_STATE_SCHEMA,
|
|
13696
|
+
version: v.version,
|
|
13697
|
+
cursor: v.cursor ?? null,
|
|
13698
|
+
pendingCursor: v.pendingCursor ?? null,
|
|
13699
|
+
frontier: frontier && frontier.version === 1 ? frontier : {
|
|
13700
|
+
version: 1,
|
|
13701
|
+
targets: {},
|
|
13702
|
+
aliases: {}
|
|
13703
|
+
},
|
|
13704
|
+
continuations
|
|
13705
|
+
};
|
|
13706
|
+
}
|
|
13707
|
+
/** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
|
|
13708
|
+
async function hashRaftSendContent(target, content, attachmentIds = []) {
|
|
13709
|
+
const bytes = new TextEncoder().encode(`${target}\n${[...attachmentIds].sort().join(",")}\n${content}`);
|
|
13710
|
+
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
13711
|
+
}
|
|
13712
|
+
/**
|
|
13713
|
+
* One client's view of the persisted state: loads once, holds the working
|
|
13714
|
+
* copy, saves on request. Also used without a store (memory only), so the
|
|
13715
|
+
* pending/committed cursor flow behaves the same with or without persistence.
|
|
13716
|
+
*/
|
|
13717
|
+
var RaftStateSession = class {
|
|
13718
|
+
store;
|
|
13719
|
+
onError;
|
|
13720
|
+
frontier;
|
|
13721
|
+
cursor = null;
|
|
13722
|
+
pendingCursor = null;
|
|
13723
|
+
continuations = [];
|
|
13724
|
+
loadedVersion = void 0;
|
|
13725
|
+
loading = null;
|
|
13726
|
+
dirty = false;
|
|
13727
|
+
constructor(store, frontier, onError) {
|
|
13728
|
+
this.store = store;
|
|
13729
|
+
this.onError = onError;
|
|
13730
|
+
this.frontier = frontier;
|
|
13731
|
+
}
|
|
13732
|
+
get persistent() {
|
|
13733
|
+
return this.store !== void 0;
|
|
13734
|
+
}
|
|
13735
|
+
/** The version loaded from (or last saved to) the store; `undefined` before the first save of an empty store. */
|
|
13736
|
+
get version() {
|
|
13737
|
+
return this.loadedVersion;
|
|
13738
|
+
}
|
|
13739
|
+
/** Load once. A failed or unreadable load starts from an empty state (reported, not thrown). */
|
|
13740
|
+
ensureLoaded() {
|
|
13741
|
+
if (!this.store) return Promise.resolve();
|
|
13742
|
+
this.loading ??= (async () => {
|
|
13743
|
+
let raw = null;
|
|
13744
|
+
try {
|
|
13745
|
+
raw = await this.store.load();
|
|
13746
|
+
} catch (error) {
|
|
13747
|
+
this.onError?.(error, {
|
|
13748
|
+
phase: "load",
|
|
13749
|
+
version: void 0
|
|
13750
|
+
});
|
|
13751
|
+
return;
|
|
13752
|
+
}
|
|
13753
|
+
const state = parseRaftState(raw);
|
|
13754
|
+
if (!state) return;
|
|
13755
|
+
this.loadedVersion = state.version;
|
|
13756
|
+
this.cursor = state.cursor;
|
|
13757
|
+
this.pendingCursor = state.pendingCursor;
|
|
13758
|
+
this.frontier.absorb(state.frontier);
|
|
13759
|
+
this.continuations = state.continuations ?? [];
|
|
13760
|
+
})();
|
|
13761
|
+
return this.loading;
|
|
13762
|
+
}
|
|
13763
|
+
markDirty() {
|
|
13764
|
+
this.dirty = true;
|
|
13765
|
+
}
|
|
13766
|
+
/** Record the cursor of a batch just returned. Acknowledges nothing. */
|
|
13767
|
+
setPending(cursor) {
|
|
13768
|
+
if (cursor === null || cursor === this.pendingCursor) return;
|
|
13769
|
+
this.pendingCursor = cursor;
|
|
13770
|
+
this.dirty = true;
|
|
13771
|
+
}
|
|
13772
|
+
/**
|
|
13773
|
+
* Promote a cursor to committed. With no argument, commits `pendingCursor`.
|
|
13774
|
+
* Returns the committed cursor, or null when there was nothing to commit.
|
|
13775
|
+
* Never moves the committed cursor backwards.
|
|
13776
|
+
*/
|
|
13777
|
+
commit(cursor) {
|
|
13778
|
+
const next = cursor === void 0 ? this.pendingCursor : cursor;
|
|
13779
|
+
if (next === null || next === void 0) return null;
|
|
13780
|
+
if (this.cursor === null || next > this.cursor) this.cursor = next;
|
|
13781
|
+
if (this.pendingCursor !== null && this.pendingCursor <= this.cursor) this.pendingCursor = null;
|
|
13782
|
+
this.dirty = true;
|
|
13783
|
+
return this.cursor;
|
|
13784
|
+
}
|
|
13785
|
+
findContinuation(target, contentHash) {
|
|
13786
|
+
return this.continuations.find((c) => c.target === target && c.contentHash === contentHash);
|
|
13787
|
+
}
|
|
13788
|
+
rememberContinuation(entry) {
|
|
13789
|
+
this.continuations = this.continuations.filter((c) => !(c.target === entry.target && c.contentHash === entry.contentHash));
|
|
13790
|
+
this.continuations.push(entry);
|
|
13791
|
+
const perTarget = this.continuations.filter((c) => c.target === entry.target);
|
|
13792
|
+
for (const drop of perTarget.slice(0, Math.max(0, perTarget.length - 3))) this.continuations.splice(this.continuations.indexOf(drop), 1);
|
|
13793
|
+
if (this.continuations.length > 20) this.continuations.splice(0, this.continuations.length - 20);
|
|
13794
|
+
this.dirty = true;
|
|
13795
|
+
}
|
|
13796
|
+
forgetContinuation(target, contentHash) {
|
|
13797
|
+
const before = this.continuations.length;
|
|
13798
|
+
this.continuations = this.continuations.filter((c) => !(c.target === target && c.contentHash === contentHash));
|
|
13799
|
+
if (this.continuations.length !== before) this.dirty = true;
|
|
13800
|
+
}
|
|
13801
|
+
snapshot(nextVersion = (this.loadedVersion ?? 0) + 1) {
|
|
13802
|
+
return {
|
|
13803
|
+
schema: RAFT_STATE_SCHEMA,
|
|
13804
|
+
version: nextVersion,
|
|
13805
|
+
cursor: this.cursor,
|
|
13806
|
+
pendingCursor: this.pendingCursor,
|
|
13807
|
+
frontier: this.frontier.snapshot(),
|
|
13808
|
+
continuations: [...this.continuations]
|
|
13809
|
+
};
|
|
13810
|
+
}
|
|
13811
|
+
/**
|
|
13812
|
+
* Save if anything changed (or `force`). One attempt; a failure is reported
|
|
13813
|
+
* and swallowed. Returns whether the save succeeded (true when nothing to save).
|
|
13814
|
+
*/
|
|
13815
|
+
async save(options = {}) {
|
|
13816
|
+
if (!this.store) return true;
|
|
13817
|
+
if (!this.dirty && !options.force) return true;
|
|
13818
|
+
const expectedVersion = this.loadedVersion;
|
|
13819
|
+
const state = this.snapshot();
|
|
13820
|
+
try {
|
|
13821
|
+
await this.store.save(state, { expectedVersion });
|
|
13822
|
+
} catch (error) {
|
|
13823
|
+
this.onError?.(error, {
|
|
13824
|
+
phase: "save",
|
|
13825
|
+
version: expectedVersion
|
|
13826
|
+
});
|
|
13827
|
+
return false;
|
|
13828
|
+
}
|
|
13829
|
+
this.loadedVersion = state.version;
|
|
13830
|
+
this.dirty = false;
|
|
13831
|
+
return true;
|
|
13832
|
+
}
|
|
13833
|
+
};
|
|
13834
|
+
//#endregion
|
|
13835
|
+
//#region ../shared/src/agentOps/actions.ts
|
|
13836
|
+
/** Text is the CLI's `raft action prepare` output. */
|
|
13837
|
+
function formatAgentActionCardPosted(target, messageId) {
|
|
13838
|
+
const shortId = messageId ? messageId.slice(0, 8) : null;
|
|
13839
|
+
return shortId ? `Action card posted to ${target} as message ${messageId} (short ${shortId}). The human can click the action verb to commit.\n` : `Action card posted to ${target}.\n`;
|
|
13840
|
+
}
|
|
13841
|
+
async function prepareActionCard(client, request) {
|
|
13842
|
+
if (!request?.target?.trim() || !request.action) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and an action are required to prepare a card." }));
|
|
13843
|
+
const result = await client.actions.prepare(request);
|
|
13844
|
+
if (!result.ok) return failureFromClientResult(result);
|
|
13845
|
+
const card = {
|
|
13846
|
+
target: request.target,
|
|
13847
|
+
messageId: result.data.messageId
|
|
13848
|
+
};
|
|
13849
|
+
return {
|
|
13850
|
+
ok: true,
|
|
13851
|
+
state: "prepared",
|
|
13852
|
+
data: card,
|
|
13853
|
+
next: {
|
|
13854
|
+
kind: "await_confirmation",
|
|
13855
|
+
command: `raft message read --target "${request.target}" --around ${card.messageId.slice(0, 8)}`,
|
|
13856
|
+
args: {
|
|
13857
|
+
target: request.target,
|
|
13858
|
+
messageId: card.messageId
|
|
13859
|
+
},
|
|
13860
|
+
why: "A human must click the card to commit it; its outcome arrives in your inbox."
|
|
13861
|
+
},
|
|
13862
|
+
text: formatAgentActionCardPosted(request.target, card.messageId)
|
|
13863
|
+
};
|
|
13864
|
+
}
|
|
13865
|
+
//#endregion
|
|
13617
13866
|
//#region src/raft.ts
|
|
13618
13867
|
function createRaft(options) {
|
|
13619
13868
|
const serverUrl = requireServerUrl(options.serverUrl);
|
|
13620
13869
|
const authorization = `Bearer ${requireAgentCredential(options.credential)}`;
|
|
13621
13870
|
const frontier = SeenFrontier.fromSnapshot(options.frontier);
|
|
13871
|
+
const session = new RaftStateSession(options.state, frontier, options.onStateSaveError);
|
|
13872
|
+
/** Load before, run, then save if the operation succeeded and changed the state. */
|
|
13873
|
+
const withState = async (run) => {
|
|
13874
|
+
await session.ensureLoaded();
|
|
13875
|
+
const outcome = await run();
|
|
13876
|
+
if (outcome.ok) await session.save();
|
|
13877
|
+
return outcome;
|
|
13878
|
+
};
|
|
13622
13879
|
const routes = createRaftRoutes({
|
|
13623
13880
|
serverUrl,
|
|
13624
13881
|
fetch: options.fetch,
|
|
@@ -13639,6 +13896,27 @@ function createRaft(options) {
|
|
|
13639
13896
|
throttle: options.throttle,
|
|
13640
13897
|
auth: { authorization }
|
|
13641
13898
|
} });
|
|
13899
|
+
/**
|
|
13900
|
+
* Send through the state session: a resend of the same logical message
|
|
13901
|
+
* (same target, content, attachments) reuses the held send's idempotency key;
|
|
13902
|
+
* a hold is remembered; a successful send clears it.
|
|
13903
|
+
*/
|
|
13904
|
+
const sendWithState = (request) => withState(async () => {
|
|
13905
|
+
const contentHash = await hashRaftSendContent(request.target, request.content ?? "", request.attachmentIds ?? []);
|
|
13906
|
+
const pending = request.idempotencyKey ? void 0 : session.findContinuation(request.target, contentHash);
|
|
13907
|
+
const outcome = await sendMessage(routes, pending ? {
|
|
13908
|
+
...request,
|
|
13909
|
+
idempotencyKey: pending.idempotencyKey
|
|
13910
|
+
} : request, frontier);
|
|
13911
|
+
if (outcome.ok && outcome.state === "held") session.rememberContinuation({
|
|
13912
|
+
target: request.target,
|
|
13913
|
+
idempotencyKey: outcome.data.idempotencyKey,
|
|
13914
|
+
contentHash,
|
|
13915
|
+
heldAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13916
|
+
});
|
|
13917
|
+
else if (outcome.ok && outcome.state === "sent") session.forgetContinuation(request.target, contentHash);
|
|
13918
|
+
return outcome;
|
|
13919
|
+
});
|
|
13642
13920
|
return {
|
|
13643
13921
|
identity: { whoami: () => getRaftContext(routes) },
|
|
13644
13922
|
wake: {
|
|
@@ -13650,14 +13928,53 @@ function createRaft(options) {
|
|
|
13650
13928
|
}
|
|
13651
13929
|
},
|
|
13652
13930
|
inbox: {
|
|
13653
|
-
check: (request) =>
|
|
13654
|
-
|
|
13931
|
+
check: (request = {}) => withState(async () => {
|
|
13932
|
+
const since = request.since ?? session.cursor ?? void 0;
|
|
13933
|
+
const outcome = await checkInbox(inboxApi, {
|
|
13934
|
+
...request,
|
|
13935
|
+
...since === void 0 ? {} : { since }
|
|
13936
|
+
}, frontier);
|
|
13937
|
+
if (outcome.ok) {
|
|
13938
|
+
session.markDirty();
|
|
13939
|
+
if (since !== void 0 && (request.ack ?? "cursor") === "cursor") session.commit(since);
|
|
13940
|
+
if (outcome.data.ackMode === "cursor" && outcome.data.messages.length > 0) session.setPending(outcome.data.cursor);
|
|
13941
|
+
}
|
|
13942
|
+
return outcome;
|
|
13943
|
+
}),
|
|
13944
|
+
commit: async (target) => {
|
|
13945
|
+
await session.ensureLoaded();
|
|
13946
|
+
const cursor = session.commit(target === void 0 ? void 0 : target.cursor);
|
|
13947
|
+
return {
|
|
13948
|
+
cursor,
|
|
13949
|
+
saved: cursor === null ? true : await session.save()
|
|
13950
|
+
};
|
|
13951
|
+
},
|
|
13952
|
+
drain: (request = {}) => (async function* () {
|
|
13953
|
+
await session.ensureLoaded();
|
|
13954
|
+
const since = request.since ?? session.cursor ?? void 0;
|
|
13955
|
+
return yield* drainInbox(inboxApi, {
|
|
13956
|
+
...request,
|
|
13957
|
+
...since === void 0 ? {} : { since }
|
|
13958
|
+
}, frontier, async (sent, batch) => {
|
|
13959
|
+
if (sent !== null) session.commit(sent);
|
|
13960
|
+
if (batch.ackMode === "cursor" && batch.messages.length > 0) session.setPending(batch.cursor);
|
|
13961
|
+
session.markDirty();
|
|
13962
|
+
await session.save();
|
|
13963
|
+
});
|
|
13964
|
+
})(),
|
|
13655
13965
|
list: (request) => listInbox(routes, request)
|
|
13656
13966
|
},
|
|
13657
13967
|
messages: {
|
|
13658
|
-
read: (request) =>
|
|
13659
|
-
|
|
13660
|
-
|
|
13968
|
+
read: (request) => withState(async () => {
|
|
13969
|
+
const outcome = await readHistory(routes, request, frontier);
|
|
13970
|
+
if (outcome.ok) session.markDirty();
|
|
13971
|
+
return outcome;
|
|
13972
|
+
}),
|
|
13973
|
+
send: (request) => sendWithState(request),
|
|
13974
|
+
reply: (message, request) => sendWithState({
|
|
13975
|
+
...request,
|
|
13976
|
+
target: message.target
|
|
13977
|
+
}),
|
|
13661
13978
|
search: (request) => searchMessages(routes, request),
|
|
13662
13979
|
resolve: (request) => resolveMessage(routes, request),
|
|
13663
13980
|
react: (request) => reactToMessage(routes, request, "add"),
|
|
@@ -13678,6 +13995,7 @@ function createRaft(options) {
|
|
|
13678
13995
|
execute: (request) => executeMentionAction(routes, request),
|
|
13679
13996
|
deliveries: (request) => senderMentionDeliveries(routes, request)
|
|
13680
13997
|
},
|
|
13998
|
+
actions: { prepare: (request) => prepareActionCard(routes, request) },
|
|
13681
13999
|
manual: {
|
|
13682
14000
|
get: (request) => getManualTopic(routes, request),
|
|
13683
14001
|
search: (request) => searchManual(routes, request)
|
|
@@ -13711,6 +14029,15 @@ function createRaft(options) {
|
|
|
13711
14029
|
update: (request) => updateProfile(routes, request)
|
|
13712
14030
|
},
|
|
13713
14031
|
frontier,
|
|
14032
|
+
state: {
|
|
14033
|
+
save: async () => {
|
|
14034
|
+
await session.ensureLoaded();
|
|
14035
|
+
session.markDirty();
|
|
14036
|
+
return session.save();
|
|
14037
|
+
},
|
|
14038
|
+
load: () => session.ensureLoaded(),
|
|
14039
|
+
snapshot: () => session.snapshot(session.version ?? 0)
|
|
14040
|
+
},
|
|
13714
14041
|
routes
|
|
13715
14042
|
};
|
|
13716
14043
|
}
|
|
@@ -13718,6 +14045,9 @@ function createRaft(options) {
|
|
|
13718
14045
|
exports.RAFT_INBOX_NOTICE_SCHEMA = RAFT_INBOX_NOTICE_SCHEMA;
|
|
13719
14046
|
exports.RAFT_NOTICE_DELIVERY_ID_HEADER = RAFT_NOTICE_DELIVERY_ID_HEADER;
|
|
13720
14047
|
exports.RAFT_NOTICE_SIGNATURE_HEADER = RAFT_NOTICE_SIGNATURE_HEADER;
|
|
14048
|
+
exports.RAFT_STATE_CONTINUATIONS_PER_TARGET = RAFT_STATE_CONTINUATIONS_PER_TARGET;
|
|
14049
|
+
exports.RAFT_STATE_CONTINUATIONS_TOTAL = RAFT_STATE_CONTINUATIONS_TOTAL;
|
|
14050
|
+
exports.RAFT_STATE_SCHEMA = RAFT_STATE_SCHEMA;
|
|
13721
14051
|
exports.RaftCredentialError = RaftCredentialError;
|
|
13722
14052
|
exports.RaftSdkConfigurationError = RaftSdkConfigurationError;
|
|
13723
14053
|
exports.SeenFrontier = SeenFrontier;
|
|
@@ -13729,10 +14059,12 @@ exports.createRaftClientFromStore = createRaftClientFromStore;
|
|
|
13729
14059
|
exports.createRaftRoutes = createRaftRoutes;
|
|
13730
14060
|
exports.describeRaftRoute = describeRaftRoute;
|
|
13731
14061
|
exports.hasAgentMessageIdentity = hasAgentMessageIdentity;
|
|
14062
|
+
exports.hashRaftSendContent = hashRaftSendContent;
|
|
13732
14063
|
exports.inferAttachmentMimeType = inferAttachmentMimeType;
|
|
13733
14064
|
exports.joinRaftChannelByTarget = joinRaftChannelByTarget;
|
|
13734
14065
|
exports.listRaftRoutes = listRaftRoutes;
|
|
13735
14066
|
exports.parseRaftRegularChannelTarget = parseRaftRegularChannelTarget;
|
|
14067
|
+
exports.parseRaftState = parseRaftState;
|
|
13736
14068
|
exports.projectRaftMessage = projectRaftMessage;
|
|
13737
14069
|
exports.projectRaftMessages = projectRaftMessages;
|
|
13738
14070
|
exports.verifyInboxNotice = verifyInboxNotice;
|
package/dist/esm/index.js
CHANGED
|
@@ -11043,13 +11043,17 @@ var SeenFrontier = class SeenFrontier {
|
|
|
11043
11043
|
aliases = /* @__PURE__ */ new Map();
|
|
11044
11044
|
static fromSnapshot(snapshot) {
|
|
11045
11045
|
const frontier = new SeenFrontier();
|
|
11046
|
-
|
|
11046
|
+
frontier.absorb(snapshot);
|
|
11047
|
+
return frontier;
|
|
11048
|
+
}
|
|
11049
|
+
/** Merge a snapshot into this frontier by the same monotonic rules (never lowers a mark). */
|
|
11050
|
+
absorb(snapshot) {
|
|
11051
|
+
if (!snapshot || snapshot.version !== 1) return;
|
|
11052
|
+
for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) this.recordAlias(alias, canonical);
|
|
11047
11053
|
for (const [target, state] of Object.entries(snapshot.targets ?? {})) {
|
|
11048
|
-
if (positiveInt(state.upTo))
|
|
11049
|
-
if (Array.isArray(state.exact))
|
|
11054
|
+
if (positiveInt(state.upTo)) this.recordUpTo(target, state.upTo);
|
|
11055
|
+
if (Array.isArray(state.exact)) this.recordExact(target, state.exact);
|
|
11050
11056
|
}
|
|
11051
|
-
for (const [alias, canonical] of Object.entries(snapshot.aliases ?? {})) frontier.recordAlias(alias, canonical);
|
|
11052
|
-
return frontier;
|
|
11053
11057
|
}
|
|
11054
11058
|
/** Remember that the Server resolved `requested` to `canonical` (a thread spelling, a `~agent` suffix, …). */
|
|
11055
11059
|
recordAlias(requested, canonical) {
|
|
@@ -11196,7 +11200,7 @@ async function checkInbox(client, request = {}, frontier) {
|
|
|
11196
11200
|
* return value summarises the drain; the batch that ends the iteration is
|
|
11197
11201
|
* acknowledged only by the caller's next pull (`summary.cursor`).
|
|
11198
11202
|
*/
|
|
11199
|
-
async function* drainInbox(client, request = {}, frontier) {
|
|
11203
|
+
async function* drainInbox(client, request = {}, frontier, onPull) {
|
|
11200
11204
|
const ack = request.ack ?? "cursor";
|
|
11201
11205
|
let cursor = request.since ?? null;
|
|
11202
11206
|
let ackMode = ack;
|
|
@@ -11219,6 +11223,7 @@ async function* drainInbox(client, request = {}, frontier) {
|
|
|
11219
11223
|
error: round
|
|
11220
11224
|
};
|
|
11221
11225
|
const batch = round.data;
|
|
11226
|
+
await onPull?.(sent, batch);
|
|
11222
11227
|
ackMode = batch.ackMode;
|
|
11223
11228
|
stillUnread = batch.stillUnreadConversations;
|
|
11224
11229
|
if (batch.messages.length > 0) {
|
|
@@ -11527,13 +11532,6 @@ async function sendMessage(client, request, frontier, internal = {}) {
|
|
|
11527
11532
|
if (data.state === "sent") return sentOutcome(request.target, data);
|
|
11528
11533
|
return failureOutcome(opError("INVALID_RESPONSE", { message: `Unexpected send state "${data.state}".` }));
|
|
11529
11534
|
}
|
|
11530
|
-
/** Reply where a received message came from. */
|
|
11531
|
-
function replyTo(client, message, request, frontier) {
|
|
11532
|
-
return sendMessage(client, {
|
|
11533
|
-
...request,
|
|
11534
|
-
target: message.target
|
|
11535
|
-
}, frontier);
|
|
11536
|
-
}
|
|
11537
11535
|
function taskTimestamps(t) {
|
|
11538
11536
|
return `${t.createdAt ? ` created=${formatUtcTimestamp(t.createdAt)}` : ""}${t.updatedAt ? ` updated=${formatUtcTimestamp(t.updatedAt)}` : ""}`;
|
|
11539
11537
|
}
|
|
@@ -12930,10 +12928,9 @@ function inferAttachmentMimeType(filename, bytes, explicit) {
|
|
|
12930
12928
|
return sniffMimeType(bytes) ?? FILENAME_MIME_MAP[ext] ?? "application/octet-stream";
|
|
12931
12929
|
}
|
|
12932
12930
|
/**
|
|
12933
|
-
*
|
|
12934
|
-
* direct-upload threshold
|
|
12935
|
-
*
|
|
12936
|
-
* next action rather than attempting silently.
|
|
12931
|
+
* Upload bytes into a conversation, choosing the path the CLI would: multipart
|
|
12932
|
+
* `POST /upload` below the Server's direct-upload threshold, an upload session
|
|
12933
|
+
* (create → PUT to a presigned URL → complete) at or above it.
|
|
12937
12934
|
*/
|
|
12938
12935
|
async function uploadAttachment(client, transport, request) {
|
|
12939
12936
|
if (!request.target?.trim() || !request.filename?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and a filename are required to upload." }));
|
|
@@ -12943,15 +12940,11 @@ async function uploadAttachment(client, transport, request) {
|
|
|
12943
12940
|
const maxBytes = capability.ok ? capability.data.maxBytes : AGENT_ATTACHMENT_UPLOAD_FALLBACK_MAX_BYTES;
|
|
12944
12941
|
const threshold = capability.ok && capability.data.directUploadEnabled ? capability.data.directUploadThresholdBytes : null;
|
|
12945
12942
|
if (request.bytes.byteLength > maxBytes) return failureOutcome(opError("INVALID_REQUEST", { message: `Attachment is ${request.bytes.byteLength} bytes; the Server's maximum is ${maxBytes}.` }));
|
|
12946
|
-
if (threshold !== null && request.bytes.byteLength >= threshold) return failureOutcome(opError("UNAVAILABLE", {
|
|
12947
|
-
message: `Files of ${threshold} bytes or more use the direct-upload session routes, which this operation does not drive yet.`,
|
|
12948
|
-
nextAction: "Use routes.attachments.createUploadSession / completeUploadSession, or upload a smaller file.",
|
|
12949
|
-
retryable: false
|
|
12950
|
-
}));
|
|
12951
12943
|
const resolved = await client.channels.resolve({ target: request.target });
|
|
12952
12944
|
if (!resolved.ok) return failureFromClientResult(resolved);
|
|
12953
12945
|
const channelId = resolved.data.channelId;
|
|
12954
12946
|
const mimeType = inferAttachmentMimeType(request.filename, request.bytes, request.mimeType);
|
|
12947
|
+
if (threshold !== null && request.bytes.byteLength >= threshold) return uploadThroughSession(client, transport, request, channelId, mimeType);
|
|
12955
12948
|
const copy = new Uint8Array(request.bytes.byteLength);
|
|
12956
12949
|
copy.set(request.bytes);
|
|
12957
12950
|
const form = new FormData();
|
|
@@ -12991,20 +12984,22 @@ async function uploadAttachment(client, transport, request) {
|
|
|
12991
12984
|
}
|
|
12992
12985
|
const parsed = agentApiContract.attachmentUpload.response.body.safeParse(body);
|
|
12993
12986
|
if (!parsed.success) return failureOutcome(opError("INVALID_RESPONSE"));
|
|
12994
|
-
|
|
12987
|
+
return uploadedOutcome(request.target, parsed.data, mimeType);
|
|
12988
|
+
}
|
|
12989
|
+
function uploadedOutcome(target, data, mimeType) {
|
|
12995
12990
|
return {
|
|
12996
12991
|
ok: true,
|
|
12997
12992
|
state: "uploaded",
|
|
12998
12993
|
data: {
|
|
12999
12994
|
...data,
|
|
13000
|
-
target
|
|
12995
|
+
target,
|
|
13001
12996
|
mimeType: data.mimeType ?? mimeType
|
|
13002
12997
|
},
|
|
13003
12998
|
next: {
|
|
13004
12999
|
kind: "send_with_attachment",
|
|
13005
|
-
command: `raft message send --target "${
|
|
13000
|
+
command: `raft message send --target "${target}" --attachment-id ${data.id}`,
|
|
13006
13001
|
args: {
|
|
13007
|
-
target
|
|
13002
|
+
target,
|
|
13008
13003
|
attachmentIds: [data.id]
|
|
13009
13004
|
},
|
|
13010
13005
|
why: "The upload alone posts nothing; send a message that links the attachment id."
|
|
@@ -13012,6 +13007,69 @@ async function uploadAttachment(client, transport, request) {
|
|
|
13012
13007
|
text: formatAgentAttachmentUploaded(data)
|
|
13013
13008
|
};
|
|
13014
13009
|
}
|
|
13010
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13011
|
+
/**
|
|
13012
|
+
* Direct upload for files at or above the Server's threshold, mirroring the
|
|
13013
|
+
* CLI: create a session, PUT the bytes to the presigned URL (fetch; one retry
|
|
13014
|
+
* when the object store may have committed), then complete, which the Server
|
|
13015
|
+
* verifies against the object store. A PUT that definitely failed cancels the
|
|
13016
|
+
* session; a PUT whose outcome is unknown leaves it for completion to verify.
|
|
13017
|
+
*/
|
|
13018
|
+
async function uploadThroughSession(client, transport, request, channelId, mimeType) {
|
|
13019
|
+
const created = await client.attachments.createUploadSession({
|
|
13020
|
+
channelId,
|
|
13021
|
+
filename: request.filename,
|
|
13022
|
+
mimeType,
|
|
13023
|
+
sizeBytes: request.bytes.byteLength,
|
|
13024
|
+
clientRequestId: crypto.randomUUID()
|
|
13025
|
+
});
|
|
13026
|
+
if (!created.ok) return failureFromClientResult(created);
|
|
13027
|
+
const { uploadId, upload } = created.data;
|
|
13028
|
+
const body = new Uint8Array(request.bytes.byteLength);
|
|
13029
|
+
body.set(request.bytes);
|
|
13030
|
+
let definitelyFailed = null;
|
|
13031
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
13032
|
+
let response;
|
|
13033
|
+
try {
|
|
13034
|
+
response = await transport.fetch(upload.url, {
|
|
13035
|
+
method: upload.method ?? "PUT",
|
|
13036
|
+
headers: { ...upload.headers },
|
|
13037
|
+
body: body.buffer,
|
|
13038
|
+
redirect: "error"
|
|
13039
|
+
});
|
|
13040
|
+
} catch {
|
|
13041
|
+
if (attempt === 0) continue;
|
|
13042
|
+
break;
|
|
13043
|
+
}
|
|
13044
|
+
if (response.ok || response.status === 412) {
|
|
13045
|
+
definitelyFailed = null;
|
|
13046
|
+
break;
|
|
13047
|
+
}
|
|
13048
|
+
const mayExist = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
13049
|
+
if (mayExist && attempt === 0) continue;
|
|
13050
|
+
if (!mayExist) definitelyFailed = response.status;
|
|
13051
|
+
break;
|
|
13052
|
+
}
|
|
13053
|
+
if (definitelyFailed !== null) {
|
|
13054
|
+
await client.attachments.cancelUploadSession({ uploadId }).catch(() => void 0);
|
|
13055
|
+
return failureOutcome(opError("HTTP_ERROR", {
|
|
13056
|
+
message: `Direct object upload failed with HTTP ${definitelyFailed}; the upload session was cancelled.`,
|
|
13057
|
+
status: definitelyFailed,
|
|
13058
|
+
nextAction: "Retry the upload; nothing was attached."
|
|
13059
|
+
}));
|
|
13060
|
+
}
|
|
13061
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
13062
|
+
const completed = await client.attachments.completeUploadSession({ uploadId });
|
|
13063
|
+
if (completed.ok) {
|
|
13064
|
+
const attachment = completed.data.attachment;
|
|
13065
|
+
return uploadedOutcome(request.target, attachment, mimeType);
|
|
13066
|
+
}
|
|
13067
|
+
const code = completed.error.kind === "http" ? completed.error.errorCode : null;
|
|
13068
|
+
if (!(code === "UPLOAD_OBJECT_NOT_FOUND" || code === "UPLOAD_VERIFICATION_IN_PROGRESS") || attempt === 2) return failureFromClientResult(completed);
|
|
13069
|
+
await wait(250 * (attempt + 1));
|
|
13070
|
+
}
|
|
13071
|
+
return failureOutcome(opError("UNAVAILABLE", { message: "Direct upload completion ended without a terminal response." }));
|
|
13072
|
+
}
|
|
13015
13073
|
async function downloadAttachment(client, request) {
|
|
13016
13074
|
if (!request.attachmentId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "An attachment id is required." }));
|
|
13017
13075
|
const result = await client.attachments.download({ attachmentId: request.attachmentId });
|
|
@@ -13613,11 +13671,210 @@ async function searchManual(client, request) {
|
|
|
13613
13671
|
};
|
|
13614
13672
|
}
|
|
13615
13673
|
//#endregion
|
|
13674
|
+
//#region ../shared/src/agentOps/state.ts
|
|
13675
|
+
const RAFT_STATE_SCHEMA = "raft-sdk-state.v1";
|
|
13676
|
+
const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
|
|
13677
|
+
const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
|
|
13678
|
+
function isNullableCursor(value) {
|
|
13679
|
+
return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
13680
|
+
}
|
|
13681
|
+
/** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
|
|
13682
|
+
function parseRaftState(value) {
|
|
13683
|
+
if (!value || typeof value !== "object") return null;
|
|
13684
|
+
const v = value;
|
|
13685
|
+
if (v.schema !== "raft-sdk-state.v1") return null;
|
|
13686
|
+
if (typeof v.version !== "number" || !Number.isInteger(v.version) || v.version < 0) return null;
|
|
13687
|
+
if (!isNullableCursor(v.cursor ?? null) || !isNullableCursor(v.pendingCursor ?? null)) return null;
|
|
13688
|
+
const frontier = v.frontier;
|
|
13689
|
+
const continuations = Array.isArray(v.continuations) ? v.continuations.filter((c) => {
|
|
13690
|
+
const r = c;
|
|
13691
|
+
return Boolean(r) && typeof r.target === "string" && typeof r.idempotencyKey === "string" && typeof r.contentHash === "string" && typeof r.heldAt === "string";
|
|
13692
|
+
}) : [];
|
|
13693
|
+
return {
|
|
13694
|
+
schema: RAFT_STATE_SCHEMA,
|
|
13695
|
+
version: v.version,
|
|
13696
|
+
cursor: v.cursor ?? null,
|
|
13697
|
+
pendingCursor: v.pendingCursor ?? null,
|
|
13698
|
+
frontier: frontier && frontier.version === 1 ? frontier : {
|
|
13699
|
+
version: 1,
|
|
13700
|
+
targets: {},
|
|
13701
|
+
aliases: {}
|
|
13702
|
+
},
|
|
13703
|
+
continuations
|
|
13704
|
+
};
|
|
13705
|
+
}
|
|
13706
|
+
/** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
|
|
13707
|
+
async function hashRaftSendContent(target, content, attachmentIds = []) {
|
|
13708
|
+
const bytes = new TextEncoder().encode(`${target}\n${[...attachmentIds].sort().join(",")}\n${content}`);
|
|
13709
|
+
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
13710
|
+
}
|
|
13711
|
+
/**
|
|
13712
|
+
* One client's view of the persisted state: loads once, holds the working
|
|
13713
|
+
* copy, saves on request. Also used without a store (memory only), so the
|
|
13714
|
+
* pending/committed cursor flow behaves the same with or without persistence.
|
|
13715
|
+
*/
|
|
13716
|
+
var RaftStateSession = class {
|
|
13717
|
+
store;
|
|
13718
|
+
onError;
|
|
13719
|
+
frontier;
|
|
13720
|
+
cursor = null;
|
|
13721
|
+
pendingCursor = null;
|
|
13722
|
+
continuations = [];
|
|
13723
|
+
loadedVersion = void 0;
|
|
13724
|
+
loading = null;
|
|
13725
|
+
dirty = false;
|
|
13726
|
+
constructor(store, frontier, onError) {
|
|
13727
|
+
this.store = store;
|
|
13728
|
+
this.onError = onError;
|
|
13729
|
+
this.frontier = frontier;
|
|
13730
|
+
}
|
|
13731
|
+
get persistent() {
|
|
13732
|
+
return this.store !== void 0;
|
|
13733
|
+
}
|
|
13734
|
+
/** The version loaded from (or last saved to) the store; `undefined` before the first save of an empty store. */
|
|
13735
|
+
get version() {
|
|
13736
|
+
return this.loadedVersion;
|
|
13737
|
+
}
|
|
13738
|
+
/** Load once. A failed or unreadable load starts from an empty state (reported, not thrown). */
|
|
13739
|
+
ensureLoaded() {
|
|
13740
|
+
if (!this.store) return Promise.resolve();
|
|
13741
|
+
this.loading ??= (async () => {
|
|
13742
|
+
let raw = null;
|
|
13743
|
+
try {
|
|
13744
|
+
raw = await this.store.load();
|
|
13745
|
+
} catch (error) {
|
|
13746
|
+
this.onError?.(error, {
|
|
13747
|
+
phase: "load",
|
|
13748
|
+
version: void 0
|
|
13749
|
+
});
|
|
13750
|
+
return;
|
|
13751
|
+
}
|
|
13752
|
+
const state = parseRaftState(raw);
|
|
13753
|
+
if (!state) return;
|
|
13754
|
+
this.loadedVersion = state.version;
|
|
13755
|
+
this.cursor = state.cursor;
|
|
13756
|
+
this.pendingCursor = state.pendingCursor;
|
|
13757
|
+
this.frontier.absorb(state.frontier);
|
|
13758
|
+
this.continuations = state.continuations ?? [];
|
|
13759
|
+
})();
|
|
13760
|
+
return this.loading;
|
|
13761
|
+
}
|
|
13762
|
+
markDirty() {
|
|
13763
|
+
this.dirty = true;
|
|
13764
|
+
}
|
|
13765
|
+
/** Record the cursor of a batch just returned. Acknowledges nothing. */
|
|
13766
|
+
setPending(cursor) {
|
|
13767
|
+
if (cursor === null || cursor === this.pendingCursor) return;
|
|
13768
|
+
this.pendingCursor = cursor;
|
|
13769
|
+
this.dirty = true;
|
|
13770
|
+
}
|
|
13771
|
+
/**
|
|
13772
|
+
* Promote a cursor to committed. With no argument, commits `pendingCursor`.
|
|
13773
|
+
* Returns the committed cursor, or null when there was nothing to commit.
|
|
13774
|
+
* Never moves the committed cursor backwards.
|
|
13775
|
+
*/
|
|
13776
|
+
commit(cursor) {
|
|
13777
|
+
const next = cursor === void 0 ? this.pendingCursor : cursor;
|
|
13778
|
+
if (next === null || next === void 0) return null;
|
|
13779
|
+
if (this.cursor === null || next > this.cursor) this.cursor = next;
|
|
13780
|
+
if (this.pendingCursor !== null && this.pendingCursor <= this.cursor) this.pendingCursor = null;
|
|
13781
|
+
this.dirty = true;
|
|
13782
|
+
return this.cursor;
|
|
13783
|
+
}
|
|
13784
|
+
findContinuation(target, contentHash) {
|
|
13785
|
+
return this.continuations.find((c) => c.target === target && c.contentHash === contentHash);
|
|
13786
|
+
}
|
|
13787
|
+
rememberContinuation(entry) {
|
|
13788
|
+
this.continuations = this.continuations.filter((c) => !(c.target === entry.target && c.contentHash === entry.contentHash));
|
|
13789
|
+
this.continuations.push(entry);
|
|
13790
|
+
const perTarget = this.continuations.filter((c) => c.target === entry.target);
|
|
13791
|
+
for (const drop of perTarget.slice(0, Math.max(0, perTarget.length - 3))) this.continuations.splice(this.continuations.indexOf(drop), 1);
|
|
13792
|
+
if (this.continuations.length > 20) this.continuations.splice(0, this.continuations.length - 20);
|
|
13793
|
+
this.dirty = true;
|
|
13794
|
+
}
|
|
13795
|
+
forgetContinuation(target, contentHash) {
|
|
13796
|
+
const before = this.continuations.length;
|
|
13797
|
+
this.continuations = this.continuations.filter((c) => !(c.target === target && c.contentHash === contentHash));
|
|
13798
|
+
if (this.continuations.length !== before) this.dirty = true;
|
|
13799
|
+
}
|
|
13800
|
+
snapshot(nextVersion = (this.loadedVersion ?? 0) + 1) {
|
|
13801
|
+
return {
|
|
13802
|
+
schema: RAFT_STATE_SCHEMA,
|
|
13803
|
+
version: nextVersion,
|
|
13804
|
+
cursor: this.cursor,
|
|
13805
|
+
pendingCursor: this.pendingCursor,
|
|
13806
|
+
frontier: this.frontier.snapshot(),
|
|
13807
|
+
continuations: [...this.continuations]
|
|
13808
|
+
};
|
|
13809
|
+
}
|
|
13810
|
+
/**
|
|
13811
|
+
* Save if anything changed (or `force`). One attempt; a failure is reported
|
|
13812
|
+
* and swallowed. Returns whether the save succeeded (true when nothing to save).
|
|
13813
|
+
*/
|
|
13814
|
+
async save(options = {}) {
|
|
13815
|
+
if (!this.store) return true;
|
|
13816
|
+
if (!this.dirty && !options.force) return true;
|
|
13817
|
+
const expectedVersion = this.loadedVersion;
|
|
13818
|
+
const state = this.snapshot();
|
|
13819
|
+
try {
|
|
13820
|
+
await this.store.save(state, { expectedVersion });
|
|
13821
|
+
} catch (error) {
|
|
13822
|
+
this.onError?.(error, {
|
|
13823
|
+
phase: "save",
|
|
13824
|
+
version: expectedVersion
|
|
13825
|
+
});
|
|
13826
|
+
return false;
|
|
13827
|
+
}
|
|
13828
|
+
this.loadedVersion = state.version;
|
|
13829
|
+
this.dirty = false;
|
|
13830
|
+
return true;
|
|
13831
|
+
}
|
|
13832
|
+
};
|
|
13833
|
+
//#endregion
|
|
13834
|
+
//#region ../shared/src/agentOps/actions.ts
|
|
13835
|
+
/** Text is the CLI's `raft action prepare` output. */
|
|
13836
|
+
function formatAgentActionCardPosted(target, messageId) {
|
|
13837
|
+
const shortId = messageId ? messageId.slice(0, 8) : null;
|
|
13838
|
+
return shortId ? `Action card posted to ${target} as message ${messageId} (short ${shortId}). The human can click the action verb to commit.\n` : `Action card posted to ${target}.\n`;
|
|
13839
|
+
}
|
|
13840
|
+
async function prepareActionCard(client, request) {
|
|
13841
|
+
if (!request?.target?.trim() || !request.action) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and an action are required to prepare a card." }));
|
|
13842
|
+
const result = await client.actions.prepare(request);
|
|
13843
|
+
if (!result.ok) return failureFromClientResult(result);
|
|
13844
|
+
const card = {
|
|
13845
|
+
target: request.target,
|
|
13846
|
+
messageId: result.data.messageId
|
|
13847
|
+
};
|
|
13848
|
+
return {
|
|
13849
|
+
ok: true,
|
|
13850
|
+
state: "prepared",
|
|
13851
|
+
data: card,
|
|
13852
|
+
next: {
|
|
13853
|
+
kind: "await_confirmation",
|
|
13854
|
+
command: `raft message read --target "${request.target}" --around ${card.messageId.slice(0, 8)}`,
|
|
13855
|
+
args: {
|
|
13856
|
+
target: request.target,
|
|
13857
|
+
messageId: card.messageId
|
|
13858
|
+
},
|
|
13859
|
+
why: "A human must click the card to commit it; its outcome arrives in your inbox."
|
|
13860
|
+
},
|
|
13861
|
+
text: formatAgentActionCardPosted(request.target, card.messageId)
|
|
13862
|
+
};
|
|
13863
|
+
}
|
|
13864
|
+
//#endregion
|
|
13616
13865
|
//#region src/raft.ts
|
|
13617
13866
|
function createRaft(options) {
|
|
13618
13867
|
const serverUrl = requireServerUrl(options.serverUrl);
|
|
13619
13868
|
const authorization = `Bearer ${requireAgentCredential(options.credential)}`;
|
|
13620
13869
|
const frontier = SeenFrontier.fromSnapshot(options.frontier);
|
|
13870
|
+
const session = new RaftStateSession(options.state, frontier, options.onStateSaveError);
|
|
13871
|
+
/** Load before, run, then save if the operation succeeded and changed the state. */
|
|
13872
|
+
const withState = async (run) => {
|
|
13873
|
+
await session.ensureLoaded();
|
|
13874
|
+
const outcome = await run();
|
|
13875
|
+
if (outcome.ok) await session.save();
|
|
13876
|
+
return outcome;
|
|
13877
|
+
};
|
|
13621
13878
|
const routes = createRaftRoutes({
|
|
13622
13879
|
serverUrl,
|
|
13623
13880
|
fetch: options.fetch,
|
|
@@ -13638,6 +13895,27 @@ function createRaft(options) {
|
|
|
13638
13895
|
throttle: options.throttle,
|
|
13639
13896
|
auth: { authorization }
|
|
13640
13897
|
} });
|
|
13898
|
+
/**
|
|
13899
|
+
* Send through the state session: a resend of the same logical message
|
|
13900
|
+
* (same target, content, attachments) reuses the held send's idempotency key;
|
|
13901
|
+
* a hold is remembered; a successful send clears it.
|
|
13902
|
+
*/
|
|
13903
|
+
const sendWithState = (request) => withState(async () => {
|
|
13904
|
+
const contentHash = await hashRaftSendContent(request.target, request.content ?? "", request.attachmentIds ?? []);
|
|
13905
|
+
const pending = request.idempotencyKey ? void 0 : session.findContinuation(request.target, contentHash);
|
|
13906
|
+
const outcome = await sendMessage(routes, pending ? {
|
|
13907
|
+
...request,
|
|
13908
|
+
idempotencyKey: pending.idempotencyKey
|
|
13909
|
+
} : request, frontier);
|
|
13910
|
+
if (outcome.ok && outcome.state === "held") session.rememberContinuation({
|
|
13911
|
+
target: request.target,
|
|
13912
|
+
idempotencyKey: outcome.data.idempotencyKey,
|
|
13913
|
+
contentHash,
|
|
13914
|
+
heldAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13915
|
+
});
|
|
13916
|
+
else if (outcome.ok && outcome.state === "sent") session.forgetContinuation(request.target, contentHash);
|
|
13917
|
+
return outcome;
|
|
13918
|
+
});
|
|
13641
13919
|
return {
|
|
13642
13920
|
identity: { whoami: () => getRaftContext(routes) },
|
|
13643
13921
|
wake: {
|
|
@@ -13649,14 +13927,53 @@ function createRaft(options) {
|
|
|
13649
13927
|
}
|
|
13650
13928
|
},
|
|
13651
13929
|
inbox: {
|
|
13652
|
-
check: (request) =>
|
|
13653
|
-
|
|
13930
|
+
check: (request = {}) => withState(async () => {
|
|
13931
|
+
const since = request.since ?? session.cursor ?? void 0;
|
|
13932
|
+
const outcome = await checkInbox(inboxApi, {
|
|
13933
|
+
...request,
|
|
13934
|
+
...since === void 0 ? {} : { since }
|
|
13935
|
+
}, frontier);
|
|
13936
|
+
if (outcome.ok) {
|
|
13937
|
+
session.markDirty();
|
|
13938
|
+
if (since !== void 0 && (request.ack ?? "cursor") === "cursor") session.commit(since);
|
|
13939
|
+
if (outcome.data.ackMode === "cursor" && outcome.data.messages.length > 0) session.setPending(outcome.data.cursor);
|
|
13940
|
+
}
|
|
13941
|
+
return outcome;
|
|
13942
|
+
}),
|
|
13943
|
+
commit: async (target) => {
|
|
13944
|
+
await session.ensureLoaded();
|
|
13945
|
+
const cursor = session.commit(target === void 0 ? void 0 : target.cursor);
|
|
13946
|
+
return {
|
|
13947
|
+
cursor,
|
|
13948
|
+
saved: cursor === null ? true : await session.save()
|
|
13949
|
+
};
|
|
13950
|
+
},
|
|
13951
|
+
drain: (request = {}) => (async function* () {
|
|
13952
|
+
await session.ensureLoaded();
|
|
13953
|
+
const since = request.since ?? session.cursor ?? void 0;
|
|
13954
|
+
return yield* drainInbox(inboxApi, {
|
|
13955
|
+
...request,
|
|
13956
|
+
...since === void 0 ? {} : { since }
|
|
13957
|
+
}, frontier, async (sent, batch) => {
|
|
13958
|
+
if (sent !== null) session.commit(sent);
|
|
13959
|
+
if (batch.ackMode === "cursor" && batch.messages.length > 0) session.setPending(batch.cursor);
|
|
13960
|
+
session.markDirty();
|
|
13961
|
+
await session.save();
|
|
13962
|
+
});
|
|
13963
|
+
})(),
|
|
13654
13964
|
list: (request) => listInbox(routes, request)
|
|
13655
13965
|
},
|
|
13656
13966
|
messages: {
|
|
13657
|
-
read: (request) =>
|
|
13658
|
-
|
|
13659
|
-
|
|
13967
|
+
read: (request) => withState(async () => {
|
|
13968
|
+
const outcome = await readHistory(routes, request, frontier);
|
|
13969
|
+
if (outcome.ok) session.markDirty();
|
|
13970
|
+
return outcome;
|
|
13971
|
+
}),
|
|
13972
|
+
send: (request) => sendWithState(request),
|
|
13973
|
+
reply: (message, request) => sendWithState({
|
|
13974
|
+
...request,
|
|
13975
|
+
target: message.target
|
|
13976
|
+
}),
|
|
13660
13977
|
search: (request) => searchMessages(routes, request),
|
|
13661
13978
|
resolve: (request) => resolveMessage(routes, request),
|
|
13662
13979
|
react: (request) => reactToMessage(routes, request, "add"),
|
|
@@ -13677,6 +13994,7 @@ function createRaft(options) {
|
|
|
13677
13994
|
execute: (request) => executeMentionAction(routes, request),
|
|
13678
13995
|
deliveries: (request) => senderMentionDeliveries(routes, request)
|
|
13679
13996
|
},
|
|
13997
|
+
actions: { prepare: (request) => prepareActionCard(routes, request) },
|
|
13680
13998
|
manual: {
|
|
13681
13999
|
get: (request) => getManualTopic(routes, request),
|
|
13682
14000
|
search: (request) => searchManual(routes, request)
|
|
@@ -13710,8 +14028,17 @@ function createRaft(options) {
|
|
|
13710
14028
|
update: (request) => updateProfile(routes, request)
|
|
13711
14029
|
},
|
|
13712
14030
|
frontier,
|
|
14031
|
+
state: {
|
|
14032
|
+
save: async () => {
|
|
14033
|
+
await session.ensureLoaded();
|
|
14034
|
+
session.markDirty();
|
|
14035
|
+
return session.save();
|
|
14036
|
+
},
|
|
14037
|
+
load: () => session.ensureLoaded(),
|
|
14038
|
+
snapshot: () => session.snapshot(session.version ?? 0)
|
|
14039
|
+
},
|
|
13713
14040
|
routes
|
|
13714
14041
|
};
|
|
13715
14042
|
}
|
|
13716
14043
|
//#endregion
|
|
13717
|
-
export { RAFT_INBOX_NOTICE_SCHEMA, RAFT_NOTICE_DELIVERY_ID_HEADER, RAFT_NOTICE_SIGNATURE_HEADER, RaftCredentialError, RaftSdkConfigurationError, SeenFrontier, bootstrapRaftCredential, createFileCredentialStore, createRaft, createRaftClient, createRaftClientFromStore, createRaftRoutes, describeRaftRoute, hasAgentMessageIdentity, inferAttachmentMimeType, joinRaftChannelByTarget, listRaftRoutes, parseRaftRegularChannelTarget, projectRaftMessage, projectRaftMessages, verifyInboxNotice };
|
|
14044
|
+
export { RAFT_INBOX_NOTICE_SCHEMA, RAFT_NOTICE_DELIVERY_ID_HEADER, RAFT_NOTICE_SIGNATURE_HEADER, RAFT_STATE_CONTINUATIONS_PER_TARGET, RAFT_STATE_CONTINUATIONS_TOTAL, RAFT_STATE_SCHEMA, RaftCredentialError, RaftSdkConfigurationError, SeenFrontier, bootstrapRaftCredential, createFileCredentialStore, createRaft, createRaftClient, createRaftClientFromStore, createRaftRoutes, describeRaftRoute, hasAgentMessageIdentity, hashRaftSendContent, inferAttachmentMimeType, joinRaftChannelByTarget, listRaftRoutes, parseRaftRegularChannelTarget, parseRaftState, projectRaftMessage, projectRaftMessages, verifyInboxNotice };
|
package/dist/index.d.ts
CHANGED
|
@@ -444,6 +444,8 @@ export declare class SeenFrontier {
|
|
|
444
444
|
private readonly targets;
|
|
445
445
|
private readonly aliases;
|
|
446
446
|
static fromSnapshot(snapshot: SeenFrontierSnapshot | null | undefined): SeenFrontier;
|
|
447
|
+
/** Merge a snapshot into this frontier by the same monotonic rules (never lowers a mark). */
|
|
448
|
+
absorb(snapshot: SeenFrontierSnapshot | null | undefined): void;
|
|
447
449
|
/** Remember that the Server resolved `requested` to `canonical` (a thread spelling, a `~agent` suffix, …). */
|
|
448
450
|
recordAlias(requested: string, canonical: string): void;
|
|
449
451
|
canonical(target: string): string;
|
|
@@ -935,6 +937,57 @@ interface ManualContext {
|
|
|
935
937
|
reason: string;
|
|
936
938
|
}
|
|
937
939
|
//#endregion
|
|
940
|
+
//#region ../shared/src/agentOps/state.d.ts
|
|
941
|
+
export declare const RAFT_STATE_SCHEMA: "raft-sdk-state.v1";
|
|
942
|
+
interface RaftStateContinuation {
|
|
943
|
+
target: string;
|
|
944
|
+
idempotencyKey: string;
|
|
945
|
+
/** Hex SHA-256 of target, content and attachment ids; a resend with the same hash is the same logical message. */
|
|
946
|
+
contentHash: string;
|
|
947
|
+
/** ISO time the send was held. */
|
|
948
|
+
heldAt: string;
|
|
949
|
+
}
|
|
950
|
+
interface RaftState {
|
|
951
|
+
schema: typeof RAFT_STATE_SCHEMA;
|
|
952
|
+
/** Revision, incremented on every save by this SDK. Compare-and-set stores compare against it. */
|
|
953
|
+
version: number;
|
|
954
|
+
/** Last committed inbox cursor; the next pull passes it as `since`. */
|
|
955
|
+
cursor: number | null;
|
|
956
|
+
/** Cursor of the batch returned but not yet committed. */
|
|
957
|
+
pendingCursor: number | null;
|
|
958
|
+
frontier: SeenFrontierSnapshot;
|
|
959
|
+
continuations?: RaftStateContinuation[];
|
|
960
|
+
}
|
|
961
|
+
interface RaftStateStore {
|
|
962
|
+
load(): Promise<RaftState | null>;
|
|
963
|
+
/**
|
|
964
|
+
* Persist `state`. `expectedVersion` is the version this client loaded
|
|
965
|
+
* (`undefined` when the store was empty). Throw to reject a stale write;
|
|
966
|
+
* stores without compare-and-set may ignore it.
|
|
967
|
+
*/
|
|
968
|
+
save(state: RaftState, options: {
|
|
969
|
+
expectedVersion: number | undefined;
|
|
970
|
+
}): Promise<void>;
|
|
971
|
+
}
|
|
972
|
+
type RaftStateSaveErrorHandler = (error: unknown, context: {
|
|
973
|
+
phase: "load" | "save";
|
|
974
|
+
version: number | undefined;
|
|
975
|
+
}) => void;
|
|
976
|
+
export declare const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
|
|
977
|
+
export declare const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
|
|
978
|
+
/** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
|
|
979
|
+
export declare function parseRaftState(value: unknown): RaftState | null;
|
|
980
|
+
/** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
|
|
981
|
+
export declare function hashRaftSendContent(target: string, content: string, attachmentIds?: readonly string[]): Promise<string>;
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region ../shared/src/agentOps/actions.d.ts
|
|
984
|
+
type PrepareActionCardRequest = AgentApiActionPrepareBody;
|
|
985
|
+
interface RaftPreparedCard {
|
|
986
|
+
target: string;
|
|
987
|
+
/** The card message; a human commits it by clicking its action verb. */
|
|
988
|
+
messageId: string;
|
|
989
|
+
}
|
|
990
|
+
//#endregion
|
|
938
991
|
//#region ../shared/src/agentApiRawClient.d.ts
|
|
939
992
|
type AgentApiRawClientErrorReason = "missing_route" | "missing_path_param" | "request_contract_mismatch" | "transport_error" | "http_error" | "empty_response" | "response_contract_mismatch";
|
|
940
993
|
interface AgentApiRawTransportRequest<K extends AgentApiRouteKey = AgentApiRouteKey> {
|
|
@@ -9299,6 +9352,22 @@ interface CreateRaftOptions {
|
|
|
9299
9352
|
* is held once and returns the unread context, which is the safe default.
|
|
9300
9353
|
*/
|
|
9301
9354
|
frontier?: SeenFrontierSnapshot | null;
|
|
9355
|
+
/**
|
|
9356
|
+
* Persist the client's state (committed and pending inbox cursors, the seen
|
|
9357
|
+
* frontier, held-send keys) across tool calls and processes. The store
|
|
9358
|
+
* implements `load()` and `save(state, { expectedVersion })`; the SDK loads
|
|
9359
|
+
* once before the first operation and saves after each successful operation
|
|
9360
|
+
* that changed the state (one attempt, never fails the operation).
|
|
9361
|
+
*/
|
|
9362
|
+
state?: RaftStateStore;
|
|
9363
|
+
/** Called when loading or saving the state fails or is rejected as stale. */
|
|
9364
|
+
onStateSaveError?: RaftStateSaveErrorHandler;
|
|
9365
|
+
}
|
|
9366
|
+
interface RaftInboxCommitResult {
|
|
9367
|
+
/** The committed cursor, or null when there was nothing to commit. */
|
|
9368
|
+
cursor: number | null;
|
|
9369
|
+
/** Whether the state was persisted (true when no store is configured). */
|
|
9370
|
+
saved: boolean;
|
|
9302
9371
|
}
|
|
9303
9372
|
interface Raft {
|
|
9304
9373
|
identity: {
|
|
@@ -9320,8 +9389,22 @@ interface Raft {
|
|
|
9320
9389
|
};
|
|
9321
9390
|
};
|
|
9322
9391
|
inbox: {
|
|
9323
|
-
/**
|
|
9392
|
+
/**
|
|
9393
|
+
* One bounded pull. Nothing is acknowledged by it. Without `since`, sends
|
|
9394
|
+
* the last committed cursor (from `state`), which is what acknowledges the
|
|
9395
|
+
* previously committed batch on the Server; the returned batch's cursor is
|
|
9396
|
+
* recorded as pending until you `commit()` it.
|
|
9397
|
+
*/
|
|
9324
9398
|
check(request?: CheckInboxRequest): Promise<CheckInboxOutcome>;
|
|
9399
|
+
/**
|
|
9400
|
+
* Mark a batch as processed. With no argument, commits the pending cursor
|
|
9401
|
+
* from the last `check()` (possibly in an earlier process, via `state`);
|
|
9402
|
+
* also accepts `{ cursor }` or a batch. Records only; the next `check()`
|
|
9403
|
+
* acknowledges it on the Server. The SDK never commits on its own.
|
|
9404
|
+
*/
|
|
9405
|
+
commit(target?: {
|
|
9406
|
+
cursor: number | null;
|
|
9407
|
+
}): Promise<RaftInboxCommitResult>;
|
|
9325
9408
|
/**
|
|
9326
9409
|
* Pull until the Server reports nothing more, like `raft message check`, as
|
|
9327
9410
|
* an async iterator: the pull that acknowledges a batch is only sent when
|
|
@@ -9376,6 +9459,14 @@ interface Raft {
|
|
|
9376
9459
|
messageId: string;
|
|
9377
9460
|
}): Promise<RaftOutcome<AgentApiSenderMentionDeliveriesResponse, "deliveries" | "empty">>;
|
|
9378
9461
|
};
|
|
9462
|
+
actions: {
|
|
9463
|
+
/**
|
|
9464
|
+
* Post an action card for a human to confirm (channel:create,
|
|
9465
|
+
* channel:add_member, agent:create, and the integration card types). The
|
|
9466
|
+
* human who clicks it executes it as themselves.
|
|
9467
|
+
*/
|
|
9468
|
+
prepare(request: PrepareActionCardRequest): Promise<RaftOutcome<RaftPreparedCard, "prepared">>;
|
|
9469
|
+
};
|
|
9379
9470
|
manual: {
|
|
9380
9471
|
/** Fetch a Manual topic; `intent` and `reason` are required and must never carry prompts, credentials, or message payloads. */
|
|
9381
9472
|
get(request: {
|
|
@@ -9455,9 +9546,17 @@ interface Raft {
|
|
|
9455
9546
|
};
|
|
9456
9547
|
/** What this process has shown its model, per target; export it to survive restarts. */
|
|
9457
9548
|
frontier: SeenFrontier;
|
|
9549
|
+
state: {
|
|
9550
|
+
/** Save now (for example after `frontier.recordHeld`). Best effort; returns whether it succeeded. */
|
|
9551
|
+
save(): Promise<boolean>;
|
|
9552
|
+
/** Load the persisted state now (operations do this automatically). */
|
|
9553
|
+
load(): Promise<void>;
|
|
9554
|
+
/** The current state value, as it would be saved. */
|
|
9555
|
+
snapshot(): RaftState;
|
|
9556
|
+
};
|
|
9458
9557
|
/** Every Agent API route, typed from the shared contract. */
|
|
9459
9558
|
routes: RaftRoutes;
|
|
9460
9559
|
}
|
|
9461
9560
|
export declare function createRaft(options: CreateRaftOptions): Raft;
|
|
9462
9561
|
//#endregion
|
|
9463
|
-
export type { AmendTaskOutcome, AmendTaskRequest, AssignTaskRequest, BootstrapRaftCredentialOptions, CheckInboxOutcome, CheckInboxRequest, ClaimTasksOutcome, ClaimTasksRequest, CreateRaftClientFromStoreOptions, CreateRaftClientOptions, CreateRaftOptions, CreateRaftRoutesOptions, CreateTasksRequest, DrainInboxRequest, ExecuteMentionActionRequest, ListInboxRequest, ListTasksRequest, ManualContext, Raft, RaftAckMode, RaftActionPrepareRequest, RaftActionPrepared, RaftApiError, RaftApiResult, RaftAppConfig, RaftAppConfigPatch, RaftAttachmentBytes, RaftAttachmentUploaded, RaftAvatarUpload, RaftChannelJoinClient, RaftChannelJoinClientResult, RaftChannelJoinError, RaftChannelJoinFailure, RaftChannelJoinOperation, RaftChannelJoinRequest, RaftChannelJoinResult, RaftChannelJoinSuccess, RaftChannelJoinTransportError, RaftChannelMuteState, RaftChannelRef, RaftClaimHeld, RaftClaimHeldLike, RaftClaimResult, RaftClaimRow, RaftClaimRowState, RaftClient, RaftClientError, RaftClientFailure, RaftClientResult, RaftClientSuccess, RaftClientThrottleOptions, RaftClientTransportRequest, RaftContextAgent, RaftContextData, RaftContextError, RaftContextResult, RaftContextServer, RaftCredentialErrorCode, RaftCredentialIdentity, RaftCredentialStore, RaftEvent, RaftEventAttachment, RaftEventExternalMessage, RaftEventsReceiveData, RaftEventsReceiveError, RaftEventsReceiveRequest, RaftEventsReceiveResult, RaftFailure, RaftHeld, RaftHeldBase, RaftHistoryPage, RaftInboxBatch, RaftInboxConversation, RaftInboxDrainSummary, RaftInboxListing, RaftInboxNotice, RaftManageClient, RaftMessage, RaftMessageAttachment, RaftMessageTask, RaftNextStep, RaftNoticeFlag, RaftNoticeTarget, RaftOpError, RaftOpErrorCode, RaftOutcome, RaftPendingMentions, RaftProfile, RaftProfileUpdate, RaftRouteAnnotations, RaftRouteInfo, RaftRouteKey, RaftRouteMeta, RaftRouteResult, RaftRouteRetryPolicy, RaftRoutes, RaftSdkConfigurationErrorCode, RaftSearchPage, RaftSendContinuation, RaftSenderType, RaftSent, RaftServerInfo, RaftServerProfile, RaftServerUpdate, RaftTaskBoard, RaftTaskStatus, RaftTasksCreated, RaftWebhookStatus, ReactRequest, ReadHistoryRequest, SearchMessagesRequest, SeenAttestation, SeenFrontierSnapshot, SendMessageOutcome, SendMessageRequest, ServerInfoRequest, ServerInfoSection, StoredRaftCredential, TaskRef, UpdateTaskStatusOutcome, UpdateTaskStatusRequest, UploadAttachmentRequest, VerifyNoticeInput, VerifyNoticeRejection, VerifyNoticeResult };
|
|
9562
|
+
export type { AmendTaskOutcome, AmendTaskRequest, AssignTaskRequest, BootstrapRaftCredentialOptions, CheckInboxOutcome, CheckInboxRequest, ClaimTasksOutcome, ClaimTasksRequest, CreateRaftClientFromStoreOptions, CreateRaftClientOptions, CreateRaftOptions, CreateRaftRoutesOptions, CreateTasksRequest, DrainInboxRequest, ExecuteMentionActionRequest, ListInboxRequest, ListTasksRequest, ManualContext, PrepareActionCardRequest, Raft, RaftAckMode, RaftActionPrepareRequest, RaftActionPrepared, RaftApiError, RaftApiResult, RaftAppConfig, RaftAppConfigPatch, RaftAttachmentBytes, RaftAttachmentUploaded, RaftAvatarUpload, RaftChannelJoinClient, RaftChannelJoinClientResult, RaftChannelJoinError, RaftChannelJoinFailure, RaftChannelJoinOperation, RaftChannelJoinRequest, RaftChannelJoinResult, RaftChannelJoinSuccess, RaftChannelJoinTransportError, RaftChannelMuteState, RaftChannelRef, RaftClaimHeld, RaftClaimHeldLike, RaftClaimResult, RaftClaimRow, RaftClaimRowState, RaftClient, RaftClientError, RaftClientFailure, RaftClientResult, RaftClientSuccess, RaftClientThrottleOptions, RaftClientTransportRequest, RaftContextAgent, RaftContextData, RaftContextError, RaftContextResult, RaftContextServer, RaftCredentialErrorCode, RaftCredentialIdentity, RaftCredentialStore, RaftEvent, RaftEventAttachment, RaftEventExternalMessage, RaftEventsReceiveData, RaftEventsReceiveError, RaftEventsReceiveRequest, RaftEventsReceiveResult, RaftFailure, RaftHeld, RaftHeldBase, RaftHistoryPage, RaftInboxBatch, RaftInboxCommitResult, RaftInboxConversation, RaftInboxDrainSummary, RaftInboxListing, RaftInboxNotice, RaftManageClient, RaftMessage, RaftMessageAttachment, RaftMessageTask, RaftNextStep, RaftNoticeFlag, RaftNoticeTarget, RaftOpError, RaftOpErrorCode, RaftOutcome, RaftPendingMentions, RaftPreparedCard, RaftProfile, RaftProfileUpdate, RaftRouteAnnotations, RaftRouteInfo, RaftRouteKey, RaftRouteMeta, RaftRouteResult, RaftRouteRetryPolicy, RaftRoutes, RaftSdkConfigurationErrorCode, RaftSearchPage, RaftSendContinuation, RaftSenderType, RaftSent, RaftServerInfo, RaftServerProfile, RaftServerUpdate, RaftState, RaftStateContinuation, RaftStateSaveErrorHandler, RaftStateStore, RaftTaskBoard, RaftTaskStatus, RaftTasksCreated, RaftWebhookStatus, ReactRequest, ReadHistoryRequest, SearchMessagesRequest, SeenAttestation, SeenFrontierSnapshot, SendMessageOutcome, SendMessageRequest, ServerInfoRequest, ServerInfoSection, StoredRaftCredential, TaskRef, UpdateTaskStatusOutcome, UpdateTaskStatusRequest, UploadAttachmentRequest, VerifyNoticeInput, VerifyNoticeRejection, VerifyNoticeResult };
|