@astralform/js 0.1.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 +349 -0
- package/dist/index.cjs +1006 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +526 -0
- package/dist/index.d.ts +526 -0
- package/dist/index.js +967 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var AstralformError = class extends Error {
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "AstralformError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var AuthenticationError = class extends AstralformError {
|
|
10
|
+
constructor(message = "Invalid or missing API key") {
|
|
11
|
+
super(message, "authentication_error");
|
|
12
|
+
this.name = "AuthenticationError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var RateLimitError = class extends AstralformError {
|
|
16
|
+
constructor(message = "Rate limit exceeded") {
|
|
17
|
+
super(message, "rate_limit_error");
|
|
18
|
+
this.name = "RateLimitError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var LLMNotConfiguredError = class extends AstralformError {
|
|
22
|
+
constructor(message = "LLM provider not configured for this project") {
|
|
23
|
+
super(message, "llm_not_configured");
|
|
24
|
+
this.name = "LLMNotConfiguredError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var ServerError = class extends AstralformError {
|
|
28
|
+
constructor(message = "Internal server error") {
|
|
29
|
+
super(message, "server_error");
|
|
30
|
+
this.name = "ServerError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var ConnectionError = class extends AstralformError {
|
|
34
|
+
constructor(message = "Failed to connect to server") {
|
|
35
|
+
super(message, "connection_error");
|
|
36
|
+
this.name = "ConnectionError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var StreamAbortedError = class extends AstralformError {
|
|
40
|
+
constructor(message = "Stream was aborted") {
|
|
41
|
+
super(message, "stream_aborted");
|
|
42
|
+
this.name = "StreamAbortedError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/streaming.ts
|
|
47
|
+
async function* streamJobSSE(options) {
|
|
48
|
+
const { url, headers, signal, fetchFn } = options;
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await fetchFn(url, {
|
|
52
|
+
method: "GET",
|
|
53
|
+
headers,
|
|
54
|
+
signal
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
58
|
+
throw new StreamAbortedError();
|
|
59
|
+
}
|
|
60
|
+
throw new ConnectionError(
|
|
61
|
+
err instanceof Error ? err.message : "Failed to connect"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const rawText = await response.text().catch(() => "");
|
|
66
|
+
const text = rawText ? rawText.slice(0, 500).replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]") : "";
|
|
67
|
+
switch (response.status) {
|
|
68
|
+
case 401:
|
|
69
|
+
throw new AuthenticationError();
|
|
70
|
+
case 429:
|
|
71
|
+
throw new RateLimitError();
|
|
72
|
+
default:
|
|
73
|
+
throw new ServerError(text || `HTTP ${response.status}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!response.body) {
|
|
77
|
+
throw new ConnectionError("Response body is null");
|
|
78
|
+
}
|
|
79
|
+
const reader = response.body.getReader();
|
|
80
|
+
const decoder = new TextDecoder();
|
|
81
|
+
let buffer = "";
|
|
82
|
+
let currentEvent = "";
|
|
83
|
+
try {
|
|
84
|
+
while (true) {
|
|
85
|
+
const { done, value } = await reader.read();
|
|
86
|
+
if (done) break;
|
|
87
|
+
buffer += decoder.decode(value, { stream: true });
|
|
88
|
+
const lines = buffer.split("\n");
|
|
89
|
+
buffer = lines.pop() ?? "";
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
if (line.startsWith("event: ")) {
|
|
92
|
+
currentEvent = line.slice(7).trim();
|
|
93
|
+
} else if (line.startsWith("data: ")) {
|
|
94
|
+
const data = line.slice(6);
|
|
95
|
+
if (data === "[DONE]") {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
yield { event: currentEvent || "message", data };
|
|
99
|
+
}
|
|
100
|
+
if (line === "") {
|
|
101
|
+
currentEvent = "";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
107
|
+
throw new StreamAbortedError();
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
} finally {
|
|
111
|
+
reader.releaseLock();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/client.ts
|
|
116
|
+
var DEFAULT_BASE_URL = "https://api.astralform.ai";
|
|
117
|
+
function validateBaseURL(url) {
|
|
118
|
+
const cleaned = url.replace(/\/+$/, "");
|
|
119
|
+
try {
|
|
120
|
+
const parsed = new URL(cleaned);
|
|
121
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Invalid baseURL protocol "${parsed.protocol}" - only http: and https: are allowed`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return parsed.origin + parsed.pathname.replace(/\/+$/, "");
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err instanceof Error && err.message.includes("Invalid baseURL")) {
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Invalid baseURL: "${cleaned}" is not a valid URL`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
var AstralformClient = class {
|
|
135
|
+
constructor(config) {
|
|
136
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
137
|
+
throw new Error("apiKey is required and must be a non-empty string");
|
|
138
|
+
}
|
|
139
|
+
this.apiKey = config.apiKey;
|
|
140
|
+
this.baseURL = validateBaseURL(config.baseURL ?? DEFAULT_BASE_URL);
|
|
141
|
+
this.userId = config.userId;
|
|
142
|
+
this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
143
|
+
}
|
|
144
|
+
get headers() {
|
|
145
|
+
return {
|
|
146
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
147
|
+
"X-End-User-ID": this.userId,
|
|
148
|
+
"Content-Type": "application/json"
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async request(method, path, body) {
|
|
152
|
+
const response = await this.fetchFn(`${this.baseURL}${path}`, {
|
|
153
|
+
method,
|
|
154
|
+
headers: this.headers,
|
|
155
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
156
|
+
}).catch((err) => {
|
|
157
|
+
throw new ConnectionError(
|
|
158
|
+
err instanceof Error ? err.message : "Failed to connect"
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
await this.handleError(response);
|
|
162
|
+
return response;
|
|
163
|
+
}
|
|
164
|
+
async get(path) {
|
|
165
|
+
const response = await this.request("GET", path);
|
|
166
|
+
return response.json();
|
|
167
|
+
}
|
|
168
|
+
async post(path, body) {
|
|
169
|
+
const response = await this.request("POST", path, body);
|
|
170
|
+
return response.json();
|
|
171
|
+
}
|
|
172
|
+
async del(path) {
|
|
173
|
+
await this.request("DELETE", path);
|
|
174
|
+
}
|
|
175
|
+
async handleError(response) {
|
|
176
|
+
if (response.ok) return;
|
|
177
|
+
const text = await response.text().catch(() => "");
|
|
178
|
+
switch (response.status) {
|
|
179
|
+
case 401:
|
|
180
|
+
throw new AuthenticationError();
|
|
181
|
+
case 429:
|
|
182
|
+
throw new RateLimitError();
|
|
183
|
+
default: {
|
|
184
|
+
const safeText = text ? text.slice(0, 500).replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]") : "";
|
|
185
|
+
throw new ServerError(safeText || `HTTP ${response.status}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// --- REST Methods ---
|
|
190
|
+
async getHealth() {
|
|
191
|
+
return this.get("/v1/health");
|
|
192
|
+
}
|
|
193
|
+
async getProjectStatus() {
|
|
194
|
+
const raw = await this.get("/v1/project/status");
|
|
195
|
+
return {
|
|
196
|
+
isReady: raw.is_ready,
|
|
197
|
+
llmConfigured: raw.llm_configured,
|
|
198
|
+
llmProvider: raw.llm_provider,
|
|
199
|
+
llmModel: raw.llm_model,
|
|
200
|
+
message: raw.message
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async getConversations(limit = 50, offset = 0) {
|
|
204
|
+
const safeLimit = Math.max(1, Math.min(200, Math.floor(Number(limit))));
|
|
205
|
+
const safeOffset = Math.max(0, Math.floor(Number(offset)));
|
|
206
|
+
const raw = await this.get(`/v1/conversations?limit=${safeLimit}&offset=${safeOffset}`);
|
|
207
|
+
return raw.map((c) => ({
|
|
208
|
+
id: c.id,
|
|
209
|
+
title: c.title,
|
|
210
|
+
messageCount: c.message_count,
|
|
211
|
+
createdAt: c.created_at,
|
|
212
|
+
updatedAt: c.updated_at
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
async getMessages(conversationId) {
|
|
216
|
+
const raw = await this.get(`/v1/conversations/${encodeURIComponent(conversationId)}/messages`);
|
|
217
|
+
return raw.map((m) => ({
|
|
218
|
+
id: m.id,
|
|
219
|
+
conversationId: m.conversation_id,
|
|
220
|
+
role: m.role,
|
|
221
|
+
content: m.content,
|
|
222
|
+
parentId: m.parent_id,
|
|
223
|
+
status: "complete",
|
|
224
|
+
createdAt: m.created_at
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
async deleteConversation(id) {
|
|
228
|
+
await this.del(`/v1/conversations/${encodeURIComponent(id)}`);
|
|
229
|
+
}
|
|
230
|
+
async getAgents() {
|
|
231
|
+
const raw = await this.get("/v1/agents");
|
|
232
|
+
return raw.map((a) => ({
|
|
233
|
+
name: a.name,
|
|
234
|
+
displayName: a.display_name,
|
|
235
|
+
description: a.description,
|
|
236
|
+
isOrchestrator: a.is_orchestrator,
|
|
237
|
+
isEnabled: a.is_enabled,
|
|
238
|
+
avatarUrl: a.avatar_url
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
async getSkills() {
|
|
242
|
+
const raw = await this.get("/v1/skills");
|
|
243
|
+
return raw.map((s) => ({
|
|
244
|
+
name: s.name,
|
|
245
|
+
displayName: s.display_name,
|
|
246
|
+
description: s.description,
|
|
247
|
+
isEnabled: s.is_enabled
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
async submitToolResult(request) {
|
|
251
|
+
await this.post("/v1/tool-result", request);
|
|
252
|
+
}
|
|
253
|
+
// --- Conversation Assets ---
|
|
254
|
+
mapAsset(raw) {
|
|
255
|
+
return {
|
|
256
|
+
id: raw.id,
|
|
257
|
+
kind: raw.kind,
|
|
258
|
+
originalName: raw.original_name,
|
|
259
|
+
mediaType: raw.media_type,
|
|
260
|
+
sizeBytes: raw.size_bytes,
|
|
261
|
+
workspacePath: raw.workspace_path,
|
|
262
|
+
sourceMessageId: raw.source_message_id,
|
|
263
|
+
agentName: raw.agent_name,
|
|
264
|
+
createdAt: raw.created_at
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async uploadFile(conversationId, file, filename) {
|
|
268
|
+
const formData = new FormData();
|
|
269
|
+
formData.append("file", file, filename);
|
|
270
|
+
const response = await this.fetchFn(
|
|
271
|
+
`${this.baseURL}/v1/conversations/${encodeURIComponent(conversationId)}/uploads`,
|
|
272
|
+
{
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: {
|
|
275
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
276
|
+
"X-End-User-ID": this.userId
|
|
277
|
+
},
|
|
278
|
+
body: formData
|
|
279
|
+
}
|
|
280
|
+
).catch((err) => {
|
|
281
|
+
throw new ConnectionError(
|
|
282
|
+
err instanceof Error ? err.message : "Failed to connect"
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
await this.handleError(response);
|
|
286
|
+
const raw = await response.json();
|
|
287
|
+
return this.mapAsset(raw);
|
|
288
|
+
}
|
|
289
|
+
async listUploads(conversationId) {
|
|
290
|
+
const raw = await this.get(
|
|
291
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}/uploads`
|
|
292
|
+
);
|
|
293
|
+
return raw.map((r) => this.mapAsset(r));
|
|
294
|
+
}
|
|
295
|
+
async listOutputs(conversationId) {
|
|
296
|
+
const raw = await this.get(
|
|
297
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}/outputs`
|
|
298
|
+
);
|
|
299
|
+
return raw.map((r) => this.mapAsset(r));
|
|
300
|
+
}
|
|
301
|
+
// --- Jobs API ---
|
|
302
|
+
async createJob(request) {
|
|
303
|
+
return this.post("/v1/jobs", request);
|
|
304
|
+
}
|
|
305
|
+
async *streamJobEvents(jobId, afterSeq = -1, signal) {
|
|
306
|
+
const url = `${this.baseURL}/v1/jobs/${encodeURIComponent(jobId)}/events?after=${afterSeq}`;
|
|
307
|
+
yield* streamJobSSE({
|
|
308
|
+
url,
|
|
309
|
+
headers: this.headers,
|
|
310
|
+
signal,
|
|
311
|
+
fetchFn: this.fetchFn
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async cancelJob(jobId) {
|
|
315
|
+
await this.post(`/v1/jobs/${encodeURIComponent(jobId)}/cancel`, {});
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/storage.ts
|
|
320
|
+
var InMemoryStorage = class {
|
|
321
|
+
constructor() {
|
|
322
|
+
this.conversations = /* @__PURE__ */ new Map();
|
|
323
|
+
this.messages = /* @__PURE__ */ new Map();
|
|
324
|
+
}
|
|
325
|
+
async fetchConversations() {
|
|
326
|
+
return Array.from(this.conversations.values()).sort(
|
|
327
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
async fetchConversation(id) {
|
|
331
|
+
return this.conversations.get(id) ?? null;
|
|
332
|
+
}
|
|
333
|
+
async createConversation(id, title) {
|
|
334
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
335
|
+
const conversation = {
|
|
336
|
+
id,
|
|
337
|
+
title,
|
|
338
|
+
messageCount: 0,
|
|
339
|
+
createdAt: now,
|
|
340
|
+
updatedAt: now
|
|
341
|
+
};
|
|
342
|
+
this.conversations.set(id, conversation);
|
|
343
|
+
this.messages.set(id, []);
|
|
344
|
+
return conversation;
|
|
345
|
+
}
|
|
346
|
+
async updateConversationTitle(id, title) {
|
|
347
|
+
const conv = this.conversations.get(id);
|
|
348
|
+
if (conv) {
|
|
349
|
+
conv.title = title;
|
|
350
|
+
conv.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async deleteConversation(id) {
|
|
354
|
+
this.conversations.delete(id);
|
|
355
|
+
this.messages.delete(id);
|
|
356
|
+
}
|
|
357
|
+
async fetchMessages(conversationId) {
|
|
358
|
+
return this.messages.get(conversationId) ?? [];
|
|
359
|
+
}
|
|
360
|
+
async addMessage(message, conversationId) {
|
|
361
|
+
const msgs = this.messages.get(conversationId) ?? [];
|
|
362
|
+
msgs.push(message);
|
|
363
|
+
this.messages.set(conversationId, msgs);
|
|
364
|
+
const conv = this.conversations.get(conversationId);
|
|
365
|
+
if (conv) {
|
|
366
|
+
conv.messageCount = msgs.length;
|
|
367
|
+
conv.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async updateMessageStatus(id, status) {
|
|
371
|
+
for (const msgs of this.messages.values()) {
|
|
372
|
+
const msg = msgs.find((m) => m.id === id);
|
|
373
|
+
if (msg) {
|
|
374
|
+
msg.status = status;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async deleteMessage(id) {
|
|
380
|
+
for (const [convId, msgs] of this.messages.entries()) {
|
|
381
|
+
const idx = msgs.findIndex((m) => m.id === id);
|
|
382
|
+
if (idx !== -1) {
|
|
383
|
+
msgs.splice(idx, 1);
|
|
384
|
+
const conv = this.conversations.get(convId);
|
|
385
|
+
if (conv) {
|
|
386
|
+
conv.messageCount = msgs.length;
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/tools.ts
|
|
395
|
+
var TOOL_NAME_PATTERN = /^[a-zA-Z0-9_.\-]+$/;
|
|
396
|
+
function sanitizeArgs(args) {
|
|
397
|
+
const clean = /* @__PURE__ */ Object.create(null);
|
|
398
|
+
for (const key of Object.keys(args)) {
|
|
399
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
clean[key] = args[key];
|
|
403
|
+
}
|
|
404
|
+
return clean;
|
|
405
|
+
}
|
|
406
|
+
var ToolRegistry = class {
|
|
407
|
+
constructor() {
|
|
408
|
+
this.tools = /* @__PURE__ */ new Map();
|
|
409
|
+
}
|
|
410
|
+
registerTool(name, description, inputSchema, handler) {
|
|
411
|
+
if (!name || !TOOL_NAME_PATTERN.test(name)) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`Invalid tool name "${name}" - must match ${TOOL_NAME_PATTERN}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
if (name.length > 256) {
|
|
417
|
+
throw new Error("Tool name must be 256 characters or fewer");
|
|
418
|
+
}
|
|
419
|
+
this.tools.set(name, { name, description, inputSchema, handler });
|
|
420
|
+
}
|
|
421
|
+
unregisterTool(name) {
|
|
422
|
+
return this.tools.delete(name);
|
|
423
|
+
}
|
|
424
|
+
hasTool(name) {
|
|
425
|
+
return this.tools.has(name);
|
|
426
|
+
}
|
|
427
|
+
async executeTool(request) {
|
|
428
|
+
const tool = this.tools.get(request.toolName);
|
|
429
|
+
if (!tool) {
|
|
430
|
+
return {
|
|
431
|
+
call_id: request.callId,
|
|
432
|
+
tool_name: request.toolName,
|
|
433
|
+
result: `Tool "${request.toolName}" not found`,
|
|
434
|
+
is_error: true
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const result = await tool.handler(sanitizeArgs(request.arguments));
|
|
439
|
+
return {
|
|
440
|
+
call_id: request.callId,
|
|
441
|
+
tool_name: request.toolName,
|
|
442
|
+
result,
|
|
443
|
+
is_error: false
|
|
444
|
+
};
|
|
445
|
+
} catch (err) {
|
|
446
|
+
return {
|
|
447
|
+
call_id: request.callId,
|
|
448
|
+
tool_name: request.toolName,
|
|
449
|
+
result: err instanceof Error ? err.message : String(err),
|
|
450
|
+
is_error: true
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
getManifest() {
|
|
455
|
+
return Array.from(this.tools.values()).map((t) => ({
|
|
456
|
+
name: t.name,
|
|
457
|
+
description: t.description,
|
|
458
|
+
parameters: t.inputSchema
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
getToolNames() {
|
|
462
|
+
return Array.from(this.tools.keys());
|
|
463
|
+
}
|
|
464
|
+
clear() {
|
|
465
|
+
this.tools.clear();
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/utils.ts
|
|
470
|
+
function generateId() {
|
|
471
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
472
|
+
return crypto.randomUUID();
|
|
473
|
+
}
|
|
474
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
475
|
+
const r = Math.random() * 16 | 0;
|
|
476
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
477
|
+
return v.toString(16);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/session.ts
|
|
482
|
+
var ChatSession = class {
|
|
483
|
+
constructor(config, storage) {
|
|
484
|
+
// State
|
|
485
|
+
this.conversationId = null;
|
|
486
|
+
this.conversations = [];
|
|
487
|
+
this.messages = [];
|
|
488
|
+
this.streamingContent = "";
|
|
489
|
+
this.isStreaming = false;
|
|
490
|
+
this.executingTool = null;
|
|
491
|
+
this.projectStatus = null;
|
|
492
|
+
this.agents = [];
|
|
493
|
+
this.skills = [];
|
|
494
|
+
this.enabledClientTools = /* @__PURE__ */ new Set();
|
|
495
|
+
this.modelDisplayName = null;
|
|
496
|
+
// New state fields
|
|
497
|
+
this.thinkingContent = "";
|
|
498
|
+
this.isThinking = false;
|
|
499
|
+
this.activeSubagents = /* @__PURE__ */ new Map();
|
|
500
|
+
this.sources = [];
|
|
501
|
+
this.capsuleOutputs = [];
|
|
502
|
+
this.todos = [];
|
|
503
|
+
this.activeTools = /* @__PURE__ */ new Map();
|
|
504
|
+
this.handlers = /* @__PURE__ */ new Set();
|
|
505
|
+
this.abortController = null;
|
|
506
|
+
/** Last received sequence number for resumable reconnection */
|
|
507
|
+
this.lastSeq = -1;
|
|
508
|
+
/** Current job ID for cancellation */
|
|
509
|
+
this.currentJobId = null;
|
|
510
|
+
this.client = new AstralformClient(config);
|
|
511
|
+
this.toolRegistry = new ToolRegistry();
|
|
512
|
+
this.storage = storage ?? new InMemoryStorage();
|
|
513
|
+
}
|
|
514
|
+
on(handler) {
|
|
515
|
+
this.handlers.add(handler);
|
|
516
|
+
return () => {
|
|
517
|
+
this.handlers.delete(handler);
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
emit(event) {
|
|
521
|
+
for (const handler of this.handlers) {
|
|
522
|
+
try {
|
|
523
|
+
handler(event);
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async connect() {
|
|
529
|
+
const [status, conversations, agents, skills] = await Promise.allSettled([
|
|
530
|
+
this.client.getProjectStatus(),
|
|
531
|
+
this.client.getConversations(),
|
|
532
|
+
this.client.getAgents().catch(() => []),
|
|
533
|
+
this.client.getSkills().catch(() => [])
|
|
534
|
+
]);
|
|
535
|
+
if (status.status === "fulfilled") {
|
|
536
|
+
this.projectStatus = status.value;
|
|
537
|
+
}
|
|
538
|
+
if (conversations.status === "fulfilled") {
|
|
539
|
+
this.conversations = conversations.value;
|
|
540
|
+
}
|
|
541
|
+
if (agents.status === "fulfilled") {
|
|
542
|
+
this.agents = agents.value;
|
|
543
|
+
}
|
|
544
|
+
if (skills.status === "fulfilled") {
|
|
545
|
+
this.skills = skills.value;
|
|
546
|
+
}
|
|
547
|
+
this.emit({ type: "connected" });
|
|
548
|
+
}
|
|
549
|
+
async send(content, options) {
|
|
550
|
+
if (this.isStreaming) return;
|
|
551
|
+
const conversationId = options?.conversationId ?? this.conversationId ?? void 0;
|
|
552
|
+
const userMessage = {
|
|
553
|
+
id: generateId(),
|
|
554
|
+
conversationId: conversationId ?? "",
|
|
555
|
+
role: "user",
|
|
556
|
+
content,
|
|
557
|
+
status: "complete",
|
|
558
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
559
|
+
};
|
|
560
|
+
if (conversationId) {
|
|
561
|
+
await this.storage.addMessage(userMessage, conversationId);
|
|
562
|
+
}
|
|
563
|
+
this.messages.push(userMessage);
|
|
564
|
+
const request = {
|
|
565
|
+
message: content,
|
|
566
|
+
conversation_id: conversationId,
|
|
567
|
+
mcp_manifest: this.toolRegistry.getManifest(),
|
|
568
|
+
enabled_mcp: Array.from(
|
|
569
|
+
options?.enabledClientTools ?? this.enabledClientTools
|
|
570
|
+
),
|
|
571
|
+
upload_ids: options?.uploadIds,
|
|
572
|
+
agent_name: options?.agentName
|
|
573
|
+
};
|
|
574
|
+
await this.processStream(request);
|
|
575
|
+
}
|
|
576
|
+
async resendFromCheckpoint(messageId, newContent) {
|
|
577
|
+
if (this.isStreaming) return;
|
|
578
|
+
const request = {
|
|
579
|
+
message: newContent,
|
|
580
|
+
conversation_id: this.conversationId ?? void 0,
|
|
581
|
+
resend_from: messageId,
|
|
582
|
+
mcp_manifest: this.toolRegistry.getManifest(),
|
|
583
|
+
enabled_mcp: Array.from(this.enabledClientTools)
|
|
584
|
+
};
|
|
585
|
+
await this.processStream(request);
|
|
586
|
+
}
|
|
587
|
+
async processStream(request) {
|
|
588
|
+
this.isStreaming = true;
|
|
589
|
+
this.streamingContent = "";
|
|
590
|
+
this.thinkingContent = "";
|
|
591
|
+
this.isThinking = false;
|
|
592
|
+
this.activeSubagents.clear();
|
|
593
|
+
this.sources = [];
|
|
594
|
+
this.capsuleOutputs = [];
|
|
595
|
+
this.todos = [];
|
|
596
|
+
this.activeTools.clear();
|
|
597
|
+
this.abortController = new AbortController();
|
|
598
|
+
try {
|
|
599
|
+
await this.consumeJobStream(request);
|
|
600
|
+
} catch (err) {
|
|
601
|
+
this.emit({
|
|
602
|
+
type: "error",
|
|
603
|
+
error: err instanceof Error ? err : new ConnectionError(String(err))
|
|
604
|
+
});
|
|
605
|
+
} finally {
|
|
606
|
+
this.isStreaming = false;
|
|
607
|
+
this.executingTool = null;
|
|
608
|
+
this.abortController = null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async consumeJobStream(request) {
|
|
612
|
+
const job = await this.client.createJob(request);
|
|
613
|
+
this.currentJobId = job.job_id;
|
|
614
|
+
let conversationId = job.conversation_id;
|
|
615
|
+
if (!this.conversationId) {
|
|
616
|
+
this.conversationId = conversationId;
|
|
617
|
+
}
|
|
618
|
+
const messageId = job.message_id;
|
|
619
|
+
this.lastSeq = -1;
|
|
620
|
+
let stopTitle;
|
|
621
|
+
const stream = this.client.streamJobEvents(
|
|
622
|
+
job.job_id,
|
|
623
|
+
this.lastSeq,
|
|
624
|
+
this.abortController?.signal
|
|
625
|
+
);
|
|
626
|
+
for await (const raw of stream) {
|
|
627
|
+
let parsed;
|
|
628
|
+
try {
|
|
629
|
+
const data = JSON.parse(raw.data);
|
|
630
|
+
if (typeof data !== "object" || data === null || typeof data.type !== "string") {
|
|
631
|
+
if (typeof data?.seq === "number") {
|
|
632
|
+
this.lastSeq = data.seq;
|
|
633
|
+
}
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
parsed = data;
|
|
637
|
+
if (typeof data.seq === "number") {
|
|
638
|
+
this.lastSeq = data.seq;
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
switch (parsed.type) {
|
|
644
|
+
case "message_start":
|
|
645
|
+
conversationId = parsed.conversation_id;
|
|
646
|
+
if (!this.conversationId) {
|
|
647
|
+
this.conversationId = conversationId;
|
|
648
|
+
}
|
|
649
|
+
if (parsed.model_display_name) {
|
|
650
|
+
this.modelDisplayName = parsed.model_display_name;
|
|
651
|
+
this.emit({
|
|
652
|
+
type: "model_info",
|
|
653
|
+
name: parsed.model_display_name
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (parsed.agent_name) {
|
|
657
|
+
this.emit({
|
|
658
|
+
type: "agent_start",
|
|
659
|
+
agentName: parsed.agent_name,
|
|
660
|
+
agentDisplayName: parsed.agent_display_name
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
case "content_block_delta":
|
|
665
|
+
this.streamingContent += parsed.delta.text;
|
|
666
|
+
this.emit({ type: "chunk", text: parsed.delta.text });
|
|
667
|
+
break;
|
|
668
|
+
case "tool_use_start": {
|
|
669
|
+
const toolCall = {
|
|
670
|
+
callId: parsed.call_id,
|
|
671
|
+
toolName: parsed.tool,
|
|
672
|
+
displayName: parsed.display_name,
|
|
673
|
+
description: parsed.description,
|
|
674
|
+
arguments: parsed.arguments,
|
|
675
|
+
isClientTool: parsed.is_client_tool
|
|
676
|
+
};
|
|
677
|
+
this.activeTools.set(parsed.call_id, {
|
|
678
|
+
toolName: parsed.tool,
|
|
679
|
+
displayName: parsed.display_name,
|
|
680
|
+
description: parsed.description,
|
|
681
|
+
arguments: parsed.arguments,
|
|
682
|
+
callId: parsed.call_id,
|
|
683
|
+
status: parsed.is_client_tool ? "calling" : "executing",
|
|
684
|
+
isClientTool: parsed.is_client_tool
|
|
685
|
+
});
|
|
686
|
+
this.emit({ type: "tool_call", request: toolCall });
|
|
687
|
+
if (parsed.is_client_tool) {
|
|
688
|
+
const results = await this.executeClientTools([toolCall]);
|
|
689
|
+
await this.client.submitToolResult({
|
|
690
|
+
conversation_id: conversationId,
|
|
691
|
+
message_id: messageId,
|
|
692
|
+
tool_results: results
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case "tool_use_end": {
|
|
698
|
+
const toolState = this.activeTools.get(parsed.call_id);
|
|
699
|
+
if (toolState) {
|
|
700
|
+
toolState.status = "completed";
|
|
701
|
+
}
|
|
702
|
+
this.emit({
|
|
703
|
+
type: "tool_end",
|
|
704
|
+
callId: parsed.call_id,
|
|
705
|
+
toolName: parsed.tool
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
case "agent_start":
|
|
710
|
+
this.emit({
|
|
711
|
+
type: "agent_start",
|
|
712
|
+
agentName: parsed.agent_name,
|
|
713
|
+
agentDisplayName: parsed.agent_display_name,
|
|
714
|
+
avatarUrl: parsed.avatar_url
|
|
715
|
+
});
|
|
716
|
+
break;
|
|
717
|
+
case "agent_end":
|
|
718
|
+
this.emit({ type: "agent_end", agentName: parsed.agent_name });
|
|
719
|
+
break;
|
|
720
|
+
case "subagent_start":
|
|
721
|
+
this.activeSubagents.set(parsed.tool_call_id, {
|
|
722
|
+
agentName: parsed.agent_name,
|
|
723
|
+
displayName: parsed.display_name,
|
|
724
|
+
avatarUrl: parsed.avatar_url,
|
|
725
|
+
description: parsed.description,
|
|
726
|
+
content: "",
|
|
727
|
+
isActive: true
|
|
728
|
+
});
|
|
729
|
+
this.emit({
|
|
730
|
+
type: "subagent_start",
|
|
731
|
+
agentName: parsed.agent_name,
|
|
732
|
+
displayName: parsed.display_name,
|
|
733
|
+
toolCallId: parsed.tool_call_id,
|
|
734
|
+
avatarUrl: parsed.avatar_url,
|
|
735
|
+
description: parsed.description
|
|
736
|
+
});
|
|
737
|
+
break;
|
|
738
|
+
case "subagent_update": {
|
|
739
|
+
const sub = this.activeSubagents.get(parsed.tool_call_id);
|
|
740
|
+
if (sub) {
|
|
741
|
+
sub.agentName = parsed.agent_name;
|
|
742
|
+
sub.displayName = parsed.display_name;
|
|
743
|
+
}
|
|
744
|
+
this.emit({
|
|
745
|
+
type: "subagent_update",
|
|
746
|
+
agentName: parsed.agent_name,
|
|
747
|
+
displayName: parsed.display_name,
|
|
748
|
+
toolCallId: parsed.tool_call_id
|
|
749
|
+
});
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case "subagent_content_delta": {
|
|
753
|
+
const subagent = this.activeSubagents.get(parsed.tool_call_id);
|
|
754
|
+
if (subagent) {
|
|
755
|
+
subagent.content += parsed.delta.text;
|
|
756
|
+
}
|
|
757
|
+
this.emit({
|
|
758
|
+
type: "subagent_chunk",
|
|
759
|
+
agentName: parsed.agent_name,
|
|
760
|
+
toolCallId: parsed.tool_call_id,
|
|
761
|
+
text: parsed.delta.text
|
|
762
|
+
});
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
case "subagent_end": {
|
|
766
|
+
const sub = this.activeSubagents.get(parsed.tool_call_id);
|
|
767
|
+
if (sub) {
|
|
768
|
+
sub.isActive = false;
|
|
769
|
+
}
|
|
770
|
+
this.emit({
|
|
771
|
+
type: "subagent_end",
|
|
772
|
+
agentName: parsed.agent_name,
|
|
773
|
+
displayName: parsed.display_name,
|
|
774
|
+
toolCallId: parsed.tool_call_id
|
|
775
|
+
});
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
case "thinking_delta":
|
|
779
|
+
this.thinkingContent += parsed.delta.text;
|
|
780
|
+
this.isThinking = true;
|
|
781
|
+
this.emit({ type: "thinking_delta", text: parsed.delta.text });
|
|
782
|
+
break;
|
|
783
|
+
case "thinking_complete":
|
|
784
|
+
this.isThinking = false;
|
|
785
|
+
this.emit({ type: "thinking_complete" });
|
|
786
|
+
break;
|
|
787
|
+
case "sources":
|
|
788
|
+
this.sources.push(...parsed.sources);
|
|
789
|
+
this.emit({ type: "sources", sources: parsed.sources });
|
|
790
|
+
break;
|
|
791
|
+
case "capsule_output": {
|
|
792
|
+
const capsule = {
|
|
793
|
+
toolName: parsed.tool_name,
|
|
794
|
+
agentName: parsed.agent_name,
|
|
795
|
+
command: parsed.command,
|
|
796
|
+
output: parsed.output,
|
|
797
|
+
durationMs: parsed.duration_ms
|
|
798
|
+
};
|
|
799
|
+
this.capsuleOutputs.push(capsule);
|
|
800
|
+
this.emit({ type: "capsule_output", ...capsule });
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
case "todo_update":
|
|
804
|
+
this.todos = parsed.todos;
|
|
805
|
+
this.emit({ type: "todo_update", todos: parsed.todos });
|
|
806
|
+
break;
|
|
807
|
+
case "subagent_tool_use":
|
|
808
|
+
this.emit({
|
|
809
|
+
type: "subagent_tool_use",
|
|
810
|
+
agentName: parsed.agent_name,
|
|
811
|
+
toolName: parsed.tool,
|
|
812
|
+
toolCallId: parsed.tool_call_id,
|
|
813
|
+
result: parsed.result
|
|
814
|
+
});
|
|
815
|
+
break;
|
|
816
|
+
case "asset_created":
|
|
817
|
+
this.emit({
|
|
818
|
+
type: "asset_created",
|
|
819
|
+
assetId: parsed.asset_id,
|
|
820
|
+
name: parsed.name,
|
|
821
|
+
url: parsed.url,
|
|
822
|
+
mediaType: parsed.media_type,
|
|
823
|
+
sizeBytes: parsed.size_bytes
|
|
824
|
+
});
|
|
825
|
+
break;
|
|
826
|
+
case "retry":
|
|
827
|
+
this.emit({
|
|
828
|
+
type: "retry",
|
|
829
|
+
attempt: parsed.attempt,
|
|
830
|
+
maxAttempts: parsed.max_attempts,
|
|
831
|
+
delaySeconds: parsed.delay_seconds
|
|
832
|
+
});
|
|
833
|
+
break;
|
|
834
|
+
case "message_stop":
|
|
835
|
+
stopTitle = parsed.title;
|
|
836
|
+
break;
|
|
837
|
+
case "error":
|
|
838
|
+
this.emit({
|
|
839
|
+
type: "error",
|
|
840
|
+
error: new AstralformError(parsed.message, parsed.code)
|
|
841
|
+
});
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
this.currentJobId = null;
|
|
846
|
+
await this.completeStream(conversationId, messageId, stopTitle);
|
|
847
|
+
}
|
|
848
|
+
async executeClientTools(toolCalls) {
|
|
849
|
+
const results = [];
|
|
850
|
+
for (const call of toolCalls) {
|
|
851
|
+
this.executingTool = call.toolName;
|
|
852
|
+
const toolState = this.activeTools.get(call.callId);
|
|
853
|
+
if (toolState) {
|
|
854
|
+
toolState.status = "executing";
|
|
855
|
+
}
|
|
856
|
+
this.emit({ type: "tool_executing", name: call.toolName });
|
|
857
|
+
const result = await this.toolRegistry.executeTool(call);
|
|
858
|
+
results.push(result);
|
|
859
|
+
if (toolState) {
|
|
860
|
+
toolState.status = "completed";
|
|
861
|
+
}
|
|
862
|
+
this.emit({
|
|
863
|
+
type: "tool_completed",
|
|
864
|
+
name: call.toolName,
|
|
865
|
+
result: result.result
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
this.executingTool = null;
|
|
869
|
+
return results;
|
|
870
|
+
}
|
|
871
|
+
async completeStream(conversationId, messageId, title) {
|
|
872
|
+
const assistantMessage = {
|
|
873
|
+
id: messageId || generateId(),
|
|
874
|
+
conversationId,
|
|
875
|
+
role: "assistant",
|
|
876
|
+
content: this.streamingContent,
|
|
877
|
+
status: "complete",
|
|
878
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
879
|
+
};
|
|
880
|
+
this.messages.push(assistantMessage);
|
|
881
|
+
await this.storage.addMessage(assistantMessage, conversationId);
|
|
882
|
+
if (title && conversationId) {
|
|
883
|
+
await this.storage.updateConversationTitle(conversationId, title);
|
|
884
|
+
const conv = this.conversations.find((c) => c.id === conversationId);
|
|
885
|
+
if (conv) {
|
|
886
|
+
conv.title = title;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
this.emit({
|
|
890
|
+
type: "complete",
|
|
891
|
+
content: this.streamingContent,
|
|
892
|
+
conversationId,
|
|
893
|
+
messageId: assistantMessage.id,
|
|
894
|
+
title
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
disconnect() {
|
|
898
|
+
if (this.currentJobId) {
|
|
899
|
+
this.client.cancelJob(this.currentJobId).catch(() => {
|
|
900
|
+
});
|
|
901
|
+
this.currentJobId = null;
|
|
902
|
+
}
|
|
903
|
+
this.abortController?.abort();
|
|
904
|
+
this.abortController = null;
|
|
905
|
+
this.isStreaming = false;
|
|
906
|
+
this.streamingContent = "";
|
|
907
|
+
this.executingTool = null;
|
|
908
|
+
this.emit({ type: "disconnected" });
|
|
909
|
+
}
|
|
910
|
+
async createNewConversation() {
|
|
911
|
+
const id = generateId();
|
|
912
|
+
const conversation = await this.storage.createConversation(
|
|
913
|
+
id,
|
|
914
|
+
"New Conversation"
|
|
915
|
+
);
|
|
916
|
+
this.conversations.unshift(conversation);
|
|
917
|
+
this.conversationId = id;
|
|
918
|
+
this.messages = [];
|
|
919
|
+
this.streamingContent = "";
|
|
920
|
+
return id;
|
|
921
|
+
}
|
|
922
|
+
async switchConversation(id) {
|
|
923
|
+
this.conversationId = id;
|
|
924
|
+
try {
|
|
925
|
+
this.messages = await this.client.getMessages(id);
|
|
926
|
+
} catch {
|
|
927
|
+
this.messages = await this.storage.fetchMessages(id);
|
|
928
|
+
}
|
|
929
|
+
this.streamingContent = "";
|
|
930
|
+
}
|
|
931
|
+
async deleteConversation(id) {
|
|
932
|
+
try {
|
|
933
|
+
await this.client.deleteConversation(id);
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
await this.storage.deleteConversation(id);
|
|
937
|
+
this.conversations = this.conversations.filter((c) => c.id !== id);
|
|
938
|
+
if (this.conversationId === id) {
|
|
939
|
+
this.conversationId = null;
|
|
940
|
+
this.messages = [];
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
toggleClientTool(name) {
|
|
944
|
+
if (this.enabledClientTools.has(name)) {
|
|
945
|
+
this.enabledClientTools.delete(name);
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
this.enabledClientTools.add(name);
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
export {
|
|
953
|
+
AstralformClient,
|
|
954
|
+
AstralformError,
|
|
955
|
+
AuthenticationError,
|
|
956
|
+
ChatSession,
|
|
957
|
+
ConnectionError,
|
|
958
|
+
InMemoryStorage,
|
|
959
|
+
LLMNotConfiguredError,
|
|
960
|
+
RateLimitError,
|
|
961
|
+
ServerError,
|
|
962
|
+
StreamAbortedError,
|
|
963
|
+
ToolRegistry,
|
|
964
|
+
generateId,
|
|
965
|
+
streamJobSSE
|
|
966
|
+
};
|
|
967
|
+
//# sourceMappingURL=index.js.map
|