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
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,13 @@ jobs:
# green without starlette/uvicorn). [otel] likewise: the GenAI-span
# assertions (BG §1.6, #192) importorskip opentelemetry.sdk, so they only
# actually execute in this job — the typecheck job stays [llm,cli] and
# relies on the absence-only opentelemetry mypy override.
# relies on the absence-only opentelemetry mypy override. [a2a] the same
# way: donkey.serve()'s round-trip tests (#224) importorskip a2a.
# -c locks the resolve per Python version (ADR 0007 rule 3, #948);
# refresh with scripts/compile_constraints.py (combo "test").
- run: >-
python -m pip install --upgrade pip &&
pip install -e ".[llm,cli,local,otel]" --group dev -c "constraints/test-py${PYTHON_VERSION}.txt"
pip install -e ".[llm,cli,local,otel,a2a]" --group dev -c "constraints/test-py${PYTHON_VERSION}.txt"
- if: github.event_name == 'push' && steps.pip-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
python-version: "3.11"
- run: >-
python -m pip install --upgrade pip &&
pip install -e ".[llm,cli,local,otel]" --group dev -c constraints/test-py3.11.txt
pip install -e ".[llm,cli,local,otel,a2a]" --group dev -c constraints/test-py3.11.txt
- run: pytest -q

build:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-testpypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
python-version: "3.11"
- run: >-
python -m pip install --upgrade pip &&
pip install -e ".[llm,cli,local,otel]" --group dev -c constraints/test-py3.11.txt
pip install -e ".[llm,cli,local,otel,a2a]" --group dev -c constraints/test-py3.11.txt
- run: pytest -q

