@botiverse/raft-sdk 1.0.0-alpha.3 → 1.0.0-alpha.4
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 +46 -0
- package/dist/cjs/index.cjs +258 -18
- package/dist/esm/index.js +254 -19
- package/dist/index.d.ts +85 -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
|
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
|
}
|
|
@@ -13614,11 +13612,179 @@ async function searchManual(client, request) {
|
|
|
13614
13612
|
};
|
|
13615
13613
|
}
|
|
13616
13614
|
//#endregion
|
|
13615
|
+
//#region ../shared/src/agentOps/state.ts
|
|
13616
|
+
const RAFT_STATE_SCHEMA = "raft-sdk-state.v1";
|
|
13617
|
+
const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
|
|
13618
|
+
const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
|
|
13619
|
+
function isNullableCursor(value) {
|
|
13620
|
+
return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
13621
|
+
}
|
|
13622
|
+
/** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
|
|
13623
|
+
function parseRaftState(value) {
|
|
13624
|
+
if (!value || typeof value !== "object") return null;
|
|
13625
|
+
const v = value;
|
|
13626
|
+
if (v.schema !== "raft-sdk-state.v1") return null;
|
|
13627
|
+
if (typeof v.version !== "number" || !Number.isInteger(v.version) || v.version < 0) return null;
|
|
13628
|
+
if (!isNullableCursor(v.cursor ?? null) || !isNullableCursor(v.pendingCursor ?? null)) return null;
|
|
13629
|
+
const frontier = v.frontier;
|
|
13630
|
+
const continuations = Array.isArray(v.continuations) ? v.continuations.filter((c) => {
|
|
13631
|
+
const r = c;
|
|
13632
|
+
return Boolean(r) && typeof r.target === "string" && typeof r.idempotencyKey === "string" && typeof r.contentHash === "string" && typeof r.heldAt === "string";
|
|
13633
|
+
}) : [];
|
|
13634
|
+
return {
|
|
13635
|
+
schema: RAFT_STATE_SCHEMA,
|
|
13636
|
+
version: v.version,
|
|
13637
|
+
cursor: v.cursor ?? null,
|
|
13638
|
+
pendingCursor: v.pendingCursor ?? null,
|
|
13639
|
+
frontier: frontier && frontier.version === 1 ? frontier : {
|
|
13640
|
+
version: 1,
|
|
13641
|
+
targets: {},
|
|
13642
|
+
aliases: {}
|
|
13643
|
+
},
|
|
13644
|
+
continuations
|
|
13645
|
+
};
|
|
13646
|
+
}
|
|
13647
|
+
/** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
|
|
13648
|
+
async function hashRaftSendContent(target, content, attachmentIds = []) {
|
|
13649
|
+
const bytes = new TextEncoder().encode(`${target}\n${[...attachmentIds].sort().join(",")}\n${content}`);
|
|
13650
|
+
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
13651
|
+
}
|
|
13652
|
+
/**
|
|
13653
|
+
* One client's view of the persisted state: loads once, holds the working
|
|
13654
|
+
* copy, saves on request. Also used without a store (memory only), so the
|
|
13655
|
+
* pending/committed cursor flow behaves the same with or without persistence.
|
|
13656
|
+
*/
|
|
13657
|
+
var RaftStateSession = class {
|
|
13658
|
+
store;
|
|
13659
|
+
onError;
|
|
13660
|
+
frontier;
|
|
13661
|
+
cursor = null;
|
|
13662
|
+
pendingCursor = null;
|
|
13663
|
+
continuations = [];
|
|
13664
|
+
loadedVersion = void 0;
|
|
13665
|
+
loading = null;
|
|
13666
|
+
dirty = false;
|
|
13667
|
+
constructor(store, frontier, onError) {
|
|
13668
|
+
this.store = store;
|
|
13669
|
+
this.onError = onError;
|
|
13670
|
+
this.frontier = frontier;
|
|
13671
|
+
}
|
|
13672
|
+
get persistent() {
|
|
13673
|
+
return this.store !== void 0;
|
|
13674
|
+
}
|
|
13675
|
+
/** The version loaded from (or last saved to) the store; `undefined` before the first save of an empty store. */
|
|
13676
|
+
get version() {
|
|
13677
|
+
return this.loadedVersion;
|
|
13678
|
+
}
|
|
13679
|
+
/** Load once. A failed or unreadable load starts from an empty state (reported, not thrown). */
|
|
13680
|
+
ensureLoaded() {
|
|
13681
|
+
if (!this.store) return Promise.resolve();
|
|
13682
|
+
this.loading ??= (async () => {
|
|
13683
|
+
let raw = null;
|
|
13684
|
+
try {
|
|
13685
|
+
raw = await this.store.load();
|
|
13686
|
+
} catch (error) {
|
|
13687
|
+
this.onError?.(error, {
|
|
13688
|
+
phase: "load",
|
|
13689
|
+
version: void 0
|
|
13690
|
+
});
|
|
13691
|
+
return;
|
|
13692
|
+
}
|
|
13693
|
+
const state = parseRaftState(raw);
|
|
13694
|
+
if (!state) return;
|
|
13695
|
+
this.loadedVersion = state.version;
|
|
13696
|
+
this.cursor = state.cursor;
|
|
13697
|
+
this.pendingCursor = state.pendingCursor;
|
|
13698
|
+
this.frontier.absorb(state.frontier);
|
|
13699
|
+
this.continuations = state.continuations ?? [];
|
|
13700
|
+
})();
|
|
13701
|
+
return this.loading;
|
|
13702
|
+
}
|
|
13703
|
+
markDirty() {
|
|
13704
|
+
this.dirty = true;
|
|
13705
|
+
}
|
|
13706
|
+
/** Record the cursor of a batch just returned. Acknowledges nothing. */
|
|
13707
|
+
setPending(cursor) {
|
|
13708
|
+
if (cursor === null || cursor === this.pendingCursor) return;
|
|
13709
|
+
this.pendingCursor = cursor;
|
|
13710
|
+
this.dirty = true;
|
|
13711
|
+
}
|
|
13712
|
+
/**
|
|
13713
|
+
* Promote a cursor to committed. With no argument, commits `pendingCursor`.
|
|
13714
|
+
* Returns the committed cursor, or null when there was nothing to commit.
|
|
13715
|
+
* Never moves the committed cursor backwards.
|
|
13716
|
+
*/
|
|
13717
|
+
commit(cursor) {
|
|
13718
|
+
const next = cursor === void 0 ? this.pendingCursor : cursor;
|
|
13719
|
+
if (next === null || next === void 0) return null;
|
|
13720
|
+
if (this.cursor === null || next > this.cursor) this.cursor = next;
|
|
13721
|
+
if (this.pendingCursor !== null && this.pendingCursor <= this.cursor) this.pendingCursor = null;
|
|
13722
|
+
this.dirty = true;
|
|
13723
|
+
return this.cursor;
|
|
13724
|
+
}
|
|
13725
|
+
findContinuation(target, contentHash) {
|
|
13726
|
+
return this.continuations.find((c) => c.target === target && c.contentHash === contentHash);
|
|
13727
|
+
}
|
|
13728
|
+
rememberContinuation(entry) {
|
|
13729
|
+
this.continuations = this.continuations.filter((c) => !(c.target === entry.target && c.contentHash === entry.contentHash));
|
|
13730
|
+
this.continuations.push(entry);
|
|
13731
|
+
const perTarget = this.continuations.filter((c) => c.target === entry.target);
|
|
13732
|
+
for (const drop of perTarget.slice(0, Math.max(0, perTarget.length - 3))) this.continuations.splice(this.continuations.indexOf(drop), 1);
|
|
13733
|
+
if (this.continuations.length > 20) this.continuations.splice(0, this.continuations.length - 20);
|
|
13734
|
+
this.dirty = true;
|
|
13735
|
+
}
|
|
13736
|
+
forgetContinuation(target, contentHash) {
|
|
13737
|
+
const before = this.continuations.length;
|
|
13738
|
+
this.continuations = this.continuations.filter((c) => !(c.target === target && c.contentHash === contentHash));
|
|
13739
|
+
if (this.continuations.length !== before) this.dirty = true;
|
|
13740
|
+
}
|
|
13741
|
+
snapshot(nextVersion = (this.loadedVersion ?? 0) + 1) {
|
|
13742
|
+
return {
|
|
13743
|
+
schema: RAFT_STATE_SCHEMA,
|
|
13744
|
+
version: nextVersion,
|
|
13745
|
+
cursor: this.cursor,
|
|
13746
|
+
pendingCursor: this.pendingCursor,
|
|
13747
|
+
frontier: this.frontier.snapshot(),
|
|
13748
|
+
continuations: [...this.continuations]
|
|
13749
|
+
};
|
|
13750
|
+
}
|
|
13751
|
+
/**
|
|
13752
|
+
* Save if anything changed (or `force`). One attempt; a failure is reported
|
|
13753
|
+
* and swallowed. Returns whether the save succeeded (true when nothing to save).
|
|
13754
|
+
*/
|
|
13755
|
+
async save(options = {}) {
|
|
13756
|
+
if (!this.store) return true;
|
|
13757
|
+
if (!this.dirty && !options.force) return true;
|
|
13758
|
+
const expectedVersion = this.loadedVersion;
|
|
13759
|
+
const state = this.snapshot();
|
|
13760
|
+
try {
|
|
13761
|
+
await this.store.save(state, { expectedVersion });
|
|
13762
|
+
} catch (error) {
|
|
13763
|
+
this.onError?.(error, {
|
|
13764
|
+
phase: "save",
|
|
13765
|
+
version: expectedVersion
|
|
13766
|
+
});
|
|
13767
|
+
return false;
|
|
13768
|
+
}
|
|
13769
|
+
this.loadedVersion = state.version;
|
|
13770
|
+
this.dirty = false;
|
|
13771
|
+
return true;
|
|
13772
|
+
}
|
|
13773
|
+
};
|
|
13774
|
+
//#endregion
|
|
13617
13775
|
//#region src/raft.ts
|
|
13618
13776
|
function createRaft(options) {
|
|
13619
13777
|
const serverUrl = requireServerUrl(options.serverUrl);
|
|
13620
13778
|
const authorization = `Bearer ${requireAgentCredential(options.credential)}`;
|
|
13621
13779
|
const frontier = SeenFrontier.fromSnapshot(options.frontier);
|
|
13780
|
+
const session = new RaftStateSession(options.state, frontier, options.onStateSaveError);
|
|
13781
|
+
/** Load before, run, then save if the operation succeeded and changed the state. */
|
|
13782
|
+
const withState = async (run) => {
|
|
13783
|
+
await session.ensureLoaded();
|
|
13784
|
+
const outcome = await run();
|
|
13785
|
+
if (outcome.ok) await session.save();
|
|
13786
|
+
return outcome;
|
|
13787
|
+
};
|
|
13622
13788
|
const routes = createRaftRoutes({
|
|
13623
13789
|
serverUrl,
|
|
13624
13790
|
fetch: options.fetch,
|
|
@@ -13639,6 +13805,27 @@ function createRaft(options) {
|
|
|
13639
13805
|
throttle: options.throttle,
|
|
13640
13806
|
auth: { authorization }
|
|
13641
13807
|
} });
|
|
13808
|
+
/**
|
|
13809
|
+
* Send through the state session: a resend of the same logical message
|
|
13810
|
+
* (same target, content, attachments) reuses the held send's idempotency key;
|
|
13811
|
+
* a hold is remembered; a successful send clears it.
|
|
13812
|
+
*/
|
|
13813
|
+
const sendWithState = (request) => withState(async () => {
|
|
13814
|
+
const contentHash = await hashRaftSendContent(request.target, request.content ?? "", request.attachmentIds ?? []);
|
|
13815
|
+
const pending = request.idempotencyKey ? void 0 : session.findContinuation(request.target, contentHash);
|
|
13816
|
+
const outcome = await sendMessage(routes, pending ? {
|
|
13817
|
+
...request,
|
|
13818
|
+
idempotencyKey: pending.idempotencyKey
|
|
13819
|
+
} : request, frontier);
|
|
13820
|
+
if (outcome.ok && outcome.state === "held") session.rememberContinuation({
|
|
13821
|
+
target: request.target,
|
|
13822
|
+
idempotencyKey: outcome.data.idempotencyKey,
|
|
13823
|
+
contentHash,
|
|
13824
|
+
heldAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13825
|
+
});
|
|
13826
|
+
else if (outcome.ok && outcome.state === "sent") session.forgetContinuation(request.target, contentHash);
|
|
13827
|
+
return outcome;
|
|
13828
|
+
});
|
|
13642
13829
|
return {
|
|
13643
13830
|
identity: { whoami: () => getRaftContext(routes) },
|
|
13644
13831
|
wake: {
|
|
@@ -13650,14 +13837,53 @@ function createRaft(options) {
|
|
|
13650
13837
|
}
|
|
13651
13838
|
},
|
|
13652
13839
|
inbox: {
|
|
13653
|
-
check: (request) =>
|
|
13654
|
-
|
|
13840
|
+
check: (request = {}) => withState(async () => {
|
|
13841
|
+
const since = request.since ?? session.cursor ?? void 0;
|
|
13842
|
+
const outcome = await checkInbox(inboxApi, {
|
|
13843
|
+
...request,
|
|
13844
|
+
...since === void 0 ? {} : { since }
|
|
13845
|
+
}, frontier);
|
|
13846
|
+
if (outcome.ok) {
|
|
13847
|
+
session.markDirty();
|
|
13848
|
+
if (since !== void 0 && (request.ack ?? "cursor") === "cursor") session.commit(since);
|
|
13849
|
+
if (outcome.data.ackMode === "cursor" && outcome.data.messages.length > 0) session.setPending(outcome.data.cursor);
|
|
13850
|
+
}
|
|
13851
|
+
return outcome;
|
|
13852
|
+
}),
|
|
13853
|
+
commit: async (target) => {
|
|
13854
|
+
await session.ensureLoaded();
|
|
13855
|
+
const cursor = session.commit(target === void 0 ? void 0 : target.cursor);
|
|
13856
|
+
return {
|
|
13857
|
+
cursor,
|
|
13858
|
+
saved: cursor === null ? true : await session.save()
|
|
13859
|
+
};
|
|
13860
|
+
},
|
|
13861
|
+
drain: (request = {}) => (async function* () {
|
|
13862
|
+
await session.ensureLoaded();
|
|
13863
|
+
const since = request.since ?? session.cursor ?? void 0;
|
|
13864
|
+
return yield* drainInbox(inboxApi, {
|
|
13865
|
+
...request,
|
|
13866
|
+
...since === void 0 ? {} : { since }
|
|
13867
|
+
}, frontier, async (sent, batch) => {
|
|
13868
|
+
if (sent !== null) session.commit(sent);
|
|
13869
|
+
if (batch.ackMode === "cursor" && batch.messages.length > 0) session.setPending(batch.cursor);
|
|
13870
|
+
session.markDirty();
|
|
13871
|
+
await session.save();
|
|
13872
|
+
});
|
|
13873
|
+
})(),
|
|
13655
13874
|
list: (request) => listInbox(routes, request)
|
|
13656
13875
|
},
|
|
13657
13876
|
messages: {
|
|
13658
|
-
read: (request) =>
|
|
13659
|
-
|
|
13660
|
-
|
|
13877
|
+
read: (request) => withState(async () => {
|
|
13878
|
+
const outcome = await readHistory(routes, request, frontier);
|
|
13879
|
+
if (outcome.ok) session.markDirty();
|
|
13880
|
+
return outcome;
|
|
13881
|
+
}),
|
|
13882
|
+
send: (request) => sendWithState(request),
|
|
13883
|
+
reply: (message, request) => sendWithState({
|
|
13884
|
+
...request,
|
|
13885
|
+
target: message.target
|
|
13886
|
+
}),
|
|
13661
13887
|
search: (request) => searchMessages(routes, request),
|
|
13662
13888
|
resolve: (request) => resolveMessage(routes, request),
|
|
13663
13889
|
react: (request) => reactToMessage(routes, request, "add"),
|
|
@@ -13711,6 +13937,15 @@ function createRaft(options) {
|
|
|
13711
13937
|
update: (request) => updateProfile(routes, request)
|
|
13712
13938
|
},
|
|
13713
13939
|
frontier,
|
|
13940
|
+
state: {
|
|
13941
|
+
save: async () => {
|
|
13942
|
+
await session.ensureLoaded();
|
|
13943
|
+
session.markDirty();
|
|
13944
|
+
return session.save();
|
|
13945
|
+
},
|
|
13946
|
+
load: () => session.ensureLoaded(),
|
|
13947
|
+
snapshot: () => session.snapshot(session.version ?? 0)
|
|
13948
|
+
},
|
|
13714
13949
|
routes
|
|
13715
13950
|
};
|
|
13716
13951
|
}
|
|
@@ -13718,6 +13953,9 @@ function createRaft(options) {
|
|
|
13718
13953
|
exports.RAFT_INBOX_NOTICE_SCHEMA = RAFT_INBOX_NOTICE_SCHEMA;
|
|
13719
13954
|
exports.RAFT_NOTICE_DELIVERY_ID_HEADER = RAFT_NOTICE_DELIVERY_ID_HEADER;
|
|
13720
13955
|
exports.RAFT_NOTICE_SIGNATURE_HEADER = RAFT_NOTICE_SIGNATURE_HEADER;
|
|
13956
|
+
exports.RAFT_STATE_CONTINUATIONS_PER_TARGET = RAFT_STATE_CONTINUATIONS_PER_TARGET;
|
|
13957
|
+
exports.RAFT_STATE_CONTINUATIONS_TOTAL = RAFT_STATE_CONTINUATIONS_TOTAL;
|
|
13958
|
+
exports.RAFT_STATE_SCHEMA = RAFT_STATE_SCHEMA;
|
|
13721
13959
|
exports.RaftCredentialError = RaftCredentialError;
|
|
13722
13960
|
exports.RaftSdkConfigurationError = RaftSdkConfigurationError;
|
|
13723
13961
|
exports.SeenFrontier = SeenFrontier;
|
|
@@ -13729,10 +13967,12 @@ exports.createRaftClientFromStore = createRaftClientFromStore;
|
|
|
13729
13967
|
exports.createRaftRoutes = createRaftRoutes;
|
|
13730
13968
|
exports.describeRaftRoute = describeRaftRoute;
|
|
13731
13969
|
exports.hasAgentMessageIdentity = hasAgentMessageIdentity;
|
|
13970
|
+
exports.hashRaftSendContent = hashRaftSendContent;
|
|
13732
13971
|
exports.inferAttachmentMimeType = inferAttachmentMimeType;
|
|
13733
13972
|
exports.joinRaftChannelByTarget = joinRaftChannelByTarget;
|
|
13734
13973
|
exports.listRaftRoutes = listRaftRoutes;
|
|
13735
13974
|
exports.parseRaftRegularChannelTarget = parseRaftRegularChannelTarget;
|
|
13975
|
+
exports.parseRaftState = parseRaftState;
|
|
13736
13976
|
exports.projectRaftMessage = projectRaftMessage;
|
|
13737
13977
|
exports.projectRaftMessages = projectRaftMessages;
|
|
13738
13978
|
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
|
}
|
|
@@ -13613,11 +13611,179 @@ async function searchManual(client, request) {
|
|
|
13613
13611
|
};
|
|
13614
13612
|
}
|
|
13615
13613
|
//#endregion
|
|
13614
|
+
//#region ../shared/src/agentOps/state.ts
|
|
13615
|
+
const RAFT_STATE_SCHEMA = "raft-sdk-state.v1";
|
|
13616
|
+
const RAFT_STATE_CONTINUATIONS_PER_TARGET = 3;
|
|
13617
|
+
const RAFT_STATE_CONTINUATIONS_TOTAL = 20;
|
|
13618
|
+
function isNullableCursor(value) {
|
|
13619
|
+
return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
13620
|
+
}
|
|
13621
|
+
/** Validate a loaded value; anything unrecognised is treated as an empty store (safe: see file header). */
|
|
13622
|
+
function parseRaftState(value) {
|
|
13623
|
+
if (!value || typeof value !== "object") return null;
|
|
13624
|
+
const v = value;
|
|
13625
|
+
if (v.schema !== "raft-sdk-state.v1") return null;
|
|
13626
|
+
if (typeof v.version !== "number" || !Number.isInteger(v.version) || v.version < 0) return null;
|
|
13627
|
+
if (!isNullableCursor(v.cursor ?? null) || !isNullableCursor(v.pendingCursor ?? null)) return null;
|
|
13628
|
+
const frontier = v.frontier;
|
|
13629
|
+
const continuations = Array.isArray(v.continuations) ? v.continuations.filter((c) => {
|
|
13630
|
+
const r = c;
|
|
13631
|
+
return Boolean(r) && typeof r.target === "string" && typeof r.idempotencyKey === "string" && typeof r.contentHash === "string" && typeof r.heldAt === "string";
|
|
13632
|
+
}) : [];
|
|
13633
|
+
return {
|
|
13634
|
+
schema: RAFT_STATE_SCHEMA,
|
|
13635
|
+
version: v.version,
|
|
13636
|
+
cursor: v.cursor ?? null,
|
|
13637
|
+
pendingCursor: v.pendingCursor ?? null,
|
|
13638
|
+
frontier: frontier && frontier.version === 1 ? frontier : {
|
|
13639
|
+
version: 1,
|
|
13640
|
+
targets: {},
|
|
13641
|
+
aliases: {}
|
|
13642
|
+
},
|
|
13643
|
+
continuations
|
|
13644
|
+
};
|
|
13645
|
+
}
|
|
13646
|
+
/** Hex SHA-256 identifying one logical message (WebCrypto; Workers-safe). */
|
|
13647
|
+
async function hashRaftSendContent(target, content, attachmentIds = []) {
|
|
13648
|
+
const bytes = new TextEncoder().encode(`${target}\n${[...attachmentIds].sort().join(",")}\n${content}`);
|
|
13649
|
+
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
13650
|
+
}
|
|
13651
|
+
/**
|
|
13652
|
+
* One client's view of the persisted state: loads once, holds the working
|
|
13653
|
+
* copy, saves on request. Also used without a store (memory only), so the
|
|
13654
|
+
* pending/committed cursor flow behaves the same with or without persistence.
|
|
13655
|
+
*/
|
|
13656
|
+
var RaftStateSession = class {
|
|
13657
|
+
store;
|
|
13658
|
+
onError;
|
|
13659
|
+
frontier;
|
|
13660
|
+
cursor = null;
|
|
13661
|
+
pendingCursor = null;
|
|
13662
|
+
continuations = [];
|
|
13663
|
+
loadedVersion = void 0;
|
|
13664
|
+
loading = null;
|
|
13665
|
+
dirty = false;
|
|
13666
|
+
constructor(store, frontier, onError) {
|
|
13667
|
+
this.store = store;
|
|
13668
|
+
this.onError = onError;
|
|
13669
|
+
this.frontier = frontier;
|
|
13670
|
+
}
|
|
13671
|
+
get persistent() {
|
|
13672
|
+
return this.store !== void 0;
|
|
13673
|
+
}
|
|
13674
|
+
/** The version loaded from (or last saved to) the store; `undefined` before the first save of an empty store. */
|
|
13675
|
+
get version() {
|
|
13676
|
+
return this.loadedVersion;
|
|
13677
|
+
}
|
|
13678
|
+
/** Load once. A failed or unreadable load starts from an empty state (reported, not thrown). */
|
|
13679
|
+
ensureLoaded() {
|
|
13680
|
+
if (!this.store) return Promise.resolve();
|
|
13681
|
+
this.loading ??= (async () => {
|
|
13682
|
+
let raw = null;
|
|
13683
|
+
try {
|
|
13684
|
+
raw = await this.store.load();
|
|
13685
|
+
} catch (error) {
|
|
13686
|
+
this.onError?.(error, {
|
|
13687
|
+
phase: "load",
|
|
13688
|
+
version: void 0
|
|
13689
|
+
});
|
|
13690
|
+
return;
|
|
13691
|
+
}
|
|
13692
|
+
const state = parseRaftState(raw);
|
|
13693
|
+
if (!state) return;
|
|
13694
|
+
this.loadedVersion = state.version;
|
|
13695
|
+
this.cursor = state.cursor;
|
|
13696
|
+
this.pendingCursor = state.pendingCursor;
|
|
13697
|
+
this.frontier.absorb(state.frontier);
|
|
13698
|
+
this.continuations = state.continuations ?? [];
|
|
13699
|
+
})();
|
|
13700
|
+
return this.loading;
|
|
13701
|
+
}
|
|
13702
|
+
markDirty() {
|
|
13703
|
+
this.dirty = true;
|
|
13704
|
+
}
|
|
13705
|
+
/** Record the cursor of a batch just returned. Acknowledges nothing. */
|
|
13706
|
+
setPending(cursor) {
|
|
13707
|
+
if (cursor === null || cursor === this.pendingCursor) return;
|
|
13708
|
+
this.pendingCursor = cursor;
|
|
13709
|
+
this.dirty = true;
|
|
13710
|
+
}
|
|
13711
|
+
/**
|
|
13712
|
+
* Promote a cursor to committed. With no argument, commits `pendingCursor`.
|
|
13713
|
+
* Returns the committed cursor, or null when there was nothing to commit.
|
|
13714
|
+
* Never moves the committed cursor backwards.
|
|
13715
|
+
*/
|
|
13716
|
+
commit(cursor) {
|
|
13717
|
+
const next = cursor === void 0 ? this.pendingCursor : cursor;
|
|
13718
|
+
if (next === null || next === void 0) return null;
|
|
13719
|
+
if (this.cursor === null || next > this.cursor) this.cursor = next;
|
|
13720
|
+
if (this.pendingCursor !== null && this.pendingCursor <= this.cursor) this.pendingCursor = null;
|
|
13721
|
+
this.dirty = true;
|
|
13722
|
+
return this.cursor;
|
|
13723
|
+
}
|
|
13724
|
+
findContinuation(target, contentHash) {
|
|
13725
|
+
return this.continuations.find((c) => c.target === target && c.contentHash === contentHash);
|
|
13726
|
+
}
|
|
13727
|
+
rememberContinuation(entry) {
|
|
13728
|
+
this.continuations = this.continuations.filter((c) => !(c.target === entry.target && c.contentHash === entry.contentHash));
|
|
13729
|
+
this.continuations.push(entry);
|
|
13730
|
+
const perTarget = this.continuations.filter((c) => c.target === entry.target);
|
|
13731
|
+
for (const drop of perTarget.slice(0, Math.max(0, perTarget.length - 3))) this.continuations.splice(this.continuations.indexOf(drop), 1);
|
|
13732
|
+
if (this.continuations.length > 20) this.continuations.splice(0, this.continuations.length - 20);
|
|
13733
|
+
this.dirty = true;
|
|
13734
|
+
}
|
|
13735
|
+
forgetContinuation(target, contentHash) {
|
|
13736
|
+
const before = this.continuations.length;
|
|
13737
|
+
this.continuations = this.continuations.filter((c) => !(c.target === target && c.contentHash === contentHash));
|
|
13738
|
+
if (this.continuations.length !== before) this.dirty = true;
|
|
13739
|
+
}
|
|
13740
|
+
snapshot(nextVersion = (this.loadedVersion ?? 0) + 1) {
|
|
13741
|
+
return {
|
|
13742
|
+
schema: RAFT_STATE_SCHEMA,
|
|
13743
|
+
version: nextVersion,
|
|
13744
|
+
cursor: this.cursor,
|
|
13745
|
+
pendingCursor: this.pendingCursor,
|
|
13746
|
+
frontier: this.frontier.snapshot(),
|
|
13747
|
+
continuations: [...this.continuations]
|
|
13748
|
+
};
|
|
13749
|
+
}
|
|
13750
|
+
/**
|
|
13751
|
+
* Save if anything changed (or `force`). One attempt; a failure is reported
|
|
13752
|
+
* and swallowed. Returns whether the save succeeded (true when nothing to save).
|
|
13753
|
+
*/
|
|
13754
|
+
async save(options = {}) {
|
|
13755
|
+
if (!this.store) return true;
|
|
13756
|
+
if (!this.dirty && !options.force) return true;
|
|
13757
|
+
const expectedVersion = this.loadedVersion;
|
|
13758
|
+
const state = this.snapshot();
|
|
13759
|
+
try {
|
|
13760
|
+
await this.store.save(state, { expectedVersion });
|
|
13761
|
+
} catch (error) {
|
|
13762
|
+
this.onError?.(error, {
|
|
13763
|
+
phase: "save",
|
|
13764
|
+
version: expectedVersion
|
|
13765
|
+
});
|
|
13766
|
+
return false;
|
|
13767
|
+
}
|
|
13768
|
+
this.loadedVersion = state.version;
|
|
13769
|
+
this.dirty = false;
|
|
13770
|
+
return true;
|
|
13771
|
+
}
|
|
13772
|
+
};
|
|
13773
|
+
//#endregion
|
|
13616
13774
|
//#region src/raft.ts
|
|
13617
13775
|
function createRaft(options) {
|
|
13618
13776
|
const serverUrl = requireServerUrl(options.serverUrl);
|
|
13619
13777
|
const authorization = `Bearer ${requireAgentCredential(options.credential)}`;
|
|
13620
13778
|
const frontier = SeenFrontier.fromSnapshot(options.frontier);
|
|
13779
|
+
const session = new RaftStateSession(options.state, frontier, options.onStateSaveError);
|
|
13780
|
+
/** Load before, run, then save if the operation succeeded and changed the state. */
|
|
13781
|
+
const withState = async (run) => {
|
|
13782
|
+
await session.ensureLoaded();
|
|
13783
|
+
const outcome = await run();
|
|
13784
|
+
if (outcome.ok) await session.save();
|
|
13785
|
+
return outcome;
|
|
13786
|
+
};
|
|
13621
13787
|
const routes = createRaftRoutes({
|
|
13622
13788
|
serverUrl,
|
|
13623
13789
|
fetch: options.fetch,
|
|
@@ -13638,6 +13804,27 @@ function createRaft(options) {
|
|
|
13638
13804
|
throttle: options.throttle,
|
|
13639
13805
|
auth: { authorization }
|
|
13640
13806
|
} });
|
|
13807
|
+
/**
|
|
13808
|
+
* Send through the state session: a resend of the same logical message
|
|
13809
|
+
* (same target, content, attachments) reuses the held send's idempotency key;
|
|
13810
|
+
* a hold is remembered; a successful send clears it.
|
|
13811
|
+
*/
|
|
13812
|
+
const sendWithState = (request) => withState(async () => {
|
|
13813
|
+
const contentHash = await hashRaftSendContent(request.target, request.content ?? "", request.attachmentIds ?? []);
|
|
13814
|
+
const pending = request.idempotencyKey ? void 0 : session.findContinuation(request.target, contentHash);
|
|
13815
|
+
const outcome = await sendMessage(routes, pending ? {
|
|
13816
|
+
...request,
|
|
13817
|
+
idempotencyKey: pending.idempotencyKey
|
|
13818
|
+
} : request, frontier);
|
|
13819
|
+
if (outcome.ok && outcome.state === "held") session.rememberContinuation({
|
|
13820
|
+
target: request.target,
|
|
13821
|
+
idempotencyKey: outcome.data.idempotencyKey,
|
|
13822
|
+
contentHash,
|
|
13823
|
+
heldAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13824
|
+
});
|
|
13825
|
+
else if (outcome.ok && outcome.state === "sent") session.forgetContinuation(request.target, contentHash);
|
|
13826
|
+
return outcome;
|
|
13827
|
+
});
|
|
13641
13828
|
return {
|
|
13642
13829
|
identity: { whoami: () => getRaftContext(routes) },
|
|
13643
13830
|
wake: {
|
|
@@ -13649,14 +13836,53 @@ function createRaft(options) {
|
|
|
13649
13836
|
}
|
|
13650
13837
|
},
|
|
13651
13838
|
inbox: {
|
|
13652
|
-
check: (request) =>
|
|
13653
|
-
|
|
13839
|
+
check: (request = {}) => withState(async () => {
|
|
13840
|
+
const since = request.since ?? session.cursor ?? void 0;
|
|
13841
|
+
const outcome = await checkInbox(inboxApi, {
|
|
13842
|
+
...request,
|
|
13843
|
+
...since === void 0 ? {} : { since }
|
|
13844
|
+
}, frontier);
|
|
13845
|
+
if (outcome.ok) {
|
|
13846
|
+
session.markDirty();
|
|
13847
|
+
if (since !== void 0 && (request.ack ?? "cursor") === "cursor") session.commit(since);
|
|
13848
|
+
if (outcome.data.ackMode === "cursor" && outcome.data.messages.length > 0) session.setPending(outcome.data.cursor);
|
|
13849
|
+
}
|
|
13850
|
+
return outcome;
|
|
13851
|
+
}),
|
|
13852
|
+
commit: async (target) => {
|
|
13853
|
+
await session.ensureLoaded();
|
|
13854
|
+
const cursor = session.commit(target === void 0 ? void 0 : target.cursor);
|
|
13855
|
+
return {
|
|
13856
|
+
cursor,
|
|
13857
|
+
saved: cursor === null ? true : await session.save()
|
|
13858
|
+
};
|
|
13859
|
+
},
|
|
13860
|
+
drain: (request = {}) => (async function* () {
|
|
13861
|
+
await session.ensureLoaded();
|
|
13862
|
+
const since = request.since ?? session.cursor ?? void 0;
|
|
13863
|
+
return yield* drainInbox(inboxApi, {
|
|
13864
|
+
...request,
|
|
13865
|
+
...since === void 0 ? {} : { since }
|
|
13866
|
+
}, frontier, async (sent, batch) => {
|
|
13867
|
+
if (sent !== null) session.commit(sent);
|
|
13868
|
+
if (batch.ackMode === "cursor" && batch.messages.length > 0) session.setPending(batch.cursor);
|
|
13869
|
+
session.markDirty();
|
|
13870
|
+
await session.save();
|
|
13871
|
+
});
|
|
13872
|
+
})(),
|
|
13654
13873
|
list: (request) => listInbox(routes, request)
|
|
13655
13874
|
},
|
|
13656
13875
|
messages: {
|
|
13657
|
-
read: (request) =>
|
|
13658
|
-
|
|
13659
|
-
|
|
13876
|
+
read: (request) => withState(async () => {
|
|
13877
|
+
const outcome = await readHistory(routes, request, frontier);
|
|
13878
|
+
if (outcome.ok) session.markDirty();
|
|
13879
|
+
return outcome;
|
|
13880
|
+
}),
|
|
13881
|
+
send: (request) => sendWithState(request),
|
|
13882
|
+
reply: (message, request) => sendWithState({
|
|
13883
|
+
...request,
|
|
13884
|
+
target: message.target
|
|
13885
|
+
}),
|
|
13660
13886
|
search: (request) => searchMessages(routes, request),
|
|
13661
13887
|
resolve: (request) => resolveMessage(routes, request),
|
|
13662
13888
|
react: (request) => reactToMessage(routes, request, "add"),
|
|
@@ -13710,8 +13936,17 @@ function createRaft(options) {
|
|
|
13710
13936
|
update: (request) => updateProfile(routes, request)
|
|
13711
13937
|
},
|
|
13712
13938
|
frontier,
|
|
13939
|
+
state: {
|
|
13940
|
+
save: async () => {
|
|
13941
|
+
await session.ensureLoaded();
|
|
13942
|
+
session.markDirty();
|
|
13943
|
+
return session.save();
|
|
13944
|
+
},
|
|
13945
|
+
load: () => session.ensureLoaded(),
|
|
13946
|
+
snapshot: () => session.snapshot(session.version ?? 0)
|
|
13947
|
+
},
|
|
13713
13948
|
routes
|
|
13714
13949
|
};
|
|
13715
13950
|
}
|
|
13716
13951
|
//#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 };
|
|
13952
|
+
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,49 @@ 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
|
|
938
983
|
//#region ../shared/src/agentApiRawClient.d.ts
|
|
939
984
|
type AgentApiRawClientErrorReason = "missing_route" | "missing_path_param" | "request_contract_mismatch" | "transport_error" | "http_error" | "empty_response" | "response_contract_mismatch";
|
|
940
985
|
interface AgentApiRawTransportRequest<K extends AgentApiRouteKey = AgentApiRouteKey> {
|
|
@@ -9299,6 +9344,22 @@ interface CreateRaftOptions {
|
|
|
9299
9344
|
* is held once and returns the unread context, which is the safe default.
|
|
9300
9345
|
*/
|
|
9301
9346
|
frontier?: SeenFrontierSnapshot | null;
|
|
9347
|
+
/**
|
|
9348
|
+
* Persist the client's state (committed and pending inbox cursors, the seen
|
|
9349
|
+
* frontier, held-send keys) across tool calls and processes. The store
|
|
9350
|
+
* implements `load()` and `save(state, { expectedVersion })`; the SDK loads
|
|
9351
|
+
* once before the first operation and saves after each successful operation
|
|
9352
|
+
* that changed the state (one attempt, never fails the operation).
|
|
9353
|
+
*/
|
|
9354
|
+
state?: RaftStateStore;
|
|
9355
|
+
/** Called when loading or saving the state fails or is rejected as stale. */
|
|
9356
|
+
onStateSaveError?: RaftStateSaveErrorHandler;
|
|
9357
|
+
}
|
|
9358
|
+
interface RaftInboxCommitResult {
|
|
9359
|
+
/** The committed cursor, or null when there was nothing to commit. */
|
|
9360
|
+
cursor: number | null;
|
|
9361
|
+
/** Whether the state was persisted (true when no store is configured). */
|
|
9362
|
+
saved: boolean;
|
|
9302
9363
|
}
|
|
9303
9364
|
interface Raft {
|
|
9304
9365
|
identity: {
|
|
@@ -9320,8 +9381,22 @@ interface Raft {
|
|
|
9320
9381
|
};
|
|
9321
9382
|
};
|
|
9322
9383
|
inbox: {
|
|
9323
|
-
/**
|
|
9384
|
+
/**
|
|
9385
|
+
* One bounded pull. Nothing is acknowledged by it. Without `since`, sends
|
|
9386
|
+
* the last committed cursor (from `state`), which is what acknowledges the
|
|
9387
|
+
* previously committed batch on the Server; the returned batch's cursor is
|
|
9388
|
+
* recorded as pending until you `commit()` it.
|
|
9389
|
+
*/
|
|
9324
9390
|
check(request?: CheckInboxRequest): Promise<CheckInboxOutcome>;
|
|
9391
|
+
/**
|
|
9392
|
+
* Mark a batch as processed. With no argument, commits the pending cursor
|
|
9393
|
+
* from the last `check()` (possibly in an earlier process, via `state`);
|
|
9394
|
+
* also accepts `{ cursor }` or a batch. Records only; the next `check()`
|
|
9395
|
+
* acknowledges it on the Server. The SDK never commits on its own.
|
|
9396
|
+
*/
|
|
9397
|
+
commit(target?: {
|
|
9398
|
+
cursor: number | null;
|
|
9399
|
+
}): Promise<RaftInboxCommitResult>;
|
|
9325
9400
|
/**
|
|
9326
9401
|
* Pull until the Server reports nothing more, like `raft message check`, as
|
|
9327
9402
|
* an async iterator: the pull that acknowledges a batch is only sent when
|
|
@@ -9455,9 +9530,17 @@ interface Raft {
|
|
|
9455
9530
|
};
|
|
9456
9531
|
/** What this process has shown its model, per target; export it to survive restarts. */
|
|
9457
9532
|
frontier: SeenFrontier;
|
|
9533
|
+
state: {
|
|
9534
|
+
/** Save now (for example after `frontier.recordHeld`). Best effort; returns whether it succeeded. */
|
|
9535
|
+
save(): Promise<boolean>;
|
|
9536
|
+
/** Load the persisted state now (operations do this automatically). */
|
|
9537
|
+
load(): Promise<void>;
|
|
9538
|
+
/** The current state value, as it would be saved. */
|
|
9539
|
+
snapshot(): RaftState;
|
|
9540
|
+
};
|
|
9458
9541
|
/** Every Agent API route, typed from the shared contract. */
|
|
9459
9542
|
routes: RaftRoutes;
|
|
9460
9543
|
}
|
|
9461
9544
|
export declare function createRaft(options: CreateRaftOptions): Raft;
|
|
9462
9545
|
//#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 };
|
|
9546
|
+
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, RaftInboxCommitResult, 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, 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 };
|