[BUG] A2A artifacts parts are reassembled to corrupted text with extra whitespaces
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 58.8k
- Forks
- 8.5k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 109
Description
Description
When a remote A2A agent streams (or persists) its reply as more than one
text chunk, CrewAI's client-side reassembly corrupts the text: words get
split apart and extra spaces are inserted.
Per the A2A spec, artifact parts sent with append=True are meant to be
concatenated directly with no separator — that's the definition of
"append". CrewAI instead does:
# crewai/a2a/task_helpers.py, process_task_state()
response_text = " ".join(result_parts) if result_parts else ""
This function is shared by all three update mechanisms
(StreamingHandler, PollingHandler, PushNotificationHandler), so
switching updates= doesn't avoid it — and there's an identical
" ".join(result_parts) fallback inside StreamingHandler.execute()
itself (crewai/a2a/updates/streaming/handler.py), plus a third,
independent occurrence with the same pattern in
crewai/a2a/wrapper.py::_handle_max_turns_exceeded.
This affects any spec-compliant server that streams its reply
incrementally — e.g. any server relaying real token-by-token LLM output —
regardless of framework. It isn't specific to one server implementation.
Steps to Reproduce
"""Minimal, framework-agnostic reproduction of a CrewAI A2A bug.
Run:
pip install crewai a2a-sdk
python repro_a2a_streaming_join_bug.py
Expected: Hello, world!
Actual: Hel lo, wor ld !
"""
from __future__ import annotations
import asyncio
import uuid
from a2a.types import (
AgentCapabilities,
AgentCard,
Artifact,
Message,
Part,
Role,
Task,
TaskArtifactUpdateEvent,
TaskState,
TaskStatus,
TaskStatusUpdateEvent,
TextPart,
)
from crewai.a2a.updates.streaming.handler import StreamingHandler
TASK_ID = "task-1"
CONTEXT_ID = "ctx-1"
# Split "Hello, world!" into several small append=True chunks -- exactly
# what any server relaying incremental/token-level output would send.
CHUNKS = ["Hel", "lo, ", "wor", "ld", "!"]
EXPECTED = "".join(CHUNKS)
def _text_part(text: str) -> Part:
return Part(root=TextPart(text=text))
async def _fake_send_message(_message: Message):
"""Stand-in for `a2a.client.Client.send_message`, yielding (Task, update)
tuples the same way a real client does while consuming a server's SSE
stream."""
task = Task(id=TASK_ID, context_id=CONTEXT_ID, status=TaskStatus(state=TaskState.working))
for i, chunk in enumerate(CHUNKS):
artifact = Artifact(artifact_id="reply", parts=[_text_part(chunk)])
update = TaskArtifactUpdateEvent(
task_id=TASK_ID,
context_id=CONTEXT_ID,
artifact=artifact,
append=i > 0,
last_chunk=i == len(CHUNKS) - 1,
)
yield (task, update)
final_status = TaskStatusUpdateEvent(
task_id=TASK_ID,
context_id=CONTEXT_ID,
status=TaskStatus(state=TaskState.completed),
final=True,
)
yield (task, final_status)
class FakeClient:
def send_message(self, message: Message):
return _fake_send_message(message)
async def main() -> None:
agent_card = AgentCard(
name="Repro Agent",
description="Minimal agent card for reproduction purposes.",
url="http://localhost:9999",
version="1.0.0",
capabilities=AgentCapabilities(streaming=True),
default_input_modes=["text"],
default_output_modes=["text"],
skills=[],
)
message = Message(
role=Role.user,
parts=[_text_part("say hello")],
message_id=str(uuid.uuid4()),
)
result = await StreamingHandler.execute(
client=FakeClient(),
message=message,
new_messages=[],
agent_card=agent_card,
endpoint=agent_card.url,
)
actual = result["result"]
print(f"Expected: {EXPECTED!r}")
print(f"Actual: {actual!r}")
assert actual == EXPECTED, "BUG: chunks were joined with a space instead of concatenated"
if __name__ == "__main__":
asyncio.run(main())
Expected behavior
Output:
Expected: 'Hello, world!'
Actual: 'Hel lo, wor ld !'
Traceback (most recent call last):
...
AssertionError: BUG: chunks were joined with a space instead of concatenated
Screenshots/Code snippets
# crewai/a2a/task_helpers.py, process_task_state()
response_text = " ".join(result_parts) if result_parts else ""
Operating System
macOS Sonoma
Python Version
3.11
crewAI Version
1.15.21
crewAI Tools Version
n/a
Virtual Environment
Venv
Evidence
see above
Possible Solution
just reassemble with "".join instead of " ".join ?
Additional context
no
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Run the supplied minimal reproduction first, then inspect process_task_state() in crewai/a2a/task_helpers.py and the fallback joins in crewai/a2a/updates/streaming/handler.py and crewai/a2a/wrapper.py. Confirm that all three update paths preserve the A2A append semantics, and rerun the reproduction to verify the result is exactly 'Hello, world!' without inserted spaces.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100