build:
Expand Down
8 changes: 7 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,15 @@ bottom:
cli the `donkey` console script (#730)
conformance → simulator → _testing dev-only siblings
donkey | experimental experimental: non-first-class names (ADR 0008)
integrations → tools → registry → llm → core
integrations | a2a → tools → registry → llm → core
```

`a2a` (`donkey.serve()`, BG §2.9.1, #224) sits beside `integrations`, not in
it: it is the inbound A2A listener, not a framework adapter. It wraps the
official `a2a-sdk` (the `[a2a]` extra). The layers contract lists the two as
independent siblings (`integrations | a2a`), so neither may import the other
(ADR 0014).

It is `exhaustive`, so a new top-level module fails `lint-imports` until it is
placed. Its one ignored import is `Donkey.simulate()`'s lazy import of
`simulator.inject`: public API whose implementation lives in the simulator.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ pip install -e ".[llm,cli]" --group dev

Extras are one per framework (`langgraph`, `adk`, `strands`, `agent_framework`,
`openai-agents`, `anthropic`, `crewai`, `llamaindex`) plus `otel`, `cli`, `local`,
`test` (the [conformance pytest plugin](https://docs.donkey-kit.dev/testing) —
`a2a` (the official `a2a-sdk`, for `donkey.serve()`), `test` (the [conformance pytest plugin](https://docs.donkey-kit.dev/testing) —
`pytest --donkey-conformance --donkey-agent=my_app.agent:build`), and `all`. `all` is
everything a user runs that installs together — `llm`, `langgraph`, `otel`, `cli`,
`local` — with no test runner, so add `test` for the conformance plugin:
`local`, `a2a` — with no test runner, so add `test` for the conformance plugin:
`donkey-kit[all,test]`. It leaves out the seven other framework extras, because
whether they install together depends on their upstream releases. The generated
[co-installability table](docs/co-installability.md) shows which combinations resolve, re-checked
Expand Down
84 changes: 84 additions & 0 deletions docs/adr/0014-a2a-server-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# ADR 0014: `donkey.serve()` wraps the official a2a-sdk, in a top-level `a2a` package beside `integrations`

- **Status:** Proposed
- **Date:** 2026-10-24
- **Issue:** #224 (BG §2.9.1). Governed inbound is #225, `donkey expose` #226,
`donkey dev` #227.

## Context

BG §2.9.1 asks for an A2A listener in one line: `donkey.serve(handler)`. Today,
accepting a task from a partner agent means writing a server and an agent card
by hand, adding logging and tracing, and then getting a certificate, opening a
port and registering the agent.

The A2A protocol already has an official Python implementation, the `a2a-sdk`.
It provides a request handler, a task store, the task lifecycle and the
Starlette routes. The build plan's *Do not build* list rules out a home-grown
protocol implementation. DDK's value is what a bare `a2a-sdk` server lacks:

- an agent card generated from the `@donkey.agent` record that `donkey scan`
also reads (#217), so the two cannot drift;
- a localhost bind by default, so the gateway is the public face;
- (#225) inbound correlation, cost tags, tracing and typed refusals.

The `layered architecture (§1.1)` contract in `python/pyproject.toml` is
`exhaustive`, so a new top-level module must be placed in it. `integrations/`
holds one adapter per agent framework, each returning native framework objects
(ADR 0004). An A2A server is not a framework adapter: it is not in the
`ADAPTERS` roster and returns no framework object.

## Decision

1. **The protocol is the `a2a-sdk`'s, never DDK's.** `donkey_kit.a2a` wraps
`AgentExecutor`, `DefaultRequestHandler`, `InMemoryTaskStore`,
`TaskUpdater`, `create_agent_card_routes`, `create_jsonrpc_routes` and
`AgentCard`, and reimplements none of them. Every wrapped name is a
`docs/verified-apis.md` §8.5 row. Enforced in review.
2. **The card comes only from the `@donkey.agent` record.** Its name,
description and skills are the `AgentSpec`'s name, docstring and skills.
An unmarked handler raises `TypeError`. Enforced by
`tests/unit/test_a2a_serve.py`.
3. **`donkey.serve()` binds to `127.0.0.1` by default.** Binding elsewhere
takes an explicit `host=`. Enforced by `tests/unit/test_a2a_serve.py`.
4. **`a2a` is a top-level package and an independent sibling of
`integrations`** (`"integrations | a2a"` in the layers contract). Neither
may import the other. `core` must never import `a2a` (the core
`forbidden` contract). Enforced by `lint-imports`.
5. **The `a2a-sdk` loads only when a server is built.** It is the `[a2a]`
extra (`a2a-sdk[http-server]`, `uvicorn`), imported inside `build_app()`
and `serve()`. `import donkey_kit` and `import donkey_kit.a2a` never load
it. A missing extra raises an `ImportError` that names
`pip install "donkey-kit[a2a]"`. The extra may bring pydantic and protobuf,
which `core` forbids; that is allowed because nothing below `a2a` imports
them. Enforced by `tests/unit/test_a2a_serve.py`.
6. **A handler failure fails the task, never the server.** The caller sees
only the exception's type name; the message goes to the
`donkey_kit.a2a._server` logger. Enforced by `tests/unit/test_a2a_serve.py`.

### Alternatives considered

- **Have users call the `a2a-sdk` directly.** Rejected: they get a bare
server, with a hand-written card that can drift from the `@donkey.agent`
marker, anonymous inbound tasks and a bind address left to them. The
differentiator is governed inbound and one-command exposure, not the
protocol.
- **Put the server in `integrations/a2a.py`.** Rejected: it would sit in the
adapter roster's package without being an adapter, and the
`integrations are mutually independent` contract and the `ADAPTERS` roster
tests assume every module there is one.
- **Implement the A2A protocol in DDK.** Rejected by the build plan's *Do not
build* list: the `a2a-sdk` tracks the spec, and a second implementation
would drift from it.

## Consequences

- `python/pyproject.toml` gains the `[a2a]` extra (also in `[all]`), the
layers placement, the core `forbidden` entry and the mypy `a2a.*` skip.
`dependency_allowlist.toml` gains `a2a-sdk`.
- The PR `test` matrix installs `[a2a]`, so the round-trip tests run there; the
base-only and typecheck jobs skip them through `importorskip`.
- `docs/verified-apis.md` §8.3 records the `a2a-sdk>=1.2.0` floor and §8.5 the
wrapped names. A new `a2a-sdk` major means re-verifying §8.5.
- #225 adds inbound governance to the executor this ADR introduces; it does not
change the package's place in the stack.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ is not rewritten.
| [0011](0011-nightly-lock-refresh-pr.md) | A green nightly opens the lock-refresh PR, and the release toolchain is locked too | Accepted | #763 |
| [0012](0012-custom-policy-registry.md) | Customer-registered refusal shapes, in one process-wide registry behind a hook slot | Accepted | #210 |
| [0013](0013-ungoverned-mcp-binding.md) | Binding refuses an MCP server that is not governed, with no opt-out | Accepted | #1150 |
| [0014](0014-a2a-server-package.md) | `donkey.serve()` wraps the official a2a-sdk, in a top-level `a2a` package beside `integrations` | Proposed | #224 |
27 changes: 27 additions & 0 deletions docs/verified-apis.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Verified APIs — output of the verification discipline

> **Status (2026-08-28): partially verified.** The **management/control-plane**
Expand Down Expand Up @@ -786,6 +786,7 @@
| `crewai` | `crewai>=1.15.3` | 1.15.3 added the `openai/` + explicit `base_url` route to the native `OpenAICompletion`. Earlier releases fall back to LiteLLM for any id that does not look like an OpenAI model, returning `crewai.LLM` and LiteLLM exceptions. Signature check against `OpenAICompletion` passes at 1.15.3. | CrewAI |
| `llamaindex` | `llama-index-llms-openai-like>=0.8` | The verified release. | LlamaIndex |
| `strands` | `strands-agents[openai]>=1.57.1` | The verified release, with Strands' own `openai` extra: the bare distribution does not install `openai`, so `model()` raised `ModuleNotFoundError`. `test_strands_real_model_is_built_and_called_through_our_transport` builds and calls the real model with only `[strands]` installed. | Strands |
| `a2a` | `a2a-sdk[http-server]>=1.2.0`, `uvicorn>=0.30` | 1.2.0 is the first a2a-sdk with `DefaultRequestHandler.aclose()`, which `build_app()`'s lifespan calls so a shutdown drains the SDK's per-task asyncio tasks (1.0.3 and 1.1.0 serve tasks but have no `aclose()`; without it, the tasks are still pending when the loop closes). Below 1.0.3, 1.0.1–1.0.2 lack `a2a.helpers.new_text_part` and 1.0.0 fails to import without `grpc`. Lowest-direct on Python 3.10.21 installs a2a-sdk 1.2.0 and uvicorn 0.30.0 and passes `pytest tests/unit`, including `test_a2a_serve.py` (#224). | §8.5 |
| `cli` / dev group | `typer>=0.26.0`, `rich>=13.8.0` | Every typer up to 0.25.1 drops a `typer.Argument(help=...)` against click 8.5+, so `donkey scan --help` and the CLI reference lose the `PATH` description (#1196, bisected with `click==8.5.0`: 0.15.4–0.25.1 drop it, 0.26.0–0.27.3 keep it). 0.26.0 vendors click (fastapi/typer#1774). Older floors: below 0.15.4 typer breaks against a fresh click 8.2+ (`make_metavar() missing … 'ctx'`), and below 0.13 it rejects `Path \| None` options. rich 13.8.0 is typer 0.26.0's own floor, so `[cli]`'s old `rich>=13.0` could never be installed. | — |
| `test` / dev group | `pytest>=8.0`, `pytest-asyncio>=0.23.1` | pytest-asyncio 0.23.0 crashes collection (`INTERNALERROR`) on a module-level `importorskip`; 0.23.1 runs the suite. pytest 8.0.0 is fine. | — |

Expand Down Expand Up @@ -839,6 +840,32 @@
| LlamaIndex | `donkey.llamaindex.llm()` | LlamaIndex | `/frameworks/llamaindex` |
<!-- END ROSTER: framework-status -->

### 8.5 A2A server classes (BG §2.9.1)

`donkey.serve()` wraps these `a2a-sdk` names and reimplements none of them
(#224). Each name below was read from the installed `a2a-sdk` 1.2.3, and the
whole path was run twice. First, in process over `httpx.ASGITransport`
(`tests/unit/test_a2a_serve.py`, also at the 1.2.0 floor). Second, on a real
port: `donkey.serve()` under uvicorn 0.54.0 on `127.0.0.1:8765`, called with
plain `curl` and no SDK on the caller side, which got back the generated card
and a `TASK_STATE_COMPLETED` task with its text artifact. That round trip is
local; no gateway fronted the agent.

| Symbol | Used for | Status | Source |
|---|---|---|---|
| `a2a.server.agent_execution.AgentExecutor` (`execute`, `cancel`) | The base class of the executor that runs the handler | VERIFIED (local round-trip) | a2a-sdk 1.2.3, 2026-10-24, #224 |
| `a2a.server.agent_execution.RequestContext` (`.message`, `.current_task`, `.task_id`, `.context_id`) | Reading the caller's message and IDs | VERIFIED (local round-trip) | same |
| `a2a.server.events.EventQueue` | Passed to `TaskUpdater`; the executor enqueues a `Task` before any status update, or the SDK rejects the event | VERIFIED (local round-trip) | same |
| `a2a.server.tasks.TaskUpdater` (`start_work`, `add_artifact`, `complete`, `failed`, `cancel`) | The task lifecycle | VERIFIED (local round-trip) | same |
| `a2a.server.tasks.InMemoryTaskStore` | The task store | VERIFIED (local round-trip) | same |
| `a2a.server.request_handlers.DefaultRequestHandler(agent_executor=, task_store=, agent_card=)`, `.aclose()` | The JSON-RPC request handler; `aclose()` (1.2.0+) runs on lifespan shutdown | VERIFIED (local round-trip) | same |
| `a2a.server.routes.create_agent_card_routes(card)`, `create_jsonrpc_routes(handler, "/")` | The card at `/.well-known/agent-card.json` and JSON-RPC at `/` (method `SendMessage`, header `A2A-Version: 1.0`) | VERIFIED (local round-trip) | same |
| `a2a.types.AgentCard` | The card, built from the `@donkey.agent` record | VERIFIED (local round-trip) | same |
| `a2a.helpers` (`new_task_from_user_message`, `new_text_part`, `new_data_part`, `new_text_message`, `get_text_parts`, `get_data_parts`) | Building and reading message parts | VERIFIED (local round-trip) | same |

Which A2A traffic Omni Gateway can front, and how, is not covered here: that is
`donkey expose` (#226).

## 9. MCP tool binding classes (BG §2.7) — verify each name

| Framework | Binding class | Status | Source |
Expand Down
4 changes: 2 additions & 2 deletions python/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions python/constraints/test-py3.10.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
# Covers ci.yml job(s): test (matrix)
# See scripts/compile_constraints.py and docs/adr/0007-dependency-policy.md rule 3.
#
a2a-sdk==1.2.3
aiologic==0.17.1
annotated-doc==0.0.5
annotated-types==0.8.0
anyio==4.15.1
ast-serialize==0.13.1
backports-asyncio-runner==1.2.0
certifi==2026.10.23
cffi==2.1.1
charset-normalizer==3.5.2
click==8.5.0
coverage==7.16.2
cryptography==50.0.2
culsans==0.11.0
exceptiongroup==1.3.1
google-api-core==2.43.0
google-auth==2.62.0
googleapis-common-protos==1.76.0
grimp==3.17
h11==0.16.0
Expand All @@ -24,6 +31,7 @@ idna==3.20
import-linter==2.15
iniconfig==2.3.1
jiter==0.18.0
json-rpc==1.15.0
librt==0.16.0
markdown-it-py==4.2.0
mdurl==0.1.2
Expand All @@ -41,7 +49,11 @@ opentelemetry-semantic-conventions==0.66b1
packaging==26.3
pathspec==1.1.1
pluggy==1.7.0
proto-plus==1.29.0
protobuf==7.36.2
pyasn1==0.6.4
pyasn1-modules==0.4.2
pycparser==3.11
pydantic==2.14.1
pydantic-core==2.50.1
pygments==2.21.0
Expand All @@ -57,6 +69,7 @@ rich==15.0.0
ruff==0.17.2
shellingham==1.5.4
sniffio==1.3.1
sse-starlette==3.5.1
starlette==1.7.0
tomli==2.5.0
truststore==0.10.5
Expand All @@ -66,3 +79,4 @@ typing-inspection==0.4.4
urllib3==2.8.0
uvicorn==0.54.0
vulture==2.16
wrapt==2.5.1
14 changes: 14 additions & 0 deletions python/constraints/test-py3.11.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@
# Covers ci.yml job(s): test (matrix)
# See scripts/compile_constraints.py and docs/adr/0007-dependency-policy.md rule 3.
#
a2a-sdk==1.2.3
aiologic==0.17.1
annotated-doc==0.0.5
annotated-types==0.8.0
anyio==4.15.1
ast-serialize==0.13.1
certifi==2026.10.23
cffi==2.1.1
charset-normalizer==3.5.2
click==8.5.0
coverage==7.17.0
cryptography==50.0.2
culsans==0.11.0
google-api-core==2.43.0
google-auth==2.62.0
googleapis-common-protos==1.76.0
grimp==3.17
h11==0.16.0
Expand All @@ -22,6 +29,7 @@ idna==3.20
import-linter==2.15
iniconfig==2.3.1
jiter==0.18.0
json-rpc==1.15.0
librt==0.16.0
markdown-it-py==4.2.0
mdurl==0.1.2
Expand All @@ -39,7 +47,11 @@ opentelemetry-semantic-conventions==0.66b1
packaging==26.3
pathspec==1.1.1
pluggy==1.7.0
proto-plus==1.29.0
protobuf==7.36.2
pyasn1==0.6.4
pyasn1-modules==0.4.2
pycparser==3.12
pydantic==2.14.1
pydantic-core==2.50.1
pygments==2.21.0
Expand All @@ -55,6 +67,7 @@ rich==15.0.0
ruff==0.17.2
shellingham==1.5.4
sniffio==1.3.1
sse-starlette==3.5.1
starlette==1.10.0
truststore==0.10.5
typer==0.27.3
Expand All @@ -63,3 +76,4 @@ typing-inspection==0.4.4
urllib3==2.8.0
uvicorn==0.54.0
vulture==2.16
wrapt==2.5.1
14 changes: 14 additions & 0 deletions python/constraints/test-py3.12.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@
# Covers ci.yml job(s): test (matrix)
# See scripts/compile_constraints.py and docs/adr/0007-dependency-policy.md rule 3.
#
a2a-sdk==1.2.3
aiologic==0.17.1
annotated-doc==0.0.5
annotated-types==0.8.0
anyio==4.15.1
ast-serialize==0.13.1
certifi==2026.10.23
cffi==2.1.1
charset-normalizer==3.5.2
click==8.5.0
coverage==7.17.0
cryptography==50.0.2
culsans==0.11.0
google-api-core==2.43.0
google-auth==2.62.0
googleapis-common-protos==1.76.0
grimp==3.17
h11==0.16.0
Expand All @@ -22,6 +29,7 @@ idna==3.20
import-linter==2.15
iniconfig==2.3.1
jiter==0.18.0
json-rpc==1.15.0
librt==0.16.0
markdown-it-py==4.2.0
mdurl==0.1.2
Expand All @@ -39,7 +47,11 @@ opentelemetry-semantic-conventions==0.66b1
packaging==26.3
pathspec==1.1.1
pluggy==1.7.0
proto-plus==1.29.0
protobuf==7.36.2
pyasn1==0.6.4
pyasn1-modules==0.4.2
pycparser==3.12
pydantic==2.14.1
pydantic-core==2.50.1
pygments==2.21.0
Expand All @@ -55,6 +67,7 @@ rich==15.0.0
ruff==0.17.2
shellingham==1.5.4
sniffio==1.3.1
sse-starlette==3.5.1
starlette==1.10.0
truststore==0.10.5
typer==0.27.3
Expand All @@ -63,3 +76,4 @@ typing-inspection==0.4.4
urllib3==2.8.0
uvicorn==0.54.0
vulture==2.16
wrapt==2.5.1
Loading
Loading