@absolutejs/ai 0.0.43 → 0.0.44

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 CHANGED
@@ -12,3 +12,36 @@ chunks over SSE, while `createProviderProxyResponse()` hosts any
12
12
  callbacks and abort objects never cross the wire. The Anthropic provider also
13
13
  accepts an injectable `fetch`, allowing hosts to retain egress policy, tracing,
14
14
  and test transports.
15
+
16
+ ## SSE event stream (`streamAIToSSE`)
17
+
18
+ `streamAIToSSE` yields `{ event, data }` SSE frames. By default `data` is
19
+ pre-rendered HTML from the renderers, and the terminal `status` event is
20
+ overloaded across completion, budget stops, and errors — a headless consumer has
21
+ to sniff `ai-usage` vs `ai-error` out of the HTML to tell them apart.
22
+
23
+ Pass `structuredEvents: true` to get typed, machine-readable frames instead: each
24
+ `data` is JSON (parse it), and the overloaded terminal splits into three distinct
25
+ event names:
26
+
27
+ | event | when | `JSON.parse(data)` |
28
+ | ---------- | ----------------------- | ----------------------------------------------------------- |
29
+ | `content` | text delta | `{ delta, full }` |
30
+ | `thinking` | reasoning delta | `{ text }` (accumulated) |
31
+ | `tools` | one per tool transition | `{ name, status: "running" \| "complete", input, result? }` |
32
+ | `images` | generated image | `{ data, format, revisedPrompt? }` |
33
+ | `complete` | normal completion | `{ usage, durationMs, model }` |
34
+ | `stopped` | ceiling / limit / abort | `{ reason, detail }` |
35
+ | `error` | thrown / lookup error | `{ message }` |
36
+ | `ping` | heartbeat keepalive | `""` (unchanged) |
37
+
38
+ `stopped.reason` is one of `"max_total_tokens" | "max_duration_ms" | "max_tokens"
39
+ | "max_turns" | "aborted"`. Exactly one terminal (`complete` / `stopped` /
40
+ `error`) fires on every path — including an externally aborted loop, which now
41
+ emits `stopped` with `reason: "aborted"` rather than masquerading as a
42
+ completion. Payload types are exported (`AISSECompletePayload`,
43
+ `AISSEStoppedPayload`, `AISSEErrorPayload`, `AISSEContentPayload`, …).
44
+
45
+ The default (HTML) path is unchanged for the built-in HTMX/default UI, except an
46
+ abort now renders the (previously unused) `canceled` renderer instead of a
47
+ misleading usage chip.
package/dist/ai/index.js CHANGED
@@ -2779,6 +2779,26 @@ var executeTool2 = async (options, toolName, toolInput) => {
2779
2779
  }
2780
2780
  };
2781
2781
  var serializeToolCall2 = (name, input) => `${name}:${JSON.stringify(input)}`;
