Skip to content

Preserve multimodal tool results#416

Open
akhilles wants to merge 3 commits intoTanStack:mainfrom
diodeinc:fix/multimodal-tool-results
Open

Preserve multimodal tool results#416
akhilles wants to merge 3 commits intoTanStack:mainfrom
diodeinc:fix/multimodal-tool-results

Conversation

@akhilles
Copy link
Copy Markdown

@akhilles akhilles commented Apr 2, 2026

Preserve multimodal tool results

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

* There is no CONTRIBUTING.md?

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Fixes #363

Summary by CodeRabbit

  • Bug Fixes

    • Multimodal tool results (text + images) are no longer collapsed to strings and are preserved across chat history and provider adapters.
  • New Features

    • Default chat message renderers (React, Solid, Vue) now display mixed text and image tool-result content.
    • Tool-result payload handling and public types updated to natively support structured multimodal content across providers.
  • Tests

    • Added tests validating preservation and correct transformation of multimodal tool results.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Preserves multimodal tool results across chat flows by introducing ToolResultContent (string | Array), replacing prior stringify/parse behavior, updating types, core chat and stream processing, provider adapters, tests, and default UI renderers to pass and render structured tool outputs.

Changes

Cohort / File(s) Summary
Types
packages/typescript/ai-client/src/types.ts, packages/typescript/ai/src/types.ts, packages/typescript/ai-vue-ui/src/types.ts
Add ToolResultContent and change ToolResultPart.content / ToolCallEndEvent.result to accept `string
Core message utilities
packages/typescript/ai/src/activities/chat/messages.ts
Add type guards and helpers (isContentPartArray, normalizeToolResultContent, parseToolResultValue, decodeToolResultContent, getToolResultContent) to detect/normalize structured tool-result content.
Chat activity & tools
packages/typescript/ai/src/activities/chat/index.ts, packages/typescript/ai/src/activities/chat/tools/tool-calls.ts
Replace inline JSON.parse/stringify with normalization/parsing helpers; preserve multimodal content when ingesting/emitting tool messages and events.
Stream processing
packages/typescript/ai/src/activities/chat/stream/processor.ts, packages/typescript/ai/src/activities/chat/stream/message-updaters.ts
Use ToolResultContent type; store/emit normalized content; handle falsy-but-valid results and use decode/parse helpers instead of inline parsing.
Provider adapters
packages/typescript/ai-openai/src/adapters/text.ts, packages/typescript/ai-anthropic/src/adapters/text.ts
Convert role: 'tool' multimodal message.content into provider-specific content parts (no longer JSON.stringify), producing structured function_call_output / tool_result payloads.
Adapter tests
packages/typescript/ai-openai/tests/openai-adapter.test.ts, packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts
Add tests asserting adapters send multimodal tool outputs preserving text and image parts in provider request payloads.
UI renderers
packages/typescript/ai-react-ui/src/chat-message.tsx, packages/typescript/ai-solid-ui/src/chat-message.tsx, packages/typescript/ai-vue-ui/src/chat-message.vue, packages/typescript/ai-vue-ui/src/message-part.vue
Update toolResultRenderer/slot typings to accept ToolResultContent; add helpers to normalize image sources and render text/image parts in default renderers.
Core tests & metrics
packages/typescript/ai/tests/message-converters.test.ts, packages/typescript/ai-code-mode/models-eval/metrics.ts
Add test verifying multimodal tool content preserved in UI conversion; update metrics code to avoid parsing non-string tool-result content.
Changeset
.changeset/multimodal-tool-results.md
Add changeset documenting patch releases and behavior changes: preserving multimodal tool results, fixing tool result handling, updating client types and UI renderers.
sequenceDiagram
    participant Tool as Tool Execution
    participant Norm as Normalization
    participant Chat as Chat Logic
    participant Adapter as Provider Adapter
    participant Provider as OpenAI/Anthropic API

    Tool->>Norm: result (string | object | Array<ContentPart>)
    Norm->>Norm: normalizeToolResultContent(result)
    Norm-->>Chat: ToolResultContent (string | ContentPart[])
    Chat->>Chat: store/emit ToolResultContent in conversation & events
    Chat->>Adapter: send ModelMessage with multimodal content
    Adapter->>Adapter: map ContentPart[] via provider converters
    Adapter-->>Provider: send function_call_output / tool_result with structured output
    Provider->>Provider: process multimodal content (images/text)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through arrays and strings,

Saved images, text, and other things.
No more squashed JSON in a heap—
Multimodal treasures now can keep.
Hooray! I nibbled bugs to sleep.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main objective: preserving multimodal tool results instead of stringifying them, which is the core change throughout the codebase.
Description check ✅ Passed The PR description follows the template with checked boxes for the Release Impact checklist (changeset generated and code affects published packages), but the Contributing guide step is marked incomplete with an explanation that the guide doesn't exist.
Linked Issues check ✅ Passed All code changes directly address the three specific stringification points (#363): tool-calls.ts normalizes results, processor.ts uses parseToolResultValue, OpenAI/Anthropic adapters preserve content arrays, types updated to ToolResultContent, and UI renderers support multimodal content.
Out of Scope Changes check ✅ Passed All changes directly support preserving multimodal tool results: type updates for ToolResultContent, normalization/parsing helpers, adapter conversions, and UI renderers. No extraneous changes detected outside the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/typescript/ai-client/src/types.ts`:
- Around line 154-160: The change to ToolResultPart (type ToolResultPart) made
content a union (ToolResultContent = string | Array<ContentPart>) which breaks
UI consumers; revert content back to string to preserve backwards compatibility
and instead add a new optional field (e.g., contentParts?: Array<ContentPart>)
to carry structured parts, updating the ToolResultContent typedef if necessary
and leaving ToolResultState untouched; ensure the interface remains: content:
string, contentParts?: Array<ContentPart> so existing React/Vue renderers (which
expect part.content as string) keep working while new consumers can use
contentParts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 382c7b77-8ddf-4ff2-a42d-c1c2c4a90606

