From dcb7b889a654419f8452f18b35e041beca235c2b Mon Sep 17 00:00:00 2001 From: Oxygen <1391083091@qq.com> Date: Sat, 6 Jun 2026 13:05:36 +0800 Subject: [PATCH] feat: add Google A2A (Agent2Agent) protocol support Implements A2A server endpoint exposing Flowise chatflows as A2A-compatible agents with AgentCard discovery. Closes #4283 --- packages/server/src/controllers/a2a/index.ts | 96 ++++ packages/server/src/index.ts | 22 + packages/server/src/routes/a2a/index.ts | 44 ++ packages/server/src/routes/index.ts | 2 + packages/server/src/services/a2a/index.ts | 517 +++++++++++++++++++ packages/server/src/services/a2a/types.ts | 175 +++++++ 6 files changed, 856 insertions(+) create mode 100644 packages/server/src/controllers/a2a/index.ts create mode 100644 packages/server/src/routes/a2a/index.ts create mode 100644 packages/server/src/services/a2a/index.ts create mode 100644 packages/server/src/services/a2a/types.ts diff --git a/packages/server/src/controllers/a2a/index.ts b/packages/server/src/controllers/a2a/index.ts new file mode 100644 index 00000000000..2d57d435f3d --- /dev/null +++ b/packages/server/src/controllers/a2a/index.ts @@ -0,0 +1,96 @@ +import { NextFunction, Request, Response } from 'express' +import a2aService from '../../services/a2a' +import { RateLimiterManager } from '../../utils/rateLimit' +import logger from '../../utils/logger' + +/** + * Extract token from the Authorization: Bearer header. + * Returns null if not present or malformed. + */ +function extractToken(req: Request): string | null { + const authHeader = req.headers.authorization + if (!authHeader || !authHeader.startsWith('Bearer ')) return null + const token = authHeader.slice(7).trim() + return token.length > 0 ? token : null +} + +/** + * Authentication middleware — validates Bearer token and attaches it to res.locals. + * If A2A_AUTH_DISABLED env var is set to 'true', authentication is bypassed. + */ +const authenticateToken = (req: Request, res: Response, next: NextFunction) => { + if (process.env.A2A_AUTH_DISABLED === 'true') { + next() + return + } + + const token = extractToken(req) + if (!token) { + res.status(401).json({ + jsonrpc: '2.0', + error: { code: -32001, message: 'Unauthorized: missing or invalid Authorization header. Use Bearer .' }, + id: null + }) + return + } + res.locals.token = token + next() +} + +/** + * Rate limiter middleware for A2A endpoint. + */ +const getRateLimiterMiddleware = async (req: Request, res: Response, next: NextFunction) => { + try { + return RateLimiterManager.getInstance().getRateLimiter()(req, res, next) + } catch (error) { + next(error) + } +} + +/** + * Handle GET /.well-known/agent-card.json for a specific chatflow. + * This is the A2A agent discovery endpoint. + */ +const handleAgentCard = async (req: Request, res: Response, next: NextFunction) => { + try { + const { chatflowId } = req.params + logger.debug(`[A2A] AgentCard request for chatflow: ${chatflowId}`) + await a2aService.handleAgentCard(chatflowId, req, res) + } catch (error) { + next(error) + } +} + +/** + * Handle POST /api/v1/a2a/:chatflowId — A2A JSON-RPC messages. + * Handles: tasks/send, tasks/sendSubscribe, tasks/get, tasks/cancel, agent/card + */ +const handlePost = async (req: Request, res: Response, next: NextFunction) => { + try { + const { chatflowId } = req.params + logger.debug(`[A2A] JSON-RPC request for chatflow: ${chatflowId}`) + await a2aService.handleJsonRpc(chatflowId, req, res) + } catch (error) { + next(error) + } +} + +/** + * Handle OPTIONS preflight requests for A2A endpoints. + */ +const handleOptions = async (req: Request, res: Response) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization') + res.setHeader('Access-Control-Max-Age', '86400') + res.status(204).end() +} + +export default { + authenticateToken, + handleAgentCard, + handlePost, + handleOptions, + getRateLimiterMiddleware +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 566e7ddf193..b939a850466 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -326,6 +326,28 @@ export class App { this.app.use('/api/v1', flowiseApiV1Router) + // ---------------------------------------- + // A2A Agent Discovery — well-known endpoint + // Returns AgentCard for a specific chatflow (requires chatflowId query param) + // ---------------------------------------- + this.app.get('/.well-known/agent-card.json', async (request, response) => { + try { + const chatflowId = request.query.chatflowId as string + if (!chatflowId) { + response.status(400).json({ + error: 'chatflowId query parameter is required. Example: /.well-known/agent-card.json?chatflowId=' + }) + return + } + // Forward to the A2A agent card handler + const a2aController = (await import('./controllers/a2a')).default + const modifiedReq = { ...request, params: { chatflowId } } as any + await a2aController.handleAgentCard(modifiedReq, response, () => {}) + } catch (error) { + response.status(500).json({ error: 'Failed to generate AgentCard' }) + } + }) + // ---------------------------------------- // Configure number of proxies in Host Environment // ---------------------------------------- diff --git a/packages/server/src/routes/a2a/index.ts b/packages/server/src/routes/a2a/index.ts new file mode 100644 index 00000000000..596523ff01c --- /dev/null +++ b/packages/server/src/routes/a2a/index.ts @@ -0,0 +1,44 @@ +import express from 'express' +import cors from 'cors' +import a2aController from '../../controllers/a2a' + +const router = express.Router() + +// Body size limit: 10MB max for A2A JSON-RPC payloads +router.use(express.json({ limit: '10mb', type: 'application/json' })) + +// CORS: Allow all origins for A2A agent-to-agent communication +// Can be restricted via A2A_CORS_ORIGINS env var +const a2aCorsOrigins = process.env.A2A_CORS_ORIGINS +const a2aCorsOptions: cors.CorsOptions = { + origin: a2aCorsOrigins + ? a2aCorsOrigins === '*' + ? true + : a2aCorsOrigins.split(',').map((o) => o.trim()) + : true, + methods: ['GET', 'POST', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'], + maxAge: 86400 +} +router.use(cors(a2aCorsOptions)) + +// Handle preflight for all A2A routes +router.options('/:chatflowId', a2aController.handleOptions) + +// GET /.well-known/agent-card.json for a specific chatflow +// AgentCard discovery endpoint +router.get( + '/:chatflowId', + a2aController.authenticateToken, + a2aController.handleAgentCard +) + +// POST — A2A JSON-RPC messages (tasks/send, tasks/sendSubscribe, tasks/get, tasks/cancel) +router.post( + '/:chatflowId', + a2aController.getRateLimiterMiddleware, + a2aController.authenticateToken, + a2aController.handlePost +) + +export default router diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index fa23bf054a4..639c800ce19 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -1,4 +1,5 @@ import express from 'express' +import a2aRouter from './a2a' import agentflowv2GeneratorRouter from './agentflowv2-generator' import apikeyRouter from './apikey' import assistantsRouter from './assistants' @@ -133,6 +134,7 @@ router.use('/text-to-speech', textToSpeechRouter) router.use('/custom-mcp-servers', customMcpServersRouter) router.use('/mcp-server', mcpServerRouter) router.use('/mcp', mcpEndpointRouter) +router.use('/a2a', a2aRouter) router.use('/auth', authRouter) router.use('/audit', IdentityManager.checkFeatureByPlan('feat:login-activity'), auditRouter) diff --git a/packages/server/src/services/a2a/index.ts b/packages/server/src/services/a2a/index.ts new file mode 100644 index 00000000000..94676516f7b --- /dev/null +++ b/packages/server/src/services/a2a/index.ts @@ -0,0 +1,517 @@ +import { Request, Response } from 'express' +import { v4 as uuidv4 } from 'uuid' +import { StatusCodes } from 'http-status-codes' +import { ChatFlow } from '../../database/entities/ChatFlow' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { utilBuildChatflow } from '../../utils/buildChatflow' +import { ChatType } from '../../Interface' +import { InternalFlowiseError } from '../../errors/internalFlowiseError' +import { getErrorMessage } from '../../errors/utils' +import logger from '../../utils/logger' +import { + A2A_PROTOCOL_VERSION, + AgentCard, + AgentSkill, + JsonRpcRequest, + JsonRpcResponse, + Task, + TaskSendParams, + TaskGetParams, + TaskCancelParams, + Message, + TextPart +} from './types' + +/** + * Map of active A2A tasks. + * In production, this should use a persistent store. + */ +const activeTasks = new Map() + +/** + * Map of task IDs to their SSE response objects. + */ +const taskSSEClients = new Map() + +/** + * Sanitize a chatflow name for use as an agent name. + */ +function sanitizeAgentName(name: string): string { + return name.replace(/[^a-zA-Z0-9 _-]/g, '').trim() || 'Flowise Agent' +} + +/** + * Generate an AgentCard from a chatflow entity. + */ +function generateAgentCard(chatflow: ChatFlow, baseURL: string, protocolVersion: string = A2A_PROTOCOL_VERSION): AgentCard { + const skills: AgentSkill[] = [ + { + id: chatflow.id, + name: sanitizeAgentName(chatflow.name), + description: chatflow.category || `A Flowise agent powered by ${chatflow.name}`, + tags: ['flowise', 'agent', chatflow.type || 'CHATFLOW'], + examples: ['What can you help me with?'] + } + ] + + return { + name: sanitizeAgentName(chatflow.name), + description: chatflow.category || `A Flowise agent powered by chatflow: ${chatflow.name}`, + url: `${baseURL}/api/v1/a2a/${chatflow.id}`, + provider: { + organization: 'FlowiseAI', + url: 'https://flowiseai.com' + }, + version: '1.0.0', + protocolVersion, + defaultInputModes: ['text/plain'], + defaultOutputModes: ['text/plain'], + capabilities: { + streaming: true + }, + skills, + authentication: { + schemes: ['bearer'] + } + } +} + +/** + * Extract user message text from A2A message parts. + */ +function extractMessageText(message: Message): string { + if (!message.parts || message.parts.length === 0) { + return '' + } + const textParts = message.parts + .filter((part): part is TextPart => part.kind === 'text') + .map((part) => part.text) + return textParts.join('\n') +} + +/** + * Convert a Flowise chatflow execution result to A2A artifacts. + */ +function buildArtifactsFromResult(result: any): any[] { + const artifacts: any[] = [] + if (result?.text) { + artifacts.push({ + kind: 'artifact', + artifactId: uuidv4(), + name: 'response', + parts: [ + { + kind: 'text', + text: result.text + } + ] + }) + } + return artifacts +} + +/** + * Create JSON-RPC error response. + */ +function createErrorResponse(id: string | number | null, code: number, message: string, data?: unknown): JsonRpcResponse { + return { + jsonrpc: '2.0', + id, + error: { code, message, data } + } +} + +/** + * Create JSON-RPC success response. + */ +function createSuccessResponse(id: string | number | null, result: unknown): JsonRpcResponse { + return { + jsonrpc: '2.0', + id, + result + } +} + +/** + * Create a mock request object compatible with utilBuildChatflow. + * This adapts A2A requests to the internal Flowise prediction pipeline. + */ +function createInternalRequest(chatflow: ChatFlow, question: string, chatId: string): Partial { + return { + params: { id: chatflow.id }, + body: { + question, + chatId, + streaming: false + }, + headers: {}, + get: (headerName: string) => { + if (headerName === 'flowise-tool') return 'true' + return undefined + }, + protocol: 'http', + user: { + activeWorkspaceId: chatflow.workspaceId + } + } as Partial +} + +/** + * Handle the A2A AgentCard endpoint. + * Returns the AgentCard JSON for the specified chatflow. + */ +async function handleAgentCard(chatflowId: string, req: Request, res: Response): Promise { + try { + const appServer = getRunningExpressApp() + const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowId }) + + if (!chatflow) { + res.status(StatusCodes.NOT_FOUND).json({ + error: `Agent not found for chatflow: ${chatflowId}` + }) + return + } + + const httpProtocol = req.get('x-forwarded-proto') || req.protocol + const baseURL = `${httpProtocol}://${req.get('host')}` + const agentCard = generateAgentCard(chatflow, baseURL) + + res.setHeader('Content-Type', 'application/json') + res.setHeader('Access-Control-Allow-Origin', '*') + res.json(agentCard) + } catch (error) { + logger.error(`[A2A] Error generating AgentCard for ${chatflowId}: ${getErrorMessage(error)}`) + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: getErrorMessage(error) }) + } +} + +/** + * Handle A2A JSON-RPC requests. + * Routes to the appropriate method handler based on the `method` field. + */ +async function handleJsonRpc(chatflowId: string, req: Request, res: Response): Promise { + try { + const payload = req.body as JsonRpcRequest + + if (!payload || payload.jsonrpc !== '2.0' || !payload.method) { + const errorResp = createErrorResponse(payload?.id ?? null, -32600, 'Invalid Request: JSON-RPC 2.0 required') + res.status(StatusCodes.BAD_REQUEST).json(errorResp) + return + } + + const appServer = getRunningExpressApp() + const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowId }) + + if (!chatflow) { + const errorResp = createErrorResponse(payload.id, -32001, `Agent not found for chatflow: ${chatflowId}`) + res.status(StatusCodes.NOT_FOUND).json(errorResp) + return + } + + switch (payload.method) { + case 'tasks/send': + await handleTaskSend(chatflow, payload, req, res) + break + case 'tasks/sendSubscribe': + await handleTaskSendSubscribe(chatflow, payload, req, res) + break + case 'tasks/get': + await handleTaskGet(chatflow, payload, res) + break + case 'tasks/cancel': + await handleTaskCancel(chatflow, payload, res) + break + case 'agent/card': + // Inline AgentCard response via JSON-RPC + { + const httpProtocol = req.get('x-forwarded-proto') || req.protocol + const baseURL = `${httpProtocol}://${req.get('host')}` + const agentCard = generateAgentCard(chatflow, baseURL) + res.json(createSuccessResponse(payload.id, agentCard)) + } + break + default: + { + const errorResp = createErrorResponse(payload.id, -32601, `Method not found: ${payload.method}`) + res.status(StatusCodes.NOT_FOUND).json(errorResp) + } + break + } + } catch (error) { + logger.error(`[A2A] Error handling JSON-RPC for ${chatflowId}: ${getErrorMessage(error)}`) + const payload = req.body as JsonRpcRequest + const errorResp = createErrorResponse( + payload?.id ?? null, + -32603, + 'Internal error', + getErrorMessage(error) + ) + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json(errorResp) + } +} + +/** + * Handle tasks/send — synchronous task execution. + * Runs the chatflow and returns the completed Task. + */ +async function handleTaskSend( + chatflow: ChatFlow, + payload: JsonRpcRequest, + req: Request, + res: Response +): Promise { + try { + const params = payload.params as unknown as TaskSendParams + const taskId = params?.id || uuidv4() + const question = params?.message ? extractMessageText(params.message) : '' + + if (!question) { + res.json(createErrorResponse(payload.id, -32602, 'Invalid params: message with text is required')) + return + } + + // Create task in submitted state + const task: Task = { + kind: 'task', + id: taskId, + contextId: params?.contextId, + status: { state: 'submitted' }, + history: params?.message ? [params.message] : [], + metadata: params?.metadata || {} + } + activeTasks.set(taskId, task) + + // Update to working + task.status = { state: 'working' } + activeTasks.set(taskId, { ...task }) + + // Execute the chatflow + const chatId = uuidv4() + const mockReq = createInternalRequest(chatflow, question, chatId) + + const result = await utilBuildChatflow(mockReq as Request, true, ChatType.INTERNAL) + + // Build response artifacts + const artifacts = buildArtifactsFromResult(result) + + // Build agent response message + const responseText = result?.text || JSON.stringify(result || {}) + const agentMessage: Message = { + kind: 'message', + messageId: uuidv4(), + role: 'agent', + parts: [{ kind: 'text', text: responseText }], + contextId: params?.contextId, + taskId + } + + // Update task to completed + const history = params?.message ? [params.message] : [] + if (agentMessage.parts && agentMessage.parts.length > 0) { + history.push(agentMessage) + } + + task.status = { state: 'completed' } + task.history = history + task.artifacts = artifacts + activeTasks.set(taskId, { ...task }) + + const response = createSuccessResponse(payload.id, task) + res.json(response) + } catch (error) { + logger.error(`[A2A] Error in tasks/send: ${getErrorMessage(error)}`) + const taskId = (payload.params as any)?.id || 'unknown' + const existingTask = activeTasks.get(taskId) + if (existingTask) { + existingTask.status = { + state: 'failed', + message: { + role: 'agent', + parts: [{ kind: 'text', text: getErrorMessage(error) }] + } + } + activeTasks.set(taskId, { ...existingTask }) + } + res.json( + createErrorResponse(payload.id, -32000, 'Task execution failed', getErrorMessage(error)) + ) + } +} + +/** + * Handle tasks/sendSubscribe — streaming task execution via SSE. + */ +async function handleTaskSendSubscribe( + chatflow: ChatFlow, + payload: JsonRpcRequest, + req: Request, + res: Response +): Promise { + try { + const params = payload.params as unknown as TaskSendParams + const taskId = params?.id || uuidv4() + const question = params?.message ? extractMessageText(params.message) : '' + + if (!question) { + res.json(createErrorResponse(payload.id, -32602, 'Invalid params: message with text is required')) + return + } + + // Set up SSE headers + res.setHeader('Content-Type', 'text/event-stream') + res.setHeader('Cache-Control', 'no-cache') + res.setHeader('Connection', 'keep-alive') + res.setHeader('X-Accel-Buffering', 'no') + res.flushHeaders() + + // Create task + const task: Task = { + kind: 'task', + id: taskId, + contextId: params?.contextId, + status: { state: 'submitted' }, + history: params?.message ? [params.message] : [], + metadata: params?.metadata || {} + } + activeTasks.set(taskId, task) + + // Send submitted event + const submittedEvent = createSuccessResponse(payload.id, task) + res.write(`data: ${JSON.stringify(submittedEvent)}\n\n`) + + // Update to working and send event + task.status = { state: 'working' } + activeTasks.set(taskId, { ...task }) + const workingEvent = createSuccessResponse(payload.id, task) + res.write(`data: ${JSON.stringify(workingEvent)}\n\n`) + + // Execute the chatflow + const chatId = uuidv4() + const mockReq = createInternalRequest(chatflow, question, chatId) + + const result = await utilBuildChatflow(mockReq as Request, true, ChatType.INTERNAL) + + // Build response + const artifacts = buildArtifactsFromResult(result) + const responseText = result?.text || JSON.stringify(result || {}) + const agentMessage: Message = { + kind: 'message', + messageId: uuidv4(), + role: 'agent', + parts: [{ kind: 'text', text: responseText }], + contextId: params?.contextId, + taskId + } + + const history = params?.message ? [params.message] : [] + if (agentMessage.parts && agentMessage.parts.length > 0) { + history.push(agentMessage) + } + + task.status = { state: 'working' } + task.artifacts = artifacts + + // Send artifact update + const artifactEvent = createSuccessResponse(payload.id, task) + res.write(`data: ${JSON.stringify(artifactEvent)}\n\n`) + + // Complete task + task.status = { state: 'completed' } + task.history = history + activeTasks.set(taskId, { ...task }) + + const completedEvent = createSuccessResponse(payload.id, task) + res.write(`data: ${JSON.stringify(completedEvent)}\n\n`) + + res.end() + } catch (error) { + logger.error(`[A2A] Error in tasks/sendSubscribe: ${getErrorMessage(error)}`) + const taskId = (payload.params as any)?.id || 'unknown' + const existingTask = activeTasks.get(taskId) + if (existingTask) { + existingTask.status = { + state: 'failed', + message: { + role: 'agent', + parts: [{ kind: 'text', text: getErrorMessage(error) }] + } + } + activeTasks.set(taskId, { ...existingTask }) + const failedEvent = createSuccessResponse(payload.id, existingTask) + res.write(`data: ${JSON.stringify(failedEvent)}\n\n`) + } else { + const errorEvent = createErrorResponse(payload.id, -32000, 'Task execution failed', getErrorMessage(error)) + res.write(`data: ${JSON.stringify(errorEvent)}\n\n`) + } + res.end() + } +} + +/** + * Handle tasks/get — retrieve a task by ID. + */ +async function handleTaskGet( + _chatflow: ChatFlow, + payload: JsonRpcRequest, + res: Response +): Promise { + const params = payload.params as unknown as TaskGetParams + const taskId = params?.id + + if (!taskId) { + res.json(createErrorResponse(payload.id, -32602, 'Invalid params: task id is required')) + return + } + + const task = activeTasks.get(taskId) + if (!task) { + res.json(createErrorResponse(payload.id, -32001, `Task not found: ${taskId}`)) + return + } + + res.json(createSuccessResponse(payload.id, task)) +} + +/** + * Handle tasks/cancel — cancel a running task. + */ +async function handleTaskCancel( + _chatflow: ChatFlow, + payload: JsonRpcRequest, + res: Response +): Promise { + const params = payload.params as unknown as TaskCancelParams + const taskId = params?.id + + if (!taskId) { + res.json(createErrorResponse(payload.id, -32602, 'Invalid params: task id is required')) + return + } + + const task = activeTasks.get(taskId) + if (!task) { + res.json(createErrorResponse(payload.id, -32001, `Task not found: ${taskId}`)) + return + } + + // Only cancel if not already in a terminal state + const terminalStates: string[] = ['completed', 'failed', 'canceled'] + if (!terminalStates.includes(task.status.state)) { + task.status = { state: 'canceled' } + activeTasks.set(taskId, { ...task }) + } + + res.json(createSuccessResponse(payload.id, task)) +} + +export default { + handleAgentCard, + handleJsonRpc, + + // Exported for testing + generateAgentCard, + extractMessageText, + buildArtifactsFromResult, + activeTasks, + taskSSEClients +} diff --git a/packages/server/src/services/a2a/types.ts b/packages/server/src/services/a2a/types.ts new file mode 100644 index 00000000000..8dc141f25b1 --- /dev/null +++ b/packages/server/src/services/a2a/types.ts @@ -0,0 +1,175 @@ +/** + * Google A2A (Agent-to-Agent) Protocol Type Definitions + * + * Based on the A2A specification: + * @see https://github.com/google/A2A + * + * Protocol version: 0.3.0 + */ + +// ─── AgentCard ─────────────────────────────────────────────────────────── + +export interface AgentCapabilities { + streaming?: boolean + pushNotifications?: boolean + stateTransitionHistory?: boolean +} + +export interface AgentSkill { + id: string + name: string + description?: string + tags?: string[] + examples?: string[] + inputModes?: string[] + outputModes?: string[] +} + +export interface AgentCard { + name: string + description: string + url: string + provider?: { + organization: string + url?: string + } + version: string + protocolVersion: string + defaultInputModes: string[] + defaultOutputModes: string[] + capabilities: AgentCapabilities + skills: AgentSkill[] + authentication?: { + schemes: string[] + credentials?: string + } +} + +// ─── JSON-RPC Envelope ─────────────────────────────────────────────────── + +export interface JsonRpcRequest { + jsonrpc: '2.0' + id: string | number | null + method: string + params?: Record +} + +export interface JsonRpcResponse { + jsonrpc: '2.0' + id: string | number | null + result?: unknown + error?: JsonRpcError +} + +export interface JsonRpcError { + code: number + message: string + data?: unknown +} + +// ─── Task ──────────────────────────────────────────────────────────────── + +export type TaskState = + | 'submitted' + | 'working' + | 'input-required' + | 'completed' + | 'failed' + | 'canceled' + +export interface TaskStatus { + state: TaskState + message?: Message + timestamp?: string +} + +export interface Task { + kind: 'task' + id: string + contextId?: string + status: TaskStatus + history?: Message[] + artifacts?: Artifact[] + metadata?: Record +} + +// ─── Message ───────────────────────────────────────────────────────────── + +export interface Message { + kind?: 'message' + messageId?: string + role: 'user' | 'agent' + parts: Part[] + contextId?: string + taskId?: string + metadata?: Record +} + +// ─── Part Types ────────────────────────────────────────────────────────── + +export type Part = TextPart | FilePart | DataPart + +export interface TextPart { + kind: 'text' + text: string + metadata?: Record +} + +export interface FilePart { + kind: 'file' + file: { + name?: string + mimeType?: string + bytes?: string // base64 encoded + uri?: string + } + metadata?: Record +} + +export interface DataPart { + kind: 'data' + data: Record + metadata?: Record +} + +// ─── Artifact ──────────────────────────────────────────────────────────── + +export interface Artifact { + kind?: 'artifact' + artifactId?: string + name?: string + description?: string + parts: Part[] + metadata?: Record + index?: number + append?: boolean + lastChunk?: boolean +} + +// ─── Method Params ────────────────────────────────────────────────────── + +export interface TaskSendParams { + id?: string + contextId?: string + message: Message + metadata?: Record +} + +export interface TaskGetParams { + id: string + historyLength?: number + metadata?: Record +} + +export interface TaskCancelParams { + id: string + metadata?: Record +} + +// ─── Default Protocol Values ───────────────────────────────────────────── + +export const A2A_PROTOCOL_VERSION = '0.3.0' +export const AGENT_CARD_WELL_KNOWN_PATH = '/.well-known/agent-card.json' +export const AGENT_WELL_KNOWN_PATH = '/.well-known/agent.json' +export const DEFAULT_INPUT_MODES = ['text/plain'] +export const DEFAULT_OUTPUT_MODES = ['text/plain']