CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-com-embabel-agent--embabel-agent-a2a

A2A protocol integration for Embabel Agent Framework enabling agent-to-agent communication

Overview
Eval results
Files

endpoint-registration.mddocs/api/

Endpoint Registration API

Automatic web endpoint registration for A2A communication at application startup.

A2AEndpointRegistrar

Component that dynamically registers web endpoints for AgentCardHandler beans.

/**
 * Registers A2A endpoints at application startup.
 * Creates GET and POST mappings for each AgentCardHandler bean.
 *
 * @param agentCardHandlers List of all AgentCardHandler beans
 * @param requestMappingHandlerMapping Spring MVC request mapping
 * @param objectMapper Jackson ObjectMapper for JSON
 */
@Component
class A2AEndpointRegistrar(
    private val agentCardHandlers: List<AgentCardHandler>,
    private val requestMappingHandlerMapping: RequestMappingHandlerMapping,
    private val objectMapper: ObjectMapper
) {
    /**
     * Triggered when Spring application context fully initialized.
     * Registers web endpoints for all AgentCardHandler beans.
     */
    @EventListener
    fun onApplicationReady(event: ApplicationReadyEvent)
}

Registered Endpoints

For each AgentCardHandler with path $path, two endpoints are created:

1. AgentCard Endpoint (GET)

URL Pattern: /$path/.well-known/agent.json

HTTP Method: GET

Content-Type: application/json

Purpose: Returns A2A AgentCard for agent discovery

Example:

GET http://localhost:8080/a2a/.well-known/agent.json

Response:

{
  "name": "MyAgent",
  "description": "Agent description",
  "url": "http://localhost:8080/a2a",
  "provider": {
    "name": "Embabel",
    "url": "https://embabel.com"
  },
  "skills": [...],
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "protocolVersion": "0.3.0"
}

2. JSON-RPC Endpoint (POST)

URL Pattern: /$path

HTTP Method: POST

Content-Type: application/json

Produces: application/json or text/event-stream

Purpose: Handles JSON-RPC requests (streaming and non-streaming)

Example:

POST http://localhost:8080/a2a
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": "msg-123",
  "method": "message/send",
  "params": {
    "message": {
      "messageId": "m1",
      "role": "user",
      "parts": [{"text": "Find news for Scorpios"}]
    }
  }
}

Registration Process

Startup Sequence

  1. Spring Boot Application Starts

    • Beans created and initialized
    • AgentCardHandler beans registered
  2. ApplicationReadyEvent Fired

    • Application context fully initialized
    • A2AEndpointRegistrar receives event
  3. Endpoint Registration

    • For each AgentCardHandler:
      • Create AgentCardHandlerWebFacade wrapper
      • Register GET for .well-known/agent.json
      • Register POST for JSON-RPC requests
      • Log registration details
  4. Endpoints Active

    • Web server accepts requests
    • AgentCard accessible via GET
    • JSON-RPC handled via POST

Request Routing

POST endpoint dynamically routes based on method:

Streaming Methods

Returns SseEmitter:

  • message/streamhandleJsonRpcStream(SendStreamingMessageRequest)
  • task/resubscribehandleCustomStreamingRequest(TaskResubscriptionRequest)

Non-Streaming Methods

Returns JSONRPCResponse:

  • message/sendhandleJsonRpc(SendMessageRequest)
  • task/gethandleJsonRpc(GetTaskRequest)
  • task/cancelhandleJsonRpc(CancelTaskRequest)

Request Processing Flow

HTTP POST /$path
    ↓
AgentCardHandlerWebFacade.handleJsonRpc(requestMap)
    ↓
Parse method field
    ↓
    ├─ If "message/stream" or "task/resubscribe":
    │    ↓
    │  Convert to streaming request type
    │    ↓
    │  Call agentCardHandler.handleJsonRpcStream(request)
    │    ↓
    │  Return SseEmitter (Spring handles SSE)
    │
    └─ Else (non-streaming):
         ↓
       Convert to request type
         ↓
       Call agentCardHandler.handleJsonRpc(request)
         ↓
       Wrap in ResponseEntity<JSONRPCResponse>
         ↓
       Return response

