agentscope-ai / agentscope-ai/agentscope

[Bug]: streamed tool data blocks lose identity during event replay

Open
#2,549 6 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
31.5k
Forks
3.5k
Avg merge
1d 23h
Merged PRs (30d)
95

Description

### Prerequisites

- [x] I searched existing issues, pull requests, discussions, and recent history for this symptom and root cause.
- [x] This is a reproducible bug rather than a usage question.

### Background / Description

A streaming tool can return multiple `DataBlock` chunks with the same block ID. `ToolChunk` explicitly uses that stable ID to group one multimodal result, and `ToolResponse.append_chunk` merges same-ID Base64 chunks by decoded bytes.

The Agent event path loses that identity. `_convert_tool_chunk_to_event` creates each `ToolResultDataDeltaEvent` without passing `block.id`, so every event receives a new generated `block_id`. `Msg.append_event` then appends every data delta as a separate `DataBlock`.

As a result, the canonical tool response / Agent context contains one complete block, while the persisted reply and SSE-reconstructed message contain multiple partial blocks with unrelated IDs. Audio, file, or image chunks can therefore be exposed as separate incomplete resources after persistence or replay.

### Reproduction

On current `main` at `f8b40f60`, use two independently Base64-encoded chunks with the same source block ID:

```python
chunks = [
DataBlock(
id="audio-1",
source=Base64Source(
data=base64.b64encode(payload).decode("ascii"),
media_type="audio/wav",
),
)
for payload in (b"hello", b"world")
]
```

Pass both through `Agent._convert_tool_chunk_to_event`, apply the resulting events to an `AssistantMsg` between `ToolResultStartEvent` and `ToolResultEndEvent`, and inspect the reconstructed tool result.

Observed before a fix:

```text
event block IDs: two different generated IDs
reconstructed block IDs: the same two generated IDs
reconstructed payloads: [b"hello", b"world"]
```

Expected, matching `ToolResponse.append_chunk`:

```text
event block IDs: ["audio-1", "audio-1"]
reconstructed block IDs: ["audio-1"]
reconstructed payloads: [b"helloworld"]
```

The regression test fails deterministically on current `main`; no timing or external service is involved.

### Root Cause

1. `Agent._convert_tool_chunk_to_event` omits `block_id=block.id` for both Base64 and URL `DataBlock` sources.
2. `Msg.append_event` handles every `TOOL_RESULT_DATA_DELTA` by appending a new block and has no same-ID Base64 accumulation path.

This violates the cross-layer invariant that replaying emitted events must reconstruct the same block boundaries, IDs, and bytes as the canonical `ToolResponse` stored in Agent context.

### Proposed Direction

- Preserve `DataBlock.id` when producing both Base64 and URL `ToolResultDataDeltaEvent` instances.
- When replaying a Base64 data delta into a tool result, merge it with an existing same-ID Base64 block by decoding both chunks, concatenating bytes, and re-encoding. This should mirror the behavior introduced for `ToolResponse.append_chunk` in #1901.
- Keep URL blocks one-shot and leave unrelated event behavior unchanged.
- Add regression coverage for same-ID padded Base64 chunks and URL ID preservation.

I have validated this focused direction locally with the new regression coverage plus `event_to_message_test.py` and `toolkit_test.py` (26 tests), and all file-level pre-commit hooks. I can prepare the focused PR after maintainer confirmation.

### Environment

- AgentScope Version: `2.0.8`
- Commit: `f8b40f60`
- Python Version: `3.11.16`
- OS: macOS

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.