@absolutejs/ai 0.3.3 → 0.4.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/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
8
 
9
+ ## 0.4.0 — 2026-10-16
10
+
11
+ ### Added
12
+
13
+ - **Report a tool call's input while it streams: a `tool_input` chunk, and a `preparing` tools event from streamAIToSSE** (`AIChunk`, `AIToolInputChunk`, `streamAIToSSE`)
14
+ A tool call with a long input, such as a whole file, takes the model a minute or more to write, and until now nothing arrived meanwhile. The Anthropic provider now yields a `tool_input` chunk when a tool call starts and every 2 KB of its input, with the tool name, the characters received and the top-level string fields already complete (a path, a name; at most 200 characters each, never read from inside other values). With structured events, streamAIToSSE turns it into a `tools` event with status `preparing`. Consumers that do not know the chunk can ignore it.
15
+
16
+ ### Fixed
17
+
18
+ - **streamAIToSSE sends a turn's text back as one block, not one per streamed delta, so agent loops stop re-writing their prompt cache** (`streamAIToSSE`)
19
+ streamAIToSSE stored every streamed text delta as its own content block, so an agent loop sent each turn's text back as dozens of blocks. Anthropic looks back only 20 blocks for an earlier cache entry, and a turn with a few sentences of text pushed the previous request's entry out of reach: the next request re-wrote the whole history at the cache-write price. A run of text is now one block, as generateAI and streamAIWithTools already kept it.
20
+
21
+ ## 0.3.4 — 2026-10-15
22
+
23
+ ### Fixed
24
+
25
+ - **Keep a prompt-cache breakpoint on the previous turn's user message as well as the last message, so an agent loop's next request reads its cached history by exact position instead of re-writing it; a production Studio build re-wrote its whole history on 7 of 48 requests and those writes were about half its cost.** (`anthropic`)
26
+
9
27
  ## 0.3.3 — 2026-10-10
10
28
 
11
29
  ### Fixed