Multiple Handlers Example

@Configuration
class MultiHandlerConfig {

    @Bean
    fun publicHandler(...): AgentCardHandler =
        EmbabelServerGoalsAgentCardHandler(path = "public", ...)

    @Bean
    fun privateHandler(...): AgentCardHandler =
        EmbabelServerGoalsAgentCardHandler(path = "private", ...)
}

// Registered endpoints:
// GET  /public/.well-known/agent.json
// POST /public
// GET  /private/.well-known/agent.json
// POST /private

Error Handling

Request Deserialization Errors

catch (e: Exception) {
    logger.error("Failed to deserialize request", e)
    ResponseEntity.status(500)
        .contentType(MediaType.APPLICATION_JSON)
        .body(
            JSONRPCErrorResponse(
                requestId,
                JSONRPCError(500, "Internal server error: ${e.message}", null)
            )
        )
}

Unsupported Methods

else -> {
    logger.warn("Unsupported method: {}", method)
    throw UnsupportedOperationException("Method ${method} is not supported")
}

Logging

Registrar logs detailed information:

logger.info("Registering ${agentCardHandlers.size} A2A endpoints")
logger.info(
    "Registering web endpoint under {} for {}",
    endpointPath,
    agentCardHandler.infoString(verbose = true)
)

Example Log Output:

INFO  c.e.a.a.s.A2AEndpointRegistrar - Registering 2 A2A endpoints
INFO  c.e.a.a.s.A2AEndpointRegistrar - Registering web endpoint under /a2a-public/.well-known/agent.json for EmbabelServerGoalsAgentCardHandler(path='a2a-public')
INFO  c.e.a.a.s.A2AEndpointRegistrar - Registering web endpoint under /a2a-internal/.well-known/agent.json for EmbabelServerGoalsAgentCardHandler(path='a2a-internal')

Testing Endpoints

Test AgentCard

curl http://localhost:8080/a2a/.well-known/agent.json

Test Non-Streaming

curl -X POST http://localhost:8080/a2a \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "test-1",
    "method": "message/send",
    "params": {
      "message": {
        "messageId": "m1",
        "role": "user",
        "parts": [{"text": "Hello"}]
      }
    }
  }'

Test Streaming

curl -X POST http://localhost:8080/a2a \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": "stream-1",
    "method": "message/stream",
    "params": {
      "message": {
        "messageId": "m2",
        "role": "user",
        "parts": [{"text": "Research topic"}]
      }
    }
  }'

Spring MVC Integration

Uses Spring MVC's RequestMappingHandlerMapping for dynamic registration:

val mapping = RequestMappingInfo.paths(endpointPath)
    .methods(RequestMethod.GET)
    .produces(MediaType.APPLICATION_JSON_VALUE)
    .build()

requestMappingHandlerMapping.registerMapping(
    mapping,
    facade,
    facade::class.java.getMethod("agentCard", ServletRequest::class.java)
)

Benefits:

  • Standard Spring MVC request processing
  • Interceptor and filter support
  • Content negotiation
  • Error handling

Path Resolution

Relative Paths

Handler paths are relative to servlet context:

path = "a2a" → /a2a
path = "api/v1/agent" → /api/v1/agent

Context Path

If application uses context path:

server:
  servlet:
    context-path: /app

Endpoints become:

  • /app/a2a/.well-known/agent.json
  • /app/a2a

See Also

  • AgentCard API - AgentCard handler interface
  • Configuration API - Spring auto-configuration
  • Request Handling API - JSON-RPC processing
  • Troubleshooting - Registration issues
tessl i tessl/maven-com-embabel-agent--embabel-agent-a2a@0.3.3

docs

api

agent-card.md

configuration.md

endpoint-registration.md

events.md

overview.md

request-handling.md

skill-factory.md

streaming.md

task-state.md

index.md

tile.json