Skip to content

feat: A2A (Agent-to-Agent) protocol support#3567

Open
Oxygen56 wants to merge 1 commit into
openai:mainfrom
Oxygen56:feat/a2a-support
Open

feat: A2A (Agent-to-Agent) protocol support#3567
Oxygen56 wants to merge 1 commit into
openai:mainfrom
Oxygen56:feat/a2a-support

Conversation

@Oxygen56
Copy link
Copy Markdown

@Oxygen56 Oxygen56 commented Jun 2, 2026

Summary

Add bidirectional A2A (Agent-to-Agent) protocol integration for the OpenAI Agents SDK, enabling interoperability between OpenAI agents and any A2A-compatible agent built with any framework/language.

Closes #472.

What is included

Bidirectional Message Converter (_converter.py)

  • A2A Message/Part/Task/Artifact ↔ OpenAI TResponseInputItem/RunResult/StreamEvent
  • Pure functions, full round-trip fidelity, 27 tests

A2A Client Tool (_client_tool.py)

tool = await A2AClientTool.from_url(url="http://agent:8080", tool_name="researcher", ...)
agent = Agent(tools=[tool.as_function_tool()])
  • Auto-fetches AgentCard from .well-known/agent-card.json
  • Timeout with automatic remote task cancellation
  • 7 tests with fake A2A client

A2A Server Agent (_server_executor.py)

executor = A2AServerAgent(agent=my_agent)
handler = DefaultRequestHandler(executor=executor)
  • Implements AgentExecutor interface
  • Streaming + non-streaming paths
  • Multi-turn conversation via context_id
  • TTL-based session eviction

AgentCard Generator (_agent_card.py)

  • Auto-generates A2A AgentCards from Agent metadata
  • FunctionTools → skills, handoffs → sub-agent skills

Design decisions

Decision Rationale
a2a-sdk as optional dependency ([a2a] extra) Follows existing pattern (redis, mongodb, dapr, etc.)
Lazy imports via __getattr__ Zero import cost unless actually used
Pure-function converters Easy to test, no side effects, composable
frozenset for terminal states Immutable, hashable, safe for async

Tests

37 passed in 1.01s
- 27 converter tests (message, history, artifact, task, streaming, round-trip)
- 7 client tool tests (construction, invocation, timeout, cancellation, context propagation)
- 3 server executor tests (completed task, session persistence, cancel cleanup)

Breaking changes

None. This is a purely additive extension under agents.extensions.a2a.

Add bidirectional A2A protocol integration for the OpenAI Agents SDK.

Includes client tool, server agent, message converter, and
AgentCard generator. A2A is an optional dependency ([a2a] extra).

fix(a2a): address Codex review feedback

- Export A2AServerAgent from extensions.a2a
- Include default protocol binding in generated AgentCards
- Honor timeout_seconds=None to disable timeout
- Treat TASK_STATE_REJECTED as tool error
- Publish A2A lifecycle events through the agent event queue

Co-Authored-By: Oxygen56 <100782273+Oxygen56@users.noreply.github.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1f0d2d226

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +113 to +116
AgentInterface(
url=url,
protocol_binding="a2a-json-rpc",
protocol_version="1.0",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the standard JSONRPC binding

When supported_interfaces is omitted, every generated AgentCard advertises a2a-json-rpc, but A2A 1.0 transport matching uses the standard binding strings such as JSONRPC, GRPC, and HTTP+JSON. Agents exposed with this helper will therefore publish cards that the default ClientFactory cannot match, making the generated server card unusable by standard A2A clients unless callers manually override the interface.

Useful? React with 👍 / 👎.

Comment on lines +141 to +143
if self.run_config is not None and getattr(
self.run_config, "streaming_enabled", False
):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enable streaming from request context, not RunConfig

This branch is effectively unreachable for normal SDK usage because RunConfig does not define a streaming_enabled field, so both message/send and message/stream executions fall through to Runner.run(). As a result the server advertises streaming support but streaming clients only receive the initial working update and final task, not the lifecycle events produced by Runner.run_streamed().

Useful? React with 👍 / 👎.

Comment on lines +271 to +275
new_items = getattr(result, "new_items", [])
if new_items:
existing, _ = self._sessions.get(context_id, ([], time.monotonic()))
existing.extend(new_items)
self._sessions[context_id] = (existing, time.monotonic())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist model input items instead of RunItem objects

On a second request with the same context_id, _get_session() returns the raw RunItem instances stored here and execute() passes them directly into Runner.run() alongside the new A2A message. The runner expects response input dicts/Pydantic items rather than SDK RunItem wrappers, so real model calls can receive non-serializable history; convert each run item via to_input_item() before extending the session.

Useful? React with 👍 / 👎.

Comment on lines +182 to +189
except asyncio.CancelledError:
# Task was cancelled by the framework
failed_task = openai_error_to_failed_task(
asyncio.CancelledError("Task was cancelled."),
task_id=task_id,
context_id=context_id,
)
await self._publish_task(event_queue, failed_task)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish cancellation as canceled, not failed

When cancel() cancels a running task, the cancelled execute() coroutine lands here and publishes openai_error_to_failed_task(), so clients can observe a final TASK_STATE_FAILED task after requesting cancellation. For cancellation flows this should emit a canceled task/status (or avoid overwriting the cancel status), otherwise successful cancels are reported as agent failures.

Useful? React with 👍 / 👎.

@seratch
Copy link
Copy Markdown
Member

seratch commented Jun 4, 2026

Thanks for sharing this. However, we don't have immediate plans to add this module within this SDK. If you need it for your projects and you're willing to maintain it as your own package, please feel free to publish such as your own project.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A2A (Agent2Agent) support

2 participants