@absolutejs/ai 0.0.45 → 0.0.47

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/ai/index.js CHANGED
@@ -2259,6 +2259,115 @@ var createMemoryStore = () => {
2259
2259
  return { get, getOrCreate, list, remove, set };
2260
2260
  };
2261
2261
 
2262
+ // src/ai/client/turnQueue.ts
2263
+ var DEFAULT_MAX_SIZE = 100;
2264
+
2265
+ class ConversationTurnQueueFullError extends Error {
2266
+ constructor(maxSize) {
2267
+ super(`Conversation turn queue is full (${maxSize} items)`);
2268
+ this.name = "ConversationTurnQueueFullError";
2269
+ }
2270
+ }
2271
+ var createConversationTurnQueue = (options) => {
2272
+ const maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
2273
+ const items = [];
2274
+ const listeners = new Set;
2275
+ let activeAbort = null;
2276
+ let draining = false;
2277
+ const snapshot2 = () => ({
2278
+ items: items.map((item) => ({ ...item })),
2279
+ running: items.some(({ status }) => status === "running")
2280
+ });
2281
+ const notify = () => {
2282
+ const next = snapshot2();
2283
+ listeners.forEach((listener) => listener(next));
2284
+ };
2285
+ const drain = async () => {
2286
+ if (draining)
2287
+ return;
2288
+ const next = items.find(({ status }) => status === "queued");
2289
+ if (!next || items.some(({ status }) => status === "failed"))
2290
+ return;
2291
+ draining = true;
2292
+ next.status = "running";
2293
+ activeAbort = new AbortController;
2294
+ notify();
2295
+ try {
2296
+ await options.execute(next.input, {
2297
+ id: next.id,
2298
+ signal: activeAbort.signal
2299
+ });
2300
+ const index = items.findIndex(({ id }) => id === next.id);
2301
+ if (index >= 0)
2302
+ items.splice(index, 1);
2303
+ } catch (error) {
2304
+ if (activeAbort.signal.aborted) {
2305
+ const index = items.findIndex(({ id }) => id === next.id);
2306
+ if (index >= 0)
2307
+ items.splice(index, 1);
2308
+ } else {
2309
+ next.error = error;
2310
+ next.status = "failed";
2311
+ options.onError?.(error, { ...next });
2312
+ }
2313
+ } finally {
2314
+ activeAbort = null;
2315
+ draining = false;
2316
+ notify();
2317
+ drain();
2318
+ }
2319
+ };
2320
+ const enqueue = (input, id = crypto.randomUUID()) => {
2321
+ if (items.length >= maxSize) {
2322
+ throw new ConversationTurnQueueFullError(maxSize);
2323
+ }
2324
+ items.push({ id, input, status: "queued" });
2325
+ notify();
2326
+ drain();
2327
+ return id;
2328
+ };
2329
+ const remove = (id) => {
2330
+ const index = items.findIndex((item) => item.id === id && item.status !== "running");
2331
+ if (index < 0)
2332
+ return false;
2333
+ items.splice(index, 1);
2334
+ notify();
2335
+ drain();
2336
+ return true;
2337
+ };
2338
+ const retry = (id) => {
2339
+ const item = items.find((candidate) => candidate.id === id && candidate.status === "failed");
2340
+ if (!item)
2341
+ return false;
2342
+ delete item.error;
2343
+ item.status = "queued";
2344
+ notify();
2345
+ drain();
2346
+ return true;
2347
+ };
2348
+ const cancel = (input = {}) => {
2349
+ activeAbort?.abort();
2350
+ if (input.clearPending) {
2351
+ for (let index = items.length - 1;index >= 0; index--) {
2352
+ if (items[index]?.status !== "running")
2353
+ items.splice(index, 1);
2354
+ }
2355
+ }
2356
+ notify();
2357
+ };
2358
+ return {
2359
+ cancel,
2360
+ enqueue,
2361
+ getSnapshot: snapshot2,
2362
+ remove,
2363
+ retry,
2364
+ subscribe: (listener) => {
2365
+ listeners.add(listener);
2366
+ return () => listeners.delete(listener);
2367
+ }
2368
+ };
2369
+ };
2370
+
2262
2371
  // types/typeGuards.ts
