Skip to content

Commit 3bb4b96

Browse files
committed
docs(migrations): document strict AgentExecutor streaming rules in v1.0 guide
1 parent 8a0f38d commit 3bb4b96

1 file changed

Lines changed: 126 additions & 22 deletions

File tree

docs/migrations/v1_0/README.md

Lines changed: 126 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ This documentation details the technical upgrades and architectural modification
1818
1. [Update Dependencies](#1-update-dependencies)
1919
2. [Types](#2-types)
2020
3. [Server: DefaultRequestHandler](#3-server-defaultrequesthandler)
21-
4. [Server: Application Setup](#4-server-application-setup)
22-
5. [Supporting v0.3 Clients](#5-supporting-v03-clients)
23-
6. [Client: Creating a Client](#6-client-creating-a-client)
24-
7. [Client: Send Message](#7-client-send-message)
25-
8. [Client: Push Notifications Config](#8-client-push-notifications-config)
26-
9. [Helper Utilities](#9-helper-utilities)
27-
10. [Summary of Key Changes](#10-summary-of-key-changes-in-v10)
28-
11. [Get Started](#11-get-started)
21+
4. [Server: AgentExecutor Streaming Rules](#4-server-agentexecutor-streaming-rules)
22+
5. [Server: Application Setup](#5-server-application-setup)
23+
6. [Supporting v0.3 Clients](#6-supporting-v03-clients)
24+
7. [Client: Creating a Client](#7-client-creating-a-client)
25+
8. [Client: Send Message](#8-client-send-message)
26+
9. [Client: Push Notifications Config](#9-client-push-notifications-config)
27+
10. [Helper Utilities](#10-helper-utilities)
28+
11. [Summary of Key Changes](#11-summary-of-key-changes-in-v10)
29+
12. [Get Started](#12-get-started)
2930

3031
---
3132

@@ -96,7 +97,7 @@ Constructing messages is simplified in v1.0. The old API required wrapping conte
9697
| Structured data | `Part(DataPart(data=..., ...))` | `Part(data=..., ...)` |
9798

9899
**Note**:
99-
* When using `File (bytes)` in v1.0, the data serialisatinon (via base64 encoding) is not required as A2A now uses Protobuf that automatically does it for you.
100+
* When using `File (bytes)` in v1.0, data serialization (via base64 encoding) is not required because A2A now uses Protobuf, which handles it automatically.
100101
* In v1.0, `Part.DataPart.data` is renamed to `Part.data` and is of type `google.protobuf.Value`. Use `ParseDict` to convert a Python dict into a suitable value. See the examples below for more details.
101102

102103
**Before (v0.3):**
@@ -175,7 +176,7 @@ message = Message(
175176
)
176177
```
177178

178-
For text-only messages, use the [A2A helper utilities](#9-helper-utilities) to reduce boilerplate:
179+
For text-only messages, use the [A2A helper utilities](#10-helper-utilities) to reduce boilerplate:
179180

180181
```python
181182
from a2a.helpers import new_text_message
@@ -191,7 +192,7 @@ message = new_text_message(text="What's the weather in Warsaw?", role=Role.ROLE_
191192
Key changes:
192193
- Added an `AgentInterface` class to support multiple transport bindings via the newly added `supported_interfaces` field in AgentCard.
193194
- The `url` parameter in `AgentCard` is removed and is now part of `AgentInterface`.
194-
- Accepted values for `AgentInterface.protocol_binding`: `'JSONRPC'`, `'HTTP+JSON'`, `'GRPC'`
195+
- Accepted values for `AgentInterface.protocol_binding`: `'JSONRPC'`, `'HTTP+JSON'`, `'GRPC'`.
195196
- The `AgentCard.supports_authenticated_extended_card` field is renamed to `AgentCapabilities.extended_agent_card`.
196197
- The `AgentCapabilities.input_modes` and `AgentCapabilities.output_modes` fields are removed; use `AgentCard.default_input_modes` and `AgentCard.default_output_modes` for card-level defaults, or `AgentSkill.input_modes` and `AgentSkill.output_modes` for per-skill overrides.
197198
- The `examples` parameter in `AgentCard` is removed and is now part of `AgentSkill`.
@@ -299,9 +300,111 @@ request_handler = DefaultRequestHandler(
299300
300301
---
301302

302-
## 4. Server: Application Setup
303+
## 4. Server: AgentExecutor Streaming Rules
303304

304-
The application wrapper classes (`A2AStarletteApplication`, `A2AFastApiApplication` and `A2ARESTFastApiApplication`) have been removed. The server setup now uses Starlette route factory functions directly, giving you better control over the routing, middleware, authentication, logging, and other aspects of the server.
305+
The server now strictly enforces the [A2A spec rules for `message/stream`](https://a2a-protocol.org/v1.0.0/specification/#312-send-streaming-message). Existing executors that mix message and task events, or emit task updates before the initial `Task`, will fail at runtime with `InvalidAgentResponseError`. See [PR #979](https://github.com/a2aproject/a2a-python/pull/979).
306+
307+
In v1.0, your `AgentExecutor` MUST follow exactly one of these two streaming patterns:
308+
309+
1. **Message-only stream** — enqueue exactly **one** `Message` and stop.
310+
2. **Task lifecycle stream** — enqueue a `Task` **first**, then zero or more `TaskStatusUpdateEvent` / `TaskArtifactUpdateEvent` objects until a terminal state is reached.
311+
312+
The following are now hard errors (each raises `InvalidAgentResponseError`):
313+
314+
| Violation | Error message |
315+
|---|---|
316+
| Enqueue a `Message` after a `Task` (mixing modes) | *Received Message object in task mode...* |
317+
| Enqueue more than one `Message` | *Multiple Message objects received.* |
318+
| Enqueue a `Task`/update event after a `Message` | *Received `<Type>` in message mode...* |
319+
| Enqueue a `TaskStatusUpdateEvent` before the initial `Task` | *Agent should enqueue Task before `<Type>` event* |
320+
321+
### Migration
322+
323+
**Before (v0.3 — silently tolerated):**
324+
```python
325+
from a2a.helpers import new_text_message
326+
from a2a.server.agent_execution import AgentExecutor
327+
from a2a.types import TaskStatusUpdateEvent
328+
329+
class MyExecutor(AgentExecutor):
330+
async def execute(self, context, event_queue):
331+
# Mixing Message and Task events — no longer allowed.
332+
await event_queue.enqueue_event(new_text_message('Working on it...'))
333+
await event_queue.enqueue_event(
334+
TaskStatusUpdateEvent(...) # ❌ raises InvalidAgentResponseError
335+
)
336+
```
337+
338+
**After (v1.0 — pick one pattern):**
339+
340+
```python
341+
from a2a.helpers import (
342+
new_task_from_user_message,
343+
new_text_artifact_update_event,
344+
new_text_message,
345+
new_text_status_update_event,
346+
)
347+
from a2a.server.agent_execution import AgentExecutor
348+
from a2a.types import Role, TaskState
349+
350+
# Pattern A: Message-only stream — one Message, then done.
351+
class GreetingExecutor(AgentExecutor):
352+
async def execute(self, context, event_queue):
353+
await event_queue.enqueue_event(
354+
new_text_message('Hello!', role=Role.ROLE_AGENT)
355+
)
356+
357+
# Pattern B: Task lifecycle stream — Task first, then updates.
358+
class WorkflowExecutor(AgentExecutor):
359+
def __init__(self, agent):
360+
self._agent = agent # Your underlying agent (LLM, tool, etc.)
361+
362+
async def execute(self, context, event_queue):
363+
task = context.current_task or new_task_from_user_message(context.message)
364+
await event_queue.enqueue_event(task) # ✅ Task MUST be first
365+
366+
await event_queue.enqueue_event(
367+
new_text_status_update_event(
368+
task_id=task.id,
369+
context_id=task.context_id,
370+
state=TaskState.TASK_STATE_WORKING,
371+
text='Processing...',
372+
)
373+
)
374+
375+
result = await self._agent.invoke(context.message)
376+
await event_queue.enqueue_event(
377+
new_text_artifact_update_event(
378+
task_id=task.id,
379+
context_id=task.context_id,
380+
name='result',
381+
text=result,
382+
)
383+
)
384+
385+
await event_queue.enqueue_event(
386+
new_text_status_update_event(
387+
task_id=task.id,
388+
context_id=task.context_id,
389+
state=TaskState.TASK_STATE_COMPLETED,
390+
text='Done!',
391+
)
392+
)
393+
```
394+
395+
**Quick checklist when migrating an executor:**
396+
- Decide upfront: is this a one-shot message reply, or a tracked task?
397+
- If task-based, always enqueue the `Task` object as the very first event.
398+
- Never mix `Message` events with `TaskStatusUpdateEvent` / `TaskArtifactUpdateEvent` in the same stream.
399+
- Send only one `Message` per stream when using the message-only pattern.
400+
401+
> **Example**: [`helloworld/agent_executor.py` in PR #474](https://github.com/a2aproject/a2a-samples/pull/474/files#diff-950e8baafcf17d50db5c10b525949407e129995df5295161fbf688e6374ad284)
402+
403+
---
404+
405+
## 5. Server: Application Setup
406+
407+
The application wrapper classes (`A2AStarletteApplication`, `A2AFastApiApplication`, and `A2ARESTFastApiApplication`) have been removed. The server setup now uses Starlette route factory functions directly, giving you better control over routing, middleware, authentication, logging, and other aspects of the server.
305408

306409
**Before (v0.3):**
307410
```python
@@ -366,9 +469,9 @@ uvicorn.run(app, host=host, port=port)
366469
367470
---
368471

369-
## 5. Supporting v0.3 Clients
472+
## 6. Supporting v0.3 Clients
370473

371-
If you cannot update all clients at once, you can run a v1.0 server that simultaneously accepts v0.3 connections. Two changes are needed.
474+
If you cannot update all clients at once, you can run a v1.0 server that also accepts v0.3 connections. Two changes are needed.
372475

373476
**1. Add the v0.3 AgentInterface to `supported_interfaces` in your `AgentCard`**:
374477

@@ -389,7 +492,7 @@ create_rest_routes(request_handler, enable_v0_3_compat=True)
389492
390493
---
391494

392-
## 6. Client: Creating a Client
495+
## 7. Client: Creating a Client
393496

394497
In `v1.0`, use the `a2a.client.create_client()` helper function to create a `Client` for the agent.
395498

@@ -423,7 +526,7 @@ client = await create_client(agent_card)
423526
424527
---
425528

426-
## 7. Client: Send Message
529+
## 8. Client: Send Message
427530

428531
The `BaseClient.send_message()` return type is standardized from `AsyncIterator[ClientEvent | Message]` to `AsyncIterator[StreamResponse]`.
429532

@@ -457,7 +560,7 @@ async for chunk in client.send_message(request):
457560

458561
---
459562

460-
## 8. Client: Push Notifications Config
563+
## 9. Client: Push Notifications Config
461564

462565
`ClientConfig.push_notification_config` is now **singular** (a single `TaskPushNotificationConfig` or `None`), not a list.
463566

@@ -478,7 +581,7 @@ config = ClientConfig(
478581

479582
---
480583

481-
## 9. Helper Utilities
584+
## 10. Helper Utilities
482585

483586
To improve the developer experience, we have consolidated helper functions into a single import. In v0.3, these helper functions were scattered across different modules. In v1.0, they are all available under `a2a.helpers`.
484587

@@ -525,19 +628,20 @@ print(text)
525628

526629
---
527630

528-
## 10. Summary of Key Changes in v1.0
631+
## 11. Summary of Key Changes in v1.0
529632

530633
- **Migration to Protobuf** — Core types have migrated from Pydantic models to Protobuf-based classes. Protobuf objects do not support arbitrary attribute assignment. Use `MessageToDict` from `google.protobuf.json_format` to convert objects to dictionaries, and `HasField('field_name')` to check for optional fields.
531634
- **Standardization to `SCREAMING_SNAKE_CASE`** — All enum values have been renamed from `snake_case` strings to `SCREAMING_SNAKE_CASE` for compliance with the ProtoJSON specification.
532635
- **`AgentCard`** — Significantly restructured to support multiple transport interfaces.
533636
- **`AgentInterface`** — The top-level `url` field is replaced by `supported_interfaces`, a list of `AgentInterface` objects. Each entry describes a single transport endpoint with fields for `protocol_binding`, `protocol_version`, and `url`.
534637
- **Input and output modes**`AgentCapabilities.input_modes` and `AgentCapabilities.output_modes` are removed and now live directly on `AgentCard` as `default_input_modes` and `default_output_modes`. Individual skills can override these with their own `input_modes` and `output_modes`.
535-
- **Application setup** — The wrapper classes (`A2AStarletteApplication`, `A2AFastApiApplication` and `A2ARESTFastApiApplication`) have been removed. Server setup now uses route factory functions — `create_jsonrpc_routes()`, `create_rest_routes()`, and `create_agent_card_routes()` — composed directly into a Starlette or FastAPI app.
638+
- **AgentExecutor streaming rules** — The server now strictly enforces the A2A spec: an executor must enqueue either a single `Message` or a `Task` followed by update events (with the `Task` first). Mixing modes, emitting multiple `Message`s, or sending updates before the initial `Task` raises `InvalidAgentResponseError`.
639+
- **Application setup** — The wrapper classes (`A2AStarletteApplication`, `A2AFastApiApplication`, and `A2ARESTFastApiApplication`) have been removed. Server setup now uses route factory functions — `create_jsonrpc_routes()`, `create_rest_routes()`, and `create_agent_card_routes()` — composed directly into a Starlette or FastAPI app.
536640
- **Helper utilities** — A new `a2a.helpers` module consolidates all helper functions under a single import, replacing the scattered `a2a.utils.*` modules and adding new helpers for constructing and reading v1.0 proto types.
537641

538642
---
539643

540-
## 11. Get Started
644+
## 12. Get Started
541645

542646
The fastest way to see v1.0 in action is to run the samples:
543647

0 commit comments

Comments
 (0)