package/changelog.json CHANGED
@@ -2,6 +2,43 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/ai",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "detail": "streamAIToSSE stored every streamed text delta as its own content block, so an agent loop sent each turn's text back as dozens of blocks. Anthropic looks back only 20 blocks for an earlier cache entry, and a turn with a few sentences of text pushed the previous request's entry out of reach: the next request re-wrote the whole history at the cache-write price. A run of text is now one block, as generateAI and streamAIWithTools already kept it.",
9
+ "kind": "fixed",
10
+ "summary": "streamAIToSSE sends a turn's text back as one block, not one per streamed delta, so agent loops stop re-writing their prompt cache",
11
+ "symbols": [
12
+ "streamAIToSSE"
13
+ ]
14
+ },
15
+ {
16
+ "detail": "A tool call with a long input, such as a whole file, takes the model a minute or more to write, and until now nothing arrived meanwhile. The Anthropic provider now yields a `tool_input` chunk when a tool call starts and every 2 KB of its input, with the tool name, the characters received and the top-level string fields already complete (a path, a name; at most 200 characters each, never read from inside other values). With structured events, streamAIToSSE turns it into a `tools` event with status `preparing`. Consumers that do not know the chunk can ignore it.",
17
+ "kind": "added",
18
+ "summary": "Report a tool call's input while it streams: a `tool_input` chunk, and a `preparing` tools event from streamAIToSSE",
19
+ "symbols": [
20
+ "AIChunk",
21
+ "AIToolInputChunk",
22
+ "streamAIToSSE"
23
+ ]
24
+ }
25
+ ],
26
+ "date": "2026-10-16",
27
+ "version": "0.4.0"
28
+ },
29
+ {
30
+ "changes": [
31
+ {
32
+ "kind": "fixed",
33
+ "summary": "Keep a prompt-cache breakpoint on the previous turn's user message as well as the last message, so an agent loop's next request reads its cached history by exact position instead of re-writing it; a production Studio build re-wrote its whole history on 7 of 48 requests and those writes were about half its cost.",
34
+ "symbols": [
35
+ "anthropic"
36
+ ]
37
+ }
38
+ ],
39
+ "date": "2026-10-15",
40
+ "version": "0.3.4"
41
+ },
5
42
  {
6
43
  "changes": [
7
44
  {
package/dist/ai/index.js CHANGED
@@ -999,10 +999,11 @@ var maybeFlushThinking = (chunkState) => {
999
999
  };
1000
1000
  var processTextChunk = function* (content, chunkState, renderers, fullResponse, options) {
1001
1001
  maybeFlushThinking(chunkState);
1002
- chunkState.contentBlocks.push({
1003
- content,
1004
- type: "text"
1005
- });
1002
+ const last = chunkState.contentBlocks.at(-1);
1003
+ if (last?.type === "text")
1004
+ last.content += content;
1005
+ else
1006
+ chunkState.contentBlocks.push({ content, type: "text" });
1006
1007
  yield contentEvent(options, renderers, content, fullResponse + content);
1007
1008
  };
1008
1009
  var processImageChunk = function* (chunk, renderers, options) {
@@ -1056,6 +1057,19 @@ var processChunk = function* (chunk, chunkState, renderers, options, fullRespons
1056
1057
  case "tool_use":
1057
1058
  processToolUseChunk(chunk, chunkState);
1058
1059
  break;
1060
+ case "tool_input":
1061
+ if (options.structuredEvents)
1062
+ yield {
1063
+ data: JSON.stringify({
1064
+ fields: chunk.fields,
1065
+ id: chunk.id,
1066
+ name: chunk.name,
1067
+ receivedChars: chunk.receivedChars,
1068
+ status: "preparing"
1069
+ }),
1070
+ event: "tools"
1071
+ };
1072
+ break;
1059
1073
  case "provider_event":
1060
1074
  maybeFlushThinking(chunkState);
1061
1075
  chunkState.contentBlocks.push({
@@ -6735,9 +6749,15 @@ var buildRequestBody3 = (params, configuredMax, configCaching) => {
6735
6749
  const options = requestOptionsFor(params);
6736
6750
  const messages = params.messages.filter((msg) => msg.role !== "system").map(mapMessage);
6737
6751
  if (caching && messages.length > 1) {
6738
- const last = messages[messages.length - 1];
6752
+ const lastIndex = messages.length - 1;
6753
+ const last = messages[lastIndex];
6739
6754
  if (last) {
6740
- messages[messages.length - 1] = cacheLastContentBlock(last);
6755
+ messages[lastIndex] = cacheLastContentBlock(last);
6756
+ }
6757
+ const previousUserIndex = messages.findLastIndex((message, index) => index < lastIndex - 1 && message.role === "user");
6758
+ const previous = messages[previousUserIndex];
6759
+ if (previous) {
6760
+ messages[previousUserIndex] = cacheLastContentBlock(previous);
6741
6761
  }
6742
6762
  }
6743
6763
  const max = typeof params.maxTokens === "number" ? params.maxTokens : configuredMax;
@@ -6872,14 +6892,79 @@ var getNumber = (obj, key) => {
6872
6892
  }
6873
6893
  return 0;
6874
6894
  };
6895
+ var TOOL_INPUT_PROGRESS_CHARS = 2048;
6896
+ var TOOL_INPUT_FIELD_MAX_CHARS = 200;
6897
+ var readJsonString = (json, start) => {
6898
+ for (let index = start + 1;index < json.length; index += 1) {
6899
+ if (json[index] === "\\")
6900
+ index += 1;
6901
+ else if (json[index] === '"')
6902
+ return { end: index + 1, raw: json.slice(start + 1, index) };
6903
+ }
6904
+ return;
6905
+ };
6906
+ var decodeJsonString = (raw) => {
6907
+ try {
6908
+ const value = JSON.parse(`"${raw}"`);
6909
+ return typeof value === "string" ? value : undefined;
6910
+ } catch {
6911
+ return;
6912
+ }
6913
+ };
6914
+ var completeStringFields = (partialJson) => {
6915
+ const fields = {};
6916
+ let depth = 0;
6917
+ let key;
6918
+ let index = 0;
6919
+ while (index < partialJson.length) {
6920
+ const character = partialJson[index];
6921
+ if (character === '"') {
6922
+ const text = readJsonString(partialJson, index);
6923
+ if (!text)
6924
+ break;
6925
+ if (depth === 1 && key === undefined) {
6926
+ const after = partialJson.slice(text.end).trimStart();
6927
+ if (after.startsWith(":"))
6928
+ key = text.raw;
6929
+ } else if (depth === 1 && key !== undefined) {
6930
+ const value = decodeJsonString(text.raw);
6931
+ if (value !== undefined && value.length <= TOOL_INPUT_FIELD_MAX_CHARS)
6932
+ fields[key] = value;
6933
+ key = undefined;
6934
+ }
6935
+ index = text.end;
6936
+ continue;
6937
+ }
6938
+ if (character === "{" || character === "[")
6939
+ depth += 1;
6940
+ else if (character === "}" || character === "]")
6941
+ depth -= 1;
6942
+ else if (character === "," && depth === 1)
6943
+ key = undefined;
6944
+ index += 1;
6945
+ }
6946
+ return fields;
6947
+ };
6948
+ var toolInputProgress = (state) => {
6949
+ state.toolInputReported = state.toolInputJson.length;
6950
+ return {
6951
+ fields: completeStringFields(state.toolInputJson),
6952
+ id: state.currentToolId,
6953
+ name: state.currentToolName,
6954
+ receivedChars: state.toolInputJson.length,
6955
+ type: "tool_input"
6956
+ };
6957
+ };
6875
6958
  var handleContentBlockStart = (parsed, state) => {
6876
6959
  const block = getRecord(parsed, "content_block");
6877
6960
  if (block && block.type === "tool_use") {
6878
6961
  state.currentToolId = getString(block, "id");
6879
6962
  state.currentToolName = getString(block, "name");
6880
6963
  state.toolInputJson = "";
6964
+ state.toolInputReported = 0;
6881
6965
  state.isThinkingBlock = false;
6882
6966
  state.currentProviderBlock = undefined;
6967
+ return toolInputProgress(state);
6883
6968
  } else if (block && block.type === "thinking") {
6884
6969
  state.isThinkingBlock = true;
6885
6970
  state.thinkingSignature = "";
@@ -6889,6 +6974,7 @@ var handleContentBlockStart = (parsed, state) => {
6889
6974
  state.currentProviderBlock = block && block.type !== "text" ? { ...block } : undefined;
6890
6975
  state.providerBlockInputJson = "";
6891
6976
  }
6977
+ return;
6892
6978
  };
6893
6979
  var handleContentBlockDelta = (parsed, state) => {
6894
6980
  const delta = getRecord(parsed, "delta");
@@ -6923,6 +7009,8 @@ var handleContentBlockDelta = (parsed, state) => {
6923
7009
  state.providerBlockInputJson += getString(delta, "partial_json");
6924
7010
  } else {
6925
7011
  state.toolInputJson += getString(delta, "partial_json");
7012
+ if (state.toolInputJson.length - state.toolInputReported >= TOOL_INPUT_PROGRESS_CHARS)
7013
+ return toolInputProgress(state);
6926
7014
  }
6927
7015
  }
6928
7016
  if (delta.type === "signature_delta") {
@@ -7042,8 +7130,7 @@ var handleError = (parsed, state) => {
7042
7130
  var processEvent = (eventType, parsed, state) => {
7043
7131
  switch (eventType) {
7044
7132
  case "content_block_start": {
7045
- handleContentBlockStart(parsed, state);
7046
- return;
7133
+ return handleContentBlockStart(parsed, state);
7047
7134
  }
7048
7135
  case "content_block_delta": {
7049
7136
  return handleContentBlockDelta(parsed, state);
@@ -7146,6 +7233,7 @@ async function* parseSSEStream4(body, providerName, signal) {
7146
7233
  stopReason: "",
7147
7234
  thinkingSignature: "",
7148
7235
  toolInputJson: "",
7236
+ toolInputReported: 0,
7149
7237
  usage: undefined,
7150
7238
  providerName,
7151
7239
  providerBlockInputJson: ""
@@ -9056,5 +9144,5 @@ export {
9056
9144
  xai
9057
9145
  };
9058
9146
 
9059
- //# debugId=C9BA825E688A09A764756E2164756E21
9147
+ //# debugId=4ED2ED351363579D64756E2164756E21
9060
9148
  //# sourceMappingURL=index.js.map