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

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

Aberta
#2,707 1 comentário 0 reações 0 responsáveis Ver no GitHub
Linguagem predominante
Python
Estrelas
15.9k
Forks
1.4k
Merge médio
1d 17h
PRs com merge (30d)
163

Descrição

### 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?

Guia de contribuição

Abrir o guia de contribuição

Avaliação

Esta issue ainda não foi avaliada.

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.