From 36af709152ce86a2920edbd5618c2ce069a9445e Mon Sep 17 00:00:00 2001 From: sokoliva Date: Tue, 14 Apr 2026 13:44:11 +0000 Subject: [PATCH 1/6] fix(samples): emit `Task(TASK_STATE_SUBMITTED)` as first streaming event --- samples/README.md | 58 ++++++++++++++++++++++++++++++++++++ samples/cli.py | 58 +++++++++++++++++------------------- samples/hello_world_agent.py | 12 ++++++++ 3 files changed, 98 insertions(+), 30 deletions(-) create mode 100644 samples/README.md diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 000000000..e61264955 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,58 @@ +# A2A Python SDK — Samples + +This directory contains runnable examples demonstrating how to build and interact with an A2A-compliant agent using the Python SDK. + +## Contents + +| File | Role | Description | +|---|---|---| +| `hello_world_agent.py` | **Server** | A2A agent server | +| `cli.py` | **Client** | Interactive terminal client | + +The samples are designed to work together out of the box: the agent listens on `http://127.0.0.1:41241`, which is the default URL used by the client. +--- + +## `hello_world_agent.py` — Agent Server + +Implements an A2A agent that responds to simple greeting messages (e.g., "hello", "how are you", "bye") with text replies, simulating a 1-second processing delay. + +Demonstrates: +- Subclassing `AgentExecutor` and implementing `execute()` / `cancel()` +- Publishing streaming status updates and artifacts via `TaskUpdater` +- Exposing all three transports in both protocol versions (v1.0 and v0.3 compat) simultaneously: + - **JSON-RPC** (v1.0 and v0.3) at `http://127.0.0.1:41241/a2a/jsonrpc` + - **HTTP+JSON (REST)** (v1.0 and v0.3) at `http://127.0.0.1:41241/a2a/rest` + - **gRPC v1.0** on port `50051` + - **gRPC v0.3 (compat)** on port `50052` +- Serving the agent card at `http://127.0.0.1:41241/.well-known/agent-card.json` + +**Run:** + +```bash +uv run python samples/hello_world_agent.py +``` + +--- + +## `cli.py` — Client + +An interactive terminal client with full visibility into the streaming event flow. Each `TaskStatusUpdate` and `TaskArtifactUpdate` event is printed as it arrives. + +Features: +- Transport selection via `--transport` flag (`JSONRPC`, `HTTP+JSON`, `GRPC`) +- Session management (`context_id` persisted across messages, `task_id` per task) +- Graceful error handling for HTTP and gRPC failures + +**Run:** + +```bash +# Connect to the local hello_world_agent (default): +uv run python samples/cli.py + +# Connect to a different URL, using gRPC: +uv run python samples/cli.py --url http://192.168.1.10:41241 --transport GRPC +``` + +Then type a message like `hello` and press Enter. + +Type `/quit` or `/exit` to stop, or press `Ctrl+C`. diff --git a/samples/cli.py b/samples/cli.py index 8515fd5a9..455deef4b 100644 --- a/samples/cli.py +++ b/samples/cli.py @@ -16,39 +16,37 @@ async def _handle_stream( stream: Any, current_task_id: str | None ) -> str | None: - async for event, task in stream: - if not task: - continue + async for event in stream: if not current_task_id: - current_task_id = task.id - - if event: - if event.HasField('status_update'): - state_name = TaskState.Name(event.status_update.status.state) - print(f'TaskStatusUpdate [state={state_name}]:', end=' ') - if event.status_update.status.HasField('message'): - for part in event.status_update.status.message.parts: - if part.text: - print(part.text, end=' ') - print() - - if ( - event.status_update.status.state - == TaskState.TASK_STATE_COMPLETED - ): - current_task_id = None - print('--- Task Completed ---') - - elif event.HasField('artifact_update'): - print( - f'TaskArtifactUpdate [name={event.artifact_update.artifact.name}]:', - end=' ', - ) - for part in event.artifact_update.artifact.parts: + if event.HasField('task'): + current_task_id = event.task.id + print(f'Task [state={TaskState.Name(event.task.status.state)}]') + else: + raise ValueError('No task found in the first event') + + if event.HasField('status_update'): + state_name = TaskState.Name(event.status_update.status.state) + print(f'TaskStatusUpdate [state={state_name}]:', end=' ') + if event.status_update.status.HasField('message'): + for part in event.status_update.status.message.parts: if part.text: print(part.text, end=' ') - print() - + print() + if ( + event.status_update.status.state + == TaskState.TASK_STATE_COMPLETED + ): + current_task_id = None + print('--- Task Completed ---') + elif event.HasField('artifact_update'): + print( + f'TaskArtifactUpdate [name={event.artifact_update.artifact.name}]:', + end=' ', + ) + for part in event.artifact_update.artifact.parts: + if part.text: + print(part.text, end=' ') + print() return current_task_id diff --git a/samples/hello_world_agent.py b/samples/hello_world_agent.py index 8db34dc03..4c9e6f18a 100644 --- a/samples/hello_world_agent.py +++ b/samples/hello_world_agent.py @@ -27,6 +27,9 @@ AgentProvider, AgentSkill, Part, + Task, + TaskState, + TaskStatus, a2a_pb2_grpc, ) @@ -75,6 +78,15 @@ async def execute( context_id, ) + await event_queue.enqueue_event( + Task( + id=task_id, + context_id=context_id, + status=TaskStatus(state=TaskState.TASK_STATE_SUBMITTED), + history=[user_message], + ) + ) + updater = TaskUpdater( event_queue=event_queue, task_id=task_id, From 5fe9e163895d5be087beb3e6e457ab5db5fdc74c Mon Sep 17 00:00:00 2001 From: sokoliva Date: Tue, 14 Apr 2026 13:53:54 +0000 Subject: [PATCH 2/6] message handling --- samples/cli.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/samples/cli.py b/samples/cli.py index 455deef4b..d83eedfdd 100644 --- a/samples/cli.py +++ b/samples/cli.py @@ -17,12 +17,20 @@ async def _handle_stream( stream: Any, current_task_id: str | None ) -> str | None: async for event in stream: + if event.HasField('message'): + print('Message:', end=' ') + for part in event.message.parts: + if part.text: + print(part.text, end=' ') + print() + return current_task_id + if not current_task_id: if event.HasField('task'): current_task_id = event.task.id print(f'Task [state={TaskState.Name(event.task.status.state)}]') else: - raise ValueError('No task found in the first event') + raise ValueError(f'Unexpected first event: {event}') if event.HasField('status_update'): state_name = TaskState.Name(event.status_update.status.state) From 48c289ec30940c16dc619f22ed6884966775eac2 Mon Sep 17 00:00:00 2001 From: sokoliva Date: Tue, 14 Apr 2026 14:00:46 +0000 Subject: [PATCH 3/6] add other termination states --- samples/cli.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/samples/cli.py b/samples/cli.py index d83eedfdd..af07c77c9 100644 --- a/samples/cli.py +++ b/samples/cli.py @@ -13,7 +13,7 @@ from a2a.types import Message, Part, Role, SendMessageRequest, TaskState -async def _handle_stream( +async def _handle_stream( # noqa: PLR0912 stream: Any, current_task_id: str | None ) -> str | None: async for event in stream: @@ -40,12 +40,14 @@ async def _handle_stream( if part.text: print(part.text, end=' ') print() - if ( - event.status_update.status.state - == TaskState.TASK_STATE_COMPLETED + if state_name in ( + 'TASK_STATE_COMPLETED', + 'TASK_STATE_FAILED', + 'TASK_STATE_CANCELED', + 'TASK_STATE_REJECTED', ): current_task_id = None - print('--- Task Completed ---') + print(f'--- Task Finished ---') elif event.HasField('artifact_update'): print( f'TaskArtifactUpdate [name={event.artifact_update.artifact.name}]:', From 00361c1ef2d57410b474740abf495dcdaf743767 Mon Sep 17 00:00:00 2001 From: sokoliva Date: Tue, 14 Apr 2026 14:05:30 +0000 Subject: [PATCH 4/6] ruff check fix --- samples/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/cli.py b/samples/cli.py index af07c77c9..67f9a400a 100644 --- a/samples/cli.py +++ b/samples/cli.py @@ -47,7 +47,7 @@ async def _handle_stream( # noqa: PLR0912 'TASK_STATE_REJECTED', ): current_task_id = None - print(f'--- Task Finished ---') + print('--- Task Finished ---') elif event.HasField('artifact_update'): print( f'TaskArtifactUpdate [name={event.artifact_update.artifact.name}]:', From 7a41a0134c5cb3301634713e02455427f4b7287b Mon Sep 17 00:00:00 2001 From: sokoliva Date: Tue, 14 Apr 2026 14:07:15 +0000 Subject: [PATCH 5/6] add one more print --- samples/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/cli.py b/samples/cli.py index 67f9a400a..9036b538b 100644 --- a/samples/cli.py +++ b/samples/cli.py @@ -28,6 +28,7 @@ async def _handle_stream( # noqa: PLR0912 if not current_task_id: if event.HasField('task'): current_task_id = event.task.id + print('--- Task Started ---') print(f'Task [state={TaskState.Name(event.task.status.state)}]') else: raise ValueError(f'Unexpected first event: {event}') From 39c4ec7fbc5e9e32689fdd35d1e9efa2cd4d9ce1 Mon Sep 17 00:00:00 2001 From: sokoliva Date: Tue, 14 Apr 2026 14:21:42 +0000 Subject: [PATCH 6/6] When message return None --- samples/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/cli.py b/samples/cli.py index 9036b538b..7f72b5494 100644 --- a/samples/cli.py +++ b/samples/cli.py @@ -23,7 +23,7 @@ async def _handle_stream( # noqa: PLR0912 if part.text: print(part.text, end=' ') print() - return current_task_id + return None if not current_task_id: if event.HasField('task'):