@absolutejs/ai 0.4.1 → 0.5.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,17 @@ 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.5.0 — 2026-10-19
10
+
11
+ ### Breaking
12
+
13
+ - **AIToolDefinition.handler may return { text, images } as well as a string** (`AIToolDefinition`, `toolOutputText`)
14
+ _Migration:_ Code that calls a tool's handler itself (a wrapper, a test) now receives AIToolOutput, a string or { text, images }. Read its text with toolOutputText(output). Handlers that return a string need no change.
15
+
16
+ ### Added
17
+
18
+ - **Tools can return images for the model to see, such as a screenshot: a handler returns { text, images } and the tool loops send the images inside the tool result, mapped for Anthropic, OpenAI Responses, OpenAI Chat Completions, Gemini and Ollama** (`AIToolResult`, `AIToolOutput`, `toolOutputText`, `AIProviderContentBlock`, `anthropic`, `openai`, `openaiResponses`, `gemini`, `ollama`, `streamAIToSSE`, `streamAIWithTools`, `generateAIWithTools`, `hardenUntrustedTool`, `createAIStoredToolResultRecovery`)
19
+
9
20
  ## 0.4.1 — 2026-10-17
10
21
 
11
22
  ### Fixed
package/changelog.json CHANGED
@@ -2,6 +2,44 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/ai",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "kind": "breaking",
9
+ "migration": {
10
+ "instruction": "Code that calls a tool's handler itself (a wrapper, a test) now receives AIToolOutput, a string or { text, images }. Read its text with toolOutputText(output). Handlers that return a string need no change.",
11
+ "manual": true
12
+ },
13
+ "summary": "AIToolDefinition.handler may return { text, images } as well as a string",
14
+ "symbols": [
15
+ "AIToolDefinition",
16
+ "toolOutputText"
17
+ ]
18
+ },
19
+ {
20
+ "kind": "added",
21
+ "summary": "Tools can return images for the model to see, such as a screenshot: a handler returns { text, images } and the tool loops send the images inside the tool result, mapped for Anthropic, OpenAI Responses, OpenAI Chat Completions, Gemini and Ollama",
22
+ "symbols": [
23
+ "AIToolResult",
24
+ "AIToolOutput",
25
+ "toolOutputText",
26
+ "AIProviderContentBlock",
27
+ "anthropic",
28
+ "openai",
29
+ "openaiResponses",
30
+ "gemini",
31
+ "ollama",
32
+ "streamAIToSSE",
33
+ "streamAIWithTools",
34
+ "generateAIWithTools",
35
+ "hardenUntrustedTool",
36
+ "createAIStoredToolResultRecovery"
37
+ ]
38
+ }
39
+ ],
40
+ "date": "2026-10-19",
41
+ "version": "0.5.0"
42
+ },
5
43
  {
6
44
  "changes": [
7
45
  {
package/dist/ai/index.js CHANGED
@@ -258,6 +258,14 @@ var parseAIMessage = (raw) => {
258
258
  };
259
259
  var serializeAIMessage = (message) => JSON.stringify(message);
260
260
 
261
+ // src/ai/toolOutput.ts
262
+ var toolOutputText = (output) => typeof output === "string" ? output : output.text;
263
+ var toolOutputImages = (output) => typeof output === "string" || !output.images?.length ? undefined : output.images;
264
+ var toolResultBlock = (toolUseId, output, content = toolOutputText(output)) => {
265
+ const images = toolOutputImages(output);
266
+ return images ? { content, images, tool_use_id: toolUseId, type: "tool_result" } : { content, tool_use_id: toolUseId, type: "tool_result" };
267
+ };
268
+
261
269
  // src/ai/inputCapacity.ts
262
270
  class AIInputError extends Error {
263
271
  code;
@@ -643,14 +651,11 @@ var processToolTurn = async (socket, options, state, messageId, conversationId,
643
651
  const toolResultBlocks = [];
644
652
  for (const toolCall of toolCalls) {
645
653
  await sendToolRunning(socket, toolCall.name, toolCall.input, messageId, conversationId);
646
- const result = await executeTool(options, toolCall.name, toolCall.input);
654
+ const output = await executeTool(options, toolCall.name, toolCall.input);
655
+ const result = toolOutputText(output);
647
656
  await sendToolComplete(socket, toolCall.name, result, messageId, conversationId);
648
657
  options.onToolUse?.(toolCall.name, toolCall.input, result);
649
- toolResultBlocks.push({
650
- content: result,
651
- tool_use_id: toolCall.id,
652
- type: "tool_result"
653
- });
658
+ toolResultBlocks.push(toolResultBlock(toolCall.id, output));
654
659
  state.executedToolKeys.add(serializeToolCall(toolCall.name, toolCall.input));
655
660
  }
656
661
  state.currentMessages.push({
@@ -1112,7 +1117,8 @@ var executeToolCalls = async function* (pendingToolCalls, options, renderers, tu
1112
1117
  turnState.allToolsHtml += renderers.toolRunning(toolCall.name, toolCall.input);
1113
1118
  yield { data: turnState.allToolsHtml, event: "tools" };
1114
1119
  }
1115
- const result = await executeTool2(options, toolCall.name, toolCall.input);
1120
+ const output = await executeTool2(options, toolCall.name, toolCall.input);
1121
+ const result = toolOutputText(output);
1116
1122
  if (options.structuredEvents) {
1117
1123
  yield {
1118
1124
  data: JSON.stringify({
@@ -1129,11 +1135,7 @@ var executeToolCalls = async function* (pendingToolCalls, options, renderers, tu
1129
1135
  }
1130
1136
  options.onToolUse?.(toolCall.name, toolCall.input, result);
1131
1137
  const resultContent = options.maxToolResultChars !== undefined ? truncateToolResult(result, options.maxToolResultChars) : result;
1132
- toolResultBlocks.push({
1133
- content: resultContent,
1134
- tool_use_id: toolCall.id,
1135
- type: "tool_result"
1136
- });
1138
+ toolResultBlocks.push(toolResultBlock(toolCall.id, output, resultContent));
1137
1139
  }
1138
1140
  return toolResultBlocks;
1139
1141
  };
@@ -1833,12 +1835,8 @@ var generateAIWithTools = async (options) => {
1833
1835
  const resultBlocks = await Promise.all(result.toolCalls.map(async (call) => {
1834
1836
  const definition = tools[call.name];
1835
1837
  const output = definition ? await Promise.resolve(definition.handler(call.input)).catch((err) => `Error: ${err instanceof Error ? err.message : String(err)}`) : `Error: unknown tool "${call.name}"`;
1836
- onToolUse?.(call.name, call.input, output);
1837
- return {
1838
- content: output,
1839
- tool_use_id: call.id,
1840
- type: "tool_result"
1841
- };
1838
+ onToolUse?.(call.name, call.input, toolOutputText(output));
1839
+ return toolResultBlock(call.id, output);
1842
1840
  }));
1843
1841
  return [
1844
1842
  ...result.requestMessages ?? messages,
@@ -1988,14 +1986,15 @@ var flushThinking3 = (blocks, thinking) => {
1988
1986
  var executeToolHandler = async (tools, call) => {
1989
1987
  const definition = tools[call.name];
1990
1988
  if (!definition) {
1991
- return { ok: false, result: `Error: unknown tool "${call.name}"` };
1989
+ return { ok: false, output: `Error: unknown tool "${call.name}"` };
1992
1990
  }
1993
1991
  try {
1994
- return { ok: true, result: await definition.handler(call.input) };
1992
+ const output = await definition.handler(call.input);
1993
+ return { ok: true, output };
1995
1994
  } catch (err) {
1996
1995
  return {
1997
1996
  ok: false,
1998
- result: `Error: ${err instanceof Error ? err.message : String(err)}`
1997
+ output: `Error: ${err instanceof Error ? err.message : String(err)}`
1999
1998
  };
2000
1999
  }
2001
2000
  };
@@ -2142,7 +2141,8 @@ var streamAIWithTools = async function* (options) {
2142
2141
  type: "tool_start"
2143
2142
  };
2144
2143
  const startedAt = Date.now();
2145
- const { ok, result } = await executeToolHandler(tools, call);
2144
+ const { ok, output } = await executeToolHandler(tools, call);
2145
+ const result = toolOutputText(output);
2146
2146
  executedKeys.add(toolCallKey(call));
2147
2147
  yield {
2148
2148
  id: call.id,
@@ -2153,11 +2153,7 @@ var streamAIWithTools = async function* (options) {
2153
2153
  result,
2154
2154
  type: "tool_result"
2155
2155
  };
2156
- resultBlocks.push({
2157
- content: result,
2158
- tool_use_id: call.id,
2159
- type: "tool_result"
2160
- });
2156
+ resultBlocks.push(toolResultBlock(call.id, output));
2161
2157
  if (ok && stopAfterTools?.includes(call.name)) {
2162
2158
  terminalToolSucceeded = true;
2163
2159
  break;
@@ -5179,7 +5175,10 @@ var convertToolBlock = (block) => {
5179
5175
  if (block.type === "tool_result") {
5180
5176
  return {
5181
5177
  call_id: block.tool_use_id,
5182
- output: typeof block.content === "string" ? block.content : "",
5178
+ output: block.images?.length ? [
5179
+ { text: block.content, type: "input_text" },
5180
+ ...block.images.map((source) => mapBlockToResponsesFormat({ source, type: "image" }))
5181
+ ] : block.content,
5183
5182
  type: "function_call_output"
5184
5183
  };
5185
5184
  }
@@ -5709,6 +5708,7 @@ var NOT_FOUND4 = -1;
5709
5708
  var isRecord3 = (value) => typeof value === "object" && value !== null;
5710
5709
  var isRecordArray2 = (value) => Array.isArray(value) && value.length > 0 && isRecord3(value[0]);
5711
5710
  var hasArrayContent = (msg) => typeof msg.content !== "string" && Array.isArray(msg.content);
5711
+ var imageUrl = (source) => source.type === "url" ? source.url : `data:${source.media_type};base64,${source.data}`;
5712
5712
  var buildToolMessages = (blocks) => {
5713
5713
  const toolUseBlocks = blocks.filter((block) => block.type === "tool_use");
5714
5714
  const toolResultBlocks = blocks.filter((block) => block.type === "tool_result");
@@ -5734,6 +5734,18 @@ var buildToolMessages = (blocks) => {
5734
5734
  tool_call_id: result.tool_use_id
5735
5735
  });
5736
5736
  }
5737
+ const images = toolResultBlocks.flatMap((result) => result.images ?? []);
5738
+ if (images.length > 0)
5739
+ messages.push({
5740
+ content: [
5741
+ { text: "Images returned by the tool calls above:", type: "text" },
5742
+ ...images.map((source) => ({
5743
+ image_url: { url: imageUrl(source) },
5744
+ type: "image_url"
5745
+ }))
5746
+ ],
5747
+ role: "user"
5748
+ });
5737
5749
  return messages;
5738
5750
  };
5739
5751
  var mapToolDefinitions = (tools) => tools.map((tool) => ({
@@ -5747,9 +5759,7 @@ var mapToolDefinitions = (tools) => tools.map((tool) => ({
5747
5759
  var mapContentBlockToOpenAI = (block) => {
5748
5760
  if (block.type === "image") {
5749
5761
  return {
5750
- image_url: {
5751
- url: block.source.type === "url" ? block.source.url : `data:${block.source.media_type};base64,${block.source.data}`
5752
- },
5762
+ image_url: { url: imageUrl(block.source) },
5753
5763
  type: "image_url"
5754
5764
  };
5755
5765
  }
@@ -6372,7 +6382,10 @@ var convertMessageContent = (content) => {
6372
6382
  if (typeof content === "string") {
6373
6383
  return [{ text: content }];
6374
6384
  }
6375
- return content.map(mapContentBlock).filter((mapped) => mapped !== null);
6385
+ return content.flatMap((block) => block.type === "tool_result" && block.images?.length ? [
6386
+ mapContentBlock(block),
6387
+ ...block.images.map((source) => mapContentBlock({ source, type: "image" }))
6388
+ ] : [mapContentBlock(block)]).filter((mapped) => mapped !== null);
6376
6389
  };
6377
6390
  var hasFunctionResponse = (content) => content.some((block) => block.type === "tool_result");
6378
6391
  var convertSingleMessage = (msg) => {
@@ -6672,7 +6685,10 @@ var mapContentBlock2 = (block) => {
6672
6685
  }
6673
6686
  if (block.type === "tool_result") {
6674
6687
  return {
6675
- content: block.content,
6688
+ content: block.images?.length ? [
6689
+ { text: block.content, type: "text" },
6690
+ ...block.images.map((source) => ({ source, type: "image" }))
6691
+ ] : block.content,
6676
6692
  tool_use_id: block.tool_use_id,
6677
6693
  type: "tool_result"
6678
6694
  };
@@ -7418,8 +7434,10 @@ var convertMessage2 = (msg) => {
7418
7434
  });
7419
7435
  }
7420
7436
  for (const block of toolResultBlocks) {
7437
+ const images = (block.images ?? []).flatMap((source) => source.type === "base64" ? [source.data] : []);
7421
7438
  results.push({
7422
7439
  content: typeof block.content === "string" ? block.content : "",
7440
+ ...images.length > 0 ? { images } : {},
7423
7441
  role: "tool"
7424
7442
  });
7425
7443
  }
@@ -9026,6 +9044,12 @@ var createAIStoredToolResultRecovery = (toolNames) => async ({ params }) => {
9026
9044
  if (block.type !== "tool_result" || !eligible.has(block.tool_use_id) || block === newest)
9027
9045
  return block;
9028
9046
  const marker = "[Earlier lookup removed from working context. Its immutable original remains saved. The original tool call and source/version arguments above are unchanged: repeat that lookup if needed to verify facts. Do not infer facts from this marker.]";
9047
+ if (block.images?.length)
9048
+ return {
9049
+ content: marker,
9050
+ tool_use_id: block.tool_use_id,
9051
+ type: block.type
9052
+ };
9029
9053
  return block.content.length > marker.length ? { ...block, content: marker } : block;
9030
9054
  })
9031
9055
  }));
@@ -9167,10 +9191,11 @@ export {
9167
9191
  streamAIWithTools,
9168
9192
  streamWithAIContext,
9169
9193
  tableCard,
9194
+ toolOutputText,
9170
9195
  verifyOpenRouterWebhookSignature,
9171
9196
  withResilience,
9172
9197
  xai
9173
9198
  };
9174
9199
 
9175
- //# debugId=A58174807DA8A73664756E2164756E21
9200
+ //# debugId=94F48F9A5CBF872A64756E2164756E21
9176
9201
  //# sourceMappingURL=index.js.map