@astralform/js 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -321,6 +321,87 @@ var AstralformClient = class {
321
321
  }
322
322
  };
323
323
 
324
+ // src/block-builder.ts
325
+ var _idCounter = 0;
326
+ var BlockBuilder = class {
327
+ constructor() {
328
+ this._blocks = [];
329
+ this._handlers = /* @__PURE__ */ new Map();
330
+ this._onChange = null;
331
+ // Active block refs — public so handlers can read/write them
332
+ this.activeTextId = null;
333
+ this.activeThinkingId = null;
334
+ this.thinkingStartMs = null;
335
+ }
336
+ // ── Registration ──────────────────────────────────────────────
337
+ on(eventType, handler) {
338
+ this._handlers.set(eventType, handler);
339
+ }
340
+ registerHandlers(handlers) {
341
+ for (const [type, handler] of Object.entries(handlers)) {
342
+ this._handlers.set(type, handler);
343
+ }
344
+ }
345
+ // ── Event processing ──────────────────────────────────────────
346
+ processEvent(event) {
347
+ const handler = this._handlers.get(event.type);
348
+ if (handler === null) return;
349
+ if (!handler) return;
350
+ handler(event, this);
351
+ }
352
+ // ── State ─────────────────────────────────────────────────────
353
+ getBlocks() {
354
+ return [...this._blocks];
355
+ }
356
+ reset() {
357
+ this._blocks = [];
358
+ this.activeTextId = null;
359
+ this.activeThinkingId = null;
360
+ this.thinkingStartMs = null;
361
+ }
362
+ setOnChange(fn) {
363
+ this._onChange = fn;
364
+ }
365
+ // ── Block manipulation (used by handlers) ─────────────────────
366
+ addBlock(block) {
367
+ this._blocks = [...this._blocks, block];
368
+ this._notify();
369
+ }
370
+ updateBlock(id, updater) {
371
+ let changed = false;
372
+ this._blocks = this._blocks.map((b) => {
373
+ if (b.id !== id) return b;
374
+ changed = true;
375
+ return updater(b);
376
+ });
377
+ if (changed) this._notify();
378
+ }
379
+ /** Update any block by id with a partial update (type-loose for handlers). */
380
+ patchBlock(id, patch) {
381
+ let changed = false;
382
+ this._blocks = this._blocks.map((b) => {
383
+ if (b.id !== id) return b;
384
+ changed = true;
385
+ return { ...b, ...patch };
386
+ });
387
+ if (changed) this._notify();
388
+ }
389
+ findBlock(predicate) {
390
+ for (let i = this._blocks.length - 1; i >= 0; i--) {
391
+ const block = this._blocks[i];
392
+ if (block && predicate(block)) return block;
393
+ }
394
+ return void 0;
395
+ }
396
+ nextId() {
397
+ return `blk_${++_idCounter}`;
398
+ }
399
+ // ── Internal ──────────────────────────────────────────────────
400
+ _notify() {
401
+ this._onChange?.();
402
+ }
403
+ };
404
+
324
405
  // src/storage.ts
