A2A protocol integration for Embabel Agent Framework enabling agent-to-agent communication
AgentCard exposure and capability advertising for A2A agent discovery.
Core interface combining AgentCard exposure with request handling.
/**
* Exposes A2A AgentCard and handles JSON-RPC requests.
* Extends A2ARequestHandler and HasInfoString.
*/
interface AgentCardHandler : A2ARequestHandler, HasInfoString {
/**
* Relative path where endpoint is exposed (e.g., "a2a" → "/a2a").
*/
val path: String
/**
* Returns AgentCard for the A2A server.
* @param scheme URL scheme ("http" or "https")
* @param host Server hostname
* @param port Server port number
* @return AgentCard with computed URLs
*/
fun agentCard(scheme: String, host: String, port: Int): AgentCard
}Default implementation exposing Embabel AgentPlatform as A2A AgentCard.
/**
* Exposes agent platform capabilities as A2A AgentCard.
* Converts filtered Goals to A2A Skills.
*
* @param path Relative endpoint path (default: "a2a")
* @param agentPlatform Embabel agent platform
* @param a2ARequestHandler Request handler to delegate to
* @param goalFilter Filter function for goal exposure
*/
class EmbabelServerGoalsAgentCardHandler(
override val path: String = DEFAULT_A2A_PATH,
private val agentPlatform: AgentPlatform,
private val a2ARequestHandler: A2ARequestHandler,
private val goalFilter: GoalFilter
) : AgentCardHandler, A2ARequestHandler by a2ARequestHandler {
override fun agentCard(scheme: String, host: String, port: Int): AgentCard
override fun infoString(verbose: Boolean?, indent: Int): String
}Constants:
const val DEFAULT_A2A_PATH = "a2a"Type Aliases:
typealias GoalFilter = (Goal) -> BooleanAgentCard.Builder()
.name(agentPlatform.name)
.description(agentPlatform.description)
.url("$scheme://$host:$port/$path")
.provider(AgentProvider("Embabel", "https://embabel.com"))
.version(Semver.DEFAULT_VERSION)
.documentationUrl("https://embabel.com/docs")
.capabilities(
AgentCapabilities.Builder()
.streaming(true)
.pushNotifications(false)
.stateTransitionHistory(false)
.extensions(emptyList())
.build()
)
.defaultInputModes(listOf("application/json", "text/plain"))
.defaultOutputModes(listOf("application/json", "text/plain"))
.skills(/* goals converted to skills via FromGoalsAgentSkillFactory */)
.supportsAuthenticatedExtendedCard(false)
.protocolVersion("0.3.0")
.build()val handler = EmbabelServerGoalsAgentCardHandler(
path = "a2a",
agentPlatform = agentPlatform,
a2ARequestHandler = requestHandler,
goalFilter = { true }
)goalFilter = { goal -> goal.tags.contains("public") }goalFilter = { goal ->
goal.tags.contains("external") && !goal.tags.contains("internal")
}goalFilter = { goal ->
goal.tags.contains("a2a-enabled") &&
!goal.tags.contains("internal") &&
goal.description.isNotEmpty()
}@Configuration
class A2AConfig {
@Bean
fun agentCardHandler(
agentPlatform: AgentPlatform,
requestHandler: AutonomyA2ARequestHandler
): AgentCardHandler {
return EmbabelServerGoalsAgentCardHandler(
path = "a2a",
agentPlatform = agentPlatform,
a2ARequestHandler = requestHandler,
goalFilter = { true }
)
}
}
// Endpoints registered automatically:
// GET http://localhost:8080/a2a/.well-known/agent.json
// POST http://localhost:8080/a2aExpose different capability sets at different paths:
@Configuration
class MultiEndpointConfig {
@Bean
fun publicHandler(
agentPlatform: AgentPlatform,
requestHandler: AutonomyA2ARequestHandler
): AgentCardHandler {
return EmbabelServerGoalsAgentCardHandler(
path = "a2a-public",
agentPlatform = agentPlatform,
a2ARequestHandler = requestHandler,
goalFilter = { goal -> goal.tags.contains("public") }
)
}
@Bean
fun partnerHandler(
agentPlatform: AgentPlatform,
requestHandler: AutonomyA2ARequestHandler
): AgentCardHandler {
return EmbabelServerGoalsAgentCardHandler(
path = "a2a-partner",
agentPlatform = agentPlatform,
a2ARequestHandler = requestHandler,
goalFilter = { goal -> goal.tags.contains("partner") }
)
}
}Result:
/a2a-public/.well-known/agent.json and /a2a-public/a2a-partner/.well-known/agent.json and /a2a-partnerclass CustomAgentCardHandler(
override val path: String,
private val customSkills: List<AgentSkill>
) : AgentCardHandler {
override fun agentCard(scheme: String, host: String, port: Int): AgentCard {
return AgentCard.Builder()
.name("Custom Agent")
.description("Custom A2A agent")
.url("$scheme://$host:$port/$path")
.provider(AgentProvider("MyCompany", "https://example.com"))
.skills(customSkills)
.capabilities(
AgentCapabilities.Builder()
.streaming(false)
.build()
)
.protocolVersion("0.3.0")
.build()
}
override fun handleJsonRpc(request: NonStreamingJSONRPCRequest<*>): JSONRPCResponse<*> {
// Custom request handling
TODO()
}
override fun handleJsonRpcStream(request: StreamingJSONRPCRequest<*>): SseEmitter {
throw UnsupportedOperationException("Streaming not supported")
}
override fun infoString(verbose: Boolean?, indent: Int): String {
return "CustomAgentCardHandler(path='$path')"
}
}EmbabelServerGoalsAgentCardHandler delegates request handling:
class EmbabelServerGoalsAgentCardHandler(
/* ... */
) : AgentCardHandler, A2ARequestHandler by a2ARequestHandler {
// AgentCard-specific methods implemented
// Request handling delegated to a2ARequestHandler
}Separation of Concerns:
Via HTTP:
curl http://localhost:8080/a2a/.well-known/agent.jsonVia Code:
@Autowired
lateinit var agentCardHandler: AgentCardHandler
fun getAgentCard(): AgentCard {
return agentCardHandler.agentCard("http", "localhost", 8080)
}tessl i tessl/maven-com-embabel-agent--embabel-agent-a2a@0.3.3