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
50 changes: 46 additions & 4 deletions python/packages/hosting-a2a/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
# A2A Hosting Helpers (`agent-framework-hosting-a2a`)

Side-effect-free conversion helpers for hosting Agent Framework agents through
the native A2A SDK.
Conversion and native card-generation helpers for hosting Agent Framework
agents and workflows through an application-owned A2A server.

## Public API

- `a2a_to_run(message, *, stream=False)` converts an A2A `Message` to
`AgentRunArgs`.
`AgentRunArgs`; pass `input_modes` for optional advertised-mode validation.
- `a2a_from_run(result)` converts an Agent Framework response, message, or
streaming update to A2A `Part` values.
streaming update to A2A `Part` values; pass `output_modes` for optional
advertised-mode validation.
- `await AgentA2AAdapter(target, ...).get_card()` creates a native `AgentCard`
from an agent or `AgentState`, target metadata, and explicit A2A discovery
policy. The adapter also re-exposes `a2a_to_run(...)` and
`a2a_from_run(...)`, validating against configured card modes by default.
- `a2a_to_workflow_run(message, workflow)` validates one text, raw, or data
part against the workflow's single start-executor input type.
- `a2a_from_workflow_run(result)` converts completed public workflow outputs
to native A2A parts and rejects pending external-input requests.
- `await WorkflowA2AAdapter(target, ...).get_card()` creates a native `AgentCard`
from a workflow or `WorkflowState` and infers defensible modes from declared
workflow types. The adapter also re-exposes the workflow conversion helpers
as `await a2a_to_run(...)` and `a2a_from_run(...)`, validating against
effective card modes by default. Inferred workflow output modes are resolved
by `get_card()` before validated output conversion.

## Boundary

Expand All @@ -20,3 +35,30 @@ constructs.
`a2a_from_run(...)` intentionally returns a flat part list. It preserves
content-level metadata, while applications own A2A message and artifact
boundaries plus message-level metadata.

Card builders return native A2A protobuf values; do not create a parallel card
model or subclass. `AgentA2AAdapter` infers built-in Agent Framework skills from
the resolved agent's `SkillsProvider` instances by default; `infer_skills=False`
disables this. The `skills` argument accepts both Agent Framework `Skill`
values and native A2A `AgentSkill` values. Do not infer A2A skills from function
tools. Capabilities such as streaming and push notifications remain explicit
because they describe the application server. Skill discovery runs with a
`SkillsSourceContext` containing the resolved agent and no session.

`supported_interfaces` contains one native `AgentInterface` per public
protocol endpoint. The URL is where the application mounted the corresponding
A2A routes, and the binding must match the protocol actually served there
(commonly `JSONRPC`, `HTTP+JSON`, or `GRPC`).

A2A mode strings are extensible, but automatic parsing is intentionally
limited to `text`, `application/json`, `application/octet-stream`, and
pass-through concrete media types (with wildcard validation such as
`image/*`). JSON-only output parses Agent Framework JSON text into native A2A
data parts; structured workflow output becomes a data part, or JSON text when
only `text` is advertised. Custom modes are accepted when a native part already
carries that media type, and otherwise raise instead of guessing a serializer.

Workflow mode inference is conservative: string schemas map to `text`, binary
strings to `application/octet-stream`, and JSON-compatible schemas to
`application/json`. Unknown application-specific representations require
explicit card modes and custom application conversion.
181 changes: 164 additions & 17 deletions python/packages/hosting-a2a/README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,185 @@
# agent-framework-hosting-a2a

A2A conversion helpers for app-owned Agent Framework hosting.
Helpers for composing Agent Framework agents and workflows with an
application-owned native A2A server.

The package deliberately does not choose a web framework or wrap the A2A SDK
server lifecycle. It provides two conversion functions:
The package converts protocol values and can generate the common discovery
fields for a native `AgentCard`. It does not provide an `AgentExecutor`, task
lifecycle, event queue, task store, routes, session policy, authentication, or
deployment.

- `a2a_to_run(...)` converts a native A2A `Message` into Agent Framework run
arguments.
- `a2a_from_run(...)` converts an `AgentResponse`, `Message`, or streaming
`AgentResponseUpdate` into native A2A `Part` values.
## Choose the level of help

Application code keeps ownership of the A2A SDK's `AgentExecutor`,
`RequestContext`, `TaskUpdater`, event queue, task store, routes, task state,
artifact IDs, authentication, and deployment.
| API | Adds |
| --- | --- |
| `a2a_to_run`, `a2a_from_run` | Native A2A-to-agent value conversion |
| `AgentA2AAdapter` | Agent card generation plus the agent conversion helpers |
| `a2a_to_workflow_run`, `a2a_from_workflow_run` | Typed workflow input and output conversion |
| `WorkflowA2AAdapter` | Workflow card generation plus the workflow conversion helpers |

`a2a_from_run(...)` preserves content-level metadata on each returned part and
flattens completed responses in message order. The application decides how to
group those parts into A2A messages or artifacts and owns their message-level
metadata and boundaries.
Each level is optional. Applications keep using native A2A SDK objects and can
construct an `AgentCard` directly when they need discovery fields beyond the
common generated surface.

## Agent conversions

The core helpers work with any native A2A `AgentExecutor`:

```python
run = a2a_to_run(context.message)
run = a2a_to_run(context.message, stream=False)
session_id = f"a2a:{context.tenant}:{context.context_id}"
session = await state.get_or_create_session(session_id)
result = await agent.run(
run["messages"],
session=session,
options=run["options"],
stream=run["stream"],
)
await state.set_session(session_id, session)
parts = a2a_from_run(result)

# Native A2A SDK application code publishes `parts` with TaskUpdater.
```

