Skip to content

fix(ag-ui): preserve feedback metadata in AG-UI streams#1074

Open
omeraplak wants to merge 2 commits intomainfrom
fix/ag-ui-feedback-metadata-stream
Open

fix(ag-ui): preserve feedback metadata in AG-UI streams#1074
omeraplak wants to merge 2 commits intomainfrom
fix/ag-ui-feedback-metadata-stream

Conversation

@omeraplak
Copy link
Member

@omeraplak omeraplak commented Feb 17, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

Agent.streamText(...).toUIMessageStream() can emit message-metadata chunks (for example feedback metadata), but the AG-UI adapter drops unknown chunk types.

As a result, chat clients do not reliably receive assistant feedback metadata.

What is the new behavior?

  • @voltagent/ag-ui now maps message-metadata chunks to AG-UI CUSTOM events (voltagent.message_metadata), which is the protocol-native channel for app-specific metadata.
  • For backward compatibility, it also emits the existing internal tool-result marker so current clients keep working.
  • Internal metadata marker tool messages remain filtered out from subsequent model input.

fixes (issue)

N/A

Notes for reviewers

  • Scope intentionally limited to packages/ag-ui/src/voltagent-agent.ts.
  • Validation: pnpm -C packages/ag-ui exec tsc --noEmit.

Summary by CodeRabbit

  • Bug Fixes
    • Preserve assistant feedback metadata across streaming so feedback reliably reaches chat clients.
  • New Features
    • Streamed message metadata is now surfaced to clients as protocol-native events for app-level handling.
  • Chores
    • Maintain a legacy compatibility marker so existing clients continue to work; internal metadata markers are not replayed to the model.

@changeset-bot
Copy link

changeset-bot bot commented Feb 17, 2026

🦋 Changeset detected

Latest commit: 81c8de5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/ag-ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@joggrbot

This comment has been minimized.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Adds preservation of assistant feedback metadata across AG‑UI streams by mapping VoltAgent "message-metadata" stream parts to AG‑UI CUSTOM events and synthesizing legacy TOOL_CALL_RESULT events with a prefixed toolCallId; message-conversion skips internal metadata carrier tool calls.

Changes

Cohort / File(s) Summary
Changeset Documentation
\.changeset/wise-pans-arrive.md
Adds a patch changeset for @voltagent/ag-ui documenting the fix to preserve assistant feedback metadata across AG‑UI streams.
AG‑UI Voltagent Integration
packages/ag-ui/src/voltagent-agent.ts
Adds metadata prefix constant and isMetadataCarrierToolCallId; rewrites convertAGUIMessagesToVoltMessages to accumulate messages and skip metadata-carrier tool calls; extends stream conversion to handle message-metadata parts by emitting a voltagent.message_metadata CustomEvent and a backward-compatible TOOL_CALL_RESULT with a prefixed toolCallId containing the metadata; updates types to include CustomEvent in StreamConversionResult.

Sequence Diagram(s)

sequenceDiagram
    participant VoltAgent as VoltAgent Stream
    participant Converter as convertVoltStreamPartToEvents
    participant Emitter as Event Emitter
    participant AGUI as AG-UI Client

    VoltAgent->>Converter: "message-metadata" stream part
    activate Converter
    Converter->>Converter: parse metadata, build prefixed toolCallId
    Converter->>Emitter: emit CustomEvent (voltagent.message_metadata) with messageId + metadata
    Converter->>Emitter: emit legacy TOOL_CALL_RESULT (prefixed toolCallId + metadata)
    deactivate Converter
    Emitter->>AGUI: deliver events to client
    AGUI->>AGUI: update UI state / attach metadata to message
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I tugged at streams with careful paws,
Mapped metadata into eventful laws,
Prefixed IDs to keep the peace,
Old markers stay while carriers cease,
Hop—assistant feedback travels without pause.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: preserving feedback metadata in AG-UI streams through message-metadata chunk handling.
Description check ✅ Passed The description covers current behavior, new behavior, includes changeset confirmation, and provides clear notes for reviewers about scope and validation approach.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/ag-ui-feedback-metadata-stream

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
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

Copy link
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ag-ui/src/voltagent-agent.ts (1)

336-354: ⚠️ Potential issue | 🟡 Minor

Metadata carrier filtering is incomplete in the assistant tool-call loop.

Lines 337–339 correctly skip ToolMessage entries with metadata-carrier toolCallId values. However, the loop processing AssistantMessage.toolCalls (lines 308–317) does not filter such entries. While the current event-to-message flow does not populate metadata carriers into assistant toolCalls, a more defensive approach would filter them at this point to prevent issues if the message reconstruction pattern changes.

Consider filtering metadata-carrier entries in the assistant tool-call loop:

