Skip to content

[Feat]: Add streaming-aware helper for artifact updates #833

@kevinlu310

Description

@kevinlu310

Problem

When implementing streaming text output via TaskArtifactUpdateEvent, the current new_text_artifact utility generates a fresh UUID for artifact_id on every call. This makes it impossible to use append=True correctly, since append semantics require a stable artifact_id across chunks.

The sample travel_planner_agent demonstrates this problem — it calls new_text_artifact in a loop:

async for event in self.agent.stream(query):
    message = TaskArtifactUpdateEvent(
        contextId=context.context_id,
        taskId=context.task_id,
        artifact=new_text_artifact(
            name='current_result',
            text=event['content'],
        ),
    )
    await event_queue.enqueue_event(message)

Each iteration produces a new artifact_id, so clients cannot merge chunks into a single artifact. This results in N separate artifact cards in the UI instead of one progressively streamed response.

Describe the solution you'd like

Add a stateful streaming helper to a2a.utils that:

  1. Generates a stable artifact_id once on construction
  2. Provides an append(text) method that returns a TaskArtifactUpdateEvent with append=True, last_chunk=False
  3. Provides a finalize() method that returns a TaskArtifactUpdateEvent with append=True, last_chunk=True

Example API:

from a2a.utils import ArtifactStreamer

streamer = ArtifactStreamer(context_id, task_id, name="response")

async for chunk in llm.stream(prompt):
    await event_queue.enqueue_event(streamer.append(chunk))

await event_queue.enqueue_event(streamer.finalize())

This would live in a2a/utils/artifact.py alongside the existing new_text_artifact and new_artifact helpers.

Describe alternatives you've considered

No response

Additional context

  • The travel_planner_agent sample should also be updated to use this helper once available.
  • The A2A spec defines append on TaskArtifactUpdateEvent as: "If true, the content of this artifact should be appended to a previously sent artifact with the same ID.". So the current new_text_artifact usage in a streaming loop is a misuse of the spec's intent.

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions