@astralform/js 1.1.0 → 1.2.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 +103 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +103 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -917,6 +917,13 @@ declare class ChatSession {
|
|
|
917
917
|
private processStream;
|
|
918
918
|
/** Last received sequence number for resumable reconnection */
|
|
919
919
|
private lastSeq;
|
|
920
|
+
/**
|
|
921
|
+
* Client-tool call_ids whose result was already submitted this turn. On a
|
|
922
|
+
* reconnect the resumed stream can replay a tool request we already handled;
|
|
923
|
+
* this dedups so each is executed + submitted at most once (but a request we
|
|
924
|
+
* never submitted still runs). Cleared at the start of each turn.
|
|
925
|
+
*/
|
|
926
|
+
private submittedToolCallIds;
|
|
920
927
|
/** Current job ID for cancellation */
|
|
921
928
|
currentJobId: string | null;
|
|
922
929
|
private consumeJobStream;
|
|
@@ -925,6 +932,16 @@ declare class ChatSession {
|
|
|
925
932
|
* minimal session state, and emits typed ChatEvents to consumers.
|
|
926
933
|
*/
|
|
927
934
|
private consumeEventStream;
|
|
935
|
+
/**
|
|
936
|
+
* Consume a single SSE stream to exhaustion. Returns whether a terminal
|
|
937
|
+
* event (``message_stop`` / ``error``) was seen, so the caller can decide
|
|
938
|
+
* whether an ended stream means "turn done" vs "dropped, reconnect".
|
|
939
|
+
*/
|
|
940
|
+
private pumpStream;
|
|
941
|
+
/** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
|
|
942
|
+
private sleepUnlessAborted;
|
|
943
|
+
/** POST a client-tool result, retrying transient failures a few times. */
|
|
944
|
+
private submitToolResultWithRetry;
|
|
928
945
|
private dispatchWireEvent;
|
|
929
946
|
/**
|
|
930
947
|
* State mutations driven by wire events. Kept separate from translation so
|
package/dist/index.d.ts
CHANGED
|
@@ -917,6 +917,13 @@ declare class ChatSession {
|
|
|
917
917
|
private processStream;
|
|
918
918
|
/** Last received sequence number for resumable reconnection */
|
|
919
919
|
private lastSeq;
|
|
920
|
+
/**
|
|
921
|
+
* Client-tool call_ids whose result was already submitted this turn. On a
|
|
922
|
+
* reconnect the resumed stream can replay a tool request we already handled;
|
|
923
|
+
* this dedups so each is executed + submitted at most once (but a request we
|
|
924
|
+
* never submitted still runs). Cleared at the start of each turn.
|
|
925
|
+
*/
|
|
926
|
+
private submittedToolCallIds;
|
|
920
927
|
/** Current job ID for cancellation */
|
|
921
928
|
currentJobId: string | null;
|
|
922
929
|
private consumeJobStream;
|
|
@@ -925,6 +932,16 @@ declare class ChatSession {
|
|
|
925
932
|
* minimal session state, and emits typed ChatEvents to consumers.
|
|
926
933
|
*/
|
|
927
934
|
private consumeEventStream;
|
|
935
|
+
/**
|
|
936
|
+
* Consume a single SSE stream to exhaustion. Returns whether a terminal
|
|
937
|
+
* event (``message_stop`` / ``error``) was seen, so the caller can decide
|
|
938
|
+
* whether an ended stream means "turn done" vs "dropped, reconnect".
|
|
939
|
+
*/
|
|
940
|
+
private pumpStream;
|
|
941
|
+
/** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
|
|
942
|
+
private sleepUnlessAborted;
|
|
943
|
+
/** POST a client-tool result, retrying transient failures a few times. */
|
|
944
|
+
private submitToolResultWithRetry;
|
|
928
945
|
private dispatchWireEvent;
|
|
929
946
|
/**
|
|
930
947
|
* State mutations driven by wire events. Kept separate from translation so
|
package/dist/index.js
CHANGED
|
@@ -1144,6 +1144,11 @@ function translateWireEvent(wire) {
|
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
1146
|
// src/session.ts
|
|
1147
|
+
var SSE_MAX_RECONNECTS = 6;
|
|
1148
|
+
var TOOL_RESULT_MAX_RETRIES = 3;
|
|
1149
|
+
function sseReconnectDelayMs(attempt) {
|
|
1150
|
+
return Math.min(500 * 2 ** (attempt - 1), 5e3);
|
|
1151
|
+
}
|
|
1147
1152
|
function pathEquals(a, b) {
|
|
1148
1153
|
if (a.length !== b.length) return false;
|
|
1149
1154
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -1180,6 +1185,13 @@ var ChatSession = class {
|
|
|
1180
1185
|
this.abortController = null;
|
|
1181
1186
|
/** Last received sequence number for resumable reconnection */
|
|
1182
1187
|
this.lastSeq = -1;
|
|
1188
|
+
/**
|
|
1189
|
+
* Client-tool call_ids whose result was already submitted this turn. On a
|
|
1190
|
+
* reconnect the resumed stream can replay a tool request we already handled;
|
|
1191
|
+
* this dedups so each is executed + submitted at most once (but a request we
|
|
1192
|
+
* never submitted still runs). Cleared at the start of each turn.
|
|
1193
|
+
*/
|
|
1194
|
+
this.submittedToolCallIds = /* @__PURE__ */ new Set();
|
|
1183
1195
|
/** Current job ID for cancellation */
|
|
1184
1196
|
this.currentJobId = null;
|
|
1185
1197
|
this.client = new AstralformClient(config);
|
|
@@ -1314,13 +1326,9 @@ var ChatSession = class {
|
|
|
1314
1326
|
}
|
|
1315
1327
|
const messageId = job.message_id;
|
|
1316
1328
|
this.lastSeq = -1;
|
|
1317
|
-
|
|
1318
|
-
job.job_id,
|
|
1319
|
-
this.lastSeq,
|
|
1320
|
-
this.abortController?.signal
|
|
1321
|
-
);
|
|
1329
|
+
this.submittedToolCallIds.clear();
|
|
1322
1330
|
await this.consumeEventStream(
|
|
1323
|
-
|
|
1331
|
+
job.job_id,
|
|
1324
1332
|
conversationId,
|
|
1325
1333
|
messageId,
|
|
1326
1334
|
true
|
|
@@ -1331,7 +1339,41 @@ var ChatSession = class {
|
|
|
1331
1339
|
* Shared event consumption loop. Parses each wire event, updates
|
|
1332
1340
|
* minimal session state, and emits typed ChatEvents to consumers.
|
|
1333
1341
|
*/
|
|
1334
|
-
async consumeEventStream(
|
|
1342
|
+
async consumeEventStream(jobId, conversationId, messageId, executeClientTools) {
|
|
1343
|
+
const signal = this.abortController?.signal;
|
|
1344
|
+
for (let attempt = 0; ; attempt++) {
|
|
1345
|
+
const stream = this.client.streamJobEvents(jobId, this.lastSeq, signal);
|
|
1346
|
+
let sawTerminal;
|
|
1347
|
+
try {
|
|
1348
|
+
sawTerminal = await this.pumpStream(
|
|
1349
|
+
stream,
|
|
1350
|
+
conversationId,
|
|
1351
|
+
messageId,
|
|
1352
|
+
executeClientTools
|
|
1353
|
+
);
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
if (signal?.aborted) return;
|
|
1356
|
+
if (err instanceof AuthenticationError || err instanceof RateLimitError) {
|
|
1357
|
+
throw err;
|
|
1358
|
+
}
|
|
1359
|
+
if (attempt >= SSE_MAX_RECONNECTS) throw err;
|
|
1360
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
if (sawTerminal || signal?.aborted) return;
|
|
1364
|
+
if (attempt >= SSE_MAX_RECONNECTS) {
|
|
1365
|
+
throw new ConnectionError("Lost connection to the response stream.");
|
|
1366
|
+
}
|
|
1367
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Consume a single SSE stream to exhaustion. Returns whether a terminal
|
|
1372
|
+
* event (``message_stop`` / ``error``) was seen, so the caller can decide
|
|
1373
|
+
* whether an ended stream means "turn done" vs "dropped, reconnect".
|
|
1374
|
+
*/
|
|
1375
|
+
async pumpStream(stream, conversationId, messageId, executeClientTools) {
|
|
1376
|
+
let sawTerminal = false;
|
|
1335
1377
|
for await (const raw of stream) {
|
|
1336
1378
|
let parsed;
|
|
1337
1379
|
try {
|
|
@@ -1349,6 +1391,9 @@ var ChatSession = class {
|
|
|
1349
1391
|
} catch {
|
|
1350
1392
|
continue;
|
|
1351
1393
|
}
|
|
1394
|
+
if (parsed.type === "message_stop" || parsed.type === "error") {
|
|
1395
|
+
sawTerminal = true;
|
|
1396
|
+
}
|
|
1352
1397
|
await this.dispatchWireEvent(
|
|
1353
1398
|
parsed,
|
|
1354
1399
|
conversationId,
|
|
@@ -1356,6 +1401,39 @@ var ChatSession = class {
|
|
|
1356
1401
|
executeClientTools
|
|
1357
1402
|
);
|
|
1358
1403
|
}
|
|
1404
|
+
return sawTerminal;
|
|
1405
|
+
}
|
|
1406
|
+
/** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
|
|
1407
|
+
sleepUnlessAborted(ms, signal) {
|
|
1408
|
+
return new Promise((resolve) => {
|
|
1409
|
+
if (signal?.aborted) return resolve();
|
|
1410
|
+
const timer = setTimeout(() => {
|
|
1411
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1412
|
+
resolve();
|
|
1413
|
+
}, ms);
|
|
1414
|
+
const onAbort = () => {
|
|
1415
|
+
clearTimeout(timer);
|
|
1416
|
+
resolve();
|
|
1417
|
+
};
|
|
1418
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
/** POST a client-tool result, retrying transient failures a few times. */
|
|
1422
|
+
async submitToolResultWithRetry(payload) {
|
|
1423
|
+
const signal = this.abortController?.signal;
|
|
1424
|
+
for (let attempt = 0; ; attempt++) {
|
|
1425
|
+
try {
|
|
1426
|
+
await this.client.submitToolResult(payload);
|
|
1427
|
+
return;
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
if (signal?.aborted) throw err;
|
|
1430
|
+
if (err instanceof AuthenticationError || err instanceof RateLimitError) {
|
|
1431
|
+
throw err;
|
|
1432
|
+
}
|
|
1433
|
+
if (attempt >= TOOL_RESULT_MAX_RETRIES) throw err;
|
|
1434
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1359
1437
|
}
|
|
1360
1438
|
async dispatchWireEvent(wire, conversationId, messageId, executeClientTools) {
|
|
1361
1439
|
this.applyWireSideEffects(wire, conversationId, messageId);
|
|
@@ -1365,18 +1443,22 @@ var ChatSession = class {
|
|
|
1365
1443
|
}
|
|
1366
1444
|
if (executeClientTools && wire.type === "block_stop" && wire.status === "awaiting_client_result" && wire.final?.call_id) {
|
|
1367
1445
|
const f = wire.final;
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1446
|
+
const callId = f.call_id ?? "";
|
|
1447
|
+
if (callId && !this.submittedToolCallIds.has(callId)) {
|
|
1448
|
+
const request = {
|
|
1449
|
+
callId,
|
|
1450
|
+
toolName: f.tool_name ?? "",
|
|
1451
|
+
arguments: f.input ?? {},
|
|
1452
|
+
isClientTool: true
|
|
1453
|
+
};
|
|
1454
|
+
const results = await this.executeClientTools([request]);
|
|
1455
|
+
await this.submitToolResultWithRetry({
|
|
1456
|
+
conversation_id: conversationId,
|
|
1457
|
+
message_id: messageId,
|
|
1458
|
+
tool_results: results
|
|
1459
|
+
});
|
|
1460
|
+
this.submittedToolCallIds.add(callId);
|
|
1461
|
+
}
|
|
1380
1462
|
}
|
|
1381
1463
|
}
|
|
1382
1464
|
/**
|
|
@@ -1473,16 +1555,12 @@ var ChatSession = class {
|
|
|
1473
1555
|
this.isStreaming = true;
|
|
1474
1556
|
this.currentJobId = jobId;
|
|
1475
1557
|
this.lastSeq = -1;
|
|
1558
|
+
this.submittedToolCallIds.clear();
|
|
1476
1559
|
this.resetStreamingState();
|
|
1477
1560
|
this.abortController = new AbortController();
|
|
1478
1561
|
try {
|
|
1479
|
-
const stream = this.client.streamJobEvents(
|
|
1480
|
-
jobId,
|
|
1481
|
-
this.lastSeq,
|
|
1482
|
-
this.abortController?.signal
|
|
1483
|
-
);
|
|
1484
1562
|
await this.consumeEventStream(
|
|
1485
|
-
|
|
1563
|
+
jobId,
|
|
1486
1564
|
this.conversationId ?? "",
|
|
1487
1565
|
"",
|
|
1488
1566
|
false
|