@absolutejs/ai 0.0.42 → 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 +33 -0
- package/dist/ai/index.js +136 -80
- package/dist/ai/index.js.map +3 -3
- package/dist/types/ai.d.ts +68 -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({
|
|
@@ -2951,57 +2976,88 @@ var streamTurns = async function* (options, renderers, messages, signal, startTi
|
|
|
2951
2976
|
turn: 0
|
|
2952
2977
|
};
|
|
2953
2978
|
const toolDefs = options.tools ? buildToolDefinitions2(options.tools) : undefined;
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
cacheSystemPrompt: options.cacheSystemPrompt,
|
|
2966
|
-
maxTokens: options.maxTokens,
|
|
2967
|
-
messages: turnState.currentMessages,
|
|
2968
|
-
model: options.model,
|
|
2969
|
-
promptCaching: options.promptCaching,
|
|
2970
|
-
reasoning: options.reasoning,
|
|
2971
|
-
signal,
|
|
2972
|
-
systemPrompt: options.systemPrompt,
|
|
2973
|
-
tools: toolDefs
|
|
2974
|
-
});
|
|
2975
|
-
yield* consumeStream2(stream, chunkState, renderers, options, turnState, signal);
|
|
2976
|
-
options.onTurn?.(turnState.turn, chunkState.usage, turnState.fullResponse.slice(responseBeforeTurn.length));
|
|
2977
|
-
runningTotalTokens += (chunkState.usage?.inputTokens ?? 0) + (chunkState.usage?.outputTokens ?? 0);
|
|
2978
|
-
if (chunkState.stopReason === "max_tokens") {
|
|
2979
|
-
yield {
|
|
2980
|
-
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.`),
|
|
2981
|
-
event: "status"
|
|
2982
|
-
};
|
|
2983
|
-
return;
|
|
2984
|
-
}
|
|
2985
|
-
if (options.maxTotalTokens && runningTotalTokens >= options.maxTotalTokens) {
|
|
2986
|
-
yield {
|
|
2987
|
-
data: renderers.error(`Stopped: token budget reached (${runningTotalTokens}/${options.maxTotalTokens} tokens over ` + `${turnState.turn} turns). Narrow the request or raise maxTotalTokens.`),
|
|
2988
|
-
event: "status"
|
|
2979
|
+
const aggregateUsage = { inputTokens: 0, outputTokens: 0 };
|
|
2980
|
+
let finishReason = "max_turns";
|
|
2981
|
+
let completedTurns = 0;
|
|
2982
|
+
try {
|
|
2983
|
+
for (;turnState.turn <= maxTurns && !signal.aborted; turnState.turn++) {
|
|
2984
|
+
const chunkState = {
|
|
2985
|
+
contentBlocks: [],
|
|
2986
|
+
currentThinking: null,
|
|
2987
|
+
pendingToolCalls: [],
|
|
2988
|
+
stopReason: undefined,
|
|
2989
|
+
usage: undefined
|
|
2989
2990
|
};
|
|
2990
|
-
|
|
2991
|
+
const responseBeforeTurn = turnState.fullResponse;
|
|
2992
|
+
const stream = options.provider.stream({
|
|
2993
|
+
cacheSystemPrompt: options.cacheSystemPrompt,
|
|
2994
|
+
maxTokens: options.maxTokens,
|
|
2995
|
+
messages: turnState.currentMessages,
|
|
2996
|
+
model: options.model,
|
|
2997
|
+
promptCaching: options.promptCaching,
|
|
2998
|
+
reasoning: options.reasoning,
|
|
2999
|
+
signal,
|
|
3000
|
+
systemPrompt: options.systemPrompt,
|
|
3001
|
+
tools: toolDefs
|
|
3002
|
+
});
|
|
3003
|
+
yield* consumeStream2(stream, chunkState, renderers, options, turnState, signal);
|
|
3004
|
+
options.onTurn?.(turnState.turn, chunkState.usage, turnState.fullResponse.slice(responseBeforeTurn.length));
|
|
3005
|
+
completedTurns += 1;
|
|
3006
|
+
aggregateUsage.inputTokens += chunkState.usage?.inputTokens ?? 0;
|
|
3007
|
+
aggregateUsage.outputTokens += chunkState.usage?.outputTokens ?? 0;
|
|
3008
|
+
aggregateUsage.cacheReadInputTokens = (aggregateUsage.cacheReadInputTokens ?? 0) + (chunkState.usage?.cacheReadInputTokens ?? 0);
|
|
3009
|
+
aggregateUsage.cacheWriteInputTokens = (aggregateUsage.cacheWriteInputTokens ?? 0) + (chunkState.usage?.cacheWriteInputTokens ?? 0);
|
|
3010
|
+
const runningTotalTokens = aggregateUsage.inputTokens + aggregateUsage.outputTokens;
|
|
3011
|
+
if (chunkState.stopReason === "max_tokens") {
|
|
3012
|
+
finishReason = "max_tokens";
|
|
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.`);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
if (options.maxTotalTokens && runningTotalTokens >= options.maxTotalTokens) {
|
|
3017
|
+
finishReason = "max_total_tokens";
|
|
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.`);
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
if (options.maxDurationMs && Date.now() - startTime >= options.maxDurationMs) {
|
|
3022
|
+
finishReason = "max_duration";
|
|
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.`);
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
if (shouldStopToolLoop(chunkState, turnState, signal)) {
|
|
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;
|
|
3035
|
+
}
|
|
3036
|
+
yield* processTurn(chunkState, options, renderers, turnState);
|
|
2991
3037
|
}
|
|
2992
|
-
if (
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3038
|
+
if (signal.aborted) {
|
|
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.`);
|
|
2998
3044
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3045
|
+
} catch (error) {
|
|
3046
|
+
finishReason = signal.aborted ? "aborted" : "error";
|
|
3047
|
+
throw error;
|
|
3048
|
+
} finally {
|
|
3049
|
+
try {
|
|
3050
|
+
await options.onFinish?.({
|
|
3051
|
+
durationMs: Date.now() - startTime,
|
|
3052
|
+
fullResponse: turnState.fullResponse,
|
|
3053
|
+
reason: finishReason,
|
|
3054
|
+
turns: completedTurns,
|
|
3055
|
+
usage: aggregateUsage
|
|
3056
|
+
});
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
console.error("[absolute-ai] onFinish rejected:", error);
|
|
3001
3059
|
}
|
|
3002
|
-
yield* processTurn(chunkState, options, renderers, turnState);
|
|
3003
3060
|
}
|
|
3004
|
-
return;
|
|
3005
3061
|
};
|
|
3006
3062
|
|
|
3007
3063
|
// src/constants.ts
|
|
@@ -5914,5 +5970,5 @@ export {
|
|
|
5914
5970
|
BUILTIN_UI_CARDS
|
|
5915
5971
|
};
|
|
5916
5972
|
|
|
5917
|
-
//# debugId=
|
|
5973
|
+
//# debugId=1FA497882C909B0464756E2164756E21
|
|
5918
5974
|
//# sourceMappingURL=index.js.map
|