325
406
  var InMemoryStorage = class {
326
407
  constructor() {
@@ -485,7 +566,7 @@ function generateId() {
485
566
 
486
567
  // src/session.ts
487
568
  var ChatSession = class {
488
- constructor(config, storage) {
569
+ constructor(config, storage, blockBuilder) {
489
570
  // State
490
571
  this.conversationId = null;
491
572
  this.conversations = [];
@@ -515,6 +596,13 @@ var ChatSession = class {
515
596
  this.client = new AstralformClient(config);
516
597
  this.toolRegistry = new ToolRegistry();
517
598
  this.storage = storage ?? new InMemoryStorage();
599
+ this.blockBuilder = blockBuilder ?? new BlockBuilder();
600
+ this.blockBuilder.setOnChange(() => {
601
+ this.emit({
602
+ type: "blocks_changed",
603
+ blocks: this.blockBuilder.getBlocks()
604
+ });
605
+ });
518
606
  }
519
607
  on(handler) {
520
608
  this.handlers.add(handler);
@@ -523,6 +611,9 @@ var ChatSession = class {
523
611
  };
524
612
  }
525
613
  emit(event) {
614
+ if (event.type !== "blocks_changed" && event.type !== "connected") {
615
+ this.blockBuilder.processEvent(event);
616
+ }
526
617
  for (const handler of this.handlers) {
527
618
  try {
528
619
  handler(event);
@@ -579,14 +670,15 @@ var ChatSession = class {
579
670
  };
580
671
  await this.processStream(request);
581
672
  }
582
- async resendFromCheckpoint(messageId, newContent) {
673
+ async resendFromCheckpoint(messageId, newContent, options) {
583
674
  if (this.isStreaming) return;
584
675
  const request = {
585
676
  message: newContent,
586
677
  conversation_id: this.conversationId ?? void 0,
587
678
  resend_from: messageId,
588
679
  mcp_manifest: this.toolRegistry.getManifest(),
589
- enabled_mcp: Array.from(this.enabledClientTools)
680
+ enabled_mcp: Array.from(this.enabledClientTools),
681
+ enable_search: options?.enableSearch
590
682
  };
591
683
  await this.processStream(request);
592
684
  }
@@ -626,12 +718,23 @@ var ChatSession = class {
626
718
  }
627
719
  const messageId = job.message_id;
628
720
  this.lastSeq = -1;
629
- let stopTitle;
630
721
  const stream = this.client.streamJobEvents(
631
722
  job.job_id,
632
723
  this.lastSeq,
633
724
  this.abortController?.signal
634
725
  );
726
+ await this.consumeEventStream(
727
+ stream,
728
+ conversationId,
729
+ messageId,
730
+ true
731
+ // executeClientTools
732
+ );
733
+ }
734
+ /**
735
+ * Shared event consumption loop used by both consumeJobStream and reconnectToJob.
736
+ */
737
+ async consumeEventStream(stream, conversationId, messageId, executeClientTools) {
635
738
  for await (const raw of stream) {
636
739
  let parsed;
637
740
  try {
@@ -655,6 +758,9 @@ var ChatSession = class {
655
758
  if (!this.conversationId) {
656
759
  this.conversationId = conversationId;
657
760
  }
761
+ if (parsed.message_id) {
762
+ messageId = parsed.message_id;
763
+ }
658
764
  if (parsed.model_display_name) {
659
765
  this.modelDisplayName = parsed.model_display_name;
660
766
  this.emit({
@@ -669,7 +775,7 @@ var ChatSession = class {
669
775
  break;
670
776
  case "tool_use_start": {
671
777
  this.applyEvent(parsed);
672
- if (parsed.is_client_tool) {
778
+ if (executeClientTools && parsed.is_client_tool) {
673
779
  const results = await this.executeClientTools([
674
780
  {
675
781
  callId: parsed.call_id,
@@ -721,7 +827,15 @@ var ChatSession = class {
721
827
  });
722
828
  break;
723
829
  case "message_stop":
724
- stopTitle = parsed.title;
830
+ await this.completeStream(
831
+ conversationId,
832
+ messageId,
833
+ parsed.title,
834
+ parsed.metrics,
835
+ parsed.job_id
836
+ );
837
+ this.isStreaming = false;
838
+ this.currentJobId = null;
725
839
  break;
726
840
  case "error":
727
841
  this.emit({
@@ -733,8 +847,6 @@ var ChatSession = class {
733
847
  this.applyEvent(parsed);
734
848
  }
735
849
  }
736
- this.currentJobId = null;
737
- await this.completeStream(conversationId, messageId, stopTitle);
738
850
  }
739
851
  /**
740
852
  * Apply a single SSE event to session state and notify consumers.
@@ -742,6 +854,45 @@ var ChatSession = class {
742
854
  */
743
855
  applyEvent(event) {
744
856
  switch (event.type) {
857
+ case "user_message":
858
+ this.emit({ type: "user_message", content: event.content });
859
+ break;
860
+ case "title_generated":
861
+ this.emit({ type: "title_generated", title: event.title });
862
+ break;
863
+ case "message_start":
864
+ if (event.conversation_id && !this.conversationId) {
865
+ this.conversationId = event.conversation_id;
866
+ }
867
+ if (event.model_display_name) {
868
+ this.modelDisplayName = event.model_display_name;
869
+ this.emit({ type: "model_info", name: event.model_display_name });
870
+ }
871
+ break;
872
+ case "content_block_delta":
873
+ this.streamingContent += event.delta.text;
874
+ this.emit({ type: "chunk", text: event.delta.text });
875
+ break;
876
+ case "thinking_delta":
877
+ this.thinkingContent += event.delta.text;
878
+ this.isThinking = true;
879
+ this.emit({ type: "thinking_delta", text: event.delta.text });
880
+ break;
881
+ case "thinking_complete":
882
+ this.isThinking = false;
883
+ this.emit({ type: "thinking_complete" });
884
+ break;
885
+ case "message_stop":
886
+ this.emit({
887
+ type: "complete",
888
+ content: this.streamingContent,
889
+ conversationId: this.conversationId ?? "",
890
+ messageId: event.job_id ?? "",
891
+ title: event.title,
892
+ metrics: event.metrics,
893
+ job_id: event.job_id
894
+ });
895
+ break;
745
896
  case "tool_use_start": {
746
897
  const request = {
747
898
  callId: event.call_id,
@@ -769,7 +920,9 @@ var ChatSession = class {
769
920
  type: "tool_end",
770
921
  callId: event.call_id,
771
922
  toolName: event.tool,
772
- result: event.result
923
+ result: event.result,
924
+ sources: event.sources,
925
+ durationMs: event.duration_ms
773
926
  });
774
927
  break;
775
928
  }
@@ -877,18 +1030,25 @@ var ChatSession = class {
877
1030
  sizeBytes: event.size_bytes
878
1031
  });
879
1032
  break;
880
- case "activity":
1033
+ case "timeline_entry":
881
1034
  this.emit({
882
- type: "activity",
883
- activityId: event.activity_id,
1035
+ type: "timeline_entry",
1036
+ id: event.id,
884
1037
  status: event.status,
885
- category: event.category,
886
- title: event.title,
1038
+ kind: event.kind,
1039
+ agent_name: event.agent_name,
1040
+ tool_name: event.tool_name,
1041
+ display_name: event.display_name,
1042
+ tool_category: event.tool_category,
1043
+ viewer: event.viewer,
1044
+ call_id: event.call_id,
887
1045
  detail: event.detail,
1046
+ started_at: event.started_at,
1047
+ duration_ms: event.duration_ms,
1048
+ output_summary: event.output_summary,
888
1049
  sources: event.sources,
889
- toolName: event.tool_name,
890
- agentName: event.agent_name,
891
- durationMs: event.duration_ms
1050
+ parent_id: event.parent_id,
1051
+ structured_output: event.structured_output
892
1052
  });
893
1053
  break;
894
1054
  case "editor_content_start":
@@ -938,7 +1098,7 @@ var ChatSession = class {
938
1098
  this.executingTool = null;
939
1099
  return results;
940
1100
  }
941
- async completeStream(conversationId, messageId, title) {
1101
+ async completeStream(conversationId, messageId, title, metrics, jobId) {
942
1102
  const assistantMessage = {
943
1103
  id: messageId || generateId(),
944
1104
  conversationId,
@@ -961,9 +1121,57 @@ var ChatSession = class {
961
1121
  content: this.streamingContent,
962
1122
  conversationId,
963
1123
  messageId: assistantMessage.id,
964
- title
1124
+ title,
1125
+ metrics,
1126
+ job_id: jobId
965
1127
  });
966
1128
  }
1129
+ /**
1130
+ * Load conversation context (messages) without replaying events.
1131
+ * Used before reconnectToJob — SSE replay handles event replay.
1132
+ */
1133
+ async loadConversation(id) {
1134
+ this.conversationId = id;
1135
+ this.resetStreamingState();
1136
+ this.blockBuilder.reset();
1137
+ this.messages = await this.client.getMessages(id).catch(() => this.storage.fetchMessages(id));
1138
+ }
1139
+ /**
1140
+ * Reconnect to a running job's SSE stream (e.g. after page reload).
1141
+ * Replays all events from the beginning and continues live.
1142
+ * Does NOT reset BlockBuilder — caller controls reset.
1143
+ */
1144
+ async reconnectToJob(jobId) {
1145
+ if (this.isStreaming) return;
1146
+ this.isStreaming = true;
1147
+ this.currentJobId = jobId;
1148
+ this.lastSeq = -1;
1149
+ this.resetStreamingState();
1150
+ this.abortController = new AbortController();
1151
+ try {
1152
+ const stream = this.client.streamJobEvents(
1153
+ jobId,
1154
+ this.lastSeq,
1155
+ this.abortController?.signal
1156
+ );
1157
+ await this.consumeEventStream(
1158
+ stream,
1159
+ this.conversationId ?? "",
1160
+ "",
1161
+ false
1162
+ // don't execute client tools on reconnect
1163
+ );
1164
+ } catch (err) {
1165
+ this.emit({
1166
+ type: "error",
1167
+ error: err instanceof Error ? err : new ConnectionError(String(err))
1168
+ });
1169
+ } finally {
1170
+ this.isStreaming = false;
1171
+ this.executingTool = null;
1172
+ this.abortController = null;
1173
+ }
1174
+ }
967
1175
  disconnect() {
968
1176
  if (this.currentJobId) {
969
1177
  this.client.cancelJob(this.currentJobId).catch(() => {
@@ -992,6 +1200,7 @@ var ChatSession = class {
992
1200
  async switchConversation(id) {
993
1201
  this.conversationId = id;
994
1202
  this.resetStreamingState();
1203
+ this.blockBuilder.reset();
995
1204
  const [messagesResult, eventsResult] = await Promise.allSettled([
996
1205
  this.client.getMessages(id).catch(() => this.storage.fetchMessages(id)),
997
1206
  this.client.getConversationEvents(id)
@@ -999,20 +1208,10 @@ var ChatSession = class {
999
1208
  this.messages = messagesResult.status === "fulfilled" ? messagesResult.value : [];
1000
1209
  if (eventsResult.status === "fulfilled") {
1001
1210
  for (const ev of eventsResult.value) {
1002
- this.replayEvent(ev.event, ev.data);
1211
+ this.applyEvent({ type: ev.event, ...ev.data });
1003
1212
  }
1004
1213
  }
1005
1214
  }
1006
- /**
1007
- * Replay a single persisted event to reconstruct session state.
1008
- * Skips text deltas (final content is already in messages[]).
1009
- */
1010
- replayEvent(eventType, data) {
1011
- if (eventType === "content_block_delta" || eventType === "thinking_delta" || eventType === "subagent_content_delta" || eventType === "thinking_complete" || eventType === "editor_content_start" || eventType === "editor_content_delta" || eventType === "editor_content_end") {
1012
- return;
1013
- }
1014
- this.applyEvent({ type: eventType, ...data });
1015
- }
1016
1215
  async deleteConversation(id) {
1017
1216
  try {
1018
1217
  await this.client.deleteConversation(id);
@@ -1034,10 +1233,231 @@ var ChatSession = class {
1034
1233
  return true;
1035
1234
  }
1036
1235
  };
1236
+
1237
+ // src/standard-handlers.ts
1238
+ function finalizeText(builder) {
1239
+ if (builder.activeTextId) {
1240
+ builder.patchBlock(builder.activeTextId, {
1241
+ isStreaming: false
1242
+ });
1243
+ builder.activeTextId = null;
1244
+ }
1245
+ }
1246
+ function finalizeThinking(builder) {
1247
+ if (builder.activeThinkingId) {
1248
+ builder.patchBlock(builder.activeThinkingId, {
1249
+ isActive: false
1250
+ });
1251
+ builder.activeThinkingId = null;
1252
+ builder.thinkingStartMs = null;
1253
+ }
1254
+ }
1255
+ var handleUserMessage = (event, builder) => {
1256
+ if (builder.findBlock((b) => b.type === "user")) return;
1257
+ const e = event;
1258
+ builder.addBlock({ type: "user", id: builder.nextId(), content: e.content });
1259
+ };
1260
+ var handleChunk = (event, builder) => {
1261
+ const e = event;
1262
+ if (!builder.activeTextId) {
1263
+ const id = builder.nextId();
1264
+ builder.activeTextId = id;
1265
+ builder.addBlock({
1266
+ type: "text",
1267
+ id,
1268
+ content: e.text,
1269
+ isStreaming: true
1270
+ });
1271
+ } else {
1272
+ const id = builder.activeTextId;
1273
+ const existing = builder.findBlock((b) => b.id === id);
1274
+ if (existing && existing.type === "text") {
1275
+ builder.patchBlock(id, {
1276
+ content: existing.content + e.text
1277
+ });
1278
+ }
1279
+ }
1280
+ };
1281
+ var handleToolCall = (event, builder) => {
1282
+ const e = event;
1283
+ finalizeText(builder);
1284
+ builder.addBlock({
1285
+ type: "tool",
1286
+ id: builder.nextId(),
1287
+ callId: e.request.callId,
1288
+ toolName: e.request.toolName,
1289
+ displayName: e.request.displayName,
1290
+ description: e.request.description,
1291
+ arguments: e.request.arguments,
1292
+ toolCategory: e.request.toolCategory,
1293
+ iconUrl: e.request.iconUrl,
1294
+ status: "calling"
1295
+ });
1296
+ };
1297
+ var handleToolExecuting = (event, builder) => {
1298
+ const e = event;
1299
+ const block = builder.findBlock(
1300
+ (b) => b.type === "tool" && b.toolName === e.name && b.status === "calling"
1301
+ );
1302
+ if (block) {
1303
+ builder.patchBlock(block.id, { status: "executing" });
1304
+ }
1305
+ };
1306
+ var handleToolEnd = (event, builder) => {
1307
+ const e = event;
1308
+ const callId = e.type === "tool_end" ? e.callId : void 0;
1309
+ const name = e.type === "tool_end" ? e.toolName : e.name;
1310
+ const block = builder.findBlock(
1311
+ (b) => b.type === "tool" && (callId ? b.callId === callId : b.toolName === name) && b.status !== "completed"
1312
+ );
1313
+ if (block) {
1314
+ const toolEnd = e.type === "tool_end" ? e : null;
1315
+ builder.patchBlock(block.id, {
1316
+ status: "completed",
1317
+ ...toolEnd?.sources ? { sources: toolEnd.sources } : {},
1318
+ ...toolEnd?.durationMs != null ? { durationMs: toolEnd.durationMs } : {},
1319
+ ...toolEnd?.result ? { result: toolEnd.result } : {}
1320
+ });
1321
+ }
1322
+ };
1323
+ var handleAgentStart = (event, builder) => {
1324
+ const e = event;
1325
+ finalizeText(builder);
1326
+ builder.addBlock({
1327
+ type: "agent",
1328
+ id: builder.nextId(),
1329
+ agentName: e.agentName,
1330
+ displayName: e.agentDisplayName,
1331
+ avatarUrl: e.avatarUrl
1332
+ });
1333
+ };
1334
+ var handleThinkingDelta = (event, builder) => {
1335
+ const e = event;
1336
+ if (!builder.activeThinkingId) {
1337
+ const id = builder.nextId();
1338
+ builder.activeThinkingId = id;
1339
+ builder.thinkingStartMs = Date.now();
1340
+ builder.addBlock({
1341
+ type: "thinking",
1342
+ id,
1343
+ content: e.text,
1344
+ isActive: true
1345
+ });
1346
+ } else {
1347
+ const id = builder.activeThinkingId;
1348
+ const existing = builder.findBlock((b) => b.id === id);
1349
+ if (existing && existing.type === "thinking") {
1350
+ builder.patchBlock(id, {
1351
+ content: existing.content + e.text
1352
+ });
1353
+ }
1354
+ }
1355
+ };
1356
+ var handleThinkingComplete = (_event, builder) => {
1357
+ if (builder.activeThinkingId) {
1358
+ const durationMs = builder.thinkingStartMs ? Math.max(0, Date.now() - builder.thinkingStartMs) : void 0;
1359
+ builder.patchBlock(builder.activeThinkingId, {
1360
+ isActive: false,
1361
+ durationMs
1362
+ });
1363
+ builder.activeThinkingId = null;
1364
+ builder.thinkingStartMs = null;
1365
+ }
1366
+ };
1367
+ var handleSubagentStart = (event, builder) => {
1368
+ const e = event;
1369
+ finalizeText(builder);
1370
+ builder.addBlock({
1371
+ type: "subagent",
1372
+ id: builder.nextId(),
1373
+ agentName: e.agentName,
1374
+ displayName: e.displayName,
1375
+ toolCallId: e.toolCallId,
1376
+ avatarUrl: e.avatarUrl,
1377
+ description: e.description,
1378
+ content: "",
1379
+ isActive: true
1380
+ });
1381
+ };
1382
+ var handleSubagentChunk = (event, builder) => {
1383
+ const e = event;
1384
+ const block = builder.findBlock(
1385
+ (b) => b.type === "subagent" && b.toolCallId === e.toolCallId
1386
+ );
1387
+ if (block && block.type === "subagent") {
1388
+ builder.patchBlock(block.id, {
1389
+ content: block.content + e.text
1390
+ });
1391
+ }
1392
+ };
1393
+ var handleSubagentUpdate = (event, builder) => {
1394
+ const e = event;
1395
+ const block = builder.findBlock(
1396
+ (b) => b.type === "subagent" && b.toolCallId === e.toolCallId
1397
+ );
1398
+ if (block) {
1399
+ builder.patchBlock(block.id, {
1400
+ displayName: e.displayName
1401
+ });
1402
+ }
1403
+ };
1404
+ var handleSubagentEnd = (event, builder) => {
1405
+ const e = event;
1406
+ const block = builder.findBlock(
1407
+ (b) => b.type === "subagent" && b.toolCallId === e.toolCallId
1408
+ );
1409
+ if (block) {
1410
+ builder.patchBlock(block.id, {
1411
+ isActive: false
1412
+ });
1413
+ }
1414
+ };
1415
+ var handleComplete = (_event, builder) => {
1416
+ finalizeText(builder);
1417
+ finalizeThinking(builder);
1418
+ for (const b of builder.getBlocks()) {
1419
+ if (b.type === "tool" && b.status !== "completed") {
1420
+ builder.patchBlock(b.id, { status: "completed" });
1421
+ }
1422
+ }
1423
+ };
1424
+ var handleError = (event, builder) => {
1425
+ const e = event;
1426
+ finalizeText(builder);
1427
+ finalizeThinking(builder);
1428
+ builder.addBlock({
1429
+ type: "error",
1430
+ id: builder.nextId(),
1431
+ message: e.error.message
1432
+ });
1433
+ };
1434
+ var handleDisconnected = (_event, builder) => {
1435
+ finalizeText(builder);
1436
+ finalizeThinking(builder);
1437
+ };
1438
+ var standardHandlers = {
1439
+ user_message: handleUserMessage,
1440
+ chunk: handleChunk,
1441
+ tool_call: handleToolCall,
1442
+ tool_executing: handleToolExecuting,
1443
+ tool_completed: handleToolEnd,
1444
+ tool_end: handleToolEnd,
1445
+ agent_start: handleAgentStart,
1446
+ thinking_delta: handleThinkingDelta,
1447
+ thinking_complete: handleThinkingComplete,
1448
+ subagent_start: handleSubagentStart,
1449
+ subagent_chunk: handleSubagentChunk,
1450
+ subagent_update: handleSubagentUpdate,
1451
+ subagent_end: handleSubagentEnd,
1452
+ complete: handleComplete,
1453
+ error: handleError,
1454
+ disconnected: handleDisconnected
1455
+ };
1037
1456
  export {
1038
1457
  AstralformClient,
1039
1458
  AstralformError,
1040
1459
  AuthenticationError,
1460
+ BlockBuilder,
1041
1461
  ChatSession,
1042
1462
  ConnectionError,
1043
1463
  InMemoryStorage,
@@ -1047,6 +1467,7 @@ export {
1047
1467
  StreamAbortedError,
1048
1468
  ToolRegistry,
1049
1469
  generateId,
1470
+ standardHandlers,
1050
1471
  streamJobSSE
1051
1472
  };
1052
1473
  //# sourceMappingURL=index.js.map