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

Labels

component: coreIssues related to base data models, auth, gRPC interfaces, observability, and fundamental utilities.help wantedExtra attention is needed

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions