Connecting Google Gemini Enterprise via A2A
The A2A (Agent-to-Agent) protocol allows external agents to receive messages from orchestrators like Google Gemini Enterprise. In this guide, you'll create an HTTP Listener in Data² that works as an A2A endpoint and receives messages sent by Gemini Enterprise users.
:::warning Draft Protocol — Limited Implementation in Gemini Enterprise
The A2A protocol is at version 0.3 (draft). Google Gemini Enterprise implements a partial and fairly limited version of this protocol. Not all features from the official spec work. This guide documents what actually works in practice with Gemini Enterprise today.
:::
Prerequisites
- Access to the Google Gemini Enterprise Console with permission to create A2A agents.
- A project in Data² Builder.
Step 1: Create the HTTP Listener in Data²
The HTTP Listener will be the endpoint that Gemini Enterprise calls whenever a user sends a message to the agent.
- In the Builder, double-click the background, select the Rush tab, then HTTP Listener.
- Data² will automatically generate a reference for the listener.
- Copy the generated URL — you'll need it in the next step.
The HTTP Listener URL appears in the component panel. Click the button next to the URL to copy it.
Step 2: Register the agent in Gemini Enterprise
Go to your app's agents section in the console:
https://console.cloud.google.com/gemini-enterprise/locations/global/engines/YOUR_APP_ID/agentic/agents
Click Add and on the next screen choose Custom agent via A2A.
On the next screen — Import agent — paste the agent card JSON into the text field and click Next.
The Agent Card
The agent card describes your agent: name, endpoint URL, capabilities, and skills. Paste this JSON into the "Agent card JSON" field, replacing the URL with the one you copied in the previous step:
{
"protocolVersion": "0.3.0",
"name": "My Data² Agent",
"description": "Agent that processes messages from Gemini Enterprise via a Data² HTTP Listener.",
"url": "https://your-app.data2.link/rush/YOUR_HTTP_LISTENER",
"version": "1.0.0",
"capabilities": {
"streaming": false,
"pushNotifications": false
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"skills": [
{
"id": "chat",
"name": "Chat",
"description": "Answers questions and performs tasks via natural language."
}
]
}
streaming: false is correct for Gemini Enterprise — it doesn't support streaming. The skills field is required and must have at least one item.
After pasting the JSON, click View agent details to confirm the card was read correctly, then click Next to configure Authorizations (optional — see the authentication section further below).
Gemini Enterprise stores a static copy of the agent card. If the HTTP Listener URL changes, you need to remove and re-register the agent.
Step 3: Understand the incoming payload
Gemini Enterprise sends messages in JSON-RPC 2.0 format with method: "message/send" via POST to your HTTP Listener URL.
What contextId is and how to persist a session
The contextId is the identifier for the conversation session. It groups all messages from the same thread — it's the equivalent of a "conversation ID" in A2A terms.
- On the first message, Gemini doesn't send a
contextId. It's up to your agent to generate one and return it in the response. - On subsequent messages in the same thread, Gemini sends the
contextIdyou returned earlier.
Gemini doesn't keep history on its own. If you need context between messages (for example, to pass conversation history to an AI model), you need to store and retrieve that state on your side.
How to do this in Data²:
- Create a DataSet to store sessions — for example, with the fields
contextId(string, search key) andhistory(text or JSON with the accumulated history). - In the
HTTP Listenerhandler, when receiving a message:- Read
body.params.message.contextId. - If there's no
contextId→ create a new ID and insert a record in the DataSet with the initial history. - If there is a
contextId→ look up the record in the DataSet by that ID and retrieve the history.
- Read
- Process the message (with the history in hand, if needed).
- Update the record in the DataSet, adding the new message and response to the history.
- Return the response with the
contextId— whether newly created or the one from the request.
First message (no contextId)
The first message of a conversation doesn't contain a contextId:
{
"id": "49506c0d-ba88-482a-9ac8-33141ded6e8f",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"configuration": {
"acceptedOutputModes": [],
"blocking": true
},
"message": {
"kind": "message",
"messageId": "cfc94f40-2d44-439c-a098-155e64b025be",
"parts": [
{
"kind": "text",
"text": "Hi, I need some help!"
}
],
"role": "user"
},
"metadata": {}
}
}
The most relevant fields inside body:
| Field | Description |
|---|---|
id | JSON-RPC request ID — must be mirrored in the response |
params.message.parts[0].text | The user's message text |
params.message.messageId | Unique message ID |
params.message.contextId | Session ID — absent on the first message |
Subsequent messages (with contextId)
From the second message in the same thread onward, Gemini sends the contextId you returned in the first response:
{
"id": "6b9ca6b5-d633-4933-9778-ff2341f009c2",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"configuration": {
"acceptedOutputModes": [],
"blocking": true
},
"message": {
"contextId": "my-session-123",
"kind": "message",
"messageId": "6d8ec61b-fe19-44d3-ae95-01264c8dfe01",
"parts": [
{
"kind": "text",
"text": "Hey, continuing..."
}
],
"role": "user"
},
"metadata": {}
}
}
Use the contextId to identify the session and keep track of the conversation history.
Step 4: Authentication
When registering the agent, you can configure an authentication mechanism so Gemini Enterprise injects credentials into the requests it sends to your listener. When this is configured, every request arrives with an authorization: Bearer <token> header:
{
"authorization": "Bearer ya29.a0ATkoCc79HA07MBCYf_TxY6W4...",
"content-type": "application/json",
"user-agent": "Google",
"host": "your-app.data2.link",
...
}
The token is a Google OAuth2 access token representing the user authenticated in Gemini Enterprise. It's generated via an OAuth2 flow with user consent — it's not a service token.
What to do with the token
Inside your HTTP Listener handler, you can access the token via headers.authorization. From there, your options are:
- Ignore it — if your agent doesn't need authentication, you can simply skip validating the token.
- Forward it to a Google Cloud API the user has authorized (e.g., Google Sheets, Drive, etc.).
- Introspect it — call
https://oauth2.googleapis.com/tokeninfo?access_token=TOKENto get the user's identity (email, sub), though this adds latency.
:::caution Limitation: a single Bearer token
Gemini Enterprise injects exactly one Authorization header. If your listener needs its own credential (e.g., authenticating against Data² itself) while also forwarding the user's token to another API, you'll have to handle that manually — you can't have two Bearer tokens at once.
:::
Step 5: Configure the handler to respond
The response must follow the A2A format. The most important field is contextId:
- If the message had no
contextId→ create a new one and return it. - If the message already had a
contextId→ echo the same one back in the response.
Response structure
{
"jsonrpc": "2.0",
"id": "<same id as the received request>",
"result": {
"id": "<task id — can be any unique string>",
"contextId": "<session id>",
"status": {
"state": "completed"
},
"artifacts": [
{
"artifactId": "<artifact id>",
"name": "response",
"parts": [
{
"text": "Your response goes here. Markdown is supported."
}
]
}
]
}
}
Possible status.state values
| State | Description |
|---|---|
completed | Task completed successfully |
failed | An error occurred |
working | In progress (streaming only — not used here) |
input-required | The agent needs more information from the user |
Known limitations of Gemini Enterprise
Gemini Enterprise implements a fairly restricted version of the A2A protocol. What doesn't work or has limited support:
- Streaming (
message/stream) — not supported; Gemini always sendsblocking: true. - Push notifications — not implemented in the Gemini Enterprise context.
/.well-known/agent.json— Gemini doesn't do automatic discovery; the agent card is submitted manually during registration.tasks/getandtasks/cancel— not used; each message is handled synchronously and independently.acceptedOutputModes— always sent empty ([]); ignored in practice.- History in the payload — Gemini doesn't send conversation history; you need to maintain context on your side using
contextId. - Intermediate states —
workingandinput-requiredaren't supported in practice; all processing must be synchronous.
Artifact types — Markdown and images by URL
The A2A spec defines several parts types within an artifact: text, file (with a URI or base64 bytes), data (structured JSON), among others. On Gemini Enterprise, most of them don't work.
In practice, what works reliably is:
1. text with Markdown content — Gemini Enterprise renders Markdown directly in the UI: tables, lists, bold text, headings, code blocks, and emojis are all supported.
"parts": [
{
"text": "## Result\n\nHere's your response in **Markdown**."
}
]
2. Images via file with a URI — images work using the file type with a public URL:
"parts": [
{
"kind": "file",
"file": {
"mimeType": "image/png",
"uri": "https://your-url.com/image.png"
}
}
]
Avoid using file with bytes (base64), data, or multiple parts per artifact — the behavior is unpredictable or simply ignored.
Summary flow
User → Gemini Enterprise → POST /your-http-listener
↓
body.params.message.parts[0].text
body.params.message.contextId (if present)
headers.authorization (if auth is configured)
↓
Handler processes the message
↓
Returns JSON-RPC with contextId
↓
User ← Gemini Enterprise ← Displays artifacts[0].parts[0].text
On the first message, create and return a contextId. On subsequent messages, Gemini sends back the same contextId so you can identify the session.