From d2d09b823444468e450e81fe83fab49c3f5d8809 Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Fri, 22 Aug 2025 04:28:41 +0400 Subject: [PATCH 01/12] refactor: consolidate test utilities and improve test structure - Extract common test doubles into tests/test_doubles.py - Create builders module for test data construction - Add fixtures module for shared test fixtures - Refactor task manager tests to use shared utilities - Add __init__.py for proper test package structure --- tests/__init__.py | 0 tests/builders.py | 209 ++++++++++++ tests/fixtures.py | 119 +++++++ tests/server/tasks/test_task_manager.py | 409 ++++++++++++------------ tests/test_doubles.py | 219 +++++++++++++ 5 files changed, 749 insertions(+), 207 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/builders.py create mode 100644 tests/fixtures.py create mode 100644 tests/test_doubles.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/builders.py b/tests/builders.py new file mode 100644 index 000000000..598fb2798 --- /dev/null +++ b/tests/builders.py @@ -0,0 +1,209 @@ +from dataclasses import dataclass, field + +from a2a.types import ( + Artifact, + Message, + Part, + Role, + Task, + TaskArtifactUpdateEvent, + TaskState, + TaskStatus, + TaskStatusUpdateEvent, + TextPart, +) + + +@dataclass +class TaskBuilder: + id: str = 'task-default' + context_id: str = 'context-default' + state: TaskState = TaskState.submitted + kind: str = 'task' + artifacts: list = field(default_factory=list) + history: list = field(default_factory=list) + metadata: dict = field(default_factory=dict) + + def with_id(self, id: str) -> 'TaskBuilder': + self.id = id + return self + + def with_context_id(self, context_id: str) -> 'TaskBuilder': + self.context_id = context_id + return self + + def with_state(self, state: TaskState) -> 'TaskBuilder': + self.state = state + return self + + def with_metadata(self, **kwargs) -> 'TaskBuilder': + self.metadata.update(kwargs) + return self + + def with_history(self, *messages: Message) -> 'TaskBuilder': + self.history.extend(messages) + return self + + def with_artifacts(self, *artifacts: Artifact) -> 'TaskBuilder': + self.artifacts.extend(artifacts) + return self + + def build(self) -> Task: + return Task( + id=self.id, + context_id=self.context_id, + status=TaskStatus(state=self.state), + kind=self.kind, + artifacts=self.artifacts if self.artifacts else None, + history=self.history if self.history else None, + metadata=self.metadata if self.metadata else None, + ) + + +@dataclass +class MessageBuilder: + role: Role = Role.user + text: str = 'default message' + message_id: str = 'msg-default' + task_id: str | None = None + context_id: str | None = None + + def as_agent(self) -> 'MessageBuilder': + self.role = Role.agent + return self + + def as_user(self) -> 'MessageBuilder': + self.role = Role.user + return self + + def with_text(self, text: str) -> 'MessageBuilder': + self.text = text + return self + + def with_id(self, message_id: str) -> 'MessageBuilder': + self.message_id = message_id + return self + + def with_task_id(self, task_id: str) -> 'MessageBuilder': + self.task_id = task_id + return self + + def with_context_id(self, context_id: str) -> 'MessageBuilder': + self.context_id = context_id + return self + + def build(self) -> Message: + return Message( + role=self.role, + parts=[Part(TextPart(text=self.text))], + message_id=self.message_id, + task_id=self.task_id, + context_id=self.context_id, + ) + + +@dataclass +class ArtifactBuilder: + artifact_id: str = 'artifact-default' + name: str = 'default artifact' + text: str = 'default content' + description: str | None = None + + def with_id(self, artifact_id: str) -> 'ArtifactBuilder': + self.artifact_id = artifact_id + return self + + def with_name(self, name: str) -> 'ArtifactBuilder': + self.name = name + return self + + def with_text(self, text: str) -> 'ArtifactBuilder': + self.text = text + return self + + def with_description(self, description: str) -> 'ArtifactBuilder': + self.description = description + return self + + def build(self) -> Artifact: + return Artifact( + artifact_id=self.artifact_id, + name=self.name, + parts=[Part(TextPart(text=self.text))], + description=self.description, + ) + + +@dataclass +class StatusUpdateEventBuilder: + task_id: str = 'task-default' + context_id: str = 'context-default' + state: TaskState = TaskState.working + message: Message | None = None + final: bool = False + metadata: dict = field(default_factory=dict) + + def for_task(self, task_id: str) -> 'StatusUpdateEventBuilder': + self.task_id = task_id + return self + + def with_state(self, state: TaskState) -> 'StatusUpdateEventBuilder': + self.state = state + return self + + def with_message(self, message: Message) -> 'StatusUpdateEventBuilder': + self.message = message + return self + + def as_final(self) -> 'StatusUpdateEventBuilder': + self.final = True + return self + + def with_metadata(self, **kwargs) -> 'StatusUpdateEventBuilder': + self.metadata.update(kwargs) + return self + + def build(self) -> TaskStatusUpdateEvent: + return TaskStatusUpdateEvent( + task_id=self.task_id, + context_id=self.context_id, + status=TaskStatus(state=self.state, message=self.message), + final=self.final, + metadata=self.metadata if self.metadata else None, + ) + + +@dataclass +class ArtifactUpdateEventBuilder: + task_id: str = 'task-default' + context_id: str = 'context-default' + artifact: Artifact | None = None + append: bool = False + last_chunk: bool = False + + def for_task(self, task_id: str) -> 'ArtifactUpdateEventBuilder': + self.task_id = task_id + return self + + def with_artifact(self, artifact: Artifact) -> 'ArtifactUpdateEventBuilder': + self.artifact = artifact + return self + + def as_append(self) -> 'ArtifactUpdateEventBuilder': + self.append = True + return self + + def as_last_chunk(self) -> 'ArtifactUpdateEventBuilder': + self.last_chunk = True + return self + + def build(self) -> TaskArtifactUpdateEvent: + if not self.artifact: + self.artifact = ArtifactBuilder().build() + return TaskArtifactUpdateEvent( + task_id=self.task_id, + context_id=self.context_id, + artifact=self.artifact, + append=self.append, + last_chunk=self.last_chunk, + ) diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 000000000..2736bc4e8 --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,119 @@ +import pytest + +from builders import ( + ArtifactBuilder, + MessageBuilder, + TaskBuilder, +) +from test_doubles import ( + FakeHttpClient, + InMemoryTaskStore, + SpyEventQueue, + StubPushNotificationConfigStore, +) + +from a2a.server.tasks import TaskManager +from a2a.types import TaskState + + +@pytest.fixture +def task_store(): + return InMemoryTaskStore() + + +@pytest.fixture +def event_queue(): + return SpyEventQueue() + + +@pytest.fixture +def push_config_store(): + return StubPushNotificationConfigStore() + + +@pytest.fixture +def http_client(): + return FakeHttpClient() + + +@pytest.fixture +def task_builder(): + return TaskBuilder() + + +@pytest.fixture +def message_builder(): + return MessageBuilder() + + +@pytest.fixture +def artifact_builder(): + return ArtifactBuilder() + + +@pytest.fixture +def submitted_task(task_builder): + return task_builder.with_state(TaskState.submitted).build() + + +@pytest.fixture +def working_task(task_builder): + return task_builder.with_state(TaskState.working).build() + + +@pytest.fixture +def completed_task(task_builder): + return task_builder.with_state(TaskState.completed).build() + + +@pytest.fixture +def task_with_history(task_builder, message_builder): + messages = [ + message_builder.as_user().with_text('Hello').build(), + message_builder.as_agent().with_text('Hi there!').build(), + ] + return task_builder.with_history(*messages).build() + + +@pytest.fixture +def task_with_artifacts(task_builder, artifact_builder): + artifacts = [ + artifact_builder.with_id('art1').with_name('file.txt').build(), + artifact_builder.with_id('art2').with_name('data.json').build(), + ] + return task_builder.with_artifacts(*artifacts).build() + + +@pytest.fixture +def task_manager(task_store): + return TaskManager( + task_id='task-123', + context_id='context-456', + task_store=task_store, + initial_message=None, + ) + + +@pytest.fixture +def task_manager_factory(task_store): + def factory(task_id=None, context_id=None, initial_message=None): + return TaskManager( + task_id=task_id, + context_id=context_id, + task_store=task_store, + initial_message=initial_message, + ) + + return factory + + +@pytest.fixture +def populated_task_store(task_store, task_builder): + tasks = [ + task_builder.with_id('task-1').with_state(TaskState.submitted).build(), + task_builder.with_id('task-2').with_state(TaskState.working).build(), + task_builder.with_id('task-3').with_state(TaskState.completed).build(), + ] + for task in tasks: + task_store.set_task(task) + return task_store diff --git a/tests/server/tasks/test_task_manager.py b/tests/server/tasks/test_task_manager.py index 4f2431574..bb09603f7 100644 --- a/tests/server/tasks/test_task_manager.py +++ b/tests/server/tasks/test_task_manager.py @@ -1,217 +1,229 @@ +import sys + +from pathlib import Path from typing import Any -from unittest.mock import AsyncMock import pytest + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from builders import ( + ArtifactUpdateEventBuilder, + StatusUpdateEventBuilder, +) +from fixtures import * +from test_doubles import InMemoryTaskStore + from a2a.server.tasks import TaskManager from a2a.types import ( - Artifact, InvalidParamsError, - Message, - Part, - Role, - Task, - TaskArtifactUpdateEvent, TaskState, - TaskStatus, - TaskStatusUpdateEvent, - TextPart, ) from a2a.utils.errors import ServerError -MINIMAL_TASK: dict[str, Any] = { - 'id': 'task-abc', - 'context_id': 'session-xyz', - 'status': {'state': 'submitted'}, - 'kind': 'task', -} - - -@pytest.fixture -def mock_task_store() -> AsyncMock: - """Fixture for a mock TaskStore.""" - return AsyncMock() - - -@pytest.fixture -def task_manager(mock_task_store: AsyncMock) -> TaskManager: - """Fixture for a TaskManager with a mock TaskStore.""" - return TaskManager( - task_id=MINIMAL_TASK['id'], - context_id=MINIMAL_TASK['context_id'], - task_store=mock_task_store, - initial_message=None, - ) - - @pytest.mark.parametrize('invalid_task_id', ['', 123]) def test_task_manager_invalid_task_id( - mock_task_store: AsyncMock, invalid_task_id: Any + task_store: InMemoryTaskStore, invalid_task_id: Any ): - """Test that TaskManager raises ValueError for an invalid task_id.""" with pytest.raises(ValueError, match='Task ID must be a non-empty string'): TaskManager( task_id=invalid_task_id, context_id='test_context', - task_store=mock_task_store, + task_store=task_store, initial_message=None, ) @pytest.mark.asyncio async def test_get_task_existing( - task_manager: TaskManager, mock_task_store: AsyncMock -) -> None: - """Test getting an existing task.""" - expected_task = Task(**MINIMAL_TASK) - mock_task_store.get.return_value = expected_task + task_manager_factory, task_store: InMemoryTaskStore, submitted_task +): + task_manager = task_manager_factory( + task_id=submitted_task.id, context_id=submitted_task.context_id + ) + task_store.set_task(submitted_task) + retrieved_task = await task_manager.get_task() - assert retrieved_task == expected_task - mock_task_store.get.assert_called_once_with(MINIMAL_TASK['id']) + + assert retrieved_task == submitted_task + task_store.assert_get_called(times=1) @pytest.mark.asyncio async def test_get_task_nonexistent( - task_manager: TaskManager, mock_task_store: AsyncMock -) -> None: - """Test getting a nonexistent task.""" - mock_task_store.get.return_value = None + task_manager: TaskManager, task_store: InMemoryTaskStore +): retrieved_task = await task_manager.get_task() + assert retrieved_task is None - mock_task_store.get.assert_called_once_with(MINIMAL_TASK['id']) + task_store.assert_get_called(times=1) @pytest.mark.asyncio async def test_save_task_event_new_task( - task_manager: TaskManager, mock_task_store: AsyncMock -) -> None: - """Test saving a new task.""" - task = Task(**MINIMAL_TASK) + task_manager_factory, task_store: InMemoryTaskStore, task_builder +): + task = task_builder.with_id('task-abc').build() + task_manager = task_manager_factory(task_id=None, context_id=None) + await task_manager.save_task_event(task) - mock_task_store.save.assert_called_once_with(task) + + task_store.assert_save_called(times=1) + task_store.assert_saved(task.id) @pytest.mark.asyncio async def test_save_task_event_status_update( - task_manager: TaskManager, mock_task_store: AsyncMock -) -> None: - """Test saving a status update for an existing task.""" - initial_task = Task(**MINIMAL_TASK) - mock_task_store.get.return_value = initial_task - new_status = TaskStatus( - state=TaskState.working, - message=Message( - role=Role.agent, - parts=[Part(TextPart(text='content'))], - message_id='message-id', - ), + task_manager_factory, + task_store: InMemoryTaskStore, + task_builder, + message_builder, +): + initial_task = ( + task_builder.with_id('task-abc').with_context_id('context-xyz').build() ) - event = TaskStatusUpdateEvent( - task_id=MINIMAL_TASK['id'], - context_id=MINIMAL_TASK['context_id'], - status=new_status, - final=False, + task_store.set_task(initial_task) + task_manager = task_manager_factory( + task_id='task-abc', context_id='context-xyz' ) + + status_message = message_builder.as_agent().with_text('Working...').build() + event = ( + StatusUpdateEventBuilder() + .for_task('task-abc') + .with_state(TaskState.working) + .with_message(status_message) + .build() + ) + event.context_id = 'context-xyz' + await task_manager.save_task_event(event) - updated_task = initial_task - updated_task.status = new_status - mock_task_store.save.assert_called_once_with(updated_task) + + saved_task = task_store.get_saved_task('task-abc') + assert saved_task.status.state == TaskState.working + assert saved_task.status.message == status_message + assert ( + saved_task.history is None + ) # History contains previous messages, not current + task_store.assert_save_called(times=1) @pytest.mark.asyncio async def test_save_task_event_artifact_update( - task_manager: TaskManager, mock_task_store: AsyncMock -) -> None: - """Test saving an artifact update for an existing task.""" - initial_task = Task(**MINIMAL_TASK) - mock_task_store.get.return_value = initial_task - new_artifact = Artifact( - artifact_id='artifact-id', - name='artifact1', - parts=[Part(TextPart(text='content'))], + task_manager_factory, + task_store: InMemoryTaskStore, + task_builder, + artifact_builder, +): + initial_task = ( + task_builder.with_id('task-abc').with_context_id('context-xyz').build() + ) + task_store.set_task(initial_task) + task_manager = task_manager_factory( + task_id='task-abc', context_id='context-xyz' + ) + + new_artifact = ( + artifact_builder.with_id('artifact-id') + .with_name('artifact1') + .with_text('content') + .build() ) - event = TaskArtifactUpdateEvent( - task_id=MINIMAL_TASK['id'], - context_id=MINIMAL_TASK['context_id'], - artifact=new_artifact, + + event = ( + ArtifactUpdateEventBuilder() + .for_task('task-abc') + .with_artifact(new_artifact) + .build() ) + event.context_id = 'context-xyz' + await task_manager.save_task_event(event) - updated_task = initial_task - updated_task.artifacts = [new_artifact] - mock_task_store.save.assert_called_once_with(updated_task) + + saved_task = task_store.get_saved_task('task-abc') + assert saved_task.artifacts == [new_artifact] + task_store.assert_save_called(times=1) @pytest.mark.asyncio async def test_save_task_event_metadata_update( - task_manager: TaskManager, mock_task_store: AsyncMock -) -> None: - """Test saving an updated metadata for an existing task.""" - initial_task = Task(**MINIMAL_TASK) - mock_task_store.get.return_value = initial_task - new_metadata = {'meta_key_test': 'meta_value_test'} - - event = TaskStatusUpdateEvent( - task_id=MINIMAL_TASK['id'], - context_id=MINIMAL_TASK['context_id'], - metadata=new_metadata, - status=TaskStatus(state=TaskState.working), - final=False, + task_manager_factory, task_store: InMemoryTaskStore, task_builder +): + initial_task = ( + task_builder.with_id('task-abc').with_context_id('context-xyz').build() ) + task_store.set_task(initial_task) + task_manager = task_manager_factory( + task_id='task-abc', context_id='context-xyz' + ) + + event = ( + StatusUpdateEventBuilder() + .for_task('task-abc') + .with_state(TaskState.working) + .with_metadata(meta_key_test='meta_value_test') + .build() + ) + event.context_id = 'context-xyz' + await task_manager.save_task_event(event) - updated_task = mock_task_store.save.call_args.args[0] - assert updated_task.metadata == new_metadata + saved_task = task_store.get_saved_task('task-abc') + assert saved_task.metadata == {'meta_key_test': 'meta_value_test'} @pytest.mark.asyncio async def test_ensure_task_existing( - task_manager: TaskManager, mock_task_store: AsyncMock -) -> None: - """Test ensuring an existing task.""" - expected_task = Task(**MINIMAL_TASK) - mock_task_store.get.return_value = expected_task - event = TaskStatusUpdateEvent( - task_id=MINIMAL_TASK['id'], - context_id=MINIMAL_TASK['context_id'], - status=TaskStatus(state=TaskState.working), - final=False, + task_manager_factory, task_store: InMemoryTaskStore, submitted_task +): + task_store.set_task(submitted_task) + task_manager = task_manager_factory( + task_id=submitted_task.id, context_id=submitted_task.context_id ) + + event = ( + StatusUpdateEventBuilder() + .for_task(submitted_task.id) + .with_state(TaskState.working) + .build() + ) + retrieved_task = await task_manager.ensure_task(event) - assert retrieved_task == expected_task - mock_task_store.get.assert_called_once_with(MINIMAL_TASK['id']) + + assert retrieved_task.id == submitted_task.id + assert retrieved_task.status.state == submitted_task.status.state + task_store.assert_get_called(times=1) @pytest.mark.asyncio async def test_ensure_task_nonexistent( - mock_task_store: AsyncMock, -) -> None: - """Test ensuring a nonexistent task (creates a new one).""" - mock_task_store.get.return_value = None - task_manager_without_id = TaskManager( - task_id=None, - context_id=None, - task_store=mock_task_store, - initial_message=None, - ) - event = TaskStatusUpdateEvent( - task_id='new-task', - context_id='some-context', - status=TaskStatus(state=TaskState.submitted), - final=False, + task_store: InMemoryTaskStore, task_manager_factory +): + task_manager = task_manager_factory(task_id=None, context_id=None) + + event = ( + StatusUpdateEventBuilder() + .for_task('new-task') + .with_state(TaskState.submitted) + .build() ) - new_task = await task_manager_without_id.ensure_task(event) + event.context_id = 'some-context' + + new_task = await task_manager.ensure_task(event) + assert new_task.id == 'new-task' assert new_task.context_id == 'some-context' assert new_task.status.state == TaskState.submitted - mock_task_store.save.assert_called_once_with(new_task) - assert task_manager_without_id.task_id == 'new-task' - assert task_manager_without_id.context_id == 'some-context' + task_store.assert_save_called(times=1) + assert task_manager.task_id == 'new-task' + assert task_manager.context_id == 'some-context' + +def test_init_task_obj(task_manager: TaskManager): + new_task = task_manager._init_task_obj('new-task', 'new-context') -def test_init_task_obj(task_manager: TaskManager) -> None: - """Test initializing a new task object.""" - new_task = task_manager._init_task_obj('new-task', 'new-context') # type: ignore assert new_task.id == 'new-task' assert new_task.context_id == 'new-context' assert new_task.status.state == TaskState.submitted @@ -220,24 +232,20 @@ def test_init_task_obj(task_manager: TaskManager) -> None: @pytest.mark.asyncio async def test_save_task( - task_manager: TaskManager, mock_task_store: AsyncMock -) -> None: - """Test saving a task.""" - task = Task(**MINIMAL_TASK) - await task_manager._save_task(task) # type: ignore - mock_task_store.save.assert_called_once_with(task) + task_manager: TaskManager, task_store: InMemoryTaskStore, submitted_task +): + await task_manager._save_task(submitted_task) + + task_store.assert_save_called(times=1) + task_store.assert_saved(submitted_task.id) @pytest.mark.asyncio async def test_save_task_event_mismatched_id_raises_error( - task_manager: TaskManager, -) -> None: - """Test that save_task_event raises ServerError on task ID mismatch.""" - # The task_manager is initialized with 'task-abc' - mismatched_task = Task( - id='wrong-id', - context_id='session-xyz', - status=TaskStatus(state=TaskState.submitted), + task_manager: TaskManager, task_builder +): + mismatched_task = ( + task_builder.with_id('wrong-id').with_context_id('session-xyz').build() ) with pytest.raises(ServerError) as exc_info: @@ -247,71 +255,58 @@ async def test_save_task_event_mismatched_id_raises_error( @pytest.mark.asyncio async def test_save_task_event_new_task_no_task_id( - mock_task_store: AsyncMock, -) -> None: - """Test saving a task event without task id in TaskManager.""" - task_manager_without_id = TaskManager( - task_id=None, - context_id=None, - task_store=mock_task_store, - initial_message=None, + task_store: InMemoryTaskStore, task_manager_factory, task_builder +): + task_manager = task_manager_factory(task_id=None, context_id=None) + + task = ( + task_builder.with_id('new-task-id') + .with_context_id('some-context') + .with_state(TaskState.working) + .build() ) - task_data: dict[str, Any] = { - 'id': 'new-task-id', - 'context_id': 'some-context', - 'status': {'state': 'working'}, - 'kind': 'task', - } - task = Task(**task_data) - await task_manager_without_id.save_task_event(task) - mock_task_store.save.assert_called_once_with(task) - assert task_manager_without_id.task_id == 'new-task-id' - assert task_manager_without_id.context_id == 'some-context' - # initial submit should be updated to working + + await task_manager.save_task_event(task) + + task_store.assert_save_called(times=1) + task_store.assert_saved(task.id) + assert task_manager.task_id == 'new-task-id' + assert task_manager.context_id == 'some-context' assert task.status.state == TaskState.working @pytest.mark.asyncio async def test_get_task_no_task_id( - mock_task_store: AsyncMock, -) -> None: - """Test getting a task when task_id is not set in TaskManager.""" - task_manager_without_id = TaskManager( - task_id=None, - context_id='some-context', - task_store=mock_task_store, - initial_message=None, - ) - retrieved_task = await task_manager_without_id.get_task() + task_store: InMemoryTaskStore, task_manager_factory +): + task_manager = task_manager_factory(task_id=None, context_id='some-context') + + retrieved_task = await task_manager.get_task() + assert retrieved_task is None - mock_task_store.get.assert_not_called() + task_store.assert_get_called(times=0) @pytest.mark.asyncio async def test_save_task_event_no_task_existing( - mock_task_store: AsyncMock, -) -> None: - """Test saving an event when no task exists and task_id is not set.""" - task_manager_without_id = TaskManager( - task_id=None, - context_id=None, - task_store=mock_task_store, - initial_message=None, - ) - mock_task_store.get.return_value = None - event = TaskStatusUpdateEvent( - task_id='event-task-id', - context_id='some-context', - status=TaskStatus(state=TaskState.completed), - final=True, + task_store: InMemoryTaskStore, task_manager_factory +): + task_manager = task_manager_factory(task_id=None, context_id=None) + + event = ( + StatusUpdateEventBuilder() + .for_task('event-task-id') + .with_state(TaskState.completed) + .as_final() + .build() ) - await task_manager_without_id.save_task_event(event) - # Check if a new task was created and saved - call_args = mock_task_store.save.call_args - assert call_args is not None - saved_task = call_args[0][0] + event.context_id = 'some-context' + + await task_manager.save_task_event(event) + + saved_task = task_store.get_saved_task('event-task-id') assert saved_task.id == 'event-task-id' assert saved_task.context_id == 'some-context' assert saved_task.status.state == TaskState.completed - assert task_manager_without_id.task_id == 'event-task-id' - assert task_manager_without_id.context_id == 'some-context' + assert task_manager.task_id == 'event-task-id' + assert task_manager.context_id == 'some-context' diff --git a/tests/test_doubles.py b/tests/test_doubles.py new file mode 100644 index 000000000..8a6cd48e2 --- /dev/null +++ b/tests/test_doubles.py @@ -0,0 +1,219 @@ +from collections import defaultdict +from typing import Any + +from a2a.server.events.event_queue import Event, EventQueue +from a2a.server.tasks import TaskStore +from a2a.server.tasks.push_notification_config_store import ( + PushNotificationConfigStore, +) +from a2a.types import PushNotificationConfig, Task + + +class InMemoryTaskStore(TaskStore): + def __init__(self): + self._tasks: dict[str, Task] = {} + self._save_count = 0 + self._get_count = 0 + self._delete_count = 0 + + async def save(self, task: Task) -> None: + self._save_count += 1 + self._tasks[task.id] = task + + async def get(self, task_id: str) -> Task | None: + self._get_count += 1 + return self._tasks.get(task_id) + + async def delete(self, task_id: str) -> None: + self._delete_count += 1 + self._tasks.pop(task_id, None) + + def assert_saved(self, task_id: str) -> None: + assert task_id in self._tasks, f'Task {task_id} was not saved' + + def assert_not_saved(self, task_id: str) -> None: + assert task_id not in self._tasks, f'Task {task_id} should not be saved' + + def assert_save_called(self, times: int = 1) -> None: + assert self._save_count == times, ( + f'Expected save to be called {times} times, but was called {self._save_count} times' + ) + + def assert_get_called(self, times: int = 1) -> None: + assert self._get_count == times, ( + f'Expected get to be called {times} times, but was called {self._get_count} times' + ) + + def assert_delete_called(self, times: int = 1) -> None: + assert self._delete_count == times, ( + f'Expected delete to be called {times} times, but was called {self._delete_count} times' + ) + + def get_saved_task(self, task_id: str) -> Task: + assert task_id in self._tasks, f'Task {task_id} not found' + return self._tasks[task_id] + + def set_task(self, task: Task) -> None: + self._tasks[task.id] = task + + def clear(self) -> None: + self._tasks.clear() + self._save_count = 0 + self._get_count = 0 + self._delete_count = 0 + + +class SpyEventQueue(EventQueue): + def __init__(self): + self.events: list[Event] = [] + self._closed = False + + async def publish(self, event: Event) -> None: + if self._closed: + raise RuntimeError('Cannot publish to closed queue') + self.events.append(event) + + async def close(self) -> None: + self._closed = True + + def is_closed(self) -> bool: + return self._closed + + def assert_event_published(self, event_type: type) -> None: + assert any(isinstance(e, event_type) for e in self.events), ( + f'No event of type {event_type.__name__} was published' + ) + + def assert_no_event_published(self, event_type: type) -> None: + assert not any(isinstance(e, event_type) for e in self.events), ( + f'Event of type {event_type.__name__} should not have been published' + ) + + def assert_event_count(self, count: int) -> None: + assert len(self.events) == count, ( + f'Expected {count} events, but got {len(self.events)}' + ) + + def get_events_of_type(self, event_type: type) -> list[Event]: + return [e for e in self.events if isinstance(e, event_type)] + + def get_last_event(self) -> Event | None: + return self.events[-1] if self.events else None + + def clear(self) -> None: + self.events.clear() + self._closed = False + + +class StubPushNotificationConfigStore(PushNotificationConfigStore): + def __init__(self): + self._configs: dict[str, list[PushNotificationConfig]] = defaultdict( + list + ) + self._set_count = 0 + self._get_count = 0 + self._delete_count = 0 + + async def set_info( + self, task_id: str, config: PushNotificationConfig + ) -> None: + self._set_count += 1 + configs = self._configs[task_id] + if config.id: + configs = [c for c in configs if c.id != config.id] + configs.append(config) + self._configs[task_id] = configs + + async def get_info(self, task_id: str) -> list[PushNotificationConfig]: + self._get_count += 1 + return self._configs.get(task_id, []) + + async def delete_info( + self, task_id: str, config_id: str | None = None + ) -> None: + self._delete_count += 1 + if config_id: + self._configs[task_id] = [ + c for c in self._configs.get(task_id, []) if c.id != config_id + ] + else: + self._configs.pop(task_id, None) + + def assert_config_set(self, task_id: str) -> None: + assert task_id in self._configs, f'No config set for task {task_id}' + + def assert_set_called(self, times: int = 1) -> None: + assert self._set_count == times, ( + f'Expected set_info to be called {times} times, but was called {self._set_count} times' + ) + + def get_config(self, task_id: str) -> PushNotificationConfig | None: + configs = self._configs.get(task_id, []) + return configs[0] if configs else None + + def clear(self) -> None: + self._configs.clear() + self._set_count = 0 + self._get_count = 0 + self._delete_count = 0 + + +class FakeHttpClient: + def __init__(self): + self.requests: list[dict[str, Any]] = [] + self.responses: list[dict[str, Any]] = [] + self._response_index = 0 + + def add_response( + self, + status: int, + json: dict | None = None, + text: str | None = None, + ): + self.responses.append({'status': status, 'json': json, 'text': text}) + + async def post(self, url: str, **kwargs): + self.requests.append({'method': 'POST', 'url': url, **kwargs}) + + if self._response_index < len(self.responses): + response = self.responses[self._response_index] + self._response_index += 1 + return FakeResponse( + response['status'], response.get('json'), response.get('text') + ) + + return FakeResponse(200, {}) + + def assert_request_made(self, url: str, method: str = 'POST') -> None: + assert any( + r['url'] == url and r.get('method', 'POST') == method + for r in self.requests + ), f'No {method} request made to {url}' + + def get_last_request(self) -> dict[str, Any] | None: + return self.requests[-1] if self.requests else None + + +class FakeResponse: + def __init__( + self, + status_code: int, + json_data: dict | None = None, + text_data: str | None = None, + ): + self.status_code = status_code + self._json = json_data + self._text = text_data or '' + + def json(self): + if self._json is None: + raise ValueError('No JSON data') + return self._json + + @property + def text(self): + return self._text + + def raise_for_status(self): + if self.status_code >= 400: + raise Exception(f'HTTP {self.status_code}') From ee76e99754fbdc09881a6064b418840cd68354fe Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Sat, 23 Aug 2025 11:53:58 -0400 Subject: [PATCH 02/12] mr comments --- tests/builders.py | 9 ++--- tests/fixtures.py | 9 +++-- tests/server/tasks/test_task_manager.py | 45 ++++++++++++++++++------- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/tests/builders.py b/tests/builders.py index 598fb2798..1aac65f2f 100644 --- a/tests/builders.py +++ b/tests/builders.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from typing import Any from a2a.types import ( Artifact, @@ -20,9 +21,9 @@ class TaskBuilder: context_id: str = 'context-default' state: TaskState = TaskState.submitted kind: str = 'task' - artifacts: list = field(default_factory=list) - history: list = field(default_factory=list) - metadata: dict = field(default_factory=dict) + artifacts: list[Artifact] = field(default_factory=list) + history: list[Message] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) def with_id(self, id: str) -> 'TaskBuilder': self.id = id @@ -141,7 +142,7 @@ class StatusUpdateEventBuilder: state: TaskState = TaskState.working message: Message | None = None final: bool = False - metadata: dict = field(default_factory=dict) + metadata: dict[str, Any] = field(default_factory=dict) def for_task(self, task_id: str) -> 'StatusUpdateEventBuilder': self.task_id = task_id diff --git a/tests/fixtures.py b/tests/fixtures.py index 2736bc4e8..1ef618ef6 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,20 +1,19 @@ import pytest -from builders import ( +from a2a.server.tasks import TaskManager +from a2a.types import TaskState +from tests.builders import ( ArtifactBuilder, MessageBuilder, TaskBuilder, ) -from test_doubles import ( +from tests.test_doubles import ( FakeHttpClient, InMemoryTaskStore, SpyEventQueue, StubPushNotificationConfigStore, ) -from a2a.server.tasks import TaskManager -from a2a.types import TaskState - @pytest.fixture def task_store(): diff --git a/tests/server/tasks/test_task_manager.py b/tests/server/tasks/test_task_manager.py index bb09603f7..fd6194cdb 100644 --- a/tests/server/tasks/test_task_manager.py +++ b/tests/server/tasks/test_task_manager.py @@ -1,32 +1,37 @@ -import sys - -from pathlib import Path from typing import Any import pytest - -sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - -from builders import ( - ArtifactUpdateEventBuilder, - StatusUpdateEventBuilder, -) -from fixtures import * -from test_doubles import InMemoryTaskStore - from a2a.server.tasks import TaskManager from a2a.types import ( InvalidParamsError, TaskState, ) from a2a.utils.errors import ServerError +from tests.builders import ( + ArtifactUpdateEventBuilder, + StatusUpdateEventBuilder, +) +from tests.fixtures import ( + artifact_builder, + event_queue, + http_client, + message_builder, + push_config_store, + submitted_task, + task_builder, + task_manager, + task_manager_factory, + task_store, +) +from tests.test_doubles import InMemoryTaskStore @pytest.mark.parametrize('invalid_task_id', ['', 123]) def test_task_manager_invalid_task_id( task_store: InMemoryTaskStore, invalid_task_id: Any ): + """Test that TaskManager raises ValueError for an invalid task_id.""" with pytest.raises(ValueError, match='Task ID must be a non-empty string'): TaskManager( task_id=invalid_task_id, @@ -40,6 +45,7 @@ def test_task_manager_invalid_task_id( async def test_get_task_existing( task_manager_factory, task_store: InMemoryTaskStore, submitted_task ): + """Test retrieving an existing task from the task store.""" task_manager = task_manager_factory( task_id=submitted_task.id, context_id=submitted_task.context_id ) @@ -55,6 +61,7 @@ async def test_get_task_existing( async def test_get_task_nonexistent( task_manager: TaskManager, task_store: InMemoryTaskStore ): + """Test retrieving a non-existent task returns None.""" retrieved_task = await task_manager.get_task() assert retrieved_task is None @@ -65,6 +72,7 @@ async def test_get_task_nonexistent( async def test_save_task_event_new_task( task_manager_factory, task_store: InMemoryTaskStore, task_builder ): + """Test saving a new task event to the task store.""" task = task_builder.with_id('task-abc').build() task_manager = task_manager_factory(task_id=None, context_id=None) @@ -81,6 +89,7 @@ async def test_save_task_event_status_update( task_builder, message_builder, ): + """Test saving a status update event for an existing task.""" initial_task = ( task_builder.with_id('task-abc').with_context_id('context-xyz').build() ) @@ -117,6 +126,7 @@ async def test_save_task_event_artifact_update( task_builder, artifact_builder, ): + """Test saving an artifact update event for an existing task.""" initial_task = ( task_builder.with_id('task-abc').with_context_id('context-xyz').build() ) @@ -151,6 +161,7 @@ async def test_save_task_event_artifact_update( async def test_save_task_event_metadata_update( task_manager_factory, task_store: InMemoryTaskStore, task_builder ): + """Test saving a metadata update event for an existing task.""" initial_task = ( task_builder.with_id('task-abc').with_context_id('context-xyz').build() ) @@ -178,6 +189,7 @@ async def test_save_task_event_metadata_update( async def test_ensure_task_existing( task_manager_factory, task_store: InMemoryTaskStore, submitted_task ): + """Test ensuring a task that already exists in the store.""" task_store.set_task(submitted_task) task_manager = task_manager_factory( task_id=submitted_task.id, context_id=submitted_task.context_id @@ -201,6 +213,7 @@ async def test_ensure_task_existing( async def test_ensure_task_nonexistent( task_store: InMemoryTaskStore, task_manager_factory ): + """Test ensuring a task that does not exist creates a new one.""" task_manager = task_manager_factory(task_id=None, context_id=None) event = ( @@ -222,6 +235,7 @@ async def test_ensure_task_nonexistent( def test_init_task_obj(task_manager: TaskManager): + """Test initializing a new task object with default values.""" new_task = task_manager._init_task_obj('new-task', 'new-context') assert new_task.id == 'new-task' @@ -234,6 +248,7 @@ def test_init_task_obj(task_manager: TaskManager): async def test_save_task( task_manager: TaskManager, task_store: InMemoryTaskStore, submitted_task ): + """Test saving a task directly to the task store.""" await task_manager._save_task(submitted_task) task_store.assert_save_called(times=1) @@ -244,6 +259,7 @@ async def test_save_task( async def test_save_task_event_mismatched_id_raises_error( task_manager: TaskManager, task_builder ): + """Test that saving a task with mismatched ID raises an error.""" mismatched_task = ( task_builder.with_id('wrong-id').with_context_id('session-xyz').build() ) @@ -257,6 +273,7 @@ async def test_save_task_event_mismatched_id_raises_error( async def test_save_task_event_new_task_no_task_id( task_store: InMemoryTaskStore, task_manager_factory, task_builder ): + """Test saving a new task event when task manager has no task_id.""" task_manager = task_manager_factory(task_id=None, context_id=None) task = ( @@ -279,6 +296,7 @@ async def test_save_task_event_new_task_no_task_id( async def test_get_task_no_task_id( task_store: InMemoryTaskStore, task_manager_factory ): + """Test get_task returns None when task manager has no task_id.""" task_manager = task_manager_factory(task_id=None, context_id='some-context') retrieved_task = await task_manager.get_task() @@ -291,6 +309,7 @@ async def test_get_task_no_task_id( async def test_save_task_event_no_task_existing( task_store: InMemoryTaskStore, task_manager_factory ): + """Test saving an event when no task exists creates a new task.""" task_manager = task_manager_factory(task_id=None, context_id=None) event = ( From 9c976f6623b43aaf5caebf92d9aefe96c92a284c Mon Sep 17 00:00:00 2001 From: "Agent2Agent (A2A) Bot" Date: Fri, 22 Aug 2025 18:58:02 +0100 Subject: [PATCH 03/12] revert: Revert "chore(gRPC): Update a2a.proto to include metadata on GetTaskRequest" (#428) Commit: https://github.com/a2aproject/A2A/commit/e6b8c654a86a6ee461bb5c7be5d5b81004b80a92 --------- Co-authored-by: Holt Skinner --- src/a2a/grpc/a2a_pb2.py | 58 ++++++++++++++++++------------------ src/a2a/grpc/a2a_pb2.pyi | 6 ++-- src/a2a/utils/proto_utils.py | 2 +- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/a2a/grpc/a2a_pb2.py b/src/a2a/grpc/a2a_pb2.py index a149ddd6a..ac42838ac 100644 --- a/src/a2a/grpc/a2a_pb2.py +++ b/src/a2a/grpc/a2a_pb2.py @@ -30,7 +30,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ta2a.proto\x12\x06\x61\x32\x61.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xde\x01\n\x18SendMessageConfiguration\x12\x32\n\x15\x61\x63\x63\x65pted_output_modes\x18\x01 \x03(\tR\x13\x61\x63\x63\x65ptedOutputModes\x12K\n\x11push_notification\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.PushNotificationConfigR\x10pushNotification\x12%\n\x0ehistory_length\x18\x03 \x01(\x05R\rhistoryLength\x12\x1a\n\x08\x62locking\x18\x04 \x01(\x08R\x08\x62locking\"\xf1\x01\n\x04Task\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12*\n\x06status\x18\x03 \x01(\x0b\x32\x12.a2a.v1.TaskStatusR\x06status\x12.\n\tartifacts\x18\x04 \x03(\x0b\x32\x10.a2a.v1.ArtifactR\tartifacts\x12)\n\x07history\x18\x05 \x03(\x0b\x32\x0f.a2a.v1.MessageR\x07history\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\x99\x01\n\nTaskStatus\x12\'\n\x05state\x18\x01 \x01(\x0e\x32\x11.a2a.v1.TaskStateR\x05state\x12(\n\x06update\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageR\x07message\x12\x38\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\ttimestamp\"t\n\x04Part\x12\x14\n\x04text\x18\x01 \x01(\tH\x00R\x04text\x12&\n\x04\x66ile\x18\x02 \x01(\x0b\x32\x10.a2a.v1.FilePartH\x00R\x04\x66ile\x12&\n\x04\x64\x61ta\x18\x03 \x01(\x0b\x32\x10.a2a.v1.DataPartH\x00R\x04\x64\x61taB\x06\n\x04part\"\x93\x01\n\x08\x46ilePart\x12$\n\rfile_with_uri\x18\x01 \x01(\tH\x00R\x0b\x66ileWithUri\x12(\n\x0f\x66ile_with_bytes\x18\x02 \x01(\x0cH\x00R\rfileWithBytes\x12\x1b\n\tmime_type\x18\x03 \x01(\tR\x08mimeType\x12\x12\n\x04name\x18\x04 \x01(\tR\x04nameB\x06\n\x04\x66ile\"7\n\x08\x44\x61taPart\x12+\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructR\x04\x64\x61ta\"\xff\x01\n\x07Message\x12\x1d\n\nmessage_id\x18\x01 \x01(\tR\tmessageId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12\x17\n\x07task_id\x18\x03 \x01(\tR\x06taskId\x12 \n\x04role\x18\x04 \x01(\x0e\x32\x0c.a2a.v1.RoleR\x04role\x12&\n\x07\x63ontent\x18\x05 \x03(\x0b\x32\x0c.a2a.v1.PartR\x07\x63ontent\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\x12\x1e\n\nextensions\x18\x07 \x03(\tR\nextensions\"\xda\x01\n\x08\x41rtifact\x12\x1f\n\x0b\x61rtifact_id\x18\x01 \x01(\tR\nartifactId\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x04 \x01(\tR\x0b\x64\x65scription\x12\"\n\x05parts\x18\x05 \x03(\x0b\x32\x0c.a2a.v1.PartR\x05parts\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\x12\x1e\n\nextensions\x18\x07 \x03(\tR\nextensions\"\xc6\x01\n\x15TaskStatusUpdateEvent\x12\x17\n\x07task_id\x18\x01 \x01(\tR\x06taskId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12*\n\x06status\x18\x03 \x01(\x0b\x32\x12.a2a.v1.TaskStatusR\x06status\x12\x14\n\x05\x66inal\x18\x04 \x01(\x08R\x05\x66inal\x12\x33\n\x08metadata\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\xeb\x01\n\x17TaskArtifactUpdateEvent\x12\x17\n\x07task_id\x18\x01 \x01(\tR\x06taskId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12,\n\x08\x61rtifact\x18\x03 \x01(\x0b\x32\x10.a2a.v1.ArtifactR\x08\x61rtifact\x12\x16\n\x06\x61ppend\x18\x04 \x01(\x08R\x06\x61ppend\x12\x1d\n\nlast_chunk\x18\x05 \x01(\x08R\tlastChunk\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\x94\x01\n\x16PushNotificationConfig\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n\x03url\x18\x02 \x01(\tR\x03url\x12\x14\n\x05token\x18\x03 \x01(\tR\x05token\x12\x42\n\x0e\x61uthentication\x18\x04 \x01(\x0b\x32\x1a.a2a.v1.AuthenticationInfoR\x0e\x61uthentication\"P\n\x12\x41uthenticationInfo\x12\x18\n\x07schemes\x18\x01 \x03(\tR\x07schemes\x12 \n\x0b\x63redentials\x18\x02 \x01(\tR\x0b\x63redentials\"@\n\x0e\x41gentInterface\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1c\n\ttransport\x18\x02 \x01(\tR\ttransport\"\xc8\x07\n\tAgentCard\x12)\n\x10protocol_version\x18\x10 \x01(\tR\x0fprotocolVersion\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x10\n\x03url\x18\x03 \x01(\tR\x03url\x12/\n\x13preferred_transport\x18\x0e \x01(\tR\x12preferredTransport\x12K\n\x15\x61\x64\x64itional_interfaces\x18\x0f \x03(\x0b\x32\x16.a2a.v1.AgentInterfaceR\x14\x61\x64\x64itionalInterfaces\x12\x31\n\x08provider\x18\x04 \x01(\x0b\x32\x15.a2a.v1.AgentProviderR\x08provider\x12\x18\n\x07version\x18\x05 \x01(\tR\x07version\x12+\n\x11\x64ocumentation_url\x18\x06 \x01(\tR\x10\x64ocumentationUrl\x12=\n\x0c\x63\x61pabilities\x18\x07 \x01(\x0b\x32\x19.a2a.v1.AgentCapabilitiesR\x0c\x63\x61pabilities\x12Q\n\x10security_schemes\x18\x08 \x03(\x0b\x32&.a2a.v1.AgentCard.SecuritySchemesEntryR\x0fsecuritySchemes\x12,\n\x08security\x18\t \x03(\x0b\x32\x10.a2a.v1.SecurityR\x08security\x12.\n\x13\x64\x65\x66\x61ult_input_modes\x18\n \x03(\tR\x11\x64\x65\x66\x61ultInputModes\x12\x30\n\x14\x64\x65\x66\x61ult_output_modes\x18\x0b \x03(\tR\x12\x64\x65\x66\x61ultOutputModes\x12*\n\x06skills\x18\x0c \x03(\x0b\x32\x12.a2a.v1.AgentSkillR\x06skills\x12O\n$supports_authenticated_extended_card\x18\r \x01(\x08R!supportsAuthenticatedExtendedCard\x12:\n\nsignatures\x18\x11 \x03(\x0b\x32\x1a.a2a.v1.AgentCardSignatureR\nsignatures\x12\x19\n\x08icon_url\x18\x12 \x01(\tR\x07iconUrl\x1aZ\n\x14SecuritySchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x16.a2a.v1.SecuritySchemeR\x05value:\x02\x38\x01\"E\n\rAgentProvider\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\"\n\x0corganization\x18\x02 \x01(\tR\x0corganization\"\x98\x01\n\x11\x41gentCapabilities\x12\x1c\n\tstreaming\x18\x01 \x01(\x08R\tstreaming\x12-\n\x12push_notifications\x18\x02 \x01(\x08R\x11pushNotifications\x12\x36\n\nextensions\x18\x03 \x03(\x0b\x32\x16.a2a.v1.AgentExtensionR\nextensions\"\x91\x01\n\x0e\x41gentExtension\x12\x10\n\x03uri\x18\x01 \x01(\tR\x03uri\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08required\x18\x03 \x01(\x08R\x08required\x12/\n\x06params\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructR\x06params\"\xf4\x01\n\nAgentSkill\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x03 \x01(\tR\x0b\x64\x65scription\x12\x12\n\x04tags\x18\x04 \x03(\tR\x04tags\x12\x1a\n\x08\x65xamples\x18\x05 \x03(\tR\x08\x65xamples\x12\x1f\n\x0binput_modes\x18\x06 \x03(\tR\ninputModes\x12!\n\x0coutput_modes\x18\x07 \x03(\tR\x0boutputModes\x12,\n\x08security\x18\x08 \x03(\x0b\x32\x10.a2a.v1.SecurityR\x08security\"\x8b\x01\n\x12\x41gentCardSignature\x12!\n\tprotected\x18\x01 \x01(\tB\x03\xe0\x41\x02R\tprotected\x12!\n\tsignature\x18\x02 \x01(\tB\x03\xe0\x41\x02R\tsignature\x12/\n\x06header\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructR\x06header\"\x8a\x01\n\x1aTaskPushNotificationConfig\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12X\n\x18push_notification_config\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.PushNotificationConfigR\x16pushNotificationConfig\" \n\nStringList\x12\x12\n\x04list\x18\x01 \x03(\tR\x04list\"\x93\x01\n\x08Security\x12\x37\n\x07schemes\x18\x01 \x03(\x0b\x32\x1d.a2a.v1.Security.SchemesEntryR\x07schemes\x1aN\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12(\n\x05value\x18\x02 \x01(\x0b\x32\x12.a2a.v1.StringListR\x05value:\x02\x38\x01\"\xe6\x03\n\x0eSecurityScheme\x12U\n\x17\x61pi_key_security_scheme\x18\x01 \x01(\x0b\x32\x1c.a2a.v1.APIKeySecuritySchemeH\x00R\x14\x61piKeySecurityScheme\x12[\n\x19http_auth_security_scheme\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.HTTPAuthSecuritySchemeH\x00R\x16httpAuthSecurityScheme\x12T\n\x16oauth2_security_scheme\x18\x03 \x01(\x0b\x32\x1c.a2a.v1.OAuth2SecuritySchemeH\x00R\x14oauth2SecurityScheme\x12k\n\x1fopen_id_connect_security_scheme\x18\x04 \x01(\x0b\x32#.a2a.v1.OpenIdConnectSecuritySchemeH\x00R\x1bopenIdConnectSecurityScheme\x12S\n\x14mtls_security_scheme\x18\x05 \x01(\x0b\x32\x1f.a2a.v1.MutualTlsSecuritySchemeH\x00R\x12mtlsSecuritySchemeB\x08\n\x06scheme\"h\n\x14\x41PIKeySecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08location\x18\x02 \x01(\tR\x08location\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\"w\n\x16HTTPAuthSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06scheme\x18\x02 \x01(\tR\x06scheme\x12#\n\rbearer_format\x18\x03 \x01(\tR\x0c\x62\x65\x61rerFormat\"\x92\x01\n\x14OAuth2SecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12(\n\x05\x66lows\x18\x02 \x01(\x0b\x32\x12.a2a.v1.OAuthFlowsR\x05\x66lows\x12.\n\x13oauth2_metadata_url\x18\x03 \x01(\tR\x11oauth2MetadataUrl\"n\n\x1bOpenIdConnectSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12-\n\x13open_id_connect_url\x18\x02 \x01(\tR\x10openIdConnectUrl\";\n\x17MutualTlsSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\"\xb0\x02\n\nOAuthFlows\x12S\n\x12\x61uthorization_code\x18\x01 \x01(\x0b\x32\".a2a.v1.AuthorizationCodeOAuthFlowH\x00R\x11\x61uthorizationCode\x12S\n\x12\x63lient_credentials\x18\x02 \x01(\x0b\x32\".a2a.v1.ClientCredentialsOAuthFlowH\x00R\x11\x63lientCredentials\x12\x37\n\x08implicit\x18\x03 \x01(\x0b\x32\x19.a2a.v1.ImplicitOAuthFlowH\x00R\x08implicit\x12\x37\n\x08password\x18\x04 \x01(\x0b\x32\x19.a2a.v1.PasswordOAuthFlowH\x00R\x08passwordB\x06\n\x04\x66low\"\x8a\x02\n\x1a\x41uthorizationCodeOAuthFlow\x12+\n\x11\x61uthorization_url\x18\x01 \x01(\tR\x10\x61uthorizationUrl\x12\x1b\n\ttoken_url\x18\x02 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x03 \x01(\tR\nrefreshUrl\x12\x46\n\x06scopes\x18\x04 \x03(\x0b\x32..a2a.v1.AuthorizationCodeOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdd\x01\n\x1a\x43lientCredentialsOAuthFlow\x12\x1b\n\ttoken_url\x18\x01 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12\x46\n\x06scopes\x18\x03 \x03(\x0b\x32..a2a.v1.ClientCredentialsOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdb\x01\n\x11ImplicitOAuthFlow\x12+\n\x11\x61uthorization_url\x18\x01 \x01(\tR\x10\x61uthorizationUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12=\n\x06scopes\x18\x03 \x03(\x0b\x32%.a2a.v1.ImplicitOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xcb\x01\n\x11PasswordOAuthFlow\x12\x1b\n\ttoken_url\x18\x01 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12=\n\x06scopes\x18\x03 \x03(\x0b\x32%.a2a.v1.PasswordOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xc1\x01\n\x12SendMessageRequest\x12.\n\x07request\x18\x01 \x01(\x0b\x32\x0f.a2a.v1.MessageB\x03\xe0\x41\x02R\x07message\x12\x46\n\rconfiguration\x18\x02 \x01(\x0b\x32 .a2a.v1.SendMessageConfigurationR\rconfiguration\x12\x33\n\x08metadata\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\x85\x01\n\x0eGetTaskRequest\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x04name\x12%\n\x0ehistory_length\x18\x02 \x01(\x05R\rhistoryLength\x12\x33\n\x08metadata\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\'\n\x11\x43\x61ncelTaskRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\":\n$GetTaskPushNotificationConfigRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"=\n\'DeleteTaskPushNotificationConfigRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xa9\x01\n\'CreateTaskPushNotificationConfigRequest\x12\x1b\n\x06parent\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06parent\x12 \n\tconfig_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x08\x63onfigId\x12?\n\x06\x63onfig\x18\x03 \x01(\x0b\x32\".a2a.v1.TaskPushNotificationConfigB\x03\xe0\x41\x02R\x06\x63onfig\"-\n\x17TaskSubscriptionRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"{\n%ListTaskPushNotificationConfigRequest\x12\x16\n\x06parent\x18\x01 \x01(\tR\x06parent\x12\x1b\n\tpage_size\x18\x02 \x01(\x05R\x08pageSize\x12\x1d\n\npage_token\x18\x03 \x01(\tR\tpageToken\"\x15\n\x13GetAgentCardRequest\"m\n\x13SendMessageResponse\x12\"\n\x04task\x18\x01 \x01(\x0b\x32\x0c.a2a.v1.TaskH\x00R\x04task\x12\'\n\x03msg\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageH\x00R\x07messageB\t\n\x07payload\"\xfa\x01\n\x0eStreamResponse\x12\"\n\x04task\x18\x01 \x01(\x0b\x32\x0c.a2a.v1.TaskH\x00R\x04task\x12\'\n\x03msg\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageH\x00R\x07message\x12\x44\n\rstatus_update\x18\x03 \x01(\x0b\x32\x1d.a2a.v1.TaskStatusUpdateEventH\x00R\x0cstatusUpdate\x12J\n\x0f\x61rtifact_update\x18\x04 \x01(\x0b\x32\x1f.a2a.v1.TaskArtifactUpdateEventH\x00R\x0e\x61rtifactUpdateB\t\n\x07payload\"\x8e\x01\n&ListTaskPushNotificationConfigResponse\x12<\n\x07\x63onfigs\x18\x01 \x03(\x0b\x32\".a2a.v1.TaskPushNotificationConfigR\x07\x63onfigs\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xfa\x01\n\tTaskState\x12\x1a\n\x16TASK_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14TASK_STATE_SUBMITTED\x10\x01\x12\x16\n\x12TASK_STATE_WORKING\x10\x02\x12\x18\n\x14TASK_STATE_COMPLETED\x10\x03\x12\x15\n\x11TASK_STATE_FAILED\x10\x04\x12\x18\n\x14TASK_STATE_CANCELLED\x10\x05\x12\x1d\n\x19TASK_STATE_INPUT_REQUIRED\x10\x06\x12\x17\n\x13TASK_STATE_REJECTED\x10\x07\x12\x1c\n\x18TASK_STATE_AUTH_REQUIRED\x10\x08*;\n\x04Role\x12\x14\n\x10ROLE_UNSPECIFIED\x10\x00\x12\r\n\tROLE_USER\x10\x01\x12\x0e\n\nROLE_AGENT\x10\x02\x32\xbb\n\n\nA2AService\x12\x63\n\x0bSendMessage\x12\x1a.a2a.v1.SendMessageRequest\x1a\x1b.a2a.v1.SendMessageResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/message:send:\x01*\x12k\n\x14SendStreamingMessage\x12\x1a.a2a.v1.SendMessageRequest\x1a\x16.a2a.v1.StreamResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x12/v1/message:stream:\x01*0\x01\x12R\n\x07GetTask\x12\x16.a2a.v1.GetTaskRequest\x1a\x0c.a2a.v1.Task\"!\xda\x41\x04name\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/{name=tasks/*}\x12[\n\nCancelTask\x12\x19.a2a.v1.CancelTaskRequest\x1a\x0c.a2a.v1.Task\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/{name=tasks/*}:cancel:\x01*\x12s\n\x10TaskSubscription\x12\x1f.a2a.v1.TaskSubscriptionRequest\x1a\x16.a2a.v1.StreamResponse\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/{name=tasks/*}:subscribe0\x01\x12\xc5\x01\n CreateTaskPushNotificationConfig\x12/.a2a.v1.CreateTaskPushNotificationConfigRequest\x1a\".a2a.v1.TaskPushNotificationConfig\"L\xda\x41\rparent,config\x82\xd3\xe4\x93\x02\x36\",/v1/{parent=tasks/*/pushNotificationConfigs}:\x06\x63onfig\x12\xae\x01\n\x1dGetTaskPushNotificationConfig\x12,.a2a.v1.GetTaskPushNotificationConfigRequest\x1a\".a2a.v1.TaskPushNotificationConfig\";\xda\x41\x04name\x82\xd3\xe4\x93\x02.\x12,/v1/{name=tasks/*/pushNotificationConfigs/*}\x12\xbe\x01\n\x1eListTaskPushNotificationConfig\x12-.a2a.v1.ListTaskPushNotificationConfigRequest\x1a..a2a.v1.ListTaskPushNotificationConfigResponse\"=\xda\x41\x06parent\x82\xd3\xe4\x93\x02.\x12,/v1/{parent=tasks/*}/pushNotificationConfigs\x12P\n\x0cGetAgentCard\x12\x1b.a2a.v1.GetAgentCardRequest\x1a\x11.a2a.v1.AgentCard\"\x10\x82\xd3\xe4\x93\x02\n\x12\x08/v1/card\x12\xa8\x01\n DeleteTaskPushNotificationConfig\x12/.a2a.v1.DeleteTaskPushNotificationConfigRequest\x1a\x16.google.protobuf.Empty\";\xda\x41\x04name\x82\xd3\xe4\x93\x02.*,/v1/{name=tasks/*/pushNotificationConfigs/*}Bi\n\ncom.a2a.v1B\x08\x41\x32\x61ProtoP\x01Z\x18google.golang.org/a2a/v1\xa2\x02\x03\x41XX\xaa\x02\x06\x41\x32\x61.V1\xca\x02\x06\x41\x32\x61\\V1\xe2\x02\x12\x41\x32\x61\\V1\\GPBMetadata\xea\x02\x07\x41\x32\x61::V1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ta2a.proto\x12\x06\x61\x32\x61.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xde\x01\n\x18SendMessageConfiguration\x12\x32\n\x15\x61\x63\x63\x65pted_output_modes\x18\x01 \x03(\tR\x13\x61\x63\x63\x65ptedOutputModes\x12K\n\x11push_notification\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.PushNotificationConfigR\x10pushNotification\x12%\n\x0ehistory_length\x18\x03 \x01(\x05R\rhistoryLength\x12\x1a\n\x08\x62locking\x18\x04 \x01(\x08R\x08\x62locking\"\xf1\x01\n\x04Task\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12*\n\x06status\x18\x03 \x01(\x0b\x32\x12.a2a.v1.TaskStatusR\x06status\x12.\n\tartifacts\x18\x04 \x03(\x0b\x32\x10.a2a.v1.ArtifactR\tartifacts\x12)\n\x07history\x18\x05 \x03(\x0b\x32\x0f.a2a.v1.MessageR\x07history\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\x99\x01\n\nTaskStatus\x12\'\n\x05state\x18\x01 \x01(\x0e\x32\x11.a2a.v1.TaskStateR\x05state\x12(\n\x06update\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageR\x07message\x12\x38\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\ttimestamp\"t\n\x04Part\x12\x14\n\x04text\x18\x01 \x01(\tH\x00R\x04text\x12&\n\x04\x66ile\x18\x02 \x01(\x0b\x32\x10.a2a.v1.FilePartH\x00R\x04\x66ile\x12&\n\x04\x64\x61ta\x18\x03 \x01(\x0b\x32\x10.a2a.v1.DataPartH\x00R\x04\x64\x61taB\x06\n\x04part\"\x93\x01\n\x08\x46ilePart\x12$\n\rfile_with_uri\x18\x01 \x01(\tH\x00R\x0b\x66ileWithUri\x12(\n\x0f\x66ile_with_bytes\x18\x02 \x01(\x0cH\x00R\rfileWithBytes\x12\x1b\n\tmime_type\x18\x03 \x01(\tR\x08mimeType\x12\x12\n\x04name\x18\x04 \x01(\tR\x04nameB\x06\n\x04\x66ile\"7\n\x08\x44\x61taPart\x12+\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructR\x04\x64\x61ta\"\xff\x01\n\x07Message\x12\x1d\n\nmessage_id\x18\x01 \x01(\tR\tmessageId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12\x17\n\x07task_id\x18\x03 \x01(\tR\x06taskId\x12 \n\x04role\x18\x04 \x01(\x0e\x32\x0c.a2a.v1.RoleR\x04role\x12&\n\x07\x63ontent\x18\x05 \x03(\x0b\x32\x0c.a2a.v1.PartR\x07\x63ontent\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\x12\x1e\n\nextensions\x18\x07 \x03(\tR\nextensions\"\xda\x01\n\x08\x41rtifact\x12\x1f\n\x0b\x61rtifact_id\x18\x01 \x01(\tR\nartifactId\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x04 \x01(\tR\x0b\x64\x65scription\x12\"\n\x05parts\x18\x05 \x03(\x0b\x32\x0c.a2a.v1.PartR\x05parts\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\x12\x1e\n\nextensions\x18\x07 \x03(\tR\nextensions\"\xc6\x01\n\x15TaskStatusUpdateEvent\x12\x17\n\x07task_id\x18\x01 \x01(\tR\x06taskId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12*\n\x06status\x18\x03 \x01(\x0b\x32\x12.a2a.v1.TaskStatusR\x06status\x12\x14\n\x05\x66inal\x18\x04 \x01(\x08R\x05\x66inal\x12\x33\n\x08metadata\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\xeb\x01\n\x17TaskArtifactUpdateEvent\x12\x17\n\x07task_id\x18\x01 \x01(\tR\x06taskId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12,\n\x08\x61rtifact\x18\x03 \x01(\x0b\x32\x10.a2a.v1.ArtifactR\x08\x61rtifact\x12\x16\n\x06\x61ppend\x18\x04 \x01(\x08R\x06\x61ppend\x12\x1d\n\nlast_chunk\x18\x05 \x01(\x08R\tlastChunk\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\x94\x01\n\x16PushNotificationConfig\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n\x03url\x18\x02 \x01(\tR\x03url\x12\x14\n\x05token\x18\x03 \x01(\tR\x05token\x12\x42\n\x0e\x61uthentication\x18\x04 \x01(\x0b\x32\x1a.a2a.v1.AuthenticationInfoR\x0e\x61uthentication\"P\n\x12\x41uthenticationInfo\x12\x18\n\x07schemes\x18\x01 \x03(\tR\x07schemes\x12 \n\x0b\x63redentials\x18\x02 \x01(\tR\x0b\x63redentials\"@\n\x0e\x41gentInterface\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1c\n\ttransport\x18\x02 \x01(\tR\ttransport\"\xc8\x07\n\tAgentCard\x12)\n\x10protocol_version\x18\x10 \x01(\tR\x0fprotocolVersion\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x10\n\x03url\x18\x03 \x01(\tR\x03url\x12/\n\x13preferred_transport\x18\x0e \x01(\tR\x12preferredTransport\x12K\n\x15\x61\x64\x64itional_interfaces\x18\x0f \x03(\x0b\x32\x16.a2a.v1.AgentInterfaceR\x14\x61\x64\x64itionalInterfaces\x12\x31\n\x08provider\x18\x04 \x01(\x0b\x32\x15.a2a.v1.AgentProviderR\x08provider\x12\x18\n\x07version\x18\x05 \x01(\tR\x07version\x12+\n\x11\x64ocumentation_url\x18\x06 \x01(\tR\x10\x64ocumentationUrl\x12=\n\x0c\x63\x61pabilities\x18\x07 \x01(\x0b\x32\x19.a2a.v1.AgentCapabilitiesR\x0c\x63\x61pabilities\x12Q\n\x10security_schemes\x18\x08 \x03(\x0b\x32&.a2a.v1.AgentCard.SecuritySchemesEntryR\x0fsecuritySchemes\x12,\n\x08security\x18\t \x03(\x0b\x32\x10.a2a.v1.SecurityR\x08security\x12.\n\x13\x64\x65\x66\x61ult_input_modes\x18\n \x03(\tR\x11\x64\x65\x66\x61ultInputModes\x12\x30\n\x14\x64\x65\x66\x61ult_output_modes\x18\x0b \x03(\tR\x12\x64\x65\x66\x61ultOutputModes\x12*\n\x06skills\x18\x0c \x03(\x0b\x32\x12.a2a.v1.AgentSkillR\x06skills\x12O\n$supports_authenticated_extended_card\x18\r \x01(\x08R!supportsAuthenticatedExtendedCard\x12:\n\nsignatures\x18\x11 \x03(\x0b\x32\x1a.a2a.v1.AgentCardSignatureR\nsignatures\x12\x19\n\x08icon_url\x18\x12 \x01(\tR\x07iconUrl\x1aZ\n\x14SecuritySchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x16.a2a.v1.SecuritySchemeR\x05value:\x02\x38\x01\"E\n\rAgentProvider\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\"\n\x0corganization\x18\x02 \x01(\tR\x0corganization\"\x98\x01\n\x11\x41gentCapabilities\x12\x1c\n\tstreaming\x18\x01 \x01(\x08R\tstreaming\x12-\n\x12push_notifications\x18\x02 \x01(\x08R\x11pushNotifications\x12\x36\n\nextensions\x18\x03 \x03(\x0b\x32\x16.a2a.v1.AgentExtensionR\nextensions\"\x91\x01\n\x0e\x41gentExtension\x12\x10\n\x03uri\x18\x01 \x01(\tR\x03uri\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08required\x18\x03 \x01(\x08R\x08required\x12/\n\x06params\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructR\x06params\"\xf4\x01\n\nAgentSkill\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x03 \x01(\tR\x0b\x64\x65scription\x12\x12\n\x04tags\x18\x04 \x03(\tR\x04tags\x12\x1a\n\x08\x65xamples\x18\x05 \x03(\tR\x08\x65xamples\x12\x1f\n\x0binput_modes\x18\x06 \x03(\tR\ninputModes\x12!\n\x0coutput_modes\x18\x07 \x03(\tR\x0boutputModes\x12,\n\x08security\x18\x08 \x03(\x0b\x32\x10.a2a.v1.SecurityR\x08security\"\x8b\x01\n\x12\x41gentCardSignature\x12!\n\tprotected\x18\x01 \x01(\tB\x03\xe0\x41\x02R\tprotected\x12!\n\tsignature\x18\x02 \x01(\tB\x03\xe0\x41\x02R\tsignature\x12/\n\x06header\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructR\x06header\"\x8a\x01\n\x1aTaskPushNotificationConfig\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12X\n\x18push_notification_config\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.PushNotificationConfigR\x16pushNotificationConfig\" \n\nStringList\x12\x12\n\x04list\x18\x01 \x03(\tR\x04list\"\x93\x01\n\x08Security\x12\x37\n\x07schemes\x18\x01 \x03(\x0b\x32\x1d.a2a.v1.Security.SchemesEntryR\x07schemes\x1aN\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12(\n\x05value\x18\x02 \x01(\x0b\x32\x12.a2a.v1.StringListR\x05value:\x02\x38\x01\"\xe6\x03\n\x0eSecurityScheme\x12U\n\x17\x61pi_key_security_scheme\x18\x01 \x01(\x0b\x32\x1c.a2a.v1.APIKeySecuritySchemeH\x00R\x14\x61piKeySecurityScheme\x12[\n\x19http_auth_security_scheme\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.HTTPAuthSecuritySchemeH\x00R\x16httpAuthSecurityScheme\x12T\n\x16oauth2_security_scheme\x18\x03 \x01(\x0b\x32\x1c.a2a.v1.OAuth2SecuritySchemeH\x00R\x14oauth2SecurityScheme\x12k\n\x1fopen_id_connect_security_scheme\x18\x04 \x01(\x0b\x32#.a2a.v1.OpenIdConnectSecuritySchemeH\x00R\x1bopenIdConnectSecurityScheme\x12S\n\x14mtls_security_scheme\x18\x05 \x01(\x0b\x32\x1f.a2a.v1.MutualTlsSecuritySchemeH\x00R\x12mtlsSecuritySchemeB\x08\n\x06scheme\"h\n\x14\x41PIKeySecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08location\x18\x02 \x01(\tR\x08location\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\"w\n\x16HTTPAuthSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06scheme\x18\x02 \x01(\tR\x06scheme\x12#\n\rbearer_format\x18\x03 \x01(\tR\x0c\x62\x65\x61rerFormat\"\x92\x01\n\x14OAuth2SecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12(\n\x05\x66lows\x18\x02 \x01(\x0b\x32\x12.a2a.v1.OAuthFlowsR\x05\x66lows\x12.\n\x13oauth2_metadata_url\x18\x03 \x01(\tR\x11oauth2MetadataUrl\"n\n\x1bOpenIdConnectSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12-\n\x13open_id_connect_url\x18\x02 \x01(\tR\x10openIdConnectUrl\";\n\x17MutualTlsSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\"\xb0\x02\n\nOAuthFlows\x12S\n\x12\x61uthorization_code\x18\x01 \x01(\x0b\x32\".a2a.v1.AuthorizationCodeOAuthFlowH\x00R\x11\x61uthorizationCode\x12S\n\x12\x63lient_credentials\x18\x02 \x01(\x0b\x32\".a2a.v1.ClientCredentialsOAuthFlowH\x00R\x11\x63lientCredentials\x12\x37\n\x08implicit\x18\x03 \x01(\x0b\x32\x19.a2a.v1.ImplicitOAuthFlowH\x00R\x08implicit\x12\x37\n\x08password\x18\x04 \x01(\x0b\x32\x19.a2a.v1.PasswordOAuthFlowH\x00R\x08passwordB\x06\n\x04\x66low\"\x8a\x02\n\x1a\x41uthorizationCodeOAuthFlow\x12+\n\x11\x61uthorization_url\x18\x01 \x01(\tR\x10\x61uthorizationUrl\x12\x1b\n\ttoken_url\x18\x02 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x03 \x01(\tR\nrefreshUrl\x12\x46\n\x06scopes\x18\x04 \x03(\x0b\x32..a2a.v1.AuthorizationCodeOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdd\x01\n\x1a\x43lientCredentialsOAuthFlow\x12\x1b\n\ttoken_url\x18\x01 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12\x46\n\x06scopes\x18\x03 \x03(\x0b\x32..a2a.v1.ClientCredentialsOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdb\x01\n\x11ImplicitOAuthFlow\x12+\n\x11\x61uthorization_url\x18\x01 \x01(\tR\x10\x61uthorizationUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12=\n\x06scopes\x18\x03 \x03(\x0b\x32%.a2a.v1.ImplicitOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xcb\x01\n\x11PasswordOAuthFlow\x12\x1b\n\ttoken_url\x18\x01 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12=\n\x06scopes\x18\x03 \x03(\x0b\x32%.a2a.v1.PasswordOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xc1\x01\n\x12SendMessageRequest\x12.\n\x07request\x18\x01 \x01(\x0b\x32\x0f.a2a.v1.MessageB\x03\xe0\x41\x02R\x07message\x12\x46\n\rconfiguration\x18\x02 \x01(\x0b\x32 .a2a.v1.SendMessageConfigurationR\rconfiguration\x12\x33\n\x08metadata\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"P\n\x0eGetTaskRequest\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x04name\x12%\n\x0ehistory_length\x18\x02 \x01(\x05R\rhistoryLength\"\'\n\x11\x43\x61ncelTaskRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\":\n$GetTaskPushNotificationConfigRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"=\n\'DeleteTaskPushNotificationConfigRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xa9\x01\n\'CreateTaskPushNotificationConfigRequest\x12\x1b\n\x06parent\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06parent\x12 \n\tconfig_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x08\x63onfigId\x12?\n\x06\x63onfig\x18\x03 \x01(\x0b\x32\".a2a.v1.TaskPushNotificationConfigB\x03\xe0\x41\x02R\x06\x63onfig\"-\n\x17TaskSubscriptionRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"{\n%ListTaskPushNotificationConfigRequest\x12\x16\n\x06parent\x18\x01 \x01(\tR\x06parent\x12\x1b\n\tpage_size\x18\x02 \x01(\x05R\x08pageSize\x12\x1d\n\npage_token\x18\x03 \x01(\tR\tpageToken\"\x15\n\x13GetAgentCardRequest\"m\n\x13SendMessageResponse\x12\"\n\x04task\x18\x01 \x01(\x0b\x32\x0c.a2a.v1.TaskH\x00R\x04task\x12\'\n\x03msg\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageH\x00R\x07messageB\t\n\x07payload\"\xfa\x01\n\x0eStreamResponse\x12\"\n\x04task\x18\x01 \x01(\x0b\x32\x0c.a2a.v1.TaskH\x00R\x04task\x12\'\n\x03msg\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageH\x00R\x07message\x12\x44\n\rstatus_update\x18\x03 \x01(\x0b\x32\x1d.a2a.v1.TaskStatusUpdateEventH\x00R\x0cstatusUpdate\x12J\n\x0f\x61rtifact_update\x18\x04 \x01(\x0b\x32\x1f.a2a.v1.TaskArtifactUpdateEventH\x00R\x0e\x61rtifactUpdateB\t\n\x07payload\"\x8e\x01\n&ListTaskPushNotificationConfigResponse\x12<\n\x07\x63onfigs\x18\x01 \x03(\x0b\x32\".a2a.v1.TaskPushNotificationConfigR\x07\x63onfigs\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xfa\x01\n\tTaskState\x12\x1a\n\x16TASK_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14TASK_STATE_SUBMITTED\x10\x01\x12\x16\n\x12TASK_STATE_WORKING\x10\x02\x12\x18\n\x14TASK_STATE_COMPLETED\x10\x03\x12\x15\n\x11TASK_STATE_FAILED\x10\x04\x12\x18\n\x14TASK_STATE_CANCELLED\x10\x05\x12\x1d\n\x19TASK_STATE_INPUT_REQUIRED\x10\x06\x12\x17\n\x13TASK_STATE_REJECTED\x10\x07\x12\x1c\n\x18TASK_STATE_AUTH_REQUIRED\x10\x08*;\n\x04Role\x12\x14\n\x10ROLE_UNSPECIFIED\x10\x00\x12\r\n\tROLE_USER\x10\x01\x12\x0e\n\nROLE_AGENT\x10\x02\x32\xbb\n\n\nA2AService\x12\x63\n\x0bSendMessage\x12\x1a.a2a.v1.SendMessageRequest\x1a\x1b.a2a.v1.SendMessageResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/message:send:\x01*\x12k\n\x14SendStreamingMessage\x12\x1a.a2a.v1.SendMessageRequest\x1a\x16.a2a.v1.StreamResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x12/v1/message:stream:\x01*0\x01\x12R\n\x07GetTask\x12\x16.a2a.v1.GetTaskRequest\x1a\x0c.a2a.v1.Task\"!\xda\x41\x04name\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/{name=tasks/*}\x12[\n\nCancelTask\x12\x19.a2a.v1.CancelTaskRequest\x1a\x0c.a2a.v1.Task\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/{name=tasks/*}:cancel:\x01*\x12s\n\x10TaskSubscription\x12\x1f.a2a.v1.TaskSubscriptionRequest\x1a\x16.a2a.v1.StreamResponse\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/{name=tasks/*}:subscribe0\x01\x12\xc5\x01\n CreateTaskPushNotificationConfig\x12/.a2a.v1.CreateTaskPushNotificationConfigRequest\x1a\".a2a.v1.TaskPushNotificationConfig\"L\xda\x41\rparent,config\x82\xd3\xe4\x93\x02\x36\",/v1/{parent=tasks/*/pushNotificationConfigs}:\x06\x63onfig\x12\xae\x01\n\x1dGetTaskPushNotificationConfig\x12,.a2a.v1.GetTaskPushNotificationConfigRequest\x1a\".a2a.v1.TaskPushNotificationConfig\";\xda\x41\x04name\x82\xd3\xe4\x93\x02.\x12,/v1/{name=tasks/*/pushNotificationConfigs/*}\x12\xbe\x01\n\x1eListTaskPushNotificationConfig\x12-.a2a.v1.ListTaskPushNotificationConfigRequest\x1a..a2a.v1.ListTaskPushNotificationConfigResponse\"=\xda\x41\x06parent\x82\xd3\xe4\x93\x02.\x12,/v1/{parent=tasks/*}/pushNotificationConfigs\x12P\n\x0cGetAgentCard\x12\x1b.a2a.v1.GetAgentCardRequest\x1a\x11.a2a.v1.AgentCard\"\x10\x82\xd3\xe4\x93\x02\n\x12\x08/v1/card\x12\xa8\x01\n DeleteTaskPushNotificationConfig\x12/.a2a.v1.DeleteTaskPushNotificationConfigRequest\x1a\x16.google.protobuf.Empty\";\xda\x41\x04name\x82\xd3\xe4\x93\x02.*,/v1/{name=tasks/*/pushNotificationConfigs/*}Bi\n\ncom.a2a.v1B\x08\x41\x32\x61ProtoP\x01Z\x18google.golang.org/a2a/v1\xa2\x02\x03\x41XX\xaa\x02\x06\x41\x32\x61.V1\xca\x02\x06\x41\x32\x61\\V1\xe2\x02\x12\x41\x32\x61\\V1\\GPBMetadata\xea\x02\x07\x41\x32\x61::V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -84,10 +84,10 @@ _globals['_A2ASERVICE'].methods_by_name['GetAgentCard']._serialized_options = b'\202\323\344\223\002\n\022\010/v1/card' _globals['_A2ASERVICE'].methods_by_name['DeleteTaskPushNotificationConfig']._loaded_options = None _globals['_A2ASERVICE'].methods_by_name['DeleteTaskPushNotificationConfig']._serialized_options = b'\332A\004name\202\323\344\223\002.*,/v1/{name=tasks/*/pushNotificationConfigs/*}' - _globals['_TASKSTATE']._serialized_start=8066 - _globals['_TASKSTATE']._serialized_end=8316 - _globals['_ROLE']._serialized_start=8318 - _globals['_ROLE']._serialized_end=8377 + _globals['_TASKSTATE']._serialized_start=8012 + _globals['_TASKSTATE']._serialized_end=8262 + _globals['_ROLE']._serialized_start=8264 + _globals['_ROLE']._serialized_end=8323 _globals['_SENDMESSAGECONFIGURATION']._serialized_start=202 _globals['_SENDMESSAGECONFIGURATION']._serialized_end=424 _globals['_TASK']._serialized_start=427 @@ -168,28 +168,28 @@ _globals['_PASSWORDOAUTHFLOW_SCOPESENTRY']._serialized_end=6039 _globals['_SENDMESSAGEREQUEST']._serialized_start=6694 _globals['_SENDMESSAGEREQUEST']._serialized_end=6887 - _globals['_GETTASKREQUEST']._serialized_start=6890 - _globals['_GETTASKREQUEST']._serialized_end=7023 - _globals['_CANCELTASKREQUEST']._serialized_start=7025 - _globals['_CANCELTASKREQUEST']._serialized_end=7064 - _globals['_GETTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7066 - _globals['_GETTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7124 - _globals['_DELETETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7126 - _globals['_DELETETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7187 - _globals['_CREATETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7190 - _globals['_CREATETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7359 - _globals['_TASKSUBSCRIPTIONREQUEST']._serialized_start=7361 - _globals['_TASKSUBSCRIPTIONREQUEST']._serialized_end=7406 - _globals['_LISTTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7408 - _globals['_LISTTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7531 - _globals['_GETAGENTCARDREQUEST']._serialized_start=7533 - _globals['_GETAGENTCARDREQUEST']._serialized_end=7554 - _globals['_SENDMESSAGERESPONSE']._serialized_start=7556 - _globals['_SENDMESSAGERESPONSE']._serialized_end=7665 - _globals['_STREAMRESPONSE']._serialized_start=7668 - _globals['_STREAMRESPONSE']._serialized_end=7918 - _globals['_LISTTASKPUSHNOTIFICATIONCONFIGRESPONSE']._serialized_start=7921 - _globals['_LISTTASKPUSHNOTIFICATIONCONFIGRESPONSE']._serialized_end=8063 - _globals['_A2ASERVICE']._serialized_start=8380 - _globals['_A2ASERVICE']._serialized_end=9719 + _globals['_GETTASKREQUEST']._serialized_start=6889 + _globals['_GETTASKREQUEST']._serialized_end=6969 + _globals['_CANCELTASKREQUEST']._serialized_start=6971 + _globals['_CANCELTASKREQUEST']._serialized_end=7010 + _globals['_GETTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7012 + _globals['_GETTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7070 + _globals['_DELETETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7072 + _globals['_DELETETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7133 + _globals['_CREATETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7136 + _globals['_CREATETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7305 + _globals['_TASKSUBSCRIPTIONREQUEST']._serialized_start=7307 + _globals['_TASKSUBSCRIPTIONREQUEST']._serialized_end=7352 + _globals['_LISTTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7354 + _globals['_LISTTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7477 + _globals['_GETAGENTCARDREQUEST']._serialized_start=7479 + _globals['_GETAGENTCARDREQUEST']._serialized_end=7500 + _globals['_SENDMESSAGERESPONSE']._serialized_start=7502 + _globals['_SENDMESSAGERESPONSE']._serialized_end=7611 + _globals['_STREAMRESPONSE']._serialized_start=7614 + _globals['_STREAMRESPONSE']._serialized_end=7864 + _globals['_LISTTASKPUSHNOTIFICATIONCONFIGRESPONSE']._serialized_start=7867 + _globals['_LISTTASKPUSHNOTIFICATIONCONFIGRESPONSE']._serialized_end=8009 + _globals['_A2ASERVICE']._serialized_start=8326 + _globals['_A2ASERVICE']._serialized_end=9665 # @@protoc_insertion_point(module_scope) diff --git a/src/a2a/grpc/a2a_pb2.pyi b/src/a2a/grpc/a2a_pb2.pyi index 2d64e9c29..064f7387b 100644 --- a/src/a2a/grpc/a2a_pb2.pyi +++ b/src/a2a/grpc/a2a_pb2.pyi @@ -488,14 +488,12 @@ class SendMessageRequest(_message.Message): def __init__(self, request: _Optional[_Union[Message, _Mapping]] = ..., configuration: _Optional[_Union[SendMessageConfiguration, _Mapping]] = ..., metadata: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ...) -> None: ... class GetTaskRequest(_message.Message): - __slots__ = ("name", "history_length", "metadata") + __slots__ = ("name", "history_length") NAME_FIELD_NUMBER: _ClassVar[int] HISTORY_LENGTH_FIELD_NUMBER: _ClassVar[int] - METADATA_FIELD_NUMBER: _ClassVar[int] name: str history_length: int - metadata: _struct_pb2.Struct - def __init__(self, name: _Optional[str] = ..., history_length: _Optional[int] = ..., metadata: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ...) -> None: ... + def __init__(self, name: _Optional[str] = ..., history_length: _Optional[int] = ...) -> None: ... class CancelTaskRequest(_message.Message): __slots__ = ("name",) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index e8f9e7181..408c47bf2 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -767,7 +767,7 @@ def task_query_params( if request.history_length else None, id=m.group(1), - metadata=request.metadata, + metadata=None, ) @classmethod From 8a6d1c73006246a66224efd28982fa2ad0c38ff9 Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Sat, 23 Aug 2025 12:12:58 -0400 Subject: [PATCH 04/12] chore: merge updates and mr comments --- .github/workflows/unit-tests.yml | 2 +- src/a2a/utils/proto_utils.py | 71 ++++++++++++++++++++++++++------ src/a2a/utils/task.py | 16 +------ tests/utils/test_task.py | 18 -------- 4 files changed, 61 insertions(+), 46 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 99e092bcd..df283c5af 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -55,6 +55,6 @@ jobs: - name: Install dependencies run: uv sync --dev --extra sql --extra encryption --extra grpc --extra telemetry - name: Run tests and check coverage - run: uv run pytest --cov=a2a --cov-report term --cov-fail-under=88 + run: PYTHONPATH=. uv run pytest --cov=a2a --cov-report term --cov-fail-under=88 - name: Show coverage summary in log run: uv run coverage report diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index 408c47bf2..7cf7a5d75 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -46,14 +46,7 @@ def metadata( ) -> struct_pb2.Struct | None: if metadata is None: return None - return struct_pb2.Struct( - # TODO: Add support for other types. - fields={ - key: struct_pb2.Value(string_value=value) - for key, value in metadata.items() - if isinstance(value, str) - } - ) + return dict_to_struct(metadata) @classmethod def part(cls, part: types.Part) -> a2a_pb2.Part: @@ -324,6 +317,23 @@ def capabilities( return a2a_pb2.AgentCapabilities( streaming=bool(capabilities.streaming), push_notifications=bool(capabilities.push_notifications), + extensions=[ + cls.extension(x) for x in capabilities.extensions or [] + ], + ) + + @classmethod + def extension( + cls, + extension: types.AgentExtension, + ) -> a2a_pb2.AgentExtension: + return a2a_pb2.AgentExtension( + uri=extension.uri, + description=extension.description, + params=dict_to_struct(extension.params) + if extension.params + else None, + required=extension.required, ) @classmethod @@ -477,11 +487,9 @@ def message(cls, message: a2a_pb2.Message) -> types.Message: @classmethod def metadata(cls, metadata: struct_pb2.Struct) -> dict[str, Any]: - return { - key: value.string_value - for key, value in metadata.fields.items() - if value.string_value - } + if not metadata.fields: + return {} + return json_format.MessageToDict(metadata) @classmethod def part(cls, part: a2a_pb2.Part) -> types.Part: @@ -777,6 +785,21 @@ def capabilities( return types.AgentCapabilities( streaming=capabilities.streaming, push_notifications=capabilities.push_notifications, + extensions=[ + cls.agent_extension(x) for x in capabilities.extensions + ], + ) + + @classmethod + def agent_extension( + cls, + extension: a2a_pb2.AgentExtension, + ) -> types.AgentExtension: + return types.AgentExtension( + uri=extension.uri, + description=extension.description, + params=json_format.MessageToDict(extension.params), + required=extension.required, ) @classmethod @@ -916,3 +939,25 @@ def role(cls, role: a2a_pb2.Role) -> types.Role: return types.Role.agent case _: return types.Role.agent + + +def dict_to_struct(dictionary: dict[str, Any]) -> struct_pb2.Struct: + """Converts a Python dict to a Struct proto. + + Unfortunately, using `json_format.ParseDict` does not work because this + wants the dictionary to be an exact match of the Struct proto with fields + and keys and values, not the traditional Python dict structure. + + Args: + dictionary: The Python dict to convert. + + Returns: + The Struct proto. + """ + struct = struct_pb2.Struct() + for key, val in dictionary.items(): + if isinstance(val, dict): + struct[key] = dict_to_struct(val) + else: + struct[key] = val + return struct diff --git a/src/a2a/utils/task.py b/src/a2a/utils/task.py index 602723670..22556cde3 100644 --- a/src/a2a/utils/task.py +++ b/src/a2a/utils/task.py @@ -28,22 +28,10 @@ def new_task(request: Message) -> Task: if isinstance(part.root, TextPart) and not part.root.text: raise ValueError('TextPart content cannot be empty') - context_id_str = request.context_id - if context_id_str is not None: - try: - uuid.UUID(context_id_str) - context_id = context_id_str - except (ValueError, AttributeError, TypeError) as e: - raise ValueError( - f"Invalid context_id: '{context_id_str}' is not a valid UUID." - ) from e - else: - context_id = str(uuid.uuid4()) - return Task( status=TaskStatus(state=TaskState.submitted), - id=(request.task_id if request.task_id else str(uuid.uuid4())), - context_id=context_id, + id=request.task_id or str(uuid.uuid4()), + context_id=request.context_id or str(uuid.uuid4()), history=[request], ) diff --git a/tests/utils/test_task.py b/tests/utils/test_task.py index 774413163..cb3dc3868 100644 --- a/tests/utils/test_task.py +++ b/tests/utils/test_task.py @@ -188,24 +188,6 @@ def test_completed_task_invalid_artifact_type(self): history=[], ) - def test_new_task_with_invalid_context_id(self): - """Test that new_task raises a ValueError for various invalid context_id formats.""" - invalid_ids = ['not-a-uuid', ''] - for invalid_id in invalid_ids: - with self.subTest(invalid_id=invalid_id): - with pytest.raises( - ValueError, - match=f"Invalid context_id: '{invalid_id}' is not a valid UUID.", - ): - new_task( - Message( - role=Role.user, - parts=[Part(root=TextPart(text='test message'))], - message_id=str(uuid.uuid4()), - context_id=invalid_id, - ) - ) - if __name__ == '__main__': unittest.main() From 1ad01bd9103484185df405a81b9e08e5095a544c Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Thu, 4 Sep 2025 15:26:02 -0400 Subject: [PATCH 05/12] patch tests --- tests/server/tasks/test_task_manager.py | 426 ++++++++++++------------ tests/test_doubles.py | 6 +- 2 files changed, 209 insertions(+), 223 deletions(-) diff --git a/tests/server/tasks/test_task_manager.py b/tests/server/tasks/test_task_manager.py index fd6194cdb..8208ca780 100644 --- a/tests/server/tasks/test_task_manager.py +++ b/tests/server/tasks/test_task_manager.py @@ -1,243 +1,217 @@ from typing import Any +from unittest.mock import AsyncMock import pytest from a2a.server.tasks import TaskManager from a2a.types import ( + Artifact, InvalidParamsError, + Message, + Part, + Role, + Task, + TaskArtifactUpdateEvent, TaskState, + TaskStatus, + TaskStatusUpdateEvent, + TextPart, ) from a2a.utils.errors import ServerError -from tests.builders import ( - ArtifactUpdateEventBuilder, - StatusUpdateEventBuilder, -) -from tests.fixtures import ( - artifact_builder, - event_queue, - http_client, - message_builder, - push_config_store, - submitted_task, - task_builder, - task_manager, - task_manager_factory, - task_store, -) -from tests.test_doubles import InMemoryTaskStore + + +MINIMAL_TASK: dict[str, Any] = { + 'id': 'task-abc', + 'context_id': 'session-xyz', + 'status': {'state': 'submitted'}, + 'kind': 'task', +} + + +@pytest.fixture +def mock_task_store() -> AsyncMock: + """Fixture for a mock TaskStore.""" + return AsyncMock() + + +@pytest.fixture +def task_manager(mock_task_store: AsyncMock) -> TaskManager: + """Fixture for a TaskManager with a mock TaskStore.""" + return TaskManager( + task_id=MINIMAL_TASK['id'], + context_id=MINIMAL_TASK['context_id'], + task_store=mock_task_store, + initial_message=None, + ) @pytest.mark.parametrize('invalid_task_id', ['', 123]) def test_task_manager_invalid_task_id( - task_store: InMemoryTaskStore, invalid_task_id: Any + mock_task_store: AsyncMock, invalid_task_id: Any ): """Test that TaskManager raises ValueError for an invalid task_id.""" with pytest.raises(ValueError, match='Task ID must be a non-empty string'): TaskManager( task_id=invalid_task_id, context_id='test_context', - task_store=task_store, + task_store=mock_task_store, initial_message=None, ) @pytest.mark.asyncio async def test_get_task_existing( - task_manager_factory, task_store: InMemoryTaskStore, submitted_task -): - """Test retrieving an existing task from the task store.""" - task_manager = task_manager_factory( - task_id=submitted_task.id, context_id=submitted_task.context_id - ) - task_store.set_task(submitted_task) - + task_manager: TaskManager, mock_task_store: AsyncMock +) -> None: + """Test getting an existing task.""" + expected_task = Task(**MINIMAL_TASK) + mock_task_store.get.return_value = expected_task retrieved_task = await task_manager.get_task() - - assert retrieved_task == submitted_task - task_store.assert_get_called(times=1) + assert retrieved_task == expected_task + mock_task_store.get.assert_called_once_with(MINIMAL_TASK['id'], None) @pytest.mark.asyncio async def test_get_task_nonexistent( - task_manager: TaskManager, task_store: InMemoryTaskStore -): - """Test retrieving a non-existent task returns None.""" + task_manager: TaskManager, mock_task_store: AsyncMock +) -> None: + """Test getting a nonexistent task.""" + mock_task_store.get.return_value = None retrieved_task = await task_manager.get_task() - assert retrieved_task is None - task_store.assert_get_called(times=1) + mock_task_store.get.assert_called_once_with(MINIMAL_TASK['id'], None) @pytest.mark.asyncio async def test_save_task_event_new_task( - task_manager_factory, task_store: InMemoryTaskStore, task_builder -): - """Test saving a new task event to the task store.""" - task = task_builder.with_id('task-abc').build() - task_manager = task_manager_factory(task_id=None, context_id=None) - + task_manager: TaskManager, mock_task_store: AsyncMock +) -> None: + """Test saving a new task.""" + task = Task(**MINIMAL_TASK) await task_manager.save_task_event(task) - - task_store.assert_save_called(times=1) - task_store.assert_saved(task.id) + mock_task_store.save.assert_called_once_with(task, None) @pytest.mark.asyncio async def test_save_task_event_status_update( - task_manager_factory, - task_store: InMemoryTaskStore, - task_builder, - message_builder, -): - """Test saving a status update event for an existing task.""" - initial_task = ( - task_builder.with_id('task-abc').with_context_id('context-xyz').build() - ) - task_store.set_task(initial_task) - task_manager = task_manager_factory( - task_id='task-abc', context_id='context-xyz' + task_manager: TaskManager, mock_task_store: AsyncMock +) -> None: + """Test saving a status update for an existing task.""" + initial_task = Task(**MINIMAL_TASK) + mock_task_store.get.return_value = initial_task + new_status = TaskStatus( + state=TaskState.working, + message=Message( + role=Role.agent, + parts=[Part(TextPart(text='content'))], + message_id='message-id', + ), ) - - status_message = message_builder.as_agent().with_text('Working...').build() - event = ( - StatusUpdateEventBuilder() - .for_task('task-abc') - .with_state(TaskState.working) - .with_message(status_message) - .build() + event = TaskStatusUpdateEvent( + task_id=MINIMAL_TASK['id'], + context_id=MINIMAL_TASK['context_id'], + status=new_status, + final=False, ) - event.context_id = 'context-xyz' - await task_manager.save_task_event(event) - - saved_task = task_store.get_saved_task('task-abc') - assert saved_task.status.state == TaskState.working - assert saved_task.status.message == status_message - assert ( - saved_task.history is None - ) # History contains previous messages, not current - task_store.assert_save_called(times=1) + updated_task = initial_task + updated_task.status = new_status + mock_task_store.save.assert_called_once_with(updated_task, None) @pytest.mark.asyncio async def test_save_task_event_artifact_update( - task_manager_factory, - task_store: InMemoryTaskStore, - task_builder, - artifact_builder, -): - """Test saving an artifact update event for an existing task.""" - initial_task = ( - task_builder.with_id('task-abc').with_context_id('context-xyz').build() + task_manager: TaskManager, mock_task_store: AsyncMock +) -> None: + """Test saving an artifact update for an existing task.""" + initial_task = Task(**MINIMAL_TASK) + mock_task_store.get.return_value = initial_task + new_artifact = Artifact( + artifact_id='artifact-id', + name='artifact1', + parts=[Part(TextPart(text='content'))], ) - task_store.set_task(initial_task) - task_manager = task_manager_factory( - task_id='task-abc', context_id='context-xyz' + event = TaskArtifactUpdateEvent( + task_id=MINIMAL_TASK['id'], + context_id=MINIMAL_TASK['context_id'], + artifact=new_artifact, ) - - new_artifact = ( - artifact_builder.with_id('artifact-id') - .with_name('artifact1') - .with_text('content') - .build() - ) - - event = ( - ArtifactUpdateEventBuilder() - .for_task('task-abc') - .with_artifact(new_artifact) - .build() - ) - event.context_id = 'context-xyz' - await task_manager.save_task_event(event) - - saved_task = task_store.get_saved_task('task-abc') - assert saved_task.artifacts == [new_artifact] - task_store.assert_save_called(times=1) + updated_task = initial_task + updated_task.artifacts = [new_artifact] + mock_task_store.save.assert_called_once_with(updated_task, None) @pytest.mark.asyncio async def test_save_task_event_metadata_update( - task_manager_factory, task_store: InMemoryTaskStore, task_builder -): - """Test saving a metadata update event for an existing task.""" - initial_task = ( - task_builder.with_id('task-abc').with_context_id('context-xyz').build() + task_manager: TaskManager, mock_task_store: AsyncMock +) -> None: + """Test saving an updated metadata for an existing task.""" + initial_task = Task(**MINIMAL_TASK) + mock_task_store.get.return_value = initial_task + new_metadata = {'meta_key_test': 'meta_value_test'} + + event = TaskStatusUpdateEvent( + task_id=MINIMAL_TASK['id'], + context_id=MINIMAL_TASK['context_id'], + metadata=new_metadata, + status=TaskStatus(state=TaskState.working), + final=False, ) - task_store.set_task(initial_task) - task_manager = task_manager_factory( - task_id='task-abc', context_id='context-xyz' - ) - - event = ( - StatusUpdateEventBuilder() - .for_task('task-abc') - .with_state(TaskState.working) - .with_metadata(meta_key_test='meta_value_test') - .build() - ) - event.context_id = 'context-xyz' - await task_manager.save_task_event(event) - saved_task = task_store.get_saved_task('task-abc') - assert saved_task.metadata == {'meta_key_test': 'meta_value_test'} + updated_task = mock_task_store.save.call_args.args[0] + assert updated_task.metadata == new_metadata @pytest.mark.asyncio async def test_ensure_task_existing( - task_manager_factory, task_store: InMemoryTaskStore, submitted_task -): - """Test ensuring a task that already exists in the store.""" - task_store.set_task(submitted_task) - task_manager = task_manager_factory( - task_id=submitted_task.id, context_id=submitted_task.context_id - ) - - event = ( - StatusUpdateEventBuilder() - .for_task(submitted_task.id) - .with_state(TaskState.working) - .build() + task_manager: TaskManager, mock_task_store: AsyncMock +) -> None: + """Test ensuring an existing task.""" + expected_task = Task(**MINIMAL_TASK) + mock_task_store.get.return_value = expected_task + event = TaskStatusUpdateEvent( + task_id=MINIMAL_TASK['id'], + context_id=MINIMAL_TASK['context_id'], + status=TaskStatus(state=TaskState.working), + final=False, ) - retrieved_task = await task_manager.ensure_task(event) - - assert retrieved_task.id == submitted_task.id - assert retrieved_task.status.state == submitted_task.status.state - task_store.assert_get_called(times=1) + assert retrieved_task == expected_task + mock_task_store.get.assert_called_once_with(MINIMAL_TASK['id'], None) @pytest.mark.asyncio async def test_ensure_task_nonexistent( - task_store: InMemoryTaskStore, task_manager_factory -): - """Test ensuring a task that does not exist creates a new one.""" - task_manager = task_manager_factory(task_id=None, context_id=None) - - event = ( - StatusUpdateEventBuilder() - .for_task('new-task') - .with_state(TaskState.submitted) - .build() + mock_task_store: AsyncMock, +) -> None: + """Test ensuring a nonexistent task (creates a new one).""" + mock_task_store.get.return_value = None + task_manager_without_id = TaskManager( + task_id=None, + context_id=None, + task_store=mock_task_store, + initial_message=None, ) - event.context_id = 'some-context' - - new_task = await task_manager.ensure_task(event) - + event = TaskStatusUpdateEvent( + task_id='new-task', + context_id='some-context', + status=TaskStatus(state=TaskState.submitted), + final=False, + ) + new_task = await task_manager_without_id.ensure_task(event) assert new_task.id == 'new-task' assert new_task.context_id == 'some-context' assert new_task.status.state == TaskState.submitted - task_store.assert_save_called(times=1) - assert task_manager.task_id == 'new-task' - assert task_manager.context_id == 'some-context' - + mock_task_store.save.assert_called_once_with(new_task, None) + assert task_manager_without_id.task_id == 'new-task' + assert task_manager_without_id.context_id == 'some-context' -def test_init_task_obj(task_manager: TaskManager): - """Test initializing a new task object with default values.""" - new_task = task_manager._init_task_obj('new-task', 'new-context') +def test_init_task_obj(task_manager: TaskManager) -> None: + """Test initializing a new task object.""" + new_task = task_manager._init_task_obj('new-task', 'new-context') # type: ignore assert new_task.id == 'new-task' assert new_task.context_id == 'new-context' assert new_task.status.state == TaskState.submitted @@ -246,22 +220,24 @@ def test_init_task_obj(task_manager: TaskManager): @pytest.mark.asyncio async def test_save_task( - task_manager: TaskManager, task_store: InMemoryTaskStore, submitted_task -): - """Test saving a task directly to the task store.""" - await task_manager._save_task(submitted_task) - - task_store.assert_save_called(times=1) - task_store.assert_saved(submitted_task.id) + task_manager: TaskManager, mock_task_store: AsyncMock +) -> None: + """Test saving a task.""" + task = Task(**MINIMAL_TASK) + await task_manager._save_task(task) # type: ignore + mock_task_store.save.assert_called_once_with(task, None) @pytest.mark.asyncio async def test_save_task_event_mismatched_id_raises_error( - task_manager: TaskManager, task_builder -): - """Test that saving a task with mismatched ID raises an error.""" - mismatched_task = ( - task_builder.with_id('wrong-id').with_context_id('session-xyz').build() + task_manager: TaskManager, +) -> None: + """Test that save_task_event raises ServerError on task ID mismatch.""" + # The task_manager is initialized with 'task-abc' + mismatched_task = Task( + id='wrong-id', + context_id='session-xyz', + status=TaskStatus(state=TaskState.submitted), ) with pytest.raises(ServerError) as exc_info: @@ -271,61 +247,71 @@ async def test_save_task_event_mismatched_id_raises_error( @pytest.mark.asyncio async def test_save_task_event_new_task_no_task_id( - task_store: InMemoryTaskStore, task_manager_factory, task_builder -): - """Test saving a new task event when task manager has no task_id.""" - task_manager = task_manager_factory(task_id=None, context_id=None) - - task = ( - task_builder.with_id('new-task-id') - .with_context_id('some-context') - .with_state(TaskState.working) - .build() + mock_task_store: AsyncMock, +) -> None: + """Test saving a task event without task id in TaskManager.""" + task_manager_without_id = TaskManager( + task_id=None, + context_id=None, + task_store=mock_task_store, + initial_message=None, ) - - await task_manager.save_task_event(task) - - task_store.assert_save_called(times=1) - task_store.assert_saved(task.id) - assert task_manager.task_id == 'new-task-id' - assert task_manager.context_id == 'some-context' + task_data: dict[str, Any] = { + 'id': 'new-task-id', + 'context_id': 'some-context', + 'status': {'state': 'working'}, + 'kind': 'task', + } + task = Task(**task_data) + await task_manager_without_id.save_task_event(task) + mock_task_store.save.assert_called_once_with(task, None) + assert task_manager_without_id.task_id == 'new-task-id' + assert task_manager_without_id.context_id == 'some-context' + # initial submit should be updated to working assert task.status.state == TaskState.working @pytest.mark.asyncio async def test_get_task_no_task_id( - task_store: InMemoryTaskStore, task_manager_factory -): - """Test get_task returns None when task manager has no task_id.""" - task_manager = task_manager_factory(task_id=None, context_id='some-context') - - retrieved_task = await task_manager.get_task() - + mock_task_store: AsyncMock, +) -> None: + """Test getting a task when task_id is not set in TaskManager.""" + task_manager_without_id = TaskManager( + task_id=None, + context_id='some-context', + task_store=mock_task_store, + initial_message=None, + ) + retrieved_task = await task_manager_without_id.get_task() assert retrieved_task is None - task_store.assert_get_called(times=0) + mock_task_store.get.assert_not_called() @pytest.mark.asyncio async def test_save_task_event_no_task_existing( - task_store: InMemoryTaskStore, task_manager_factory -): - """Test saving an event when no task exists creates a new task.""" - task_manager = task_manager_factory(task_id=None, context_id=None) - - event = ( - StatusUpdateEventBuilder() - .for_task('event-task-id') - .with_state(TaskState.completed) - .as_final() - .build() + mock_task_store: AsyncMock, +) -> None: + """Test saving an event when no task exists and task_id is not set.""" + task_manager_without_id = TaskManager( + task_id=None, + context_id=None, + task_store=mock_task_store, + initial_message=None, ) - event.context_id = 'some-context' - - await task_manager.save_task_event(event) - - saved_task = task_store.get_saved_task('event-task-id') + mock_task_store.get.return_value = None + event = TaskStatusUpdateEvent( + task_id='event-task-id', + context_id='some-context', + status=TaskStatus(state=TaskState.completed), + final=True, + ) + await task_manager_without_id.save_task_event(event) + # Check if a new task was created and saved + call_args = mock_task_store.save.call_args + assert call_args is not None + saved_task = call_args[0][0] assert saved_task.id == 'event-task-id' assert saved_task.context_id == 'some-context' assert saved_task.status.state == TaskState.completed - assert task_manager.task_id == 'event-task-id' - assert task_manager.context_id == 'some-context' + assert task_manager_without_id.task_id == 'event-task-id' + assert task_manager_without_id.context_id == 'some-context' diff --git a/tests/test_doubles.py b/tests/test_doubles.py index 8a6cd48e2..d52939ba7 100644 --- a/tests/test_doubles.py +++ b/tests/test_doubles.py @@ -16,15 +16,15 @@ def __init__(self): self._get_count = 0 self._delete_count = 0 - async def save(self, task: Task) -> None: + async def save(self, task: Task, context: Any = None) -> None: self._save_count += 1 self._tasks[task.id] = task - async def get(self, task_id: str) -> Task | None: + async def get(self, task_id: str, context: Any = None) -> Task | None: self._get_count += 1 return self._tasks.get(task_id) - async def delete(self, task_id: str) -> None: + async def delete(self, task_id: str, context: Any = None) -> None: self._delete_count += 1 self._tasks.pop(task_id, None) From bd566319b8073192427928b9561c2efd84cddf12 Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Wed, 10 Sep 2025 10:43:41 -0400 Subject: [PATCH 06/12] Update tests/fixtures.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/fixtures.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 1ef618ef6..7eb4de29d 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -66,14 +66,15 @@ def completed_task(task_builder): @pytest.fixture -def task_with_history(task_builder, message_builder): +def task_with_history(task_builder): messages = [ - message_builder.as_user().with_text('Hello').build(), - message_builder.as_agent().with_text('Hi there!').build(), + MessageBuilder().as_user().with_text('Hello').build(), + MessageBuilder().as_agent().with_text('Hi there!').build(), ] return task_builder.with_history(*messages).build() + @pytest.fixture def task_with_artifacts(task_builder, artifact_builder): artifacts = [ From 3af518afb96b62cdfc4f5b1de97485ae4759e64a Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Wed, 10 Sep 2025 10:43:51 -0400 Subject: [PATCH 07/12] Update tests/builders.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/builders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/builders.py b/tests/builders.py index 1aac65f2f..043f7a4df 100644 --- a/tests/builders.py +++ b/tests/builders.py @@ -25,10 +25,11 @@ class TaskBuilder: history: list[Message] = field(default_factory=list) metadata: dict[str, Any] = field(default_factory=dict) - def with_id(self, id: str) -> 'TaskBuilder': - self.id = id + def with_id(self, task_id: str) -> 'TaskBuilder': + self.id = task_id return self + def with_context_id(self, context_id: str) -> 'TaskBuilder': self.context_id = context_id return self From 4928889d02c0eb38f5ef0ca6fcad78f222c92ee1 Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Wed, 10 Sep 2025 10:44:11 -0400 Subject: [PATCH 08/12] Update tests/builders.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/builders.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/builders.py b/tests/builders.py index 043f7a4df..e723bc2ee 100644 --- a/tests/builders.py +++ b/tests/builders.py @@ -200,12 +200,14 @@ def as_last_chunk(self) -> 'ArtifactUpdateEventBuilder': return self def build(self) -> TaskArtifactUpdateEvent: - if not self.artifact: - self.artifact = ArtifactBuilder().build() + artifact = self.artifact + if not artifact: + artifact = ArtifactBuilder().build() return TaskArtifactUpdateEvent( task_id=self.task_id, context_id=self.context_id, - artifact=self.artifact, + artifact=artifact, append=self.append, last_chunk=self.last_chunk, ) + From 2e0d293a6c1d6b9ebb109010d200d0121ce40d05 Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Wed, 10 Sep 2025 10:44:34 -0400 Subject: [PATCH 09/12] Update tests/fixtures.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/fixtures.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 7eb4de29d..a6c24312c 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -108,12 +108,13 @@ def factory(task_id=None, context_id=None, initial_message=None): @pytest.fixture -def populated_task_store(task_store, task_builder): +def populated_task_store(task_store): tasks = [ - task_builder.with_id('task-1').with_state(TaskState.submitted).build(), - task_builder.with_id('task-2').with_state(TaskState.working).build(), - task_builder.with_id('task-3').with_state(TaskState.completed).build(), + TaskBuilder().with_id('task-1').with_state(TaskState.submitted).build(), + TaskBuilder().with_id('task-2').with_state(TaskState.working).build(), + TaskBuilder().with_id('task-3').with_state(TaskState.completed).build(), ] for task in tasks: task_store.set_task(task) return task_store + From 40656b26ed363bd067f3b39f5c6c5fb72bf5502f Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Wed, 10 Sep 2025 10:45:40 -0400 Subject: [PATCH 10/12] Update tests/fixtures.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/fixtures.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index a6c24312c..5f4100be0 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -76,14 +76,15 @@ def task_with_history(task_builder): @pytest.fixture -def task_with_artifacts(task_builder, artifact_builder): +def task_with_artifacts(task_builder): artifacts = [ - artifact_builder.with_id('art1').with_name('file.txt').build(), - artifact_builder.with_id('art2').with_name('data.json').build(), + ArtifactBuilder().with_id('art1').with_name('file.txt').build(), + ArtifactBuilder().with_id('art2').with_name('data.json').build(), ] return task_builder.with_artifacts(*artifacts).build() + @pytest.fixture def task_manager(task_store): return TaskManager( From c7f156db286f82c7d92848d84bd5cd3cf0955095 Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Wed, 10 Sep 2025 10:52:58 -0400 Subject: [PATCH 11/12] ruff --- tests/builders.py | 2 -- tests/fixtures.py | 3 --- 2 files changed, 5 deletions(-) diff --git a/tests/builders.py b/tests/builders.py index e723bc2ee..c1317242b 100644 --- a/tests/builders.py +++ b/tests/builders.py @@ -29,7 +29,6 @@ def with_id(self, task_id: str) -> 'TaskBuilder': self.id = task_id return self - def with_context_id(self, context_id: str) -> 'TaskBuilder': self.context_id = context_id return self @@ -210,4 +209,3 @@ def build(self) -> TaskArtifactUpdateEvent: append=self.append, last_chunk=self.last_chunk, ) - diff --git a/tests/fixtures.py b/tests/fixtures.py index 5f4100be0..19d75f7b1 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -74,7 +74,6 @@ def task_with_history(task_builder): return task_builder.with_history(*messages).build() - @pytest.fixture def task_with_artifacts(task_builder): artifacts = [ @@ -84,7 +83,6 @@ def task_with_artifacts(task_builder): return task_builder.with_artifacts(*artifacts).build() - @pytest.fixture def task_manager(task_store): return TaskManager( @@ -118,4 +116,3 @@ def populated_task_store(task_store): for task in tasks: task_store.set_task(task) return task_store - From 2aba8b861fb904350a6714f665722aa9b31d8629 Mon Sep 17 00:00:00 2001 From: Andrew Hoblitzell Date: Thu, 18 Sep 2025 09:51:02 -0400 Subject: [PATCH 12/12] linter --- tests/integration/test_client_server_integration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/test_client_server_integration.py b/tests/integration/test_client_server_integration.py index 88d4d3d11..323ff00fd 100644 --- a/tests/integration/test_client_server_integration.py +++ b/tests/integration/test_client_server_integration.py @@ -1,4 +1,5 @@ import asyncio + from collections.abc import AsyncGenerator from typing import NamedTuple from unittest.mock import ANY, AsyncMock @@ -7,6 +8,7 @@ import httpx import pytest import pytest_asyncio + from grpc.aio import Channel from a2a.client.transports import JsonRpcTransport, RestTransport @@ -36,6 +38,7 @@ TransportProtocol, ) + # --- Test Constants --- TASK_FROM_STREAM = Task(