2782
+ var contentEvent = (options, renderers, delta, full) => options.structuredEvents ? { data: JSON.stringify({ delta, full }), event: "content" } : { data: renderers.chunk(delta, full), event: "content" };
2783
+ var thinkingEvent = (options, renderers, text) => options.structuredEvents ? { data: JSON.stringify({ text }), event: "thinking" } : { data: renderers.thinking(text), event: "thinking" };
2784
+ var imageEvent = (options, renderers, data, format, revisedPrompt) => options.structuredEvents ? { data: JSON.stringify({ data, format, revisedPrompt }), event: "images" } : { data: renderers.image(data, format, revisedPrompt), event: "images" };
2785
+ var completeEvent = (options, renderers, fullResponse, usage, durationMs) => {
2786
+ options.onComplete?.(fullResponse, usage);
2787
+ return options.structuredEvents ? {
2788
+ data: JSON.stringify({ durationMs, model: options.model, usage }),
2789
+ event: "complete"
2790
+ } : {
2791
+ data: renderers.complete(usage, durationMs, options.model),
2792
+ event: "status"
2793
+ };
2794
+ };
2795
+ var stoppedEvent = (options, renderers, reason, detail) => {
2796
+ if (options.structuredEvents) {
2797
+ return { data: JSON.stringify({ detail, reason }), event: "stopped" };
2798
+ }
2799
+ return reason === "aborted" ? { data: renderers.canceled(), event: "status" } : { data: renderers.error(detail), event: "status" };
2800
+ };
2801
+ var errorEvent = (options, renderers, message) => options.structuredEvents ? { data: JSON.stringify({ message }), event: "error" } : { data: renderers.error(message), event: "status" };
2782
2802
  var streamAIToSSE = async function* (conversationId, messageId, options, renderers) {
2783
2803
  const signal = options.signal ?? new AbortController().signal;
2784
2804
  const startTime = Date.now();
@@ -2787,12 +2807,11 @@ var streamAIToSSE = async function* (conversationId, messageId, options, rendere
2787
2807
  try {
2788
2808
  yield* withHeartbeat(streamTurns(options, renderers, messages, signal, startTime, maxTurns), signal, options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
2789
2809
  } catch (err) {
2790
- if (signal.aborted)
2810
+ if (signal.aborted) {
2811
+ yield stoppedEvent(options, renderers, "aborted", "Aborted by caller.");
2791
2812
  return;
2792
- yield {
2793
- data: renderers.error(err instanceof Error ? err.message : String(err)),
2794
- event: "status"
2795
- };
2813
+ }
2814
+ yield errorEvent(options, renderers, err instanceof Error ? err.message : String(err));
2796
2815
  }
2797
2816
  };
2798
2817
  var flushThinking2 = (thinking, contentBlocks) => {
@@ -2802,22 +2821,11 @@ var flushThinking2 = (thinking, contentBlocks) => {
2802
2821
  type: "thinking"
2803
2822
  });
2804
2823
  };
2805
- var yieldCompletion = (renderers, options, fullResponse, usage, startTime) => {
2806
- const durationMs = Date.now() - startTime;
2807
- options.onComplete?.(fullResponse, usage);
2808
- return {
2809
- data: renderers.complete(usage, durationMs, options.model),
2810
- event: "status"
2811
- };
2812
- };
2813
- var processThinkingChunk = function* (content, signature, chunkState, renderers) {
2824
+ var processThinkingChunk = function* (content, signature, chunkState, renderers, options) {
2814
2825
  chunkState.currentThinking ??= { signature: "", text: "" };
2815
2826
  chunkState.currentThinking.text += content;
2816
2827
  chunkState.currentThinking.signature = signature ?? chunkState.currentThinking.signature;
2817
- yield {
2818
- data: renderers.thinking(chunkState.currentThinking.text),
2819
- event: "thinking"
2820
- };
2828
+ yield thinkingEvent(options, renderers, chunkState.currentThinking.text);
2821
2829
  };
2822
2830
  var maybeFlushThinking = (chunkState) => {
2823
2831
  if (!chunkState.currentThinking)
@@ -2825,22 +2833,16 @@ var maybeFlushThinking = (chunkState) => {
2825
2833
  flushThinking2(chunkState.currentThinking, chunkState.contentBlocks);
2826
2834
  chunkState.currentThinking = null;
2827
2835
  };
2828
- var processTextChunk = function* (content, chunkState, renderers, fullResponse) {
2836
+ var processTextChunk = function* (content, chunkState, renderers, fullResponse, options) {
2829
2837
  maybeFlushThinking(chunkState);
2830
2838
  chunkState.contentBlocks.push({
2831
2839
  content,
2832
2840
  type: "text"
2833
2841
  });
2834
- yield {
2835
- data: renderers.chunk(content, fullResponse + content),
2836
- event: "content"
2837
- };
2842
+ yield contentEvent(options, renderers, content, fullResponse + content);
2838
2843
  };
2839
2844
  var processImageChunk = function* (chunk, renderers, options) {
2840
- yield {
2841
- data: renderers.image(chunk.data, chunk.format, chunk.revisedPrompt),
2842
- event: "images"
2843
- };
2845
+ yield imageEvent(options, renderers, chunk.data, chunk.format, chunk.revisedPrompt);
2844
2846
  options.onImage?.({
2845
2847
  data: chunk.data,
2846
2848
  format: chunk.format,
@@ -2866,10 +2868,10 @@ var processToolUseChunk = (chunk, chunkState) => {
2866
2868
  var processChunk2 = function* (chunk, chunkState, renderers, options, fullResponse) {
2867
2869
  switch (chunk.type) {
2868
2870
  case "thinking":
2869
- yield* processThinkingChunk(chunk.content, chunk.signature, chunkState, renderers);
2871
+ yield* processThinkingChunk(chunk.content, chunk.signature, chunkState, renderers, options);
2870
2872
  break;
2871
2873
  case "text":
2872
- yield* processTextChunk(chunk.content, chunkState, renderers, fullResponse);
2874
+ yield* processTextChunk(chunk.content, chunkState, renderers, fullResponse, options);
2873
2875
  break;
2874
2876
  case "image":
2875
2877
  yield* processImageChunk(chunk, renderers, options);
@@ -2898,11 +2900,34 @@ var truncateToolResult = (result, max) => {
2898
2900
  var executeToolCalls = async function* (pendingToolCalls, options, renderers, turnState) {
2899
2901
  const toolResultBlocks = [];
2900
2902
  for (const toolCall of pendingToolCalls) {
2901
- turnState.allToolsHtml += renderers.toolRunning(toolCall.name, toolCall.input);
2902
- yield { data: turnState.allToolsHtml, event: "tools" };
2903
+ if (options.structuredEvents) {
2904
+ yield {
2905
+ data: JSON.stringify({
2906
+ input: toolCall.input,
2907
+ name: toolCall.name,
2908
+ status: "running"
2909
+ }),
2910
+ event: "tools"
2911
+ };
2912
+ } else {
2913
+ turnState.allToolsHtml += renderers.toolRunning(toolCall.name, toolCall.input);
2914
+ yield { data: turnState.allToolsHtml, event: "tools" };
2915
+ }
2903
2916
  const result = await executeTool2(options, toolCall.name, toolCall.input);
2904
- turnState.allToolsHtml = turnState.allToolsHtml.replace(renderers.toolRunning(toolCall.name, toolCall.input), renderers.toolComplete(toolCall.name, result));
2905
- yield { data: turnState.allToolsHtml, event: "tools" };
2917
+ if (options.structuredEvents) {
2918
+ yield {
2919
+ data: JSON.stringify({
2920
+ input: toolCall.input,
2921
+ name: toolCall.name,
2922
+ result,
2923
+ status: "complete"
2924
+ }),
2925
+ event: "tools"
2926
+ };
2927
+ } else {
2928
+ turnState.allToolsHtml = turnState.allToolsHtml.replace(renderers.toolRunning(toolCall.name, toolCall.input), renderers.toolComplete(toolCall.name, result));
2929
+ yield { data: turnState.allToolsHtml, event: "tools" };
2930
+ }
2906
2931
  options.onToolUse?.(toolCall.name, toolCall.input, result);
2907
2932
  const resultContent = options.maxToolResultChars !== undefined ? truncateToolResult(result, options.maxToolResultChars) : result;
2908
2933
  toolResultBlocks.push({
@@ -2955,7 +2980,7 @@ var streamTurns = async function* (options, renderers, messages, signal, startTi
2955
2980
  let finishReason = "max_turns";
2956
2981
  let completedTurns = 0;
2957
2982
  try {
2958
- for (;turnState.turn < maxTurns && !signal.aborted; turnState.turn++) {
2983
+ for (;turnState.turn <= maxTurns && !signal.aborted; turnState.turn++) {
2959
2984
  const chunkState = {
2960
2985
  contentBlocks: [],
2961
2986
  currentThinking: null,
@@ -2985,36 +3010,38 @@ var streamTurns = async function* (options, renderers, messages, signal, startTi
2985
3010
  const runningTotalTokens = aggregateUsage.inputTokens + aggregateUsage.outputTokens;
2986
3011
  if (chunkState.stopReason === "max_tokens") {
2987
3012
  finishReason = "max_tokens";
2988
- yield {
2989
- data: renderers.error(`Response truncated at max_tokens (output=${chunkState.usage?.outputTokens ?? "?"}). ` + `Raise maxTokens on the provider/options, split the request, or reduce upstream context.`),
2990
- event: "status"
2991
- };
3013
+ yield stoppedEvent(options, renderers, "max_tokens", `Response truncated at max_tokens (output=${chunkState.usage?.outputTokens ?? "?"}). ` + `Raise maxTokens on the provider/options, split the request, or reduce upstream context.`);
2992
3014
  return;
2993
3015
  }
2994
3016
  if (options.maxTotalTokens && runningTotalTokens >= options.maxTotalTokens) {
2995
3017
  finishReason = "max_total_tokens";
2996
- yield {
2997
- data: renderers.error(`Stopped: token budget reached (${runningTotalTokens}/${options.maxTotalTokens} tokens over ` + `${turnState.turn} turns). Narrow the request or raise maxTotalTokens.`),
2998
- event: "status"
2999
- };
3018
+ yield stoppedEvent(options, renderers, "max_total_tokens", `Stopped: token budget reached (${runningTotalTokens}/${options.maxTotalTokens} tokens over ` + `${turnState.turn} turns). Narrow the request or raise maxTotalTokens.`);
3000
3019
  return;
3001
3020
  }
3002
3021
  if (options.maxDurationMs && Date.now() - startTime >= options.maxDurationMs) {
3003
3022
  finishReason = "max_duration";
3004
- yield {
3005
- data: renderers.error(`Stopped: time budget reached (${Math.round((Date.now() - startTime) / 1000)}s over ` + `${turnState.turn} turns). Narrow the request or raise maxDurationMs.`),
3006
- event: "status"
3007
- };
3023
+ yield stoppedEvent(options, renderers, "max_duration_ms", `Stopped: time budget reached (${Math.round((Date.now() - startTime) / 1000)}s over ` + `${turnState.turn} turns). Narrow the request or raise maxDurationMs.`);
3008
3024
  return;
3009
3025
  }
3010
3026
  if (shouldStopToolLoop(chunkState, turnState, signal)) {
3011
- finishReason = signal.aborted ? "aborted" : "complete";
3012
- return void (yield yieldCompletion(renderers, options, turnState.fullResponse, chunkState.usage, startTime));
3027
+ if (signal.aborted) {
3028
+ finishReason = "aborted";
3029
+ yield stoppedEvent(options, renderers, "aborted", "Aborted by caller.");
3030
+ } else {
3031
+ finishReason = "complete";
3032
+ yield completeEvent(options, renderers, turnState.fullResponse, chunkState.usage, Date.now() - startTime);
3033
+ }
3034
+ return;
3013
3035
  }
3014
3036
  yield* processTurn(chunkState, options, renderers, turnState);
3015
3037
  }
3016
- if (signal.aborted)
3038
+ if (signal.aborted) {
3017
3039
  finishReason = "aborted";
3040
+ yield stoppedEvent(options, renderers, "aborted", "Aborted by caller.");
3041
+ } else {
3042
+ finishReason = "max_turns";
3043
+ yield stoppedEvent(options, renderers, "max_turns", `Stopped: reached maxTurns (${maxTurns}) without a final answer.`);
3044
+ }
3018
3045
  } catch (error) {
3019
3046
  finishReason = signal.aborted ? "aborted" : "error";
3020
3047
  throw error;
@@ -5943,5 +5970,5 @@ export {
5943
5970
  BUILTIN_UI_CARDS
5944
5971
  };
5945
5972
 
5946
- //# debugId=DACDBFA8148E663564756E2164756E21
5973
+ //# debugId=1FA497882C909B0464756E2164756E21
5947
5974
  //# sourceMappingURL=index.js.map