From 6f0f71df1bbeb291059fd1391f9e66eb5bd0d445 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:37:13 +0000 Subject: [PATCH 01/18] ci: run mandatory and capabilities TCK tests for JSON-RPC transport --- .github/workflows/run-tck.yaml | 112 +++++++++++++++++++ tck/sut_agent.py | 189 +++++++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 .github/workflows/run-tck.yaml create mode 100644 tck/sut_agent.py diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml new file mode 100644 index 000000000..2e981a371 --- /dev/null +++ b/.github/workflows/run-tck.yaml @@ -0,0 +1,112 @@ +name: Run TCK + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + paths-ignore: + - '**.md' + - 'LICENSE' + - '.github/CODEOWNERS' + +env: + TCK_VERSION: 0.3.0.beta3 + SUT_BASE_URL: http://localhost:41241 + SUT_JSONRPC_URL: http://localhost:41241/a2a/jsonrpc + UV_SYSTEM_PYTHON: 1 + TCK_STREAMING_TIMEOUT: 5.0 + +concurrency: + group: '${{ github.workflow }} @ ${{ github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + tck-test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - name: Checkout a2a-python + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install Dependencies + run: uv sync --all-extras --locked + + - name: Checkout a2a-tck + uses: actions/checkout@v4 + with: + repository: a2aproject/a2a-tck + path: tck/a2a-tck + ref: ${{ env.TCK_VERSION }} + + - name: Install TCK dependencies + run: | + cd tck/a2a-tck + pip install uv + uv pip install -e . + + - name: Start SUT + run: | + uv run tck/sut_agent.py & + env: + HTTP_PORT: 41241 + + - name: Wait for SUT to start + run: | + URL="${{ env.SUT_BASE_URL }}/.well-known/agent-card.json" + EXPECTED_STATUS=200 + TIMEOUT=120 + RETRY_INTERVAL=2 + START_TIME=$(date +%s) + + while true; do + CURRENT_TIME=$(date +%s) + ELAPSED_TIME=$((CURRENT_TIME - START_TIME)) + + if [ "$ELAPSED_TIME" -ge "$TIMEOUT" ]; then + echo "❌ Timeout: Server did not respond with status $EXPECTED_STATUS within $TIMEOUT seconds." + exit 1 + fi + + HTTP_STATUS=$(curl --output /dev/null --silent --write-out "%{http_code}" "$URL") || true + echo "STATUS: ${HTTP_STATUS}" + + if [ "$HTTP_STATUS" -eq "$EXPECTED_STATUS" ]; then + echo "✅ Server is up! Received status $HTTP_STATUS after $ELAPSED_TIME seconds." + break; + fi + + echo "⏳ Server not ready (status: $HTTP_STATUS). Retrying in $RETRY_INTERVAL seconds..." + sleep "$RETRY_INTERVAL" + done + + - name: Run TCK (mandatory) + id: run-tck-mandatory + timeout-minutes: 5 + run: | + ./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category mandatory --transports jsonrpc + working-directory: tck/a2a-tck + + - name: Run TCK (capabilities) + id: run-tck-capabilities + timeout-minutes: 5 + run: | + ./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category capabilities --transports jsonrpc + working-directory: tck/a2a-tck + + - name: Stop SUT + if: always() + run: | + pkill -f sut_agent.py || true + sleep 2 diff --git a/tck/sut_agent.py b/tck/sut_agent.py new file mode 100644 index 000000000..084d7bd8b --- /dev/null +++ b/tck/sut_agent.py @@ -0,0 +1,189 @@ + +import asyncio +import logging +import os +import uuid +from datetime import datetime, timezone + +from datetime import datetime, timezone + +from fastapi import FastAPI +from uvicorn import Config, Server + + +from a2a.server.agent_execution.agent_executor import AgentExecutor +from a2a.server.agent_execution.context import RequestContext +from a2a.server.events.event_queue import EventQueue +from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager +from a2a.server.request_handlers.default_request_handler import DefaultRequestHandler +from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication +from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication +from a2a.types import ( + AgentCard, + AgentCapabilities, + AgentProvider, + Message, + TextPart, + Task, + TaskState, + TaskStatus, + TaskStatusUpdateEvent, +) +from a2a.auth.user import UnauthenticatedUser +from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("SUTAgent") + +class SUTAgentExecutor(AgentExecutor): + def __init__(self): + self.running_tasks = set() + self.last_context_id = None + + async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: + api_task_id = context.task_id + if api_task_id in self.running_tasks: + self.running_tasks.remove(api_task_id) + + status_update = TaskStatusUpdateEvent( + task_id=api_task_id, + context_id=self.last_context_id or str(uuid.uuid4()), + status=TaskStatus( + state=TaskState.canceled, + timestamp=datetime.now(timezone.utc).isoformat(), + ), + final=True, + ) + await event_queue.enqueue_event(status_update) + + async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: + user_message = context.message + task_id = context.task_id + context_id = context.context_id + self.last_context_id = context_id + + self.running_tasks.add(task_id) + + logger.info( + f"[SUTAgentExecutor] Processing message {user_message.message_id} " + f"for task {task_id} (context: {context_id})" + ) + + working_status = TaskStatusUpdateEvent( + task_id=task_id, + context_id=context_id, + status=TaskStatus( + state=TaskState.working, + message=Message( + role="agent", + message_id=str(uuid.uuid4()), + parts=[TextPart(text="Processing your question")], + task_id=task_id, + context_id=context_id, + ), + timestamp=datetime.now(timezone.utc).isoformat(), + ), + final=False, + ) + await event_queue.enqueue_event(working_status) + + agent_reply_text = "Hello world!" + await asyncio.sleep(3) # Simulate processing delay + + if task_id not in self.running_tasks: + logger.info(f"Task {task_id} was cancelled.") + return + + logger.info(f"[SUTAgentExecutor] Response: {agent_reply_text}") + + agent_message = Message( + role="agent", + message_id=str(uuid.uuid4()), + parts=[TextPart(text=agent_reply_text)], + task_id=task_id, + context_id=context_id, + ) + + final_update = TaskStatusUpdateEvent( + task_id=task_id, + context_id=context_id, + status=TaskStatus( + state=TaskState.input_required, + message=agent_message, + timestamp=datetime.now(timezone.utc).isoformat(), + ), + final=True, + ) + await event_queue.enqueue_event(final_update) + + + +async def main(): + HTTP_PORT = int(os.environ.get("HTTP_PORT", 41241)) + + # 1. Setup Executor and Handlers + agent_executor = SUTAgentExecutor() + task_store = InMemoryTaskStore() + queue_manager = InMemoryQueueManager() + + request_handler = DefaultRequestHandler( + task_store=task_store, + queue_manager=queue_manager, + agent_executor=agent_executor, + ) + + # 2. Create Agent Card (JSON-RPC only) + sut_agent_card = AgentCard( + name="SUT Agent", + description="A sample agent to be used as SUT against tck tests.", + url=f"http://localhost:{HTTP_PORT}/a2a/jsonrpc", + provider=AgentProvider( + organization="A2A Samples", + url="https://example.com/a2a-samples", + ), + version="1.0.0", + protocol_version="0.3.0", + capabilities=AgentCapabilities( + streaming=True, + push_notifications=False, + state_transition_history=True, + ), + default_input_modes=["text"], + default_output_modes=["text", "task-status"], + skills=[ + { + "id": "sut_agent", + "name": "SUT Agent", + "description": "Simulate the general flow of a streaming agent.", + "tags": ["sut"], + "examples": ["hi", "hello world", "how are you", "goodbye"], + "input_modes": ["text"], + "output_modes": ["text", "task-status"], + } + ], + supports_authenticated_extended_card=False, + preferred_transport="JSONRPC", + additional_interfaces=[ + {"url": f"http://localhost:{HTTP_PORT}/a2a/jsonrpc", "transport": "JSONRPC"}, + ], + ) + + # 3. Setup HTTP App + json_rpc_app = A2AFastAPIApplication( + agent_card=sut_agent_card, + http_handler=request_handler, + ) + app = json_rpc_app.build( + rpc_url="/a2a/jsonrpc", + agent_card_url="/.well-known/agent-card.json" + ) + + logger.info(f"Starting HTTP server on port {HTTP_PORT}...") + config = Config(app, host="0.0.0.0", port=HTTP_PORT, log_level="info") + server = Server(config) + + await server.serve() + +if __name__ == "__main__": + asyncio.run(main()) From 2a7d74cf9c1576db33ac2720c7b1d62df6257f31 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:39:56 +0000 Subject: [PATCH 02/18] Reformat --- tck/sut_agent.py | 93 +++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 084d7bd8b..cd9b00faf 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -1,4 +1,3 @@ - import asyncio import logging import os @@ -15,7 +14,9 @@ from a2a.server.agent_execution.context import RequestContext from a2a.server.events.event_queue import EventQueue from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager -from a2a.server.request_handlers.default_request_handler import DefaultRequestHandler +from a2a.server.request_handlers.default_request_handler import ( + DefaultRequestHandler, +) from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication from a2a.types import ( @@ -34,14 +35,17 @@ # Configure logging logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("SUTAgent") +logger = logging.getLogger('SUTAgent') + class SUTAgentExecutor(AgentExecutor): def __init__(self): self.running_tasks = set() self.last_context_id = None - async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: + async def cancel( + self, context: RequestContext, event_queue: EventQueue + ) -> None: api_task_id = context.task_id if api_task_id in self.running_tasks: self.running_tasks.remove(api_task_id) @@ -57,7 +61,9 @@ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None ) await event_queue.enqueue_event(status_update) - async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: + async def execute( + self, context: RequestContext, event_queue: EventQueue + ) -> None: user_message = context.message task_id = context.task_id context_id = context.context_id @@ -66,8 +72,8 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non self.running_tasks.add(task_id) logger.info( - f"[SUTAgentExecutor] Processing message {user_message.message_id} " - f"for task {task_id} (context: {context_id})" + f'[SUTAgentExecutor] Processing message {user_message.message_id} ' + f'for task {task_id} (context: {context_id})' ) working_status = TaskStatusUpdateEvent( @@ -76,9 +82,9 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non status=TaskStatus( state=TaskState.working, message=Message( - role="agent", + role='agent', message_id=str(uuid.uuid4()), - parts=[TextPart(text="Processing your question")], + parts=[TextPart(text='Processing your question')], task_id=task_id, context_id=context_id, ), @@ -88,17 +94,17 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non ) await event_queue.enqueue_event(working_status) - agent_reply_text = "Hello world!" + agent_reply_text = 'Hello world!' await asyncio.sleep(3) # Simulate processing delay if task_id not in self.running_tasks: - logger.info(f"Task {task_id} was cancelled.") + logger.info(f'Task {task_id} was cancelled.') return - logger.info(f"[SUTAgentExecutor] Response: {agent_reply_text}") + logger.info(f'[SUTAgentExecutor] Response: {agent_reply_text}') agent_message = Message( - role="agent", + role='agent', message_id=str(uuid.uuid4()), parts=[TextPart(text=agent_reply_text)], task_id=task_id, @@ -118,72 +124,71 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non await event_queue.enqueue_event(final_update) - async def main(): - HTTP_PORT = int(os.environ.get("HTTP_PORT", 41241)) - - # 1. Setup Executor and Handlers + HTTP_PORT = int(os.environ.get('HTTP_PORT', 41241)) + agent_executor = SUTAgentExecutor() task_store = InMemoryTaskStore() queue_manager = InMemoryQueueManager() - + request_handler = DefaultRequestHandler( task_store=task_store, queue_manager=queue_manager, agent_executor=agent_executor, ) - # 2. Create Agent Card (JSON-RPC only) sut_agent_card = AgentCard( - name="SUT Agent", - description="A sample agent to be used as SUT against tck tests.", - url=f"http://localhost:{HTTP_PORT}/a2a/jsonrpc", + name='SUT Agent', + description='A sample agent to be used as SUT against tck tests.', + url=f'http://localhost:{HTTP_PORT}/a2a/jsonrpc', provider=AgentProvider( - organization="A2A Samples", - url="https://example.com/a2a-samples", + organization='A2A Samples', + url='https://example.com/a2a-samples', ), - version="1.0.0", - protocol_version="0.3.0", + version='1.0.0', + protocol_version='0.3.0', capabilities=AgentCapabilities( streaming=True, push_notifications=False, state_transition_history=True, ), - default_input_modes=["text"], - default_output_modes=["text", "task-status"], + default_input_modes=['text'], + default_output_modes=['text', 'task-status'], skills=[ { - "id": "sut_agent", - "name": "SUT Agent", - "description": "Simulate the general flow of a streaming agent.", - "tags": ["sut"], - "examples": ["hi", "hello world", "how are you", "goodbye"], - "input_modes": ["text"], - "output_modes": ["text", "task-status"], + 'id': 'sut_agent', + 'name': 'SUT Agent', + 'description': 'Simulate the general flow of a streaming agent.', + 'tags': ['sut'], + 'examples': ['hi', 'hello world', 'how are you', 'goodbye'], + 'input_modes': ['text'], + 'output_modes': ['text', 'task-status'], } ], supports_authenticated_extended_card=False, - preferred_transport="JSONRPC", + preferred_transport='JSONRPC', additional_interfaces=[ - {"url": f"http://localhost:{HTTP_PORT}/a2a/jsonrpc", "transport": "JSONRPC"}, + { + 'url': f'http://localhost:{HTTP_PORT}/a2a/jsonrpc', + 'transport': 'JSONRPC', + }, ], ) - # 3. Setup HTTP App json_rpc_app = A2AFastAPIApplication( agent_card=sut_agent_card, http_handler=request_handler, ) app = json_rpc_app.build( - rpc_url="/a2a/jsonrpc", - agent_card_url="/.well-known/agent-card.json" + rpc_url='/a2a/jsonrpc', agent_card_url='/.well-known/agent-card.json' ) - logger.info(f"Starting HTTP server on port {HTTP_PORT}...") - config = Config(app, host="0.0.0.0", port=HTTP_PORT, log_level="info") + logger.info(f'Starting HTTP server on port {HTTP_PORT}...') + config = Config(app, host='0.0.0.0', port=HTTP_PORT, log_level='info') server = Server(config) - + await server.serve() -if __name__ == "__main__": + +if __name__ == '__main__': asyncio.run(main()) From f9e3c1af5e110632523939f9432a89df9a633d50 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 12:40:16 +0100 Subject: [PATCH 03/18] Potential fix for code scanning alert no. 1: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/run-tck.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 2e981a371..7885c24ba 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -10,6 +10,9 @@ on: - 'LICENSE' - '.github/CODEOWNERS' +permissions: + contents: read + env: TCK_VERSION: 0.3.0.beta3 SUT_BASE_URL: http://localhost:41241 From 53f0ca75e6d78b8752cb880a6bd6725fe209027f Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:42:30 +0000 Subject: [PATCH 04/18] Fix run-tck.yaml syntax --- .github/workflows/run-tck.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 2e981a371..9c5af1281 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -108,5 +108,5 @@ jobs: - name: Stop SUT if: always() run: | - pkill -f sut_agent.py || true + pkill -f sut_agent.py || true sleep 2 From 08a5bc00c05b99b4afc9ceb1ecb5836749658ea3 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:48:49 +0000 Subject: [PATCH 05/18] Fixes --- .github/workflows/run-tck.yaml | 2 +- tck/__init__.py | 0 tck/sut_agent.py | 45 ++++++++++++++++++---------------- 3 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 tck/__init__.py diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 1e5d25991..92e2e0602 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -44,7 +44,7 @@ jobs: run: uv python install ${{ matrix.python-version }} - name: Install Dependencies - run: uv sync --all-extras --locked + run: uv sync --all-extras - name: Checkout a2a-tck uses: actions/checkout@v4 diff --git a/tck/__init__.py b/tck/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tck/sut_agent.py b/tck/sut_agent.py index cd9b00faf..93055e5d5 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -2,36 +2,31 @@ import logging import os import uuid -from datetime import datetime, timezone from datetime import datetime, timezone -from fastapi import FastAPI from uvicorn import Config, Server - from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext +from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication from a2a.server.events.event_queue import EventQueue from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager from a2a.server.request_handlers.default_request_handler import ( DefaultRequestHandler, ) -from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication -from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication +from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore from a2a.types import ( - AgentCard, AgentCapabilities, + AgentCard, AgentProvider, Message, - TextPart, - Task, TaskState, TaskStatus, TaskStatusUpdateEvent, + TextPart, ) -from a2a.auth.user import UnauthenticatedUser -from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore + # Configure logging logging.basicConfig(level=logging.INFO) @@ -39,13 +34,17 @@ class SUTAgentExecutor(AgentExecutor): - def __init__(self): + """Execution logic for the SUT agent.""" + + def __init__(self) -> None: + """Initializes the SUT agent executor.""" self.running_tasks = set() self.last_context_id = None async def cancel( self, context: RequestContext, event_queue: EventQueue ) -> None: + """Cancels a task.""" api_task_id = context.task_id if api_task_id in self.running_tasks: self.running_tasks.remove(api_task_id) @@ -64,6 +63,7 @@ async def cancel( async def execute( self, context: RequestContext, event_queue: EventQueue ) -> None: + """Executes a task.""" user_message = context.message task_id = context.task_id context_id = context.context_id @@ -72,8 +72,10 @@ async def execute( self.running_tasks.add(task_id) logger.info( - f'[SUTAgentExecutor] Processing message {user_message.message_id} ' - f'for task {task_id} (context: {context_id})' + '[SUTAgentExecutor] Processing message %s for task %s (context: %s)', + user_message.message_id, + task_id, + context_id, ) working_status = TaskStatusUpdateEvent( @@ -98,10 +100,10 @@ async def execute( await asyncio.sleep(3) # Simulate processing delay if task_id not in self.running_tasks: - logger.info(f'Task {task_id} was cancelled.') + logger.info('Task %s was cancelled.', task_id) return - logger.info(f'[SUTAgentExecutor] Response: {agent_reply_text}') + logger.info('[SUTAgentExecutor] Response: %s', agent_reply_text) agent_message = Message( role='agent', @@ -124,8 +126,9 @@ async def execute( await event_queue.enqueue_event(final_update) -async def main(): - HTTP_PORT = int(os.environ.get('HTTP_PORT', 41241)) +async def main() -> None: + """Main entrypoint.""" + http_port = int(os.environ.get('HTTP_PORT', '41241')) agent_executor = SUTAgentExecutor() task_store = InMemoryTaskStore() @@ -140,7 +143,7 @@ async def main(): sut_agent_card = AgentCard( name='SUT Agent', description='A sample agent to be used as SUT against tck tests.', - url=f'http://localhost:{HTTP_PORT}/a2a/jsonrpc', + url=f'http://localhost:{http_port}/a2a/jsonrpc', provider=AgentProvider( organization='A2A Samples', url='https://example.com/a2a-samples', @@ -169,7 +172,7 @@ async def main(): preferred_transport='JSONRPC', additional_interfaces=[ { - 'url': f'http://localhost:{HTTP_PORT}/a2a/jsonrpc', + 'url': f'http://localhost:{http_port}/a2a/jsonrpc', 'transport': 'JSONRPC', }, ], @@ -183,8 +186,8 @@ async def main(): rpc_url='/a2a/jsonrpc', agent_card_url='/.well-known/agent-card.json' ) - logger.info(f'Starting HTTP server on port {HTTP_PORT}...') - config = Config(app, host='0.0.0.0', port=HTTP_PORT, log_level='info') + logger.info('Starting HTTP server on port %s...', http_port) + config = Config(app, host='127.0.0.1', port=http_port, log_level='info') server = Server(config) await server.serve() From 4d90cf059c9fdeeda4829042cfb76e0d89325111 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:52:29 +0000 Subject: [PATCH 06/18] Fixes --- .github/workflows/run-tck.yaml | 4 ++-- tck/sut_agent.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 92e2e0602..8823938d0 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -53,11 +53,11 @@ jobs: path: tck/a2a-tck ref: ${{ env.TCK_VERSION }} - - name: Install TCK dependencies + - name: Install uv and Python dependencies run: | - cd tck/a2a-tck pip install uv uv pip install -e . + working-directory: tck/a2a-tck - name: Start SUT run: | diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 93055e5d5..03dc29518 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -39,7 +39,6 @@ class SUTAgentExecutor(AgentExecutor): def __init__(self) -> None: """Initializes the SUT agent executor.""" self.running_tasks = set() - self.last_context_id = None async def cancel( self, context: RequestContext, event_queue: EventQueue @@ -51,7 +50,7 @@ async def cancel( status_update = TaskStatusUpdateEvent( task_id=api_task_id, - context_id=self.last_context_id or str(uuid.uuid4()), + context_id=context.context_id or str(uuid.uuid4()), status=TaskStatus( state=TaskState.canceled, timestamp=datetime.now(timezone.utc).isoformat(), @@ -67,7 +66,6 @@ async def execute( user_message = context.message task_id = context.task_id context_id = context.context_id - self.last_context_id = context_id self.running_tasks.add(task_id) From b849c28618dead57ce19161f47d799a0140f6c84 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:53:35 +0000 Subject: [PATCH 07/18] Remove extra step --- .github/workflows/run-tck.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 8823938d0..c08fefb09 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -53,12 +53,6 @@ jobs: path: tck/a2a-tck ref: ${{ env.TCK_VERSION }} - - name: Install uv and Python dependencies - run: | - pip install uv - uv pip install -e . - working-directory: tck/a2a-tck - - name: Start SUT run: | uv run tck/sut_agent.py & From 5f19010f384616d67055bf1844831620016e34a0 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:54:52 +0000 Subject: [PATCH 08/18] Use uv run for TCK --- .github/workflows/run-tck.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index c08fefb09..695a5c415 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -92,7 +92,7 @@ jobs: id: run-tck-mandatory timeout-minutes: 5 run: | - ./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category mandatory --transports jsonrpc + uv run run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category mandatory --transports jsonrpc working-directory: tck/a2a-tck - name: Run TCK (capabilities) From da8acbace1a98c1a4db54339255ec2832e55d8f9 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:56:37 +0000 Subject: [PATCH 09/18] Use uv run for capabilities as well --- .github/workflows/run-tck.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 695a5c415..957df2ddc 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -99,7 +99,7 @@ jobs: id: run-tck-capabilities timeout-minutes: 5 run: | - ./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category capabilities --transports jsonrpc + uv run run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category capabilities --transports jsonrpc working-directory: tck/a2a-tck - name: Stop SUT From dd60f251cd89bb94800b49d1838b8b547cf690d9 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:57:17 +0000 Subject: [PATCH 10/18] Use same python versions for TCK as for tests --- .github/workflows/run-tck.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 957df2ddc..79c88209c 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ['3.10', '3.13'] steps: - name: Checkout a2a-python uses: actions/checkout@v4 From eaf0cb967367a5b877b9e23122a6f886d3408d8a Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 11:58:38 +0000 Subject: [PATCH 11/18] Allow SUT for spelling --- .github/actions/spelling/allow.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 27b5cb4c3..dc589ffce 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -90,3 +90,5 @@ Tful tiangolo typeerror vulnz +SUT +sut From 1037f876de8fa73ae73040790925df6aa80c1b87 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 13:02:12 +0000 Subject: [PATCH 12/18] Simplify boilerplate --- tck/sut_agent.py | 50 ++++++++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 03dc29518..001abc8d9 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -5,13 +5,12 @@ from datetime import datetime, timezone -from uvicorn import Config, Server +import uvicorn from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext -from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication +from a2a.server.apps import A2AStarletteApplication from a2a.server.events.event_queue import EventQueue -from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager from a2a.server.request_handlers.default_request_handler import ( DefaultRequestHandler, ) @@ -28,7 +27,8 @@ ) -# Configure logging +JSONRPC_URL = '/a2a/jsonrpc' + logging.basicConfig(level=logging.INFO) logger = logging.getLogger('SUTAgent') @@ -124,24 +124,15 @@ async def execute( await event_queue.enqueue_event(final_update) -async def main() -> None: + +def main() -> None: """Main entrypoint.""" http_port = int(os.environ.get('HTTP_PORT', '41241')) - agent_executor = SUTAgentExecutor() - task_store = InMemoryTaskStore() - queue_manager = InMemoryQueueManager() - - request_handler = DefaultRequestHandler( - task_store=task_store, - queue_manager=queue_manager, - agent_executor=agent_executor, - ) - - sut_agent_card = AgentCard( + agent_card = AgentCard( name='SUT Agent', - description='A sample agent to be used as SUT against tck tests.', - url=f'http://localhost:{http_port}/a2a/jsonrpc', + description='An agent to be used as SUT against TCK tests.', + url=f'http://localhost:{http_port}{JSONRPC_URL}', provider=AgentProvider( organization='A2A Samples', url='https://example.com/a2a-samples', @@ -170,26 +161,27 @@ async def main() -> None: preferred_transport='JSONRPC', additional_interfaces=[ { - 'url': f'http://localhost:{http_port}/a2a/jsonrpc', + 'url': f'http://localhost:{http_port}{JSONRPC_URL}', 'transport': 'JSONRPC', }, ], ) - json_rpc_app = A2AFastAPIApplication( - agent_card=sut_agent_card, - http_handler=request_handler, + request_handler = DefaultRequestHandler( + agent_executor=SUTAgentExecutor(), + task_store=InMemoryTaskStore(), ) - app = json_rpc_app.build( - rpc_url='/a2a/jsonrpc', agent_card_url='/.well-known/agent-card.json' + + server = A2AStarletteApplication( + agent_card=agent_card, + http_handler=request_handler, ) - logger.info('Starting HTTP server on port %s...', http_port) - config = Config(app, host='127.0.0.1', port=http_port, log_level='info') - server = Server(config) + app = server.build(rpc_url=JSONRPC_URL) - await server.serve() + logger.info('Starting HTTP server on port %s...', http_port) + uvicorn.run(app, host='127.0.0.1', port=http_port, log_level='info') if __name__ == '__main__': - asyncio.run(main()) + main() From dee72f9c77202b36674c8086f84e8bd03c88ae68 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 13:04:04 +0000 Subject: [PATCH 13/18] Reformat --- tck/sut_agent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 001abc8d9..525631ca0 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -124,7 +124,6 @@ async def execute( await event_queue.enqueue_event(final_update) - def main() -> None: """Main entrypoint.""" http_port = int(os.environ.get('HTTP_PORT', '41241')) From 5c698c343eb0bab69f14d7fb2208606db2ad2dce Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 13:10:58 +0000 Subject: [PATCH 14/18] Minor --- .github/workflows/run-tck.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 79c88209c..6cc19e190 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -56,8 +56,6 @@ jobs: - name: Start SUT run: | uv run tck/sut_agent.py & - env: - HTTP_PORT: 41241 - name: Wait for SUT to start run: | @@ -90,14 +88,12 @@ jobs: - name: Run TCK (mandatory) id: run-tck-mandatory - timeout-minutes: 5 run: | uv run run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category mandatory --transports jsonrpc working-directory: tck/a2a-tck - name: Run TCK (capabilities) id: run-tck-capabilities - timeout-minutes: 5 run: | uv run run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category capabilities --transports jsonrpc working-directory: tck/a2a-tck From 0f039cc8e8316f369fba0b1e30d5211b911862c1 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 21 Jan 2026 13:13:24 +0000 Subject: [PATCH 15/18] Name tck-test job --- .github/workflows/run-tck.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 6cc19e190..085af99a5 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -26,6 +26,7 @@ concurrency: jobs: tck-test: + name: Run TCK with Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: matrix: From 119dc5bf500ad247db42ae7c6d4066ef99d85fc1 Mon Sep 17 00:00:00 2001 From: Holt Skinner <13262395+holtskinner@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:11:28 -0600 Subject: [PATCH 16/18] Formatting --- .github/actions/spelling/allow.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index dc589ffce..11496c9f7 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -52,8 +52,8 @@ JPY JSONRPCt jwk jwks -JWS jws +JWS kid kwarg langgraph @@ -83,6 +83,8 @@ RUF SLF socio sse +sut +SUT tagwords taskupdate testuuid @@ -90,5 +92,3 @@ Tful tiangolo typeerror vulnz -SUT -sut From 06fe1f8caafe17749c0600d2fc877e20c49a75f0 Mon Sep 17 00:00:00 2001 From: Holt Skinner <13262395+holtskinner@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:11:42 -0600 Subject: [PATCH 17/18] Update github actions packages --- .github/workflows/run-tck.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 085af99a5..10f5785d1 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -33,10 +33,10 @@ jobs: python-version: ['3.10', '3.13'] steps: - name: Checkout a2a-python - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "uv.lock" @@ -48,7 +48,7 @@ jobs: run: uv sync --all-extras - name: Checkout a2a-tck - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: a2aproject/a2a-tck path: tck/a2a-tck From 676cec9b415e0bd7e59cf18ceba9c8ce589acdbb Mon Sep 17 00:00:00 2001 From: Ivan Shimko Date: Wed, 21 Jan 2026 18:24:34 +0100 Subject: [PATCH 18/18] --locked --- .github/workflows/run-tck.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 10f5785d1..0f3452b37 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -45,7 +45,7 @@ jobs: run: uv python install ${{ matrix.python-version }} - name: Install Dependencies - run: uv sync --all-extras + run: uv sync --locked --all-extras - name: Checkout a2a-tck uses: actions/checkout@v6