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

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

未关闭
#2,707 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
主要语言
Python
星标
15.9k
派生
1.4k
平均合并
1 天 17 小时
30 天内合并 PR
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 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。