Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0d55b6e
First commit for a2a work
rodrigobr-msft Sep 14, 2026
371542e
More updates
rodrigobr-msft Sep 14, 2026
9c79de6
A2AActivity implementation of artifact creation methods
rodrigobr-msft Sep 14, 2026
b546e6b
Progress on A2A
rodrigobr-msft Sep 17, 2026
6c804bf
Context state propagation for ClaimsIdentity
rodrigobr-msft Sep 18, 2026
602780a
Adding get_streaming_entity method to Activity and related tests
rodrigobr-msft Sep 18, 2026
f0ff83a
Merge branch 'main' into users/robrandao/get-streaming-entity
rodrigobr-msft Sep 18, 2026
57cb659
Another commit
rodrigobr-msft Sep 18, 2026
8a128d1
Merge branch 'users/robrandao/get-streaming-entity' of https://github…
rodrigobr-msft Sep 18, 2026
821ff0e
SendActivities handling
rodrigobr-msft Sep 18, 2026
5f5ff65
Removing files
rodrigobr-msft Sep 18, 2026
cede7de
Removing more files
rodrigobr-msft Sep 18, 2026
6f7581f
Adding default TaskStore creation for A2AAdapter
rodrigobr-msft Sep 18, 2026
286f771
Initial implementation of A2AAgentExtension
rodrigobr-msft Sep 18, 2026
a3a6985
Adding licensing headers
rodrigobr-msft Sep 18, 2026
4ac39ff
A2AActivity ChannelId freezing and new A2AHttpAdapter protocol
rodrigobr-msft Sep 21, 2026
ca5971b
A2A extension folder restructure
rodrigobr-msft Sep 21, 2026
da83385
add_a2a functionality to setup A2A on existing FastAPI app
rodrigobr-msft Sep 21, 2026
0bf5a49
Initial draft of A2A sample
rodrigobr-msft Sep 21, 2026
c7f15a8
Test sample work
rodrigobr-msft Sep 21, 2026
97e7481
Agent card route handling
rodrigobr-msft Sep 21, 2026
eff47f9
Improvements for hosting-level route registration and improvements to…
rodrigobr-msft Sep 21, 2026
1d50baa
Cleaning up imports and docstrings
rodrigobr-msft Sep 22, 2026
eac83b8
A2A metadata generation
rodrigobr-msft Sep 22, 2026
95670d0
Removing AgentRequestContext
rodrigobr-msft Sep 22, 2026
b83df37
Improved testing and minor tweaks to Task handling
rodrigobr-msft Sep 22, 2026
d7d5a72
Adding more tests and refining BlobTaskStore
rodrigobr-msft Sep 23, 2026
3af7291
Another commit
rodrigobr-msft Sep 23, 2026
f7dbfd9
Formatting
rodrigobr-msft Sep 23, 2026
22dd53f
Improving test coverage
rodrigobr-msft Sep 23, 2026
8cd0f09
Adding jwt e2e test for a2a
rodrigobr-msft Sep 23, 2026
5d6c519
Better tests
rodrigobr-msft Sep 23, 2026
1b5aa0a
A2ATurnContext initialization improvements and more tests
rodrigobr-msft Sep 23, 2026
4401149
Fixing endOfConversation missing conversation field
rodrigobr-msft Sep 23, 2026
a832d84
add_a2a accepts AgentApplication rather than just Agent
rodrigobr-msft Sep 23, 2026
5232177
Fixing BlobTaskStore list method
rodrigobr-msft Sep 24, 2026
25f6fd4
Adding BlobTaskStore tests
rodrigobr-msft Sep 24, 2026
f3aa606
Another commit
rodrigobr-msft Sep 24, 2026
7352aac
Merge branch 'main' of https://github.com/microsoft/Agents-for-python…
rodrigobr-msft Sep 29, 2026
0188cd8
Part to attachment conversion fix
rodrigobr-msft Sep 29, 2026
d0a8e3f
Improvement to Agent Card routing logic
rodrigobr-msft Sep 29, 2026
c3bae50
Reorganizing the a2a extension
rodrigobr-msft Oct 5, 2026
9ccb9f9
Updating tests
rodrigobr-msft Oct 5, 2026
a242159
Updating tests
rodrigobr-msft Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dev/a2a/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.tck/
*.log

52 changes: 52 additions & 0 deletions dev/a2a/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# A2A TCK development harness

This directory contains a local A2A agent and a PowerShell runner for the
official [A2A Technology Compatibility Kit][tck].

## Prerequisites

From the repository root, prepare the development environment:

```powershell
.\scripts\dev_setup.ps1
```

