ag-ui-protocol / ag-ui-protocol/ag-ui

[Bug]: MESSAGES_SNAPSHOT keeps client order and appends re-keyed messages after the final answer

Đang mở
#2,707 1 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
Python
Star
15.9k
Fork
1.4k
Merge trung bình
1 ngày 17 giờ
Pull request đã merge (30 ngày)
163

Mô tả

### Pre-flight Checklist

- [x] I have searched [existing issues](https://github.com/ag-ui-protocol/ag-ui/issues) and this hasn't been reported yet.
- [x] I am using the **latest** version AG-UI.

### Describe the Bug

`defaultApplyEvents` merges a `MESSAGES_SNAPSHOT` by keeping the client's existing message order and appending every snapshot message it has not seen yet at the end. The snapshot's own order is never used.

This breaks as soon as a backend re-keys a message. A common case is a tool result: the client streams it under the id from `TOOL_CALL_RESULT`, the backend persists it under a canonical id, and the next snapshot carries the canonical copy. The client drops the streamed copy and appends the canonical copy after the final assistant answer. A later snapshot cannot repair the order because every id now survives and nothing is appended.

The same rule also lets a snapshot that repeats an id produce two messages, while `RUN_STARTED` input handling already guarantees that the reducer never duplicates messages by id.

`#2184` made activity all-or-nothing, but the trade-off noted there still stands: a backend that stores activity for one activity type cannot send it in a snapshot without deleting every other activity type the client still owns. The fix for the ordering bug is the place where that scope can be expressed, so I would like to handle both in one PR unless you prefer them split.

### Steps to Reproduce

1. Client messages: `user`, `assistant(tool call)`, `tool(id: result-browser)`, `assistant("Applied.")`.
2. Apply `MESSAGES_SNAPSHOT` with `user`, `assistant(tool call)`, `tool(id: result-canonical)`, `assistant("Applied.")`.
3. Read `agent.messages`.

```ts
const snapshot = [user, toolCall, { ...result, id: "result-canonical" }, answer];
const messages = await applySnapshot([user, toolCall, { ...result, id: "result-browser" }, answer], snapshot);
// messages.map(m => m.id) is ["u", "call", "answer", "result-canonical"]
```

The existing test `preserves activity position when a message ID changes in snapshot` encodes this: it expects `["m1", "asst-1", "act-1", "asst-2", "tool-canon"]`, with the renamed tool result after the final answer.

### Expected Behavior

The snapshot owns transcript order: `["u", "call", "result-canonical", "answer"]`, and applying the same snapshot again is a no-op.

Client-only messages the snapshot leaves out (activity and reasoning under the existing rules) keep their place relative to the messages around them: before the next surviving message, or after the last survivor when nothing after them survives.

A snapshot may declare which activity types it owns, so a producer that rebuilds one activity kind from durable data can replace that kind, including replacing it with nothing, while other producers' activity survives on the client.

### Environment

- `@ag-ui/client` 0.0.59 (`main` at 48ee0d56)
- Node 22, macOS

### Logs & Errors

_No response_

### Additional Context

I have a PR ready for this and will open it once the issue is triaged. @ag-ui-protocol/copilotkit could you assign it to me?

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.