@absolutejs/ai 0.3.4 → 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,18 @@ 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
+
9
21
  ## 0.3.4 — 2026-10-15
10
22
 
11
23
  ### Fixed
package/changelog.json CHANGED
@@ -2,6 +2,30 @@
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
+ },
5
29
  {
6
30
  "changes": [
7
31
  {
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({
@@ -6878,14 +6892,79 @@ var getNumber = (obj, key) => {
6878
6892
  }
6879
6893
  return 0;
6880
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
+ };
6881
6958
  var handleContentBlockStart = (parsed, state) => {
6882
6959
  const block = getRecord(parsed, "content_block");
6883
6960
  if (block && block.type === "tool_use") {
6884
6961
  state.currentToolId = getString(block, "id");
6885
6962
  state.currentToolName = getString(block, "name");
6886
6963
  state.toolInputJson = "";
6964
+ state.toolInputReported = 0;
6887
6965
  state.isThinkingBlock = false;
6888
6966
  state.currentProviderBlock = undefined;
6967
+ return toolInputProgress(state);
6889
6968
  } else if (block && block.type === "thinking") {
6890
6969
  state.isThinkingBlock = true;
6891
6970
  state.thinkingSignature = "";
@@ -6895,6 +6974,7 @@ var handleContentBlockStart = (parsed, state) => {
6895
6974
  state.currentProviderBlock = block && block.type !== "text" ? { ...block } : undefined;
6896
6975
  state.providerBlockInputJson = "";
6897
6976
  }
6977
+ return;
6898
6978
  };
6899
6979
  var handleContentBlockDelta = (parsed, state) => {
6900
6980
  const delta = getRecord(parsed, "delta");
@@ -6929,6 +7009,8 @@ var handleContentBlockDelta = (parsed, state) => {
6929
7009
  state.providerBlockInputJson += getString(delta, "partial_json");
6930
7010
  } else {
6931
7011
  state.toolInputJson += getString(delta, "partial_json");
7012
+ if (state.toolInputJson.length - state.toolInputReported >= TOOL_INPUT_PROGRESS_CHARS)
7013
+ return toolInputProgress(state);
6932
7014
  }
6933
7015
  }
6934
7016
  if (delta.type === "signature_delta") {
@@ -7048,8 +7130,7 @@ var handleError = (parsed, state) => {
7048
7130
  var processEvent = (eventType, parsed, state) => {
7049
7131
  switch (eventType) {
7050
7132
  case "content_block_start": {
7051
- handleContentBlockStart(parsed, state);
7052
- return;
7133
+ return handleContentBlockStart(parsed, state);
7053
7134
  }
7054
7135
  case "content_block_delta": {
7055
7136
  return handleContentBlockDelta(parsed, state);
@@ -7152,6 +7233,7 @@ async function* parseSSEStream4(body, providerName, signal) {
7152
7233
  stopReason: "",
7153
7234
  thinkingSignature: "",
7154
7235
  toolInputJson: "",
7236
+ toolInputReported: 0,
7155
7237
  usage: undefined,
7156
7238
  providerName,
7157
7239
  providerBlockInputJson: ""
@@ -9062,5 +9144,5 @@ export {
9062
9144
  xai
9063
9145
  };
9064
9146
 
9065
- //# debugId=16913D2F7231C56164756E2164756E21
9147
+ //# debugId=4ED2ED351363579D64756E2164756E21
9066
9148
  //# sourceMappingURL=index.js.map