📥 Commits

Reviewing files that changed from the base of the PR and between 54abae0 and 0a6bba4.

📒 Files selected for processing (14)
  • .changeset/multimodal-tool-results.md
  • packages/typescript/ai-anthropic/src/adapters/text.ts
  • packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts
  • packages/typescript/ai-client/src/types.ts
  • packages/typescript/ai-code-mode/models-eval/metrics.ts
  • packages/typescript/ai-openai/src/adapters/text.ts
  • packages/typescript/ai-openai/tests/openai-adapter.test.ts
  • packages/typescript/ai/src/activities/chat/index.ts
  • packages/typescript/ai/src/activities/chat/messages.ts
  • packages/typescript/ai/src/activities/chat/stream/message-updaters.ts
  • packages/typescript/ai/src/activities/chat/stream/processor.ts
  • packages/typescript/ai/src/activities/chat/tools/tool-calls.ts
  • packages/typescript/ai/src/types.ts
  • packages/typescript/ai/tests/message-converters.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/typescript/ai-react-ui/src/chat-message.tsx (1)

69-89: Avoid silently dropping unsupported tool-result part types.

Line [87] returns null for unknown part types, so valid multimodal payloads outside text/image render as empty content. A minimal fallback improves debuggability and user feedback.

Suggested fallback
       default:
-        return null
+        return (
+          <div key={index} data-tool-result-part-type="unsupported">
+            Unsupported tool result part: {part.type}
+          </div>
+        )

