@absolutejs/ai 0.0.43 → 0.0.45
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 +33 -0
- package/dist/ai/index.js +137 -81
- package/dist/ai/index.js.map +4 -4
- package/dist/types/ai.d.ts +55 -0
- package/package.json +1 -1
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
|
-
|
|
2793
|
-
|
|
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
|
|
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
|
-
|
|
2902
|
-
|
|
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
|
-
|
|
2905
|
-
|
|
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
|
|
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
|
-
|
|
3012
|
-
|
|
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;
|
|
@@ -3730,49 +3757,78 @@ var errorPayload = (error) => {
|
|
|
3730
3757
|
type: providerError.type
|
|
3731
3758
|
};
|
|
3732
3759
|
};
|
|
3733
|
-
var streamResponseBody = (iterator, heartbeatMs, onError) =>
|
|
3734
|
-
|
|
3760
|
+
var streamResponseBody = (iterator, heartbeatMs, onError, onCancel) => {
|
|
3761
|
+
let closed = false;
|
|
3762
|
+
const send = (controller, value) => {
|
|
3763
|
+
if (closed)
|
|
3764
|
+
return false;
|
|
3735
3765
|
try {
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3766
|
+
controller.enqueue(value);
|
|
3767
|
+
return true;
|
|
3768
|
+
} catch {
|
|
3769
|
+
closed = true;
|
|
3770
|
+
return false;
|
|
3771
|
+
}
|
|
3772
|
+
};
|
|
3773
|
+
return new ReadableStream({
|
|
3774
|
+
cancel(reason) {
|
|
3775
|
+
closed = true;
|
|
3776
|
+
onCancel?.(reason);
|
|
3777
|
+
},
|
|
3778
|
+
async start(controller) {
|
|
3779
|
+
try {
|
|
3739
3780
|
for (;; ) {
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3781
|
+
const pending = iterator.next();
|
|
3782
|
+
let next;
|
|
3783
|
+
for (;; ) {
|
|
3784
|
+
let timer;
|
|
3785
|
+
const heartbeat = new Promise((resolve) => {
|
|
3786
|
+
timer = setTimeout(() => resolve("heartbeat"), heartbeatMs);
|
|
3787
|
+
});
|
|
3788
|
+
const winner = heartbeatMs > 0 ? await Promise.race([pending, heartbeat]) : await pending;
|
|
3789
|
+
if (timer)
|
|
3790
|
+
clearTimeout(timer);
|
|
3791
|
+
if (winner === "heartbeat") {
|
|
3792
|
+
if (!send(controller, encoder.encode(`: ping
|
|
3749
3793
|
|
|
3750
|
-
`))
|
|
3751
|
-
|
|
3794
|
+
`)))
|
|
3795
|
+
return;
|
|
3796
|
+
continue;
|
|
3797
|
+
}
|
|
3798
|
+
next = winner;
|
|
3799
|
+
break;
|
|
3752
3800
|
}
|
|
3753
|
-
|
|
3754
|
-
|
|
3801
|
+
if (closed || next.done)
|
|
3802
|
+
break;
|
|
3803
|
+
if (!send(controller, encodeEvent("chunk", next.value)))
|
|
3804
|
+
break;
|
|
3805
|
+
}
|
|
3806
|
+
} catch (error) {
|
|
3807
|
+
if (closed)
|
|
3808
|
+
return;
|
|
3809
|
+
await onError?.(error);
|
|
3810
|
+
send(controller, encodeEvent("error", errorPayload(error)));
|
|
3811
|
+
} finally {
|
|
3812
|
+
await iterator.return?.();
|
|
3813
|
+
if (!closed) {
|
|
3814
|
+
closed = true;
|
|
3815
|
+
try {
|
|
3816
|
+
controller.close();
|
|
3817
|
+
} catch {}
|
|
3755
3818
|
}
|
|
3756
|
-
if (next.done)
|
|
3757
|
-
break;
|
|
3758
|
-
controller.enqueue(encodeEvent("chunk", next.value));
|
|
3759
3819
|
}
|
|
3760
|
-
} catch (error) {
|
|
3761
|
-
await onError?.(error);
|
|
3762
|
-
controller.enqueue(encodeEvent("error", errorPayload(error)));
|
|
3763
|
-
} finally {
|
|
3764
|
-
await iterator.return?.();
|
|
3765
|
-
controller.close();
|
|
3766
3820
|
}
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3821
|
+
});
|
|
3822
|
+
};
|
|
3769
3823
|
var createProviderProxyResponse = async (provider, value, options = {}) => {
|
|
3770
3824
|
const params = parseProviderProxyParams(value);
|
|
3771
3825
|
if (!params) {
|
|
3772
3826
|
return Response.json({ error: "invalid provider stream request" }, { status: 400 });
|
|
3773
3827
|
}
|
|
3774
|
-
const
|
|
3775
|
-
|
|
3828
|
+
const disconnectController = new AbortController;
|
|
3829
|
+
const signal = options.signal ? AbortSignal.any([options.signal, disconnectController.signal]) : disconnectController.signal;
|
|
3830
|
+
const iterator = provider.stream({ ...params, signal })[Symbol.asyncIterator]();
|
|
3831
|
+
return new Response(streamResponseBody(iterator, options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS2, options.onError, (reason) => disconnectController.abort(reason)), {
|
|
3776
3832
|
headers: {
|
|
3777
3833
|
"cache-control": "no-cache",
|
|
3778
3834
|
"content-type": "text/event-stream; charset=utf-8",
|
|
@@ -5943,5 +5999,5 @@ export {
|
|
|
5943
5999
|
BUILTIN_UI_CARDS
|
|
5944
6000
|
};
|
|
5945
6001
|
|
|
5946
|
-
//# debugId=
|
|
6002
|
+
//# debugId=4DB8BD5EBB62E6E464756E2164756E21
|
|
5947
6003
|
//# sourceMappingURL=index.js.map
|