@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/README.md
CHANGED
|
@@ -57,7 +57,7 @@ session.disconnect();
|
|
|
57
57
|
import { ChatSession } from "@astralform/js";
|
|
58
58
|
|
|
59
59
|
const session = new ChatSession({
|
|
60
|
-
apiKey: "your-api-key", // Required — Astralform
|
|
60
|
+
apiKey: "your-api-key", // Required — Astralform agent API key
|
|
61
61
|
userId: "user-123", // Required — identifies the end user
|
|
62
62
|
baseURL: "http://localhost:8000", // Optional — defaults to https://api.astralform.ai
|
|
63
63
|
fetch: customFetch, // Optional — custom fetch implementation
|
|
@@ -72,7 +72,7 @@ Subscribe to events with `.on()`, which returns an unsubscribe function. The SDK
|
|
|
72
72
|
const unsubscribe = session.on((event) => {
|
|
73
73
|
switch (event.type) {
|
|
74
74
|
case "connected":
|
|
75
|
-
// Session connected,
|
|
75
|
+
// Session connected, agent status and tools loaded
|
|
76
76
|
break;
|
|
77
77
|
|
|
78
78
|
// --- Turn lifecycle ---
|
|
@@ -212,10 +212,10 @@ import { ChatSession, parseEmbeddedResource } from "@astralform/js";
|
|
|
212
212
|
|
|
213
213
|
await session.connect();
|
|
214
214
|
|
|
215
|
-
// Gate registration on the
|
|
216
|
-
if (session.
|
|
215
|
+
// Gate registration on the agent's configured protocol.
|
|
216
|
+
if (session.agentStatus?.uiComponents.enabled) {
|
|
217
217
|
session.protocols.register({
|
|
218
|
-
mimeType: session.
|
|
218
|
+
mimeType: session.agentStatus.uiComponents.mimeType!,
|
|
219
219
|
render: (payload) => {
|
|
220
220
|
/* framework-specific render */
|
|
221
221
|
},
|
|
@@ -305,7 +305,7 @@ const client = new AstralformClient({
|
|
|
305
305
|
});
|
|
306
306
|
|
|
307
307
|
// REST endpoints
|
|
308
|
-
const status = await client.
|
|
308
|
+
const status = await client.getAgentStatus();
|
|
309
309
|
const conversations = await client.getConversations();
|
|
310
310
|
const messages = await client.getMessages("conversation-id");
|
|
311
311
|
const agents = await client.getAgents();
|
package/dist/index.cjs
CHANGED
|
@@ -66,7 +66,7 @@ var RateLimitError = class extends AstralformError {
|
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
68
|
var LLMNotConfiguredError = class extends AstralformError {
|
|
69
|
-
constructor(message = "LLM provider not configured for this
|
|
69
|
+
constructor(message = "LLM provider not configured for this agent") {
|
|
70
70
|
super(message, "llm_not_configured");
|
|
71
71
|
this.name = "LLMNotConfiguredError";
|
|
72
72
|
}
|
|
@@ -338,11 +338,11 @@ var AstralformClient = class {
|
|
|
338
338
|
"accessToken is required and must be a non-empty string in user-token mode"
|
|
339
339
|
);
|
|
340
340
|
}
|
|
341
|
-
const
|
|
341
|
+
const agentId = typeof config.agentId === "string" && config.agentId.length > 0 ? config.agentId : null;
|
|
342
342
|
this.auth = {
|
|
343
343
|
kind: "user_token",
|
|
344
344
|
accessToken: config.accessToken,
|
|
345
|
-
|
|
345
|
+
agentId,
|
|
346
346
|
endUserId: typeof config.endUserId === "string" && config.endUserId.length > 0 ? config.endUserId : null
|
|
347
347
|
};
|
|
348
348
|
}
|
|
@@ -364,17 +364,17 @@ var AstralformClient = class {
|
|
|
364
364
|
this.auth = { ...this.auth, accessToken };
|
|
365
365
|
}
|
|
366
366
|
/**
|
|
367
|
-
* Swap the active
|
|
368
|
-
* current developer has access to the new
|
|
367
|
+
* Swap the active agent for a user-token client. The backend verifies the
|
|
368
|
+
* current developer has access to the new agent; a 403 comes back if not.
|
|
369
369
|
*/
|
|
370
|
-
|
|
370
|
+
updateAgentId(agentId) {
|
|
371
371
|
if (this.auth.kind !== "user_token") {
|
|
372
|
-
throw new Error("
|
|
372
|
+
throw new Error("updateAgentId is only valid in user-token mode");
|
|
373
373
|
}
|
|
374
|
-
if (!
|
|
375
|
-
throw new Error("
|
|
374
|
+
if (!agentId || typeof agentId !== "string") {
|
|
375
|
+
throw new Error("agentId must be a non-empty string");
|
|
376
376
|
}
|
|
377
|
-
this.auth = { ...this.auth,
|
|
377
|
+
this.auth = { ...this.auth, agentId };
|
|
378
378
|
}
|
|
379
379
|
/**
|
|
380
380
|
* Set (or clear) the end-user override for user-token mode.
|
|
@@ -396,13 +396,13 @@ var AstralformClient = class {
|
|
|
396
396
|
return this.auth.kind === "user_token" ? this.auth.endUserId : null;
|
|
397
397
|
}
|
|
398
398
|
/**
|
|
399
|
-
* Active
|
|
400
|
-
* was constructed without one). For API-key mode the
|
|
399
|
+
* Active agent for user-token mode, or `null` if pre-pick (client
|
|
400
|
+
* was constructed without one). For API-key mode the agent is baked
|
|
401
401
|
* into the key, so this getter returns `null` there too — use
|
|
402
402
|
* `authMode` to disambiguate.
|
|
403
403
|
*/
|
|
404
|
-
get
|
|
405
|
-
return this.auth.kind === "user_token" ? this.auth.
|
|
404
|
+
get agentId() {
|
|
405
|
+
return this.auth.kind === "user_token" ? this.auth.agentId : null;
|
|
406
406
|
}
|
|
407
407
|
/** Which auth mode this client was constructed with. */
|
|
408
408
|
get authMode() {
|
|
@@ -424,8 +424,8 @@ var AstralformClient = class {
|
|
|
424
424
|
const headers = {
|
|
425
425
|
Authorization: `Bearer ${this.auth.accessToken}`
|
|
426
426
|
};
|
|
427
|
-
if (this.auth.
|
|
428
|
-
headers["X-Project-ID"] = this.auth.
|
|
427
|
+
if (this.auth.agentId) {
|
|
428
|
+
headers["X-Project-ID"] = this.auth.agentId;
|
|
429
429
|
}
|
|
430
430
|
if (this.auth.endUserId) {
|
|
431
431
|
headers["X-End-User-ID"] = this.auth.endUserId;
|
|
@@ -480,7 +480,9 @@ var AstralformClient = class {
|
|
|
480
480
|
async getHealth() {
|
|
481
481
|
return this.get("/v1/health");
|
|
482
482
|
}
|
|
483
|
-
|
|
483
|
+
// Agent readiness check. The path is a legacy wire name (shared with the
|
|
484
|
+
// iOS SDK) — it scopes to the client's active agent via X-Project-ID.
|
|
485
|
+
async getAgentStatus() {
|
|
484
486
|
const raw = await this.get("/v1/project/status");
|
|
485
487
|
const ui = raw.ui_components ?? {};
|
|
486
488
|
return {
|
|
@@ -523,6 +525,12 @@ var AstralformClient = class {
|
|
|
523
525
|
async deleteConversation(id) {
|
|
524
526
|
await this.del(`/v1/conversations/${encodeURIComponent(id)}`);
|
|
525
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* List the AI personas (sub-agents) available INSIDE the client's active
|
|
530
|
+
* agent workspace — orchestrator + specialists, addressed per message via
|
|
531
|
+
* `ChatStreamRequest.agent_name`. Not to be confused with `listAgents()`,
|
|
532
|
+
* which enumerates the team-level agents a signed-in user can open.
|
|
533
|
+
*/
|
|
526
534
|
async getAgents() {
|
|
527
535
|
const raw = await this.get("/v1/agents");
|
|
528
536
|
return raw.map((a) => ({
|
|
@@ -644,7 +652,7 @@ var AstralformClient = class {
|
|
|
644
652
|
}
|
|
645
653
|
// --- Account-scoped discovery (user-token mode) ---
|
|
646
654
|
//
|
|
647
|
-
// Lets a signed-in user pick which team/
|
|
655
|
+
// Lets a signed-in user pick which team/agent they want to act on.
|
|
648
656
|
// Backend gates these on OIDC user context (no X-Project-ID required) —
|
|
649
657
|
// sending them in API-key mode yields 401.
|
|
650
658
|
async listTeams() {
|
|
@@ -657,14 +665,19 @@ var AstralformClient = class {
|
|
|
657
665
|
role: t.role
|
|
658
666
|
}));
|
|
659
667
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
+
/**
|
|
669
|
+
* List the team-level agents (formerly "projects") the signed-in user can
|
|
670
|
+
* open — the pickable workspaces under a team. Not to be confused with
|
|
671
|
+
* `getAgents()`, which lists the AI personas inside the active agent.
|
|
672
|
+
*/
|
|
673
|
+
async listAgents(teamId) {
|
|
674
|
+
const raw = await this.get(`/v1/teams/${encodeURIComponent(teamId)}/agents`);
|
|
675
|
+
return raw.map((a) => ({
|
|
676
|
+
id: a.id,
|
|
677
|
+
name: a.name,
|
|
678
|
+
teamId: a.team_id,
|
|
679
|
+
createdAt: a.created_at,
|
|
680
|
+
updatedAt: a.updated_at
|
|
668
681
|
}));
|
|
669
682
|
}
|
|
670
683
|
// --- Jobs API ---
|
|
@@ -1190,6 +1203,11 @@ function translateWireEvent(wire) {
|
|
|
1190
1203
|
}
|
|
1191
1204
|
|
|
1192
1205
|
// src/session.ts
|
|
1206
|
+
var SSE_MAX_RECONNECTS = 6;
|
|
1207
|
+
var TOOL_RESULT_MAX_RETRIES = 3;
|
|
1208
|
+
function sseReconnectDelayMs(attempt) {
|
|
1209
|
+
return Math.min(500 * 2 ** (attempt - 1), 5e3);
|
|
1210
|
+
}
|
|
1193
1211
|
function pathEquals(a, b) {
|
|
1194
1212
|
if (a.length !== b.length) return false;
|
|
1195
1213
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -1202,7 +1220,7 @@ var ChatSession = class {
|
|
|
1202
1220
|
/**
|
|
1203
1221
|
* Pluggable UI protocol adapters. Consumers register a framework-
|
|
1204
1222
|
* specific adapter (e.g. React) for each MIME type they can render,
|
|
1205
|
-
* typically gated on ``session.
|
|
1223
|
+
* typically gated on ``session.agentStatus.uiComponents.protocol``.
|
|
1206
1224
|
* ``ToolBlock``-style consumers look up the adapter for an incoming
|
|
1207
1225
|
* embedded resource and hand off rendering.
|
|
1208
1226
|
*/
|
|
@@ -1212,7 +1230,7 @@ var ChatSession = class {
|
|
|
1212
1230
|
this.conversations = [];
|
|
1213
1231
|
this.messages = [];
|
|
1214
1232
|
this.isStreaming = false;
|
|
1215
|
-
this.
|
|
1233
|
+
this.agentStatus = null;
|
|
1216
1234
|
this.agents = [];
|
|
1217
1235
|
this.skills = [];
|
|
1218
1236
|
this.enabledClientTools = /* @__PURE__ */ new Set();
|
|
@@ -1226,6 +1244,13 @@ var ChatSession = class {
|
|
|
1226
1244
|
this.abortController = null;
|
|
1227
1245
|
/** Last received sequence number for resumable reconnection */
|
|
1228
1246
|
this.lastSeq = -1;
|
|
1247
|
+
/**
|
|
1248
|
+
* Client-tool call_ids whose result was already submitted this turn. On a
|
|
1249
|
+
* reconnect the resumed stream can replay a tool request we already handled;
|
|
1250
|
+
* this dedups so each is executed + submitted at most once (but a request we
|
|
1251
|
+
* never submitted still runs). Cleared at the start of each turn.
|
|
1252
|
+
*/
|
|
1253
|
+
this.submittedToolCallIds = /* @__PURE__ */ new Set();
|
|
1229
1254
|
/** Current job ID for cancellation */
|
|
1230
1255
|
this.currentJobId = null;
|
|
1231
1256
|
this.client = new AstralformClient(config);
|
|
@@ -1248,13 +1273,13 @@ var ChatSession = class {
|
|
|
1248
1273
|
}
|
|
1249
1274
|
async connect() {
|
|
1250
1275
|
const [status, conversations, agents, skills] = await Promise.allSettled([
|
|
1251
|
-
this.client.
|
|
1276
|
+
this.client.getAgentStatus(),
|
|
1252
1277
|
this.client.getConversations(),
|
|
1253
1278
|
this.client.getAgents().catch(() => []),
|
|
1254
1279
|
this.client.getSkills().catch(() => [])
|
|
1255
1280
|
]);
|
|
1256
1281
|
if (status.status === "fulfilled") {
|
|
1257
|
-
this.
|
|
1282
|
+
this.agentStatus = status.value;
|
|
1258
1283
|
}
|
|
1259
1284
|
if (conversations.status === "fulfilled") {
|
|
1260
1285
|
this.conversations = conversations.value;
|
|
@@ -1360,13 +1385,9 @@ var ChatSession = class {
|
|
|
1360
1385
|
}
|
|
1361
1386
|
const messageId = job.message_id;
|
|
1362
1387
|
this.lastSeq = -1;
|
|
1363
|
-
|
|
1364
|
-
job.job_id,
|
|
1365
|
-
this.lastSeq,
|
|
1366
|
-
this.abortController?.signal
|
|
1367
|
-
);
|
|
1388
|
+
this.submittedToolCallIds.clear();
|
|
1368
1389
|
await this.consumeEventStream(
|
|
1369
|
-
|
|
1390
|
+
job.job_id,
|
|
1370
1391
|
conversationId,
|
|
1371
1392
|
messageId,
|
|
1372
1393
|
true
|
|
@@ -1377,7 +1398,41 @@ var ChatSession = class {
|
|
|
1377
1398
|
* Shared event consumption loop. Parses each wire event, updates
|
|
1378
1399
|
* minimal session state, and emits typed ChatEvents to consumers.
|
|
1379
1400
|
*/
|
|
1380
|
-
async consumeEventStream(
|
|
1401
|
+
async consumeEventStream(jobId, conversationId, messageId, executeClientTools) {
|
|
1402
|
+
const signal = this.abortController?.signal;
|
|
1403
|
+
for (let attempt = 0; ; attempt++) {
|
|
1404
|
+
const stream = this.client.streamJobEvents(jobId, this.lastSeq, signal);
|
|
1405
|
+
let sawTerminal;
|
|
1406
|
+
try {
|
|
1407
|
+
sawTerminal = await this.pumpStream(
|
|
1408
|
+
stream,
|
|
1409
|
+
conversationId,
|
|
1410
|
+
messageId,
|
|
1411
|
+
executeClientTools
|
|
1412
|
+
);
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
if (signal?.aborted) return;
|
|
1415
|
+
if (err instanceof AuthenticationError || err instanceof RateLimitError) {
|
|
1416
|
+
throw err;
|
|
1417
|
+
}
|
|
1418
|
+
if (attempt >= SSE_MAX_RECONNECTS) throw err;
|
|
1419
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
if (sawTerminal || signal?.aborted) return;
|
|
1423
|
+
if (attempt >= SSE_MAX_RECONNECTS) {
|
|
1424
|
+
throw new ConnectionError("Lost connection to the response stream.");
|
|
1425
|
+
}
|
|
1426
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Consume a single SSE stream to exhaustion. Returns whether a terminal
|
|
1431
|
+
* event (``message_stop`` / ``error``) was seen, so the caller can decide
|
|
1432
|
+
* whether an ended stream means "turn done" vs "dropped, reconnect".
|
|
1433
|
+
*/
|
|
1434
|
+
async pumpStream(stream, conversationId, messageId, executeClientTools) {
|
|
1435
|
+
let sawTerminal = false;
|
|
1381
1436
|
for await (const raw of stream) {
|
|
1382
1437
|
let parsed;
|
|
1383
1438
|
try {
|
|
@@ -1395,6 +1450,9 @@ var ChatSession = class {
|
|
|
1395
1450
|
} catch {
|
|
1396
1451
|
continue;
|
|
1397
1452
|
}
|
|
1453
|
+
if (parsed.type === "message_stop" || parsed.type === "error") {
|
|
1454
|
+
sawTerminal = true;
|
|
1455
|
+
}
|
|
1398
1456
|
await this.dispatchWireEvent(
|
|
1399
1457
|
parsed,
|
|
1400
1458
|
conversationId,
|
|
@@ -1402,6 +1460,39 @@ var ChatSession = class {
|
|
|
1402
1460
|
executeClientTools
|
|
1403
1461
|
);
|
|
1404
1462
|
}
|
|
1463
|
+
return sawTerminal;
|
|
1464
|
+
}
|
|
1465
|
+
/** Sleep for ``ms``, resolving early if the turn is aborted mid-backoff. */
|
|
1466
|
+
sleepUnlessAborted(ms, signal) {
|
|
1467
|
+
return new Promise((resolve) => {
|
|
1468
|
+
if (signal?.aborted) return resolve();
|
|
1469
|
+
const timer = setTimeout(() => {
|
|
1470
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1471
|
+
resolve();
|
|
1472
|
+
}, ms);
|
|
1473
|
+
const onAbort = () => {
|
|
1474
|
+
clearTimeout(timer);
|
|
1475
|
+
resolve();
|
|
1476
|
+
};
|
|
1477
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
/** POST a client-tool result, retrying transient failures a few times. */
|
|
1481
|
+
async submitToolResultWithRetry(payload) {
|
|
1482
|
+
const signal = this.abortController?.signal;
|
|
1483
|
+
for (let attempt = 0; ; attempt++) {
|
|
1484
|
+
try {
|
|
1485
|
+
await this.client.submitToolResult(payload);
|
|
1486
|
+
return;
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
if (signal?.aborted) throw err;
|
|
1489
|
+
if (err instanceof AuthenticationError || err instanceof RateLimitError) {
|
|
1490
|
+
throw err;
|
|
1491
|
+
}
|
|
1492
|
+
if (attempt >= TOOL_RESULT_MAX_RETRIES) throw err;
|
|
1493
|
+
await this.sleepUnlessAborted(sseReconnectDelayMs(attempt + 1), signal);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1405
1496
|
}
|
|
1406
1497
|
async dispatchWireEvent(wire, conversationId, messageId, executeClientTools) {
|
|
1407
1498
|
this.applyWireSideEffects(wire, conversationId, messageId);
|
|
@@ -1411,18 +1502,22 @@ var ChatSession = class {
|
|
|
1411
1502
|
}
|
|
1412
1503
|
if (executeClientTools && wire.type === "block_stop" && wire.status === "awaiting_client_result" && wire.final?.call_id) {
|
|
1413
1504
|
const f = wire.final;
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1505
|
+
const callId = f.call_id ?? "";
|
|
1506
|
+
if (callId && !this.submittedToolCallIds.has(callId)) {
|
|
1507
|
+
const request = {
|
|
1508
|
+
callId,
|
|
1509
|
+
toolName: f.tool_name ?? "",
|
|
1510
|
+
arguments: f.input ?? {},
|
|
1511
|
+
isClientTool: true
|
|
1512
|
+
};
|
|
1513
|
+
const results = await this.executeClientTools([request]);
|
|
1514
|
+
await this.submitToolResultWithRetry({
|
|
1515
|
+
conversation_id: conversationId,
|
|
1516
|
+
message_id: messageId,
|
|
1517
|
+
tool_results: results
|
|
1518
|
+
});
|
|
1519
|
+
this.submittedToolCallIds.add(callId);
|
|
1520
|
+
}
|
|
1426
1521
|
}
|
|
1427
1522
|
}
|
|
1428
1523
|
/**
|
|
@@ -1519,16 +1614,12 @@ var ChatSession = class {
|
|
|
1519
1614
|
this.isStreaming = true;
|
|
1520
1615
|
this.currentJobId = jobId;
|
|
1521
1616
|
this.lastSeq = -1;
|
|
1617
|
+
this.submittedToolCallIds.clear();
|
|
1522
1618
|
this.resetStreamingState();
|
|
1523
1619
|
this.abortController = new AbortController();
|
|
1524
1620
|
try {
|
|
1525
|
-
const stream = this.client.streamJobEvents(
|
|
1526
|
-
jobId,
|
|
1527
|
-
this.lastSeq,
|
|
1528
|
-
this.abortController?.signal
|
|
1529
|
-
);
|
|
1530
1621
|
await this.consumeEventStream(
|
|
1531
|
-
|
|
1622
|
+
jobId,
|
|
1532
1623
|
this.conversationId ?? "",
|
|
1533
1624
|
"",
|
|
1534
1625
|
false
|