The surrounding A2A application may use Starlette, FastAPI, another ASGI
framework, or the SDK's own application builders. These helpers do not care.
`a2a_from_run(...)` returns a flat part list and preserves content-level
metadata. The application decides how to group those parts into A2A messages
or artifacts and owns their message-level metadata and boundaries.

Standalone conversions are permissive by default. Pass `input_modes` or
`output_modes` to validate the converted parts against an advertised contract:

```python
run = a2a_to_run(message, input_modes=["text", "image/*"])
parts = a2a_from_run(result, output_modes=["text"])
```

### Mode parsing

A2A mode strings are extensible; there is no exhaustive protocol-wide list.
The helpers have an exhaustive set of built-in parsing behaviors:

| Mode | Automatic behavior |
| --- | --- |
| `text` | Uses A2A text parts and Agent Framework text content |
| `application/json` | Parses JSON text into an A2A data part and parses A2A data into typed workflow input |
| `application/octet-stream` | Uses raw byte parts |
| Concrete media types such as `image/png` or `audio/wav` | Preserves matching raw or URL parts |
| Wildcards such as `image/*` | Validates matching concrete media types |

Custom mode strings may still be advertised. They pass validation when the
native part already carries that exact media type, but conversion raises when
it would need to synthesize that representation without a built-in parser.
Configured mode values must be non-empty strings.

## Supported interfaces

`supported_interfaces` tells an A2A client where and how it can call the
application. Add one `AgentInterface` for each protocol binding the server
actually exposes:

```python
supported_interfaces = [
AgentInterface(
url="https://example.com/a2a",
protocol_binding="JSONRPC",
)
]
```

The `url` is the public base URL where the matching A2A routes are mounted;
include a path such as `/a2a` when the application mounts them below the
domain root. `protocol_binding` identifies the wire protocol implemented at
that URL, commonly `JSONRPC`, `HTTP+JSON`, or `GRPC`. Advertise only bindings
that the application has configured. `protocol_version` and `tenant` are
optional native A2A interface fields for deployments that use them.

## Generate an agent card

`AgentA2AAdapter` infers the public name and description from the agent and uses
conservative text input/output modes. Pass either an agent or an existing
`AgentState`; `get_card()` is async so factory-backed states can resolve their
target.

By default, the card also discovers Agent Framework `Skill` values from
`SkillsProvider` instances on the agent. The guaranteed skill frontmatter
name and description become a native A2A `AgentSkill`, using the card's input
and output modes. Set `infer_skills=False` to disable discovery. The `skills`
parameter also accepts explicit Agent Framework `Skill` values or fully
specified native A2A `AgentSkill` values when tags, examples, security, or
skill-specific modes need to be controlled directly. Card discovery happens
outside an agent run, so context-aware skill sources receive no session; use
explicit skills or disable inference when the advertised list is
session-specific.

Server capabilities stay explicit because they describe the public
application contract, not the agent's `run` method.

The adapter re-exposes `a2a_to_run(...)` and `a2a_from_run(...)`, so a native
executor can use the same object for card setup and request conversion without
importing the standalone helpers. Adapter conversions validate against the
configured card modes by default; pass `validate_modes=False` to opt out:

```python
run = adapter.a2a_to_run(context.message, stream=True)
parts = adapter.a2a_from_run(result)
```

```python
from a2a.types import AgentCapabilities, AgentInterface
from agent_framework_hosting_a2a import AgentA2AAdapter

card = await AgentA2AAdapter(
state,
version="1.0.0",
supported_interfaces=[
AgentInterface(url="https://example.com/a2a", protocol_binding="JSONRPC")
],
capabilities=AgentCapabilities(streaming=True),
).get_card()
```

## Host a workflow

Workflow input conversion follows the single start-executor input type:

- strings use one A2A text part;
- bytes use one raw part;
- structured and scalar JSON values use one data part.

The output helper converts public workflow outputs to native A2A parts.
Pending human-input requests raise so the application can implement its own
continuation policy.

```python
workflow_input = a2a_to_workflow_run(context.message, workflow)
result = await workflow.run(workflow_input, stream=False)
parts = a2a_from_workflow_run(result)
```

`WorkflowA2AAdapter` infers modes from the workflow's declared input and output
types. It accepts a workflow or `WorkflowState`. Supply explicit modes for an
application-specific representation:

```python
card = await WorkflowA2AAdapter(
workflow_state,
version="1.0.0",
supported_interfaces=[
AgentInterface(url="https://example.com/a2a", protocol_binding="JSONRPC")
],
skills=[workflow_skill],
).get_card()
```

It also exposes `await adapter.a2a_to_run(message)` and
`adapter.a2a_from_run(result)` for workflow conversion. These methods validate
against the effective card modes by default. When workflow output modes are
inferred, call `get_card()` before converting output so the adapter has
resolved the advertised contract.

Streaming workflow progress, artifacts, task status, checkpoints, and
human-in-the-loop continuation remain part of the native executor and
application contract.

The surrounding application may use Starlette, FastAPI, another ASGI
framework, or the A2A SDK's application builders.
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@

import importlib.metadata

from ._conversion import a2a_from_run, a2a_to_run
from ._adapters import AgentA2AAdapter, WorkflowA2AAdapter
from ._conversion import a2a_from_run, a2a_from_workflow_run, a2a_to_run, a2a_to_workflow_run

try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0"

__all__ = [
"AgentA2AAdapter",
"WorkflowA2AAdapter",
"__version__",
"a2a_from_run",
"a2a_from_workflow_run",
"a2a_to_run",
"a2a_to_workflow_run",
]
Loading
Loading