Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/examples/a2a_protocol_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,15 @@ if __name__ == "__main__":

## API Reference

### `serve_a2a(executor, agent_card=None, *, port=9000, host=None, ...)`
### `serve_a2a(executor, agent_card=None, *, port=None, host=None, ...)`

Starts a Bedrock-compatible A2A server with `uvicorn`.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `executor` | `AgentExecutor` | required | An a2a-sdk `AgentExecutor` that implements the agent logic |
| `agent_card` | `AgentCard` | `None` | Agent metadata. Auto-built from executor if omitted (works best with Strands) |
| `port` | `int` | `9000` | Port to serve on |
| `port` | `int \| None` | `None` | Port to serve on. Uses `PORT`, then `9000`, when omitted |
| `host` | `str` | `None` | Host to bind to. Auto-detected: `0.0.0.0` in Docker, `127.0.0.1` otherwise |
| `task_store` | `TaskStore` | `None` | Custom task store; defaults to `InMemoryTaskStore` |
| `context_builder` | `CallContextBuilder` | `None` | Custom context builder; defaults to `BedrockCallContextBuilder` |
Expand Down
16 changes: 12 additions & 4 deletions src/bedrock_agentcore/runtime/a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ def build_a2a_app(
executor: Any,
agent_card: Any = None,
*,
runtime_url: Optional[str] = None,
task_store: Any = None,
context_builder: Any = None,
ping_handler: Optional[Callable[[], PingStatus]] = None,
Expand All @@ -251,6 +252,9 @@ def build_a2a_app(
executor: An ``AgentExecutor`` that implements the agent logic.
agent_card: Optional ``a2a.types.AgentCard`` describing the agent.
If ``None``, one is built automatically by introspecting the executor.
runtime_url: URL advertised by an automatically generated agent card.
Defaults to ``http://localhost:9000/``. ``AGENTCORE_RUNTIME_URL``
takes precedence when set.
task_store: Optional ``TaskStore``; defaults to ``InMemoryTaskStore``.
context_builder: Optional ``ServerCallContextBuilder``; defaults to
``BedrockCallContextBuilder``.
Expand All @@ -269,7 +273,7 @@ def build_a2a_app(
from starlette.responses import JSONResponse
from starlette.routing import Route

runtime_url = os.environ.get(AGENTCORE_RUNTIME_URL_ENV, "http://localhost:9000/")
runtime_url = os.environ.get(AGENTCORE_RUNTIME_URL_ENV, runtime_url or "http://localhost:9000/")
is_a2a_v1 = _is_a2a_v1()

if agent_card is None:
Expand Down Expand Up @@ -335,7 +339,7 @@ def serve_a2a(
executor: Any,
agent_card: Any = None,
*,
port: int = 9000,
port: Optional[int] = None,
host: Optional[str] = None,
task_store: Any = None,
context_builder: Any = None,
Expand All @@ -348,7 +352,8 @@ def serve_a2a(
executor: An ``AgentExecutor`` that implements the agent logic.
agent_card: Optional ``a2a.types.AgentCard`` describing the agent.
If ``None``, one is built automatically by introspecting the executor.
port: Port to serve on (default 9000).
port: Port to serve on. Defaults to the ``PORT`` environment variable,
or 9000 when it is unset.
host: Host to bind to; auto-detected if ``None``.
task_store: Optional ``TaskStore``; defaults to ``InMemoryTaskStore``.
context_builder: Optional ``ServerCallContextBuilder``; defaults to
Expand All @@ -360,9 +365,12 @@ def serve_a2a(

import uvicorn

resolved_port = port if port is not None else int(os.environ.get("PORT", "9000"))

app = build_a2a_app(
executor,
agent_card,
runtime_url=f"http://localhost:{resolved_port}/",
task_store=task_store,
context_builder=context_builder,
ping_handler=ping_handler,
Expand All @@ -376,7 +384,7 @@ def serve_a2a(

uvicorn_params: dict[str, Any] = {
"host": host,
"port": port,
"port": resolved_port,
"log_level": "info",
}
uvicorn_params.update(kwargs)
Expand Down
19 changes: 19 additions & 0 deletions tests/bedrock_agentcore/runtime/test_a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,12 +447,31 @@ class TestServeA2A:
@patch("uvicorn.run")
def test_default_localhost(self, mock_uvicorn_run):
with patch.dict("os.environ", {}, clear=False):
import os

os.environ.pop("PORT", None)
with patch("os.path.exists", return_value=False):
serve_a2a(_EchoExecutor(), _make_agent_card())
kw = mock_uvicorn_run.call_args[1]
assert kw["host"] == "127.0.0.1"
assert kw["port"] == 9000

@patch("uvicorn.run")
def test_port_from_environment(self, mock_uvicorn_run):
with patch.dict("os.environ", {"PORT": "9001"}, clear=True):
serve_a2a(_EchoExecutor())

app = mock_uvicorn_run.call_args.args[0]
response = TestClient(app).get("/.well-known/agent-card.json")
assert mock_uvicorn_run.call_args.kwargs["port"] == 9001
assert _card_response_url(response.json()) == "http://localhost:9001/"

@patch("uvicorn.run")
def test_explicit_port_overrides_environment(self, mock_uvicorn_run):
with patch.dict("os.environ", {"PORT": "9001"}):
serve_a2a(_EchoExecutor(), _make_agent_card(), port=8888)
assert mock_uvicorn_run.call_args.kwargs["port"] == 8888

@patch("uvicorn.run")
def test_docker_detection_dockerenv(self, mock_uvicorn_run):
with patch("os.path.exists", return_value=True):
Expand Down
Loading