Install [uv](https://docs.astral.sh/uv/) and ensure `git` is available on
`PATH`.

## Run the TCK

From the repository root:

```powershell
.\dev\a2a\run_tck.ps1
```

The script:

1. Clones or updates the TCK under `dev\a2a\.tck`.
2. Creates the TCK virtual environment and installs its dependencies.
3. Starts the local agent on `http://127.0.0.1:41241`.
4. Runs the TCK against the JSON-RPC and HTTP+JSON interfaces.
5. Stops the local agent.

By default, the complete suite runs for both supported transports. Examples:

```powershell
# Run only MUST requirements over JSON-RPC.
.\dev\a2a\run_tck.ps1 -Transport jsonrpc -Level must

# Run only HTTP+JSON against a specific TCK revision.
.\dev\a2a\run_tck.ps1 -Transport http_json -TckRevision main

# Forward additional arguments to pytest through the TCK.
.\dev\a2a\run_tck.ps1 -PytestArgs "-x", "--pdb"
```

TCK reports are written under `dev\a2a\.tck\reports`.

The test agent intentionally disables JWT authentication. It is only intended
for local protocol compatibility testing.

[tck]: https://github.com/a2aproject/a2a-tck

236 changes: 236 additions & 0 deletions dev/a2a/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from __future__ import annotations

import argparse
import asyncio
from uuid import uuid4

import uvicorn
from a2a.types import AgentInterface, AgentSkill
from a2a.utils.constants import TransportProtocol
from fastapi import FastAPI
from starlette.routing import Route

from microsoft_agents.activity import (
Activity,
ActivityTypes,
Attachment,
EndOfConversationCodes,
InputHints,
StreamInfo,
)
from microsoft_agents.hosting.a2a import A2AAdapter, add_a2a
from microsoft_agents.hosting.core import (
AgentApplication,
AgentAuthConfiguration,
AnonymousTokenProvider,
ApplicationOptions,
ConnectionManager,
MemoryStorage,
TurnContext,
TurnState,
)


def create_agent() -> AgentApplication[TurnState]:
connection_manager = ConnectionManager(
provider_factory=lambda _: AnonymousTokenProvider(),
connections_configurations={
"SERVICE_CONNECTION": AgentAuthConfiguration(
anonymous_allowed=True,
)
},
)
application = AgentApplication[TurnState](
options=ApplicationOptions(storage=MemoryStorage()),
connection_manager=connection_manager,
)

@application.activity("message")
async def on_message(context: TurnContext, _state: TurnState) -> None:
message_id = getattr(context.activity.channel_data, "message_id", "")

async def send_artifact(
*,
text: str | None = None,
value: dict | None = None,
attachment: Attachment | None = None,
) -> None:
await context.send_activity(
Activity(
type=ActivityTypes.message,
text=text,
value=value,
attachments=[attachment] if attachment else [],
entities=[
StreamInfo(
stream_id=str(uuid4()),
stream_type="content",
stream_sequence=1,
)
],
)
)

if message_id.startswith("test-resubscribe-message-id"):
await context.send_activity("Working")
await asyncio.sleep(4)
elif message_id.startswith("tck-input-required"):
await context.send_activity(
Activity(
type=ActivityTypes.message,
input_hint=InputHints.expecting_input,
)
)
return
elif message_id.startswith("tck-message-response"):
await context.send_activity("Direct message response")
return
elif message_id.startswith("tck-artifact-file-url"):
await send_artifact(
attachment=Attachment(
content_type="text/plain",
content_url="https://example.com/output.txt",
name="output.txt",
)
)
elif message_id.startswith("tck-artifact-file"):
await send_artifact(
attachment=Attachment(
content_type="text/plain",
content=b"tck",
name="output.txt",
)
)
elif message_id.startswith("tck-artifact-data"):
await send_artifact(value={"key": "value", "count": 42})
elif message_id.startswith("tck-artifact-text"):
await send_artifact(text="Generated text content")
elif message_id.startswith("tck-stream-artifact-chunked"):
response = context.streaming_response
response.queue_text_chunk("chunk-1 ")
response.queue_text_chunk("chunk-2")
await response.end_stream()
return
elif message_id.startswith("tck-stream-artifact-file"):
await send_artifact(
attachment=Attachment(
content_type="text/plain",
content=b"tck",
name="output.txt",
)
)
elif message_id.startswith("tck-stream-artifact-text"):
response = context.streaming_response
response.queue_text_chunk("Streamed text content")
await response.end_stream()
return
elif message_id.startswith("tck-stream-ordering-001"):
response = context.streaming_response
response.queue_informative_update("Working")
response.queue_text_chunk("Ordered output")
await response.end_stream()
return
elif message_id.startswith("tck-stream-001"):
response = context.streaming_response
response.queue_text_chunk("Stream hello from TCK")
await response.end_stream()
return
elif message_id.startswith("tck-stream-002"):
pass
elif message_id.startswith("tck-stream-003"):
response = context.streaming_response
response.queue_informative_update("Working")
response.queue_text_chunk("Stream task lifecycle")
await response.end_stream()
return
else:
await context.send_activity("Hello from TCK")

await context.send_activity(
Activity(
type=ActivityTypes.end_of_conversation,
code=EndOfConversationCodes.completed_successfully,
)
)

return application


def create_app() -> FastAPI:
agent = create_agent()
adapter = A2AAdapter(
agent,
agent_card_name="Microsoft Agents SDK TCK Agent",
agent_card_description="A deterministic agent for A2A TCK validation.",
agent_card_version="1.0.0",
agent_interfaces=[
AgentInterface(
url="/rpc",
protocol_binding=TransportProtocol.JSONRPC,
),
AgentInterface(
url="/rest",
protocol_binding=TransportProtocol.HTTP_JSON,
),
],
skills=[
AgentSkill(
id="echo",
name="Echo",
description="Echoes incoming text.",
tags=["test", "echo"],
examples=["Hello"],
input_modes=["text/plain"],
output_modes=["text/plain"],
)
],
)

app = FastAPI(title="Microsoft Agents SDK A2A TCK Agent", version="1.0.0")
add_a2a(
app,
agent,
adapter=adapter,
use_jwt_middleware=False,
)

rpc_route = next(
route
for route in app.router.routes
if isinstance(route, Route) and route.path == "/rpc"
)
mount_index = next(
index
for index, route in enumerate(app.router.routes)
if route.path == "/{tenant}"
)
app.router.routes.insert(
mount_index,
Route(
path="/rpc/",
endpoint=rpc_route.endpoint,
methods=["POST"],
),
)

@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}

return app


def main() -> None:
parser = argparse.ArgumentParser(description="Run the local A2A TCK agent.")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", default=41241, type=int)
args = parser.parse_args()

uvicorn.run(create_app(), host=args.host, port=args.port)


if __name__ == "__main__":
main()
Loading
Loading