mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-09 01:27:41 -07:00
feat: split chat bridge into standalone microservice
Separate the Google Chat bridge from the A2A agent server so each can scale independently on Cloud Run. The bridge is a lightweight proxy (concurrency=80) while the agent needs concurrency=1. - Add standalone entry point (src/chat-bridge/server.ts) - Add Dockerfile.chat-bridge and cloudbuild-chat-bridge.yaml - Remove chat bridge setup from app.ts - Inline A2UI constants in a2a-bridge-client.ts (no agent deps) - Update README for two-service architecture
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# Standalone Google Chat bridge server.
|
||||
# Connects to the A2A agent server over HTTP — no agent dependencies needed.
|
||||
FROM docker.io/library/node:20-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy pre-installed node_modules and pre-built dist
|
||||
COPY package.json package-lock.json ./
|
||||
COPY node_modules/ node_modules/
|
||||
COPY packages/a2a-server/ packages/a2a-server/
|
||||
|
||||
USER node
|
||||
|
||||
ENV PORT=8080
|
||||
ENV NODE_ENV=production
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["node", "packages/a2a-server/dist/src/chat-bridge/server.js"]
|
||||
@@ -14,16 +14,18 @@ Google Chat ──webhook──> Chat Bridge ──A2A──> A2A Server
|
||||
└── Chat REST API (push responses back to Chat)
|
||||
```
|
||||
|
||||
The server runs as a single Cloud Run container with two logical components:
|
||||
This package contains two independently deployable services:
|
||||
|
||||
1. **A2A Server** - Standard A2A protocol endpoint (JSON-RPC + SSE streaming)
|
||||
that wraps the Gemini CLI agent
|
||||
2. **Chat Bridge** - Translates between Google Chat webhook events and A2A
|
||||
protocol, pushing streamed results back via the Chat REST API
|
||||
1. **A2A Server** (`src/http/server.ts`) - Standard A2A protocol endpoint
|
||||
(JSON-RPC + SSE streaming) that wraps the Gemini CLI agent. Heavy workload —
|
||||
deploy with `concurrency=1`.
|
||||
2. **Chat Bridge** (`src/chat-bridge/server.ts`) - Lightweight proxy that
|
||||
translates Google Chat webhooks into A2A protocol calls. Connects to the A2A
|
||||
server over HTTP. Deploy with high concurrency (`concurrency=80`).
|
||||
|
||||
The Chat Bridge responds immediately to webhooks with "Processing..." (avoiding
|
||||
Google Chat's 30s timeout), then streams results from the A2A agent and pushes
|
||||
them to Chat as they arrive.
|
||||
them to Chat via the REST API.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -39,19 +41,28 @@ them to Chat as they arrive.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
| ---------------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `GEMINI_API_KEY` | Yes | Gemini API key for the agent |
|
||||
| `CODER_AGENT_PORT` | No | Server port (default: `8080`) |
|
||||
| `CODER_AGENT_HOST` | No | Bind host (default: `localhost`, set `0.0.0.0` for containers) |
|
||||
| `CODER_AGENT_WORKSPACE_PATH` | No | Agent workspace directory (default: `/workspace`) |
|
||||
| `GCS_BUCKET_NAME` | No | GCS bucket for task & session persistence |
|
||||
| `GEMINI_YOLO_MODE` | No | Set `true` to auto-approve all tool calls |
|
||||
| `CHAT_BRIDGE_A2A_URL` | No | A2A server URL for the Chat bridge (e.g. `http://localhost:8080`). Presence enables the Chat bridge. |
|
||||
| `CHAT_PROJECT_NUMBER` | No | Google Chat project number for JWT verification |
|
||||
| `CHAT_SA_KEY_PATH` | No | Path to service account key for Chat API auth (uses ADC if not set) |
|
||||
| `CHAT_BRIDGE_DEBUG` | No | Set `true` for verbose bridge logging |
|
||||
| `GIT_TERMINAL_PROMPT` | No | Set `0` to prevent git credential prompts in headless environments |
|
||||
### A2A Server
|
||||
|
||||
| Variable | Required | Description |
|
||||
| ---------------------------- | -------- | -------------------------------------------------------------- |
|
||||
| `GEMINI_API_KEY` | Yes | Gemini API key for the agent |
|
||||
| `CODER_AGENT_PORT` | No | Server port (default: `8080`) |
|
||||
| `CODER_AGENT_HOST` | No | Bind host (default: `localhost`, set `0.0.0.0` for containers) |
|
||||
| `CODER_AGENT_WORKSPACE_PATH` | No | Agent workspace directory (default: `/workspace`) |
|
||||
| `GCS_BUCKET_NAME` | No | GCS bucket for task persistence |
|
||||
| `GEMINI_YOLO_MODE` | No | Set `true` to auto-approve all tool calls |
|
||||
| `GIT_TERMINAL_PROMPT` | No | Set `0` to prevent git credential prompts in headless env |
|
||||
|
||||
### Chat Bridge
|
||||
|
||||
| Variable | Required | Description |
|
||||
| --------------------- | -------- | -------------------------------------------------------------- |
|
||||
| `A2A_SERVER_URL` | Yes | URL of the A2A agent server (e.g. `http://localhost:8080`) |
|
||||
| `PORT` | No | Server port (default: `8080`) |
|
||||
| `CHAT_PROJECT_NUMBER` | No | Google Chat project number for JWT verification |
|
||||
| `CHAT_SA_KEY_PATH` | No | Path to service account key for Chat API (uses ADC if not set) |
|
||||
| `GCS_BUCKET_NAME` | No | GCS bucket for session persistence |
|
||||
| `CHAT_BRIDGE_DEBUG` | No | Set `true` for verbose bridge logging |
|
||||
|
||||
## Local Development
|
||||
|
||||
@@ -64,16 +75,24 @@ npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run
|
||||
### Run the A2A Server
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-api-key"
|
||||
export CODER_AGENT_PORT=8080
|
||||
export CHAT_BRIDGE_A2A_URL=http://localhost:8080
|
||||
|
||||
node packages/a2a-server/dist/src/http/server.js
|
||||
```
|
||||
|
||||
### Run the Chat Bridge (separate terminal)
|
||||
|
||||
```bash
|
||||
export A2A_SERVER_URL=http://localhost:8080
|
||||
export PORT=8090
|
||||
|
||||
node packages/a2a-server/dist/src/chat-bridge/server.js
|
||||
```
|
||||
|
||||
### Test the A2A endpoint
|
||||
|
||||
```bash
|
||||
@@ -143,21 +162,27 @@ gcloud artifacts repositories create gemini-a2a \
|
||||
gsutil mb -l $REGION gs://gemini-a2a-sessions-$PROJECT_ID
|
||||
```
|
||||
|
||||
### 3. Build with Cloud Build
|
||||
### 3. Build both images
|
||||
|
||||
```bash
|
||||
# Build A2A agent server
|
||||
gcloud builds submit \
|
||||
--config=packages/a2a-server/cloudbuild.yaml \
|
||||
--project=$PROJECT_ID
|
||||
|
||||
# Build Chat bridge
|
||||
gcloud builds submit \
|
||||
--config=packages/a2a-server/cloudbuild-chat-bridge.yaml \
|
||||
--project=$PROJECT_ID
|
||||
```
|
||||
|
||||
### 4. Deploy to Cloud Run
|
||||
### 4. Deploy A2A agent server
|
||||
|
||||
```bash
|
||||
export IMAGE=us-central1-docker.pkg.dev/$PROJECT_ID/gemini-a2a/a2a-server:latest
|
||||
export AGENT_IMAGE=us-central1-docker.pkg.dev/$PROJECT_ID/gemini-a2a/a2a-server:latest
|
||||
|
||||
gcloud run deploy gemini-a2a-server \
|
||||
--image=$IMAGE \
|
||||
--image=$AGENT_IMAGE \
|
||||
--region=$REGION \
|
||||
--project=$PROJECT_ID \
|
||||
--platform=managed \
|
||||
@@ -167,14 +192,38 @@ gcloud run deploy gemini-a2a-server \
|
||||
--timeout=300 \
|
||||
--concurrency=1 \
|
||||
--max-instances=1 \
|
||||
--set-env-vars="GEMINI_YOLO_MODE=true,GCS_BUCKET_NAME=gemini-a2a-sessions-$PROJECT_ID,CHAT_BRIDGE_A2A_URL=http://localhost:8080" \
|
||||
--set-env-vars="GEMINI_YOLO_MODE=true,GCS_BUCKET_NAME=gemini-a2a-sessions-$PROJECT_ID" \
|
||||
--set-secrets="GEMINI_API_KEY=gemini-api-key:latest"
|
||||
```
|
||||
|
||||
### 5. Deploy Chat bridge
|
||||
|
||||
Get the A2A server URL first:
|
||||
|
||||
```bash
|
||||
export A2A_URL=$(gcloud run services describe gemini-a2a-server \
|
||||
--region=$REGION --project=$PROJECT_ID --format='value(status.url)')
|
||||
|
||||
export BRIDGE_IMAGE=us-central1-docker.pkg.dev/$PROJECT_ID/gemini-a2a/chat-bridge:latest
|
||||
|
||||
gcloud run deploy gemini-chat-bridge \
|
||||
--image=$BRIDGE_IMAGE \
|
||||
--region=$REGION \
|
||||
--project=$PROJECT_ID \
|
||||
--platform=managed \
|
||||
--allow-unauthenticated \
|
||||
--memory=512Mi \
|
||||
--cpu=1 \
|
||||
--timeout=60 \
|
||||
--concurrency=80 \
|
||||
--max-instances=5 \
|
||||
--set-env-vars="A2A_SERVER_URL=$A2A_URL,GCS_BUCKET_NAME=gemini-a2a-sessions-$PROJECT_ID"
|
||||
```
|
||||
|
||||
> **Important**: After initial deployment, always use `--update-env-vars`
|
||||
> instead of `--set-env-vars` to avoid wiping existing environment variables.
|
||||
|
||||
### 5. Update an existing deployment
|
||||
### 6. Update an existing deployment
|
||||
|
||||
```bash
|
||||
# Update env vars without replacing existing ones
|
||||
@@ -218,9 +267,9 @@ the Chat app's service account.
|
||||
[Google Cloud Console > APIs & Services > Google Chat API > Configuration](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat)
|
||||
2. Set **App name** and **Description**
|
||||
3. Under **Connection settings**, select **HTTP endpoint URL**
|
||||
4. Set the URL to your Cloud Run service URL + `/chat/webhook`:
|
||||
4. Set the URL to your **Chat bridge** Cloud Run service URL + `/chat/webhook`:
|
||||
```
|
||||
https://gemini-a2a-server-HASH-uc.a.run.app/chat/webhook
|
||||
https://gemini-chat-bridge-HASH-uc.a.run.app/chat/webhook
|
||||
```
|
||||
5. Under **Authentication Audience**, select **HTTP endpoint URL**
|
||||
6. Under **Visibility**, choose who can use the app
|
||||
@@ -235,7 +284,7 @@ If your Cloud Run service requires authentication (recommended):
|
||||
# Get the Chat service account
|
||||
# It's usually chat@system.gserviceaccount.com
|
||||
|
||||
gcloud run services add-iam-policy-binding gemini-a2a-server \
|
||||
gcloud run services add-iam-policy-binding gemini-chat-bridge \
|
||||
--region=$REGION \
|
||||
--project=$PROJECT_ID \
|
||||
--member="serviceAccount:chat@system.gserviceaccount.com" \
|
||||
@@ -259,10 +308,15 @@ When messaging the bot in Google Chat:
|
||||
|
||||
### "Gemini CLI Agent is not responding" in Google Chat
|
||||
|
||||
This usually means the agent hit Google Chat's 30-second webhook timeout before
|
||||
the bridge could return "Processing...". Check Cloud Run logs:
|
||||
This usually means the bridge couldn't return "Processing..." within Google
|
||||
Chat's 30-second timeout. Check Cloud Run logs for both services:
|
||||
|
||||
```bash
|
||||
# Chat bridge logs
|
||||
gcloud run services logs read gemini-chat-bridge \
|
||||
--region=$REGION --project=$PROJECT_ID --limit=50
|
||||
|
||||
# A2A agent logs
|
||||
gcloud run services logs read gemini-a2a-server \
|
||||
--region=$REGION --project=$PROJECT_ID --limit=50
|
||||
```
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
steps:
|
||||
# Step 1: Install all dependencies and build
|
||||
- name: 'node:20-slim'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- '-c'
|
||||
- |
|
||||
apt-get update && apt-get install -y python3 make g++ git
|
||||
npm pkg delete scripts.prepare
|
||||
npm install
|
||||
npm run build
|
||||
env:
|
||||
- 'HUSKY=0'
|
||||
|
||||
# Step 2: Build Docker image for Chat bridge
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args:
|
||||
- 'build'
|
||||
- '-t'
|
||||
- 'us-central1-docker.pkg.dev/$PROJECT_ID/gemini-a2a/chat-bridge:latest'
|
||||
- '-f'
|
||||
- 'packages/a2a-server/Dockerfile.chat-bridge'
|
||||
- '.'
|
||||
|
||||
# Step 3: Push to Artifact Registry
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args:
|
||||
- 'push'
|
||||
- 'us-central1-docker.pkg.dev/$PROJECT_ID/gemini-a2a/chat-bridge:latest'
|
||||
|
||||
images:
|
||||
- 'us-central1-docker.pkg.dev/$PROJECT_ID/gemini-a2a/chat-bridge:latest'
|
||||
timeout: '1800s'
|
||||
options:
|
||||
machineType: 'E2_HIGHCPU_8'
|
||||
@@ -29,7 +29,10 @@ import {
|
||||
} from '@a2a-js/sdk/client';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { A2UI_EXTENSION_URI, A2UI_MIME_TYPE } from '../a2ui/a2ui-extension.js';
|
||||
|
||||
// Inline A2UI constants so the chat bridge has no dependency on ../a2ui/
|
||||
const A2UI_EXTENSION_URI = 'https://a2ui.org/a2a-extension/a2ui/v0.10';
|
||||
const A2UI_MIME_TYPE = 'application/json+a2ui';
|
||||
|
||||
export type A2AResponse = Message | Task;
|
||||
export type A2AStreamEventData =
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Standalone Google Chat bridge server.
|
||||
* Runs independently from the A2A agent server — connects to it
|
||||
* via the A2A protocol over HTTP. Deploy as a separate Cloud Run
|
||||
* service for independent scaling.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { createChatBridgeRoutes } from './routes.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
function main() {
|
||||
const a2aServerUrl = process.env['A2A_SERVER_URL'];
|
||||
if (!a2aServerUrl) {
|
||||
logger.error(
|
||||
'[ChatBridge] A2A_SERVER_URL is required. Set it to the A2A agent server URL.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const chatRoutes = createChatBridgeRoutes({
|
||||
a2aServerUrl,
|
||||
projectNumber: process.env['CHAT_PROJECT_NUMBER'],
|
||||
debug: process.env['CHAT_BRIDGE_DEBUG'] === 'true',
|
||||
gcsBucket: process.env['GCS_BUCKET_NAME'],
|
||||
serviceAccountKeyPath: process.env['CHAT_SA_KEY_PATH'],
|
||||
});
|
||||
app.use(chatRoutes);
|
||||
|
||||
// Root health check
|
||||
app.get('/', (_req, res) => {
|
||||
res.json({
|
||||
service: 'gemini-chat-bridge',
|
||||
status: 'ok',
|
||||
a2aServerUrl,
|
||||
});
|
||||
});
|
||||
|
||||
const port = Number(process.env['PORT'] || 8080);
|
||||
const host = process.env['HOST'] || '0.0.0.0';
|
||||
|
||||
app.listen(port, host, () => {
|
||||
logger.info(`[ChatBridge] Server started on http://${host}:${port}`);
|
||||
logger.info(`[ChatBridge] Connected to A2A agent at ${a2aServerUrl}`);
|
||||
});
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
logger.error('[ChatBridge] Unhandled exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
main();
|
||||
@@ -29,7 +29,6 @@ import { debugLogger, SimpleExtensionLoader } from '@google/gemini-cli-core';
|
||||
import type { Command, CommandArgument } from '../commands/types.js';
|
||||
import { GitService } from '@google/gemini-cli-core';
|
||||
import { getA2UIAgentExtension } from '../a2ui/a2ui-extension.js';
|
||||
import { createChatBridgeRoutes } from '../chat-bridge/routes.js';
|
||||
|
||||
type CommandResponse = {
|
||||
name: string;
|
||||
@@ -203,29 +202,8 @@ export async function createApp() {
|
||||
requestStorage.run({ req }, next);
|
||||
});
|
||||
|
||||
// Mount Google Chat bridge routes BEFORE A2A SDK routes.
|
||||
// The A2A SDK's setupRoutes registers a catch-all jsonRpcHandler middleware
|
||||
// at baseUrl="" that intercepts ALL POST requests and returns 400 for
|
||||
// non-JSON-RPC payloads. Chat bridge must be registered first.
|
||||
const chatBridgeUrl =
|
||||
process.env['CHAT_BRIDGE_A2A_URL'] || process.env['CODER_AGENT_PORT']
|
||||
? `http://localhost:${process.env['CODER_AGENT_PORT'] || '8080'}`
|
||||
: undefined;
|
||||
if (chatBridgeUrl) {
|
||||
expressApp.use(express.json());
|
||||
const chatRoutes = createChatBridgeRoutes({
|
||||
a2aServerUrl: chatBridgeUrl,
|
||||
projectNumber: process.env['CHAT_PROJECT_NUMBER'],
|
||||
debug: process.env['CHAT_BRIDGE_DEBUG'] === 'true',
|
||||
gcsBucket: process.env['GCS_BUCKET_NAME'],
|
||||
serviceAccountKeyPath: process.env['CHAT_SA_KEY_PATH'],
|
||||
});
|
||||
expressApp.use(chatRoutes);
|
||||
logger.info(
|
||||
`[CoreAgent] Google Chat bridge enabled at /chat/webhook (A2A: ${chatBridgeUrl})`,
|
||||
);
|
||||
}
|
||||
|
||||
// Google Chat bridge runs as a separate service (src/chat-bridge/server.ts).
|
||||
// It connects to this A2A server over HTTP.
|
||||
const appBuilder = new A2AExpressApp(requestHandler);
|
||||
expressApp = appBuilder.setupRoutes(expressApp, '');
|
||||
expressApp.use(express.json());
|
||||
|
||||
Reference in New Issue
Block a user