agentscope-ai / agentscope-ai/agentscope
[Bug]: streamed tool data blocks lose identity during event replay
- 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
Assessment
This issue has not been assessed yet.