ag-ui-protocol / ag-ui-protocol/ag-ui
[Bug]: MESSAGES_SNAPSHOT keeps client order and appends re-keyed messages after the final answer
- 主要言語
- Python
- スター
- 15.9k
- フォーク
- 1.4k
- 平均マージ
- 1日 17時間
- マージ済み PR(30日)
- 163
説明
### 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?
コントリビューションガイド
評価
この issue はまだ評価されていません。