Based on learnings: Maintain type safety through multimodal content support (image, audio, video, document) with model capability awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-react-ui/src/chat-message.tsx` around lines 69 - 89,
The renderer currently returns null for unknown part.type values inside
content.map, which silently drops supported multimodal parts; update the
fallback in the map (the switch default) to render a visible placeholder instead
of null — include part.type and basic info (e.g., a small div with a
data-tool-result-part-type attribute and minimal message or serialized metadata)
so developers/users see unsupported/unknown types; reference the content.map
callback and getContentPartSourceUrl usage for locating the switch and ensure
the new fallback preserves keys (index) and accessibility attributes.
packages/typescript/ai-vue-ui/src/message-part.vue (1)

152-165: Add a default branch for unsupported content-part types.

Current rendering only handles text and image; other multimodal parts disappear silently in default UI. A small fallback avoids invisible tool output.

Suggested fallback branch
         <template v-for="(contentPart, index) in part.content" :key="index">
           <div
             v-if="contentPart.type === 'text'"
             data-tool-result-part-type="text"
           >
             {{ contentPart.content }}
           </div>
           <img
             v-else-if="contentPart.type === 'image'"
             data-tool-result-part-type="image"
             :src="getContentPartSourceUrl(contentPart.source)"
             alt="Tool result"
           />
+          <div v-else data-tool-result-part-type="unsupported">
+            Unsupported tool result part: {{ contentPart.type }}
+          </div>
         </template>

Based on learnings: Maintain type safety through multimodal content support (image, audio, video, document) with model capability awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-vue-ui/src/message-part.vue` around lines 152 - 165,
The template currently only renders contentPart.type === 'text' and 'image' so
any other multimodal parts vanish; update the template in message-part.vue (the
v-for over part.content and the contentPart.type checks) to add a default v-else
branch that renders a visible fallback (e.g., a small placeholder showing the
contentPart.type and/or a safe stringify of contentPart) and optionally logs a
console.warn with the contentPart to aid debugging; keep use of
getContentPartSourceUrl for image branch unchanged and ensure the fallback is
small and clearly labeled so unsupported audio/video/document parts don't
disappear silently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/typescript/ai-react-ui/src/chat-message.tsx`:
- Around line 9-13: The ToolResultContentSource type allows a missing mimeType
even for data sources, which can produce invalid URIs like
data:undefined;base64,...; fix this by turning ToolResultContentSource into a
discriminated union where the branch with type: 'data' requires mimeType (e.g.,
{ type: 'data'; value: string; mimeType: string }) while the branch(s) for type:
'url' keep mimeType optional, then update any code that constructs data URIs
(references to ToolResultContentSource usages) to rely on the required mimeType
for the 'data' branch.

In `@packages/typescript/ai-solid-ui/src/chat-message.tsx`:
- Around line 10-14: ToolResultContentSource currently allows mimeType to be
undefined but code that renders tool results (e.g., the consumer that reads
source.mimeType in chat-message.tsx) expects mimeType when source.type ===
'data'; change ToolResultContentSource into a discriminated union: one variant
for { type: 'url'; value: string; mimeType?: string } and one for { type:
'data'; value: string; mimeType: string } so that mimeType is required for data
blobs, then update any callsites or tests that create a {type: 'data'} source to
include mimeType and adjust rendering logic to rely on the strengthened type.

In `@packages/typescript/ai-vue-ui/src/message-part.vue`:
- Around line 6-10: ToolResultContentSource allows mimeType to be optional even
when type === 'data', which can produce malformed data: URLs where
source.mimeType is missing; change the type to a discriminated union so the
'data' variant requires mimeType (e.g. { type: 'data', value: string, mimeType:
string } vs { type: 'url', value: string, mimeType?: string }) and update any
usages in message-part.vue that interpolate source.mimeType to rely on the
stronger type (or add a runtime guard) so you never build a data: URL without a
defined mimeType.

---

Nitpick comments:
In `@packages/typescript/ai-react-ui/src/chat-message.tsx`:
- Around line 69-89: The renderer currently returns null for unknown part.type
values inside content.map, which silently drops supported multimodal parts;
update the fallback in the map (the switch default) to render a visible
placeholder instead of null — include part.type and basic info (e.g., a small
div with a data-tool-result-part-type attribute and minimal message or
serialized metadata) so developers/users see unsupported/unknown types;
reference the content.map callback and getContentPartSourceUrl usage for
locating the switch and ensure the new fallback preserves keys (index) and
accessibility attributes.

In `@packages/typescript/ai-vue-ui/src/message-part.vue`:
- Around line 152-165: The template currently only renders contentPart.type ===
'text' and 'image' so any other multimodal parts vanish; update the template in
message-part.vue (the v-for over part.content and the contentPart.type checks)
to add a default v-else branch that renders a visible fallback (e.g., a small
placeholder showing the contentPart.type and/or a safe stringify of contentPart)
and optionally logs a console.warn with the contentPart to aid debugging; keep
use of getContentPartSourceUrl for image branch unchanged and ensure the
fallback is small and clearly labeled so unsupported audio/video/document parts
don't disappear silently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bf5ce1b-3477-42d5-aef2-685b572cfe5c

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6bba4 and 39eb727.

📒 Files selected for processing (6)
  • .changeset/multimodal-tool-results.md
  • packages/typescript/ai-react-ui/src/chat-message.tsx
  • packages/typescript/ai-solid-ui/src/chat-message.tsx
  • packages/typescript/ai-vue-ui/src/chat-message.vue
  • packages/typescript/ai-vue-ui/src/message-part.vue
  • packages/typescript/ai-vue-ui/src/types.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/typescript/ai-vue-ui/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/multimodal-tool-results.md

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/typescript/ai-react-ui/src/chat-message.tsx (1)

70-100: Consider more stable keys if content parts have unique identifiers.

Using array index as a React key works here since tool result content is static after being received and won't be reordered. However, if ContentPart objects ever include a unique identifier (like an id field), using that would provide more stable rendering behavior.

The rendering logic correctly handles the union type with a type guard for strings and a switch statement for structured parts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai-react-ui/src/chat-message.tsx` around lines 70 - 100,
The renderToolResultContent function uses array index as React keys; update it
to prefer a stable unique identifier when available by checking each ContentPart
for a unique field (e.g., part.id or part.key) and using that as the key in the
returned elements, falling back to index only when no identifier exists; ensure
the switch cases (text/image/default) and their JSX elements use this computed
key so rendering remains stable if ContentPart gains an id field in the future.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/typescript/ai-react-ui/src/chat-message.tsx`:
- Around line 70-100: The renderToolResultContent function uses array index as
React keys; update it to prefer a stable unique identifier when available by
checking each ContentPart for a unique field (e.g., part.id or part.key) and
using that as the key in the returned elements, falling back to index only when
no identifier exists; ensure the switch cases (text/image/default) and their JSX
elements use this computed key so rendering remains stable if ContentPart gains
an id field in the future.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f21ffe0f-9d75-47e4-b38e-23b9fe1e9269

📥 Commits

Reviewing files that changed from the base of the PR and between 39eb727 and 8233d38.

📒 Files selected for processing (3)
  • packages/typescript/ai-react-ui/src/chat-message.tsx
  • packages/typescript/ai-solid-ui/src/chat-message.tsx
  • packages/typescript/ai-vue-ui/src/message-part.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/typescript/ai-solid-ui/src/chat-message.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool results are always stringified, preventing multimodal (image) tool responses with OpenAI Responses API

1 participant