@astralform/js 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +196 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -23
- package/dist/index.d.ts +87 -23
- package/dist/index.js +195 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
AstralformClient: () => AstralformClient,
|
|
24
24
|
AstralformError: () => AstralformError,
|
|
25
25
|
AuthenticationError: () => AuthenticationError,
|
|
26
|
+
CONVERSATION_PAGE_SIZE: () => CONVERSATION_PAGE_SIZE,
|
|
26
27
|
ChatEventType: () => ChatEventType,
|
|
27
28
|
ChatSession: () => ChatSession,
|
|
28
29
|
ConnectionError: () => ConnectionError,
|
|
@@ -353,7 +354,7 @@ var AstralformClient = class {
|
|
|
353
354
|
}
|
|
354
355
|
/**
|
|
355
356
|
* Replace the current OIDC access token without reconstructing the client.
|
|
356
|
-
* Use after refreshing via the host's token manager
|
|
357
|
+
* Use after refreshing via the host app's own token manager.
|
|
357
358
|
* Throws if the client was created in API-key mode.
|
|
358
359
|
*/
|
|
359
360
|
updateAccessToken(accessToken) {
|
|
@@ -1278,6 +1279,7 @@ function translateWireEvent(wire) {
|
|
|
1278
1279
|
// src/session.ts
|
|
1279
1280
|
var SSE_MAX_RECONNECTS = 6;
|
|
1280
1281
|
var TOOL_RESULT_MAX_RETRIES = 3;
|
|
1282
|
+
var CONVERSATION_PAGE_SIZE = 50;
|
|
1281
1283
|
function sseReconnectDelayMs(attempt) {
|
|
1282
1284
|
return Math.min(500 * 2 ** (attempt - 1), 5e3);
|
|
1283
1285
|
}
|
|
@@ -1301,6 +1303,17 @@ var ChatSession = class {
|
|
|
1301
1303
|
// State
|
|
1302
1304
|
this.conversationId = null;
|
|
1303
1305
|
this.conversations = [];
|
|
1306
|
+
/**
|
|
1307
|
+
* Whether another page of conversations may exist on the server.
|
|
1308
|
+
*
|
|
1309
|
+
* Inferred from the last page being full, since the list endpoint returns a
|
|
1310
|
+
* bare array with no total. A total that happens to be an exact multiple of
|
|
1311
|
+
* the page size therefore costs one extra empty request before this flips —
|
|
1312
|
+
* cheaper than adding a count query to every list call.
|
|
1313
|
+
*/
|
|
1314
|
+
this.hasMoreConversations = false;
|
|
1315
|
+
/** True while ``loadMoreConversations`` is in flight. */
|
|
1316
|
+
this.isLoadingConversations = false;
|
|
1304
1317
|
this.messages = [];
|
|
1305
1318
|
this.isStreaming = false;
|
|
1306
1319
|
this.agentStatus = null;
|
|
@@ -1308,6 +1321,32 @@ var ChatSession = class {
|
|
|
1308
1321
|
this.skills = [];
|
|
1309
1322
|
this.enabledClientTools = /* @__PURE__ */ new Set();
|
|
1310
1323
|
this.modelDisplayName = null;
|
|
1324
|
+
/**
|
|
1325
|
+
* Ids of conversations the SERVER has handed us, which is the paging offset.
|
|
1326
|
+
*
|
|
1327
|
+
* Deliberately not ``conversations.length``. That array also holds
|
|
1328
|
+
* conversations created locally and unshifted on top (``createNewConversation``,
|
|
1329
|
+
* and the auto-created conversation in ``consumeJobStream``), so using its
|
|
1330
|
+
* length as the offset would over-count and silently skip a row of real
|
|
1331
|
+
* history on the next page. Tracking ids rather than a counter also makes
|
|
1332
|
+
* deletion self-correcting: removing a server-sourced conversation shifts
|
|
1333
|
+
* every later page up by one, and dropping its id from this set is exactly
|
|
1334
|
+
* that shift — while deleting a purely local one correctly changes nothing.
|
|
1335
|
+
*/
|
|
1336
|
+
this.serverConversationIds = /* @__PURE__ */ new Set();
|
|
1337
|
+
/**
|
|
1338
|
+
* Bumped every time ``connect()`` re-seeds the conversation list.
|
|
1339
|
+
*
|
|
1340
|
+
* A ``loadMoreConversations`` request issued before a re-seed describes the
|
|
1341
|
+
* OLD paging state, so applying its response afterwards both appends the
|
|
1342
|
+
* wrong rows and corrupts the offset. Concretely: with 100 rows held, an
|
|
1343
|
+
* offset-100 response landing after a reconnect has reset to rows 0-49 would
|
|
1344
|
+
* append rows 100-149 — a 50-row hole — and leave the id set at 100, so every
|
|
1345
|
+
* later page re-requests offset 100 and never advances again. The generation
|
|
1346
|
+
* is captured before the await and rechecked after, so a superseded response
|
|
1347
|
+
* is discarded instead.
|
|
1348
|
+
*/
|
|
1349
|
+
this.conversationsGeneration = 0;
|
|
1311
1350
|
// Minimal in-session accumulation for the assistant message record.
|
|
1312
1351
|
// Only top-level ``text`` blocks contribute; subagent / tool output
|
|
1313
1352
|
// is tracked by the consumer's own block store.
|
|
@@ -1347,7 +1386,7 @@ var ChatSession = class {
|
|
|
1347
1386
|
async connect() {
|
|
1348
1387
|
const [status, conversations, agents, skills] = await Promise.allSettled([
|
|
1349
1388
|
this.client.getAgentStatus(),
|
|
1350
|
-
this.client.getConversations(),
|
|
1389
|
+
this.client.getConversations(CONVERSATION_PAGE_SIZE),
|
|
1351
1390
|
this.client.getAgents().catch(() => []),
|
|
1352
1391
|
this.client.getSkills().catch(() => [])
|
|
1353
1392
|
]);
|
|
@@ -1355,7 +1394,12 @@ var ChatSession = class {
|
|
|
1355
1394
|
this.agentStatus = status.value;
|
|
1356
1395
|
}
|
|
1357
1396
|
if (conversations.status === "fulfilled") {
|
|
1397
|
+
this.conversationsGeneration++;
|
|
1358
1398
|
this.conversations = conversations.value;
|
|
1399
|
+
this.serverConversationIds = new Set(
|
|
1400
|
+
conversations.value.map((c) => c.id)
|
|
1401
|
+
);
|
|
1402
|
+
this.hasMoreConversations = conversations.value.length === CONVERSATION_PAGE_SIZE;
|
|
1359
1403
|
}
|
|
1360
1404
|
if (agents.status === "fulfilled") {
|
|
1361
1405
|
this.agents = agents.value;
|
|
@@ -1776,11 +1820,16 @@ var ChatSession = class {
|
|
|
1776
1820
|
* ``message_start`` in the stream (e.g. ``memory_recall`` from prompt prep),
|
|
1777
1821
|
* so leading with the prompt keeps the turn in order.
|
|
1778
1822
|
*/
|
|
1779
|
-
replayTurn(id, events, userMessageContent) {
|
|
1823
|
+
replayTurn(id, events, userMessageContent, userMessageId, isSteer = false) {
|
|
1780
1824
|
this.conversationId = id;
|
|
1781
1825
|
this.resetStreamingState();
|
|
1782
1826
|
if (userMessageContent) {
|
|
1783
|
-
this.emit({
|
|
1827
|
+
this.emit({
|
|
1828
|
+
type: "user_message",
|
|
1829
|
+
content: userMessageContent,
|
|
1830
|
+
...userMessageId ? { id: userMessageId } : {},
|
|
1831
|
+
...isSteer ? { steer: true } : {}
|
|
1832
|
+
});
|
|
1784
1833
|
}
|
|
1785
1834
|
for (const ev of events) {
|
|
1786
1835
|
const type = ev.data.type || ev.event;
|
|
@@ -1814,12 +1863,69 @@ var ChatSession = class {
|
|
|
1814
1863
|
eventsResult.status === "fulfilled" ? eventsResult.value : []
|
|
1815
1864
|
);
|
|
1816
1865
|
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Append the next page of conversation history to ``conversations``.
|
|
1868
|
+
*
|
|
1869
|
+
* The list is ordered ``updated_at DESC`` and paged by offset, so a
|
|
1870
|
+
* conversation bumped to the top mid-scroll can surface again in a later
|
|
1871
|
+
* page; ids already held are dropped rather than duplicated. Returns only
|
|
1872
|
+
* the conversations actually appended, which may be empty even on a full
|
|
1873
|
+
* page. Rejects on network failure with ``hasMoreConversations`` still true,
|
|
1874
|
+
* so the caller can retry.
|
|
1875
|
+
*
|
|
1876
|
+
* KNOWN LIMITATION — offset paging is only stable while the prefix already
|
|
1877
|
+
* consumed stays put. The offset tracking here corrects for perturbations
|
|
1878
|
+
* THIS session causes (local unshifts, ``deleteConversation``), but not for
|
|
1879
|
+
* ones it never sees:
|
|
1880
|
+
*
|
|
1881
|
+
* - a conversation this session hasn't loaded yet is bumped to the top (a
|
|
1882
|
+
* headless routine or another device posting to it), pushing the whole
|
|
1883
|
+
* list down — it lands inside the consumed prefix, which no later offset
|
|
1884
|
+
* revisits;
|
|
1885
|
+
* - a conversation is deleted from another tab/device, shrinking the list so
|
|
1886
|
+
* the next offset lands one row too far in.
|
|
1887
|
+
*
|
|
1888
|
+
* Each perturbation costs at most one conversation off the sidebar, and only
|
|
1889
|
+
* until the next ``connect()`` — that re-seeds page 1 and resets the paging
|
|
1890
|
+
* state, so a reload or reconnect always recovers it. Nothing is lost
|
|
1891
|
+
* server-side. Both cases are pinned by tests in
|
|
1892
|
+
* ``tests/conversation-paging.test.ts``.
|
|
1893
|
+
*
|
|
1894
|
+
* Closing the gap properly needs a stable server cursor (keyset paging on
|
|
1895
|
+
* ``(updated_at, id)``) rather than a raw offset, which is a backend change —
|
|
1896
|
+
* tracking ids client-side cannot discover a row that moved into a region
|
|
1897
|
+
* already scanned.
|
|
1898
|
+
*/
|
|
1899
|
+
async loadMoreConversations() {
|
|
1900
|
+
if (this.isLoadingConversations || !this.hasMoreConversations) return [];
|
|
1901
|
+
this.isLoadingConversations = true;
|
|
1902
|
+
const generation = this.conversationsGeneration;
|
|
1903
|
+
try {
|
|
1904
|
+
const page = await this.client.getConversations(
|
|
1905
|
+
CONVERSATION_PAGE_SIZE,
|
|
1906
|
+
this.serverConversationIds.size
|
|
1907
|
+
);
|
|
1908
|
+
if (generation !== this.conversationsGeneration) return [];
|
|
1909
|
+
this.hasMoreConversations = page.length === CONVERSATION_PAGE_SIZE;
|
|
1910
|
+
const fresh = page.filter((c) => !this.serverConversationIds.has(c.id));
|
|
1911
|
+
for (const c of page) this.serverConversationIds.add(c.id);
|
|
1912
|
+
const known = new Set(this.conversations.map((c) => c.id));
|
|
1913
|
+
const appended = fresh.filter((c) => !known.has(c.id));
|
|
1914
|
+
this.conversations.push(...appended);
|
|
1915
|
+
return appended;
|
|
1916
|
+
} finally {
|
|
1917
|
+
this.isLoadingConversations = false;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1817
1920
|
async deleteConversation(id) {
|
|
1818
1921
|
try {
|
|
1819
1922
|
await this.client.deleteConversation(id);
|
|
1820
1923
|
} catch {
|
|
1821
1924
|
}
|
|
1822
1925
|
await this.storage.deleteConversation(id);
|
|
1926
|
+
if (this.serverConversationIds.delete(id)) {
|
|
1927
|
+
this.conversationsGeneration++;
|
|
1928
|
+
}
|
|
1823
1929
|
this.conversations = this.conversations.filter((c) => c.id !== id);
|
|
1824
1930
|
if (this.conversationId === id) {
|
|
1825
1931
|
this.conversationId = null;
|
|
@@ -1836,6 +1942,64 @@ var ChatSession = class {
|
|
|
1836
1942
|
}
|
|
1837
1943
|
};
|
|
1838
1944
|
|
|
1945
|
+
// src/restore-plan.ts
|
|
1946
|
+
function planRestore(args) {
|
|
1947
|
+
const { completedJobs, userMessages } = args;
|
|
1948
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1949
|
+
userMessages.forEach((m, i) => {
|
|
1950
|
+
if (m.id) byId.set(m.id, i);
|
|
1951
|
+
});
|
|
1952
|
+
const linkOf = (j) => j.message_id ? byId.get(j.message_id) : void 0;
|
|
1953
|
+
const positional = (jobs, msgs) => jobs.map((job, i) => ({
|
|
1954
|
+
kind: "turn",
|
|
1955
|
+
jobId: job.job_id,
|
|
1956
|
+
content: msgs[i]?.content,
|
|
1957
|
+
messageId: msgs[i]?.id
|
|
1958
|
+
}));
|
|
1959
|
+
const firstLinked = completedJobs.findIndex((j) => linkOf(j) !== void 0);
|
|
1960
|
+
if (firstLinked === -1) {
|
|
1961
|
+
return positional(completedJobs, userMessages);
|
|
1962
|
+
}
|
|
1963
|
+
const cutover = linkOf(completedJobs[firstLinked]);
|
|
1964
|
+
const steps = positional(
|
|
1965
|
+
completedJobs.slice(0, firstLinked),
|
|
1966
|
+
userMessages.slice(0, cutover)
|
|
1967
|
+
);
|
|
1968
|
+
let cursor = cutover;
|
|
1969
|
+
const isSteer = (m) => !!m?.id && !completedJobs.some((j) => j.message_id === m.id);
|
|
1970
|
+
const drainTo = (stopAt) => {
|
|
1971
|
+
while (cursor < stopAt) {
|
|
1972
|
+
const m = userMessages[cursor++];
|
|
1973
|
+
if (isSteer(m)) {
|
|
1974
|
+
steps.push({ kind: "steer", content: m.content, messageId: m.id });
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
cursor = stopAt + 1;
|
|
1978
|
+
};
|
|
1979
|
+
for (const job of completedJobs.slice(firstLinked)) {
|
|
1980
|
+
const at = linkOf(job);
|
|
1981
|
+
if (at !== void 0) {
|
|
1982
|
+
drainTo(at);
|
|
1983
|
+
const prompt = userMessages[at];
|
|
1984
|
+
steps.push({
|
|
1985
|
+
kind: "turn",
|
|
1986
|
+
jobId: job.job_id,
|
|
1987
|
+
content: prompt.content,
|
|
1988
|
+
messageId: prompt.id
|
|
1989
|
+
});
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
steps.push({ kind: "turn", jobId: job.job_id });
|
|
1993
|
+
}
|
|
1994
|
+
for (let i = cursor; i < userMessages.length; i++) {
|
|
1995
|
+
const m = userMessages[i];
|
|
1996
|
+
if (isSteer(m)) {
|
|
1997
|
+
steps.push({ kind: "steer", content: m.content, messageId: m.id });
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return steps;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1839
2003
|
// src/types.ts
|
|
1840
2004
|
var ChatEventType = {
|
|
1841
2005
|
// Connection lifecycle (SDK-local, not wire)
|
|
@@ -2104,16 +2268,40 @@ var StreamManager = class {
|
|
|
2104
2268
|
const userMessages = this.session.messages.filter(
|
|
2105
2269
|
(m) => m.role === "user"
|
|
2106
2270
|
);
|
|
2271
|
+
const plan = planRestore({
|
|
2272
|
+
completedJobs: completedJobs.map((j) => ({
|
|
2273
|
+
job_id: j.job_id,
|
|
2274
|
+
message_id: j.message_id
|
|
2275
|
+
})),
|
|
2276
|
+
userMessages: userMessages.map((m) => ({
|
|
2277
|
+
id: m.id,
|
|
2278
|
+
content: m.content
|
|
2279
|
+
}))
|
|
2280
|
+
});
|
|
2107
2281
|
const eventLists = await Promise.all(
|
|
2108
2282
|
completedJobs.map(
|
|
2109
2283
|
(job) => this.session.client.getConversationEvents(conversationId, job.job_id).catch(() => [])
|
|
2110
2284
|
)
|
|
2111
2285
|
);
|
|
2112
|
-
|
|
2286
|
+
const eventsByJobId = new Map(
|
|
2287
|
+
completedJobs.map((job, i) => [job.job_id, eventLists[i] ?? []])
|
|
2288
|
+
);
|
|
2289
|
+
for (const step of plan) {
|
|
2290
|
+
if (step.kind === "steer") {
|
|
2291
|
+
this.session.replayTurn(
|
|
2292
|
+
conversationId,
|
|
2293
|
+
[],
|
|
2294
|
+
step.content,
|
|
2295
|
+
step.messageId,
|
|
2296
|
+
true
|
|
2297
|
+
);
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2113
2300
|
this.session.replayTurn(
|
|
2114
2301
|
conversationId,
|
|
2115
|
-
|
|
2116
|
-
|
|
2302
|
+
eventsByJobId.get(step.jobId) ?? [],
|
|
2303
|
+
step.content,
|
|
2304
|
+
step.messageId
|
|
2117
2305
|
);
|
|
2118
2306
|
}
|
|
2119
2307
|
if (completedJobs.length > 0) {
|
|
@@ -2205,6 +2393,7 @@ function parseEmbeddedResource(value) {
|
|
|
2205
2393
|
AstralformClient,
|
|
2206
2394
|
AstralformError,
|
|
2207
2395
|
AuthenticationError,
|
|
2396
|
+
CONVERSATION_PAGE_SIZE,
|
|
2208
2397
|
ChatEventType,
|
|
2209
2398
|
ChatSession,
|
|
2210
2399
|
ConnectionError,
|