@astralform/js 1.1.0 → 2.0.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/README.md +6 -6
- package/dist/index.cjs +146 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -21
- package/dist/index.d.ts +54 -21
- package/dist/index.js +146 -55
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ var RateLimitError = class extends AstralformError {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
var LLMNotConfiguredError = class extends AstralformError {
|
|
23
|
-
constructor(message = "LLM provider not configured for this
|
|
23
|
+
constructor(message = "LLM provider not configured for this agent") {
|
|
24
24
|
super(message, "llm_not_configured");
|
|
25
25
|
this.name = "LLMNotConfiguredError";
|
|
26
26
|
}
|
|
@@ -292,11 +292,11 @@ var AstralformClient = class {
|
|
|
292
292
|
"accessToken is required and must be a non-empty string in user-token mode"
|
|
293
293
|
);
|
|
294
294
|
}
|
|
295
|
-
const
|
|
295
|
+
const agentId = typeof config.agentId === "string" && config.agentId.length > 0 ? config.agentId : null;
|
|
296
296
|
this.auth = {
|
|
297
297
|
kind: "user_token",
|
|
298
298
|
accessToken: config.accessToken,
|
|
299
|
-
|
|
299
|
+
agentId,
|
|
300
300
|
endUserId: typeof config.endUserId === "string" && config.endUserId.length > 0 ? config.endUserId : null
|
|
301
301
|
};
|
|
302
302
|
}
|
|
@@ -318,17 +318,17 @@ var AstralformClient = class {
|
|
|
318
318
|
this.auth = { ...this.auth, accessToken };
|
|
319
319
|
}
|
|
320
320
|
/**
|
|
321
|
-
* Swap the active
|
|
322
|
-
* current developer has access to the new
|
|
321
|
+
* Swap the active agent for a user-token client. The backend verifies the
|
|
322
|
+
* current developer has access to the new agent; a 403 comes back if not.
|
|
323
323
|
*/
|
|
324
|
-
|
|
324
|
+
updateAgentId(agentId) {
|
|
325
325
|
if (this.auth.kind !== "user_token") {
|
|
326
|
-
throw new Error("
|
|
326
|
+
throw new Error("updateAgentId is only valid in user-token mode");
|
|
327
327
|
}
|
|
328
|
-
if (!
|
|
329
|
-
throw new Error("
|
|
328
|
+
if (!agentId || typeof agentId !== "string") {
|
|
329
|
+
throw new Error("agentId must be a non-empty string");
|
|
330
330
|
}
|
|
331
|
-
this.auth = { ...this.auth,
|
|
331
|
+
this.auth = { ...this.auth, agentId };
|
|
332
332
|
}
|
|
333
333
|
/**
|
|
334
334
|
* Set (or clear) the end-user override for user-token mode.
|
|
@@ -350,13 +350,13 @@ var AstralformClient = class {
|
|
|
350
350
|
return this.auth.kind === "user_token" ? this.auth.endUserId : null;
|
|
351
351
|
}
|
|
352
352
|
/**
|
|
353
|
-
* Active
|
|
354
|
-
* was constructed without one). For API-key mode the
|
|
353
|
+
* Active agent for user-token mode, or `null` if pre-pick (client
|
|
354
|
+
* was constructed without one). For API-key mode the agent is baked
|
|
355
355
|
* into the key, so this getter returns `null` there too — use
|
|
356
356
|
* `authMode` to disambiguate.
|
|
357
357
|
*/
|
|
358
|
-
get
|
|
359
|
-
return this.auth.kind === "user_token" ? this.auth.
|
|
358
|
+
get agentId() {
|
|
359
|
+
return this.auth.kind === "user_token" ? this.auth.agentId : null;
|
|
360
360
|
}
|
|
361
361
|
/** Which auth mode this client was constructed with. */
|
|
362
362
|
get authMode() {
|
|
@@ -378,8 +378,8 @@ var AstralformClient = class {
|
|
|
378
378
|
const headers = {
|
|
379
379
|
Authorization: `Bearer ${this.auth.accessToken}`
|
|
380
380
|
};
|
|
381
|
-
if (this.auth.
|
|
382
|
-
headers["X-Project-ID"] = this.auth.
|
|
381
|
+
if (this.auth.agentId) {
|
|
382
|
+
headers["X-Project-ID"] = this.auth.agentId;
|
|
383
383
|
}
|
|
384
384
|
if (this.auth.endUserId) {
|
|
385
385
|
headers["X-End-User-ID"] = this.auth.endUserId;
|
|
@@ -434,7 +434,9 @@ var AstralformClient = class {
|
|
|
434
434
|
async getHealth() {
|
|
435
435
|
return this.get("/v1/health");
|
|
436
436
|
}
|
|
437
|
-
|
|
437
|
+
// Agent readiness check. The path is a legacy wire name (shared with the
|
|
438
|
+
// iOS SDK) — it scopes to the client's active agent via X-Project-ID.
|
|
439
|
+
async getAgentStatus() {
|
|
438
440
|
const raw = await this.get("/v1/project/status");
|
|
439
441
|
const ui = raw.ui_components ?? {};
|
|
440
442
|
return {
|
|
@@ -477,6 +479,12 @@ var AstralformClient = class {
|
|
|
477
479
|
async deleteConversation(id) {
|
|
478
480
|
await this.del(`/v1/conversations/${encodeURIComponent(id)}`);
|
|
479
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* List the AI personas (sub-agents) available INSIDE the client's active
|
|
484
|
+
* agent workspace — orchestrator + specialists, addressed per message via
|
|
485
|
+
* `ChatStreamRequest.agent_name`. Not to be confused with `listAgents()`,
|
|
486
|
+
* which enumerates the team-level agents a signed-in user can open.
|
|
487
|
+
*/
|
|
480
488
|
async getAgents() {
|
|
481
489
|
const raw = await this.get("/v1/agents");
|
|
482
490
|
return raw.map((a) => ({
|
|
@@ -598,7 +606,7 @@ var AstralformClient = class {
|
|
|
598
606
|
}
|
|
599
607
|
// --- Account-scoped discovery (user-token mode) ---
|
|
600
608
|
//
|
|
601
|
-
// Lets a signed-in user pick which team/
|
|
609
|
+
// Lets a signed-in user pick which team/agent they want to act on.
|
|
602
610
|
// Backend gates these on OIDC user context (no X-Project-ID required) —
|
|
603
611
|
// sending them in API-key mode yields 401.
|
|
604
612
|
async listTeams() {
|
|
@@ -611,14 +619,19 @@ var AstralformClient = class {
|
|
|
611
619
|
role: t.role
|
|
612
620
|
}));
|
|
613
621
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
+
/**
|
|
623
|
+
* List the team-level agents (formerly "projects") the signed-in user can
|
|
624
|
+
* open — the pickable workspaces under a team. Not to be confused with
|
|
625
|
+
* `getAgents()`, which lists the AI personas inside the active agent.
|
|
626
|
+
*/
|
|
627
|
+
async listAgents(teamId) {
|
|
628
|
+
const raw = await this.get(`/v1/teams/${encodeURIComponent(teamId)}/agents`);
|
|
629
|
+
return raw.map((a) => ({
|
|
630
|
+
id: a.id,
|
|
631
|
+
name: a.name,
|
|
632
|
+
teamId: a.team_id,
|
|
633
|
+
createdAt: a.created_at,
|
|
634
|
+
updatedAt: a.updated_at
|
|
622
635
|
}));
|
|
623
636
|
}
|
|
624
637
|
// --- Jobs API ---
|
|
@@ -1144,6 +1157,11 @@ function translateWireEvent(wire) {
|
|
|
1144
1157
|
}
|
|
1145
1158
|
|
|
1146
1159
|
// src/session.ts
|
|
1160
|
+
var SSE_MAX_RECONNECTS = 6;
|
|
1161
|
+
var TOOL_RESULT_MAX_RETRIES = 3;
|
|
1162
|
+
function sseReconnectDelayMs(attempt) {
|
|
1163
|
+
return Math.min(500 * 2 ** (attempt - 1), 5e3);
|
|
1164
|
+
}
|
|
1147
1165
|
function pathEquals(a, b) {
|
|
1148
1166
|
if (a.length !== b.length) return false;
|
|
1149
1167
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -1156,7 +1174,7 @@ var ChatSession = class {
|
|
|
1156
1174
|
/**
|
|
1157
1175
|
* Pluggable UI protocol adapters. Consumers register a framework-
|
|
1158
1176
|
* specific adapter (e.g. React) for each MIME type they can render,
|
|
1159
|
-
* typically gated on ``session.
|
|
1177
|
+
* typically gated on ``session.agentStatus.uiComponents.protocol``.
|
|
1160
1178
|
* ``ToolBlock``-style consumers look up the adapter for an incoming
|
|
1161
1179
|
* embedded resource and hand off rendering.
|
|
1162
1180
|
*/
|
|
@@ -1166,7 +1184,7 @@ var ChatSession = class {
|
|
|
1166
1184
|
this.conversations = [];
|
|
1167
1185
|
this.messages = [];
|
|
1168
1186
|
this.isStreaming = false;
|
|
1169
|
-
this.
|
|
1187
|
+
this.agentStatus = null;
|
|
1170
1188
|
this.agents = [];
|
|
1171
1189
|
this.skills = [];
|
|
1172
1190
|
this.enabledClientTools = /* @__PURE__ */ new Set();
|
|
@@ -1180,6 +1198,13 @@ var ChatSession = class {
|
|
|
1180
1198
|
this.abortController = null;
|
|
1181
1199
|
/** Last received sequence number for resumable reconnection */
|
|
1182
1200
|
this.lastSeq = -1;
|
|
1201
|
+
/**
|
|
1202
|
+
* Client-tool call_ids whose result was already submitted this turn. On a
|
|
1203
|
+
* reconnect the resumed stream can replay a tool request we already handled;
|
|
1204
|
+
* this dedups so each is executed + submitted at most once (but a request we
|
|
1205
|
+
* never submitted still runs). Cleared at the start of each turn.
|
|
1206
|
+
*/
|
|
1207
|
+
this.submittedToolCallIds = /* @__PURE__ */ new Set();
|
|
1183
1208
|
/** Current job ID for cancellation */
|
|
1184
1209
|
this.currentJobId = null;
|
|
1185
1210
|
this.client = new AstralformClient(config);
|
|
@@ -1202,13 +1227,13 @@ var ChatSession = class {
|
|
|
1202
1227
|
}
|
|
1203
1228
|
async connect() {
|
|
1204
1229
|
const [status, conversations, agents, skills] = await Promise.allSettled([
|
|
1205
|
-
this.client.
|
|
1230
|
+
this.client.getAgentStatus(),
|
|
1206
1231
|
this.client.getConversations(),
|
|
1207
1232
|
this.client.getAgents().catch(() => []),
|
|
1208
1233
|
this.client.getSkills().catch(() => [])
|
|
1209
1234
|
]);
|
|
1210
1235
|
if (status.status === "fulfilled") {
|
|
1211
|
-
this.
|
|
1236
|
+
this.agentStatus = status.value;
|
|
1212
1237
|
}
|
|
1213
1238
|
if (conversations.status === "fulfilled") {
|
|
1214
1239
|
this.conversations = conversations.value;
|
|
@@ -1314,13 +1339,9 @@ var ChatSession = class {
|
|
|
1314
1339
|
}
|
|
1315
1340
|
const messageId = job.message_id;
|
|
1316
1341
|
this.lastSeq = -1;
|
|
1317
|
-
|
|
1318
|
-
job.job_id,
|
|
1319
|
-
this.lastSeq,
|
|
1320
|
-
this.abortController?.signal
|
|
1321
|
-
);
|
|
1342
|
+
this.submittedToolCallIds.clear();
|
|
1322
1343
|
await this.consumeEventStream(
|
|
1323
|
-
|
|
1344
|
+
job.job_id,
|
|
1324
1345
|
conversationId,
|
|
1325
1346
|
messageId,
|
|
1326
1347
|
true
|
|
@@ -1331,7 +1352,41 @@ var ChatSession = class {
|
|
|
1331
1352
|
* Shared event consumption loop. Parses each wire event, updates
|
|
1332
1353
|
* minimal session state, and emits typed ChatEvents to consumers.
|
|
1333
1354
|
*/
|
|
1334
|
-
async consumeEventStream(
|
|
1355
|
+
async consumeEventStream(jobId, conversationId, messageId, executeClientTools) {
|
|
1356
|
+
const signal = this.abortController?.signal;
|
|
1357
|
+
for (let attempt = 0; ; attempt++) {
|
|
1358
|
+
const stream = this.client.streamJobEvents(jobId, this.lastSeq, signal);
|
|
1359
|
+
let sawTerminal;
|
|
1360
|
+
try {
|
|
1361
|
+
sawTerminal = await this.pumpStream(
|
|
1362
|
+
stream,
|
|
1363
|
+
conversationId,
|
|
1364
|
+
messageId,
|
|
1365
|
+
executeClientTools
|
|
1366
|
+
);
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
if (signal?.aborted) return;
|
|
1369
|
+
if (err instanceof AuthenticationError || err instanceof RateLimitError) {
|
|
1370
|
+
throw err;
|
|
1371
|
+
}
|
|
1372
|
+
if (attempt >= SSE_MAX_RECONNECTS) throw err;
|
|
1373
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
if (sawTerminal || signal?.aborted) return;
|
|
1377
|
+
if (attempt >= SSE_MAX_RECONNECTS) {
|
|
1378
|
+
throw new ConnectionError("Lost connection to the response stream.");
|
|
1379
|
+
}
|
|
1380
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Consume a single SSE stream to exhaustion. Returns whether a terminal
|
|
1385
|
+
* event (``message_stop`` / ``error``) was seen, so the caller can decide
|
|
1386
|
+
* whether an ended stream means "turn done" vs "dropped, reconnect".
|
|
1387
|
+
*/
|
|
1388
|
+
async pumpStream(stream, conversationId, messageId, executeClientTools) {
|
|
1389
|
+
let sawTerminal = false;
|
|
1335
1390
|
for await (const raw of stream) {
|
|
1336
1391
|
let parsed;
|
|
1337
1392
|
try {
|
|
@@ -1349,6 +1404,9 @@ var ChatSession = class {
|
|
|
1349
1404
|
} catch {
|
|
1350
1405
|
continue;
|
|
1351
1406
|
}
|
|
1407
|
+
if (parsed.type === "message_stop" || parsed.type === "error") {
|
|
1408
|
+
sawTerminal = true;
|
|
1409
|
+
}
|
|
1352
1410
|
await this.dispatchWireEvent(
|
|
1353
1411
|
parsed,
|
|
1354
1412
|
conversationId,
|
|
@@ -1356,6 +1414,39 @@ var ChatSession = class {
|
|
|
1356
1414
|
executeClientTools
|
|
1357
1415
|
);
|
|
1358
1416
|
}
|
|
1417
|
+
return sawTerminal;
|
|
1418
|
+
}
|
|
1419
|
+
/** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
|
|
1420
|
+
sleepUnlessAborted(ms, signal) {
|
|
1421
|
+
return new Promise((resolve) => {
|
|
1422
|
+
if (signal?.aborted) return resolve();
|
|
1423
|
+
const timer = setTimeout(() => {
|
|
1424
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1425
|
+
resolve();
|
|
1426
|
+
}, ms);
|
|
1427
|
+
const onAbort = () => {
|
|
1428
|
+
clearTimeout(timer);
|
|
1429
|
+
resolve();
|
|
1430
|
+
};
|
|
1431
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
/** POST a client-tool result, retrying transient failures a few times. */
|
|
1435
|
+
async submitToolResultWithRetry(payload) {
|
|
1436
|
+
const signal = this.abortController?.signal;
|
|
1437
|
+
for (let attempt = 0; ; attempt++) {
|
|
1438
|
+
try {
|
|
1439
|
+
await this.client.submitToolResult(payload);
|
|
1440
|
+
return;
|
|
1441
|
+
} catch (err) {
|
|
1442
|
+
if (signal?.aborted) throw err;
|
|
1443
|
+
if (err instanceof AuthenticationError || err instanceof RateLimitError) {
|
|
1444
|
+
throw err;
|
|
1445
|
+
}
|
|
1446
|
+
if (attempt >= TOOL_RESULT_MAX_RETRIES) throw err;
|
|
1447
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1359
1450
|
}
|
|
1360
1451
|
async dispatchWireEvent(wire, conversationId, messageId, executeClientTools) {
|
|
1361
1452
|
this.applyWireSideEffects(wire, conversationId, messageId);
|
|
@@ -1365,18 +1456,22 @@ var ChatSession = class {
|
|
|
1365
1456
|
}
|
|
1366
1457
|
if (executeClientTools && wire.type === "block_stop" && wire.status === "awaiting_client_result" && wire.final?.call_id) {
|
|
1367
1458
|
const f = wire.final;
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1459
|
+
const callId = f.call_id ?? "";
|
|
1460
|
+
if (callId && !this.submittedToolCallIds.has(callId)) {
|
|
1461
|
+
const request = {
|
|
1462
|
+
callId,
|
|
1463
|
+
toolName: f.tool_name ?? "",
|
|
1464
|
+
arguments: f.input ?? {},
|
|
1465
|
+
isClientTool: true
|
|
1466
|
+
};
|
|
1467
|
+
const results = await this.executeClientTools([request]);
|
|
1468
|
+
await this.submitToolResultWithRetry({
|
|
1469
|
+
conversation_id: conversationId,
|
|
1470
|
+
message_id: messageId,
|
|
1471
|
+
tool_results: results
|
|
1472
|
+
});
|
|
1473
|
+
this.submittedToolCallIds.add(callId);
|
|
1474
|
+
}
|
|
1380
1475
|
}
|
|
1381
1476
|
}
|
|
1382
1477
|
/**
|
|
@@ -1473,16 +1568,12 @@ var ChatSession = class {
|
|
|
1473
1568
|
this.isStreaming = true;
|
|
1474
1569
|
this.currentJobId = jobId;
|
|
1475
1570
|
this.lastSeq = -1;
|
|
1571
|
+
this.submittedToolCallIds.clear();
|
|
1476
1572
|
this.resetStreamingState();
|
|
1477
1573
|
this.abortController = new AbortController();
|
|
1478
1574
|
try {
|
|
1479
|
-
const stream = this.client.streamJobEvents(
|
|
1480
|
-
jobId,
|
|
1481
|
-
this.lastSeq,
|
|
1482
|
-
this.abortController?.signal
|
|
1483
|
-
);
|
|
1484
1575
|
await this.consumeEventStream(
|
|
1485
|
-
|
|
1576
|
+
jobId,
|
|
1486
1577
|
this.conversationId ?? "",
|
|
1487
1578
|
"",
|
|
1488
1579
|
false
|