Skip to content

feat: add Google A2A (Agent2Agent) protocol support#6484

Open
Oxygen56 wants to merge 1 commit into
FlowiseAI:mainfrom
Oxygen56:feat/a2a-protocol-4283
Open

feat: add Google A2A (Agent2Agent) protocol support#6484
Oxygen56 wants to merge 1 commit into
FlowiseAI:mainfrom
Oxygen56:feat/a2a-protocol-4283

Conversation

@Oxygen56
Copy link
Copy Markdown

@Oxygen56 Oxygen56 commented Jun 6, 2026

Implements A2A protocol server for exposing Flowise agents.

Closes #4283

Implements A2A server endpoint exposing Flowise chatflows as
A2A-compatible agents with AgentCard discovery.

Closes FlowiseAI#4283
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements Google A2A (Agent-to-Agent) protocol support, adding discovery endpoints, JSON-RPC message handlers, and task management (including streaming via SSE). Key feedback includes fixing a critical runtime crash caused by shallow copying the Express request object, addressing a potential memory leak from an unbounded in-memory task map, removing unused dead code (taskSSEClients), removing redundant async qualifiers on rate-limiting middleware, and validating chatflowId as a UUID to prevent database query failures.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +344 to +345
const modifiedReq = { ...request, params: { chatflowId } } as any
await a2aController.handleAgentCard(modifiedReq, response, () => {})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

Shallow copying the Express request object using { ...request } breaks the prototype chain, meaning helper methods like req.get() are lost. This will cause a runtime crash (TypeError: req.get is not a function) when handleAgentCard tries to determine the protocol. Instead, mutate request.params directly to preserve the prototype chain.

Suggested change
const modifiedReq = { ...request, params: { chatflowId } } as any
await a2aController.handleAgentCard(modifiedReq, response, () => {})
request.params = { ...request.params, chatflowId }
await a2aController.handleAgentCard(request, response, () => {})

* Map of active A2A tasks.
* In production, this should use a persistent store.
*/
const activeTasks = new Map<string, Task>()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

activeTasks is an unbounded in-memory Map that stores task states indefinitely. Because there is no eviction policy, TTL, or cleanup mechanism, this will cause a memory leak over time as more tasks are processed, eventually leading to Out Of Memory (OOM) crashes. Consider using a TTL cache or a periodic cleanup interval to prune old tasks.

Comment on lines +31 to +34
/**
* Map of task IDs to their SSE response objects.
*/
const taskSSEClients = new Map<string, Response>()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The taskSSEClients map is defined but never populated, updated, or read from anywhere in the codebase. It is dead code and should be removed to maintain clean code.

Comment on lines +43 to +49
const getRateLimiterMiddleware = async (req: Request, res: Response, next: NextFunction) => {
try {
return RateLimiterManager.getInstance().getRateLimiter()(req, res, next)
} catch (error) {
next(error)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The getRateLimiterMiddleware is unnecessarily marked as async and returns a Promise. Express middleware should be synchronous unless they perform asynchronous operations. Wrapping the rate limiter call in an async function is redundant.

Suggested change
const getRateLimiterMiddleware = async (req: Request, res: Response, next: NextFunction) => {
try {
return RateLimiterManager.getInstance().getRateLimiter()(req, res, next)
} catch (error) {
next(error)
}
}
const getRateLimiterMiddleware = (req: Request, res: Response, next: NextFunction) => {
try {
RateLimiterManager.getInstance().getRateLimiter()(req, res, next)
} catch (error) {
next(error)
}
}

Comment on lines +163 to +166
async function handleAgentCard(chatflowId: string, req: Request, res: Response): Promise<void> {
try {
const appServer = getRunningExpressApp()
const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowId })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The chatflowId parameter is passed directly to findOneBy({ id: chatflowId }) without validation. If the database is PostgreSQL and chatflowId is not a valid UUID format, the query will throw a fatal QueryFailedError (invalid input syntax for type uuid), resulting in an unhandled 500 error instead of a clean 400/404 response. Validate the UUID format before querying.

Suggested change
async function handleAgentCard(chatflowId: string, req: Request, res: Response): Promise<void> {
try {
const appServer = getRunningExpressApp()
const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowId })
async function handleAgentCard(chatflowId: string, req: Request, res: Response): Promise<void> {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
if (!uuidRegex.test(chatflowId)) {
res.status(StatusCodes.BAD_REQUEST).json({
error: 'Invalid chatflowId format: ' + chatflowId
})
return
}
try {
const appServer = getRunningExpressApp()
const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowId })

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Support the Google A2A (Agent2Agent) Protocol

1 participant