Skip to content

a2a: donkey.serve() — an A2A listener in one line, wrapping the official a2a-sdk #224

Closed

Description

@amirkhan-ak-sf

Summary

A2A requires the agent to be a server. That listener can be hidden behind one
line, but it cannot be removed.

@donkey.agent(name="support-triage", skills=["triage", "draft-reply"])
async def handle(task: A2ATask) -> A2AResult:
    return await graph.ainvoke(task.input)

donkey.serve(handle)   # A2A server on 127.0.0.1:8000, card auto-generated

Hard rule

Never reimplement the A2A protocol. Wrap the official a2a-sdk. The
differentiator is governed inbound and one-command exposure, not the protocol.

Acceptance criteria

  • Binds to localhost by default — the agent is never the public face.
  • The agent card is generated from the same @donkey.tool / @donkey.agent
    markers the scanner reads (2.5), so it cannot drift.
  • The official a2a-sdk is a dependency, not a reference implementation.
  • A plain A2A client can talk to it without any SDK on the caller side.

Cross-surface impact

  • area:a2a + area:scanner: shared card generation.
  • area:release: a new [a2a] extra.

Out of scope

Governed inbound, exposure and local dev — separate issues.

Activity

  1. added
    enhancementNew feature or request
    priority/p1Must-have: blocks a real adapter or CI gate
    size/m1-2 days, multiple files
    area:a2aA2A serve / expose / dev (2.9)
    on Sep 4, 2026
  2. amirkhan-ak-sf commented on Sep 9, 2026

    @amirkhan-ak-sf
    CollaboratorAuthor

    Scope question raised by the platform roadmap

    The Omni Gateway / Agent Fabric roadmap includes an A2A Bridge item — turning
    Microsoft Copilot agents into A2A-compatible assets. That surfaces a gap in how
    A2A is scoped across this issue and BG §2.9, and it is worth settling before
    this one is implemented rather than after.

    BG §2.9 and this issue cover the SDK as an A2A server: serve(), expose,
    and the local dev loop. Nothing in the build guide covers the SDK as an A2A
    client — consuming a remote A2A agent as a tool inside your own graph.

    If the platform is about to make a population of Copilot agents reachable over
    A2A, the consumption side becomes the more commonly exercised direction. A
    developer's likely first question is not "how do I publish my agent" but "how do
    I call the one the platform just published", and the answer today is that they
    write their own A2A client and lose every governance property the skeleton
    provides — no correlation ID propagation, no attribution headers, no typed
    refusals, no budget observation.

    Concretely, the missing shape is something like:

    support = await fabric.a2a.agent("copilot://support-triage")
    graph = build_graph(
        model=fabric.langgraph.chat_model("gpt-4o"),
        tools=[*fabric.langgraph.tools(toolset), support.as_tool()],
    )

    That is the inverse of what this issue builds, and it reuses the same card
    generation and the same a2a-sdk dependency — so it is a sibling, not a rewrite.

    What I am asking for, not asserting

    I have not filed an issue for this and have not changed this issue's
    scope. Three possible resolutions, and the call belongs to whoever owns A2A here:

    1. It is in scope and needs its own issue plus a BG §2.9 amendment.
    2. It is deliberately out of scope, in which case the reason belongs in the build
      guide so it is a recorded decision rather than an omission.
    3. It waits for demand evidence, the same way adapters: a second deep adapter, chosen by Phase 1 demand #223 picks the second deep adapter.

    Whichever it is, the hard rule on this issue still governs: wrap the official
    a2a-sdk, never reimplement the protocol. That applies identically to a client.

    Raised while triaging the platform roadmap against the backlog; no code touched.

  3. changed the title [-]a2a: fabric.serve() — an A2A listener in one line, wrapping the official a2a-sdk[/-] [+]a2a: donkey.serve() — an A2A listener in one line, wrapping the official a2a-sdk[/+] on Sep 11, 2026
  4. tommasobolis commented on Oct 3, 2026

    @tommasobolis
    Collaborator

    Packaging note: #744 removes the a2a extra from 0.1.x because no code uses it yet. When this lands, add it back in the same change (a2a = ["a2a-sdk>=…"], plus a2a in [all]) so pip install "donkey-kit[a2a]", which website/content/tool-access/a2a.mdx already tells users to run, works again.

  5. self-assigned this
    on Oct 24, 2026
  6. tommasobolis commented on Oct 24, 2026

    @tommasobolis
    Collaborator

    Implementation plan

    Verified against the installed a2a-sdk (§0.3). The class names below were read from a2a-sdk 1.2.3. A throwaway spike served a card and completed a task for a raw JSON-RPC SendMessage with no SDK on the caller side, on 1.0.3, 1.1.0, 1.2.0 and 1.2.3. 1.0.0 fails to import without grpc; 1.0.1–1.0.2 lack a2a.helpers.new_text_part. Floor: a2a-sdk[http-server]>=1.0.3.

    Wrapped, never reimplemented: AgentExecutor, DefaultRequestHandler, InMemoryTaskStore, TaskUpdater, create_agent_card_routes, create_jsonrpc_routes, a2a.types.AgentCard, served by uvicorn on Starlette.

    Surface

    @donkey.agent(name="support-triage", skills=["triage", "draft-reply"])
    async def handle(task: A2ATask) -> A2AResult | str: ...
    
    donkey.serve(handle)                      # blocks; 127.0.0.1:8000
    app = donkey_kit.a2a.build_app(handle)    # the same server as an ASGI app, for tests or your own server
    • A2ATask / A2AResult: frozen dataclasses (ADR 0001) in donkey_kit.a2a, exported top-level. task.input is the text parts joined; task.data holds the data parts; task.message is the native a2a Message for anything else.
    • Return str → text artifact, A2AResult(text=, data=) → text and/or data artifacts. Sync handlers run in a worker thread.
    • The card comes only from the @donkey.agent record (AgentSpec): name, docstring, skills. An unmarked handler raises TypeError that says to add the marker.
    • host="127.0.0.1" by default; url= sets the card's interface URL when a gateway fronts the agent; version= the card version.
    • An exception in the handler fails the task with the exception type name only (its message is logged, not sent to the caller). Typed refusals on served tasks are a2a: governance-aware inbound — correlation, tags, spans and typed refusals on served tasks #225.

    Packaging & architecture

    • New [a2a] extra (a2a-sdk[http-server]>=1.0.3, uvicorn>=0.30), added to [all] as noted above. The dependency_allowlist.toml entry and the verified-apis.md floor row ship in the same PR.
    • New top-level package donkey_kit/a2a/, placed beside integrations in the layers contract (integrations | a2a), which takes an ADR (0014). import donkey_kit never loads a2a-sdk; a missing extra raises the curated pip install "donkey-kit[a2a]" ImportError.
    • CI: the test matrix installs [a2a]; the constraint locks are regenerated with scripts/compile_constraints.py.

    Docs

    website/content/a2a.mdx gains a "Why not call the a2a-sdk directly?" table (raw a2a-sdk vs donkey.serve()), with each row marked as shipping now or as #225 (inbound correlation/spans, typed refusals). serve moves off "planned".

    Out of scope, as the issue says: governed inbound (#225), expose (#226), dev (#227). The A2A client side raised above is already planned as AgentHandle.as_tool() (website/content/tool-access/a2a.mdx).

  7. tommasobolis commented on Oct 25, 2026

    @tommasobolis
    Collaborator

    Fixed by #1212 (merged as 5f88330).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:a2aA2A serve / expose / dev (2.9)enhancementNew feature or requestpriority/p1Must-have: blocks a real adapter or CI gatesize/m1-2 days, multiple files

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions