@astralform/js 1.0.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 +146 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +146 -25
- package/dist/index.js.map +1 -1
- package/package.json +11 -10
package/dist/index.cjs
CHANGED
|
@@ -554,6 +554,49 @@ var AstralformClient = class {
|
|
|
554
554
|
async submitToolApproval(request) {
|
|
555
555
|
await this.post("/v1/tool-approval", request);
|
|
556
556
|
}
|
|
557
|
+
// --- End-user tool-permission self-service ---
|
|
558
|
+
/**
|
|
559
|
+
* List the current end user's own remembered tool-permission grants.
|
|
560
|
+
* Only `conversation`/`always` grants exist (`once` is never persisted).
|
|
561
|
+
* Paginated via `limit` (default 100, max 200) / `offset`; `total` lets you
|
|
562
|
+
* page through all of them.
|
|
563
|
+
*/
|
|
564
|
+
async getMyToolPermissions(options) {
|
|
565
|
+
const params = new URLSearchParams();
|
|
566
|
+
if (options?.limit != null) {
|
|
567
|
+
const safeLimit = Math.max(
|
|
568
|
+
1,
|
|
569
|
+
Math.min(200, Math.floor(Number(options.limit)))
|
|
570
|
+
);
|
|
571
|
+
params.set("limit", String(safeLimit));
|
|
572
|
+
}
|
|
573
|
+
if (options?.offset != null) {
|
|
574
|
+
const safeOffset = Math.max(0, Math.floor(Number(options.offset)));
|
|
575
|
+
params.set("offset", String(safeOffset));
|
|
576
|
+
}
|
|
577
|
+
const qs = params.toString();
|
|
578
|
+
const raw = await this.get(`/v1/me/tool-permissions${qs ? `?${qs}` : ""}`);
|
|
579
|
+
return {
|
|
580
|
+
grants: raw.grants.map((g) => ({
|
|
581
|
+
id: g.id,
|
|
582
|
+
toolName: g.tool_name,
|
|
583
|
+
decision: g.decision,
|
|
584
|
+
scope: g.scope,
|
|
585
|
+
conversationId: g.conversation_id,
|
|
586
|
+
createdAt: g.created_at
|
|
587
|
+
})),
|
|
588
|
+
total: raw.total,
|
|
589
|
+
limit: raw.limit,
|
|
590
|
+
offset: raw.offset
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Revoke one of the current end user's remembered grants by id. The agent
|
|
595
|
+
* will ask again the next time that tool is used.
|
|
596
|
+
*/
|
|
597
|
+
async revokeToolPermission(id) {
|
|
598
|
+
await this.del(`/v1/me/tool-permissions/${encodeURIComponent(id)}`);
|
|
599
|
+
}
|
|
557
600
|
// --- Conversation Assets ---
|
|
558
601
|
mapAsset(raw) {
|
|
559
602
|
return {
|
|
@@ -1147,6 +1190,11 @@ function translateWireEvent(wire) {
|
|
|
1147
1190
|
}
|
|
1148
1191
|
|
|
1149
1192
|
// src/session.ts
|
|
1193
|
+
var SSE_MAX_RECONNECTS = 6;
|
|
1194
|
+
var TOOL_RESULT_MAX_RETRIES = 3;
|
|
1195
|
+
function sseReconnectDelayMs(attempt) {
|
|
1196
|
+
return Math.min(500 * 2 ** (attempt - 1), 5e3);
|
|
1197
|
+
}
|
|
1150
1198
|
function pathEquals(a, b) {
|
|
1151
1199
|
if (a.length !== b.length) return false;
|
|
1152
1200
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -1183,6 +1231,13 @@ var ChatSession = class {
|
|
|
1183
1231
|
this.abortController = null;
|
|
1184
1232
|
/** Last received sequence number for resumable reconnection */
|
|
1185
1233
|
this.lastSeq = -1;
|
|
1234
|
+
/**
|
|
1235
|
+
* Client-tool call_ids whose result was already submitted this turn. On a
|
|
1236
|
+
* reconnect the resumed stream can replay a tool request we already handled;
|
|
1237
|
+
* this dedups so each is executed + submitted at most once (but a request we
|
|
1238
|
+
* never submitted still runs). Cleared at the start of each turn.
|
|
1239
|
+
*/
|
|
1240
|
+
this.submittedToolCallIds = /* @__PURE__ */ new Set();
|
|
1186
1241
|
/** Current job ID for cancellation */
|
|
1187
1242
|
this.currentJobId = null;
|
|
1188
1243
|
this.client = new AstralformClient(config);
|
|
@@ -1317,13 +1372,9 @@ var ChatSession = class {
|
|
|
1317
1372
|
}
|
|
1318
1373
|
const messageId = job.message_id;
|
|
1319
1374
|
this.lastSeq = -1;
|
|
1320
|
-
|
|
1321
|
-
job.job_id,
|
|
1322
|
-
this.lastSeq,
|
|
1323
|
-
this.abortController?.signal
|
|
1324
|
-
);
|
|
1375
|
+
this.submittedToolCallIds.clear();
|
|
1325
1376
|
await this.consumeEventStream(
|
|
1326
|
-
|
|
1377
|
+
job.job_id,
|
|
1327
1378
|
conversationId,
|
|
1328
1379
|
messageId,
|
|
1329
1380
|
true
|
|
@@ -1334,7 +1385,41 @@ var ChatSession = class {
|
|
|
1334
1385
|
* Shared event consumption loop. Parses each wire event, updates
|
|
1335
1386
|
* minimal session state, and emits typed ChatEvents to consumers.
|
|
1336
1387
|
*/
|
|
1337
|
-
async consumeEventStream(
|
|
1388
|
+
async consumeEventStream(jobId, conversationId, messageId, executeClientTools) {
|
|
1389
|
+
const signal = this.abortController?.signal;
|
|
1390
|
+
for (let attempt = 0; ; attempt++) {
|
|
1391
|
+
const stream = this.client.streamJobEvents(jobId, this.lastSeq, signal);
|
|
1392
|
+
let sawTerminal;
|
|
1393
|
+
try {
|
|
1394
|
+
sawTerminal = await this.pumpStream(
|
|
1395
|
+
stream,
|
|
1396
|
+
conversationId,
|
|
1397
|
+
messageId,
|
|
1398
|
+
executeClientTools
|
|
1399
|
+
);
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
if (signal?.aborted) return;
|
|
1402
|
+
if (err instanceof AuthenticationError || err instanceof RateLimitError) {
|
|
1403
|
+
throw err;
|
|
1404
|
+
}
|
|
1405
|
+
if (attempt >= SSE_MAX_RECONNECTS) throw err;
|
|
1406
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (sawTerminal || signal?.aborted) return;
|
|
1410
|
+
if (attempt >= SSE_MAX_RECONNECTS) {
|
|
1411
|
+
throw new ConnectionError("Lost connection to the response stream.");
|
|
1412
|
+
}
|
|
1413
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Consume a single SSE stream to exhaustion. Returns whether a terminal
|
|
1418
|
+
* event (``message_stop`` / ``error``) was seen, so the caller can decide
|
|
1419
|
+
* whether an ended stream means "turn done" vs "dropped, reconnect".
|
|
1420
|
+
*/
|
|
1421
|
+
async pumpStream(stream, conversationId, messageId, executeClientTools) {
|
|
1422
|
+
let sawTerminal = false;
|
|
1338
1423
|
for await (const raw of stream) {
|
|
1339
1424
|
let parsed;
|
|
1340
1425
|
try {
|
|
@@ -1352,6 +1437,9 @@ var ChatSession = class {
|
|
|
1352
1437
|
} catch {
|
|
1353
1438
|
continue;
|
|
1354
1439
|
}
|
|
1440
|
+
if (parsed.type === "message_stop" || parsed.type === "error") {
|
|
1441
|
+
sawTerminal = true;
|
|
1442
|
+
}
|
|
1355
1443
|
await this.dispatchWireEvent(
|
|
1356
1444
|
parsed,
|
|
1357
1445
|
conversationId,
|
|
@@ -1359,6 +1447,39 @@ var ChatSession = class {
|
|
|
1359
1447
|
executeClientTools
|
|
1360
1448
|
);
|
|
1361
1449
|
}
|
|
1450
|
+
return sawTerminal;
|
|
1451
|
+
}
|
|
1452
|
+
/** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
|
|
1453
|
+
sleepUnlessAborted(ms, signal) {
|
|
1454
|
+
return new Promise((resolve) => {
|
|
1455
|
+
if (signal?.aborted) return resolve();
|
|
1456
|
+
const timer = setTimeout(() => {
|
|
1457
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1458
|
+
resolve();
|
|
1459
|
+
}, ms);
|
|
1460
|
+
const onAbort = () => {
|
|
1461
|
+
clearTimeout(timer);
|
|
1462
|
+
resolve();
|
|
1463
|
+
};
|
|
1464
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
/** POST a client-tool result, retrying transient failures a few times. */
|
|
1468
|
+
async submitToolResultWithRetry(payload) {
|
|
1469
|
+
const signal = this.abortController?.signal;
|
|
1470
|
+
for (let attempt = 0; ; attempt++) {
|
|
1471
|
+
try {
|
|
1472
|
+
await this.client.submitToolResult(payload);
|
|
1473
|
+
return;
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
if (signal?.aborted) throw err;
|
|
1476
|
+
if (err instanceof AuthenticationError || err instanceof RateLimitError) {
|
|
1477
|
+
throw err;
|
|
1478
|
+
}
|
|
1479
|
+
if (attempt >= TOOL_RESULT_MAX_RETRIES) throw err;
|
|
1480
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1362
1483
|
}
|
|
1363
1484
|
async dispatchWireEvent(wire, conversationId, messageId, executeClientTools) {
|
|
1364
1485
|
this.applyWireSideEffects(wire, conversationId, messageId);
|
|
@@ -1368,18 +1489,22 @@ var ChatSession = class {
|
|
|
1368
1489
|
}
|
|
1369
1490
|
if (executeClientTools && wire.type === "block_stop" && wire.status === "awaiting_client_result" && wire.final?.call_id) {
|
|
1370
1491
|
const f = wire.final;
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1492
|
+
const callId = f.call_id ?? "";
|
|
1493
|
+
if (callId && !this.submittedToolCallIds.has(callId)) {
|
|
1494
|
+
const request = {
|
|
1495
|
+
callId,
|
|
1496
|
+
toolName: f.tool_name ?? "",
|
|
1497
|
+
arguments: f.input ?? {},
|
|
1498
|
+
isClientTool: true
|
|
1499
|
+
};
|
|
1500
|
+
const results = await this.executeClientTools([request]);
|
|
1501
|
+
await this.submitToolResultWithRetry({
|
|
1502
|
+
conversation_id: conversationId,
|
|
1503
|
+
message_id: messageId,
|
|
1504
|
+
tool_results: results
|
|
1505
|
+
});
|
|
1506
|
+
this.submittedToolCallIds.add(callId);
|
|
1507
|
+
}
|
|
1383
1508
|
}
|
|
1384
1509
|
}
|
|
1385
1510
|
/**
|
|
@@ -1476,16 +1601,12 @@ var ChatSession = class {
|
|
|
1476
1601
|
this.isStreaming = true;
|
|
1477
1602
|
this.currentJobId = jobId;
|
|
1478
1603
|
this.lastSeq = -1;
|
|
1604
|
+
this.submittedToolCallIds.clear();
|
|
1479
1605
|
this.resetStreamingState();
|
|
1480
1606
|
this.abortController = new AbortController();
|
|
1481
1607
|
try {
|
|
1482
|
-
const stream = this.client.streamJobEvents(
|
|
1483
|
-
jobId,
|
|
1484
|
-
this.lastSeq,
|
|
1485
|
-
this.abortController?.signal
|
|
1486
|
-
);
|
|
1487
1608
|
await this.consumeEventStream(
|
|
1488
|
-
|
|
1609
|
+
jobId,
|
|
1489
1610
|
this.conversationId ?? "",
|
|
1490
1611
|
"",
|
|
1491
1612
|
false
|