Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ updates:
day: "monday"
time: "09:00"
open-pull-requests-limit: 10
groups:
uv-minor-and-patch:
update-types:
- "minor"
- "patch"
labels:
- "dependencies"
- "python"
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ import asyncio
import os

from band import Agent, configure_logging
from band.adapters.a2a_gateway import A2AGatewayAdapter
from band.adapters.a2a_gateway import A2AGatewayAdapter, A2AGatewayAdapterConfig

configure_logging()

Expand All @@ -582,6 +582,8 @@ async def main() -> None:
api_key=os.environ["GATEWAY_API_KEY"],
gateway_url=gateway_url,
port=gateway_port,
# The default is 300 seconds. Use None for no response deadline.
config=A2AGatewayAdapterConfig(response_timeout_s=300),
)

agent = Agent.create(
Expand All @@ -601,6 +603,9 @@ Discovery endpoints include:

```bash
curl http://localhost:10000/peers
curl http://localhost:10000/agents/weather-agent/.well-known/agent-card.json

# Legacy card discovery remains available for older clients:
curl http://localhost:10000/agents/weather-agent/.well-known/agent.json
```

Expand Down
2 changes: 1 addition & 1 deletion examples/a2a_bridge/01_basic_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
python -m app --host localhost --port 10000

2. Verify the agent is running:
curl http://localhost:10000/.well-known/agent.json
curl http://localhost:10000/.well-known/agent-card.json

Run with:
uv run examples/a2a_bridge/01_basic_agent.py
Expand Down
6 changes: 0 additions & 6 deletions examples/a2a_bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,6 @@ export A2A_AGENT_URL=http://localhost:10000

Before starting the bridge, confirm the remote agent card is reachable:

```bash
curl http://localhost:10000/.well-known/agent.json
```

or:

```bash
curl http://localhost:10000/.well-known/agent-card.json
```
Expand Down
4 changes: 2 additions & 2 deletions examples/a2a_gateway/01_basic_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
uv run examples/a2a_gateway/01_basic_gateway.py

Then remote agents can connect:
- Discovery: GET http://localhost:10000/agents/weather/.well-known/agent.json
- Discovery: GET http://localhost:10000/agents/weather/.well-known/agent-card.json
- JSON-RPC: POST http://localhost:10000/agents/weather
- Stream: POST http://localhost:10000/agents/weather/v1/message:stream
"""
Expand Down Expand Up @@ -108,7 +108,7 @@ async def main() -> None:
logger.info("Starting A2A Gateway on %s...", gateway_url)
logger.info("Peers will be exposed at:")
logger.info(
" - %s/agents/{peer_id}/.well-known/agent.json (discovery)", gateway_url
" - %s/agents/{peer_id}/.well-known/agent-card.json (discovery)", gateway_url
)
logger.info(" - %s/agents/{peer_id}/v1/message:stream (messaging)", gateway_url)
logger.info("Waiting for peers to be discovered...")
Expand Down
39 changes: 29 additions & 10 deletions examples/a2a_gateway/02_with_demo_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@

Test the demo:
# Check orchestrator agent card
curl http://localhost:10001/.well-known/agent.json
curl http://localhost:10001/.well-known/agent-card.json

# Send a JSON-RPC message to the orchestrator (it will route to gateway peers)
curl -X POST http://localhost:10001/ \\
-H "Content-Type: application/json" \\
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"method": "SendMessage",
"params": {
"message": {
"role": "user",
Expand All @@ -68,14 +68,17 @@
sys.path.insert(0, str(Path(__file__).parent))

import uvicorn
from a2a.server.apps import A2AStarletteApplication
from a2a.server.routes.agent_card_routes import create_agent_card_routes
from a2a.server.routes.jsonrpc_routes import create_jsonrpc_routes
from a2a.server.routes.rest_routes import create_rest_routes
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import (
InMemoryPushNotificationConfigStore,
InMemoryTaskStore,
)
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
from dotenv import load_dotenv
from starlette.applications import Starlette

from demo_orchestrator.agent import OrchestratorAgent
from demo_orchestrator.agent_executor import OrchestratorAgentExecutor
Expand Down Expand Up @@ -175,7 +178,13 @@ def run_orchestrator() -> None:
agent_card = AgentCard(
name="Demo Orchestrator",
description="Routes user requests to Band platform peers via A2A Gateway",
url=f"http://{ORCHESTRATOR_HOST}:{ORCHESTRATOR_PORT}/",
supported_interfaces=[
AgentInterface(
protocol_binding="JSONRPC",
protocol_version="1.0",
url=f"http://{ORCHESTRATOR_HOST}:{ORCHESTRATOR_PORT}/",
)
],
version="1.0.0",
default_input_modes=OrchestratorAgent.SUPPORTED_CONTENT_TYPES,
default_output_modes=OrchestratorAgent.SUPPORTED_CONTENT_TYPES,
Expand All @@ -187,12 +196,20 @@ def run_orchestrator() -> None:
request_handler = DefaultRequestHandler(
agent_executor=OrchestratorAgentExecutor(agent),
task_store=InMemoryTaskStore(),
agent_card=agent_card,
push_config_store=InMemoryPushNotificationConfigStore(),
)

server = A2AStarletteApplication(
agent_card=agent_card,
http_handler=request_handler,
server = Starlette(
routes=(
create_agent_card_routes(agent_card)
+ create_jsonrpc_routes(
request_handler,
rpc_url="/",
enable_v0_3_compat=True,
)
+ create_rest_routes(request_handler, enable_v0_3_compat=True)
)
)

logger.info(
Expand All @@ -202,7 +219,7 @@ def run_orchestrator() -> None:
)

# Run uvicorn (blocking)
uvicorn.run(server.build(), host=ORCHESTRATOR_HOST, port=ORCHESTRATOR_PORT)
uvicorn.run(server, host=ORCHESTRATOR_HOST, port=ORCHESTRATOR_PORT)


async def main() -> None:
Expand All @@ -221,7 +238,9 @@ async def main() -> None:
)
logger.info("")
logger.info("Test with:")
logger.info(" curl http://localhost:%s/.well-known/agent.json", ORCHESTRATOR_PORT)
logger.info(
" curl http://localhost:%s/.well-known/agent-card.json", ORCHESTRATOR_PORT
)
logger.info("")

# Run gateway in background, orchestrator in foreground
Expand Down
27 changes: 16 additions & 11 deletions examples/a2a_gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ uv run examples/a2a_gateway/01_basic_gateway.py
Once it is up, the most useful endpoints are:

- `GET http://localhost:10000/peers`
- `GET http://localhost:10000/agents/<peer-id>/.well-known/agent.json`
- `GET http://localhost:10000/agents/<peer-id>/.well-known/agent-card.json`
- `POST http://localhost:10000/agents/<peer-id>`
- `POST http://localhost:10000/agents/<peer-id>/v1/message:stream`

Expand All @@ -118,22 +118,24 @@ Pick one peer id from the response.
### 3. Fetch the peer's agent card

```bash
curl http://localhost:10000/agents/<peer-id>/.well-known/agent.json
curl http://localhost:10000/agents/<peer-id>/.well-known/agent-card.json
```

### 4. Send a message

Use a stable `contextId` so you can test room reuse.

For JSON-RPC clients, send requests to the base agent route:
For A2A 1.0 JSON-RPC clients, send requests to the base agent route with the
`A2A-Version: 1.0` header. The 1.0 method is `SendMessage`:

```bash
curl -X POST http://localhost:10000/agents/<peer-id> \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"method": "SendMessage",
"params": {
"message": {
"role": "user",
Expand All @@ -145,16 +147,19 @@ curl -X POST http://localhost:10000/agents/<peer-id> \
}'
```

If you want the legacy streaming route, send a raw A2A `Message` body instead:
For the REST streaming binding, use the versioned route and wrap the message
inside a `SendMessageRequest`:

```bash
curl -N -X POST http://localhost:10000/agents/<peer-id>/v1/message:stream \
-H "Content-Type: application/json" \
-d '{
"role": "user",
"parts": [{"kind": "text", "text": "Hello from an A2A client"}],
"messageId": "msg-1",
"contextId": "ctx-1"
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Hello from an A2A client"}],
"messageId": "msg-1",
"contextId": "ctx-1"
}
}'
```

Expand Down Expand Up @@ -191,7 +196,7 @@ uv run examples/a2a_gateway/02_with_demo_agent.py
Then check the orchestrator card:

```bash
curl http://localhost:10001/.well-known/agent.json
curl http://localhost:10001/.well-known/agent-card.json
```

The orchestrator's job is to accept an incoming A2A request and route it to one of the peers exposed by the gateway.
Expand All @@ -201,7 +206,7 @@ The orchestrator's job is to accept an incoming A2A request and route it to one
### Basic gateway

- `/peers` returns real Band peers
- `/.well-known/agent.json` returns a valid card for a peer
- `/.well-known/agent-card.json` returns a valid card for a peer
- a message call returns a peer response
- the same `contextId` reuses the same room

Expand Down
35 changes: 26 additions & 9 deletions examples/a2a_gateway/demo_orchestrator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
uv run python examples/a2a_gateway/demo_orchestrator/__main__.py --gateway-url http://localhost:10000

This starts an A2A-compliant server that:
1. Exposes itself at /.well-known/agent.json
2. Accepts messages at /v1/message:stream
1. Exposes itself at /.well-known/agent-card.json
2. Accepts messages at /message:stream and / (JSON-RPC)
3. Routes requests to Band peers via the A2A Gateway
"""

Expand All @@ -24,14 +24,17 @@

import click
import uvicorn
from a2a.server.apps import A2AStarletteApplication
from a2a.server.routes.agent_card_routes import create_agent_card_routes
from a2a.server.routes.jsonrpc_routes import create_jsonrpc_routes
from a2a.server.routes.rest_routes import create_rest_routes
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import (
InMemoryPushNotificationConfigStore,
InMemoryTaskStore,
)
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
from dotenv import load_dotenv
from starlette.applications import Starlette

from agent import OrchestratorAgent
from agent_executor import OrchestratorAgentExecutor
Expand Down Expand Up @@ -135,7 +138,13 @@ def main(host: str, port: int, gateway_url: str, peers: str, model: str) -> None
"Band platform peers via the A2A Gateway. It intelligently "
"determines which peer can best handle each request."
),
url=f"http://{host}:{port}/",
supported_interfaces=[
AgentInterface(
protocol_binding="JSONRPC",
protocol_version="1.0",
url=f"http://{host}:{port}/",
)
],
version="1.0.0",
default_input_modes=OrchestratorAgent.SUPPORTED_CONTENT_TYPES,
default_output_modes=OrchestratorAgent.SUPPORTED_CONTENT_TYPES,
Expand All @@ -149,16 +158,24 @@ def main(host: str, port: int, gateway_url: str, peers: str, model: str) -> None
request_handler = DefaultRequestHandler(
agent_executor=OrchestratorAgentExecutor(agent),
task_store=InMemoryTaskStore(),
agent_card=agent_card,
push_config_store=push_config_store,
)

server = A2AStarletteApplication(
agent_card=agent_card,
http_handler=request_handler,
server = Starlette(
routes=(
create_agent_card_routes(agent_card)
+ create_jsonrpc_routes(
request_handler,
rpc_url="/",
enable_v0_3_compat=True,
)
+ create_rest_routes(request_handler, enable_v0_3_compat=True)
)
)

# Run server
uvicorn.run(server.build(), host=host, port=port)
uvicorn.run(server, host=host, port=port)

except Exception as e:
logger.error("Error starting server: %s", e)
Expand Down
Loading
Loading