2263
2372
  var isValidAIClientMessage = (data) => {
2264
2373
  if (!data || typeof data !== "object") {
@@ -2295,6 +2404,12 @@ var isValidAIServerMessage = (data) => {
2295
2404
  return "data" in data && typeof data.data === "string" && "format" in data && typeof data.format === "string" && "isPartial" in data && typeof data.isPartial === "boolean" && "messageId" in data && "conversationId" in data;
2296
2405
  case "complete":
2297
2406
  return "messageId" in data && "conversationId" in data;
2407
+ case "turn_queued":
2408
+ return "conversationId" in data && typeof data.conversationId === "string" && "messageId" in data && typeof data.messageId === "string" && "position" in data && typeof data.position === "number";
2409
+ case "turn_started":
2410
+ return "conversationId" in data && typeof data.conversationId === "string" && "messageId" in data && typeof data.messageId === "string";
2411
+ case "branched":
2412
+ return "content" in data && typeof data.content === "string" && "fromMessageId" in data && typeof data.fromMessageId === "string" && "messageId" in data && typeof data.messageId === "string" && "newConversationId" in data && typeof data.newConversationId === "string" && "oldConversationId" in data && typeof data.oldConversationId === "string";
2298
2413
  case "rag_retrieved":
2299
2414
  return "conversationId" in data && "messageId" in data && "sources" in data && Array.isArray(data.sources);
2300
2415
  case "error":
@@ -3210,14 +3325,20 @@ var aiChat = (config2) => {
3210
3325
  const store = config2.store ?? createMemoryStore();
3211
3326
  const parseProvider = config2.parseProvider ?? defaultParseProvider;
3212
3327
  const abortControllers = new Map;
3328
+ const turnQueues = new Map;
3329
+ const sendServerEvent = (ws, event) => {
3330
+ if (ws.readyState === 1)
3331
+ ws.send(JSON.stringify(event));
3332
+ };
3213
3333
  const handleCancel = (conversationId) => {
3334
+ turnQueues.get(conversationId)?.cancel({ clearPending: true });
3214
3335
  const controller = abortControllers.get(conversationId);
3215
3336
  if (controller) {
3216
3337
  controller.abort();
3217
3338
  abortControllers.delete(conversationId);
3218
3339
  }
3219
3340
  };
3220
- const handleBranch = async (ws, messageId, conversationId) => {
3341
+ const handleBranch = async (ws, messageId, conversationId, content) => {
3221
3342
  const source = await store.get(conversationId);
3222
3343
  if (!source) {
3223
3344
  return;
@@ -3225,55 +3346,123 @@ var aiChat = (config2) => {
3225
3346
  const newConv = branchConversation(source, messageId);
3226
3347
  if (newConv) {
3227
3348
  await store.set(newConv.id, newConv);
3228
- ws.send(JSON.stringify({ conversationId: newConv.id, type: "branched" }));
3349
+ const clientMessageId = generateId();
3350
+ sendServerEvent(ws, {
3351
+ content,
3352
+ fromMessageId: messageId,
3353
+ messageId: clientMessageId,
3354
+ newConversationId: newConv.id,
3355
+ oldConversationId: conversationId,
3356
+ type: "branched"
3357
+ });
3358
+ enqueueUserMessage({
3359
+ clientMessageId,
3360
+ content,
3361
+ conversationId: newConv.id,
3362
+ ws
3363
+ });
3229
3364
  }
3230
3365
  };
3231
- const handleUserMessage = async (ws, rawContent, rawConversationId, attachments) => {
3232
- const conversationId = rawConversationId ?? generateId();
3233
- const messageId = generateId();
3366
+ const handleUserMessage = async (ws, rawContent, conversationId, clientMessageId, attachments, queueSignal) => {
3367
+ const assistantMessageId = generateId();
3234
3368
  const parsed = parseProvider(rawContent);
3235
3369
  const { content, providerName } = parsed;
3236
3370
  const conversation = await store.getOrCreate(conversationId);
3237
3371
  const history = getHistory(conversation);
3238
3372
  const controller = new AbortController;
3239
3373
  abortControllers.set(conversationId, controller);
3374
+ const abortFromQueue = () => controller.abort();
3375
+ queueSignal?.addEventListener("abort", abortFromQueue, { once: true });
3240
3376
  appendMessage(conversation, {
3241
3377
  attachments,
3242
3378
  content,
3243
3379
  conversationId,
3244
- id: messageId,
3380
+ id: clientMessageId,
3245
3381
  role: "user",
3246
3382
  timestamp: Date.now()
3247
3383
  });
3248
3384
  await store.set(conversationId, conversation);
3249
3385
  const model = resolveModel(config2, parsed);
3250
3386
  const userMessage = buildUserMessage(content, attachments);
3251
- await streamAI(ws, conversationId, messageId, {
3252
- maxTurns: config2.maxTurns,
3253
- messages: [...history, userMessage],
3254
- model,
3255
- provider: config2.provider(providerName),
3256
- signal: controller.signal,
3257
- systemPrompt: config2.systemPrompt,
3258
- reasoning: resolveReasoning(config2, providerName, model),
3259
- tools: resolveTools(config2, providerName, model),
3260
- onComplete: async (fullResponse, usage) => {
3261
- const conv = await store.get(conversationId);
3262
- if (conv) {
3263
- appendMessage(conv, {
3264
- content: fullResponse,
3265
- conversationId,
3266
- id: generateId(),
3267
- role: "assistant",
3268
- timestamp: Date.now()
3269
- });
3270
- await store.set(conversationId, conv);
3387
+ try {
3388
+ await streamAI(ws, conversationId, assistantMessageId, {
3389
+ maxTurns: config2.maxTurns,
3390
+ messages: [...history, userMessage],
3391
+ model,
3392
+ provider: config2.provider(providerName),
3393
+ signal: controller.signal,
3394
+ systemPrompt: config2.systemPrompt,
3395
+ reasoning: resolveReasoning(config2, providerName, model),
3396
+ tools: resolveTools(config2, providerName, model),
3397
+ onComplete: async (fullResponse, usage) => {
3398
+ const conv = await store.get(conversationId);
3399
+ if (conv) {
3400
+ appendMessage(conv, {
3401
+ content: fullResponse,
3402
+ conversationId,
3403
+ id: assistantMessageId,
3404
+ role: "assistant",
3405
+ timestamp: Date.now()
3406
+ });
3407
+ await store.set(conversationId, conv);
3408
+ }
3409
+ config2.onComplete?.(conversationId, fullResponse, usage);
3271
3410
  }
3411
+ });
3412
+ } finally {
3413
+ queueSignal?.removeEventListener("abort", abortFromQueue);
3414
+ if (abortControllers.get(conversationId) === controller) {
3272
3415
  abortControllers.delete(conversationId);
3273
- config2.onComplete?.(conversationId, fullResponse, usage);
3416
+ }
3417
+ }
3418
+ };
3419
+ const queueForConversation = (conversationId) => {
3420
+ const existing = turnQueues.get(conversationId);
3421
+ if (existing)
3422
+ return existing;
3423
+ const queue = createConversationTurnQueue({
3424
+ execute: async (turn, { signal }) => {
3425
+ sendServerEvent(turn.ws, {
3426
+ conversationId: turn.conversationId,
3427
+ messageId: turn.clientMessageId,
3428
+ type: "turn_started"
3429
+ });
3430
+ await handleUserMessage(turn.ws, turn.content, turn.conversationId, turn.clientMessageId, turn.attachments, signal);
3431
+ },
3432
+ onError: (error, turn) => {
3433
+ sendServerEvent(turn.input.ws, {
3434
+ message: error instanceof Error ? error.message : "AI turn failed",
3435
+ type: "error"
3436
+ });
3437
+ }
3438
+ });
3439
+ turnQueues.set(conversationId, queue);
3440
+ let unsubscribe = () => {
3441
+ return;
3442
+ };
3443
+ unsubscribe = queue.subscribe(({ items }) => {
3444
+ if (items.length === 0 && turnQueues.get(conversationId) === queue) {
3445
+ turnQueues.delete(conversationId);
3446
+ unsubscribe();
3274
3447
  }
3275
3448
  });
3449
+ return queue;
3276
3450
  };
3451
+ function enqueueUserMessage(turn) {
3452
+ const queue = queueForConversation(turn.conversationId);
3453
+ const wasBusy = queue.getSnapshot().items.length > 0;
3454
+ queue.enqueue(turn, turn.clientMessageId);
3455
+ if (wasBusy) {
3456
+ const queued = queue.getSnapshot().items.filter(({ status }) => status === "queued");
3457
+ const position = queued.findIndex(({ id }) => id === turn.clientMessageId) + 1;
3458
+ sendServerEvent(turn.ws, {
3459
+ conversationId: turn.conversationId,
3460
+ messageId: turn.clientMessageId,
3461
+ position,
3462
+ type: "turn_queued"
3463
+ });
3464
+ }
3465
+ }
3277
3466
  const htmxRoutes = () => {
3278
3467
  if (!config2.htmx) {
3279
3468
  return new Elysia;
@@ -3366,11 +3555,18 @@ var aiChat = (config2) => {
3366
3555
  return;
3367
3556
  }
3368
3557
  if (msg.type === "branch") {
3369
- await handleBranch(ws, msg.messageId, msg.conversationId);
3558
+ await handleBranch(ws, msg.messageId, msg.conversationId, msg.content);
3370
3559
  return;
3371
3560
  }
3372
3561
  if (msg.type === "message") {
3373
- await handleUserMessage(ws, msg.content, msg.conversationId, msg.attachments);
3562
+ const conversationId = msg.conversationId ?? generateId();
3563
+ enqueueUserMessage({
3564
+ attachments: msg.attachments,
3565
+ clientMessageId: msg.messageId ?? generateId(),
3566
+ content: msg.content,
3567
+ conversationId,
3568
+ ws
3569
+ });
3374
3570
  }
3375
3571
  }
3376
3572
  }).get(`${path}/conversations`, () => store.list()).get(`${path}/conversations/:id`, async ({ params }) => {
@@ -5202,6 +5398,28 @@ var serverMessageToAction = (message) => {
5202
5398
  type: "complete",
5203
5399
  usage: message.usage
5204
5400
  };
5401
+ case "turn_queued":
5402
+ return {
5403
+ conversationId: message.conversationId,
5404
+ messageId: message.messageId,
5405
+ position: message.position,
5406
+ type: "turn_queued"
5407
+ };
5408
+ case "turn_started":
5409
+ return {
5410
+ conversationId: message.conversationId,
5411
+ messageId: message.messageId,
5412
+ type: "turn_started"
5413
+ };
5414
+ case "branched":
5415
+ return {
5416
+ content: message.content,
5417
+ fromMessageId: message.fromMessageId,
5418
+ messageId: message.messageId,
5419
+ newConversationId: message.newConversationId,
5420
+ oldConversationId: message.oldConversationId,
5421
+ type: "branch"
5422
+ };
5205
5423
  case "rag_retrieving":
5206
5424
  return {
5207
5425
  conversationId: message.conversationId,
@@ -5403,6 +5621,7 @@ var handleSend = (state, action) => {
5403
5621
  content: action.content,
5404
5622
  conversationId: action.conversationId,
5405
5623
  id: action.messageId,
5624
+ isQueued: false,
5406
5625
  role: "user",
5407
5626
  timestamp: Date.now()
5408
5627
  };
@@ -5411,6 +5630,19 @@ var handleSend = (state, action) => {
5411
5630
  state.error = null;
5412
5631
  state.isStreaming = true;
5413
5632
  };
5633
+ var updateUserQueueState = (state, conversationId, messageId, isQueued) => {
5634
+ const conversation = state.conversations.get(conversationId);
5635
+ if (!conversation)
5636
+ return;
5637
+ conversation.messages = conversation.messages.map((message) => message.id === messageId && message.role === "user" ? { ...message, isQueued } : message);
5638
+ };
5639
+ var handleTurnQueued = (state, action) => {
5640
+ updateUserQueueState(state, action.conversationId, action.messageId, true);
5641
+ };
5642
+ var handleTurnStarted = (state, action) => {
5643
+ updateUserQueueState(state, action.conversationId, action.messageId, false);
5644
+ state.isStreaming = true;
5645
+ };
5414
5646
  var handleChunk = (state, action) => {
5415
5647
  const conversation = getOrCreate(state, action.conversationId);
5416
5648
  const existingIdx = conversation.messages.findIndex((msg) => msg.id === action.messageId && msg.role === "assistant");
@@ -5591,10 +5823,21 @@ var handleBranch = (state, action) => {
5591
5823
  const newConversation = {
5592
5824
  createdAt: Date.now(),
5593
5825
  id: action.newConversationId,
5594
- messages: branchedMessages
5826
+ messages: [
5827
+ ...branchedMessages,
5828
+ {
5829
+ content: action.content,
5830
+ conversationId: action.newConversationId,
5831
+ id: action.messageId,
5832
+ isQueued: false,
5833
+ role: "user",
5834
+ timestamp: Date.now()
5835
+ }
5836
+ ]
5595
5837
  };
5596
5838
  state.conversations.set(action.newConversationId, newConversation);
5597
5839
  state.activeConversationId = action.newConversationId;
5840
+ state.isStreaming = true;
5598
5841
  };
5599
5842
  var applyAction = (state, action) => {
5600
5843
  switch (action.type) {
@@ -5616,6 +5859,12 @@ var applyAction = (state, action) => {
5616
5859
  case "complete":
5617
5860
  handleComplete(state, action);
5618
5861
  break;
5862
+ case "turn_queued":
5863
+ handleTurnQueued(state, action);
5864
+ break;
5865
+ case "turn_started":
5866
+ handleTurnStarted(state, action);
5867
+ break;
5619
5868
  case "error":
5620
5869
  state.error = action.message;
5621
5870
  state.isStreaming = false;
@@ -5723,6 +5972,7 @@ var createAIStream = (path, conversationId) => {
5723
5972
  attachments,
5724
5973
  content,
5725
5974
  conversationId: convId,
5975
+ messageId: msgId,
5726
5976
  type: "message"
5727
5977
  });
5728
5978
  };
@@ -5999,5 +6249,5 @@ export {
5999
6249
  BUILTIN_UI_CARDS
6000
6250
  };
6001
6251
 
6002
- //# debugId=4DB8BD5EBB62E6E464756E2164756E21
6252
+ //# debugId=79EC11089E8CB24964756E2164756E21
6003
6253
  //# sourceMappingURL=index.js.map