Proposed fix
       for (const call of msg.toolCalls ?? []) {
+        if (isMetadataCarrierToolCallId(call.id)) {
+          continue;
+        }
         const args = safelyParseJson(call.function.arguments);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ag-ui/src/voltagent-agent.ts` around lines 336 - 354, The assistant
tool-call processing loop must also skip metadata-carrier entries: when
iterating AssistantMessage.toolCalls, call
isMetadataCarrierToolCallId(toolCallId) and continue/skip those entries (the
same check used in the isToolMessage branch). Update the
AssistantMessage.toolCalls handling (the loop that reads each toolCall, uses
toolCallId and toolNameById) to perform this guard before converting/pushing the
tool-call into convertedMessages so metadata-carrier toolCallIds are not
included.
🧹 Nitpick comments (1)
packages/ag-ui/src/voltagent-agent.ts (1)

412-423: Null metadata silently falls through to the default case.

When partType === "message-metadata" but messageMetadata is falsy (line 421–423), null is returned. Then execution would not reach the switch (line 438) since line 422 returns. This is correct—no bug here. However, a debug log when metadata is unexpectedly missing could help with troubleshooting in production.

Proposed enhancement
     if (!messageMetadata || typeof messageMetadata !== "object") {
+      debugLog("message-metadata part has no valid metadata object", { messageId });
       return null;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ag-ui/src/voltagent-agent.ts` around lines 412 - 423, When partType
=== "message-metadata" and messageMetadata is falsy, add a debug log before
returning null to aid troubleshooting: log the messageId (computed from
messageId or fallbackMessageId), partType and any relevant part/payload
identifiers; use the module's existing logger (e.g., processLogger or logger) or
console.debug if none exists, then return null as before. This change touches
the block that computes messageId and messageMetadata (symbols: partType,
messageId, messageMetadata, fallbackMessageId).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/ag-ui/src/voltagent-agent.ts`:
- Around line 336-354: The assistant tool-call processing loop must also skip
metadata-carrier entries: when iterating AssistantMessage.toolCalls, call
isMetadataCarrierToolCallId(toolCallId) and continue/skip those entries (the
same check used in the isToolMessage branch). Update the
AssistantMessage.toolCalls handling (the loop that reads each toolCall, uses
toolCallId and toolNameById) to perform this guard before converting/pushing the
tool-call into convertedMessages so metadata-carrier toolCallIds are not
included.

---

Nitpick comments:
In `@packages/ag-ui/src/voltagent-agent.ts`:
- Around line 412-423: When partType === "message-metadata" and messageMetadata
is falsy, add a debug log before returning null to aid troubleshooting: log the
messageId (computed from messageId or fallbackMessageId), partType and any
relevant part/payload identifiers; use the module's existing logger (e.g.,
processLogger or logger) or console.debug if none exists, then return null as
before. This change touches the block that computes messageId and
messageMetadata (symbols: partType, messageId, messageMetadata,
fallbackMessageId).

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 17, 2026

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: 81c8de5
Status: ✅  Deploy successful!
Preview URL: https://45157141.voltagent.pages.dev
Branch Preview URL: https://fix-ag-ui-feedback-metadata.voltagent.pages.dev

View logs

Copy link
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/ag-ui/src/voltagent-agent.ts (1)

439-448: The backward-compat ToolCallResultEvent uses generateId() for messageId — consider using the actual messageId instead.

Line 446 assigns a fresh random ID as the messageId of the synthesized ToolCallResultEvent, while the actual message's messageId is already available and embedded in the toolCallId prefix and the content payload. Clients trying to correlate this event back to the originating assistant message won't be able to do so via messageId.

Was this intentional to avoid ID collisions, or should it mirror the source message's ID for traceability?

Suggested change for consideration
     const resultEvent: ToolCallResultEvent = {
       type: EventType.TOOL_CALL_RESULT,
       toolCallId: `${VOLTAGENT_METADATA_TOOL_CALL_ID_PREFIX}${messageId || generateId()}`,
       content: safeStringify({
         messageId: messageId || undefined,
         metadata: messageMetadata,
       }),
-      messageId: generateId(),
+      messageId: messageId || generateId(),
       role: "tool",
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ag-ui/src/voltagent-agent.ts` around lines 439 - 448, The
ToolCallResultEvent currently always sets messageId to generateId(), which
breaks traceability to the original assistant message; update the construction
in voltagent-agent.ts to reuse the originating messageId when present (e.g.,
compute a single id like const correlatedId = messageId || generateId() and use
correlatedId for both toolCallId suffix and the event's messageId) so
ToolCallResultEvent (type EventType.TOOL_CALL_RESULT) and its content/metadata
consistently reference the same message identifier (referencing
VOLTAGENT_METADATA_TOOL_CALL_ID_PREFIX, messageId, generateId(), and
ToolCallResultEvent).
🤖 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/ag-ui/src/voltagent-agent.ts`:
- Around line 439-448: The ToolCallResultEvent currently always sets messageId
to generateId(), which breaks traceability to the original assistant message;
update the construction in voltagent-agent.ts to reuse the originating messageId
when present (e.g., compute a single id like const correlatedId = messageId ||
generateId() and use correlatedId for both toolCallId suffix and the event's
messageId) so ToolCallResultEvent (type EventType.TOOL_CALL_RESULT) and its
content/metadata consistently reference the same message identifier (referencing
VOLTAGENT_METADATA_TOOL_CALL_ID_PREFIX, messageId, generateId(), and
ToolCallResultEvent).

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.

1 participant