@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/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